
We are writing extensions with Roslyn for 2015 studio (part 2)
- Tutorial
... 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 be the static methods of the SyntaxFactory class. Some methods require you to specify a keyword / expression type / token type, for this there is an enumeration - SyntaxKind, which contains all this together.
OK, let's generate code for example, containing the number 10. This is done simply.
I was not joking when I said that the easiest way to create code 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 a real samurai.
Ok, let’s get rid of the very first mistake I made. I forgot that C # is now version 6. And one of the features of C # 6 is static import. Let's clutter up our global scope.
A little better, but still not looking very good. If we write this in code, then we will quickly lose context and forget what we wanted to do. And get bogged down in implementation details. This is unreadable code.
And the solution we have is essentially the same - to make our own auxiliary methods, which hide the low level to hell.
For example, so here:
Already a little better. You may not like that we clutter the scope for all ints with our methods. But for me it's ok to write DSL.
Ok, let's try calling the method. To do this, there is the InvocationExpression method, the first parameter of which is an expression that describes the method (for example this.Invoke, or my.LittlePony.Invoke ()), the second parameter is a list of arguments to the method.
Those. if we want to call the this.Add (1, 2) method, then we need to write something like this:
No code beauty. Let's write a couple of DSL methods (these are DSLs, not helpers. They bring nothing to the code, neither the possibility of reusing the code, nor the grouping of the code in meaning).
Firstly, the 1st line can be written like this:
Everything is simple here, I will not describe what extension methods I wrote - they are stupid and obvious.
Already an order of magnitude better, but still not beautiful. I wish I could get rid of this dull ToLiteral (). Only after all, our arguments are at least ExpressionSyntax, we won’t get anywhere from this. If we force the ToInvocation method to accept only numbers, we will not be able to transfer anything else there. Or shall we?
Let's introduce an AutoExpressionWrapper type.
Beauty.
The truth is achieved with so many left-handed codes that Mom Don't Cry. Well, okay. But it’s beautiful and much more understandable.
Here, everyone has their own choice, you can not write such a bunch of code, but stop early - wherever your heart desires. Personally, my development process looks like this:
Let's do something simple, for example, we want to generate code like "! Condition". Here we have an identifier and a logical negation. In code, it looks like this:
With a slight movement of the hand it turns out like this:
You may have trouble understanding which SyntaxKind in which SyntaxFactory method you can use. SyntaxKindFacts.cs analysis can help you with this .
Similarly, for example, “a! = B” is generated:
Ok, let's do something more complicated - declare a whole variable! To do this, we just need to create a VariableDeclaration. But since in C # the entry is int a = 2, b = 3; is a valid correct record, then VariableDeclaration is a type of variables + list of variables. The variable itself (a = 2) for example is VariableDeclarator. What is an initializer? Just an expression representing the number "2"? Netushki, this is an expression representing "= 2". And if we want to declare the variable “int a = 2;”, then we will have the following code:
Okay, what if we want to declare a protected field? Well we have to do this:
The biggest trick is that the entities of the field, property, event, method, constructor have access modifiers. But on the code it is not displayed in any way. Each class that represents an entity simply has specific methods for working with modifiers. Those. you cannot write a general method that does everything beautifully (unless through dynamic).
Now assign a variable some value. To do this, you need to declare an expression (Expression) assignment (Assigment) and wrap it in something more independent - ExpressionStatement or ReturnStatement.
And if you want to define a method in which such an assignment is performed, then at first it would be nice to combine a bunch of Statements into one using BlockSyntax. By the way, the method is surprisingly simple.
You can also specify access modifiers if you still want to.
But, fortunately, not everything is so bad. SyntaxFactory is a low-level API for generating code nodes. You need to know about him. But many things can be done with SyntaxGenerator, and your code will be cleaner and more beautiful. Its only drawback is that it is not a static class. This may interfere with the development of your DSLs, but SyntaxGenerator is a clear forward readability.
You can get it like this:
And now you can try to generate the field.
You can study in more detail what methods SyntaxGenerator provides and look at the implementation of CSharpSyntaxGenerator .
Also in SyntaxGenerator you can find all sorts of methods like WithStatements, which are designed to receive information or create an adjusted code node. They are also slightly higher level than the methods defined in SyntaxNode and derivatives.
Do you remember that all the nodes are persistent immutable trees? So, after creating any 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 unit in the root with the changed one. And if there are a lot of them? It is not comfortable.
But there is such a cool thing DocumentEditor - it turns the work with immutable trees into a sequence of iterative actions. It is created like this:
Well, or you can create a SyntaxEditor (whose inheritor is DocumentEditor).
SyntaxEditor defines methods for replacing a node, adding, deleting, and retrieving a modified tree. There are also tons of useful extension methods in SyntaxEditorExtensions . Then the changed tree can be obtained with GetChangedRoot, and the changed document with GetChangedDocument. A similar functionality but in the 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.
Have a nice code generation.
Here I will describe what to do if we want to generate / change some code. To generate the code, we will be the static methods of the SyntaxFactory class. Some methods require you to specify a keyword / expression type / token type, for this there is an enumeration - SyntaxKind, which contains all this together.
OK, let's generate code for example, containing the number 10. This is done simply.
SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(10))
I was not joking when I said that the easiest way to create code 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 a real samurai.
So SyntaxFactory
Ok, let’s get rid of the very first mistake I made. I forgot that C # is now version 6. And one of the features of C # 6 is static import. Let's clutter up our global scope.
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Microsoft.CodeAnalysis.SymbolKind;
...
LiteralExpression(NumericLiteralExpression, Literal(10))
A little better, but still not looking very good. If we write this in code, then we will quickly lose context and forget what we wanted to do. And get bogged down in implementation details. This is unreadable code.
And the solution we have is essentially the same - to make our own auxiliary methods, which hide the low level to hell.
For example, so here:
public static LiteralExpressionSyntax ToLiteral(this int number) {
return LiteralExpression(NumericLiteralExpression, Literal(number));
}
10.ToLiteral()
Already a little better. You may not like that we clutter the scope for all ints with our methods. But for me it's ok to write DSL.
Ok, let's try calling the method. To do this, there is the InvocationExpression method, the first parameter of which is an expression that describes the method (for example this.Invoke, or my.LittlePony.Invoke ()), the second parameter is a list of arguments to the method.
Those. if we want to call the 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 code beauty. Let's write a couple of DSL methods (these are DSLs, not helpers. They bring nothing to the code, neither the possibility of reusing the code, nor the grouping of the code in meaning).
Firstly, the 1st line can be written like this:
var method = This().Member("Add");
And the last four can be written like this:InvocationExpression(method, 1.ToLiteral(), 2.ToLiteral())
Next, reduce to one line:This().Member("Add").ToInvocation(1.ToLiteral(), 2.ToLiteral())
Or so:This().ToInvocation("Add", 1. ToLiteral(), 2.ToLiteral())
Everything is simple here, I will not describe what extension methods I wrote - they are stupid and obvious.
Already an order of magnitude better, but still not beautiful. I wish I could get rid of this dull ToLiteral (). Only after all, our arguments are at least ExpressionSyntax, we won’t get anywhere from this. If we force the ToInvocation method to accept only numbers, we will not be able to transfer anything else there. Or shall we?
Let's introduce an AutoExpressionWrapper type.
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 achieved with so many left-handed codes that Mom Don't Cry. Well, okay. But it’s beautiful and much more understandable.
Here, everyone has their own choice, you can not write such a bunch of code, but stop early - wherever your heart desires. Personally, my development process looks like this:
- Wrote an example of the code I want to generate
- With the help of Roslyn Syntax Visualizer, I looked at what it was parsing.
- I found the appropriate methods in SyntaxFactory, wrote the code, checked that it works correctly
- I rewrote everything so that it was beautiful and not too lazy
- He returned a couple of days later, realized that the code was bad, rewrote
- I found out about the previously unknown API, I was upset
Let's do something simple, for example, we want to generate 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 turns out like this:
LogicalNot(IdentifierName("condition"))
You may have trouble understanding which SyntaxKind in which SyntaxFactory method you can use. SyntaxKindFacts.cs analysis can help you with this .
Similarly, for example, “a! = B” is generated:
BinaryExpression(NotEqualsExpression, left, right)
Ok, let's do something more complicated - declare a whole variable! To do this, we just need to create a VariableDeclaration. But since in C # the entry is int a = 2, b = 3; is a valid correct record, then VariableDeclaration is a type of variables + list of variables. The variable itself (a = 2) for example is VariableDeclarator. What is an initializer? Just an expression representing the number "2"? Netushki, this 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 have to do this:
FieldDeclaration(variableDeclaration).WithModifiers(TokenList(new [] { Token(ProtectedKeyword) }))
The biggest trick is that the entities of the field, property, event, method, constructor have access modifiers. But on the code it is not displayed in any way. Each class that represents an entity simply has specific methods for working with modifiers. Those. you cannot write a general method that does everything beautifully (unless through dynamic).
Now assign a variable some value. To do this, you need to declare an expression (Expression) assignment (Assigment) and wrap it in something more independent - ExpressionStatement or ReturnStatement.
ExpressionStatement(AssignmentExpression(SimpleAssignmentExpression, IdentifierName("a"), 10.ToLiteral())))
And if you want to define a method in which such an assignment is performed, then at first it would be nice to combine a bunch of Statements into one using BlockSyntax. By the way, the method is surprisingly simple.
SyntaxFactory
.MethodDeclaration(returnType: returnType, identifier: "DoAssignment")
.WithParameterList(SyntaxFactory.ParameterList()) // заменить со своим списком параметров
.WithBody(codeBlock) // codeBlock - это BlockSyntax
You can also specify access modifiers if you still want to.
Syntaxgenerator
But, fortunately, not everything is so bad. SyntaxFactory is a low-level API for generating code nodes. You need to know about him. But many things can be done with SyntaxGenerator, and your code will be cleaner and more beautiful. Its only drawback is that it is not a static class. This may interfere with the development of your DSLs, but SyntaxGenerator is a clear forward readability.
You can get it like this:
SyntaxGenerator.GetGenerator(document)
And now you can try to generate the field.
generator.FieldDeclaration(
"_myField", // имя поля
IdentifierName("int"), // здесь указываем тип
Accessibility.ProtectedOrInternal, // будет преобразовано в protected internal для С#
DeclarationModifiers.ReadOnly,
2.ToLiteral() // да, не нужно оборачивать EqualsValueClause
)
You can study in more detail what methods SyntaxGenerator provides and look at the implementation of CSharpSyntaxGenerator .
Also in SyntaxGenerator you can find all sorts of methods like WithStatements, which are designed to receive information or create an adjusted code node. They are also slightly higher level than the methods defined in SyntaxNode and derivatives.
DocumentEditor
Do you remember that all the nodes are persistent immutable trees? So, after creating any 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 unit in the root with the changed one. And if there are a lot of them? It is not comfortable.
But there is such a cool thing DocumentEditor - it turns the 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 (whose inheritor is DocumentEditor).
SyntaxEditor defines methods for replacing a node, adding, deleting, and retrieving a modified tree. There are also tons of useful extension methods in SyntaxEditorExtensions . Then the changed tree can be obtained with GetChangedRoot, and the changed document with GetChangedDocument. A similar functionality but in the 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.
Have a nice code generation.