Kom igång med syntaxomvandling

Den här självstudien bygger på begrepp och tekniker som utforskas i snabbstarten Komma igång med syntaxanalys och Komma igång med semantisk analys . Om du inte redan har gjort det bör du slutföra dessa snabbstarter innan du påbörjar den här.

I den här snabbstarten utforskar du tekniker för att skapa och transformera syntaxträd. I kombination med de tekniker som du lärde dig i tidigare snabbstarter skapar du din första kommandoradsrefaktorisering!

Installationsinstruktioner – Visual Studio Installer

Det finns två olika sätt att hitta .NET Compiler Platform SDK i Visual Studio Installer:

Installera med Visual Studio Installer – arbetsbelastningsvyn

.NET Compiler Platform SDK väljs inte automatiskt som en del av arbetsbelastningen för utveckling av Visual Studio-tillägget. Du måste välja den som en valfri komponent.

  1. Kör Visual Studio Installer
  2. Välj Ändra
  3. Kontrollera arbetsuppgiften utveckling av tillägg i Visual Studio.
  4. Öppna Visual Studio-tilläggsutveckling i sammanfattningsträdet.
  5. Kontrollera att rutan för .NET Compiler Platform SDK är markerad.
  6. Välj Ändra.

Du vill också att DGML-redigeraren ska visa grafer i visualiseraren:

  1. Öppna noden Enskilda komponenter i sammanfattningsträdet.
  2. Markera kryssrutan för DGML-redigeraren

Installera med hjälp av fliken Installationsprogram för Visual Studio – enskilda komponenter

  1. Kör Visual Studio Installer
  2. Välj Ändra
  3. Välj fliken Enskilda komponenter
  4. Markera kryssrutan för .NET Compiler Platform SDK. Du hittar den längst upp under avsnittet Kompilatorer, byggverktyg och körning.
  5. Välj Ändra.

Du vill också att DGML-redigeraren ska visa grafer i visualiseraren:

  1. Markera kryssrutan för DGML-redigeraren. Du hittar den under avsnittet Kodverktyg .

Oföränderlighet och .NET-kompilatorplattformen

Oföränderlighet är en grundläggande grundsats i .NET-kompilatorplattformen. Oföränderliga datastrukturer kan inte ändras när de har skapats. Oföränderliga datastrukturer kan delas och analyseras på ett säkert sätt av flera konsumenter samtidigt. Det finns ingen risk att en konsument påverkar en annan på oförutsägbara sätt. Analysatorn behöver inte lås eller andra samtidighetsåtgärder. Den här regeln gäller för syntaxträd, kompileringar, symboler, semantiska modeller och alla andra datastrukturer som du stöter på. I stället för att ändra befintliga strukturer skapar API:er nya objekt baserat på angivna skillnader till de gamla. Du använder det här konceptet för syntaxträd för att skapa nya träd med transformeringar.

Skapa och transformera träd

Du väljer en av två strategier för syntaxtransformeringar. Fabriksmetoder används bäst när du söker efter specifika noder som ska ersättas eller specifika platser där du vill infoga ny kod. Skrivmaskiner är bäst när du vill söka igenom ett helt projekt efter kodmönster som du vill ersätta.

Skapa noder med fabriksmetoder

Den första syntaxomvandlingen visar fabriksmetoderna. Du kommer att ersätta en using System.Collections; instruktion med en using System.Collections.Generic; instruktion. Det här exemplet visar hur du skapar Microsoft.CodeAnalysis.CSharp.CSharpSyntaxNode objekt med hjälp av fabriksmetoderna Microsoft.CodeAnalysis.CSharp.SyntaxFactory . För varje typ av nod, token eller trivia finns det en fabriksmetod som skapar en instans av den typen. Du skapar syntaxträd genom att skapa noder hierarkiskt nedifrån och upp. Sedan transformerar du det befintliga programmet genom att ersätta befintliga noder med det nya träd som du har skapat.

Starta Visual Studio och skapa ett nytt C# Stand-Alone Code Analysis Tool-projekt . I Visual Studio väljer du Arkiv>Nytt>projekt för att visa dialogrutan Nytt projekt. Under Visual C#>Extensibility väljer du ett Stand-Alone kodanalysverktyg. Den här snabbstarten har två exempelprojekt, så ge lösningen namnet SyntaxTransformationQuickStart och ge projektet namnet ConstructionCS. Klicka på OK.

Det här projektet använder klassmetoderna Microsoft.CodeAnalysis.CSharp.SyntaxFactory för att konstruera en Microsoft.CodeAnalysis.CSharp.Syntax.NameSyntax som representerar System.Collections.Generic namnområdet.

Lägg till följande using direktiv överst i Program.cs.

using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static System.Console;

Du skapar namnsyntaxnoder för att bygga trädet som representerar using System.Collections.Generic;-instruktionen. NameSyntax är basklassen för fyra typer av namn som visas i C#. Du skapar dessa fyra typer av namn tillsammans för att skapa ett namn som kan visas på C#-språket:

Du använder IdentifierName(String) metoden för att skapa en NameSyntax nod. Lägg till följande kod i din Main metod i Program.cs:

NameSyntax name = IdentifierName("System");
WriteLine($"\tCreated the identifier {name}");

Föregående kod skapar ett IdentifierNameSyntax objekt och tilldelar det till variabeln name. Många av Roslyn-API:erna returnerar basklasser för att göra det enklare att arbeta med relaterade typer. Variabeln name, en NameSyntax, kan återanvändas när du skapar QualifiedNameSyntax. Använd inte typinferens när du skapar exemplet. Du automatiserar det steget i det här projektet.

Du har skapat namnet. Nu är det dags att skapa fler noder i trädet genom att skapa en QualifiedNameSyntax. Det nya trädet använder name som vänster sida av namnet och ett nytt IdentifierNameSyntax för Collections namnområdet som höger sida av QualifiedNameSyntax. Lägg till följande kod i program.cs:

name = QualifiedName(name, IdentifierName("Collections"));
WriteLine(name.ToString());

Kör koden igen och se resultatet. Du skapar ett träd med noder som representerar kod. Du fortsätter det här mönstret för att skapa QualifiedNameSyntax för namnområdet System.Collections.Generic. Lägg till följande kod i Program.cs:

name = QualifiedName(name, IdentifierName("Generic"));
WriteLine(name.ToString());

Kör programmet igen för att se att du har skapat trädet för koden som ska läggas till.

Skapa ett modifierat träd

Du har skapat ett litet syntaxträd som innehåller en sats. API:erna för att skapa nya noder är rätt val för att skapa enkla instruktioner eller andra små kodblock. Men om du vill skapa större kodblock bör du använda metoder som ersätter noder eller infogar noder i ett befintligt träd. Kom ihåg att syntaxträd är oföränderliga. Syntax-API:et tillhandahåller ingen mekanism för att ändra ett befintligt syntaxträd efter konstruktionen. I stället tillhandahåller den metoder som skapar nya träd baserat på ändringar i befintliga träd. With* metoder definieras i konkreta klasser som härleds från SyntaxNode eller i tilläggsmedlemmar som deklareras i SyntaxNodeExtensions klassen. Dessa metoder skapar en ny nod genom att tillämpa ändringar i en befintlig nods underordnade egenskaper. Dessutom ReplaceNode kan tilläggsmedlemmen användas för att ersätta en underordnad nod i ett underträd. Den här metoden uppdaterar även föräldern så att den pekar på det nyligen skapade barnet och upprepar processen uppåt i hela trädet – en process som kallas att generera om trädet.

Nästa steg är att skapa ett träd som representerar ett helt (litet) program och sedan ändra det. Lägg till följande kod i början av Program klassen:

        private const string sampleCode =
@"using System;
using System.Collections;
using System.Linq;
using System.Text;

namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(""Hello, World!"");
        }
    }
}";

Anmärkning

Exempelkoden använder System.Collections namnområdet och inte System.Collections.Generic namnområdet.

Lägg sedan till följande kod längst ned i Main metoden för att parsa texten och skapa ett träd:

SyntaxTree tree = CSharpSyntaxTree.ParseText(sampleCode);
var root = (CompilationUnitSyntax)tree.GetRoot();

I det här WithName(NameSyntax) exemplet används metoden för att ersätta namnet i en UsingDirectiveSyntax nod med den som konstruerades i den föregående koden.

Skapa en ny UsingDirectiveSyntax nod med hjälp av WithName(NameSyntax) metoden för att uppdatera System.Collections namnet med det namn som du skapade i föregående kod. Lägg till följande kod längst ned i Main metoden:

var oldUsing = root.Usings[1];
var newUsing = oldUsing.WithName(name);
WriteLine(root.ToString());

Kör programmet och titta noga på utdata. newUsing har inte placerats i rotträdet. Det ursprungliga trädet har inte ändrats.

Lägg till följande kod med hjälp av ReplaceNode tilläggsmetoden för att skapa ett nytt träd. Det nya trädet är resultatet av att ersätta den befintliga importen med den uppdaterade newUsing noden. Du tilldelar det här nya trädet till den befintliga root variabeln:

root = root.ReplaceNode(oldUsing, newUsing);
WriteLine(root.ToString());

Kör programmet igen. Den här gången importerar trädet nu System.Collections.Generic-namnområdet korrekt.

Transformera träd med hjälp av SyntaxRewriters

Metoderna With* och ReplaceNode ger praktiska sätt att transformera enskilda grenar i ett syntaxträd. Klassen Microsoft.CodeAnalysis.CSharp.CSharpSyntaxRewriter utför flera transformeringar i ett syntaxträd. Klassen Microsoft.CodeAnalysis.CSharp.CSharpSyntaxRewriter är en underklass av Microsoft.CodeAnalysis.CSharp.CSharpSyntaxVisitor<TResult>. CSharpSyntaxRewriter Tillämpar en transformering på en viss typ av SyntaxNode. Du kan använda transformeringar för flera typer av SyntaxNode objekt var de än visas i ett syntaxträd. Det andra projektet i den här snabbstarten skapar en kommandoradsrefaktorisering som tar bort explicita typer i lokala variabeldeklarationer var som helst där typinferens kan användas.

Skapa ett nytt C# Stand-Alone Code Analysis Tool-projekt . Högerklicka på lösningsnoden i SyntaxTransformationQuickStart Visual Studio. Välj Lägg till>nytt projekt för att visa dialogrutan Nytt projekt. Under Visual C#>Utökningsbarhet väljer du Stand-Alone Kodanalysverktyg. Ge projektet TransformationCS ett namn och klicka på OK.

Det första steget är att skapa en klass som härleds från CSharpSyntaxRewriter för att utföra dina transformeringar. Lägg till en ny klassfil i projektet. I Visual Studio väljer du Project>Add Class.... I dialogrutan Lägg till nytt objekt skriver du TypeInferenceRewriter.cs som filnamn.

Lägg till följande using direktiv i TypeInferenceRewriter.cs filen:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

Gör sedan att TypeInferenceRewriter klassen utökar CSharpSyntaxRewriter klassen:

public class TypeInferenceRewriter : CSharpSyntaxRewriter

Lägg till följande kod för att deklarera ett privat skrivskyddat fält för att lagra ett SemanticModel och initiera det i konstruktorn. Du behöver det här fältet senare för att avgöra var typinferens kan användas:

private readonly SemanticModel SemanticModel;

public TypeInferenceRewriter(SemanticModel semanticModel) => SemanticModel = semanticModel;

Åsidosätt VisitLocalDeclarationStatement(LocalDeclarationStatementSyntax) metoden:

public override SyntaxNode VisitLocalDeclarationStatement(LocalDeclarationStatementSyntax node)
{

}

Anmärkning

Många av Roslyn-API:erna deklarerar returtyper som är basklasser för de faktiska körningstyper som returneras. I många scenarier kan en typ av nod ersättas av en annan typ av nod helt eller till och med tas bort. I det här exemplet VisitLocalDeclarationStatement(LocalDeclarationStatementSyntax) returnerar metoden en SyntaxNode, i stället för den härledda typen av LocalDeclarationStatementSyntax. Den här omskrivaren returnerar en ny LocalDeclarationStatementSyntax-nod baserad på den befintliga.

Den här snabbstarten hanterar lokala variabeldeklarationer. Du kan utöka den till andra deklarationer som foreach loopar, for loopar, LINQ-uttryck och lambda-uttryck. Dessutom omvandlar den här omskrivaren endast deklarationer av den enklaste formen.

Type variable = expression;

Om du vill utforska på egen hand kan du överväga att utöka det färdiga exemplet för dessa typer av variabeldeklarationer:

// Multiple variables in a single declaration.
Type variable1 = expression1,
     variable2 = expression2;
// No initializer.
Type variable;

Lägg till följande kod i brödtexten i VisitLocalDeclarationStatement metoden för att hoppa över att skriva om dessa typer av deklarationer:

if (node.Declaration.Variables.Count > 1)
{
    return node;
}
if (node.Declaration.Variables[0].Initializer == null)
{
    return node;
}

Metoden anger att ingen omskrivning sker genom att parametern node returneras oförändrad. Om inget av dessa if uttryck är sant representerar noden en möjlig deklaration med initiering. Lägg till dessa instruktioner för att extrahera typnamnet som anges i deklarationen och binda det med hjälp av SemanticModel fältet för att hämta en typsymbol:

var declarator = node.Declaration.Variables.First();
var variableTypeName = node.Declaration.Type;

var variableType = (ITypeSymbol)SemanticModel
    .GetSymbolInfo(variableTypeName)
    .Symbol;

Lägg nu till den här instruktionen för att binda initieringsuttrycket:

var initializerInfo = SemanticModel.GetTypeInfo(declarator.Initializer.Value);

Lägg slutligen till följande if instruktion för att ersätta det befintliga typnamnet med nyckelordet var om typen av initialiseraruttryck matchar den angivna typen:

if (SymbolEqualityComparer.Default.Equals(variableType, initializerInfo.Type))
{
    TypeSyntax varTypeName = SyntaxFactory.IdentifierName("var")
        .WithLeadingTrivia(variableTypeName.GetLeadingTrivia())
        .WithTrailingTrivia(variableTypeName.GetTrailingTrivia());

    return node.ReplaceNode(variableTypeName, varTypeName);
}
else
{
    return node;
}

Villkoret krävs eftersom deklarationen kan omvandla initialiseraruttrycket till en basklass eller ett gränssnitt. Om det önskas matchar inte typerna på vänster och höger sida av tilldelningen. Om du tar bort den explicita typen i dessa fall ändras semantiken för ett program. var anges som en identifierare i stället för ett nyckelord eftersom var det är ett kontextuellt nyckelord. Inledande och avslutande trivia (tomt utrymme) överförs från det gamla typnamnet till nyckelordet var för att upprätthålla lodrätt tomt utrymme och indrag. Det är enklare att använda ReplaceNode i stället för With* för att transformera LocalDeclarationStatementSyntax eftersom typnamnet faktiskt är en underordnad del av deklarationssatsen.

Du har slutfört TypeInferenceRewriter. Gå tillbaka till Program.cs filen för att slutföra exemplet. Skapa ett test Compilation och hämta SemanticModel från det. Använd det SemanticModel för att prova din TypeInferenceRewriter. Du gör det här steget sist. Under tiden deklarerar du en platshållarvariabel som representerar din testkompilering:

Compilation test = CreateTestCompilation();

När du har pausat en stund bör du se en felmarkering som visar att det inte finns någon CreateTestCompilation metod. Tryck på Ctrl+Period för att öppna glödlampan och tryck sedan på Retur för att anropa kommandot Generera metodstub . Det här kommandot genererar en metodstub för CreateTestCompilation -metoden i Program klassen. Du kommer tillbaka för att fylla i den här metoden senare:

C# Generera metod från användning

Skriv följande kod för att iterera över var och en SyntaxTree i testet Compilation. För var och en initierar du en ny TypeInferenceRewriter med SemanticModel för det trädet:

foreach (SyntaxTree sourceTree in test.SyntaxTrees)
{
    SemanticModel model = test.GetSemanticModel(sourceTree);

    TypeInferenceRewriter rewriter = new TypeInferenceRewriter(model);

    SyntaxNode newSource = rewriter.Visit(sourceTree.GetRoot());

    if (newSource != sourceTree.GetRoot())
    {
        File.WriteAllText(sourceTree.FilePath, newSource.ToFullString());
    }
}

I -instruktionen foreach som du skapade lägger du till följande kod för att utföra transformeringen på varje källträd. Den här koden skriver villkorligt ut det nya transformerade trädet om några ändringar har gjorts. Din omskrivare bör bara ändra ett träd om den stöter på en eller flera lokala variabeldeklarationer som kan förenklas med hjälp av typinferens.

SyntaxNode newSource = rewriter.Visit(sourceTree.GetRoot());

if (newSource != sourceTree.GetRoot())
{
    File.WriteAllText(sourceTree.FilePath, newSource.ToFullString());
}

Du bör se vågiga linjer under koden File.WriteAllText. Välj glödlampan och lägg till den nödvändiga using System.IO; instruktionen.

Du är nästan klar! Det finns ett steg kvar: att skapa ett test Compilation. Eftersom du inte har använt typinferens alls under den här snabbstarten skulle det ha gjort ett perfekt testfall. Tyvärr ligger det utanför omfånget för den här genomgången att skapa en kompilering från en C#-projektfil. Men lyckligtvis, om du har följt instruktionerna noggrant, finns det hopp. Ersätt innehållet i CreateTestCompilation metoden med följande kod. Den skapar en testkompilering som av en tillfällighet matchar projektet som beskrivs i den här snabbstarten:

String programPath = @"..\..\..\Program.cs";
String programText = File.ReadAllText(programPath);
SyntaxTree programTree =
               CSharpSyntaxTree.ParseText(programText)
                               .WithFilePath(programPath);

String rewriterPath = @"..\..\..\TypeInferenceRewriter.cs";
String rewriterText = File.ReadAllText(rewriterPath);
SyntaxTree rewriterTree =
               CSharpSyntaxTree.ParseText(rewriterText)
                               .WithFilePath(rewriterPath);

SyntaxTree[] sourceTrees = { programTree, rewriterTree };

MetadataReference mscorlib =
        MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
MetadataReference codeAnalysis =
        MetadataReference.CreateFromFile(typeof(SyntaxTree).Assembly.Location);
MetadataReference csharpCodeAnalysis =
        MetadataReference.CreateFromFile(typeof(CSharpSyntaxTree).Assembly.Location);

MetadataReference[] references = { mscorlib, codeAnalysis, csharpCodeAnalysis };

return CSharpCompilation.Create("TransformationCS",
    sourceTrees,
    references,
    new CSharpCompilationOptions(OutputKind.ConsoleApplication));

Håll tummarna och kör projektet. I Visual Studio väljer du Felsökning>Starta felsökning. Du ska få en uppmaning från Visual Studio om att filerna i ditt projekt har ändrats. Klicka på "Ja till alla" för att läsa in de ändrade filerna igen. Undersök dem för att observera dina fantastiska egenskaper. Observera hur mycket renare koden ser ut utan alla explicita och redundanta typspecificerare.

Grattis! Du har använt API:er för kompilatorer för att skriva din egen refaktorisering som söker igenom alla filer i ett C#-projekt efter vissa syntaktiska mönster, analyserar semantiken för källkod som matchar dessa mönster och transformerar den. Nu är du officiellt en expert på refaktorisering!