Go to the second part
For a start, we will need:
1.
2015 studio
2.
SDK for developing extensions
3.
Project Templates
4.
Syntax Visualizer
5. Strong nerves
')
Useful links:
roslyn sources, roslyn sources and documentation ,
roadmap with C # 6 features .
Probably you were embarrassed that you need strong nerves and you want an explanation. The thing is that the entire compiler API is a low-level code-generated API. You will laugh, but the easiest way to create code is to parse a string. Otherwise, you will either get bogged down in a heap of unreadable code, or you will write thousands of extension-methods so that your code does not look syntactically like full Kaka. And another two thousand extension-methods to stay at an acceptable level of abstraction. Okay, I convinced you that writing Roslyn extensions to the studio is a bad idea? And it is very good that he convinced, and then someone who reads this article can write a second ReSharper on the voracity of resources. Not convinced? The platform is still raw, there are bugs and no modifications.
Are you still here? Getting started. Let's write the simplest refactoring, which for a binary operation will swap two arguments. For example, it was: 1 - 5. It became: 5 - 1.
First, create a project using one of the preset templates.
In order to present some kind of refactoring, you need to declare the provider of the refactorings. Those. the thing that will say “Oh, you want to make the code more beautiful here? Well, you can do it like this:…. Like?". In general, refactorings - they are not only about how to make beautiful. They are more about how to automate some tedious actions.
Ok, let's write SwapBinaryExpressionArgumentsProvider (I hope you like my naming style).
First, it must inherit from the CodeRefactoringProvider abstract class, because otherwise the IDE will not be able to work with it. Secondly, it must be marked with the attribute ExportCodeRefactoringProvider, because otherwise the IDE will not be able to find your provider. The Shared attribute is here for beauty.
[ExportCodeRefactoringProvider("SwapBinary", LanguageNames.CSharp), Shared] public class SwapBinaryExpressionArgumentsProvider : CodeRefactoringProvider
Now, of course, you need to implement our provider. We need to do just one asynchronous method, like this:
public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) {
CodeRefactoringContext is just a gizmo in which the current document (Document) lies, the current place in the text (TextSpan), the token for cancellation (CancellationToken). And he provides the ability to register your action with the code.
Those. at the entrance we have information about the document, the output promises to do something. Why is the method asynchronous? Because the text is primary. And all sorts of nishtyaki like parsed code or information about classes in a non-crashed project is slow. And you can write a very slow code, and no one likes it. Even studio developers.
Now it would be nice to get parsed syntax tree. This is done like this:
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken)
Be careful, root may be null. But it does not matter. Another thing is important - your code should not throw exceptions. Since we are not all geniuses here, the only way to avoid exceptions is to wrap your try / catch code.
try {
Even this code, with an empty catch block, is the best solution you can think of. Otherwise, you will annoy the user with the fact that the studio throws a MessageBox “you have installed an extension written by a Krivorot mutant” and will no longer allow the user to use your extension even in another code section (before restarting the studio). But it is better to still write to the log and send to your server for analysis.
So, we received information about the syntax tree, but we are asked to suggest refactoring for the code segment where the user's cursor is located. You can find this node like this:
root.FindNode(context.Span)
But we need to find the closest binary operator. With the help of Roslyn Syntax Visualizer, we can find out that it is represented by the BinaryExpressionSyntax class. Those. we have a node (SyntaxNode) - it must be a BinaryExpressionSyntax, or its ancestor must be it, or an ancestor-ancestor, .... It would be nice if we had a way from the current node to try to find some specific node. For example, so that we can write like this:
node.FindUp<BinaryExpressionSyntax>(limit: 3)
. The concept is very simple - we take the current node and its ancestors, filter them so that they are of a certain type, return the first one that is found.
public static IEnumerable<SyntaxNode> GetThisAndParents(this SyntaxNode node, int limit) { while (limit> 0 && node != null) { yield return node; node = node.Parent; limit--; } } public static T FindUp<T>(this SyntaxNode node, int limit = int.Max) where T : SyntaxNode { return node .GetThisAndParents(limit) .OfType<T>() .FirstOrDefault(); }
Now we have a binary expression that needs to be refactored. Well or not, in this case we simply return.
Now we need to tell the environment that we have a way to rewrite this code. This concept is represented by the CodeAction class. The easiest code:
context.RegisterRefactoring(CodeAction.Create(", ?", newDocument))
The second parameter is the modified version of the document. Or a modified version of the solution. Or asynchronous method, which will generate a modified version of the document / solution. In the latter case, your changes will not be calculated before the user hovers over your suggestion for changing the code. It does not make sense to make simple conversions asynchronous.
So back to our sheep. We have a BinaryExpressionSyntax expression, we need to create a new one in which the arguments are inverted. The important fact is all unchangeable. We can not change something in the current node, we can only create a new one. Each class representing a code entity has methods to generate a new, slightly modified code essence. In the binary expression, we are now interested in the Left / Right properties and WithLeft / WithRight methods. So here it is:
var newExpression = expression .WithLeft(expression.Right) .WithRight(expression.Left) .Nicefy()
Nicefy is my helper who makes candy out of code. It looks like this:
public static T Nicefy<T>(this T node) where T : SyntaxNode { return node.WithAdditionalAnnotations( Formatter.Annotation, Simplifier.Annotation); }
The fact is that we cannot simply work with the code. We work primarily with the textual representation of the code. Even if our code is parsed, it still contains information about the textual representation of the code. At best, with the wrong textual representation, you will get a bad looking code. But if you generate the code yourself and do not arrange the formatting, you can get for example “vari = 5”, which is incorrect code.
Abstract Formatter makes your code beautiful and syntactically correct. Abstract Simplifier removes all redudant things from the code, such as System.String -> string; System.DateTime -> DateTime (the latter is done under the condition that the namespace System is connected).
We have a new binary expression, but it would be nice if it somehow turned out to be in the document. First, we generate a new root with the replaced expression:
var newRoot = root.ReplaceNode(expression, newExpression);
And now we can get a new document:
var newDocument = context.Document.WithSyntaxRoot(newRoot);
There is an important point - we cannot put Formatter and Simplifier annotations on the root of the document. Because by this we can ruin the life of the user. Yes, and preview action, which rewrites a couple of dozen lines, when in fact replaces one expression - it is a sadness.
It remains to arrange everything in a bunch. We did it! We wrote the first extension for the studio.
Now run it using F5 / Ctrl + F5. At the same time, a new studio is launched in Roslyn mode, with an empty set of extensions and default settings. They are not reset after a restart, i.e. if you want, you can customize this studio copy for yourself.
We write some code, like:
var a = 5 - 1;
We check that everything works. Checked out? All OK? Congratulations!
Congratulations, you have written a code that will fall and annoy the user in rare cases. And our try / catch will not help. I started a connected issue on this
studio bug.
In short, what happens:
1. The user writes "1 - 1"
2. We generate a new syntax tree, which looks like this: "1 - 1"
3. But at the same time it is not the source (in the sense of reference equality, i.e., the equality of references), so the studio thinks that the original and the new tree are completely different.
4. And since they are completely different, the contract falls inside the studio, which checks that the original and the new tree are completely different.
To fix the bug, you need to check that the original and the new syntax tree are not the same:
!SyntaxFactory.AreEquivalent(root, newRoot, false);
In this part, I tried to tell which API is presented to you; and how to do the simplest code refactoring.
In the following parts you will learn:
- How to generate a new code using SyntaxFactory
- what is SemanticModel and how to work with it (using the example of an extension that allows you to automatically replace the List with an ICollection, IEnumerable; i.e. replace the type with a base / interface)
- how to write unit tests for this whole thing
- diagnostic code
If you want to move on, but you do not have enough code examples, then you
will be helped by examples from the developers , a
set of diagnostics from FxCop and the
code of my extension .
Go to the second part
PS: If you are interested in some kind of refactorings (automation tools for tedious actions), then write in the comments comments.