Поделиться через


Начало работы с функциями синтаксического анализа

В этом учебнике рассматривается синтаксический API. Синтаксический API предоставляет доступ к структурам данных, описывающих программы на языке C# или Visual Basic. Эти структуры данных достаточно подробны, чтобы обеспечивать полное представление любой программы любого размера. Эти структуры могут описывать готовые программы, обеспечивающие корректное выполнение и работу. Они также могут описывать неполные программы по мере их написания в редакторе.

Для обеспечения такого богатого выражения структуры данных и API, составляющие синтаксический API, имеют высокую сложность. Начнем с описания структуры данных для типичной программы "Hello, World!".

using System;
using System.Collections.Generic;
using System.Linq;

namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

Посмотрите на текст предыдущей программы. Вы узнаете знакомые элементы. Весь текст представляет один исходный файл, или блок компиляции. Первые три строки этого исходного файла являются директивами using. Остальной исходный код содержится в объявлении пространства имен. Объявление пространства имен содержит дочернее объявление класса. Объявление класса содержит одно объявление метода.

Синтаксический API создает древовидную структуру, корень которой представляет блок компиляции. Узлы в дереве представляют директивы using, объявление пространства имен и все остальные элементы программы. Древовидная структура продолжается до самых низких уровней: строка "Hello World!" — это токен строкового литерала, который является потомком аргумента. Синтаксический API предоставляет доступ к структуре программы. Можно создавать запросы к определенным практикам написания кода, выполнять обход всего дерева для понимания кода и создавать новые деревья путем изменения существующего дерева.

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

Чтобы приступить к работе, потребуется установить пакет SDK для .NET Compiler Platform:

Инструкции по установке — Visual Studio Installer

Найти SDK-пакет .NET Compiler Platform в Visual Studio Installer можно двумя способами:

Установка с помощью Visual Studio Installer — представление "Рабочие нагрузки"

SDK-пакет .NET Compiler Platform не выбирается автоматически в рамках рабочей нагрузки разработки расширений Visual Studio. Его необходимо выбрать как дополнительный компонент.

  1. Запустите Visual Studio Installer.
  2. Выберите Изменить.
  3. Отметьте рабочую нагрузку Разработка расширений Visual Studio.
  4. Откройте узел Разработка расширений Visual Studio в дереве сводки.
  5. Установите флажок SDK-пакет .NET Compiler Platform. Нужный пакет будет представлен последним в списке дополнительных компонентов.

Кроме того, вы можете настроить редактор DGML для отображения диаграмм в средстве визуализации:

  1. Откройте узел Отдельные компоненты в дереве сводки.
  2. Установите флажок Редактор DGML

Установка с помощью Visual Studio Installer — вкладка "Отдельные компоненты"

  1. Запустите Visual Studio Installer.
  2. Выберите Изменить.
  3. Откройте вкладку Отдельные компоненты.
  4. Установите флажок SDK-пакет .NET Compiler Platform. Нужный пакет будет представлен в разделе Компиляторы, средства сборки и среды выполнения в самом начале.

Кроме того, вы можете настроить редактор DGML для отображения диаграмм в средстве визуализации:

  1. Установите флажок Редактор DGML. Нужный пакет будет представлен в разделе Средства для работы с кодом.

Основные сведения о синтаксисе деревьев

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

Дерево синтаксиса — это структура данных, которая используется компиляторами C# и Visual Basic для понимания программ на языках C# и Visual Basic. Деревья синтаксиса создает тот же синтаксический анализатор, что выполняется при построении проекта или при нажатии на клавишу F5. Деревья синтаксиса полностью соответствуют языку. Каждый бит сведений в файле кода представлен в дереве. При записи дерева синтаксиса в текст воспроизводится в точности тот исходный текст, который был проанализирован. Деревья синтаксиса являются неизменяемыми. После создания дерево синтаксиса невозможно изменить. Пользователи могут анализировать деревья в нескольких потоках без блокировки и принятия других меры по обеспечению параллелизма, поскольку данные не изменяются. Можно использовать API для создания новых деревьев посредством изменения существующего дерева.

Ниже перечислены четыре основных стандартных блока деревьев синтаксиса.

  • Класс Microsoft.CodeAnalysis.SyntaxTree, экземпляр которого представляет дерево синтаксического анализа целиком. SyntaxTree является абстрактным классом с производными, соответствующими конкретному языку. Для синтаксического анализа текста на языке C# (или Visual Basic) используются методы синтаксического анализа класса Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree (или Microsoft.CodeAnalysis.VisualBasic.VisualBasicSyntaxTree).
  • Класс Microsoft.CodeAnalysis.SyntaxNode, экземпляры которого представляют такие синтаксические конструкции, как объявления, инструкции, предложения и выражения.
  • Структура Microsoft.CodeAnalysis.SyntaxToken, которая представляет отдельные ключевые слова, идентификаторы, операторы или знаки препинания.
  • И, наконец, структура Microsoft.CodeAnalysis.SyntaxTrivia, которая представляет малозначимые с точки зрения синтаксиса элементы сведений, такие как пробелы между токенами, директивы предварительной обработки и комментарии.

Заметки, токены и узлы иерархически компонуются для формирования дерева, которое полностью представляет все содержимое в фрагменте кода Visual Basic или C#. Эту структуру можно посмотреть с помощью окна визуализатора синтаксиса. В Visual Studio выберите Вид>Другие окна>Визуализатор синтаксиса. Например, предыдущий исходный файл на языке C# при просмотре с помощью визуализатора синтаксиса выглядит, как показано на следующем рисунке.

SyntaxNode: Blue | SyntaxToken: зеленый | SyntaxTrivia: красный файл кода C#

При перемещении по структуре дерева вы можете найти в файле кода все инструкции, выражения, токены и пробелы.

С помощью синтаксического API можно найти все, что угодно, в файле кода, но большинство сценариев предусматривает исследование небольших фрагментов кода либо поиск определенных инструкций или фрагментов. Ниже приводятся два примера, иллюстрирующие типичные сценарии использования для просмотра структуры кода или поиска отдельных инструкций.

Обход деревьев

Узлы дерева синтаксиса можно исследовать двумя способами. Можно выполнить обход дерева для исследования каждого узла или выполнить запрос к определенным элементам или узлам.

Обход вручную

Окончательный код этого примера доступен в репозитории на сайте GitHub.

Примечание

Типы синтаксического дерева используют наследование для описания различных элементов синтаксиса, которые являются допустимыми в разных местах в программе. Применение этих API часто означает приведение свойств или элементов коллекции к конкретным производным типам. В следующих примерах назначения и приведения являются отдельными инструкциями, использующими явно типизированные переменные. Прочитайте код, чтобы увидеть типы возвращаемых значений API и тип среды выполнения возвращаемых объектов. На практике более распространено использование неявно типизированных переменных и имен API для описания типа рассматриваемых объектов.

Создайте новый проект C# для автономного средства анализа кода:

  • В Visual Studio последовательно выберите Файл>Создать>Проект, чтобы открыть диалоговое окно "Новый проект".
  • В разделе Visual C#>Расширяемость выберите Автономное средство анализа кода.
  • Присвойте проекту имя "SyntaxTreeManualTraversal" и нажмите кнопку ОК.

Вы проанализируете базовую программу "Hello World!", показанную ранее. Добавьте текст для программы "Hello World" в качестве константы в класс Program:

        const string programText =
@"using System;
using System.Collections;
using System.Linq;
using System.Text;

namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(""Hello, World!"");
        }
    }
}";

Затем добавьте в константу programText следующий код для создания дерева синтаксиса для текста кода. Добавьте следующую строку в метод Main:

SyntaxTree tree = CSharpSyntaxTree.ParseText(programText);
CompilationUnitSyntax root = tree.GetCompilationUnitRoot();

Эти две строки создают дерево и извлекают корневой узел этого дерева. Теперь можно исследовать узлы в дереве. Добавьте следующие строки в метод Main для отображения некоторых свойств корневого узла дерева:

WriteLine($"The tree is a {root.Kind()} node.");
WriteLine($"The tree has {root.Members.Count} elements in it.");
WriteLine($"The tree has {root.Usings.Count} using statements. They are:");
foreach (UsingDirectiveSyntax element in root.Usings)
    WriteLine($"\t{element.Name}");

Запустите приложение для просмотра сведений, которые код собрал о корневом узле этого дерева.

Как правило, для получения сведений о коде выполняется обход дерева. В этом примере анализируется известный вам код для изучения API. Добавьте следующий код для исследования первого элемента узла root:

MemberDeclarationSyntax firstMember = root.Members[0];
WriteLine($"The first member is a {firstMember.Kind()}.");
var helloWorldDeclaration = (NamespaceDeclarationSyntax)firstMember;

Этот элемент — Microsoft.CodeAnalysis.CSharp.Syntax.NamespaceDeclarationSyntax. Он представляет все, что входит в объявление namespace HelloWorld. Добавьте следующий код, чтобы посмотреть, какие узлы объявляются внутри пространства имен HelloWorld:

WriteLine($"There are {helloWorldDeclaration.Members.Count} members declared in this namespace.");
WriteLine($"The first member is a {helloWorldDeclaration.Members[0].Kind()}.");

Запустите программу, чтобы посмотреть, что вы узнали.

Теперь, когда известно, что объявление является Microsoft.CodeAnalysis.CSharp.Syntax.ClassDeclarationSyntax, объявите новую переменную этого типа для исследования объявления класса. Этот класс содержит только один элемент: метод Main. Добавьте следующий код, чтобы найти метод Main, и приведите его к Microsoft.CodeAnalysis.CSharp.Syntax.MethodDeclarationSyntax.

var programDeclaration = (ClassDeclarationSyntax)helloWorldDeclaration.Members[0];
WriteLine($"There are {programDeclaration.Members.Count} members declared in the {programDeclaration.Identifier} class.");
WriteLine($"The first member is a {programDeclaration.Members[0].Kind()}.");
var mainDeclaration = (MethodDeclarationSyntax)programDeclaration.Members[0];

Узел объявления метода содержит полные синтаксические сведения о методе. Давайте посмотрим тип возвращаемого значения для метода Main, количество и типы аргументов, а также основной текст метода. Добавьте следующий код:

WriteLine($"The return type of the {mainDeclaration.Identifier} method is {mainDeclaration.ReturnType}.");
WriteLine($"The method has {mainDeclaration.ParameterList.Parameters.Count} parameters.");
foreach (ParameterSyntax item in mainDeclaration.ParameterList.Parameters)
    WriteLine($"The type of the {item.Identifier} parameter is {item.Type}.");
WriteLine($"The body text of the {mainDeclaration.Identifier} method follows:");
WriteLine(mainDeclaration.Body?.ToFullString());

var argsParameter = mainDeclaration.ParameterList.Parameters[0];

Запустите программу, чтобы просмотреть все полученные сведения об этой программе:

The tree is a CompilationUnit node.
The tree has 1 elements in it.
The tree has 4 using statements. They are:
        System
        System.Collections
        System.Linq
        System.Text
The first member is a NamespaceDeclaration.
There are 1 members declared in this namespace.
The first member is a ClassDeclaration.
There are 1 members declared in the Program class.
The first member is a MethodDeclaration.
The return type of the Main method is void.
The method has 1 parameters.
The type of the args parameter is string[].
The body text of the Main method follows:
        {
            Console.WriteLine("Hello, World!");
        }

Методы запросов

Помимо обхода деревьев можно также исследовать деревья синтаксиса с помощью методов запросов, определенных в Microsoft.CodeAnalysis.SyntaxNode. Эти методы известны всем, кто знаком с XPath. Эти методы можно использовать совместно с LINQ для быстрого поиска в дереве. SyntaxNode содержит такие методы запросов, как DescendantNodes, AncestorsAndSelf и ChildNodes.

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

var firstParameters = from methodDeclaration in root.DescendantNodes()
                                        .OfType<MethodDeclarationSyntax>()
                      where methodDeclaration.Identifier.ValueText == "Main"
                      select methodDeclaration.ParameterList.Parameters.First();

var argsParameter2 = firstParameters.Single();

WriteLine(argsParameter == argsParameter2);

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

Запустите программу, и выражение LINQ обнаружит тот же параметр, что и при навигации по дереву вручную.

В этом образце используются инструкции WriteLine для отображения сведений о деревьях синтаксиса по мере их обхода. Гораздо больше сведений можно получить, запустив готовую программу в отладчике. Можно просмотреть другие свойства и методы в составе дерева синтаксиса, созданного для программы "hello world".

Средства обхода синтаксиса

Часто бывает необходимо найти все узлы определенного типа в дереве синтаксиса, например каждое объявление свойств в файле. Путем расширения класса Microsoft.CodeAnalysis.CSharp.CSharpSyntaxWalker и переопределения метода VisitPropertyDeclaration(PropertyDeclarationSyntax) можно обработать каждое объявление свойств в дереве синтаксиса, не зная заранее его структуры. CSharpSyntaxWalker — это особый тип CSharpSyntaxVisitor, который рекурсивно обходит узел и все его дочерние элементы.

Этот пример реализует CSharpSyntaxWalker для исследования дерева синтаксиса. Он собирает обнаруженные директивы using, которые не импортируют пространство имен System.

Создайте новый проект C# для автономного средства анализа кода. Присвойте ему имя "SyntaxWalker".

Окончательный код этого примера доступен в репозитории на сайте GitHub. Пример на GitHub содержит оба проекта, описанные в этом учебнике.

Как показано в предыдущем примере, можно определить строковую константу, которая будет содержать текст программы для анализа:

        const string programText =
@"using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace TopLevel
{
    using Microsoft;
    using System.ComponentModel;

    namespace Child1
    {
        using Microsoft.Win32;
        using System.Runtime.InteropServices;

        class Foo { }
    }

    namespace Child2
    {
        using System.CodeDom;
        using Microsoft.CSharp;

        class Bar { }
    }
}";

Этот исходный текст содержит директивы using, расположенные в четырех разных местах: на уровне файлов, в пространстве имен верхнего уровня и в двух вложенных пространствах имен. В этом примере представлен основной сценарий использования класса CSharpSyntaxWalker для выполнения запросов к коду. Было бы сложно посетить каждый узел в корневом дереве синтаксиса для поиска объявлений using. Вместо этого создается производный класс и переопределяется метод, который вызывается только в том случае, когда текущий узел в дереве является директивой using. Обходчик не работает с узлами других типов. Этот единичный метод проверяет каждую из инструкций using и создает коллекцию пространств имен, которые не входят в пространство имен System. Создается CSharpSyntaxWalker для проверки всех инструкций using, но только инструкций using.

Теперь, когда вы определили текст программы, необходимо создать SyntaxTree и получить корень этого дерева:

SyntaxTree tree = CSharpSyntaxTree.ParseText(programText);
CompilationUnitSyntax root = tree.GetCompilationUnitRoot();

Теперь создайте новый класс. В Visual Studio выберите Проект>Добавить новый элемент. В диалоге Добавление нового элемента введите имя файла UsingCollector.cs.

Вы реализуете функцию обходчика using в классе UsingCollector. Для начала сделайте класс UsingCollector производным от CSharpSyntaxWalker.

class UsingCollector : CSharpSyntaxWalker

Потребуется хранилище для хранения узлов пространства имен, которые вы будете собирать. Объявите свойство только для чтения в классе UsingCollector. Эта переменная будет использоваться для хранения найденных узлов UsingDirectiveSyntax:

public ICollection<UsingDirectiveSyntax> Usings { get; } = new List<UsingDirectiveSyntax>();

Базовый класс, CSharpSyntaxWalker, реализует логику для посещения каждого узла в дереве синтаксиса. Производный класс переопределяет методы, вызываемые для определенных интересующих вас узлов. В этом примере вас интересует любая директива using. Это означает, что требуется переопределить метод VisitUsingDirective(UsingDirectiveSyntax). Единственный аргумент этого метода — объект Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax. Это важное преимущество обходчиков: они вызывают переопределенные методы с использованием аргументов, уже приведенных к типу конкретного узла. Класс Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax имеет свойство Name, которое содержит имя импортируемого пространства имен. Это Microsoft.CodeAnalysis.CSharp.Syntax.NameSyntax. Добавьте следующий код в переопределение VisitUsingDirective(UsingDirectiveSyntax):

public override void VisitUsingDirective(UsingDirectiveSyntax node)
{
    WriteLine($"\tVisitUsingDirective called with {node.Name}.");
    if (node.Name.ToString() != "System" &&
        !node.Name.ToString().StartsWith("System."))
    {
        WriteLine($"\t\tSuccess. Adding {node.Name}.");
        this.Usings.Add(node);
    }
}

Как и в прошлом примере, были добавлены разнообразные инструкции WriteLine для облегчения понимания этого метода. Вы можете посмотреть, когда он вызывается и какие при этом передаются аргументы.

Наконец, необходимо добавить две строки кода для создания UsingCollector и направить его в корневой узел для сбора всех инструкций using. Затем добавьте цикл foreach для отображения всех инструкций using, найденных сборщиком:

var collector = new UsingCollector();
collector.Visit(root);
foreach (var directive in collector.Usings)
{
    WriteLine(directive.Name);
}

Скомпилируйте и запустите программу. Вы должны увидеть следующий результат.

        VisitUsingDirective called with System.
        VisitUsingDirective called with System.Collections.Generic.
        VisitUsingDirective called with System.Linq.
        VisitUsingDirective called with System.Text.
        VisitUsingDirective called with Microsoft.CodeAnalysis.
                Success. Adding Microsoft.CodeAnalysis.
        VisitUsingDirective called with Microsoft.CodeAnalysis.CSharp.
                Success. Adding Microsoft.CodeAnalysis.CSharp.
        VisitUsingDirective called with Microsoft.
                Success. Adding Microsoft.
        VisitUsingDirective called with System.ComponentModel.
        VisitUsingDirective called with Microsoft.Win32.
                Success. Adding Microsoft.Win32.
        VisitUsingDirective called with System.Runtime.InteropServices.
        VisitUsingDirective called with System.CodeDom.
        VisitUsingDirective called with Microsoft.CSharp.
                Success. Adding Microsoft.CSharp.
Microsoft.CodeAnalysis
Microsoft.CodeAnalysis.CSharp
Microsoft
Microsoft.Win32
Microsoft.CSharp
Press any key to continue . . .

Поздравляем! Вы использовали синтаксический API для поиска определенных типов инструкций и объявлений C# в исходном коде C#.