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


Руководство: Как написать ваш первый анализатор и исправление кода

Пакет SDK для платформы компилятора .NET предоставляет средства, необходимые для создания пользовательских диагностических средств (анализаторов), исправлений кода, рефакторинг кода и диагностических подавителей, предназначенных для кода C# или Visual Basic. Анализатор содержит код, который распознает нарушения правила. Исправление кода содержит код, который исправляет нарушение. Правила, которые вы реализуете, могут быть все, от структуры кода до стиля программирования до соглашений об именовании и многое другое. Платформа компилятора .NET предоставляет основу для выполнения анализа параллельно с написанием кода разработчиками и включает все функции пользовательского интерфейса Visual Studio для исправления кода: отображение волнообразных подчёркиваний в редакторе, заполнение списка ошибок Visual Studio, создание предложений с "лампочкой" и отображение подробного предварительного просмотра предлагаемых исправлений.

В этом руководстве вы изучите создание анализатора и соответствующее исправление кода с помощью API Roslyn. Анализатор — это способ выполнения анализа исходного кода и сообщения о проблеме пользователю. При необходимости исправление кода может быть связано с анализатором для представления изменения исходного кода пользователя. В этом руководстве создается анализатор, который находит объявления локальных переменных, которые можно было бы объявить с помощью модификатора const, но они этого не делают. Исправление сопутствующего кода изменяет эти объявления для добавления const модификатора.

Предпосылки

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

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

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

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

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

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

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

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

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

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

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

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

Существует несколько шагов по созданию и проверке анализатора.

  1. Создайте решение.
  2. Зарегистрируйте имя и описание анализатора.
  3. Предупреждения и рекомендации анализатора отчетов.
  4. Реализуйте исправление кода для принятия рекомендаций.
  5. Улучшение анализа с помощью модульных тестов.

Создание решения

  • В 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. Это сохраняет параметры различными.

Hive включает не только анализатор разработки, но и все анализаторы, открытые ранее. Чтобы сбросить куст Roslyn, необходимо вручную удалить его из %LocalAppData%\Microsoft\VisualStudio. Имя папки Roslyn hive будет заканчиваться 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 предоставляет API-интерфейсы, упрощающие выполнение этого анализа.

Создание регистраций анализаторов

Шаблон создает начальный DiagnosticAnalyzer класс в файле MakeConstAnalyzer.cs . В этом исходном анализаторе показаны два важных свойства каждого анализатора.

  • Каждый диагностический [DiagnosticAnalyzer] анализатор должен предоставить атрибут, описывающий язык, на котором он работает.
  • Каждый диагностический анализатор должен быть производным (напрямую или косвенно) от DiagnosticAnalyzer класса.

В шаблоне также показаны основные функции, которые являются частью любого анализатора:

  1. Запись действий. Действия представляют изменения кода, которые должны активировать анализатор для проверки кода на наличие нарушений. Когда Visual Studio обнаруживает изменения кода, соответствующие зарегистрированным действиям, он вызывает зарегистрированный метод анализатора.
  2. Создайте диагностику. При обнаружении нарушения анализатор создает диагностический объект, который Visual Studio использует для уведомления пользователя о нарушении.

Вы регистрируете действия в вашем переопределении метода DiagnosticAnalyzer.Initialize(AnalysisContext). В этом руководстве вы будете просматривать узлы синтаксиса, ища локальные объявления, и выясните, какие из них имеют константные значения. Если объявление может быть константой, ваш анализатор создаст и сообщит диагностическое сообщение.

Первым шагом является обновление констант регистрации и метода Initialize, чтобы эти константы обозначали анализатор "Make Const". Большинство строковых констант определяются в файле строковых ресурсов. Следует следовать этой практике для упрощения локализации. Откройте файл 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. A 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), чтобы сообщить о доступных исправлениях кода. В RegisterCodeFixesAsync измените тип узла-предка, который вы ищете, на LocalDeclarationStatementSyntax, чтобы он соответствовал диагностике.

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 маркер ключевого слова для вставки в начале инструкции объявления. Будьте осторожны, чтобы сначала удалить любые ведущие тривии из первого маркера инструкции объявления и прикрепить его к маркеру 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;

Последний шаг — сделать редактирование. Для этого процесса необходимо выполнить три шага.

  1. Получите дескриптор существующего документа.
  2. Создайте новый документ, заменив существующее объявление новым объявлением.
  3. Верните новый документ.

Добавьте следующий код в конец 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++);
    }
}
");
        }
        [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);
    }
}
");
        }

Эти тесты проходят, так как анализатор уже обрабатывает следующие условия:

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

Затем добавьте методы тестирования для условий, которые еще не обработаны:

  • Объявления, в которых инициализатор не является константой, так как они не могут быть константами во время компиляции:

            [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;

Запустите тесты, и все они должны пройти. Поздравьте себя, запустив готовый анализатор. Нажмите клавиши CTRL+F5 , чтобы запустить проект анализатора во втором экземпляре Visual Studio с загруженным расширением Roslyn Preview.

  • Во втором экземпляре 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, которое выполняет анализ кода на лету для обнаружения проблемы и быстро исправляет ее. На этом этапе вы узнали многие API кода, которые являются частью пакета SDK платформы компилятора .NET (API Roslyn). Вы можете сравнить свою работу с готовым примером в нашем репозитории на GitHub.

Другие ресурсы