From be8ea8aff2d503d00b19cab49a06f7516ee853d9 Mon Sep 17 00:00:00 2001 From: Sergey Tepliakov Date: Wed, 20 May 2026 13:49:33 -0700 Subject: [PATCH] EPC30: don't warn when same method is called from a nested function (#318) RecursiveCallAnalyzer descended into the full method body, so calls to the containing method from inside a lambda, anonymous method, or local function were flagged as unconditional recursion. Those calls don't execute as part of the enclosing method's control flow, so skip invocations nested inside IAnonymousFunctionOperation / ILocalFunctionOperation. --- .../RecursiveCallAnalyzerTests.cs | 46 +++++++++++++++++++ .../RecursiveCallAnalyzer.cs | 20 ++++++++ 2 files changed, 66 insertions(+) diff --git a/src/ErrorProne.NET.CoreAnalyzers.Tests/RecursiveCallAnalyzerTests.cs b/src/ErrorProne.NET.CoreAnalyzers.Tests/RecursiveCallAnalyzerTests.cs index 0a5c88c..cc43e0c 100644 --- a/src/ErrorProne.NET.CoreAnalyzers.Tests/RecursiveCallAnalyzerTests.cs +++ b/src/ErrorProne.NET.CoreAnalyzers.Tests/RecursiveCallAnalyzerTests.cs @@ -127,6 +127,52 @@ void Foo(bool b) { if (b) Foo(false); } } +"; + await Verify.VerifyAsync(test); + } + + [Test] + public async Task NoWarn_When_Same_Method_Is_Called_From_Lambda() + { + // Issue #318: a call to the same method from within a lambda is not + // an unconditional recursive call -- the lambda body is deferred. + var test = @" +using System; +class C { + void Foo() { + Action a = () => Foo(); + a(); + } +} +"; + await Verify.VerifyAsync(test); + } + + [Test] + public async Task NoWarn_When_Same_Method_Is_Called_From_AnonymousMethod() + { + var test = @" +using System; +class C { + void Foo() { + Action a = delegate { Foo(); }; + a(); + } +} +"; + await Verify.VerifyAsync(test); + } + + [Test] + public async Task NoWarn_When_Same_Method_Is_Called_From_LocalFunction() + { + var test = @" +class C { + void Foo() { + void Local() { Foo(); } + Local(); + } +} "; await Verify.VerifyAsync(test); } diff --git a/src/ErrorProne.NET.CoreAnalyzers/RecursiveCallAnalyzer.cs b/src/ErrorProne.NET.CoreAnalyzers/RecursiveCallAnalyzer.cs index 5b1b8e9..31ff73b 100644 --- a/src/ErrorProne.NET.CoreAnalyzers/RecursiveCallAnalyzer.cs +++ b/src/ErrorProne.NET.CoreAnalyzers/RecursiveCallAnalyzer.cs @@ -31,6 +31,13 @@ private static void AnalyzeMethodBody(OperationAnalysisContext context) foreach (var invocation in methodBody.Descendants().OfType()) { + // Calls inside a lambda or local function don't execute as part of this method's + // immediate control flow, so they aren't unconditional recursion. See issue #318. + if (IsInsideNestedFunction(invocation)) + { + continue; + } + // Check if all parameters are passed as-is // So Factorial(n - 1) should be totally fine! if (invocation.Arguments.Length == method.Parameters.Length && @@ -62,6 +69,19 @@ arg.Value is IParameterReferenceOperation paramRef && } } + private static bool IsInsideNestedFunction(IOperation operation) + { + for (var parent = operation.Parent; parent != null; parent = parent.Parent) + { + if (parent is IAnonymousFunctionOperation or ILocalFunctionOperation) + { + return true; + } + } + + return false; + } + private static bool HasTouchedRefParameterBeforeCall(IInvocationOperation recursiveCall, IMethodSymbol method, HashSet touchedRefParameters) { // Check if any ref parameter in the recursive call was touched