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!

One of the feature that I like in Resharper is the one that allow you to easily change the modifiers of any properties:

image

Thanks to Visual Studio 2015 and Roslyn, it’s now possible to provide the same kind of experience using a custom extension.

To implement this feature, we need to create a diagnostic analyser and a code fix provider. So let’s take a look at the diagnostic analyser:

public class CSharpEssentialsAnalyzer : DiagnosticAnalyzer
{
    public const string DiagnosticId = "CSharpEssentials";

    #region Change Modifier

    internal const string ChangeModifierRuleTitle = "Change modifier";
    internal const string ChangeModifierRuleMessageFormat = "Change modifier for '{0}'";
    internal const string ChangeModifierRuleCategory = "Refactoring";

    internal static DiagnosticDescriptor ChangePropertyModifierRule = new DiagnosticDescriptor(
        DiagnosticId,
        ChangeModifierRuleTitle,
        ChangeModifierRuleMessageFormat,
        ChangeModifierRuleCategory,
        DiagnosticSeverity.Info,
        isEnabledByDefault: true);

    #endregion

    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
    {
        get
        {
            return ImmutableArray.Create(ChangePropertyModifierRule);
        }
    }

    public override void Initialize(AnalysisContext context)
    {
        context.RegisterSymbolAction(AnalyzePropertySymbol, SymbolKind.Property);
    }

    private static void AnalyzePropertySymbol(SymbolAnalysisContext context)
    {
        var propertySymbol = context.Symbol as IPropertySymbol;
        if (propertySymbol != null)
        {
            // Add "contextual" menu to change property modifiers.
            var diagnostic = Diagnostic.Create(ChangePropertyModifierRule, propertySymbol.Locations[0], propertySymbol.Name);

            context.ReportDiagnostic(diagnostic);
        }
    }
}

The code is simple to understand: on the Initiliaze method, we register a action that will be called for each property of the source code (of the current document). On that action, we check if we are really on a property (this should always be the case but it’s still a good thing to check) and we add a diagnostic at the property’s location. A diagnostic is the way to add an indicator on Visual Studio’s margin, telling “hey! there is something to check here”. This can be an error, a warning or an information.

Running the previous code, we can see the indicator:

image

By changing the severity of the rule that’s broken, we can change the way the indicator is shown. Here is the result if we specify a severity of kind “Error” (the property’s identifier is underlined in red)

image

Now that the indicator (the yellow light bulb) is show, we need to indicate to the user the different kind of fixes he can do. So we have to implement a Code fix provider (by inherited from the class CodeFixProvider):

public class CSharpEssentialsCodeFixProvider : CodeFixProvider
{
    private SyntaxToken _whitespaceToken =
        SyntaxFactory.Token(SyntaxTriviaList.Create(SyntaxFactory.Space), SyntaxKind.StringLiteralToken, SyntaxTriviaList.Empty);

    public sealed override ImmutableArray<string> GetFixableDiagnosticIds()
    {
        return ImmutableArray.Create(CSharpEssentialsAnalyzer.DiagnosticId);
    }

    public sealed override FixAllProvider GetFixAllProvider()
    {
        return WellKnownFixAllProviders.BatchFixer;
    }

    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 property = root.FindNode(diagnosticSpan) as PropertyDeclarationSyntax;
        if (property != null)
        {
            if (property.Modifiers.Any(m => m.CSharpKind() == SyntaxKind.PublicKeyword))
            {
                context.RegisterFix(
                    CodeAction.Create("To private", c => ReplacePropertyModifierAsync(context.Document, property, SyntaxKind.PrivateKeyword, c)),
                    diagnostic);

                context.RegisterFix(
                    CodeAction.Create("To protected", c => ReplacePropertyModifierAsync(context.Document, property, SyntaxKind.ProtectedKeyword, c)),
                    diagnostic);

                context.RegisterFix(
                    CodeAction.Create("To internal", c => ReplacePropertyModifierAsync(context.Document, property, SyntaxKind.InternalKeyword, c)),
                    diagnostic);
            }
            else if (property.Modifiers.Any(m => m.CSharpKind() == SyntaxKind.PrivateKeyword))
            {
                context.RegisterFix(
                    CodeAction.Create("To public", c => ReplacePropertyModifierAsync(context.Document, property, SyntaxKind.PublicKeyword, c)),
                    diagnostic);

                context.RegisterFix(
                    CodeAction.Create("To protected", c => ReplacePropertyModifierAsync(context.Document, property, SyntaxKind.ProtectedKeyword, c)),
                    diagnostic);

                context.RegisterFix(
                    CodeAction.Create("To internal", c => ReplacePropertyModifierAsync(context.Document, property, SyntaxKind.InternalKeyword, c)),
                    diagnostic);
            }
            else if (property.Modifiers.Any(m => m.CSharpKind() == SyntaxKind.ProtectedKeyword))
            {
                context.RegisterFix(
                    CodeAction.Create("To public", c => ReplacePropertyModifierAsync(context.Document, property, SyntaxKind.PublicKeyword, c)),
                    diagnostic);

                context.RegisterFix(
                    CodeAction.Create("To private", c => ReplacePropertyModifierAsync(context.Document, property, SyntaxKind.PrivateKeyword, c)),
                    diagnostic);

                context.RegisterFix(
                    CodeAction.Create("To internal", c => ReplacePropertyModifierAsync(context.Document, property, SyntaxKind.InternalKeyword, c)),
                    diagnostic);
            }
            else if (property.Modifiers.Any(m => m.CSharpKind() == SyntaxKind.InternalKeyword))
            {
                context.RegisterFix(
                    CodeAction.Create("To public", c => ReplacePropertyModifierAsync(context.Document, property, SyntaxKind.PublicKeyword, c)),
                    diagnostic);

                context.RegisterFix(
                    CodeAction.Create("To private", c => ReplacePropertyModifierAsync(context.Document, property, SyntaxKind.PrivateKeyword, c)),
                    diagnostic);

                context.RegisterFix(
                    CodeAction.Create("To protected", c => ReplacePropertyModifierAsync(context.Document, property, SyntaxKind.ProtectedKeyword, c)),
                    diagnostic);
            }

            if (!property.Modifiers.Any(m => m.CSharpKind() == SyntaxKind.VirtualKeyword))
            {
                context.RegisterFix(
                    CodeAction.Create("To virtual", c => AddPropertyModifierAsync(context.Document, property, SyntaxKind.VirtualKeyword, c)),
                    diagnostic);
            }
            else if (property.Modifiers.Any(m => m.CSharpKind() == SyntaxKind.VirtualKeyword))
            {
                context.RegisterFix(
                    CodeAction.Create("To non virtual", c => RemovePropertyModifierAsync(context.Document, property, SyntaxKind.VirtualKeyword, c)),
                    diagnostic);
            }
        }
    }

    private async Task<Document> AddPropertyModifierAsync(
        Document document,
        PropertyDeclarationSyntax property,
        SyntaxKind propertyModifier,
        CancellationToken cancellationToken)
    {
        //var semanticModel = await document.GetSemanticModelAsync(cancellationToken);
        //var propertySymbol = semanticModel.GetDeclaredSymbol(property, cancellationToken);

        var newProperty = property.AddModifiers(SyntaxFactory.Token(propertyModifier), _whitespaceToken);

        return await ReplacePropertyInDocumentAsync(document, property, newProperty, cancellationToken);
    }

    private async Task<Document> RemovePropertyModifierAsync(
        Document document,
        PropertyDeclarationSyntax property,
        SyntaxKind propertyModifier,
        CancellationToken cancellationToken)
    {
        var syntaxTokenList = new SyntaxTokenList();

        var existingModifiersWithoutSpecifiedModifier = property.Modifiers.Where(m => m.CSharpKind() != propertyModifier);
        foreach (var item in existingModifiersWithoutSpecifiedModifier)
        {
            syntaxTokenList = syntaxTokenList.Add(item);
        }

        var newProperty = SyntaxFactory.PropertyDeclaration(new SyntaxList<AttributeListSyntax>(), syntaxTokenList, property.Type, null, property.Identifier, property.AccessorList);

        return await ReplacePropertyInDocumentAsync(document, property, newProperty, cancellationToken);
    }

    private async Task<Document> ReplacePropertyModifierAsync(
        Document document,
        PropertyDeclarationSyntax property,
        SyntaxKind propertyModifier,
        CancellationToken cancellationToken)
    {
        var previousWhiteSpacesToken = SyntaxFactory.Token(property.GetLeadingTrivia(), SyntaxKind.StringLiteralToken, SyntaxTriviaList.Empty);

        var newProperty = property.WithModifiers(SyntaxTokenList.Create(previousWhiteSpacesToken)
            .Add(SyntaxFactory.Token(propertyModifier))
            .Add(_whitespaceToken));

        if (property.Modifiers.Any(m => m.CSharpKind() == SyntaxKind.VirtualKeyword))
        {
            newProperty = newProperty.AddModifiers(SyntaxFactory.Token(SyntaxKind.VirtualKeyword), _whitespaceToken);
        }

        return await ReplacePropertyInDocumentAsync(document, property, newProperty, cancellationToken);
    }

    private static async Task<Document> ReplacePropertyInDocumentAsync(
        Document document,
        PropertyDeclarationSyntax property,
        PropertyDeclarationSyntax newProperty,
        CancellationToken cancellationToken)
    {
        var root = await document.GetSyntaxRootAsync(cancellationToken);
        var newRoot = root.ReplaceNode(property, new[] { newProperty });

        return document.WithSyntaxRoot(newRoot);
    }
}

This class might be a bit more complicated but, in fact, it’s pretty simple. The method ComputeFixesAsync is used to show to the user the available fixes that he can used. To display this fixes, we need to get the different diagnostics of the location we are and show the fixes available for the dedicated diagnostics.

In our case, we want to change the modifiers of the property we first need to check the current modifier and show the fixes:

if (property.Modifiers.Any(m => m.CSharpKind() == SyntaxKind.PublicKeyword))
{
    context.RegisterFix(
        CodeAction.Create("To private", c => ReplacePropertyModifierAsync(context.Document, property, SyntaxKind.PrivateKeyword, c)),
        diagnostic);

    context.RegisterFix(
        CodeAction.Create("To protected", c => ReplacePropertyModifierAsync(context.Document, property, SyntaxKind.ProtectedKeyword, c)),
        diagnostic);

    context.RegisterFix(
        CodeAction.Create("To internal", c => ReplacePropertyModifierAsync(context.Document, property, SyntaxKind.InternalKeyword, c)),
        diagnostic);
}

In a code fix provider class, we have access to the current document and we’ll use it to show the preview of what will be the result’s code if the user select a dedicated fix. We can’t just update the current document so we need to create a copy of the document and edit it on the fly:

private async Task<Document> ReplacePropertyModifierAsync(
    Document document,
    PropertyDeclarationSyntax property,
    SyntaxKind propertyModifier,
    CancellationToken cancellationToken)
{
    var previousWhiteSpacesToken = SyntaxFactory.Token(property.GetLeadingTrivia(), SyntaxKind.StringLiteralToken, SyntaxTriviaList.Empty);

    var newProperty = property.WithModifiers(SyntaxTokenList.Create(previousWhiteSpacesToken)
        .Add(SyntaxFactory.Token(propertyModifier))
        .Add(_whitespaceToken));

    if (property.Modifiers.Any(m => m.CSharpKind() == SyntaxKind.VirtualKeyword))
    {
        newProperty = newProperty.AddModifiers(SyntaxFactory.Token(SyntaxKind.VirtualKeyword), _whitespaceToken);
    }

    return await ReplacePropertyInDocumentAsync(document, property, newProperty, cancellationToken);
}

private static async Task<Document> ReplacePropertyInDocumentAsync(
    Document document,
    PropertyDeclarationSyntax property,
    PropertyDeclarationSyntax newProperty,
    CancellationToken cancellationToken)
{
    var root = await document.GetSyntaxRootAsync(cancellationToken);
    var newRoot = root.ReplaceNode(property, new[] { newProperty });

    return document.WithSyntaxRoot(newRoot);
}

As you can see, changing a property’s modifier is simple: we copy the property and add the specified modifier (if the “virtual” modifier has been already added, we copy it again). Note that we also add a whitespace after the modifier so the syntax will be respected.

Once executed, the code will allow to display the following fixes:

image

image

When user put its mouse over a fix, we can see a preview of the fix (the red lines consists of the code that will be removed and the green lines are the code that will be added):

image

image

Of course, the “Preview changes” feature works here too:

image

The good point is that we just modified the modifiers without changing the accessors. So if you have dedicated code in the get/set, it will be persisted:

image

 

As you can see, Roslyn give you a powerful object model to manipulate your code. You will now be able to create powerful extensions in a simple manner!

 

Happy coding!