Skip to content

feat!: only expose indexer accessors the mock intercepts - #830

Merged
vbreuss merged 2 commits into
mainfrom
feat/suppress-absent-indexer-accessor-surface
Jul 29, 2026
Merged

feat!: only expose indexer accessors the mock intercepts#830
vbreuss merged 2 commits into
mainfrom
feat/suppress-absent-indexer-accessor-surface

Conversation

@vbreuss

@vbreuss vbreuss commented Jul 29, 2026

Copy link
Copy Markdown
Member

A generated mock advertised setup and verification for both accessors of every indexer, even when it intercepted only one. For a get-only indexer

    int this[string shelf] { get; }

Setup[...].OnSet and Verify[...].Set(3) compiled and silently did nothing: no setter body is emitted, so a write can never be recorded and the verification could only ever report zero.

The generator now picks the facade from the accessors it actually emits a body for, mirroring the property narrowing:

  • IIndexerGetterOnlySetup<TValue, T1..Tn> / IIndexerSetterOnlySetup< TValue, T1..Tn> (arities 1-4) replace IndexerSetup<TValue, T1..Tn> on the setup surface, with parallel getter-only and setter-only builder hierarchies so chaining through Returns/Throws/Do/TransitionTo stays on the narrowed surface, plus Forever/OnlyOnce extensions for the narrowed when-builders.
  • VerificationIndexerGetterResult<TSubject, T1..Tn> and VerificationIndexerSetterResult<TSubject, T1..Tn, TParameter> replace VerificationIndexerResult<TSubject, TParameter> on the verify surface, each taking only the member id of the accessor it exposes.

GetIndexerInterceptedAccessors reuses the property classification keyed on the same Getter/Setter nullity that guards body emission, so the surface cannot drift from what the proxy intercepts. This also covers the cross-assembly case: an indexer such as { get; internal set; } whose setter is invisible to the mock's assembly narrows to the getter.

BREAKING CHANGE: on an indexer the mock reads or writes but not both, Setup[...].OnSet/OnGet, Setup[...].Returns/Throws/InitializeWith and Verify[...].Set(...)/Got() no longer compile for the absent accessor. Each previously compiled and silently had no effect. The library API is additive; only generated mock surfaces narrow. Indexers with more than four keys keep the full surface, as their setup types are generated per-compilation.

A generated mock advertised setup and verification for both accessors of
every indexer, even when it intercepted only one. For a get-only indexer

    int this[string shelf] { get; }

Setup[...].OnSet and Verify[...].Set(3) compiled and silently did
nothing: no setter body is emitted, so a write can never be recorded and
the verification could only ever report zero.

The generator now picks the facade from the accessors it actually emits
a body for, mirroring the property narrowing:

- IIndexerGetterOnlySetup<TValue, T1..Tn> / IIndexerSetterOnlySetup<
  TValue, T1..Tn> (arities 1-4) replace IndexerSetup<TValue, T1..Tn> on
  the setup surface, with parallel getter-only and setter-only builder
  hierarchies so chaining through Returns/Throws/Do/TransitionTo stays
  on the narrowed surface, plus Forever/OnlyOnce extensions for the
  narrowed when-builders.
- VerificationIndexerGetterResult<TSubject, T1..Tn> and
  VerificationIndexerSetterResult<TSubject, T1..Tn, TParameter> replace
  VerificationIndexerResult<TSubject, TParameter> on the verify surface,
  each taking only the member id of the accessor it exposes.

GetIndexerInterceptedAccessors reuses the property classification keyed
on the same Getter/Setter nullity that guards body emission, so the
surface cannot drift from what the proxy intercepts. This also covers
the cross-assembly case: an indexer such as { get; internal set; } whose
setter is invisible to the mock's assembly narrows to the getter.

BREAKING CHANGE: on an indexer the mock reads or writes but not both,
Setup[...].OnSet/OnGet, Setup[...].Returns/Throws/InitializeWith and
Verify[...].Set(...)/Got() no longer compile for the absent accessor.
Each previously compiled and silently had no effect. The library API is
additive; only generated mock surfaces narrow. Indexers with more than
four keys keep the full surface, as their setup types are generated
per-compilation.
@vbreuss vbreuss self-assigned this Jul 29, 2026
@vbreuss vbreuss added the enhancement New feature or request label Jul 29, 2026
@github-actions

Copy link
Copy Markdown

Test Results

    24 files  ±  0      24 suites  ±0   12m 3s ⏱️ + 1m 12s
 4 293 tests + 30   4 291 ✅ + 30  2 💤 ±0  0 ❌ ±0 
27 399 runs  +190  27 395 ✅ +190  4 💤 ±0  0 ❌ ±0 

Results for commit 1ac5610. ± Comparison against base commit 01a814a.

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

🚀 Benchmark Results

Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.61GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
[Host] : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Event Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 311.1 ns 9.62 ns 9.00 ns 1.01 1.7 KB 1.00
Mockolate 309.5 ns 7.83 ns 7.32 ns 1.00 1.7 KB 1.00
Imposter 1,426.6 ns 9.95 ns 9.31 ns 4.61 8.8 KB 5.17
TUnitMocks 198.4 ns 3.65 ns 3.42 ns 0.64 1.34 KB 0.79
Moq 15,774.3 ns 116.31 ns 108.80 ns 50.99 12.51 KB 7.34
NSubstitute 6,015.9 ns 41.69 ns 39.00 ns 19.44 9.05 KB 5.31
FakeItEasy 215,531.1 ns 1,089.27 ns 1,018.91 ns 696.63 15.26 KB 8.96
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.66GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
[Host] : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Indexer N Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 1 1,089.7 ns 33.52 ns 29.71 ns 0.62 3.77 KB 1.00
Mockolate 1 1,768.9 ns 15.60 ns 14.59 ns 1.00 3.77 KB 1.00
Imposter 1 880.0 ns 9.35 ns 8.29 ns 0.50 5.16 KB 1.37
Moq 1 217,954.4 ns 978.44 ns 915.24 ns 123.22 20.37 KB 5.41
NSubstitute 1 9,401.1 ns 77.13 ns 72.15 ns 5.31 12.78 KB 3.39
FakeItEasy 1 12,687.4 ns 48.80 ns 45.65 ns 7.17 13.63 KB 3.62
baseline* 10 2,612.9 ns 8.36 ns 7.82 ns 1.02 4.82 KB 1.00
Mockolate 10 2,562.3 ns 29.17 ns 25.85 ns 1.00 4.82 KB 1.00
Imposter 10 2,421.7 ns 30.96 ns 27.45 ns 0.95 7.97 KB 1.65
Moq 10 232,247.5 ns 750.70 ns 702.20 ns 90.65 29.89 KB 6.20
NSubstitute 10 22,647.3 ns 112.15 ns 99.41 ns 8.84 25.58 KB 5.31
FakeItEasy 10 24,819.2 ns 139.81 ns 116.75 ns 9.69 32.97 KB 6.84
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
[Host] : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Property N Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 1 538.6 ns 7.87 ns 6.98 ns 0.89 2.41 KB 1.00
Mockolate 1 605.2 ns 39.35 ns 36.80 ns 1.00 2.41 KB 1.00
Imposter 1 479.9 ns 35.29 ns 33.01 ns 0.80 3.13 KB 1.29
TUnitMocks 1 481.0 ns 13.00 ns 12.16 ns 0.80 1.64 KB 0.68
Moq 1 12,042.9 ns 152.92 ns 135.56 ns 19.96 10.39 KB 4.30
NSubstitute 1 8,075.6 ns 73.69 ns 68.93 ns 13.39 11.45 KB 4.74
FakeItEasy 1 8,928.4 ns 80.13 ns 74.95 ns 14.80 11.24 KB 4.66
baseline* 10 1,045.8 ns 21.98 ns 20.56 ns 0.92 2.91 KB 1.00
Mockolate 10 1,132.3 ns 14.97 ns 14.01 ns 1.00 2.91 KB 1.00
Imposter 10 1,210.5 ns 23.66 ns 22.13 ns 1.07 4.67 KB 1.61
TUnitMocks 10 1,680.2 ns 45.89 ns 42.93 ns 1.48 3.94 KB 1.35
Moq 10 18,811.2 ns 177.77 ns 166.28 ns 16.62 18.28 KB 6.29
NSubstitute 10 18,178.4 ns 90.13 ns 79.90 ns 16.06 21.08 KB 7.25
FakeItEasy 10 20,833.1 ns 223.87 ns 198.45 ns 18.40 30.81 KB 10.60
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Intel Xeon Platinum 8370C CPU 2.80GHz (Max: 2.51GHz), 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
[Host] : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v4

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

CreateMock Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 19.25 ns 0.262 ns 0.245 ns 0.95 160 B 1.00
Mockolate 20.36 ns 0.749 ns 0.701 ns 1.00 160 B 1.00
Imposter 299.67 ns 9.614 ns 8.993 ns 14.74 2248 B 14.05
TUnitMocks 37.57 ns 1.395 ns 1.236 ns 1.85 200 B 1.25
Moq 1,417.40 ns 8.224 ns 7.290 ns 69.70 2096 B 13.10
NSubstitute 1,923.82 ns 21.080 ns 19.718 ns 94.60 5048 B 31.55
FakeItEasy 1,717.98 ns 26.667 ns 23.639 ns 84.48 2772 B 17.32
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
[Host] : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Callback Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 315.9 ns 7.47 ns 6.99 ns 0.94 1.57 KB 1.00
Mockolate 337.7 ns 9.20 ns 8.16 ns 1.00 1.57 KB 1.00
Imposter 442.5 ns 21.08 ns 19.72 ns 1.31 2.38 KB 1.52
TUnitMocks 490.9 ns 10.94 ns 10.23 ns 1.45 1.99 KB 1.27
Moq 97,315.4 ns 474.67 ns 444.01 ns 288.34 8.88 KB 5.66
NSubstitute 4,700.6 ns 41.40 ns 38.73 ns 13.93 7.71 KB 4.91
FakeItEasy 4,675.5 ns 70.08 ns 65.55 ns 13.85 6.81 KB 4.33
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.63GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
[Host] : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Method N Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 1 431.7 ns 8.18 ns 7.65 ns 1.15 1.93 KB 1.00
Mockolate 1 374.8 ns 13.26 ns 12.40 ns 1.00 1.93 KB 1.00
Imposter 1 567.4 ns 17.26 ns 13.48 ns 1.52 4.04 KB 2.09
TUnitMocks 1 510.6 ns 10.09 ns 9.43 ns 1.36 2.02 KB 1.04
Moq 1 187,046.1 ns 670.11 ns 594.03 ns 499.50 14.58 KB 7.55
NSubstitute 1 6,036.2 ns 49.65 ns 44.02 ns 16.12 9.12 KB 4.72
FakeItEasy 1 6,364.0 ns 47.81 ns 39.93 ns 17.00 8.05 KB 4.17
baseline* 10 735.9 ns 10.41 ns 9.74 ns 1.06 2.14 KB 1.00
Mockolate 10 692.6 ns 12.38 ns 11.58 ns 1.00 2.14 KB 1.00
Imposter 10 1,165.4 ns 25.32 ns 23.68 ns 1.68 5.52 KB 2.58
TUnitMocks 10 1,452.4 ns 22.75 ns 21.28 ns 2.10 3.73 KB 1.74
Moq 10 191,181.4 ns 732.72 ns 649.53 ns 276.12 18.48 KB 8.63
NSubstitute 10 8,709.1 ns 58.63 ns 54.84 ns 12.58 12.07 KB 5.64
FakeItEasy 10 9,587.0 ns 62.81 ns 55.68 ns 13.85 15.42 KB 7.20

baseline* rows show the corresponding Mockolate benchmark from the most recent successful main branch build with results, for regression comparison.

- add a cross-assembly test proving an indexer whose getter is
  inaccessible narrows to the setter-only surface
- add a generator test proving indexers with more than four keys keep
  the full, unnarrowed surface for asymmetric accessors
- link Mockolate0002 from the indexer single-accessor docs section,
  mirroring the property page
- correct the AppendIndexerVerifyType summary: the setter result is
  additionally keyed by the value type
@sonarqubecloud

Copy link
Copy Markdown

@vbreuss
vbreuss merged commit d2f56ed into main Jul 29, 2026
15 checks passed
@vbreuss
vbreuss deleted the feat/suppress-absent-indexer-accessor-surface branch July 29, 2026 12:22
github-actions Bot added a commit that referenced this pull request Jul 29, 2026
github-actions Bot added a commit that referenced this pull request Jul 29, 2026
vbreuss added a commit that referenced this pull request Jul 29, 2026
The accessor narrowing introduced for indexers (#830) stopped at four keys: the narrowed setup/verify types only ship with the library for arities 1-4, so a five-key get-only indexer still offered OnSet and Verify[...].Set(...), which compiled and silently did nothing.

Since the setup types for indexers with more than four keys are generated per-compilation anyway, generate the narrowed surface there too:

- IndexerSetups.g.cs now emits IIndexerGetterOnlySetup / IIndexerSetterOnlySetup interface hierarchies (builders included) for each affected arity, implements them on the generated IndexerSetup class, and adds the Forever/OnlyOnce extensions for the narrowed when-builders.
- Narrowed VerificationIndexerGetterResult / -SetterResult classes are generated in Mockolate.Verify, dispatching through the predicate-based MockRegistry.IndexerGot/IndexerSet overloads that the full VerificationIndexerResult already uses above four keys. Each takes only the member id and predicate of the accessor it exposes.
- The facade selection in the mock emission now uses the plain accessor classification for every arity; the narrowed hierarchies are only generated for arities that need them, keyed on the same Getter/Setter nullity that guards body emission.

BREAKING CHANGE: on an indexer with more than four keys that the mock reads or writes but not both, Setup[...].OnSet/OnGet, Setup[...].Returns/Throws/InitializeWith and Verify[...].Set(...)/Got() no longer compile for the absent accessor. Each previously compiled and silently had no effect. This closes the gap the four-key narrowing left; the shipped library API is unchanged.
@github-actions

Copy link
Copy Markdown

This is addressed in release v3.4.0.

@github-actions github-actions Bot added the state: released The issue is released label Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request state: released The issue is released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant