Skip to content

Commit 0d79513

Browse files
committed
fix: harden params matchers against null arrays and null element matchers
Guard the typed IParameterMatch<T[]> dispatch of It.SequenceEquals and It.Contains against a null collection value, so a recorded null params array yields a non-match instead of a NullReferenceException. Previously only the non-generic IParameter.Matches(object?) path was null-safe. Also guard ParamsArrayParameterMatch against null per-element matchers in both Matches and InvokeCallbacks, and add direct unit tests covering the match/callback/ToString branches.
1 parent 1fb37ca commit 0d79513

7 files changed

Lines changed: 180 additions & 10 deletions

File tree

Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3235,7 +3235,7 @@ bool MethodPredicate(Method method)
32353235
{
32363236
AppendMethodSetupDefinition(sb, @class, method, false,
32373237
hasOverloadResolutionPriority: hasOverloadResolutionPriority);
3238-
if (TryGetPerElementParamsParameter(method, out _))
3238+
if (TryGetPerElementParamsParameter(method))
32393239
{
32403240
AppendMethodSetupDefinition(sb, @class, method, false,
32413241
hasOverloadResolutionPriority: hasOverloadResolutionPriority, perElementParams: true);
@@ -3321,11 +3321,11 @@ private static void AppendOverloadDifferentiatorRemark(StringBuilder sb,
33213321
/// <summary>
33223322
/// Detects whether <paramref name="method" /> ends in a <c>params T[]</c> parameter that can carry a
33233323
/// per-element matcher overload. The element type must flow through the regular <c>IParameter&lt;T&gt;</c>
3324-
/// pipeline (ref-struct element types are excluded for this prototype).
3324+
/// pipeline; ref-struct element types are intentionally not supported, as they cannot satisfy
3325+
/// <c>IParameter&lt;T&gt;</c>.
33253326
/// </summary>
3326-
private static bool TryGetPerElementParamsParameter(Method method, out string elementType)
3327+
private static bool TryGetPerElementParamsParameter(Method method)
33273328
{
3328-
elementType = "";
33293329
if (method.Parameters.Count == 0)
33303330
{
33313331
return false;
@@ -3337,7 +3337,6 @@ private static bool TryGetPerElementParamsParameter(Method method, out string el
33373337
return false;
33383338
}
33393339

3340-
elementType = last.Type.ElementType.Fullname;
33413340
return true;
33423341
}
33433342

@@ -3703,7 +3702,7 @@ bool MethodPredicate(Method method)
37033702
{
37043703
AppendMethodSetupImplementation(sb, method, mockRegistryName, setupName, false,
37053704
memberIds, memberIdPrefix, scopeExpression: scopeExpression);
3706-
if (TryGetPerElementParamsParameter(method, out _))
3705+
if (TryGetPerElementParamsParameter(method))
37073706
{
37083707
AppendMethodSetupImplementation(sb, method, mockRegistryName, setupName, false,
37093708
memberIds, memberIdPrefix, scopeExpression: scopeExpression, perElementParams: true);
@@ -5083,7 +5082,7 @@ bool MethodPredicate(Method method)
50835082
{
50845083
AppendMethodVerifyDefinition(sb, method, verifyName, false,
50855084
hasOverloadResolutionPriority: hasOverloadResolutionPriority);
5086-
if (TryGetPerElementParamsParameter(method, out _))
5085+
if (TryGetPerElementParamsParameter(method))
50875086
{
50885087
AppendMethodVerifyDefinition(sb, method, verifyName, false,
50895088
hasOverloadResolutionPriority: hasOverloadResolutionPriority, perElementParams: true);
@@ -5368,7 +5367,7 @@ bool MethodPredicate(Method method)
53685367
{
53695368
AppendMethodVerifyImplementation(sb, method, mockRegistryName, verifyName, false,
53705369
memberIds, memberIdPrefix, useFastBuffers);
5371-
if (TryGetPerElementParamsParameter(method, out _))
5370+
if (TryGetPerElementParamsParameter(method))
53725371
{
53735372
AppendMethodVerifyImplementation(sb, method, mockRegistryName, verifyName, false,
53745373
memberIds, memberIdPrefix, useFastBuffers, perElementParams: true);

Source/Mockolate/It.Contains.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ IContainsParameter<T> IContainsParameter<T>.Using(IEqualityComparer<T> comparer,
7575
/// <inheritdoc cref="CollectionMatchCore{T}.MatchesCollection(IEnumerable{T})" />
7676
protected override bool MatchesCollection(IEnumerable<T> value)
7777
{
78+
if (value is null)
79+
{
80+
return false;
81+
}
82+
7883
IEqualityComparer<T> comparer = _comparer ?? EqualityComparer<T>.Default;
7984
return value.Contains(item, comparer);
8085
}

Source/Mockolate/It.SequenceEquals.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ ISequenceEqualsParameter<T> ISequenceEqualsParameter<T>.Using(IEqualityComparer<
7272
/// <inheritdoc cref="CollectionMatchCore{T}.MatchesCollection(IEnumerable{T})" />
7373
protected override bool MatchesCollection(IEnumerable<T> value)
7474
{
75+
if (value is null)
76+
{
77+
return false;
78+
}
79+
7580
IEqualityComparer<T> comparer = _comparer ?? EqualityComparer<T>.Default;
7681
return value.SequenceEqual(expected, comparer);
7782
}

Source/Mockolate/Parameters/ParamsArrayParameterMatch.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public bool Matches(TElement[] value)
3737

3838
for (int i = 0; i < _matchers.Length; i++)
3939
{
40-
if (!_matchers[i].Matches(value[i]))
40+
if (_matchers[i] is null || !_matchers[i].Matches(value[i]))
4141
{
4242
return false;
4343
}
@@ -56,7 +56,7 @@ public void InvokeCallbacks(TElement[] value)
5656

5757
for (int i = 0; i < _matchers.Length; i++)
5858
{
59-
_matchers[i].InvokeCallbacks(value[i]);
59+
_matchers[i]?.InvokeCallbacks(value[i]);
6060
}
6161
}
6262

Tests/Mockolate.Tests/ItTests.ContainsTests.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,16 @@ public async Task ShouldNotMatchWhenValueIsNull()
157157
await That(result).IsFalse();
158158
}
159159

160+
[Fact]
161+
public async Task TypedArrayMatch_WithNullValue_ShouldNotMatch()
162+
{
163+
IParameter<int[]> sut = It.Contains(5);
164+
165+
bool result = ((IParameterMatch<int[]>)sut).Matches(null!);
166+
167+
await That(result).IsFalse();
168+
}
169+
160170
[Fact]
161171
public async Task ShouldSupportVerify()
162172
{

Tests/Mockolate.Tests/ItTests.SequenceEqualsTests.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,16 @@ public async Task WithComparer_ShouldUseComparer()
193193
await That(result).IsTrue();
194194
}
195195

196+
[Fact]
197+
public async Task TypedArrayMatch_WithNullValue_ShouldNotMatch()
198+
{
199+
IParameter<int[]> sut = It.SequenceEquals(1, 2, 3);
200+
201+
bool result = ((IParameterMatch<int[]>)sut).Matches(null!);
202+
203+
await That(result).IsFalse();
204+
}
205+
196206
public sealed class DoTests
197207
{
198208
[Fact]
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
using System.Collections.Generic;
2+
using Mockolate.Parameters;
3+
4+
namespace Mockolate.Tests.Parameters;
5+
6+
public sealed class ParamsArrayParameterMatchTests
7+
{
8+
[Fact]
9+
public async Task Matches_WhenLengthMatchesAndAllElementsSatisfy_ShouldReturnTrue()
10+
{
11+
StubParameter first = new(true, "first");
12+
StubParameter second = new(true, "second");
13+
ParamsArrayParameterMatch<int> sut = new(first, second);
14+
15+
bool result = sut.Matches([1, 2,]);
16+
17+
await That(result).IsTrue();
18+
await That(first.MatchedValues).IsEqualTo([1,]);
19+
await That(second.MatchedValues).IsEqualTo([2,]);
20+
}
21+
22+
[Fact]
23+
public async Task Matches_WhenAnElementFails_ShouldReturnFalse()
24+
{
25+
ParamsArrayParameterMatch<int> sut = new(new StubParameter(true, "first"), new StubParameter(false, "second"));
26+
27+
bool result = sut.Matches([1, 2,]);
28+
29+
await That(result).IsFalse();
30+
}
31+
32+
[Fact]
33+
public async Task Matches_WhenLengthDiffers_ShouldReturnFalseWithoutInvokingMatchers()
34+
{
35+
StubParameter only = new(true, "only");
36+
ParamsArrayParameterMatch<int> sut = new(only);
37+
38+
bool result = sut.Matches([1, 2,]);
39+
40+
await That(result).IsFalse();
41+
await That(only.MatchedValues).IsEmpty();
42+
}
43+
44+
[Fact]
45+
public async Task Matches_WhenValueIsNull_ShouldReturnFalse()
46+
{
47+
ParamsArrayParameterMatch<int> sut = new(new StubParameter(true, "only"));
48+
49+
bool result = sut.Matches(null!);
50+
51+
await That(result).IsFalse();
52+
}
53+
54+
[Fact]
55+
public async Task Matches_WhenMatcherElementIsNull_ShouldReturnFalseWithoutThrowing()
56+
{
57+
ParamsArrayParameterMatch<int> sut = new(new StubParameter(true, "first"), null!);
58+
59+
bool result = sut.Matches([1, 2,]);
60+
61+
await That(result).IsFalse();
62+
}
63+
64+
[Fact]
65+
public async Task InvokeCallbacks_WhenLengthMatches_ShouldInvokeEachMatcher()
66+
{
67+
StubParameter first = new(true, "first");
68+
StubParameter second = new(true, "second");
69+
ParamsArrayParameterMatch<int> sut = new(first, second);
70+
71+
sut.InvokeCallbacks([1, 2,]);
72+
73+
await That(first.InvokedValues).IsEqualTo([1,]);
74+
await That(second.InvokedValues).IsEqualTo([2,]);
75+
}
76+
77+
[Fact]
78+
public async Task InvokeCallbacks_WhenLengthDiffers_ShouldNotInvokeMatchers()
79+
{
80+
StubParameter only = new(true, "only");
81+
ParamsArrayParameterMatch<int> sut = new(only);
82+
83+
sut.InvokeCallbacks([1, 2,]);
84+
85+
await That(only.InvokedValues).IsEmpty();
86+
}
87+
88+
[Fact]
89+
public async Task InvokeCallbacks_WhenValueIsNull_ShouldNotInvokeMatchers()
90+
{
91+
StubParameter only = new(true, "only");
92+
ParamsArrayParameterMatch<int> sut = new(only);
93+
94+
sut.InvokeCallbacks(null!);
95+
96+
await That(only.InvokedValues).IsEmpty();
97+
}
98+
99+
[Fact]
100+
public async Task InvokeCallbacks_WhenMatcherElementIsNull_ShouldSkipItWithoutThrowing()
101+
{
102+
StubParameter first = new(true, "first");
103+
ParamsArrayParameterMatch<int> sut = new(first, null!);
104+
105+
sut.InvokeCallbacks([1, 2,]);
106+
107+
await That(first.InvokedValues).IsEqualTo([1,]);
108+
}
109+
110+
[Fact]
111+
public async Task ToString_ShouldRenderMatchersInOrder()
112+
{
113+
ParamsArrayParameterMatch<int> sut = new(new StubParameter(true, "first"), new StubParameter(true, "second"));
114+
115+
await That(sut.ToString()).IsEqualTo("[first, second]");
116+
}
117+
118+
[Fact]
119+
public async Task ToString_WithNullMatcher_ShouldRenderNullToken()
120+
{
121+
ParamsArrayParameterMatch<int> sut = new(new StubParameter(true, "first"), null!);
122+
123+
await That(sut.ToString()).IsEqualTo("[first, null]");
124+
}
125+
126+
private sealed class StubParameter(bool matches, string label) : IParameter<int>
127+
{
128+
public List<int> MatchedValues { get; } = [];
129+
public List<int> InvokedValues { get; } = [];
130+
131+
public bool Matches(object? value)
132+
{
133+
MatchedValues.Add((int)value!);
134+
return matches;
135+
}
136+
137+
public void InvokeCallbacks(object? value) => InvokedValues.Add((int)value!);
138+
139+
public override string ToString() => label;
140+
}
141+
}

0 commit comments

Comments
 (0)