How to write a ReSharper plugin

January 17, 2010 22:34

In the previous post your humble correspondent introduced Riant plugin which allows you to specify restricted internal access for types and methods. (You say "method Foo should be accessible only within My.Name.Space" - and lo and behold, R# screams bloody murder if Foo is called outside that namespace.)

This post will shed some light on how to write such a plugin. Take it with a fine grain of salt - I'm not working for JetBrains and everything I know comes from Resharper community,
other open source plugins, and protracted hours with Reflector.

Requirements.


Requirements are simple:

  • A method restricted to some namespace should contain an XML documentation node describing that restriction, and the plugin should be able to understand it.

  • The plugin should listen to source code changes, detect method calls, and analyze whether they're allowed or not.

  • It should underline the offending calls and display R# errors in the Marker Bar, providing helpful messages.

Let's look at what needs to be implemented. Note that everything I'll be talking about is based on R# 4.5. It's still beta times for R#5 and things might change, so I'll update this post with R#5 details when it's ready.

IRecursiveElementProcessor.

So we want to test if methods are accessed from the "allowed" namespaces only. In order to hook into Resharper AST tree analysis we need to implement this guy

public interface IRecursiveElementProcessor
{
    //Here we can skip some subtrees from iteration. 
    //If true is returned, the subtree behind the element is always visited.
    bool InteriorShouldBeProcessed(IElement element);
 
    //This one is executed before visiting the subtree. 
    void ProcessBeforeInterior(IElement element);
 
    //Is executed after visitng the subtree. 
    void ProcessAfterInterior(IElement element);
 
    //Here we can prematurely stop the subtree processing.
    bool ProcessingIsFinished { get; }
}


Usually, there is no difference whether to put the logic in
ProcessBefore.. or ProcessAfter.. - these methods are useful if you need pre- and post- processing. Say, you want to save the state of your tree before processing and compare it to the one after processing.

IDaemonStageProcess.

Then, R# analysis comes in so called stages - all errors and warnings, suggestions and hints come from those stages.
There's a stage that validates correctness of return types, as there's a stage that yields an error if you'd try to throw something that is not an Exception. (For more details, look at TypeMemberErrorStageProcess in Reflector).

Each stage comes along with a process that performs the analysis for it, and to provide a process we need to implement this guy
:

public interface IDaemonStageProcess
{
    //committer provides analysis result from stage to the daemon engine. 
    //It should be called as the last stage statement.
    void Execute(Action<DaemonStageResult> commiter);
}


Implementation.

Time is ripe for implementing those interfaces!

The class below is the heart of our plugin, it contains the logic for AST tree analysis, and if it finds "incorrect" calls it creates
hightlightings for each of them and provides the list to the daemon engine.

internal class RestrictedInternalAccessStageProcess : IDaemonStageProcess, IRecursiveElementProcessor
{
    private readonly IDaemonProcess _process;
    private readonly List<HighlightingInfo> _highlightings = new List<HighlightingInfo>();
 
    public RestrictedInternalAccessStageProcess(IDaemonProcess process)
    {
        _process = process;
    }
 
    public void Execute(Action<DaemonStageResult> committer)
    {
        //Retrieve the project file
        //(using the daemon we stored in the constructor)
        var file = (ICSharpFile)PsiManager.GetInstance(_process.Solution)
                                          .GetPsiFile(_process.ProjectFile);
 
        //Run the AST tree processing - at some point it will come 
        //to our IRecursiveElementProcessor implementation
        file.ProcessDescendants(this);
 
        //The last thing we should do is notify the daemon
        committer(new DaemonStageResult(_highlightings));
    }
 
    public bool InteriorShouldBeProcessed(IElement element)
    {
        return true;
    }
 
    public void ProcessAfterInterior(IElement element)
    {
    }
 
    public void ProcessBeforeInterior(IElement element)
    {
        var checker = new CallViolationChecker(element);
        if (checker.IsUsageAllowed) return;
 
        //Test failed - the call is not allowed, so
        //we create an error and add it to our internal list
        var range = element.GetDocumentRange();
        var highlighting = new RestrictedInternalAccessHighlighting(checker.AllowedNamespace, range));
        var info = new HighlightingInfo(range, highlighting);
        _highlightings.Add(info);
    }
 
    public bool ProcessingIsFinished
    {
        get
        {
            if (_process.InterruptFlag)
                throw new ProcessCancelledException();
            return false;
        }
    }
}

 
Now, once we have the stage process, we need to tell Resharper about it.

IDaemonStage and implementation.

As mentioned above, stage process doesn't live on its own - it should be returned from the corresponding stage (that implements IDaemonStage). A daemon keeps a list of all stages and goes throught them in a sophisticated loop.

Here goes the interface:

public interface IDaemonStage
{
    //Here we can create our stage process (or return null, if analysis is not applicable)
    IDaemonStageProcess CreateProcess(IDaemonProcess daemon, DaemonProcessKind kind);
 
    //Specify which kind of notification we need - either none, or only  
    //Marker Bar highlightings, or Marker Bar and underlines in source code
    ErrorStripeRequest NeedsErrorStripe(IProjectFile projectFile);
}

And here's our implementation (instead of directly implementing the interface, we derive from CSharpDaemonStageBase, to reuse its IsSupported logic):

//Specify DaemonStage attribute so that R# could find this stage
[DaemonStage]
public class RestrictedInternalAccessStage : CSharpDaemonStageBase
{
    public override IDaemonStageProcess CreateProcess(IDaemonProcess daemon, DaemonProcessKind kind)
    {
        var run = IsSupported(daemon.ProjectFile);
        return run ? new RestrictedInternalAccessStageProcess(daemon) : null;
    }
 
    public override ErrorStripeRequest NeedsErrorStripe(IProjectFile projectFile)
    {
        //Tell that we need both MarkerBar and source code highlightings
        return ErrorStripeRequest.STRIPE_AND_ERRORS;
    }
}


IHighlighting and implementation.

So we're nearly done. We've created 

  • a stage that can spawn its own process
  • a stage process that can inform the daemon about inconsistencies in method calls, via a list of highlightings.

Now we need the highlighting itself. You may have noticed that the stage process refers to RestrictedInternalAccessHighlighting type - below goes its implementation.


[ConfigurableSeverityHighlighting(Severity.ERROR, OverlapResolve = OverlapResolveKind.UNRESOLVED_ERROR)]
public class RestrictedInternalAccessHighlighting : CSharpHighlightingBase, IHighlighting
{
    private readonly DocumentRange _range;
    private readonly string _tooltip;
 
    internal RestrictedInternalAccessHighlighting(string allowedNamespace, DocumentRange range)
    {
        //Create the tooltip to display in source code and on the Marker Bar
        _tooltip = string.Format("Method access is restricted to {0}", allowedNamespace);
 
        //Remember the range (location of the error in the code)
        _range = range;
    }
 
    public override bool IsValid() { return true; }
    public string ErrorStripeToolTip { get { return ToolTip; } }
    public int NavigationOffsetPatch { get { return 0; } }
    public override DocumentRange Range { get { return _range; } }
    public string ToolTip { get { return _tooltip; } }
}


That's pretty much it. The only thing left is CallViolationChecker - it needs to retrieve XML documentation from the invocation expression, find the <restrict> tag, and if it's available then test whether the place of invocation is ok.

I'm leaving that as an excercise to the reader.

And when you get your own variant, go ahead and compare it with my implementation :)


Comments are closed