Skip to content

fix(mocks): skip MockBridge for class targets with static-abstract interfaces (#5677)#5681

Merged
thomhurst merged 6 commits intomainfrom
fix/mocks-static-abstract-5677
Apr 23, 2026
Merged

fix(mocks): skip MockBridge for class targets with static-abstract interfaces (#5677)#5681
thomhurst merged 6 commits intomainfrom
fix/mocks-static-abstract-5677

Conversation

@thomhurst
Copy link
Copy Markdown
Owner

Summary

  • Stops generating _MockBridge.g.cs when the mock target is a class. Bridge shape is only required for interface targets that need a runtime dispatch hook for static-abstract members; a class already provides the concrete static implementation directly.
  • Adds a TypeKind guard in MemberDiscovery so static-abstract interface members are not collected for class targets, plus a defensive model.IsInterface check in MockGenerator.GenerateSingleTypeMock.
  • Restores the SKIPPED T15_Class_Implementing_Static_Abstract_Interface_Mockable kitchen-sink test.

Why

Previously MemberDiscovery walked a class's implemented interfaces and pulled in their static-abstract members, which set HasStaticAbstractMembers = true on the model. MockBridgeBuilder then emitted a bridge that put the class in its own interface base list (CS0527) and produced explicit interface impls without listing the interface (CS0540), making any class that implements a static-abstract interface unmockable.

Test plan

  • T15 (TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs) passes
  • StaticAbstractMemberTests (interface-target path): 13/13
  • TUnit.Mocks.Tests full suite (net10.0): 953/953
  • TUnit.Mocks.SourceGenerator.Tests (net10.0): 45/45

Closes #5677

…terface (#5677)

When the mock target is a *class* that implements an interface containing
static-abstract members, MockBridgeBuilder previously emitted a bridge
interface that listed the class in its base list (CS0527) and added
explicit interface impls without listing the interface (CS0540).

The class already provides the concrete static implementation, so no
bridge is needed: only the instance-virtual surface needs mock dispatch.

Two-layer guard:
- MemberDiscovery now skips collecting static-abstract interface members
  when the target's TypeKind is Class.
- MockGenerator only invokes MockBridgeBuilder when the target is an
  interface (defensive).

Existing static-abstract-on-interface mocks (StaticAbstractMemberTests)
remain unchanged.
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

Clean, focused fix. The root-cause analysis in the PR description is solid and the chosen approach is correct. A few observations:


Architecture: Duplicate logic in two loops

The flag is independently computed and applied in two separate loops within (~line 60 and ~line 211). Both are identical one-liners with the same rationale. If a third collection pass is ever added, it's easy to forget this guard.

A more resilient approach would be to pass this flag as a parameter to a shared helper, or compute it once at the top of the public method and thread it through:

// Compute once, pass to both collection phases
bool collectStaticAbstracts = typeSymbol.TypeKind != TypeKind.Class;
CollectInstanceMembers(..., collectStaticAbstracts);
CollectBridgeMembers(..., collectStaticAbstracts);

This removes the need to remember to duplicate the guard everywhere that walks interfaces for static-abstract members.


Redundant guard in

After the fix, HasStaticAbstractMembers will never be true for a class target, so the new && model.IsInterface condition in MockGenerator.cs is defence-in-depth that can never be triggered. That's not necessarily wrong, but future maintainers may be confused by what looks like a required gate when it's actually unreachable.

If kept, a brief comment explaining why it can never be true for classes (pointing to MemberDiscovery) would clarify intent. If removed, the contract lives entirely in MemberDiscovery, which is arguably cleaner.


Issue-number references in source comments

The code comments cite (#5677) directly:

// For class targets, the class itself provides the concrete static impl that
// satisfies any static-abstract interface members (#5677).

Per the project's code-comment principle, issue numbers belong in commit/PR descriptions — not in source — because they become meaningless without GitHub context and rot as the codebase evolves. The why (CS0527/CS0540, class vs. interface semantics) is already captured in the comment body; dropping the issue number would make the comment self-contained and timeless.


Test coverage

The T15 test is well-constructed:

  • Guards correctly with
  • Verifies the mock can intercept the instance-virtual surface ()
  • Confirms the class's concrete static impl is untouched by the mock engine
  • The cast workaround for CS8920 is explained inline — good

No complaints here.


Summary

The fix is correct and the test plan is comprehensive. The main structural suggestion is consolidating the duplicated collectStaticAbstractFromInterfaces flag into a single shared parameter rather than re-deriving it in each loop — this makes the invariant harder to accidentally break in future maintenance.

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

Clean, focused fix. The root-cause analysis in the PR description is solid and the chosen approach is correct. A few observations:


Architecture: Duplicate logic in two MemberDiscovery loops

The collectStaticAbstractFromInterfaces flag is independently computed and applied in two separate loops within MemberDiscovery.cs (~line 60 and ~line 211). Both are identical one-liners with the same rationale. If a third collection pass is ever added, it is easy to forget this guard.

A more resilient approach would be to pass this flag as a parameter to a shared helper, or compute it once at the top of the public method and thread it through:

// Compute once, pass to both collection phases
bool collectStaticAbstracts = typeSymbol.TypeKind != TypeKind.Class;
CollectInstanceMembers(..., collectStaticAbstracts);
CollectBridgeMembers(..., collectStaticAbstracts);

This removes the need to remember to duplicate the guard everywhere that walks interfaces for static-abstract members.


Redundant guard in MockGenerator.cs

After the MemberDiscovery fix, HasStaticAbstractMembers will never be true for a class target, so the new && model.IsInterface condition in MockGenerator.cs is defence-in-depth that can never be triggered. That is not necessarily wrong, but future maintainers may be confused by what looks like a required gate when it is actually unreachable.

If kept, a brief comment explaining why it can never be true for classes (pointing to MemberDiscovery) would clarify intent. If removed, the contract lives entirely in MemberDiscovery, which is arguably cleaner.


Issue-number references in source comments

The code comments cite (#5677) directly in the source. Issue numbers belong in commit/PR descriptions rather than source code, because they become meaningless without GitHub context and rot as the codebase evolves. The why (CS0527/CS0540, class vs. interface semantics) is already captured in the comment body; dropping the issue number would make the comment self-contained and timeless.


Test coverage

The T15 test is well-constructed:

  • Guards correctly with #if NET8_0_OR_GREATER
  • Verifies the mock can intercept the instance-virtual surface (InstanceValue.Returns(42))
  • Confirms the class concrete static impl is untouched by the mock engine
  • The object? cast workaround for CS8920 is explained inline

No complaints here.


Summary

The fix is correct and the test plan is comprehensive. The main structural suggestion is consolidating the duplicated collectStaticAbstractFromInterfaces flag into a single shared parameter rather than re-deriving it in each loop — this makes the invariant harder to accidentally break in future maintenance.

@codacy-production
Copy link
Copy Markdown

codacy-production Bot commented Apr 23, 2026

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 0 complexity

Metric Results
Complexity 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

- Extract `ShouldCollectStaticAbstractFromInterfaces` helper so both
  `DiscoverMembers` and `DiscoverMembersFromMultipleTypes` (and any future
  collection loop) honor the same TypeKind guard.
- Tighten `MockGenerator.GenerateSingleTypeMock` comment to call out the
  defence-in-depth role of the `model.IsInterface` check.
- Strip issue-number references from source comments (kept the WHY:
  CS0527 / CS0540 / class vs. interface semantics).
@thomhurst
Copy link
Copy Markdown
Owner Author

Applied review feedback in 46f5894:

  1. Consolidated the static-abstract guard. Extracted ShouldCollectStaticAbstractFromInterfaces(ITypeSymbol) in MemberDiscovery.cs. Both DiscoverMembers and DiscoverMembersFromMultipleTypes now route through the same helper, and its doc comment spells out that any new collection loop MUST gate static-abstract collection through it.
  2. Trimmed the MockGenerator.GenerateSingleTypeMock comment to a one-liner pointing at the MemberDiscovery contract: // Defence-in-depth: MemberDiscovery already filters static-abstract collection on TypeKind, so HasStaticAbstractMembers is always false here for class targets. Kept the && model.IsInterface guard.
  3. Stripped (#5677) issue references from source comments in MemberDiscovery.cs and MockGenerator.cs. The WHY (CS0527 / CS0540 / class-vs-interface semantics) is preserved on the new helper's doc comment.

Tests (net10.0):

  • KitchenSinkEdgeCasesTests/T15_* -> 1/1 pass
  • StaticAbstractMemberTests/* -> 13/13 pass
  • TUnit.Mocks.Tests (full) -> 953/953 pass
  • TUnit.Mocks.SourceGenerator.Tests (full) -> 45/45 pass

No snapshot diffs produced; nothing to commit beyond the source edits.

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 (follow-up)

Two previous reviews were already submitted by claude[bot] at the same timestamp — this appears to be a duplicate-submission artifact. Let me provide a clean assessment and correct one factual error from those reviews.


Previous review: correction on issue-number references

The earlier reviews flagged (#5677) appearing in source code comments. This is incorrect. Looking at the actual diff:

  • MockGenerator.cs: // Defence-in-depth: MemberDiscovery already filters static-abstract collection on TypeKind, so HasStaticAbstractMembers is always false here for class targets. — no issue reference
  • MemberDiscovery.cs XML doc: references CS0527/CS0540 by name, no issue number
  • Test file comments: no issue numbers

The #5677 reference appears only in the PR title/description, which is exactly where it belongs. No action needed.


Open concern: duplicate flag computation (still valid)

The collectStaticAbstractFromInterfaces flag is independently computed in two separate loops inside MemberDiscovery.cs:

// ~line 44 (first collection phase)
var collectStaticAbstractFromInterfaces = ShouldCollectStaticAbstractFromInterfaces(typeSymbol);

// ~line 193 (second collection phase — separate scope)
var collectStaticAbstractFromInterfaces = ShouldCollectStaticAbstractFromInterfaces(typeSymbol);

The XML doc on ShouldCollectStaticAbstractFromInterfaces mitigates this with a "MUST gate through this helper" directive — which is documentation-based enforcement. That is better than nothing, but still fragile. A third loop added under deadline pressure could easily miss it.

The better approach: if both loops are in the same public entry-point method, compute once and thread the value through. If they are in separate methods with the same typeSymbol parameter, make collectStaticAbstractFromInterfaces part of a small context struct or compute it once in the caller:

var ctx = new MemberCollectionContext(typeSymbol)
{
    CollectStaticAbstracts = ShouldCollectStaticAbstractFromInterfaces(typeSymbol)
};
// pass ctx to both phases

This makes it structurally impossible to forget the guard rather than relying on a doc comment.


Redundant guard in MockGenerator.cs — already well-handled

The && model.IsInterface defence-in-depth guard comes with a clear comment explaining it is unreachable for class targets. The intent is legible. This is acceptable as written.


Test coverage

T15 is well-structured: proper #if NET8_0_OR_GREATER guard, exercises mock interception on the instance surface, confirms the static implementation is not hijacked, and the object? cast for CS8920 is documented inline. No issues.


Overall

The root cause is correctly diagnosed and the fix is sound. The structural concern about the duplicate flag computation remains open from the previous reviews — the XML doc comment on ShouldCollectStaticAbstractFromInterfaces partially addresses it but structural enforcement via a shared parameter would be more robust. The rest of the implementation is clean.

… helper

Both DiscoverMembers and DiscoverMembersFromMultipleTypes used to hoist
ShouldCollectStaticAbstractFromInterfaces(typeSymbol) into a local boolean
and then check it inline at each static-member branch. This relied on
documentation/discipline -- a future third loop could silently skip the
gate and reintroduce the #5677 regression.

Funnel both call sites through TryCollectStaticAbstractFromInterface,
which derives the flag from the typeSymbol it was handed. The only way
to collect a static-abstract interface member is now through this helper,
so the bypass is structurally impossible.
@thomhurst
Copy link
Copy Markdown
Owner Author

Round 2 review applied in 8efe4cf.

Item 1 (structural enforcement): Funnelled both DiscoverMembers and DiscoverMembersFromMultipleTypes static-member branches through a single new private helper TryCollectStaticAbstractFromInterface(member, typeSymbol, interfaceFqn, ...). The helper derives the static-abstract collection flag from the typeSymbol argument internally (calling ShouldCollectStaticAbstractFromInterfaces). The two per-loop var collectStaticAbstractFromInterfaces = ... locals are removed, so the only code path that reaches CollectStaticAbstractMember now goes through the gated helper. A future third loop physically cannot skip the gate without re-implementing the helper.

Chose this shape over a MemberCollectionContext struct because the call sites already had typeSymbol in scope and only needed one piece of derived state — a 12-line helper makes the bypass impossible with strictly less surface area than a context type.

Item 2 (defence-in-depth && model.IsInterface): Left as-is per follow-up review.

Tests (net10.0):

  • T15 KitchenSinkEdgeCasesTests: 1/1 pass
  • StaticAbstractMemberTests: 13/13 pass
  • TUnit.Mocks.Tests: 953/953 pass
  • TUnit.Mocks.SourceGenerator.Tests: 45/45 pass (no snapshot diffs)

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: fix(mocks): skip MockBridge for class targets with static-abstract interfaces

Overall: The fix is correct and the iterative refactoring toward structural enforcement via TryCollectStaticAbstractFromInterface is the right direction. The PR is in good shape. The observations below range from logic precision to missing test coverage.


Finding 1 — ShouldCollectStaticAbstractFromInterfaces guard is too permissive

File: TUnit.Mocks.SourceGenerator/Discovery/MemberDiscovery.cs

// Current:
private static bool ShouldCollectStaticAbstractFromInterfaces(ITypeSymbol typeSymbol)
    => typeSymbol.TypeKind != TypeKind.Class;

The "collect unless class" guard would also pass TypeKind.Struct, TypeKind.Enum, TypeKind.Delegate, etc. There's no current execution path that triggers those, but if a third kind is ever accepted (e.g., a record struct mock) the guard silently allows collection for it. The adjacent branching logic in DiscoverMembers uses == TypeKind.Interface — the same pattern should apply here:

private static bool ShouldCollectStaticAbstractFromInterfaces(ITypeSymbol typeSymbol)
    => typeSymbol.TypeKind == TypeKind.Interface;

Why this is better: It encodes the intended semantic (bridge only applies to interface targets), matches the existing pattern in the codebase, and is robust against future kinds being introduced upstream.


Finding 2 — Missing snapshot test for the new code path (most important gap)

File: TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs

The PR adds a runtime test (T15_Class_Implementing_Static_Abstract_Interface_Mockable) that verifies the mock compiles and runs. However, there's no snapshot generator test for a class that implements a static-abstract interface.

The three existing Interface_With_Static_Abstract_Members snapshot tests only cover the interface-target path. The fixed class-target path has no snapshot to verify:

  1. That no _MockBridge.g.cs is emitted for the class target.
  2. That the class's own instance-virtual surface is still correctly mocked.

Without this, a future generator refactor could re-enable bridge generation for class targets and only the runtime test (in a later CI stage) would catch it. A snapshot test would lock in the "no bridge" invariant at the generator level.

Suggested test: Class_Implementing_Static_Abstract_Interface — mirror the T15 types in a source-gen snapshot test, and assert the generated output does not include a MockBridge type.


Finding 3 — Cleanup commit missed a #5677 reference in the doc-comment

File: TUnit.Mocks.SourceGenerator/Discovery/MemberDiscovery.cs (the TryCollectStaticAbstractFromInterface XML doc)

The third commit message explicitly states "Strip issue-number references from source comments." However, the TryCollectStaticAbstractFromInterface doc-comment still contains #5677. The cleanup was incomplete — worth removing for consistency.


Finding 4 — Oversized inline comment in MockGenerator.cs

File: TUnit.Mocks.SourceGenerator/MockGenerator.cs:93

The comment explaining the defence-in-depth check is ~165 characters on a single line. The codebase convention is short single-line // comments. For an invariant explanation this important, a multi-line block comment above the if is more readable:

// Defence-in-depth: MemberDiscovery never sets HasStaticAbstractMembers for class targets
// (TypeKind guard in TryCollectStaticAbstractFromInterface), so this branch is only entered
// for interface targets. The IsInterface check makes the intent explicit.
if (model.HasStaticAbstractMembers && model.IsInterface)

Finding 5 — DiscoverMembersFromMultipleTypes doc-comment misleads about valid input

File: TUnit.Mocks.SourceGenerator/Discovery/MemberDiscovery.cs (~line 166)

The XML doc says "Used for multi-interface mocks like Mock.Of<T1, T2>()" — implying T1 is always an interface. But MockTypeDiscovery.TransformToModels can pass a class as the first element (for isPartialMock == true). TryCollectStaticAbstractFromInterface handles this correctly, but a future reader might assume the TypeKind guard is redundant. A one-line doc addition clarifying that the first element can be a class prevents that misunderstanding.


Finding 6 — ShouldCollectStaticAbstractFromInterfaces as a single-use method may invite misuse (minor style)

File: TUnit.Mocks.SourceGenerator/Discovery/MemberDiscovery.cs

The helper has exactly one caller and its doc-comment explicitly warns it should only be consumed by TryCollectStaticAbstractFromInterface. A named single-use private helper that warns against other callers can paradoxically attract wrong usage later. Inlining it as a named condition with the same comment inside TryCollectStaticAbstractFromInterface removes the ambiguity. That said, if the team prefers the named-helper style for readability, it's fine to keep — this is a style observation, not a correctness issue.


Summary

# Severity Description
1 Medium Guard should be == TypeKind.Interface, not != TypeKind.Class
2 Medium No snapshot generator test for the class-target (fixed) path
3 Low Stray #5677 reference in doc-comment after the cleanup commit
4 Low Oversized inline comment; prefer multi-line block above the if
5 Low DiscoverMembersFromMultipleTypes doc misleads about valid input types
6 Nit ShouldCollectStaticAbstractFromInterfaces could be inlined

Findings 1 and 2 are the most actionable before merge. The rest are polish.

- ShouldCollectStaticAbstractFromInterfaces now uses TypeKind == Interface
  rather than != Class, so structs/enums/delegates can never satisfy the
  guard either. Robust against future TypeKinds.
- Adds Class_Implementing_Static_Abstract_Interface snapshot mirroring T15:
  a class implementing an interface with a static-abstract member plus an
  instance virtual member. Verified output contains zero MockBridge types.
- Removes the lingering #5677 reference from TryCollectStaticAbstractFromInterface
  (keeps the CS0527/CS0540 rationale).
- Reformats the long single-line invariant comment in MockGenerator into a
  3-line block above the IsInterface gate.
- Clarifies DiscoverMembersFromMultipleTypes XML doc that the first element
  may be a class (via MockTypeDiscovery.TransformToModels with isPartialMock),
  so the TypeKind guard inside the static-collection helper is genuinely
  required and not redundant.
@thomhurst
Copy link
Copy Markdown
Owner Author

3rd-round review feedback applied in 5b24c60.

Per item:

  1. (M) Guard tightenedShouldCollectStaticAbstractFromInterfaces now uses typeSymbol.TypeKind == TypeKind.Interface (was != TypeKind.Class). Encodes intent and stays robust against future TypeKinds (struct/enum/delegate would have slipped past the old check).
  2. (M) Snapshot addedClass_Implementing_Static_Abstract_Interface mirrors the T15 type shape. The new Snapshots/Class_Implementing_Static_Abstract_Interface.verified.txt contains zero MockBridge occurrences (confirmed via grep -c MockBridge0).
  3. (L) Stray #5677 — removed from the TryCollectStaticAbstractFromInterface XML doc. Kept the CS0527/CS0540 + class-vs-interface rationale.
  4. (L) Oversized commentMockGenerator.cs invariant comment reformatted to the 3-line block recommended in the review.
  5. (L) Misleading XML docDiscoverMembersFromMultipleTypes summary now notes that the first element may be a class (via MockTypeDiscovery.TransformToModels with isPartialMock == true), so the TypeKind guard in TryCollectStaticAbstractFromInterface is genuinely required.
  6. Skipped per reviewer.

Test status (all net10.0):

  • T15 (KitchenSinkEdgeCasesTests.T15_Class_Implementing_Static_Abstract_Interface_Mockable): 1/1 passed.
  • StaticAbstractMemberTests: 13/13 passed.
  • TUnit.Mocks.Tests: 953/953 passed.
  • TUnit.Mocks.SourceGenerator.Tests (MockGeneratorTests/*): 46/46 passed (including the new snapshot).

No *.received.txt files committed.

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: fix(mocks): skip MockBridge for class targets with static-abstract interfaces

The fix is correct, well-structured, and the iterative refinement across the commits has landed in a good state. The structural guard via TryCollectStaticAbstractFromInterface is the right approach — it makes it impossible to reintroduce the regression without going through the gated helper. Below are remaining observations, roughly ordered by actionability.


1. seenFullMethods is not threaded through CollectStaticAbstractMember (pre-existing gap, now visible)

DiscoverMembers maintains two deduplication sets: seenMethods (keyed on name + parameter types, ignoring return type) and seenFullMethods (keyed on name + params + return type). For ordinary instance members, both are used to detect the return-type-differing overload case (e.g. IEnumerable<T>.GetEnumerator vs IEnumerable.GetEnumerator) and emit the explicit interface impl variant.

CollectStaticAbstractMember — and therefore TryCollectStaticAbstractFromInterface — only uses seenMethods. If a static-abstract interface hierarchy ever has two methods with the same name/params but different return types, the second one is silently dropped. In practice this is exotic for static-abstract members, but it represents an inconsistency between the two deduplication paths. This PR didn't introduce it, but now that this code is being touched, cleaning it up would keep the two paths aligned.


2. #5677 reference remains in a test comment (stale after the cleanup commit)

The commit message says "Strip issue-number references from source comments." That was done in production source, but MockGeneratorTests.cs still contains a // ... see #5677 reference. The compiler-error codes (CS0527, CS0540) are self-contained and timeless; the issue reference is not. Worth removing for consistency.


3. The snapshot test implicitly encodes "no bridge" but doesn't assert it explicitly

Class_Implementing_Static_Abstract_Interface.verified.txt correctly omits any bridge interface type — confirming the fix. The assertion is implicit: if bridge generation is accidentally re-enabled, the snapshot diff will catch it via a new ===== FILE SEPARATOR ===== section. That is acceptable.

A brief comment in the test explaining why the output contains exactly N file sections (no bridge = one fewer than the interface-with-static-abstract snapshot) would make future debugging faster and turn the implicit assertion into explicit intent.


4. TryCollectStaticAbstractFromInterface has 11 parameters — symptom of a missing MemberCollectionContext

private static void TryCollectStaticAbstractFromInterface(
    ISymbol member,
    ITypeSymbol typeSymbol,
    string interfaceFqn,
    List<MockMemberModel> methods,
    List<MockMemberModel> properties,
    List<MockEventModel> events,
    Dictionary<string, (int Index, ITypeSymbol? ReturnType)> seenMethods,
    Dictionary<string, int?> seenProperties,
    HashSet<string> seenEvents,
    ref int memberIdCounter)

The same 7-parameter "collection context" tuple (methods, properties, events, seenMethods, seenProperties, seenEvents, memberIdCounter) is threaded through CollectStaticAbstractMember, CollectInstanceMembers, ProcessClassMembers, and MergePropertyAccessors throughout the file. Introducing a MemberCollectionContext ref struct would reduce each call site from 7 positional arguments to one, eliminating the risk of accidentally swapping similarly-typed arguments (e.g. the two Dictionary<...> parameters).

This is clearly out of scope for a targeted bug fix — noting it as a future improvement rather than a blocker.


5. ShouldCollectStaticAbstractFromInterfaces is a one-line predicate with a single caller

The predicate centralises the TypeKind.Interface check with a named concept and a doc comment explaining the CS0527/CS0540 rationale. The doc comment on TryCollectStaticAbstractFromInterface already carries that full explanation. The extra indirection adds a little navigation overhead without adding information not already present at the call site.

Options:

  • Inline it with the same comment, removing one level of indirection.
  • Keep it as-is if the team prefers named predicates for readability (perfectly valid).

Either is fine. The implementation is correct either way.


Summary

# Severity Actionable now? Description
1 Low Optional seenFullMethods not threaded to CollectStaticAbstractMember — pre-existing inconsistency now visible
2 Low Yes Stale #5677 reference in test comment after cleanup commit
3 Low Optional Snapshot test comment could note expected file-section count and why
4 Nit Future 11-parameter helper is a symptom of missing MemberCollectionContext
5 Nit Optional ShouldCollectStaticAbstractFromInterfaces could be inlined

Item 2 is the only one worth addressing before merge. Everything else is polish or future work. The core fix is sound and the gating via TryCollectStaticAbstractFromInterface is the right architectural decision.

Doc-only follow-up to review feedback on PR #5681:
- Remove `#5677` from the `Class_Implementing_Static_Abstract_Interface`
  test comment; compiler-error codes (CS0527, CS0540) are timeless, the
  issue reference is not.
- Make the snapshot's "no bridge" assertion explicit: add a one-line note
  that the verified output intentionally omits `_MockBridge.g.cs` for
  class targets, turning the implicit file-count assertion into stated
  intent for future debugging.

No logic changes.
@thomhurst
Copy link
Copy Markdown
Owner Author

Final-round review feedback addressed in 8cdd519 (doc-only follow-up to 5b24c60):

Applied (both items 2 and 3):

  • Removed the stray #5677 reference from the Class_Implementing_Static_Abstract_Interface test comment in MockGeneratorTests.cs — the WHY (CS0527 / CS0540 explanation) is preserved; only the now-redundant issue link was stripped.
  • Added an explicit one-line note that the verified snapshot for that test intentionally OMITS a _MockBridge.g.cs file section, so the implicit "no bridge" assertion is now stated intent for future debuggers.

Deferred (per reviewer guidance):

  • Item 1 (seenFullMethods not threaded to CollectStaticAbstractMember) — pre-existing inconsistency, not introduced by this PR.
  • Item 4 (MemberCollectionContext ref struct refactor) — out of scope for a targeted bug fix.
  • Item 5 (inline ShouldCollectStaticAbstractFromInterfaces) — reviewer marked either form acceptable; current shape kept for the structural-guard intent already documented in the helper's XML.

Tests (net10.0, all green):

  • T15 (KitchenSinkEdgeCasesTests): 1/1
  • StaticAbstractMemberTests (TUnit.Mocks.Tests): 13/13
  • TUnit.Mocks.Tests full suite: 953/953
  • TUnit.Mocks.SourceGenerator.Tests full suite: 46/46 (incl. Class_Implementing_Static_Abstract_Interface snapshot)
  • No new .received.txt.

Codacy: ran dotnet format --verify-no-changes on the changed projects. The whitespace/imports findings that surfaced are all in unchanged code (pre-existing in MemberDiscovery.cs lines ~1159-1181 and KitchenSinkEdgeCasesTests.cs lines 358-360 / 446) — none touch lines this PR added or modified, so no in-diff format fixes to apply.

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 (follow-up): previous issues addressed

This is a status check against the concerns raised in the earlier claude[bot] reviews. The PR is now in good shape.


Previously raised — now resolved

1. Duplicate logic in two collection loops
Both DiscoverMembers (~line 56) and DiscoverMembersFromMultipleTypes (~line 202) now funnel through TryCollectStaticAbstractFromInterface, so the guard is enforced in exactly one place. No future loop can silently bypass it. ✅

2. Guard too permissive (!= TypeKind.Class vs == TypeKind.Interface)
ShouldCollectStaticAbstractFromInterfaces now uses:

=> typeSymbol.TypeKind == TypeKind.Interface;

This correctly rejects structs, enums, delegates, etc. ✅


Still open (pre-existing gap, not introduced by this PR)

seenFullMethods is not passed to CollectStaticAbstractMember
The caller (TryCollectStaticAbstractFromInterface) receives seenMethods (name + param key) but not seenFullMethods (name + params + return type). This means if a static-abstract interface ever declares methods that differ only by return type, deduplication would miss the collision and silently emit both. In practice this is extremely unlikely for static-abstract members (they can't do return-type covariance the same way instance members can), but it is an inconsistency. No action needed for this PR — just worth tracking as a follow-up cleanup.


Observation: snapshot correctly omits bridge file

The verified snapshot for Class_Implementing_Static_Abstract_Interface contains only three sections (ImplFactory, Members, StaticExtension) and intentionally has no _MockBridge.g.cs section. The comment in the test method explaining this absence is clear and valuable for future readers. Good call.


Overall: The previous review concerns have been addressed. The fix is correct, the guard is tight, and the test coverage directly exercises the regression case. Ready to merge.

…ract-5677

# Conflicts:
#	TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs
@thomhurst
Copy link
Copy Markdown
Owner Author

Resolved merge conflicts with origin/main after sister PRs #5680 (T8 SelfEquatable) and #5683 (T14 indexer / T14b in-indexer) merged.

Conflict scope: Two regions in TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs — the type-declaration block and the test-method block. Both sides were purely additive:

  • main: T14 (IHasIndexer, IHasInIndexer) types + 3 test methods, plus a "T15 SKIPPED" comment.
  • this branch: T15 (IStaticAbstractFactory, StaticAbstractImpl) types + test, plus a "T14 SKIPPED" comment.

Resolution: kept all real types and tests from both sides (T14, T14b, T15) and dropped both "SKIPPED" placeholder comments since each side now has a working implementation. No type or method names collide.

Verification (net10.0):

  • TUnit.Mocks.SourceGenerator.Tests: 47/47 pass.
  • TUnit.Mocks.Tests full suite: 957/957 pass.
  • KitchenSinkEdgeCasesTests filtered: 20/20 pass — confirmed T14_*, T14b_*, and T15_Class_Implementing_Static_Abstract_Interface_Mockable all green.

No .received.txt snapshot files produced. Merge commit: e39e5e03d.

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 (Final)

All concerns raised in the previous reviews have been addressed across the iterative commits. Here is a clean summary of the current state.


Previous issues — all resolved

Issue Resolution
Duplicate guard in two collection loops Both now funnel through — the guard is enforced in one place
Guard was (too permissive) Now correctly , rejecting structs, enums, delegates, etc.
Redundant in MockGenerator Kept as defence-in-depth with a clear comment explaining it is unreachable for class targets — intent is legible
DiscoverMembersFromMultipleTypes doc misleading about class inputs Doc-comment updated to note the first element can be a class
Stray #5677 reference in test comment Stripped in the doc-only cleanup commit
Snapshot test missing for class-target path Class_Implementing_Static_Abstract_Interface.verified.txt added; absence of a bridge section is the assertion

Merge-from-main commit (e39e5e03d5d0)

The only substantive change to the mocks source in the merge commit is in MemberDiscovery.cs:

-  Direction = ParameterDirection.In
+  Direction = p.GetParameterDirection()

This is unrelated to the static-abstract fix — it is the T14b in-parameter indexer correction from main. It is correct and does not interact with the TypeKind guard.


Remaining pre-existing gap (non-blocking)

seenFullMethods is not threaded through CollectStaticAbstractMember, meaning if a static-abstract interface ever declares two methods with the same name/params but differing return types, the second would be silently dropped. This is an extremely unlikely scenario for static-abstract members and was not introduced by this PR. Worth tracking as future cleanup.


Overall

The fix is correct, the guard is tight (== TypeKind.Interface), the test coverage directly exercises the regression case (T15 restored, snapshot locks the no-bridge invariant), and all previous review suggestions have been incorporated. The PR is ready to merge.

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 (Final)

All concerns raised in the previous claude[bot] reviews have been addressed across the iterative commits. Here is a clean summary of the current state.


Previous issues — all resolved

Issue Resolution
Duplicate guard in two collection loops Both now funnel through TryCollectStaticAbstractFromInterface — the guard is enforced in one place
Guard was != TypeKind.Class (too permissive) Now correctly == TypeKind.Interface, rejecting structs, enums, delegates, etc.
Redundant && model.IsInterface in MockGenerator Kept as defence-in-depth with a clear comment explaining it is unreachable for class targets — intent is legible
DiscoverMembersFromMultipleTypes doc misleading about class inputs Doc-comment updated to note the first element can be a class
Stray #5677 reference in test comment Stripped in the doc-only cleanup commit
Snapshot test missing for class-target path Class_Implementing_Static_Abstract_Interface.verified.txt added; absence of a bridge section is the assertion

Merge-from-main commit (e39e5e0)

The only substantive change to the mocks source in the merge commit is in MemberDiscovery.cs:

-  Direction = ParameterDirection.In
+  Direction = p.GetParameterDirection()

This is unrelated to the static-abstract fix — it is the T14b in-parameter indexer correction from main. It is correct and does not interact with the TypeKind guard.


Remaining pre-existing gap (non-blocking)

seenFullMethods is not threaded through CollectStaticAbstractMember, meaning if a static-abstract interface ever declares two methods with the same name/params but differing return types, the second would be silently dropped. This is an extremely unlikely scenario for static-abstract members and was not introduced by this PR. Worth tracking as future cleanup.


Overall

The fix is correct, the guard is tight (== TypeKind.Interface), the test coverage directly exercises the regression case (T15 restored, snapshot locks the no-bridge invariant), and all previous review suggestions have been incorporated. The PR is ready to merge.

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] TUnit.Mocks: mocking a class that implements a static-abstract interface emits invalid MockBridge

1 participant