Статьи

Круг вокруг Roslyn CTP: переписывание синтаксиса с символьной информацией

В прошлый раз мы заменяли 42 числовых литерала на 43. На этот раз давайте представим, что делаем что-то более полезное. Предположим , что вы на самом деле не любят разработчиков , призывающих к Console.Write метод и настаивают на использовании Console.WriteLine вместо этого. Вы можете неохотно использовать поиск и замену, потому что — как и в прошлый раз — вы не хотите изменять вызовы Console.Write внутри комментариев, внутри строковых литералов или — и это порочно — вызовы консоли. Напишите метод для чего-то, что не является классом System.Console из сборки mscorlib , как, например, свойство с именем Console !

C # парсер, который мы встретились в его SyntaxTree воплощения, не связывает MethodInvocationExpression случаи к фактическому вызываемому методу. Все, что его волнует, — это правильная структура выражения. Несмотря на все это, Console может быть закрытым классом, а не BCL.

Введите семантическую модель ( класс SemanticModel ), которая представляет все, что компилятор знает о вашем коде после привязки синтаксического дерева к символам. В этом случае семантическая модель даст нам символ для выражения вызова Console.Write , и мы сможем определить, какой Console.Write вызывается, и заменить его соответствующим образом.

Чтобы получить экземпляр SemanticModel , нам нужно предоставить Roslyn всю информацию для выполнения привязки, то есть ссылки на сборки для нашего кода. Напомним, что мы могли бы создать SyntaxTree без указания их! Эта информация обернута экземпляром класса Compilation , который может (в конце концов) использоваться для генерации реального кода.

Compilation compilation = Compilation.Create(
    "MyCompilation",
    CompilationOptions.Default,
    new SyntaxTree[] { tree },
    new MetadataReference[] {
        new AssemblyFileReference(
            typeof(object).Assembly.Location)
    },
    null, null);
SemanticModel model = compilation.GetSemanticModel(tree);

Теперь, когда у нас есть семантическая модель, мы можем передать ее конструктору нашего редактора:

/// <summary>
/// Replaces Console.Write calls with equivalent
/// Console.WriteLine calls.
/// </summary>
class MyConsoleWriteRewriter : SyntaxRewriter
{
    private readonly SemanticModel _semanticModel;

    public MyConsoleWriteRewriter(SemanticModel model)
    {
        _semanticModel = model;
    }

    protected override SyntaxNode
        VisitInvocationExpression(
            InvocationExpressionSyntax node)
    {
        SemanticInfo info =
            _semanticModel.GetSemanticInfo(node);
        MethodSymbol symbol = (MethodSymbol)info.Symbol;
        if (symbol.Name == "Write" &&
            symbol.ContainingType.Name == "Console" &&
            symbol.ContainingNamespace.Name == "System" &&
            symbol.ContainingAssembly.Name == "mscorlib")
        {
            MemberAccessExpressionSyntax old =
                (MemberAccessExpressionSyntax)
                node.Expression;
            return node.ReplaceNode(
                old,
                old.Update(
                    old.Expression,
                    old.OperatorToken,
                    Syntax.IdentifierName("WriteLine")));
        }            
        return node;
    }
}

Чтобы понять, что здесь происходит, давайте взглянем на структуру узла MethodInvocationExpression для типичного вызова Console.Write :

образ

MethodInvocationExpression , в данном случае, состоит из MemberAccessExpression , который определяет метод вызова, и ArgumentList , который задает аргументы. Поскольку мы доверяем Console.WriteLine для принятия тех же аргументов, что и Console.Write , нам не нужно прикасаться к узлу ArgumentList . Более того, нам даже не нужно прикасаться к первому IdentifierName в выражении MemberAccessExp; все, что нам нужно заменить, это второе IdentifierName .

Поэтому мы возвращаем новый узел из нашего метода VisitInvocationExpression всякий раз, когда нам есть чем заменить существующий узел. В частности, мы просим семантическую модель предоставить нам символьную информацию для выражения вызова метода — если он соответствует методу System.Console.Write из сборки mscorlib , мы сохраняем все выражение, кроме идентификатора имени метода.

Конечно, чтобы применить этот переписчик к нашему дереву, нам нужно предоставить ему экземпляр SemanticModel, полученный ранее:

SyntaxNode newRoot =
    new MyConsoleWriteRewriter(model).Visit(tree.Root);

Это на самом деле начинает выглядеть полезным. В следующий раз мы не будем переписывать, а проведем более сложный анализ синтаксического дерева и семантической модели, включая поток данных и поток управления внутри метода.