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

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

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

2017年1月26日

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

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

 最後となる連載第5回は、コンパイラー拡張使用時に外部ファイルを読み込んでAnalyzerの内部で利用する方法と、AnalyzerとCode Fix Actionのメッセージのローカライズについて説明する。

外部ファイルの読み込み

 コンパイラー拡張を作る場合、利用者が設定できる項目を用意したいことがあるだろう。通常のVisual Studioの拡張であればVisual Studio SDKの設定が利用できるが、コンパイラー拡張はNuGetライブラリとして提供する方法もあるため別の方法が用意されている。今回はユーザーがクラス名に使用できない単語の一覧を設定ファイルとして用意し、クラス名にその単語が含まれていることを検知する例を基に説明してみよう。

外部ファイルの配置

 コンパイラー拡張で外部ファイルを読み込む方法は3種類ある。

(1)cscコンパイラーを使う方法

 Visual Studio Codeなどのテキストエディターを使ってコマンドラインで開発する場合には、cscコンパイラーを実行する際に/additionalfileオプションで指定する方法が使える。

 そうではなくVisual Studioで開発する場合には、プロジェクトファイル(.csprojファイル)を編集する以下のいずれかの方法の方が使いやすいだろう。個別のファイルを読み込ませたい場合は、そのファイルのItem typeをAdditionalFilesに設定する。もちろん、複数の外部ファイルを指定することも可能だ。

(2)Visual Studioで[プロパティ]ウィンドウを使う方法

 Visual StudioのGUIからItem typeを設定するには、[ソリューション エクスプローラー]で対象の外部ファイル(本稿の例では「Terms.txt」)をプロジェクト内に含めて選択したうえで、図1のように[プロパティ]ウィンドウの[ビルド アクション]にAdditionalFilesを指定しよう。

図1 ファイルの[プロパティ]で[ビルド アクション]を「AdditionalFiles」に設定した様子
図1 ファイルの[プロパティ]で[ビルド アクション]を「AdditionalFiles」に設定した様子
(3)Visual Studioのプロジェクトファイルを編集する方法

 上記の方法でファイルのItem typeをAdditionalFilesに変更できない場合、例えばリソースファイル(.resxファイル)を指定したい場合は、AdditionalFileItemNamesプロパティを上書きすることで設定できる。

 リスト1のように.csprojファイルを編集し、<PropertyGroup>要素の下に<AdditionalFileItemNames>要素を追加しよう。Visual Studioのテンプレートでプロジェクトを作成すると、通常、<PropertyGroup>要素は複数記述されているが、これはビルド構成によってPropertyGroupを切り替えているためである。ビルド構成によらず設定ファイルとして扱うためには、Condition属性が記述されていない<PropertyGroup>要素の子として記述する必要がある。リスト1のように記述することで、ビルド構成によらず、Item typeがEmbeddedResourceのものが全てAdditionalFilesとして読み込まれることになる。

XML
<PropertyGroup>
<AdditionalFileItemNames>$(AdditionalFileItemNames);EmbeddedResource</AdditionalFileItemNames>
</PropertyGroup>
<ItemGroup>
  <EmbeddedResource Include="Terms.resx">
  </EmbeddedResource>
</ItemGroup>
リスト1 EmbeddedResourceをAdditionalFilesに追加する.csprojファイルの設定例

 この方法はVisual StudioのGUIから操作することができないため、.csprojファイルを直接編集することになる。.csprojファイルを直接編集する場合は、Visual Studioの[ソリューション エクスプローラー]でプロジェクト項目を右クリック後、コンテキストメニューから[プロジェクトのアンロード]を選び、アンロードした後に再度プロジェクト項目を右クリックして[編集]を選ぶ。編集し終わったら同じ右クリックで表示される[プロジェクトの再読み込み]を選ぶと、.csprojファイルが閉じ、プロジェクトが再度読み込まれる。

 今回は、2番目の方法でTerms.txtをAdditionalFilesに追加した。

AdditionalFilesの読み込み

 追加したAdditionalFilesの一覧は、CompilationStartAnalysisContextOptionsプロパティのAdditionalFilesプロパティで取得できる。CompilationStartAnalysisContextはRegisterCompilationStartActionメソッドの引数として渡される。以上を踏まえて、AdditionalFilesにアクセスするAnalyzerのInitializeメソッドの例をリスト2に記した。

C#
public const string DiagnosticId = "RoslynDemo";

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

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

public override void Initialize(AnalysisContext context)
{
  context.RegisterCompilationStartAction(compilationStartContext =>
  {
    var additionalFiles = compilationStartContext.Options.AdditionalFiles; //……1
    var termsFile = additionalFiles.FirstOrDefault(file => Path.GetFileName(file.Path) == "Terms.txt"); //……2

    if (termsFile != null) //……3
    {
      var terms = termsFile.GetText(compilationStartContext.CancellationToken).Lines
        .Select(line => line.ToString())
        .ToImmutableHashSet(); //……4

      compilationStartContext.RegisterSymbolAction(symbolAnalysisContext =>//……5
      {
        var namedTypeSymbol = (INamedTypeSymbol)symbolAnalysisContext.Symbol;
        var symbolName = namedTypeSymbol.Name;

        foreach (var term in terms.Where(term => symbolName.Contains(term)))
        {
          symbolAnalysisContext.ReportDiagnostic(Diagnostic.Create(Rule, namedTypeSymbol.Locations[0], term, symbolName));
        }
      },
      SymbolKind.NamedType);
    }
  });
}
リスト2 AdditionalFilesにアクセスするAnalyzerのInitializeメソッドの例(例えばDiagnosticAnalyzer.csファイル)

 RegisterCompilationStartActionメソッドでコンパイル開始時のアクションを登録する。その中で、まずAdditionalFiles一覧を取得し(1)、パスを基に該当のファイルを取得する(2)。複数のファイルを読み込む場合は、2の処理を適宜変更する。該当のファイルが存在しないケースを除外し(3)、存在する場合はファイルを読み込む処理を行う。今回は、Terms.txtファイルの中に1行に1単語ずつ禁止したい単語を記述している想定であるため、GetTextメソッドで全テキストを取得した後、Linesプロパティで1行ごとに文字列に変換して、HashSetに変換している(4)。その後、型名を検査対象とするため、RegisterSymbolActionメソッドを呼び出して、禁止している単語を含んでいないか検査するアクションを記述する。アクションの書き方は連載第3回で紹介しているので参考にしてほしい。

 取得したファイルの読み込み手段としてはGetTextメソッドしか用意されていないため、いったんこのメソッドの返り値であるSourceTextクラスのオブジェクトを取得して操作することになる。例えば、Streamとして扱いたい場合はMemoryStreamを経由して、SourceTextクラスのWriteメソッドでMemoryStreamに出力することになるだろう。

コンパイラー拡張のメッセージのローカライズ

リソースファイルの準備

 コンパイラー拡張が多くの人に使われていると、メッセージをローカライズすることでより使いやすくなるだろう。メッセージをローカライズする際に必要となるリソースは、通常の.NET Frameworkのアプリケーションと同様に、各言語のリソースファイル(.resxファイル)である。Roslyn SDKのVisual Studio拡張で用意されているプロジェクトテンプレートでコンパイラー拡張プロジェクトを作成すると、Resources.resxというリソースファイルが用意されているので、今回はこれに加えて日本語リソースとしてResources.ja-JP.resxファイルを用意する。今回のコード例で使用しているリソースの値は表1、表2に記した。なお、リソースのロケールをjaなどと言語名のみで指定するとNuGetパッケージとして利用する場合にローカライズされない問題を確認しているため、言語名(ja)とカルチャ名(JP)を両方指定すること(=.ja-JP)をお勧めする。

名前
AnalyzerDescription Type name contains the forbidden keyword.
AnalyzerMessageFormat Type name '{1}' contains the forbidden keyword '{0}'.
AnalyzerTitle Type name contains the forbidden keyword.
表1 Resources.resxファイルの値(デフォルトロケールとして今回は英語のリソースを設定している)
名前
AnalyzerDescription 型名に禁止された単語が含まれています
AnalyzerMessageFormat 型名'{1}' に禁止された単語'{0}'が含まれています
AnalyzerTitle 型名に禁止された単語が含まれています
表2 Resources.ja-JP.resxファイルの値(日本語ロケールでの値)

リソースの参照

 用意したリソースの参照方法だが、AnalyzerとCode Fixで異なる。Code Fixの方は通常のリソースと同様に、リスト3に示したように{リソースのファイル名}.{リソース名前}という形式で参照できる(リスト3)。

C#
private static readonly string title = Resources.AnalyzerTitle;
リスト3 Code Fixで利用するメッセージのローカライズ例

 一方、Analyzerの方は、Visual Studio拡張としてインストールする場合はリスト3で記述したコードでも動作するが、コマンドラインから操作する場合に異なった挙動を示すので注意が必要である。cscコンパイラーやMSBuildではPreferredUILangというオプションがある。リスト3で示したコードでリソースのロケールによる切り替えを実装していると、このPreferredUILangオプションによる切り替えには対応できない。そこでリスト4のようにリソースを参照するとよい。これはプロジェクトテンプレートで生成されたAnalyzerクラスにもコメントアウトされて記述されている。LocalizableStringクラスを受け入れられるようにDiagnosticDescriptorのコンストラクターはstringLocalizableStringをそれぞれ引数にとる2つのパターンが用意されている。

C#
// You can change these strings in the Resources.resx file. If you do not want your analyzer to be localize-able, you can use regular strings for Title and MessageFormat.
// See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/Localizing%20Analyzers.md for more on localization
private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.AnalyzerTitle), Resources.ResourceManager, typeof(Resources));
private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.AnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources));
private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.AnalyzerDescription), Resources.ResourceManager, typeof(Resources));

private static readonly DiagnosticDescriptor Rule
  = new DiagnosticDescriptor(DiagnosticId,
                             Title,
                             MessageFormat,
                             "Naming",
                             DiagnosticSeverity.Error,
                             true,
                             helpLinkUri: "http://tech.tanaka733.net",
                             description: Description);
リスト4 Analyzerのメッセージのローカライズ例(例えばDiagnosticAnalyzer.csファイル)

 リスト4のコードは、リスト2のコードの前半部分を置き換えて使うことができる。

 では実際に実行して確かめてみよう。Visual Studioで実行している場合、ロケールの切り替えは(メニューバーの)[ツール]メニューから[オプション]を選んで、[オプション]ダイアログの[環境]-[国際対応の設定]で言語を切り替えて行う。日本語を設定すれば、日本語リソースで設定した値が、それ以外の言語であればデフォルトの英語の値が図2のように表示されるはずだ。なお、言語を新規に追加する際は、言語リソースのダウンロードとVisual Studioの再起動が必要になる場合がある。

図2 実行結果のサンプル(上:言語設定が英語の場合、下:言語設定が日本語の場合)

 さらにNuGetパッケージとして配布する場合、.nuspecファイルもリスト5のように編集して、各リソースの.dllファイルを含める必要がある。リスト5では日本語ロケールのみだが、複数のロケールを用意した場合は、本体の.dllファイルが存在するフォルダーの下にそれらのロケール名ごとにフォルダーを作成し、そのロケール名フォルダーにリソースを配置したものを.nupkgファイルとして用意する必要がある。

XML
<?xml version="1.0"?>
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
  <!-- ……省略…… -->
  <files>
    <file src="*.dll" target="analyzers\dotnet\cs" exclude="**\Microsoft.CodeAnalysis.*;**\System.Collections.Immutable.*;**\System.Reflection.Metadata.*;**\System.Composition.*" />
    <file src="tools\*.ps1" target="tools\" />
    <file src="ja-JP\RoslynDemo.resources.dll" target="analyzers\dotnet\cs\ja-JP" />
  </files>
</package>
リスト5 .nuspecファイルの要素にロケールごとのリソースを追加した様子(例えばDiagnostic.nuspecファイル)

 こうして作成した.nupkgファイルを参照したプロジェクトでは、ロケールを切り替えるとメッセージがローカライズされる。MSBuildコマンドで切り替える場合は、/p:PerferredUILang=ja-JPとオプションを指定する。もしくは.csprojファイルのプロパティとして、<PreferredUILang>ja-JP</PreferredUILang>を指定することもできる。

 今回をもってRoslynで作るC#コンパイラー拡張連載は終了である。コンパイラー拡張は、コンパイラーパイプラインをサービスとして公開できるようにしたRoslynプロジェクトの成果の一つであり、オープンソース化の流れを受けて既存の多くのコンパイラー拡張が公開されている。利用する際に、コンパイラー拡張のコードを読むことで挙動が理解できるとともに、自分自身でコンパイラー拡張を書くときにも参考になることが多い。この連載がそれらの助けとなれば幸いである。

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でつぶやこう!