Roslyn の SemanticModel 入門

.NET Compiler Platform (Roslyn) が簡単に利用できるようになり、 Analyzer などでよく見かけるようになりました。記事やブログのポストを覗いてみると、 Roslyn 関係の投稿は大抵 SyntaxTree を解説したもののような気がします。

この記事では、より詳しく分析できる Semantic Model というものについて紹介します。

Semantic Model について

Semantic Model は意味論モデルと訳されます。 Syntax Tree (構文木)がソースコードの構造のみから作られるのに対し、 Semantic Model は使用するライブラリの情報を含んで作られます。

構文の形のみによって判別できるものであれば Syntax Tree で十分ですが、メソッドの使い方のチェック(引数等)や複雑なコードに対しては、より詳しくできる方法が必要となります。

Semantic Model を使うことによる欠点もありますが、下手に Syntax Tree だけで組むよりは良いはずです。

サンプルコード

使うための準備

まず、通常のプログラムで使う場合は以下のように書きます。

14
var code = @""
0
"";/* 解析対象のプログラムコード */
44
45
46
47
48
49
50
51
var tree = CSharpSyntaxTree.ParseText(code);
var mscorlib = MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location);
var compilation = CSharpCompilation.Create("SemanticModel", new[] { tree }, new[] { mscorlib });

// SemanticModelを取得
var semanticModel = compilation.GetSemanticModel(tree);

var root = tree.GetRoot();

この場合、 Syntax Tree を生成し、コンパイル、 Semantic Model の生成という手順を踏みます。見ての通り Semantic Model を使うためにはコンパイルが必要で、リアルタイムで動かすプログラムではネックになる可能性があります。

Analyzer で使う場合は以下のように書きます。 RegisterSemanticModelAction を使うと、内部で Semantic Model が生成されたタイミングで実行する処理を指定できます。

1
2
3
4
5
6
7
8
9
public override void Initialize(AnalysisContext context)
{
context.RegisterSemanticModelAction(Analyze);
}

private static async void Analyze(SemanticModelAnalysisContext context)
{
var semanticModel = context.SemanticModel;
var root = await semanticModel.SyntaxTree.GetRootAsync();

この場合は Semantic Model は IDE の動作の副産物として得られている雰囲気なので、あまりネックにはなりにくい感じはします。

Semantic Model でできること

Syntax Tree と Semantic Model が得られたところ、ようやくお楽しみの分析処理に入ります。 基本的な処理として、 Syntax Tree に対して探索により分析したい部分や候補を見つけ、そのノードを Semantic Model にある各種メソッドに渡すことで、必要な情報を取得するというのが手順となります。

Semantic Model を使ってよかったと思える処理は、識別名が被っているものに対して行う処理だと私は思っているので、 WriteLine() メソッドを例に処理方法を見ていきます。例として、下のソースコードを解析するとします。

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
namespace Hoge
{
using static System.Console;

class Foo
{
void Method()
{
WriteLine("Hello Notebook!");

Write("Not important");
}
}
}
namespace Piyo
{
class Bar
{
void Method()
{
WriteLine("Hello Canvas!");
System.Console.WriteLine("Log...");
System.Console.Write("Not important");
}

void WriteLine(string str) { /* painting code... */ }
}
}

例として行う分析では、標準の WriteLine() と自分で定義したもののどちらが呼ばれるかを調べます。 「 using で何が呼ばれているか、クラスに同名のメソッドがあるかを調べればいい」と思われるかもしれませんが、 partial class の存在や探索の深さを考慮すると、それだけでは問題が解決しない場面も考えられます。

まずは Syntax Tree を使って、 WriteLine() メソッドが使われている部分をすべて列挙します。これは Roslyn の紹介などで出てくるサンプルと同じようなことをやっています。(もう少し短いコードで書けるとは思いますが、念のために長く。)

53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// WriteLineメソッドのSyntax Nodeを取得
var writeLines = root.DescendantNodes().OfType<InvocationExpressionSyntax>()
.Select(x => x.Expression).Where(x =>
{
if (x is IdentifierNameSyntax)
{
return (x as IdentifierNameSyntax).Identifier.Text == nameof(Console.WriteLine);
}
else if (x is MemberAccessExpressionSyntax)
{
return (x as MemberAccessExpressionSyntax).Name.Identifier.Text == nameof(Console.WriteLine);
}
else
{
return false;
}
});

そして、いよいよ Semantic Model を利用します。ここでは GetSymbolInfo() メソッドでシンボル情報、つまり簡単に言えばどのような定義されているかという情報を取得しています。

71
72
73
74
75
76
77
78
79
80
81
foreach (var wl in writeLines)
{
// シンボル情報を取得し、メソッドの定義を調べる
var info = semanticModel.GetSymbolInfo(wl);
var symbol = info.Symbol;
var definition = symbol.OriginalDefinition;

Console.WriteLine(wl.Parent.ToString());
Console.WriteLine(definition.ToString());
Console.WriteLine();
}

Semantic Model には GetSymbolInfo() 以外にも、 Get????() というメソッドが多く用意されています。よく使うのは GetSymbolInfo()GetTypeInfo() でしょうか。

実行結果は次の通りです。

1
2
3
4
5
6
7
8
WriteLine("Hello Notebook!")
System.Console.WriteLine(string)

WriteLine("Hello Canvas!")
Piyo.Bar.WriteLine(string)

System.Console.WriteLine("Log...")
System.Console.WriteLine(string)

いろいろな方法で WriteLine() を呼び出していますが、どの WriteLine() かがすぐにわかります。 Syntax Tree でこれを行うと、かなり複雑で面倒なコードになるでしょう。

まとめ

Roslyn の Semantic Model を紹介しました。難しい問題では、無理に Syntax Tree だけで解決せずに、 Semantic Model の利用を考えてみましょう。

また、 Semantic Model には、この記事では出ていない Flow Analysis という機能もあります。変数のスコープ関係や構文関係( ifwhile など)の分析に使えるもののようです。