Refactor with Roslyn

  • Tutorial

Refactoring usually seems like hard work on bugs. Monotonous correction of errors of the past 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 many such cases - inversion of dependencies (as an example of architectural changes), adding attributes, introducing aspects (example of adding end-to-end functionality) and various code layouts in classes and methods, as well as transition to a new version of the API - in this article we will consider this case in detail.


All projects use the API. API modules, components, frameworks, operating system, public services APIs - in most cases, these APIs are presented as interfaces. But everything around is changing, so are the APIs. New versions appear, obsolete methods appear. It would be nice to be able to automatically switch to the new version without the overhead of refactoring the project.


Tool selection


Veeam provides its developers with all the tools that the developer himself considers necessary. And I have the best that can come in handy for refactoring - ReSharper. But…


In 2015, ReSharper had an issue . In early 2016, issue RSRP-451569 changed its status to Submitted. Also in 2016, the request was updated .


I checked on the last update - there is no necessary functionality, there are no prompts from the resolver, nor a special attribute in JetBrains.Annotations. Instead of waiting for ReSharper to have this functionality, I decided to do this task on my own.


The first thing that comes to mind when working on .NET code refactoring is IntelliSense. But its API, in my opinion, is rather complicated and confusing, and it is also strongly tied to Visual Studio. Then I came across such a thing as DTE (EnvDTE) - Development Tools Environment, in fact, it’s an interface to all the features of the studio that are accessible through the UI or command line, with it you can automate any sequence of actions that can be performed 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 searching for a definition of a method, were difficult. In the process of overcoming the difficulties of working with DTE, I came across a video report by Alexander Kugushev:



The report interested me, I tried and realized that it was much more natural to solve such problems with the help of Roslyn. And do not forget about the useful Syntax Visualizer, which will help you understand how your code works at the language level.


So I chose the Roslyn .NET Compiler Platform as my tool.


Automate our task


Consider this problem with an artificial example. Suppose we have an API with obsolete methods, mark them [Obsolete]with an attribute indicating 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 implementation unallocated by 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 deprecated.


    public class Test
    {
        public IAppService service
        {
            get;
            set;
        }
        public Test()
        {
            service = new DoService();
            service.DoSomething();
            service.Test();
        }
    }

And here we are already using an instance of the implementation of this interface and we will not receive any 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();
    }
}

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 carefully describe it. Our Main Goal is to replace all obsolete methods with their new counterparts. We describe.


  1. Replace the legacy method with a new one.
    How?
  2. Find a pair of obsolete-new method and replace the obsolete method with a new one.
    Where?
  3. In the API class, find a pair of legacy-new methods and replace the legacy method with a new one.
    How?
  4. Find the definition of a method that has an attribute [Obsolete]in the API class and find a pair of obsolete-new method and replace the obsolete method with a new one.
    Where to get a new one?
  5. In the attribute message you [Obsolete]will find the name of the new method for the found method definition, which has an attribute [Obsolete]in the API class, where you will find a pair of obsolete-new method and replace the obsolete method with a new one.
    Where to replace?
  6. For all references to the deprecated method (although you can make exceptions for some projects and classes), in the attribute message of [Obsolete]which you will find the name of the new method for the found method definition, which has an attribute [Obsolete]in the API class, where you will find the deprecated-new method pair and replace the deprecated method to a new one.

The refactoring algorithm is ready, with the exception of the lack of technical aspects of working with the three pillars of .NET code - Document, Syntax Tree, Semantic Model. It all looks a lot like lambdas. In them we will express our Main Goal.


Infrastructure The
infrastructure for our solution are 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)
{...}

take the missing data from the outside, you will need the full path to the solution, in which the project with the API lies and the projects in which it is used - with a little refinement, you can place them in different solutions. And you also need to specify the class name of the API, if your API will be built on an abstract class or something else, then use Syntax Visualizer to look at 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 ");

we will receive private infrastructure elements 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, you need to start from the end.
4. Find a method definition that has attribute [Obsolete]3. In the API class


// direction from point 3
var targetInterfaceClass =
    root.DescendantNodes().OfType()
        .FirstOrDefault(c => c.Identifier.Text == interfaceClassName);
var methodDeclarations = targetInterfaceClass.DescendantNodes().OfType().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 replacementMap = new List();
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 obsolete method with a new one


private void UpdateSolutionWithAction(List replacementMap, Action 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();
    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);
    }
}

We answer auxiliary questions.
5. In the attribute message you [Obsolete]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()
        .FirstOrDefault(arg => arg.ChildNodes().OfType().Count() != 0);
    var message = messageArgument.ChildNodes().FirstOrDefault().GetText();
    return message.ToString().Trim('\"');
}

6. For all references to the obsolete method (although you can make exceptions for some projects and classes)


private IEnumerable 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;
}

Clarification of exceptions can be represented by the following filters.


/// Exclude method declarations that using in excluded classes in current Solution
private bool ContainInClasses(IEnumerable usingReferences, List 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;
}

/// Exclude method declarations that using in excluded projects in current Solution
private bool ContainInProjects(IEnumerable usingReferences, List 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;
}

We launch and get just such beauty.


Conclusion


The project can be designed as an extension of the studio vsix or, for example, put on a version control server and used as an analyzer. And you can run it if necessary as a Tulu.


The whole project is published on the github .

Only registered users can participate in the survey. Please come in.

If you write a sequel, then what?

  • 37.9% Update article describing how to make a plug-in for Visual Studio 11 from this
  • 58.6% Other Roslyn 17 Use Cases
  • 3.4% Do not write more, please 1

Also popular now: