Является ли Roslyn правильным инструментом для проверки Expression во время компиляции?

У меня есть набор инструментов, который имеет множество методов, часто принимающих Expression<Func<T,TProperty>> в качестве параметров. Некоторые могут быть только одноуровневыми (o=>o.Name), а некоторые могут быть многоуровневыми (o=>o.EmployeeData.Address.Street).

Я хочу что-то разработать (задача MSBuild - плагин Visual Studio, надеюсь, первый), который читает все пользовательские .cs файлы и дает ошибки сборки, если данный параметр не является выражением свойства (но что-то вроде o=>o.Contains("foo")), или если задано многоуровневое выражение, где разрешен только один уровень.

Я попытался сначала скомпилировать IL-код, но поскольку деревья выражений являются "трюком" компилятора С#, в IL все, что я вижу, это создание экземпляров экземпляра и т.д., и хотя я мог проверять каждый, если только MemberExpressions (и правильный номер из них), это не так здорово.

Тогда Рослин пришла мне в голову. Можно ли написать что-то подобное с Roslyn?

Ответ 1

Да, я думаю, что Roslyn и его проблемы с кодом - это именно тот инструмент. С их помощью вы можете анализировать код во время ввода и создавать ошибки (или предупреждения), которые отображаются как другие ошибки в Visual Studio.

Я попытался создать такую ​​проблему с кодом:

[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

В приведенном выше коде есть несколько проблем:

  • Он полностью неоптимизирован.
  • Возможно, потребуется еще немного проверки ошибок.
  • Он не работает. По крайней мере, в текущем CTP. Выражение semanticModel.GetSemanticInfo(memberAccess, cancellationToken).Symbol рядом с концом всегда возвращает null. Это связано с тем, что семантика деревьев выражений находится в не реализованных в настоящее время функциях.

Ответ 2

Да, это вполне возможно. Проблема в том, что Roslyn еще не поддерживает все языковые конструкции, поэтому вы можете столкнуться с некоторыми неподдерживаемыми вещами. Деревья выражений не поддерживаются тем, что Roslyn не может скомпилировать код, который генерирует выражения, но вы должны быть достаточно далеко, чтобы заставить некоторые вещи работать.

На высоком уровне, если вы хотите реализовать это как задачу MSBuild, в своей задаче сборки вы можете вызвать Roslyn.Services.Workspace.LoadSolution или Roslyn.Services.Workspace.LoadStandaloneProject. Затем вы проходите через деревья синтаксиса, которые ищут упоминания о ваших различных методах, а затем связывают их, чтобы убедиться, что это фактически тот метод, который, как вы думаете, вы вызываете. Оттуда вы можете найти узлы синтаксиса лямбда и выполнить любой синтаксис/семантический анализ, который вы хотите оттуда.

В CTP есть несколько примеров проектов, которые могут вам пригодиться, например проект RFxCopConsoleCS, который реализует в Roslyn простое правило стиля FxCop.

Я должен также упомянуть, что синтаксический анализатор завершен для Roslyn, поэтому чем больше вы можете обойтись без семантической информации, тем лучше.