Учебник. Создание средства для анализа и исправления кода
Пакет SDK для .NET Compiler Platform предоставляет средства, необходимые для создания пользовательской диагностики (анализаторов), исправления и рефакторинга кода, а также подавления диагностики для кода на C# или Visual Basic. Анализатор содержит код, который распознает нарушения правила. Исправление кода содержит код, который исправляет эти нарушения. Правилами, которые вы реализуете, может быть что угодно — от структуры кода до его стиля или соглашений об именовании и многое другое. .NET Compiler Platform предоставляет платформу для выполнения анализа во время написания кода, а также все функции пользовательского интерфейса Visual Studio для отладки, включая отображение волнистых линий в редакторе, вывод списка ошибок в Visual Studio и отображение значка лампочки, указывающего на наличие предложений и предлагаемых исправлений.
В этом руководстве описано, как создать анализатор и соответствующее исправление кода с помощью API Roslyn. Анализатор выполняет анализ исходного кода и сообщает о проблеме пользователю. При необходимости исправление кода можно связать с анализатором, чтобы представить изменения в исходном коде пользователя. В этом руководстве описано, как создать анализатор, ищущий локальные переменные, которые можно объявить с помощью модификатора const
, но которые не объявлены. Сопутствующее исправление кода добавляет модификатор const
в эти объявления.
Предварительные требования
- Visual Studio 2019 версии 16.8 или более поздней.
Необходимо установить пакет SDK для .NET Compiler Platform через Visual Studio Installer:
Инструкции по установке — Visual Studio Installer
Найти SDK-пакет .NET Compiler Platform в Visual Studio Installer можно двумя способами:
Установка с помощью Visual Studio Installer — представление "Рабочие нагрузки"
SDK-пакет .NET Compiler Platform не выбирается автоматически в рамках рабочей нагрузки разработки расширений Visual Studio. Его необходимо выбрать как дополнительный компонент.
- Запустите Visual Studio Installer.
- Выберите Изменить.
- Отметьте рабочую нагрузку Разработка расширений Visual Studio.
- Откройте узел Разработка расширений Visual Studio в дереве сводки.
- Установите флажок SDK-пакет .NET Compiler Platform. Нужный пакет будет представлен последним в списке дополнительных компонентов.
Кроме того, вы можете настроить редактор DGML для отображения диаграмм в средстве визуализации:
- Откройте узел Отдельные компоненты в дереве сводки.
- Установите флажок Редактор DGML
Установка с помощью Visual Studio Installer — вкладка "Отдельные компоненты"
- Запустите Visual Studio Installer.
- Выберите Изменить.
- Откройте вкладку Отдельные компоненты.
- Установите флажок SDK-пакет .NET Compiler Platform. Нужный пакет будет представлен в разделе Компиляторы, средства сборки и среды выполнения в самом начале.
Кроме того, вы можете настроить редактор DGML для отображения диаграмм в средстве визуализации:
- Установите флажок Редактор DGML. Нужный пакет будет представлен в разделе Средства для работы с кодом.
Создать и проверить анализатор можно несколькими способами:
- Создайте решение.
- Зарегистрируйте имя и описание анализатора.
- Создайте отчет анализатора о предупреждениях и рекомендациях.
- Внедрите исправление кода, чтобы принимать рекомендации.
- Улучшите анализ с помощью модульных тестов.
Создание решения
- В Visual Studio выберите Файл > Новый > проект... , чтобы открыть диалоговое окно Новый проект.
- В разделе Расширяемость Visual C# >выберите Анализатор с исправлением кода (.NET Standard).
- Присвойте проекту имя MakeConst и нажмите кнопку "ОК".
Примечание
Может возникнуть ошибка компиляции (MSB4062: не удалось загрузить задачу CompareBuildTaskVersion). Чтобы устранить эту проблему, обновите пакеты NuGet в решении с помощью диспетчера пакетов NuGet или используйте Update-Package
в окне консоли диспетчера пакетов.
Изучение шаблона анализатора
Анализатор с шаблоном исправления кода создает пять проектов:
- MakeConst — включает анализатор;
- MakeConst.CodeFixes — включает исправление кода;
- MakeConst.Package — используется для создания пакета NuGet для анализатора и исправления кода;
- MakeConst.Test — проект модульного теста;
- MakeConst.Vsix — проект запуска по умолчанию, который запускает второй экземпляр Visual Studio с загруженным новым анализатором. Нажмите клавишу F5, чтобы запустить проект VSIX.
Примечание
Анализаторам следует использовать .NET Standard 2.0, так как они могут запускаться в средах .NET Core (сборки командной строки) и .NET Framework (Visual Studio).
Совет
Когда вы запустите анализатор, откроется вторая копия Visual Studio. Эта вторая копия использует другой куст реестра для хранения параметров, что позволяет различить параметры визуальных элементов в обоих копиях Visual Studio. Вы можете выбрать другую тему для экспериментального запуска Visual Studio. Кроме того, не следует перемещать параметры или выполнять вход в учетную запись Visual Studio в экспериментальном экземпляре Visual Studio. Так параметры останутся разными.
В состав куста входит не только разрабатываемый анализатор, но и все открытые ранее. Чтобы сбросить куст Roslyn, необходимо вручную удалить его из %LocalAppData%\Microsoft\VisualStudio. Имя папки куста Roslyn будет заканчиваться на Roslyn
, например 16.0_9ae182f9Roslyn
. Обратите внимание, что после удаления куста может потребоваться очистить решение и перестроить его.
Во втором экземпляре Visual Studio, который вы только что запустили, создайте проект C# консольного приложения (целевая платформа может быть любой, так как анализаторы работают на уровне исходного кода). Наведите указатель мыши на токен с волнистым подчеркиванием. Появится текст предупреждения от анализатора.
Шаблон создает анализатор, который выдает предупреждение на каждое объявление типа, где имя типа состоит из букв нижнего регистра, как показано ниже:
Также шаблон содержит исправление кода, которое меняет любые буквы нижнего регистра в имени типа на буквы верхнего регистра. Предлагаемые исправления можно просмотреть, щелкнув значок лампочки рядом с предупреждением. После принятия изменений имя типа и все ссылки на этот тип будут обновлены. Теперь, когда вы увидели работу начального анализатора, закройте второй экземпляр Visual Studio и вернитесь к проекту анализатора.
Для тестирования изменений в анализаторе не требуется каждый раз запускать вторую копию Visual Studio, так как шаблон создает проект модульного теста. В этом проекте содержатся два теста. TestMethod1
показывает обычный формат теста, при котором анализ кода происходит без активации диагностики. TestMethod2
— формат, при котором сначала активируется диагностика, а затем применяется исправление кода. Во время сборки анализатора и исправления кода вы напишете тесты для проверки разных структур. Модульные тесты проводятся гораздо быстрее, чем интерактивное тестирование анализаторов в Visual Studio.
Совет
Модульные тесты анализатора — отличный инструмент, если вы знаете, какие конструкции кода должны и не должны запускать анализатор. В свою очередь запуск анализатора в другой копии Visual Studio позволяет определить и найти конструкции, о которых вы еще не задумывались.
В этом руководстве показано, как написать анализатор, информирующий пользователя о любых объявлениях локальной переменной, которые можно преобразовать в локальные константы. Рассмотрим следующий пример кода:
int x = 0;
Console.WriteLine(x);
В приведенном выше коде x
присваивается значение константы, которое никогда не меняется. Ее можно объявить с помощью модификатора const
:
const int x = 0;
Console.WriteLine(x);
Чтобы определить, можно ли изменить переменную на константу, используется синтаксический анализ, анализ константы из выражения инициализатора, а также анализ потока данных, чтобы убедиться, что переменная не записана. .NET Compiler Platform предоставляет API для упрощения такого анализа.
Регистрация анализатора
В файле MakeConstAnalyzer.cs с помощью шаблона создается начальный класс DiagnosticAnalyzer
. В этом начальном анализаторе отображены два важных свойства каждого анализатора.
- В каждом диагностическом анализаторе должен быть указан атрибут
[DiagnosticAnalyzer]
, который описывает язык, на котором он работает. - Каждый диагностический анализатор должен быть производным (прямо или косвенно) от класса DiagnosticAnalyzer.
Также в шаблоне отображены базовые функции любого анализатора:
- Регистрация действий. Действия представляют изменения кода, которые запускают анализатор для проверки нарушений. Когда Visual Studio обнаруживает изменения в коде, которые соответствуют зарегистрированному действию, происходит вызов зарегистрированного метода.
- Создание диагностики. Обнаружив нарушения, анализатор создает объект диагностики, с помощью которого Visual Studio уведомляет пользователя об этом нарушении.
Действия регистрируются в переопределении метода DiagnosticAnalyzer.Initialize(AnalysisContext). При работе с этим руководстве вы просмотрите синтаксические узлы локальных объявлений и узнаете, какие из них имеют значения констант. Если объявление может быть константой, анализатор создаст диагностику и сформирует отчет.
Сначала обновите константы регистрации и метод Initialize
, так как константы определяют ваш анализатор MakeConst. Большинство строковых констант определены в файле строковых ресурсов. Используйте их, чтобы упростить локализацию. Откройте файл Resources.resx для проекта анализатора MakeConst. Откроется редактор ресурсов. Измените строковые ресурсы, как показано ниже:
- Измените
AnalyzerDescription
на Variables that are not modified should be made constants.. - Измените
AnalyzerMessageFormat
на Variable '{0}' can be made constant. - Измените
AnalyzerTitle
на Variable can be made constant.
После настройки редактор ресурсов будет выглядеть как на рисунке ниже:
Остальные изменения будут в файле анализатора. Откройте файл MakeConstAnalyzer.cs в Visual Studio. Измените зарегистрированное действие на символы на действие на синтаксис. В методе MakeConstAnalyzerAnalyzer.Initialize
найдите строку с действием на символы:
context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);
Замените ее приведенным ниже кодом:
context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.LocalDeclarationStatement);
После этого метод AnalyzeSymbol
можно удалить. Этот анализатор проверяет SyntaxKind.LocalDeclarationStatement, а не операторы SymbolKind.NamedType. Обратите внимание на то, что AnalyzeNode
подчеркивается красной волнистой линией, так как код, который вы только что вставили, ссылается на метод AnalyzeNode
, который еще не объявлен. Объявите этот метод, используя приведенный ниже код:
private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
}
Измените Category
на Usage в файле MakeConstAnalyzer.cs, как показано в следующем коде:
private const string Category = "Usage";
Поиск локальных объявлений, которые могут быть константами
Теперь мы напишем первую версию метода AnalyzeNode
. Он должен найти одно локальное объявление, которое может быть const
, но таковым не является. Пример приведен ниже:
int x = 0;
Console.WriteLine(x);
Сначала найдите локальные объявления. Добавьте приведенный ниже код в AnalyzeNode
в файле MakeConstAnalyzer.cs:
var localDeclaration = (LocalDeclarationStatementSyntax)context.Node;
Это приведение всегда завершается успешно, так как ваш анализатор зарегистрирован для отслеживания изменений только локальных объявлений. Другие типы узлов не вызывают метод AnalyzeNode
. Затем проверьте объявление на наличие модификаторов const
. Если они есть, сразу же выполните возврат. Приведенный ниже код ищет модификаторы const
в локальном объявлении:
// make sure the declaration isn't already const:
if (localDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword))
{
return;
}
В конце проверьте, может ли переменная быть const
. Это означает, что она не может быть назначена после инициализации.
Выполните семантический анализ с помощью SyntaxNodeAnalysisContext. Используйте аргумент context
, чтобы определить, можно ли объявление локальной переменной сделать const
. Microsoft.CodeAnalysis.SemanticModel представляет все семантические сведения в одном исходном файле. См. дополнительные сведения о семантических моделях. С помощью Microsoft.CodeAnalysis.SemanticModel выполните анализ потока данных на операторе локального объявления. Затем, используя результаты этого анализа потока данных, убедитесь, что в локальную переменную не записывается новое значение где-либо еще. Вызовите метод расширения GetDeclaredSymbol, чтобы извлечь ILocalSymbol переменной и убедиться, что она не содержится в коллекции DataFlowAnalysis.WrittenOutside из анализа потока данных. Добавьте в конце метода AnalyzeNode
приведенный ниже код:
// Perform data flow analysis on the local declaration.
DataFlowAnalysis dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);
// Retrieve the local symbol for each variable in the local declaration
// and ensure that it is not written outside of the data flow analysis region.
VariableDeclaratorSyntax variable = localDeclaration.Declaration.Variables.Single();
ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
{
return;
}
Этот код гарантирует, что переменная не изменена и может быть const
. Теперь мы активируем диагностику. Добавьте приведенный ниже код в последнюю строку метода AnalyzeNode
:
context.ReportDiagnostic(Diagnostic.Create(Rule, context.Node.GetLocation(), localDeclaration.Declaration.Variables.First().Identifier.ValueText));
Чтобы проверить состояние, запустите анализатор, нажав клавишу F5. Загрузите консольное приложение, созданное ранее, и добавьте приведенный ниже код теста:
int x = 0;
Console.WriteLine(x);
Появится лампочка и анализатор выдаст отчет с диагностическими сведениями. Однако в зависимости от версии Visual Studio вы увидите следующее:
- Лампочка, которая по-прежнему использует исправление кода, созданного шаблоном, сообщит, что ее можно сделать прописной.
- Сообщение-баннер в верхней части редактора с сообщением "MakeConstCodeFixProvider" обнаружил ошибку и был отключен. Это связано с тем, что поставщик исправлений кода еще не был изменен и по-прежнему ожидает найти
TypeDeclarationSyntax
элементы вместоLocalDeclarationStatementSyntax
элементов.
В следующем разделе показано, как написать исправление кода.
Написание исправления кода
Анализатор поддерживает одно или несколько исправлений кода. Исправление кода определяет, какие правки нужно внести для решения обнаруженной проблемы. Исправление кода вашего анализатора предоставляет код с ключевым словом const:
- int x = 0;
+ const int x = 0;
Console.WriteLine(x);
Пользователь выбирает исправление в интерфейсе лампочки в редакторе, а Visual Studio изменяет код.
Откройте файл CodeFixResources.resx и измените CodeFixTitle
на Make constant.
Откройте файл MakeConstCodeFixProvider.cs, добавленный шаблоном. Это исправление уже привязано к идентификатору диагностики вашего анализатора, но оно пока не реализует преобразование кода.
Затем удалите метод MakeUppercaseAsync
. Он больше не применяется.
Все поставщики исправлений кода являются производными от CodeFixProvider. и переопределяют CodeFixProvider.RegisterCodeFixesAsync(CodeFixContext) на сообщение о доступных исправлениях. В LocalDeclarationStatementSyntax измените тип узла-предка на RegisterCodeFixesAsync
в соответствии с диагностикой:
var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<LocalDeclarationStatementSyntax>().First();
Затем, чтобы зарегистрировать исправление кода, измените последнюю строку. Исправление создаст документ, полученный после добавления модификатора const
к существующему объявлению:
// Register a code action that will invoke the fix.
context.RegisterCodeFix(
CodeAction.Create(
title: CodeFixResources.CodeFixTitle,
createChangedDocument: c => MakeConstAsync(context.Document, declaration, c),
equivalenceKey: nameof(CodeFixResources.CodeFixTitle)),
diagnostic);
Под символом MakeConstAsync
появятся красные волнистые линии. Добавьте объявление в MakeConstAsync
, например как указано ниже:
private static async Task<Document> MakeConstAsync(Document document,
LocalDeclarationStatementSyntax localDeclaration,
CancellationToken cancellationToken)
{
}
Новый метод MakeConstAsync
преобразует Document исходного файла пользователя в новый экземпляр Document, содержащий объявление const
.
Создайте новый токен ключевого слова const
и вставьте его перед оператором объявления. Не забудьте сначала удалить все элементы trivia из первого оператора объявления и подключить к токену const
. Добавьте следующий код в метод MakeConstAsync
:
// Remove the leading trivia from the local declaration.
SyntaxToken firstToken = localDeclaration.GetFirstToken();
SyntaxTriviaList leadingTrivia = firstToken.LeadingTrivia;
LocalDeclarationStatementSyntax trimmedLocal = localDeclaration.ReplaceToken(
firstToken, firstToken.WithLeadingTrivia(SyntaxTriviaList.Empty));
// Create a const token with the leading trivia.
SyntaxToken constToken = SyntaxFactory.Token(leadingTrivia, SyntaxKind.ConstKeyword, SyntaxFactory.TriviaList(SyntaxFactory.ElasticMarker));
Затем добавьте токен const
к объявлению с помощью приведенного ниже кода:
// Insert the const token into the modifiers list, creating a new modifiers list.
SyntaxTokenList newModifiers = trimmedLocal.Modifiers.Insert(0, constToken);
// Produce the new local declaration.
LocalDeclarationStatementSyntax newLocal = trimmedLocal
.WithModifiers(newModifiers)
.WithDeclaration(localDeclaration.Declaration);
Отформатируйте новое объявление в соответствии с правилами форматирования C#. Форматирование изменений в соответствии с существующим кодом упрощает работу. Добавьте приведенный ниже оператор сразу после существующего кода:
// Add an annotation to format the new local declaration.
LocalDeclarationStatementSyntax formattedLocal = newLocal.WithAdditionalAnnotations(Formatter.Annotation);
Для выполнения этого кода требуется новое пространство имен. Добавьте следующую директиву using
в начало файла.
using Microsoft.CodeAnalysis.Formatting;
В конце нужно внести правки. Для этого выполните эти три шага:
- Получите дескриптор существующего документа.
- Создайте документ, заменив существующее объявление на новое.
- Верните новый документ.
Добавьте в конце метода MakeConstAsync
приведенный ниже код:
// Replace the old local declaration with the new local declaration.
SyntaxNode oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
SyntaxNode newRoot = oldRoot.ReplaceNode(localDeclaration, formattedLocal);
// Return document with transformed tree.
return document.WithSyntaxRoot(newRoot);
Ваше исправление кода готово. Нажмите клавишу F5, чтобы запустить проект анализатора во втором экземпляре Visual Studio. Во втором экземпляре Visual Studio создайте проект C# консольного приложения и добавьте несколько объявлений локальной переменной, инициализированных со значениями константы в методе main. Они будут отмечены как предупреждения. См. пример ниже.
Мы уже много сделали. Объявления, которые могут быть const
, подчеркнуты волнистой линией. Но нам еще нужно с ними поработать. Здесь следует добавить const
в объявления i
, затем j
и k
. Но если вы добавите модификатор const
в обратном порядке, начиная с k
, анализатор выдаст ошибки: k
не может быть объявлен как const
, пока i
и j
не являются const
. Нужно выполнить дополнительный анализ, чтобы убедиться, что переменные можно объявить и инициализировать различными путями.
Создание модульных тестов
Анализатор и исправление кода работают на простом примере одного объявления, которое может быть константой. Есть множество возможных операторов объявления, где они выдают ошибки. В этих ситуациях можно обратиться к библиотеке модульных тестов, созданной шаблоном. Это гораздо быстрее, чем каждый раз запускать вторую копию Visual Studio.
Откройте файл MakeConstUnitTests.cs в проекте модульного теста. В нем созданы два теста по общим шаблонам для анализатора и для модульного теста исправления кода. Метод TestMethod1
гарантирует, что анализатор не выдаст отчет о диагностике, когда это не нужно. Метод TestMethod2
выдает отчет о диагностике и запускает исправление кода.
Этот шаблон использует пакеты Microsoft.CodeAnalysis.Testing для работы с модульными тестами.
Совет
Библиотека тестирования поддерживает специальный синтаксис разметки, в том числе следующие элементы:
[|text|]
— указывает, что дляtext
предоставляются данные диагностики. По умолчанию эта форма может использоваться только для анализаторов тестирования с одним экземпляромDiagnosticDescriptor
, предоставляемымDiagnosticAnalyzer.SupportedDiagnostics
.{|ExpectedDiagnosticId:text|}
: указывает, что диагностические данные с IdExpectedDiagnosticId
параметром передаются дляtext
.
Замените тесты шаблона в классе MakeConstUnitTest
следующим методом теста.
[TestMethod]
public async Task LocalIntCouldBeConstant_Diagnostic()
{
await VerifyCS.VerifyCodeFixAsync(@"
using System;
class Program
{
static void Main()
{
[|int i = 0;|]
Console.WriteLine(i);
}
}
", @"
using System;
class Program
{
static void Main()
{
const int i = 0;
Console.WriteLine(i);
}
}
");
}
Запустите этот тест, чтобы убедиться, что он проходит. Откройте обозреватель тестов в Visual Studio, последовательно выбрав Тест>Windows>Обозреватель тестов. Затем выберите Выполнить все.
Создание тестов для допустимых объявлений
Как правило, анализаторы должны завершать работу как можно быстрее, выполняя минимум операций. Когда пользователь правит код, Visual Studio вызывает зарегистрированные анализаторы. Основным требованием здесь является скорость реагирования. Существует несколько тестовых случаев для кода, который не должен вызывать диагностику. Анализатор уже обрабатывает один из них — тот, при котором переменная присваивается после инициализации. Добавьте следующий метод теста, чтобы представить этот случай:
[TestMethod]
public async Task VariableIsAssigned_NoDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using System;
class Program
{
static void Main()
{
int i = 0;
Console.WriteLine(i++);
}
}
");
}
Этот тест также проходит. Далее добавьте методы теста для условий, которые еще не обработаны:
Объявления, которые представляют
const
, так как они уже являются константами:[TestMethod] public async Task VariableIsAlreadyConst_NoDiagnostic() { await VerifyCS.VerifyAnalyzerAsync(@" using System; class Program { static void Main() { const int i = 0; Console.WriteLine(i); } } "); }
Объявления, у которых нет инициализатора, так как нет соответствующего значения:
[TestMethod] public async Task NoInitializer_NoDiagnostic() { await VerifyCS.VerifyAnalyzerAsync(@" using System; class Program { static void Main() { int i; i = 0; Console.WriteLine(i); } } "); }
Объявления, у которых инициализатор не является константой, так как они не могут быть константами времени компиляции:
[TestMethod] public async Task InitializerIsNotConstant_NoDiagnostic() { await VerifyCS.VerifyAnalyzerAsync(@" using System; class Program { static void Main() { int i = DateTime.Now.DayOfYear; Console.WriteLine(i); } } "); }
Трудность заключается еще в том, что в C# допускается несколько объявлений, работающих как один оператор. Рассмотрите приведенную ниже строковую константу тестового случая:
[TestMethod]
public async Task MultipleInitializers_NoDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using System;
class Program
{
static void Main()
{
int i = 0, j = DateTime.Now.DayOfYear;
Console.WriteLine(i);
Console.WriteLine(j);
}
}
");
}
Переменная i
может быть константой, а переменная j
— нет. Поэтому этот оператор не может быть объявлением константы.
Снова запустите тесты. Произойдет сбой в новых тестовых случаях.
Обновление анализатора для пропуска верных объявлений
Чтобы отфильтровать код, который соответствует условиям, необходимо преобразовать метод AnalyzeNode
анализатора. Эти условия связаны между собой, и изменения в одном из них будут применены ко всем. Внесите следующие изменения в AnalyzeNode
:
- Для объявления одной переменной был выполнен семантический анализ. Этот код должен находиться в цикле
foreach
, который проверяет все переменные, объявленные в одном и том же операторе. - Каждая объявленная переменная должна иметь инициализатор.
- Каждый инициализатор переменной должен быть константой времени компиляции.
В методе AnalyzeNode
замените исходный семантический анализ:
// Perform data flow analysis on the local declaration.
DataFlowAnalysis dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);
// Retrieve the local symbol for each variable in the local declaration
// and ensure that it is not written outside of the data flow analysis region.
VariableDeclaratorSyntax variable = localDeclaration.Declaration.Variables.Single();
ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
{
return;
}
приведенным ниже фрагментом кода:
// Ensure that all variables in the local declaration have initializers that
// are assigned with constant values.
foreach (VariableDeclaratorSyntax variable in localDeclaration.Declaration.Variables)
{
EqualsValueClauseSyntax initializer = variable.Initializer;
if (initializer == null)
{
return;
}
Optional<object> constantValue = context.SemanticModel.GetConstantValue(initializer.Value, context.CancellationToken);
if (!constantValue.HasValue)
{
return;
}
}
// Perform data flow analysis on the local declaration.
DataFlowAnalysis dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);
foreach (VariableDeclaratorSyntax variable in localDeclaration.Declaration.Variables)
{
// Retrieve the local symbol for each variable in the local declaration
// and ensure that it is not written outside of the data flow analysis region.
ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
{
return;
}
}
Первый цикл foreach
проверяет каждое объявление переменной с помощью синтаксического анализа. В первой проверке подтверждается наличие инициализатора. Во второй — что этот инициализатор является константой. Во втором цикле запускается исходный семантический анализ. Семантические проверки проходят в отдельных циклах, так как они серьезно влияют на производительность. Снова запустите тест. Все проверки должны быть пройдены.
Финальные штрихи
Вы почти у цели. Анализатор должен обработать еще несколько условий. Пока пользователь пишет код, Visual Studio вызывает анализаторы. Часто бывает так, что анализатор вызван для кода, который не компилируется. Метод AnalyzeNode
диагностического анализатора не проверяет, можно ли преобразовать значение константы в тип переменной. Поэтому в текущей реализации все неправильные объявления, такие как int i = "abc"
, преобразуются в локальные константы. Добавьте метод теста для этого случая:
[TestMethod]
public async Task DeclarationIsInvalid_NoDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using System;
class Program
{
static void Main()
{
int x = {|CS0029:""abc""|};
}
}
");
}
Кроме того, ссылочные типы обрабатываются неправильно. Единственное допустимое значение константы для ссылочного типа — null
, кроме случаев с использованием System.String, когда допускаются строковые литералы. Другими словами, const string s = "abc"
является допустимым, а const object s = "abc"
— нет. Этот фрагмент кода проверяет это условие:
[TestMethod]
public async Task DeclarationIsNotString_NoDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using System;
class Program
{
static void Main()
{
object s = ""abc"";
}
}
");
}
Чтобы убедиться в том, что можно создать объявление константы для строки, добавьте еще один тест. В приведенным ниже фрагменте кода определен как код, вызывающий диагностику, так и код после исправления:
[TestMethod]
public async Task StringCouldBeConstant_Diagnostic()
{
await VerifyCS.VerifyCodeFixAsync(@"
using System;
class Program
{
static void Main()
{
[|string s = ""abc"";|]
}
}
", @"
using System;
class Program
{
static void Main()
{
const string s = ""abc"";
}
}
");
}
Если переменная объявлена с ключевым словом var
, исправление выдает объявление const var
, которое не поддерживается в C#. Чтобы исправить эту ошибку, исправление должно заменить ключевое слово var
именем выведенного типа:
[TestMethod]
public async Task VarIntDeclarationCouldBeConstant_Diagnostic()
{
await VerifyCS.VerifyCodeFixAsync(@"
using System;
class Program
{
static void Main()
{
[|var item = 4;|]
}
}
", @"
using System;
class Program
{
static void Main()
{
const int item = 4;
}
}
");
}
[TestMethod]
public async Task VarStringDeclarationCouldBeConstant_Diagnostic()
{
await VerifyCS.VerifyCodeFixAsync(@"
using System;
class Program
{
static void Main()
{
[|var item = ""abc"";|]
}
}
", @"
using System;
class Program
{
static void Main()
{
const string item = ""abc"";
}
}
");
}
К счастью, все вышеперечисленные ошибки можно устранить с помощью методов, которые вы только что изучили.
Чтобы исправить первую ошибку, сначала откройте файл MakeConstAnalyzer.cs и найдите цикл foreach, в котором проверяются инициализаторы локального объявления. Им должны быть назначены значения констант. Сразу же, до выполнения цикла foreach, вызовите context.SemanticModel.GetTypeInfo()
, чтобы извлечь подробные сведения об объявленном типе из локального объявления:
TypeSyntax variableTypeName = localDeclaration.Declaration.Type;
ITypeSymbol variableType = context.SemanticModel.GetTypeInfo(variableTypeName, context.CancellationToken).ConvertedType;
Затем проверьте каждый инициализатор внутри цикла foreach
, чтобы убедиться в том, что его можно преобразовать в тип переменной. Убедившись в том, что инициализатор является константой, добавьте приведенную ниже проверку:
// Ensure that the initializer value can be converted to the type of the
// local declaration without a user-defined conversion.
Conversion conversion = context.SemanticModel.ClassifyConversion(initializer.Value, variableType);
if (!conversion.Exists || conversion.IsUserDefined)
{
return;
}
Следующее изменение основано на последнем. Перед закрывающей фигурной скобкой первого цикла foreach добавьте приведенный ниже код. Он проверит тип локального объявления, когда константа является строкой или имеет значение null.
// Special cases:
// * If the constant value is a string, the type of the local declaration
// must be System.String.
// * If the constant value is null, the type of the local declaration must
// be a reference type.
if (constantValue.Value is string)
{
if (variableType.SpecialType != SpecialType.System_String)
{
return;
}
}
else if (variableType.IsReferenceType && constantValue.Value != null)
{
return;
}
Необходимо заменить ключевое слово var
правильным именем типа в вашем поставщике исправлений кода. Вернитесь к MakeConstCodeFixProvider.cs. Код, который вы добавите, выполняет следующие шаги:
- Проверяет, является ли объявление
var
, если да: - Создает тип для выводимого типа.
- Проверяет, что объявление типа не является псевдонимом. Если это так, можно объявить
const var
. - Проверяет, что
var
не является именем типа в программе (если это так,const var
является допустимым). - Упрощает полное имя типа.
Кажется, что здесь довольно много кода. Но это не так. Замените строку, которая объявляет и инициализирует newLocal
приведенным ниже кодом. Он выполняется сразу после инициализации newModifiers
:
// If the type of the declaration is 'var', create a new type name
// for the inferred type.
VariableDeclarationSyntax variableDeclaration = localDeclaration.Declaration;
TypeSyntax variableTypeName = variableDeclaration.Type;
if (variableTypeName.IsVar)
{
SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
// Special case: Ensure that 'var' isn't actually an alias to another type
// (e.g. using var = System.String).
IAliasSymbol aliasInfo = semanticModel.GetAliasInfo(variableTypeName, cancellationToken);
if (aliasInfo == null)
{
// Retrieve the type inferred for var.
ITypeSymbol type = semanticModel.GetTypeInfo(variableTypeName, cancellationToken).ConvertedType;
// Special case: Ensure that 'var' isn't actually a type named 'var'.
if (type.Name != "var")
{
// Create a new TypeSyntax for the inferred type. Be careful
// to keep any leading and trailing trivia from the var keyword.
TypeSyntax typeName = SyntaxFactory.ParseTypeName(type.ToDisplayString())
.WithLeadingTrivia(variableTypeName.GetLeadingTrivia())
.WithTrailingTrivia(variableTypeName.GetTrailingTrivia());
// Add an annotation to simplify the type name.
TypeSyntax simplifiedTypeName = typeName.WithAdditionalAnnotations(Simplifier.Annotation);
// Replace the type in the variable declaration.
variableDeclaration = variableDeclaration.WithType(simplifiedTypeName);
}
}
}
// Produce the new local declaration.
LocalDeclarationStatementSyntax newLocal = trimmedLocal.WithModifiers(newModifiers)
.WithDeclaration(variableDeclaration);
Чтобы использовать тип Simplifier, потребуется добавить одну директиву using
:
using Microsoft.CodeAnalysis.Simplification;
Запустите тесты. Они все должны быть пройдены. Можете поздравить себя, запустив готовый анализатор. Чтобы запустить проект анализатора во втором экземпляре Visual Studio с загруженным расширением Roslyn (предварительная версия), нажмите клавиши CTRL+F5.
- Во втором экземпляре Visual Studio создайте новый проект C# консольного приложения и добавьте
int x = "abc";
в метод main. Так как первая ошибка была исправлена, для объявления локальной переменной не должно выдаваться предупреждение (хотя есть ошибка компилятора, как и предполагалось). - Затем добавьте
object s = "abc";
в метод main. Так как вторая ошибка была исправлена, не должно быть никаких предупреждений. - Наконец, добавьте другую локальную переменную, использующую ключевое слово
var
. Внизу слева появится предупреждение и предложение исправлений. - Наведите курсор редактора на волнистую линию и нажмите клавиши CTRL+. Отобразятся предложенные исправления. Обратите внимание, что после выбора исправления ключевое слово
var
теперь обрабатывается правильно.
В конце добавьте приведенный ниже код:
int i = 2;
int j = 32;
int k = i + j;
После этого только две переменные будут подчеркнуты красными волнистыми линиями. Добавьте const
в i
и j
. Появится предупреждение, указывающее на то, что k
может быть const
.
Поздравляем! Вы создали свое первое расширение .NET Compiler Platform, которое выполняет анализ кода в режиме реального времени, находит проблемы и быстро их исправляет. Кроме того, вы изучили множество API, входящих в пакет SDK .NET Compiler Platform (API Roslyn). Вы можете сравнить результат своей работы с готовым примером в нашем репозитории в GitHub.