Search code examples
roslynroslyn-code-analysis

Roslyn - obtain reference to a string assigned to property


Throughout Visual Studio Solution with multiple projects, I have code snippets similar to:

sqlCommand.CommandText = "Some SQL statement";

I am able to obtain all references to CommandText being a Callee inside of a solution via:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.FindSymbols;
// ...

var project = _solution.GetProject("my-project");
            

var compilation = await project.GetCompilationAsync();
var sqlCmdSymbol = compilation.GetTypeByMetadataName(typeof(SqlCommand).FullName);
var cmdTextSymbol = sqlCmdSymbol.GetMembers("CommandText").First();

IEnumerable<SymbolCallerInfo> allReferences = await SymbolFinder.FindCallersAsync(cmdTextSymbol, _solution);
var calledSymbolReferences = allReferences.Where(r => SymbolEqualityComparer.Default.Equals(r.CalledSymbol, cmdTextSymbol)).ToArray();

foreach (SymbolCallerInfo reference in calledSymbolReferences)
{
    // How to get from `SymbolCallerInfo` to a `LiteralExpressionSyntax` following it?
    // (eg .CommandText = "Some SQL statement";)

}

There is a code example from Microsoft Docs how to capture string literal:

// Use the syntax model to find the literal string:
LiteralExpressionSyntax helloWorldString = root.DescendantNodes()
    .OfType<LiteralExpressionSyntax>()
    .Single();

// Use the semantic model for type information:
TypeInfo literalInfo = model.GetTypeInfo(helloWorldString);

But I'm not sure how to get from the reference of type SymbolCallerInfo to a property CommandText to the string that is assigned to it?


Solution

  • You need to look for a AssignmentExpressionSyntax that has syntax for the CommandText property as its Left expression.

    You can find that syntax from the Locations property of the SymbolCallerInfo reference you have, but in my experience it's easier to work the other way around.

    You're looking for text being assigned to a property, so I would search for AssignmentExpressionSyntax.

    In an Analyzer, you would override the Initialize method as follows:

    public override void Initialize(AnalysisContext context)
    {
        context.EnableConcurrentExecution();
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.ReportDiagnostics);
    
        context.RegisterSyntaxNodeAction(
            AnalyzeAssignmentExpression, SyntaxKind.SimpleAssignmentExpression);
    }
    

    Note that we're only looking for simple assignments here; i.e. assignments using =. I suppose you could consider looking for SyntaxKind.AddAssignmentExpression as well, i.e. assignments using +=, but it would be less easy to determine what exactly is the value being assigned, so I left that out.

    So now we need to implement the AnalyzeAssignmentExpression method.

    private void AnalyzeAssignmentExpression(SyntaxNodeAnalysisContext context)
    

    The first thing we do is get the AssignmentExpressionSyntax node we're analyzing:

    private void AnalyzeAssignmentExpression(SyntaxNodeAnalysisContext context)
    {
        var node = (AssignmentExpressionSyntax)context.Node;
    

    That node has Left and Right properties we need to look at. Let's start with the Left property, and find out what symbol this refers to.

        ISymbol left = context.SemanticModel.GetSymbolInfo(
            node.Left, context.CancellationToken).Symbol;
    

    We need to find out if this Symbol is indeed our CommandText property.

    Here's a small helper method that will do just that:

    private static bool IsCommandText(ISymbol symbol)
        => symbol is IPropertySymbol
        {
            Name: "CommandText",
            ContainingType:
            {
                Name: "SqlCommand",
                ContainingNamespace:
                {
                    Name: "SqlClient",
                    ContainingNamespace:
                    {
                        Name: "Data",
                        ContainingNamespace:
                        {
                            // Allow both Microsoft.Data.SqlClient and System.Data.SqlClient
                            ContainingNamespace:
                            {
                                IsGlobalNamespace: true
                            }
                        }
                    }
                }
            }
        };
    

    (You could also compare its ContainingType to a symbol you looked up once using Compilation.GetTypeByMetadataName(), if you know the exact type you're looking for. I didn't, so I used this pattern.)

    Using that helper method, we can test our left symbol. If it's not our CommandText property, we can stop analyzing this node.

        if (!IsCommandText(left))
        {
            return;
        }
    

    Now let's look at the Right property, the syntax being assigned. You said you're looking for a LiteralExpressionSyntax, but I would suggest you look at any syntax that evaluates to a constant string. It doesn't have to be a literal. It could be the name of a constant for example, a concatenation of two literals, or many other kinds of expressions. But I would think that all you really care about, is its value. So let's see if the Right property has a constant string value. (I am assuming you're not interested is the value null here.) If it has a constant string value, we have found what we're looking for.

        var right = context.SemanticModel.GetConstantValue(node.Right, context.CancellationToken);
    
        if (right.HasValue && right.Value is string value)
        {
            // value is a constant being assigned to SqlCommand.CommandText
            // your further analysis goes here
        }
    }
    

    The entire method:

    private void AnalyzeAssignmentExpression(SyntaxNodeAnalysisContext context)
    {
        var node = (AssignmentExpressionSyntax)context.Node;
    
        ISymbol left = context.SemanticModel.GetSymbolInfo(
            node.Left, context.CancellationToken).Symbol;
    
        if (!IsCommandText(left))
        {
            return;
        }
    
        var right = context.SemanticModel.GetConstantValue(node.Right, context.CancellationToken);
    
        if (right.HasValue && right.Value is string value)
        {
            // value is a constant being assinged to SqlCommand.CommandText
            // your further analysis goes here
        }
    }
    

    If you're not doing this in an analyzer, you can find the AssignmentExpressionSyntax nodes using root.DescendantNodes().OfType<AssignmentExpressionSyntax>(), and from your question I take it you already know how to get the SemanticModel for them.