Roslynで作るC#コンパイラー拡張(2)

Roslynで作るC#コンパイラー拡張(2)

.NETコンパイラープラットフォーム拡張の作り方

2016年7月27日

C# 6.0と同時にリリースされた.NETコンパイラープラットフォーム「Roslyn」。そのコンパイラー拡張の作り方を解説する連載の第2回。

  • このエントリーをはてなブックマークに追加

 連載第2回は、SDKが提供しているVisual Studioのプロジェクトテンプレートを見ながら、Analyzer with Code Fix(=Diagnostic with Code Fix)とCode Refactoringのプロジェクトの仕組みを紹介する。

Analyzer with Code Fixプロジェクトの作成

 前回の開発環境の項で説明した通り、.NET Compiler Platform SDKを事前にVisual Studio 2015もしくは次バージョン(現在はプレビュー版)の“Visual Studio 15”にインストールしておく必要がある。今回はVisual Studio 2015 Update 3を利用している。

 新しいプロジェクトの作成で図1のように、[Visual C#]-[Extensibility]-[Analyzer with Code Fix (NuGet + VSIX)]テンプレートを選ぶ。名前は「RoslynDemo」とした。

図1 新しいプロジェクトの作成で、Analyzer with Code Fixを選んだ状態

テンプレートが表示されない場合は、適切な.NET Frameworkのバージョン(4.5.2以降)が選択されているかを確認してほしい。

 上記の内容で作成すると、図2のように3つのプロジェクトが生成される。

図2 Analyzer with Code Fixで生成された3つのプロジェクト
図2 Analyzer with Code Fixで生成された3つのプロジェクト

 各プロジェクトの内容は以下の通りだ。

  • RoslynDemo入力したプロジェクト名): CodeFixの本体となるプロジェクト
  • RoslynDemo.Test入力したプロジェクト名.Test): CodeFixの単体テストを行うためのプロジェクト
  • RoslynDemo.Vsix入力したプロジェクト名.Vsix): 開発中のテストのためにVSIX(=Visual Studio Integration Extension: VS拡張機能)で実行するためのプロジェクト

 このうち、RoslynDemoプロジェクト内にある2つの.csファイル(CodeFixProviderクラスとAnalyzerクラス)がAnalyzer with Code Fixの中心となる処理を記述している。この2つのクラスは次章以降で詳細に説明するので、NuGetライブラリに関するファイルを先に説明しておこう。

 RoslynDemoプロジェクトにあるDiagnosctics.nuspecファイルは、このAnalyzer with Code FixをNuGetライブラリとして作成するときに使用するファイルである。基本的には、サンプル値が入っている場所を適切な値に書き換えた後、プロジェクトをリリースビルドすると、.nupkgファイルも一緒に生成される。あとは、この.nupkgファイルをNuGet.orgなどのリポジトリに登録することで、NuGetライブラリとして公開できる。詳しくはNuspec Referenceを参考にしてほしい。

 また、toolsフォルダー以下にある2つの.psファイルもNuGetライブラリに含めるスクリプトである。

NuGetライブラリの更新

 RoslynDemoプロジェクトはいくつかのNuGetライブラリ(例えばMicrosoft.CodeAnalysis.Commonなど)を参照している。これらのライブラリも機能追加が行われているため、バージョンアップを行いたいことがあるだろう。しかし、バージョンアップするには注意が必要だ。

 なぜなら、これらのライブラリはAnalyzer with Code Fixプロジェクトの新規作成時にNuGet経由でダウンロードされて参照に追加されるわけではなく、Visual Studio自体が最初から参照しているDLLに含まれているからである。つまりNuGetでバージョンアップした場合、Analyzerを開発・ビルドする際に参照するバージョンと、Analyzerをプロジェクトに追加して実行する際に参照するバージョンが食い違う可能性が出てくるということだ。

 従ってまず、バージョンを上げる場合は(次の参考画像を参照)、Visual Studioのどのバージョン以上をターゲットにするかを決めよう。決めたら、GitHubのRoslynのReleaseページを見て、対応するバージョンを探す。例えばリリースページを見ると[Visual-Studio-2015-Update-3-Micro-Update-1]という項目が見付かり、そのリンク先を見ると[version-1.3.2]とういタグが付いているのが分かる。この情報から、Visual Studio 2015 Update 3最新のパッチ(KB3165756)を当てた状態では、そのversion-1.3.2に対応していると判断できる。そのため、開発しているAnalyzerが動作する環境として「Visual Studio 2015 Update 3(さらに正確にはパッチKB316756を当てた状態)以上」をターゲットとするのであれば、参考画像にあるライブラリのバージョンを「1.3.2」まで上げることができる。

参考画像: NuGetライブラリのバージョンアップデート

NuGetパッケージマネージャーで[更新プログラム]を参照しているが、インストール済みのバージョンは「1.0.0」となっており、NuGet最新の安定版「1.3.2」までアップデート可能であることが分かる。

 これに基づき、実際にMicrosoft.CodeAnalysis.Common1.3.2にアップデートすると、依存している他のライブラリも同じバージョンにそろう。なお、そのバージョンより下のVisual Studioでは作成したAnalyzerが正常に動作しなくなるため注意が必要だ。

 なお、この連載で参照しているコードはGitHubでversion-1.3.2のタグのものを引用している。

VSIXによるデバッグ実行

 VSIXプロジェクトは主に開発中のデバッグ実行のために用意されている(が、もちろんAnalyzerを配布するためのVSIXの作成に使うこともできる)。このプロジェクトのおかげで、デバッグ実行で起動されるVisual Studioは、開発で現在起動しているVisual Studioとは別のインスタンスとなり、隔離された設定ファイルで動くようになる。

 ただし、1つのソリューション内で複数のAnalyzerを開発する場合、それぞれのデバッグ実行で起動するVisual Studioは同じインスタンスになり、つまりインストールされるAnalyzerが全てのAnalyzerプロジェクト間で共有されてしまう。これを避けたい場合は、Visual StudioがAnalyzerを読み込むフォルダーから該当のAnalyzerを削除するために、[ソリューションのクリーン]を行えばよい。

 もしくは、その読み込むフォルダーのパスが実行ユーザー別の%LOCALAPPDATA%\Microsoft\VisualStudio\14.0Roslyn\Extensions\となるので、その配下に存在する該当Analyzer用フォルダーを手動で直接削除してもよい。なお、このフォルダーパスにおける14.0RoslynRoslynの部分は、デバッグ開始時のコマンドライン引数に指定する/rootsuffixオプションの値によって変わるので注意してほしい。デフォルト値は図3にある通り、VSIXプロジェクトプロパティの[デバッグ]タブの[コマンドライン引数]に指定されている「Roslyn」という値である。例えばこれを別の値に変更してデバッグ実行すると、異なるフォルダーパスにVisual Studioインスタンス用の設定ファイルやVisual Studio拡張機能となるAnalyzerが生成されるのが確認できる。

図3 RoslynDemo.VSIXプロジェクトプロパティの[デバッグ]タブで指定されている/rootsuffixオプションの値

DiagnosticAnalyzerクラス

 最初に「<プロジェクト名>Analyzer」という名前で生成されるクラスを見てみよう。

 本稿のサンプルではRoslynDemoAnalyzerクラスがそれだ(リスト1)。まず、[DiagnosticAnalyzer(LanguageNames.CSharp)]という属性が付いているが、これはこのクラスがAnalyzerを提供することを表している。

C#
……省略……
namespace RoslynDemo
{
  [DiagnosticAnalyzer(LanguageNames.CSharp)]
  public class RoslynDemoAnalyzer : DiagnosticAnalyzer
  {
    ……省略……
  }
}
リスト1 Analyzerを表すRoslynDemoAnalyzerクラス(DiagnosticAnalyzer.cs)

 このクラスはDiagnosticAnalyzerという抽象クラス(Microsoft.CodeAnalysis.Diagnostics名前空間)を継承している。このクラスの抽象メンバーを見てみよう。

C#
using System.Collections.Immutable;
namespace Microsoft.CodeAnalysis.Diagnostics
{
  public abstract class DiagnosticAnalyzer
  {
    public abstract ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
    public abstract void Initialize(AnalysisContext context);
    ……省略……
  }
}
リスト2 DiagnosticAnalyzer抽象クラスの抽象メンバー(Microsoft.CodeAnalysisアセンブリ)

 継承先のRoslynDemoAnalyzerクラスでは、この2つのメンバーを実装することになる。順に見ていこう。

SupportedDiagnosticsプロパティ

 このAnalyzerが対応しているDiagnostics機能(=診断機能)に関する説明を、外部から取得するためのプロパティである。説明はDiagnosticDescriptorクラス(Microsoft.CodeAnalysis名前空間)で指定するが、必要な情報はこのクラスのコンストラクターで与えることになる。コンストラクターに与えるパラメーターを説明しよう。

  • id: このDiagnosticsを一意に識別するID文字列(以下、Diagnostics ID)。CodeFixProviderクラスからも参照する必要があるため、定数フィールドで定義することが多い
  • title: このDiagnosticsのタイトル
  • messageFormat: このDiagnosticsの詳細メッセージを生成するためのフォーマット文字列
  • category: このDiagnosticsのカテゴリ
  • defaultSeverity: デフォルトのSeverity(重要度)。DiagnosticSeverity列挙体(Microsoft.CodeAnalysis名前空間)のメンバーで指定する。Errorを指定すればコンパイルエラー扱いにできる。Severityはプロジェクトごとに設定できる
  • isEnabledByDefault: デフォルトで有効にするかどうかのbool値
  • helpLink: Diagnosticsに関する詳細な記述があるURL。ハイパーリンクとして表示される
  • description: 省略可能。追加の説明
  • customTags: 省略可能。可変長引数として指定するタグ

 このうちtitlemessageFormatdescriptionの3つは、多言語化のためのLocalizableResourceStringクラス(Microsoft.CodeAnalysis名前空間)を利用できる。単一言語で問題なければstring型を指定することもできる。

 このプロパティのシンプルな実装例をリスト3に示す。

C#
……省略……
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class RoslynDemoAnalyzer : DiagnosticAnalyzer
{
  public const string DiagnosticId = "RoslynDemo";

  private static readonly DiagnosticDescriptor Rule 
      = new DiagnosticDescriptor(DiagnosticId,
           "Type name contains '_'.",
           "Type name '{0}' contains '_'", 
           "Naming", 
           DiagnosticSeverity.Error, 
           true,
           helpLinkUri: "http://tech.tanaka733.net",
           description: "Type name must not include '_'.");

  public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

  ……省略……
}
リスト3 SupportedDiagnosticsプロパティの実装例

 次節で説明するInitializeメソッド(リスト4)を実装してから(診断レポートのロジックは後述)、このDiagnosticDescriptorを指定したAnalyzerをデバッグ実行して、コンソールアプリケーションのプロジェクトなどを作成したうえで「Meta_data」という名前のクラスを記述してみると、図4に示すエラーが[エラー一覧]ウィンドウに表示される。[説明]列にフォーマットされたメッセージが、詳細のところにdescriptionパラメーターで指定した値が表示されているのが分かる。

図4 RoslynDemoAnalyzerのエラー表示例

 またVSIXではなく、NuGetライブラリとしてプロジェクトに追加した場合は、[ソリューション エクスプローラー]上のプロジェクト項目-[参照]-[アナライザー]にAnalyzerが表示される。各Analyzerの重要度(Severity)をプロジェクトごとに変更することもできる(図5)。

図5 NuGetライブラリから追加した場合のRoslynDemoAnalyzerの表示例と、重要度の変更方法

Initializeメソッド

 次に説明するのがInitializeメソッドである。

 このメソッドは引数にAnalysisContext型(Microsoft.CodeAnalysis.Diagnostics名前空間)のインスタンスを取る。Initializeメソッドの実装方法としては、この引数(context)が持つRegisterXXXActionメソッドを呼び出して、“Action”と呼ばれる「コンパイル時に行うDiagnostics処理」を登録することである。登録した処理でDiagnosticsを報告する際に、対応するDiagnosticDescriptor(=リスト3のRuleオブジェクト)を指定することになる。

 1つのAnalyzerクラス内で複数のActionを登録することもでき、また複数のAnalyzerクラスに1つのActionを分けて登録することもできる。複数のAnalyzerが存在する場合、それぞれのAnalyzerは並列に実行され得るため、Analyzer間で状態の共有はできない。1つのAnalyzerの中で複数のActionを登録した場合、デフォルトではそのActionは直列に実行される。その順番に関しては、どのRegisterXXXActionメソッドで登録したかに依存し、優先順位が同じActionの実行順は不定である。この実行順に関してはGitHubのドキュメント(英語)に詳しい記述があるので参考にしてほしい。

 どのRegisterXXXActionメソッドを利用するかは、どのようなActionを実装したいかによって決まるだろう。ActionはAction<T>型で指定するが、TにはDiagnosticsの判定に必要な情報が渡される。どのようなDiagnosticsを実装するかを決めた後、実装に必要なContextが決まり、どのRegisterXXXActionを利用するかが決まる、という流れになる。今回のサンプルコードではクラス名という名前シンボルを使って判定したいため、Symbolに関する情報(=SymbolAnalysisContextオブジェクト)が必要であることが分かるので、SymbolKind.NamedTypeを指定してRegisterSymbolActionメソッドを呼び出すことにする、という流れになる。

 ここまでの説明を参考にして、Initializeメソッドを登録するActionも含めて実装した例が以下のコードである。

C#
……省略……
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class RoslynDemoAnalyzer : DiagnosticAnalyzer
{
  ……省略……

  public override void Initialize(AnalysisContext context)
  {
    context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);
  }

  private static void AnalyzeSymbol(SymbolAnalysisContext context)
  {
    // SymbolKind.NamedTypeで登録したので対応するINamedTypeSymbolオブジェクトが渡される
    var namedTypeSymbol = (INamedTypeSymbol)context.Symbol;
    if (namedTypeSymbol.Name.Contains("_"))
    {
      var diagnostic = Diagnostic.Create(Rule, namedTypeSymbol.Locations[0], namedTypeSymbol.Name);
      context.ReportDiagnostic(diagnostic);
    }
  }
}
リスト4 Initializeメソッドの実装例

 どのようなActionに対してどのRegisterXXXActionを使うかについては、いくつか具体例を次回以降で紹介していく予定である。

CodeFixProviderクラス

 Analyzerとして動作するためには、DiagnosticsAnalyzerクラスだけでもよい。その場合、記述しているコードに対して指定したSeverityで表示される。警告を表示するだけでなく、修正候補を提示したい場合は、CodeFixProvider抽象クラスを実装したクラス(本稿のサンプルではRoslynDemoCodeFixProviderクラス)を記述することになる。

 このクラスも[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(RoslynDemoCodeFixProvider)), Shared]という属性を付けることでCodeFixProviderであることを表す(リスト5)。なお、Severityが「警告(Warning)」もしくは「エラー(Error)」でないと修正候補は表示されない。

C#
……省略……
namespace RoslynDemo
{
  [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(RoslynDemoCodeFixProvider)), Shared]
  public class RoslynDemoCodeFixProvider : CodeFixProvider
  {
    ……省略……
  }
}
リスト5 CodeFixProviderを表すRoslynDemoCodeFixProviderクラス(CodeFixProvider.cs)

 このクラスの継承元であるCodeFixProviderは抽象クラスであり、そのメンバーは下記の通りである。

C#
using System.Collections.Immutable;
using System.Threading.Tasks;
namespace Microsoft.CodeAnalysis.CodeFixes
{
  public abstract class CodeFixProvider
  {
    public abstract ImmutableArray<string> FixableDiagnosticIds { get; }

    public abstract Task RegisterCodeFixesAsync(CodeFixContext context);

    public virtual FixAllProvider GetFixAllProvider()
    {
      return null;
    }
    ……省略……
  }
}
リスト6 CodeFixProvider抽象クラスの抽象メンバー(Microsoft.CodeAnalysis.Workspacesアセンブリ)

 これも順に見ていこう。

FixableDiagnosticIdsプロパティ

 このCodeFixが修正する対象のDiagnostics IDを指定する。IDはDiagnosticsAnalyzerクラスのDiagnosticDescriptorに指定したIDと同じ値となるので、実際にはそのIDを参照することになるだろう。

C#
……省略……
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(RoslynDemoCodeFixProvider)), Shared]
public class RoslynDemoCodeFixProvider : CodeFixProvider
{
  public sealed override ImmutableArray<string> FixableDiagnosticIds 
    => ImmutableArray.Create(RoslynDemoAnalyzer.DiagnosticId);

  ……省略……
}
リスト7 FixableDiagnosticIdsプロパティの実装例

GetFixAllProviderメソッド

 GetFixAllProviderメソッドはFixAllProviderクラスのインスタンスを返すが、これはCode Fixを表示した箇所と同様の箇所がある場合に、それらもまとめて修正する機能である。継承元のクラスのGetFixAllProviderメソッドはnullを返す実装になっているが、nullを返すと実行時にエラーが発生するため、適切なインスタンスを返す必要がある。多くの場合はあらかじめ用意されている、WellKnownFixAllProviders.BatchFixerプロパティ(BatchFixAllProvider型。Microsoft.CodeAnalysis.CodeFixes名前空間)を返すことになるため、実装としてはリスト8のようになるだろう。

C#
……省略……
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(RoslynDemoCodeFixProvider)), Shared]
public class RoslynDemoCodeFixProvider : CodeFixProvider
{
  ……省略……

  public sealed override FixAllProvider GetFixAllProvider()
  {
    return WellKnownFixAllProviders.BatchFixer;
  }

  ……省略……
}
リスト8 GetFixAllProviderメソッドの実装例

 この実装を行った場合、Code Fix Actionの表示は図6のようになる。これはAnalyzerで対象となるクラス名の箇所でマウスオーバーして表示されるランプアイコンをクリックすると表示される。

図6 WellKnownFixAllProviders.BatchFixerを指定した場合のCode Fix Action表示例
図6 WellKnownFixAllProviders.BatchFixerを指定した場合のCode Fix Action表示例

 複雑なFixAll機能を実装したい場合は、FixAllProviderクラスを継承して作成することになる。この場合の詳細については、GitHubのドキュメント(英語)に詳細があるので参考にしてほしい。

RegisterCodeFixesAsyncメソッド

 3つ目がRegisterCodeFixesAsyncメソッドだ。これはコンパイル結果に変更があるたびに呼ばれるメソッドであり、Code Fix Actionを登録する処理を実装する。まず実装を見てみよう。

C#
……省略……
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(RoslynDemoCodeFixProvider)), Shared]
public class RoslynDemoCodeFixProvider : CodeFixProvider
{
  ……省略……

  public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
  {
    // SyntaxRootの取得
    var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

    // 対応するDiagnosticとそのドキュメント上での位置を取得
    var diagnostic = context.Diagnostics.First();
    var diagnosticSpan = diagnostic.Location.SourceSpan;

    // Code Fix Actionのために必要な情報(ここでは「型宣言のSyntax」)を取得。
    // どのSyntaxが必要かはActionの実装に必要な情報によって変わる ……1
    var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<TypeDeclarationSyntax>().First();

    // Actionの登録
    context.RegisterCodeFix(
      CodeAction.Create(
        title: title,
        createChangedSolution: c => RemoveUnderscoreAsync(context.Document, declaration, c),
        equivalenceKey: title),
      diagnostic); //……2
  }

  private async Task<Solution> RemoveUnderscoreAsync(Document document, TypeDeclarationSyntax typeDecl, CancellationToken cancellationToken)
  {
    // 型宣言のSyntaxから型名を取得し、修正候補となる名前を生成
    var identifierToken = typeDecl.Identifier;
    var newName = identifierToken.Text.Replace("_", "");

    // SemanticModelを取得し、修正対象となるSymbolを取得
    var semanticModel = await document.GetSemanticModelAsync(cancellationToken);
    var typeSymbol = semanticModel.GetDeclaredSymbol(typeDecl, cancellationToken);

    // Symbolを書き換えて修正後のSolutionを生成する。
    // Renameの場合はRenamerクラスが書き換えるメソッドを提供している
    var originalSolution = document.Project.Solution;
    var optionSet = originalSolution.Workspace.Options;
    var newSolution = await Renamer.RenameSymbolAsync(document.Project.Solution, typeSymbol, newName, optionSet, cancellationToken).ConfigureAwait(false);

    return newSolution;
  }
}
リスト9 RegisterCodeFixesAsyncメソッドの実装例

 Code Fix Actionは、RegisterCodeFixesAsyncメソッドの引数で渡されるCodeFixContextMicrosoft.CodeAnalysis.CodeFixes名前空間)のRegisterCodeFixメソッドで登録する(2)。このとき、CodeActionクラス(Microsoft.CodeAnalysis.CodeActions名前空間)のCreateメソッドにより作成されたActionとともに、そのActionに対応するDiagnosticsをメソッドに渡している。

 実際のCodeFixで行う処理は、(CodeAction.Createメソッドで引数として渡している)RemoveUnderscoreAsyncメソッドで実装している。このメソッドは、書き換えた後のSolutionインスタンス(Microsoft.CodeAnalysis名前空間)を返せばよいが、その書き換えに必要な情報として、DocumentMicrosoft.CodeAnalysis名前空間)とSyntaxが多くの場合で必要になる(本稿のサンプルでも、RemoveUnderscoreAsyncメソッドに引数として渡されたDocumentとSyntaxを、メソッド内で使用しているのが分かるだろう)。Actionの実装により「どのSyntaxが必要か」も変わるため、1の部分とRemoveUnderscoreAsyncメソッドの部分が実装ごとに変わることになるだろう。

Code Refactoringプロジェクト

 さて、VSIXとして提供するCode Refactoringのテンプレートについても説明しよう。Visual Studioの新規ソリューションの作成で、[Code Refactoring (VSIX)]テンプレートを選ぶ(図7)。名前は「RoslynDemoRefactoring」とした。

図7 新しいプロジェクトの作成でCode Refactoringを選んだ様子

 こうして作成されたプロジェクトの内容が図8でなる。

図8 Code Refactoringテンプレートで作成したプロジェクト
図8 Code Refactoringテンプレートで作成したプロジェクト

 各プロジェクトの内容は以下の通りだ。

  • RoslynDemoRefactoring入力したプロジェクト名): ロジックを記述するためのプロジェクト
  • RoslynDemoRefactoring.Vsix入力したプロジェクト名.Vsix): VSIXにパッケージングするためのプロジェクト

 RoslynDemoRefactoringプロジェクトにはCodeRefactoringProvider.csというファイルが含まれており、そこにはRoslynDemoRefactoringCodeRefactoringProviderクラスが実装されている。このクラスも(前述のDiagnosticAnalyzerクラスの場合と同様に)[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = nameof(RoslynDemoRefactoringCodeRefactoringProvider)), Shared]という属性の指定により、CodeRefactoringProviderであることを表している(リスト10)。

C#
……省略……
namespace RoslynDemoRefactoring
{
  [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = nameof(RoslynDemoRefactoringCodeRefactoringProvider)), Shared]
  internal class RoslynDemoRefactoringCodeRefactoringProvider : CodeRefactoringProvider
  {
    ……省略……
  }
}
リスト10 CodeFixProviderを表すRoslynDemoRefactoringCodeRefactoringProviderクラス(CodeRefactoringProvider.cs)

 このクラスはCodeRefactoringProviderという抽象クラス(Microsoft.CodeAnalysis.CodeRefactorings名前空間)を継承している。このクラスの抽象メンバーを見てみよう。

C#
using System.Threading.Tasks;
namespace Microsoft.CodeAnalysis.CodeRefactorings
{
  public abstract class CodeRefactoringProvider
  {
    public abstract Task ComputeRefactoringsAsync(CodeRefactoringContext context);
  }
}
リスト11 CodeRefactoringProvider抽象クラスの抽象メンバー(Microsoft.CodeAnalysis.Workspacesアセンブリ)

 継承先のRoslynDemoRefactoringCodeRefactoringProviderクラスでは、このメンバーを実装することになる。その内容を見ていこう。

ComputeRefactoringsAsyncメソッド

 ComputeRefactoringsAsyncメソッドで実装する内容は、CodeFixProviderのRegisterCodeFixesAsyncメソッドの内容とよく似ている。具体的には、ここではRefactoring Actionを登録する処理を実装する。そのサンプルとなる実装をリスト12に掲載する。

C#
public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
    var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

    // 選択箇所のSyntaxNodeを取得する
    var node = root.FindNode(context.Span);

    // このRefactoringは選択箇所が「型の名前」のときだけ利用できる
    var typeDecl = node as TypeDeclarationSyntax;
    if (typeDecl == null)
    {
        return;
    }

    // 任意の型の名前に対し、Myを頭に付けた名前に変更するActionを生成する
    var action = CodeAction.Create("Add 'My' prefix", c => AddMyPrefixToTypeNameAsync(context.Document, typeDecl, c));

    // Actionを登録する
    context.RegisterRefactoring(action);
}

private async Task<Solution> AddMyPrefixToTypeNameAsync(Document document, TypeDeclarationSyntax typeDecl, CancellationToken cancellationToken)
{
    // 変更後の「型の名前」を生成する
    var identifierToken = typeDecl.Identifier;
    var newName = "My" + identifierToken.Text;

    // 変更対象となる型の名前に対応するSymbolを取得する
    var semanticModel = await document.GetSemanticModelAsync(cancellationToken);
    var typeSymbol = semanticModel.GetDeclaredSymbol(typeDecl, cancellationToken);

    // Renamerを利用して、名前を変更した後のSolutionを生成する
    var originalSolution = document.Project.Solution;
    var optionSet = originalSolution.Workspace.Options;
    var newSolution = await Renamer.RenameSymbolAsync(document.Project.Solution, typeSymbol, newName, optionSet, cancellationToken).ConfigureAwait(false);
    return newSolution;
}
リスト12 ComputeRefactoringsAsyncメソッドの実装例

 Refactoring Actionでは、Code Fix Actionの場合と違ってDiagnosticsが不要なだけで、処理の流れはほぼ同じだ。具体的には、ComputeRefactoringsAsyncメソッドの引数で渡されたCodeRefactoringContextMicrosoft.CodeAnalysis.CodeRefactorings名前空間)からDocumentとSyntaxの情報を取り出し、AddMyPrefixToTypeNameAsyncメソッドにRefactoring処理を記述したうえで、それらをCodeAction.Createメソッドの引数として渡して生成したActionをRegisterRefactoringメソッドで登録する。

 次回は、Analyzer with Code Fixプロジェクトを利用し、いくつかAnalyzerとCode Fix Actionの具体例を取り上げて実装の仕方を説明していく予定である。

1. .NETコンパイラープラットフォーム「Roslyn」の概要とコンパイラー拡張

C# 6.0と同時にリリースされた.NETコンパイラープラットフォーム「Roslyn」。そのコンパイラー拡張の作り方を解説する連載の第1回。

2. 【現在、表示中】≫ .NETコンパイラープラットフォーム拡張の作り方

C# 6.0と同時にリリースされた.NETコンパイラープラットフォーム「Roslyn」。そのコンパイラー拡張の作り方を解説する連載の第2回。

3. Analyzerの作り方と、各メソッドの使い方

.NETコンパイラープラットフォーム「Roslyn」でコンパイラー拡張を作ってみよう。Analyzer with Code FixプロジェクトでAnalyzerを実装するために必要な各メソッドの使い方と、Analyzerの作り方を説明する。

4. Code Fix Actionの作り方

.NETコンパイラープラットフォーム「Roslyn」でコンパイラー拡張を作ってみよう。CodeFixProviderの実装方法を説明し、code-crackerのソースコードから引用する形で基本的なコード修正候補の作成例を示す。

5. 外部ファイルの読み込みとローカライズ

Roslynのコンパイラー拡張で外部ファイルを読み込んで活用する方法と、AnalyzerやCode Fix Actionのメッセージをローカライズする方法について説明する。連載最終回。

サイトからのお知らせ

Twitterでつぶやこう!