David Rutten
David Rutten

Reputation: 4806

Replacing string literals with private const statements

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()

Upvotes: 1

Views: 797

Answers (1)

NValchev
NValchev

Reputation: 3005

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)))));

    }

Upvotes: 3

Related Questions