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


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

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

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

Вам нужно установить пакет 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. Нужный пакет будет представлен в разделе Средства для работы с кодом.

Общие сведения о компиляциях и символах

В процессе работы с пакетом SDK для компилятора .NET вы узнаете различия между синтаксическим API и семантическим API. Синтаксический API позволяет получить представление о структуре программы. Однако часто требуются более полные сведения о семантике или значении программы. Хотя синтаксический анализ свободного файла либо фрагмента кода Visual Basic или C# можно выполнить изолированно, задавать вопросы, к примеру о типе переменной, в отрыве от реальности не имеет смысла. Значение имени типа может зависеть от ссылок на сборки, операций импорта пространств имен или других файлов кода. Ответы на эти вопросы можно получить с помощью семантического API, в частности класса Microsoft.CodeAnalysis.Compilation.

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

Как Microsoft.CodeAnalysis.SyntaxTree, Compilation является абстрактным классом с производными, соответствующими конкретному языку. При создании экземпляра компиляции необходимо вызвать фабричный метод в классе Microsoft.CodeAnalysis.CSharp.CSharpCompilation (или Microsoft.CodeAnalysis.VisualBasic.VisualBasicCompilation).

Выполнение запросов к символам

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

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

Примечание

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

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

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

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

        const string programText =
@"using System;
using System.Collections.Generic;
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();

Далее постройте CSharpCompilation из уже созданного дерева. В примере "Hello World!" используются типы String и Console. Необходимо сослаться на сборку, объявляющую эти два типа при компиляции. Добавьте следующую строку в метод Main для создания компиляции дерева синтаксиса, включая ссылку на соответствующую сборку:

var compilation = CSharpCompilation.Create("HelloWorld")
    .AddReferences(MetadataReference.CreateFromFile(
        typeof(string).Assembly.Location))
    .AddSyntaxTrees(tree);

Метод CSharpCompilation.AddReferences добавит ссылки на компиляцию. Метод MetadataReference.CreateFromFile загрузит сборку как ссылку.

Выполнение запросов к семантической модели

К существующему объекту Compilation можно отправить запрос на класс SemanticModel для любого класса SyntaxTree, содержащегося в этом объекте Compilation. Семантическую модель можно представить как источник всей информации, которая обычно доступна из IntelliSense. Может SemanticModel отвечать на такие вопросы, как "Какие имена находятся в область в этом расположении?", "Какие элементы доступны из этого метода?", "Какие переменные используются в этом блоке текста?" и "На что ссылается это имя или выражение?" Добавьте следующую инструкцию для создания семантической модели:

SemanticModel model = compilation.GetSemanticModel(tree);

Привязка имени

создает Compilation из SemanticModelSyntaxTree. После создания модели можно выполнить запрос на поиск первой директивы using и получение сведений о символах для пространства имен System. Добавьте следующие две строки в метод Main для создания семантической модели и получения символа для первого оператора using:

// Use the syntax tree to find "using System;"
UsingDirectiveSyntax usingSystem = root.Usings[0];
NameSyntax systemName = usingSystem.Name;

// Use the semantic model for symbol information:
SymbolInfo nameInfo = model.GetSymbolInfo(systemName);

В предыдущем коде показано, как привязать имя в первой директиве using, чтобы получить Microsoft.CodeAnalysis.SymbolInfo для пространства имен System. В предыдущем коде также демонстрируется, что для поиска структуры кода используется синтаксическая модель, а для понимания ее значения — семантическая модель. Синтаксическая модель находит строку System в операторе using. Семантическая модель располагает всеми сведениями о типах, определенных в пространстве имен System.

Из объекта SymbolInfo можно получить интерфейс Microsoft.CodeAnalysis.ISymbol с помощью свойства SymbolInfo.Symbol. Это свойство возвращает символ, на который ссылается это выражение. Для выражений, не ссылающихся ни на какие объекты (например, числовые литералы), это свойство имеет значение null. Если SymbolInfo.Symbol не задано значение NULL, ISymbol.Kind обозначает тип символа. В этом примере свойству ISymbol.Kind присваивается значение SymbolKind.Namespace. Добавьте приведенный ниже код в метод Main. Он возвращает символ для пространства имен System, а затем отображает все дочерние пространства имен, объявленные в пространстве имен System:

var systemSymbol = (INamespaceSymbol?)nameInfo.Symbol;
if (systemSymbol?.GetNamespaceMembers() is not null)
{
    foreach (INamespaceSymbol ns in systemSymbol?.GetNamespaceMembers()!)
    {
        Console.WriteLine(ns);
    }
}

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

System.Collections
System.Configuration
System.Deployment
System.Diagnostics
System.Globalization
System.IO
System.Numerics
System.Reflection
System.Resources
System.Runtime
System.Security
System.StubHelpers
System.Text
System.Threading
Press any key to continue . . .

Примечание

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

Привязка выражения

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

Программа "Hello World" содержит строку "Hello, World!", отображаемую Microsoft.CodeAnalysis.CSharp.Syntax.LiteralExpressionSyntaxв консоли.

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

// Use the syntax model to find the literal string:
LiteralExpressionSyntax helloWorldString = root.DescendantNodes()
.OfType<LiteralExpressionSyntax>()
.Single();

// Use the semantic model for type information:
TypeInfo literalInfo = model.GetTypeInfo(helloWorldString);

Структура Microsoft.CodeAnalysis.TypeInfo содержит свойство TypeInfo.Type, которое обеспечивает доступ к семантическим сведениям о типе литерала. В этом примере типом является string. Добавьте объявление, которое присваивает это свойство локальной переменной:

var stringTypeSymbol = (INamedTypeSymbol?)literalInfo.Type;

Чтобы завершить действия в этом учебнике, сформируем запрос LINQ, который создает последовательность всех открытых методов, объявленных в типе string и возвращающих string. Поскольку запрос сложный, мы будем создавать его построчно, а затем перестроим в один запрос. Источником для этого запроса является последовательность всех элементов, объявленных в типе string:

var allMembers = stringTypeSymbol?.GetMembers();

Эта исходная последовательность содержит все элементы, включая свойства и поля, поэтому ее нужно отфильтровать с помощью метода ImmutableArray<T>.OfType, чтобы найти элементы, которые являются объектами Microsoft.CodeAnalysis.IMethodSymbol:

var methods = allMembers?.OfType<IMethodSymbol>();

Затем добавьте другой фильтр, чтобы вернуть только открытые методы, возвращающие string:

var publicStringReturningMethods = methods?
    .Where(m => SymbolEqualityComparer.Default.Equals(m.ReturnType, stringTypeSymbol) &&
    m.DeclaredAccessibility == Accessibility.Public);

Выберите только свойство имени и только уникальные имена, удаляя все перегрузки:

var distinctMethods = publicStringReturningMethods?.Select(m => m.Name).Distinct();

Вы также можете создать полный запрос с помощью синтаксиса запроса LINQ, а затем отобразить все имена методов в консоли:

foreach (string name in (from method in stringTypeSymbol?
                         .GetMembers().OfType<IMethodSymbol>()
                         where SymbolEqualityComparer.Default.Equals(method.ReturnType, stringTypeSymbol) &&
                         method.DeclaredAccessibility == Accessibility.Public
                         select method.Name).Distinct())
{
    Console.WriteLine(name);
}

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

Join
Substring
Trim
TrimStart
TrimEnd
Normalize
PadLeft
PadRight
ToLower
ToLowerInvariant
ToUpper
ToUpperInvariant
ToString
Insert
Replace
Remove
Format
Copy
Concat
Intern
IsInterned
Press any key to continue . . .

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