はじめに
この記事は株式会社ラグザイア Advent Calendar 2023の記事です。
今回から複数回に分けて、Roslyn API について調べて分かったことを紹介したいと思います。
なお、この記事は、もともと作りたいものありきで Roslyn API を使ったので、網羅的な記事ではなく作りたいものを作るために必要な情報だけに絞った記事になります。ご了承下さい。
背景
先日C#で書かれたソースコードのクラスのフィールド、プロパティについて調べる機会がありました。調査対象が膨大だったので手作業ではなく、プログラムを書いて調査することにしました。
調査では、下記の内容を調べる必要がありました。
- フィールド(プロパティ)の型
- フィールド(プロパティ)の名称
- フィールド(プロパティ)の説明
型と名称についてはリフレクションを使えば行けそうな気がします。
説明については、ソースコードに書いてあるコメントをとってくると良さそうですが、一概にソースコードに書いてあるコメントと言っても様々なパターンがありそうです。
同じ調査をやっていた同僚が、うまいやり方は無いか?とチームの朝会で相談したところ、.NET Compiler Platform SDK 通称、Roslyn API を使うと良さそうと意見をもらうことができました。
Roslyn の概要
Roslyn とは何でしょう?公式の GitHub によると下記のように記述があります。
Roslyn is the open-source implementation of both the C# and Visual Basic compilers with an API surface for building code analysis tools.
DeepLによる翻訳だと下記のとおりです。
Roslynは、C#およびVisual Basicコンパイラのオープンソース実装で、コード解析ツールを構築するためのAPIサーフェスを備えています。
要はコンパイラで、コード解析するために必要な情報を提供してくれるAPIを持っているようです。
Roslyn の GitHub には Microsoft のページへのリンクもありました。
ここから先は構文(コードの構造)を解析するアナライザーに絞って話を進めます。
アナライザーの構文解析を使って何かしたい場合、とても乱暴に手順を説明するとこんな感じです。
- 解析したいソースコードを Roslyn API に食わせます
- Roslyn API がコードを構文ツリーというデータにして吐き出します
- その結果をつかってあれこれします
「あれこれ」というのは使う人しだいです。今回のようにクラスのフィールドなどの情報を収集したり、静的解析ツール、修正ツールやIDE拡張などなど、色々なことに使えそうです。
さて、API が吐き出す構文ツリーについてですが、公式に説明があります。
…私にはイメージが沸きにくかったです orz。色々調べていくとSyntax Visualizer というものを使うと実際の構文ツリーが見れることが分かりました。まずは、これを使ってみます。
Syntax Visualizer で構文ツリーを見てみよう
Syntax Visualizer は API が吐き出した構文ツリーを可視化してみることができる Visual Studio のツールの1つです。
導入方法、使い方は公式にあるので詳細は割愛します。Visual Studio インストーラーで .NET Compiler Platform SDK を入れておかないと使用できないので注意です。
今回はクラスのフィールドとプロパティの情報をとるのが最終目標です。
なので、Syntax Visualizer でクラスのフィールドやプロパティの情報がツリー構造のどの辺にあるのかをざっくり見てみましょう。
ということで雑なサンプルコードを書きます。
namespace DummyNamespace
{
internal class Dummy
{
/// <summary>
/// フィールド
/// </summary>
public string DummyField = "フィールドです";
/// <summary>
/// プロパティ
/// </summary>
public string DummyProperty => "プロパティです";
}
}
このコードを Syntax Visualizer で見てみましょう。
結果はこんな感じになりました。
ClassDeclaration
というものの中に FieldDeclaration
と PropertyDeclaration
というズバリなワードがありました。
FieldDeclaration
の Properties の Declaration の値を見ると string DummyField = "フィールドです"
の値が入っています。どうやらフィールドについて、あれこれしたいときは FieldDeclaration
から値を取得すれば良さそうです。
構文ツリーを展開していくと、色分けされていることに気づきました。これはノード、トークン、トリビアを色分けして表現しているようです。
構文ツリーは、ノード、トークン、およびトリビアの 3 つの項目の型で構成されています。 これらの型については、構文の使用 の記事で詳しく説明されています。 項目は型ごとに異なる色を使用して表されています。 使用されている色の概要については、[凡例] ボタンをクリックします。(https://learn.microsoft.com/ja-jp/dotnet/csharp/roslyn-sdk/syntax-visualizer?tabs=csharp より引用)
ノード、トークン、トリビアについて下記リンクのように説明がありますが、正直良く分かりませんでした。
なんとなーくわかった気になりました。ツリーでもう一度見てみます。
ノードが一番大きな構成要素で、ノードはトークンとトリビアから構成されていそうです。
フィールドの名称をとってみよう
なんとなーく、ツリーがどんなものかは分かってきました。ここらへんで実際に値をとってみたいと思います。
今回はフィールドの名称を取るのをやってみたいと思います。これを実現するためには下記ステップで進めると良さそうです。
- 解析したいコードを Roslyn API に食わせて構文ツリーをゲットする
-
FieldDeclaration
の値のみを構文ツリーからとってくる -
FieldDeclaration
からフィールドの名称をとってくる
先にコードをお見せすると、こんな感じのコードになります。(コンソールアプリケーションです)
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace SyntaxQuickStart
{
class Program
{
const string programText =
@"namespace DummyNamespace
{
internal class Dummy
{
/// <summary>
/// フィールド
/// </summary>
public string DummyField = ""フィールドです"";
}
}";
static void Main()
{
var tree = CSharpSyntaxTree.ParseText(programText);
var root = tree.GetCompilationUnitRoot();
// フィールドを取得
var field = root.DescendantNodes().OfType<FieldDeclarationSyntax>().First();
// フィールドの名称を標準出力に表示
Console.WriteLine(field.Declaration.Variables.First().Identifier.ValueText);
}
}
}
では、各ステップごとにやり方を考えていきましょう。
解析したいコードを Roslyn API に食わせて構文ツリーをゲットする
まず構文ツリーをゲットする必要があります。公式の解説 と GitHub の内容 を参考にして、下記のようにすれば構文ツリーがゲットできることが分かりました。
var tree = CSharpSyntaxTree.ParseText(programText);
var root = tree.GetCompilationUnitRoot();
programText
は解析したいコードの文字列になります。たったこれだけです。すごーい。CSharpSyntaxTree
を使うには Microsoft.CodeAnalysis.CSharp
が必要です。
ParseText
でツリーを作って、ルートにアクセスするには GetCompilationUnitRoot()
を実行すれば良いようです。
FieldDeclaration
の値のみを構文ツリーからとってくる
つぎに、FieldDeclaration
を取りたいです。こちらも公式に似たようなものがあったので 参考にします。
まずルートから子ノードをとってきます。それには、DescendantNodes メソッドを使えば良いようです。また、ツリーに対して、LINQ が使えるようです。 FieldDeclaration
の Type は FieldDeclarationSyntax
でした。これを OfType で絞り込めばいけそうです。
下記のように絞り込みます。
var fields = root.DescendantNodes().OfType<FieldDeclarationSyntax>();
FieldDeclaration
からフィールドの名称をとってくる
最後は FieldDeclaration
から名称を取りたいです。ここまでは公式情報を参考にできましたが、名称の取り方は公式のサンプルからはフィールドの名称の取り方は分かりませんでした。
なので雑に ChatGPT に聞きました。
下記はChatGPTの回答を一部切り取ったものになります。
どうやら、fieldDeclaration.Declaration.Variables
の中の Identifier
というプロパティから名称が取れるようです。
Identifier.ValueText
でググると公式 の記事にヒットしました。
一つずつ、見ていきます。
Declarationは何が入っているか見ましょう。Syntax Visualizer で見ます。
Declaration は日本語で「宣言」という意味のようです。確かに Declaration にはフィールドの宣言部分が丸ごと入っています。
field.Declaration.Variables
の部分で Declaration の値が取得できます。フィールドのとき、複数の Declaration が取れるケースというのはイメージできません。Variables は VariableDeclarationSyntax
のプロパティなので他の場合に複数とれるケースがあるのだろうと推測します。
ということで、フィールドのときは必ず Declaration は1つと想定して First() で Declaration の値をとってくれば良さそうです。
あとはここからプロパティの名称を取れば良いです。ChatGPT から Identifier
を使うと良さそうと聞いています。Identifer
プロパティについて調べると識別子がとれるプロパティということが判明しました。識別子とは名前のこと ですので、field.Declaration.Variables.First().Identifier
でフィールドの名前が取得できそうです!
あとは標準出力に渡す文字列にすればいいだけです。 Identifer
は SyntaxToken
型です。調べると ValueText
プロパティで値を文字列で取得できることが分かりました。
ということでFieldDeclaration
からフィールドの名称をとってくるには下記のようにやれば良いことが分かりました。
// フィールドを取得
var field = root.DescendantNodes().OfType<FieldDeclarationSyntax>().First();
// フィールドの名称を標準出力に表示
Console.WriteLine(field.Declaration.Variables.First().Identifier.ValueText);
実行してみよう
以上よりコードを書きました。今回はファイルからコードを読み込むのではなくヒアドキュメントから読み込むようにします。
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace SyntaxQuickStart
{
class Program
{
const string programText =
@"namespace DummyNamespace
{
internal class Dummy
{
/// <summary>
/// フィールド
/// </summary>
public string DummyField = ""フィールドです"";
}
}";
static void Main()
{
var tree = CSharpSyntaxTree.ParseText(programText);
var root = tree.GetCompilationUnitRoot();
// フィールドを取得
var field = root.DescendantNodes().OfType<FieldDeclarationSyntax>().First();
// フィールドの名称を標準出力に表示
Console.WriteLine(field.Declaration.Variables.First().Identifier.ValueText);
}
}
}
見事にフィールドの名称(識別子)である DummyField
が出力できました!(説明のわりに実行結果がしょぼく見えるのが残念です)
まとめ
ということで今回の概要編では以下のことが分かりました。
- Roslyn API を使うとコードの構文を解析できる
- 解析結果を使って色々できそう(静的解析ツール、修正ツール、IDE拡張などなど)
- 構造の解析結果は構文ツリーでゲットできる
- 構文ツリーからフィールドの解析結果を取得し、そこから名称のみを取得するなどの操作がC#のコードで書ける
今回は概要編ということでフィールドの名称だけを取得しました。
次回以降ではフィールドやプロパティの型、名称、コメント内容を取得する方法について考えていきますので、乞うご期待!