[clang] [webkit.UncountedLambdaCapturesChecker] NOESCAPE is ignored in a template function (PR #223942)

via cfe-commits cfe-commits at lists.llvm.org
Wed Sep 16 01:01:48 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-clang-static-analyzer-1

Author: Ryosuke Niwa (rniwa)

<details>
<summary>Changes</summary>

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.

---
Full diff: https://github.com/llvm/llvm-project/pull/223942.diff


2 Files Affected:

- (modified) clang/lib/StaticAnalyzer/Checkers/WebKit/RawPtrRefLambdaCapturesChecker.cpp (+63-5) 
- (modified) clang/test/Analysis/Checkers/WebKit/uncounted-lambda-captures.cpp (+165) 


``````````diff
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 at -1{{Captured variable 'obj' is a raw pointer to RefPtr-capable type 'RefCountable' [webkit.UncountedLambdaCapturesChecker]}}
+    obj->method();
+    someFunction();
+  });
+  templateEscape([obj] {
+    // expected-warning at -1{{Captured variable 'obj' is a raw pointer to RefPtr-capable type 'RefCountable' [webkit.UncountedLambdaCapturesChecker]}}
+    obj->method();
+    someFunction();
+  });
+  holder.member([obj] {
+    // expected-warning at -1{{Captured variable 'obj' is a raw pointer to RefPtr-capable type 'RefCountable' [webkit.UncountedLambdaCapturesChecker]}}
+    obj->method();
+    someFunction();
+  });
+  dependentHolder.member([obj] {
+    // expected-warning at -1{{Captured variable 'obj' is a raw pointer to RefPtr-capable type 'RefCountable' [webkit.UncountedLambdaCapturesChecker]}}
+    obj->method();
+    someFunction();
+  });
+  EscapeHolder holderFromParenInit([obj] {
+    // expected-warning at -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 at -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 at -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 at -1{{Captured variable 'obj' is a raw pointer to RefPtr-capable type 'RefCountable' [webkit.UncountedLambdaCapturesChecker]}}
+    callAsync([obj] {
+      // expected-warning at -1{{Captured variable 'obj' is a raw pointer to RefPtr-capable type 'RefCountable' [webkit.UncountedLambdaCapturesChecker]}}
+      obj->method();
+      someFunction();
+    });
+    (void)value;
+  });
+}

``````````

</details>


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


More information about the cfe-commits mailing list