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

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

intthis[stringshelf]{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.
@vbreussvbreuss self-assigned this Jul 29, 2026
@vbreussvbreuss 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-actionsBot 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

EventMeanErrorStdDevRatioAllocatedAlloc Ratio
baseline*311.1 ns9.62 ns9.00 ns1.011.7 KB1.00
Mockolate309.5 ns7.83 ns7.32 ns1.001.7 KB1.00
Imposter1,426.6 ns9.95 ns9.31 ns4.618.8 KB5.17
TUnitMocks198.4 ns3.65 ns3.42 ns0.641.34 KB0.79
Moq15,774.3 ns116.31 ns108.80 ns50.9912.51 KB7.34
NSubstitute6,015.9 ns41.69 ns39.00 ns19.449.05 KB5.31
FakeItEasy215,531.1 ns1,089.27 ns1,018.91 ns696.6315.26 KB8.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

IndexerNMeanErrorStdDevRatioAllocatedAlloc Ratio
baseline*11,089.7 ns33.52 ns29.71 ns0.623.77 KB1.00
Mockolate11,768.9 ns15.60 ns14.59 ns1.003.77 KB1.00
Imposter1880.0 ns9.35 ns8.29 ns0.505.16 KB1.37
Moq1217,954.4 ns978.44 ns915.24 ns123.2220.37 KB5.41
NSubstitute19,401.1 ns77.13 ns72.15 ns5.3112.78 KB3.39
FakeItEasy112,687.4 ns48.80 ns45.65 ns7.1713.63 KB3.62
baseline*102,612.9 ns8.36 ns7.82 ns1.024.82 KB1.00
Mockolate102,562.3 ns29.17 ns25.85 ns1.004.82 KB1.00
Imposter102,421.7 ns30.96 ns27.45 ns0.957.97 KB1.65
Moq10232,247.5 ns750.70 ns702.20 ns90.6529.89 KB6.20
NSubstitute1022,647.3 ns112.15 ns99.41 ns8.8425.58 KB5.31
FakeItEasy1024,819.2 ns139.81 ns116.75 ns9.6932.97 KB6.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

PropertyNMeanErrorStdDevRatioAllocatedAlloc Ratio
baseline*1538.6 ns7.87 ns6.98 ns0.892.41 KB1.00
Mockolate1605.2 ns39.35 ns36.80 ns1.002.41 KB1.00
Imposter1479.9 ns35.29 ns33.01 ns0.803.13 KB1.29
TUnitMocks1481.0 ns13.00 ns12.16 ns0.801.64 KB0.68
Moq112,042.9 ns152.92 ns135.56 ns19.9610.39 KB4.30
NSubstitute18,075.6 ns73.69 ns68.93 ns13.3911.45 KB4.74
FakeItEasy18,928.4 ns80.13 ns74.95 ns14.8011.24 KB4.66
baseline*101,045.8 ns21.98 ns20.56 ns0.922.91 KB1.00
Mockolate101,132.3 ns14.97 ns14.01 ns1.002.91 KB1.00
Imposter101,210.5 ns23.66 ns22.13 ns1.074.67 KB1.61
TUnitMocks101,680.2 ns45.89 ns42.93 ns1.483.94 KB1.35
Moq1018,811.2 ns177.77 ns166.28 ns16.6218.28 KB6.29
NSubstitute1018,178.4 ns90.13 ns79.90 ns16.0621.08 KB7.25
FakeItEasy1020,833.1 ns223.87 ns198.45 ns18.4030.81 KB10.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

CreateMockMeanErrorStdDevRatioAllocatedAlloc Ratio
baseline*19.25 ns0.262 ns0.245 ns0.95160 B1.00
Mockolate20.36 ns0.749 ns0.701 ns1.00160 B1.00
Imposter299.67 ns9.614 ns8.993 ns14.742248 B14.05
TUnitMocks37.57 ns1.395 ns1.236 ns1.85200 B1.25
Moq1,417.40 ns8.224 ns7.290 ns69.702096 B13.10
NSubstitute1,923.82 ns21.080 ns19.718 ns94.605048 B31.55
FakeItEasy1,717.98 ns26.667 ns23.639 ns84.482772 B17.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

CallbackMeanErrorStdDevRatioAllocatedAlloc Ratio
baseline*315.9 ns7.47 ns6.99 ns0.941.57 KB1.00
Mockolate337.7 ns9.20 ns8.16 ns1.001.57 KB1.00
Imposter442.5 ns21.08 ns19.72 ns1.312.38 KB1.52
TUnitMocks490.9 ns10.94 ns10.23 ns1.451.99 KB1.27
Moq97,315.4 ns474.67 ns444.01 ns288.348.88 KB5.66
NSubstitute4,700.6 ns41.40 ns38.73 ns13.937.71 KB4.91
FakeItEasy4,675.5 ns70.08 ns65.55 ns13.856.81 KB4.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

MethodNMeanErrorStdDevRatioAllocatedAlloc Ratio
baseline*1431.7 ns8.18 ns7.65 ns1.151.93 KB1.00
Mockolate1374.8 ns13.26 ns12.40 ns1.001.93 KB1.00
Imposter1567.4 ns17.26 ns13.48 ns1.524.04 KB2.09
TUnitMocks1510.6 ns10.09 ns9.43 ns1.362.02 KB1.04
Moq1187,046.1 ns670.11 ns594.03 ns499.5014.58 KB7.55
NSubstitute16,036.2 ns49.65 ns44.02 ns16.129.12 KB4.72
FakeItEasy16,364.0 ns47.81 ns39.93 ns17.008.05 KB4.17
baseline*10735.9 ns10.41 ns9.74 ns1.062.14 KB1.00
Mockolate10692.6 ns12.38 ns11.58 ns1.002.14 KB1.00
Imposter101,165.4 ns25.32 ns23.68 ns1.685.52 KB2.58
TUnitMocks101,452.4 ns22.75 ns21.28 ns2.103.73 KB1.74
Moq10191,181.4 ns732.72 ns649.53 ns276.1218.48 KB8.63
NSubstitute108,709.1 ns58.63 ns54.84 ns12.5812.07 KB5.64
FakeItEasy109,587.0 ns62.81 ns55.68 ns13.8515.42 KB7.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 mainJul 29, 2026
15 checks passed
@vbreuss
vbreuss deleted the feat/suppress-absent-indexer-accessor-surface branch July 29, 2026 12:22
github-actionsBot added a commit that referenced this pull request Jul 29, 2026
github-actionsBot 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.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancementNew feature or requeststate: releasedThe issue is released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@vbreuss