diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Emitter.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Emitter.cs index cb67c7ee1f5fb9..7206d549041147 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Emitter.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Emitter.cs @@ -1,10 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Collections.Immutable; -using System.Diagnostics; -using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; using SourceGenerators; @@ -16,12 +13,6 @@ private sealed partial class Emitter { private readonly SourceProductionContext _context; private readonly SourceGenerationSpec _sourceGenSpec; - - private bool _emitBlankLineBeforeNextStatement; - private int _valueSuffixIndex; - - private static readonly Regex s_arrayBracketsRegex = new(Regex.Escape("[]")); - private readonly SourceWriter _writer = new(); public Emitter(SourceProductionContext context, SourceGenerationSpec sourceGenSpec) @@ -64,163 +55,6 @@ file static class {{Identifier.BindingExtensions}} _context.AddSource($"{Identifier.BindingExtensions}.g.cs", _writer.ToSourceText()); } - private void EmitBindCoreCall( - TypeSpec type, - string memberAccessExpr, - string configArgExpr, - InitializationKind initKind, - Action? writeOnSuccess = null) - { - Debug.Assert(type.CanInitialize); - - if (!type.NeedsMemberBinding) - { - EmitObjectInit(memberAccessExpr, initKind); - return; - } - - string tempIdentifier = GetIncrementalIdentifier(Identifier.temp); - if (initKind is InitializationKind.AssignmentWithNullCheck) - { - Debug.Assert(!type.IsValueType); - _writer.WriteLine($"{type.DisplayString}? {tempIdentifier} = {memberAccessExpr};"); - EmitBindCoreCall(tempIdentifier, InitializationKind.AssignmentWithNullCheck); - } - else if (initKind is InitializationKind.None && type.IsValueType) - { - EmitBindCoreCall(tempIdentifier, InitializationKind.Declaration); - _writer.WriteLine($"{memberAccessExpr} = {tempIdentifier};"); - } - else - { - EmitBindCoreCall(memberAccessExpr, initKind); - } - - void EmitBindCoreCall(string instanceExpr, InitializationKind initKind) - { - string bindCoreCall = $@"{nameof(MethodsToGen_CoreBindingHelper.BindCore)}({configArgExpr}, ref {instanceExpr}, {Identifier.binderOptions});"; - EmitObjectInit(instanceExpr, initKind); - _writer.WriteLine(bindCoreCall); - writeOnSuccess?.Invoke(instanceExpr); - } - - void EmitObjectInit(string instanceExpr, InitializationKind initKind) - { - if (initKind is not InitializationKind.None) - { - this.EmitObjectInit(type, instanceExpr, initKind, configArgExpr); - } - } - } - - private void EmitBindLogicFromString( - ParsableFromStringSpec type, - string sectionValueExpr, - string sectionPathExpr, - Action? writeOnSuccess, - bool checkForNullSectionValue, - bool useIncrementalStringValueIdentifier) - { - StringParsableTypeKind typeKind = type.StringParsableTypeKind; - Debug.Assert(typeKind is not StringParsableTypeKind.None); - - string nonNull_StringValue_Identifier = useIncrementalStringValueIdentifier ? GetIncrementalIdentifier(Identifier.value) : Identifier.value; - string stringValueToParse_Expr = checkForNullSectionValue ? nonNull_StringValue_Identifier : sectionValueExpr; - - string parsedValueExpr; - if (typeKind is StringParsableTypeKind.AssignFromSectionValue) - { - parsedValueExpr = stringValueToParse_Expr; - } - else if (typeKind is StringParsableTypeKind.Enum) - { - parsedValueExpr = $"ParseEnum<{type.DisplayString}>({stringValueToParse_Expr}, () => {sectionPathExpr})"; - } - else - { - parsedValueExpr = $"{type.ParseMethodName}({stringValueToParse_Expr}, () => {sectionPathExpr})"; - } - - if (!checkForNullSectionValue) - { - InvokeWriteOnSuccess(); - } - else - { - EmitStartBlock($"if ({sectionValueExpr} is string {nonNull_StringValue_Identifier})"); - InvokeWriteOnSuccess(); - EmitEndBlock(); - } - - void InvokeWriteOnSuccess() => writeOnSuccess?.Invoke(parsedValueExpr); - } - - private bool EmitObjectInit(TypeSpec type, string memberAccessExpr, InitializationKind initKind, string configArgExpr) - { - Debug.Assert(type.CanInitialize && initKind is not InitializationKind.None); - - string initExpr; - CollectionSpec? collectionType = type as CollectionSpec; - - string effectiveDisplayString = type.DisplayString; - if (collectionType is not null) - { - if (collectionType is EnumerableSpec { InitializationStrategy: InitializationStrategy.Array }) - { - initExpr = $"new {s_arrayBracketsRegex.Replace(effectiveDisplayString, "[0]", 1)}"; - } - else - { - effectiveDisplayString = (collectionType.ConcreteType ?? collectionType).DisplayString; - initExpr = $"new {effectiveDisplayString}()"; - } - } - else if (type.InitializationStrategy is InitializationStrategy.ParameterlessConstructor) - { - initExpr = $"new {effectiveDisplayString}()"; - } - else - { - Debug.Assert(type.InitializationStrategy is InitializationStrategy.ParameterizedConstructor); - string initMethodIdentifier = GetInitalizeMethodDisplayString(((ObjectSpec)type)); - initExpr = $"{initMethodIdentifier}({configArgExpr}, {Identifier.binderOptions})"; - } - - if (initKind == InitializationKind.Declaration) - { - Debug.Assert(!memberAccessExpr.Contains(".")); - _writer.WriteLine($"var {memberAccessExpr} = {initExpr};"); - } - else if (initKind == InitializationKind.AssignmentWithNullCheck) - { - if (collectionType is CollectionSpec - { - InitializationStrategy: InitializationStrategy.ParameterizedConstructor or InitializationStrategy.ToEnumerableMethod - }) - { - if (collectionType.InitializationStrategy is InitializationStrategy.ParameterizedConstructor) - { - _writer.WriteLine($"{memberAccessExpr} = {memberAccessExpr} is null ? {initExpr} : new {effectiveDisplayString}({memberAccessExpr});"); - } - else - { - _writer.WriteLine($"{memberAccessExpr} = {memberAccessExpr} is null ? {initExpr} : {memberAccessExpr}.{collectionType.ToEnumerableMethodCall!};"); - } - } - else - { - _writer.WriteLine($"{memberAccessExpr} ??= {initExpr};"); - } - } - else - { - Debug.Assert(initKind is InitializationKind.SimpleAssignment); - _writer.WriteLine($"{memberAccessExpr} = {initExpr};"); - } - - return true; - } - private void EmitInterceptsLocationAttrDecl() { _writer.WriteLine(); @@ -250,18 +84,6 @@ private void EmitUsingStatements() _writer.WriteLine($"using {@namespace};"); } } - - private void EmitIConfigurationHasValueOrChildrenCheck(bool voidReturn) - { - string returnPostfix = voidReturn ? string.Empty : " null"; - _writer.WriteLine($$""" - if (!{{Identifier.HasValueOrChildren}}({{Identifier.configuration}})) - { - return{{returnPostfix}}; - } - """); - _writer.WriteLine(); - } } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs index deb0f4c0aabc0d..2a6f5d2126e8c8 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs @@ -52,15 +52,15 @@ public Parser(SourceProductionContext context, KnownTypeSymbols typeSymbols, Imm if (SymbolEqualityComparer.Default.Equals(candidateBinderType, _typeSymbols.ConfigurationBinder)) { - RegisterMethodInvocation_ConfigurationBinder(invocation); + ParseInvocation_ConfigurationBinder(invocation); } else if (SymbolEqualityComparer.Default.Equals(candidateBinderType, _typeSymbols.OptionsBuilderConfigurationExtensions)) { - RegisterMethodInvocation_OptionsBuilderExt(invocation); + ParseInvocation_OptionsBuilderExt(invocation); } else if (SymbolEqualityComparer.Default.Equals(candidateBinderType, _typeSymbols.OptionsConfigurationServiceCollectionExtensions)) { - RegisterMethodInvocation_ServiceCollectionExt(invocation); + ParseInvocation_ServiceCollectionExt(invocation); } } @@ -120,7 +120,7 @@ type.TypeKind is TypeKind.TypeParameter or TypeKind.Pointer or TypeKind.Error || if (IsNullable(type, out ITypeSymbol? underlyingType)) { - spec = TryGetTypeSpec(underlyingType, Diagnostics.NullableUnderlyingTypeNotSupported, out TypeSpec? underlyingTypeSpec) + spec = MemberTypeIsBindable(type, underlyingType, Diagnostics.NullableUnderlyingTypeNotSupported, out TypeSpec? underlyingTypeSpec) ? new NullableSpec(type, underlyingTypeSpec) : null; } @@ -165,13 +165,7 @@ type.TypeKind is TypeKind.TypeParameter or TypeKind.Pointer or TypeKind.Error || RegisterTypeDiagnostic(type, diag); } - if (spec is null) - { - return null; - } - - string @namespace = spec.Namespace; - if (@namespace is not null and not "") + if (spec is { Namespace: string @namespace } && @namespace is not "") { _sourceGenSpec.Namespaces.Add(@namespace); } @@ -179,28 +173,36 @@ type.TypeKind is TypeKind.TypeParameter or TypeKind.Pointer or TypeKind.Error || return _createdSpecs[type] = spec; } - private void RegisterTypeForBindCoreMainGen(TypeSpec typeSpec) + private bool TryRegisterTypeForBindCoreMainGen(ComplexTypeSpec type) { - if (typeSpec.NeedsMemberBinding) + if (type.HasBindableMembers) { - RegisterTypeForMethodGen(MethodsToGen_CoreBindingHelper.BindCoreMain, typeSpec); - RegisterTypeForBindCoreGen(typeSpec); - _sourceGenSpec.MethodsToGen_CoreBindingHelper |= MethodsToGen_CoreBindingHelper.AsConfigWithChildren; + bool registeredForBindCoreGen = TryRegisterTypeForBindCoreGen(type); + Debug.Assert(registeredForBindCoreGen); + + RegisterTypeForMethodGen(MethodsToGen_CoreBindingHelper.BindCoreMain, type); + Register_AsConfigWithChildren_HelperForGen_IfRequired(type); + return true; } + + return false; } - private void RegisterTypeForBindCoreGen(TypeSpec typeSpec) + private bool TryRegisterTypeForBindCoreGen(ComplexTypeSpec type) { - if (typeSpec.NeedsMemberBinding) + if (type.HasBindableMembers) { - RegisterTypeForMethodGen(MethodsToGen_CoreBindingHelper.BindCore, typeSpec); + RegisterTypeForMethodGen(MethodsToGen_CoreBindingHelper.BindCore, type); + return true; } + + return false; } private void RegisterTypeForGetCoreGen(TypeSpec typeSpec) { RegisterTypeForMethodGen(MethodsToGen_CoreBindingHelper.GetCore, typeSpec); - _sourceGenSpec.MethodsToGen_CoreBindingHelper |= MethodsToGen_CoreBindingHelper.AsConfigWithChildren; + Register_AsConfigWithChildren_HelperForGen_IfRequired(typeSpec); } private void RegisterTypeForMethodGen(MethodsToGen_CoreBindingHelper method, TypeSpec type) @@ -214,11 +216,19 @@ private void RegisterTypeForMethodGen(MethodsToGen_CoreBindingHelper method, Typ _sourceGenSpec.MethodsToGen_CoreBindingHelper |= method; } + private void Register_AsConfigWithChildren_HelperForGen_IfRequired(TypeSpec possibleComplexType) + { + if (possibleComplexType is ComplexTypeSpec) + { + _sourceGenSpec.MethodsToGen_CoreBindingHelper |= MethodsToGen_CoreBindingHelper.AsConfigWithChildren; + } + } + /// /// Registers interceptors for root binding methods, except for ConfigurationBinder.Bind, - /// which is handled by + /// which is handled by /// - private void RegisterAsInterceptor(Enum method, IInvocationOperation operation) => + private void RegisterInterceptor(Enum method, IInvocationOperation operation) => _sourceGenSpec.InterceptionInfo.RegisterCacheEntry(method, new InterceptorLocationInfo(operation)); private static bool IsNullable(ITypeSymbol type, [NotNullWhen(true)] out ITypeSymbol? underlyingType) @@ -339,61 +349,33 @@ private bool IsParsableFromString(ITypeSymbol type, out StringParsableTypeKind t } } - private bool TryGetTypeSpec(ITypeSymbol type, DiagnosticDescriptor descriptor, out TypeSpec? spec) + private EnumerableSpec? CreateArraySpec(IArrayTypeSymbol arrayTypeSymbol) { - spec = GetOrCreateTypeSpec(type); + ITypeSymbol elementTypeSymbol = arrayTypeSymbol.ElementType; - if (spec is null) - { - RegisterUnsupportedType(type, descriptor); - return false; - } - - return true; - } - - private EnumerableSpec? CreateArraySpec(IArrayTypeSymbol arrayType) - { - if (!TryGetTypeSpec(arrayType.ElementType, Diagnostics.ElementTypeNotSupported, out TypeSpec elementSpec)) + if (!MemberTypeIsBindable(arrayTypeSymbol, elementTypeSymbol, Diagnostics.ElementTypeNotSupported, out TypeSpec elementTypeSpec)) { return null; } - // We want a BindCore method for List as a temp holder for the array values. We know the element type is supported. - EnumerableSpec listSpec = (GetOrCreateTypeSpec(_typeSymbols.List.Construct(arrayType.ElementType)) as EnumerableSpec)!; - RegisterTypeForBindCoreGen(listSpec); + // We want a BindCore method for List as a temp holder for the array values. + // Since the element type is supported, we can certainly a list of elements. + EnumerableSpec listTypeSpec = (EnumerableSpec)GetOrCreateTypeSpec(_typeSymbols.List.Construct(elementTypeSymbol)); - EnumerableSpec spec = new EnumerableSpec(arrayType) + EnumerableSpec spec = new EnumerableSpec(arrayTypeSymbol) { - ElementType = elementSpec, - ConcreteType = listSpec, - InitializationStrategy = InitializationStrategy.Array, + ElementType = elementTypeSpec, + InstantiationStrategy = InstantiationStrategy.Array, PopulationStrategy = CollectionPopulationStrategy.Cast_Then_Add, // Using the concrete list type as a temp holder. - ToEnumerableMethodCall = null, + TypeToInstantiate = listTypeSpec, + PopulationCastType = null, }; - Debug.Assert(spec.CanInitialize); - RegisterTypeForBindCoreGen(spec); - + bool registeredForBindCore = TryRegisterTypeForBindCoreGen(listTypeSpec) && TryRegisterTypeForBindCoreGen(spec); + Debug.Assert(registeredForBindCore); return spec; } - private bool IsSupportedArrayType(ITypeSymbol type) - { - if (type is not IArrayTypeSymbol arrayType) - { - return false; - } - - if (arrayType.Rank > 1) - { - RegisterUnsupportedType(arrayType, Diagnostics.MultiDimArraysNotSupported); - return false; - } - - return true; - } - private CollectionSpec? CreateCollectionSpec(INamedTypeSymbol type) { CollectionSpec? spec; @@ -407,38 +389,38 @@ private bool IsSupportedArrayType(ITypeSymbol type) spec = CreateEnumerableSpec(type); } - if (spec is not null) + if (spec is null) { - RegisterTypeForBindCoreGen(spec); - spec.InitExceptionMessage ??= spec.ElementType.InitExceptionMessage; + return null; } + bool registerForBindCoreGen = TryRegisterTypeForBindCoreGen(spec); + Debug.Assert(registerForBindCoreGen); return spec; } private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, ITypeSymbol keyType, ITypeSymbol elementType) { - if (!TryGetTypeSpec(keyType, Diagnostics.DictionaryKeyNotSupported, out TypeSpec keySpec) || - !TryGetTypeSpec(elementType, Diagnostics.ElementTypeNotSupported, out TypeSpec elementSpec)) + if (!MemberTypeIsBindable(type, keyType, Diagnostics.DictionaryKeyNotSupported, out TypeSpec keySpec) || + !MemberTypeIsBindable(type, elementType, Diagnostics.ElementTypeNotSupported, out TypeSpec elementSpec)) { return null; } - if (keySpec.SpecKind != TypeSpecKind.ParsableFromString) + if (keySpec.SpecKind is not TypeSpecKind.ParsableFromString) { RegisterUnsupportedType(type, Diagnostics.DictionaryKeyNotSupported); return null; } - InitializationStrategy constructionStrategy; + InstantiationStrategy constructionStrategy; CollectionPopulationStrategy populationStrategy; - INamedTypeSymbol? concreteType = null; + INamedTypeSymbol? typeToInstantiate = null; INamedTypeSymbol? populationCastType = null; - string? toEnumerableMethodCall = null; if (HasPublicParameterLessCtor(type)) { - constructionStrategy = InitializationStrategy.ParameterlessConstructor; + constructionStrategy = InstantiationStrategy.ParameterlessConstructor; if (HasAddMethod(type, keyType, elementType)) { @@ -457,17 +439,16 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, ITypeSymbol k } else if (IsInterfaceMatch(type, _typeSymbols.GenericIDictionary_Unbound) || IsInterfaceMatch(type, _typeSymbols.IDictionary)) { - concreteType = _typeSymbols.Dictionary; - constructionStrategy = InitializationStrategy.ParameterlessConstructor; + typeToInstantiate = _typeSymbols.Dictionary; + constructionStrategy = InstantiationStrategy.ParameterlessConstructor; populationStrategy = CollectionPopulationStrategy.Add; } else if (IsInterfaceMatch(type, _typeSymbols.IReadOnlyDictionary_Unbound)) { - concreteType = _typeSymbols.Dictionary; + typeToInstantiate = _typeSymbols.Dictionary; populationCastType = _typeSymbols.GenericIDictionary; - constructionStrategy = InitializationStrategy.ToEnumerableMethod; + constructionStrategy = InstantiationStrategy.ToEnumerableMethod; populationStrategy = CollectionPopulationStrategy.Cast_Then_Add; - toEnumerableMethodCall = "ToDictionary(pair => pair.Key, pair => pair.Value)"; _sourceGenSpec.Namespaces.Add("System.Linq"); } else @@ -476,38 +457,37 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, ITypeSymbol k return null; } + Debug.Assert(!(populationStrategy is CollectionPopulationStrategy.Cast_Then_Add && populationCastType is null)); + DictionarySpec spec = new(type) { KeyType = (ParsableFromStringSpec)keySpec, ElementType = elementSpec, - InitializationStrategy = constructionStrategy, + InstantiationStrategy = constructionStrategy, PopulationStrategy = populationStrategy, - ToEnumerableMethodCall = toEnumerableMethodCall, + TypeToInstantiate = ConstructGenericCollectionSpecIfRequired(typeToInstantiate, keyType, elementType) as DictionarySpec, + PopulationCastType = ConstructGenericCollectionSpecIfRequired(populationCastType, keyType, elementType) as DictionarySpec, }; - Debug.Assert(!(populationStrategy is CollectionPopulationStrategy.Cast_Then_Add && populationCastType is null)); - spec.ConcreteType = ConstructGenericCollectionSpecIfRequired(concreteType, keyType, elementType); - spec.PopulationCastType = ConstructGenericCollectionSpecIfRequired(populationCastType, keyType, elementType); - return spec; } private EnumerableSpec? CreateEnumerableSpec(INamedTypeSymbol type) { if (!TryGetElementType(type, out ITypeSymbol? elementType) || - !TryGetTypeSpec(elementType, Diagnostics.ElementTypeNotSupported, out TypeSpec elementSpec)) + !MemberTypeIsBindable(type, elementType, Diagnostics.ElementTypeNotSupported, out TypeSpec elementSpec)) { return null; } - InitializationStrategy constructionStrategy; + InstantiationStrategy instantiationStrategy; CollectionPopulationStrategy populationStrategy; - INamedTypeSymbol? concreteType = null; + INamedTypeSymbol? typeToInstantiate = null; INamedTypeSymbol? populationCastType = null; if (HasPublicParameterLessCtor(type)) { - constructionStrategy = InitializationStrategy.ParameterlessConstructor; + instantiationStrategy = InstantiationStrategy.ParameterlessConstructor; if (HasAddMethod(type, elementType)) { @@ -527,35 +507,35 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, ITypeSymbol k else if (IsInterfaceMatch(type, _typeSymbols.GenericICollection_Unbound) || IsInterfaceMatch(type, _typeSymbols.GenericIList_Unbound)) { - concreteType = _typeSymbols.List; - constructionStrategy = InitializationStrategy.ParameterlessConstructor; + typeToInstantiate = _typeSymbols.List; + instantiationStrategy = InstantiationStrategy.ParameterlessConstructor; populationStrategy = CollectionPopulationStrategy.Add; } else if (IsInterfaceMatch(type, _typeSymbols.GenericIEnumerable_Unbound)) { - concreteType = _typeSymbols.List; + typeToInstantiate = _typeSymbols.List; populationCastType = _typeSymbols.GenericICollection; - constructionStrategy = InitializationStrategy.ParameterizedConstructor; + instantiationStrategy = InstantiationStrategy.ParameterizedConstructor; populationStrategy = CollectionPopulationStrategy.Cast_Then_Add; } else if (IsInterfaceMatch(type, _typeSymbols.ISet_Unbound)) { - concreteType = _typeSymbols.HashSet; - constructionStrategy = InitializationStrategy.ParameterlessConstructor; + typeToInstantiate = _typeSymbols.HashSet; + instantiationStrategy = InstantiationStrategy.ParameterlessConstructor; populationStrategy = CollectionPopulationStrategy.Add; } else if (IsInterfaceMatch(type, _typeSymbols.IReadOnlySet_Unbound)) { - concreteType = _typeSymbols.HashSet; + typeToInstantiate = _typeSymbols.HashSet; populationCastType = _typeSymbols.ISet; - constructionStrategy = InitializationStrategy.ParameterizedConstructor; + instantiationStrategy = InstantiationStrategy.ParameterizedConstructor; populationStrategy = CollectionPopulationStrategy.Cast_Then_Add; } else if (IsInterfaceMatch(type, _typeSymbols.IReadOnlyList_Unbound) || IsInterfaceMatch(type, _typeSymbols.IReadOnlyCollection_Unbound)) { - concreteType = _typeSymbols.List; + typeToInstantiate = _typeSymbols.List; populationCastType = _typeSymbols.GenericICollection; - constructionStrategy = InitializationStrategy.ParameterizedConstructor; + instantiationStrategy = InstantiationStrategy.ParameterizedConstructor; populationStrategy = CollectionPopulationStrategy.Cast_Then_Add; } else @@ -564,40 +544,37 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, ITypeSymbol k return null; } - Register_AsConfigWithChildren_HelperForGen_IfRequired(elementSpec); + Debug.Assert(!(populationStrategy is CollectionPopulationStrategy.Cast_Then_Add && populationCastType is null)); EnumerableSpec spec = new(type) { ElementType = elementSpec, - InitializationStrategy = constructionStrategy, + InstantiationStrategy = instantiationStrategy, PopulationStrategy = populationStrategy, - ToEnumerableMethodCall = null, + TypeToInstantiate = ConstructGenericCollectionSpecIfRequired(typeToInstantiate, elementType) as EnumerableSpec, + PopulationCastType = ConstructGenericCollectionSpecIfRequired(populationCastType, elementType) as EnumerableSpec, }; - Debug.Assert(!(populationStrategy is CollectionPopulationStrategy.Cast_Then_Add && populationCastType is null)); - spec.ConcreteType = ConstructGenericCollectionSpecIfRequired(concreteType, elementType); - spec.PopulationCastType = ConstructGenericCollectionSpecIfRequired(populationCastType, elementType); - return spec; } - private ObjectSpec? CreateObjectSpec(INamedTypeSymbol type) + private ObjectSpec? CreateObjectSpec(INamedTypeSymbol objectSymbol) { // Add spec to cache before traversing properties to avoid stack overflow. - ObjectSpec objectSpec = new(type); - _createdSpecs.Add(type, objectSpec); + ObjectSpec objectSpec = new(objectSymbol); + _createdSpecs.Add(objectSymbol, objectSpec); string typeName = objectSpec.Name; IMethodSymbol? ctor = null; - DiagnosticDescriptor? diagnosticDescriptor = null; + DiagnosticDescriptor? initDiagDescriptor = null; - if (!(type.IsAbstract || type.TypeKind is TypeKind.Interface)) + if (!(objectSymbol.IsAbstract || objectSymbol.TypeKind is TypeKind.Interface)) { IMethodSymbol? parameterlessCtor = null; IMethodSymbol? parameterizedCtor = null; bool hasMultipleParameterizedCtors = false; - foreach (IMethodSymbol candidate in type.InstanceConstructors) + foreach (IMethodSymbol candidate in objectSymbol.InstanceConstructors) { if (candidate.DeclaredAccessibility is not Accessibility.Public) { @@ -618,35 +595,36 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, ITypeSymbol k } } - bool hasPublicParameterlessCtor = type.IsValueType || parameterlessCtor is not null; + bool hasPublicParameterlessCtor = objectSymbol.IsValueType || parameterlessCtor is not null; if (!hasPublicParameterlessCtor && hasMultipleParameterizedCtors) { - diagnosticDescriptor = Diagnostics.MultipleParameterizedConstructors; + initDiagDescriptor = Diagnostics.MultipleParameterizedConstructors; objectSpec.InitExceptionMessage = string.Format(Emitter.ExceptionMessages.MultipleParameterizedConstructors, typeName); } - ctor = type.IsValueType + ctor = objectSymbol.IsValueType // Roslyn ctor fetching APIs include paramerterless ctors for structs, unlike System.Reflection. ? parameterizedCtor ?? parameterlessCtor : parameterlessCtor ?? parameterizedCtor; } - objectSpec.InitializationStrategy = ctor?.Parameters.Length is 0 ? InitializationStrategy.ParameterlessConstructor : InitializationStrategy.ParameterizedConstructor; - if (ctor is null) { - diagnosticDescriptor = Diagnostics.MissingPublicInstanceConstructor; + initDiagDescriptor = Diagnostics.MissingPublicInstanceConstructor; objectSpec.InitExceptionMessage = string.Format(Emitter.ExceptionMessages.MissingPublicInstanceConstructor, typeName); } + else + { + objectSpec.InstantiationStrategy = ctor.Parameters.Length is 0 ? InstantiationStrategy.ParameterlessConstructor : InstantiationStrategy.ParameterizedConstructor; + } - if (diagnosticDescriptor is not null) + if (initDiagDescriptor is not null) { Debug.Assert(objectSpec.InitExceptionMessage is not null); - RegisterUnsupportedType(type, diagnosticDescriptor); - return objectSpec; + RegisterUnsupportedType(objectSymbol, initDiagDescriptor); } - INamedTypeSymbol current = type; + INamedTypeSymbol current = objectSymbol; while (current is not null) { ImmutableArray members = current.GetMembers(); @@ -655,12 +633,12 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, ITypeSymbol k if (member is IPropertySymbol { IsIndexer: false, IsImplicitlyDeclared: false } property) { string propertyName = property.Name; - TypeSpec? propertyTypeSpec = GetOrCreateTypeSpec(property.Type); + TypeSpec propertyTypeSpec = GetOrCreateTypeSpec(property.Type); - if (propertyTypeSpec?.CanInitialize is not true) + if (propertyTypeSpec?.CanBindTo is not true) { - InvocationDiagnosticInfo propertyDiagnostic = new InvocationDiagnosticInfo(Diagnostics.PropertyNotSupported, new string[] { propertyName, type.ToDisplayString() }); - RegisterTypeDiagnostic(causingType: type, propertyDiagnostic); + InvocationDiagnosticInfo propertyDiagnostic = new InvocationDiagnosticInfo(Diagnostics.PropertyNotSupported, new string[] { propertyName, objectSymbol.ToDisplayString() }); + RegisterTypeDiagnostic(causingType: objectSymbol, propertyDiagnostic); _invocationTargetTypeDiags.Add(propertyDiagnostic); } @@ -668,8 +646,8 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, ITypeSymbol k { AttributeData? attributeData = property.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, _typeSymbols.ConfigurationKeyNameAttribute)); string configKeyName = attributeData?.ConstructorArguments.FirstOrDefault().Value as string ?? propertyName; - PropertySpec spec = new(property) { Type = propertyTypeSpec, ConfigurationKeyName = configKeyName }; + objectSpec.Properties[propertyName] = spec; Register_AsConfigWithChildren_HelperForGen_IfRequired(propertyTypeSpec); } @@ -678,7 +656,7 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, ITypeSymbol k current = current.BaseType; } - if (objectSpec.InitializationStrategy is InitializationStrategy.ParameterizedConstructor) + if (objectSpec.InstantiationStrategy is InstantiationStrategy.ParameterizedConstructor) { List missingParameters = new(); List invalidParameters = new(); @@ -714,9 +692,9 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, ITypeSymbol k } else if (missingParameters.Count > 0) { - if (type.IsValueType) + if (objectSymbol.IsValueType) { - objectSpec.InitializationStrategy = InitializationStrategy.ParameterlessConstructor; + objectSpec.InstantiationStrategy = InstantiationStrategy.ParameterlessConstructor; } else { @@ -724,7 +702,7 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, ITypeSymbol k } } - if (objectSpec.CanInitialize) + if (objectSpec.CanInstantiate) { RegisterTypeForMethodGen(MethodsToGen_CoreBindingHelper.Initialize, objectSpec); } @@ -732,26 +710,25 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, ITypeSymbol k static string FormatParams(List names) => string.Join(",", names); } - Debug.Assert((objectSpec.CanInitialize && objectSpec.InitExceptionMessage is null) || - (!objectSpec.CanInitialize && objectSpec.InitExceptionMessage is not null)); - - if (objectSpec.NeedsMemberBinding) - { - RegisterTypeForBindCoreGen(objectSpec); - } + Debug.Assert((objectSpec.CanInstantiate && objectSpec.InitExceptionMessage is null) || + (!objectSpec.CanInstantiate && objectSpec.InitExceptionMessage is not null) || + (!objectSpec.CanInstantiate && (objectSymbol.IsAbstract || objectSymbol.TypeKind is TypeKind.Interface))); + TryRegisterTypeForBindCoreGen(objectSpec); return objectSpec; } - private void Register_AsConfigWithChildren_HelperForGen_IfRequired(TypeSpec type) + private bool MemberTypeIsBindable(ITypeSymbol containingTypeSymbol, ITypeSymbol memberTypeSymbol, DiagnosticDescriptor containingTypeDiagDescriptor, out TypeSpec? memberTypeSpec) { - if (type.SpecKind is TypeSpecKind.Object or - TypeSpecKind.Enumerable or - TypeSpecKind.Dictionary) + if (GetOrCreateTypeSpec(memberTypeSymbol) is TypeSpec { CanBindTo: true } spec) { - - _sourceGenSpec.MethodsToGen_CoreBindingHelper |= MethodsToGen_CoreBindingHelper.AsConfigWithChildren; + memberTypeSpec = spec; + return true; } + + RegisterUnsupportedType(containingTypeSymbol, containingTypeDiagDescriptor); + memberTypeSpec = null; + return false; } private bool TryGetElementType(INamedTypeSymbol type, out ITypeSymbol? elementType) @@ -794,6 +771,22 @@ private bool IsCandidateDictionary(INamedTypeSymbol type, out ITypeSymbol? keyTy private bool IsCollection(ITypeSymbol type) => type is INamedTypeSymbol namedType && GetInterface(namedType, _typeSymbols.IEnumerable) is not null; + private bool IsSupportedArrayType(ITypeSymbol type) + { + if (type is not IArrayTypeSymbol arrayType) + { + return false; + } + + if (arrayType.Rank > 1) + { + RegisterUnsupportedType(arrayType, Diagnostics.MultiDimArraysNotSupported); + return false; + } + + return true; + } + private static INamedTypeSymbol? GetInterface(INamedTypeSymbol type, INamedTypeSymbol @interface) { if (IsInterfaceMatch(type, @interface)) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Emitter/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/ConfigurationBinder.cs similarity index 94% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Emitter/ConfigurationBinder.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/ConfigurationBinder.cs index c420097d99d6ae..c4f128cffd6c1d 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Emitter/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/ConfigurationBinder.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using SourceGenerators; +using System.Diagnostics; namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { @@ -132,24 +132,20 @@ private void EmitBindMethods_ConfigurationBinder() void EmitMethods(MethodsToGen_ConfigurationBinder method, string additionalParams, string configExpression, bool configureOptions) { - foreach (KeyValuePair> pair in _sourceGenSpec.InterceptionInfo_ConfigBinder.GetOverloadInfo(method)) + foreach ((ComplexTypeSpec type, List interceptorInfoList) in _sourceGenSpec.InterceptionInfo_ConfigBinder.GetOverloadInfo(method)) { - (TypeSpec type, List interceptorInfoList) = (pair.Key, pair.Value); - EmitBlankLineIfRequired(); _writer.WriteLine($"/// Attempts to bind the given object instance to configuration values by matching property names against configuration keys recursively."); EmitInterceptsLocationAnnotations(interceptorInfoList); EmitStartBlock($"public static void {Identifier.Bind}_{type.IdentifierCompatibleSubstring}(this {Identifier.IConfiguration} {Identifier.configuration}, {additionalParams})"); - if (type.NeedsMemberBinding) + if (type.HasBindableMembers) { + Debug.Assert(!type.IsValueType); string binderOptionsArg = configureOptions ? $"{Identifier.GetBinderOptions}({Identifier.configureOptions})" : $"{Identifier.binderOptions}: null"; EmitCheckForNullArgument_WithBlankLine(Identifier.configuration); - if (!type.IsValueType) - { - EmitCheckForNullArgument_WithBlankLine(Identifier.instance, voidReturn: true); - } + EmitCheckForNullArgument_WithBlankLine(Identifier.instance, voidReturn: true); _writer.WriteLine($$""" var {{Identifier.typedObj}} = ({{type.EffectiveType.DisplayString}}){{Identifier.instance}}; {{nameof(MethodsToGen_CoreBindingHelper.BindCore)}}({{configExpression}}, ref {{Identifier.typedObj}}, {{binderOptionsArg}}); diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Emitter/CoreBindingHelpers.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/CoreBindingHelpers.cs similarity index 60% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Emitter/CoreBindingHelpers.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/CoreBindingHelpers.cs index 089095aa472d5b..7b698544c91514 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Emitter/CoreBindingHelpers.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/CoreBindingHelpers.cs @@ -5,8 +5,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; -using SourceGenerators; namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { @@ -14,6 +14,10 @@ public sealed partial class ConfigurationBindingGenerator { private sealed partial class Emitter { + private int _valueSuffixIndex; + private bool _emitBlankLineBeforeNextStatement; + private static readonly Regex s_arrayBracketsRegex = new(Regex.Escape("[]")); + private bool ShouldEmitMethods(MethodsToGen_CoreBindingHelper methods) => (_sourceGenSpec.MethodsToGen_CoreBindingHelper & methods) != 0; private void EmitCoreBindingHelpers() @@ -82,36 +86,56 @@ private void EmitGetCoreMethod() EmitStartBlock($"{conditionKindExpr} ({Identifier.type} == typeof({type.DisplayString}))"); - if (effectiveType is ParsableFromStringSpec stringParsableType) + switch (effectiveType) { - _writer.WriteLine($$""" - if ({{Identifier.configuration}} is not {{Identifier.IConfigurationSection}} {{Identifier.section}}) + case ParsableFromStringSpec stringParsableType: { - throw new {{Identifier.InvalidOperationException}}(); + EmitCastToIConfigurationSection(); + EmitBindingLogic( + stringParsableType, + Expression.sectionValue, + Expression.sectionPath, + writeOnSuccess: parsedValueExpr => _writer.WriteLine($"return {parsedValueExpr};"), + checkForNullSectionValue: stringParsableType.StringParsableTypeKind is not StringParsableTypeKind.AssignFromSectionValue, + useIncrementalStringValueIdentifier: false); } - """); - - EmitBindLogicFromString( - stringParsableType, - Expression.sectionValue, - Expression.sectionPath, - writeOnSuccess: parsedValueExpr => _writer.WriteLine($"return {parsedValueExpr};"), - checkForNullSectionValue: stringParsableType.StringParsableTypeKind is not StringParsableTypeKind.AssignFromSectionValue, - useIncrementalStringValueIdentifier: false); - } - else if (!EmitInitException(effectiveType)) - { - EmitBindCoreCall(effectiveType, Identifier.instance, Identifier.configuration, InitializationKind.Declaration); - _writer.WriteLine($"return {Identifier.instance};"); + break; + case ConfigurationSectionSpec configurationSectionSpec: + { + EmitCastToIConfigurationSection(); + _writer.WriteLine($"return {Identifier.section};"); + } + break; + case ComplexTypeSpec complexType: + { + if (complexType.CanInstantiate) + { + EmitBindingLogic(complexType, Identifier.instance, Identifier.configuration, InitializationKind.Declaration); + _writer.WriteLine($"return {Identifier.instance};"); + } + else if (type is ObjectSpec { InitExceptionMessage: string exMsg }) + { + _writer.WriteLine($@"throw new {Identifier.InvalidOperationException}(""{exMsg}"");"); + } + } + break; } - EmitEndBlock(); + EmitEndBlock(); // End if-check for input type. } _writer.WriteLine(); Emit_NotSupportedException_TypeNotDetectedAsInput(); EmitEndBlock(); _emitBlankLineBeforeNextStatement = true; + + void EmitCastToIConfigurationSection() => + _writer.WriteLine($$""" + if ({{Identifier.configuration}} is not {{Identifier.IConfigurationSection}} {{Identifier.section}}) + { + throw new {{Identifier.InvalidOperationException}}(); + } + """); } private void EmitGetValueCoreMethod() @@ -143,7 +167,7 @@ private void EmitGetValueCoreMethod() string conditionKindExpr = GetConditionKindExpr(ref isFirstType); EmitStartBlock($"{conditionKindExpr} ({Identifier.type} == typeof({type.DisplayString}))"); - EmitBindLogicFromString( + EmitBindingLogic( (ParsableFromStringSpec)type.EffectiveType, Identifier.value, Expression.sectionPath, @@ -175,18 +199,16 @@ private void EmitBindCoreMainMethod() _writer.WriteLine(); bool isFirstType = true; - foreach (TypeSpec type in targetTypes) + foreach (ComplexTypeSpec type in targetTypes) { - TypeSpec effectiveType = type.EffectiveType; + ComplexTypeSpec effectiveType = (ComplexTypeSpec)type.EffectiveType; + Debug.Assert(effectiveType.HasBindableMembers); string conditionKindExpr = GetConditionKindExpr(ref isFirstType); EmitStartBlock($"{conditionKindExpr} ({Identifier.type} == typeof({type.DisplayString}))"); - if (!EmitInitException(effectiveType)) - { - _writer.WriteLine($"var {Identifier.temp} = ({effectiveType.DisplayString}){Identifier.instance};"); - EmitBindCoreCall(type, Identifier.temp, Identifier.configuration, InitializationKind.None); - _writer.WriteLine($"return;"); - } + _writer.WriteLine($"var {Identifier.temp} = ({effectiveType.DisplayString}){Identifier.instance};"); + EmitBindingLogic(type, Identifier.temp, Identifier.configuration, InitializationKind.None); + _writer.WriteLine($"return;"); EmitEndBlock(); } @@ -202,23 +224,23 @@ private void EmitBindCoreMethods() return; } - foreach (TypeSpec type in targetTypes) + foreach (ComplexTypeSpec type in targetTypes) { - Debug.Assert(type.NeedsMemberBinding); + Debug.Assert(type.HasBindableMembers); EmitBlankLineIfRequired(); EmitBindCoreMethod(type); } } - private void EmitBindCoreMethod(TypeSpec type) + private void EmitBindCoreMethod(ComplexTypeSpec type) { string objParameterExpression = $"ref {type.DisplayString} {Identifier.instance}"; EmitStartBlock(@$"public static void {nameof(MethodsToGen_CoreBindingHelper.BindCore)}({Identifier.IConfiguration} {Identifier.configuration}, {objParameterExpression}, {Identifier.BinderOptions}? {Identifier.binderOptions})"); - TypeSpec effectiveType = type.EffectiveType; + ComplexTypeSpec effectiveType = (ComplexTypeSpec)type.EffectiveType; if (effectiveType is EnumerableSpec enumerable) { - if (effectiveType.InitializationStrategy is InitializationStrategy.Array) + if (effectiveType.InstantiationStrategy is InstantiationStrategy.Array) { Debug.Assert(type == effectiveType); EmitPopulationImplForArray((EnumerableSpec)type); @@ -256,7 +278,7 @@ private void EmitInitializeMethods() private void EmitInitializeMethod(ObjectSpec type) { - Debug.Assert(type.CanInitialize); + Debug.Assert(type.CanInstantiate); List ctorParams = type.ConstructorParameters; IEnumerable initOnlyProps = type.Properties.Values.Where(prop => prop is { SetOnInit: true }); List ctorArgList = new(); @@ -283,7 +305,7 @@ private void EmitInitializeMethod(ObjectSpec type) foreach (PropertySpec property in initOnlyProps) { - if (property.ShouldBind() && property.MatchingCtorParam is null) + if (property.ShouldBindTo && property.MatchingCtorParam is null) { EmitBindImplForMember(property); } @@ -314,47 +336,47 @@ void EmitBindImplForMember(MemberSpec member) TypeSpec memberType = member.Type; bool errorOnFailedBinding = member.ErrorOnFailedBinding; - string parsedMemberIdentifierDeclarationPrefix = $"{memberType.DisplayString} {member.Name}"; - string parsedMemberIdentifier; + string parsedMemberDeclarationLhs = $"{memberType.DisplayString} {member.Name}"; + string configKeyName = member.ConfigurationKeyName; + string parsedMemberAssignmentLhsExpr; - if (memberType is ParsableFromStringSpec { StringParsableTypeKind: StringParsableTypeKind.AssignFromSectionValue }) + switch (memberType) { - parsedMemberIdentifier = parsedMemberIdentifierDeclarationPrefix; + case ParsableFromStringSpec { StringParsableTypeKind: StringParsableTypeKind.AssignFromSectionValue }: + { + if (errorOnFailedBinding) + { + string condition = $@"if ({Identifier.configuration}[""{configKeyName}""] is not {parsedMemberDeclarationLhs})"; + EmitThrowBlock(condition); + _writer.WriteLine(); + return; + } - if (errorOnFailedBinding) - { - string condition = $@" if ({Identifier.configuration}[""{member.ConfigurationKeyName}""] is not {memberType.DisplayString} {member.Name})"; - EmitThrowBlock(condition); - _writer.WriteLine(); - return; - } - } - else - { - parsedMemberIdentifier = member.Name; + parsedMemberAssignmentLhsExpr = parsedMemberDeclarationLhs; + } + break; + case ConfigurationSectionSpec: + { + _writer.WriteLine($"{parsedMemberDeclarationLhs} = {GetSectionFromConfigurationExpression(configKeyName)};"); + return; + } + default: + { + string bangExpr = memberType.IsValueType ? string.Empty : "!"; + string parsedMemberIdentifierDeclaration = $"{parsedMemberDeclarationLhs} = {member.DefaultValueExpr}{bangExpr};"; - string declarationSuffix; - if (errorOnFailedBinding) - { - declarationSuffix = ";"; - } - else - { - string bangExpr = memberType.IsValueType ? string.Empty : "!"; - declarationSuffix = memberType.CanInitialize - ? $" = {member.DefaultValueExpr}{bangExpr};" - : ";"; - } + _writer.WriteLine(parsedMemberIdentifierDeclaration); + _emitBlankLineBeforeNextStatement = false; - string parsedMemberIdentifierDeclaration = $"{parsedMemberIdentifierDeclarationPrefix}{declarationSuffix}"; - _writer.WriteLine(parsedMemberIdentifierDeclaration); - _emitBlankLineBeforeNextStatement = false; + parsedMemberAssignmentLhsExpr = member.Name; + } + break; } bool canBindToMember = this.EmitBindImplForMember( member, - parsedMemberIdentifier, - sectionPathExpr: GetSectionPathFromConfigurationExpression(member.ConfigurationKeyName), + parsedMemberAssignmentLhsExpr, + sectionPathExpr: GetSectionPathFromConfigurationExpression(configKeyName), canSet: true); if (canBindToMember) @@ -607,11 +629,11 @@ private void EmitPrimitiveParseMethod(ParsableFromStringSpec type) private void EmitPopulationImplForArray(EnumerableSpec type) { - EnumerableSpec concreteType = (EnumerableSpec)type.ConcreteType; + EnumerableSpec typeToInstantiate = (EnumerableSpec)type.TypeToInstantiate; // Create list and bind elements. string tempIdentifier = GetIncrementalIdentifier(Identifier.temp); - EmitBindCoreCall(concreteType, tempIdentifier, Identifier.configuration, InitializationKind.Declaration); + EmitBindingLogic(typeToInstantiate, tempIdentifier, Identifier.configuration, InitializationKind.Declaration); // Resize array and add binded elements. _writer.WriteLine($$""" @@ -627,22 +649,32 @@ private void EmitPopulationImplForEnumerableWithAdd(EnumerableSpec type) Emit_Foreach_Section_In_ConfigChildren_StartBlock(); - TypeSpec elementType = type.ElementType; + string addExpr = $"{instanceIdentifier}.{Identifier.Add}"; - if (elementType is ParsableFromStringSpec stringParsableType) - { - EmitBindLogicFromString( - stringParsableType, - Expression.sectionValue, - Expression.sectionPath, - (parsedValueExpr) => _writer.WriteLine($"{instanceIdentifier}.{Identifier.Add}({parsedValueExpr});"), - checkForNullSectionValue: true, - useIncrementalStringValueIdentifier: false); - } - else + switch (type.ElementType) { - EmitBindCoreCall(elementType, Identifier.value, Identifier.section, InitializationKind.Declaration); - _writer.WriteLine($"{instanceIdentifier}.{Identifier.Add}({Identifier.value});"); + case ParsableFromStringSpec stringParsableType: + { + EmitBindingLogic( + stringParsableType, + Expression.sectionValue, + Expression.sectionPath, + (parsedValueExpr) => _writer.WriteLine($"{addExpr}({parsedValueExpr});"), + checkForNullSectionValue: true, + useIncrementalStringValueIdentifier: false); + } + break; + case ConfigurationSectionSpec configurationSection: + { + _writer.WriteLine($"{addExpr}({Identifier.section});"); + } + break; + case ComplexTypeSpec { CanInstantiate: true } complexType: + { + EmitBindingLogic(complexType, Identifier.value, Identifier.section, InitializationKind.Declaration); + _writer.WriteLine($"{addExpr}({Identifier.value});"); + } + break; } EmitEndBlock(); @@ -658,7 +690,7 @@ private void EmitBindCoreImplForDictionary(DictionarySpec type) TypeSpec elementType = type.ElementType; // Parse key - EmitBindLogicFromString( + EmitBindingLogic( keyType, Expression.sectionKey, Expression.sectionPath, @@ -668,63 +700,56 @@ private void EmitBindCoreImplForDictionary(DictionarySpec type) void Emit_BindAndAddLogic_ForElement(string parsedKeyExpr) { - if (elementType is ParsableFromStringSpec stringParsableElementType) - { - EmitBindLogicFromString( - stringParsableElementType, - Expression.sectionValue, - Expression.sectionPath, - writeOnSuccess: parsedValueExpr => _writer.WriteLine($"{instanceIdentifier}[{parsedKeyExpr}] = {parsedValueExpr};"), - checkForNullSectionValue: true, - useIncrementalStringValueIdentifier: false); - } - else // For complex types: + switch (elementType) { - Debug.Assert(elementType.CanInitialize); - - if (keyType.StringParsableTypeKind is not StringParsableTypeKind.AssignFromSectionValue) - { - // Save value to local to avoid parsing twice - during look-up and during add. - _writer.WriteLine($"{keyType.DisplayString} {Identifier.key} = {parsedKeyExpr};"); - parsedKeyExpr = Identifier.key; - } - - bool isValueType = elementType.IsValueType; - string expressionForElementIsNotNull = $"{Identifier.element} is not null"; - string elementTypeDisplayString = elementType.DisplayString + (elementType.IsValueType ? string.Empty : "?"); - - string expressionForElementExists = $"{instanceIdentifier}.{Identifier.TryGetValue}({parsedKeyExpr}, out {elementTypeDisplayString} {Identifier.element})"; - string conditionToUseExistingElement = expressionForElementExists; - - // If key already exists, bind to existing element instance if not null (for ref types). - if (!isValueType) - { - conditionToUseExistingElement += $" && {expressionForElementIsNotNull}"; - } + case ParsableFromStringSpec stringParsableElementType: + { + EmitBindingLogic( + stringParsableElementType, + Expression.sectionValue, + Expression.sectionPath, + writeOnSuccess: parsedValueExpr => _writer.WriteLine($"{instanceIdentifier}[{parsedKeyExpr}] = {parsedValueExpr};"), + checkForNullSectionValue: true, + useIncrementalStringValueIdentifier: false); + } + break; + case ConfigurationSectionSpec configurationSection: + { + _writer.WriteLine($"{instanceIdentifier}[{parsedKeyExpr}] = {Identifier.section};"); + } + break; + case ComplexTypeSpec complexElementType: + { + Debug.Assert(complexElementType.CanInstantiate); - EmitStartBlock($"if (!({conditionToUseExistingElement}))"); - EmitObjectInit(elementType, Identifier.element, InitializationKind.SimpleAssignment, Identifier.section); - EmitEndBlock(); + if (keyType.StringParsableTypeKind is not StringParsableTypeKind.AssignFromSectionValue) + { + // Save value to local to avoid parsing twice - during look-up and during add. + _writer.WriteLine($"{keyType.DisplayString} {Identifier.key} = {parsedKeyExpr};"); + parsedKeyExpr = Identifier.key; + } - if (elementType is CollectionSpec { InitializationStrategy: InitializationStrategy.ParameterizedConstructor or InitializationStrategy.ToEnumerableMethod } collectionSpec) - { - // This is a read-only collection. If the element exists and is not null, - // we need to copy its contents into a new instance & then append/bind to that. + bool isValueType = complexElementType.IsValueType; + string expressionForElementIsNotNull = $"{Identifier.element} is not null"; + string elementTypeDisplayString = complexElementType.DisplayString + (complexElementType.IsValueType ? string.Empty : "?"); - string initExpression = collectionSpec.InitializationStrategy is InitializationStrategy.ParameterizedConstructor - ? $"new {collectionSpec.ConcreteType.DisplayString}({Identifier.element})" - : $"{Identifier.element}.{collectionSpec.ToEnumerableMethodCall!}"; + string expressionForElementExists = $"{instanceIdentifier}.{Identifier.TryGetValue}({parsedKeyExpr}, out {elementTypeDisplayString} {Identifier.element})"; + string conditionToUseExistingElement = expressionForElementExists; - _writer.WriteLine($$""" - else + // If key already exists, bind to existing element instance if not null (for ref types). + if (!isValueType) { - {{Identifier.element}} = {{initExpression}}; + conditionToUseExistingElement += $" && {expressionForElementIsNotNull}"; } - """); - } - EmitBindCoreCall(elementType, Identifier.element, Identifier.section, InitializationKind.None); - _writer.WriteLine($"{instanceIdentifier}[{parsedKeyExpr}] = {Identifier.element};"); + EmitStartBlock($"if (!({conditionToUseExistingElement}))"); + EmitObjectInit(complexElementType, Identifier.element, InitializationKind.SimpleAssignment, Identifier.section); + EmitEndBlock(); + + EmitBindingLogic(complexElementType, Identifier.element, Identifier.section, InitializationKind.None); + _writer.WriteLine($"{instanceIdentifier}[{parsedKeyExpr}] = {Identifier.element};"); + } + break; } } @@ -733,7 +758,7 @@ void Emit_BindAndAddLogic_ForElement(string parsedKeyExpr) private void EmitBindCoreImplForObject(ObjectSpec type) { - Debug.Assert(type.NeedsMemberBinding); + Debug.Assert(type.HasBindableMembers); string keyCacheFieldName = GetConfigKeyCacheFieldName(type); string validateMethodCallExpr = $"{Identifier.ValidateConfigurationKeys}(typeof({type.DisplayString}), {keyCacheFieldName}, {Identifier.configuration}, {Identifier.binderOptions});"; @@ -741,8 +766,8 @@ private void EmitBindCoreImplForObject(ObjectSpec type) foreach (PropertySpec property in type.Properties.Values) { - bool noSetter_And_IsReadonly = !property.CanSet && property.Type is CollectionSpec { InitializationStrategy: InitializationStrategy.ParameterizedConstructor }; - if (property.ShouldBind() && !noSetter_And_IsReadonly) + bool noSetter_And_IsReadonly = !property.CanSet && property.Type is CollectionSpec { InstantiationStrategy: InstantiationStrategy.ParameterizedConstructor }; + if (property.ShouldBindTo && !noSetter_And_IsReadonly) { string containingTypeRef = property.IsStatic ? type.DisplayString : Identifier.instance; EmitBindImplForMember( @@ -761,56 +786,60 @@ private bool EmitBindImplForMember( bool canSet) { TypeSpec effectiveMemberType = member.Type.EffectiveType; - - if (effectiveMemberType is ParsableFromStringSpec stringParsableType) - { - if (canSet) - { - bool checkForNullSectionValue = member is ParameterSpec - ? true - : stringParsableType.StringParsableTypeKind is not StringParsableTypeKind.AssignFromSectionValue; - - string nullBangExpr = checkForNullSectionValue ? string.Empty : "!"; - - EmitBlankLineIfRequired(); - EmitBindLogicFromString( - stringParsableType, - $@"{Identifier.configuration}[""{member.ConfigurationKeyName}""]", - sectionPathExpr, - writeOnSuccess: parsedValueExpr => _writer.WriteLine($"{memberAccessExpr} = {parsedValueExpr}{nullBangExpr};"), - checkForNullSectionValue, - useIncrementalStringValueIdentifier: true); - } - - return true; - } - string sectionParseExpr = GetSectionFromConfigurationExpression(member.ConfigurationKeyName); - EmitBlankLineIfRequired(); - - if (effectiveMemberType.SpecKind is TypeSpecKind.IConfigurationSection) + switch (effectiveMemberType) { - _writer.WriteLine($"{memberAccessExpr} = {sectionParseExpr};"); - return true; - } + case ParsableFromStringSpec stringParsableType: + { + if (canSet) + { + bool checkForNullSectionValue = member is ParameterSpec + ? true + : stringParsableType.StringParsableTypeKind is not StringParsableTypeKind.AssignFromSectionValue; + + string nullBangExpr = checkForNullSectionValue ? string.Empty : "!"; + + EmitBlankLineIfRequired(); + EmitBindingLogic( + stringParsableType, + $@"{Identifier.configuration}[""{member.ConfigurationKeyName}""]", + sectionPathExpr, + writeOnSuccess: parsedValueExpr => _writer.WriteLine($"{memberAccessExpr} = {parsedValueExpr}{nullBangExpr};"), + checkForNullSectionValue, + useIncrementalStringValueIdentifier: true); + } - string sectionValidationCall = $"{MethodsToGen_CoreBindingHelper.AsConfigWithChildren}({sectionParseExpr})"; - string sectionIdentifier = GetIncrementalIdentifier(Identifier.section); + return true; + } + case ConfigurationSectionSpec: + { + if (canSet) + { + EmitBlankLineIfRequired(); + _writer.WriteLine($"{memberAccessExpr} = {sectionParseExpr};"); + } - EmitStartBlock($"if ({sectionValidationCall} is {Identifier.IConfigurationSection} {sectionIdentifier})"); + return true; + } + case ComplexTypeSpec complexType: + { + string sectionValidationCall = $"{MethodsToGen_CoreBindingHelper.AsConfigWithChildren}({sectionParseExpr})"; + string sectionIdentifier = GetIncrementalIdentifier(Identifier.section); - bool canInit = !EmitInitException(effectiveMemberType); - if (canInit) - { - EmitBindCoreCallForMember(member, memberAccessExpr, sectionIdentifier, canSet); - } + EmitBlankLineIfRequired(); + EmitStartBlock($"if ({sectionValidationCall} is {Identifier.IConfigurationSection} {sectionIdentifier})"); + EmitBindingLogicForComplexMember(member, memberAccessExpr, sectionIdentifier, canSet); + EmitEndBlock(); - EmitEndBlock(); - return canInit; + return complexType.CanInstantiate; + } + default: + return false; + } } - private void EmitBindCoreCallForMember( + private void EmitBindingLogicForComplexMember( MemberSpec member, string memberAccessExpr, string configArgExpr, @@ -818,7 +847,7 @@ private void EmitBindCoreCallForMember( { TypeSpec memberType = member.Type; - TypeSpec effectiveMemberType = memberType.EffectiveType; + ComplexTypeSpec effectiveMemberType = (ComplexTypeSpec)memberType.EffectiveType; string tempIdentifier = GetIncrementalIdentifier(Identifier.temp); InitializationKind initKind; @@ -872,7 +901,7 @@ private void EmitBindCoreCallForMember( } }; - EmitBindCoreCall( + EmitBindingLogic( effectiveMemberType, targetObjAccessExpr, configArgExpr, @@ -880,6 +909,205 @@ private void EmitBindCoreCallForMember( writeOnSuccess); } + private void EmitBindingLogic( + ComplexTypeSpec type, + string memberAccessExpr, + string configArgExpr, + InitializationKind initKind, + Action? writeOnSuccess = null) + { + if (!type.HasBindableMembers) + { + if (initKind is not InitializationKind.None) + { + if (type.CanInstantiate) + { + EmitObjectInit(type, memberAccessExpr, initKind, configArgExpr); + } + else if (type is ObjectSpec { InitExceptionMessage: string exMsg }) + { + _writer.WriteLine($@"throw new {Identifier.InvalidOperationException}(""{exMsg}"");"); + } + } + + return; + } + + string tempIdentifier = GetIncrementalIdentifier(Identifier.temp); + if (initKind is InitializationKind.AssignmentWithNullCheck) + { + Debug.Assert(!type.IsValueType); + _writer.WriteLine($"{type.DisplayString}? {tempIdentifier} = {memberAccessExpr};"); + EmitBindingLogic(tempIdentifier, InitializationKind.AssignmentWithNullCheck); + } + else if (initKind is InitializationKind.None && type.IsValueType) + { + EmitBindingLogic(tempIdentifier, InitializationKind.Declaration); + _writer.WriteLine($"{memberAccessExpr} = {tempIdentifier};"); + } + else + { + EmitBindingLogic(memberAccessExpr, initKind); + } + + void EmitBindingLogic(string instanceToBindExpr, InitializationKind initKind) + { + string bindCoreCall = $@"{nameof(MethodsToGen_CoreBindingHelper.BindCore)}({configArgExpr}, ref {instanceToBindExpr}, {Identifier.binderOptions});"; + + if (type.CanInstantiate) + { + if (initKind is not InitializationKind.None) + { + EmitObjectInit(type, instanceToBindExpr, initKind, configArgExpr); + } + + EmitBindCoreCall(); + } + else + { + Debug.Assert(!type.IsValueType); + + if (type is ObjectSpec { InitExceptionMessage: string exMsg }) + { + _writer.WriteLine($@"throw new {Identifier.InvalidOperationException}(""{exMsg}"");"); + } + else + { + EmitStartBlock($"if ({instanceToBindExpr} is not null)"); + EmitBindCoreCall(); + EmitEndBlock(); + } + } + + void EmitBindCoreCall() + { + _writer.WriteLine(bindCoreCall); + writeOnSuccess?.Invoke(instanceToBindExpr); + } + } + } + + private void EmitBindingLogic( + ParsableFromStringSpec type, + string sectionValueExpr, + string sectionPathExpr, + Action? writeOnSuccess, + bool checkForNullSectionValue, + bool useIncrementalStringValueIdentifier) + { + StringParsableTypeKind typeKind = type.StringParsableTypeKind; + Debug.Assert(typeKind is not StringParsableTypeKind.None); + + string nonNull_StringValue_Identifier = useIncrementalStringValueIdentifier ? GetIncrementalIdentifier(Identifier.value) : Identifier.value; + string stringValueToParse_Expr = checkForNullSectionValue ? nonNull_StringValue_Identifier : sectionValueExpr; + string parsedValueExpr = typeKind switch + { + StringParsableTypeKind.AssignFromSectionValue => stringValueToParse_Expr, + StringParsableTypeKind.Enum => $"ParseEnum<{type.DisplayString}>({stringValueToParse_Expr}, () => {sectionPathExpr})", + _ => $"{type.ParseMethodName}({stringValueToParse_Expr}, () => {sectionPathExpr})", + }; + + if (!checkForNullSectionValue) + { + InvokeWriteOnSuccess(); + } + else + { + EmitStartBlock($"if ({sectionValueExpr} is string {nonNull_StringValue_Identifier})"); + InvokeWriteOnSuccess(); + EmitEndBlock(); + } + + void InvokeWriteOnSuccess() => writeOnSuccess?.Invoke(parsedValueExpr); + } + + private bool EmitObjectInit(ComplexTypeSpec type, string memberAccessExpr, InitializationKind initKind, string configArgExpr) + { + CollectionSpec? collectionType = type as CollectionSpec; + string initExpr; + + string effectiveDisplayString = type.DisplayString; + if (collectionType is not null) + { + if (collectionType is EnumerableSpec { InstantiationStrategy: InstantiationStrategy.Array }) + { + initExpr = $"new {s_arrayBracketsRegex.Replace(effectiveDisplayString, "[0]", 1)}"; + } + else + { + effectiveDisplayString = (collectionType.TypeToInstantiate ?? collectionType).DisplayString; + initExpr = $"new {effectiveDisplayString}()"; + } + } + else if (type.InstantiationStrategy is InstantiationStrategy.ParameterlessConstructor) + { + initExpr = $"new {effectiveDisplayString}()"; + } + else + { + Debug.Assert(type.InstantiationStrategy is InstantiationStrategy.ParameterizedConstructor); + string initMethodIdentifier = GetInitalizeMethodDisplayString(((ObjectSpec)type)); + initExpr = $"{initMethodIdentifier}({configArgExpr}, {Identifier.binderOptions})"; + } + + switch (initKind) + { + case InitializationKind.Declaration: + { + Debug.Assert(!memberAccessExpr.Contains(".")); + _writer.WriteLine($"var {memberAccessExpr} = {initExpr};"); + } + break; + case InitializationKind.AssignmentWithNullCheck: + { + if (collectionType is CollectionSpec + { + InstantiationStrategy: InstantiationStrategy.ParameterizedConstructor or InstantiationStrategy.ToEnumerableMethod + }) + { + if (collectionType.InstantiationStrategy is InstantiationStrategy.ParameterizedConstructor) + { + _writer.WriteLine($"{memberAccessExpr} = {memberAccessExpr} is null ? {initExpr} : new {effectiveDisplayString}({memberAccessExpr});"); + } + else + { + Debug.Assert(collectionType is DictionarySpec); + _writer.WriteLine($"{memberAccessExpr} = {memberAccessExpr} is null ? {initExpr} : {memberAccessExpr}.ToDictionary(pair => pair.Key, pair => pair.Value);"); + } + } + else + { + _writer.WriteLine($"{memberAccessExpr} ??= {initExpr};"); + } + } + break; + case InitializationKind.SimpleAssignment: + { + _writer.WriteLine($"{memberAccessExpr} = {initExpr};"); + } + break; + default: + { + Debug.Fail($"Invaild initialization kind: {initKind}"); + } + break; + } + + return true; + } + + private void EmitIConfigurationHasValueOrChildrenCheck(bool voidReturn) + { + string returnPostfix = voidReturn ? string.Empty : " null"; + _writer.WriteLine($$""" + if (!{{Identifier.HasValueOrChildren}}({{Identifier.configuration}})) + { + return{{returnPostfix}}; + } + """); + _writer.WriteLine(); + } + private void EmitCollectionCastIfRequired(CollectionSpec type, out string instanceIdentifier) { instanceIdentifier = Identifier.instance; diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Emitter/ExceptionMessages.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/ExceptionMessages.cs similarity index 100% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Emitter/ExceptionMessages.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/ExceptionMessages.cs diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Emitter/Helpers.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/Helpers.cs similarity index 96% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Emitter/Helpers.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/Helpers.cs index fe59e46f73fc0b..8bac0f4f5af03d 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Emitter/Helpers.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/Helpers.cs @@ -12,10 +12,10 @@ public sealed partial class ConfigurationBindingGenerator { private sealed partial class Emitter { - private string? _emittedExtsTargetType; - internal static readonly AssemblyName s_assemblyName = typeof(ConfigurationBindingGenerator).Assembly.GetName(); + private string? _emittedExtsTargetType; + private enum InitializationKind { None = 0, @@ -23,6 +23,7 @@ private enum InitializationKind AssignmentWithNullCheck = 2, Declaration = 3, } + private static class Expression { public const string configurationGetSection = "configuration.GetSection"; @@ -229,19 +230,6 @@ private void EmitCheckForNullArgument_WithBlankLine(string paramName, bool voidR _writer.WriteLine(); } - private bool EmitInitException(TypeSpec type) - { - Debug.Assert(type.InitializationStrategy is not InitializationStrategy.None); - - if (!type.CanInitialize) - { - _writer.WriteLine($@"throw new {Identifier.InvalidOperationException}(""{type.InitExceptionMessage}"");"); - return true; - } - - return false; - } - private string GetIncrementalIdentifier(string prefix) => $"{prefix}{_valueSuffixIndex++}"; private static string GetInitalizeMethodDisplayString(ObjectSpec type) => diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Emitter/OptionsBuilderConfigurationExtensions.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/OptionsBuilderConfigurationExtensions.cs similarity index 100% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Emitter/OptionsBuilderConfigurationExtensions.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/OptionsBuilderConfigurationExtensions.cs diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Emitter/OptionsConfigurationServiceCollectionExtensions.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/OptionsConfigurationServiceCollectionExtensions.cs similarity index 100% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Emitter/OptionsConfigurationServiceCollectionExtensions.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/OptionsConfigurationServiceCollectionExtensions.cs diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Microsoft.Extensions.Configuration.Binder.SourceGeneration.csproj b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Microsoft.Extensions.Configuration.Binder.SourceGeneration.csproj index f63ddec1f4b72e..92c4c04cfa67c9 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Microsoft.Extensions.Configuration.Binder.SourceGeneration.csproj +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Microsoft.Extensions.Configuration.Binder.SourceGeneration.csproj @@ -30,31 +30,30 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ConfigurationSectionSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ConfigurationSectionSpec.cs deleted file mode 100644 index ed1fcac7636ba7..00000000000000 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ConfigurationSectionSpec.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.CodeAnalysis; - -namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration -{ - internal sealed record ConfigurationSectionSpec : TypeSpec - { - public ConfigurationSectionSpec(ITypeSymbol type) : base(type) { } - - public override TypeSpecKind SpecKind => TypeSpecKind.IConfigurationSection; - } -} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/InitializationStrategy.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/InitializationStrategy.cs deleted file mode 100644 index 866dd254e0181e..00000000000000 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/InitializationStrategy.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration -{ - internal enum InitializationStrategy - { - None = 0, - ParameterlessConstructor = 1, - ParameterizedConstructor = 2, - ToEnumerableMethod = 3, - Array = 4, - } -} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Parser/BinderInvocation.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Parser/BinderInvocation.cs similarity index 100% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Parser/BinderInvocation.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Parser/BinderInvocation.cs diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Parser/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Parser/ConfigurationBinder.cs similarity index 88% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Parser/ConfigurationBinder.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Parser/ConfigurationBinder.cs index bce30222493841..9feafd72ce7c59 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Parser/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Parser/ConfigurationBinder.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis.Operations; @@ -14,31 +13,29 @@ public sealed partial class ConfigurationBindingGenerator { private sealed partial class Parser { - private void RegisterMethodInvocation_ConfigurationBinder(BinderInvocation invocation) + private void ParseInvocation_ConfigurationBinder(BinderInvocation invocation) { switch (invocation.Operation.TargetMethod.Name) { case nameof(MethodsToGen_ConfigurationBinder.Bind): { - RegisterBindInvocation(invocation); + ParseBindInvocation_ConfigurationBinder(invocation); } break; case nameof(MethodsToGen_ConfigurationBinder.Get): { - RegisterGetInvocation(invocation); + ParseGetInvocation(invocation); } break; case nameof(MethodsToGen_ConfigurationBinder.GetValue): { - RegisterGetValueInvocation(invocation); + ParseGetValueInvocation(invocation); } break; - default: - return; } } - private void RegisterBindInvocation(BinderInvocation invocation) + private void ParseBindInvocation_ConfigurationBinder(BinderInvocation invocation) { IInvocationOperation operation = invocation.Operation!; ImmutableArray @params = operation.TargetMethod.Parameters; @@ -102,7 +99,7 @@ private void RegisterBindInvocation(BinderInvocation invocation) if (GetTargetTypeForRootInvocationCore(type, invocation.Location) is TypeSpec typeSpec) { - RegisterAsInterceptor_ConfigBinder_BindMethod(overload, typeSpec, invocation.Operation); + RegisterInterceptor(overload, typeSpec, invocation.Operation); } static ITypeSymbol? ResolveType(IOperation conversionOperation) => @@ -122,7 +119,7 @@ private void RegisterBindInvocation(BinderInvocation invocation) }; } - private void RegisterGetInvocation(BinderInvocation invocation) + private void ParseGetInvocation(BinderInvocation invocation) { IInvocationOperation operation = invocation.Operation!; IMethodSymbol targetMethod = operation.TargetMethod; @@ -176,13 +173,13 @@ private void RegisterGetInvocation(BinderInvocation invocation) if (GetTargetTypeForRootInvocation(type, invocation.Location) is TypeSpec typeSpec) { - RegisterAsInterceptor_ConfigBinder(overload, invocation.Operation); + RegisterInvocation(overload, invocation.Operation); RegisterTypeForGetCoreGen(typeSpec); } } - private void RegisterGetValueInvocation(BinderInvocation invocation) + private void ParseGetValueInvocation(BinderInvocation invocation) { IInvocationOperation operation = invocation.Operation!; IMethodSymbol targetMethod = operation.TargetMethod; @@ -245,23 +242,23 @@ private void RegisterGetValueInvocation(BinderInvocation invocation) if (IsParsableFromString(effectiveType, out _) && GetTargetTypeForRootInvocationCore(type, invocation.Location) is TypeSpec typeSpec) { - RegisterAsInterceptor_ConfigBinder(overload, invocation.Operation); + RegisterInvocation(overload, invocation.Operation); RegisterTypeForMethodGen(MethodsToGen_CoreBindingHelper.GetValueCore, typeSpec); } } - private void RegisterAsInterceptor_ConfigBinder(MethodsToGen_ConfigurationBinder overload, IInvocationOperation operation) + private void RegisterInvocation(MethodsToGen_ConfigurationBinder overload, IInvocationOperation operation) { _sourceGenSpec.MethodsToGen_ConfigurationBinder |= overload; - RegisterAsInterceptor(overload, operation); + RegisterInterceptor(overload, operation); } /// /// Registers generated Bind methods as interceptors. This is done differently from other root - /// methods because we need to + /// methods because we need to /// explicitly account for the type to bind, to avoid type-check issues for polymorphic objects. /// - private void RegisterAsInterceptor_ConfigBinder_BindMethod(MethodsToGen_ConfigurationBinder overload, TypeSpec typeSpec, IInvocationOperation operation) + private void RegisterInterceptor(MethodsToGen_ConfigurationBinder overload, TypeSpec typeSpec, IInvocationOperation operation) { _sourceGenSpec.MethodsToGen_ConfigurationBinder |= overload; _sourceGenSpec.InterceptionInfo_ConfigBinder.RegisterOverloadInfo(overload, typeSpec, operation); diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Parser/Diagnostics.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Parser/Diagnostics.cs similarity index 100% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Parser/Diagnostics.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Parser/Diagnostics.cs diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Parser/Extensions.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Parser/Extensions.cs similarity index 89% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Parser/Extensions.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Parser/Extensions.cs index 0ac57e88ed9ee5..fa0b3691ec4047 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Parser/Extensions.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Parser/Extensions.cs @@ -28,6 +28,12 @@ public static void RegisterCacheEntry(this Dictionary> source, out ComplexTypeSpec Key, out List Value) + { + Key = (ComplexTypeSpec)source.Key; + Value = source.Value; + } + public static string ToIdentifierCompatibleSubstring(this ITypeSymbol type) { if (type is IArrayTypeSymbol arrayType) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Parser/OptionsBuilderConfigurationExtensions.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Parser/OptionsBuilderConfigurationExtensions.cs similarity index 77% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Parser/OptionsBuilderConfigurationExtensions.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Parser/OptionsBuilderConfigurationExtensions.cs index f65c67a7878b1a..9cf59a120e1fdc 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Parser/OptionsBuilderConfigurationExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Parser/OptionsBuilderConfigurationExtensions.cs @@ -12,7 +12,7 @@ public sealed partial class ConfigurationBindingGenerator { private sealed partial class Parser { - private void RegisterMethodInvocation_OptionsBuilderExt(BinderInvocation invocation) + private void ParseInvocation_OptionsBuilderExt(BinderInvocation invocation) { IMethodSymbol targetMethod = invocation.Operation.TargetMethod; ImmutableArray @params = targetMethod.Parameters; @@ -25,20 +25,18 @@ private void RegisterMethodInvocation_OptionsBuilderExt(BinderInvocation invocat return; } - ITypeSymbol? typeSymbol = targetMethod.TypeArguments[0].WithNullableAnnotation(NullableAnnotation.None); // This would violate generic type constraint; any such invocation could not have been included in the initial parser. Debug.Assert(typeSymbol?.IsValueType is not true); - TypeSpec typeSpec = GetTargetTypeForRootInvocation(typeSymbol, invocation.Location); - if (typeSpec is null) + if (GetTargetTypeForRootInvocation(typeSymbol, invocation.Location) is not ComplexTypeSpec typeSpec) { return; } if (targetMethod.Name is "Bind") { - RegisterBindInvocation(invocation, typeSpec); + ParseBindInvocation_OptionsBuilderExt(invocation, typeSpec); } else if (targetMethod.Name is "BindConfiguration") { @@ -46,7 +44,7 @@ private void RegisterMethodInvocation_OptionsBuilderExt(BinderInvocation invocat } } - private void RegisterBindInvocation(BinderInvocation invocation, TypeSpec typeSpec) + private void ParseBindInvocation_OptionsBuilderExt(BinderInvocation invocation, ComplexTypeSpec typeSpec) { IInvocationOperation operation = invocation.Operation!; IMethodSymbol targetMethod = operation.TargetMethod; @@ -68,14 +66,14 @@ 3 when SymbolEqualityComparer.Default.Equals(_typeSymbols.ActionOfBinderOptions, _ => MethodsToGen_Extensions_OptionsBuilder.None }; - if (overload is not MethodsToGen_Extensions_OptionsBuilder.None) + if (overload is not MethodsToGen_Extensions_OptionsBuilder.None && + TryRegisterTypeForMethodGen(MethodsToGen_Extensions_ServiceCollection.Configure_T_name_BinderOptions, typeSpec)) { - RegisterAsInterceptor_OptionsBuilder(overload, operation); - RegisterTypeForMethodGen(MethodsToGen_Extensions_ServiceCollection.Configure_T_name_BinderOptions, typeSpec); + RegisterInvocation(overload, operation); } } - private void ParseBindConfigurationInvocation(BinderInvocation invocation, TypeSpec typeSpec) + private void ParseBindConfigurationInvocation(BinderInvocation invocation, ComplexTypeSpec typeSpec) { IMethodSymbol targetMethod = invocation.Operation.TargetMethod; ImmutableArray @params = targetMethod.Parameters; @@ -85,17 +83,17 @@ private void ParseBindConfigurationInvocation(BinderInvocation invocation, TypeS if (paramCount is 3 && @params[1].Type.SpecialType is SpecialType.System_String && - SymbolEqualityComparer.Default.Equals(_typeSymbols.ActionOfBinderOptions, @params[2].Type)) + SymbolEqualityComparer.Default.Equals(_typeSymbols.ActionOfBinderOptions, @params[2].Type) && + TryRegisterTypeForBindCoreMainGen(typeSpec)) { - RegisterAsInterceptor_OptionsBuilder(MethodsToGen_Extensions_OptionsBuilder.BindConfiguration_T_path_BinderOptions, invocation.Operation); - RegisterTypeForBindCoreMainGen(typeSpec); + RegisterInvocation(MethodsToGen_Extensions_OptionsBuilder.BindConfiguration_T_path_BinderOptions, invocation.Operation); } } - private void RegisterAsInterceptor_OptionsBuilder(MethodsToGen_Extensions_OptionsBuilder overload, IInvocationOperation operation) + private void RegisterInvocation(MethodsToGen_Extensions_OptionsBuilder overload, IInvocationOperation operation) { _sourceGenSpec.MethodsToGen_OptionsBuilderExt |= overload; - RegisterAsInterceptor(overload, operation); + RegisterInterceptor(overload, operation); // Emitting refs to IOptionsChangeTokenSource, ConfigurationChangeTokenSource. _sourceGenSpec.Namespaces.Add("Microsoft.Extensions.Options"); diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Parser/OptionsConfigurationServiceCollectionExtensions.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Parser/OptionsConfigurationServiceCollectionExtensions.cs similarity index 77% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Parser/OptionsConfigurationServiceCollectionExtensions.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Parser/OptionsConfigurationServiceCollectionExtensions.cs index d89b124b0695fb..e86231f32e42ab 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Parser/OptionsConfigurationServiceCollectionExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Parser/OptionsConfigurationServiceCollectionExtensions.cs @@ -12,7 +12,7 @@ public sealed partial class ConfigurationBindingGenerator { private sealed partial class Parser { - private void RegisterMethodInvocation_ServiceCollectionExt(BinderInvocation invocation) + private void ParseInvocation_ServiceCollectionExt(BinderInvocation invocation) { IInvocationOperation operation = invocation.Operation!; IMethodSymbol targetMethod = operation.TargetMethod; @@ -72,25 +72,26 @@ @params[1].Type.SpecialType is SpecialType.System_String && ITypeSymbol? typeSymbol = targetMethod.TypeArguments[0].WithNullableAnnotation(NullableAnnotation.None); // This would violate generic type constraint; any such invocation could not have been included in the initial parser. Debug.Assert(typeSymbol?.IsValueType is not true); - TypeSpec typeSpec = GetTargetTypeForRootInvocation(typeSymbol, invocation.Location); - if (typeSpec is null) + if (GetTargetTypeForRootInvocation(typeSymbol, invocation.Location) is ComplexTypeSpec typeSpec && + TryRegisterTypeForMethodGen(overload, typeSpec)) { - return; + RegisterInterceptor(overload, operation); } - - RegisterTypeForMethodGen(overload, typeSpec); - RegisterAsInterceptor(overload, operation); } - private void RegisterTypeForMethodGen(MethodsToGen_Extensions_ServiceCollection overload, TypeSpec typeSpec) + private bool TryRegisterTypeForMethodGen(MethodsToGen_Extensions_ServiceCollection overload, ComplexTypeSpec typeSpec) { - RegisterTypeForBindCoreMainGen(typeSpec); + if (TryRegisterTypeForBindCoreMainGen(typeSpec)) + { + _sourceGenSpec.MethodsToGen_ServiceCollectionExt |= overload; + _sourceGenSpec.Namespaces.Add("Microsoft.Extensions.DependencyInjection"); + // Emitting refs to IOptionsChangeTokenSource, ConfigurationChangeTokenSource, IConfigureOptions<>, ConfigureNamedOptions<>. + _sourceGenSpec.Namespaces.Add("Microsoft.Extensions.Options"); + return true; + } - _sourceGenSpec.MethodsToGen_ServiceCollectionExt |= overload; - _sourceGenSpec.Namespaces.Add("Microsoft.Extensions.DependencyInjection"); - // Emitting refs to IOptionsChangeTokenSource, ConfigurationChangeTokenSource, IConfigureOptions<>, ConfigureNamedOptions<>. - _sourceGenSpec.Namespaces.Add("Microsoft.Extensions.Options"); + return false; } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/InterceptorLocationInfo.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/InterceptorLocationInfo.cs similarity index 100% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/InterceptorLocationInfo.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/InterceptorLocationInfo.cs diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/KnownTypeSymbols.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/KnownTypeSymbols.cs similarity index 100% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/KnownTypeSymbols.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/KnownTypeSymbols.cs diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/MemberSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/MemberSpec.cs similarity index 100% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/MemberSpec.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/MemberSpec.cs diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ParameterSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/ParameterSpec.cs similarity index 100% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ParameterSpec.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/ParameterSpec.cs diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/PropertySpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/PropertySpec.cs similarity index 95% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/PropertySpec.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/PropertySpec.cs index 584e8d570b8a9f..4e9c468c4e3352 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/PropertySpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/PropertySpec.cs @@ -29,6 +29,6 @@ public PropertySpec(IPropertySymbol property) : base(property) public override bool CanSet { get; } - public bool ShouldBind() => CanGet || CanSet; + public bool ShouldBindTo => CanGet || CanSet; } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/MethodsToGen.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/MethodsToGen.cs similarity index 100% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/MethodsToGen.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/MethodsToGen.cs diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/SourceGenerationSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/SourceGenerationSpec.cs similarity index 100% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/SourceGenerationSpec.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/SourceGenerationSpec.cs diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/CollectionSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Types/CollectionSpec.cs similarity index 64% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/CollectionSpec.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Types/CollectionSpec.cs index 280ecc4c536482..f565d245cc5502 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/CollectionSpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Types/CollectionSpec.cs @@ -5,25 +5,19 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { - internal abstract record CollectionSpec : TypeSpec + internal abstract record CollectionSpec : ComplexTypeSpec { public CollectionSpec(ITypeSymbol type) : base(type) { } - public required TypeSpec ElementType { get; init; } - - public CollectionSpec? ConcreteType { get; set; } + public sealed override bool CanInstantiate => TypeToInstantiate?.CanInstantiate ?? InstantiationStrategy is not InstantiationStrategy.None; - public CollectionSpec? PopulationCastType { get; set; } + public required TypeSpec ElementType { get; init; } public required CollectionPopulationStrategy PopulationStrategy { get; init; } - public override bool CanInitialize => ConcreteType?.CanInitialize ?? CanInitComplexObject(); - - public override required InitializationStrategy InitializationStrategy { get; set; } + public required CollectionSpec? TypeToInstantiate { get; init; } - public required string? ToEnumerableMethodCall { get; init; } - - public sealed override bool NeedsMemberBinding => true; + public required CollectionSpec? PopulationCastType { get; init; } } internal sealed record EnumerableSpec : CollectionSpec @@ -31,6 +25,8 @@ internal sealed record EnumerableSpec : CollectionSpec public EnumerableSpec(ITypeSymbol type) : base(type) { } public override TypeSpecKind SpecKind => TypeSpecKind.Enumerable; + + public override bool HasBindableMembers => PopulationStrategy is not CollectionPopulationStrategy.Unknown && ElementType.CanBindTo; } internal sealed record DictionarySpec : CollectionSpec @@ -39,6 +35,8 @@ public DictionarySpec(INamedTypeSymbol type) : base(type) { } public override TypeSpecKind SpecKind => TypeSpecKind.Dictionary; + public override bool HasBindableMembers => PopulationStrategy is not CollectionPopulationStrategy.Unknown; + public required ParsableFromStringSpec KeyType { get; init; } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Types/ComplexTypeSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Types/ComplexTypeSpec.cs new file mode 100644 index 00000000000000..da5a5130141a53 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Types/ComplexTypeSpec.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration +{ + internal abstract record ComplexTypeSpec : TypeSpec + { + public ComplexTypeSpec(ITypeSymbol type) : base(type) { } + + public InstantiationStrategy InstantiationStrategy { get; set; } + + public sealed override bool CanBindTo => CanInstantiate || HasBindableMembers; + + public sealed override TypeSpec EffectiveType => this; + + public abstract bool HasBindableMembers { get; } + } + + internal enum InstantiationStrategy + { + None = 0, + ParameterlessConstructor = 1, + ParameterizedConstructor = 2, + ToEnumerableMethod = 3, + Array = 4, + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/NullableSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Types/NullableSpec.cs similarity index 73% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/NullableSpec.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Types/NullableSpec.cs index 9dcca27596e7ee..3de6d7d465ad98 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/NullableSpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Types/NullableSpec.cs @@ -9,10 +9,11 @@ internal sealed record NullableSpec : TypeSpec { private readonly TypeSpec _underlyingType; - public NullableSpec(ITypeSymbol type, TypeSpec underlyingType) : base(type) - { - _underlyingType = underlyingType; - } + public NullableSpec(ITypeSymbol type, TypeSpec underlyingType) : base(type) => _underlyingType = underlyingType; + + public override bool CanBindTo => _underlyingType.CanBindTo; + + public override bool CanInstantiate => _underlyingType.CanInstantiate; public override TypeSpecKind SpecKind => TypeSpecKind.Nullable; diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ObjectSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Types/ObjectSpec.cs similarity index 65% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ObjectSpec.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Types/ObjectSpec.cs index 09008dde159ad8..f6978fa9cf470a 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ObjectSpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Types/ObjectSpec.cs @@ -8,20 +8,20 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { - internal sealed record ObjectSpec : TypeSpec + internal sealed record ObjectSpec : ComplexTypeSpec { public ObjectSpec(INamedTypeSymbol type) : base(type) { } public override TypeSpecKind SpecKind => TypeSpecKind.Object; - public override InitializationStrategy InitializationStrategy { get; set; } + public override bool HasBindableMembers => Properties.Values.Any(p => p.ShouldBindTo); - public override bool CanInitialize => CanInitComplexObject(); + public override bool CanInstantiate => InstantiationStrategy is not InstantiationStrategy.None && InitExceptionMessage is null; public Dictionary Properties { get; } = new(StringComparer.OrdinalIgnoreCase); public List ConstructorParameters { get; } = new(); - public override bool NeedsMemberBinding => Properties.Values.Any(p => p.ShouldBind()); + public string? InitExceptionMessage { get; set; } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ParsableFromStringSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Types/SimpleTypeSpec.cs similarity index 71% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ParsableFromStringSpec.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Types/SimpleTypeSpec.cs index e19e3e61d210f3..50e488008ad68d 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ParsableFromStringSpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Types/SimpleTypeSpec.cs @@ -6,7 +6,25 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { - internal sealed record ParsableFromStringSpec : TypeSpec + internal abstract record SimpleTypeSpec : TypeSpec + { + public SimpleTypeSpec(ITypeSymbol type) : base(type) { } + + public sealed override bool CanBindTo => true; + + public sealed override TypeSpec EffectiveType => this; + + public sealed override bool CanInstantiate => true; + } + + internal sealed record ConfigurationSectionSpec : SimpleTypeSpec + { + public ConfigurationSectionSpec(ITypeSymbol type) : base(type) { } + + public override TypeSpecKind SpecKind => TypeSpecKind.IConfigurationSection; + } + + internal sealed record ParsableFromStringSpec : SimpleTypeSpec { public ParsableFromStringSpec(ITypeSymbol type) : base(type) { } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/TypeSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Types/TypeSpec.cs similarity index 78% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/TypeSpec.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Types/TypeSpec.cs index cf03e58e5231b1..651a40639f0ced 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/TypeSpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Types/TypeSpec.cs @@ -1,10 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using Microsoft.CodeAnalysis; namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { + [DebuggerDisplay("Name={DisplayString}, Kind={SpecKind}")] internal abstract record TypeSpec { private static readonly SymbolDisplayFormat s_minimalDisplayFormat = new SymbolDisplayFormat( @@ -34,17 +36,11 @@ public TypeSpec(ITypeSymbol type) public abstract TypeSpecKind SpecKind { get; } - public virtual InitializationStrategy InitializationStrategy { get; set; } + public abstract bool CanBindTo { get; } - public virtual string? InitExceptionMessage { get; set; } + public abstract bool CanInstantiate { get; } - public virtual bool CanInitialize => true; - - public virtual bool NeedsMemberBinding { get; } - - public virtual TypeSpec EffectiveType => this; - - protected bool CanInitComplexObject() => InitializationStrategy is not InitializationStrategy.None && InitExceptionMessage is null; + public abstract TypeSpec EffectiveType { get; } } internal enum TypeSpecKind diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs index 76ed9d959e6220..e92ed061808740 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs @@ -3,10 +3,12 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.ComponentModel; using System.Globalization; -using System.Text.Json; using System.Linq; +using System.Net; +using System.Text.Json; using Microsoft.Extensions.Configuration; using Xunit; @@ -730,5 +732,69 @@ public record OidcProviderOptions { public string? Authority { get; set; } } + + public class AClass + { + public EndPointCollection EndPoints { get; init; } = new EndPointCollection(); + + public bool Property { get; set; } = false; + } + + public sealed class EndPointCollection : Collection, IEnumerable + { + public EndPointCollection() { } + + public void Add(string hostAndPort) + { + EndPoint? endpoint; + + if (IPAddress.TryParse(hostAndPort, out IPAddress? address)) + { + endpoint = new IPEndPoint(address, 0); + } + else + { + endpoint = new DnsEndPoint(hostAndPort, 0); + } + + Add(endpoint); + } + } + + internal abstract class AbstractBase + { + public int Value { get; set; } + } + + internal sealed class Derived : AbstractBase { } + + internal sealed class DerivedWithAnotherProp : AbstractBase + { + public int Value2 { get; set; } + } + + internal class ClassWithAbstractCtorParam + { + public AbstractBase AbstractProp { get; } + + public ClassWithAbstractCtorParam(AbstractBase abstractProp) => AbstractProp = abstractProp; + } + + internal class ClassWithOptionalAbstractCtorParam + { + public AbstractBase AbstractProp { get; } + + public ClassWithOptionalAbstractCtorParam(AbstractBase? abstractProp = null) => AbstractProp = abstractProp; + } + + internal class ClassWith_DirectlyAssignable_CtorParams + { + public IConfigurationSection MySection { get; } + public object MyObject { get; } + public string MyString { get; } + + public ClassWith_DirectlyAssignable_CtorParams(IConfigurationSection mySection, object myObject, string myString) => + (MySection, MyObject, MyString) = (mySection, myObject, myString); + } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs index 3846145f71486a..2a6006b38a2711 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs @@ -1940,12 +1940,17 @@ public void BindRootStructIsNoOp() } """); - StructWithNestedStructs.DeeplyNested obj = new(); #pragma warning disable SYSLIB1103 + StructWithNestedStructs.DeeplyNested obj = new(); configuration.Bind(obj); -#pragma warning restore SYSLIB1103 Assert.Equal(0, obj.Int32); Assert.False(obj.Boolean); + + StructWithNestedStructs.DeeplyNested? nullableObj = new(); + configuration.Bind(nullableObj); + Assert.Equal(0, obj.Int32); + Assert.False(obj.Boolean); +#pragma warning restore SYSLIB1103 } [Fact] @@ -2023,7 +2028,7 @@ public void ComplexObj_As_Dictionary_Element() [Fact] public void ComplexObj_As_Enumerable_Element() { - var configuration = TestHelpers.GetConfigurationFromJsonString("""{ "Enumerable": [{ "Latitude": 3, "Longitude": 4 }] }""") + var configuration = TestHelpers.GetConfigurationFromJsonString("""{ "Enumerable": [{ "Latitude": 3, "Longitude": 4 }] }""") .GetSection("Enumerable"); Geolocation obj = configuration.Get>()[0]; @@ -2192,5 +2197,129 @@ public void TestNullHandling_Bind() configuration.Bind(location, _ => { }); configuration.Bind("", location); } + + [Fact] + public void TestAbstractTypeAsNestedMemberForBinding() + { + // Regression test for https://github.com/dotnet/runtime/issues/91324. + + IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection( + new KeyValuePair[] + { + new KeyValuePair("ConfigBindRepro:EndPoints:0", "localhost"), + new KeyValuePair("ConfigBindRepro:Property", "true") + }) + .Build(); + + AClass settings = new(); + configuration.GetSection("ConfigBindRepro").Bind(settings); + + Assert.Empty(settings.EndPoints); // Need custom binding feature to map "localhost" string into Endpoint instance. + Assert.True(settings.Property); + } + + [Fact] + public static void TestGettingAbstractType() + { + IConfiguration configuration = TestHelpers.GetConfigurationFromJsonString(@"{""Value"":1}"); + Assert.Throws(() => configuration.Get()); + } + + [Fact] + public static void TestBindingAbstractInstance() + { + // Regression tests for https://github.com/dotnet/runtime/issues/90974. + // We only bind members on the declared binding type, i.e. AbstractBase, even + // though the actual instances are derived types that may have their own properties. + + IConfiguration configuration = TestHelpers.GetConfigurationFromJsonString(@"{""Value"":1,""Value2"":2}"); + + AbstractBase d = new Derived(); + configuration.Bind(d); + Assert.Equal(1, d.Value); + + d = new DerivedWithAnotherProp(); + configuration.Bind(d); + Assert.Equal(1, d.Value); + +#if BUILDING_SOURCE_GENERATOR_TESTS + // Divergence from reflection impl: reflection binds using instance type, + // while src-gen can only use declared type (everything has to be known AOT). + // This could change if we add an explicit API to indicate the expected runtime type(s). + Assert.Equal(0, ((DerivedWithAnotherProp)d).Value2); +#else + Assert.Equal(2, ((DerivedWithAnotherProp)d).Value2); +#endif + } + + [Fact] + public static void TestBindingAbstractMember_AsCtorParam() + { + IConfiguration configuration = TestHelpers.GetConfigurationFromJsonString(@"{ ""AbstractProp"": {""Value"":1} }"); + Assert.Throws(configuration.Get); + Assert.Throws(configuration.Get); + } + + [Fact] + public void GetIConfigurationSection() + { + var configuration = TestHelpers.GetConfigurationFromJsonString(""" + { + "vaLue": "MyString", + } + """); + + var obj = configuration.GetSection("value").Get(); + Assert.Equal("MyString", obj.Value); + + configuration = TestHelpers.GetConfigurationFromJsonString(""" + { + "vaLue": [ "MyString", { "nested": "value" } ], + } + """); + + var list = configuration.GetSection("value").Get>(); + ValidateList(list); + + var dict = configuration.Get>>(); + Assert.Equal(1, dict.Count); + ValidateList(dict["vaLue"]); + + static void ValidateList(List list) + { + Assert.Equal(2, list.Count); + Assert.Equal("0", list[0].Key); + Assert.Equal("MyString", list[0].Value); + + Assert.Equal("1", list[1].Key); + var nestedSection = Assert.IsAssignableFrom(list[1].GetSection("nested")); + Assert.Equal("value", nestedSection.Value); + } + } + + [Fact] + public void NullableDictKeys() + { + var configuration = TestHelpers.GetConfigurationFromJsonString("""{ "1": "MyString" }"""); + var dict = configuration.Get>(); + Assert.Empty(dict); + } + + [Fact] + public void IConfigurationSectionAsCtorParam() + { + var configuration = TestHelpers.GetConfigurationFromJsonString(""" + { + "MySection": "MySection", + "MyObject": "MyObject", + "MyString": "MyString", + } + """); + + var obj = configuration.Get(); + Assert.Equal("MySection", obj.MySection.Value); + Assert.Equal("MyObject", obj.MyObject); + Assert.Equal("MyString", obj.MyString); + } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/ConfigurationBinder/Bind_ParseTypeFromMethodParam.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/ConfigurationBinder/Bind_ParseTypeFromMethodParam.generated.txt new file mode 100644 index 00000000000000..14753a4a1f8e4b --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/ConfigurationBinder/Bind_ParseTypeFromMethodParam.generated.txt @@ -0,0 +1,72 @@ +// +#nullable enable +#pragma warning disable CS0612, CS0618 // Suppress warnings about [Obsolete] member usage in generated code. + +namespace System.Runtime.CompilerServices +{ + using System; + using System.CodeDom.Compiler; + + [GeneratedCode("Microsoft.Extensions.Configuration.Binder.SourceGeneration", "42.42.42.42")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(string filePath, int line, int column) + { + } + } +} + +namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration +{ + using Microsoft.Extensions.Configuration; + using System; + using System.CodeDom.Compiler; + using System.Collections.Generic; + using System.Globalization; + using System.Runtime.CompilerServices; + + [GeneratedCode("Microsoft.Extensions.Configuration.Binder.SourceGeneration", "42.42.42.42")] + file static class BindingExtensions + { + #region IConfiguration extensions. + /// Attempts to bind the given object instance to configuration values by matching property names against configuration keys recursively. + [InterceptsLocation(@"src-0.cs", 18, 16)] + public static void Bind_ProgramMyClass0(this IConfiguration configuration, object? instance) + { + } + + /// Attempts to bind the given object instance to configuration values by matching property names against configuration keys recursively. + [InterceptsLocation(@"src-0.cs", 23, 16)] + public static void Bind_ProgramMyClass1(this IConfiguration configuration, object? instance, Action? configureOptions) + { + } + + /// Attempts to bind the given object instance to configuration values by matching property names against configuration keys recursively. + [InterceptsLocation(@"src-0.cs", 28, 16)] + public static void Bind_ProgramMyClass2(this IConfiguration configuration, string key, object? instance) + { + } + #endregion IConfiguration extensions. + + #region Core binding extensions. + public static BinderOptions? GetBinderOptions(Action? configureOptions) + { + if (configureOptions is null) + { + return null; + } + + BinderOptions binderOptions = new(); + configureOptions(binderOptions); + + if (binderOptions.BindNonPublicProperties) + { + throw new NotSupportedException($"The configuration binding source generator does not support 'BinderOptions.BindNonPublicProperties'."); + } + + return binderOptions; + } + #endregion Core binding extensions. + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Baselines.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Baselines.cs index 7b3251e7fc85dc..c48148c74c5891 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Baselines.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Baselines.cs @@ -117,6 +117,49 @@ public class MyClass2 { } await VerifyAgainstBaselineUsingFile("Bind_Key_Instance.generated.txt", source, extType: ExtensionClassType.ConfigurationBinder); } + [Fact] + public async Task Bind_CanParseTargetConfigType_FromMethodParam() + { + string source = """ + using System; + using Microsoft.Extensions.Configuration; + + public class Program + { + public static void Main() + { + ConfigurationBuilder configurationBuilder = new(); + IConfiguration config = configurationBuilder.Build(); + + BindOptions(config, new MyClass0()); + BindOptions(config, new MyClass1(), _ => { }); + BindOptions(config, "", new MyClass2()); + } + + private static void BindOptions(IConfiguration config, MyClass0 instance) + { + config.Bind(instance); + } + + private static void BindOptions(IConfiguration config, MyClass1 instance, Action? configureOptions) + { + config.Bind(instance, configureOptions); + } + + private static void BindOptions(IConfiguration config, string path, MyClass2 instance) + { + config.Bind(path, instance); + } + + public class MyClass0 { } + public class MyClass1 { } + public class MyClass2 { } + } + """; + + await VerifyAgainstBaselineUsingFile("Bind_ParseTypeFromMethodParam.generated.txt", source, extType: ExtensionClassType.ConfigurationBinder); + } + [Fact] public async Task Get() { @@ -622,5 +665,54 @@ await VerifyAgainstBaselineUsingFile("Collections.generated.txt", source, valida Assert.Equal(6, d.Where(diag => diag.Id == Diagnostics.PropertyNotSupported.Id).Count()); }); } + + [Fact] + public async Task MinimalGenerationIfNoBindableMembers() + { + string source = """ + using System.Collections.Generic; + using Microsoft.Extensions.Configuration; + + public class Program + { + public static void Main() + { + ConfigurationBuilder configurationBuilder = new(); + IConfiguration configuration = configurationBuilder.Build(); + + TypeWithNoMembers obj = new(); + configuration.Bind(obj); + + TypeWithNoMembers_Wrapper obj2 = new(); + configuration.Bind(obj2); + + List obj3 = new(); + configuration.Bind(obj3); + } + } + + public class TypeWithNoMembers + { + } + + public class TypeWithNoMembers_Wrapper + { + public TypeWithNoMembers Member { get; set; } + } + + public abstract class AbstractType_CannotInit + { + } + """; + + await VerifyAgainstBaselineUsingFile( + "EmptyConfigType.generated.txt", + source, + assessDiagnostics: (d) => + { + Assert.Equal(2, d.Where(diag => diag.Id == Diagnostics.TypeNotSupported.Id).Count()); + }, + validateOutputCompDiags: false); + } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Helpers.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Helpers.cs index d8818bcf3c1d4c..0053cb41f37020 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Helpers.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Helpers.cs @@ -145,7 +145,8 @@ private static async Task VerifyAgainstBaselineUsingFile( if (validateOutputCompDiags) { - Assert.False(outputCompilation.GetDiagnostics().Any(d => d.Severity > DiagnosticSeverity.Info)); + ImmutableArray diagnostics = outputCompilation.GetDiagnostics(); + Assert.False(diagnostics.Any(d => d.Severity > DiagnosticSeverity.Info)); } return (runResult.Results[0].Diagnostics, runResult.Results[0].GeneratedSources); diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.cs index 3fddad379397ef..846e64d904d531 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.cs @@ -177,60 +177,7 @@ public class MyClass { } } [Fact] - public async Task BindCanParseMethodParam() - { - string source = """ - using System; - using Microsoft.AspNetCore.Builder; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - - public class Program - { - public static void Main() - { - ConfigurationBuilder configurationBuilder = new(); - IConfiguration config = configurationBuilder.Build(); - - BindOptions(config, new MyClass0()); - BindOptions(config, new MyClass1(), (_) => { }); - BindOptions(config, "", new MyClass2()); - } - - private void BindOptions(IConfiguration config, MyClass0 instance) - { - config.Bind(instance); - } - - private void BindOptions(IConfiguration config, MyClass1 instance, Action? configureOptions) - { - config.Bind(instance, configureOptions); - } - - private void BindOptions(IConfiguration config, string path, MyClass2 instance) - { - config.Bind(path, instance); - } - - public class MyClass0 { } - public class MyClass1 { } - public class MyClass2 { } - } - """; - - var (d, r) = await RunGenerator(source); - Assert.Single(r); - - string generatedSource = string.Join('\n', r[0].SourceText.Lines.Select(x => x.ToString())); - Assert.Contains("public static void Bind_ProgramMyClass0(this IConfiguration configuration, object? instance)", generatedSource); - Assert.Contains("public static void Bind_ProgramMyClass1(this IConfiguration configuration, object? instance, Action? configureOptions)", generatedSource); - Assert.Contains("public static void Bind_ProgramMyClass2(this IConfiguration configuration, string key, object? instance)", generatedSource); - - Assert.Empty(d); - } - - [Fact] - public async Task SucceedForMinimalInput() + public async Task SucceedWhenGivenMinimumRequiredReferences() { string source = """ using System; @@ -337,8 +284,8 @@ public class AnotherGraphWithUnsupportedMembers var (d, r) = await RunGenerator(source, references: GetAssemblyRefsWithAdditional(typeof(ImmutableArray<>), typeof(Encoding), typeof(JsonSerializer))); Assert.Single(r); - Assert.Equal(12, d.Where(diag => diag.Id == Diagnostics.TypeNotSupported.Id).Count()); - Assert.Equal(10, d.Where(diag => diag.Id == Diagnostics.PropertyNotSupported.Id).Count()); + Assert.Equal(47, d.Where(diag => diag.Id == Diagnostics.TypeNotSupported.Id).Count()); + Assert.Equal(44, d.Where(diag => diag.Id == Diagnostics.PropertyNotSupported.Id).Count()); } } }