... This article is a continuation of the
first part about writing extensions to the studio with Roslyn.
Here I will describe what to do if we want to generate / change some code. To generate the code, we will use the static methods of the SyntaxFactory class. Some methods require you to specify a keyword / type of expression / type of token, for this there is an enumeration - SyntaxKind, which contains all this together.
Well, let's generate for example a code containing the number 10. This is done simply.
')
SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(10))
I was not joking when I said that to create code, the easiest way is to parse a string. Fortunately, SyntaxFactory provides a bunch of methods for this (ParseSyntaxTree, ParseToken, ParseName, ParseTypeName, ParseExpression, ParseStatement, ParseCompilationUnit, Parse * List).
But this is not the way of the real samurai.
So syntaxfactory
Ok, let's get rid of the first mistake I made. I forgot that C # is now version 6. And one of the C # 6 chips is static imports. Let's trash our global scope.
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Microsoft.CodeAnalysis.SymbolKind; ... LiteralExpression(NumericLiteralExpression, Literal(10))
A little better, but still does not look very. If we write this in code, we will very quickly lose context and forget what we wanted to do. And get bogged down with implementation details. This is unreadable code.
But the solution we have is essentially one thing - to make our own auxiliary methods that hide low level far away.
For example, like this:
public static LiteralExpressionSyntax ToLiteral(this int number) { return LiteralExpression(NumericLiteralExpression, Literal(number)); } 10.ToLiteral()
Already a little better. You may not like that we litter the scope for all ints with our methods. But for me to write DSL normally.
Well, let's try calling a method. For this, there is the InvocationExpression method, the first parameter of which is an expression describing the method (for example this.Invoke, or my.LittlePony.Invoke ()), the second parameter is the list of arguments to the method.
Those. if we want to call this.Add (1, 2) method, then we need to write something like this:
var method = MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, ThisExpression(), IdentifierName("Add")) // this.Add var argument1 = Argument(1.ToLiteral()); var argument2 = Argument(2.ToLiteral()); var arguments = ArgumentList(SeparatedList(new [] { argument1, argument2 })); var invocation = InvocationExpression(method, arguments)
No beauty code. Let's write a couple of DSL methods (these are DSL, not helpers. They do not bring anything into the code, neither the possibility of code reuse, nor the grouping of the code by meaning).
First, the 1st line can be written as:
var method = This().Member("Add");
And the last four can be written like this:
InvocationExpression(method, 1.ToLiteral(), 2.ToLiteral())
Further, we reduce to one line:
This().Member("Add").ToInvocation(1.ToLiteral(), 2.ToLiteral())
Or so:
This().ToInvocation("Add", 1. ToLiteral(), 2.ToLiteral())
Everything is simple, I will not describe what extension methods I wrote - they are stupid and obvious.
Already an order of magnitude better, but still not beautiful. That would get rid of this dull ToLiteral (). After all, we have arguments that are at least ExpressionSyntax, we are not going anywhere. If we force the ToInvocation method to accept only numbers, we will not be able to transfer something else there. Or go away?
Let's introduce the type of AutoExpressionWrapper.
public struct AutoExpressionWrapper { public ExpressionSyntax Value { get; } public AutoExpressionWrapper(ExpressionSyntax value) { Value = value; } public static implicit operator AutoExpressionWrapper(int value) { return new AutoExpressionWrapper(value.ToLiteral()) } public static implicit operator AutoExpressionWrapper(ExpressionSyntax value) { return new AutoExpressionWrapper(value); } } public static InvocationExpressionSyntax ToInvocation(this string member, params AutoExpressionWrapper[] expressions) { return InvocationExpression(IdentifierName(member), expressions.ToArgumentList()); } This().ToInvocation("Add", 1, 2);
Beauty.
The truth is that it is achieved in such a quantity of the left code that the mother does not worry. Well, okay. But beautiful and much more understandable.
Everyone has their own choice, you can not write such a bunch of code, and stop early - where your heart desires. Personally, my development process looks like this:
- Wrote sample code that I want to generate.
- With the help of Roslyn Syntax Visualizer looked into what it parses
- I found the corresponding methods in SyntaxFactory, wrote the code, checked that it works correctly
- I rewrote everything to be beautiful and not too lazy.
- A couple of days came back, I realized that the code is bad, I rewrote
- I learned about the previously unknown API, I was upset
Let's do something simple, for example, we want to generate a code like "! Condition". Here we have an identifier and a logical negation. In code, it looks like this:
PrefixUnaryExpression(LogicalNotExpression, IdentifierName("condition"))
With a slight movement of the hand, it is as follows
LogicalNot(IdentifierName("condition"))
You may have trouble understanding which SyntaxKind you can use in which SyntaxFactory method.
SyntaxKindFacts.cs analysis can help you with this
.Similarly generated for example, "a! = B":
BinaryExpression(NotEqualsExpression, left, right)
Ok, let's do something more complicated - let's declare the whole variable! To do this, we just need to create a VariableDeclaration. But since in C # the record is
int a = 2, b = 3; is a valid correct entry, then VariableDeclaration is a type of variables + a list of variables. The variable itself (a = 2), for example, is the VariableDeclarator. And what is the initializer? Just an expression representing the number "2"? Netushka is an expression representing "= 2". And if we want to declare the variable “int a = 2;”, then we will have the following code:
VariableDeclaration( IdentifierName("int"), SeparatedList(new [] { VariableDeclarator("a").WithInitializer(EqualsValueClause(2.ToLiteral())) }))
Okay, what if we want to declare a protected field? Well, we should do this:
FieldDeclaration(variableDeclaration).WithModifiers(TokenList(new [] { Token(ProtectedKeyword) }))
The biggest joke is that the entities of the field, properties, events, method, constructor have access modifiers. But on the code it is not displayed in any way. Each class that represents an entity simply has methods for working with modifiers. Those. you cannot write a generic method that makes everything beautiful (except through dynamic).
Now let's assign some value to the variable. To do this, you need to declare an expression (Expression) of an assignment (Assigment) and wrap it in something more independent — ExpressionStatement or ReturnStatement.
ExpressionStatement(AssignmentExpression(SimpleAssignmentExpression, IdentifierName(), 10.ToLiteral())))
And if you want to determine the method in which such an assignment is performed, then first it would be nice to combine a bunch of Statements into one using BlockSyntax. By the way, the method is determined surprisingly simply
SyntaxFactory .MethodDeclaration(returnType: returnType, identifier: "DoAssignment") .WithParameterList(SyntaxFactory.ParameterList())
You can also specify access modifiers if you still want to.
SyntaxGenerator
But, to your happiness, not everything is so bad. SyntaxFactory is a low-level API for generating code nodes. You need to know about him. But a lot of things can be done with the help of SyntaxGenerator, and your code will be cleaner and more beautiful. His only drawback is that he is not a static class. This may interfere with the development of its DSL, but the SyntaxGenerator is a clear move forward on the readability of the code.
You can get it like this:
SyntaxGenerator.GetGenerator(document)
And now you can try to generate a field.
generator.FieldDeclaration( "_myField", // IdentifierName("int"), // Accessibility.ProtectedOrInternal, // protected internal
You can learn more about what methods the
SyntaxGenerator provides and look at the implementation of the
CSharpSyntaxGenerator .
Also in the SyntaxGenerator you can find all sorts of methods like WithStatements, which are designed to get information or create a corrected code point. They are also a bit higher-level than the methods defined in SyntaxNode and derivatives.
DocumentEditor
You remember that all code points are persistent unchanging trees? So, after creating some code node, you need to put it in order, add the missing points, and then insert it into some other code node. And then replace the old code-knot in the root with a modified one. And if there are a lot of them? It is not comfortable.
But there is such a cool thing DocumentEditor - it turns work with immutable trees into a sequence of iterative actions. It is created like this:
DocumentEditor.CreateAsync(document, token)
Well, or you can create a SyntaxEditor (of which DocumentEditor is the heir).
SyntaxEditor defines methods for replacing a node, adding, deleting, and getting a modified tree. There are also a bunch of useful extension methods in
SyntaxEditorExtensions . Then the modified tree can be obtained with GetChangedRoot, and the modified document with GetChangedDocument. Similar functionality but in size of the solution is organized in the form of SolutionEditor.
Alas, the high-level API has not yet been fully tested and there are some bugs.
Nice code generation.