Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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 @@ -17,6 +17,7 @@ private sealed partial class Emitter
private readonly bool _emitGenericParseEnum;
private readonly bool _emitNotNullIfNotNull;
private readonly bool _emitThrowIfNullMethod;
private readonly bool _useUpdatedMemorySafetyRules;

private readonly SourceWriter _writer = new();

Expand All @@ -29,6 +30,7 @@ public Emitter(SourceGenerationSpec sourceGenSpec)
_emitGenericParseEnum = sourceGenSpec.EmitGenericParseEnum;
_emitNotNullIfNotNull = sourceGenSpec.EmitNotNullIfNotNull;
_emitThrowIfNullMethod = sourceGenSpec.EmitThrowIfNullMethod;
_useUpdatedMemorySafetyRules = sourceGenSpec.UseUpdatedMemorySafetyRules;
}

public void Emit(SourceProductionContext context)
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ private static class Expression

private static class TypeDisplayString
{
public const string NullableActionOfBinderOptions = "Action<BinderOptions>?";
public const string NullableActionOfBinderOptions = "global::System.Action<global::Microsoft.Extensions.Configuration.BinderOptions>?";
public const string OptionsBuilderOfTOptions = $"global::Microsoft.Extensions.Options.OptionsBuilder<{Identifier.TOptions}>";
public const string HashSetOfString = "global::System.Collections.Generic.HashSet<string>";
public const string LazyHashSetOfString = "Lazy<global::System.Collections.Generic.HashSet<string>>";
public const string LazyHashSetOfString = "global::System.Lazy<global::System.Collections.Generic.HashSet<string>>";
public const string ListOfString = "global::System.Collections.Generic.List<string>";
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<Compile Include="$(CommonPath)\SourceGenerators\SourceWriter.cs" Link="Common\SourceGenerators\SourceWriter.cs" />
<Compile Include="$(CommonPath)\SourceGenerators\TypeModelHelper.cs" Link="Common\SourceGenerators\TypeModelHelper.cs" />
<Compile Include="$(CommonPath)\SourceGenerators\TypeRef.cs" Link="Common\SourceGenerators\TypeRef.cs" />
<Compile Include="$(CommonPath)\SourceGenerators\UnsafeAccessorEmitter.cs" Link="Common\SourceGenerators\UnsafeAccessorEmitter.cs" />
<Compile Include="ConfigurationBindingGenerator.cs" />
<Compile Include="ConfigurationBindingGenerator.Emitter.cs" />
<Compile Include="ConfigurationBindingGenerator.Parser.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ internal sealed class KnownTypeSymbols
public INamedTypeSymbol? ConfigurationBinder { get; }
public INamedTypeSymbol? ConfigurationIgnoreAttribute { get; }
public INamedTypeSymbol? ConfigurationKeyNameAttribute { get; }
public INamedTypeSymbol? SetsRequiredMembersAttribute { get; }
public INamedTypeSymbol? OptionsBuilderConfigurationExtensions { get; }
public INamedTypeSymbol? OptionsBuilderOfT { get; }
public INamedTypeSymbol? OptionsBuilderOfT_Unbound { get; }
Expand Down Expand Up @@ -66,6 +67,15 @@ internal sealed class KnownTypeSymbols
public INamedTypeSymbol? ParameterInfo { get; }
public INamedTypeSymbol? Delegate { get; }
public INamedTypeSymbol? NotNullIfNotNullAttribute { get; }
public INamedTypeSymbol? UnsafeAccessorAttribute { get; }
public INamedTypeSymbol? OverloadResolutionPriorityAttribute { get; }

/// <summary>
/// Whether <c>[UnsafeAccessor]</c> can target generic declaring types. Pre-.NET 9 <c>[UnsafeAccessor]</c> does
/// not support generics; the .NET 9 <c>OverloadResolutionPriorityAttribute</c> is used as a proxy for that
/// runtime support (it shipped in the same release), alongside <c>UnsafeAccessorAttribute</c> (.NET 8).
/// </summary>
public bool SupportsGenericUnsafeAccessors => UnsafeAccessorAttribute is not null && OverloadResolutionPriorityAttribute is not null;

public KnownTypeSymbols(CSharpCompilation compilation)
{
Expand All @@ -91,6 +101,7 @@ public KnownTypeSymbols(CSharpCompilation compilation)
ConfigurationBinder = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Configuration.ConfigurationBinder");
ConfigurationIgnoreAttribute = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Configuration.ConfigurationIgnoreAttribute");
ConfigurationKeyNameAttribute = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Configuration.ConfigurationKeyNameAttribute");
SetsRequiredMembersAttribute = compilation.GetBestTypeByMetadataName("System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute");
IConfiguration = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Configuration.IConfiguration");
IConfigurationSection = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Configuration.IConfigurationSection");
IServiceCollection = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.DependencyInjection.IServiceCollection");
Expand Down Expand Up @@ -138,6 +149,12 @@ public KnownTypeSymbols(CSharpCompilation compilation)

// Only generate nullable attributes if available
NotNullIfNotNullAttribute = compilation.GetBestTypeByMetadataName("System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute");

// Used to decide whether generated code can set init-only/required members and bypass the required-member
// check via [UnsafeAccessor] (.NET 8+) instead of falling back to reflection. OverloadResolutionPriorityAttribute
// (.NET 9) is a proxy for [UnsafeAccessor] supporting generic declaring types.
UnsafeAccessorAttribute = compilation.GetBestTypeByMetadataName("System.Runtime.CompilerServices.UnsafeAccessorAttribute");
OverloadResolutionPriorityAttribute = compilation.GetBestTypeByMetadataName("System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -183,11 +183,13 @@ bool TryRegisterCore()

// A type with a parameterized constructor gets its constructor parameters bound
// in the Initialize method regardless of whether it also has other bindable
// members; that binding capability must be registered even when
// HasBindableMembers is false (e.g. the only member is a ctor parameter backed
// by a non-bindable read-only collection type), otherwise the emitter can end up
// calling an Initialize method that was never generated.
bool needsInitializeMethod = objectSpec is { InstantiationStrategy: ObjectInstantiationStrategy.ParameterizedConstructor, InitExceptionMessage: null };
// members; a parameterless-constructor type with a required or init-only property
// also needs an Initialize method to assign those members in an object initializer.
// That binding capability must be registered even when HasBindableMembers is false
// (e.g. the only member is a ctor parameter backed by a non-bindable read-only
// collection type), otherwise the emitter can end up calling an Initialize method
// that was never generated.
bool needsInitializeMethod = TypeIndex.HasInitializeMethod(objectSpec);

if (hasBindableMembers || needsInitializeMethod)
{
Expand All @@ -207,6 +209,14 @@ bool TryRegisterCore()
{
RegisterForGen_AsConfigWithChildrenHelper();
}

// An init-only member is set post-construction only when its configuration is
// present, which the generated BindCore checks with HasValueOrChildren, so ensure
// that helper is emitted.
if (property.CanSetViaAccessor && _typeIndex.ShouldBindTo(property))
{
RegisterForGen_HasValueOrChildrenHelper();
}
}

if (hasBindableMembers)
Expand Down Expand Up @@ -268,6 +278,10 @@ private void RegisterStringParsableTypeIfApplicable(ParsableFromStringSpec type)
}

private void RegisterForGen_AsConfigWithChildrenHelper() => _methodsToGen |= MethodsToGen_CoreBindingHelper.AsConfigWithChildren;

// HasValueOrChildren is backed by AsConfigWithChildren, so registering it also registers that helper.
private void RegisterForGen_HasValueOrChildrenHelper() =>
_methodsToGen |= MethodsToGen_CoreBindingHelper.HasValueOrChildren | MethodsToGen_CoreBindingHelper.AsConfigWithChildren;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ public ParameterSpec(IParameterSymbol parameter, TypeRef typeRef) : base(paramet

public RefKind RefKind { get; }

/// <summary>The open (type-parameter-referencing) form of the parameter type, used inside a generic constructor-accessor wrapper class; <see langword="null"/> when the declaring type is non-generic or the parameter type contains no type parameters.</summary>
public string? OpenTypeFQN { get; init; }

public override bool CanGet => false;

public override bool CanSet => true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,18 @@ public PropertySpec(IPropertySymbol property, TypeRef typeRef) : base(property,
bool isInitOnly = setMethod?.IsInitOnly is true;

IsStatic = property.IsStatic;
// Only public setters are considered here, consistent with CanSet. A required or init-only property with a
// non-public (e.g. internal) setter is therefore not treated as SetOnInit: the generator does not set it
// (matching the reflection binder, which does not bind non-public members by default), and the member keeps
// its default value.
SetOnInit = setterIsPublic && (property.IsRequired || isInitOnly);
Comment on lines +18 to 22

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I have added the diagnostic.

CanSet = setterIsPublic && !isInitOnly;
// An init-only property can only be assigned at construction time through normal C#. Post-construction the
// generator sets it through an [UnsafeAccessor] setter (or reflection downlevel), which lets absent config
// keys preserve the property's default value instead of overwriting it.
CanSetViaAccessor = setterIsPublic && isInitOnly;
CanGet = property.GetMethod?.DeclaredAccessibility is Accessibility.Public;
IsRequired = property.IsRequired;
}

public ParameterSpec? MatchingCtorParam { get; set; }
Expand All @@ -28,8 +37,42 @@ public PropertySpec(IPropertySymbol property, TypeRef typeRef) : base(property,

public bool SetOnInit { get; }

public bool IsRequired { get; }

public override bool CanGet { get; }

public override bool CanSet { get; }

/// <summary>
/// Whether the property has a public init-only setter, so it is assignable post-construction only through an
/// <c>[UnsafeAccessor]</c> setter (or a reflection fallback downlevel) rather than a direct assignment.
/// </summary>
public bool CanSetViaAccessor { get; }

/// <summary>
/// The declaring type an accessor for this property must target, when it differs from the type being bound (an
/// inherited property's setter is declared on a base type, and <c>[UnsafeAccessor]</c> resolves against the exact
/// type named). <see langword="null"/> when the property is declared on the bound type itself.
/// </summary>
public TypeRef? AccessorDeclaringTypeRef { get; init; }

/// <summary>
/// Whether the init-only setter accessor for this property can use <c>[UnsafeAccessor]</c> (the framework
/// supports it and, for a generic declaring type, supports generics). <see langword="false"/> falls back to reflection.
/// </summary>
public bool SetterCanUseUnsafeAccessor { get; init; }

/// <summary>Type-parameter names of the (generic) declaring type when a generic wrapper class is used for the setter (.NET 9+), otherwise <see langword="null"/>.</summary>
public ImmutableEquatableArray<string>? DeclaringTypeParameterNames { get; init; }
public string? OpenDeclaringTypeFQN { get; init; }
public string? OpenPropertyTypeFQN { get; init; }
public string? DeclaringTypeParameterConstraintClauses { get; init; }

/// <summary>
/// The zero-based position of the property's declaring type in the bound type's inheritance hierarchy (the bound
/// type itself is 0, its base 1, and so on). Disambiguates the generic setter-accessor wrapper class between
/// members inherited from different generic base types.
/// </summary>
public int DeclaringTypeIndex { get; init; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,11 @@ public sealed record SourceGenerationSpec
public required bool EmitGenericParseEnum { get; set; }
public required bool EmitNotNullIfNotNull { get; set; }
public required bool EmitThrowIfNullMethod { get; set; }

/// <summary>
/// Whether the compilation uses the updated memory-safety rules, under which <c>[UnsafeAccessor]</c> externs must
/// be marked <c>safe</c>. Threaded to the shared accessor emitter so it emits the correct modifier.
/// </summary>
public required bool UseUpdatedMemorySafetyRules { get; init; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,28 @@ private bool CanConstructElementsOf(CollectionSpec typeSpec)
};
}

/// <summary>
/// Whether an <c>Initialize</c> method is generated for <paramref name="type"/> to construct it. This is the
/// case for a parameterized-constructor type (whose parameters are bound and passed to the constructor) and for
/// a parameterless-constructor type whose required members force construction through an accessor that bypasses
/// the required-member check. Init-only and required members are then set post-construction in <c>BindCore</c>
/// (only when their config key is present), not in an object initializer, so their defaults are preserved.
/// </summary>
public static bool HasInitializeMethod(ObjectSpec type)
{
if (type.InitExceptionMessage is not null)
{
return false;
}

return type.InstantiationStrategy switch
{
ObjectInstantiationStrategy.ParameterizedConstructor => true,
ObjectInstantiationStrategy.ParameterlessConstructor => type.ConstructionRequiresAccessor,
_ => false,
Comment on lines +78 to +82
};
}

public bool ShouldBindTo(PropertySpec property)
{
if (property.IsIgnored || !IsAccessible())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,36 @@ public ObjectSpec(
public ImmutableEquatableArray<ParameterSpec>? ConstructorParameters { get; }

public string? InitExceptionMessage { get; }

/// <summary>
/// Whether the constructor accessor for this type can use <c>[UnsafeAccessor(Constructor)]</c>. Requires the
/// framework to support <c>[UnsafeAccessor]</c> (.NET 8+); a generic type additionally requires generic
/// <c>[UnsafeAccessor]</c> support (.NET 9+) and that it is not nested in a generic type, in which case the extern
/// is emitted inside a generic wrapper class. When <see langword="false"/> the constructor accessor uses a cached
/// <see cref="System.Reflection.ConstructorInfo"/>.
/// </summary>
public bool ConstructorCanUseUnsafeAccessor { get; init; }

/// <summary>Type-parameter names of the type when a generic constructor-accessor wrapper class is used (.NET 9+), otherwise <see langword="null"/>.</summary>
public ImmutableEquatableArray<string>? DeclaringTypeParameterNames { get; init; }
public string? OpenTypeFQN { get; init; }
public string? DeclaringTypeParameterConstraintClauses { get; init; }

/// <summary>
/// Whether the type has required members that are not satisfied by a <c>[SetsRequiredMembers]</c> constructor, so
/// it cannot be created with a plain <c>new T(...)</c> (which would require an object initializer, CS9035).
/// Construction goes through an accessor (<c>[UnsafeAccessor(Constructor)]</c> or reflection) that bypasses the
/// check, then the required members are set post-construction, preserving their defaults for absent config keys.
/// </summary>
public bool ConstructionRequiresAccessor { get; init; }

/// <summary>
/// Whether the type is a value type with required members not satisfied by a <c>[SetsRequiredMembers]</c>
/// constructor. Such a struct cannot be created with <c>new T()</c> (CS9035) and has no constructor an accessor
/// could target, so it is constructed with <c>default(T)</c> (which bypasses the required-member check) and its
/// required members are set post-construction.
/// </summary>
public bool ConstructValueTypeWithDefault { get; init; }
}

public enum ObjectInstantiationStrategy
Expand Down
Loading
Loading