From 5e4a2243fedcb6b4764bcbd21bcdf2018068623b Mon Sep 17 00:00:00 2001 From: Richard Murillo Date: Mon, 3 Feb 2025 14:34:17 -0800 Subject: [PATCH 01/15] Add report generation tools for tests --- .config/dotnet-tools.json | 7 +++++++ build/targets/tests/Packages.props | 1 + 2 files changed, 8 insertions(+) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index b1ead7d6f..70108a480 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -22,6 +22,13 @@ "dotnet-squigglecop" ], "rollForward": false + }, + "dotnet-reportgenerator-globaltool": { + "version": "5.4.3", + "commands": [ + "reportgenerator" + ], + "rollForward": false } } } \ No newline at end of file diff --git a/build/targets/tests/Packages.props b/build/targets/tests/Packages.props index 4e25aa8ad..d88df00db 100644 --- a/build/targets/tests/Packages.props +++ b/build/targets/tests/Packages.props @@ -8,5 +8,6 @@ + From 9894756424ba2e4fab2e8bf8dd3f098371ec0744 Mon Sep 17 00:00:00 2001 From: Richard Murillo Date: Mon, 3 Feb 2025 14:36:39 -0800 Subject: [PATCH 02/15] Fix typos in comments --- src/Common/DiagnosticEditProperties.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Common/DiagnosticEditProperties.cs b/src/Common/DiagnosticEditProperties.cs index b485c54a6..09cf5454a 100644 --- a/src/Common/DiagnosticEditProperties.cs +++ b/src/Common/DiagnosticEditProperties.cs @@ -37,7 +37,7 @@ internal enum EditType /// /// Returns the current object as an . /// - /// The current objbect as an immutable dictionary. + /// The current object as an immutable dictionary. public ImmutableDictionary ToImmutableDictionary() { return new Dictionary(StringComparer.Ordinal) @@ -48,10 +48,10 @@ internal enum EditType } /// - /// Tries to convert an immuatble dictionary to a . + /// Tries to convert an immutable dictionary to a . /// /// The dictionary to try to convert. - /// The output edit properties if parsing suceeded, otherwise null. + /// The output edit properties if parsing succeeded, otherwise null. /// true if parsing succeeded; false otherwise. public static bool TryGetFromImmutableDictionary(ImmutableDictionary dictionary, [NotNullWhen(true)] out DiagnosticEditProperties? editProperties) { From de1e7069f5573a8faf3c698b01b80ce9456a0db8 Mon Sep 17 00:00:00 2001 From: Richard Murillo Date: Mon, 3 Feb 2025 14:42:03 -0800 Subject: [PATCH 03/15] Change initialization to match project convention --- src/Common/DiagnosticEditProperties.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Common/DiagnosticEditProperties.cs b/src/Common/DiagnosticEditProperties.cs index 09cf5454a..9c9469f4f 100644 --- a/src/Common/DiagnosticEditProperties.cs +++ b/src/Common/DiagnosticEditProperties.cs @@ -27,12 +27,12 @@ internal enum EditType /// /// Gets the type of edit operation to perform. /// - public EditType TypeOfEdit { get; init; } + public EditType TypeOfEdit { get; private init; } /// /// Gets the zero-based position where the edit should be applied. /// - public int EditPosition { get; init; } + public int EditPosition { get; private init; } /// /// Returns the current object as an . From 8dde1ce71d847efaf874b7592e14b37d2050b4dc Mon Sep 17 00:00:00 2001 From: Richard Murillo Date: Mon, 3 Feb 2025 14:43:03 -0800 Subject: [PATCH 04/15] Revert "Change initialization to match project convention" This reverts commit de1e7069f5573a8faf3c698b01b80ce9456a0db8. --- src/Common/DiagnosticEditProperties.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Common/DiagnosticEditProperties.cs b/src/Common/DiagnosticEditProperties.cs index 9c9469f4f..09cf5454a 100644 --- a/src/Common/DiagnosticEditProperties.cs +++ b/src/Common/DiagnosticEditProperties.cs @@ -27,12 +27,12 @@ internal enum EditType /// /// Gets the type of edit operation to perform. /// - public EditType TypeOfEdit { get; private init; } + public EditType TypeOfEdit { get; init; } /// /// Gets the zero-based position where the edit should be applied. /// - public int EditPosition { get; private init; } + public int EditPosition { get; init; } /// /// Returns the current object as an . From ed84e940112740129ae53f1fcb5f1e1053c9b49e Mon Sep 17 00:00:00 2001 From: Richard Murillo Date: Mon, 3 Feb 2025 14:48:30 -0800 Subject: [PATCH 05/15] Remove unused code --- src/Common/EnumerableExtensions.cs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/Common/EnumerableExtensions.cs b/src/Common/EnumerableExtensions.cs index d523b4863..f34294f7d 100644 --- a/src/Common/EnumerableExtensions.cs +++ b/src/Common/EnumerableExtensions.cs @@ -10,12 +10,6 @@ internal static class EnumerableExtensions return source.DefaultIfNotSingle(static _ => true); } - /// - public static TSource? DefaultIfNotSingle(this ImmutableArray source) - { - return source.DefaultIfNotSingle(static _ => true); - } - /// /// The collection to enumerate. /// A function to test each element for a condition. @@ -57,16 +51,4 @@ internal static class EnumerableExtensions return item; } - - public static IEnumerable WhereNotNull(this IEnumerable source) - where TSource : class - { - return source.Where(item => item is not null)!; - } - - public static IEnumerable WhereNotNull(this IEnumerable source) - where TSource : struct - { - return source.Where(item => item.HasValue).Select(item => item!.Value); - } } From 0a235c2974421bcf7be6a0524340bea08bf7ace0 Mon Sep 17 00:00:00 2001 From: Richard Murillo Date: Mon, 3 Feb 2025 15:09:59 -0800 Subject: [PATCH 06/15] Add test project for common elements --- Moq.Analyzers.sln | 8 +++++++ .../Moq.Analyzers.Common.Tests.csproj | 22 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 tests/Moq.Analyzers.Common.Tests/Moq.Analyzers.Common.Tests.csproj diff --git a/Moq.Analyzers.sln b/Moq.Analyzers.sln index e317f228e..8c14ceab0 100644 --- a/Moq.Analyzers.sln +++ b/Moq.Analyzers.sln @@ -17,6 +17,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Analyzers.Test.Analyzer EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "src\Common\Common.csproj", "{622DB72F-5609-4C08-838D-6937A680094A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moq.Analyzers.Common.Tests", "tests\Moq.Analyzers.Common.Tests\Moq.Analyzers.Common.Tests.csproj", "{71AE51EE-8ADA-438E-BF73-1EE2DBC18BC3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -47,6 +49,10 @@ Global {622DB72F-5609-4C08-838D-6937A680094A}.Debug|Any CPU.Build.0 = Debug|Any CPU {622DB72F-5609-4C08-838D-6937A680094A}.Release|Any CPU.ActiveCfg = Release|Any CPU {622DB72F-5609-4C08-838D-6937A680094A}.Release|Any CPU.Build.0 = Release|Any CPU + {71AE51EE-8ADA-438E-BF73-1EE2DBC18BC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {71AE51EE-8ADA-438E-BF73-1EE2DBC18BC3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {71AE51EE-8ADA-438E-BF73-1EE2DBC18BC3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {71AE51EE-8ADA-438E-BF73-1EE2DBC18BC3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -55,6 +61,7 @@ Global {D2348836-7129-4BE5-8AE6-D05FC8C28FC1} = {013D4F80-9978-45DA-9BB8-6638239355E4} {11B3412F-456C-452E-94D2-B42D5C52F61C} = {013D4F80-9978-45DA-9BB8-6638239355E4} {C68B6F38-838B-4BD9-963D-95779B6F418B} = {013D4F80-9978-45DA-9BB8-6638239355E4} + {71AE51EE-8ADA-438E-BF73-1EE2DBC18BC3} = {013D4F80-9978-45DA-9BB8-6638239355E4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8C917BC1-C0DE-4A46-BEE8-32FD66B447B1} @@ -62,6 +69,7 @@ Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Common\Common.projitems*{41ecc571-f586-460a-9bed-23528c8210c4}*SharedItemsImports = 5 src\Common\Common.projitems*{622db72f-5609-4c08-838d-6937a680094a}*SharedItemsImports = 5 + src\Common\Common.projitems*{71ae51ee-8ada-438e-bf73-1ee2dbc18bc3}*SharedItemsImports = 5 src\Common\Common.projitems*{8e99c15c-e80a-49e5-988c-1b5071ce775f}*SharedItemsImports = 5 EndGlobalSection EndGlobal diff --git a/tests/Moq.Analyzers.Common.Tests/Moq.Analyzers.Common.Tests.csproj b/tests/Moq.Analyzers.Common.Tests/Moq.Analyzers.Common.Tests.csproj new file mode 100644 index 000000000..1af8c344b --- /dev/null +++ b/tests/Moq.Analyzers.Common.Tests/Moq.Analyzers.Common.Tests.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + false + true + enable + enable + + + + + + + + + + From 588d569763d8b2e3ff0615c3d2538e2e3b261ac3 Mon Sep 17 00:00:00 2001 From: Richard Murillo Date: Mon, 3 Feb 2025 15:10:07 -0800 Subject: [PATCH 07/15] Add unit tests for ArrayExtensions --- .../ArrayExtensionTests.cs | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 tests/Moq.Analyzers.Common.Tests/ArrayExtensionTests.cs diff --git a/tests/Moq.Analyzers.Common.Tests/ArrayExtensionTests.cs b/tests/Moq.Analyzers.Common.Tests/ArrayExtensionTests.cs new file mode 100644 index 000000000..4aaed0011 --- /dev/null +++ b/tests/Moq.Analyzers.Common.Tests/ArrayExtensionTests.cs @@ -0,0 +1,81 @@ +namespace Moq.Analyzers.Common.Tests; + +public class ArrayExtensionTests +{ + [Fact] + public void RemoveAt_RemovesElementAtIndex() + { + // Arrange + int[] actual = [1, 2, 3, 4, 5]; + int[] expected = [1, 2, 3, 4, 5]; + const int indexToRemove = 2; + + // Act + int[] result = actual.RemoveAt(indexToRemove); + + // Assert + Assert.Equal(expected, result); + } + + [Fact] + public void RemoveAt_FirstElement_RemovesCorrectly() + { + // Arrange + int[] input = [1, 2, 3]; + int[] expected = [2, 3]; + + // Act + int[] result = input.RemoveAt(0); + + // Assert + Assert.Equal(expected, result); + } + + [Fact] + public void RemoveAt_LastElement_RemovesCorrectly() + { + // Arrange + int[] input = [1, 2, 3]; + int[] expected = [1, 2]; + + // Act + int[] result = input.RemoveAt(input.Length - 1); + + // Assert + Assert.Equal(expected, result); + } + + [Fact] + public void RemoveAt_SingleElementArray_ReturnsEmptyArray() + { + // Arrange + int[] input = [42]; + + // Act + int[] result = input.RemoveAt(0); + + // Assert + Assert.Empty(result); + } + + [Fact] + public void RemoveAt_IndexOutOfRange_ThrowsException() + { + // Arrange + int[] input = [1, 2, 3]; + + // Act & Assert + Assert.Throws(() => input.RemoveAt(-1)); + Assert.Throws(() => input.RemoveAt(3)); + } + + [Fact] + public void RemoveAt_EmptyArray_ThrowsException() + { + // Arrange + int[] input = []; + + // Act & Assert + Assert.Throws(() => input.RemoveAt(0)); + } +} From 1fa77a88efb708f779fc25efd58258b7c50a0fc6 Mon Sep 17 00:00:00 2001 From: Richard Murillo Date: Mon, 3 Feb 2025 15:23:47 -0800 Subject: [PATCH 08/15] Move extension tests to existing unit test project --- Moq.Analyzers.sln | 9 +------- .../Moq.Analyzers.Common.Tests.csproj | 22 ------------------- .../Common}/ArrayExtensionTests.cs | 4 ++-- .../Moq.Analyzers.Test.csproj | 3 +++ 4 files changed, 6 insertions(+), 32 deletions(-) delete mode 100644 tests/Moq.Analyzers.Common.Tests/Moq.Analyzers.Common.Tests.csproj rename tests/{Moq.Analyzers.Common.Tests => Moq.Analyzers.Test/Common}/ArrayExtensionTests.cs (95%) diff --git a/Moq.Analyzers.sln b/Moq.Analyzers.sln index 8c14ceab0..9247a2a67 100644 --- a/Moq.Analyzers.sln +++ b/Moq.Analyzers.sln @@ -17,8 +17,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Analyzers.Test.Analyzer EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "src\Common\Common.csproj", "{622DB72F-5609-4C08-838D-6937A680094A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moq.Analyzers.Common.Tests", "tests\Moq.Analyzers.Common.Tests\Moq.Analyzers.Common.Tests.csproj", "{71AE51EE-8ADA-438E-BF73-1EE2DBC18BC3}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -49,10 +47,6 @@ Global {622DB72F-5609-4C08-838D-6937A680094A}.Debug|Any CPU.Build.0 = Debug|Any CPU {622DB72F-5609-4C08-838D-6937A680094A}.Release|Any CPU.ActiveCfg = Release|Any CPU {622DB72F-5609-4C08-838D-6937A680094A}.Release|Any CPU.Build.0 = Release|Any CPU - {71AE51EE-8ADA-438E-BF73-1EE2DBC18BC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {71AE51EE-8ADA-438E-BF73-1EE2DBC18BC3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {71AE51EE-8ADA-438E-BF73-1EE2DBC18BC3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {71AE51EE-8ADA-438E-BF73-1EE2DBC18BC3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -61,7 +55,6 @@ Global {D2348836-7129-4BE5-8AE6-D05FC8C28FC1} = {013D4F80-9978-45DA-9BB8-6638239355E4} {11B3412F-456C-452E-94D2-B42D5C52F61C} = {013D4F80-9978-45DA-9BB8-6638239355E4} {C68B6F38-838B-4BD9-963D-95779B6F418B} = {013D4F80-9978-45DA-9BB8-6638239355E4} - {71AE51EE-8ADA-438E-BF73-1EE2DBC18BC3} = {013D4F80-9978-45DA-9BB8-6638239355E4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8C917BC1-C0DE-4A46-BEE8-32FD66B447B1} @@ -69,7 +62,7 @@ Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Common\Common.projitems*{41ecc571-f586-460a-9bed-23528c8210c4}*SharedItemsImports = 5 src\Common\Common.projitems*{622db72f-5609-4c08-838d-6937a680094a}*SharedItemsImports = 5 - src\Common\Common.projitems*{71ae51ee-8ada-438e-bf73-1ee2dbc18bc3}*SharedItemsImports = 5 src\Common\Common.projitems*{8e99c15c-e80a-49e5-988c-1b5071ce775f}*SharedItemsImports = 5 + src\Common\Common.projitems*{d2348836-7129-4be5-8ae6-d05fc8c28fc1}*SharedItemsImports = 5 EndGlobalSection EndGlobal diff --git a/tests/Moq.Analyzers.Common.Tests/Moq.Analyzers.Common.Tests.csproj b/tests/Moq.Analyzers.Common.Tests/Moq.Analyzers.Common.Tests.csproj deleted file mode 100644 index 1af8c344b..000000000 --- a/tests/Moq.Analyzers.Common.Tests/Moq.Analyzers.Common.Tests.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - net8.0 - false - true - enable - enable - - - - - - - - - - diff --git a/tests/Moq.Analyzers.Common.Tests/ArrayExtensionTests.cs b/tests/Moq.Analyzers.Test/Common/ArrayExtensionTests.cs similarity index 95% rename from tests/Moq.Analyzers.Common.Tests/ArrayExtensionTests.cs rename to tests/Moq.Analyzers.Test/Common/ArrayExtensionTests.cs index 4aaed0011..a303fcd52 100644 --- a/tests/Moq.Analyzers.Common.Tests/ArrayExtensionTests.cs +++ b/tests/Moq.Analyzers.Test/Common/ArrayExtensionTests.cs @@ -1,4 +1,4 @@ -namespace Moq.Analyzers.Common.Tests; +namespace Moq.Analyzers.Test.Common; public class ArrayExtensionTests { @@ -7,7 +7,7 @@ public void RemoveAt_RemovesElementAtIndex() { // Arrange int[] actual = [1, 2, 3, 4, 5]; - int[] expected = [1, 2, 3, 4, 5]; + int[] expected = [1, 2, 4, 5]; const int indexToRemove = 2; // Act diff --git a/tests/Moq.Analyzers.Test/Moq.Analyzers.Test.csproj b/tests/Moq.Analyzers.Test/Moq.Analyzers.Test.csproj index 2e46677bf..4d3f52517 100644 --- a/tests/Moq.Analyzers.Test/Moq.Analyzers.Test.csproj +++ b/tests/Moq.Analyzers.Test/Moq.Analyzers.Test.csproj @@ -25,9 +25,12 @@ + + + From eec9e70299b212841822d743f860ba8e07113aa7 Mon Sep 17 00:00:00 2001 From: Richard Murillo Date: Mon, 3 Feb 2025 15:31:32 -0800 Subject: [PATCH 09/15] Replace tail recursive implementation FindSetupMethodFromCallbackInvocation with loop --- src/Common/SemanticModelExtensions.cs | 35 +++++++++++++++------------ 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/Common/SemanticModelExtensions.cs b/src/Common/SemanticModelExtensions.cs index 08620c0b9..d03646480 100644 --- a/src/Common/SemanticModelExtensions.cs +++ b/src/Common/SemanticModelExtensions.cs @@ -9,24 +9,27 @@ internal static class SemanticModelExtensions { internal static InvocationExpressionSyntax? FindSetupMethodFromCallbackInvocation(this SemanticModel semanticModel, MoqKnownSymbols knownSymbols, ExpressionSyntax expression, CancellationToken cancellationToken) { - InvocationExpressionSyntax? invocation = expression as InvocationExpressionSyntax; - if (invocation?.Expression is not MemberAccessExpressionSyntax method) + while (true) { - return null; + InvocationExpressionSyntax? invocation = expression as InvocationExpressionSyntax; + if (invocation?.Expression is not MemberAccessExpressionSyntax method) + { + return null; + } + + SymbolInfo symbolInfo = semanticModel.GetSymbolInfo(method, cancellationToken); + if (symbolInfo.Symbol is null) + { + return null; + } + + if (semanticModel.IsMoqSetupMethod(knownSymbols, symbolInfo.Symbol, cancellationToken)) + { + return invocation; + } + + expression = method.Expression; } - - SymbolInfo symbolInfo = semanticModel.GetSymbolInfo(method, cancellationToken); - if (symbolInfo.Symbol is null) - { - return null; - } - - if (semanticModel.IsMoqSetupMethod(knownSymbols, symbolInfo.Symbol, cancellationToken)) - { - return invocation; - } - - return semanticModel.FindSetupMethodFromCallbackInvocation(knownSymbols, method.Expression, cancellationToken); } internal static IEnumerable GetAllMatchingMockedMethodSymbolsFromSetupMethodInvocation(this SemanticModel semanticModel, InvocationExpressionSyntax? setupMethodInvocation) From 0467cfd4e12abd7e58115acc6506791719248b12 Mon Sep 17 00:00:00 2001 From: Richard Murillo Date: Mon, 3 Feb 2025 15:44:34 -0800 Subject: [PATCH 10/15] Add unit tests for EnumerableExtensions --- .../Common/EnumerableExtensionTests.cs | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 tests/Moq.Analyzers.Test/Common/EnumerableExtensionTests.cs diff --git a/tests/Moq.Analyzers.Test/Common/EnumerableExtensionTests.cs b/tests/Moq.Analyzers.Test/Common/EnumerableExtensionTests.cs new file mode 100644 index 000000000..e663e8e23 --- /dev/null +++ b/tests/Moq.Analyzers.Test/Common/EnumerableExtensionTests.cs @@ -0,0 +1,76 @@ +namespace Moq.Analyzers.Test.Common; + +public class EnumerableExtensionsTests +{ + [Fact] + public void DefaultIfNotSingle_ReturnsNull_WhenSourceIsEmpty() + { + IEnumerable source = []; + int result = source.DefaultIfNotSingle(); + Assert.Equal(0, result); + } + + [Fact] + public void DefaultIfNotSingle_ReturnsElement_WhenSourceContainsSingleElement() + { + int[] source = [42]; + int result = source.DefaultIfNotSingle(); + Assert.Equal(42, result); + } + + [Fact] + public void DefaultIfNotSingle_ReturnsNull_WhenSourceContainsMultipleElements() + { + int[] source = [1, 2, 3]; + int result = source.DefaultIfNotSingle(); + Assert.Equal(0, result); + } + + [Fact] + public void DefaultIfNotSingle_WithPredicate_ReturnsNull_WhenNoElementsMatch() + { + int[] source = [1, 2, 3]; + int result = source.DefaultIfNotSingle(x => x > 10); + Assert.Equal(0, result); + } + + [Fact] + public void DefaultIfNotSingle_WithPredicate_ReturnsElement_WhenOnlyOneMatches() + { + int[] source = [1, 2, 3]; + int result = source.DefaultIfNotSingle(x => x == 2); + Assert.Equal(2, result); + } + + [Fact] + public void DefaultIfNotSingle_WithPredicate_ReturnsNull_WhenMultipleElementsMatch() + { + int[] source = [1, 2, 2, 3]; + int result = source.DefaultIfNotSingle(x => x > 1); + Assert.Equal(0, result); + } + + [Fact] + public void DefaultIfNotSingle_ImmutableArray_ReturnsNull_WhenEmpty() + { + ImmutableArray source = ImmutableArray.Empty; + int result = source.DefaultIfNotSingle(x => x > 0); + Assert.Equal(0, result); + } + + [Fact] + public void DefaultIfNotSingle_ImmutableArray_ReturnsElement_WhenSingleMatch() + { + ImmutableArray source = [.. new[] { 5, 10, 15 }]; + int result = source.DefaultIfNotSingle(x => x == 10); + Assert.Equal(10, result); + } + + [Fact] + public void DefaultIfNotSingle_ImmutableArray_ReturnsNull_WhenMultipleMatches() + { + ImmutableArray source = [.. new[] { 5, 10, 10, 15 }]; + int result = source.DefaultIfNotSingle(x => x > 5); + Assert.Equal(0, result); + } +} From 8dd1fbf03f9f6d82237ca419603eb11c8bdcd6ce Mon Sep 17 00:00:00 2001 From: Richard Murillo Date: Mon, 3 Feb 2025 15:48:49 -0800 Subject: [PATCH 11/15] Fix issue with possible null deference --- src/Common/SemanticModelExtensions.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Common/SemanticModelExtensions.cs b/src/Common/SemanticModelExtensions.cs index d03646480..8fbc2d2b4 100644 --- a/src/Common/SemanticModelExtensions.cs +++ b/src/Common/SemanticModelExtensions.cs @@ -122,6 +122,12 @@ private static bool IsCallbackOrReturnSymbol(ISymbol? symbol) } string? methodName = methodSymbol.ToString(); + + if (string.IsNullOrEmpty(methodName)) + { + return false; + } + return methodName.StartsWith("Moq.Language.ICallback", StringComparison.Ordinal) || methodName.StartsWith("Moq.Language.IReturns", StringComparison.Ordinal); } From c5439bbd3d59e55f7565dff8b8f78dbe893da0d9 Mon Sep 17 00:00:00 2001 From: Richard Murillo Date: Mon, 3 Feb 2025 15:48:58 -0800 Subject: [PATCH 12/15] Add tests for EnumerableExtensions --- ...tensionTests.cs => EnumerableExtensionsTests.cs} | 0 tests/Moq.Analyzers.Test/SquiggleCop.Baseline.yaml | 13 +++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) rename tests/Moq.Analyzers.Test/Common/{EnumerableExtensionTests.cs => EnumerableExtensionsTests.cs} (100%) diff --git a/tests/Moq.Analyzers.Test/Common/EnumerableExtensionTests.cs b/tests/Moq.Analyzers.Test/Common/EnumerableExtensionsTests.cs similarity index 100% rename from tests/Moq.Analyzers.Test/Common/EnumerableExtensionTests.cs rename to tests/Moq.Analyzers.Test/Common/EnumerableExtensionsTests.cs diff --git a/tests/Moq.Analyzers.Test/SquiggleCop.Baseline.yaml b/tests/Moq.Analyzers.Test/SquiggleCop.Baseline.yaml index 38cfc887a..e39927f7a 100644 --- a/tests/Moq.Analyzers.Test/SquiggleCop.Baseline.yaml +++ b/tests/Moq.Analyzers.Test/SquiggleCop.Baseline.yaml @@ -319,13 +319,13 @@ - {Id: CS8669, Title: The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source., Category: Compiler, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Warning], IsEverSuppressed: true} - {Id: CS9216, Title: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement., Category: Compiler, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Warning], IsEverSuppressed: true} - {Id: ECS0100, Title: Prefer implicitly typed local variables, Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} -- {Id: ECS0200, Title: Prefer readonly over const, Category: Maintainability, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} +- {Id: ECS0200, Title: Prefer readonly over const, Category: Maintainability, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: true} - {Id: ECS0400, Title: Replace string.Format with interpolated string, Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: ECS0500, Title: Prefer FormattableString or string.Create for culture-specific strings, Category: Globalization, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: ECS0600, Title: Avoid stringly-typed APIs, Category: Refactoring, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: ECS0700, Title: Express callbacks with delegates, Category: Design, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: ECS0800, Title: Use the Null Conditional Operator for Event Invocations, Category: Usage, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} -- {Id: ECS0900, Title: Minimize boxing and unboxing, Category: Performance, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} +- {Id: ECS0900, Title: Minimize boxing and unboxing, Category: Performance, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: true} - {Id: ECS1200, Title: Prefer member initializers to assignment statements, Category: Maintainability, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: ECS1300, Title: Use proper initialization for static class members, Category: Initialization, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: ECS1400, Title: Minimize duplicate initialization logic, Category: Initialization, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} @@ -361,7 +361,7 @@ - {Id: HAA0603, Title: Delegate allocation from a method group, Category: Performance, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: HAA0604, Title: Delegate allocation from a method group, Category: Performance, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} - {Id: IDE0004, Title: Remove Unnecessary Cast, Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: true} -- {Id: IDE0005, Title: Using directive is unnecessary., Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} +- {Id: IDE0005, Title: Using directive is unnecessary., Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: true} - {Id: IDE0005_gen, Title: Using directive is unnecessary., Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: true} - {Id: IDE0007, Title: Use implicit type, Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} - {Id: IDE0008, Title: Use explicit type, Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} @@ -381,6 +381,7 @@ - {Id: IDE0026, Title: Use expression body for indexer, Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} - {Id: IDE0027, Title: Use expression body for accessor, Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} - {Id: IDE0028, Title: Simplify collection initialization, Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} +- {Id: IDE0028, Title: Simplify collection initialization, Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} - {Id: IDE0029, Title: Use coalesce expression, Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} - {Id: IDE0030, Title: Use coalesce expression, Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} - {Id: IDE0031, Title: Use null propagation, Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} @@ -657,8 +658,8 @@ - {Id: RCS1020, Title: 'Simplify Nullable to T?', Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} - {Id: RCS1021, Title: Convert lambda expression body to expression body, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} - {Id: RCS1021FadeOut, Title: Convert lambda expression body to expression body, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} -- {Id: RCS1031, Title: Remove unnecessary braces in switch section, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} -- {Id: RCS1031FadeOut, Title: Remove unnecessary braces in switch section, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} +- {Id: RCS1031, Title: Remove unnecessary braces in switch section, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: true} +- {Id: RCS1031FadeOut, Title: Remove unnecessary braces in switch section, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: true} - {Id: RCS1032, Title: Remove redundant parentheses, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} - {Id: RCS1032FadeOut, Title: Remove redundant parentheses, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} - {Id: RCS1033, Title: Remove redundant boolean literal, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} @@ -837,7 +838,7 @@ - {Id: RCS1228, Title: Unused element in a documentation comment, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} - {Id: RCS1229, Title: Use async/await when necessary, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} - {Id: RCS1230, Title: Unnecessary explicit use of enumerator, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} -- {Id: RCS1231, Title: Make parameter ref read-only, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: false, EffectiveSeverities: [None], IsEverSuppressed: false} +- {Id: RCS1231, Title: Make parameter ref read-only, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: false, EffectiveSeverities: [None], IsEverSuppressed: true} - {Id: RCS1232, Title: Order elements in documentation comment, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} - {Id: RCS1233, Title: Use short-circuiting operator, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} - {Id: RCS1234, Title: Duplicate enum value, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} From 9761db54cf8157790826159c41a84f33645d5982 Mon Sep 17 00:00:00 2001 From: Richard Murillo Date: Mon, 3 Feb 2025 16:12:29 -0800 Subject: [PATCH 13/15] Extend support to ignore valuetask on setup method --- ...BeUsedOnlyForOverridableMembersAnalyzer.cs | 26 ++++++++++++++++--- src/Common/WellKnown/KnownSymbols.cs | 10 ------- ...dOnlyForOverridableMembersAnalyzerTests.cs | 16 +++++++++--- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs b/src/Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs index 71c11409d..c446599d0 100644 --- a/src/Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs +++ b/src/Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs @@ -73,7 +73,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context) { case IPropertySymbol propertySymbol: // Check if the property is Task.Result and skip diagnostic if it is - if (IsTaskResultProperty(propertySymbol, knownSymbols)) + if (IsTaskOrValueResultProperty(propertySymbol, knownSymbols)) { return; } @@ -97,6 +97,11 @@ private static void Analyze(SyntaxNodeAnalysisContext context) context.ReportDiagnostic(diagnostic); } + private static bool IsTaskOrValueResultProperty(IPropertySymbol propertySymbol, MoqKnownSymbols knownSymbols) + { + return IsTaskResultProperty(propertySymbol, knownSymbols) || IsValueTaskResultProperty(propertySymbol, knownSymbols); + } + private static bool IsTaskResultProperty(IPropertySymbol propertySymbol, MoqKnownSymbols knownSymbols) { // Check if the property is named "Result" @@ -108,11 +113,24 @@ private static bool IsTaskResultProperty(IPropertySymbol propertySymbol, MoqKnow // Check if the containing type is Task INamedTypeSymbol? taskOfTType = knownSymbols.Task1; - if (taskOfTType == null) + return taskOfTType != null && + // If Task type cannot be found, we skip it + SymbolEqualityComparer.Default.Equals(propertySymbol.ContainingType, taskOfTType); + } + + private static bool IsValueTaskResultProperty(IPropertySymbol propertySymbol, MoqKnownSymbols knownSymbols) + { + // Check if the property is named "Result" + if (!string.Equals(propertySymbol.Name, "Result", StringComparison.Ordinal)) { - return false; // If Task type cannot be found, we skip it + return false; } - return SymbolEqualityComparer.Default.Equals(propertySymbol.ContainingType, taskOfTType); + // Check if the containing type is ValueTask + INamedTypeSymbol? valueTaskOfType = knownSymbols.ValueTask1; + + return valueTaskOfType != null && + // If ValueTask type cannot be found, we skip it + SymbolEqualityComparer.Default.Equals(propertySymbol.ContainingType, valueTaskOfType); } } diff --git a/src/Common/WellKnown/KnownSymbols.cs b/src/Common/WellKnown/KnownSymbols.cs index 7f8e466a8..b3b1d756f 100644 --- a/src/Common/WellKnown/KnownSymbols.cs +++ b/src/Common/WellKnown/KnownSymbols.cs @@ -22,21 +22,11 @@ public KnownSymbols(Compilation compilation) { } - /// - /// Gets the class System.Threading.Tasks.Task. - /// - public INamedTypeSymbol? Task => TypeProvider.GetOrCreateTypeByMetadataName("System.Threading.Tasks.Task"); - /// /// Gets the class System.Threading.Tasks.Task<T>. /// public INamedTypeSymbol? Task1 => TypeProvider.GetOrCreateTypeByMetadataName("System.Threading.Tasks.Task`1"); - /// - /// Gets the class System.Threading.Tasks.ValueTask. - /// - public INamedTypeSymbol? ValueTask => TypeProvider.GetOrCreateTypeByMetadataName("System.Threading.Tasks.ValueTask"); - /// /// Gets the class System.Threading.Tasks.ValueTask<T>. /// diff --git a/tests/Moq.Analyzers.Test/SetupShouldBeUsedOnlyForOverridableMembersAnalyzerTests.cs b/tests/Moq.Analyzers.Test/SetupShouldBeUsedOnlyForOverridableMembersAnalyzerTests.cs index f3eb676d1..0802a067c 100644 --- a/tests/Moq.Analyzers.Test/SetupShouldBeUsedOnlyForOverridableMembersAnalyzerTests.cs +++ b/tests/Moq.Analyzers.Test/SetupShouldBeUsedOnlyForOverridableMembersAnalyzerTests.cs @@ -16,7 +16,10 @@ public static IEnumerable TestData() ["""new Mock().Setup(x => x.TestProperty);"""], ["""new Mock().Setup(x => x.Calculate(It.IsAny(), It.IsAny()));"""], ["""new Mock().Setup(x => x.DoSth());"""], - ["""new Mock().Setup(x => x.DoSomethingAsync().Result).Returns(true);"""], + ["""new Mock().Setup(x => x.DoSomethingAsync());"""], + ["""new Mock().Setup(x => x.GetBooleanAsync().Result).Returns(true);"""], + ["""new Mock().Setup(x => x.DoSomethingValueTask());"""], + ["""new Mock().Setup(x => x.GetNumberAsync()).Returns(ValueTask.FromResult(42));"""], }.WithNamespaces().WithMoqReferenceAssemblyGroups(); } @@ -33,9 +36,16 @@ public interface ISampleInterface int TestProperty { get; set; } } - public interface IParameterlessAsyncMethod + public interface IAsyncMethods { - Task DoSomethingAsync(); + Task DoSomethingAsync(); + Task GetBooleanAsync(); + } + + public interface IValueTaskMethods + { + ValueTask DoSomethingValueTask(); + ValueTask GetNumberAsync(); } public abstract class BaseSampleClass From c373da6c9bd1475867710a4431ee2fda86d05f1e Mon Sep 17 00:00:00 2001 From: Richard Murillo Date: Mon, 3 Feb 2025 16:19:58 -0800 Subject: [PATCH 14/15] Fix for SA1515 --- .../SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs b/src/Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs index c446599d0..1ec83de18 100644 --- a/src/Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs +++ b/src/Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs @@ -114,6 +114,7 @@ private static bool IsTaskResultProperty(IPropertySymbol propertySymbol, MoqKnow INamedTypeSymbol? taskOfTType = knownSymbols.Task1; return taskOfTType != null && + // If Task type cannot be found, we skip it SymbolEqualityComparer.Default.Equals(propertySymbol.ContainingType, taskOfTType); } @@ -130,6 +131,7 @@ private static bool IsValueTaskResultProperty(IPropertySymbol propertySymbol, Mo INamedTypeSymbol? valueTaskOfType = knownSymbols.ValueTask1; return valueTaskOfType != null && + // If ValueTask type cannot be found, we skip it SymbolEqualityComparer.Default.Equals(propertySymbol.ContainingType, valueTaskOfType); } From 8765c5a532d60124898a6984c129d443a44edb9a Mon Sep 17 00:00:00 2001 From: Richard Murillo Date: Mon, 3 Feb 2025 16:30:55 -0800 Subject: [PATCH 15/15] Reduce code duplication when checking for Task and ValueTask result --- ...BeUsedOnlyForOverridableMembersAnalyzer.cs | 29 ++++--------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/src/Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs b/src/Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs index 1ec83de18..8fba4c92f 100644 --- a/src/Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs +++ b/src/Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs @@ -99,10 +99,11 @@ private static void Analyze(SyntaxNodeAnalysisContext context) private static bool IsTaskOrValueResultProperty(IPropertySymbol propertySymbol, MoqKnownSymbols knownSymbols) { - return IsTaskResultProperty(propertySymbol, knownSymbols) || IsValueTaskResultProperty(propertySymbol, knownSymbols); + return IsGenericResultProperty(propertySymbol, knownSymbols.Task1) + || IsGenericResultProperty(propertySymbol, knownSymbols.ValueTask1); } - private static bool IsTaskResultProperty(IPropertySymbol propertySymbol, MoqKnownSymbols knownSymbols) + private static bool IsGenericResultProperty(IPropertySymbol propertySymbol, INamedTypeSymbol? genericType) { // Check if the property is named "Result" if (!string.Equals(propertySymbol.Name, "Result", StringComparison.Ordinal)) @@ -110,29 +111,9 @@ private static bool IsTaskResultProperty(IPropertySymbol propertySymbol, MoqKnow return false; } - // Check if the containing type is Task - INamedTypeSymbol? taskOfTType = knownSymbols.Task1; - - return taskOfTType != null && + return genericType != null && // If Task type cannot be found, we skip it - SymbolEqualityComparer.Default.Equals(propertySymbol.ContainingType, taskOfTType); - } - - private static bool IsValueTaskResultProperty(IPropertySymbol propertySymbol, MoqKnownSymbols knownSymbols) - { - // Check if the property is named "Result" - if (!string.Equals(propertySymbol.Name, "Result", StringComparison.Ordinal)) - { - return false; - } - - // Check if the containing type is ValueTask - INamedTypeSymbol? valueTaskOfType = knownSymbols.ValueTask1; - - return valueTaskOfType != null && - - // If ValueTask type cannot be found, we skip it - SymbolEqualityComparer.Default.Equals(propertySymbol.ContainingType, valueTaskOfType); + SymbolEqualityComparer.Default.Equals(propertySymbol.ContainingType.OriginalDefinition, genericType); } }