Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Comment thread
rjmurillo marked this conversation as resolved.
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>
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);
Comment thread
rjmurillo marked this conversation as resolved.
}

/// <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
Comment thread
rjmurillo marked this conversation as resolved.
SymbolEqualityComparer.Default.Equals(propertySymbol.ContainingType.OriginalDefinition, genericType);
Comment thread
rjmurillo marked this conversation as resolved.
}
}
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)
Comment thread
rjmurillo marked this conversation as resolved.
{
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;
}

Comment thread
rjmurillo marked this conversation as resolved.
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;
}
Comment thread
rjmurillo marked this conversation as resolved.

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");
Comment thread
rjmurillo marked this conversation as resolved.

/// <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