Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c3f56c5
Add benchmarks for DefaultIfNotSingle variants
rjmurillo Jun 1, 2025
2993c6c
Fix: Ensure DefaultIfNotSingle optimizations and tests pass all local…
rjmurillo Jun 8, 2025
79a49d1
Add null checks to DefaultIfNotSingleOptimizedMethod using ArgumentNu…
rjmurillo Jun 8, 2025
4909c58
Rebaseline SquiggleCop
rjmurillo Jun 8, 2025
3de2741
Use ArgumentNullException.ThrowIfNull for parameter validation in Def…
rjmurillo Jun 8, 2025
128aebf
Revert to classic null-check pattern in DefaultIfNotSingle extension …
rjmurillo Jun 8, 2025
bf4a05b
Update SquiggleCop.Baseline.yaml to set IsEverSuppressed to true for …
rjmurillo Jun 8, 2025
9b2502f
Add tests to cover ArgumentNullException for null source and predicat…
rjmurillo Jun 8, 2025
ad58295
fix(test): reset CountingEnumerable<T>.Count at start of each enumera…
rjmurillo Jun 8, 2025
5f03f64
test: add missing usings and use explicit types to fix ECS0900 and fo…
rjmurillo Jun 8, 2025
fc0ed86
test: use int? and Assert.Null for DefaultIfNotSingle results to fix …
rjmurillo Jun 8, 2025
1284a4d
test: add ECS0900-compliant string-based tests for DefaultIfNotSingle…
rjmurillo Jun 8, 2025
f78c22f
test: remove int-based DefaultIfNotSingle null predicate tests to res…
rjmurillo Jun 8, 2025
7cccbda
test: remove int-based null source test for DefaultIfNotSingle to res…
rjmurillo Jun 8, 2025
972bf0c
test: expect 0 (default int) instead of null for DefaultIfNotSingle<i…
rjmurillo Jun 8, 2025
0990b09
test: update DefaultIfNotSingle test to expect null for empty object …
rjmurillo Jun 8, 2025
669f446
chore: update SquiggleCop.Baseline.yaml and test baseline to change E…
rjmurillo Jun 8, 2025
65ff0e0
test: add CountingEnumerable_Count test to verify Count resets on eac…
rjmurillo Jun 8, 2025
7c6fa95
chore: update SquiggleCop.Baseline.yaml and test baseline to change E…
rjmurillo Jun 8, 2025
ae2f96a
refactor: extract argument validation into a separate method for Defa…
rjmurillo Jun 8, 2025
994abca
Update tests/Moq.Analyzers.Test/SquiggleCop.Baseline.yaml
rjmurillo Jun 8, 2025
7c97fea
Resolve merge: accept deletion of SquiggleCop.Baseline.yaml files as …
rjmurillo Jun 8, 2025
14c44f8
Merge branch 'codex/refactor-loop-to-replace-where-predicate' of http…
rjmurillo Jun 8, 2025
68d8648
test: add tests for DefaultIfNotSingle method to verify behavior with…
rjmurillo Jun 8, 2025
6df8c51
test: cover early returns in SetStrictMockBehaviorFixer.RegisterCodeF…
rjmurillo Jun 8, 2025
b433321
test: add DiagnosticEditPropertiesTests to cover all branches and edg…
rjmurillo Jun 8, 2025
caecc7a
refactor: remove untestable early return tests from SetExplicitMockBe…
rjmurillo Jun 8, 2025
a8eb22b
test: enhance DefaultIfNotSingle tests to assert parameter name in Ar…
rjmurillo Jun 8, 2025
ec8564d
Merge branch 'main' into codex/refactor-loop-to-replace-where-predicate
rjmurillo Jun 9, 2025
b197af1
Merge branch 'main' into codex/refactor-loop-to-replace-where-predicate
rjmurillo Jun 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions src/Common/EnumerableExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System;
Comment thread
rjmurillo marked this conversation as resolved.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;

namespace Moq.Analyzers.Common;

Expand All @@ -16,6 +18,11 @@ internal static class EnumerableExtensions
[SuppressMessage("Performance", "ECS0900:Minimize boxing and unboxing", Justification = "Should revisit. Suppressing for now to unblock refactor.")]
public static TSource? DefaultIfNotSingle<TSource>(this ImmutableArray<TSource> source, Func<TSource, bool> predicate)
{
if (predicate == null)
{
throw new ArgumentNullException(nameof(predicate));
}

return source.AsEnumerable().DefaultIfNotSingle(predicate);
}

Expand All @@ -34,11 +41,18 @@ internal static class EnumerableExtensions
/// </remarks>
public static TSource? DefaultIfNotSingle<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
ValidateDefaultIfNotSingleArguments(source, predicate);

bool isFound = false;
TSource? item = default;

foreach (TSource element in source.Where(predicate))
foreach (TSource element in source)
{
if (!predicate(element))
{
continue;
}

if (isFound)
{
// We already found an element, thus there's multiple matches; return default.
Expand All @@ -51,4 +65,18 @@ internal static class EnumerableExtensions

return item;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ValidateDefaultIfNotSingleArguments<TSource>(IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}

if (predicate == null)
{
throw new ArgumentNullException(nameof(predicate));
}
}
}
36 changes: 36 additions & 0 deletions tests/Moq.Analyzers.Benchmarks/DefaultIfNotSingleBaseline.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;
using System.Collections.Immutable;
using System.Linq;

namespace Moq.Analyzers.Benchmarks;

#pragma warning disable ECS0900 // Minimize boxing and unboxing
internal static class DefaultIfNotSingleBaseline
{
public static T? DefaultIfNotSingleBaselineMethod<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
Comment thread
coderabbitai[bot] marked this conversation as resolved.
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(predicate);
bool found = false;
T? item = default;
foreach (T element in source.Where(predicate))
{
if (found)
{
return default;
}

found = true;
item = element;
}

return item;
}

public static T? DefaultIfNotSingleBaselineMethod<T>(this IEnumerable<T> source)
=> source.DefaultIfNotSingleBaselineMethod(static _ => true);

public static T? DefaultIfNotSingleBaselineMethod<T>(this ImmutableArray<T> source, Func<T, bool> predicate)
=> source.AsEnumerable().DefaultIfNotSingleBaselineMethod(predicate);
#pragma warning restore ECS0900
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Linq;
using BenchmarkDotNet.Attributes;

namespace Moq.Analyzers.Benchmarks;

[MemoryDiagnoser]
[InProcess]
public class DefaultIfNotSingleEnumerableBenchmarks
{
private readonly IEnumerable<int> _source = Enumerable.Range(0, 100).ToArray();

[Benchmark(Baseline = true)]
public int? Baseline() => _source.DefaultIfNotSingleBaselineMethod(x => x == 50);

[Benchmark]
public int? Optimized() => _source.DefaultIfNotSingleOptimizedMethod(x => x == 50);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Linq;
using BenchmarkDotNet.Attributes;

namespace Moq.Analyzers.Benchmarks;

[MemoryDiagnoser]
[InProcess]
public class DefaultIfNotSingleEnumerableNoPredicateBenchmarks
{
private readonly IEnumerable<int> _source = new[] { 0 };

[Benchmark(Baseline = true)]
public int? Baseline() => _source.DefaultIfNotSingleBaselineMethod();

[Benchmark]
public int? Optimized() => _source.DefaultIfNotSingleOptimizedMethod();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Collections.Immutable;
using System.Linq;
using BenchmarkDotNet.Attributes;

namespace Moq.Analyzers.Benchmarks;

[MemoryDiagnoser]
[InProcess]
public class DefaultIfNotSingleImmutableArrayBenchmarks
{
private readonly ImmutableArray<int> _source = ImmutableArray.CreateRange(Enumerable.Range(0, 100));

#pragma warning disable ECS0900 // Minimize boxing and unboxing
[Benchmark(Baseline = true)]
public int? Baseline() => _source.DefaultIfNotSingleBaselineMethod(x => x == 50);

[Benchmark]
public int? Optimized() => _source.DefaultIfNotSingleOptimizedMethod(x => x == 50);
#pragma warning restore ECS0900
}
41 changes: 41 additions & 0 deletions tests/Moq.Analyzers.Benchmarks/DefaultIfNotSingleOptimized.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System;
using System.Collections.Immutable;
using System.Linq;

namespace Moq.Analyzers.Benchmarks;

#pragma warning disable ECS0900 // Minimize boxing and unboxing
internal static class DefaultIfNotSingleOptimized
{
public static T? DefaultIfNotSingleOptimizedMethod<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
Comment thread
coderabbitai[bot] marked this conversation as resolved.
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(predicate);
bool found = false;
T? item = default;
foreach (T element in source)
{
if (!predicate(element))
{
continue;
}

if (found)
{
return default;
}

found = true;
item = element;
}

return item;
}

public static T? DefaultIfNotSingleOptimizedMethod<T>(this IEnumerable<T> source)
=> source.DefaultIfNotSingleOptimizedMethod(static _ => true);

public static T? DefaultIfNotSingleOptimizedMethod<T>(this ImmutableArray<T> source, Func<T, bool> predicate)
=> source.AsEnumerable().DefaultIfNotSingleOptimizedMethod(predicate);
#pragma warning restore ECS0900
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System.Collections.Immutable;
using Moq.Analyzers.Common;
using Xunit;

namespace Moq.Analyzers.Test.Common;

public class DiagnosticEditPropertiesTests
{
[Fact]
public void ToImmutableDictionary_RoundTripsProperties()
{
DiagnosticEditProperties props = new DiagnosticEditProperties { TypeOfEdit = DiagnosticEditProperties.EditType.Insert, EditPosition = 2 };
ImmutableDictionary<string, string?> dict = props.ToImmutableDictionary();
Assert.Equal("Insert", dict[DiagnosticEditProperties.EditTypeKey]);
Assert.Equal("2", dict[DiagnosticEditProperties.EditPositionKey]);
}

[Fact]
public void TryGetFromImmutableDictionary_Succeeds_WithValidDictionary()
{
ImmutableDictionary<string, string?> dict = ImmutableDictionary<string, string?>.Empty
.Add(DiagnosticEditProperties.EditTypeKey, "Replace")
.Add(DiagnosticEditProperties.EditPositionKey, "5");
bool result = DiagnosticEditProperties.TryGetFromImmutableDictionary(dict, out DiagnosticEditProperties? props);
Assert.True(result);
Assert.NotNull(props);
Assert.Equal(DiagnosticEditProperties.EditType.Replace, props!.TypeOfEdit);
Assert.Equal(5, props.EditPosition);
}

[Fact]
public void TryGetFromImmutableDictionary_Fails_WhenEditTypeKeyMissing()
{
ImmutableDictionary<string, string?> dict = ImmutableDictionary<string, string?>.Empty.Add(DiagnosticEditProperties.EditPositionKey, "1");
bool result = DiagnosticEditProperties.TryGetFromImmutableDictionary(dict, out DiagnosticEditProperties? props);
Assert.False(result);
Assert.Null(props);
}

[Fact]
public void TryGetFromImmutableDictionary_Fails_WhenEditPositionKeyMissing()
{
ImmutableDictionary<string, string?> dict = ImmutableDictionary<string, string?>.Empty.Add(DiagnosticEditProperties.EditTypeKey, "Insert");
bool result = DiagnosticEditProperties.TryGetFromImmutableDictionary(dict, out DiagnosticEditProperties? props);
Assert.False(result);
Assert.Null(props);
}

[Fact]
public void TryGetFromImmutableDictionary_Fails_WhenEditTypeInvalid()
{
ImmutableDictionary<string, string?> dict = ImmutableDictionary<string, string?>.Empty
.Add(DiagnosticEditProperties.EditTypeKey, "NotAType")
.Add(DiagnosticEditProperties.EditPositionKey, "1");
bool result = DiagnosticEditProperties.TryGetFromImmutableDictionary(dict, out DiagnosticEditProperties? props);
Assert.False(result);
Assert.Null(props);
}

[Fact]
public void TryGetFromImmutableDictionary_Fails_WhenEditPositionInvalid()
{
ImmutableDictionary<string, string?> dict = ImmutableDictionary<string, string?>.Empty
.Add(DiagnosticEditProperties.EditTypeKey, "Insert")
.Add(DiagnosticEditProperties.EditPositionKey, "notAnInt");
bool result = DiagnosticEditProperties.TryGetFromImmutableDictionary(dict, out DiagnosticEditProperties? props);
Assert.False(result);
Assert.Null(props);
}
}
Loading
Loading