Skip to content
Open
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
48 changes: 48 additions & 0 deletions src/Analyzers/MethodSetupShouldSpecifyReturnValueAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,15 @@ private static bool HasReturnValueSpecification(
return true;
}

// When a non-Moq method (e.g., an extension method) appears in the chain,
// check if its return type implements a Moq fluent interface. The analyzer
// assumes such methods handle return value specification internally or pass
// the chain through for further configuration.
if (IsFluentChainWrapperMethod(symbolInfo, knownSymbols))
{
return true;
}

Comment thread
rjmurillo marked this conversation as resolved.
current = memberAccess.Parent as InvocationExpressionSyntax;
}

Expand Down Expand Up @@ -229,4 +238,43 @@ private static bool HasReturnValueSymbol(SymbolInfo symbolInfo, MoqKnownSymbols
.OfType<IMethodSymbol>()
.Any(method => method.IsMoqReturnValueSpecificationMethod(knownSymbols));
}

/// <summary>
/// Determines whether a method in the chain is a wrapper (e.g., an extension method)
/// whose return type implements a Moq fluent interface. Such methods are assumed to
/// handle return value specification internally, as indicated by their Moq fluent
/// return type.
/// </summary>
private static bool IsFluentChainWrapperMethod(SymbolInfo symbolInfo, MoqKnownSymbols knownSymbols)
{
if (symbolInfo.Symbol is IMethodSymbol resolved)
{
return IsWrapperMethod(resolved, knownSymbols);
}

// When overload resolution fails, check all candidates. A false negative
// occurs if only the first candidate is inspected and it does not return a
// Moq fluent type while another candidate does.
return symbolInfo.CandidateSymbols
.OfType<IMethodSymbol>()
.Any(candidate => IsWrapperMethod(candidate, knownSymbols));
}

/// <summary>
/// Returns true when <paramref name="method"/> is a user-defined wrapper whose return
/// type implements a Moq fluent interface. Known Moq return-value, callback, and raises
/// methods are excluded because they are already handled by earlier checks in the
/// chain walk.
/// </summary>
private static bool IsWrapperMethod(IMethodSymbol method, MoqKnownSymbols knownSymbols)
{
if (method.IsMoqReturnValueSpecificationMethod(knownSymbols)
|| method.IsMoqCallbackMethod(knownSymbols)
|| method.IsMoqRaisesMethod(knownSymbols))
{
return false;
Comment thread
rjmurillo marked this conversation as resolved.
}
Comment thread
cursor[bot] marked this conversation as resolved.

return method.ReturnType.ImplementsMoqFluentInterface(knownSymbols);
}
Comment thread
rjmurillo marked this conversation as resolved.
}
91 changes: 89 additions & 2 deletions src/Common/ISymbolExtensions.Moq.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,22 @@
IsConcreteSetupPhraseRaisesMethod(symbol, knownSymbols);
}

/// <summary>
/// Determines whether a type symbol is or implements any Moq fluent interface from
/// the <c>Moq.Language</c> or <c>Moq.Language.Flow</c> namespaces.
/// </summary>
/// <param name="typeSymbol">The type symbol to check.</param>
/// <param name="knownSymbols">Known Moq symbols resolved from the compilation.</param>
/// <returns>
/// <see langword="true"/> if the type is or implements a Moq fluent interface;
/// otherwise, <see langword="false"/>.
/// </returns>
internal static bool ImplementsMoqFluentInterface(this ITypeSymbol typeSymbol, MoqKnownSymbols knownSymbols)
{
return IsMoqFluentType(typeSymbol, knownSymbols)
|| HasMoqFluentInterfaceInHierarchy(typeSymbol, knownSymbols);
}

/// <summary>
/// Checks if the symbol is a Raises method from ISetup / ISetupPhrase interfaces.
/// </summary>
Expand All @@ -208,11 +224,11 @@
}

/// <summary>
/// Checks if the symbol is a Raises method from ICallback interfaces.
/// Checks if the symbol is a Raises method from ICallback, ISetupGetter, or ISetupSetter interfaces.
/// </summary>
/// <param name="symbol">The symbol to check.</param>
/// <param name="knownSymbols">The known symbols for type checking.</param>
/// <returns>True if the symbol is a callback Raises method; otherwise false.</returns>
/// <returns>True if the symbol is a Raises method from one of these interfaces; otherwise false.</returns>
private static bool IsCallbackRaisesMethod(ISymbol symbol, MoqKnownSymbols knownSymbols)
{
return symbol.IsInstanceOf(knownSymbols.ICallbackRaises) ||
Expand Down Expand Up @@ -265,4 +281,75 @@
return genericType != null &&
SymbolEqualityComparer.Default.Equals(propertySymbol.ContainingType.OriginalDefinition, genericType);
}

/// <summary>
/// Checks if the type itself is a known Moq fluent interface.
/// </summary>
private static bool IsMoqFluentType(ITypeSymbol typeSymbol, MoqKnownSymbols knownSymbols)
{
if (typeSymbol is not INamedTypeSymbol namedType)
{
return false;
}

INamedTypeSymbol original = namedType.IsGenericType ? namedType.ConstructedFrom : namedType;

return IsMatchingFluentSymbol(original, knownSymbols);
}

/// <summary>
/// Walks the AllInterfaces list to find any Moq fluent interface in the type hierarchy.
/// </summary>
private static bool HasMoqFluentInterfaceInHierarchy(ITypeSymbol typeSymbol, MoqKnownSymbols knownSymbols)
{
foreach (INamedTypeSymbol iface in typeSymbol.AllInterfaces)
{
INamedTypeSymbol original = iface.IsGenericType ? iface.ConstructedFrom : iface;

if (IsMatchingFluentSymbol(original, knownSymbols))
{
return true;
}
}

return false;
}

/// <summary>
/// Compares a type symbol against the set of known Moq fluent interface symbols
/// that indicate the fluent chain is active and a return value can be configured.
/// </summary>
/// <remarks>
/// <para>
/// This method intentionally excludes <c>ICallback</c>, <c>ICallback{TMock}</c>, and
/// <c>ICallback{TMock, TResult}</c> because these interfaces do not inherit from
/// <c>IReturns{TMock, TResult}</c> and do not indicate that a return value can be
/// configured. Types like <c>ISetup{TMock, TResult}</c> that inherit from
/// <c>IReturns{TMock, TResult}</c> are still caught via
/// <see cref="HasMoqFluentInterfaceInHierarchy"/> when checking
/// <see cref="ITypeSymbol.AllInterfaces"/>.
/// </para>
/// <para>
/// Only <c>IReturns{TMock, TResult}</c> (arity 2) is checked. <c>Moq.Language.IReturns</c>
/// (arity 0) and <c>Moq.Language.IReturns{T}</c> (arity 1) resolve to null in Moq 4.18+
/// and are not part of the fluent setup chain.
/// </para>
/// <para>
/// <c>IThrows</c>, <c>ISetup{TResult}</c>, <c>ISetupGetter{TMock, TProperty}</c>, and
/// <c>ISetupSetter{TMock, TProperty}</c> are included because a wrapper method may
/// return these types directly. <c>IReturnsResult{TMock}</c> is the return type of
/// <c>.Returns()</c> and <c>.ReturnsAsync()</c>. <c>IThrowsResult</c> is the return
/// type of <c>.Throws()</c>.
/// </para>
/// </remarks>
private static bool IsMatchingFluentSymbol(INamedTypeSymbol type, MoqKnownSymbols knownSymbols)
{
return SymbolEqualityComparer.Default.Equals(type, knownSymbols.IReturns2)

Check notice on line 347 in src/Common/ISymbolExtensions.Moq.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/Common/ISymbolExtensions.Moq.cs#L347

Reduce the number of conditional operators (6) used in the expression (maximum allowed 3).
|| SymbolEqualityComparer.Default.Equals(type, knownSymbols.IThrows)
|| SymbolEqualityComparer.Default.Equals(type, knownSymbols.ISetup1)
Comment thread
rjmurillo marked this conversation as resolved.
|| SymbolEqualityComparer.Default.Equals(type, knownSymbols.ISetupGetter)
|| SymbolEqualityComparer.Default.Equals(type, knownSymbols.ISetupSetter)
|| SymbolEqualityComparer.Default.Equals(type, knownSymbols.IReturnsResult1)
|| SymbolEqualityComparer.Default.Equals(type, knownSymbols.IThrowsResult);
}
Comment thread
rjmurillo marked this conversation as resolved.
}
14 changes: 13 additions & 1 deletion src/Common/WellKnown/MoqKnownSymbols.cs
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ internal MoqKnownSymbols(Compilation compilation)
internal INamedTypeSymbol? MockRepository => TypeProvider.GetOrCreateTypeByMetadataName("Moq.MockRepository");

/// <summary>
/// Gets the methods for <c>Moq.MockRepository.Of</c>.
/// Gets the methods for <c>Moq.MockRepository.Create</c>.
/// </summary>
/// <remarks>
/// <c>MockRepository</c> is a subclass of <c>MockFactory</c>.
Expand Down Expand Up @@ -422,6 +422,18 @@ internal MoqKnownSymbols(Compilation compilation)
/// </summary>
internal ImmutableArray<IMethodSymbol> IReturns2Raises => _iReturns2Raises.Value;

/// <summary>
/// Gets the interface <c>Moq.Language.Flow.IReturnsResult{TMock}</c>.
/// This is the actual return type of <c>.Returns()</c> and <c>.ReturnsAsync()</c>.
/// </summary>
internal INamedTypeSymbol? IReturnsResult1 => TypeProvider.GetOrCreateTypeByMetadataName("Moq.Language.Flow.IReturnsResult`1");

/// <summary>
/// Gets the interface <c>Moq.Language.Flow.IThrowsResult</c>.
/// This is the return type of <c>.Throws()</c>.
/// </summary>
internal INamedTypeSymbol? IThrowsResult => TypeProvider.GetOrCreateTypeByMetadataName("Moq.Language.Flow.IThrowsResult");

/// <summary>
/// Gets the interface <c>Moq.Language.Flow.ISetup{T}</c>.
/// </summary>
Expand Down
32 changes: 32 additions & 0 deletions tests/Moq.Analyzers.Test/Common/MoqKnownSymbolsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,20 @@ public void NonVoidSetupPhrase2_WithoutMoqReference_ReturnsNull()
Assert.Null(symbols.NonVoidSetupPhrase2);
}

[Fact]
public void IReturnsResult1_WithoutMoqReference_ReturnsNull()
{
MoqKnownSymbols symbols = CreateSymbolsWithoutMoq();
Assert.Null(symbols.IReturnsResult1);
}

[Fact]
public void IThrowsResult_WithoutMoqReference_ReturnsNull()
{
MoqKnownSymbols symbols = CreateSymbolsWithoutMoq();
Assert.Null(symbols.IThrowsResult);
}

[Fact]
public void IRaiseable_WithoutMoqReference_ReturnsNull()
{
Expand Down Expand Up @@ -902,6 +916,24 @@ public async Task IRaise1_WithMoqReference_ReturnsNamedTypeSymbol()
Assert.Equal(1, symbols.IRaise1!.Arity);
}

[Fact]
public async Task IReturnsResult1_WithMoqReference_ReturnsNamedTypeSymbol()
{
MoqKnownSymbols symbols = await CreateSymbolsWithMoqAsync();
Assert.NotNull(symbols.IReturnsResult1);
Assert.Equal("IReturnsResult", symbols.IReturnsResult1!.Name);
Assert.Equal(1, symbols.IReturnsResult1.Arity);
}

[Fact]
public async Task IThrowsResult_WithMoqReference_ReturnsNamedTypeSymbol()
{
MoqKnownSymbols symbols = await CreateSymbolsWithMoqAsync();
Assert.NotNull(symbols.IThrowsResult);
Assert.Equal("IThrowsResult", symbols.IThrowsResult!.Name);
Assert.Equal(0, symbols.IThrowsResult.Arity);
}

[Fact]
public void Task_WithCoreReferences_ReturnsNamedTypeSymbol()
{
Expand Down
Loading
Loading