feat: support out/ref/ref-readonly ref-struct parameters#774
Conversation
Test Results 24 files ± 0 24 suites ±0 9m 20s ⏱️ -40s Results for commit 114f5d3. ± Comparison against base commit 9cf4fad. This pull request removes 8 and adds 55 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
🚀 Benchmark ResultsDetails
Details
Details
Details
Details
Details
|
There was a problem hiding this comment.
Pull request overview
This PR extends Mockolate’s ref-struct support to allow out, ref, and ref readonly ref-struct parameters on interface/class members by introducing a dedicated matcher/parameter pipeline (IRefStructOutParameter<T> / IRefStructRefParameter<T>) and updating the source generator + analyzer accordingly.
Changes:
- Added new ref-struct-safe delegates and parameter interfaces (
RefStructFactory<T>,RefStructTransform<T>,IRefStructOutParameter<T>,IRefStructRefParameter<T>) and newIt.*matcher overloads. - Updated the source generator to route
out/ref/ref readonlyref-struct parameters through the new pipeline and to expose per-slot matcher accessors on ref-struct setup types. - Updated analyzer rules and tests/snapshots to reflect the newly supported signatures (while keeping delegate
Invokerestrictions).
Reviewed changes
Copilot reviewed 34 out of 36 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| Tests/Mockolate.Tests/RefStruct/RefStructOutRefParameterTests.cs | New end-to-end tests for out/ref/ref readonly ref-struct parameters (Packet + Span/ROS cases). |
| Tests/Mockolate.Tests/GeneratorCoverage/Packet.cs | Splits Packet ref struct into its own file for generator coverage inputs. |
| Tests/Mockolate.Tests/GeneratorCoverage/MyStruct.cs | Splits generator-coverage helper type into its own file. |
| Tests/Mockolate.Tests/GeneratorCoverage/MyEventArgs.cs | Splits generator-coverage helper type into its own file. |
| Tests/Mockolate.Tests/GeneratorCoverage/MyEnum.cs | Splits generator-coverage helper type into its own file. |
| Tests/Mockolate.Tests/GeneratorCoverage/MyBase.cs | Splits generator-coverage helper type into its own file. |
| Tests/Mockolate.Tests/GeneratorCoverage/MyAbstractBase.cs | Splits generator-coverage helper base class into its own file. |
| Tests/Mockolate.Tests/GeneratorCoverage/IRefStructConsumer.cs | Adds new out/ref/in ref-struct members; removes inline Packet definition. |
| Tests/Mockolate.Tests/GeneratorCoverage/IKeywordEdgeCases.cs | New generator-coverage interface to validate keyword escaping / qualification logic. |
| Tests/Mockolate.Tests/GeneratorCoverage/IComprehensiveInterface.cs | Removes previously inline helper type definitions (now in separate files). |
| Tests/Mockolate.Tests/GeneratorCoverage/ICombinationMockB.cs | Extracts ICombinationMockB into its own file for snapshot scenarios. |
| Tests/Mockolate.Tests/GeneratorCoverage/ICombinationMockA.cs | Removes inline ICombinationMockB definition. |
| Tests/Mockolate.Tests/GeneratorCoverage/ComprehensiveAbstractClass.cs | Removes inline MyAbstractBase definition (now separate file). |
| Tests/Mockolate.SourceGenerators.Tests/Snapshot/MockGenerationSnapshotTests.cs | Updates snapshot scenarios’ coverage-file lists for renamed/extracted inputs. |
| Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/RefStructConsumer_CanBeCreated/RefStructMethodSetups.g.cs | Removes outdated expected snapshot output file. |
| Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/RefStructConsumer_CanBeCreated/Mock.IRefStructConsumer.g.cs | Updates expected generated mock output for new ref/out/refreadonly handling. |
| Tests/Mockolate.SourceGenerators.Tests/MockTests.RefStructTests.cs | Adjusts generator tests to expect the new out-ref-struct pipeline. |
| Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt | Updates API surface expectations for new Span/ROS out/ref helper matchers. |
| Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt | Updates API surface expectations for new ref-struct out/ref pipeline types + delegates. |
| Tests/Mockolate.Analyzers.Tests/MockabilityAnalyzerRefStructTests.cs | Updates analyzer expectations: allow out/ref ref-struct params on interfaces/classes; keep delegate restriction. |
| Source/Mockolate/Setup/SpanWrapper.cs | Makes implicit conversion from wrapper to Span<T> null-safe. |
| Source/Mockolate/Setup/ReadOnlySpanWrapper.cs | Makes implicit conversion from wrapper to ReadOnlySpan<T> null-safe. |
| Source/Mockolate/Setup/RefStructVoidMethodSetup.cs | Adds per-slot GetMatcher{n}() accessors for generated mock bodies. |
| Source/Mockolate/Setup/RefStructReturnMethodSetup.cs | Adds per-slot GetMatcher{n}() accessors for generated mock bodies. |
| Source/Mockolate/RefStructTransform.cs | Adds ref-struct-safe transform delegate used by It.IsRef overloads. |
| Source/Mockolate/RefStructFactory.cs | Adds ref-struct-safe factory delegate used by It.IsOut overloads. |
| Source/Mockolate/Parameters/IVerifyRefParameter.cs | Allows ref-struct type arguments under NET9+ via allows ref struct. |
| Source/Mockolate/Parameters/IVerifyOutParameter.cs | Allows ref-struct type arguments under NET9+ via allows ref struct. |
| Source/Mockolate/Parameters/IRefStructRefParameter.cs | New ref-struct-safe ref-parameter interface for setup payloads. |
| Source/Mockolate/Parameters/IRefStructOutParameter.cs | New ref-struct-safe out-parameter interface for setup payloads. |
| Source/Mockolate/It.IsRef.cs | Adds Span/ROS ref helpers + new ref-struct-safe It.IsRef overloads and matchers. |
| Source/Mockolate/It.IsOut.cs | Adds Span/ROS out helpers + new ref-struct-safe It.IsOut overloads and matchers. |
| Source/Mockolate.SourceGenerators/Sources/Sources.RefStructMethodSetups.cs | Emits per-slot matcher accessors for generated ref-struct setup types. |
| Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs | Updates mock-body emission for out/ref/refreadonly ref-struct params + Span out temp unpacking. |
| Source/Mockolate.SourceGenerators/Helpers.cs | Updates parameter-type mapping to select new ref-struct out/ref parameter interfaces when needed. |
| Source/Mockolate.Analyzers/MockabilityAnalyzer.cs | Narrows delegate restriction + updates diagnostic messaging for delegate out/ref ref-struct params. |
Comments suppressed due to low confidence (1)
Tests/Mockolate.Tests/GeneratorCoverage/IComprehensiveInterface.cs:38
- Several supporting types used by
IComprehensiveInterface(e.g.,MyBase,MyEnum,MyStruct,MyEventArgs) were split into separate files. The source-generator snapshot harness compiles scenarios from an explicit coverage-file list, so any snapshot scenario that only includesIComprehensiveInterface.cswill now see these as error types and stop covering the intended generator branches. Update the relevant snapshot scenario inputs to include the new files so coverage remains end-to-end and symbol-accurate.
#if NET10_0_OR_GREATER
using System;
using System.Threading.Tasks;
namespace Mockolate.Tests.GeneratorCoverage;
/// <summary>
/// Squeezes every interface-shaped generator branch we can fit into a single type:
/// property accessor combinations, indexers (single + arity-5), all three event flavors,
/// static abstract members, every parameter modifier, every default-value kind,
/// every "reserved" parameter name, nullable annotations, async returns, special return
/// shapes (Span/ReadOnlySpan/ref/ref-readonly/tuple/Nullable<T>),
/// every generic-constraint kind, plus arity-5 and arity-17 methods to trigger the
/// <c>MethodSetups.g.cs</c> / <c>ActionFunc.g.cs</c> aggregates.
/// </summary>
public interface IComprehensiveInterface
{
int GetSet { get; set; }
int GetOnly { get; }
int SetOnly { set; }
string? NullableProp { get; set; }
string InitOnly { get; init; }
string this[int i] { get; set; }
string this[int a, int b, int c, int d, int e] { get; set; }
static abstract int StaticAbstractValue { get; }
event EventHandler PlainEvent;
event EventHandler<MyEventArgs> TypedEvent;
event ComprehensiveDelegate CustomEvent;
static abstract int StaticAbstractMethod();
void WithModifiers(ref int a, out string b, in long c, params int[] tail);
void WithDefaults(int i = 5, MyEnum e = MyEnum.B, decimal d = 1.5m,
float f = 0.25f, char c = 'x', string? s = null, MyStruct st = default);
a01b5d6 to
fc95481
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 48 out of 49 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs:2277
- For
ref Span<T>/ref ReadOnlySpan<T>parameters, this branch currently capturesvar ref_x = x(a span) rather than aSpanWrapper<T>. That means everyMatches(ref_x)call relies on the implicit conversion fromSpan<T>toSpanWrapper<T>, which allocates (ToArray()) each time it runs (potentially once per setup scanned). Consider handling Span/ReadOnlySpan before the by-ref capture (or special-casing ref-span here) so you materialize the wrapper once viap.ToNameOrWrapper()and avoid repeated allocations during matching.
if (p.RefKind == RefKind.Ref || p.RefKind == RefKind.In || p.RefKind == RefKind.RefReadOnlyParameter)
{
string paramRef = Helpers.GetUniqueLocalVariableName($"ref_{p.Name}", method.Parameters);
sb.Append("\t\t\tvar ").Append(paramRef).Append(" = ").Append(p.Name).Append(';').AppendLine();
sb2.Append(paramRef);
}
else if (p.RefKind != RefKind.Out &&
(p.Type.SpecialGenericType == SpecialGenericType.Span ||
p.Type.SpecialGenericType == SpecialGenericType.ReadOnlySpan))
{
string paramRef = Helpers.GetUniqueLocalVariableName($"ref_{p.Name}", method.Parameters);
sb.Append("\t\t\tvar ").Append(paramRef).Append(" = ").Append(p.ToNameOrWrapper()).Append(';')
.AppendLine();
Lift the analyzer and generator rejection of `out`, `ref`, and `ref readonly` ref-struct parameters on interface and class methods, and route them through a new `IRefStructOutParameter<T>` / `IRefStructRefParameter<T>` pipeline. Delegates continue to reject these via a narrower analyzer rule.
New matchers: `It.IsOut<T>(RefStructFactory<T>)`, `It.IsAnyRefStructOut<T>()`, `It.IsRef<T>(RefStructTransform<T>)`, `It.IsAnyRefStructRef<T>()` plus predicate-gated variants. Setup-side mapping in Helpers.ToParameter() branches on NeedsRefStructPipeline; mock body emits per-slot casts via new `GetMatcher{n}()` accessors on RefStructVoidMethodSetup / RefStructReturnMethodSetup. Out-slot fallback writes `default!` directly because MockBehavior.DefaultValue cannot produce ref-struct values.
Lift the compile and setup-side mismatch for `out`/`ref` Span<T> and ReadOnlySpan<T> parameters. The generator now emits a wrapper-typed temp local for `out` so the implicit conversion fires at assignment, and the setup-side type for `out`/`ref` Span/ROS slots resolves to the existing SpanWrapper / ReadOnlySpanWrapper carve-out (previously routed to a bare inner-type matcher that the user setup never matched). Add `It.IsOutSpan` / `It.IsAnyOutSpan` / `It.IsRefSpan` / `It.IsAnyRefSpan` (and ReadOnlySpan analogs, with full predicate-gated symmetry on the ref side) so users don't have to type the wrapper explicitly. The `.Do(Action<SpanWrapper<T>>)` callback path remains available. `ref readonly` x Span/ReadOnlySpan now falls back to the generic ref-struct pipeline; users reach for the existing `It.IsAnyRefStructRef<Span<T>>()` / `It.IsRef<Span<T>>(...)`. `Helpers.GetMethodParameterType` preserves the full `Span<T>` / `ReadOnlySpan<T>` type in the ref-struct pipeline so those matchers bind correctly. Make `SpanWrapper<T>` / `ReadOnlySpanWrapper<T>` null-safe at the implicit conversion to span: a null wrapper yields `default`, which is the natural no-match fallback for `out` Span/ROS. Also fix two stale coverage file references in the snapshot scenarios (`ICombinationParts.cs` was split into `ICombinationMockA.cs` and `ICombinationMockB.cs`; `KeywordEdgeCases.cs` was renamed to `IKeywordEdgeCases.cs`). Without these the snapshot acceptance test could not run at all.
Aligns ref-struct matcher names with the Span/ReadOnlySpan convention where the parameter modifier (Ref/Out) precedes the type qualifier: - It.IsAnyRefStructRef<T>() -> It.IsAnyRefRefStruct<T>() - It.IsAnyRefStructOut<T>() -> It.IsAnyOutRefStruct<T>() - IRefStructRefParameter<T> -> IRefRefStructParameter<T> - IRefStructOutParameter<T> -> IOutRefStructParameter<T> Also unifies the preprocessor gate for the ref-struct out methods from NET10_0_OR_GREATER to NET9_0_OR_GREATER to match the ref-struct ref methods and the underlying allows-ref-struct delegate types.
- Reject by-value ref-struct parameters on delegate types in MockabilityAnalyzer; the emitted VoidMethodSetup<T> / ReturnMethodSetup<T> lack the 'allows ref struct' constraint and otherwise fail with CS9244. - Strengthen RefReadOnlySpan_WithPredicateOnly_GatesMatchWithoutMutating and RefReadOnlySpan_WithIsAnyRefRefStruct_MatchesViaRefStructPipeline to actually exercise the gate via Throws(), instead of relying on the unchanged-span post-condition that holds even without a matching setup.
5e2ddb2 to
114f5d3
Compare
|
| @@ -304,9 +304,14 @@ private static bool TryGetRefStructIssue(IMethodSymbol method, string? pipelineU | |||
|
|
|||
| hasRefStructParam = true; | |||
|
|
|||
| if (p.RefKind is RefKind.Out or RefKind.Ref or RefKind.RefReadOnlyParameter) | |||
| // Delegates don't go through the ref-struct setup pipeline at all, so any ref-struct | |||
| // parameter (by-value or by-ref) is unsupported on delegate Invoke methods — the | |||
| // emitted VoidMethodSetup<T> / ReturnMethodSetup<T> lacks an 'allows ref struct' | |||
| // constraint. Interface/class methods route through the IOutRefStructParameter / | |||
| // IRefRefStructParameter pipeline. | |||
| if (isDelegate) | |||
| { | |||
| issue = "out/ref ref-struct parameters are not supported"; | |||
| issue = "ref-struct parameters are not supported on delegate types"; | |||
| return true; | |||
| } | |||
| } | |||
…struct parameters (#774) by Valentin Breuß
…struct parameters (#774) by Valentin Breuß
|
This is addressed in release v3.2.0. |



Lift the analyzer and generator rejection of
out,ref, andref readonlyref-struct parameters on interface and class methods, and route them through a newIRefStructOutParameter<T>/IRefStructRefParameter<T>pipeline. Delegates continue to reject these via a narrower analyzer rule.New matchers:
It.IsOut<T>(RefStructFactory<T>),It.IsAnyRefStructOut<T>(),It.IsRef<T>(RefStructTransform<T>),It.IsAnyRefStructRef<T>()plus predicate-gated variants. Setup-side mapping in Helpers.ToParameter() branches on NeedsRefStructPipeline; mock body emits per-slot casts via newGetMatcher{n}()accessors on RefStructVoidMethodSetup / RefStructReturnMethodSetup. Out-slot fallback writesdefault!directly because MockBehavior.DefaultValue cannot produce ref-struct values.