1

I'm building an analyzer for C# code which generates errors when a string literal is used instead of a const string for certain arguments for certain functions. Ie.

class MyClass
{
  private void MyMethod(IWriter writer)
  {
    writer.WriteInteger("NamedValue", 4);
  }
}

Should become:

class MyClass
{
  private const string IoNamedValueKey = "NamedValue";
  private void MyMethod(IWriter writer)
  {
    writer.WriteInteger(IoNamedValueKey , 4);
  }
}

I've got the bit working where it displays the error, but I want to provide a CodeFixProvider as well. I've run into two problems:

  1. I need to add the private const string IoNamedValueKey = "NamedValue"; statement, ideally just above the offending method.
  2. But only if it doesn't exist already.

I'm not entirely sure the template approach for the CodeFixProvider uses the appropriate overloads for my purposes (it merely replaces type names with upper case variants), so what would be the best way forward from within the RegisterCodeFixesAsync method?

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
  // ... now what?
}

According to roslynquoter the required node can be constructed as below, but I'm still somewhat at a loss about how to inject it into the context.

CompilationUnit()
.WithMembers(
    SingletonList<MemberDeclarationSyntax>(
        FieldDeclaration(
            VariableDeclaration(
                PredefinedType(
                    Token(SyntaxKind.StringKeyword)))
            .WithVariables(
                SingletonSeparatedList<VariableDeclaratorSyntax>(
                    VariableDeclarator(
                        Identifier("IoNamedValueKey"))
                    .WithInitializer(
                        EqualsValueClause(
                            LiteralExpression(
                                SyntaxKind.StringLiteralExpression,
                                Literal("NamedValue")))))))
        .WithModifiers(
            TokenList(
                new []{
                    Token(SyntaxKind.PrivateKeyword),
                    Token(SyntaxKind.ConstKeyword)}))))
.NormalizeWhitespace()
David Rutten
  • 4,716
  • 6
  • 43
  • 72

1 Answers1

2

You should register a CodeAction that introduces the changed document through the context. For

  • Generating the SyntaxNodes - you can use use CSharp SyntaxFactory
  • Getting unique name for your consant - look at Roslyn's UniqueNameGenerator and NameGenerator, they are not exposed by the API but it would be very easy to re-implement some simplified version of them.

Here is an example scratch of what your code might look like (updated):

    public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
    {
        var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

        var diagnostic = context.Diagnostics.First();
        var diagnosticSpan = diagnostic.Location.SourceSpan;

        var argument = root.FindNode(diagnosticSpan);
        if (!IsBadStringLiteralArgument(argument))
        {
            return;
        }

        // Register a code action that will invoke the fix.
        context.RegisterCodeFix(
            CodeAction.Create(
                title: title,
                createChangedDocument: (ct) => InlineConstField(context.Document, root, argument, ct),
                equivalenceKey: title),
            diagnostic);
    }

    private async Task<Document> InlineConstField(Document document, SyntaxNode root, SyntaxNode argument, CancellationToken cancellationToken)
    {
        var stringLiteral = (argument as ArgumentSyntax).Expression as LiteralExpressionSyntax;
        string suggestdName = this.GetSuggestedName(stringLiteral);
        var containingMember = argument.FirstAncestorOrSelf<MemberDeclarationSyntax>();
        var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
        var containingMemberSymbol = semanticModel.GetDeclaredSymbol(containingMember);


        var takenNames = containingMemberSymbol.ContainingType.MemberNames;
        string uniqueName = this.GetUniqueName(suggestdName, takenNames);
        FieldDeclarationSyntax constField = CreateConstFieldDeclaration(uniqueName, stringLiteral).WithAdditionalAnnotations(Formatter.Annotation);

        var newRoot = root.ReplaceNode(containingMember, new[] { constField, containingMember });
        newRoot = Formatter.Format(newRoot, Formatter.Annotation, document.Project.Solution.Workspace);
        return document.WithSyntaxRoot(newRoot);
    }

    private FieldDeclarationSyntax CreateConstFieldDeclaration(string uniqueName, LiteralExpressionSyntax stringLiteral)
    {
        return SyntaxFactory.FieldDeclaration(
            SyntaxFactory.List<AttributeListSyntax>(),
            SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PrivateKeyword), SyntaxFactory.Token(SyntaxKind.ConstKeyword)),
            SyntaxFactory.VariableDeclaration(
                SyntaxFactory.ParseTypeName("string"), 
                SyntaxFactory.SingletonSeparatedList(
                    SyntaxFactory.VariableDeclarator(
                        SyntaxFactory.Identifier(uniqueName), 
                        argumentList: null, 
                        initializer: SyntaxFactory.EqualsValueClause(stringLiteral)))));

    }
NValchev
  • 2,855
  • 2
  • 15
  • 17
  • can't get that to work I'm afraid, I'm getting an `Unable to cast object of type 'Microsoft.CodeAnalysis.CSharp.Syntax.CompilationUnitSyntax' to type 'Microsoft.CodeAnalysis.CSharp.Syntax.MemberDeclarationSyntax'.` error when I try to use either `root.ReplaceNode()` or `root.InsertNodesBefore()`. I'm using this code to create the const declaration: `ParseCompilationUnit(string.Format("private const string {0} = \"{1}\";", name, value));` – David Rutten Jun 11 '16 at 19:41
  • It's not possible inserting CompilationUnitSyntax in TypeDeclarationyntax because it isn't valid syntax, try creating a FieldDeclarationSyntax and inserting it, look at the SyntaxFactory class reference I've sent you in the answer – NValchev Jun 11 '16 at 19:48
  • I managed to get it to insert the text, but this _cannot_ be the right way to do it: VariableDeclarationSyntax variable = VariableDeclaration(ParseTypeName("private const string")); variable = variable.AddVariables(VariableDeclarator(" " + name + " = \"" + value + "\"")); return FieldDeclaration(variable).NormalizeWhitespace(); – David Rutten Jun 11 '16 at 20:50
  • I've updated the answer by adding a sample how you could create a `constant field`, I hope it would help – NValchev Jun 12 '16 at 07:14
  • Yeah that worked. I couldn't find anything online about how to do this sort of stuff with Roslyn except for roslynquoter. Shame the approach to making these nodes is so different from the way they would be typed in regular C#... – David Rutten Jun 12 '16 at 09:37