えんじにあ雑記!

開発していて学んだことをまとめていきます!

Roslyn APIを使って、構文ツリー解析を行う方法

Roslyn APIを使用して、CSharpコードの構文ツリー解析を行う方法を解説します。

※こちらの記事は2021.12月のC#アドベントカレンダー6日目の記事になります。

はじめに

Roslyn APIをあまり知らない、まだ触ったことはないという方向けに、Roslyn APIを使用して構文ツリー解析を行う方法をサンプルコードをベースに説明していきます。

.NET Compiler Platform SDK(Roslyn APIs)とは

まず、.NET Compiler Platform SDK(Rolsyn APIs)とは、コンパイラコンパイル時にソースコードを解析し、構築するデータに対するアクセス方法を提供するAPIです。 このAPIを利用することで、コードの誤りや独自のルール違反を見つけるアナライザー機能や、それらに対する修正機能を構築することができます。例えば、「チームのコーディング規約をチェック」したり、「ライブラリと共に提供することで、一般的な使用方法を伝える」といったことが実現できます。

構文ツリーとは

コンパイラソースコードを理解するために使用するツリー構造のデータを「構文ツリー」と呼びます。この構文ツリーは不変であることが確約されているため、構文ツリーを操作する側が複数スレッドからでも構文ツリーの解析ができるようになっています。

この構文ツリーは主に下記の四つのクラスで構成されています。

Microsoft.CodeAnalysis.SyntaxTreeクラス

解析ツリー全体を表すクラスであり、CsharpならMicrosoft.CodeAnalysis.CSharp.CSharpSyntaxTreeクラスが実装されています。 このクラスのメソッドを使用することで、CSharpコードを解析することができます。

Microsoft.CodeAnalysis.SyntaxNodeクラス

宣言、ステートメント、句、式などの構文構造を表すクラスで、using, namespace, classなどの宣言部分の取得などができます。

Microsoft.CodeAnalysis.SyntaxTokenクラス

個々のキーワードやID、演算子、句読点などを表すクラスで、メソッド名などの取得ができます。

Microsoft.CodeAnalysis.SyntaxTriviaクラス

トークン、プリプロセスディレクティブ、コメントの間の空白などの重要でない情報を表したものになります。

構文ツリーの解析(Rootからの手動探索)

Roslyn APIを使った具体的なコード例として、今回は下記のソースコードの構文ツリー解析方法を説明します。

using System;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace RoslynApiTutorial
{
    public class HelloWorld
    {
        public void Say()
        {
            Console.WriteLine(""Hello, World!"");
    }
}

1. 構文ツリーの作成

まずはCSharp用に実装されたCSharpSyntaxTreeクラスを使用して、構文ツリーを構築し、Rootを取得します。

const string HELLO_WORLD_TEXT = 
@"using System;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace RoslynApiTutorial
{
    public class HelloWorld
    {
        public void Say()
        {
            Console.WriteLine(""Hello, World!"");
    }
}";

// CSharpコードから構文ツリーを構築する
SyntaxTree tree = CSharpSyntaxTree.ParseText(HELLO_WORLD_TEXT);
CompilationUnitSyntax root = tree.GetCompilationUnitRoot();

2. Rootからツリーを探索していく

構文ツリーを構築できたら、Rootを起点に必要な箇所が見つかるまでツリーを探索していきます。 実際にいくつかキーワードを取得する実装例を紹介します。

2.1 トップレベルで宣言されたusings一覧を取得する

SyntaxList<UsingDirectiveSyntax> usingDirectives = root.Usings;

2.2 RoslynApiTutorialというnamespace名を取得する

SyntaxList<UsingDirectiveSyntax> usingDirectives = root.Usings;
NamespaceDeclarationSyntax namespaceDeclaration = (NamespaceDeclarationSyntax)root.Members[0];
Console.WriteLine($"namespace: {namespaceDeclaration.Name}");

2.3 RoslynApiTutorialに含まれるクラスを取得する

SyntaxList<UsingDirectiveSyntax> usingDirectives = root.Usings;
NamespaceDeclarationSyntax namespaceDeclaration = (NamespaceDeclarationSyntax)root.Members[0];
ClassDeclarationSyntax classDeclaration = (ClassDeclarationSyntax)namespaceDeclaration.Members[0];
Console.WriteLine($"class: {classDeclaration.Identifier}");

構文ツリーをRootから順番に辿り「~~DeclarationSyntax」を取得して、さらにそのNodeが保持しているMembersにアクセスしていき、欲しいnodeに到達することで取得が可能となっています。

構文ツリーの解析(構文ウォーカーによる解析)

上記で解説したように、構文ツリーを構築しRootから順番に辿っていくことで欲しいNodeまで探索していくという方法もありますが、Visitorパターンを利用した実装方法(構文ウォーカー)もあるのでそちらも解説します。 構文ウォーカーでは、構文ツリーの探索対象のノードが特定の型の際に呼び出されるメソッドをオーバーライドすることで、そのノードが見つかったら呼び出される処理を挟み込めるようになります。

3.1 CSharpSyntaxWalkerを継承したクラスを定義

class UsingCollector : CSharpSyntaxWalker
{
}

3.2 Usingの宣言ノードに到達した際のメソッドをオーバーライド

class UsingCollector : CSharpSyntaxWalker
{
    // Visit~~という名前でvirtualなメソッドが定義されている
    // using宣言ノードはこのメソッド
    public override void VisitUsingDirective(UsingDirectiveSyntax node)
    {
        base.VisitUsingDirective(node);
    }
}

3.3 オーバーライドしたメソッド内で挟み込みたい処理を行う

今回はusing宣言されているノードを全てListに格納し、外部に公開できるように実装します。

class UsingCollector : CSharpSyntaxWalker
{
    public ICollection<UsingDirectiveSyntax> Usings { get; } = new List<UsingDirectiveSyntax>();

    public override void VisitUsingDirective(UsingDirectiveSyntax node)
    {
        base.VisitUsingDirective(node);
        Usings.Add(node);
    }
}

3.4 実装した構文ウォーカーのVisitを実行する

実装した構文ウォーカークラスのインスタンスを作成し、構文ツリーのRootを渡してVisitを実行します。 先ほどオーバーライドしたVisitUsingDirectiveが呼び出され、Usingsに要素が追加されていることが確認できます。

SyntaxTree tree = CSharpSyntaxTree.ParseText(HELLO_WORLD_TEXT);
CompilationUnitSyntax root = tree.GetCompilationUnitRoot();
var collector = new UsingCollector();
collector.Visit(root);
foreach (var node in collector.Usings)
{
    Console.WriteLine(node);
}

さいごに

今回はRoslyn APIを使って簡単な構文ツリー解析を行うコードの解説を行いました。Roslyn APIを使用したAnalyzerは公式でもいくつか実装されたものがあるのでそう言ったものを使用したり、プロジェクト独自に開発してみるのも良いかもしれません。 公開されているAnalyzerやより詳細を学んでいく際に参考になりそうなサイトをいくつか記載しておきます。

参考リンク

github.com

github.com

swet.dena.com

www.buildinsider.net

docs.microsoft.com