Roslyn の Semantic Model で型情報を取得

.NET Compiler Platform (Roslyn) で型情報を取得する方法。

取得できる情報は 2 種類あるというお話。

コードの Compilation や Analyzer の Context から取得した Semantic Model からノードの型情報を取得する際には、 GetTypeInfo() メソッドを使います( object#GetType() メソッドではないことに注意!)。このメソッドからは、与えたノードについて2種類の型についての情報を得られます。

一つはそのノードのシンボルの型情報そのもの( Type プロパティ)で、 typeof 演算子や GetType() からの情報と似たものまで取得できたりします。ノードがシンボルでない場合は null となるようです。

もう一つは、シンボルが型変換されるときの型情報( ConvertedType プロパティ)です。これは、初期化構文や引数として渡すとき等に暗示的変換が行われる場合は、違っているようです。(何もなければ上と同じ情報)

これらの型情報を文字列として表現する場合には、いつもの ToString() メソッドと ToDisplayString() メソッドのどちらかを使います。後者にはフォーマットを指定することができ、フォーマットの自作も可能ですが SymbolDisplayFormat クラスの静的プロパティの中から選ぶのが楽です。

長いので分割してコードを載せていきます。全体のソースコードはこちらから
まずは解析対象となる埋め込まれたコードを整形して載せます。

var code = @"...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System.Collections;
using System.Collections.Generic;

class Class1
{
void Method1()
{
int foo = 1;
var array = new object[] {
1.0 + foo, // double
typeof(Class1) // System.Type
};

// first
Method2(array, array, array);

// second
Method2(new Class1[1], null, array as IEnumerable<object>);
}

// this is just called, do nothing.
void Method2(object[] _, IEnumerable<object> __, IEnumerable ___) { }
}

このプログラムは、 3 つのそれぞれ型が違う引数のあるダミーのメソッドを呼び出して、どう解析情報が取れるか確かめるように組んであります。また、インスタンス化と null 渡しと `as によるキャストではどうなるかも確認します。

次に解析するほうのプログラムです。 SemanticModel を得るための決まった処理に続き、配列の初期化、メソッド呼び出し 2 つの場所での型情報をそれぞれ取得しています。

39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
var tree = CSharpSyntaxTree.ParseText(code);
var mscorlib = MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location);
var compilation = CSharpCompilation.Create("TypeInfo", new[] { tree }, new[] { mscorlib });
var model = compilation.GetSemanticModel(tree);


var root = tree.GetRoot();
var values = root.DescendantNodes().OfType<ArrayCreationExpressionSyntax>().First()
.ChildNodes().OfType<InitializerExpressionSyntax>().First()
.ChildNodes();
foreach (var item in values)
{
var info = model.GetTypeInfo(item);
Console.WriteLine($"{item.WithoutTrivia().ToFullString()} : {info.Type}, "
+ $"(minimum){info.Type?.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)}, "
+ $"(converted){ info.ConvertedType}");
}
Console.WriteLine();

var calling = root.DescendantNodes().OfType<InvocationExpressionSyntax>().First();
var symbols = calling.ArgumentList.Arguments.Select(a => a.Expression);
Console.WriteLine("first " + calling.WithoutTrivia().ToString());
foreach (var item in symbols.Select((v, i) => new { Index = i, Value = v }))
{
var info = model.GetTypeInfo(item.Value);
Console.WriteLine($"No.{item.Index} : {info.Type}, "
+ $"(minimum){info.Type?.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)}, "
+ $"(converted){info.ConvertedType}");
}
Console.WriteLine();

calling = root.DescendantNodes().OfType<InvocationExpressionSyntax>().Skip(1).First();
symbols = calling.ArgumentList.Arguments.Select(a => a.Expression);
Console.WriteLine("second " + calling.WithoutTrivia().ToString());
foreach (var item in symbols.Select((v, i) => new { Index = i, Value = v }))
{
var info = model.GetTypeInfo(item.Value);
Console.WriteLine($"No.{item.Index} : {info.Type}, "
+ $"(minimum){info.Type?.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)}, "
+ $"(converted){info.ConvertedType}");
}

コードの説明に移る前に、下が出力結果です。

1
2
3
4
5
6
7
8
9
10
11
12
1.0 + foo : double, (minimum)double, (converted)object
typeof(Class1) : System.Type, (minimum)Type, (converted)object

first Method2(array, array, array)
No.0 : object[], (minimum)object[], (converted)object[]
No.1 : object[], (minimum)object[], (converted)System.Collections.Generic.IEnumerable<object>
No.2 : object[], (minimum)object[], (converted)System.Collections.IEnumerable

second Method2(new Class1[1], null, array as IEnumerable<object>)
No.0 : Class1[], (minimum)Class1[], (converted)object[]
No.1 : , (minimum), (converted)System.Collections.Generic.IEnumerable<object>
No.2 : System.Collections.Generic.IEnumerable<object>, (minimum)IEnumerable<object>, (converted)System.Collections.IEnumerable

それぞれの出力の行に関して、左は元の型について ToString() メソッドによるもの、真ん中は元の型について ToDisplay() メソッドに MinimumQualifiedFormat を指定した場合、右は ConvertedType により変換後の表示です。
配列の初期化部分では、元の型がそれぞれ doubleSystem.Type なのに対し、変換された方ではそれぞれ object になっています。
また、メソッド呼び出しでも同様に変わっているのがわかります。なお、出力結果の “second Method2” の “No.1” では空欄になっていますが、これは解析対象のソース内で null を渡しているため、こうなっています。