如何使用 Roslyn 查找调用方法的参数是变量(通过 "var/string")还是内联字符串

How to find if a parameter to an invoked method is a variable (via "var/string") or an in-line string using Roslyn

我目前正在尝试查找 .ExecuteSqlCommand 的调用并检查传递给 sql 参数的第一个值。

这是我在我们的代码库中发现的差异示例。

ExecuteSqlCommand("[sql statement here]");

对比

var sql = "sql statement";
ExecuteSqlCommand(sql);

到目前为止,我有这个:

var invocations = root.DescendantNodes()
    .OfType<InvocationExpressionSyntax>()
    .Select(ie => ModelExtensions.GetSymbolInfo(model, ie).Symbol)
    .Where(symbol => symbol != null && symbol.Name == "ExecuteSqlCommand");

foreach (var invocation in invocations)
{
    var method = (IMethodSymbol)invocation;
    foreach (var param in method.Parameters)
    {
        //I can't quite seem to get information from IParameterSymbol about whether the param is a string literal, or a reference to a string via a variable.
    }
}

如果参数不是字符串,而是 var,那么我需要获取 var 的值(尽可能多地在运行时定义)。

我不太确定这是 SemanticModel 还是 SyntaxTree 的工作,但我的猜测是 SemanticModel 应该有更丰富的信息,我需要让我发现我正在寻找的东西。

我的总体目标是询问 sql 传递给 ExecuteSqlCommand 方法。

谢谢!

不幸的是,在运行时定义值的常见情况下,Roslyn 无法提供变量的值,因为 Roslyn 实际上不知道可能从程序外部传递的所有可能值,它不计算它们等等等等。但是,如果您可以将所需的情况限制为在字符串中声明和初始化的内联字符串或变量,Roslyn 可能会帮助您:

  • 你需要保留InvocationExpressionSyntax(或直接他们的第一个参数)
var invocations = root.DescendantNodes()
    .OfType<InvocationExpressionSyntax>()
    .Select(ie => (ModelExtensions.GetSymbolInfo(model, ie).Symbol, ie))
    // Would be better to compare not method's name but it FQN
    .Where((symbol, node)) => symbol != null && symbol.Name == "ExecuteSqlCommand" && 
            symbol.Parameters.Length == 1 && 
            symbol.Parameters[0].Type.SpecialType == SpecialType.System_String && 
            node.ArgumentList.Arguments.Count == 1)
    .Select((symbol, node) => node);
  • 您需要检查的不是方法参数,而是传递给它的方法参数
foreach (var invocation in invocations)
{
    var argument = invocation .ArgumentList.Arguments[0];
    if (argument is LiteralExpressionSyntax literal && literal.IsKind(SyntaxKind.StringLiteralExpression))
    {
        // You find invocation of kind `ExecuteSqlCommand("sql")`
    }
    else
    {
        var argSymbol = ModelExtensions.GetSymbolInfo(model, argument).Symbol;
        if (!(argSymbol is null) && (argSymbol.Kind == SymbolKind.Field || argSymbol.Kind == SymbolKind.Property || argSymbol.Kind == SymbolKind.Local))
        {
            if (argSymbol.DeclaringSyntaxReferences.Length == 1)
            {
                var declarationNode = argSymbol.DeclaringSyntaxReferences[0].GetSyntax();

                // I'm actually don't remember what exactlly `GetSyntax` returns for fields or locals: 
                // VariableDeclaratorSyntax or one of it parent LocalDeclarationStatementSyntax and FieldDeclarationSyntax, but if it returns declarations you also can 
                // get from them VariableDeclaratorSyntax that you need, it's just be a more deep pattern matching

                if (declarationNode is VariableDeclaratorSyntax declaratorSyntax && 
                    declaratorSyntax.EqualsValueClauseSyntax?.Value is LiteralExpressionSyntax literal2 && literal2.IsKind(SyntaxKind.StringLiteralExpression) )
                {
                    // You find invocation of kind `ExecuteSqlCommand(variable)` where variable is local variable or field 
                }
                else if (declarationNode is PropertyDeclarationSyntax property)
                {
                    // You can do the same things for properties initializer or expression body that you was do for fields and locals to check your case,
                    // but it doesn't work for property with get/set accessors in a common cases
                }
            }
        }
    }
}

语法 API 可用于提取 sql 语句值,但取决于变量声明(即 var sql = "sql statement";)是否包含在提交给语法的代码中树。

例如,如果它与调用 ExcuteSqlCommand() 的方法实现相同,那么您可以首先获取传递给它的变量名称(即 sql)并使用它来查找相同方法 中的匹配变量声明语句。最后,可以从中提取 sql 语句值(即 "sql statement")。

以下代码首先检查 sql 值是否作为字符串文字传递,否则查找变量声明。假设它们都在同一个方法中:

        // whatever c# *method* code contains the sql. 
        // otherwise the root of the tree would need to be changed to filter to a specific single `MethodDeclarationSyntax`.
        string submittedCode = "public void SomeMethodContainingSql(){ ...//rest of code...";

        var tree = CSharpSyntaxTree.ParseText(submittedCode);
        var root = (CompilationUnitSyntax) tree.GetRoot();

        var arguments = root
            .DescendantNodes()
            .OfType<InvocationExpressionSyntax>()
            .First(node => node.DescendantNodes().OfType<IdentifierNameSyntax>()
                               .First()
                               .Identifier.Text == "ExecuteSqlCommand")
            .ArgumentList.DescendantNodes().ToList();

        string sqlStatementValue = "";

        var literalExpression = arguments.OfType<LiteralExpressionSyntax>().FirstOrDefault();

        if (literalExpression != null)
        {
            sqlStatementValue = literalExpression.GetText().ToString();
        }
        else
        {
            var variableName = arguments
                .First()
                .ToFullString();

            var variableDeclaration = root
                .DescendantNodes()
                .OfType<VariableDeclarationSyntax>()
                .Single(node => node.DescendantNodes().OfType<VariableDeclaratorSyntax>()
                                    .First()
                                    .Identifier.Text == variableName);

            sqlStatementValue = variableDeclaration.DescendantNodes()
                .OfType<LiteralExpressionSyntax>()
                .First()
                .DescendantTokens()
                .First()
                .Text;
        }

否则,可能需要在提交代码的其他部分(例如class字段、属性、其他方法等)寻找变量声明,这有点麻烦。

SemanticModel.GetConstantValue 是我们处理这种情况的 API。

它可以接受两个syntax node and an expression。您仍然需要跟踪变量的状态回到它们的声明位置,并确定它们是否被赋予常量表达式。

我会使用 SemanticModel.GetSymbolInfo.Symbol?.DeclaringSyntaxReferences.First() 来查找变量的声明位置,然后检查它是否是常量表达式。