Бөлісу құралы:


Начало работы с анализом синтаксиса

В этом руководстве вы изучите 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!");
        }
    }
}

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

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

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

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

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

Существует два разных способа найти пакет SDK платформы компилятора .NET в установщике Visual Studio:

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

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

  1. Запуск установщика Visual Studio
  2. Выберите Изменить.
  3. Проверьте рабочую нагрузку разработки расширений Visual Studio .
  4. Откройте узел разработки расширений Visual Studio в дереве сводки.
  5. Убедитесь, что установлен флажок для пакета SDK для платформы компилятора .NET .
  6. Нажмите кнопку Изменить.

Кроме того, вы также хотите, чтобы редактор DGML отображал графы в визуализаторе:

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

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

  1. Запуск установщика Visual Studio
  2. Выберите Изменить.
  3. Выберите вкладку "Отдельные компоненты"
  4. Установите флажок для пакета SDK платформы компилятора .NET. Его можно найти в верхней части раздела "Компиляторы", "Средства сборки" и "Среды выполнения ".
  5. Нажмите кнопку Изменить.

Кроме того, вы также хотите, чтобы редактор DGML отображал графы в визуализаторе:

  1. Установите флажок для редактора DGML. Его можно найти в разделе "Инструменты кода ".

Общие сведения о деревах синтаксиса

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

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

Четыре основных блока деревьев синтаксиса:

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

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

SyntaxNode: Синий | 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 directives. 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 directives. 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.CSharpSyntaxWalkerVisitPropertyDeclaration(PropertyDeclarationSyntax) метод, вы обрабатываете каждое объявление свойств в дереве синтаксиса, не зная его структуру заранее. CSharpSyntaxWalker — это конкретный вид CSharpSyntaxVisitor , который рекурсивно посещает узел и каждый из его дочерних элементов.

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

Создайте новый проект на C# Standalone-средства анализа кода; присвойте ему имя "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#.