From 9ab153230a091674d0865e1e6d1928aa1e9a93e9 Mon Sep 17 00:00:00 2001 From: Jaroslav Ruzicka <14963300+rosebyte@users.noreply.github.com> Date: Fri, 21 Aug 2026 19:27:05 +0200 Subject: [PATCH] Don't report unsupported types for never bound properties (#132455) Fixes #132377. ## Problem The configuration binder source generator reports `SYSLIB1101` ("Property 'X' on type 'Y' is not supported.") and `SYSLIB1100` at the `Bind`/`Get` call site for properties the generated binder never binds: - properties annotated with `[ConfigurationIgnore]` - properties with no public accessor ## Root cause `CreateObjectSpec` queued every property's type via `EnqueueTransitiveType`, passing the `PropertyNotSupported` descriptor, before `isIgnored` was computed and without consulting accessibility. When that type was later found unbindable, the attached diagnostic info was reported against the property. Both checks were only honoured later, at emit time, in `TypeIndex.ShouldBindTo`. ## Fix Build the `PropertySpec` first, then enqueue its type only when the property can take part in binding. A property backing a constructor parameter is still registered, since those are bound regardless of the accessibility of the property describing them. `ShouldBindTo` now short-circuits on the ignore flag and accessibility before resolving the type spec, because excluded types are no longer in the index. --------- Co-authored-by: rosebyte --- .../ConfigurationBindingGenerator.Parser.cs | 21 ++- .../gen/Specs/TypeIndex.cs | 7 +- .../SourceGenerationTests/GeneratorTests.cs | 134 ++++++++++++++++++ 3 files changed, 157 insertions(+), 5 deletions(-) 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() {