Udostępnij za pomocą


Samouczek: tworzenie pierwszego analizatora i poprawki kodu

Zestaw .NET Compiler Platform SDK udostępnia narzędzia potrzebne do tworzenia niestandardowych diagnostyki (analizatorów), poprawek kodu, refaktoryzacji kodu i tłumików diagnostycznych przeznaczonych dla języka C# lub Visual Basic. Analizator zawiera kod, który rozpoznaje naruszenia reguły. Poprawka kodu zawiera kod, który naprawia naruszenie. Zaimplementowane reguły mogą obejmować wszystko, od struktury kodu, poprzez styl kodowania, aż po konwencje nazewnictwa i więcej. Platforma kompilatora .NET dostarcza ramy do przeprowadzania analizy, gdy deweloperzy piszą kod, oraz wszystkie funkcje interfejsu użytkownika programu Visual Studio służące do naprawiania kodu: wyświetlanie podkreśleń falistych w edytorze, wypełnianie listy błędów programu Visual Studio, tworzenie sugestii w formie ikony żarówki i wyświetlanie bogatych podglądów sugerowanych poprawek.

W tym samouczku zapoznasz się z tworzeniem analizatora oraz towarzyszącą poprawką kodu przy użyciu interfejsów API Roslyn. Analizator to sposób przeprowadzania analizy kodu źródłowego i zgłaszania problemu użytkownikowi. Opcjonalnie poprawkę kodu można skojarzyć z analizatorem w celu reprezentowania modyfikacji kodu źródłowego użytkownika. W tym samouczku tworzony jest analizator, który znajduje deklaracje zmiennych lokalnych, które można by zadeklarować przy użyciu modyfikatora const, ale tak się nie dzieje. Poprawka towarzyszącego kodu modyfikuje te deklaracje w celu dodania const modyfikatora.

Wymagania wstępne

Musisz zainstalować zestaw SDK platformy kompilatora .NET za pomocą Instalatora programu Visual Studio:

Instrukcje dotyczące instalacji — Instalator programu Visual Studio

Istnieją dwa różne sposoby znajdowania zestawu SDK platformy kompilatora .NET w Instalatorze programu Visual Studio:

Instalowanie przy użyciu widoku Instalator programu Visual Studio — obciążenia

Zestaw SDK platformy kompilatora .NET nie jest automatycznie wybierany w ramach obciążenia programistycznego rozszerzenia programu Visual Studio. Musisz wybrać go jako składnik opcjonalny.

  1. Uruchamianie Instalatora programu Visual Studio
  2. Wybierz pozycję Modyfikuj
  3. Sprawdź obciążenie programistyczne rozszerzenia programu Visual Studio .
  4. Otwórz węzeł rozwoju rozszerzeń Visual Studio w drzewie podsumowania.
  5. Zaznacz pole wyboru dla zestawu SDK platformy kompilatora .NET. Znajdziesz go jako ostatni w składnikach opcjonalnych.

Opcjonalnie chcesz również, aby edytor DGML wyświetlał wykresy w wizualizatorze:

  1. Otwórz węzeł Poszczególne składniki w drzewie podsumowania.
  2. Zaznacz pole wyboru edytora DGML

Zainstaluj za pomocą Instalatora programu Visual Studio – zakładka poszczególne składniki

  1. Uruchamianie Instalatora programu Visual Studio
  2. Wybierz pozycję Modyfikuj
  3. Wybierz kartę Poszczególne składniki
  4. Zaznacz pole wyboru dla zestawu SDK platformy kompilatora .NET. Znajdziesz ją u góry w sekcji Kompilatory, narzędzia kompilacji i środowiska uruchomieniowe .

Opcjonalnie chcesz również, aby edytor DGML wyświetlał wykresy w wizualizatorze:

  1. Zaznacz pole wyboru edytora DGML. Znajdziesz go w sekcji Narzędzia kodu .

Istnieje kilka kroków tworzenia i weryfikowania analizatora:

  1. Utwórz rozwiązanie.
  2. Zarejestruj nazwę i opis analizatora.
  3. Ostrzeżenia i zalecenia analizatora raportów.
  4. Zaimplementuj poprawkę kodu, aby zaakceptować zalecenia.
  5. Ulepszanie analizy za pomocą testów jednostkowych.

Tworzenie rozwiązania

  • W programie Visual Studio wybierz pozycję Plik > nowy > projekt... , aby wyświetlić okno dialogowe Nowy projekt.
  • W obszarze Rozszerzalność języka Visual C# >wybierz pozycję Analizator z poprawką kodu (.NET Standard).
  • Nadaj projektowi nazwę "MakeConst" i kliknij przycisk OK.

Uwaga / Notatka

Może wystąpić błąd kompilacji (MSB4062: nie można załadować zadania "CompareBuildTaskVersion". pl-PL: Aby rozwiązać ten problem, zaktualizuj pakiety NuGet w rozwiązaniu za pomocą Menedżera pakietów NuGet lub użyj Update-Package w oknie Konsoli Menedżera Pakietów.

Eksplorowanie szablonu analizatora

Analizator z szablonem poprawki kodu tworzy pięć projektów:

  • MakeConst, który zawiera analizator.
  • MakeConst.CodeFixes, który zawiera poprawkę kodu.
  • MakeConst.Package, który służy do tworzenia pakietu NuGet dla analizatora i poprawki kodu.
  • MakeConst.Test, który jest projektem testu jednostkowego.
  • MakeConst.Vsix, czyli domyślny projekt startowy, który uruchamia drugie wystąpienie programu Visual Studio, które załadowało nowy analizator. Naciśnij F5 , aby uruchomić projekt VSIX.

Uwaga / Notatka

Analizatory powinny być przeznaczone dla platformy .NET Standard 2.0, ponieważ mogą działać w środowisku .NET Core (kompilacje wiersza polecenia) i środowisku .NET Framework (Visual Studio).

Wskazówka

Po uruchomieniu analizatora uruchomisz drugą kopię programu Visual Studio. Ta druga kopia używa innej gałęzi rejestru do przechowywania ustawień. Umożliwia to odróżnienie ustawień wizualizacji w dwóch kopiach programu Visual Studio. Możesz wybrać inny motyw dla eksperymentalnego uruchomienia programu Visual Studio. Ponadto nie przenoś ustawień ani nie loguj się do konta programu Visual Studio w wersji eksperymentalnej programu Visual Studio. To sprawia, że ustawienia są inne.

Gałąź zawiera nie tylko analizator opracowywany, ale także wszystkie poprzednie otwarte analizatory. Aby zresetować gałąź Roslyn, należy ręcznie usunąć ją z %LocalAppData%\Microsoft\VisualStudio. Nazwa folderu gałęzi Roslyn zakończy się na przykład w Roslyn, jak w przypadku 16.0_9ae182f9Roslyn. Pamiętaj, że może być konieczne wyczyszczenie rozwiązania i ponowne skompilowanie go po usunięciu hive'u.

W drugim wystąpieniu programu Visual Studio, które właśnie uruchomiono, utwórz nowy projekt aplikacji konsolowej w języku C# (każda struktura docelowa będzie odpowiednia — analizatory działają na poziomie kodu źródłowego). Przytrzymaj wskaźnik myszy nad tokenem z falistym podkreśleniem, aby zobaczyć tekst ostrzeżenia dostarczony przez analizator.

Szablon tworzy analizator, który zgłasza ostrzeżenie dla każdej deklaracji typu, w której nazwa typu zawiera małe litery, jak pokazano na poniższej ilustracji:

Ostrzeżenie dotyczące raportowania analizatora

Szablon zawiera również poprawkę kodu, która zmienia dowolną nazwę typu zawierającą małe litery na wszystkie wielkie litery. Możesz kliknąć żarówkę wyświetlaną z ostrzeżeniem, aby zobaczyć sugerowane zmiany. Zaakceptowanie sugerowanych zmian aktualizuje nazwę typu i wszystkie odwołania do tego typu w rozwiązaniu. Teraz, gdy zobaczyłeś początkowy analizator w akcji, zamknij drugie wystąpienie programu Visual Studio i wróć do projektu analizatora.

Nie musisz uruchamiać drugiej kopii programu Visual Studio i tworzyć nowy kod, aby przetestować każdą zmianę w analizatorze. Szablon tworzy również dla ciebie projekt testów jednostkowych. Ten projekt zawiera dwa testy. TestMethod1 przedstawia typowy format testu, który analizuje kod bez wyzwalania diagnostyki. TestMethod2 Wyświetla format testu, który wyzwala diagnostykę, a następnie stosuje sugerowaną poprawkę kodu. Podczas tworzenia analizatora i poprawki kodu napiszesz testy dla różnych struktur kodu, aby zweryfikować swoją pracę. Testy jednostkowe analizatorów są znacznie szybsze niż testowanie ich interaktywnie za pomocą programu Visual Studio.

Wskazówka

Testy jednostkowe analizatora to doskonałe narzędzie, gdy wiesz, jakie konstrukcje kodu powinny i nie powinny wyzwalać analizatora. Ładowanie analizatora w innej kopii programu Visual Studio to doskonałe narzędzie do eksplorowania i znajdowania konstrukcji, o których być może jeszcze nie myśleliśmy.

W tym samouczku napiszesz analizator, który raportuje użytkownikowi wszelkie deklaracje zmiennych lokalnych, które można przekonwertować na stałe lokalne. Rozważmy na przykład następujący kod:

int x = 0;
Console.WriteLine(x);

Kod powyżej ma przypisaną stałą wartość x i nigdy nie jest modyfikowany. Można go zadeklarować przy użyciu const modyfikatora:

const int x = 0;
Console.WriteLine(x);

Analiza określająca, czy zmienna może być stała, wymaga analizy składni, stałej analizy wyrażenia inicjatora i analizy przepływu danych w celu zapewnienia, że zmienna nigdy nie jest zapisywana. Platforma kompilatora .NET udostępnia interfejsy API, które ułatwiają wykonywanie tej analizy.

Dokonaj rejestracji analizatora

Szablon tworzy klasę początkową DiagnosticAnalyzer w pliku MakeConstAnalyzer.cs . Ten początkowy analizator przedstawia dwie ważne właściwości każdego analizatora.

  • Każdy analizator diagnostyczny musi podać [DiagnosticAnalyzer] atrybut opisujący język, na którym działa.
  • Każdy analizator diagnostyczny musi pochodzić (bezpośrednio lub pośrednio) z DiagnosticAnalyzer klasy .

Szablon zawiera również podstawowe funkcje, które są częścią dowolnego analizatora:

  1. Rejestrowanie akcji. Akcje reprezentują zmiany kodu, które powinny wyzwolić analizator w celu sprawdzenia kodu pod kątem naruszeń. Gdy program Visual Studio wykryje edycję kodu zgodną z zarejestrowaną akcją, wywołuje metodę zarejestrowaną w analizatorze.
  2. Utwórz diagnostykę. Gdy analizator wykryje naruszenie, tworzy obiekt diagnostyczny, którego program Visual Studio używa do powiadamiania użytkownika o naruszeniu.

W swojej implementacji zastępującej metodę DiagnosticAnalyzer.Initialize(AnalysisContext) rejestrujesz akcje. W tym samouczku przejdziesz przez węzły składniowe, szukając deklaracji lokalnych, i sprawdzisz, które z nich mają stałe wartości. Jeśli deklaracja może być stała, analizator utworzy i zgłosi diagnozę.

Pierwszym krokiem jest zaktualizowanie stałych rejestracyjnych i metody Initialize, aby te stałe odpowiadały analizatorowi "Make Const". Większość stałych ciągów tekstowych jest definiowana w pliku zasobów tekstowych. Należy postępować zgodnie z tym rozwiązaniem w celu ułatwienia lokalizacji. Otwórz plik Resources.resx dla projektu MakeConst analyzer. Spowoduje to wyświetlenie edytora zasobów. Zaktualizuj zasoby ciągów znaków w następujący sposób:

  • Zmień AnalyzerDescription na "Variables that are not modified should be made constants.".
  • Zmień AnalyzerMessageFormat na "Variable '{0}' can be made constant".
  • Zmień AnalyzerTitle na "Variable can be made constant".

Po zakończeniu edytor zasobów powinien zostać wyświetlony, jak pokazano na poniższej ilustracji:

Zaktualizuj zasoby tekstowe

Pozostałe zmiany znajdują się w pliku analizatora. Otwórz MakeConstAnalyzer.cs w programie Visual Studio. Zmień zarejestrowaną akcję z działania na symbole na działanie na składnię. W metodzie MakeConstAnalyzerAnalyzer.Initialize znajdź wiersz rejestrujący akcję w symbolach:

context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);

Zastąp go następującym wierszem:

context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.LocalDeclarationStatement);

Po tej zmianie można usunąć metodę AnalyzeSymbol . Ten analizator sprawdza instrukcje SyntaxKind.LocalDeclarationStatement, a nie SymbolKind.NamedType . Zwróć uwagę, że pod AnalyzeNode znajdują się czerwone zygzaki. Właśnie dodany kod odwołuje się do AnalyzeNode metody, która nie została zadeklarowana. Zadeklaruj tę metodę przy użyciu następującego kodu:

private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
}

Zmień wartość Category na "Usage" w MakeConstAnalyzer.cs , jak pokazano w poniższym kodzie:

private const string Category = "Usage";

Znajdowanie lokalnych deklaracji, które mogą być const

Nadszedł czas, aby napisać pierwszą wersję AnalyzeNode metody. Powinna ona wyszukać pojedynczą lokalną deklarację, która może być const , ale nie jest taka jak następujący kod:

int x = 0;
Console.WriteLine(x);

Pierwszym krokiem jest znalezienie deklaracji lokalnych. Dodaj następujący kod do AnalyzeNode w MakeConstAnalyzer.cs:

var localDeclaration = (LocalDeclarationStatementSyntax)context.Node;

Rzutowanie zawsze kończy się powodzeniem, ponieważ analizator zarejestrował zmiany w deklaracjach lokalnych i tylko deklaracje lokalne. Żaden inny typ węzła nie wywołuje metody AnalyzeNode. Następnie sprawdź deklarację dla wszystkich const modyfikatorów. Jeśli je znajdziesz, wróć natychmiast. Poniższy kod wyszukuje wszelkie const modyfikatory w deklaracji lokalnej:

// make sure the declaration isn't already const:
if (localDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword))
{
    return;
}

Na koniec należy sprawdzić, czy zmienna może mieć constwartość . Oznacza to, że należy upewnić się, iż nigdy nie zostanie przypisane po jego zainicjowaniu.

Wykonasz analizę semantyczną przy użyciu elementu SyntaxNodeAnalysisContext. Użyj argumentu context, aby określić, czy można dokonać deklaracji zmiennej lokalnej jako const. Obiekt Microsoft.CodeAnalysis.SemanticModel reprezentuje wszystkie informacje semantyczne w jednym pliku źródłowym. Więcej informacji można dowiedzieć się w artykule dotyczącym modeli semantycznych. Użyjesz narzędzia Microsoft.CodeAnalysis.SemanticModel, aby przeprowadzić analizę przepływu danych w zakresie deklaracji lokalnej. Następnie użyjesz wyników tej analizy przepływu danych, aby upewnić się, że zmienna lokalna nie jest zapisywana z nową wartością nigdzie indziej. Wywołaj element członkowski rozszerzenia GetDeclaredSymbol, aby uzyskać ILocalSymbol przypisane do zmiennej i sprawdź, czy nie znajduje się w kolekcji DataFlowAnalysis.WrittenOutside używanej do analizy przepływu danych. Dodaj następujący kod na końcu AnalyzeNode metody:

// 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;
}

Właśnie dodany kod gwarantuje, że zmienna nie zostanie zmodyfikowana i dlatego może zostać wykonana const. Nadszedł czas, aby udoskonalić diagnostykę. Dodaj następujący kod jako ostatni wiersz w pliku AnalyzeNode:

context.ReportDiagnostic(Diagnostic.Create(Rule, context.Node.GetLocation(), localDeclaration.Declaration.Variables.First().Identifier.ValueText));

Postęp możesz sprawdzić, naciskając F5 , aby uruchomić analizator. Możesz załadować utworzoną wcześniej aplikację konsolową, a następnie dodać następujący kod testowy:

int x = 0;
Console.WriteLine(x);

Powinna się pojawić żarówka, a analizator powinien rozpocząć diagnozowanie. Jednak w zależności od używanej wersji programu Visual Studio zobaczysz:

  • Żarówka, która nadal używa poprawki kodu generowanego na podstawie szablonu, poinformuje Cię, że można to zamienić na wielkie litery.
  • W górnej części edytora pojawiła się wiadomość na banerze, że 'MakeConstCodeFixProvider' napotkał błąd i został wyłączony. Jest to spowodowane tym, że dostawca poprawki kodu nie został jeszcze zmieniony i nadal oczekuje znalezienia elementów TypeDeclarationSyntax zamiast elementów LocalDeclarationStatementSyntax.

W następnej sekcji wyjaśniono, jak napisać poprawkę kodu.

Pisanie poprawki kodu

Analizator może zapewnić co najmniej jedną poprawkę kodu. Poprawka kodu definiuje edycję, która rozwiązuje zgłoszony problem. W przypadku utworzonego analizatora możesz podać poprawkę kodu, która wstawia słowo kluczowe const:

- int x = 0;
+ const int x = 0;
Console.WriteLine(x);

Użytkownik wybiera go z interfejsu użytkownika żarówki w edytorze, a program Visual Studio zmienia kod.

Otwórz plik CodeFixResources.resx i zmień go CodeFixTitle na "Make constant".

Otwórz plik MakeConstCodeFixProvider.cs dodany przez szablon. Ta poprawka kodu jest już podłączona do identyfikatora diagnostycznego wygenerowanego przez analizatora diagnostycznego, ale nie implementuje jeszcze właściwej transformacji kodu.

Następnie usuń metodę MakeUppercaseAsync . Nie ma już zastosowania.

Wszyscy dostawcy poprawki kodu pochodzą z elementu CodeFixProvider. Wszystkie one zastępują CodeFixProvider.RegisterCodeFixesAsync(CodeFixContext) raport dostępnych poprawek kodu. W RegisterCodeFixesAsync zmień typ węzła przodka, którego szukasz, na LocalDeclarationStatementSyntax, aby był zgodny z diagnostyką:

var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<LocalDeclarationStatementSyntax>().First();

Następnie zmień ostatni wiersz, aby zarejestrować poprawkę kodu. Poprawka spowoduje utworzenie nowego dokumentu, który wynika z dodania const modyfikatora do istniejącej deklaracji:

// 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);

W kodzie, który właśnie dodałeś, zauważysz czerwone zygzaki przy symbolu MakeConstAsync. Dodaj deklarację dla MakeConstAsync jak w poniższym kodzie:

private static async Task<Document> MakeConstAsync(Document document,
    LocalDeclarationStatementSyntax localDeclaration,
    CancellationToken cancellationToken)
{
}

Nowa MakeConstAsync metoda przekształci Document reprezentujący plik źródłowy użytkownika w nowy Document , który zawiera teraz deklarację const .

Utworzysz nowy const token słowa kluczowego, który zostanie wstawiony na początku wyrażenia deklaracji. Należy najpierw usunąć wszelkie początkowe nieistotne elementy z pierwszego tokenu deklaracji i dołączyć je do tokenu const. Dodaj do metody MakeConstAsync następujący kod:

// 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));

Następnie dodaj const token do deklaracji przy użyciu następującego kodu:

// 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);

Następnie sformatuj nową deklarację tak, aby odpowiadała regułom formatowania języka C#. Formatowanie zmian w celu dopasowania do istniejącego kodu powoduje lepsze środowisko pracy. Dodaj następującą instrukcję natychmiast po istniejącym kodzie:

// Add an annotation to format the new local declaration.
LocalDeclarationStatementSyntax formattedLocal = newLocal.WithAdditionalAnnotations(Formatter.Annotation);

Dla tego kodu jest wymagana nowa przestrzeń nazw. Dodaj następującą using dyrektywę na początku pliku:

using Microsoft.CodeAnalysis.Formatting;

Ostatnim krokiem jest wprowadzenie edycji. Ten proces obejmuje trzy kroki:

  1. Pobierz dojście do istniejącego dokumentu.
  2. Utwórz nowy dokument, zastępując istniejącą deklarację nową deklaracją.
  3. Zwróć nowy dokument.

Dodaj następujący kod na końcu MakeConstAsync metody:

// 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);

Poprawka kodu jest gotowa do wypróbowania. Naciśnij F5 , aby uruchomić projekt analizatora w drugim wystąpieniu programu Visual Studio. W drugim wystąpieniu programu Visual Studio utwórz nowy projekt aplikacji konsolowej języka C# i dodaj kilka lokalnych deklaracji zmiennych zainicjowanych przy użyciu wartości stałych do metody Main. Zobaczysz, że są one zgłaszane jako ostrzeżenia, jak pokazano poniżej.

Może tworzyć ostrzeżenia const

Poczyniłeś wiele postępów. W deklaracjach, które można wykonać const, są zygzaki. Ale nadal jest praca do zrobienia. To działa dobrze, jeśli dodasz const do deklaracji rozpoczynających się od i, następnie j i na końcu k. Jeśli jednak dodasz const modyfikator w innej kolejności, zaczynając od k, analizator tworzy błędy: k nie może być zadeklarowany jako const, chyba że zarówno i jak i j już są const. Musisz wykonać większą analizę, aby upewnić się, że obsłużysz różne sposoby deklarowania i inicjowania zmiennych.

Kompilowanie testów jednostkowych

Analizator i poprawka kodu odnoszą się do prostego przypadku, w którym pojedynczą deklarację można oznaczyć jako stałą. Istnieje wiele możliwych deklaracji instrukcji, w których ta implementacja popełnia błędy. Będziesz rozwiązywać te przypadki, pracując z biblioteką testów jednostkowych napisaną według szablonu. Jest to znacznie szybsze niż wielokrotne otwieranie drugiej kopii programu Visual Studio.

Otwórz plik MakeConstUnitTests.cs w projekcie testów jednostkowych. Szablon utworzył dwa testy zgodne z dwoma powszechnymi wzorcami - dla analizatora oraz dla testu jednostkowego poprawki kodu. TestMethod1 pokazuje wzorzec testu, który gwarantuje, że analizator nie zgłasza diagnostyki, gdy nie powinien. TestMethod2 Pokazuje wzorzec raportowania diagnostyki i uruchamiania poprawki kodu.

Szablon używa pakietów Microsoft.CodeAnalysis.Testing do testowania jednostkowego.

Wskazówka

Biblioteka testowania obsługuje specjalną składnię znaczników, w tym następujące:

  • [|text|]: wskazuje, że diagnostyka jest zgłaszana dla elementu text. Domyślnie ten formularz może być używany tylko do testowania analizatorów z dokładnie jednym DiagnosticDescriptor podanym przez DiagnosticAnalyzer.SupportedDiagnostics program.
  • {|ExpectedDiagnosticId:text|}: wskazuje, że diagnostyka dotycząca elementu IdExpectedDiagnosticId jest zgłaszana dla elementu text.

Zastąp testy szablonów w MakeConstUnitTest klasie następującą metodą testową:

        [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);
    }
}
");
        }

Uruchom ten test, aby upewnić się, że przebiegnie pomyślnie. W programie Visual Studio otwórz Eksploratora testów, wybierając Test>Windows>Eksplorator testów. Następnie wybierz pozycję Uruchom wszystko.

Tworzenie testów dla prawidłowych deklaracji

Ogólnie rzecz biorąc, analizatory powinny wyjść tak szybko, jak to możliwe, wykonując minimalną pracę. Program Visual Studio wywołuje zarejestrowane analizatory, gdy użytkownik edytuje kod. Kluczowym wymaganiem jest elastyczność reagowania. Istnieje kilka przypadków testowych kodu, które nie powinny zgłaszać diagnostyki. Analizator obsługuje już kilka z tych testów. Dodaj następujące metody testowe, aby reprezentować te przypadki:

        [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);
    }
}
");
        }

Te testy przechodzą, ponieważ analizator obsługuje już te warunki.

  • Zmienne przypisane po zainicjowaniu są wykrywane przez analizę przepływu danych.
  • Deklaracje, które już zostały const odfiltrowane, sprawdzając słowo kluczowe const.
  • Deklaracje bez inicjatora są obsługiwane przez analizę przepływu danych, która wykrywa przypisania poza deklaracją.

Następnie dodaj metody testowania dla warunków, które nie zostały jeszcze obsłużone:

  • Deklaracje, w których inicjator nie jest stałą, ponieważ nie mogą być stałymi w czasie kompilacji:

            [TestMethod]
            public async Task InitializerIsNotConstant_NoDiagnostic()
            {
                await VerifyCS.VerifyAnalyzerAsync(@"
    using System;
    
    class Program
    {
        static void Main()
        {
            int i = DateTime.Now.DayOfYear;
            Console.WriteLine(i);
        }
    }
    ");
            }
    

Może to być jeszcze bardziej skomplikowane, ponieważ język C# zezwala na wiele deklaracji jako jedną instrukcję. Rozważmy następującą stałą ciąg przypadku testowego:

        [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);
    }
}
");
        }

Zmienna i może być stała, ale zmienna j nie może. W związku z tym nie można dokonać deklaracji const.

Ponownie uruchom testy i zobaczysz, że te dwa ostatnie przypadki testowe kończą się niepowodzeniem.

Zaktualizuj analizator, aby ignorował poprawne deklaracje

Potrzebujesz pewnych ulepszeń metody analizatora AnalyzeNode , aby odfiltrować kod zgodny z tymi warunkami. Są to wszystkie powiązane warunki, więc podobne zmiany naprawią wszystkie te warunki. Wprowadź następujące zmiany w :AnalyzeNode

  • Analiza semantyczna zbadała pojedynczą deklarację zmiennej. Ten kod musi znajdować się w foreach pętli, która sprawdza wszystkie zmienne zadeklarowane w tej samej instrukcji.
  • Każda zadeklarowana zmienna musi mieć inicjator.
  • Inicjator każdej zadeklarowanej zmiennej musi być stałą czasu kompilacji.

W metodzie AnalyzeNode zastąp oryginalną analizę semantyczną:

// 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;
}

z następującym fragmentem kodu:

// 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;
    }
}

Pierwsza foreach pętla sprawdza każdą deklarację zmiennej przy użyciu analizy składniowej. Pierwsza kontrola gwarantuje, że zmienna ma inicjator. Druga kontrola gwarantuje, że inicjalizator jest stałą. Druga pętla ma oryginalną analizę semantyczną. Testy semantyczne znajdują się w oddzielnej pętli, ponieważ ma większy wpływ na wydajność. Uruchom testy ponownie, a powinny wszystkie przejść pomyślnie.

Dodaj ostateczne wykończenie

To już prawie koniec. Istnieje jeszcze kilka warunków obsługi analizatora. Program Visual Studio wywołuje analizatory podczas pisania kodu przez użytkownika. Często zdarza się, że analizator będzie wywoływany dla kodu, który nie jest kompilowany. Metoda analizatora AnalyzeNode diagnostycznego nie sprawdza, czy stała wartość jest konwertowana na typ zmiennej. W związku z tym bieżąca implementacja bezproblemowo przekonwertuje nieprawidłową deklarację, taką jak int i = "abc", na stałą lokalną. Dodaj metodę testową dla tego przypadku:

        [TestMethod]
        public async Task DeclarationIsInvalid_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        int x = {|CS0029:""abc""|};
    }
}
");
        }

Ponadto typy referencyjne nie są odpowiednio obsługiwane. Jedyną wartością stałą dozwoloną dla typu odwołania jest null, z wyjątkiem przypadku System.String, która zezwala na literały ciągu. Innymi słowy, const string s = "abc" jest legalne, ale const object s = "abc" nie. Ten fragment kodu weryfikuje ten warunek:

        [TestMethod]
        public async Task DeclarationIsNotString_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        object s = ""abc"";
    }
}
");
        }

Aby dokładnie sprawdzić, należy dodać kolejny test, aby upewnić się, że można utworzyć stałą deklarację dla ciągu. Poniższy fragment kodu definiuje zarówno kod, który zgłasza diagnostykę, jak i kod po zastosowaniu poprawki:

        [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"";
    }
}
");
        }

Na koniec, jeśli zmienna jest zadeklarowana za pomocą słowa kluczowego var , poprawka kodu wykonuje nieprawidłową czynność i generuje deklarację const var , która nie jest obsługiwana przez język C#. Aby naprawić tę usterkę, poprawka kodu musi zastąpić var słowo kluczowe nazwą wywnioskowanego typu:

        [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"";
    }
}
");
        }

Na szczęście wszystkie powyższe usterki można rozwiązać przy użyciu tych samych technik, które właśnie znasz.

Aby naprawić pierwszą usterkę, najpierw otwórz MakeConstAnalyzer.cs i znajdź pętlę foreach, w której wszystkie inicjatory deklaracji lokalnej są sprawdzane, aby upewnić się, że są one przypisane ze stałymi wartościami. Bezpośrednio przed pierwszą pętlą foreach wywołaj metodę context.SemanticModel.GetTypeInfo() , aby pobrać szczegółowe informacje o deklarowanym typie deklaracji lokalnej:

TypeSyntax variableTypeName = localDeclaration.Declaration.Type;
ITypeSymbol variableType = context.SemanticModel.GetTypeInfo(variableTypeName, context.CancellationToken).ConvertedType;

Następnie wewnątrz foreach pętli sprawdź każdy inicjalizator, aby upewnić się, że jest możliwy do konwersji na typ zmiennej. Dodaj następujące sprawdzenie po upewnieniu się, że inicjalizator jest stałą:

// 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;
}

Następna zmiana opiera się na ostatnim. Przed zamykającym nawiasem klamrowym pierwszej pętli foreach dodaj następujący kod, aby sprawdzić typ deklaracji lokalnej, gdy stała jest ciągiem lub wartością 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;
}

Musisz napisać nieco więcej kodu u dostawcy poprawki kodu, aby zastąpić var słowo kluczowe poprawną nazwą typu. Wróć do MakeConstCodeFixProvider.cs. Dodany kod wykonuje następujące czynności:

  • Sprawdź, czy deklaracja jest deklaracją var i czy jest:
  • Utwórz nowy typ dla wnioskowanego typu.
  • Upewnij się, że deklaracja typu nie jest aliasem. Jeśli tak, jest to legalne deklarowanie const var.
  • Upewnij się, że var nie jest to nazwa typu w tym programie. (Jeśli tak, const var jest legalne).
  • Uproszczenie pełnej nazwy typu

To brzmi jak dużo kodu. To nie jest. Zastąp wiersz, który deklaruje i inicjuje newLocal w następujący sposób. Następuje ono natychmiast po zainicjowaniu elementu 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);

Musisz dodać jedną dyrektywę using, aby użyć typu Simplifier.

using Microsoft.CodeAnalysis.Simplification;

Uruchom testy i wszystkie powinny przejść. Gratuluj sobie, uruchamiając gotowy analizator. Naciśnij Ctrl+F5 , aby uruchomić projekt analizatora w drugim wystąpieniu programu Visual Studio z załadowanym rozszerzeniem Roslyn Preview.

  • W drugim wystąpieniu programu Visual Studio utwórz nowy projekt aplikacji konsolowej języka C# i dodaj int x = "abc"; do metody Main. Dzięki pierwszej poprawce błędu nie powinno być zgłaszane żadne ostrzeżenie dla tej deklaracji zmiennej lokalnej (choć występuje błąd kompilatora zgodnie z oczekiwaniami).
  • Następnie dodaj object s = "abc"; do metody Main. Ze względu na drugą poprawkę usterki nie należy zgłaszać żadnego ostrzeżenia.
  • Na koniec dodaj kolejną zmienną lokalną, która używa słowa kluczowego var . Zobaczysz, że zostanie zgłoszone ostrzeżenie, a sugestia pojawi się poniżej, po lewej stronie.
  • Przenieś kursor edytora nad faliste podkreślenie i naciśnij Ctrl+.. aby wyświetlić sugerowaną poprawkę kodu. Po wybraniu poprawki kodu zwróć uwagę, że var słowo kluczowe jest teraz poprawnie obsługiwane.

Na koniec dodaj następujący kod:

int i = 2;
int j = 32;
int k = i + j;

Po tych zmianach uzyskasz czerwone zygzaki tylko dla dwóch pierwszych zmiennych. Dodaj const do i i j, a otrzymasz nowe ostrzeżenie na k ponieważ teraz może to być const.

Gratulacje! Udało Ci się utworzyć pierwsze rozszerzenie platformy kompilatora .NET, które wykonuje analizę kodu na bieżąco w celu wykrycia problemu i zapewnia szybką poprawkę, aby rozwiązać ten problem. Po drodze nauczyłeś się wielu interfejsów API kodu, które są częścią zestawu SDK platformy kompilatora .NET (Roslyn API). Możesz sprawdzić swoją pracę z ukończonym przykładem w naszym repozytorium GitHub próbek.

Inne zasoby