W poprzednich wpisach pokazałem jak przy użyciu wyrażeń drzewiastych wyciągać kod.
Jak jednak kod ten zmienić.
Do spacerów po kodzie używaliśmy klas opartych na wzorcu “Vistor”. W tym wypadku podobne rozwiązanie jest zalecane.
Chociaż na siłę można zmieniać wyrażenie drzewiaste jakby to były pliki XML. Jest to jednak trudne i kod taki nie jest czytelny.
Obecnie mam taki kod:
using System;
class Hello
{
static Logger logger = new Logger();
static public void Write()
{
if (true)
{
}
Console.WriteLine("Hello");
}
static public void Write(int a, string b)
{
Console.WriteLine("Hello"+a+b);
}
static public void Main(string[] prams)
{
Write();
}
}
Używając klas Rewrite chcę w jak najprostszy sposób dodać na początku i na końcu każdej metody wyrażenie Console.WriteLine .
Kod po tej operacji ma wyglądać tak. Warto też dodać, że chcę wyświetlić nazwę danej metody w wyrażeniu Console.WriteLine.
using System;
class Hello
{
static Logger logger = new Logger();
static public void Write()
{
Console.WriteLine(" Called Write");
if (true)
{
}
Console.WriteLine("Hello");
Console.WriteLine(" Ended Write");
}
static public void Write(int a, string b)
{
Console.WriteLine(" Called Write");
Console.WriteLine("Hello" + a + b);
Console.WriteLine(" Ended Write");
}
static public void Main(string[] prams)
{
Console.WriteLine(" Called Main");
Write();
Console.WriteLine(" Ended Main");
}
}
To jednak jeszcze nie koniec.
Pójdźmy na całość z programowaniem Aspektowym. Obecnie dynamicznie dodaje się wyrażenia logujące przy pomocy interceptorów Castle.Windsor lub płatnego wynalazku PostSharp.
Dlaczego więc nie zrobić tego przy pomocy API kompilatora Roslyn. Oto prosta klasa logująca tablicę parametrów.
public class Logger
{
public void Log(params object[] obj)
{
}
}
Chcę do kodu dodać wywołanie metody Log z klasy Logger na początku każdej metody. Do metody Log chce przekazać wszystkie parametry, które zostały przesłane do metody.
Ostatecznie więc kod ma wglądać tak.
using System;
class Hello
{
static Logger logger = new Logger();
static public void Write()
{
logger.Log();
Console.WriteLine(" Called Write");
if (true)
{
}
Console.WriteLine("Hello");
Console.WriteLine(" Ended Write");
}
static public void Write(int a, string b)
{
logger.Log(a, b);
Console.WriteLine(" Called Write");
Console.WriteLine("Hello" + a + b);
Console.WriteLine(" Ended Write");
}
static public void Main(string[] prams)
{
logger.Log(prams);
Console.WriteLine(" Called Main");
Write();
Console.WriteLine(" Ended Main");
}
}
Zacznijmy więc od klasy dodającej Console.WriteLine. Klasy zmieniające kod muszą dziedziczyć po klasie abstrakcyjnej CSharpSyntaxRewriter.
W zależności od tego, co chcemy nadpisać wybieramy odpowiednią metodę odwiedzającą.
Chcemy zmienić kod wewnątrz metody.
Dlatego spróbujemy uzyskać to zadanie nadpisującą metodę “VisitMethodDeclaration”.
class AddMethodsRewrite : CSharpSyntaxRewriter
{
public override SyntaxNode VisitMethodDeclaration(MethodDeclarationSyntax node)
{
StatementSyntax syn1 =
SyntaxFactory.ParseStatement
("Console.WriteLine(\" Called " + node.Identifier.Text + "\")\n; ");
StatementSyntax syn2 =
SyntaxFactory.ParseStatement
("Console.WriteLine(\" Ended " + node.Identifier.Text + "\")\n; ");
SyntaxList<StatementSyntax> blockWithNewStatements = node.Body.Statements;
blockWithNewStatements = blockWithNewStatements.Insert(0, syn1);
blockWithNewStatements = blockWithNewStatements.Add(syn2);
BlockSyntax newBlock = SyntaxFactory.Block(blockWithNewStatements);
MethodDeclarationSyntax newMethod = SyntaxFactory.MethodDeclaration
(node.AttributeLists, node.Modifiers, node.ReturnType,
node.ExplicitInterfaceSpecifier, node.Identifier, node.TypeParameterList,
node.ParameterList, node.ConstraintClauses,
newBlock,
node.ExpressionBody, node.SemicolonToken).NormalizeWhitespace();
var newNode = node.ReplaceNode(node, newMethod);
return base.VisitMethodDeclaration(newNode);
}
}
Co się tutaj dzieje? Na początku korzystając z metody SyntaxFactory “ParseStatement” tworzę dwa wyrażenia, które będę chciał dodać. Nazwa danej metody znajduje się w zmiennej node.Identifier.Text.
Później pobieram listę obecnych wyrażeń w metodzie. Do listy dodaję, na początku jedno wyrażenie, a na końcu drugie.
Później tworze nową składnię metody za pomocą konstruktora.
Zamieniam starą metodę na nową korzystając z metody “ReplaceNode”.
Na końcu zwracam nowe wyrażenie metody posiadającej wywoływanie Console.WriteLine.
Czy kod można napisać lepiej? Oczywiście.
class AddMethodsRewrite : CSharpSyntaxRewriter
{
public override SyntaxNode VisitMethodDeclaration(MethodDeclarationSyntax node)
{
StatementSyntax syn1 =
SyntaxFactory.ParseStatement
("Console.WriteLine(\" Called " + node.Identifier.Text + "\")\n; ");
StatementSyntax syn2 =
SyntaxFactory.ParseStatement
("Console.WriteLine(\" Ended " + node.Identifier.Text + "\")\n; ");
SyntaxList<StatementSyntax> blockWithNewStatements = node.Body.Statements;
blockWithNewStatements = blockWithNewStatements.Insert(0, syn1);
blockWithNewStatements = blockWithNewStatements.Add(syn2);
BlockSyntax newBlock = SyntaxFactory.Block(blockWithNewStatements);
MethodDeclarationSyntax newMethod2 = node.WithBody(newBlock);
var newNode = node.ReplaceNode(node, newMethod2);
return base.VisitMethodDeclaration(newNode);
}
}
Po pierwsze nie trzeba korzystać z konstruktora tworzącego nową metodę. Lepszy rezultat da metoda “WithBody”. Metoda ta kopiuje dane wyrażenie, w tym wypadku jest to wyrażenie metody i dodaje do niej nowe ciało.
public override SyntaxNode VisitBlock(BlockSyntax node)
{
if (node.Parent is MethodDeclarationSyntax)
{
var parent = node.Parent as MethodDeclarationSyntax;
StatementSyntax syn1 =
SyntaxFactory.ParseStatement
("Console.WriteLine(\" Called " + parent.Identifier.Text + "\")\n; ");
StatementSyntax syn2 =
SyntaxFactory.ParseStatement
("Console.WriteLine(\" Ended " + parent.Identifier.Text + "\")\n; ");
SyntaxList<StatementSyntax> blockWithNewStatements = node.Statements;
blockWithNewStatements = blockWithNewStatements.Insert(0, syn1);
blockWithNewStatements = blockWithNewStatements.Add(syn2);
BlockSyntax newBlock = SyntaxFactory.Block(blockWithNewStatements);
return base.VisitBlock(node.ReplaceNode(node, newBlock));
}
else if (node.Parent is IfStatementSyntax)
{
}
return base.VisitBlock(node);
}
Alternatywnie to samo zadanie można osiągnąć korzystając z innej metody odwiedzającej. Przykładowo z metody odwiedzającej każdy blok kod, w tym też blok kodu znajdujący się wewnątrz danej metody.
Muszę tylko sprawdzić, czy rodzic bloku kodu jest na pewno metodą, a nie czymś innym, jak wyrażeniem if.
Metoda ReplaceNode wydaje się być teraz prostsza bo podmieniam tylko blok kodu, a nie całą metodę.
Jak klasa działa w praktyce. Standardowo wczytuje plik tekstowy “Code.txt” i na nim dokonuje wizyty przy użyciu klasy, którą utworzyłem.
string code = "";
using (StreamReader sr = new StreamReader("Code.txt"))
{
code = sr.ReadToEnd();
}
var tree = CSharpSyntaxTree.ParseText(code);
var node = tree.GetRoot();
AddMethodsRewrite add = new AddMethodsRewrite();
var newNode = add.Visit(node);
Console.WriteLine(newNode.NormalizeWhitespace());
Console.ReadKey();
Cel został spełniony.
Przejdźmy więc do drugiego przypadku, w którym mamy metodę logującą.
Tutaj sprawa jest trochę skomplikowała głównie z powodu przekazywania listy parametrów. Zakomentowana metoda rzeczywiście tworzy listę parametrów, ale dopisuje do nich jawne odwołania do ich nazw.
public class LoggingInserter : CSharpSyntaxRewriter
{
public override SyntaxNode VisitMethodDeclaration(MethodDeclarationSyntax node)
{
List<ArgumentSyntax> f = new List<ArgumentSyntax>();
//f.AddRange((from param in node.ParameterList.Parameters
// select SyntaxFactory.Argument(
// SyntaxFactory.NameColon(param.Identifier.ValueText),
// SyntaxFactory.Token(SyntaxKind.None),
// SyntaxFactory.ParseExpression(param.Identifier.Text))).ToArray());
foreach (var param in node.ParameterList.Parameters)
{
f.Add(SyntaxFactory.Argument(
SyntaxFactory.ParseExpression(param.Identifier.Text)
));
}
var loggingInvocation =
SyntaxFactory.ExpressionStatement(
SyntaxFactory.InvocationExpression(
SyntaxFactory.IdentifierName("logger.Log"),
SyntaxFactory.ArgumentList(
SyntaxFactory.SeparatedList(f))));
return node.WithBody(SyntaxFactory.Block(loggingInvocation)
.AddStatements(node.Body.Statements.ToArray()));
}
}
Zakomentowana metoda daje możliwość dodania słów kluczowych jak ref i out. Niestety jawne odwołanie parametru niszczy wszystko, a SynaxtFactory.Argument nie ma innej opcji utworzenie argumentu, poza podaniem bezpośredniego wyrażenia.
Uznałem więc, że lepiej jest właśnie utworzyć bezpośrednie wyrażenie i do tego użyć metody ParseExpression, która będzie konwertować nazwę parametru.
Z dodaniem listy argumentów trochę się na ćwiczyłem, ale się udało
Udało się przetransformować kod
Swoje zmiany można zapisać do pliku.
LoggingInserter log = new LoggingInserter();
var newestNode = log.Visit(newNode);
Console.WriteLine(newestNode.NormalizeWhitespace());
using (StreamWriter sr = new StreamWriter("Code2.txt"))
{
sr.Write(newestNode.NormalizeWhitespace());
}
Console.ReadKey();