Roslynで作るC#コンパイラー拡張(5)
外部ファイルの読み込みとローカライズ
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を指定しよう。
(3)Visual Studioのプロジェクトファイルを編集する方法
上記の方法でファイルのItem typeをAdditionalFilesに変更できない場合、例えばリソースファイル(.resxファイル)を指定したい場合は、AdditionalFileItemNames
プロパティを上書きすることで設定できる。
リスト1のように.csprojファイルを編集し、<PropertyGroup>
要素の下に<AdditionalFileItemNames>
要素を追加しよう。Visual Studioのテンプレートでプロジェクトを作成すると、通常、<PropertyGroup>
要素は複数記述されているが、これはビルド構成によってPropertyGroupを切り替えているためである。ビルド構成によらず設定ファイルとして扱うためには、Condition
属性が記述されていない<PropertyGroup>
要素の子として記述する必要がある。リスト1のように記述することで、ビルド構成によらず、Item typeがEmbeddedResourceのものが全てAdditionalFilesとして読み込まれることになる。
<PropertyGroup>
<AdditionalFileItemNames>$(AdditionalFileItemNames);EmbeddedResource</AdditionalFileItemNames>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Terms.resx">
</EmbeddedResource>
</ItemGroup>
|
この方法はVisual StudioのGUIから操作することができないため、.csprojファイルを直接編集することになる。.csprojファイルを直接編集する場合は、Visual Studioの[ソリューション エクスプローラー]でプロジェクト項目を右クリック後、コンテキストメニューから[プロジェクトのアンロード]を選び、アンロードした後に再度プロジェクト項目を右クリックして[編集]を選ぶ。編集し終わったら同じ右クリックで表示される[プロジェクトの再読み込み]を選ぶと、.csprojファイルが閉じ、プロジェクトが再度読み込まれる。
今回は、2番目の方法でTerms.txtをAdditionalFilesに追加した。
AdditionalFilesの読み込み
追加したAdditionalFilesの一覧は、CompilationStartAnalysisContext
のOptions
プロパティのAdditionalFiles
プロパティで取得できる。CompilationStartAnalysisContextはRegisterCompilationStartAction
メソッドの引数として渡される。以上を踏まえて、AdditionalFilesにアクセスするAnalyzerのInitialize
メソッドの例をリスト2に記した。
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);
}
});
}
|
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. |
名前 | 値 |
---|---|
AnalyzerDescription | 型名に禁止された単語が含まれています |
AnalyzerMessageFormat | 型名'{1}' に禁止された単語'{0}'が含まれています |
AnalyzerTitle | 型名に禁止された単語が含まれています |
リソースの参照
用意したリソースの参照方法だが、AnalyzerとCode Fixで異なる。Code Fixの方は通常のリソースと同様に、リスト3に示したように{リソースのファイル名}.{リソース名前}
という形式で参照できる(リスト3)。
private static readonly string title = Resources.AnalyzerTitle;
|
一方、Analyzerの方は、Visual Studio拡張としてインストールする場合はリスト3で記述したコードでも動作するが、コマンドラインから操作する場合に異なった挙動を示すので注意が必要である。csc
コンパイラーやMSBuildではPreferredUILang
というオプションがある。リスト3で示したコードでリソースのロケールによる切り替えを実装していると、このPreferredUILang
オプションによる切り替えには対応できない。そこでリスト4のようにリソースを参照するとよい。これはプロジェクトテンプレートで生成されたAnalyzerクラスにもコメントアウトされて記述されている。LocalizableString
クラスを受け入れられるようにDiagnosticDescriptor
のコンストラクターはstring
とLocalizableString
をそれぞれ引数にとる2つのパターンが用意されている。
// 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のコードは、リスト2のコードの前半部分を置き換えて使うことができる。
では実際に実行して確かめてみよう。Visual Studioで実行している場合、ロケールの切り替えは(メニューバーの)[ツール]メニューから[オプション]を選んで、[オプション]ダイアログの[環境]-[国際対応の設定]で言語を切り替えて行う。日本語を設定すれば、日本語リソースで設定した値が、それ以外の言語であればデフォルトの英語の値が図2のように表示されるはずだ。なお、言語を新規に追加する際は、言語リソースのダウンロードとVisual Studioの再起動が必要になる場合がある。
さらにNuGetパッケージとして配布する場合、.nuspecファイルもリスト5のように編集して、各リソースの.dllファイルを含める必要がある。リスト5では日本語ロケールのみだが、複数のロケールを用意した場合は、本体の.dllファイルが存在するフォルダーの下にそれらのロケール名ごとにフォルダーを作成し、そのロケール名フォルダーにリソースを配置したものを.nupkgファイルとして用意する必要がある。
<?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>
|
こうして作成した.nupkgファイルを参照したプロジェクトでは、ロケールを切り替えるとメッセージがローカライズされる。MSBuild
コマンドで切り替える場合は、/p:PerferredUILang=ja-JP
とオプションを指定する。もしくは.csprojファイルのプロパティとして、<PreferredUILang>ja-JP</PreferredUILang>
を指定することもできる。
■
今回をもってRoslynで作るC#コンパイラー拡張連載は終了である。コンパイラー拡張は、コンパイラーパイプラインをサービスとして公開できるようにしたRoslynプロジェクトの成果の一つであり、オープンソース化の流れを受けて既存の多くのコンパイラー拡張が公開されている。利用する際に、コンパイラー拡張のコードを読むことで挙動が理解できるとともに、自分自身でコンパイラー拡張を書くときにも参考になることが多い。この連載がそれらの助けとなれば幸いである。
1. .NETコンパイラープラットフォーム「Roslyn」の概要とコンパイラー拡張
C# 6.0と同時にリリースされた.NETコンパイラープラットフォーム「Roslyn」。そのコンパイラー拡張の作り方を解説する連載の第1回。
3. Analyzerの作り方と、各メソッドの使い方
.NETコンパイラープラットフォーム「Roslyn」でコンパイラー拡張を作ってみよう。Analyzer with Code FixプロジェクトでAnalyzerを実装するために必要な各メソッドの使い方と、Analyzerの作り方を説明する。
4. Code Fix Actionの作り方
.NETコンパイラープラットフォーム「Roslyn」でコンパイラー拡張を作ってみよう。CodeFixProviderの実装方法を説明し、code-crackerのソースコードから引用する形で基本的なコード修正候補の作成例を示す。