Skip to content

fix(mocks): respect generic type argument accessibility (#5453)#5460

Merged
thomhurst merged 1 commit intomainfrom
fix/mocks-5453-generic-type-arg-accessibility
Apr 8, 2026
Merged

fix(mocks): respect generic type argument accessibility (#5453)#5460
thomhurst merged 1 commit intomainfrom
fix/mocks-5453-generic-type-arg-accessibility

Conversation

@thomhurst
Copy link
Copy Markdown
Owner

Summary

  • Fixes [Bug]: CS9338 for class mock when class is not public #5453: source generator emitted public mock wrappers and extension classes for public generic interfaces closed over a non-public type argument (e.g. Mock.Of<ILogger<InternalClass>>()), producing CS9338/CS0051 inconsistent-accessibility errors.
  • Extends the effective-accessibility check from [Bug]: CS9338 for interface mock when interface is not public #5426 — which only walked containing types — to also recursively walk generic type arguments and array element types. Collapses the previous helper pair into a single IsEffectivelyPublic(ITypeSymbol) that switches on type kind.
  • Adds compile-time regression tests in TUnit.Mocks.Tests/Issue5453Tests.cs. If the generator regresses, the test project will not build.

Test plan

  • New Issue5453Tests (3 tests, including a separate internal sealed partial case matching the original report) pass
  • All 775 TUnit.Mocks.Tests pass
  • All 35 TUnit.Mocks.SourceGenerator.Tests snapshot tests pass — no snapshot churn
  • All 30 TUnit.Mocks.Analyzers.Tests pass

Extends the effective-accessibility check from #5426 to also walk
generic type arguments and array element types. Previously, mocking a
public generic interface closed over an internal type argument (e.g.
Mock.Of<ILogger<InternalClass>>()) emitted a public wrapper whose base
signature leaked the internal type, producing CS9338/CS0051.

IsEffectivelyPublic now switches on the type kind and recurses into
TypeArguments/ElementType, collapsing the previous two helpers into one.
Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

Code Review

This is a clean, well-scoped fix for the generic type argument accessibility leak. The analysis is correct and the implementation handles the full problem well.

What the fix does right

The original IsEffectivelyPublic only walked the containing-type chain, so ILogger<InternalClass> would pass (the interface itself is public), while the bound type argument InternalClass was never inspected. The new approach recursively descends all structural positions — containing types, type arguments, array elements — which is the correct complete model of effective public accessibility.

The collapse from a helper pair into one IsEffectivelyPublic(ITypeSymbol) switch is a good simplification; the single-entry-point recursive approach is easier to reason about and extend.

One architectural observation worth considering

The ITypeParameterSymbol → true branch is defensively correct (open generics can't be mocked directly at a call site), but the comment could be tightened: if an unbound generic were somehow discovered, emitting a public wrapper for an unconstrained T is only safe because Roslyn won't bind a generic type argument to a non-public symbol in a context visible to the generator. This is a reasonable assumption but might be worth a one-liner note so a future reader understands the invariant, not just the decision.

Edge case coverage looks solid

A quick mental walkthrough of harder cases:

  • IReadOnlyDictionary<string, IEnumerable<InternalClass>> → descends two levels, correctly returns false
  • Jagged arrays InternalClass[][] → recursion through nested IArrayTypeSymbolINamedTypeSymbolfalse
  • Nullable<InternalStruct>Nullable<T> is public, descends to InternalStructfalse

Test file

The three test cases are well-chosen:

  1. Basic internal type argument
  2. partial modifier (preserves traceability to the original report)
  3. Task<T> return path (different generated extension overload)

The compile-time-failure-as-regression-test pattern is the right call here — if the generator regresses, you find out at build time, not at runtime.

One cosmetic note: the nested classes InternalConsumer / InternalPartialConsumer could sit in a dedicated Issue5453TestTypes static class at the top to make the internal visibility unambiguous (nested classes inside a public outer class are internal-by-default in C# but readers occasionally have to stop and think about it). Not a blocking concern.

Summary

Correct fix, correct scope, good tests. No issues blocking merge.

@codacy-production
Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 2 minor

Alerts:
⚠ 2 issues (≤ 0 issues of at least minor severity)

Results:
2 new issues

Category Results
CodeStyle 2 minor

View in Codacy

🟢 Metrics 4 complexity

Metric Results
Complexity 4

View in Codacy

TIP This summary will be updated as you push new changes. Give us feedback

@thomhurst thomhurst enabled auto-merge (squash) April 8, 2026 22:22
@thomhurst thomhurst merged commit 617c4f0 into main Apr 8, 2026
14 of 15 checks passed
@thomhurst thomhurst deleted the fix/mocks-5453-generic-type-arg-accessibility branch April 8, 2026 22:49
@claude claude bot mentioned this pull request Apr 9, 2026
1 task
intellitect-bot pushed a commit to IntelliTect/EssentialCSharp.Web that referenced this pull request Apr 9, 2026
Updated [TUnit](https://github.com/thomhurst/TUnit) from 1.29.0 to
1.30.0.

<details>
<summary>Release notes</summary>

_Sourced from [TUnit's
releases](https://github.com/thomhurst/TUnit/releases)._

## 1.30.0

<!-- Release notes generated using configuration in .github/release.yml
at v1.30.0 -->

## What's Changed
### Other Changes
* perf: eliminate locks from mock invocation and verification hot paths
by @​thomhurst in thomhurst/TUnit#5422
* feat: TUnit0074 analyzer for redundant hook attributes on overrides by
@​thomhurst in thomhurst/TUnit#5459
* fix(mocks): respect generic type argument accessibility (#​5453) by
@​thomhurst in thomhurst/TUnit#5460
* fix(mocks): skip inaccessible internal accessors when mocking
Azure.Response by @​thomhurst in
thomhurst/TUnit#5461
* fix: apply CultureAttribute and STAThreadExecutorAttribute to hooks
(#​5452) by @​thomhurst in thomhurst/TUnit#5463
### Dependencies
* chore(deps): update tunit to 1.29.0 by @​thomhurst in
thomhurst/TUnit#5446
* chore(deps): update react to ^19.2.5 by @​thomhurst in
thomhurst/TUnit#5457
* chore(deps): update opentelemetry to 1.15.2 by @​thomhurst in
thomhurst/TUnit#5456
* chore(deps): update dependency qs to v6.15.1 by @​thomhurst in
thomhurst/TUnit#5458


**Full Changelog**:
thomhurst/TUnit@v1.29.0...v1.30.0

Commits viewable in [compare
view](thomhurst/TUnit@v1.29.0...v1.30.0).
</details>

[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=TUnit&package-manager=nuget&previous-version=1.29.0&new-version=1.30.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This was referenced Apr 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: CS9338 for class mock when class is not public

1 participant