diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD110ObserveResultOfAsyncCallsAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD110ObserveResultOfAsyncCallsAnalyzer.cs
index 864c3f5e4..1bc7a2004 100644
--- a/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD110ObserveResultOfAsyncCallsAnalyzer.cs
+++ b/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD110ObserveResultOfAsyncCallsAnalyzer.cs
@@ -43,6 +43,98 @@ public override void Initialize(AnalysisContext context)
});
}
+ ///
+ /// Determines whether an invocation is within a lambda expression that is being converted to an Expression tree.
+ ///
+ /// The invocation operation to check.
+ /// True if the invocation is within a lambda converted to an Expression; false otherwise.
+ 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;
+ }
+
+ ///
+ /// Determines whether a lambda is being converted to an Expression tree type.
+ ///
+ /// The lambda operation to check.
+ /// True if the lambda is being converted to an Expression; false otherwise.
+ 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;
+ }
+
+ ///
+ /// Determines whether a type is an Expression tree type (Expression<T> or related types).
+ ///
+ /// The type to check.
+ /// True if the type is an Expression tree type; false otherwise.
+ private static bool IsExpressionTreeType(ITypeSymbol? type)
+ {
+ if (type is not INamedTypeSymbol namedType)
+ {
+ return false;
+ }
+
+ // Check for System.Linq.Expressions.Expression
+ 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;
@@ -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;
diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD110ObserveResultOfAsyncCallsAnalyzerTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD110ObserveResultOfAsyncCallsAnalyzerTests.cs
index 95cdeffad..c9fca9dd6 100644
--- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD110ObserveResultOfAsyncCallsAnalyzerTests.cs
+++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD110ObserveResultOfAsyncCallsAnalyzerTests.cs
@@ -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(Expression> expression)
+ {
+ }
+ }
+
+ class Test
+ {
+ void TestMethod()
+ {
+ var logger = new MockLogger();
+ MockVerifier.Verify(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> 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
+ {
+ public void Verify(Expression> expression, Times times, string message)
+ {
+ }
+ }
+
+ enum Times
+ {
+ Never
+ }
+
+ class Test
+ {
+ void TestMethod()
+ {
+ var mock = new Mock();
+ 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> expr = x => x.InfoAsync("test");
+ }
+ }
+ """;
+
+ await CSVerify.VerifyAnalyzerAsync(test);
+ }
}