Versie: 1.0
Auteur: Maarten Grootoonk (LinkedIn)
Bio: Maarten Grootoonk is een Software Engineer en trainer bij Ordina die zich specialiseert in het Microsoft landschap. Zo ontwikkelt hij .NET applicaties voor de klant en geeft hij verschillende Azure trainingen en workshops.
Artikel
Introductie
Bij het ontwikkelen van software worden we vaak geholpen door integrated development environments (IDE’s) zoals Visual Studio. Visual Studio geeft ons bijvoorbeeld suggesties met de gedefinieerde classes en methodes in de vorm van IntelliSense. Ook kunnen IDE’s vaak helpen met code genereren,verplaatsen en refactoren.
Om deze suggesties te kunnen geven en code acties uit te kunnen voeren moet de IDE de geschreven code begrijpen. Dit wordt meestal gedaan met statische code analyse. Hierbij wordt de code ingelezen en omgevormd naar een abstract syntax tree, afgekort AST.
Een AST bevat een schematische boom van onze gedefinieerde classes, methodes en variabelen, maar bevat deze ook informatie over waar in het bestand de code staat die bij een Node hoort. Zo kun je precies herleiden in welke rij en kolom van het bestand een methode of variabele is gedefinieerd.
Aan de hand van deze informatie kun je zelf programma’s maken om code te controleren en/of suggesties te tonen. Ook kun je aan de hand van een AST automatisch code genereren en aanpassen. Zo kun je repetitieve zaken geautomatiseerd aanpakken zonder bang te hoeven zijn voor knip en plak fouten.
Kortom, AST’s helpen jou met het automatiseren van repetitief werk en het gebruik ervan voorkomt fouten die kunnen ontstaan door handmatig code schrijven.
Aan de slag
Nu denk je misschien, hoe ziet zo’n AST er nou uit en hoe werk ik ermee? In dit segment gaan we kijken naar tools in het Microsoft landschap die jou bij deze twee vraagstukken helpen.
In het Microsoft landschap is Roslyn de open-source C# en Visual Basic compiler. Roslyn biedt naast compilers ook APIs om met deze compilers te werken. Zo is er een API om code om te zetten naar een AST. Roslyn kan als C# library aan .NET projecten worden toegevoegd.
Naast de Roslyn C# library zijn er componenten voor Visual Studio om het werken met Roslyn makkelijker te maken. Zo is er de Syntax Graph en Syntax Visualiser om AST’s te visualiseren. Beide kunnen geïnstalleerd door in de Visual Studio Installer onder het “Individual Components” tabblad de volgende componenten te installeren:
De eerste AST
Voor onze eerste introductie met een AST, beginnen we met het bekijken van een stuk C# code in Visual Studio. Naast de standaard editor is Syntax Graph geopend, deze visualiseert de AST van de geschreven code. Als je wijzigingen maakt aan de code is dit meteen in de AST visualisatie terug te zien.
De AST bestaat uit Nodes, elk van deze Nodes representeert een deel van de code. Zo is er een ClassDeclaration welke een class definitie representeert en is er een MethodDeclaration die een methode representeert. Zoals de naam “Abstract Syntax Tree” doet vermoeden zijn deze Nodes gestructureerd als takken van een boom.
Dit betekent dat elke Node eigen Nodes onder zich kan hebben. Deze worden in de Members property bijgehouden. Bij een C# programma is de CompilationUnit de root Node, helemaal bovenaan de AST. In het code voorbeeld heeft de CompilationUnit Node ook weer Nodes onder zich zoals ClassDeclaration en deze heeft op zijn beurt dan weer MethodDeclaration Nodes onder zich.
Gegevens uit de AST halen
Nu we hebben gezien hoe een AST is opgebouwd, gaan we kijken hoe we er informatie uit kunnen halen. Omdat de AST als een boom met takken is gestructureerd kunnen we namelijk niet zomaar door een lijst van alle Nodes lopen. We moeten er doorheen navigeren.
Voordat we kijken naar de best practice voor het doorlopen van een AST, kijken we eerst naar een basisopzet. In deze opzet construeren we een AST en halen we de namen van de gedefinieerde classes op. Het opstellen van de AST kan met CSharpSyntaxTree.ParseText.
In bovenstaande voorbeeld bouwen we een AST op basis van C# code in de programText. De ParseText methode bouwt de AST op en geeft de Node bovenaan de boom terug. Dit is bij C# programma’s de CompilationUnit Node. Onder deze Node vallen alle andere Nodes van het programma.
Via de Members property van de CompilationUnit Node kunnen we de directe kinderen van de Node uitlezen. In dit geval is dat een ClassDeclaration Node, deze Node heeft de Identifier property waarmee we de naam van de class kunnen uitlezen.
Omdat de Members property van een Node alleen zijn directe kinderen bevat, vinden we alleen ClassDeclarations. Als we ook de MethodDeclaration willen ophalen, moeten we ook door de Members van elke ClassDeclaration lopen. Dit zou er als volgt uit zien:
In dit voorbeeld valt het op dat we voor wat basisgegevens ophalen al veel code moeten schrijven. Ook maakt deze code veel aannames, zo werkt deze logica momenteel alleen als het programma geen namespace declaratie bevat. Dit zou namelijk betekenen dat de NamespaceDeclaration het enige kind is van CompilationUnit, de ClassDeclaration’s zijn namelijk dan weer kinderen van NamespaceDeclaration.
Ook gaat het voorbeeldprogramma maar tot twee niveau’s diep. Als de MethodDeclaration kinderen zou hebben worden deze nooit langsgelopen omdat hiervoor een extra for-loop voorgeschreven moet worden.
Kortom, de AST doorlopen met for loops is vaak foutgevoelig en maakt je code lastiger te begrijpen, onderhouden en uit te breiden. Om dit probleem te verhelpen levert de Roslyn library de CSharpSyntaxWalker class aan. Deze class loopt voor jou door de AST en laat je gemakkelijk gegevens uit de AST halen.
Werken met CSharpSyntaxWalker
Door een eigen class de CSharpSyntaxWalker class te laten implementeren kun je aan de hand van het Visitor design pattern door de AST lopen. Bij het Visitor design pattern worden Nodes in een Tree data structuur doorgelopen, elke keer als een Node wordt bezocht wordt de Visit methode aangeroepen.
Bij het schrijven van logica kunnen we zelf een Visit methode implementeren. Zo kunnen we een VisitClassDeclaration methode implementeren die aangeroepen wordt als de CSharpSyntaxWalker een ClassDeclaration “visit” in de AST.
Bovenstaande code bereikt hetzelfde doel als het eerste voorbeeld met de for loops. Echter is de code met CSharpSyntaxWalker flexibeler doordat er geen aannames worden gedaan over de structuur van het programma. Hierdoor blijft analyse logica werken ook al worden ClassDeclaration’s of MethodDeclarations verplaatst in de te analyseren code.
Gegevens van Nodes bijhouden met CSharpSyntaxWalker
Nu we met de CSharpSyntaxWalker door de AST kunnen lopen gaan we bezig met het schrijven van analyse logica. Hiervoor willen we informatie uit de AST verzamelen, doordat de DemoASTWalker een standaard class is kunnen we met een standaard C# constructie gegevens verzamelen.
In onderstaande voorbeeld maken we gebruik van een Data Transfer Object (DTO) om waardes bij te houden. Deze DTO geven we door in de constructor en slaan we op in het _results veld. In de analyse logica zoals VisitClassDeclaration kunnen we waardes schrijven naar de DTO.
In onderstaand stuk code geven we de referentie naar de NodeCountResult instantie mee in de constructor. Vervolgens lopen we door AST met de ASTWalker via walker.Visit(root), tijdens het doorlopen wordt de DTO gevuld.
Hierna kunnen de resultaten via de countResult object referentie worden uitgelezen. In dit geval bewaren we een numerieke waarde, maar dit kunnen ook complexe types zoals IEnumerable zijn.
Aan de hand van deze opzet met de CSharpSyntaxWalker en de DTO kun je zelf analyse logica schrijven. Deze logica kan zo simpel of uitgebreid zijn als je zelf wilt.
Code aanpassen via CSharpSyntaxRewriter
Nu we hebben gezien hoe we met CSharpSyntaxWalker C# programma’s kunnen analyseren, gaan we kijken hoe we bestaande code kunnen aanpassen met Roslyn. In de voorbeelden gaan we logica schrijven om private velden in classes altijd met een underscore te laten starten.
Terwijl het mogelijk is om dit soort code wijzigingen met “zoeken en vervangen” of regex te doen, kan dit resulteren in code die niet compileert of ongevraagde code wijzigingen. In onderstaand voorbeeld is te zien dat ook het commentaar wordt aangepast terwijl we alleen de class velden willen aanpassen.
Omdat Roslyn semantisch de code begrijpt weet Roslyn het verschil tussen een class veld en een stuk commentaar. Hierdoor kun je veilig geautomatiseerd code aanpassen met veel minder kans op onjuiste resultaten. Om dit te demonstreren gaan we met de CSharpSyntaxRewriter class van Roslyn private velden automatisch prefixen met een underscore. Hieronder is het gewenste verschil uitgewerkt:
De CSharpSyntaxRewriter lijkt in veel opzichten op de CSharpSyntaxWalker, zo hebben ze beide methodes zoals VisitVariableDeclarator die je kan overschrijven. Het grootste verschil is dat je bij CSharpSyntaxRewriter een aangepaste versie van een AST Node kan teruggeven. Deze bijgewerkte Node wordt dan in de AST geplaatst en zorgt uiteindelijk voor de gevraagde code-wijzigingen. Hieronder is een voorbeeld implementatie voor het toevoegen van underscores uitgewerkt:
In bovenstaand voorbeeld lopen we door alle field declarations van de ingelezen code heen. Per field declaration kijken we of deze private is, en als de declaration private is, halen we de eerste Variable Declaration binnen die Field Declaration op.
Hierbij kijken we of de huidige veldnaam al een underscore heeft. Als dit niet het geval is, voegen we een underscore toe aan de veldnaam. Hierbij is het goed om te weten dat Nodes in een AST niet direct aangepast kunnen worden. We moeten een kopie maken van de Node en deze aanpassen.
Om dit proces te versimpelen levert Roslyn verschillende With… Methodes aan. Met deze methodes kun je makkelijk een kopie van de Node maken en deze kopie aanpassen. In dit voorbeeld vervangen we de Identifier met de nieuwe naam van de field declaration.
Als laatste stap vervangen we de oude Node zonder underscore met de nieuwe Node die wel een underscore in de naam heeft. Dit doen we door op de hoofd node(FieldDeclaration) de ReplaceNode methode aan te roepen. Deze methode vervangt de node zelf of een van z’n onderliggende Nodes.
AST omzetten naar Code.
Nu we hebben gezien hoe code aangepast kan worden gaan we kijken hoe je een AST omzet naar code. Dit kan worden gedaan door de ToFullString methode aan te roepen op de Node die Visit() terug geeft:
Hierin is te zien dat de namen van de private velden zijn aangepast en de velden die al een underscore hadden niet zijn aangepast. Dit is precies het gewenste resultaat, ook weten we zeker dat we geen onjuiste resultaten krijgen omdat Roslyn de semantiek van de code begrijpt.
Zelf aan de slag
We hebben nu gezien hoe je met de Roslyn API analyses kan uitvoeren en bestaande code kan aanpassen. In de voorbeelden zijn een aantal basis scenario’s uitgewerkt om een idee te geven wat je met Roslyn kan. Om meer te weten te komen over Roslyn en de API is het volgende artikel een goed startpunt: https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/.