
次世代コンパイラー“Roslyn”概説(後編) 【正式版対応】
新しいコンパイラー“Roslyn”を用いたプログラミングを体験!
.NET Compiler Platform SDKをインストールしたVisual Studio 2015開発環境を使って、Roslynを用いたプログラミング方法を実際のコードで示しながら説明する。Visual Studio “14” CTP3→正式版に合わせて改訂。
前回は、Visual Studio 2015に搭載された、新しい.NETコンパイラープラットフォーム(コード名“Roslyn”。以下、Roslynと表記)の概要を説明した。今回はVisual Studio 2015とRoslynを用いた、プログラミング手法を簡単に紹介する。
Roslynで開発を行うための準備
Visual Studio 2015でRoslynによる開発を行うには、Visual Studioの追加機能と.NET Compiler Platform SDKをインストールする必要がある。
Visual Studioの追加機能のインストール
※Visual Studio 2015のインストール時に、全ての機能をインストールした場合はこの手順は、この節の手順は読み飛ばしてよい。
コントロールパネルの[プログラムのアンインストールまたは変更]から、インストール済みのMicrosoft Visual Studio 2015項目を右クリックし、(表示されるコンテキストメニューから)[変更]をクリックする。Visual Studioのダイアログが表示され、[変更][修復][アンインストール]のいずれかを選択する画面になったら、[変更]ボタンをクリックする。これにより、図4.1の画面が表示される。
なお、これからVisual Studio 2015をインストールする場合には、インストール時に[カスタム]を選択することで、以下の説明と同様の画面で進めることができる。
インストールする機能の選択画面(図4.1)の中から、以下の項目がチェックされているかを確認し、チェックされていなければチェックを付ける。
- [Windows 開発と Web 開発]の中の[Windows 8.1 および Windows Phone 8.0/8.1 ツール]-[ツールと Windows SDK]
- [共通ツール]の中の[Visual Studio 拡張性ツール]

図中で赤く囲まれた項目にチェックを付ける必要がある。
チェックしたら、[次へ]ボタンをクリックして、インストールを完了しよう。
SDKのダウンロードとインストール
続いて、Visual Studio 2015向けの.NET Compiler Platform SDKをインストールする。これはVisual Studio向け拡張機能を作成する際に必要となる。
「.NET Compiler Platform SDK 拡張機能」にアクセスして[ダウンロード]ボタンをクリックすると(図4.2)、.vsixファイルがダウンロードされる。このファイルを開くと、自動的にVSIXインストーラーが起動するので、画面の指示に従ってインストールを進めよう。
なお、CTPの時点までは別々の拡張機能となっていたSyntax Visualizerは、このパッケージをインストールすることで同時に導入されるようになっている。Syntax Visualizerは、現在、表示・選択しているコードが、Roslynではどのような構文木で構築されているのかをリアルタイムに視覚化できるツールである(図4.3)。後に解説するCode Fixなどの作成時に大いに役立つため、ぜひ覚えておいてほしい。
エディターで入力されているコードに対応する、Roslyn上での構文木が表示される。ツリーに表示されているアイテムをクリックすることで、該当する部分のコードがコードエディターで選択される。
以上で準備は完了だ。さっそくRoslynを使ってみよう。
サンプル: C#のコードを色付きでHTMLに出力するプログラム
サンプルプログラムのダウンロード
Roslynを用いた例として、構文解析・意味解析を行い、その結果に基づいて簡易的な色付けを行ったコードを出力する、スタンドアロンなコード解析プログラムの作成を行う(図5.1)。ソースコードは、「bonprosoft/cs2html - GitHub」で公開している。
重要箇所の解説(1): 構文解析と意味解析
さっそくコードを見ていこう。今回、Roslynを用いて重要な処理を行っているのは、SampleSyntaxWalker.cs
ファイルのみである。構文解析と意味解析は、それぞれ以下のコードで行っている。
public void Analyze(string code, HtmlBuilder builder)
{
this.builder = builder;
var tree = CSharpSyntaxTree.ParseText(code); // ……1
foreach (var item in tree.GetDiagnostics()) // ……2
{
this.builder.AppendSyntaxDiagnostic(item);
}
var compilation = CSharpCompilation.Create("sample",
syntaxTrees: new[] { tree },
references: new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) }) // ……3
semanticModel = compilation.GetSemanticModel(tree); // ……4
foreach (var item in compilation.GetDiagnostics()) // ……5
{
this.builder.AppendSemanticDiagnostic(item);
}
// ……後略……
|
CSharpSyntaxTree
/CSharpCompilation
はMicrosoft.CodeAnalysis.CSharp
名前空間、MetadataFileReference
はMicrosoft.CodeAnalysis
名前空間に所属するクラス。
HtmlBuilder
は、HTMLソースを出力するために独自に実装したクラスである。
- 1コードに対して構文解析を行い、構文木を含む結果を変数
tree
に格納する。 - 2構文解析を行った結果から、コードに対する診断(構文エラーなど)を取得する。
- 31で得た構文解析結果と、参照(今回はmscorlibアセンブリのみを含めている)情報を基に、コンパイル環境を作成し、意味解析を行う。
- 43で解析した結果から、意味モデル(Semantic Model)を取得する。
- 53で解析した結果から、コードに対する診断(構文エラー、意味エラーなど)を取得する。
重要箇所の解説(2): 文字列/文字/数値リテラルのシンタックスハイライト
続いてシンタックスハイライトの実現方法であるが、これは構文解析で得た構文木の全ノードを走査するだけである。RoslynにはVisitorパターンで実装されたSyntaxWalker
クラス(Microsoft.CodeAnalysis
名前空間)が用意されているため、今回はそのクラスを継承して、Visit
メソッドをオーバーライドすることで、走査を行った。
protected override void VisitToken(SyntaxToken token)
{
// ……中略……
bool isProcessed = false;
if (token.IsKeyword()) // ……1
{
this.builder.Write(TokenKind.Keyword, token.ValueText);
isProcessed = true;
}
else
{
switch (token.Kind())
{
case SyntaxKind.StringLiteralToken: // ……2
this.builder.Write(TokenKind.StringLiteral, '"' + token.ValueText + '"');
isProcessed = true;
break;
case SyntaxKind.CharacterLiteralToken: // ……3
this.builder.Write(TokenKind.CharacterLiteral, token.ValueText);
isProcessed = true;
break;
case SyntaxKind.NumericLiteralToken: // ……4
this.builder.Write(TokenKind.NumberLiteral, token.ValueText);
isProcessed = true;
break;
// ……後略……
|
SyntaxToken
はMicrosoft.CodeAnalysis
名前空間、SyntaxKind
はMicrosoft.CodeAnalysis.CSharp
名前空間に所属するクラス。
TokenKind
は、トークン種別管理用に独自に実装した列挙体である。
- 1トークンがキーワードであるかを確認する。
- 234トークンがキーワードでない場合、どのようなものであるかを確認する(ここでは、文字列リテラル、文字リテラル、数値リテラル、……の順で確認している)。
重要箇所の解説(3): 識別子のシンタックスハイライト
また解析対象のコードに、foo.Action()
のようなコードが含まれていた際に、foo
がどのようなものであるかを確認して、そのハイライトを適切に行う必要がある。そこで、意味解析の段階で作成した意味モデルを用いて、その識別子(Identifier)が何を表すかを取得する。
// ……前略……
case SyntaxKind.IdentifierToken:
if (token.Parent is SimpleNameSyntax) // ……1
{
var name = (SimpleNameSyntax)token.Parent; // ……2
var info = semanticModel.GetSymbolInfo(name); // ……3
if (info.Symbol != null && info.Symbol.Kind != SymbolKind.ErrorType)
{
switch (info.Symbol.Kind)
{
case SymbolKind.NamedType: // ……4
this.builder.Write(TokenKind.Identifier, token.ValueText);
isProcessed = true;
break;
case SymbolKind.Namespace: // ……5
case SymbolKind.Parameter:
case SymbolKind.Local:
case SymbolKind.Field:
case SymbolKind.Property:
this.builder.Write(TokenKind.None, token.ValueText);
isProcessed = true;
break;
}
}
}
else if (token.Parent is TypeDeclarationSyntax) // ……6
{
// ……後略……
|
SimpleNameSyntax
はMicrosoft.CodeAnalysis.CSharp.Syntax
名前空間、SymbolKind
はMicrosoft.CodeAnalysis
名前空間に所属するクラス。
- 1識別子トークンの親(例:
foo.Action()
やBar hoge;
)の型が、SimpleNameSyntax
と互換性があるかを判定する。 - 2トークンの親(=SyntaxNodeの派生クラス、この場合はSimpleNameSyntaxクラスのオブジェクト)を取得する。
- 3取得した情報を基に、その親の識別子がどのようなものであるか、意味モデルを用いて判別する。
- 4識別子が
NamedType
(=クラス名や列挙型名など)を指している場合は、色付けを行う。 - 5識別子が名前空間や変数やプロパティである場合は、色付けなしで出力を行う。
- 6識別子が型宣言文(例:
Class foo {}
)であるかを確認する。
識別子がどのような意味を持つか、判断する仕組みを独自で実装した場合、シンボルテーブルや名前解決の仕組みなど非常に複雑な実装を行う必要があったが、それらの情報がRoslynを用いることで、非常に簡潔に取得できることがよく分かる。
サンプルプログラムの実行
サンプルとして、このプログラムにリスト5.4を入力とした出力結果を、リスト5.5、図5.2に示す。
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace CSvNextSample
{
class BadCode
{
static void Main(string[] args)
{
Console.WriteLine("Hello,World!") // ……1
}
いんと Temp() // ……2
{
return 0;
}
}
}
|
このコードを、.csファイルとしてUTF-8エンコードで保存する。
- 1セミコロンが抜けているため、構文エラーが発生すると考えられる。
- 2
いんと
と呼ばれる名前を持つ型は宣言されていないが、構文に問題はない。
Syntax Diagnostics
Semantic Diagnostics
|
cs2html BadCode.cs output.html
というコマンドで、サンプルプログラムを実行すると、このような結果がHTMLページとして出力される。
- 12セミコロンが抜けていることが検知されている。
- 3意味解析部分では、
いんと
と呼ばれる名前を持つ型が存在しないことを示している。
サンプル: 変数の定数化を支援する拡張機能
最後に、Visual Studio 2015へと組み込むことのできる拡張機能を、Roslynを用いて作成する。
サンプルプログラムのダウンロード
今回、公式サイトが用意しているWalkthrough(英語)で用いられているサンプル(MakeConstプログラム)が、非常に分かりやすいため、そちらのコードを利用して解説していく(※こちらのリンク先から閲覧・ダウンロードできる)。また、筆者が日本語のコメントを付け加えたものを「bonprosoft/MakeConstFix - GitHub」に用意したので、併せて参照してほしい。
今回は、このサンプルプログラムと同じものを、「MakeConstFix」という名前で作成する。
サンプルプログラム「MakeConstFix」の内容
このプログラムでは、定数化可能な変数を検出すると通知を発行し、ユーザーへ修正の提示を行う(図6.1)。このような修正機能を「Code Fix」(コード修正)と呼ぶ(※前回ではFeatures APIsの1つとして紹介した)。

この拡張機能をインストールしたVisual Studioは自動的に診断が行われ、定数化可能なコード(例: int foo = 10;
)を検出すると、通知を発行し、ユーザーへ修正の提示を行う。
MakeConstFixが正常に動作するための条件
このプログラムが正常な動作を行うには、純粋に変数の宣言文を検出するだけではなく、以下の条件を満たす必要があることに注意したい。
const
キーワードが付与されていないものに限る(const int foo = 10;
は定数化済みである)- 宣言文が複数の宣言で構成されるとき、全ての変数に初期化子が付与されている必要がある(
int foo,bar = 10;
はfoo
が初期化子を持たないため、定数化できない) - コンパイル時定数を持つことが必要(
int foo = somefunc();
は定数化できない) - 宣言後、代入によって値が変更されない(
const
キーワードを設定できるのは、値が変更されないことが保証されるものに限られるためである)
また、修正内容についても、単純なconst
キーワードを付与した宣言文への置換ではなく、型推論が使われている場合は、その型を補完する必要があることに注意したい(var foo = 10;
はconst int foo = 10;
となる必要がある)。
プロジェクトの作成
それでは、Roslynを用いたCode Fixプロジェクトを作成しよう。
Visual Studioの[新しいプロジェクト]ダイアログで、[インストール済み]-[テンプレート]の中の[Visual C#]にある[Extensibility]を選択する(図6.2)。テンプレート一覧から[Analyzer with Code Fix (NuGet + VSIX)]を選択して、プロジェクト名を入力し、[OK]ボタンをクリックしよう。
診断機能の作成
まずは、コードに対して診断機能を提供し、もし条件に当てはまるようであればVisual Studioおよびユーザーに通知する処理が必要となる。リスト6.1では、診断機能がどのような構文を対象とし、どのような処理を行うかを宣言している。
using Microsoft.CodeAnalysis.CSharp;
……中略……
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class MakeConstFixAnalyzer : DiagnosticAnalyzer
{
public const string MakeConstDiagnosticId = "MakeConstFix";
public static readonly DiagnosticDescriptor MakeConstRule = new DiagnosticDescriptor(MakeConstDiagnosticId,
"定数化",
"定数化できます",
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true); // ……1
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(MakeConstRule); } } // ……2
public override void Initialize(AnalysisContext context)
{
context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.LocalDeclarationStatement); // ……3
}
……中略……
}
|
プロジェクト作成時にひな型として基本的なコードは生成されている。ここで追記・編集した部分は太字にしている。※実際に自分で生成したひな型コードと、一部、変数名が違う場合もあるので注意してほしい(例えば、変数「MakeConstDiagnosticId」は、ひな型では「DiagnosticId」という名前で出力されている場合がある)。
- 12この診断に関する情報(カテゴリや警告レベル)を宣言する。
- 3Visual Studioから受け取る情報を宣言する。
リスト6.1のRegisterSyntaxNodeAction
メソッド呼び出しの引数で指定したSyntaxNodeがRoslynによって検出されたとき、引数で指定したAnalyzeNode
メソッドが呼び出される。このAnalyzeNode
メソッド内で定数化できるかを判別し、可能である場合には通知を発行する処理を記述する(リスト6.2)。
AnalyzeNode
メソッドは、同じDiagnosticAnalyzer
クラス内に以下のように記述すればよい。
private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
if (CanBeMadeConst((LocalDeclarationStatementSyntax)context.Node, context.SemanticModel)) // ……1
{
context.ReportDiagnostic(Diagnostic.Create(MakeConstRule, context.Node.GetLocation())); // ……2
}
}
|
ひな型コードではAnalyzeSymbol
メソッドが生成されているが、それを消してこのAnalyzeNode
メソッドに書き換える。
- 1
CanBeMadeConst
メソッド(後述)に、検出されたSyntaxNodeを渡して、定数化可能であるかを判定する。 - 2判定可能と判断された場合には、通知を発行する。
続いて、CanBeMadeConst
メソッドを以下のように記述する。
private static bool CanBeMadeConst(LocalDeclarationStatementSyntax localDeclaration, SemanticModel semanticModel)
{
if (localDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword)) // ……1
{
return false;
}
foreach (var variable in localDeclaration.Declaration.Variables)
{
var initializer = variable.Initializer; // ……2
if (initializer == null)
{
return false;
}
var constantValue = semanticModel.GetConstantValue(initializer.Value); // ……3
if (!constantValue.HasValue)
{
return false;
}
……中略……
}
var dataFlowAnalysis = semanticModel.AnalyzeDataFlow(localDeclaration); // ……4
foreach (var variable in localDeclaration.Declaration.Variables)
{
var variableSymbol = semanticModel.GetDeclaredSymbol(variable);
if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol)) // ……5
{
return false;
}
}
return true; // ……6
}
|
- 1修飾子に
const
がすでに含まれている場合は対象外とする。 - 2初期化子を持つことを確認し、持たない場合には対象外とする(例:
int x,y = 0;
は定数化できない)。 - 3コンパイル時定数であることを確認し、そうでない場合は対象外とする(例:
int x = foo();
は定数化できない)。 - 4宣言された変数に対してデータフロー解析(=その変数の参照状況の解析など)を行う。
- 5宣言された変数が他の箇所で書き換えられていないことを確認し、書き換えられている場合は対象外とする。
const
キーワードを設定できるのは、値が変更されないことが保証されるものに限られるためである。 - 61~5の条件に全て当てはまる文は定数化できるとして、trueを返す。
MakeConstFixサンプルプログラムの「診断」機能の実行
以上までのコードで、診断機能が動作する。Ctrl+F5キーを押して、Visual Studioの実験的なインスタンスを起動した後、(コンソールアプリのプログラムを新規に作成するなどして)動作確認してみよう。正常に診断機能が動作している場合、図6.3のような通知が発行されるはずである*1。

診断機能が正常に動作している場合、Visual Studioによって自動的に診断が行われ、定数化可能なコード(例: int bar = 10;
)を検出すると通知を発行する。この例では、定数化されているfoo
、宣言後に代入が行われているbaz
には通知が発行されず、bar
のみに発行されていることが分かる。
- *1 Code Fixで表示される文字(例:図6.4)に文字化けが発生する場合は、先ほどのDiagnosticAnalyzer.csファイルの文字エンコーディングを見直す必要がある。文字エンコーディングは(メニューバーにある)[ファイル]メニューの[保存オプションの詳細設定]から変更可能である。基本的には[エンコード]の値を「Unicode (UTF-8 シグネチャ付き) - コードページ 65001」に設定しよう。

修正機能の作成
続いて、ユーザーが通知を確認した箇所に対して発行する、修正機能の作成を行う。
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
……中略……
var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<LocalDeclarationStatementSyntax>().First(); // ……1
// 定数化を行うCodeFixを作成する
context.RegisterCodeFix(
CodeAction.Create(
title: title,
createChangedDocument: c => MakeConstAsync(context.Document, declaration, c),
equivalenceKey: title),
diagnostic);
}
|
プロジェクト作成時にひな型として基本的なコードは生成されている。ここで追記・編集した部分は太字にしている。
- 1通知を行った範囲において、
LocalDeclarationStatementSyntax
クラス(Microsoft.CodeAnalysis.CSharp.Syntax
名前空間)であるトークンを見つける。 - 2修正情報を返す(修正を適用する場合、
MakeConstAsync()
を実行した結果、返される構文木を採用する)。
それでは、木の改変を行うMakeConstAsync
メソッドのコードを見ていこう。
using Microsoft.CodeAnalysis.Formatting;
……中略……
private async Task<Document> MakeConstAsync(Document document,
LocalDeclarationStatementSyntax localDeclaration,
CancellationToken cancellationToken)
{
var firstToken = localDeclaration.GetFirstToken();
var leadingTrivia = firstToken.LeadingTrivia;
var trimmedLocal = localDeclaration.ReplaceToken(
firstToken, firstToken.WithLeadingTrivia(SyntaxTriviaList.Empty)); // ……1
var constToken = SyntaxFactory.Token(leadingTrivia, SyntaxKind.ConstKeyword,
SyntaxFactory.TriviaList(SyntaxFactory.ElasticMarker)); // ……2
var newModifiers = trimmedLocal.Modifiers.Insert(0, constToken); // ……3
var variableDeclaration = localDeclaration.Declaration; // ……4
// ……中略(プログラム全体は、GitHub上のサンプルコードを参照してほしい)……
var newLocal = trimmedLocal.WithModifiers(newModifiers)
.WithDeclaration(variableDeclaration); // ……5
var formattedLocal =
newLocal.WithAdditionalAnnotations(Formatter.Annotation); // ……6
var root = await document.GetSyntaxRootAsync(cancellationToken); // ……7
var newRoot = root.ReplaceNode(localDeclaration, formattedLocal);// ……8
return document.WithSyntaxRoot(newRoot); // ……9
}
|
ひな型コードではMakeUppercaseAsync
メソッドが生成されているが、それを消してこのMakeConstAsync
メソッドに書き換える。
※中略部分のコードを追記する場合、using Microsoft.CodeAnalysis.Simplification;
というコードがファイルの冒頭に必要になる。
- 1前方にあるTrivia(=空白など)を削除した宣言文を新しく作成する。
- 2前方にTriviaを付与した、
const
キーワードを新しく作成する。 - 3もともとあった修飾子リストの先頭に、
const
キーワードを追加した修飾子リストを新しく作成する。 - 4宣言部分のみを取得する。
- 53で作成した新しい修飾子リストと新しい宣言部分で、1で作成した宣言文を置き換えた、変数の宣言文を新しく作成する。
- 65で作成した宣言文に整形を行った宣言文を新しく作成する。
- 7構文木のルートを取得する。
- 8
localDeclaration
(=今回の修正対象部分)をformattedLocal
(=新しいノード)で置き換えた構文木を、新たに作成する。 - 98で作成した構文木で構成されるドキュメントを新しく作成し、その結果を返す。
リスト6.5のコードでは、既存の構文木に対して変更を加えるメソッドの呼び出しや代入を一切行っておらず、どの処理においても、ある部分のみを改変したクローンの構文木を生成している(Immutableな構造となっている)ことに注目したい。これはRoslynによって生成された構文木が、悪意のあるプラグインによって変更されるのを防ぐためである。
MakeConstFixサンプルプログラムの「修正」機能の実行
以上のコードで診断機能と修正機能の作成が完了した。Ctrl+F5キーを押して動作確認を行おう。正常に動作している場合、図6.5のような通知が発行され、修正機能が提示されるはずである。
正常に動作している場合、定数化可能なコード(例: var bar = 10;
)を検出すると、通知を発行し、修正の提案を行う。この例では修正後のコードとして、明示的に型を示したconst int bar = 10;
が提案されている。
診断機能や修正機能を作成するに当たって
今回はサンプル通りに作成したため、構文木のどの部分が診断の対象・修正の対象となるかを考える必要がなかったが、例えばCode Fixを一から作成することになった場合、この作業は非常に労力がかかるであろう。そこで、冒頭で紹介したSyntax Visualizerが非常に役立つことになる(※メニューバーの[表示]-[その他のウインドウ]-[Syntax Visualizer]で表示できる)。
Roslynを用いたプログラムの作成で、構文木の改変や構築の箇所に行き詰った際は、Syntax Visualizerを活用すると、その解決策が見えてくるかもしれないことを覚えておこう。
まとめ
前回はRoslynの概要を取り上げ、今回は、サンプルプログラムを用いながら、その機能の一部を使用したプログラミング方法を学んだ。
Roslynは現在も開発途中であり、日々多くの人々が議論を行いながら、魅力的な進化を遂げている。Roslynが今後の.NETやVisual Studioにとって重要な基盤の一部となっているのは間違いないであろう。Roslynによって、今後の.NETがより大きく成長していくことに期待したい。

1. Visual Studio 2015の新機能“Roslyn”とは
Visual Studio 2015に採用された次世代コンパイラー「Roslyn」の概要を解説。これまでの進化過程を振り返り、そのAPIレイヤーや、Roslynにより強化されたC#/VB言語機能の一部について紹介する。Visual Studio “14” CTP3→正式版に合わせて改訂。