W poprzednim wpisie omówiłem w skrócie kompilator Roslyn i to jakie problemy on rozwiązuje.
W poprzednim wpisie też napisaliśmy proste rozszerzenie do Visual Studio, które korzysta z kompilatora Roslyn.
Zanim jednak zaczniemy zabawę na całego z każdym API kompilatora Roslyn, trzeba zacząć od podstaw. Taką podstawą jest API Syntax Trees .
Okno “Roslyn Syntax Visualizer” jest dobrym narzędziem do nauki składni takich wyrażeń na tle prawdziwego kodu napisanego w C#.
Czym są więc te wyrażenia drzewiaste?
Są one reprezentacją kodu źródłowego. Kod źródłowy według tego API jest sekwencją tokenów. Każdy token określa inną część języka. Przykładowo, jak widać na obrazku powyżej słowo kluczowe “new” jest tokenem “New Keyword”.
Tokeny więc reprezentują wiele elementów języka. Jak:
- Identyfikatory: System, Console, WriteLine, x
- Słowa kluczowe: new, static, class, foreach
- Operatory: =, + , -
- Punktacje: . , ;
Są też elementy “Trivia” , które nie mają znaczenia dla działania programu. Są to:
- Komentarze
- Białe znaki: Spacje, Tabulacje .itp
Oznaczone są one kolorem czerwonym.
Skoro już mówimy o kolorach. To kolor niebieski określa “gałąź” jak n.p lista możliwych atrybutów. Kolor zielony określa jeden token.
Po co to wszystko?.
Otóż przykładowo, jeśli wystąpi błąd, to wiemy dokładnie, w którym miejscu. Same API potrafi zasugerować jaki token tam powinien się znajdować.
Tokeny i wyrażenia drzewiaste Roslyn są też bardzo pomocne przy transformacji tekstowej. Gdzie nasze tokeny zmieniają kolejność bądź znaczenie.
W oknie Roslyn Syntax Visualizer możemy zauważyć jak API wyłapuje błędy w składni. API działa dokładnie tak samo, jak kompilator działający w tle, ale nie powinno nas to dziwić ponieważ właśnie to API jest wtedy używane.
Potrafi ono także sugerować czego brakuje w obecnej gałęzi. Na różowo są podkreślone wyrażenia, które są błędne lub niekompletne. Potrafi ono określić, gdzie dokładnie występuje błąd.
Warto tutaj zaznaczyć, że mówimy o API badającej składnie kodu. Pewne błędy można sprawdzić dopiero po procesie kompilacji.
W API Syntax Tree nie występuje żadna kompilacja kodu.
Warto wracać do tego okna, gdyż nikt nie będzie pamiętać, jaki element kodu, jaką jest klasą w Syntax Tree API.
Syntax Tree API jest więc narzędziem do analizy kodu. Używając tego API jesteśmy w stanie przeanalizować kod na kilka sposobów:
- “Visitors” . Klasy, które spacerują po danym wyrażeniu drzewiastym reprezentującym kod, szukając odpowiednich rzeczy.
- Używając wyrażeń i zapytań jak w przypadku LINQ do XML.
- Używając pętl foreach.
- Używając modelu obiektowego: Member foreach
Wyrażenia drzewiaste są niezmienne. Gdy zostaną one raz utworzone nie ulegają zmianie. Ma to dużo zalet ponieważ nie musimy się martwić o nasz kod, jeśli działa on wielowątkowo. Nie musimy używać żadnych locków itp.
Skoro wyrażenia drzewiaste są niezmienne oznacza to, że jedno wyrażenie może być użyte ponownie przy edycji, które utworzy tak naprawdę nowe wyrażenie.
Syntax Tree API służy więc do dwóch rzeczy, do:
- Diagnozowania kodu
- Refactoryzowania kodu
Skanowanie kodu źródłowego
Przejdźmy więc do przykładu. Do prezentacji Syntax Tree API użyje prostej aplikacji konsolowej.
Początki mogą być trudne. W końcu jak zdobyć biblioteki potrzebne do zabawy. Można to zrobić na dwa sposoby. Można utworzyć swoją aplikację i pobrać z NuGet bibliotekę “Microsoft.CodeAnalysis”.
Obecnie wszystkie biblioteki związane z Roslyn są w wersji beta. Miejmy nadzieję, że kod nie zmieni się za bardzo, gdy stanie się oficjalny.
Można też pójść na skróty i utworzyć aplikację konsolową według wzorca “Compiler Platform Console Application”. Projekt ten automatycznie ma dodane odpowiednie biblioteki, więc nie trzeba grzebać w NuGet.
Mam więc aplikację konsolową z odpowiednimi referencjami.
W projekcie znajduje się plik tekstowy z kodem. Oto co w nim się znajduje.
using System; class Hello { static public void Write() { Console.WriteLine("Hello");} static public void Main(string[] prams){Write();}}
Języki programowania obsługiwane przez kompilator Roslyn posiadają oddzielne klasy do zarządzania wyrażeniami drzewiastymi.
W C# jest to klasa “CSharpSyntaxTree”. Dziedziczy ona po klasie abstrakcyjnej SyntaxTree. Gdybyś więc chciał napisać swój własny język programowania, musiałbyś zacząć od zbudowania swojego Syntax Tree API.
Klasa “CSharpSyntaxTree” posiada dwie metody. W metodzie Create musimy podać główną gałąź wyrażenia drzewiastego. W metodzie ParseText musimy przekazać tekst, który jest kodem napisanym w C#.
Kod poniżej przeanalizuje kod zapisany w “Code.txt”. Później używając pętli foreach wyświetlę odpowiednie gałęzie i tokeny z gałęzi głównej utworzonej na podstawie parsowania pliku tekstowego.
static void Main(string[] args)
{
string code = "";
using (StreamReader sr = new StreamReader("Code.txt"))
{
code = sr.ReadToEnd();
}
var tree = CSharpSyntaxTree.ParseText(code);
var node = tree.GetRoot();
foreach (SyntaxNode child in node.ChildNodesAndTokens())
{
PrintSyntaxTree2(child);
}
Console.ReadKey();
}
Obecnie w Visual Studio 2015 Preview występuje pewien bug. API działa poprawnie jednak w Visual Studio nie można podejrzeć składni drzewa. Z jakiegoś powodu Visual Studio kompiluje główną gałąź i wyrzuca wyjątkiem.
Samo API nie wymaga żadnej kompilacji, z jakiegoś jednak powodu tooltip w Visual Studio kompiluje kod w wyrażeniu drzewiastym.
Postanowiłem to zignorować i przejść dalej.
W metodzie PrintSyntaxTree mogę sprawdzić, czy obecny element (SyntaxNode) jest gałęzią czy tokenem.
static void PrintSyntaxTree(SyntaxNode node)
{
if (node != null)
{
foreach (var item in node.ChildNodesAndTokens())
{
if (item.IsNode)
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write("\n \n "+item+"\n \n");
}
if (item.IsToken)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.Write(" "+item);
}
Console.ForegroundColor = ConsoleColor.Yellow;
Console.Write(" ||| ");
}
}
}
Konsola przemieli więc drzewo i wyświetli je w taki sposób.
Gałęzie w Syntax Tree mają podobne metody do klasy XML z zestawu bibliotek LINQ to XML.
Używając metod rozszerzeniowych LINQ, mogę z głównej gałęzi pobrać wszystkie wyrażenia metody w niej występującej.
Później mając listę wyrażeń metod mogę wyświetlić ich nazwy.
var methdods = node.DescendantNodes(). OfType<MethodDeclarationSyntax>();
foreach (var item in methdods)
{
Console.WriteLine(item.Identifier.Text);
}
Analogicznie mogę też przefiltrować całą gałąź w poszukiwaniu deklaracji klas.
var classes = node.DescendantNodes().OfType<ClassDeclarationSyntax>();
foreach (var item in classes)
{
Console.WriteLine(item.Identifier.Text);
}
Mogę też pobrać pierwszą metodę, która ma jakiekolwiek parametry. A później z tej metody wyciągnąć typ zwracany.
var methods2 = node.DescendantNodes().OfType<MethodDeclarationSyntax>()
.Where(n => n.ParameterList.Parameters.Any()).First();
var containingType = methods2.Ancestors().OfType<TypeDeclarationSyntax>().First();
Możemy też utworzyć drzewo od zera i zapisać je potem w postacie tekstowej.
Używając klasy statycznej “SyntaxFactory” mogę utworzyć swoje własne drzewo. Prawdziwe schody zaczynają się, gdy próbuje utworzyć kod metody. Coś jak “Console.WriteLine”. Jest to trudniejsze niż się wydaje.
var classm = SyntaxFactory.ClassDeclaration("Hello").WithMembers(
SyntaxFactory.List<MemberDeclarationSyntax>(new[] {
SyntaxFactory.MethodDeclaration(
SyntaxFactory.PredefinedType(
SyntaxFactory.Token(SyntaxKind.VoidKeyword)),
"Say").WithBody(SyntaxFactory.Block())
})).NormalizeWhitespace();
Console.WriteLine(classm.ToString());
SyntaxNode i jego klasy pochodne
Sprawdzanie, czym jest tak naprawdę dany “SyntaxNode”, wydaje się być jednym z podstawowych działań w tym API.
Dlatego istnieje metoda rozszerzeniowa “CSharpKind” wraz z typem wyliczeniowym zawierającym wszystkie dostępne składnie. Wszystko po to, aby ułatwić to zadanie.
Pamiętasz jak mówiłem, że warto korzystać z okna Roslyn Syntax Visualizer.Teraz wiesz dlaczego. Nikt nie zapamięta jak dana część kodu się nazywa jako klasa.
Jak zapewne widzisz w niektórych tych przykładach operujemy na bardziej złożonych klasach niż “SyntaxNode”.
Domyślnie wyrażenie “GetRoot()” zwraca “SyntaxNode”. Klasa “SyntaxNode” reprezentuje wszystkie wyrażenia z każdego języka. Kod, który podajemy do drzewa nie musi być w całości klasą lub metodą.
Logicznie jest więc to, że metoda “GetRoot()” dla bezpieczeństwa zwraca typ ogólny.
Ja jednak wiem, że moje drzewo reprezentuje coś więcej niż fragment metody, czy deklarację jednej klasy. Taki element nazywa się “CompilationUnitSyntax”.
Klasa ta zawiera przydatne właściwości jak “Members” and Usings, które nie są oczywiście dostępne dla klasy bazowej “SyntaxNode”.
W następnym punkcie użyjemy klasy CompilationUnitSyntax. Omówimy także klasę Visitor.
Jak widać w kodzie każdy CSharpowy SyntaxNode może przyjmować klasę dziedziczącą po CSharpSyntaxVisitor.
Model obiektowy i klasy Visitors : Odwiedzajacy
Teraz, gdy wiemy, że główna gałąź jest klasą “CompilationUnitSyntax”, to mogę sprawdzić każdy z jej członów w sposób bardziej obiektowy.
Co może być członem na poziomie klasy CompilationUnitSyntax.
- Klasą
- Strukturą
- Typem Wyliczeniowym
Oto przykład pętli foreach, która sprawdzi każdy człon. Jeśli człon jest klasą to sprawdzę w niej metody. Jeśli metoda nazywa się “Write” to przypiszę ją do zmiennej “search“.
string code = "";
using (StreamReader sr = new StreamReader("Code.txt"))
{
code = sr.ReadToEnd();
}
var tree = CSharpSyntaxTree.ParseText(code);
var node = (CompilationUnitSyntax)tree.GetRoot();
MethodDeclarationSyntax search = null;
foreach (var item in node.Members)
{
if (item.CSharpKind() == SyntaxKind.ClassDeclaration)
{
var c = (ClassDeclarationSyntax)item;
foreach (var m in c.Members)
{
var method = (MethodDeclarationSyntax)m;
if (method.Identifier.Text == "Write")
{
search = method;
}
}
}
}
Cały zapis oczywiście mogę skrócić do wyrażenia LINQ ze słowami kluczowymi “form”.
var c2 = from member in node.Members.OfType<ClassDeclarationSyntax>()
from member2 in member.Members.OfType<MethodDeclarationSyntax>()
where member2.Identifier.Text == "Write"
select member2;
Trzeba jednak przyznać, że wyszukiwania poszczególnych elementów w drzewie wciąż jest trochę niewygodne.
Po pierwsze, wymaga to od programisty znajomości układu elementów składni kodu. Po drugie, nawet z zapisem w stylu LINQ całe wyrażenie szukające może wydawać się bardzo skomplikowane.
Dlatego mamy rozwiązanie alternatywne pod postacią klas “Visitor”.
Utworzenie swojego odwiedzającego jest bardzo proste. Nasza klasa musi dziedziczyć po klasie Visitor i nadpisać odpowiednią metodę. Jest dużo metod odwiedzających. Nie powinno nas to dziwić, gdyż możemy w ten sposób odwiedzić wszystkie elementy składniowe.
Jak chcę odwiedzić deklarację metody, więc przeciążam metodę “VisitMethodDeclaration”.
Oto klasa mojego odwiedzającego, która robi to samo, co wcześniej pętla foreach i wyrażenia LINQ.
public class MethodVisitor : CSharpSyntaxWalker
{
public override void VisitMethodDeclaration(MethodDeclarationSyntax node)
{
if (node.Identifier.Text == "Write")
{
Method = node;
}
base.VisitMethodDeclaration(node);
}
public MethodDeclarationSyntax Method { get; set; }
}
Użycie odwiedzającego wygląda tak.
string code = "";
using (StreamReader sr = new StreamReader("Code.txt"))
{
code = sr.ReadToEnd();
}
var tree = CSharpSyntaxTree.ParseText(code);
var node = (CompilationUnitSyntax)tree.GetRoot();
MethodVisitor m = new MethodVisitor();
m.Visit(node);
MethodDeclarationSyntax search = m.Method;
Kod rzeczywiście jest teraz bardziej przejrzystszy. Wynika to z tego, że nie muszę się interesować tym, jak mam przeszukiwać drzewo. Cała magia dzieje się za mnie w klasie “Visitor”.
To wszystko, co musisz wiedzieć od Syntax Tree API.
Na koniec jeszcze wspomnę, że każdy obiekt “SyntaxNode” posiada metodę “GetDiagnostics”, zwróci ona wszystkie błędy i ostrzeżenia, jeśli wystąpiły w kodzie. Są to jednak błędy i ostrzeżenia, które mogą być wykryte tylko przed kompilacją.