Является ли Roslyn правильным инструментом для проверки выражений во время компиляции? - PullRequest
11 голосов
/ 19 марта 2012

У меня есть инструментарий, в котором есть много методов, часто принимающих 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 все, что я вижу, - это создание экземпляров выражений и тому подобное, и хотя я мог бы проверять каждый, если только MemberExpressions (и правильное их количество) созданы, это не так уж и здорово.

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

Ответы [ 2 ]

10 голосов
/ 20 марта 2012

Да, я думаю, что 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

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

  1. Это совершенно неоптимизировано.
  2. Вероятно, требуется дополнительная проверка ошибок.
  3. Не работает. По крайней мере, в текущем CTP. Выражение semanticModel.GetSemanticInfo(memberAccess, cancellationToken).Symbol ближе к концу всегда возвращает null. Это связано с тем, что семантика деревьев выражений входит в число нереализованных функций .
5 голосов
/ 20 марта 2012

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

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

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

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

...