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!




Friday, March 2, 2012

How To: Write a context action using NRefactory

Prerequisites
Introduction

With the new NRefactory version a new API is introduced: Context Actions. This is an easy API to create refactorings for future MonoDevelop and SharpDevelop versions (yes both IDEs can share them).

Writing Refactorings was a difficult task in older MonoDevelop versions. You had to bring many APIs together, now you just need NRefactory. A context action is an edit operation that takes a specific location in the file as it's 'interresting' point. And it provides a code transformation that provides something useful with the information at that point. It can range from small local statement transformations up to altering source code in the whole project.

Some examples are:
  • Convert a foreach loop to a for
  • Generate a new method out of an incovation expression and insert the newly generated method in the class (may be another file than the current open one - and should always jump to the code generated)
  • Declare a local variable out of an expression
  • Add/Remove method parameter (that may alter much more than just the open files)

API

A context actions command is very simple -  basically it's implementing:

public interface IContextAction
{
    bool IsValid (RefactoringContext context);
  void Run (RefactoringContext context);
}

The interresting thing is the RefactoringContext. In short it's a facade to the IDE/Refactoring/Type System/AST subsystem.

There you gather all the information about 'what' to do in the text editor. The actual action is performed as a 'script':

Script RefactoringContext.StartScript ();

You may ask: Why it's not just done as AST transformation ?
That's a good question - it's mostly because for our existing refactorings we needed to do:
  • Do text selections (like "select generated code")
  • Start a "linked" text editor mode (for example letting a user change a generated name of a local variable)
  • Let the refactoring insert code at a position selected by the user 'insertion point mode'
  • Doing a 'format text' command 
  • etc.
All these operations have nothing to do with an AST transformation. However inside the script are functions like:

void Replace (AstNode node, AstNode replaceWith)
void Select (AstNode node)
void Link (params AstNode[] nodes)

There are many functions tightly integrated into the NRefactory AST model - no need to manually calculate (or care for) text editor offsets.

Example

Now I'll make an easy example of an action Remove a "#region" ... "#endregion" pair.
Let's start with a basic action:

public class RemoveRegion : IContextAction
{
    public bool IsValid (RefactoringContext context)
    {
        return GetDirective (context) != null;
    }

    public void Run (RefactoringContext context)
    {
        var directive = GetDirective (context);
        // TODO
    }


    static PreProcessorDirective GetDirective (RefactoringContext context)
    {
        var directive = context.GetNode<PreProcessorDirective> ();
        if (directive == null || directive.Type != PreProcessorDirectiveType.Region)
            return null;
        return directive;
    }
}

The GetDirective function just looks at the current AST position (e.g. caret position) if there is a pre processor directive found there check if it is a "#region" directive and give that back. In this case the action is valid.

Now we need to find the "end directive" and since #region directives can be nested we can't just do a 'text' search - we have the directives already parsed inside our AST stored as nodes. That's why we can just write an ast visitor to find the right directive. It would look like:

class DirectiveVisitor : DepthFirstAstVisitor
{
    readonly PreProcessorDirective startDirective;
    bool searchDirectives = false;
    int depth;
    public PreProcessorDirective Endregion {
        get;
        private set;
    }
    public DirectiveVisitor (PreProcessorDirective startDirective)
    {
        this.startDirective = startDirective;
    }
    public override void VisitPreProcessorDirective (PreProcessorDirective preProcessorDirective)
    {
        if (searchDirectives) {
            if (preProcessorDirective.Type == PreProcessorDirectiveType.Region) {
                depth++;
            else if (preProcessorDirective.Type == PreProcessorDirectiveType.Endregion) {
                depth--;
                if (depth == 0) {
                    Endregion = preProcessorDirective;
                    searchDirectives = false;
                }
            }
        else if (preProcessorDirective == startDirective) {
            searchDirectives = true;
            depth = 1;
        }
        base.VisitPreProcessorDirective (preProcessorDirective);
    }
}

This visitor is pretty straightforward, it visits the whole ast and starts "counting" at the given start directive.

Now we can complete our Run method:

public void Run (RefactoringContext context)
{
    var directive = GetDirective (context);
    var visitor = new DirectiveVisitor (directive);
    context.Unit.AcceptVisitor (visitor);
    if (visitor.Endregion == null)
        return;
    using (var script = context.StartScript ()) {
        script.Remove (directive);
        script.Remove (visitor.Endregion);
    }
}

Now we're basically done. Pretty easy? The action can be extended:
  • It could work at the "#endregion" directive - or a new action could be written that only works at that directive. 
  • We could add an Endregion check to the IsValid method (but that'll consume some time).
  • Unit test. That's very important. NRefactory only accepts context action contributions that are unit tested.
The only thing left is how to put that into the IDE, but I just wanted to give a quick overview of the context action subsystem. Putting a context action inside MonoDevelop is quite easy. The end result looks like: Video. If you're really interested in that I'm often available in the #monodevelop IRC at irc.gnome.org. Or post a comment to this. Maybe I'll make more blog posts about the refactoring system, if there is interrest in that topic.