Usually, refactoring seems to be hard work on bugs. Monotonous correction of past mistakes manually. But if our actions can be reduced to a transformation algorithm over A to get B, then why not automate this process?
There can be a lot of such cases - dependency inversion (as an example of architectural changes), adding attributes, introducing aspects (an example of adding end-to-end functionality) and various code layouts to classes and methods, as well as moving to a new version of the API - in this article we will look at this case in detail.
All projects use API. API modules, components, frameworks, operating systems, public API services - in most cases, these APIs are presented in the form of interfaces. But everything is changing, changing API. New versions appear, obsolete methods appear. It would be cool to be able to automatically upgrade to the new version without the overhead of refactoring the project.
Veeam provides its developers with all the tools that the developer himself deems necessary. And I have the best that can be useful for refactoring - ReSharper. But…
In 2015, ReSharper had an issue . At the beginning of 2016, the issue RSRP-451569 changed its status to Submitted. Also in 2016 the request was updated .
I checked it on the last update - there is no necessary functionality, there are no prompts from the resharper or a special attribute in JetBrains.Annotations. Instead of waiting for this functionality to appear with ReSharper, I decided to do this task on my own.
The first thing that comes to mind when working on refactoring .NET code is IntelliSense. But its API, in my opinion, is quite complex and confusing, and it is also strongly tied to Visual Studio. Then I got my hand on such a thing as DTE (EnvDTE) - Development Tools Environment, in fact, it is an interface to all the features of the studio, which are accessible via the UI or the command line, you can use it to automate any sequence of actions that can be done in Visual Studio . But DTE is an uncomfortable thing, it constantly requires an action context, i.e. emulate a whole programmer. Trivial actions, such as finding a definition of a method, were not easy. In the process of overcoming the difficulties of working with DTE, I came across a video of the report by Alexander Kugushev:
I was interested in the report, I tried and realized that solving such problems with the help of Roslyn is much more natural. And do not forget about the useful Syntax Visualizer, which will help you understand how your code is organized at the language level.
So I chose my tool .NET Compiler Platform "Roslyn".
Consider this problem on an artificial example. Suppose we have an API with outdated methods, label them with the [Obsolete]
attribute and indicate in the message which method should be replaced.
public interface IAppService { [Obsolete("Use DoSomethingNew")] void DoSomething(); void DoSomethingNew(); [Obsolete("Use TestNew")] void Test(); void TestNew(); }
and its untagged implementation of the attributes
public class DoService : IAppService { public void DoSomething() { Console.WriteLine("This is obsolete method. Use DoSomethingNew."); } public void DoSomethingNew() { Console.WriteLine("Good."); } public void Test() { Console.WriteLine("This is obsolete method. Use TestNew."); } public void TestNew() { Console.WriteLine("Good."); } }
An example of using our interface, since here we turn to IAppService, we get a compiler warning that the method is outdated.
public class Test { public IAppService service { get; set; } public Test() { service = new DoService(); service.DoSomething(); service.Test(); } }
And here we use an instance of the implementation of this interface and no longer receive a warning.
class Program { static void Main(string[] args) { //no warning highlighted var doService = new DoService(); doService.DoSomething(); //will be highlighted //IAppService service = doService as IAppService; //service.DoSomething(); doService.Test(); } static void Test() { var doService = new DoService(); doService.DoSomething(); doService.Test(); } }
We correct the situation
The use of Roslyn entails the use of declarative programming and a functional approach, and here the main thing is to set the Main Goal and describe it carefully. Our main goal is to replace all outdated methods with their new analogues. We describe.
[Obsolete]
attribute in the API class and find the outdated-new pair and replace the outdated method with a new one.[Obsolete]
attribute message, you will find the name of the new method for the found method definition, which has the [Obsolete]
attribute in the API class, where you will find the outdated-new method pair and replace the outdated method with a new one.[Obsolete]
attribute you will find the name of the new method for the found definition of the method that has the [Obsolete]
attribute in the API class where you find the outdated pair -New method and replace the outdated method with a new one.The refactoring algorithm is ready, except for the lack of technical aspects of working with the three pillars of the .NET code - Document, Syntax Tree, Semantic Model. This is all very similar to lambda. In them we will express our main goal.
Infrastructure
The infrastructure for our solution is the Document, Syntax Tree, Semantic Model.
public Project Project { get; set; } public MSBuildWorkspace Workspace { get; set; } public Solution Solution { get; set; } public Refactorer(string solutionPath, string projectName) { // start Roslyn workspace Workspace = MSBuildWorkspace.Create(); // open solution we want to analyze Solution = Workspace.OpenSolutionAsync(solutionPath).Result; // find target project Project = Solution.Projects.FirstOrDefault(p => p.Name == projectName); } public void ReplaceObsoleteApiCalls(string interfaceClassName, string obsoleteMessagePattern) {...}
We take the missing data from the outside, you need the full path to the solution, which contains the project with the API and the projects in which it is used - with a little refinement you can place them in different solutions. You also need to specify the class name of the API interface. If your API is built on an abstract class or something else, then use Syntax Visualizer to see what type of definition of this class is.
var solutionPath = "..\Sample.sln"; var projectName = "ObsoleteApi"; var interfaceClassName = "IAppService"; var refactorererApi = new Refactorer(solutionPath, projectName); refactorererApi.ReplaceObsoleteApiCalls(interfaceClassName, "Use ");
private infrastructure elements will be obtained in ReplaceObsoleteApiCalls
var document = GetDocument(interfaceClassName); var model = document.GetSemanticModelAsync().Result; SyntaxNode root = document.GetSyntaxRootAsync().Result;
We return to the algorithm and answer simple questions in it, we need to start from the end.
4. Find the definition of the method that has the [Obsolete]
attribute 3. In the API class
// direction from point 3 var targetInterfaceClass = root.DescendantNodes().OfType<InterfaceDeclarationSyntax>() .FirstOrDefault(c => c.Identifier.Text == interfaceClassName); var methodDeclarations = targetInterfaceClass.DescendantNodes().OfType<MethodDeclarationSyntax>().ToList(); var obsoleteMethods = methodDeclarations .Where(m => m.AttributeLists .FirstOrDefault(a => a.Attributes .FirstOrDefault(atr => (atr.Name as IdentifierNameSyntax).Identifier.Text == "Obsolete") != null) != null).ToList();
2. Find a pair of obsolete-new method
List<ObsoleteReplacement> replacementMap = new List<ObsoleteReplacement>(); foreach (var method in obsoleteMethods) { // find new mthod for replace - explain in point 5 var methodName = GetMethodName(obsoleteMessagePattern, method); if (methodDeclarations.FirstOrDefault(m => m.Identifier.Text == methodName) != null) { // find all reference of obsolete call - explain in point 6 var usingReferences = GetUsingReferences(model, method); replacementMap.Add(new ObsoleteReplacement() { ObsoleteMethod = SyntaxFactory.IdentifierName(method.Identifier.Text), ObsoleteReferences = usingReferences, NewMethod = SyntaxFactory.IdentifierName(methodName) }); } }
1. Replace the outdated method with a new one.
private void UpdateSolutionWithAction(List<ObsoleteReplacement> replacementMap, Action<DocumentEditor, ObsoleteReplacement, SyntaxNode> action) { var workspace = MSBuildWorkspace.Create(); foreach (var item in replacementMap) { var solution = workspace.OpenSolutionAsync(Solution.FilePath).Result; var project = solution.Projects.FirstOrDefault(p => p.Name == Project.Name); foreach (var reference in item.ObsoleteReferences) { var docs = reference.Locations.Select(l => l.Document); foreach (var doc in docs) { var document = project.Documents.FirstOrDefault(d => d.Name == doc.Name); var documentEditor = DocumentEditor.CreateAsync(document).Result; action(documentEditor, item, document.GetSyntaxRootAsync().Result); document = documentEditor.GetChangedDocument(); solution = solution.WithDocumentSyntaxRoot(document.Id, document.GetSyntaxRootAsync().Result.NormalizeWhitespace()); } } var result = workspace.TryApplyChanges(solution); workspace.CloseSolution(); } UpdateRefactorerEnv(); } private void ReplaceMethod(DocumentEditor documentEditor, ObsoleteReplacement item, SyntaxNode root) { var identifiers = root.DescendantNodes().OfType<IdentifierNameSyntax>(); var usingTokens = identifiers.Where(i => i.Identifier.Text == item.ObsoleteMethod.Identifier.Text); foreach (var oldMethod in usingTokens) { // The Most Impotant Moment Of Point 1 documentEditor.ReplaceNode(oldMethod, item.NewMethod); } }
Answering support questions.
5. In the message of the [Obsolete]
attribute you will find the name of the new method
private string GetMethodName(string obsoleteMessagePattern, MethodDeclarationSyntax method) { var message = GetAttributeMessage(method); int index = message.LastIndexOf(obsoleteMessagePattern) + obsoleteMessagePattern.Length; return message.Substring(index); } private static string GetAttributeMessage(MethodDeclarationSyntax method) { var obsoleteAttribute = method.AttributeLists.FirstOrDefault().Attributes.FirstOrDefault(atr => (atr.Name as IdentifierNameSyntax).Identifier.Text == "Obsolete"); var messageArgument = obsoleteAttribute.ArgumentList.DescendantNodes().OfType<AttributeArgumentSyntax>() .FirstOrDefault(arg => arg.ChildNodes().OfType<LiteralExpressionSyntax>().Count() != 0); var message = messageArgument.ChildNodes().FirstOrDefault().GetText(); return message.ToString().Trim('\"'); }
6. For all references to the outdated method (although you can make exceptions for some projects and classes)
private IEnumerable<ReferencedSymbol> GetUsingReferences(SemanticModel model, MethodDeclarationSyntax method) { var methodSymbol = model.GetDeclaredSymbol(method); var usingReferences = SymbolFinder.FindReferencesAsync(methodSymbol, Solution).Result.Where(r => r.Locations.Count() > 0); return usingReferences; }
Exceptions can be clarified by the following filters.
/// <param name="excludedClasses">Exclude method declarations that using in excluded classes in current Solution</param> private bool ContainInClasses(IEnumerable<ReferencedSymbol> usingReferences, List<string> excludedClasses) { if (excludedClasses.Count <= 0) { return false; } foreach (var reference in usingReferences) { foreach (var location in reference.Locations) { var node = location.Location.SourceTree.GetRoot().FindNode(location.Location.SourceSpan); ClassDeclarationSyntax classDeclaration = null; if (SyntaxNodeHelper.TryGetParentSyntax(node, out classDeclaration)) { if (excludedClasses.Contains(classDeclaration.Identifier.Text)) { return true; } } } } return false; }
/// <param name="excludedProjects">Exclude method declarations that using in excluded projects in current Solution</param> private bool ContainInProjects(IEnumerable<ReferencedSymbol> usingReferences, List<Microsoft.CodeAnalysis.Project> excludedProjects) { if (excludedProjects.Count <= 0) { return false; } foreach (var reference in usingReferences) { if (excludedProjects.FirstOrDefault(p => reference.Locations.FirstOrDefault(l => l.Document.Project.Id == p.Id) != null) != null) { return true; } } return false; }
Run and get just such a beauty.
The project can be designed as an extension of the vsix studio or, for example, put it on a version control server and used as an analyzer. And you can run as needed as Tulu.
The entire project is published on github .
Source: https://habr.com/ru/post/343244/
All Articles