📜 ⬆️ ⬇️

We write the simplest plugin for ReSharper

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

Article layout:
  1. Setting up the development environment
  2. Example # 1: the simplest plug-in extension
  3. Plug-in installation
  4. Debugging, tips
  5. Example 2: code modification using the R # API
  6. Functional testing of plugins with R # API

Cast:
Visual Studio 2015
ReSharper Ultimate 10

Interested I invite under kat.

1. Setting up the development environment


In order not to interfere with the work of the main instance of Visual Studio, in which we will write code, it is best to immediately prepare a separate “ecosystem” for our future plugin. We start by downloading the so-called. “Checked build” is an R # Ultimate build with extended diagnostic information, and the rest is identical natural.
We also need a Visual Studio Experimental Instance - in some way a “profile” containing all user settings from the location of the windows to the installed extensions (aha!). Profiles are isolated from each other exactly at the settings level, and Studio executable files are not copied anywhere. For VS2015, it is possible to manage profiles using the CreateExpInstance.exe utility, but we have an even easier way. Run the previously installed installer checked build'a, go to Options - Experimental Hive and enter the name of the new profile, which will later be used to install the developed plugin, and the clever R # this profile will create and install it there too. Exactly, it is possible to install your own set of extensions in each profile, including its own version of R #, which facilitates testing plug-ins for several versions at once. As already mentioned, the profiles are independent of each other, so your Visual Studio working profile will not be affected.

To launch Studio with a new profile, you need a shortcut like:
"X: \ Microsoft Visual Studio 14.0 \ Common7 \ IDE \ devenv.exe" / rootSuffix YourHive /ReSharper.Internal

Here, YourHive is the name of a previously created profile, and the /ReSharper.Internal parameter starts R # in development mode , thereby turning on useful features, such as exception notifications generated inside plugins.
')

2. Example number 1: the simplest extension-stub


R # offers various ways to modify and generate code, including Quick-Fix, Context Action, Refactoring, etc. It seemed to me that the most simple to implement is Quick-Fix, so we will begin with it. Quick-Fixes are R # commands known to any user with icons in the form of a yellow / red light bulb in an Alt + Enter pop-up menu, such as "Remove unused variable", "Initialize property from constructor", etc .:



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

About versions of R #, R # SDK and backward compatibility, or rather, its absence
At the time of this writing, the latest released version of JetBrains.ReSharper.SDK is version 10.0.20151101.194233, corresponding to R # 10. JetBrains does not provide compatibility between major and minor versions of products, so the plug-in assembled in SDK 9.1 is not guaranteed in R # 9.2 and etc. Hereinafter SDK 10 will be used, which means support only R # 10 and the impossibility of installation and correct operation in R # 9.2. At the same time, there are no obstacles to reassemble all the code considered in the article under the SDK 9.2, thereby obtaining the plug-in working in R # 9.2 - verified.

Let's place the following class in our project:
[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; } } 

Please note that any Quick-Fix must have at least one such constructor with an input parameter of type SomeError / SomeWarning. The type of the parameter and determines for which error in the code Quick-Fix will be available in the drop-down menu. Our Quick-Fix will be available when using a non-initialized local variable:


The R # SDK defines several hundreds of error classes available in the JetBrains.ReSharper.Daemon.CSharp.Errors namespace. Classes corresponding to compilation errors have the error postfix in the name, various non-critical improvements - the warning postfix. The whole next section, we will suffer with the deployment of our Quick-Fix.

3. Installing the plugin


Let's add one more class to our project:
 [ZoneMarker] public class ZoneMarker { } 

Zones is a new functionality of R # SDK, which appeared in version 9.0, and is still being developed. In particular, with the help of a zone it is indicated for which product from the R # Platform composition the extension being developed is intended. Fortunately, at the moment it is enough for us to restrict ourselves to the stub class.
Important: ZoneMarker must be in the same namespace as the SimpleFix class created earlier, or above.

The next nuance - the distribution and installation of the plug-in in R # 9+ is done only through NuGet-packages. To create the correct package, add a file with the .nuspec extension and the following contents to the project:
 <?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> ,  id    !</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 the Wave package is required. Wave is a new distribution model of R # Platform, which, in addition to R #, includes 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 # that are not listed in the <dependency> tag, the plug-in will be absent in the Extention Manager — hence, unavailable for installation. To specify multiple versions, you must use an entry of the form [A, B), while "[" means "inclusive", etc.

2. The name of the plug-in specified in the <id> tag must contain a period. That is simply a must, and that’s all! The recommended format is “Company.Package”.

After installing NuGet.exe, open the Package Manager Console and execute the command:
nuget pack "PaperSource.ReSharper.HelloWorld \ package.nuspec"

The finished .nupkg file will appear in the folder of your solution (or use the -OutputDirectory parameter to create a package in the folder you need). You can ignore warnings like "Issue: Assembly outside lib folder." To install the plug-in, in our experimental instance of Visual Studio, go to ReSharper - Options - Extention Manager and specify the path to the folder with the .nupkg file.

The moment of truth: open ReSharper - Extention Manager, look for our plugin by name through search, install. If everything is done correctly - SimpleFix will be available:


Known installation problems:

In both cases, I would advise you to start by checking the .nuspec file, and then reading the official manual for finding errors . By the way, the so-called. installer logs at the approximate% LOCALAPPDATA% \ JetBrains \ Shared \ v02 \ InstallerLogXXX address for me every time turned out to be useless. Even in the case of a successful installation, a lot of information is written to the logs about any exceptions thrown in, and it’s difficult to understand what causes the installation error.

4. Debugging, useful tips


In order to go through the debugger through the extension code, it is sufficient through the Debug - Attach to Process to join the denenv.exe process of the experimental instance.

Any plugin must be installed via the Extension Manager. When making changes to the code, a guaranteed way to deploy these changes is a similar update / reinstallation. However, there is an exception to this rule: if new files / classes were not added to the code, and there were no changes to the integration points with the studio (for example, the type of error for the already existing quick-fix was not changed), it is not necessary to reinstall the plugin. It is enough to replace the plugin assembly with a new version. R # stores assemblies with plug-ins deep in their depths, and in order not to dive into them once again, you should use MSBuild target, which copies the assembly where necessary after compilation. To do this, in the .csproj file we place the following code:
 <PropertyGroup> <HostFullIdentifier>ReSharperPlatformVs14YourHive</HostFullIdentifier> </PropertyGroup> 

The <HostFullIdentifier> tag is manually populated and has the following format: {Host} {Visual Studio version} {Visual Studio instance}. The version shown in the listing will work for R # Ultimate, VS 2015 and a profile called YourHive. If you specify an incorrect HostFullIdentifier, when you build the project, all possible HostFullIdentifier options will be displayed in Output.

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


“What do we all stubs? Let's get the real code, the code! ”- in this section we will write a simple plugin that honestly reads and modifies the syntactic tree of the R # code. I wanted to show you something that does not duplicate the functionality of R #, while being both quite simple and applicable in practice. And that's what managed to come up. Suppose we have a method that returns a List type, and for some reason we want to quickly replace in the return statement the value null with the empty collection corresponding to the method signature. For example:


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

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

We got a special case of the Null Object pattern. Of course, I agree that null and the empty collection differ semantically and one could talk about the pros / cons of this approach, but this is not part of the objectives of this article. Therefore, we turn to the technical implementation. You may notice that the source code is correct (using'and omitted) - from the point of view of the compiler and R # there is no need to issue even a warning here. Therefore, the Quick-Fix mechanism discussed above will not work for us, and we use Context Action - a much more flexible tool that allows you to assign user actions to almost any part of the 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"; } 

Context Action visibility is managed in the same way as Quick-Fix by overriding the IsAvailable method:
  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; } 

Determining that we are inside a method is quite simple (see code). Next, we need to determine the following:


Checking the return:
ReturnsNullOrEmpty ()

  private bool ReturnsNullOrEmpty() { var returnStatement = Provider.GetSelectedElement<IReturnStatement>(false); if (returnStatement != null) { ICSharpExpression value = returnStatement.Value; return value == null || value.ConstantValue.IsPureNull(CSharpLanguage.Instance); } return false; } 


The return type of the method is more complicated. Check whether the returned type is a generic List, or inherited from it (for other collections, the principle is the same):
HasCorrectReturnType () - option number 1

 private static bool HasCorrectReturnType(IMethodDeclaration method) { IDeclaredType declaredType = method.DeclaredElement.ReturnType as IDeclaredType; if (declaredType == null || declaredType.IsVoid()) return false; ISubstitution sub = declaredType.GetSubstitution(); if (sub.IsEmpty()) return false; IType parameterType = sub.Apply(sub.Domain[0]); IMethod declaredElement = method.DeclaredElement; IType realType = declaredElement.Type(); var predefinedType = declaredElement.Module.GetPredefinedType(); ITypeElement generic = predefinedType.GenericList.GetTypeElement(); IType sampleType = EmptySubstitution.INSTANCE .Extend(generic.TypeParameters, new IType[] { parameterType }) .Apply(predefinedType.GenericList); bool isGenericList = realType.IsImplicitlyConvertibleTo(sampleType, new CSharpTypeConversionRule(declaredElement.Module)); return isGenericList; } 


There is a simpler option, but not so flexible - compare types by CLR name:
HasCorrectReturnType () - option number 2

 private static bool HasCorrectReturnType(IMethodDeclaration method) { IDeclaredType declaredType = method.DeclaredElement.ReturnType as IDeclaredType; if (declaredType == null || declaredType.IsVoid()) return false; ITypeElement element = declaredType.GetTypeElement(); string fullName = element.GetClrName().FullName; bool isGenericList = fullName == "System.Collections.Generic.List`1"; return isGenericList; } 


Finally, the most delicious thing is to replace null with the new List <Foo> ():
ReplaceType ()
 protected override Action<ITextControl> ExecutePsiTransaction(ISolution solution, IProgressIndicator progress) { ReplaceType(); return null; } private void ReplaceType() { IMethodDeclaration method = Provider.GetSelectedElement<IMethodDeclaration>(); IType type = method.DeclaredElement.ReturnType; string typePresentableName = type.GetPresentableName(CSharpLanguage.Instance); CSharpElementFactory factory = CSharpElementFactory.GetInstance(Provider.PsiModule); string code = $"new {typePresentableName}()"; ICSharpExpression newExpression = factory.CreateExpression(code); IReturnStatement returnStatement = Provider.GetSelectedElement<IReturnStatement>(false); returnStatement.SetValue(newExpression); } 


I didn’t intend to comment on the above listings to vent all the pain in one place: writing or parsing the code in R # API ... not easy. There are gaps in the documentation, few examples are available, even the XML comments on the code are missing. You have to suffer with each developed method and actively use the debugger. I emphasize that the debugger when working with R # API becomes the most important tool for traveling through the syntax tree. Serious help is also a search for key classes in GitHub - a certain number of code samples can be found.

6. Functional testing of plugins using R # API


One of the great features of the R # API is the ability to implicitly deploy an instance of R # in memory, feed it a piece of text (by applying the Quick-Fix or Context Action being tested to the text), and then compare the converted text with the expected one. And all this by writing a small amount of code comparable to writing the simplest unit tests. By the way, R # uses NUnit. Go!

Add to the solution with our plugin another project that will contain tests. Install the JetBrains.ReSharper.SDK.Tests package. To create a minimal working example, you need to create the following file structure in the project:


This structure is not canonical , but easier to deploy and closer to the traditional structure of the C # solution. The nuget.config and TestEnvironment.cs files are required:
nuget.config

 <?xml version="1.0" encoding="utf-8" ?> <configuration> <config> </config> <packageSources> <clear /> <add key="jb-gallery" value="http://jb-gallery.azurewebsites.net/api/v2/curated-feeds/TestNuggets/" /> <add key="nuget.org" value="http://www.nuget.org/api/v2/" /> </packageSources> <disabledPackageSources> <clear /> </disabledPackageSources> <packageRestore> <add key="enabled" value="True" /> <add key="automatic" value="False" /> </packageRestore> </configuration> 


TestEnvironment.cs
 [assembly: RequiresSTA] [ZoneDefinition] public class TestEnvironmentZone : ITestsZone, IRequire<PsiFeatureTestZone>{ } [SetUpFixture] public class ReSharperTestEnvironmentAssembly : ExtensionTestEnvironmentAssembly<TestEnvironmentZone> { } 


With the preparations finished, we proceed directly to writing tests. Classes containing Context Action tests 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"); } } 

You have already seen the Test01.cs file in the screenshot; this is the source file with the code to which our Context Action will be applied. Test01.cs.gold - a kind of “expected output”, the expected code after applying the Context Action. A test is considered passed if applying the Context Action to Test01.cs, we get Test01.cs.gold.
When writing your own tests, you need to determine the values ​​of the ExtraPath and RelativeTestDataPath properties, setting them equal to the name of the folder containing the source and the gold file. There is no need to compile these files, so they need to boldly set BuildAction: None and add R # to the blacklist to get rid of imaginary error messages. As for the contents of the source and gold files, for the Context Action it is necessary to specify the position of the caret when the context action is called, this is done using the {caret} instruction:
 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 (source file + Context Action)! = Gold-file, the test will fall, and a tmp-file will be created in the same folder containing the actual result of applying the Context Action.

We run the test for execution, and ... I will immediately go to the list of problems and ways to solve them:

  1. The exception “file does not exist” is the simplest, we check the folder structure and the corresponding values ​​of the properties ExtraPath, RelativeTestDataPath;
  2. The test fails with the exception in SetUpFixture - we check the location and contents of the nuget.config and TestEnvironment.cs files;
  3. Exception “The test output differs from the gold file” - we study the created tmp-file, run the test with a debugger;
  4. The tmp file instead of the code contains one line “NOT AVAILABLE” - perhaps there is no caret symbol {caret} inside the source file, or Context Action threw an exception while working;
  5. The most interesting case is that the test always passes successfully, regardless of the contents of the source and gold files. At the same time falls - if you delete the source file. I came across such an unpleasant behavior when I inherited a test for Context Action from ContextActionTestBase and did not set the ExtraPath property.


That's all. A complete, workable example is available on GitHub . I hope that it will take you much less time to develop my first plug-in than it took me =)

UPD: useful links:
ReSharper DevGuide
JetBrains Developer Community -> ReSharper Open API / SDK
Google Groups: resharper-plugins

Source: https://habr.com/ru/post/270155/


All Articles