https://github.com/rniwa created 
https://github.com/llvm/llvm-project/pull/223942

RawPtrRefLambdaCapturesChecker only consults NOESCAPE when it can find a 
FunctionDecl for a call. Because a lambda argument is type-dependent, a call 
taking one inside a template is dependent, and its callee is often still 
unresolved in the template pattern: an UnresolvedLookupExpr for an unqualified 
call or a call to a function template, an UnresolvedMemberExpr for an 
overloaded member function or a member function template, and a 
CXXDependentScopeMemberExpr for a member of a dependent object. checkParameters 
never ran for those, so a lambda was reported even when the parameter it's 
passed to is annotated with NOESCAPE. The same happened for a lambda passed to 
a constructor which isn't resolved until the instantiation, which appears as a 
CXXUnresolvedConstructExpr or as a dependent ParenListExpr / InitListExpr.

Whether such a lambda can escape isn't known before the enclosing template is 
instantiated, so ignore these lambdas and let the instantiation check them, 
matching what RetainPtrCtorAdoptChecker already does. A lambda in a template 
which is never instantiated is never used, so not checking it is fine.

Waiting for the instantiation also requires traversing the instantiations of a 
generic lambda's call operator. RecursiveASTVisitor::TraverseLambdaExpr only 
traverses the lambda's body, which is a template pattern for a generic lambda, 
so those instantiations were never visited and the calls in them were never 
resolved. Traverse the body of each specialization, not the CXXMethodDecl, so 
that the lambda stays associated with the class enclosing it and captures of 
this are still reported.

>From 1c2ea534250c8241a3e06d7a7503fd4f2aded686 Mon Sep 17 00:00:00 2001
From: Ryosuke Niwa <[email protected]>
Date: Wed, 16 Sep 2026 00:57:04 -0700
Subject: [PATCH] [webkit.UncountedLambdaCapturesChecker] NOESCAPE is ignored
 in a template function

RawPtrRefLambdaCapturesChecker only consults NOESCAPE when it can find a
FunctionDecl for a call. Because a lambda argument is type-dependent, a call
taking one inside a template is dependent, and its callee is often still
unresolved in the template pattern: an UnresolvedLookupExpr for an unqualified
call or a call to a function template, an UnresolvedMemberExpr for an overloaded
member function or a member function template, and a CXXDependentScopeMemberExpr
for a member of a dependent object. checkParameters never ran for those, so a
lambda was reported even when the parameter it's passed to is annotated with
NOESCAPE. The same happened for a lambda passed to a constructor which isn't
resolved until the instantiation, which appears as a CXXUnresolvedConstructExpr
or as a dependent ParenListExpr / InitListExpr.

Whether such a lambda can escape isn't known before the enclosing template is
instantiated, so ignore these lambdas and let the instantiation check them,
matching what RetainPtrCtorAdoptChecker already does. A lambda in a template
which is never instantiated is never used, so not checking it is fine.

Waiting for the instantiation also requires traversing the instantiations of a
generic lambda's call operator. RecursiveASTVisitor::TraverseLambdaExpr only
traverses the lambda's body, which is a template pattern for a generic lambda,
so those instantiations were never visited and the calls in them were never
resolved. Traverse the body of each specialization, not the CXXMethodDecl, so
that the lambda stays associated with the class enclosing it and captures of
this are still reported.
---
 .../WebKit/RawPtrRefLambdaCapturesChecker.cpp |  68 +++++++-
 .../WebKit/uncounted-lambda-captures.cpp      | 165 ++++++++++++++++++
 2 files changed, 228 insertions(+), 5 deletions(-)

diff --git 
a/clang/lib/StaticAnalyzer/Checkers/WebKit/RawPtrRefLambdaCapturesChecker.cpp 
b/clang/lib/StaticAnalyzer/Checkers/WebKit/RawPtrRefLambdaCapturesChecker.cpp
index 02b6c31c47223..43db3065dd068 100644
--- 
a/clang/lib/StaticAnalyzer/Checkers/WebKit/RawPtrRefLambdaCapturesChecker.cpp
+++ 
b/clang/lib/StaticAnalyzer/Checkers/WebKit/RawPtrRefLambdaCapturesChecker.cpp
@@ -118,6 +118,24 @@ class RawPtrRefLambdaCapturesChecker
         return true;
       }
 
+      bool TraverseLambdaExpr(LambdaExpr *L) override {
+        if (!DynamicRecursiveASTVisitor::TraverseLambdaExpr(L))
+          return false;
+        // The body of a generic lambda is a template pattern in which calls 
may
+        // not have been resolved yet, so traverse the instantiations of its
+        // call operator as well. Only the body is traversed so that the lambda
+        // stays associated with the class enclosing it, like the pattern is.
+        if (auto *FTD = L->getLambdaClass()->getDependentLambdaCallOperator()) 
{
+          for (auto *Spec : FTD->specializations()) {
+            if (auto *Body = Spec->getBody()) {
+              if (!TraverseStmt(Body))
+                return false;
+            }
+          }
+        }
+        return true;
+      }
+
       bool VisitVarDecl(VarDecl *VD) override {
         auto *Init = VD->getInit();
         if (!Init)
@@ -263,15 +281,55 @@ class RawPtrRefLambdaCapturesChecker
           if (isVisitFunction(CE, Callee))
             return true;
           checkParameters(CE, Callee);
-        } else if (auto *CalleeE = CE->getCallee()) {
-          if (auto *DRE = dyn_cast<DeclRefExpr>(CalleeE->IgnoreParenCasts())) {
-            if (auto *Callee = dyn_cast_or_null<FunctionDecl>(DRE->getDecl()))
-              checkParameters(CE, Callee);
-          }
+          return true;
         }
+        auto *CalleeE = CE->getCallee();
+        if (!CalleeE)
+          return true;
+        CalleeE = CalleeE->IgnoreParenCasts();
+        if (auto *DRE = dyn_cast<DeclRefExpr>(CalleeE)) {
+          if (auto *Callee = dyn_cast_or_null<FunctionDecl>(DRE->getDecl()))
+            checkParameters(CE, Callee);
+          return true;
+        }
+        // The callee of a call in an uninstantiated template may not have been
+        // resolved yet, in which case whether each lambda argument can escape
+        // isn't known. Wait for the instantiation to check those lambdas.
+        if (isa<OverloadExpr, CXXDependentScopeMemberExpr,
+                DependentScopeDeclRefExpr>(CalleeE))
+          ignoreLambdasInArgs({CE->getArgs(), CE->getNumArgs()});
         return true;
       }
 
+      // Lambdas passed to a constructor which isn't resolved until the
+      // enclosing template is instantiated are checked in the instantiation.
+      bool
+      VisitCXXUnresolvedConstructExpr(CXXUnresolvedConstructExpr *CE) override 
{
+        ignoreLambdasInArgs({CE->arg_begin(), CE->arg_end()});
+        return true;
+      }
+
+      bool VisitParenListExpr(ParenListExpr *PLE) override {
+        if (PLE->isTypeDependent())
+          ignoreLambdasInArgs(PLE->exprs());
+        return true;
+      }
+
+      bool VisitInitListExpr(InitListExpr *ILE) override {
+        if (ILE->isTypeDependent())
+          ignoreLambdasInArgs(ILE->inits());
+        return true;
+      }
+
+      void ignoreLambdasInArgs(ArrayRef<Expr *> Args) {
+        for (auto *Arg : Args) {
+          if (!Arg)
+            continue;
+          if (auto *L = findLambdaInArg(Arg->IgnoreParenCasts()))
+            LambdasToIgnore.insert(L);
+        }
+      }
+
       bool isVisitFunction(CallExpr *CallExpr, FunctionDecl *FnDecl) {
         bool IsVisitFn = safeGetName(FnDecl) == "visit";
         if (!IsVisitFn)
diff --git a/clang/test/Analysis/Checkers/WebKit/uncounted-lambda-captures.cpp 
b/clang/test/Analysis/Checkers/WebKit/uncounted-lambda-captures.cpp
index 3609014deedd8..d1cc7588ed8d6 100644
--- a/clang/test/Analysis/Checkers/WebKit/uncounted-lambda-captures.cpp
+++ b/clang/test/Analysis/Checkers/WebKit/uncounted-lambda-captures.cpp
@@ -520,3 +520,168 @@ class LambdaInConstructorDestructor {
 
   void doWork();
 };
+
+void callNoEscape([[clang::noescape]] const WTF::Function<void()>&);
+
+template <typename... Callbacks>
+void variadicNoEscape([[clang::noescape]] Callbacks&&... callbacks) {
+  someFunction();
+}
+
+template <typename Callback>
+void templateNoEscape([[clang::noescape]] Callback&& callback) {
+  someFunction();
+}
+
+struct NoEscapeHolder {
+  NoEscapeHolder([[clang::noescape]] const WTF::Function<void()>&);
+  void member([[clang::noescape]] const WTF::Function<void()>&);
+  void overloaded([[clang::noescape]] const WTF::Function<void()>&);
+  void overloaded(int);
+  template <typename Callback> void memberTemplate([[clang::noescape]] 
Callback&&);
+};
+
+template <typename T>
+void noescape_in_template(NoEscapeHolder& holder, T& dependentHolder) {
+  RefCountable* obj = make_obj();
+  callNoEscape([obj] {
+    obj->method();
+    someFunction();
+  });
+  templateNoEscape([obj] {
+    obj->method();
+    someFunction();
+  });
+  variadicNoEscape([obj] {
+    obj->method();
+    someFunction();
+  }, [obj] {
+    obj->method();
+    someFunction();
+  });
+  holder.overloaded([obj] {
+    obj->method();
+    someFunction();
+  });
+  holder.memberTemplate([obj] {
+    obj->method();
+    someFunction();
+  });
+  dependentHolder.member([obj] {
+    obj->method();
+    someFunction();
+  });
+  NoEscapeHolder holderFromParenInit([obj] {
+    obj->method();
+    someFunction();
+  });
+  NoEscapeHolder holderFromListInit { [obj] {
+    obj->method();
+    someFunction();
+  } };
+  auto holderFromTemporary = NoEscapeHolder([obj] {
+    obj->method();
+    someFunction();
+  });
+}
+
+struct EscapeHolder {
+  EscapeHolder(const WTF::Function<void()>&);
+  void member(const WTF::Function<void()>&);
+};
+
+template <typename Callback>
+void templateEscape(Callback&& callback);
+
+template <typename T>
+void escape_in_template(EscapeHolder& holder, T& dependentHolder) {
+  RefCountable* obj = make_obj();
+  callAsync([obj] {
+    // expected-warning@-1{{Captured variable 'obj' is a raw pointer to 
RefPtr-capable type 'RefCountable' [webkit.UncountedLambdaCapturesChecker]}}
+    obj->method();
+    someFunction();
+  });
+  templateEscape([obj] {
+    // expected-warning@-1{{Captured variable 'obj' is a raw pointer to 
RefPtr-capable type 'RefCountable' [webkit.UncountedLambdaCapturesChecker]}}
+    obj->method();
+    someFunction();
+  });
+  holder.member([obj] {
+    // expected-warning@-1{{Captured variable 'obj' is a raw pointer to 
RefPtr-capable type 'RefCountable' [webkit.UncountedLambdaCapturesChecker]}}
+    obj->method();
+    someFunction();
+  });
+  dependentHolder.member([obj] {
+    // expected-warning@-1{{Captured variable 'obj' is a raw pointer to 
RefPtr-capable type 'RefCountable' [webkit.UncountedLambdaCapturesChecker]}}
+    obj->method();
+    someFunction();
+  });
+  EscapeHolder holderFromParenInit([obj] {
+    // expected-warning@-1{{Captured variable 'obj' is a raw pointer to 
RefPtr-capable type 'RefCountable' [webkit.UncountedLambdaCapturesChecker]}}
+    obj->method();
+    someFunction();
+  });
+}
+
+void instantiate_templates(NoEscapeHolder& noEscapeHolder, EscapeHolder& 
escapeHolder) {
+  noescape_in_template(noEscapeHolder, noEscapeHolder);
+  escape_in_template(escapeHolder, escapeHolder);
+}
+
+// The overloads disagree about NOESCAPE, so which one is picked isn't known
+// until the template is instantiated.
+void mixedNoEscape([[clang::noescape]] const WTF::Function<void()>&, int);
+void mixedNoEscape(const WTF::Function<void()>&, const char*);
+
+template <typename T>
+void mixed_noescape_overloads_in_template() {
+  RefCountable* obj = make_obj();
+  mixedNoEscape([obj] {
+    obj->method();
+    someFunction();
+  }, 1);
+  mixedNoEscape([obj] {
+    // expected-warning@-1{{Captured variable 'obj' is a raw pointer to 
RefPtr-capable type 'RefCountable' [webkit.UncountedLambdaCapturesChecker]}}
+    obj->method();
+    someFunction();
+  }, "");
+}
+
+void instantiate_mixed_noescape_overloads() {
+  mixed_noescape_overloads_in_template<int>();
+}
+
+// The body of a generic lambda is a template pattern, so the calls in it are
+// checked in the instantiations of its call operator.
+template <typename Callback>
+void withValue(Callback callback) {
+  callback(3);
+  callback(4U);
+}
+
+void noescape_in_generic_lambda(RefCountable* obj) {
+  withValue([obj](auto value) {
+    // expected-warning@-1{{Captured variable 'obj' is a raw pointer to 
RefPtr-capable type 'RefCountable' [webkit.UncountedLambdaCapturesChecker]}}
+    callNoEscape([obj] {
+      obj->method();
+      someFunction();
+    });
+    templateNoEscape([obj] {
+      obj->method();
+      someFunction();
+    });
+    (void)value;
+  });
+}
+
+void escape_in_generic_lambda(RefCountable* obj) {
+  withValue([obj](auto value) {
+    // expected-warning@-1{{Captured variable 'obj' is a raw pointer to 
RefPtr-capable type 'RefCountable' [webkit.UncountedLambdaCapturesChecker]}}
+    callAsync([obj] {
+      // expected-warning@-1{{Captured variable 'obj' is a raw pointer to 
RefPtr-capable type 'RefCountable' [webkit.UncountedLambdaCapturesChecker]}}
+      obj->method();
+      someFunction();
+    });
+    (void)value;
+  });
+}

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to