Samouczek: pisanie pierwszego analizatora i poprawki kodu

Zestaw SDK .NET Compiler Platform 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ą być niczym — od struktury kodu po styl kodowania po konwencje nazewnictwa i nie tylko. .NET Compiler Platform udostępnia platformę do uruchamiania analizy, ponieważ deweloperzy piszą kod, a wszystkie funkcje interfejsu użytkownika programu Visual Studio do naprawiania kodu: wyświetlanie zygzaków w edytorze, wypełnianie listy błędów programu Visual Studio, tworzenie sugestii "żarówki" i wyświetlanie bogatej wersji zapoznawczej sugerowanych poprawek.

W tym samouczku zapoznasz się z tworzeniem analizatora i towarzyszącej poprawki 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 zadeklarować przy użyciu const modyfikatora, ale nie. Towarzysząca poprawka kodu modyfikuje te deklaracje w celu dodania const modyfikatora.

Wymagania wstępne

Musisz zainstalować zestaw SDK .NET Compiler Platform za pośrednictwem Instalator programu Visual Studio:

Instrukcje dotyczące instalacji — Instalator programu Visual Studio

Istnieją dwa różne sposoby znajdowania zestawu SDK .NET Compiler Platform w Instalator programu Visual Studio:

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

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

  1. Uruchamianie Instalator programu Visual Studio
  2. Wybierz pozycję Modyfikuj
  3. Sprawdź obciążenie programistyczne rozszerzenia programu Visual Studio .
  4. Otwórz węzeł programowania rozszerzenia programu Visual Studio w drzewie podsumowania.
  5. Zaznacz pole wyboru dla zestawu SDK .NET Compiler Platform. Znajdziesz go ostatnio w obszarze składników 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

Instalowanie przy użyciu karty Instalator programu Visual Studio — poszczególne składniki

  1. Uruchamianie Instalator programu Visual Studio
  2. Wybierz pozycję Modyfikuj
  3. Wybieranie karty Poszczególne składniki
  4. Zaznacz pole wyboru dla zestawu SDK .NET Compiler Platform. Znajduje się on 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 dotyczące 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

Może wystąpić błąd kompilacji (MSB4062: Nie można załadować zadania "CompareBuildTaskVersion"). Aby rozwiązać ten problem, zaktualizuj pakiety NuGet w rozwiązaniu za pomocą Menedżera pakietów NuGet lub użyj Update-Package go w oknie Konsola 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, który jest domyślnym projektem startowym, który uruchamia drugie wystąpienie programu Visual Studio, które załadowało nowy analizator. Naciśnij klawisz F5 , aby uruchomić projekt VSIX.

Uwaga

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

Porada

Po uruchomieniu analizatora uruchamiasz drugą kopię programu Visual Studio. Ta druga kopia używa innej gałęzi rejestru do przechowywania ustawień. Umożliwia to odróżnienie ustawień wizualnych w dwóch kopiach programu Visual Studio. Możesz wybrać inny motyw dla eksperymentalnego przebiegu programu Visual Studio. Ponadto nie przejmij ustawień ani nie loguj się do konta programu Visual Studio przy użyciu eksperymentalnego uruchomienia programu Visual Studio. Dzięki temu ustawienia są inne.

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

W drugim wystąpieniu programu Visual Studio, które właśnie uruchomiono, utwórz nowy projekt aplikacji konsolowej języka C# (każda platforma docelowa będzie działać — analizatory będą działać na poziomie źródłowym). Umieść kursor na tokenie z falistym podkreśleniu, a zostanie wyświetlony tekst ostrzeżenia dostarczony przez analizator.

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

Ostrzeżenie raportowania analizatora

Szablon zawiera również poprawkę kodu, która zmienia nazwę typu zawierającą małe litery na 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. Po wyświetleniu początkowego analizatora w działaniu zamknij drugie wystąpienie programu Visual Studio i wróć do projektu analizatora.

Nie musisz uruchamiać drugiej kopii programu Visual Studio i tworzyć nowego kodu, aby przetestować każdą zmianę w analizatorze. Szablon tworzy również 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 będziesz pisać testy dla różnych struktur kodu w celu zweryfikowania pracy. Testy jednostkowe analizatorów są znacznie szybsze niż interaktywne testowanie ich za pomocą programu Visual Studio.

Porada

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ślałeś.

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

W powyższym x kodzie jest przypisana stała wartość i nigdy nie jest modyfikowana. Można ją 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, aby upewnić się, że zmienna nigdy nie jest zapisywana. .NET Compiler Platform udostępnia interfejsy API, które ułatwiają wykonywanie tej analizy.

Tworzenie 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ć analizatora w celu zbadania 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.

Akcje są rejestrowane w przesłonięciu DiagnosticAnalyzer.Initialize(AnalysisContext) metody. W tym samouczku poznasz węzły składni szukające deklaracji lokalnych i zobaczysz, które z nich mają stałe wartości. Jeśli deklaracja może być stała, analizator utworzy i zgłosi diagnostykę.

Pierwszym krokiem jest zaktualizowanie stałych rejestracji i Initialize metody, aby te stałe wskazywały analizatora "Make Const". Większość stałych ciągów jest definiowana w pliku zasobu ciągu. Należy przestrzegać tej praktyki, aby ułatwić lokalizację. Otwórz plik Resources.resx dla projektu MakeConst analyzer. Spowoduje to wyświetlenie edytora zasobów. Zaktualizuj zasoby ciągów w następujący sposób:

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

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

Aktualizowanie zasobów ciągów

Pozostałe zmiany znajdują się w pliku analizatora. Otwórz plik MakeConstAnalyzer.cs w programie Visual Studio. Zmień zarejestrowaną akcję z jednej, która działa na symbole, która działa 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 AnalyzeNode pod nim ma 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ść na Category "Usage" w pliku 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ą deklarację lokalną, która może być const , ale nie, jak w poniższym kodzie:

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

Pierwszym krokiem jest znalezienie deklaracji lokalnych. Dodaj następujący kod do AnalyzeNode pliku w pliku 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 wyzwala wywołania AnalyzeNode metody . Następnie sprawdź deklarację wszelkich 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 po zainicjowaniu nigdy nie jest przypisywana.

Wykonasz analizę semantyczną przy użyciu elementu SyntaxNodeAnalysisContext. Argument służy context do określania, czy można wprowadzić constdeklarację zmiennej lokalnej . 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 elementu , Microsoft.CodeAnalysis.SemanticModel aby przeprowadzić analizę przepływu danych w instrukcji deklaracji lokalnej. Następnie należy użyć wyników analizy przepływu danych, aby upewnić się, że zmienna lokalna nie jest zapisywana z nową wartością nigdzie indziej. Wywołaj metodę GetDeclaredSymbol rozszerzenia, aby pobrać ILocalSymbol zmienną i sprawdzić, czy nie jest ona zawarta w DataFlowAnalysis.WrittenOutside kolekcji 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 w związku z tym może zostać wykonana .const Nadszedł czas, aby podnieść 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));

Możesz sprawdzić postęp, naciskając klawisz 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);

Żarówka powinna zostać wyświetlona, a analizator powinien zgłosić diagnostykę. Jednak w zależności od używanej wersji programu Visual Studio zobaczysz:

  • Żarówka, która nadal używa poprawki wygenerowanego kodu szablonu, poinformuje o tym, że może zostać wykonana wielkie litery.
  • Komunikat baneru w górnej części edytora z informacją o błędzie "MakeConstCodeFixProvider" napotkał błąd i został wyłączony. Jest to spowodowane tym, że dostawca poprawki kodu nie został jeszcze zmieniony i nadal oczekuje znalezienia TypeDeclarationSyntax elementów zamiast LocalDeclarationStatementSyntax elementów.

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

Pisanie poprawki kodu

Analizator może dostarczyć 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 utworzonego 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 klasy CodeFixProvider. Wszystkie one zastępują raportowanie CodeFixProvider.RegisterCodeFixesAsync(CodeFixContext) dostępnych poprawek kodu. W RegisterCodeFixesAsyncpliku zmień typ węzła nadrzędny, którego szukasz, aby LocalDeclarationStatementSyntax 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 będzie 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 został dodany do symbolu MakeConstAsync, zauważysz czerwone zygzaki . Dodaj deklarację dla polecenia MakeConstAsync , podobnie 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 .

Należy utworzyć nowy const token słowa kluczowego, który ma zostać wstawiony przed instrukcją deklaracji. Należy najpierw usunąć wszelkie wiodące ciekawostki z pierwszego tokenu instrukcji deklaracji i dołączyć je do tokenu const . Dodaj następujący kod do metody 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));

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 zapewnia lepsze środowisko pracy. Dodaj następującą instrukcję bezpośrednio 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 klawisz 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ą wywłasze. Ale nadal jest praca do zrobienia. To działa dobrze, jeśli dodasz const do deklaracji rozpoczynających się od i, a następnie j na koniec k. Jeśli jednak dodasz const modyfikator w innej kolejności, zaczynając od k, analizator tworzy błędy: k nie można zadeklarować const, chyba że i oba j te elementy są już 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 działają na prostym przypadku pojedynczej deklaracji, którą można wykonać const. Istnieje wiele możliwych stwierdzeń deklaracji, w których ta implementacja popełnia błędy. Te przypadki zostaną rozwiązane przez pracę z biblioteką testów jednostkowych napisanych przez szablon. Jest to znacznie szybsze niż wielokrotne otwieranie drugiej kopii programu Visual Studio.

Otwórz plik MakeConstUnitTests.cs w projekcie testu jednostkowego. Szablon utworzył dwa testy, które są zgodne z dwoma typowymi wzorcami analizatora i 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.

Porada

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

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

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 pozycję Testuj>Eksplorator testów systemuWindows>. Następnie wybierz pozycję Uruchom wszystko.

Tworzenie testów dla prawidłowych deklaracji

Ogólnie rzecz biorąc, analizatory powinny zakończyć się tak szybko, jak to możliwe, wykonując minimalną pracę. Program Visual Studio wywołuje zarejestrowane analizatory, gdy użytkownik edytuje kod. Czas odpowiedzi jest kluczowym wymaganiem. Istnieje kilka przypadków testowych dla kodu, który nie powinien zgłaszać diagnostyki. Analizator obsługuje już jeden z tych testów, przypadek, w którym zmienna jest przypisywana po zainicjowaniu. Dodaj następującą metodę testową, aby reprezentować ten przypadek:

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

class Program
{
    static void Main()
    {
        int i = 0;
        Console.WriteLine(i++);
    }
}
");
        }

Ten test również zakończy się pomyślnie. Następnie dodaj metody testowe dla warunków, które nie zostały jeszcze obsłużone:

  • Deklaracje, które są już const, ponieważ są już const:

            [TestMethod]
            public async Task VariableIsAlreadyConst_NoDiagnostic()
            {
                await VerifyCS.VerifyAnalyzerAsync(@"
    using System;
    
    class Program
    {
        static void Main()
        {
            const int i = 0;
            Console.WriteLine(i);
        }
    }
    ");
            }
    
  • Deklaracje, które nie mają inicjatora, ponieważ nie ma wartości do użycia:

            [TestMethod]
            public async Task NoInitializer_NoDiagnostic()
            {
                await VerifyCS.VerifyAnalyzerAsync(@"
    using System;
    
    class Program
    {
        static void Main()
        {
            int i;
            i = 0;
            Console.WriteLine(i);
        }
    }
    ");
            }
    
  • Deklaracje, w których inicjator nie jest stałą, ponieważ nie mogą być stałe czasu 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 to stwierdzenie nie może zostać złożone w oświadczeniu const.

Uruchom ponownie testy i zobaczysz, że te nowe przypadki testowe kończą się niepowodzeniem.

Aktualizowanie analizatora w celu ignorowania poprawnych deklaracji

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 inicjator jest stałą. Druga pętla ma oryginalną analizę semantyczną. Kontrole semantyczne znajdują się w oddzielnej pętli, ponieważ ma większy wpływ na wydajność. Uruchom ponownie testy i powinny zostać wyświetlone wszystkie testy.

Dodawanie ostatecznego polaka

To już prawie koniec. Istnieje jeszcze kilka warunków obsługi analizatora. Program Visual Studio wywołuje analizatory, gdy użytkownik pisze kod. Często zdarza się, że analizator będzie wywoływany dla kodu, który nie jest kompilowany. Metoda analizatora AnalyzeNode diagnostycznego nie sprawdza, czy wartość stała jest konwertowana na typ zmiennej. W związku z tym bieżąca implementacja szczęśliwie przekonwertuje nieprawidłową deklarację, taką jak int i = "abc" stała lokalna. 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 odwołań nie są prawidłowo 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 jest. 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 rozwiązać ten błąd, 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 plik MakeConstAnalyzer.cs i znajdź pętlę foreach, w której są sprawdzane poszczególne inicjatory deklaracji lokalnej, 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 inicjator, aby upewnić się, że jest konwertowany na typ zmiennej. Dodaj następujące sprawdzanie po upewnieniu się, że inicjator 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 pliku MakeConstCodeFixProvider.cs. Dodany kod wykonuje następujące czynności:

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

Brzmi to jak wiele kodu. To nie jest. Zastąp wiersz, który deklaruje i inicjuje newLocal następujący kod. Następuje to 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);

Aby użyć typu, musisz dodać jedną using dyrektywę Simplifier :

using Microsoft.CodeAnalysis.Simplification;

Uruchom testy i wszystkie powinny przejść. Gratuluj sobie, uruchamiając gotowy analizator. Naciśnij klawisze 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"; go do metody Main. Dzięki pierwszej poprawce usterki 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"; element do metody Main. Z powodu drugiej poprawki usterki nie należy zgłaszać żadnych ostrzeżeń.
  • Na koniec dodaj kolejną zmienną lokalną, która używa słowa kluczowego var . Zobaczysz, że zostanie zgłoszone ostrzeżenie i pod lewej stronie pojawi się sugestia.
  • Przenieś karetki edytora nad zwijaną podkreślenie i naciśnij klawisze Ctrl+.. aby wyświetlić sugerowaną poprawkę kodu. Po wybraniu poprawki kodu pamiętaj, ż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 uzyskujesz czerwone zygzaki tylko dla dwóch pierwszych zmiennych. Dodaj const element zarówno i do , jak i j, i otrzymasz nowe ostrzeżenie k , ponieważ może to być constteraz .

Gratulacje! Udało Ci się utworzyć pierwsze rozszerzenie .NET Compiler Platform, które wykonuje analizę kodu na bieżąco w celu wykrycia problemu i zapewnia szybkie rozwiązanie problemu. Po drodze przedstawiono wiele interfejsów API kodu, które są częścią zestawu SDK .NET Compiler Platform (Interfejsy API Roslyn). Możesz sprawdzić swoją pracę z ukończonym przykładem w naszym repozytorium GitHub przykładów.

Inne zasoby