Solution-wide Rename from Code Fix Provider – Fix async method naming

Last time when I posted about Diagnostic with Code Fix using Microsoft .NET Compiler Platform, aka “Roslyn” the fix was completely local. This time, inspired by a tweet by Luke Sigler (@Schandlich) I decided to go for solution-wide rename.

tweet
Idea is very simple: Diagnostic checks every declared method and makes sure, that those marked with async modifier have a name that ends with Async. If not, CodeFix is proposed to fix that issue.

Diagnostic

Diagnostic part of the problem if extremely simple. The only thing you have to do, is check if method declaration contains async modifier, and if its name ends with Async. That’s it:

public override void AnalyzeNode(SyntaxNode node, SemanticModel semanticModel, Action<Location, object[]> addDiagnostic, CancellationToken cancellationToken)
{
    var methodDeclaration = node as MethodDeclarationSyntax;
    if (methodDeclaration == null)
        return;

    if (methodDeclaration.Modifiers.Any(SyntaxKind.AsyncKeyword) &&
        !methodDeclaration.Identifier.Text.EndsWith("Async"))
        addDiagnostic(methodDeclaration.Identifier.GetLocation(), new object[] { methodDeclaration.Identifier, methodDeclaration.Identifier.Text + "Async" });
}

Method signature differs from the default one you get when implementing ISyntaxNodeAnalyzer. That’s because when I started RoslynDiagnostics project, I decided to create abstract class called SyntaxNodeAnalyzer, which is a base class for all diagnostics within that project. It just makes some staff easier. You can check how that class looks like on github: MarcinJuraszek / RoslynDiagnostics / src / RoslynDiagnostics / SyntaxNodeAnalyzer.cs

Code Fix

Second part of the store is also not very complicated. The most important peace is the renaming process. Fortunately, Roslyn API provides great solution-wide refactoring experience, which includes Rename option. It’s exposed by static class named Renamer which can be found in Microsoft.CodeAnalysis.Rename namespace. Method we care about is, as you’d probably expect, called RenameSymbolAsync:

public static async Task<Solution> RenameSymbolAsync(
    Solution solution,
    ISymbol symbol,
    string newName,
    OptionSet optionSet
    CancellationToken cancellationToken = default(CancellationToken))

Now, the only thing we have to do is get all necessary data to make Renamer.RenameSymbolAsync call. Starting with ISymbol symbol:

public async override Task<IEnumerable<CodeAction>> GetFixesAsync(Document document, TextSpan span, IEnumerable<Diagnostic> diagnostics, CancellationToken cancellationToken)
{
    var root = await document.GetSyntaxRootAsync(cancellationToken);
    var token = root.FindToken(span.Start);

    var methodDeclaration = token.Parent as MethodDeclarationSyntax;
    if (methodDeclaration == null)
        return null;

    var semanticModel = await document.GetSemanticModelAsync(cancellationToken);
    if (semanticModel == null)
        return null;

    var symbol = semanticModel.GetDeclaredSymbol(methodDeclaration, cancellationToken);
    if (symbol == null)
        return null;

All these null checks may not be necessary, but just to be safe… Lets move on to Solution instance, OptionsSet and new method name:

    var project = document.Project;
    if (project == null)
        return null;

    var solution = document.Project.Solution;
    if (solution == null)
        return null;

    var options = solution.Workspace.GetOptions();
    var newName = token.Text + "Async";

Now, we’re ready to call Rename method. But remember – almost entire Roslyn API uses immutable data model, which means instead of modifying instances, you’re getting new ones when content updated. There is no difference here. Rename method returns new instance of Solution class without modifying the one provided as method parameter:

    var newSolution = await Renamer.RenameSymbolAsync(solution, symbol, newName, options);

As we’ve now defined how solution should look like after we fix it, we can return CodeAction back to IDE:

    return new[] { CodeAction.Create("Change method name to '" + newName + "'.", newSolution) };

You can find entire class and base CodeFixProvider on github as well.

Result

Now, we can test our Diagnostic and Code Fix.

AsyncMethodNaming

Thanks to Roslyn integration within VS, you can see what changes will be made when you use the fix. As you can see, method name rename affects not only method declaration, but also all method calls within entire solution. Also Undo feature plays nice with the fix: hitting Ctrl+Z undos rename across entire solution as well.

5 thoughts on “Solution-wide Rename from Code Fix Provider – Fix async method naming”

  1. You should check whether the return type is or inherits Task; not whether the implementation is async.

    Methods that return Tasks directly (but do not use await) should also be suffixed Async, whereas async void methods should not. (or rather, they shouldn’t be public at all)

  2. Instead of computing the rename upfront, you should create the CodeAction with a delegate that does the work only after it is selected. As it is now, the CodeAction which represents the menu choice of the code fix, has to do the work of the rename before the menu can be displayed.

    1. Yeah, that’s probably a good advice. Computing rename might be heavy for bigger solutions, so delaying it as long as possible is a good idea. I have some other thoughts after I wrote that post, so I’m going to prepare another one, lets call it v2, soon.

Comments are closed.