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 a3c5d705c4deb8..2e18263bac1001 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs @@ -157,8 +157,11 @@ private void EnqueueTargetTypeForRootInvocation(ITypeSymbol? typeSymbol, Methods } } - private TypeRef EnqueueTransitiveType(TypeParseInfo containingTypeParseInfo, ITypeSymbol memberTypeSymbol, DiagnosticDescriptor diagDescriptor, string? memberName = null) + private TypeRef EnqueueTransitiveType(TypeParseInfo containingTypeParseInfo, ITypeSymbol memberTypeSymbol, DiagnosticDescriptor diagDescriptor, string? memberName = null, TypeRef? knownTypeRef = null) { + Debug.Assert(knownTypeRef is null || knownTypeRef.FullyQualifiedName == memberTypeSymbol.GetFullyQualifiedName(), + $"'{nameof(knownTypeRef)}' must describe '{nameof(memberTypeSymbol)}'."); + TypeParseInfo memberTypeParseInfo = containingTypeParseInfo.ToTransitiveTypeParseInfo(memberTypeSymbol, diagDescriptor, memberName); if (_createdTypeSpecs.TryGetValue(memberTypeSymbol, out TypeSpec? memberTypeSpec)) @@ -168,7 +171,7 @@ private TypeRef EnqueueTransitiveType(TypeParseInfo containingTypeParseInfo, ITy } _typesToParse.Enqueue(memberTypeParseInfo); - return new TypeRef(memberTypeSymbol); + return knownTypeRef ?? new TypeRef(memberTypeSymbol); } private TypeSpec CreateTypeSpec(TypeParseInfo typeParseInfo) @@ -641,6 +644,12 @@ private bool ConstructorParametersContainUnsupportedType(IMethodSymbol ctor) return false; } + private static bool BacksConstructorParameter(IMethodSymbol? ctor, string propertyName) + { + return ctor is not null + && ctor.Parameters.Any(parameter => string.Equals(parameter.Name, propertyName, StringComparison.OrdinalIgnoreCase)); + } + private ObjectSpec CreateObjectSpec(TypeParseInfo typeParseInfo) { INamedTypeSymbol typeSymbol = (INamedTypeSymbol)typeParseInfo.TypeSymbol; @@ -745,19 +754,23 @@ private ObjectSpec CreateObjectSpec(TypeParseInfo typeParseInfo) continue; } - TypeRef propertyTypeRef = EnqueueTransitiveType(typeParseInfo, property.Type, DiagnosticDescriptors.PropertyNotSupported, propertyName); ImmutableArray attributes = property.GetAttributes(); AttributeData? attributeData = attributes.FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, _typeSymbols.ConfigurationKeyNameAttribute)); string configKeyName = attributeData?.ConstructorArguments.FirstOrDefault().Value as string ?? propertyName; bool isIgnored = attributes.Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, _typeSymbols.ConfigurationIgnoreAttribute)); - PropertySpec spec = new(property, propertyTypeRef) + PropertySpec spec = new(property, new TypeRef(property.Type)) { ConfigurationKeyName = configKeyName, IsIgnored = isIgnored, }; + if (!spec.IsIgnored && (spec.CanGet || spec.CanSet || BacksConstructorParameter(ctor, propertyName))) + { + EnqueueTransitiveType(typeParseInfo, property.Type, DiagnosticDescriptors.PropertyNotSupported, propertyName, spec.TypeRef); + } + (properties ??= new(StringComparer.OrdinalIgnoreCase))[propertyName] = spec; } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/TypeIndex.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/TypeIndex.cs index 029c6e848c7c1f..ef662f0ec4afdd 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/TypeIndex.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/TypeIndex.cs @@ -39,8 +39,13 @@ public bool HasBindableMembers(ComplexTypeSpec typeSpec) => public bool ShouldBindTo(PropertySpec property) { + if (property.IsIgnored || !IsAccessible()) + { + return false; + } + TypeSpec propTypeSpec = GetEffectiveTypeSpec(property.TypeRef); - return IsAccessible() && !property.IsIgnored && !IsCollectionAndCannotOverride() && !IsDictWithUnsupportedKey(); + return !IsCollectionAndCannotOverride() && !IsDictWithUnsupportedKey(); bool IsAccessible() => property.CanGet || property.CanSet; 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 a6874b6a91b966..601b46d1a705d0 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.cs @@ -713,6 +713,140 @@ public class UnreachableChild AssertCanCreateAssemblyImage(result.OutputCompilation); } + [Theory] + [InlineData("private UnbindableType Lazy => UnbindableType.Create();")] + [InlineData("internal UnbindableType Lazy { get; set; }")] + [InlineData("protected UnbindableType Lazy { get; set; }")] + [InlineData("[ConfigurationIgnore] public UnbindableType Lazy { get; set; }")] + public async Task PropertyExcludedFromBindingDoesNotReportItsType(string propertyDeclaration) + { + string source = $$""" + using Microsoft.Extensions.Configuration; + + public class Program + { + public static void Main() + { + ConfigurationBuilder configurationBuilder = new(); + IConfigurationRoot config = configurationBuilder.Build(); + + MySettings settings = new(); + config.Bind(settings); + } + } + + public sealed class UnbindableType + { + private UnbindableType() { } + public static UnbindableType Create() => new UnbindableType(); + public int Value { get; set; } + } + + public class MySettings + { + public int Supported { get; set; } + {{propertyDeclaration}} + } + """; + + ConfigBindingGenRunResult result = await RunGeneratorAndUpdateCompilation(source); + + Assert.Empty(result.Diagnostics); + Assert.NotNull(result.GeneratedSource); + + string generated = result.GeneratedSource.Value.SourceText.ToString(); + Assert.Contains("instance.Supported = ", generated); + Assert.DoesNotContain("UnbindableType", generated); + } + + [Fact] + public async Task UnbindableTypeIsStillReportedWhenAlsoReachedThroughABindableProperty() + { + // The excluded property is declared first, so it would be the one to pull the type into the graph. + string source = """ + using Microsoft.Extensions.Configuration; + + public class Program + { + public static void Main() + { + ConfigurationBuilder configurationBuilder = new(); + IConfigurationRoot config = configurationBuilder.Build(); + + MySettings settings = new(); + config.Bind(settings); + } + } + + public sealed class UnbindableType + { + private UnbindableType() { } + public static UnbindableType Create() => new UnbindableType(); + public int Value { get; set; } + } + + public class MySettings + { + [ConfigurationIgnore] + public UnbindableType Excluded { get; set; } + + public UnbindableType Bindable { get; set; } + } + """; + + ConfigBindingGenRunResult result = await RunGeneratorAndUpdateCompilation(source); + + Assert.Contains(result.Diagnostics, diagnostic => + diagnostic.Id == Diagnostics.PropertyNotSupported.Id && + diagnostic.GetMessage(CultureInfo.InvariantCulture).Contains("'Bindable'")); + + Assert.DoesNotContain(result.Diagnostics, diagnostic => + diagnostic.GetMessage(CultureInfo.InvariantCulture).Contains("'Excluded'")); + + Assert.Contains(result.Diagnostics, diagnostic => diagnostic.Id == Diagnostics.TypeNotSupported.Id); + } + + [Fact] + public async Task NonPublicPropertyBackingConstructorParameterKeepsItsTypeRegistered() + { + // The binder cannot reach the property, but it does bind the constructor parameter it backs, so the + // type must stay registered and an unbindable one must still be reported. + string source = """ + using Microsoft.Extensions.Configuration; + + public class Program + { + public static void Main() + { + ConfigurationBuilder configurationBuilder = new(); + IConfigurationRoot config = configurationBuilder.Build(); + + MySettings settings = config.Get()!; + } + } + + public sealed class UnbindableType + { + private UnbindableType() { } + public static UnbindableType Create() => new UnbindableType(); + public int Value { get; set; } + } + + public class MySettings + { + public MySettings(UnbindableType inner) => Inner = inner; + + private UnbindableType Inner { get; } + } + """; + + ConfigBindingGenRunResult result = await RunGeneratorAndUpdateCompilation(source); + + Assert.Contains(result.Diagnostics, diagnostic => + diagnostic.Id == Diagnostics.PropertyNotSupported.Id && + diagnostic.GetMessage(CultureInfo.InvariantCulture).Contains("'Inner'")); + } + [Fact] public async Task BindingToCollectionOnlyTest() {