Skip to content
Closed
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 @@ -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))
Expand All @@ -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)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -745,19 +754,23 @@ private ObjectSpec CreateObjectSpec(TypeParseInfo typeParseInfo)
continue;
}

TypeRef propertyTypeRef = EnqueueTransitiveType(typeParseInfo, property.Type, DiagnosticDescriptors.PropertyNotSupported, propertyName);
ImmutableArray<AttributeData> 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;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<MySettings>()!;
}
}

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()
{
Expand Down
Loading