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

# 前書き

[Roslynによるインターフェースの実装クラスの構築](https://qiita.com/ktz_alias/items/3cf626f1522311f3abf2)で、ソースコードをパーズし、ASTを作るところまでは見たので、続きとして戻り値の型のインスタンスを生成する。

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

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

が必要。

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

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

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

# 調査結果

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

```csharp
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を駆使して以下のメソッドを組み上げた。

```csharp
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),
                }
            ;
        }
    }
}
```

# ユニットテスト

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

```csharp
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]");
            }
        }  
    }
}
```