目的:R#の最も簡単なプラグインを作成、テスト、デプロイし、カスタムのクイック修正とコンテキストアクションを含みます。
記事の概要:
- 開発環境のセットアップ
- 例1:最も単純なスタブ拡張
- プラグインのインストール
- デバッグのヒント
- 例2:R#APIを使用したコード変更
- R#APIを使用したプラグインの機能テスト
キャスト:
Visual Studio 2015
ReSharper Ultimate 10
キャットの下で私が招待する利害関係者
1.開発環境のセットアップ
コードを記述するVisual Studioのメインインスタンスの作業を妨げないように、将来のプラグイン用に別の「エコシステム」をすぐに準備するのが最善です。 いわゆる
ダウンロードすることから始め
ます 「チェックビルド」-拡張診断情報を備えたR#Ultimateのアセンブリ。その他は自然と同じです。
また、Visual Studio Experimentalインスタンスも必要です。これは、ウィンドウの場所からインストールされた拡張機能までのすべてのユーザー設定を含む一種の「プロファイル」です(そうです!)。 プロファイルは設定レベルで正確に相互に分離されており、Studioの実行可能ファイルはどこにもコピーされません。 VS2015では、CreateExpInstance.exeユーティリティを使用してプロファイルを管理できますが、さらに簡単な方法があります。 以前にインストールしたチェックビルドのインストーラーを起動し、(オプション-実験ハイブ)に移動して、開発されたプラグインのインストールに使用される新しいプロファイルの名前を入力します。巧妙なR#がこのプロファイルを作成し、そこにインストールします。 そうです、R#の独自のバージョンを含む各プロファイルに一連の拡張機能をインストールすることができます。これにより、複数のバージョンのプラグインを一度にテストできます。 既に述べたように、プロファイルは互いに独立しているため、Visual Studioの作業プロファイルは影響を受けません。
新しいプロファイルでStudioを起動するには、次の形式のショートカットが必要です。
「X:\ Microsoft Visual Studio 14.0 \ Common7 \ IDE \ devenv.exe」/ rootSuffix YourHive /ReSharper.Internal
ここで、YourHiveは以前に作成されたプロファイルの名前であり、/ ReSharper.Internalパラメーターは
開発モードでR#を起動し、プラグイン内で生成された例外の通知などの便利な機能を有効にします。
2.例1:単純なプラグイン拡張
R#は、クイックフィックス、コンテキストアクション、リファクタリングなど、コードを変更および生成するためのさまざまな方法を提供します。 Quick-Fixを実装するのが最も簡単であるように思えたので、それから始めます。 クイックフィックスは、「使用されていない変数を削除」、「コンストラクターからプロパティを初期化する」など、Alt + Enterでポップアップするメニューに黄色/赤色のアイコンで表示されるR#ユーザーに知られているコマンドです。
そのため、スタジオのメインインスタンス(実験的ではない)を開き、新しいクラスライブラリプロジェクトを作成します。 R#SDKをインストールします。
インストールパッケージJetBrains.ReSharper.SDK
R#、R#SDK、および下位互換性のバージョンではなく、その不在執筆時点で、JetBrains.ReSharper.SDKの最新リリースバージョンはバージョン10.0.20151101.194233で、R#10に対応
しています 。JetBrainsは製品のメジャーバージョンとマイナーバージョン間の互換性を
提供しないため 、SDK 9.1でビルドされたプラグインの動作はR#9.2では保証されませんなど 以降、SDK 10が使用されます。これは、R#10のみをサポートし、R#9.2で正しくインストールおよび動作できないことを意味します。 同時に、SDK 9.2の記事で説明されているすべてのコードを再構築するための障害がなく、それによってR#9.2で動作するプラグインを取得できます-検証済みです。
プロジェクトに次のクラスを配置しましょう。
[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; } }
Quick-Fixには、SomeError / SomeWarning型の入力パラメーターを持つ少なくとも1つの類似したコンストラクターが必要であることに注意してください。 パラメーターのタイプにより、ドロップダウンメニューで使用可能なQuick-Fixコードのエラーが決まります。 初期化されていないローカル変数を使用すると、Quick-Fixが利用可能になります。
R#SDKは、JetBrains.ReSharper.Daemon.CSharp.Errors名前空間で使用可能な数百のエラークラスを定義しています。 コンパイルエラーに対応するクラスには、名前にpostfix Errorがあり、重要ではないさまざまな改善点-Warning postfixがあります。 次のセクション全体では、Quick-Fixの展開に苦しみます。
3.プラグインのインストール
プロジェクトにもう1つのクラスを追加します。
[ZoneMarker] public class ZoneMarker { }
ゾーンは、バージョン9.0で導入された新しいR#SDK機能で
あり、現在も開発中です。 含む、ゾーンの助けを借りて、開発中の拡張機能がR#プラットフォームのどの製品に向けられているかが示されます。 幸いなことに、今のところは、スタブクラスに限定する必要があります。
重要:ZoneMarkerは、以前に作成されたSimpleFixクラスと同じ名前空間に存在する必要があります。
次のニュアンスは、R#9+でのプラグインの配布とインストールがNuGetパッケージを介してのみ行われることです。 正しいパッケージを作成するには、拡張子が.nuspecのファイルと次の内容をプロジェクトに追加します。
<?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>
重要なポイント:
1. Waveパッケージへの依存が必要です。 WaveはR#プラットフォームの新しい配布モデルであり、R#に加えて、dotPeek、dotTraceなどが含まれます。
ReSharper 9.0-Wave 1.0;
ReSharper 9.1-Wave 2.0;
ReSharper 9.2-Wave 3.0;
ReSharper 10.0-Wave 4.0;
<dependency>タグにリストされていないバージョンのR#の場合、プラグインはExtention Managerに存在しないため、インストールに使用できません。 複数のバージョンを指定するには、[A、B)という形式のレコードを使用する必要がありますが、「[」は「包括的」などを意味します。
2. <id>タグで示されるプラグイン名にはドットを含める必要があります。 それだけでなく、それだけです!
推奨される形式は「Company.Package」です。
NuGet.exeをインストールし
たら、パッケージマネージャーコンソールを開き、次のコマンドを実行します。
nugetパック「PaperSource.ReSharper.HelloWorld \ package.nuspec」
完成した.nupkgファイルがソリューションのフォルダーに表示されます(または-OutputDirectoryパラメーターを使用して、必要なフォルダーにパッケージを作成します)。 「問題:libフォルダー外のアセンブリ」などの警告は無視できます。 プラグインをインストールするには、Visual Studioの実験的なインスタンスで、ReSharper-オプション-エクステンションマネージャーに移動し、.nupkgファイルがあるフォルダーへのパスを指定します。
真実の瞬間:ReSharper-Extention Managerを開き、検索してプラグインを名前で探し、インストールします。 すべてが正しく行われた場合-SimpleFixが利用可能になります。
既知のインストールの問題:
- プラグインは拡張マネージャーにありません。
- プラグインはインストールできますが、Quick-Fixは適切な場所に表示されません。
どちらの場合も、.nuspecファイルを確認してから公式の
トラブルシューティングガイドを読むことから始めることをお勧めします。 ちなみに、いわゆる インストーラーはおおよそのアドレス%LOCALAPPDATA%\ JetBrains \ Shared \ v02 \ InstallerLogXXXにログを記録しますが、毎回役に立たないことがわかりました。 インストールが成功した場合でも、スローされた例外に関する多くの情報がログに書き込まれ、インストールエラーの原因を理解することは完全に困難です。
4.デバッグ、役に立つヒント
デバッガーを使用して拡張コードをウォークスルーするには、実験インスタンスのdenenv.exeプロセスをDebug-Attach to Processで結合するだけで十分です。
すべてのプラグインは、Extension Managerを通じてインストールする必要があります。 コードを変更する場合、これらの変更を展開するための保証された方法は、同様の更新/再インストールです。 ただし、このルールには例外があります:新しいファイル/クラスがコードに追加されず、スタジオとの統合ポイントに変更がなかった場合(たとえば、既存のクイックフィックスのエラーのタイプが変更されなかった場合)、プラグインを再インストールする必要はありません。 プラグインアセンブリを新しいバージョンに置き換えるだけで十分です。 R#は、プラグインが含まれるアセンブリを腸の奥深くに保存します。再び飛び込むのを防ぐには、コンパイル後にアセンブリを「必要に応じて」コピーするMSBuildターゲットを使用する価値があります。 これを行うには、.csprojファイルに次のコードを配置します。
<PropertyGroup> <HostFullIdentifier>ReSharperPlatformVs14YourHive</HostFullIdentifier> </PropertyGroup>
<HostFullIdentifier>タグは手動で入力され、次の形式になります:{Host} {Visual Studioバージョン} {Visual Studioインスタンス名}。 リストオプションは、R#Ultimate、VS 2015、およびYourHiveというプロファイルで機能します。 無効なHostFullIdentifierを指定すると、出力でプロジェクトをビルドするときに、HostFullIdentifierのすべての可能なバリアントが表示されます。
5.例2:R#APIを使用したコード変更
「あらゆる種類のスタブとは何ですか? 実際のコード、コードに来てください!」-このセクションでは、R#コードの構文ツリーを正直に読み取り、変更する簡単なプラグインを作成します。 R#の機能を複製しないものをお見せしたかったのですが、実際には非常にシンプルで適用可能です。 そして、それが私たちが思いついたものです。 List型を返すメソッドがあり、何らかの理由でreturnステートメントのnull値をメソッドシグネチャに対応する空のコレクションにすばやく置き換えたいとします。 例:
それは:
public List<object> FooText() { return null; }
次のようになりました:
public List<object> FooText() { return new List<object>(); }
Null Objectパターンの特殊なケースがあります。 もちろん、nullコレクションとemptyコレクションは意味的に異なることに同意し、このアプローチの長所/短所について話すこともできますが、これはこの記事の目的ではありません。 そのため、技術的な実装に進みます。 ソースコードが正しいことに気付くかもしれません( 'を使用して省略してください)-コンパイラとR#の観点からは、ここで警告すらする必要はありません。 したがって、上記のQuick-Fixメカニズムは私たちには適していません。コンテキストアクション-カスタムアクションをコードのほとんどすべての部分に割り当てることができる、はるかに柔軟なツールを使用します。
[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"; }
コンテキストアクションの可視性は、IsAvailableメソッドをオーバーライドすることにより、Quick-Fixと同様に制御されます。
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; }
メソッド内にいると判断するのは非常に簡単です(コードを参照)。 次に、以下を決定する必要があります。
- 適切な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; }
メソッドの戻り値の型はより複雑です。 返された型が一般的なリストであるか、それから継承されているかを確認しましょう(原則は他のコレクションでも同じです):
HasCorrectReturnType()-オプション番号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; }
より単純なオプションがありますが、それほど柔軟ではありません-CLR名で型を比較します。
HasCorrectReturnType()-オプション番号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; }
最後に、最良の部分はnullを新しいリスト<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); }
R#APIでコードを記述したり分解したりするのは簡単ではありません。1つの場所にすべての痛みを注ぎ込むために、上記のリストに故意にコメントしませんでした。 ドキュメントにはギャップがあり、利用可能な例はほとんどありません。コードに関するXMLコメントもありません。 開発された各メソッドに苦しみ、デバッガーを積極的に使用する必要があります。 私は強調します-R#APIを操作するときのデバッガーは、構文ツリーを移動するための最も重要なツールになりつつあります。 GitHubでのキークラスの検索は、深刻な助けにもなります。一定数のコードサンプルを見つけることができます。
6. R#APIを使用したプラグインの機能テスト
R#APIの優れた機能の1つは、R#インスタンスをメモリに暗黙的にデプロイし、テスト済みのQuick-Fixまたはコンテキストアクションをテキストに適用することでテキストをフィードし、変換されたテキストを予想されるテキストと比較する機能です。 そして、これはすべて、最も単純な単体テストを書くことに匹敵する、少量のコードを書くことで実現します。 ところで、R#はNUnitを使用します。 行こう!
プラグインを使用して、テストを含む別のプロジェクトをソリューションに追加します。 JetBrains.ReSharper.SDK.Testsパッケージをインストールします。 最小限の作業例を作成するには、プロジェクトに次のファイル構造を作成する必要があります。
この構造は
標準的ではありませんが、デプロイが簡単で、従来のC#ソリューション構造に近くなっています。 nuget.configおよびTestEnvironment.csファイルが必要です。
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> { }
準備が完了したら、テストの作成に直接進みます。 コンテキストアクションテストを含むクラスは、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ファイルは、コンテキストアクションが適用されるコードを含むソースファイルです。 Test01.cs.goldは一種の「期待される出力」であり、コンテキストアクションの適用後に予想されるコードです。 コンテキストアクションをTest01.csに適用すると、テストは合格と見なされ、Test01.cs.goldが取得されます。
独自のテストを作成する場合、ExtraPathプロパティとRelativeTestDataPathプロパティの値を決定し、ソースとゴールドファイルを含むフォルダーの名前と同じ値に設定する必要があります。 これらのファイルをコンパイルする必要はありません。したがって、架空のエラーメッセージを取り除くために、BuildAction:Noneを安全に設定し、無視するR#を追加する必要があります。 ソースファイルとゴールドファイルのコンテンツに関しては、コンテキストアクションの場合、コンテキストアクションが呼び出されたときのキャレットの位置を示す必要があります。これは{caret}命令を使用して行われます。
using System; using System.Collections.Generic; namespace N { public class C { public List<int> FooMethod() { return {caret}null; } } }
対応するゴールドファイル:
using System; using System.Collections.Generic; namespace N { public class C { public List<int> FooMethod() { return { caret}new List<int>(); } } }
テスト中に(ソースファイル+コンテキストアクション)!=ゴールドファイルの場合、テストは失敗し、コンテキストアクションを適用した実際の結果を含む同じフォルダーにtmpファイルが作成されます。
テストを実行するために実行します。...問題のリストとそれらを解決する方法にすぐに行きます。
- 例外「ファイルが存在しません」は最も単純な例外です。フォルダ構造と、ExtraPath、RelativeTestDataPathプロパティの対応する値を確認します。
- SetUpFixtureの例外でテストがクラッシュします-nuget.configファイルとTestEnvironment.csファイルの場所と内容を確認します。
- 例外「テスト出力はゴールドファイルと異なります」-作成されたtmpファイルを調べ、デバッガでテストを実行します。
- コードの代わりに、tmp-fileには「NOT AVAILABLE」という1行が含まれています。おそらく、ソースファイル内にキャレットシンボルがないか、操作中にコンテキストアクションが例外をスローしました。
- 最も興味深いケースは、ソースファイルとゴールドファイルの内容に関係なく、テストが常に成功することです。 元のファイルを削除すると、クラッシュします。 ContextActionTestBaseからContext Actionのテストを継承し、ExtraPathプロパティを設定しなかったときに、この不快な動作が発生しました。
以上です。 完全な実例が
GitHubにあります 。 最初のプラグインを開発する時間が、私にかかった時間よりはるかに短くなることを願っています=)
UPD:便利なリンク:ReSharper DevGuideJetBrains開発者コミュニティ-> ReSharper Open API / SDKGoogleグループ:resharper-plugins