Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,11 @@ private bool IsAssignableTo(ITypeSymbol source, ITypeSymbol dest)

private bool IsUnsupportedType(ITypeSymbol type, HashSet<ITypeSymbol>? visitedTypes = null)
{
if (ContainsErrorType(type))
{
return true;
}

if (type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T)
{
type = ((INamedTypeSymbol)type).TypeArguments[0]; // extract the T from a Nullable<T>
Expand Down Expand Up @@ -713,8 +718,18 @@ private ObjectSpec CreateObjectSpec(TypeParseInfo typeParseInfo)
ImmutableArray<ISymbol> members = current.GetMembers();
foreach (ISymbol member in members)
{
if (member is IPropertySymbol { IsIndexer: false, IsImplicitlyDeclared: false } property && !IsUnsupportedType(property.Type))
if (member is IPropertySymbol { IsIndexer: false, IsImplicitlyDeclared: false } property)
{
if (IsUnsupportedType(property.Type))
{
if (ContainsErrorType(property.Type))
Comment thread
rosebyte marked this conversation as resolved.
{
RecordDiagnostic(DiagnosticDescriptors.PropertyNotSupported, typeParseInfo.BinderInvocation?.Location, [property.Name, typeParseInfo.FullName]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: this SYSLIB1101 is emitted before the override/shadowing dedup that the supported-property path performs below (property.IsOverride || properties?.ContainsKey(...)). Because the loop walks base types, a property that contains an error type and is overridden (or name-shadowed) in a derived class can produce a duplicate SYSLIB1101 for the same property name — once for the derived declaration and once for the base. Consider deduping (e.g. tracking already-reported names, or moving the override/ContainsKey check ahead of the diagnostic) so the skip warning is reported at most once per property. Not blocking.

}

continue;
}

string propertyName = property.Name;

if (property.IsOverride || properties?.ContainsKey(propertyName) is true)
Comment on lines +721 to 735

@tarekgh tarekgh Jul 16, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the property loop, this diagnostic is emitted before the IsOverride / duplicate-name check below. Because the loop walks the base chain (current = current.BaseType), an error-typed property that is overridden reports SYSLIB1101 twice: once for the override on the derived type and again for the declaration on the base type. Before this change those members went through the normal path where the override/duplicate filter capped it at a single diagnostic, so this is a new double-warning.

Computing the override/duplicate condition first and gating the diagnostic on it keeps the single-warning behavior, and leaves binding for supported properties unchanged:

Suggested change
if (member is IPropertySymbol { IsIndexer: false, IsImplicitlyDeclared: false } property)
{
if (IsUnsupportedType(property.Type))
{
if (ContainsErrorType(property.Type))
{
RecordDiagnostic(DiagnosticDescriptors.PropertyNotSupported, typeParseInfo.BinderInvocation?.Location, [property.Name, typeParseInfo.FullName]);
}
continue;
}
string propertyName = property.Name;
if (property.IsOverride || properties?.ContainsKey(propertyName) is true)
if (member is IPropertySymbol { IsIndexer: false, IsImplicitlyDeclared: false } property)
{
string propertyName = property.Name;
bool isDuplicateOrOverride = property.IsOverride || properties?.ContainsKey(propertyName) is true;
if (IsUnsupportedType(property.Type))
{
if (ContainsErrorType(property.Type) && !isDuplicateOrOverride)
{
RecordDiagnostic(DiagnosticDescriptors.PropertyNotSupported, typeParseInfo.BinderInvocation?.Location, [propertyName, typeParseInfo.FullName]);
}
continue;
}
if (isDuplicateOrOverride)

This handles the override case. A new-shadowed error-typed property would still warn twice, since a skipped member is never added to properties for the base occurrence to match against, which is likely rare enough to leave as-is but worth a note.

Expand Down Expand Up @@ -885,6 +900,36 @@ private static bool IsInterfaceMatch(INamedTypeSymbol type, INamedTypeSymbol? @i
return SymbolEqualityComparer.Default.Equals(type, @interface);
}

// A member type is unsupported when it, a nested array element, or a generic type argument is an
// error symbol — i.e. an unresolved or ambiguous metadata type from a reference that isn't available
// to the current compilation. Emitting binding code that names such a type produces uncompilable
// output (missing/ambiguous type errors), so callers skip these members instead.
private static bool ContainsErrorType(ITypeSymbol type)
{
if (type.TypeKind is TypeKind.Error)
{
return true;
}

if (type is IArrayTypeSymbol arrayType)
{
return ContainsErrorType(arrayType.ElementType);
}

if (type is INamedTypeSymbol { IsGenericType: true } genericType)
{
foreach (ITypeSymbol typeArgument in genericType.TypeArguments)
{
if (ContainsErrorType(typeArgument))
{
return true;
}
}
}

return false;
}

private static bool ContainsGenericParameters(ITypeSymbol type)
{
if (type is not INamedTypeSymbol { IsGenericType: true } genericType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,18 @@ internal sealed class ConfigBindingGenTestDriver

private readonly LanguageVersion _langVersion;
private readonly IEnumerable<Assembly>? _assemblyReferences;
private readonly IEnumerable<MetadataReference>? _metadataReferences;
private Compilation _compilation = null;

public ConfigBindingGenTestDriver(
LanguageVersion langVersion = LanguageVersion.LatestMajor,
IEnumerable<Assembly>? assemblyReferences = null)
IEnumerable<Assembly>? assemblyReferences = null,
IEnumerable<MetadataReference>? metadataReferences = null)
{
_langVersion = langVersion;

_assemblyReferences = assemblyReferences ?? s_compilationAssemblyRefs;
_metadataReferences = metadataReferences;

_parseOptions = new CSharpParseOptions(langVersion).WithFeatures(new[] {
new KeyValuePair<string, string>("InterceptorsNamespaces", "Microsoft.Extensions.Configuration.Binder.SourceGeneration")
Expand Down Expand Up @@ -86,6 +89,12 @@ private async Task UpdateCompilationWithSource(string? source = null)
.WithCompilationOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary).WithNullableContextOptions(NullableContextOptions.Annotations))
.WithParseOptions(_parseOptions)
.WithDocuments(new string[] { source });

if (_metadataReferences is not null)
{
project = project.AddMetadataReferences(_metadataReferences);
}

Assert.True(project.Solution.Workspace.TryApplyChanges(project.Solution));

_compilation = (await project.GetCompilationAsync(CancellationToken.None).ConfigureAwait(false))!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,10 @@ private static async Task<ConfigBindingGenRunResult> VerifyAgainstBaselineUsingF
private static async Task<ConfigBindingGenRunResult> RunGeneratorAndUpdateCompilation(
string source,
LanguageVersion langVersion = LanguageVersion.CSharp12,
IEnumerable<Assembly>? assemblyReferences = null)
IEnumerable<Assembly>? assemblyReferences = null,
IEnumerable<MetadataReference>? metadataReferences = null)
{
ConfigBindingGenTestDriver driver = new ConfigBindingGenTestDriver(langVersion, assemblyReferences);
ConfigBindingGenTestDriver driver = new ConfigBindingGenTestDriver(langVersion, assemblyReferences, metadataReferences);
return await driver.RunGeneratorAndUpdateCompilation(source);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,103 @@ internal class BType {}
Assert.Empty(result.Diagnostics);
}

[Fact]
public async Task IgnorePropertiesWithUnresolvableMetadataTypes()
{
CSharpCompilationOptions compilationOptions = new(OutputKind.DynamicallyLinkedLibrary);
MetadataReference[] commonReferences = s_compilationAssemblyRefs
.Select(a => MetadataReference.CreateFromFile(a.Location))
.ToArray();

CSharpCompilation transitiveDependencyCompilation = CSharpCompilation.Create(
assemblyName: $"TransitiveDependency_{Guid.NewGuid():N}",
syntaxTrees:
[
CSharpSyntaxTree.ParseText("""
namespace MissingTypes;

public struct ValueTypeMessage {}
public sealed class HttpRequestMessage {}
public sealed class CredentialDescription {}
""")
],
references: commonReferences,
options: compilationOptions);

byte[] transitiveDependencyImage = CreateAssemblyImage(transitiveDependencyCompilation);
MetadataReference transitiveDependencyReference = MetadataReference.CreateFromImage(transitiveDependencyImage);

CSharpCompilation modelCompilation = CSharpCompilation.Create(
assemblyName: $"UnresolvableModel_{Guid.NewGuid():N}",
syntaxTrees:
[
CSharpSyntaxTree.ParseText("""
namespace UnresolvableModel;

public sealed class Wrapper<T>
{
public int Count { get; set; }
}

public sealed class DstsOptions
{
public MissingTypes.ValueTypeMessage? ValueTypeMessage { get; set; }
public MissingTypes.HttpRequestMessage? HttpRequestMessage { get; set; }
public MissingTypes.CredentialDescription? CredentialDescription { get; set; }
public Wrapper<MissingTypes.HttpRequestMessage>? WrappedMessage { get; set; }
public System.Tuple<int, MissingTypes.CredentialDescription>? TupleMessage { get; set; }
public int Value { get; set; }
}
""")
],
references: commonReferences.Concat([transitiveDependencyReference]),
options: compilationOptions);

// Reference the model as an in-memory metadata reference and omit the transitive dependency, so the
// generator sees the affected member types as unresolved error symbols. Using a metadata reference
// (rather than writing the assemblies to disk and using Assembly.LoadFrom) avoids locking files and
// leaking a temp directory on every run.
MetadataReference modelReference = MetadataReference.CreateFromImage(CreateAssemblyImage(modelCompilation));

string source = """
using Microsoft.Extensions.Configuration;
using UnresolvableModel;

public static class Program
{
public static void Main()
{
var configuration = new ConfigurationBuilder().Build();
_ = configuration.Get<DstsOptions>();
}
}
""";

ConfigBindingGenRunResult result = await RunGeneratorAndUpdateCompilation(source, metadataReferences: [modelReference]);

result.ValidateDiagnostics(ExpectedDiagnostics.None);
Assert.NotNull(result.GeneratedSource);
Assert.Contains("Value", result.GeneratedSource.Value.SourceText.ToString());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion doesn't actually verify that DstsOptions.Value binds. The generated binder always contains the substring "Value" from boilerplate unrelated to the property, for example HasValueOrChildren(...) (emitted unconditionally by GetCore for any Get<T>), TryGetConfigurationValue, and (configuration as IConfigurationSection)?.Value. So it passes for any Get<T> even if the Value property were dropped, which is the regression this test is meant to guard against.

Asserting on the actual assignment makes it meaningful:

Suggested change
Assert.Contains("Value", result.GeneratedSource.Value.SourceText.ToString());
Assert.Contains("instance.Value = ", result.GeneratedSource.Value.SourceText.ToString());

The DoesNotContain(... "'Value'") diagnostic check further down is already specific enough thanks to the quotes, so no change needed there.

Assert.DoesNotContain("ValueTypeMessage", result.GeneratedSource.Value.SourceText.ToString());
Assert.DoesNotContain("HttpRequestMessage", result.GeneratedSource.Value.SourceText.ToString());
Assert.DoesNotContain("CredentialDescription", result.GeneratedSource.Value.SourceText.ToString());
Assert.DoesNotContain("WrappedMessage", result.GeneratedSource.Value.SourceText.ToString());
Assert.DoesNotContain("TupleMessage", result.GeneratedSource.Value.SourceText.ToString());
Comment thread
rosebyte marked this conversation as resolved.
Comment on lines +287 to +294

// Each skipped member surfaces a SYSLIB1101 warning so the incomplete binding is not silent.
foreach (string skippedProperty in new[] { "ValueTypeMessage", "HttpRequestMessage", "CredentialDescription", "WrappedMessage", "TupleMessage" })
{
Assert.Contains(result.Diagnostics, diagnostic =>
diagnostic.Id == "SYSLIB1101" &&
diagnostic.Severity == DiagnosticSeverity.Warning &&
diagnostic.GetMessage(CultureInfo.InvariantCulture).Contains($"'{skippedProperty}'"));
}

// The bindable member must still bind without a diagnostic.
Assert.DoesNotContain(result.Diagnostics, diagnostic =>
diagnostic.GetMessage(CultureInfo.InvariantCulture).Contains("'Value'"));
}

[Fact]
public async Task SucceedWhenGivenMinimumRequiredReferences()
{
Expand Down
Loading