Sunday, April 8, 2012

How To: Write a C# Issue Provider

Prerequisites

Introduction

A month ago I blogged about context actions using NRefactory. I got positive feedback about the new possibilities of context actions and we worked hard to improve the API and getting the NRefactory capabilities into Monodevelop.
In SharpDevelop the actions and issue providers are running too. With NRefactory as base we can share these features.
And now we have a custom refactoring infrastructure that makes it easy to write actions and issue providers. A code issue doesn't just analyze one point in the file like the context actions it analyzes the whole file in a background thread and shows it's findings as underlines in the text editor and/or in the task bar.
Furthermore a code issue can provide a code action to fix the issue.
In this post we'll create a code issue provider and the code action to fix that issue using the NRefatory API.

Writing the Code Issue Provider

I've decided to do what others already did (then you know that it works and is helpful)- creating a code issue provider for local variable declarations that can be converted to constants.

Look at that code:



In the code above pi is assigned to a constant value that never changes (I would recommend using Math.PI, but that's not part of this blog) and can be converted to a local constant:


For the analysis we need to do:
  • Check that the variable declaration is not constant
  • Check that all variables of the declaration have an initializer that is a constant expression
  • Analyze the data flow to ensure that the variables are read only

Creating the provider class

A code issue provider needs to implement the ICodeIssueProvider interface. This is nothing more than yielding a list of all identified  CodeIssues in a file. (In our case all variable declarations that can be made constant.)
For making the thing work in the IDE an IssueDescriptionAttribute gives all the information the IDE needs to display this issue.

In code that looks like the following one.


Code Issue logic

For analyzing a syntax tree it's a good approach to use the visitor pattern. There is a pre defined visitior that is specialized in code issues. We just create a visitor to analyze the syntax tree that is handling variable declaration statements.

That looks like.

The VisitVariableDeclarationStatement is called for all local variable declaration statements and we need to analyze each of them, if they can be converted to a constant.
First we check, if the variable declaration is already a constant, if so we can safely ignore it:


After that we need to ensure that all variable initializers are constant. We need to resolve the initializers and determine, if the resolve result is a constant one:

Note that all elements in the syntax trees are always != null, therefore null checks are not needed. That makes writing refactoring less error prone. However relative nodes like the Parent or FirstChild can be null, otherwise it would break the tree structure. But never things like an embedded statement in an if. To mark that as 'not in ast' a null object is given back which can be checked with IsNull.

Now things become a bit more complicated because we need to analyze the data flow. We need to analyze the data flow from the declaration statement to the end of the containing block to ensure that the variable is never changed.

Thats what the definite assignment analysis is for. It determines the state of a variable at a given point. We can use SetAnalyzedRange to limit the analyzing range. That is needed because an initializer will always set the variable state and we need to know it the variable is set between its declaration and the end of the containing block. Thefore we calls SetAnalyzedRange with the variable declaration (excluded - an initializer always sets a variable) to the end of the containing block (included).

Then we just need to do is to ask the assignmentAnalysis, if a variable gets assigned in its life time.


Adding the issue and fix action

Now all necessary analysis is done. If the code hasn't returned the variable declaration statement can be made constant.
Now time for creating the code issue and a fix action. Note that the fix action is optional. The code issues share the code action infrastructure with the code actions.
The difference here is that the issue and fix action is added to a node in the syntax tree - in our case the variable declaration.
An action contains a script. This is basically an syntax tree transformation, but can contain more text editor like actions as well like activating a linked name mode in the text editor (for naming newly created variables for example).
In our case we just need to add a 'const' before the variable declaration.  That would be fairly easy with a script.InsertBefore (varDecl, new CSharpModifierToken (Modifiers.Const)); call - but let's do it with a more complex syntax tree transformation.

We need to change the variable declarations modifiers. But the tree is immutable. However it is possible to clone an immutable node which can be altered. That's what we do - we clone the declaration, add a const modifier to it and replace the original variable declaration with it's altered version:


The TranslateString function should be used for all strings displayed (The IssueDescription is included as well) so that they're automatically added to our translation database.

Whole source code

Here is the whole source code of the issue provider - it just takes 50 lines:

After adding this class to the NRefactory project and starting Monodevelop we'll have the code issue provider working inside the IDE:


And after the fix:


Now you've your first code issue provider using the NRefactory API that performs syntactic and semantic analysis and also changes code. Congratulations!




3 comments:

  1. Hey Mike
    This looks truly awesome, Great work!
    You said "After adding this class to the NRefactory project and starting Monodevelop we'll have the code issue provider working inside the IDE." Does this mean that the only way to add these are by extending the monodevelop source code and then compiling it and so on to get it running locally? or is there a way to plug it like the add ins?
    Also I was wondering whether you guys have a similar thing for 'suggestions', like changing methods and classes access modifiers for example..?
    Looking forward to start playing with these in my refactoring add in, looks like some of the things you got here could make certain things so much easier, that I might be able to start refactoring some stuff, reduce some code and make it much cleaner. Good stuff!

    ReplyDelete
    Replies
    1. We've various issues for changing access modifiers - for example redundant 'internal' modifiers on classes. And we've a small set of general code issue providers.

      I hope that they'll be extended over the GSoC - as well as the context actions.

      Delete
  2. You can plug it like the add ins. Our C# backend for example contains a move type to file (file name == class name) action.

    But I advertise that the majority of code issues/actions is put into NRefactory because that would enable both sharpdevelop and monodevelop to use them. That's open source :)

    As a plus of that approach is that refactoring implementors don't need to learn/know the internals of the IDEs. You can write refactorings without knowing (the very flexible) add-in system.

    ReplyDelete