diff --git a/Source/Awaiten.SourceGenerators/AwaitenGenerator.Classification.cs b/Source/Awaiten.SourceGenerators/AwaitenGenerator.Classification.cs
index 7b46d6e..1e204d3 100644
--- a/Source/Awaiten.SourceGenerators/AwaitenGenerator.Classification.cs
+++ b/Source/Awaiten.SourceGenerators/AwaitenGenerator.Classification.cs
@@ -188,12 +188,14 @@ private static void ReportWhenUnregistered(ParameterModel parameterModel, ImplIn
///
/// 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 [ImportService<T>] 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
+ /// [ImportService<T>] external service types (a member of one resolves externally rather than
+ /// being reported missing), and the diagnostics sink.
///
private sealed record InjectionContext(
INamedTypeSymbol ContainerSymbol,
+ Compilation Compilation,
Dictionary ServiceToImpl,
HashSet ConstraintRejected,
HashSet ConsumedConditionals,
@@ -341,11 +343,12 @@ private static IEnumerable 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,
@@ -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);
///
/// Classifies a constructor parameter as a runtime argument ([Arg]), a deferred relationship type
diff --git a/Source/Awaiten.SourceGenerators/AwaitenGenerator.Collection.cs b/Source/Awaiten.SourceGenerators/AwaitenGenerator.Collection.cs
index 06a8e0b..20ac8fe 100644
--- a/Source/Awaiten.SourceGenerators/AwaitenGenerator.Collection.cs
+++ b/Source/Awaiten.SourceGenerators/AwaitenGenerator.Collection.cs
@@ -45,7 +45,7 @@ private static (List Raw, HashSet 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
diff --git a/Source/Awaiten.SourceGenerators/AwaitenGenerator.Decorators.cs b/Source/Awaiten.SourceGenerators/AwaitenGenerator.Decorators.cs
index fd09795..6caff21 100644
--- a/Source/Awaiten.SourceGenerators/AwaitenGenerator.Decorators.cs
+++ b/Source/Awaiten.SourceGenerators/AwaitenGenerator.Decorators.cs
@@ -321,7 +321,7 @@ private readonly record struct ServiceChain(
///
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;
@@ -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;
diff --git a/Source/Awaiten.SourceGenerators/AwaitenGenerator.OpenGenerics.cs b/Source/Awaiten.SourceGenerators/AwaitenGenerator.OpenGenerics.cs
index ec07031..a3e5e21 100644
--- a/Source/Awaiten.SourceGenerators/AwaitenGenerator.OpenGenerics.cs
+++ b/Source/Awaiten.SourceGenerators/AwaitenGenerator.OpenGenerics.cs
@@ -190,6 +190,7 @@ private static void ExpandOpenGenerics(
List raw,
List open,
INamedTypeSymbol containerSymbol,
+ Compilation compilation,
ExternalSurface external,
List diagnostics,
HashSet constraintRejected)
@@ -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)))
@@ -379,6 +380,7 @@ private static void ReportExpansionTooDeep(INamedTypeSymbol impl, int depth, Exp
private static IEnumerable RequiredServiceTypes(
INamedTypeSymbol implementation,
INamedTypeSymbol containerSymbol,
+ Compilation compilation,
List raw,
HashSet openServices,
ExternalSurface external)
@@ -386,6 +388,7 @@ private static IEnumerable RequiredServiceTypes(
IMethodSymbol? constructor = AwaitenGenerator.SelectConstructor(
implementation,
containerSymbol,
+ compilation,
raw.Select(r => r.ServiceType),
external,
p => IsOpenGenericSatisfiable(p.Type, openServices));
diff --git a/Source/Awaiten.SourceGenerators/AwaitenGenerator.Production.cs b/Source/Awaiten.SourceGenerators/AwaitenGenerator.Production.cs
index 2aadf01..c31a4b9 100644
--- a/Source/Awaiten.SourceGenerators/AwaitenGenerator.Production.cs
+++ b/Source/Awaiten.SourceGenerators/AwaitenGenerator.Production.cs
@@ -172,7 +172,7 @@ private static List 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;
@@ -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(
@@ -298,7 +298,7 @@ private static (string? Hook, EquatableArray Parameters) Resolve
Compilation compilation = context.Compilation;
List 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.
@@ -591,7 +591,7 @@ private static void ValidateInstanceMember(
List 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
{
@@ -666,12 +666,17 @@ private static string DescribeOwner(ImplInfo info)
internal static IMethodSymbol? SelectConstructor(
INamedTypeSymbol implementation,
INamedTypeSymbol containerSymbol,
+ Compilation compilation,
IEnumerable registeredServices,
ExternalSurface external,
Func? 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 constructors = implementation.InstanceConstructors
- .Where(c => IsAccessibleConstructor(c, containerSymbol))
+ .Where(c => compilation.IsSymbolAccessibleWithin(c, containerSymbol))
.ToList();
if (constructors.Count <= 1)
{
@@ -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,
- };
- }
}
///
/// The members named 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].
///
- private static IEnumerable AccessibleMembers(INamedTypeSymbol container, string name)
+ private static IEnumerable AccessibleMembers(INamedTypeSymbol container, string name, Compilation compilation)
{
foreach (ISymbol member in container.GetMembers(name))
{
@@ -728,20 +722,11 @@ private static IEnumerable 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,
- };
}
///
@@ -756,7 +741,7 @@ private static List FindFactoryCandidates(
INamedTypeSymbol container, string name, ITypeSymbol serviceType, Compilation compilation)
{
List 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))
diff --git a/Source/Awaiten.SourceGenerators/AwaitenGenerator.Scan.cs b/Source/Awaiten.SourceGenerators/AwaitenGenerator.Scan.cs
index c462a74..6ced642 100644
--- a/Source/Awaiten.SourceGenerators/AwaitenGenerator.Scan.cs
+++ b/Source/Awaiten.SourceGenerators/AwaitenGenerator.Scan.cs
@@ -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;
}
@@ -1134,13 +1134,14 @@ private static bool PruneUnconstructableScanRound(
private static string? FirstUnconstructableReason(
INamedTypeSymbol implementation,
INamedTypeSymbol containerSymbol,
+ Compilation compilation,
HashSet services,
HashSet 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";
@@ -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;
}
@@ -1182,12 +1183,13 @@ model.Kind is DependencyKind.Arg or DependencyKind.External
private static string? UnsatisfiableInjectedMemberReason(
IPropertySymbol property,
INamedTypeSymbol containerSymbol,
+ Compilation compilation,
HashSet services,
HashSet constraintRejected,
HashSet 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;
diff --git a/Tests/Awaiten.SourceGenerators.Tests/CrossAssemblyAccessibilityTests.cs b/Tests/Awaiten.SourceGenerators.Tests/CrossAssemblyAccessibilityTests.cs
new file mode 100644
index 0000000..2705885
--- /dev/null
+++ b/Tests/Awaiten.SourceGenerators.Tests/CrossAssemblyAccessibilityTests.cs
@@ -0,0 +1,218 @@
+using System.Linq;
+
+namespace Awaiten.SourceGenerators.Tests;
+
+///
+/// Accessibility across an assembly boundary: the container reaches an internal constructor or setter of
+/// a referenced assembly exactly when that assembly grants it [InternalsVisibleTo], and not otherwise.
+/// The generated container lives in "TestAssembly", the referenced source compiles as "ReferencedAssembly".
+///
+public class CrossAssemblyAccessibilityTests
+{
+ [Fact]
+ public async Task InternalConstructorAcrossInternalsVisibleTo_IsSelected()
+ {
+ GeneratorResult result = Generator.RunWithReferencedAssembly("""
+ [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("TestAssembly")]
+
+ namespace Lib;
+
+ public interface IEquipment { }
+ public sealed class Roaster : IEquipment { internal Roaster() { } }
+ """, """
+ using Awaiten;
+
+ namespace MyCode;
+
+ [Container]
+ [Scan(typeof(Lib.IEquipment), InAssembliesOf = new[] { typeof(Lib.IEquipment) })]
+ public static partial class MyContainer
+ {
+ }
+ """);
+
+ // The scan admits Roaster (it asks Roslyn, which honors the grant), so constructor selection must agree:
+ // rejecting the constructor here would raise AWT104 on a 'new' the generated code may legally emit. An
+ // empty diagnostic set covers the emitted source too, so this also proves the construction compiles.
+ await That(result.Diagnostics).IsEmpty()
+ .Because("[InternalsVisibleTo] makes the internal constructor callable from the container's assembly");
+ await That(result.Sources["Awaiten.MyCode.MyContainer.g.cs"]).Contains("new global::Lib.Roaster()")
+ .Because("the container constructs the scanned implementation through its internal constructor");
+ }
+
+ [Fact]
+ public async Task InternalConstructorWithoutInternalsVisibleTo_ReportsAwt104()
+ {
+ GeneratorResult result = Generator.RunWithReferencedAssembly("""
+ namespace Lib;
+
+ public interface IEquipment { }
+ public sealed class Roaster : IEquipment { internal Roaster() { } }
+ """, """
+ using Awaiten;
+
+ namespace MyCode;
+
+ [Container]
+ [Scan(typeof(Lib.IEquipment), InAssembliesOf = new[] { typeof(Lib.IEquipment) })]
+ public static partial class MyContainer
+ {
+ }
+ """);
+
+ await That(result.Diagnostics).Contains("*AWT104*Lib.Roaster*").AsWildcard()
+ .Because("without the grant the container's assembly cannot call the internal constructor");
+ }
+
+ [Fact]
+ public async Task InternalImplementationAcrossInternalsVisibleTo_IsScannedAndConstructed()
+ {
+ GeneratorResult result = Generator.RunWithReferencedAssembly("""
+ [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("TestAssembly")]
+
+ namespace Lib;
+
+ public interface IEquipment { }
+ internal sealed class Roaster : IEquipment { }
+ """, """
+ using Awaiten;
+
+ namespace MyCode;
+
+ [Container]
+ [Scan(typeof(Lib.IEquipment), InAssembliesOf = new[] { typeof(Lib.IEquipment) })]
+ public static partial class MyContainer
+ {
+ }
+ """);
+
+ await That(result.Diagnostics).IsEmpty()
+ .Because("the grant makes the internal implementation nameable from the container's assembly");
+ await That(result.Sources["Awaiten.MyCode.MyContainer.g.cs"]).Contains("new global::Lib.Roaster()")
+ .Because("an internal type the container can see is constructed like a public one");
+ }
+
+ [Fact]
+ public async Task InternalSetterAcrossInternalsVisibleTo_IsInjected()
+ {
+ GeneratorResult result = Generator.RunWithReferencedAssembly("""
+ using Awaiten;
+
+ [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("TestAssembly")]
+
+ namespace Lib;
+
+ public sealed class Grinder { }
+ public sealed class Roaster
+ {
+ [Inject] public Grinder Grinder { get; internal set; }
+ }
+ """, """
+ using Awaiten;
+
+ namespace MyCode;
+
+ [Container]
+ [Transient]
+ [Transient]
+ public static partial class MyContainer
+ {
+ }
+ """);
+
+ await That(result.Diagnostics).IsEmpty()
+ .Because("the grant lets the container's object initializer assign the internal setter");
+ await That(result.Sources["Awaiten.MyCode.MyContainer.g.cs"]).Contains("Grinder =")
+ .Because("the injected member is filled from the object initializer");
+ }
+
+ [Fact]
+ public async Task InternalSetterWithoutInternalsVisibleTo_ReportsAwt136()
+ {
+ GeneratorResult result = Generator.RunWithReferencedAssembly("""
+ using Awaiten;
+
+ namespace Lib;
+
+ public sealed class Grinder { }
+ public sealed class Roaster
+ {
+ [Inject] public Grinder Grinder { get; internal set; }
+ }
+ """, """
+ using Awaiten;
+
+ namespace MyCode;
+
+ [Container]
+ [Transient]
+ [Transient]
+ public static partial class MyContainer
+ {
+ }
+ """);
+
+ await That(result.Diagnostics).Contains("*AWT136*Grinder*").AsWildcard()
+ .Because("without the grant the setter is out of the container's reach, so it must surface as AWT136 rather than an inaccessible-setter error in generated code");
+ }
+
+ [Fact]
+ public async Task MultipleConstructors_ResolvableInternalOneSelectedAcrossInternalsVisibleTo()
+ {
+ GeneratorResult result = Generator.RunWithReferencedAssembly("""
+ [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("TestAssembly")]
+
+ namespace Lib;
+
+ public sealed class Grinder { }
+ public sealed class Roaster
+ {
+ public Roaster(Grinder grinder) { }
+ internal Roaster() { }
+ }
+ """, """
+ using Awaiten;
+
+ namespace MyCode;
+
+ [Container]
+ [Transient]
+ public static partial class MyContainer
+ {
+ }
+ """);
+
+ await That(result.Diagnostics).IsEmpty()
+ .Because("[InternalsVisibleTo] admits the internal constructor as a candidate, and it is the resolvable overload");
+ await That(result.Sources["Awaiten.MyCode.MyContainer.g.cs"]).Contains("new global::Lib.Roaster()")
+ .Because("constructor selection prefers the resolvable parameterless overload over the greedy public one");
+ }
+
+ [Fact]
+ public async Task MultipleConstructors_WithoutInternalsVisibleTo_FallsToPublicConstructorAndReportsAwt101()
+ {
+ GeneratorResult result = Generator.RunWithReferencedAssembly("""
+ namespace Lib;
+
+ public sealed class Grinder { }
+ public sealed class Roaster
+ {
+ public Roaster(Grinder grinder) { }
+ internal Roaster() { }
+ }
+ """, """
+ using Awaiten;
+
+ namespace MyCode;
+
+ [Container]
+ [Transient]
+ public static partial class MyContainer
+ {
+ }
+ """);
+
+ await That(result.Diagnostics).Contains("*AWT101*Grinder*").AsWildcard()
+ .Because("without the grant only the public constructor is reachable, and its dependency is unregistered");
+ }
+}