Conversation
…ck generator (#5559) The mock source generator failed to compile when mocking interfaces that inherit from IEnumerable<T> or have same-named members with different return types across multiple interfaces. Three bugs fixed: 1. Method/property deduplication used signature keys without return types, dropping explicit interface implementations like IEnumerable.GetEnumerator() when IEnumerable<T>.GetEnumerator() was already seen. Added two-level dedup: signature keys for primary collision detection, full keys (with return type) to distinguish genuinely different methods needing explicit impl. 2. Explicit interface method implementations blindly delegated to the public method, failing when return types are incompatible (e.g. string vs int). Added ExplicitInterfaceCanDelegate flag using a TypeKind heuristic — interface return types delegate safely (IEnumerator<T> : IEnumerator), class/struct mismatches get their own engine dispatch. 3. Sealed overrides in class hierarchies were skipped but not registered in seen sets, allowing base virtuals to leak through and generate invalid override on sealed members.
|
Azure Pipelines: 1 pipeline(s) were filtered out due to trigger conditions. |
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| BestPractice | 4 medium 2 minor |
| CodeStyle | 8 minor |
🟢 Metrics 155 complexity
Metric Results Complexity 155
TIP This summary will be updated as you push new changes. Give us feedback
Code ReviewThe fix is well-reasoned and the test coverage is excellent — 888 behavioral tests across interfaces, abstract classes, concrete classes, and multi-level inheritance, plus 3 snapshot tests for the IEnumerable variants. The two-level deduplication design is sound for the target use case. Issue 1:
|
Replace the TypeKind heuristic in IsReturnTypeConvertible with a proper check using Roslyn's AllInterfaces. The heuristic returned true for any interface return type, which would generate broken delegation code for unrelated interfaces sharing a method name (e.g. IAlpha.Method():IFoo vs IBeta.Method():IBar). Now stores the ITypeSymbol alongside the method index in seenMethods to enable CanDelegateReturnType to verify the public return type actually implements the explicit interface's return type. Also fixes misleading comment on explicit interface property generation that said "delegates" when it dispatches independently.
|
Azure Pipelines: 1 pipeline(s) were filtered out due to trigger conditions. |
There was a problem hiding this comment.
Follow-up Review
All four issues flagged in the previous review have been addressed in the second commit. Here's the audit:
Issues from Previous Review — All Resolved ✅
Issue 1 — IsReturnTypeConvertible heuristic: Fixed. Replaced with CanDelegateReturnType(ITypeSymbol publicReturnType, ITypeSymbol explicitReturnType) using proper Roslyn AllInterfaces traversal. The fix correctly handles the IAlpha/IBeta scenario (unrelated interfaces sharing a method name with different return types) that would have produced CS0266 compile errors in generated code.
Issue 2 — Dead existingIdx variable: Fixed. The seenMethods dictionary now stores (int Index, ITypeSymbol? ReturnType) and existing.ReturnType is actively passed to CanDelegateReturnType, making the delegation decision semantically correct.
Issue 3 — Misleading comment on GenerateInterfaceProperty: Fixed. The comment now reads "Dispatches independently through the engine with a dedicated MemberId", which accurately describes the behavior.
Sealed-override registration: The NonMockableEntry = (-1, null) sentinel for seenMethods is a clean way to block base virtuals without allocating a dedicated data structure. Good.
Remaining Minor Points (non-blocking)
Duplicated collision-detection logic — the same two-level dedup block (TryGetValue → seenFullMethods.Add → CanDelegateReturnType → methods.Add) appears in both DiscoverMembers and DiscoverMembersFromMultipleTypes, and the property collision check similarly. Any future evolution to CanDelegateReturnType would need to be applied in both places. An AddInterfaceMethod helper that owns the entire collision policy would prevent drift, though this is a refactor, not a correctness issue.
CollectStaticAbstractMember doesn't receive seenFullMethods — the overloaded-static-abstract edge case (same name+params, different return type across two static-abstract interfaces) would fall through to the old single-level dedup. Extremely unlikely in practice, but worth a // TODO: extend seenFullMethods to CollectStaticAbstractMember if static covariant return types become relevant comment if you want to leave a breadcrumb.
Overall
The fix is correct and safe. The critical correctness hazard (heuristic delegation emitting uncompilable code for unrelated interface pairs) has been eliminated. Test coverage is thorough — 8 IEnumerable behavioral tests, 3 snapshot tests, and 112 kitchen-sink tests across interfaces/abstract/concrete/inheritance. Good to merge.
There was a problem hiding this comment.
Follow-up Review
All four issues flagged in the previous review have been addressed in the second commit. Here is the audit:
Issues from Previous Review — All Resolved
Issue 1 — IsReturnTypeConvertible heuristic: Fixed. Replaced with CanDelegateReturnType(ITypeSymbol publicReturnType, ITypeSymbol explicitReturnType) using proper Roslyn AllInterfaces traversal. The fix correctly handles the IAlpha/IBeta scenario (unrelated interfaces sharing a method name with different return types) that would have produced CS0266 compile errors in generated code.
Issue 2 — Dead existingIdx variable: Fixed. The seenMethods dictionary now stores (int Index, ITypeSymbol? ReturnType) and existing.ReturnType is actively passed to CanDelegateReturnType, making the delegation decision semantically correct.
Issue 3 — Misleading comment on GenerateInterfaceProperty: Fixed. The comment now reads "Dispatches independently through the engine with a dedicated MemberId", which accurately describes the behavior.
Sealed-override registration: The NonMockableEntry = (-1, null) sentinel for seenMethods is a clean way to block base virtuals without allocating a dedicated data structure. Good call.
Remaining Minor Points (non-blocking)
Duplicated collision-detection logic — the same two-level dedup block (TryGetValue + seenFullMethods.Add + CanDelegateReturnType + methods.Add) appears in both DiscoverMembers and DiscoverMembersFromMultipleTypes, and the property collision check is similarly duplicated. Any future evolution to CanDelegateReturnType would need to be applied in both places. An AddInterfaceMethod helper that owns the entire collision policy would prevent drift. Non-blocking refactor.
CollectStaticAbstractMember does not receive seenFullMethods — the overloaded-static-abstract edge case (same name+params, different return type across two static-abstract interfaces) would fall through to single-level dedup. Extremely unlikely in practice, but worth a comment as a breadcrumb.
Overall
The fix is correct and safe. The critical correctness hazard — the heuristic delegation that would emit uncompilable code for unrelated interface pairs — has been eliminated. Test coverage is thorough: 8 IEnumerable behavioral tests, 3 snapshot tests, and 112 kitchen-sink tests across interfaces/abstract/concrete/inheritance hierarchies. Good to merge.
Updated [TUnit](https://github.com/thomhurst/TUnit) from 1.33.0 to 1.35.2. <details> <summary>Release notes</summary> _Sourced from [TUnit's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.35.2 <!-- Release notes generated using configuration in .github/release.yml at v1.35.2 --> ## What's Changed ### Other Changes * fix: restore SourceLink and deterministic builds in published packages by @thomhurst in thomhurst/TUnit#5579 ### Dependencies * chore(deps): update tunit to 1.35.0 by @thomhurst in thomhurst/TUnit#5578 **Full Changelog**: thomhurst/TUnit@v1.35.0...v1.35.2 ## 1.35.0 <!-- Release notes generated using configuration in .github/release.yml at v1.35.0 --> ## What's Changed ### Other Changes * fix: support open generic transitive auto-mocks by @thomhurst in thomhurst/TUnit#5568 * refactor: separate test and lifecycle tracing by @thomhurst in thomhurst/TUnit#5572 * fix: expand nested And/Or expectations in failure messages (#5573) by @thomhurst in thomhurst/TUnit#5577 ### Dependencies * chore(deps): update tunit to 1.34.5 by @thomhurst in thomhurst/TUnit#5566 * chore(deps): bump follow-redirects from 1.15.11 to 1.16.0 in /docs by @dependabot[bot] in thomhurst/TUnit#5538 * chore(deps): update verify to 31.16.0 by @thomhurst in thomhurst/TUnit#5570 * chore(deps): update verify to 31.16.1 by @thomhurst in thomhurst/TUnit#5574 * chore(deps): update gittools/actions action to v4 by @thomhurst in thomhurst/TUnit#5575 **Full Changelog**: thomhurst/TUnit@v1.34.5...v1.35.0 ## 1.34.5 <!-- Release notes generated using configuration in .github/release.yml at v1.34.5 --> ## What's Changed ### Other Changes * fix: cap test output at 1M chars to prevent OOM by @thomhurst in thomhurst/TUnit#5561 * fix: handle explicit interface impl with different return types in mock generator by @thomhurst in thomhurst/TUnit#5564 * fix: include XML documentation files in NuGet packages by @thomhurst in thomhurst/TUnit#5565 ### Dependencies * chore(deps): update tunit to 1.34.0 by @thomhurst in thomhurst/TUnit#5562 **Full Changelog**: thomhurst/TUnit@v1.34.0...v1.34.5 ## 1.34.0 <!-- Release notes generated using configuration in .github/release.yml at v1.34.0 --> ## What's Changed ### Other Changes * refactor: move CorrelatedTUnitLogger to TUnit.Logging.Microsoft and auto-inject handlers by @thomhurst in thomhurst/TUnit#5532 * feat: add Dev Drive setup for Windows in CI workflow by @thomhurst in thomhurst/TUnit#5544 * fix: start session activity before discovery so discovery spans parent correctly by @thomhurst in thomhurst/TUnit#5534 * feat: cross-process test log correlation via OTLP receiver by @thomhurst in thomhurst/TUnit#5533 * refactor: use natural OTEL trace propagation instead of synthetic TraceIds by @thomhurst in thomhurst/TUnit#5557 * fix: route ITestOutput writes through synchronized ConcurrentStringWriter by @thomhurst in thomhurst/TUnit#5558 ### Dependencies * chore(deps): update tunit to 1.33.0 by @thomhurst in thomhurst/TUnit#5527 * chore(deps): update dependency dompurify to v3.4.0 by @thomhurst in thomhurst/TUnit#5537 * chore(deps): update dependency docusaurus-plugin-llms to ^0.3.1 by @thomhurst in thomhurst/TUnit#5541 * chore(deps): update dependency microsoft.sourcelink.github to 10.0.202 by @thomhurst in thomhurst/TUnit#5543 * chore(deps): update dependency microsoft.entityframeworkcore to 10.0.6 by @thomhurst in thomhurst/TUnit#5542 * chore(deps): update dependency microsoft.templateengine.authoring.templateverifier to 10.0.202 by @thomhurst in thomhurst/TUnit#5546 * chore(deps): update dependency microsoft.templateengine.authoring.cli to v10.0.202 by @thomhurst in thomhurst/TUnit#5545 * chore(deps): update dependency system.commandline to 2.0.6 by @thomhurst in thomhurst/TUnit#5547 * chore(deps): update microsoft.aspnetcore to 10.0.6 by @thomhurst in thomhurst/TUnit#5548 * chore(deps): update dependency nuget.protocol to 7.3.1 by @thomhurst in thomhurst/TUnit#5549 * chore(deps): update microsoft.extensions to 10.0.6 by @thomhurst in thomhurst/TUnit#5550 * chore(deps): update dependency dotnet-sdk to v10.0.202 by @thomhurst in thomhurst/TUnit#5551 * chore(deps): update opentelemetry by @thomhurst in thomhurst/TUnit#5552 * chore(deps): update microsoft.extensions to 10.5.0 by @thomhurst in thomhurst/TUnit#5554 **Full Changelog**: thomhurst/TUnit@v1.33.0...v1.34.0 Commits viewable in [compare view](thomhurst/TUnit@v1.33.0...v1.35.2). </details> [](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>
Summary
Fixes #5559 — mock generator failed to compile when mocking interfaces inheriting from
IEnumerable<T>or having same-named members with different return types across multiple interfaces.ExplicitInterfaceCanDelegateflag — interface return types (e.g.IEnumerator<T>→IEnumerator) delegate safely; incompatible types (intvsstring) get their own engine dispatchoverrideTest plan
EnumerableInterfaceTests)newhiding, constructor forwarding