Roslyn API でサマリータグのコメントの一行目だけを取得する

# はじめに

この記事は株式会社ラグザイア [Advent Calendar 2023](https://qiita.com/advent-calendar/2023/luxiar)の記事です。

[前回](https://qiita.com/lx-sasabo/items/5732e944e290391771bc)は フィールドやプロパティの型と名称を取得してみました。
コメントはまだ取得できていませんでしたね。
さて、コメントと言っても色々ありますが当初の目的ではフィールドやプロパティを説明するコメントを取得するのが目的でした。なので次の3パターンのコメントが取得できれば良さそうと考えました。

1. サマリータグのコメント（1行目だけ）
1. 定義行の直上のコメント
1. 定義行末のコメント

```cs
public class TestClass
{
    /// <summary>
    /// 1. Summary タグのコメント
    /// 捕捉情報や注意事項などのコメント～
    /// </summary>
    public string Hoge => "Hoge";

    // 2. 定義行直上のコメント
    public string Fuga => "Fuga";

    public string Piyo => "Piyo"; // 3.定義行末のコメント
}
```

（コードは面倒くさかったのでプロパティのときの例を書いています。）
今回は、サマリータグのコメントの1行目だけを取得するのをやってみます。

# サマリータグのコメントの1行目だけを取得する

サマリータグのコメントを Roslyn API を使って取得してみます。
なお、1行目だけが欲しいのは2行目以降は、大抵、補足情報だったり注意事項だったりすることが多く、今回調査したかった情報としては冗長だったためです。

## トリビアからサマリーの定義内容を取得する

さて、ここで一旦おさらいをします。まずフィールドの情報は、[FieldDeclarationSyntax クラス](https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.codeanalysis.csharp.syntax.fielddeclarationsyntax?view=roslyn-dotnet-4.6.0) から取得できました。プロパティの場合は [PropertyDeclarationSyntax クラス](https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.codeanalysis.csharp.syntax.propertydeclarationsyntax?view=roslyn-dotnet-4.6.0) から取得できました。
そして、コメントは [構文トリビア](https://learn.microsoft.com/ja-jp/dotnet/csharp/roslyn-sdk/work-with-syntax#syntax-trivia) から取得できそうなことが分かっていました。

コメントですが、フィールドのときでもプロパティのときでも、さほど差があるように思えません。なので、`FieldDeclarationSyntax` と `PropertyDeclarationSyntax` が共に継承しているクラスがあれば、そこから構文トリビアが取得できそうな気がします。
確認してみると[MemberDeclarationSyntax クラス](https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.codeanalysis.csharp.syntax.memberdeclarationsyntax?view=roslyn-dotnet-4.6.0) をどちらのクラスも継承していました。

`MemberDeclarationSyntax` のプロパティやメソッドを確認してみます。[MemberDeclarationSyntax クラスのページ](https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.codeanalysis.csharp.syntax.memberdeclarationsyntax?view=roslyn-dotnet-4.6.0) で「トリビア」を検索すると、いい感じに使えそうなものを発見できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/13e6bf51-ae32-63bd-5f46-19e73e6e7702.png)

[GetTrailingTrivia()](https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.codeanalysis.csharp.csharpsyntaxnode.gettrailingtrivia?view=roslyn-dotnet-4.6.0#microsoft-codeanalysis-csharp-csharpsyntaxnode-gettrailingtrivia)メソッドを使えば、メンバの定義より前に書いてあるコメントがまとめて取得できそうです。

プロパティの TrailingTrivia を取得して表示するコンソールアプリケーションのコードを書いてみます。

```program.cs
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace SyntaxQuickStart
{
    class Program
    {
        const string programText =
@"namespace DummyNamespace
{
    internal class Dummy
    {
        /// <Summary>
        /// ダミープロパティです
        /// </Summary>
        public int DummyProperty => 123;
    }
}";

        static void Main()
        {
            SyntaxTree tree = CSharpSyntaxTree.ParseText(programText);
            CompilationUnitSyntax root = tree.GetCompilationUnitRoot();

            // プロパティを取得
            PropertyDeclarationSyntax property = root.DescendantNodes().OfType<PropertyDeclarationSyntax>().First();

            SyntaxTriviaList trivia = property.GetLeadingTrivia();

            // トリビアの表示
            foreach (SyntaxTrivia item in trivia)
            {
                Console.WriteLine(item.ToString());
            }
        }
    }
}
```

実行結果です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/20d7e406-4319-3e27-daa7-4b383d685c75.png)

おー、今回欲しいサマリーのコメントが入っています。いい感じです。

`GetLeadingTrivia` メソッドがどんな値を返してくれているのか、VisualStudio のデバッガで見てみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/1023ae34-e09e-369a-e091-43cd6d462e50.png)

どうやら今回欲しい部分のコメントは `SingleLineDocumentationCommentTrivia` に含まれていそうですので、フィルタリングします。

```cs
IEnumerable<SyntaxTrivia> trivia = property
    .GetLeadingTrivia()
    .Where(t => t.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia));
```

これでどんな値が取得できるのか、また VisualStudio のデバッガで確認しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/41aaa992-dd5f-f9c4-81bb-54cf4216041c.png)

↑の画像だと小さくて見にくいので値のコピーを行いました。↓の値が取れていました。

```
SyntaxTrivia SingleLineDocumentationCommentTrivia  <Summary>
        /// ダミープロパティです
        /// </Summary>
```

ここまでの処理でサマリータグの定義内容が取得できるようになりました。


## トリビアの構文ツリーから目的のコメントを取得する

欲しいのは「ダミープロパティです」の部分でした。ここからどう取得しましょう？
正規表現を使って「ダミープロパティです」の部分を取得する手もありますが、上記で取得できる `SyntaxTrivia` はさらに構文ツリーをもっていますので、構文ツリーから値を取得したほうが Roslyn を使っている感が出ているのでかっこいいです。

ということで、[`GetStructure()`](https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.codeanalysis.syntaxtrivia.getstructure?view=roslyn-dotnet-4.7.0#microsoft-codeanalysis-syntaxtrivia-getstructure)メソッドを使って、構文ツリーのノードを取得します。さらに、サマリーは一つしかない前提で、`FirstOrDefault()` で絞り込みます。
これでサマリー部分についての構文ツリーが取得できるようになったので、この構文ツリーが構成しているトークンを[`DecendantTokens()`](https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.codeanalysis.syntaxnode.descendanttokens?view=roslyn-dotnet-4.7.0)メソッドで取得し VisualStudio で値をチェックしてみましょう。

```cs
SyntaxNode? node = property
    .GetLeadingTrivia()
    .Where(t => t.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia))
    .Select(t => t.GetStructure())
    .FirstOrDefault();

IEnumerable<SyntaxToken>? tokens = node?.DescendantTokens();
```

すると「ダミープロパティです」の値を持つトークンを見つけることができました。
![Monosnap SyntaxQuickStart (デバッグ中) - Microsoft Visu.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/5cd2c55c-2d21-e844-ac22-56f8f0d16ea7.png)

どうやら `Where(token => token.IsKind(SyntaxKind.XmlTextLiteralToken))` のようにでフィルタリングすれば「ダミープロパティです」の値は取得できそうです。
また、`XmlTextLiteralToken` でフィルタリングすると空白も取得されるので、空白でないものを取得するようにします。
そして、サマリー行の1行目だけ今回は欲しいので `FirstOrDefault` で絞り込みます。

ということで、サマリータグのコメントの1行目だけを取得するプログラムは下記のようになります。

```program.cs
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace SyntaxQuickStart
{
    class Program
    {
        const string programText =
@"namespace DummyNamespace
{
    internal class Dummy
    {
        /// <Summary>
        /// ダミープロパティです。コメントの1行目です。
        /// コメントの2行目です。
        /// </Summary>
        public int DummyProperty => 123;
    }
}";

        static void Main()
        {
            SyntaxTree tree = CSharpSyntaxTree.ParseText(programText);
            CompilationUnitSyntax root = tree.GetCompilationUnitRoot();

            // プロパティを取得
            PropertyDeclarationSyntax property = root.DescendantNodes().OfType<PropertyDeclarationSyntax>().First();

            SyntaxNode? node = property
                .GetLeadingTrivia()
                .Where(t => t.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia))
                .Select(t => t.GetStructure())
                .FirstOrDefault();

            var comment = node?.DescendantTokens()
                .Where(token => token.IsKind(SyntaxKind.XmlTextLiteralToken))
                .FirstOrDefault(token => !string.IsNullOrWhiteSpace(token.ValueText))
                .ValueText;

            System.Console.WriteLine(comment);
        }
    }
}
```

こちらが実行結果です。ちゃんとサマリータグのコメントの1行目だけ取得できています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/151b87f2-5bd3-255e-5276-d5863ea20c27.png)

# 次回予告

ということでサマリータグのコメントを取得することができました。次回以降では、 定義行の直上のコメント、定義行末のコメントの取得方法について記事を書きたいと思います。ご期待下さい。