Roslyn是用于编译时表达式检查的合适工具吗

本文关键字:工具 检查 表达式 用于 编译 Roslyn | 更新日期: 2023-09-27 18:28:10

我有一个工具包,它有许多方法,通常将Expression<Func<T,TProperty>>作为参数。有些可以是单级(o=>o.Name),而有些可以是多级(o=>o.EmployeeData.Address.Street)。

我想开发一些东西(MSBuild Task?Visual Studio Plugin?希望是第一个),它可以读取所有用户的.cs文件,并且如果给定的参数不是属性表达式(而是类似于o=>o.Contains("foo")的东西),或者如果给定了只允许单个级别的多级别表达式,则会产生生成错误。

我试着先查看编译后的IL代码,但由于表达式树是C#编译器的"窍门",在IL中,我所看到的只是创建表达式实例之类的,虽然如果只创建MemberExpression(以及它们的正确数量),我可以检查每个实例,但这并不是很好。

然后我想起了罗斯林有可能和罗斯林一起写这样的东西吗

Roslyn是用于编译时表达式检查的合适工具吗

是的,我认为Roslyn及其代码问题正是解决这一问题的正确工具。使用它们,您可以在键入和创建在VisualStudio中显示为其他错误的错误(或警告)时分析代码。

我曾试图创建这样的代码问题:

[ExportSyntaxNodeCodeIssueProvider("PropertyExpressionCodeIssue", LanguageNames.CSharp, typeof(InvocationExpressionSyntax))]
class PropertyExpressionCodeIssueProvider : ICodeIssueProvider
{
    [ImportingConstructor]
    public PropertyExpressionCodeIssueProvider()
    {}
    public IEnumerable<CodeIssue> GetIssues(IDocument document, CommonSyntaxNode node, CancellationToken cancellationToken)
    {
        var invocation = (InvocationExpressionSyntax)node;
        var semanticModel = document.GetSemanticModel(cancellationToken);
        var semanticInfo = semanticModel.GetSemanticInfo(invocation, cancellationToken);
        var methodSymbol = (MethodSymbol)semanticInfo.Symbol;
        if (methodSymbol == null)
            yield break;
        var attributes = methodSymbol.GetAttributes();
        if (!attributes.Any(a => a.AttributeClass.Name == "PropertyExpressionAttribute"))
            yield break;
        var arguments = invocation.ArgumentList.Arguments;
        foreach (var argument in arguments)
        {
            var lambdaExpression = argument.Expression as SimpleLambdaExpressionSyntax;
            if (lambdaExpression == null)
                continue;
            var parameter = lambdaExpression.Parameter;
            var memberAccess = lambdaExpression.Body as MemberAccessExpressionSyntax;
            if (memberAccess != null)
            {
                var objectIdentifierSyntax = memberAccess.Expression as IdentifierNameSyntax;
                if (objectIdentifierSyntax != null
                    && objectIdentifierSyntax.PlainName == parameter.Identifier.ValueText
                    && semanticModel.GetSemanticInfo(memberAccess, cancellationToken).Symbol is PropertySymbol)
                    continue;
            }
            yield return
                new CodeIssue(
                    CodeIssue.Severity.Error, argument.Span,
                    string.Format("Has to be simple property access of '{0}'", parameter.Identifier.ValueText));
        }
    }
    #region Unimplemented ICodeIssueProvider members
    public IEnumerable<CodeIssue> GetIssues(IDocument document, CommonSyntaxToken token, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
    public IEnumerable<CodeIssue> GetIssues(IDocument document, CommonSyntaxTrivia trivia, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
    #endregion
}

用法如下:

[AttributeUsage(AttributeTargets.Method)]
class PropertyExpressionAttribute : Attribute
{ }
…
[PropertyExpression]
static void Foo<T>(Expression<Func<SomeType, T>> expr)
{ }
…
Foo(x => x.P);   // OK
Foo(x => x.M()); // error
Foo(x => 42);    // error

上面的代码有几个问题:

  1. 它完全没有优化
  2. 它可能需要更多的错误检查
  3. 它不起作用至少在当前CTP中。接近末尾的表达式semanticModel.GetSemanticInfo(memberAccess, cancellationToken).Symbol总是返回null。这是因为表达式树的语义是目前尚未实现的特性之一

是的,这是完全可能的。问题是Roslyn还不支持所有的语言构造,所以您可能会遇到一些不受支持的东西。表达式树是不受支持的,因为Roslyn无法编译生成表达式的代码,但您应该能够做得足够远,使某些事情正常工作。

在高层,如果您想将其实现为MSBuild任务,那么在构建任务中可以调用Roslyn.Services.Workspace.LoadSolutionRoslyn.Services.Workspace.LoadStandaloneProject。然后,您可以遍历语法树,查找各种方法的提及,然后绑定它们,以确保它实际上是您认为要调用的方法。从那里,您可以找到lambda语法节点,并从那里执行您想要的任何语法/语义分析。

CTP中有一些示例项目可能会很有用,例如RFxCopConsoleCS项目,它在Roslyn中实现了一个简单的FxCop样式规则。

我还应该提到,Roslyn的解析器是完整的,所以在没有语义信息的情况下可以做得越多越好。