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
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,98 @@ public override void Initialize(AnalysisContext context)
});
}

/// <summary>
/// Determines whether an invocation is within a lambda expression that is being converted to an Expression tree.
/// </summary>
/// <param name="operation">The invocation operation to check.</param>
/// <returns>True if the invocation is within a lambda converted to an Expression; false otherwise.</returns>
private static bool IsWithinExpressionLambda(IInvocationOperation operation)
{
// Walk up the operation tree to find the containing lambda
IOperation? current = operation.Parent;
while (current is not null)
{
if (current is IAnonymousFunctionOperation lambda)
{
// Found a lambda, now check if it's being converted to an Expression<>
return IsLambdaConvertedToExpression(lambda);
}

current = current.Parent;
}

return false;
}

/// <summary>
/// Determines whether a lambda is being converted to an Expression tree type.
/// </summary>
/// <param name="lambda">The lambda operation to check.</param>
/// <returns>True if the lambda is being converted to an Expression; false otherwise.</returns>
private static bool IsLambdaConvertedToExpression(IAnonymousFunctionOperation lambda)
{
// Walk up from the lambda to find conversion or argument operations
IOperation? current = lambda.Parent;
while (current is not null)
{
// Check if the lambda's parent is a conversion operation
if (current is IConversionOperation conversion)
{
// Check if the target type is Expression<> or a related expression tree type
return IsExpressionTreeType(conversion.Type);
}

// Check if the lambda is being passed as an argument to a method expecting Expression<>
if (current is IArgumentOperation argument &&
argument.Parameter?.Type is INamedTypeSymbol parameterType)
{
return IsExpressionTreeType(parameterType);
}

// Allow certain operations to be skipped (like parentheses)
if (current is IParenthesizedOperation)
{
current = current.Parent;
continue;
}

// Stop walking up at other operation types to avoid false positives
break;
}

return false;
}

/// <summary>
/// Determines whether a type is an Expression tree type (Expression&lt;T&gt; or related types).
/// </summary>
/// <param name="type">The type to check.</param>
/// <returns>True if the type is an Expression tree type; false otherwise.</returns>
private static bool IsExpressionTreeType(ITypeSymbol? type)
{
if (type is not INamedTypeSymbol namedType)
{
return false;
}

// Check for System.Linq.Expressions.Expression<T>
if (namedType.Name == "Expression" &&
namedType.ContainingNamespace?.ToDisplayString() == "System.Linq.Expressions" &&
namedType.IsGenericType)
{
return true;
}

// Check for LambdaExpression and other expression types
if (namedType.ContainingNamespace?.ToDisplayString() == "System.Linq.Expressions" &&
(namedType.Name == "LambdaExpression" || namedType.Name.EndsWith("Expression")))
{
return true;
}

return false;
}

private void AnalyzeInvocation(OperationAnalysisContext context, CommonInterest.AwaitableTypeTester awaitableTypes)
{
var operation = (IInvocationOperation)context.Operation;
Expand All @@ -57,6 +149,13 @@ private void AnalyzeInvocation(OperationAnalysisContext context, CommonInterest.
return;
}

// Check if this invocation is within a lambda that's being converted to an Expression<>
if (IsWithinExpressionLambda(operation))
{
// This invocation is within a lambda converted to an expression tree, so it's not actually being invoked.
return;
}

// Only consider invocations that are direct statements (or are statements through limited steps).
// Otherwise, we assume their result is awaited, assigned, or otherwise consumed.
IOperation? parentOperation = operation.Parent;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -542,4 +542,158 @@ void DoOperation()

await CSVerify.VerifyAnalyzerAsync(test);
}

[Fact]
public async Task ExpressionLambda_ProducesNoDiagnostic()
{
string test = """
using System;
using System.Linq.Expressions;
using System.Threading.Tasks;

interface ILogger
{
Task InfoAsync(string message);
}

class MockVerifier
{
public static void Verify<T>(Expression<Func<T, Task>> expression)
{
}
}

class Test
{
void TestMethod()
{
var logger = new MockLogger();
MockVerifier.Verify<ILogger>(x => x.InfoAsync("test"));
}
}

class MockLogger : ILogger
{
public Task InfoAsync(string message) => Task.CompletedTask;
}
""";

await CSVerify.VerifyAnalyzerAsync(test);
}

[Fact]
public async Task ExpressionFuncLambda_ProducesNoDiagnostic()
{
string test = """
using System;
using System.Linq.Expressions;
using System.Threading.Tasks;

class Test
{
void TestMethod()
{
SomeMethod(x => x.InfoAsync("test"));
}

void SomeMethod(Expression<Func<ILogger, Task>> expression)
{
}

Task InfoAsync(string message) => Task.CompletedTask;
}

interface ILogger
{
Task InfoAsync(string message);
}
""";

await CSVerify.VerifyAnalyzerAsync(test);
}

[Fact]
public async Task MoqLikeScenario_ProducesNoDiagnostic()
{
string test = """
using System;
using System.Linq.Expressions;
using System.Threading.Tasks;

interface ILogger
{
Task InfoAsync(string message);
}

class Mock<T>
{
public void Verify(Expression<Func<T, Task>> expression, Times times, string message)
{
}
}

enum Times
{
Never
}

class Test
{
void TestMethod()
{
var mock = new Mock<ILogger>();
mock.Verify(x => x.InfoAsync("test"), Times.Never, "No Log should have been written");
}
}
""";

await CSVerify.VerifyAnalyzerAsync(test);
}

[Fact]
public async Task DirectTaskCall_StillProducesDiagnostic()
{
string test = """
using System.Threading.Tasks;

class Test
{
void TestMethod()
{
// This should still trigger VSTHRD110 - direct call not in expression
[|TaskReturningMethod()|];
}

Task TaskReturningMethod() => Task.CompletedTask;
}
""";

await CSVerify.VerifyAnalyzerAsync(test);
}

[Fact]
public async Task ExpressionAssignment_ProducesNoDiagnostic()
{
string test = """
using System;
using System.Linq.Expressions;
using System.Threading.Tasks;

interface ILogger
{
Task InfoAsync(string message);
}

class Test
{
void TestMethod()
{
// Assignment to Expression<> variable should not trigger VSTHRD110
Expression<Func<ILogger, Task>> expr = x => x.InfoAsync("test");
}
}
""";

await CSVerify.VerifyAnalyzerAsync(test);
}
}