LoginSignup
1
3

More than 3 years have passed since last update.

[Roslyn] オブジェクトインスタンス化コード生成に必要な要素の調査

Posted at

前書き

Roslynによるインターフェースの実装クラスの構築で、ソースコードをパーズし、ASTを作るところまでは見たので、続きとして戻り値の型のインスタンスを生成する。

インスタンスを生成するためには、少なくとも

  • 目的の型の名称
  • 引数
    • 引数の型名
    • 仮引数名
      • 名前付き引数形式で値を渡す場合
    • デフォルト引数の有無
    • インスタンス科においてパラメータ修飾子(outrefなど)は不要そう
  • 初期化子
    • コンストラクタが明示されず、プロパティやフィールドへの直接代入する場合

が必要。

また、IEnumerable<T>Nullable<T>でエンベロープする場合は、それらの型も必要。

実行時であれば、リフレクションで取得できるが、まだビルド前のためそれも叶わない。
このような状況で、Roslynでは意味解析の結果を取得するAPI(セマンティックAPI)が提供されている。

そこでオブジェクトインスタンス化に必要な要素の収集するため、このセマンティックAPIを使用してを調べた記録を残す。

調査結果

ここでは結果を保持しておく型として

using System.Linq;

using System.Collections.Immutable;
using System.Collections.Generic;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace SemSymbols {
    public enum TypeVerCategory {
        Unknown,
        Constructor,
        Properties,
        Fields,
    }

    public struct TypeVar<TSymbol> 
        where TSymbol: ISymbol
    {
        public TypeVerCategory Category { get; private set; }
        public ImmutableArray<TSymbol> Symbols { get; private set; } 

        public TypeVar(TypeVerCategory inCategory, ImmutableArray<TSymbol> inSymbols) {
            this.Category = inCategory;
            this.Symbols = inSymbols;
        }
    }

    public static class TypeVar {
        public static TypeVar<IParameterSymbol> OfConstructor(ImmutableArray<IParameterSymbol> inSymbols) {
            return new TypeVar<IParameterSymbol>(TypeVerCategory.Constructor, inSymbols);
        }
        public static TypeVar<IPropertySymbol> OfProperties(ImmutableArray<IPropertySymbol> inSymbols) {
            return new TypeVar<IPropertySymbol>(TypeVerCategory.Properties, inSymbols);
        }
        public static TypeVar<IFieldSymbol> OfFields(ImmutableArray<IFieldSymbol> inSymbols) {
            return new TypeVar<IFieldSymbol>(TypeVerCategory.Fields, inSymbols);
        }
    }

    public struct MethodParamTypeVar {
        IParameterSymbol[] Parameters { get; set; }
    }

    public struct EntitiyContext {
        public SpecialType ContainerType { get; set; }
        public INamedTypeSymbol NamedType { get; set; }
        public TypeVar<IParameterSymbol>[] Constructors { get; set; }
        public TypeVar<IPropertySymbol> PropertyTypeVars { get; set; }
        public TypeVar<IFieldSymbol> FieldTypeVars { get; set; }
    }
}

を用意し、セマンティックAPIを駆使して以下のメソッドを組み上げた。

namespace SemSymbols {
    // (snip)

    public static class SemSymbolHelper {
        public static bool TryCResolveReturnTypeContext(Microsoft.CodeAnalysis.TypeInfo inReturnTypeInfo, out EntitiyContext outCtx) {
            outCtx = default;

            var typeSymbol = inReturnTypeInfo.Type as INamedTypeSymbol;
            if (typeSymbol == null) return false;

            var fmtFqcn = new SymbolDisplayFormat(
                typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
                miscellaneousOptions: SymbolDisplayMiscellaneousOptions.ExpandNullable
            );

            (outCtx.ContainerType, outCtx.NamedType) = ResolveAsContainerType(typeSymbol);

            if (outCtx.NamedType == null) return false;

            var memberSymbols = outCtx.NamedType.GetMembers();

            outCtx.Constructors = 
                memberSymbols
                .OfType<IMethodSymbol>()
                .Where(s => s.MethodKind == MethodKind.Constructor)
                .Where(s => s.Parameters.Length > 0)
                .Select(s => TypeVar.OfConstructor(s.Parameters))
                .ToArray()
            ;

            outCtx.PropertyTypeVars = TypeVar.OfProperties(
                memberSymbols.OfType<IPropertySymbol>().Where(s => ! s.IsReadOnly).ToImmutableArray()
            );

            outCtx.FieldTypeVars = TypeVar.OfFields(
                memberSymbols.OfType<IFieldSymbol>().Where(s => ! s.IsImplicitlyDeclared).ToImmutableArray()
            );

            return true;
        }

        private static System.ValueTuple<SpecialType, INamedTypeSymbol> ResolveAsContainerType(INamedTypeSymbol inSymbol) {
            var fmtFqcn = new SymbolDisplayFormat(
                typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
                miscellaneousOptions: SymbolDisplayMiscellaneousOptions.ExpandNullable
            );

            return 
                inSymbol.ToDisplayString(fmtFqcn) switch {
                    "System.Nullable" => 
                        (SpecialType.System_Nullable_T, inSymbol.TypeArguments[0] as INamedTypeSymbol), 
                    "System.Collections.Generic.IEnumerable" =>
                        (SpecialType.System_Collections_Generic_IEnumerable_T, inSymbol.TypeArguments[0] as INamedTypeSymbol),
                    _ => 
                        (SpecialType.None, inSymbol),
                }
            ;
        }
    }
}

ユニットテスト

以下のユニットテスト、(最低限の要素が)収集できたことを確認した。

using NUnit.Framework;

using System;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

using GenSyntaxTestHelpers;
using SemSymbols;

namespace SemModelTests {
    public class SemModelTest {
        private static readonly string IntfSource1 = @"
        namespace SemModels {
            public interface IColorDao1 {
                ColorData? FindById(int id);
            }
        }            
        ";
        private static readonly string EntitySource1 = @"
        namespace SemModels {
            public readonly struct ColorData {
                public int Id { get; }
                public string Name { get; }
                public int Red { get; }
                public int Green { get; }
                public int Blue { get; }

                public ColorData(int id, string name, int red = default, int green = default, int blue = default) => 
                    (Id, Name, Red, Green, Blue) = (id, name, red, green, blue);
            }    
        }    
        ";

        private static readonly string IntfSource2 = @"
        namespace SemModels {
            public interface IColorDao2 {
                ColorDataMut FindById(int id);
            }
        }            
        ";
        private static readonly string EntitySource2 = @"
        namespace SemModels {
            public struct ColorDataMut {
                public int Id { get; set; }
                public int Code { get => this.Id; }
                public string Name { get; set; }
                public int Red { get; set; }
                public int Green { get; set; }
                public int Blue { get; set; }
            }    
        }    
        ";

        private static readonly string IntfSource3 = @"
        using System.Collections.Generic;

        namespace SemModels {
            public interface IColorDao3 {
                IEnumerable<ColorDataMut2> FindAll();
            }
        }            
        ";

        private static readonly string EntitySource3 = @"
        namespace SemModels {
            public struct ColorDataMut2 {
                public int id;
                public string name;
                public int red;
                public int green;
                public int blue;
            }    
        }    
        ";

        [Test]
        public void _戻り値型のコンテキストを解決_コンストラクタを持つ場合() {
            var intfTree = SyntaxFactory.ParseSyntaxTree(IntfSource1);
            var entityTree = SyntaxFactory.ParseSyntaxTree(EntitySource1);

            var compiler = SyntaxGeneratorHelper.CreateCompilation(intfTree, entityTree);

            var ns = (NamespaceDeclarationSyntax)intfTree.GetCompilationUnitRoot().Members[0];
            var intf = (InterfaceDeclarationSyntax)ns.Members[0];
            var meth = (MethodDeclarationSyntax)intf.Members[0];

            var model = compiler.GetSemanticModel(intfTree);
            var entityInfo = model.GetTypeInfo(meth.ReturnType);    

            Assert.IsTrue(SemSymbolHelper.TryCResolveReturnTypeContext(entityInfo, out var ctx), "解決できていること");
            Assert.That(ctx.ContainerType, Is.EqualTo(SpecialType.System_Nullable_T), "Nullableによるコンテナ型");
            Assert.That(ctx.NamedType, Is.Not.Null, "型名のシンボルが取得できていること");
            Assert.That(ctx.NamedType.ToDisplayString(), Is.EqualTo("SemModels.ColorData"), "型名が一致すること");

            Assert.That(ctx.Constructors.Length, Is.EqualTo(1), "1つコンストラクタが定義されていること(デフォルトコンストラクタは除外)");
            Assert.That(ctx.Constructors[0].Category, Is.EqualTo(TypeVerCategory.Constructor), "カテゴリがコンストラクタであること");
            Assert.That(ctx.Constructors[0].Symbols.Length, Is.EqualTo(5), "コンストラクタ引数の数が一致していること");

            var arg1 = ctx.Constructors[0].Symbols[0];
            {
                Assert.That(arg1.Type.SpecialType, Is.EqualTo(SpecialType.System_Int32), "引数[1]がint型であること");
                Assert.That(arg1.Name, Is.EqualTo("id"), "引数名[1]");
                Assert.That(arg1.IsOptional, Is.Not.True, "引数[1はオプショナルではないこと");
            }
            var arg2 = ctx.Constructors[0].Symbols[1];
            {
                Assert.That(arg2.Type.SpecialType, Is.EqualTo(SpecialType.System_String), "引数[2]がstring型であること");
                Assert.That(arg2.Name, Is.EqualTo("name"), "引数名[2]");
                Assert.That(arg2.IsOptional, Is.Not.True, "引数[2]はオプショナルではないこと");
            }

            var arg3 = ctx.Constructors[0].Symbols[2];
            {
                Assert.That(arg3.Type.SpecialType, Is.EqualTo(SpecialType.System_Int32), "引数[3]がint型であること");
                Assert.That(arg3.Name, Is.EqualTo("red"), "引数名[3]");
                Assert.That(arg3.IsOptional, Is.True, "引数[3]はオプショナル");
            }
            var arg4 = ctx.Constructors[0].Symbols[3];
            {
                Assert.That(arg4.Type.SpecialType, Is.EqualTo(SpecialType.System_Int32), "引数[4]がint型であること");
                Assert.That(arg4.Name, Is.EqualTo("green"), "引数名[4]");
                Assert.That(arg4.IsOptional, Is.True, "引数[4]はオプショナル");
            }
            var arg5 = ctx.Constructors[0].Symbols[4];
            {
                Assert.That(arg5.Type.SpecialType, Is.EqualTo(SpecialType.System_Int32), "引数[5]がint型であること");
                Assert.That(arg5.Name, Is.EqualTo("blue"), "引数名[5]");
                Assert.That(arg5.IsOptional, Is.True, "引数[5]はオプショナル");
            }

            Assert.That(ctx.PropertyTypeVars.Category, Is.EqualTo(TypeVerCategory.Properties), "カテゴリがプロパティであること");
            Assert.That(ctx.PropertyTypeVars.Symbols.Length, Is.EqualTo(0), "Readonlyプロパティは含めないこと");

            Assert.That(ctx.FieldTypeVars.Category, Is.EqualTo(TypeVerCategory.Fields), "カテゴリがフィールドであること");
            Assert.That(ctx.FieldTypeVars.Symbols.Length, Is.EqualTo(0), "暗黙宣言されたフィールドは含めないこと");
        }

        [Test]
        public void _戻り値型のコンテキストを解決_セッタープロパティによる初期化が要求される場合() {
            var intfTree = SyntaxFactory.ParseSyntaxTree(IntfSource2);
            var entityTree = SyntaxFactory.ParseSyntaxTree(EntitySource2);

            var compiler = SyntaxGeneratorHelper.CreateCompilation(intfTree, entityTree);

            var ns = (NamespaceDeclarationSyntax)intfTree.GetCompilationUnitRoot().Members[0];
            var intf = (InterfaceDeclarationSyntax)ns.Members[0];
            var meth = (MethodDeclarationSyntax)intf.Members[0];

            var model = compiler.GetSemanticModel(intfTree);
            var entityInfo = model.GetTypeInfo(meth.ReturnType);    

            Assert.IsTrue(SemSymbolHelper.TryCResolveReturnTypeContext(entityInfo, out var ctx), "解決できていること");
            Assert.That(ctx.ContainerType, Is.EqualTo(SpecialType.None), "エンティティ型");
            Assert.That(ctx.NamedType, Is.Not.Null, "型名のシンボルが取得できていること");
            Assert.That(ctx.NamedType.ToDisplayString(), Is.EqualTo("SemModels.ColorDataMut"), "型名が一致すること");

            Assert.That(ctx.Constructors.Length, Is.EqualTo(0), "1つもコンストラクタが定義されていないこと(デフォルトコンストラクタは除外)");

            Assert.That(ctx.PropertyTypeVars.Category, Is.EqualTo(TypeVerCategory.Properties), "カテゴリがプロパティであること");
            Assert.That(ctx.PropertyTypeVars.Symbols.Length, Is.EqualTo(5), "Writableプロパティを持つこと");

            var prop1 = ctx.PropertyTypeVars.Symbols[0];
            {
                Assert.That(prop1.Type.SpecialType, Is.EqualTo(SpecialType.System_Int32), "プロパティ[1]がint型であること");
                Assert.That(prop1.Name, Is.EqualTo("Id"), "プロパティ名[1]");
            }
            var prop2 = ctx.PropertyTypeVars.Symbols[1];
            {
                Assert.That(prop2.Type.SpecialType, Is.EqualTo(SpecialType.System_String), "プロパティ[2]がstring型であること");
                Assert.That(prop2.Name, Is.EqualTo("Name"), "プロパティ名[2]");
            }
            var prop3 = ctx.PropertyTypeVars.Symbols[2];
            {
                Assert.That(prop3.Type.SpecialType, Is.EqualTo(SpecialType.System_Int32), "プロパティ[3]がint型であること");
                Assert.That(prop3.Name, Is.EqualTo("Red"), "プロパティ名[3]");
            }
            var prop4 = ctx.PropertyTypeVars.Symbols[3];
            {
                Assert.That(prop4.Type.SpecialType, Is.EqualTo(SpecialType.System_Int32), "プロパティ[4]がint型であること");
                Assert.That(prop4.Name, Is.EqualTo("Green"), "プロパティ名[4]");
            }
            var prop5 = ctx.PropertyTypeVars.Symbols[4];
            {
                Assert.That(prop5.Type.SpecialType, Is.EqualTo(SpecialType.System_Int32), "プロパティ[5]がint型であること");
                Assert.That(prop5.Name, Is.EqualTo("Blue"), "プロパティ名[5]");
            }

            Assert.That(ctx.FieldTypeVars.Category, Is.EqualTo(TypeVerCategory.Fields), "カテゴリがフィールドであること");
            Assert.That(ctx.FieldTypeVars.Symbols.Length, Is.EqualTo(0), "暗黙フィールドは除外されていること");
        }

        [Test]
        public void _戻り値型のコンテキストを解決_公開フィールドを持つ場合() {
            var intfTree = SyntaxFactory.ParseSyntaxTree(IntfSource3);
            var entityTree = SyntaxFactory.ParseSyntaxTree(EntitySource3);

            var compiler = SyntaxGeneratorHelper.CreateCompilation(intfTree, entityTree);

            var ns = (NamespaceDeclarationSyntax)intfTree.GetCompilationUnitRoot().Members[0];
            var intf = (InterfaceDeclarationSyntax)ns.Members[0];
            var meth = (MethodDeclarationSyntax)intf.Members[0];

            var model = compiler.GetSemanticModel(intfTree);
            var entityInfo = model.GetTypeInfo(meth.ReturnType);  

            Assert.IsTrue(SemSymbolHelper.TryCResolveReturnTypeContext(entityInfo, out var ctx), "解決できていること");

            Assert.That(ctx.NamedType, Is.Not.Null, "型名のシンボルが取得できていること");
            Assert.That(ctx.NamedType.ToDisplayString(), Is.EqualTo("SemModels.ColorDataMut2"), "型名が一致すること");
            Assert.That(ctx.ContainerType, Is.EqualTo(SpecialType.System_Collections_Generic_IEnumerable_T), "コレクションコンテナ型");

            Assert.That(ctx.Constructors.Length, Is.EqualTo(0), "1つもコンストラクタが定義されていないこと(デフォルトコンストラクタは除外)");

            Assert.That(ctx.PropertyTypeVars.Category, Is.EqualTo(TypeVerCategory.Properties), "カテゴリがプロパティであること");
            Assert.That(ctx.PropertyTypeVars.Symbols.Length, Is.EqualTo(0), "Readonlyプロパティは含めないこと");

            Assert.That(ctx.FieldTypeVars.Category, Is.EqualTo(TypeVerCategory.Fields), "カテゴリがフィールドであること");
            Assert.That(ctx.FieldTypeVars.Symbols.Length, Is.EqualTo(5), "暗黙宣言されたフィールドは含めないこと");

            var f1 = ctx.FieldTypeVars.Symbols[0];
            {
                Assert.That(f1.Type.SpecialType, Is.EqualTo(SpecialType.System_Int32), "フィールド[1]がint型であること");
                Assert.That(f1.Name, Is.EqualTo("id"), "フィールド名[1]");
            }
            var f2 = ctx.FieldTypeVars.Symbols[1];
            {
                Assert.That(f2.Type.SpecialType, Is.EqualTo(SpecialType.System_String), "フィールド[2]がstring型であること");
                Assert.That(f2.Name, Is.EqualTo("name"), "フィールド名[2]");
            }
            var f3 = ctx.FieldTypeVars.Symbols[2];
            {
                Assert.That(f3.Type.SpecialType, Is.EqualTo(SpecialType.System_Int32), "フィールド[3]がint型であること");
                Assert.That(f3.Name, Is.EqualTo("red"), "フィールド名[3]");
            }
            var f4 = ctx.FieldTypeVars.Symbols[3];
            {
                Assert.That(f4.Type.SpecialType, Is.EqualTo(SpecialType.System_Int32), "フィールド[4]がint型であること");
                Assert.That(f4.Name, Is.EqualTo("green"), "フィールド名[4]");
            }
            var f5 = ctx.FieldTypeVars.Symbols[4];
            {
                Assert.That(f5.Type.SpecialType, Is.EqualTo(SpecialType.System_Int32), "フィールド[5]がint型であること");
                Assert.That(f5.Name, Is.EqualTo("blue"), "フィールド名[5]");
            }
        }  
    }
}
1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3