We are writing extensions with Roslyn for the 2015 studio (part 1)

  • Tutorial
Go to the second part

To start with, we need to:

1. 2015 Studio
2. the SDK for developing extensions
3. Project templates
4. Visualizer syntax
5. Strong nerves

Useful links: source Roslyn , source code and documentation Roslyn , a roadmap with features The C # 6 .

Perhaps you were embarrassed that you would need strong nerves and want explanation. The thing is that the whole compiler API is a low-level code-generated API. You will laugh, but the easiest way to create code is to parse the 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 a complete kaka. And two thousand extension methods to stay at an acceptable level of abstractions. Okay, I convinced you that writing a Roslyn extension to a studio is a bad idea? And it’s very good that I convinced, otherwise someone reading this article can write the second ReSharper in terms of gluttony of resources. Not convinced? The platform is still raw, there are bugs and not improvements.



Are you still here Getting down. 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 predefined templates.

In order to introduce some kind of refactoring, you need to declare a refactoring provider. Those. the thing that says “Oh, you want to make the code more beautiful here?” Well, you can do it like this: .... Like?". In general, refactoring - they are not only about how to make more 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 abstract CodeRefactoringProvider class, because otherwise the IDE will not be able to work with it. Secondly, it must be marked with the ExportCodeRefactoringProvider attribute, 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, naturally, we need to implement our provider. You need to make only one asynchronous method, like this:

public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) {

CodeRefactoringContext is just the thing in which the current document (Document) lies, the current place in the text (TextSpan), the token for cancellation (CancellationToken). It also provides an opportunity to register your action with a code.

Those. at the entrance we have information about the document, at the exit we promise to do something. Why is the method asynchronous? Because the text is primary. And all sorts of nishtyaki such as parsed code or class information in an unbroken project is slow. You can also write very slow code, but no one likes it. Even the developers of the studio.

Now it would be nice to get a parsed syntax tree. It is done like this:

var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken)

Caution, root can be null. However, it doesn’t matter. Another thing is important - your code should not throw exceptions. Since we're not geniuses here, the only way to avoid exceptions is to wrap your try / catch code.

try {
      // ваш код
}
catch (Exception ex) {
      // TODO: add logging
}

Even this code, with an empty catch block, is the best solution you can come up with. Otherwise, you will annoy the user with the fact that the studio throws a MessageBox “you installed the extension written by the curved-armed mutant” and will no longer allow the user to use your extension even in another section of the code (before restarting the studio). But it’s better to write to the log and send it to your server for analysis.

So, we got information about the syntax tree, but we are asked to offer refactoring for the part of the code where the user 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 BinaryExpressionSyntax, or its ancestor must be it, or its 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(limit: 3)
. The concept is very simple - we take the current node and its ancestors, filter it so that they are of a certain type, and return the first one that we get.

public static IEnumerable GetThisAndParents(this SyntaxNode node, int limit) {
     while (limit> 0 && node != null) {
        yield return node;
         node = node.Parent;
         limit--;
    }
}
public static T FindUp(this SyntaxNode node, int limit = int.Max) 
     where T : SyntaxNode {
     return node
         .GetThisAndParents(limit)
         .OfType()
         .FirstOrDefault();
}

Now we have a binary expression that needs to be refactored. Well or not, in this case we just do 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 simplest code:

context.RegisterRefactoring(CodeAction.Create("Хотите, поменяю?", newDocument))

The second parameter is the modified version of the document. Or a modified version of the solution. Or an asynchronous method that a modified version of a document / solution will generate. In the latter case, your changes will not be calculated until the user hovers over your suggestion to change the code with the mouse. Simple conversions do not make sense asynchronous.

So, back to our rams. We have a BinaryExpressionSyntax expression, we need to create a new one in which the arguments will be inverted. An important fact is all immutable. We cannot change something in the current node, we can only create a new one. Each class representing a code essence has methods to generate a new slightly changed code essence. We are now interested in the binary expression properties Left / Right properties and WithLeft / WithRight methods. Like this:

var newExpression = expression
    .WithLeft(expression.Right)
    .WithRight(expression.Left)
    .Nicefy()

Nicefy is my helper that makes candy out of code. It looks like this:

public static T Nicefy(this T node) where T : SyntaxNode {
    return node.WithAdditionalAnnotations(
        Formatter.Annotation,
        Simplifier.Annotation);
}

The fact is that we cannot just work with code. We work primarily with textual representation of code. Even if our code is parsed, it still contains information about the textual representation of the code. In the best case, with the wrong text representation, you will get a bad looking code. But if you generate the code yourself and do not set the formatting, then you can get for example "vari = 5", which is incorrect code.

Annotation Formatter makes your code beautiful and syntactically correct. Annotation Simplifier removes all redudant things from the code, such as System.String -> string; System.DateTime -> DateTime (the latter is done provided that the namespace System is connected).

We have a new binary expression, but it would be nice if it somehow appeared in the document. First, 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 must not put Formatter and Simplifier annotations at the root of the document. Because in this way we can ruin the life of the user. Yes, and the preview of the action, which redraws a couple of dozen lines when it actually replaces one expression, is sadness.

It remains to put everything together. We did it! We wrote the first extension for the studio.

Now run it with 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 instance of the studio for yourself.

We are writing some code, such as:
var a = 5 - 1;

Check that everything works. Have you checked? All OK? Congratulations!

Congratulations, you wrote code that will crash and annoy the user in rare cases. And our try / catch will not help. I brought a connected issue to this studio bug.

Briefly, what happens:
1. The user writes “1 - 1”
2. We generate a new syntax tree that looks like this: “1 - 1”
3. But at the same time it is not the original one (in sense of reference equality, i.e. equality of links), so the studio thinks that the original and the new tree are completely different.
4. And since they are completely different, then 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 new syntax trees are not the same:
!SyntaxFactory.AreEquivalent(root, newRoot, false);

In this part, I tried to tell you which API appears to you; and how to make simple code refactoring.

In the following parts you will learn:
- how to generate new code using SyntaxFactory
- what SemanticModel is and how to work with it (using the example of an extension that will allow you to automatically replace List with ICollection, IEnumerable; i.e. replace the type with a base / interface)
- how to write unit tests for this whole thing
- code diagnostics

If you want to move on, but you don’t have enough code examples, then examples from developers , a set of diagnostics from FxCop and my extension code will help you .

Go to Part Two

PS: If you are interested in some kind of refactoring (means of automating tedious actions), then write in the comments of the proposal.

Also popular now: