Tuesday, June 28, 2016

Simply write a plugin for ReSharper

The goal: to write, test, and deploy a simple plug-in for R #, contains custom Quick-Fix and Context Action.

Article Plan:
Setting up the development environment
Example №1: the simplest extension stub
Installing the plugin
Debugging tips
Example №2: code modification using the R # API
Functional testing tools plugins R # API

Cast:
Visual Studio 2015
ReSharper Ultimate 10

I invite interested under the cut.

1. Setting up the development environment





In order not to interfere with the main instance of Visual Studio, in which we will write the code, it is best to immediately prepare the separate "ecosystem" for our future plug. Begin by downloading the so-called «Checked build» - R # Ultimate assembly with enhanced diagnostic information, and the rest - a kind of identical.
We also need Visual Studio Experimental Instance - a kind of "profile" containing all the custom settings on the window layout to the installed extensions (yeah!). Profiles are isolated from each other exactly on the level settings and executable files Studio will not be copied. For VS2015 manage profiles possible with CreateExpInstance.exe utility, but we have a way even easier. Run the installer downloaded previously checked build'a, go to Options - Experimental Hive and enter the new profile name, which later will be used to install the plug-in developed and clever R # myself and the profile will be set to the same. That is, in each profile is possible to establish a set of extensions, including - their version of R #, which facilitates the testing of plug-ins for multiple versions at once. As already mentioned, the profiles are independent of each other, so your desktop Visual Studio profile will not be affected.

To launch the Studio with the new profile will need a label type:
«X: \ Microsoft Visual Studio 14.0 \ Common7 \ IDE \ devenv.exe» / rootSuffix YourHive /ReSharper.Internal

There YourHive - the name of a previously created profile and /ReSharper.Internal option runs R # in development mode, thus enabling useful features, such as notification of exceptions generated inside the plug-ins.

2. Example №1: the simplest extension stub

R # offers a variety of ways to modify and code generation, including the Quick-Fix, Context Action, Refactoring, etc. It seemed to me, the easiest to implement is the Quick-Fix, so we'll start with him. Quick-Fix'y - is known to any user R # command icons in the form of a yellow / red light in a pop-up on the Alt + Enter menu, such as the «Remove unused variable», «Initialize property from constructor», etc .:



So, open a copy of our main studio (not experimental), and create a new project Class Library. Install R # SDK:
Install-Package JetBrains.ReSharper.SDK

About versions of R #, R # SDK and backwards compatibility - or rather its absence
Located in our project the following class:
[QuickFix]
public class SimpleFix: QuickFixBase
{
public SimpleFix (NotInitializedLocalVariableError error)
{
}

protected override Action <ITextControl> ExecutePsiTransaction (ISolution solution, IProgressIndicator progress)
{
return null;
}

public override string Text => "SimpleFix";

public override bool IsAvailable (IUserDataHolder cache)
{
return true;
}
}

Note any Quick-Fix is ​​required to have at least one such designer with an input parameter of type SomeError / SomeWarning. Parameter type and determines for any errors in the Quick-Fix code will be available in the drop-down menu. Our Quick-Fix will be available in the case of neinizializirovannoy local variable:


The R # SDK determined few hundred errors classes available in the space JetBrains.ReSharper.Daemon.CSharp.Errors names. Classes corresponding compilation errors are in the name of the Error postfix, a variety of non-critical improvements - postfix Warning. For the next section we will suffer with the deployment of our Quick-Fix.

3. Install the plugin

We add to our project another class:
[ZoneMarker]
public class ZoneMarker {}

Zone - a new functionality R # SDK, introduced in version 9.0, and developed so far. In particular, using the zone indicated for any product from the R # Platform is designed develop extensions. Fortunately, at the moment it is sufficient to restrict the class stub.
Important: ZoneMarker must be in the same namespace with the class created earlier SimpleFix, or higher.

Next nuance - the distribution and installation of the plug-in R # 9+ only through NuGet-packs. To create the right package of the project to add the file extension .nuspec and the following contents:
<? Xml version = "1.0"?>
<Package>
<Metadata>
<Id> PaperSource.ReSharper.HelloWorld </ id>
<Version> 1.0.5 </ version>
<Authors> You </ authors>
<Owners> You </ owners>
<RequireLicenseAcceptance> false </ requireLicenseAcceptance>
<Description> Do not forget that the package should contain the id point! </ Description>
<Tags> habrahabr.ru </ tags>
<Dependencies>
<Dependency id = "Wave" version = "4.0" />
</ Dependencies>
</ Metadata>
<Files>
<File src = "bin \ Debug \ PluginV10.dll" target = "dotFiles \" />
<File src = "bin \ Debug \ PluginV10.pdb" target = "dotFiles \" />
</ Files>
</ Package>

Important points:
1. Dependence on «Wave» is obligatory package. Wave - a new model of distribution of R # Platform, which, in addition to R #, enter dotPeek, dotTrace etc ... Without going into details:
ReSharper 9.0 - Wave 1.0;
ReSharper 9.1 - Wave 2.0;
ReSharper 9.2 - Wave 3.0;
ReSharper 10.0 - Wave 4.0;

For versions of R #, are not listed in the tag <dependency>, the plugin will not appear on Extention Manager'e - therefore not available for installation. To specify multiple versions, you must use a type of recording [A, B), while "[" - means "inclusive", etc.

2. The plug-in name specified in the tag <id>, is required to contain the point. That simply must, and all! Recommended format - "Company.Package".

After installing NuGet.exe open the Package Manager Console and run the command:
nuget pack «PaperSource.ReSharper.HelloWorld \ package.nuspec»

Ready .nupkg file will appear in your solution folder (or use a parameter -OutputDirectory to create a package in the desired folder). On prevention of type «Issue:. Assembly outside lib folder» can be ignored. To install the plug-in, in our experimental instance of Visual Studio go ReSharper - Options - Extention Manager, and specify the path to a file folder .nupkg.

Moment of Truth: open ReSharper - Extention Manager, looking for our plug-in by name through the search, set. If everything is done correctly - SimpleFix will be available:


Known issues during the installation:
No plug-in Extention Manager;
Plugin can be established, but the Quick-Fix does not appear in the right place.

In both cases, I would suggest to start with checking .nuspec file, and then - with the reading of the official search for the error management. Incidentally, so-called installer logs an exemplary address% LOCALAPPDATA% \ JetBrains \ Shared \ v02 \ InstallerLogXXX for me every time proved futile. Even in the case of a successful installation in the logs written a lot of information about any exceptions thrown, let alone understand, leading to installation errors - and at inconvenient.

4. Debugging tips

To walk through the code debugger extensions through enough Debug - Attach to Process to join the process denenv.exe experimental instance.

Any plug-in must be installed through the Extension Manager. When you make edits to the code, guaranteed way to deploy these changes is similar to update / reinstall. However, this rule there is an exception: if the code does not add new files / classes, and there was no change of integration points with the studio (for example, did not change the type of error to an existing quick-fix), then reinstall the plug-in is optional. It is enough to replace the assembly of the new version of the plugin. R # stores the assembly with plug-ins deep in its bosom, and that once again they did not sink, it is necessary to use MSBuild target, copying assembly "where necessary" after compilation. To do this, place the .csproj file the following code:
<PropertyGroup>
<HostFullIdentifier> ReSharperPlatformVs14YourHive </ HostFullIdentifier>
</ PropertyGroup>

The tag <HostFullIdentifier> is filled manually, and has the following format: {Host} {Visual Studio version} {Visual Studio instance name}. Powered Listing option works for R # Ultimate, VS 2015 and a profile with the name YourHive. If you enter an incorrect HostFullIdentifier, when the project is assembling all the possible options HostFullIdentifier will be displayed in the Output.

5. Example №2: code modification using the R # API

"What we all sorts of caps? The actual code let's code! "- In this section, write a simple plug-in, to be honest reading and modifying sintasicheskoe tree R # code. I wanted to show you something, does not duplicate the functionality of R #, at the same time as a simple enough and applicable in practice. And that could come up. Suppose we have a method that returns the type of List, and for some reason we want to quickly replace the manual return a null value to an empty collection, the appropriate method signature. For example:


It was:
public List <object> FooText ()
{
return null;
}

So:
public List <object> FooText ()
{
return new List <object> ();
}

We got a special case of the pattern Null Object. Of course, I agree that the null and empty collection differ semantically and the pros / cons of this approach would be to talk to, but it is not included in this article. So we move on to the technical implementation. You may notice that the source code is correct (using'i omit) - from the point of view of the compiler and R # is no need to issue even a warning. Therefore, the mechanism discussed above Quick-Fix we do not fit, and we use the Context Action - a much more flexible tool that allows you to assign custom actions on virtually any piece of code:
[ContextAction (Group = "C #", Name = "Empty Collection Action", Description = "something new")]
public class EmptyCollectionContextAction: ContextActionBase
{
public ICSharpContextActionDataProvider Provider {get; set; }

public EmptyCollectionContextAction (ICSharpContextActionDataProvider provider)
{
Provider = provider;
}

public override string Text {get; } = "Return empty collection";
}

Control Visibility Context Action is carried out similar to Quick-Fix by redefining the method IsAvailable:
public override bool IsAvailable (IUserDataHolder cache)
{
var method = Provider.GetSelectedElement <IMethodDeclaration> ();

bool insideOfMethod = method = null!;

if (insideOfMethod)
{
bool returnsNull = ReturnsNullOrEmpty ();

bool isGenericList = HasCorrectReturnType (method);

return returnsNull && isGenericList;
}

return false;
}

To determine that we are within the method - simple enough (see the code.). Next, we need to define the following:

Whether we are on a suitable return statement;
whether the method returns the appropriate type;

Check the return:
ReturnsNullOrEmpty ()
With the return type of the method difficult. Check whether the return type - generic List'om or inherited from it (for other collections of the same principle):
HasCorrectReturnType () - a variant №1
There is a version easier, but not so flexible - compare CLR types on behalf of:
HasCorrectReturnType () - a variant №2
Finally, the most delicious - the replacement of null on the new List <Foo> ():
ReplaceType ()
I intentionally did not comment on the above listings, to pour all the pain in the same place: to write or to disassemble the code R # API ... difficult. In the documentation there are spaces available examples of small, even XML-code comments are absent. We have to suffer every develops methods and use the debugger is active. I stress - the debugger when working with R # API becomes an essential tool for exploring the syntactic tree. 'Serious help also supports the search for key classes in GitHub - a certain amount of sample code can be found.

6. Functional testing tools plugins R # API

One of the remarkable features R # API - it is an opportunity to deploy an instance implicitly R # in memory, fed him a piece of text (the text applying the test Quick-Fix or Context Action), and then compare the converted text with the expected. And all this by writing a small amount of code, comparable to write simple unit tests. Incidentally, R # use NUnit. Go!

Add in the solution with our plug-in is another project that will contain tests. Install the package JetBrains.ReSharper.SDK.Tests. To create a minimal working example, you must create the following folder structure in the project:


This structure is not canonical, but it is easier to deploy and is closer to the traditional structure of C # solution. nuget.config TestEnvironment.cs and files are required:
nuget.config
TestEnvironment.cs
With preparations completed, we go directly to the test writing. The classes containing tests Context Action, you must inherit from CSharpContextActionExecuteTestBase:
[TestFixture]
public class EmptyCollectionContextActionTests: CSharpContextActionExecuteTestBase <EmptyCollectionContextAction>
{
protected override string ExtraPath => "EmptyCollectionContextActionTests";

protected override string RelativeTestDataPath => "EmptyCollectionContextActionTests";

[Test]
public void Test01 ()
{
DoTestFiles ( "Test01.cs");
}
}


Test01.cs file you've already seen in the screenshot, it is the original file with the code, which will apply our Context Action. Test01.cs.gold - a kind of «expected output», the expected code after applying the Context Action. The test is considered passed if applying Context Action to Test01.cs, we get Test01.cs.gold.
When you are writing your own tests necessary to determine the value of properties and ExtraPath RelativeTestDataPath, setting them equal to the name of the folder that contains the source and gold-file. There is no need to compile these files, so they need to safely set BuildAction: None and add to ignore R #, to get rid of the imaginary error messages. With regard to the contents of the source and gold-file, then be sure to specify the Context Action position of the carriage at the time of the call context of action, it is done with the help of the instructions {caret}:
using System;
using System.Collections.Generic;

namespace N
{
public class C
{
public List <int> FooMethod ()
{
return {caret} null;
}
}
}


The corresponding gold-file:
using System;
using System.Collections.Generic;

namespace N
{
public class C
{
public List <int> FooMethod ()
{
return {caret} new List <int> ();
}
}
}

If during the test (the original file + Context Action)! = Gold-file, the test will fall, and in the same folder tmp-file that contains the actual result of the application Context Action will be established.

Run a test to perform, and ... I'll get right to the list of problems and ways to solve them:

Excluding «file does not exist» - the most simple, check the folder structure and the corresponding values ​​ExtraPath properties, RelativeTestDataPath;
The test with the exception of drops in SetUpFixture - check the location and contents of nuget.config TestEnvironment.cs and files;
Excluding «The test output differs from the gold file» - study created tmp-file run the test with the debugger;
Tmp-file instead contains one line of code «NOT AVAILABLE» - may not caret {caret} in the source file, or Context Action at work threw exception;
The most interesting case - always successful test, regardless of the content source and gold-files. This falls - if you delete the original file. With such unpleasant behavior I encountered when the test for inherited Context Action on ContextActionTestBase not asked ExtraPath property.


That's all. Fully functional demo is available on GitHub. I hope for the development of its first plug-in, you now will take a lot less time than it took me =)

1 comment:

  1. Wow, amazing block structure! How long
    Have you written a blog before? Working on a blog seems easy.
    The overview of your website is pretty good, not to mention what it does.
    ReSharper Ultimate Crack Free Download

    ReplyDelete