ネコのために鐘は鳴る

寺院に住み着くパソコ〇好き

(C#) Source Generator で typeof の型名を取得する

C# 9.0 から使える Source Generator、良いですね。アプリケーションを作る側の人が Source Generator を作ることは少ないと思いますが、ライブラリを作る側の人には便利ですよね。

基本的な使い方・作り方はここでは解説しません。公式の説明公式サンプルを見たり、調べたりすれば出てきます。少ないのが残念なのですけれども。

Source Generator で属性を作って、属性の情報からコードを生成したりするというのは典型的な使い方ですね。

// ユーザーコード
[GenerateProperty(typeof(SomeType), "Prop1")]
partial class TargetType
{
}
// ------------------------
// 生成されたコード
partial class TargetType
{
    public SomeType Prop1 { get; set; }
}

上記のサンプルコードに特に意味はありません、ただの説明用の例です。GeneratePropertyという属性を使ってプロパティを自動生成してくれるソースジェネレーターを想定してください。(GeneratePropertyという属性自体もソースジェネレーターが生成した属性です。BCL にこんな属性があるわけではないです。)

シンタックスの読み取りはISourceGeneratorから提供される Roslyn の API を使うしかないのですが、ネットで検索しても情報が少なすぎて非常に苦労します。日本語で検索してもほとんど出てきません。英語でもかなり情報は少ないです。

このソースジェネレータ―を作るためには、typeof(SomeType)の部分から"SomeType"を読み取ってくる必要があるのですが、その方法の情報がなさ過ぎて苦労したので覚書です。

以下、ソースジェネレータ―の実装の一部。(例なので雑です。実際はもうちょっとちゃんと書いてください)

[2021/02/01 追記] サンプルコードと説明が色々と間違っていました。完全に別のものと勘違いしていました。以下、正しく書きなおしました。

private const string AttributeDef = 
@"namespace Sample
{
    internal sealed class GeneratePropertyAttribute : System.Attribute
    {

    }
}
";

private readonly Regex _attrRegex =
    new Regex(@"^(global::)?(Sample\.)?GenerateProperty(Attribute)?$");

public void Execute(GeneratorExecutionContext context)
{
    // ここで最初に属性自体を出力する (略)

    // シンタックスを総なめして属性を探す
    var compilation = context.Compilation;
    var attrs = compilation
        .SyntaxTrees
        .SelectMany(s => s.GetRoot().DescendantNodes())
        .Where(s => s.IsKind(SyntaxKind.Attribute))
        .OfType<AttributeSyntax>()
        .Where(s => _attrRegex.IsMatch(s.Name.ToString()))
        .ToArray();
    foreach(AttributeSyntax attr in attrs) {
        var attrSemantic = compilation.GetSemanticModel(attr.SyntaxTree);
        var expr = attr.ArgumentList.Arguments[0].Expression;

        // これが typeof の中身の型のフルネーム "SomeNamespace.SomeType"
        string typeName = attrSemantic
            .GetSymbolInfo((expr as TypeOfExpressionSyntax).Type)
            .Symbol!.ToString();

        // ↓ 間違い
        // string typeName = attrSemantic.GetConstantValue(expr).ToString();

        // 以下略 (生成コードの出力)
    }
}

SyntaxReceiverを使う方法もありますが、私はサクッと LINQ で syntax tree を全部なめるのが好きなので LINQ を使いました。どっちが良いのかはわかりませんが、特に問題はないでしょう。

ここで取得できる型名の文字列は、書いたソースコードの expression がそのまま取得できるので、typeof(SomeNamespace.SomeType)と書いた場合は"SomeNamespace.SomeType"となります。

セマンティクスをきちんと考慮してくれるため、typeof(SomeType)と書かれていた場合でも、きちんと名前空間付きのフルネーム"SomeNamespace.SomeType"を取得できます。

属性引数にtypeofを指定するのはけっこう頻出パターンだと思うので、日本語で情報があると助かる人が世の中に1人ぐらいはいるんじゃないかな。(いてほしいなぁ)