Skip to content

fix: honor InternalsVisibleTo when checking constructor accessibility#116

Merged
vbreuss merged 3 commits into
mainfrom
fix/ivt-accessibility
Jul 17, 2026
Merged

fix: honor InternalsVisibleTo when checking constructor accessibility#116
vbreuss merged 3 commits into
mainfrom
fix/ivt-accessibility

Conversation

@vbreuss

@vbreuss vbreuss commented Jul 17, 2026

Copy link
Copy Markdown
Member

Awaiten used two different accessibility tests and only one of them was correct. Module member resolution asks Roslyn (compilation.IsSymbolAccessibleWithin(candidate, containerSymbol)), which honors InternalsVisibleTo. Constructor selection, setter injection and the container's inherited-member lookup instead hand-rolled a SymbolEqualityComparer.Default.Equals(a.ContainingAssembly, b.ContainingAssembly) "same assembly" comparison, which does not.

The two disagreed within a single scan. A referenced assembly that grants [assembly: InternalsVisibleTo("MyApp")] and exposes a public sealed class Roaster with an internal constructor passed IsScanCandidate (that check already asked Roslyn), and was then rejected by IsAccessibleConstructor — failing the build with AWT104 on a new the generated container could legally have emitted.

Replaces the hand-rolled comparison with Roslyn's own check at all three sites:

  • SelectConstructor / IsAccessibleConstructor (AWT104): an internal constructor across an InternalsVisibleTo boundary is now selected, and without the grant still reports AWT104.
  • IsAccessibleSetter (AWT136): property injection into an internal setter across the boundary now works; without the grant it still surfaces as AWT136 rather than an inaccessible-setter error in generated code.
  • AccessibleMembers / IsAccessibleFromDerived: the base-type walk for factory / instance / hook lookup. The container's own members keep their deliberate permissiveness — they are yielded by the first loop untouched, and IsSymbolAccessibleWithin(privateMember, containerSymbol) would return true for them anyway, since a generated partial can use its container's private members.

Threads Compilation to the sites that lacked it (SelectConstructor and its five callers, InjectionContext, ExpandOpenGenerics / RequiredServiceTypes, and the scan's FirstUnconstructableReason / UnsatisfiableInjectedMemberReason).

Beyond InternalsVisibleTo, Roslyn's check also walks containing types, so a constructor that is public on a type nested privately in some other class is no longer treated as reachable. That code never compiled anyway.

AwaitenGenerator.OpenGenerics.cs:593 looks like the same pattern but is not: it checks DeclaredAccessibility == Accessibility.Public for a new() constraint, and the constraint genuinely requires a public parameterless constructor. where T : new() over a type whose only parameterless constructor is internal is CS0310 even within one assembly, so relaxing it would make expansion synthesize closed generics that do not compile. Left as is.

All 591 generator tests pass unchanged, including CrossAssemblyModuleTests.CrossAssemblyModule_InternalFactory_ReportsAwt108NamingTheModule, which pins the opposite boundary: a cross-assembly internal member without InternalsVisibleTo is not imported into the referencing compilation's symbol tables at all, so it stays AWT108 (invisibility) rather than becoming AWT153 (inaccessibility).

Awaiten used two different accessibility tests and only one of them was correct. Module member resolution asks Roslyn (`compilation.IsSymbolAccessibleWithin(candidate, containerSymbol)`), which honors `InternalsVisibleTo`. Constructor selection, setter injection and the container's inherited-member lookup instead hand-rolled a `SymbolEqualityComparer.Default.Equals(a.ContainingAssembly, b.ContainingAssembly)` "same assembly" comparison, which does not.

The two disagreed within a single scan. A referenced assembly that grants `[assembly: InternalsVisibleTo("MyApp")]` and exposes a `public sealed class Roaster` with an `internal` constructor passed `IsScanCandidate` (that check already asked Roslyn), and was then rejected by `IsAccessibleConstructor` — failing the build with AWT104 on a `new` the generated container could legally have emitted.

Replaces the hand-rolled comparison with Roslyn's own check at all three sites:

- `SelectConstructor` / `IsAccessibleConstructor` (AWT104): an internal constructor across an `InternalsVisibleTo` boundary is now selected, and without the grant still reports AWT104.
- `IsAccessibleSetter` (AWT136): property injection into an internal setter across the boundary now works; without the grant it still surfaces as AWT136 rather than an inaccessible-setter error in generated code.
- `AccessibleMembers` / `IsAccessibleFromDerived`: the base-type walk for factory / instance / hook lookup. The container's own members keep their deliberate permissiveness — they are yielded by the first loop untouched, and `IsSymbolAccessibleWithin(privateMember, containerSymbol)` would return true for them anyway, since a generated partial can use its container's private members.

Threads `Compilation` to the sites that lacked it (`SelectConstructor` and its five callers, `InjectionContext`, `ExpandOpenGenerics` / `RequiredServiceTypes`, and the scan's `FirstUnconstructableReason` / `UnsatisfiableInjectedMemberReason`).

Beyond `InternalsVisibleTo`, Roslyn's check also walks containing types, so a constructor that is public on a type nested privately in some other class is no longer treated as reachable. That code never compiled anyway.

`AwaitenGenerator.OpenGenerics.cs:593` looks like the same pattern but is not: it checks `DeclaredAccessibility == Accessibility.Public` for a `new()` constraint, and the constraint genuinely requires a public parameterless constructor. `where T : new()` over a type whose only parameterless constructor is `internal` is CS0310 even within one assembly, so relaxing it would make expansion synthesize closed generics that do not compile. Left as is.

All 591 generator tests pass unchanged, including `CrossAssemblyModuleTests.CrossAssemblyModule_InternalFactory_ReportsAwt108NamingTheModule`, which pins the opposite boundary: a cross-assembly internal member without `InternalsVisibleTo` is not imported into the referencing compilation's symbol tables at all, so it stays AWT108 (invisibility) rather than becoming AWT153 (inaccessibility).
@vbreuss vbreuss self-assigned this Jul 17, 2026
@vbreuss vbreuss added the bug Something isn't working label Jul 17, 2026
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown

Test Results

   18 files  ± 0     18 suites  ±0   8m 56s ⏱️ +19s
1 135 tests + 7  1 134 ✅ + 7  1 💤 ±0  0 ❌ ±0 
5 522 runs  +21  5 521 ✅ +21  1 💤 ±0  0 ❌ ±0 

Results for commit 678b7e9. ± Comparison against base commit bc305b8.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown

🚀 Benchmark Results

Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
[Host] : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Build Size Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 8 16.37 ns 0.108 ns 0.090 ns 0.97 144 B 1.00
Awaiten 8 16.936 ns 0.2115 ns 0.1875 ns 1.00 144 B 1.00
MsDI 8 1,409.918 ns 7.5525 ns 6.6951 ns 83.26 5688 B 39.50
Autofac 8 34,119.371 ns 164.8775 ns 154.2265 ns 2,014.78 33098 B 229.85
Jab 8 9.840 ns 0.0281 ns 0.0220 ns 0.58 96 B 0.67
PureDI 8 14.789 ns 0.1489 ns 0.1320 ns 0.87 128 B 0.89
DryIoc 8 744.705 ns 10.0350 ns 9.3868 ns 43.98 1528 B 10.61
SimpleInjector 8 11,082.052 ns 82.3888 ns 73.0355 ns 654.40 24760 B 171.94
baseline* 256 86.97 ns 0.352 ns 0.329 ns 1.06 2128 B 1.00
Awaiten 256 81.692 ns 0.3295 ns 0.2921 ns 1.00 2128 B 1.00
MsDI 256 13,721.151 ns 66.3990 ns 62.1097 ns 167.96 61016 B 28.67
Autofac 256 826,597.188 ns 2,661.0898 ns 2,077.6039 ns 10,118.63 720917 B 338.78
Jab 256 65.700 ns 0.3695 ns 0.3276 ns 0.80 2080 B 0.98
PureDI 256 75.709 ns 0.0893 ns 0.0746 ns 0.93 2112 B 0.99
DryIoc 256 44,695.060 ns 333.7634 ns 312.2025 ns 547.13 80099 B 37.64
SimpleInjector 256 336,052.200 ns 1,674.9132 ns 1,484.7669 ns 4,113.72 573088 B 269.31
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
[Host] : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Realistic Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 245.6 ns 1.71 ns 1.51 ns 1.06 568 B 1.00
Awaiten 232.7 ns 3.39 ns 3.17 ns 1.00 568 B 1.00
MsDI 592.5 ns 11.57 ns 10.26 ns 2.55 1104 B 1.94
Autofac 6,623.8 ns 28.00 ns 26.19 ns 28.47 10784 B 18.99
Jab 173.0 ns 0.19 ns 0.16 ns 0.74 432 B 0.76
DryIoc 404.0 ns 4.68 ns 4.15 ns 1.74 944 B 1.66
SimpleInjector 711.1 ns 4.06 ns 3.80 ns 3.06 1096 B 1.93
PureDI 174.6 ns 0.93 ns 0.78 ns 0.75 632 B 1.11
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
[Host] : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Resolve Size Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 8 7.022 ns 0.0083 ns 0.0065 ns 1.00 - NA
Awaiten 8 7.040 ns 0.0755 ns 0.0630 ns 1.00 - NA
MsDI 8 7.497 ns 0.0049 ns 0.0044 ns 1.06 - NA
Autofac 8 144.620 ns 4.5940 ns 4.2972 ns 20.54 656 B NA
Jab 8 2.653 ns 0.0050 ns 0.0042 ns 0.38 - NA
PureDI 8 5.436 ns 0.0210 ns 0.0186 ns 0.77 - NA
DryIoc 8 9.075 ns 0.0054 ns 0.0045 ns 1.29 - NA
SimpleInjector 8 10.897 ns 0.0183 ns 0.0162 ns 1.55 - NA
baseline* 256 8.092 ns 0.0144 ns 0.0121 ns 1.03 - NA
Awaiten 256 7.836 ns 0.0179 ns 0.0159 ns 1.00 - NA
MsDI 256 7.357 ns 0.0133 ns 0.0111 ns 0.94 - NA
Autofac 256 140.319 ns 2.5967 ns 2.4290 ns 17.91 656 B NA
Jab 256 43.134 ns 0.0487 ns 0.0407 ns 5.50 - NA
PureDI 256 8.267 ns 0.0075 ns 0.0067 ns 1.06 - NA
DryIoc 256 9.030 ns 0.0082 ns 0.0069 ns 1.15 - NA
SimpleInjector 256 14.493 ns 0.1070 ns 0.1001 ns 1.85 - NA

baseline* rows show the corresponding Awaiten benchmark from the most recent successful main branch build with results, for regression comparison.

vbreuss added 2 commits July 17, 2026 12:20
Three comments added by the accessibility fix used a spaced hyphen where the surrounding code expresses the same aside with a comma or a colon: SelectConstructor's rationale, IsAccessibleSetter's, and the AccessibleMembers remarks. No behavior change.
Honoring [InternalsVisibleTo] can change which constructor selection picks,
not just whether a lone internal one is admitted. Add a with/without-grant
pair where a resolvable internal parameterless constructor is preferred over
an unresolvable greedy public one only when the grant makes it a candidate.
@vbreuss
vbreuss force-pushed the fix/ivt-accessibility branch from 127f133 to 678b7e9 Compare July 17, 2026 13:00
@sonarqubecloud

Copy link
Copy Markdown

@vbreuss
vbreuss enabled auto-merge (squash) July 17, 2026 13:04
@vbreuss
vbreuss merged commit 3bceeb2 into main Jul 17, 2026
14 checks passed
@vbreuss
vbreuss deleted the fix/ivt-accessibility branch July 17, 2026 13:07
github-actions Bot added a commit that referenced this pull request Jul 17, 2026
…ecking constructor accessibility (#116) by Valentin Breuß
github-actions Bot added a commit that referenced this pull request Jul 17, 2026
…ecking constructor accessibility (#116) by Valentin Breuß
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant