Analiza kodu za pomocą LINQ to chyba esencja Roslyn. Bez Roslyn, kod był dla nas jak zwykły tekst (string) i w przypadku jakiejkolwiek analizy, musieliśmy sami parsować tekst i rozpoznawać odpowiednie fragmenty.
Najpierw tworzymy SyntaxTree za pomocą zwykłego tekstu:
SyntaxTree tree = CSharpSyntaxTree.ParseText(@" public class Class1 { public void TestMethod1() { } } public class Class2 { public void TestMethod2() { } } "); SyntaxNode root = tree.GetRoot();
Metoda DescendantNodes zwraca wszystkich potomków, również tych niebezpośrednich. Jeśli chcemy zwrócić wszystkie klasy wystarczy szukać węzłów typu ClassDecelarionSyntax:
foreach (ClassDeclarationSyntax classDeclarationSyntax in root.DescendantNodes().OfType<ClassDeclarationSyntax>()) { Console.WriteLine(classDeclarationSyntax.Identifier); }
Analogicznie, w celu wyświetlenia metod wystarczy:
foreach(ClassDeclarationSyntax classDeclarationSyntax in root.DescendantNodes().OfType<ClassDeclarationSyntax>()) { Console.WriteLine(classDeclarationSyntax.Identifier); foreach (var method in classDeclarationSyntax.DescendantNodes().OfType<MethodDeclarationSyntax>()) { Console.WriteLine("\t{0}",method.Identifier); } }
Jak wspomniałem, DescendantNodes zwraca wszystkich potomków więc możemy wyświetlić metody bezpośrednio korzystając z korzenia:
foreach (MethodDeclarationSyntax methodDeclarationSyntax in root.DescendantNodes().OfType<MethodDeclarationSyntax>()) { Console.WriteLine(methodDeclarationSyntax.Identifier); }
Jeśli chcemy tylko bezpośrednich potomków (czyli dzieci) wtedy korzystamy z ChildNodes:
foreach (MethodDeclarationSyntax methodDeclarationSyntax in root.ChildNodes().OfType<MethodDeclarationSyntax>()) { Console.WriteLine(methodDeclarationSyntax.Identifier); }
Zmodyfikujmy trochę kod, aby zawierał metodę z parametrami:
SyntaxTree tree = CSharpSyntaxTree.ParseText(@" public class Class1 { public void TestMethod1() { } public void TestMethod1(string a, string b, int c) { } } ");
Zapytanie zwracające wszystkie metody, które zawierają parametry to:
SyntaxNode root = tree.GetRoot(); MethodDeclarationSyntax methodWithPars = root.DescendantNodes().OfType<MethodDeclarationSyntax>().First(n => n.ParameterList.Parameters.Any()); Console.WriteLine(methodWithPars.Identifier);
Wyświetlenie parametrów sprowadza się do prostej pętli:
foreach (var par in methodWithPars.ParameterList.Parameters) { Console.WriteLine(par.ToString()); }
Często potrzebujemy pójść w drugą strony, to znaczy mamy wskaźnik na potomka, ale chcemy znaleźć rodzica. Do dyspozycji mamy metodę Ancestors:
foreach (var par in methodWithPars.ParameterList.Parameters) { string className = par.Ancestors().OfType<ClassDeclarationSyntax>().Single().Identifier.ToString(); Console.WriteLine("{0}:{1}",className,par.ToString()); }
Za pomocą LINQ możemy uzyskać dostęp do każdego fragmentu kodu, nie tylko deklaracji. Możemy np. wylistować IF”Y:
SyntaxTree tree = CSharpSyntaxTree.ParseText(@" public class Class1 { public void TestMethod1() { } public void TestMethod1(string a, string b, int c) { if(c==5 { } } } "); SyntaxNode root = tree.GetRoot(); var ifStatements = root.DescendantNodes().OfType<IfStatementSyntax>(); foreach (var ifStatementSyntax in ifStatements) { Console.WriteLine(ifStatementSyntax.ToString()); }
Analogicznie sprawa wygląda z wywołaniami metod, klauzulami using, switch\case, pętlami, zwracaniem wartości jak i z KAŻDYM elementem kodu. Innymi słowy drzewo jest na tyle szczegółowe, że “bez problemu” możemy napisać konwerter z C# na jakikolwiek inny język np. JavaScript, bez konieczności ręcznego parsowania kodu C#.