Skip to content

Commit

Permalink
Improve code coverage (#342)
Browse files Browse the repository at this point in the history
Addition of new unit tests for extension methods, removing dead code,
and fixing some documentation typos

- Added configuration for a new reporting tool to generate coverage
reports.
- Enhanced async method analysis and streamlined APIs by removing
redundant extension methods.
- Corrected documentation typos to improve clarity.
- Expanded unit test coverage for array operations, enumerable behavior,
and asynchronous scenarios.
  • Loading branch information
rjmurillo authored Feb 4, 2025
1 parent f9aee2c commit 314a50b
Show file tree
Hide file tree
Showing 13 changed files with 235 additions and 69 deletions.
7 changes: 7 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@
"dotnet-squigglecop"
],
"rollForward": false
},
"dotnet-reportgenerator-globaltool": {
"version": "5.4.3",
"commands": [
"reportgenerator"
],
"rollForward": false
}
}
}
1 change: 1 addition & 0 deletions Moq.Analyzers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,6 @@ Global
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*{8e99c15c-e80a-49e5-988c-1b5071ce775f}*SharedItemsImports = 5
src\Common\Common.projitems*{d2348836-7129-4be5-8ae6-d05fc8c28fc1}*SharedItemsImports = 5
EndGlobalSection
EndGlobal
1 change: 1 addition & 0 deletions build/targets/tests/Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.0.1" />
<PackageVersion Include="Meziantou.Xunit.ParallelTestFramework" Version="2.3.0" />
<PackageVersion Include="coverlet.msbuild" Version="6.0.4" />
</ItemGroup>
</Project>
25 changes: 13 additions & 12 deletions src/Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ private static bool IsPropertyOrMethod(ISymbol mockedMemberSymbol, MoqKnownSymbo
switch (mockedMemberSymbol)
{
case IPropertySymbol propertySymbol:
// If the property is Task<T>.Result, skip diagnostic
if (IsTaskResultProperty(propertySymbol, knownSymbols))
// Check if the property is Task<T>.Result and skip diagnostic if it is
if (IsTaskOrValueResultProperty(propertySymbol, knownSymbols))
{
return true;
}
Expand Down Expand Up @@ -150,25 +150,26 @@ private static bool IsPropertyOrMethod(ISymbol mockedMemberSymbol, MoqKnownSymbo
return null;
}

private static bool IsTaskOrValueResultProperty(IPropertySymbol propertySymbol, MoqKnownSymbols knownSymbols)
{
return IsGenericResultProperty(propertySymbol, knownSymbols.Task1)
|| IsGenericResultProperty(propertySymbol, knownSymbols.ValueTask1);
}

/// <summary>
/// Checks if a property is the 'Result' property on <see cref="Task{TResult}"/>.
/// Checks if a property is the 'Result' property on <see cref="Task{TResult}"/> or <see cref="ValueTask{TResult}"/>.
/// </summary>
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))
{
return false;
}

// Check if the containing type is Task<T>
INamedTypeSymbol? taskOfTType = knownSymbols.Task1;

if (taskOfTType == null)
{
return false; // If Task<T> type cannot be found, we skip it
}
return genericType != null &&

return SymbolEqualityComparer.Default.Equals(propertySymbol.ContainingType, taskOfTType);
// If Task<T> type cannot be found, we skip it
SymbolEqualityComparer.Default.Equals(propertySymbol.ContainingType.OriginalDefinition, genericType);
}
}
6 changes: 3 additions & 3 deletions src/Common/DiagnosticEditProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ internal enum EditType
/// <summary>
/// Returns the current object as an <see cref="ImmutableDictionary{TKey, TValue}"/>.
/// </summary>
/// <returns>The current objbect as an immutable dictionary.</returns>
/// <returns>The current object as an immutable dictionary.</returns>
public ImmutableDictionary<string, string?> ToImmutableDictionary()
{
return new Dictionary<string, string?>(StringComparer.Ordinal)
Expand All @@ -48,10 +48,10 @@ internal enum EditType
}

/// <summary>
/// Tries to convert an immuatble dictionary to a <see cref="DiagnosticEditProperties"/>.
/// Tries to convert an immutable dictionary to a <see cref="DiagnosticEditProperties"/>.
/// </summary>
/// <param name="dictionary">The dictionary to try to convert.</param>
/// <param name="editProperties">The output edit properties if parsing suceeded, otherwise <c>null</c>.</param>
/// <param name="editProperties">The output edit properties if parsing succeeded, otherwise <c>null</c>.</param>
/// <returns><c>true</c> if parsing succeeded; <c>false</c> otherwise.</returns>
public static bool TryGetFromImmutableDictionary(ImmutableDictionary<string, string?> dictionary, [NotNullWhen(true)] out DiagnosticEditProperties? editProperties)
{
Expand Down
18 changes: 0 additions & 18 deletions src/Common/EnumerableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,6 @@ internal static class EnumerableExtensions
return source.DefaultIfNotSingle(static _ => true);
}

/// <inheritdoc cref="DefaultIfNotSingle{TSource}(ImmutableArray{TSource}, Func{TSource, bool})"/>
public static TSource? DefaultIfNotSingle<TSource>(this ImmutableArray<TSource> source)
{
return source.DefaultIfNotSingle(static _ => true);
}

/// <inheritdoc cref="DefaultIfNotSingle{TSource}(IEnumerable{TSource}, Func{TSource, bool})"/>
/// <param name="source">The collection to enumerate.</param>
/// <param name="predicate">A function to test each element for a condition.</param>
Expand Down Expand Up @@ -57,16 +51,4 @@ internal static class EnumerableExtensions

return item;
}

public static IEnumerable<TSource> WhereNotNull<TSource>(this IEnumerable<TSource?> source)
where TSource : class
{
return source.Where(item => item is not null)!;
}

public static IEnumerable<TSource> WhereNotNull<TSource>(this IEnumerable<TSource?> source)
where TSource : struct
{
return source.Where(item => item.HasValue).Select(item => item!.Value);
}
}
47 changes: 30 additions & 17 deletions src/Common/SemanticModelExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,33 @@ namespace Moq.Analyzers.Common;
/// </summary>
internal static class SemanticModelExtensions
{
internal static InvocationExpressionSyntax? FindSetupMethodFromCallbackInvocation(this SemanticModel semanticModel, MoqKnownSymbols knownSymbols, ExpressionSyntax expression, CancellationToken cancellationToken)
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 (symbolInfo.Symbol.IsMoqSetupMethod(knownSymbols))
{
return invocation;
}

expression = method.Expression;
}

SymbolInfo symbolInfo = semanticModel.GetSymbolInfo(method, cancellationToken);
if (symbolInfo.Symbol is null)
{
return null;
}

if (symbolInfo.Symbol.IsMoqSetupMethod(knownSymbols))
{
return invocation;
}

return semanticModel.FindSetupMethodFromCallbackInvocation(knownSymbols, method.Expression, cancellationToken);
}

internal static IEnumerable<IMethodSymbol> GetAllMatchingMockedMethodSymbolsFromSetupMethodInvocation(this SemanticModel semanticModel, InvocationExpressionSyntax? setupMethodInvocation)
Expand Down Expand Up @@ -114,6 +121,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);
}
Expand Down
10 changes: 0 additions & 10 deletions src/Common/WellKnown/KnownSymbols.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,11 @@ public KnownSymbols(Compilation compilation)
{
}

/// <summary>
/// Gets the class <c>System.Threading.Tasks.Task</c>.
/// </summary>
public INamedTypeSymbol? Task => TypeProvider.GetOrCreateTypeByMetadataName("System.Threading.Tasks.Task");

/// <summary>
/// Gets the class <c>System.Threading.Tasks.Task&lt;T&gt;</c>.
/// </summary>
public INamedTypeSymbol? Task1 => TypeProvider.GetOrCreateTypeByMetadataName("System.Threading.Tasks.Task`1");

/// <summary>
/// Gets the class <c>System.Threading.Tasks.ValueTask</c>.
/// </summary>
public INamedTypeSymbol? ValueTask => TypeProvider.GetOrCreateTypeByMetadataName("System.Threading.Tasks.ValueTask");

/// <summary>
/// Gets the class <c>System.Threading.Tasks.ValueTask&lt;T&gt;</c>.
/// </summary>
Expand Down
81 changes: 81 additions & 0 deletions tests/Moq.Analyzers.Test/Common/ArrayExtensionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
namespace Moq.Analyzers.Test.Common;

public class ArrayExtensionTests
{
[Fact]
public void RemoveAt_RemovesElementAtIndex()
{
// Arrange
int[] actual = [1, 2, 3, 4, 5];
int[] expected = [1, 2, 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<ArgumentOutOfRangeException>(() => input.RemoveAt(-1));
Assert.Throws<ArgumentOutOfRangeException>(() => input.RemoveAt(3));
}

[Fact]
public void RemoveAt_EmptyArray_ThrowsException()
{
// Arrange
int[] input = [];

// Act & Assert
Assert.Throws<ArgumentOutOfRangeException>(() => input.RemoveAt(0));
}
}
76 changes: 76 additions & 0 deletions tests/Moq.Analyzers.Test/Common/EnumerableExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
namespace Moq.Analyzers.Test.Common;

public class EnumerableExtensionsTests
{
[Fact]
public void DefaultIfNotSingle_ReturnsNull_WhenSourceIsEmpty()
{
IEnumerable<int> 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<int> source = ImmutableArray<int>.Empty;
int result = source.DefaultIfNotSingle(x => x > 0);
Assert.Equal(0, result);
}

[Fact]
public void DefaultIfNotSingle_ImmutableArray_ReturnsElement_WhenSingleMatch()
{
ImmutableArray<int> source = [.. new[] { 5, 10, 15 }];
int result = source.DefaultIfNotSingle(x => x == 10);
Assert.Equal(10, result);
}

[Fact]
public void DefaultIfNotSingle_ImmutableArray_ReturnsNull_WhenMultipleMatches()
{
ImmutableArray<int> source = [.. new[] { 5, 10, 10, 15 }];
int result = source.DefaultIfNotSingle(x => x > 5);
Assert.Equal(0, result);
}
}
3 changes: 3 additions & 0 deletions tests/Moq.Analyzers.Test/Moq.Analyzers.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@
<PackageReference Include="Microsoft.CodeAnalysis.AnalyzerUtilities" />
<PackageReference Include="Verify.Nupkg" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="$(RepoRoot)/src/Analyzers/Moq.Analyzers.csproj" AddPackageAsOutput="true" />
<ProjectReference Include="$(RepoRoot)/tests/Moq.Analyzers.Test.Analyzers/Moq.Analyzers.Test.Analyzers.csproj" />
</ItemGroup>

<Import Project="$(RepoRoot)/src/Common/Common.projitems" Label="Shared" />

</Project>
Loading

0 comments on commit 314a50b

Please sign in to comment.