Read InternalsVisibleTo grants from the compiled assembly manifest #85

Merged
rob merged 4 commits from feat/internals-visible-to-test into main 2026-08-06 14:40:17 +00:00
Owner

Closes task #145.

Review found the original .csproj/Roslyn pair still missed two forms confirmed against built code: an InternalsVisibleTo item in Directory.Build.props (grants every project at once) and an AssemblyAttribute item naming InternalsVisibleToAttribute (a second MSBuild mechanism, invisible to both the item reader and the source sweep). Rather than extend the pair a third and fourth time — the same instalment plan ADR-0070's History paid for a different rule — CompiledAssemblies now reads every project's compiled assembly manifest with MetadataLoadContext. Every expression of the grant converges on the same InternalsVisibleToAttribute manifest entry, so the manifest is downstream of all of them at once. The item-form .csproj reader and the Roslyn source-attribute sweep are deleted, not kept alongside it — both are fully subsumed.

ADR-0074 records the decision, including why it inverts ADR-0030's own reasoning rather than contradicting it (ADR-0030's objection to compiled output was that it under-reports a ProjectReference; an InternalsVisibleTo grant has no effect unless it reaches the manifest, so there's nothing to under-report), and states the manifest reader's own limits: it can't say a grant is still needed rather than merely declared, and a grant behind an MSBuild Condition that never evaluates true for the built configuration won't be found. ADR-0030 cross-references ADR-0074 in both directions.

All four forms — the MSBuild item, a hand-written [assembly: InternalsVisibleTo(...)] attribute, an AssemblyAttribute item in a .csproj, and an item in Directory.Build.props — were verified reddening the test by mutation, then reverted.

Closes task #145. Review found the original `.csproj`/Roslyn pair still missed two forms confirmed against built code: an `InternalsVisibleTo` item in `Directory.Build.props` (grants every project at once) and an `AssemblyAttribute` item naming `InternalsVisibleToAttribute` (a second MSBuild mechanism, invisible to both the item reader and the source sweep). Rather than extend the pair a third and fourth time — the same instalment plan ADR-0070's History paid for a different rule — `CompiledAssemblies` now reads every project's compiled assembly manifest with `MetadataLoadContext`. Every expression of the grant converges on the same `InternalsVisibleToAttribute` manifest entry, so the manifest is downstream of all of them at once. The item-form `.csproj` reader and the Roslyn source-attribute sweep are deleted, not kept alongside it — both are fully subsumed. ADR-0074 records the decision, including why it inverts ADR-0030's own reasoning rather than contradicting it (ADR-0030's objection to compiled output was that it *under-reports* a `ProjectReference`; an `InternalsVisibleTo` grant has no effect unless it reaches the manifest, so there's nothing to under-report), and states the manifest reader's own limits: it can't say a grant is still *needed* rather than merely declared, and a grant behind an MSBuild `Condition` that never evaluates true for the built configuration won't be found. ADR-0030 cross-references ADR-0074 in both directions. All four forms — the MSBuild item, a hand-written `[assembly: InternalsVisibleTo(...)]` attribute, an `AssemblyAttribute` item in a `.csproj`, and an item in `Directory.Build.props` — were verified reddening the test by mutation, then reverted.
Assert the exact InternalsVisibleTo grants in the architecture tests
All checks were successful
CI / build (pull_request) Successful in 2m30s
27062f43a1
rob left a comment

Verdict: changes needed

Two confirmed, unstated evasions — both verified by building the mutation and watching every architecture test stay green:

  1. InternalsVisibleTo in Directory.Build.props. <ItemGroup><InternalsVisibleTo Include="X" /></ItemGroup> there grants X visibility into every project in the solution (confirmed via -getItem:InternalsVisibleTo, DefiningProjectFullPath = Directory.Build.props). Solution.AllProjects()/Read() only opens each project's own .csproj, so EveryOtherProject_InternalsVisibleTo_IsNone never sees it. It's also not caught by the existing BuildFileTests.BuildFiles_AcrossTheRepository_DeclareNothingThatLeadsElsewhere, because Solution.DeclarationsForbiddenIn checks DependencyItemNames (ProjectReference + the external-reference items) and InternalsVisibleToItemName was deliberately kept out of that list. This is worse than a single-project miss — it's a solution-wide grant, invisible to both the new tests and the pre-existing build-file guard whose whole purpose is exactly this class of hole for every other item type.

  2. AssemblyAttribute item in a project's own .csproj. <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute"><_Parameter1>X</_Parameter1></AssemblyAttribute> compiles to a real [assembly: InternalsVisibleToAttribute("X")] (confirmed in the generated AssemblyInfo.cs). Invisible to the item-form reader (only matches the InternalsVisibleTo item name) and to the Roslyn sweep (only parses .cs files; this is .csproj XML).

Neither gap is disclosed. The class remarks on InternalsVisibleToTests carefully name the using-alias gap and the declared-vs-still-used gap, but say nothing about either of these, despite Directory.Build.props being singled out elsewhere in this same test project as exactly the place a reference arrives from unseen. Close at least the Directory.Build.props one (add InternalsVisibleToItemName to what BuildFileTests forbids there, or an equivalent explicit check) — it's a solution-wide bypass of a test whose entire job is catching a widened grant. If either is left open, say so in the remarks the same way the alias gap is.

Everything else checks out: widened PlaceMark.Api grant, an untracked new .csproj with a grant, and the fully-qualified attribute form (System.Runtime.CompilerServices.InternalsVisibleTo(...)) all correctly redden; PublicKeyToken/wildcard qualification isn't a real vector since InternalsVisibleTo doesn't support wildcards and any qualifier changes the string, which fails the exact-match ShouldBe. No ADR is a reasonable call — this applies ADR-0030's XML reading and ADR-0070's Roslyn precedent to their respective questions rather than deciding anything new. README paragraph matches what's implemented and doesn't overclaim. 0073 is free (highest ADR on this branch and on main is 0072).

Verdict: changes needed Two confirmed, unstated evasions — both verified by building the mutation and watching every architecture test stay green: 1. **`InternalsVisibleTo` in `Directory.Build.props`.** `<ItemGroup><InternalsVisibleTo Include="X" /></ItemGroup>` there grants `X` visibility into *every* project in the solution (confirmed via `-getItem:InternalsVisibleTo`, `DefiningProjectFullPath` = `Directory.Build.props`). `Solution.AllProjects()`/`Read()` only opens each project's own `.csproj`, so `EveryOtherProject_InternalsVisibleTo_IsNone` never sees it. It's also not caught by the existing `BuildFileTests.BuildFiles_AcrossTheRepository_DeclareNothingThatLeadsElsewhere`, because `Solution.DeclarationsForbiddenIn` checks `DependencyItemNames` (`ProjectReference` + the external-reference items) and `InternalsVisibleToItemName` was deliberately kept out of that list. This is worse than a single-project miss — it's a solution-wide grant, invisible to both the new tests and the pre-existing build-file guard whose whole purpose is exactly this class of hole for every other item type. 2. **`AssemblyAttribute` item in a project's own `.csproj`.** `<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute"><_Parameter1>X</_Parameter1></AssemblyAttribute>` compiles to a real `[assembly: InternalsVisibleToAttribute("X")]` (confirmed in the generated `AssemblyInfo.cs`). Invisible to the item-form reader (only matches the `InternalsVisibleTo` item name) and to the Roslyn sweep (only parses `.cs` files; this is `.csproj` XML). Neither gap is disclosed. The class remarks on `InternalsVisibleToTests` carefully name the `using`-alias gap and the declared-vs-still-used gap, but say nothing about either of these, despite `Directory.Build.props` being singled out elsewhere in this same test project as exactly the place a reference arrives from unseen. Close at least the `Directory.Build.props` one (add `InternalsVisibleToItemName` to what `BuildFileTests` forbids there, or an equivalent explicit check) — it's a solution-wide bypass of a test whose entire job is catching a widened grant. If either is left open, say so in the remarks the same way the alias gap is. Everything else checks out: widened `PlaceMark.Api` grant, an untracked new `.csproj` with a grant, and the fully-qualified attribute form (`System.Runtime.CompilerServices.InternalsVisibleTo(...)`) all correctly redden; PublicKeyToken/wildcard qualification isn't a real vector since `InternalsVisibleTo` doesn't support wildcards and any qualifier changes the string, which fails the exact-match `ShouldBe`. No ADR is a reasonable call — this applies ADR-0030's XML reading and ADR-0070's Roslyn precedent to their respective questions rather than deciding anything new. README paragraph matches what's implemented and doesn't overclaim. 0073 is free (highest ADR on this branch and on main is 0072).
rob changed title from Assert the exact InternalsVisibleTo grants in the architecture tests to Read InternalsVisibleTo grants from the compiled assembly manifest 2026-08-06 14:21:19 +00:00
rob left a comment

Verdict: mergeable

Both prior evasions (Directory.Build.props item, AssemblyAttribute item) verified closed by mutation, alongside the two forms already caught (widened item, item on a clean project) — all four reddened as claimed, then reverted cleanly. Also tried an MSBuild-target-generated [assembly: InternalsVisibleTo] (a fifth mechanism, not in the author's list) — caught correctly, confirming the manifest-is-downstream-of-everything claim rather than just the four enumerated forms.

Deletion confirmed genuine: Solution.AllProjects(), InternalsVisibleToItemName, ProjectFile.InternalsVisibleTo, and the Roslyn source sweep are gone with no remnants (grep -rn "AllProjects\|InternalsVisibleToItemName" across tests/ and src/ is empty).

The three stated limits all verified true: a Condition="'$(Configuration)'=='Debug'" grant on a project this reader considers clean is invisible when built/read in Release, exactly as disclosed; a missing compiled assembly throws the named InvalidOperationException rather than passing vacuously (confirmed by building only the test project, which leaves ~7 unreferenced projects unbuilt).

One real limit isn't named alongside the other three. The reader opens every project but PlaceMark.Api by file path with no ProjectReference, so nothing forces a rebuild before the read. Building/testing tests/PlaceMark.Architecture.Tests in isolation transitively rebuilds PlaceMark.Api and its dependency chain (confirmed — those DLLs are freshly written), but not PlaceMark.WebUI, PlaceMark.Database, or the sibling *.Tests projects. Mutating PlaceMark.WebUI's source to add a real InternalsVisibleTo("Evil") grant, without rebuilding it, then running dotnet test tests/PlaceMark.Architecture.Tests -c Release — with or without --no-build — passes green: the reader silently opens the stale, pre-mutation PlaceMark.WebUI.dll and finds nothing. No exception, unlike the missing-assembly case. CI is unaffected (PlaceMark.slnx is always built and tested whole, per .forgejo/workflows/ci.yml), but the class remarks otherwise commit to naming every honest limit here (the alias gap, the declared-vs-needed gap, the Condition gap) and this one — present specifically because no ProjectReference is taken to the other ~7 projects — isn't among them. Worth a line in CompiledAssemblies' or InternalsVisibleToTests' remarks alongside the other three.

Cross-reference reasoning between ADR-0030 and ADR-0074 is sound as written and present in both directions: ADR-0030's objection to compiled output is under-reporting a declared dependency (an unused ProjectReference that hasn't hit the manifest yet is still a real, if latent, coupling risk); an InternalsVisibleTo grant that never reaches the manifest has no effect at all, so there's nothing to under-report. Title, PR body, README and ADR README index all match the manifest-reading approach; ADR-0074 is genuinely the next free number (0073 landed on main since the last review).

CI green on fb3cf14 (run #400). Merges cleanly against main (git merge-tree reports no conflicts, matching the PR's own mergeable: true). Full solution builds and all test projects pass under the pinned 10.0.100 SDK.

Verdict: mergeable Both prior evasions (`Directory.Build.props` item, `AssemblyAttribute` item) verified closed by mutation, alongside the two forms already caught (widened item, item on a clean project) — all four reddened as claimed, then reverted cleanly. Also tried an MSBuild-target-generated `[assembly: InternalsVisibleTo]` (a fifth mechanism, not in the author's list) — caught correctly, confirming the manifest-is-downstream-of-everything claim rather than just the four enumerated forms. Deletion confirmed genuine: `Solution.AllProjects()`, `InternalsVisibleToItemName`, `ProjectFile.InternalsVisibleTo`, and the Roslyn source sweep are gone with no remnants (`grep -rn "AllProjects\|InternalsVisibleToItemName"` across `tests/` and `src/` is empty). The three stated limits all verified true: a `Condition="'$(Configuration)'=='Debug'"` grant on a project this reader considers clean is invisible when built/read in Release, exactly as disclosed; a missing compiled assembly throws the named `InvalidOperationException` rather than passing vacuously (confirmed by building only the test project, which leaves ~7 unreferenced projects unbuilt). **One real limit isn't named alongside the other three.** The reader opens every project but `PlaceMark.Api` by file path with no `ProjectReference`, so nothing forces a rebuild before the read. Building/testing `tests/PlaceMark.Architecture.Tests` in isolation transitively rebuilds `PlaceMark.Api` and its dependency chain (confirmed — those DLLs are freshly written), but not `PlaceMark.WebUI`, `PlaceMark.Database`, or the sibling `*.Tests` projects. Mutating `PlaceMark.WebUI`'s source to add a real `InternalsVisibleTo("Evil")` grant, without rebuilding it, then running `dotnet test tests/PlaceMark.Architecture.Tests -c Release` — with or without `--no-build` — passes green: the reader silently opens the stale, pre-mutation `PlaceMark.WebUI.dll` and finds nothing. No exception, unlike the missing-assembly case. CI is unaffected (`PlaceMark.slnx` is always built and tested whole, per `.forgejo/workflows/ci.yml`), but the class remarks otherwise commit to naming every honest limit here (the alias gap, the declared-vs-needed gap, the Condition gap) and this one — present specifically because no `ProjectReference` is taken to the other ~7 projects — isn't among them. Worth a line in `CompiledAssemblies`' or `InternalsVisibleToTests`' remarks alongside the other three. Cross-reference reasoning between ADR-0030 and ADR-0074 is sound as written and present in both directions: ADR-0030's objection to compiled output is under-reporting a *declared* dependency (an unused `ProjectReference` that hasn't hit the manifest yet is still a real, if latent, coupling risk); an `InternalsVisibleTo` grant that never reaches the manifest has no effect at all, so there's nothing to under-report. Title, PR body, README and ADR README index all match the manifest-reading approach; ADR-0074 is genuinely the next free number (0073 landed on `main` since the last review). CI green on `fb3cf14` (run #400). Merges cleanly against `main` (`git merge-tree` reports no conflicts, matching the PR's own `mergeable: true`). Full solution builds and all test projects pass under the pinned `10.0.100` SDK.
Document the stale-assembly gap in the InternalsVisibleTo manifest reader
All checks were successful
CI / build (pull_request) Successful in 2m32s
2a22cdb4a5
rob merged commit b2ebb9f360 into main 2026-08-06 14:40:17 +00:00
rob deleted branch feat/internals-visible-to-test 2026-08-06 14:40:17 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rob/PlaceMark!85
No description provided.