はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【C#】ISymbol.ToDisplay(SymbolDisplayFormat.FullyQualified)を用いてSymbolの完全修飾名を取得する(プロパティなどのメンバーシンボルでは取得できないので注意)

はじめに

今回はISymbol.ToDisplay(SymbolDisplayFormat.FullyQualified)を用いてSymbolの完全修飾名を取得する方法を紹介したいと思います。
learn.microsoft.com

やり方

ISymbol.ToDisplay(SymbolDisplayFormat.FullyQualified)を利用することで、完全修飾名を取得することができます。

// global::SourceGenerators.Sample.SampleClass
ISymbol symbol = namedSymbol;
symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);

まずはISymbol.ToDisplayStringについては以下のような実装になってます。

namespace Microsoft.CodeAnalysis
{
    /// <summary>
    /// Represents a symbol (namespace, class, method, parameter, etc.)
    /// exposed by the compiler.
    /// </summary>
    /// <remarks>
    /// This interface is reserved for implementation by its associated APIs. We reserve the right to
    /// change it in the future.
    /// </remarks>
    [InternalImplementationOnly]
    public interface ISymbol : IEquatable<ISymbol?>
    {
        // ...
        
        /// <summary>
        /// Converts the symbol to a string representation.
        /// </summary>
        /// <param name="format">Format or null for the default.</param>
        /// <returns>A formatted string representation of the symbol.</returns>
        string ToDisplayString(SymbolDisplayFormat? format = null);
        
        // ...
    }
}

learn.microsoft.com

引数にSymbolDisplayFormatを指定するわけですが、これによってどうようなフォーマットで出力されるかを決定します。今回の場合はFullyQualifiedというプリセットを利用します。
SymbolDisplayFormat クラス (Microsoft.CodeAnalysis) | Microsoft Learn

namespace Microsoft.CodeAnalysis
{
    /// <summary>
    /// Describes the formatting rules that should be used when displaying symbols.
    /// </summary>
    public class SymbolDisplayFormat
    {
        /// <summary>
        /// Formats the names of all types and namespaces in a fully qualified style (including the global alias).
        /// </summary>
        /// <remarks>
        /// The current behavior will not output the fully qualified style as expected for member symbols (such as properties) because memberOptions is not set.
        /// For example, MyNamespace.MyClass.MyPublicProperty will return as MyPublicProperty.
        /// The current behavior displayed here will be maintained for backwards compatibility.
        /// </remarks>
        public static SymbolDisplayFormat FullyQualifiedFormat { get; } =
            new SymbolDisplayFormat(
                globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included,
                typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
                genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
                miscellaneousOptions:
                SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers |
                SymbolDisplayMiscellaneousOptions.UseSpecialTypes);
    }
}

SymbolDisplayFormat クラス (Microsoft.CodeAnalysis) | Microsoft Learn

結構細かく指定できて、気になる方は公式ドキュメントやコードを読むとよいかと思います。

すべての型と名前空間の名前を完全修飾スタイル (グローバル エイリアスを含む) で書式設定します。

SymbolDisplayFormat.FullyQualifiedFormat プロパティ (Microsoft.CodeAnalysis) | Microsoft Learn

実験

SourceGeneratorの場合、まずISymbolを実装したSymbolを取得します。例として雑に[SampleMarker]を定義し、それが付与されているclassに対してソースコード生成処理を走らせます。

[Generator(LanguageNames.CSharp)]
public class SampleSourceGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        // Attribute定義
        context.RegisterPostInitializationOutput(static context =>
        {
            var source = """
                         using System;
                         
                         namespace SourceGenerators.Sample;
                         
                         public class SampleMarkerAttribute : Attribute
                         {
                         }
                         """;
            context.AddSource("SampleMarkerAttribute.Generated.cs", source);
        });
                
        // 上記で定義したAttributeが付与されているClassを探す
        var provider = context.SyntaxProvider.ForAttributeWithMetadataName("SourceGenerators.Sample.SampleMarkerAttribute",
            predicate: static (context, token) =>
            {
                token.ThrowIfCancellationRequested();
                return context is ClassDeclarationSyntax;
            },
            transform: static (context, token) =>
            {
                token.ThrowIfCancellationRequested();
                return context.TargetSymbol as INamedTypeSymbol;
            })
            .Where(x => x != null);
        
        context.RegisterSourceOutput(provider, static (context, namedSymbol) =>
        {
            // 完全修飾名を取得する
            // global::SourceGenerators.Sample.SampleClass
            var typeName = namedSymbol?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
        });
    }
}

RegisterSourceOutputにて、INamedTypeSymbol(厳密な型はNonErrorNamedTypeSymbolですね)からToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)を呼んでみると、以下のような出力が得られました。

// 完全修飾名を取得する
// global::SourceGenerators.Sample.SampleClass
var typeName = namedSymbol?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);

ちなみに余談ですが、型の継承関係は以下のとおりです。

internal sealed class NonErrorNamedTypeSymbol : NamedTypeSymbol
internal abstract class NamedTypeSymbol : TypeSymbol, INamedTypeSymbol
public interface INamedTypeSymbol : ITypeSymbol
public interface ITypeSymbol : INamespaceOrTypeSymbol
public interface INamespaceOrTypeSymbol : ISymbol

動作しないケース

実はSymbolDisplayFormat.FullyQualifiedを利用しても完全修飾名が得られない場合があります。例えばプロパティ(IPropertySymbol)に対して実験してみます。

context.RegisterSourceOutput(provider, static (context, propertySymbol) =>
{
    // 完全修飾名を取得する
    // SampleProperty
    var typeName = propertySymbol?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
});

なんとNameSpaceClassNameもないプロパティ名のみが返ってきました。その理由も公式ドキュメントに記載されています。

memberOptions が設定されていないため、メンバー シンボル (プロパティなど) の場合、現在の動作では完全修飾スタイルは出力されません。 たとえば、MyNamespace.MyClass.MyPublicProperty は MyPublicProperty として返されます。 ここに表示されている現在の動作は、下位互換性のために維持されます。

SymbolDisplayFormat.FullyQualifiedFormat プロパティ (Microsoft.CodeAnalysis) | Microsoft Learn

結構罠だと思うので要注意です。