Skip to content
Merged
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
36 changes: 17 additions & 19 deletions Source/Awaiten.SourceGenerators/AwaitenGenerator.Classification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,14 @@ private static void ReportWhenUnregistered(ParameterModel parameterModel, ImplIn

/// <summary>
/// The container-wide inputs an injected-member classification reads, bundled so the classification methods
/// stay small: the container symbol, the coalesced service map, the constraint-rejected and consumed-context
/// sets, the <c>[ImportService&lt;T&gt;]</c> external service types (a member of one resolves externally
/// rather than being reported missing), and the diagnostics sink.
/// stay small: the container symbol, the compilation (which answers whether a setter is reachable from the
/// container), the coalesced service map, the constraint-rejected and consumed-context sets, the
/// <c>[ImportService&lt;T&gt;]</c> external service types (a member of one resolves externally rather than
/// being reported missing), and the diagnostics sink.
/// </summary>
private sealed record InjectionContext(
INamedTypeSymbol ContainerSymbol,
Compilation Compilation,
Dictionary<ServiceKey, string> ServiceToImpl,
HashSet<string> ConstraintRejected,
HashSet<ServiceKey> ConsumedConditionals,
Expand Down Expand Up @@ -341,11 +343,12 @@ private static IEnumerable<IPropertySymbol> InjectedProperties(INamedTypeSymbol
LocationInfo? location = spec.EntryLocation ?? LocationInfo.From(property.Locations.FirstOrDefault());

// AWT136: an injected property must have a set/init accessor the container can assign through the object
// initializer. The container is not a derived type, so a protected/private-protected setter (and a
// cross-assembly internal one) is out of reach even though not private. Apply the same accessibility test
// the constructor path uses rather than a bare not-private check, so an unreachable setter surfaces as
// AWT136 instead of an inaccessible-setter error in generated code.
if (property.SetMethod is not { } setter || !IsAccessibleSetter(setter, context.ContainerSymbol))
// initializer. The container is not a derived type, so a protected/private-protected setter (and an
// internal one in an assembly that grants the container no [InternalsVisibleTo]) is out of reach even
// though not private. Apply the same accessibility test the constructor path uses rather than a bare
// not-private check, so an unreachable setter surfaces as AWT136 instead of an inaccessible-setter error
// in generated code.
if (property.SetMethod is not { } setter || !IsAccessibleSetter(setter, context.ContainerSymbol, context.Compilation))
{
context.Diagnostics.Add(new DiagnosticInfo(
Diagnostics.InjectedPropertyNotSettable,
Expand Down Expand Up @@ -496,17 +499,12 @@ private static bool RejectsInjectedPropertyShape(
return false;
}

// The setter must be reachable from the container's object initializer, which is not a derived context.
// Mirrors IsAccessibleConstructor: public always, internal/protected-internal only within the container's own
// assembly, and protected/private-protected/private never (the container cannot reach them).
private static bool IsAccessibleSetter(IMethodSymbol setter, INamedTypeSymbol containerSymbol)
=> setter.DeclaredAccessibility switch
{
Accessibility.Public => true,
Accessibility.Internal or Accessibility.ProtectedOrInternal =>
SymbolEqualityComparer.Default.Equals(setter.ContainingAssembly, containerSymbol.ContainingAssembly),
_ => false,
};
// The setter must be reachable from the container's object initializer. Asks Roslyn the same way constructor
// selection does (see SelectConstructor), so the two agree: public always, internal/protected-internal within
// the container's own assembly or across an [InternalsVisibleTo] boundary, and protected/private-protected/
// private never, since the container neither derives from the implementation nor sits inside it.
private static bool IsAccessibleSetter(IMethodSymbol setter, INamedTypeSymbol containerSymbol, Compilation compilation)
=> compilation.IsSymbolAccessibleWithin(setter, containerSymbol);

/// <summary>
/// Classifies a constructor parameter as a runtime argument (<c>[Arg]</c>), a deferred relationship type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ private static (List<RawRegistration> Raw, HashSet<string> ConstraintRejectedSer
// implementation (iterating to a fixpoint over its own generic dependencies).
if (open.Count > 0)
{
ExpandOpenGenerics(result, open, containerSymbol, external, diagnostics, constraintRejected);
ExpandOpenGenerics(result, open, containerSymbol, compilation, external, diagnostics, constraintRejected);
}

// ...then moved back to the end: coalescing is first-wins per service, so the explicit registrations and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ private readonly record struct ServiceChain(
/// </summary>
private string? SingleInnerParameterType(INamedTypeSymbol decorator, INamedTypeSymbol service)
{
IMethodSymbol? constructor = SelectConstructor(decorator, _containerSymbol, _serviceToImpl.Keys.Select(k => k.Service), _external);
IMethodSymbol? constructor = SelectConstructor(decorator, _containerSymbol, _compilation, _serviceToImpl.Keys.Select(k => k.Service), _external);
if (constructor is null)
{
return null;
Expand Down Expand Up @@ -612,7 +612,7 @@ private static CompositeCollectionKind ClassifyCompositeCollection(
out string? relatedElement)
{
relatedElement = null;
IMethodSymbol? constructor = SelectConstructor(composite, containerSymbol, serviceToImpl.Keys.Select(k => k.Service), external);
IMethodSymbol? constructor = SelectConstructor(composite, containerSymbol, compilation, serviceToImpl.Keys.Select(k => k.Service), external);
if (constructor is null)
{
return CompositeCollectionKind.Missing;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ private static void ExpandOpenGenerics(
List<RawRegistration> raw,
List<OpenRegistration> open,
INamedTypeSymbol containerSymbol,
Compilation compilation,
ExternalSurface external,
List<DiagnosticInfo> diagnostics,
HashSet<string> constraintRejected)
Expand Down Expand Up @@ -247,7 +248,7 @@ private static void ExpandOpenGenerics(
continue;
}

foreach (ITypeSymbol required in RequiredServiceTypes(impl, containerSymbol, raw, openServices, external))
foreach (ITypeSymbol required in RequiredServiceTypes(impl, containerSymbol, compilation, raw, openServices, external))
{
if (required is INamedTypeSymbol { IsGenericType: true, IsUnboundGenericType: false, } closed
&& expandedServices.Add(closed.ToDisplayString(FullyQualified)))
Expand Down Expand Up @@ -379,13 +380,15 @@ private static void ReportExpansionTooDeep(INamedTypeSymbol impl, int depth, Exp
private static IEnumerable<ITypeSymbol> RequiredServiceTypes(
INamedTypeSymbol implementation,
INamedTypeSymbol containerSymbol,
Compilation compilation,
List<RawRegistration> raw,
HashSet<INamedTypeSymbol> openServices,
ExternalSurface external)
{
IMethodSymbol? constructor = AwaitenGenerator.SelectConstructor(
implementation,
containerSymbol,
compilation,
raw.Select(r => r.ServiceType),
external,
p => IsOpenGenericSatisfiable(p.Type, openServices));
Expand Down
47 changes: 16 additions & 31 deletions Source/Awaiten.SourceGenerators/AwaitenGenerator.Production.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ private static List<MemberModel> BuildInjectedMembers(ImplInfo info, BuildContex

DiscoverInjectedMembers(
info,
new InjectionContext(context.ContainerSymbol, context.ServiceToImpl, context.ConstraintRejected, context.ConsumedConditionals, context.External.ServiceTypes, context.Diagnostics),
new InjectionContext(context.ContainerSymbol, context.Compilation, context.ServiceToImpl, context.ConstraintRejected, context.ConsumedConditionals, context.External.ServiceTypes, context.Diagnostics),
injectProperties,
members);
return members;
Expand Down Expand Up @@ -253,7 +253,7 @@ private static InstanceModel BuildPrebuiltInstance(
return null;
}

IMethodSymbol? constructor = SelectConstructor(info.Symbol, containerSymbol, serviceToImpl.Keys.Select(k => k.Service), external);
IMethodSymbol? constructor = SelectConstructor(info.Symbol, containerSymbol, compilation, serviceToImpl.Keys.Select(k => k.Service), external);
if (constructor is null)
{
diagnostics.Add(new DiagnosticInfo(
Expand Down Expand Up @@ -298,7 +298,7 @@ private static (string? Hook, EquatableArray<ParameterModel> Parameters) Resolve
Compilation compilation = context.Compilation;

List<IMethodSymbol> matches = new();
foreach (ISymbol member in AccessibleMembers(info.Origin ?? containerSymbol, hookName))
foreach (ISymbol member in AccessibleMembers(info.Origin ?? containerSymbol, hookName, compilation))
{
// A module hook must also be accessible from the generated container (its own private members are
// reachable from the partial, a module's are not); an inaccessible module method is not a usable hook.
Expand Down Expand Up @@ -591,7 +591,7 @@ private static void ValidateInstanceMember(
List<DiagnosticInfo> diagnostics)
{
bool inaccessibleMatch = false;
foreach (ISymbol member in AccessibleMembers(info.Origin ?? containerSymbol, info.ProductionMember!))
foreach (ISymbol member in AccessibleMembers(info.Origin ?? containerSymbol, info.ProductionMember!, compilation))
{
ITypeSymbol? memberType = member switch
{
Expand Down Expand Up @@ -666,12 +666,17 @@ private static string DescribeOwner(ImplInfo info)
internal static IMethodSymbol? SelectConstructor(
INamedTypeSymbol implementation,
INamedTypeSymbol containerSymbol,
Compilation compilation,
IEnumerable<string> registeredServices,
ExternalSurface external,
Func<IParameterSymbol, bool>? additionallySatisfiable = null)
{
// The generated partial emits the 'new' from inside the container, so what the container can reach is
// exactly Roslyn's own accessibility question, which honors [InternalsVisibleTo]. A bare same-assembly
// comparison would reject a referenced assembly's internal constructor the generated code could in fact
// call. The module member checks ask it the same way (see ResolveFactory).
List<IMethodSymbol> constructors = implementation.InstanceConstructors
.Where(c => IsAccessibleConstructor(c, containerSymbol))
.Where(c => compilation.IsSymbolAccessibleWithin(c, containerSymbol))
.ToList();
if (constructors.Count <= 1)
{
Expand Down Expand Up @@ -699,27 +704,16 @@ private static string DescribeOwner(ImplInfo info)

// Fall back to the greediest constructor so its unresolved parameters surface as AWT101.
return resolvable ?? constructors.OrderByDescending(c => c.Parameters.Length).First();

static bool IsAccessibleConstructor(IMethodSymbol constructor, INamedTypeSymbol containerSymbol)
{
return constructor.DeclaredAccessibility switch
{
Accessibility.Public => true,
Accessibility.Internal or Accessibility.ProtectedOrInternal =>
SymbolEqualityComparer.Default.Equals(
constructor.ContainingAssembly, containerSymbol.ContainingAssembly),
_ => false,
};
}
}

/// <summary>
/// The members named <paramref name="name" /> the generated container partial can reach: the
/// container's own members (any accessibility, since a partial can use its own private members) plus
/// inherited members a derived type can access (everything but private, with internal / private-protected
/// restricted to the same assembly).
/// inherited members the container can access, which is Roslyn's own check: a base type's protected member
/// qualifies (the container derives from it) and an internal one does when the declaring assembly is the
/// container's or grants it [InternalsVisibleTo].
/// </summary>
private static IEnumerable<ISymbol> AccessibleMembers(INamedTypeSymbol container, string name)
private static IEnumerable<ISymbol> AccessibleMembers(INamedTypeSymbol container, string name, Compilation compilation)
{
foreach (ISymbol member in container.GetMembers(name))
{
Expand All @@ -728,20 +722,11 @@ private static IEnumerable<ISymbol> AccessibleMembers(INamedTypeSymbol container

for (INamedTypeSymbol? baseType = container.BaseType; baseType is not null; baseType = baseType.BaseType)
{
foreach (ISymbol member in baseType.GetMembers(name).Where(m => IsAccessibleFromDerived(m, container)))
foreach (ISymbol member in baseType.GetMembers(name).Where(m => compilation.IsSymbolAccessibleWithin(m, container)))
{
yield return member;
}
}

static bool IsAccessibleFromDerived(ISymbol member, INamedTypeSymbol container)
=> member.DeclaredAccessibility switch
{
Accessibility.Public or Accessibility.Protected or Accessibility.ProtectedOrInternal => true,
Accessibility.Internal or Accessibility.ProtectedAndInternal =>
SymbolEqualityComparer.Default.Equals(member.ContainingAssembly, container.ContainingAssembly),
_ => false,
};
}

/// <summary>
Expand All @@ -756,7 +741,7 @@ private static List<IMethodSymbol> FindFactoryCandidates(
INamedTypeSymbol container, string name, ITypeSymbol serviceType, Compilation compilation)
{
List<IMethodSymbol> candidates = new();
foreach (ISymbol member in AccessibleMembers(container, name))
foreach (ISymbol member in AccessibleMembers(container, name, compilation))
{
if (member is IMethodSymbol { MethodKind: MethodKind.Ordinary, } method
&& compilation.HasImplicitConversion(ProducedType(method.ReturnType, compilation), serviceType))
Expand Down
10 changes: 6 additions & 4 deletions Source/Awaiten.SourceGenerators/AwaitenGenerator.Scan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1102,7 +1102,7 @@ private static bool PruneUnconstructableScanRound(
{
if (pinnedImpls.Contains(registration.ImplementationType)
|| !checkedImpls.Add(registration.ImplementationType)
|| FirstUnconstructableReason(registration.Implementation, containerSymbol, services, constraintRejected, external, variance) is not { } reason)
|| FirstUnconstructableReason(registration.Implementation, containerSymbol, compilation, services, constraintRejected, external, variance) is not { } reason)
{
continue;
}
Expand Down Expand Up @@ -1134,13 +1134,14 @@ private static bool PruneUnconstructableScanRound(
private static string? FirstUnconstructableReason(
INamedTypeSymbol implementation,
INamedTypeSymbol containerSymbol,
Compilation compilation,
HashSet<ServiceKey> services,
HashSet<string> constraintRejected,
ExternalSurface external,
VarianceState variance)
{
IMethodSymbol? constructor = SelectConstructor(
implementation, containerSymbol, services.Select(service => service.Service), external);
implementation, containerSymbol, compilation, services.Select(service => service.Service), external);
if (constructor is null)
{
return "it has no constructor accessible to the container";
Expand All @@ -1164,7 +1165,7 @@ model.Kind is DependencyKind.Arg or DependencyKind.External

foreach (IPropertySymbol property in InjectedProperties(implementation))
{
if (UnsatisfiableInjectedMemberReason(property, containerSymbol, services, constraintRejected, external.ServiceTypes) is { } reason)
if (UnsatisfiableInjectedMemberReason(property, containerSymbol, compilation, services, constraintRejected, external.ServiceTypes) is { } reason)
{
return reason;
}
Expand All @@ -1182,12 +1183,13 @@ model.Kind is DependencyKind.Arg or DependencyKind.External
private static string? UnsatisfiableInjectedMemberReason(
IPropertySymbol property,
INamedTypeSymbol containerSymbol,
Compilation compilation,
HashSet<ServiceKey> services,
HashSet<string> constraintRejected,
HashSet<string> externalServiceTypes)
{
ParameterModel member = ClassifyDependency(property.Type, property.GetAttributes(), asyncFactory: false, location: null, externalServiceTypes);
if (property.SetMethod is not { } setter || !IsAccessibleSetter(setter, containerSymbol)
if (property.SetMethod is not { } setter || !IsAccessibleSetter(setter, containerSymbol, compilation)
|| member.Kind is DependencyKind.Arg or DependencyKind.External || IsInjectOptional(property.GetAttributes()))
{
return null;
Expand Down
Loading
Loading