2

I am doing a code analyser but I can't find a way to change the class and the members being accessed when you are calling a method. For example if i have

public class FirstClass
{
    public static void DoSomething(object SomeParameter)
    {
        //method body 
    }
}
public class SecondClass
{
    public static void ReplaceDoSomething(object SomeParameter)
    {
        //other method's body
    }
}

How could i make my code fix change FirstClass.DoSomething(new object()); to SecondClass.ReplaceDoSomething(new object());?

I tried to use SyntaxFactory but i can't realy figure out what parameters are expected for most methods and the MSDN provides nearly no details on them...

The code analyzer

public override void Initialize(AnalysisContext context)
{
    context.RegisterOperationAction(Analyze, OperationKind.Invocation);
}
private void Analyze(OperationAnalysisContext context)
{
    IInvocationOperation operation = (IInvocationOperation)context.Operation;
    if (operation.TargetMethod.Name == "DoSomething" && operation.TargetMethod.ContainingNamespace.Name == "FirstNamespace" && operation.TargetMethod.ContainingType.Inherits("FirstNamespace.FirstClass"))
    {
        //Rule is an automaticaly generated variable by visual studio when you create the analyzer
        Diagnostic diagnostic = Diagnostic.Create(Rule, operation.Syntax.GetLocation());
        context.ReportDiagnostic(diagnostic);
    }
}
public static class INamedTypeSymbolExtensions
{
    public static bool Inherits(this INamedTypeSymbol first, string otherFullName)
    {
        if (first.GetFullName() == otherFullName)
        {
            return true;
        }
        else if (typeof(object).FullName == first.GetFullName())
        {
            return false;
        }
        else
        {
            return first.BaseType.Inherits(otherFullName);
        }
    }

    public static string GetFullName(this INamedTypeSymbol element)
    {
        StringBuilder ret = new StringBuilder(element.Name);
        INamespaceSymbol ContainingNamespace = element.ContainingNamespace;
        while (ContainingNamespace.ContainingNamespace != null)
        {
            ret.Insert(0, ContainingNamespace.Name + ".");
            ContainingNamespace = ContainingNamespace.ContainingNamespace;
        }
        return ret.ToString();
    }
}

EDIT : The best result i have had was with

private async Task<Document> ProvideCodeFix(Document document, InvocationExpressionSyntax invocation, CancellationToken cancellationToken)
{
    SyntaxToken replaced = invocation.GetFirstToken();
    SyntaxToken replacing = SyntaxFactory.IdentifierName("SecondClass").GetFirstToken().WithLeadingTrivia(replaced.LeadingTrivia));
    return document.WithSyntaxRoot((await document.GetSyntaxRootAsync()).ReplaceToken(replaced, replacing);
}

with this it would change FirstClass.DoSomething(new object()); to SecondClass.DoSomething(new object());

But the main problem with this approach is that if i'd have to change FirstNamespace.FirstClass.DoSomething(new object()); to SecondClass.ReplaceDoSomething(new object()); then it'd not work

PS : I use .WithLeadingTrivia(...) to keep the comments and such stuff that was written before FirstClass

nalka
  • 1,894
  • 11
  • 26
  • I tried to use loads of methods mostly `.WithSomething(...)` or `.ReplaceSomething(...)` so i can't realy write them all so i wrote the most successful result i have had for now @NineBerry – nalka Jan 09 '19 at 16:01
  • @NineBerry are you talking about the code that calls `ProvideCodeFix(...)`? – nalka Jan 09 '19 at 16:35
  • ah the analyzer, it works fine and underlines correctly, both are quite independant anyways aren't them? (i mean expect the expression being underlined and its location) – nalka Jan 09 '19 at 16:45
  • hmmm yeah from this point of view that's right, i had made the (wrong) assumption that people would probably not need to test it as this question isn't that advanced. Give me enough time and i'll add the analyzer. – nalka Jan 09 '19 at 16:53
  • The code that registers `ProvideCodeFix`is still missing – NineBerry Jan 09 '19 at 17:58
  • it's been auto generated too – nalka Jan 10 '19 at 07:32

1 Answers1

2

This code works. We get the InvocationExpressionSyntax.Expression which represents the whole part before the () argument list. Then we replace that whole expression.

private async Task<Document> ProvideCodeFix(Document document, InvocationExpressionSyntax invocationExpression, CancellationToken cancellationToken)
{
    // Get the expression that represents the entity that the invocation happens on
    // (In this case this is the method name including possible class name, namespace, etc)
    var invokedExpression = invocationExpression.Expression;

    // Construct an expression for the new classname and methodname
    var classNameSyntax = SyntaxFactory.IdentifierName("SecondClass");
    var methodNameSyntax = SyntaxFactory.IdentifierName("ReplaceDoSomething");
    var classNameAndMethodNameSyntax = SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, classNameSyntax, methodNameSyntax);

    // Add the surrounding trivia from the original expression
    var replaced = classNameAndMethodNameSyntax;
    replaced = replaced.WithLeadingTrivia(invokedExpression.GetLeadingTrivia());
    replaced = replaced.WithTrailingTrivia(invokedExpression.GetTrailingTrivia());

    // Replace the whole original expression with the new expression
    return document.WithSyntaxRoot((await document.GetSyntaxRootAsync()).ReplaceNode(invokedExpression, replaced));
}
NineBerry
  • 26,306
  • 3
  • 62
  • 93
  • It works alright except when you have comments between `FirstClass` and `DoSomething` for example `FirstClass./*wierd comment*/DoSomething(new object());` will be changed to `SecondClass.ReplaceDoSomething(new object());` anyways your answer is enough to me, thanks – nalka Jan 10 '19 at 07:56
  • @nalka Keeping such comments requires a bit more code and is a bit more complex because the `invokedExpression` can be different types of syntaxes (a simple `IdentifierNameSyntax` or a `MemberAccessExpressionSyntax` in several varieties). But it can be done. I think what you should take away from my answer is to work with SyntaxNodes instead of on the token level. – NineBerry Jan 10 '19 at 15:31