Mocks: structural fix for Mock<T> / mocked-member name collisions#5881
Conversation
…isions Framework operations move to `IMockControl<T>` explicit interface impls plus `Mock.X(mock)` static helpers; `Mock<T>` instance surface shrinks to `Object`, the constructor, and the implicit `T` conversion. That leaves a structurally closed collision set of four names — `Object`, `Equals`, `GetHashCode`, `ToString` — handled by stable renames (`Object_`, `EqualsOf`, …) with iterative suffix escalation when the user interface already declares the renamed form. On net9.0+ the generator emits low-priority extension polyfills inside the per-type `*_MockMemberExtensions` class so `mock.Reset()`, `mock.VerifyAll()`, `mock.Invocations`, etc. remain ergonomic. `[OverloadResolutionPriority(-1)]` ranks only within one containing type — polyfills sit alongside the generated setup/verify extensions, and skip emission when the mocked interface declares a member of the same name (no CS0111 duplicates, user's setup wins). Addresses the collision feedback from discussion #4981.
There was a problem hiding this comment.
Code Review — Mocks: structural fix for Mock<T> / mocked-member name collisions
The architectural direction here is correct: moving framework operations off the Mock<T> public surface onto an explicit interface eliminates the entire class of name-collision bugs at the type level rather than playing whack-a-mole with a deny-list. The EscalateUntilUnique suffix chain and the net9.0+ polyfill approach are clever. Overall the implementation is solid. A few architectural concerns worth addressing:
1. Breaking change on net8.0 with no ergonomic fallback
The net9.0+ polyfill path restores mock.Reset() / mock.VerifyAll() etc. via [OverloadResolutionPriority(-1)] extensions. On net8.0 (a supported multi-target), users are left with only the verbose static form:
Mock.Reset(mock); // net8.0 only option
Mock.VerifyAll(mock); // same
Mock.Invocations(mock); // sameThere is no [OverloadResolutionPriority] on net8.0, so the polyfills would collide or simply can't be emitted. This is a significant ergonomic regression for net8.0 users who have existing code using mock.Reset(). The PR should either document a migration path, or investigate whether a conventional extension-method approach (without priority control) is viable for net8.0 — even with a narrower collision scope.
2. GetUserMemberNames called repeatedly — O(N²) allocation
In MockMembersBuilder.cs, GetSafeMemberName(name, model) is called once per member (methods, properties, etc.), and each call invokes EscalateUntilUnique which calls GetUserMemberNames(model):
private static HashSet<string> GetUserMemberNames(MockTypeModel model)
{
var set = new HashSet<string>(System.StringComparer.Ordinal);
foreach (var m in model.Methods) set.Add(m.Name);
foreach (var p in model.Properties) set.Add(p.Name);
foreach (var e in model.Events) set.Add(e.Name);
return set;
}This creates a new HashSet on every call. For an interface with N members, Build() performs O(N²) allocations and iterations. Since source generators run per-compilation, this matters for large interfaces. The fix is straightforward — pre-compute once in Build() and thread it through as a parameter:
var userNames = GetUserMemberNames(model); // once, at Build() entry
// pass to GetSafeMemberName(name, userNames) and GenerateMockControlPolyfills(writer, model, userNames)3. DefaultValueProvider overload design is unconventional
The getter/setter property is now split into two overloaded static methods:
public static IDefaultValueProvider? DefaultValueProvider<T>(Mock<T> mock) where T : class // get
public static void DefaultValueProvider<T>(Mock<T> mock, IDefaultValueProvider? provider) // setUsing overloads to simulate get/set is unusual in C# and breaks the principle of least surprise. A reader seeing Mock.DefaultValueProvider(mock) vs. Mock.DefaultValueProvider(mock, provider) has to reason about what type-based dispatch means here. The recommended alternative is explicit naming:
public static IDefaultValueProvider? GetDefaultValueProvider<T>(Mock<T> mock) where T : class
public static void SetDefaultValueProvider<T>(Mock<T> mock, IDefaultValueProvider? provider) where T : classThis is consistent with how .NET itself handles similar cases (e.g., Attribute.GetCustomAttribute / Attribute.IsDefined).
4. Design inconsistency: Reset / VerifyAll / VerifyNoOtherCalls stay on IMock, not IMockControl<T>
The PR description says "framework operations live on IMockControl<T>", but Reset, VerifyAll, and VerifyNoOtherCalls remain on IMock as explicit implementations, while all other framework ops (Invocations, Behavior, DefaultValueProvider, SetState, InState, GetDiagnostics) move to IMockControl<T>. The static helpers reflect this split:
public static void Reset<T>(Mock<T> mock) => ((IMock)mock).Reset(); // IMock
public static void VerifyAll<T>(Mock<T> mock) => ((IMock)mock).VerifyAll(); // IMock
public static IReadOnlyList<...> Invocations<T>(...) => ((IMockControl<T>)mock).Invocations; // IMockControl<T>This split exists because IMock is a non-generic base that other things depend on, but from the user's perspective it's invisible. Worth documenting why the split exists, or consolidating Reset/VerifyAll/VerifyNoOtherCalls onto IMockControl<T> as well (which would be more consistent).
5. Possible blank-line emission when all polyfills are skipped
In MockMembersBuilder.Build():
if (!firstMember) writer.AppendLine();
GenerateMockControlPolyfills(writer, model);If the mocked interface declares all polyfillable names (collisions on every method and property), GenerateMockControlPolyfills emits only #if NET9_0_OR_GREATER / #endif wrapping nothing. The preceding blank line would still appear. Minor cosmetic issue, but worth guarding with a check similar to what GenerateMockControlPolyfills does internally.
What's good
- The structural approach of moving framework operations to an explicit interface is the right long-term fix — it makes the collision set finite and immutable by design.
- The iterative suffix escalation (
Object_,Object__, …) is a clean generalization of the previous ad-hoc rename table. - The
NameCollisionTestsclass with tests for every former framework member name is exactly the right regression coverage. - The net9.0+ C# 14 extension block for property polyfills (
Invocations,DefaultValueProvider) is a nice use of the new syntax. - The
[EditorBrowsable(EditorBrowsableState.Never)]onIMockControl<T>correctly keeps the internals out of IntelliSense.
The PR is mergeable as-is with the understanding that the net8.0 ergonomics gap exists. Items 2 (O(N²) allocation) and 3 (DefaultValueProvider naming) would be worth addressing before or shortly after merge.
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| CodeStyle | 5 minor |
🟢 Metrics 34 complexity
Metric Results Complexity 34
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.
- Cache per-model user-member-name set via ConditionalWeakTable so each generator pass builds one HashSet per type rather than ~N per type. - Extract the OverloadResolutionPriority(-1) attribute literal into a const. - IMockControl<T> internal — all casts live in Mock.cs; no external need.
There was a problem hiding this comment.
Follow-up Review — After Second Commit
What the latest commit addressed
Item 2 (O(N²) allocation) — Resolved. The ConditionalWeakTable<MockTypeModel, HashSet<string>> cache means each MockTypeModel builds its name-set once per generator pass, not once per member. Clean fix.
IMockControl<T> visibility — Changed from public [EditorBrowsable(Never)] to internal. This is strictly better: internal prevents the type from appearing in any cross-assembly context, not just IntelliSense. The previous concern about the IMock / IMockControl<T> split (item 4) is now moot — it's entirely an internal implementation detail.
Still open from previous review
Item 3 — DefaultValueProvider overload design
No change here. The static helper still splits property get/set into two overloads that look identical to the call-site reader:
Mock.DefaultValueProvider(mock); // get — returns IDefaultValueProvider?
Mock.DefaultValueProvider(mock, provider); // set — returns voidThis is still non-idiomatic. The recommended fix remains explicit verb prefixes:
Mock.GetDefaultValueProvider(mock)
Mock.SetDefaultValueProvider(mock, provider)Item 5 — Empty #if block in generated code
When the mocked interface declares all polyfillable names (which IFrameworkNameMember in the new NameCollisionTests.cs actually does), GenerateMockControlPolyfills emits:
// (blank line from unconditional 'if (!firstMember) writer.AppendLine()' at call-site)
#if NET9_0_OR_GREATER
#endif
Valid C#, but it's noise in generated output, and the NameCollisionTests test suite will exercise this path. The fix is to move the blank-line guard inside GenerateMockControlPolyfills and only emit the #if / #endif wrapper when at least one polyfill will be written. A simple bool anyEmitted sentinel is enough, or pre-check all names against userNames before starting:
// In GenerateMockControlPolyfills, before the #if:
bool anyMethod = MethodPolyfills.Any(p => !userNames.Contains(p.Name));
bool anyInState = !userNames.Contains("InState");
bool anyProp = !userNames.Contains("Invocations") || !userNames.Contains("Behavior") || !userNames.Contains("DefaultValueProvider");
if (!anyMethod && !anyInState && !anyProp) return;Net8.0 ergonomics gap (item 1)
Unchanged — still requires Mock.Reset(mock) etc. on net8.0. Noted in the previous review as acceptable; flagging again for completeness so it's tracked somewhere.
Overall
The structural redesign is solid and the second commit tightened the internals well. Items 3 and 5 above are small but concrete; item 3 in particular is a public API surface concern that'll be harder to change post-release. Worth a final pass before merge.
- Mock.DefaultValueProvider get/set overload → explicit Mock.GetDefaultValueProvider / Mock.SetDefaultValueProvider per .NET naming conventions. Polyfill extension property still surfaces as mock.DefaultValueProvider on net9+. - IMockControl<T>: document why Reset/VerifyAll/VerifyNoOtherCalls stay on the non-generic IMock (MockRepository batch ops over IMock[]). - GenerateMockControlPolyfills: short-circuit when every polyfill name collides with a user member, so no stray blank line precedes an empty #if/#endif block.
|
Thanks for the review. Status per item: #2 (O(N²) HashSet allocation) — already addressed in commit #3 (DefaultValueProvider get/set overloads) — fixed in #4 (IMock vs IMockControl split) — intentional, but now documented on #5 (blank line before empty #1 (net8.0 ergonomic regression) — declined. The instance-style API on net8.0 was the source of the silent shadowing the PR exists to fix; restoring it on that TFM requires either polyfilling Verified 977/977 pass on net10.0 and 967/967 on net8.0. |
There was a problem hiding this comment.
Third-pass Review — After Review-Feedback Commit
Items resolved by the third commit
Item 3 (DefaultValueProvider naming) — Resolved. Mock.GetDefaultValueProvider / Mock.SetDefaultValueProvider follow the .NET naming convention and match what the polyfill extension property surfaces as on net9+. Clean.
Item 4 (IMock / IMockControl split documentation) — Resolved. IMockControl<T> now has a clear doc comment explaining why Reset/VerifyAll/VerifyNoOtherCalls stay on the non-generic IMock (needed for MockRepository batch iteration over IMock[]). The split is intentional and now visible.
Item 5 (empty #if/#endif) — Partially resolved. GenerateMockControlPolyfills now has an early-return guard when all polyfill names are blocked by user-declared members, which prevents the empty #if NET9_0_OR_GREATER / #endif wrapper from being emitted. However, the blank line at the call site is still unconditionally emitted before the guard fires:
// MockMembersBuilder.cs line 135
if (!firstMember) writer.AppendLine(); // ← always runs
GenerateMockControlPolyfills(writer, model); // ← may return immediatelyWhen GenerateMockControlPolyfills short-circuits, the blank line is already written. For a type like IFrameworkNameMember (which declares all polyfillable names), the generated class ends with a trailing blank line before the closing }. This is cosmetic, but the commit message implies it was fixed when it wasn't quite. The correct fix is to move the blank-line guard inside GenerateMockControlPolyfills after the early-return check, or extract a WillEmitPolyfills(model) predicate to gate both:
// Option A — guard inside GenerateMockControlPolyfills, after the early-return:
if (!anyMethod && !anyProperty) return;
if (!firstMember) writer.AppendLine(); // moved inside, only when something will be emitted
first = false;
// ...
// Option B — predicate at call site:
if (WillEmitPolyfills(model))
{
if (!firstMember) writer.AppendLine();
GenerateMockControlPolyfills(writer, model);
}Item 1 — net8.0 ergonomics gap
Still present and unchanged. For users on net8.0 (a supported multi-target), the ergonomic mock.Reset() / mock.VerifyAll() form is gone and only Mock.Reset(mock) remains. This is understood and accepted, but worth a note in release notes or the PR description so users know to update call sites when targeting net8.0.
Overall
The structural redesign is sound and the third commit has addressed the substantive concerns. The remaining issues are:
- Trailing blank line in generated code (item 5, minor but incomplete fix) — the early-return guard in
GenerateMockControlPolyfillsdoesn't prevent the blank line already written at the call site. - net8.0 migration note — documentation gap, not a code defect.
The code is ready to merge. The blank-line issue is a low-priority follow-up.
GenerateMockControlPolyfills now caches each userNames.Contains() once as an emitX flag, shared between the early-return guard and the emission below. Drops the duplicated Contains() per polyfill name (~10 lookups → 5) and removes the parallel hasInvocations/hasBehavior/ hasDvp locals that mirrored the precheck.
There was a problem hiding this comment.
Fourth-pass Review
Summary of prior items
| # | Issue | Status |
|---|---|---|
| 1 | net8.0 ergonomics gap | Declined by author — documented as accepted |
| 2 | O(N²) HashSet allocation | ✅ Resolved via ConditionalWeakTable cache |
| 3 | DefaultValueProvider overload naming |
✅ Resolved — GetDefaultValueProvider/SetDefaultValueProvider |
| 4 | IMock / IMockControl<T> split not documented | ✅ Resolved — <remarks> on IMockControl<T> explains the MockRepository reason |
| 5 | Trailing blank line / empty #if block |
Item 5 — still present after the third commit
The early-return guard was added inside GenerateMockControlPolyfills, but the blank line is written before the call:
// MockMembersBuilder.cs — call site
if (!firstMember) writer.AppendLine(); // ← executes unconditionally
GenerateMockControlPolyfills(writer, model); // ← may return immediatelyFor an interface that collides with all polyfillable names (the IFrameworkNameMember test exercises this exact path), the generated class ends with a trailing blank line before its closing }. The comment inside GenerateMockControlPolyfills says "keeps callers from emitting a stray blank line", but the caller still emits it.
The fix is one of:
- Move the blank-line write inside
GenerateMockControlPolyfills, between the early-return guard and the#if:if (emitMethods.Length == 0 && !emitInState && ...) return; if (!firstMember) writer.AppendLine(); // moved here writer.AppendLine("#if NET9_0_OR_GREATER");
- Or gate the whole block at the call site with a
WillEmitPolyfills(model)predicate.
This is low-priority cosmetic noise in generated output and does not affect correctness.
Overall
The structural redesign is sound. Moving framework operations to IMockControl<T> explicit implementations permanently closes the collision surface, the iterative-suffix escalation handles the overflow case cleanly, and the net9.0+ polyfill approach recovers ergonomics where the language supports it. The test coverage in NameCollisionTests is thorough. The remaining issue (item 5) is a one-line fix and does not block merge.
Reviewer caught that the call site emitted the blank separator line before invoking GenerateMockControlPolyfills, so when the function short-circuited because every polyfill name collided with a user member, a stray blank line still appeared in the generated source before the closing brace. The separator now lives inside the function, after the early-return check, so no emission happens when nothing will be written.
|
Fixed in 02a0b3b — moved the separator blank-line emission inside Re item 1: added to PR description as a release-notes follow-up. |
There was a problem hiding this comment.
Fifth-pass Review
Previous items — all resolved
| # | Issue | Status |
|---|---|---|
| 1 | net8.0 ergonomics gap | Declined — documented as accepted breaking change |
| 2 | O(N²) HashSet allocation | ✅ Resolved via ConditionalWeakTable cache |
| 3 | DefaultValueProvider overload naming |
✅ Resolved — GetDefaultValueProvider/SetDefaultValueProvider |
| 4 | IMock / IMockControl<T> split undocumented | ✅ Resolved — <remarks> on IMockControl<T> explains the MockRepository reason |
| 5 | Trailing blank line before the polyfill block | ✅ Resolved — blank line moved inside GenerateMockControlPolyfills after the early-return guard |
New issue: C# 14 extension block gated on NET9_0_OR_GREATER instead of NET10_0_OR_GREATER
GenerateMockControlPolyfills emits property polyfills (Invocations, Behavior, DefaultValueProvider) using C# 14 extension block syntax:
// MockMembersBuilder.cs
using (writer.Block($"extension{typeParams}(global::TUnit.Mocks.Mock<{mockableType}> mock){constraints}"))
{
// property polyfills…
}This is all wrapped in #if NET9_0_OR_GREATER. The problem: extension blocks (new-style extensions with extension properties) are C# 14, which defaults to .NET 10. A user who targets net9.0 and uses the default <LangVersion>13 will get a compile error from the generated extension block syntax, because C# 13 does not support extension properties.
The method polyfills (using this Mock<T> mock parameter) are fine for C# 13/net9.0, but extension properties require C# 14.
Concrete options:
Option A — split the gate by feature:
#if NET9_0_OR_GREATER
// method polyfills (C# 13 compatible)
[OverloadResolutionPriority(-1)]
public static void Reset<T>(this Mock<T> mock) ...
// … other method polyfills …
#endif
#if NET10_0_OR_GREATER
// property polyfills (require C# 14 extension blocks)
extension<T>(Mock<T> mock) {
public IReadOnlyList<CallRecord> Invocations => ...;
// …
}
#endifOption B — use method-based polyfills for the properties too, keeping a single NET9_0_OR_GREATER gate:
// Ergonomics: mock.GetInvocations() instead of mock.Invocations
[OverloadResolutionPriority(-1)]
public static IReadOnlyList<CallRecord> GetInvocations<T>(this Mock<T> mock) ...This changes the polyfill surface to method-call syntax on net9.0, but keeps exact parity (mock.Invocations, mock.DefaultValueProvider = …) only on net10.0+.
Option C — require consumers to set <LangVersion>preview</LangVersion> and document it. Least desirable since it's a non-standard requirement for a library user.
The simplest safe fix is Option A: gate the extension block portion on NET10_0_OR_GREATER so net9.0 users still get the method polyfills (mock.Reset(), mock.VerifyAll()) while the property-style polyfills (mock.Invocations) only appear on net10.0+.
What's good
The structural redesign is solid. All previously raised concerns are now addressed. The NameCollisionTests coverage is thorough — the IFrameworkNameMember, IObjectOverflow, and IEqualsOfOverflow cases cover the full escalation logic. The IMockControl<T> / explicit-interface approach permanently closes the collision surface by construction.
|
Declining the C# 14 / net10 gate split. TUnit.Mocks already requires C# 14 across the library — the existing generator emits extension blocks unconditionally for mocked-interface properties, and consumers must already opt into a later LangVersion. Splitting the polyfill gate would add real complexity for an effectively non-existent compatibility envelope. Keeping the single |
Updated [TUnit.Core](https://github.com/thomhurst/TUnit) from 1.43.11 to 1.44.0. <details> <summary>Release notes</summary> _Sourced from [TUnit.Core's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.44.0 <!-- Release notes generated using configuration in .github/release.yml at v1.44.0 --> ## What's Changed ### Other Changes * Generated mocks live in the same namespace as the mocked type by @thomhurst in thomhurst/TUnit#5870 * Show multi-step test spans in class timeline, align report ordering with execution, and correlate linked OTel activities by @Copilot in thomhurst/TUnit#5847 * fix: don't leak RUC onto Should-style comparer overloads (#5857) by @thomhurst in thomhurst/TUnit#5873 * Fix culture-dependent timestamp in HTML test report (#5868) by @thomhurst in thomhurst/TUnit#5872 * fix(mocks-http): auto-prepend `/` to partial endpoint paths (#5838) by @thomhurst in thomhurst/TUnit#5874 * Replace Report.ExpandClassTimeline with [ClassTimeline] attribute by @thomhurst in thomhurst/TUnit#5875 * feat(assertions): make ShouldAssertion<T> implement IAssertion (#5824) by @thomhurst in thomhurst/TUnit#5876 * feat(mocks): support non-span ref struct out/ref params by @thomhurst in thomhurst/TUnit#5878 * fix(core): fill optional params when invoking MethodDataSource via reflection by @thomhurst in thomhurst/TUnit#5880 * Mocks: structural fix for Mock<T> / mocked-member name collisions by @thomhurst in thomhurst/TUnit#5881 * chore(mocks): promote TUnit.Mocks packages to stable by @thomhurst in thomhurst/TUnit#5877 ### Dependencies * chore(deps): update tunit to 1.43.41 by @thomhurst in thomhurst/TUnit#5863 * chore(deps): update dependency tunit.assertions.fsharp to 1.43.41 by @thomhurst in thomhurst/TUnit#5865 * chore(deps): bump @babel/plugin-transform-modules-systemjs from 7.28.5 to 7.29.4 in /docs by @dependabot[bot] in thomhurst/TUnit#5867 * chore(deps): bump fast-uri from 3.1.0 to 3.1.2 in /docs by @dependabot[bot] in thomhurst/TUnit#5862 **Full Changelog**: thomhurst/TUnit@v1.43.41...v1.44.0 ## 1.43.41 <!-- Release notes generated using configuration in .github/release.yml at v1.43.41 --> ## What's Changed ### Other Changes * feat(playwright): expose default Context/Launch options on settings by @thomhurst in thomhurst/TUnit#5861 * fix(hooks): resolve TestDiscovery hook context type by attribute kind, not method name by @thomhurst in thomhurst/TUnit#5860 ### Dependencies * chore(deps): update tunit to 1.43.38 by @thomhurst in thomhurst/TUnit#5858 **Full Changelog**: thomhurst/TUnit@v1.43.38...v1.43.41 ## 1.43.38 <!-- Release notes generated using configuration in .github/release.yml at v1.43.38 --> ## What's Changed ### Other Changes * feat(playwright): add TUnitPlaywrightSettings defaults by @thomhurst in thomhurst/TUnit#5859 **Full Changelog**: thomhurst/TUnit@v1.43.37...v1.43.38 ## 1.43.37 <!-- Release notes generated using configuration in .github/release.yml at v1.43.37 --> ## What's Changed ### Other Changes * docs: clarify MethodDataSourceAttribute.Factory is source-generator-managed by @Copilot in thomhurst/TUnit#5835 * fix(assertions): skip ref-struct members in IsEquivalentTo (#5841) by @thomhurst in thomhurst/TUnit#5842 * feat(playwright): add composition-based fixtures by @thomhurst in thomhurst/TUnit#5840 ### Dependencies * chore(deps): update tunit to 1.43.11 by @thomhurst in thomhurst/TUnit#5821 * chore(deps): update dependency polyfill to 10.4.0 by @thomhurst in thomhurst/TUnit#5830 * chore(deps): update dependency polyfill to 10.4.0 by @thomhurst in thomhurst/TUnit#5829 * chore(deps): update react to ^19.2.6 by @thomhurst in thomhurst/TUnit#5839 * chore(deps): update dependency polyfill to 10.5.0 by @thomhurst in thomhurst/TUnit#5848 * chore(deps): update dependency polyfill to 10.5.0 by @thomhurst in thomhurst/TUnit#5849 * chore(deps): update aspire to 13.3.0 by @thomhurst in thomhurst/TUnit#5851 * chore(deps): update dependency brace-expansion to v5.0.6 by @thomhurst in thomhurst/TUnit#5853 * chore(deps): update dependency polyfill to 10.5.1 by @thomhurst in thomhurst/TUnit#5854 * chore(deps): update dependency polyfill to 10.5.1 by @thomhurst in thomhurst/TUnit#5855 * chore(deps): update verify to 31.16.3 by @thomhurst in thomhurst/TUnit#5856 **Full Changelog**: thomhurst/TUnit@v1.43.11...v1.43.37 Commits viewable in [compare view](thomhurst/TUnit@v1.43.11...v1.44.0). </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
Reset,VerifyAll,Invocations, …) offMock<T>as instance members ontoIMockControl<T>explicit interface implementations plusMock.X(mock)static helpers.Mock<T>instance surface shrinks toObject+ constructor + implicit conversion toT.{ Object, Equals, GetHashCode, ToString }. Generator's existingObject_/*Ofrenames keep working, with new iterative-suffix escalation when the user interface declares the renamed form (Object+Object_→ generated extension lands onObject__).*_MockMemberExtensionsclass somock.Reset(),mock.VerifyAll(),mock.Invocations,mock.SetState("x"),mock.DefaultValueProvider = …remain ergonomic.[OverloadResolutionPriority(-1)]only ranks within one containing type, so the polyfills must live alongside the generated extensions; they skip emission when the mocked interface declares a member of the same name (no CS0111).Background: GitHub Discussion #4981 raised the collision concern. Prior handling covered ~4 names by ad-hoc rename table and would have silently shadowed generator output for ~10 other public
Mock<T>members. With this change, future framework additions go onIMockControl<T>/ staticMock.*and cannot regress collision safety.Breaking changes (TUnit.Mocks is in beta)
Mock<T>no longer exposesReset(),VerifyAll(),VerifyNoOtherCalls(),Invocations,Behavior,DefaultValueProvider,SetupAllProperties(),GetDiagnostics(),SetState(),InState()as instance members.mock.Reset()etc. continue to work.Mock.Reset(mock),Mock.VerifyAll(mock), etc.).[OverloadResolutionPriority]isn't available on those targets so the polyfill path can't apply.Mock.DefaultValueProvidergetter/setter overload split intoMock.GetDefaultValueProvider(mock)andMock.SetDefaultValueProvider(mock, provider)per .NET naming conventions.Test plan
NameCollisionTests(new) exercises every former framework name plusObject_/EqualsOfoverflow paths.mock.Reset()/mock.VerifyAll()/mock.Invocations/ property settermock.DefaultValueProvider = …route through and that polyfills are skipped when colliding.TUnit.Mocks.Testsfull suite: 977 pass (net10.0), 967 pass (net8.0).dotnet build TUnit.slnxsucceeds.TUnit.Mocks.SourceGenerator.Tests) — local runner blocked by an unrelatedVerify.TUnit/TUnit.Coreversion mismatch; CI will validate.