Roslynを使用したコードアクションの実装

Roslyn Services APIを使用すると、Visual Studioでコードの問題を見つけて修正する拡張機能を簡単に実装できます。 Roslyn Services APIは、 Roslyn CTPの一部として利用可能です。



この投稿では、EnumerableのCount()メソッドの呼び出しを検出するVisual Studioの拡張機能を実装します。その後、someSequence.Count()> 0など、ゼロより大きい等値を結果で確認します。コードの問題は、Count ()は、結果を返す前にシーケンス全体を実行する必要があります。 この場合のより正しいアプローチは、Enumerable.Any()メソッドを呼び出すことです。



これを修正するために、問題を検出するCodeIssueProviderと、必要に応じて条件をEnumerable.Any()の呼び出しに置き換えるCodeActionを実装します。 つまり CodeActionはsomeSequence.Count()> 0のようなものをsomeSequence.Any()に変更します。



満たしたい追加の条件がいくつかあります。まず、式を反転し、0 <someSequence.Count()として書き込むことができます。 次のケースは、> 0ではなくtype> = 1のレコードです。これは論理的には以前と同じです。 両方の場合に機能する拡張機能が必要です。



明らかに、Count()シグネチャを持つすべての呼び出しを変更するのではなく、Enumerableで定義されたIEnumerableからの拡張メソッドに関連する場合にのみ変更します。



はじめに


Roslyn CTPには、APIを使い始めるのに役立つ一連のテンプレートが付属しています。 はじめに、RoslynテンプレートからCode Issueタイプの新しいプロジェクトを作成します。 プロジェクトにReplaceCountWithAnyという名前を付けましょう。







テンプレートは、文字「a」を含む単語を強調表示する単純な作業プロバイダーを生成します。 実際の例を見るために、テンプレートによって作成されたプロジェクトをビルドして実行します。 これにより、拡張機能を有効にしてVisual Studioの新しいインスタンスが起動します。 起動したばかりのVisual Studioからコンソールアプリケーションを作成し、名前空間、クラスなどのキーワードがどのように表示されるかを確認します。 拡張機能によって下線が引かれています。







この例はVisual Studioの拡張機能ほど有用ではありませんが、独自の拡張機能の実装を開始するために必要なすべてを準備します。 生成されたGetIssueメソッドのコンテンツを置き換えるだけです。 GetIssuesには3つのオーバーロードがあることに注意してください。 パラメーターの1つがCommonSyntaxNode型であるオーバーロードを処理します。 この場合、残りの2つのオーバーロードはそのままにしておくことができます。



生成されたCodeIssueProviderクラスはICodeIssueProviderインターフェイスを実装し、ExportSyntaxNodeCodeIssueProvide属性で装飾されています。 これにより、Visual Studioはこのタイプを、ICodeIssueProvideインターフェイスによって提供されるコントラクトを含む拡張機能としてインポートできます。



GetIssuesを実装します


GetIssuesメソッドは構文構文ごとに呼び出されるため、最初にすべきことは、興味のないノードを取り除くことです。 someSequence.Count()> 0型の構造が必要なので、BinaryExpressionSyntax型のノードのみが必要です。 ExportSyntaxNodeCodeIssueProvide属性を介してタイプのリストを提供することにより、特定のノードに対してのみプロバイダーを使用するようにVisual Studioに指示できます。 そのため、以下に示すように属性を更新します。

[ExportSyntaxNodeCodeIssueProvider("ReplaceCountWithAny", LanguageNames.CSharp, typeof(BinaryExpressionSyntax))] class CodeIssueProvider : ICodeIssueProvider ...
      
      





これにより、GetIssuesメソッドでCommonSyntaxNodeノードをBinaryExpressionSyntax型に安全にキャストできます。



処理するケースを強調表示するには、Enumerable.Count()の呼び出しの式の一部と、比較自体の式の一部を確認する必要があります。 検証データを補助メソッドに分離し、GetIssues実装が次のようになるようにします。

 public IEnumerable<CodeIssue> GetIssues(IDocument document, CommonSyntaxNode node, CancellationToken cancellationToken) { var binaryExpression = (BinaryExpressionSyntax)node; var left = binaryExpression.Left; var right = binaryExpression.Right; var kind = binaryExpression.Kind; if (IsCallToEnumerableCount(document, left, cancellationToken) && IsRelevantRightSideComparison(document, right, kind, cancellationToken) || IsCallToEnumerableCount(document, right, cancellationToken) && IsRelevantLeftSideComparison(document, left, kind, cancellationToken)) { yield return new CodeIssue(CodeIssue.Severity.Info, binaryExpression.Span, string.Format("Change {0} to use Any() instead of Count() to avoid " + "possible enumeration of entire sequence.", binaryExpression)); } }
      
      







返すCodeIssueクラスのインスタンスは、エラー、警告、または情報などの問題のレベル、コードの一部を強調表示するために使用される説明、およびユーザーに問題を説明するテキストを示します。



ヘルパーメソッド


ここで、GetIssuesで使用されるヘルパーメソッドに注目します。 IsCallToEnumerableCountメソッドは、検討している式の一部が何らかのシーケンスでCount()メソッドの呼び出しである場合にtrueを返します。 もう一度お知らせします。まず、不必要な式をフィルタリングすることから始めます。



まず、式はメソッド呼び出しでなければなりません。 この場合、式プロパティから必要な呼び出しを取得します。 そのため、デザインがsomeSequence.Count()> 0のように見える場合、Count()の部分があります。 しかし、それがEnumerable型に属しているかどうかを確認するにはどうすればよいですか?



このような質問に答えるには、セマティックモデルを要求する必要があります。 幸いなことに、GetIssuesメソッドのパラメーターの1つは、プロジェクトとソリューション内のドキュメントであるIDocumentです。 必要に応じて、SymbolInfo自体から既にセマンティックモデルを取得できます。

SymbolInfoを使用すると、メソッド呼び出しが目的の[Enumerable.Count()]に属しているかどうかを確認できます。 Count()は拡張メソッドであるため、操作方法は若干異なります。 C#では、拡張メソッドを型の一部として呼び出すことができます。 セマンティックモデルは、元の型を参照して、MethodSymbolクラスのConstructedFromプロパティを通じてこの情報を提供します。 これを少し簡単にする機会がありますので、API名にご注目ください。



私たちがやるべきことは、拡張メソッドのタイプを示すことだけです。 Enumerableと一致する場合、Enumerable.Count()の呼び出しが見つかりました。



実装は次のとおりです。

 private bool IsCallToEnumerableCount(IDocument document, ExpressionSyntax expression, CancellationToken cancellationToken) { var invocation = expression as InvocationExpressionSyntax; if (invocation == null) { return false; } var call = invocation.Expression as MemberAccessExpressionSyntax; if (call == null) { return false; } var semanticModel = document.GetSemanticModel(cancellationToken); var methodSymbol = semanticModel.GetSemanticInfo(call, cancellationToken).Symbol as MethodSymbol; if (methodSymbol == null || methodSymbol.Name != "Count" || methodSymbol.ConstructedFrom == null) { return false; } var enumerable = semanticModel.Compilation.GetTypeByMetadataName( typeof(Enumerable).FullName); if (enumerable == null || !methodSymbol.ConstructedFrom.ContainingType.Equals(enumerable)) { return false; } return true; }
      
      





先に進む前に、バイナリ式の別の部分の比較の正確性について式を確認する必要もあります。 これは、ヘルパーメソッドIsRelevantRightSideComparisonおよびIsRelevantLeftSideComparisonの仕事です。



以下に実装を示します。

 private bool IsRelevantRightSideComparison(IDocument document, ExpressionSyntax expression, SyntaxKind kind, CancellationToken cancellationToken) { var semanticInfo = document.GetSemanticModel(cancellationToken). GetSemanticInfo(expression); int? value; if (!semanticInfo.IsCompileTimeConstant || (value = semanticInfo.ConstantValue as int?) == null) { return false; } if (kind == SyntaxKind.GreaterThanExpression && value == 0 || kind == SyntaxKind.GreaterThanOrEqualExpression && value == 1) { return true; } return false; } private bool IsRelevantLeftSideComparison(IDocument document, ExpressionSyntax expression, SyntaxKind kind, CancellationToken cancellationToken) { var semanticInfo = document.GetSemanticModel(cancellationToken). GetSemanticInfo(expression); int? value; if (!semanticInfo.IsCompileTimeConstant || (value = semanticInfo.ConstantValue as int?) == null) { return false; } if (kind == SyntaxKind.LessThanExpression && value == 0 || kind == SyntaxKind.LessThanOrEqualExpression && value == 1) { return true; } return false; }
      
      





はい、これらはほとんど同じですが、唯一の違いは、両方の比較オプションがチェックされることと、値自体の正確さです。したがって、Count()> = 0のようなものを強調する必要はありません。



CodeIssueProviderのテスト


現時点では、当社のプロバイダーは当社にとって関心のある問題を検出することができます。 Visual Studioの新しいインスタンスと含まれる拡張機能とともにプロジェクトをコンパイルして実行します。 コードを追加すると、Enumerable.Count()の呼び出しには正しく下線が引かれますが、Count()のシグネチャを持つ他のメソッドの呼び出しには下線が引かれません。







次のステップは、問題を解決するためのアクションを提供することです。



CodeAction


アクションを実装するには、ICodeActionインターフェイスを実装するクラスが必要です。 ICodeActionは、アクションの説明とアイコン、および現在の構文ツリーを変換するリビジョンを返す唯一のGetEditメソッドを定義するシンプルなインターフェイスです。 それでは、CodeActionクラスのコンストラクターから始めましょう。

 public CodeAction(ICodeActionEditFactory editFactory, IDocument document, BinaryExpressionSyntax binaryExpression) { this.editFactory = editFactory; this.document = document; this.binaryExpression = binaryExpression; }
      
      





見つかった問題ごとに、CodeActionクラスの新しいインスタンスが作成されるため、便宜上、一部のパラメーターを省略し、コンストラクター自体を変更します。 これには、新しく作成された構文ツリーの変換を作成するためのICodeActionEditFactoryの実装が必要です。 Roslynプロジェクトの構文ツリーは不変なので、新しいツリーを返すことが変更を行う唯一の方法です。 幸いなことに、Roslynは可能な限りツリーを再利用しようとするため、冗長な構文ノードの作成を防ぎます。



さらに、構文ツリー、プロジェクト、およびソリューションへのコードアクセスを提供するドキュメントと、置換する構文ノードへのリンクが必要です。



それで、GetEditメソッドに近づきました。 ここで、Any()メソッドを呼び出して、検出されたバイナリ式を新しい式で置き換える変換を作成します。 新しいノードの作成は、単純なヘルパーメソッドGetNewNodeに委任されます。 両方のメソッドの実装を以下に示します。

 public ICodeActionEdit GetEdit(CancellationToken cancellationToken) { var syntaxTree = (SyntaxTree)document.GetSyntaxTree(cancellationToken); var newExpression = GetNewNode(binaryExpression). WithLeadingTrivia(binaryExpression.GetLeadingTrivia()). WithTrailingTrivia(binaryExpression.GetTrailingTrivia()); var newRoot = syntaxTree.Root.ReplaceNode(binaryExpression, newExpression); return editFactory.CreateTreeTransformEdit( document.Project.Solution, syntaxTree, newRoot, cancellationToken: cancellationToken); } private ExpressionSyntax GetNewNode(BinaryExpressionSyntax node) { var invocation = node.DescendentNodes(). OfType<InvocationExpressionSyntax>().Single(); var caller = invocation.DescendentNodes(). OfType<MemberAccessExpressionSyntax>().Single(); return invocation.Update( caller.Update(caller.Expression, caller.OperatorToken, Syntax.IdentifierName("Any")), invocation.ArgumentList); }
      
      





Roslyn構文ツリーは元のコードとまったく同じであるため、ツリー内の各ノードには余分なスペースとコメントを含めることができます。 つまり ノード自体を変更するときに、コメントとコード構造とともに元のノードを保存します。 これを行うには、拡張メソッドWithLeadingTriviaおよびWithTrailingTriviaを呼び出します。



GetNewNodeメソッドはCount()メソッドのパラメーターのリストを保存するため、シーケンス内の特定の要素をカウントするためにラムダ式を介して拡張メソッドが呼び出された場合、Any()に置き換えられます。



まとめると


アクションを有効にするには、CodeIssueProviderクラスのGetIssuesメソッドを更新して、各CodeIssueのCodeActionインスタンスを返す必要があります。 コードの問題のある各セクションにはいくつかのアクションがあり、ユーザーはそれらの中から選択できます。 この場合、次のように単一のアクションを返します。



GetIssuesメソッドの更新された部分は次のとおりです。

 yield return new CodeIssue(CodeIssue.Severity.Info, binaryExpression.Span, string.Format("Change {0} to use Any() instead of Count() to avoid " + "possible enumeration of entire sequence.", binaryExpression), new CodeAction(editFactory, document, binaryExpression));
      
      





プロジェクトを再構築して実行し、ダウンロードした拡張機能でVisual Studioの新しいインスタンスを起動します。 これで、問題領域に、コードを修正するためのオプションを含むドロップダウンリストが表示されます。







そのため、コードの改善に役立つVisual Studioの拡張機能を実装しました。




All Articles