[Roslyn] Implement the ‘Use var’ feature using a Visual Studio’s extension

November 27th, 2014 | Posted by Tom in .NET | Article | Roslyn | Visual Studio | Read: 1,717

In my previous article, I’ve explained how to use Roslyn to implement a feature available in Resharper that allows you to easily change the modifier(s) of a property.

Today, we’ll see how we can implement another great feature: we’ll allow the user to easily replace a type by “var” in the declaration of a variable:

image

To do this, we’ll use a diagnostic analyser that will analyse our syntax tree:

public override void Initialize(AnalysisContext context)
{
    context.RegisterSyntaxTreeAction(DetectInitializedVariableDeclaration);
}

The method will inspect the syntax tree to detect all the element of type VariableDeclarationSyntax.  For each element, we’ll retrieve the EqualsValueClauseSyntax object and, if there is one (meaning the variable has been initialized), we’ll add our diagnostic on the type element:

private async static void DetectInitializedVariableDeclaration(SyntaxTreeAnalysisContext context)
{
    var root = await context.Tree.GetRootAsync();
    var variableDeclarationSyntaxElements = root.DescendantNodesAndSelf().OfType<VariableDeclarationSyntax>();

    if (variableDeclarationSyntaxElements.Any())
    {
        for (int i = 0; i < variableDeclarationSyntaxElements.Count(); i++)
        {
            var variableDeclaration = variableDeclarationSyntaxElements.ElementAt(i);

            var equalsValueClauseSyntaxElements = variableDeclaration.DescendantNodesAndSelf().OfType<EqualsValueClauseSyntax>();
            if (equalsValueClauseSyntaxElements.Any())
            {
                foreach (var equalsValueClause in equalsValueClauseSyntaxElements)
                {
                    var nodeType = variableDeclaration.DescendantNodesAndSelf().OfType<PredefinedTypeSyntax>().FirstOrDefault();
                    if (nodeType != null && !nodeType.IsVar)
                    {
                        // Add "contextual" menu to use 'var' instead of the real type
                        var diagnostic = Diagnostic.Create(UseVarRule, nodeType.GetLocation());

                        context.ReportDiagnostic(diagnostic);
                    }
                }
            }
        }
    }
}

The code fix provider associated is pretty simple:

public sealed override async Task ComputeFixesAsync(CodeFixContext context)
{
    var diagnostic = context.Diagnostics.First();

    var diagnosticSpan = diagnostic.Location.SourceSpan;

    var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

    var predefinedType = root.FindNode(diagnostic.Location.SourceSpan) as PredefinedTypeSyntax;
    if (predefinedType != null)
    {
        var variableDeclarationSyntaxElement = predefinedType.Parent as VariableDeclarationSyntax;
        if (variableDeclarationSyntaxElement != null)
        {
            context.RegisterFix(CodeAction.Create("Use 'var'", c => UseVarKeywordAsync(context.Document, variableDeclarationSyntaxElement, c)), diagnostic);
        }
    }
}

The method “UseVarKeyword” will perform all the magic here: it will create a copy of our element by adding the previous trivia and changing the type (using ‘var’ instead of the actual type).

private static async Task<Document> ReplaceNodeInDocumentAsync(
    Document document,
    SyntaxNode oldNode,
    SyntaxNode newNode,
    CancellationToken cancellationToken)
{
    var root = await document.GetSyntaxRootAsync(cancellationToken);
    var newRoot = root.ReplaceNode(oldNode, newNode);

    return document.WithSyntaxRoot(newRoot);
}

private static async Task<Document> UseVarKeywordAsync(
    Document document,
    VariableDeclarationSyntax variableDeclarationSyntaxElement,
    CancellationToken cancellationToken)
{
    // var is not a real keyword in C# so create an identifier named 'var': http://stackoverflow.com/questions/14777133/declaring-var-variables-with-roslyn-sept-2012-ctp
    var newVariableDeclarationSyntaxElement = variableDeclarationSyntaxElement.WithType(SyntaxFactory.IdentifierName("var "));
    newVariableDeclarationSyntaxElement = newVariableDeclarationSyntaxElement.WithLeadingTrivia(variableDeclarationSyntaxElement.GetLeadingTrivia());

    return await ReplaceNodeInDocumentAsync(document, variableDeclarationSyntaxElement, newVariableDeclarationSyntaxElement, cancellationToken);
}

Note that ‘var’ is not a real C# keyword so we’ll need to create a “custom” identifier named ‘var’.

Once executed, the extension allows us to have the following:

image

image

As you can see, using Roslyn, it’s really easy to add your custom rules to Visual Studio!

 

Happy coding!

You can follow any responses to this entry through the RSS 2.0 You can leave a response, or trackback.

One Response

Add Comment Register



Leave a Reply