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だけで組むよりは良いはずです。

使うための準備

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

			string code = "???"; // 解析対象のプログラム
			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が生成されたタイミングで実行する処理を指定できます。

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メソッドを例に処理方法を見ていきます。例として、下のソースコードを解析するとします。

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の紹介などで出てくるサンプルと同じようなことをやっています。(もう少し短いコードで書けるとは思いますが、念のために長く。)

			// 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メソッドでシンボル情報、つまり簡単に言えばどのような定義されているかという情報を取得しています。

			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????というメソッドが多く用意されています。よく使うのはGetSymbolInfoGetTypeInfoでしょうか。

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

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という機能もあります。変数のスコープ関係や構文関係(ifやwhileなど)の分析に使えるもののようです。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です