Author: Kashika Akhouri
Date: 2026-10-30T17:10:14+05:30
New Revision: 216b5a53b30701e0553c62dbcb8dc02ba3ecdfdb

URL: 
https://github.com/llvm/llvm-project/commit/216b5a53b30701e0553c62dbcb8dc02ba3ecdfdb
DIFF: 
https://github.com/llvm/llvm-project/commit/216b5a53b30701e0553c62dbcb8dc02ba3ecdfdb.diff

LOG: Add support for capture_by on method declarations (#219423)

This PR adds support for `[[clang::lifetime_capture_by(...)]]` attribute
on method declarations (which denote that the implicit this object is
being captured).

- The PR checks the Type attribute (attached to the method's
`TypeSourceInfo`) to check for lifetime_capture_by attribute on method
declaration using a new helper method:
`getCaptureByAttrFromFunctionType`.
- Adds the support for capture_by on method declarations in
`handleLifetimeCaptureBy`.

Example:
```cpp
struct X {
} x;

struct S {
    void capture(X &x) [[clang::lifetime_capture_by(x)]];
};

void use() {
    {
        S s;
        s.capture(x);
    }
    (void)x;
}
```

Warnings Generated
```cpp
c4.cpp:16:9: warning: local variable 's' does not live long enough 
[-Wlifetime-safety-use-after-scope]
   16 |         s.capture(x);
      |         ^
c4.cpp:17:5: note: local variable 's' is destroyed here
   17 |     }
      |     ^
c4.cpp:18:11: note: later used here
   18 |     (void)x;
      |           ^
```

- The PR extends on [Support [[clang::lifetime_capture_by(X)]] in Plain
Containers](https://github.com/llvm/llvm-project/pull/204361) to support
method-level attributes in `collectCaptureBy` ensuring that parameters
capturing this are properly registered as lifetime-tracked types.

- This PR removes `CallArgs` shifting logic from
`handleLifetimeCaptureBy` in `FactsGenerator.cpp` to fix crashes. The
`CapturingArgIdx` generated by the attribute parser already accounts for
the offset of the implicit this, which removed the need for CallArgs.

Added: 
    

Modified: 
    clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h
    clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
    clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp
    clang/lib/Analysis/LifetimeSafety/Origins.cpp
    clang/test/Sema/LifetimeSafety/capture-by.cpp

Removed: 
    


################################################################################
diff  --git 
a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h 
b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h
index 9097bbb267124d..8cf32eabcbbc35 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h
@@ -10,6 +10,7 @@
 #ifndef LLVM_CLANG_ANALYSIS_ANALYSES_LIFETIMEANNOTATIONS_H
 #define LLVM_CLANG_ANALYSIS_ANALYSES_LIFETIMEANNOTATIONS_H
 
+#include "clang/AST/Attr.h"
 #include "clang/AST/DeclCXX.h"
 #include "llvm/ADT/ArrayRef.h"
 #include "llvm/ADT/PointerUnion.h"
@@ -63,6 +64,12 @@ getImplicitObjectParamLifetimeBoundAttr(const FunctionDecl 
*FD);
 /// method or because it's a normal assignment operator.
 bool implicitObjectParamIsLifetimeBound(const FunctionDecl *FD);
 
+/// Check if a function has a lifetime_capture_by attribute on its declaration
+/// or its function type (which represents the implicit 'this' parameter for
+/// methods). Returns the attribute if found, nullptr otherwise.
+const LifetimeCaptureByAttr *
+getCaptureByAttrFromFunctionType(const FunctionDecl *FD);
+
 using LifetimeBoundParamInfo =
     llvm::PointerUnion<const ParmVarDecl *, const CXXMethodDecl *>;
 

diff  --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp 
b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index f3b29ca63fe158..4c39268b99ec21 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -1025,23 +1025,31 @@ void FactsGenerator::handleLifetimeCaptureBy(const 
FunctionDecl *FD,
   bool HasImplicitObjectArg = Method &&
                               Method->isImplicitObjectMemberFunction() &&
                               !isa<CXXConstructorDecl>(FD);
-  auto getParamDeclAt =
-      [FD, HasImplicitObjectArg](unsigned I) -> const ParmVarDecl * {
+  auto getArgCaptureByAttr =
+      [FD, HasImplicitObjectArg](
+          unsigned I) -> std::pair<const LifetimeCaptureByAttr *, QualType> {
     if (HasImplicitObjectArg) {
-      // FIXME: Add support for I == 0 i.e. capture_by on function declarations
-      if (I > 0 && I - 1 < FD->getNumParams())
-        return FD->getParamDecl(I - 1);
+      if (I == 0) {
+        auto *MethodDecl = cast<CXXMethodDecl>(FD);
+        QualType ParamType = MethodDecl->getFunctionObjectParameterType();
+        return {getCaptureByAttrFromFunctionType(FD), ParamType};
+      }
+      if (I > 0 && I - 1 < FD->getNumParams()) {
+        const ParmVarDecl *PVD = FD->getParamDecl(I - 1);
+        return {PVD ? PVD->getAttr<LifetimeCaptureByAttr>() : nullptr,
+                PVD->getType()};
+      }
     } else {
-      if (I < FD->getNumParams())
-        return FD->getParamDecl(I);
+      if (I < FD->getNumParams()) {
+        const ParmVarDecl *PVD = FD->getParamDecl(I);
+        return {PVD ? PVD->getAttr<LifetimeCaptureByAttr>() : nullptr,
+                PVD->getType()};
+      }
     }
-    return nullptr;
+    return {nullptr, QualType()};
   };
   for (unsigned I = 0; I < Args.size(); ++I) {
-    const ParmVarDecl *PVD = getParamDeclAt(I);
-    if (!PVD)
-      continue;
-    const auto *Attr = PVD->getAttr<LifetimeCaptureByAttr>();
+    auto [Attr, ParamType] = getArgCaptureByAttr(I);
     if (!Attr)
       continue;
     OriginList *CapturedOriginList = getOriginsList(*Args[I]);
@@ -1049,7 +1057,7 @@ void FactsGenerator::handleLifetimeCaptureBy(const 
FunctionDecl *FD,
       continue;
     // For references to pointer-like types, peel the outer origin (the pointer
     // object itself) so that we capture the underlying data (the inner 
origin).
-    if (QualType ParamType = PVD->getType();
+    if (!ParamType.isNull() &&
         (ParamType->isReferenceType() &&
          isPointerLikeType(ParamType->getPointeeType())) &&
         CapturedOriginList->getLength() > 1)

diff  --git a/clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp 
b/clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp
index 93cff4764e2533..15f874a142701c 100644
--- a/clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp
@@ -57,40 +57,36 @@ bool isAssignmentOperatorLifetimeBound(const CXXMethodDecl 
*CMD) {
          CMD->getParamDecl(0)->hasAttr<clang::LifetimeBoundAttr>();
 }
 
-/// Check if a function has a lifetimebound attribute on its function type
+/// Check if a function has an attribute on its function type
 /// (which represents the implicit 'this' parameter for methods).
 /// Returns the attribute if found, nullptr otherwise.
-static const LifetimeBoundAttr *
-getLifetimeBoundAttrFromFunctionType(const TypeSourceInfo &TSI) {
-  // Walk through the type layers looking for a lifetimebound attribute.
-  TypeLoc TL = TSI.getTypeLoc();
+template <typename AttrType>
+static const AttrType *getAttrFromFunctionType(const FunctionDecl *FD) {
+  const TypeSourceInfo *TSI = FD->getTypeSourceInfo();
+  if (!TSI)
+    return nullptr;
+  TypeLoc TL = TSI->getTypeLoc();
   while (true) {
     auto ATL = TL.getAsAdjusted<AttributedTypeLoc>();
     if (!ATL)
       break;
-    if (auto *LBAttr = ATL.getAttrAs<LifetimeBoundAttr>())
-      return LBAttr;
+    if (auto *Attr = ATL.getAttrAs<AttrType>())
+      return Attr;
     TL = ATL.getModifiedLoc();
   }
   return nullptr;
 }
 
-const LifetimeBoundAttr *
-getDirectImplicitObjectLifetimeBoundAttr(const FunctionDecl *FD) {
-  if (const TypeSourceInfo *TSI = FD->getTypeSourceInfo())
-    if (const auto *Attr = getLifetimeBoundAttrFromFunctionType(*TSI))
-      return Attr;
-  return nullptr;
-}
-
-const LifetimeBoundAttr *
-getImplicitObjectParamLifetimeBoundAttr(const FunctionDecl *FD) {
+template <typename AttrType>
+static const AttrType *getImplicitObjectParamAttr(const FunctionDecl *FD) {
+  // getDeclWithMergedLifetimeBoundAttrs just returns the most recent decl,
+  // so it is perfectly safe to reuse for capture_by as well.
   FD = getDeclWithMergedLifetimeBoundAttrs(FD);
   // Attribute merging doesn't work well with attributes on function types 
(like
   // 'this' param). We need to check all redeclarations.
-  auto CheckRedecls = [](const FunctionDecl *F) -> const LifetimeBoundAttr * {
+  auto CheckRedecls = [](const FunctionDecl *F) -> const AttrType * {
     for (const FunctionDecl *Redecl : F->redecls())
-      if (const auto *Attr = getDirectImplicitObjectLifetimeBoundAttr(Redecl))
+      if (const auto *Attr = getAttrFromFunctionType<AttrType>(Redecl))
         return Attr;
     return nullptr;
   };
@@ -102,12 +98,27 @@ getImplicitObjectParamLifetimeBoundAttr(const FunctionDecl 
*FD) {
   return nullptr;
 }
 
+const LifetimeBoundAttr *
+getDirectImplicitObjectLifetimeBoundAttr(const FunctionDecl *FD) {
+  return getAttrFromFunctionType<LifetimeBoundAttr>(FD);
+}
+
+const LifetimeBoundAttr *
+getImplicitObjectParamLifetimeBoundAttr(const FunctionDecl *FD) {
+  return getImplicitObjectParamAttr<LifetimeBoundAttr>(FD);
+}
+
 bool implicitObjectParamIsLifetimeBound(const FunctionDecl *FD) {
   if (getImplicitObjectParamLifetimeBoundAttr(FD))
     return true;
   return isNormalAssignmentOperator(FD);
 }
 
+const LifetimeCaptureByAttr *
+getCaptureByAttrFromFunctionType(const FunctionDecl *FD) {
+  return getImplicitObjectParamAttr<LifetimeCaptureByAttr>(FD);
+}
+
 FunctionCallInfo::FunctionCallInfo(const Expr *Call) {
   if (!Call)
     return;

diff  --git a/clang/lib/Analysis/LifetimeSafety/Origins.cpp 
b/clang/lib/Analysis/LifetimeSafety/Origins.cpp
index 4aaf2e2d0b7309..566e9ffa0eab0b 100644
--- a/clang/lib/Analysis/LifetimeSafety/Origins.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Origins.cpp
@@ -107,24 +107,29 @@ class LifetimeAnnotatedOriginTypeCollector
     const auto *MD = dyn_cast<CXXMethodDecl>(FD);
     bool IsInstance = MD && MD->isInstance();
     int Offset = (MD && MD->isImplicitObjectMemberFunction()) ? 1 : 0;
-    for (const auto *Param : FD->parameters()) {
-      if (auto *Attr = Param->getAttr<LifetimeCaptureByAttr>()) {
-        for (int Idx : Attr->params()) {
-          if (Idx == LifetimeCaptureByAttr::Global ||
-              Idx == LifetimeCaptureByAttr::Unknown ||
-              Idx == LifetimeCaptureByAttr::Invalid)
-            continue;
-          if (Idx == LifetimeCaptureByAttr::This) {
-            if (IsInstance)
-              CollectedTypes.push_back(MD->getFunctionObjectParameterType());
-          } else if (int LogicalIdx = Idx - Offset;
-                     LogicalIdx >= 0 &&
-                     (unsigned)LogicalIdx < FD->getNumParams()) {
-            CollectedTypes.push_back(
-                FD->getParamDecl(LogicalIdx)->getType().getNonReferenceType());
-          }
+    auto ProcessAttr = [&](const LifetimeCaptureByAttr *Attr) {
+      for (int Idx : Attr->params()) {
+        if (Idx == LifetimeCaptureByAttr::Global ||
+            Idx == LifetimeCaptureByAttr::Unknown ||
+            Idx == LifetimeCaptureByAttr::Invalid)
+          continue;
+        if (Idx == LifetimeCaptureByAttr::This) {
+          if (IsInstance)
+            CollectedTypes.push_back(MD->getFunctionObjectParameterType());
+        } else if (int LogicalIdx = Idx - Offset;
+                   LogicalIdx >= 0 &&
+                   (unsigned)LogicalIdx < FD->getNumParams()) {
+          CollectedTypes.push_back(
+              FD->getParamDecl(LogicalIdx)->getType().getNonReferenceType());
         }
       }
+    };
+    if (const auto *MethodAttr =
+            IsInstance ? getCaptureByAttrFromFunctionType(FD) : nullptr)
+      ProcessAttr(MethodAttr);
+    for (const auto *Param : FD->parameters()) {
+      if (auto *Attr = Param->getAttr<LifetimeCaptureByAttr>())
+        ProcessAttr(Attr);
     }
   }
 };

diff  --git a/clang/test/Sema/LifetimeSafety/capture-by.cpp 
b/clang/test/Sema/LifetimeSafety/capture-by.cpp
index 99924bed821457..0b69a4b51201e3 100644
--- a/clang/test/Sema/LifetimeSafety/capture-by.cpp
+++ b/clang/test/Sema/LifetimeSafety/capture-by.cpp
@@ -252,19 +252,84 @@ void initializer_list_capture() {
 // Implicit object param 'this' is captured
 // ****************************************************************************
 namespace this_is_captured {
-struct X {} x;
+struct X {} x;    // cfg-note {{this global dangles}}
 struct S {
-  void capture(X &x) [[clang::lifetime_capture_by(x)]];
+  void gets_captured_by(X &x) [[clang::lifetime_capture_by(x)]];
 };
 
-// FIXME: Add support for capture of method declarations in -Wlifetime-safety
 void test() {
-  S{}.capture(x); // expected-warning {{object whose reference is captured by 
'x' will be destroyed at the end of the full-expression}}
-  S s;
-  s.capture(x);
+  S{}.gets_captured_by(x); // expected-warning {{object whose reference is 
captured by 'x' will be destroyed at the end of the full-expression}} \
+                  // cfg-warning {{temporary object does not live long 
enough}} \
+                  // cfg-note {{temporary object is destroyed here}}
+  use(x);        // cfg-note {{later used here}}
+  S s;            
+  s.gets_captured_by(x);   // cfg-warning {{stack memory associated with local 
variable 's' escapes to the global variable 'x' which will dangle}}
 }
 } // namespace this_is_captured
 
+namespace method_decl_capture {
+struct Container {
+  const void* stored = nullptr;
+  void add(const void* s) { stored = s; }
+};
+struct Obj {
+  void register_into(Container& c) const [[clang::lifetime_capture_by(c)]] {
+    c.add(this);
+  }
+};
+void test() {
+  Container c;
+  {
+    Obj local_obj;
+    local_obj.register_into(c);   // cfg-warning {{local variable 'local_obj' 
does not live long enough}}
+  }                               // cfg-note {{local variable 'local_obj' is 
destroyed here}}
+  use(c);                // cfg-note {{later used here}}
+}
+} // namespace method_decl_capture
+
+namespace method_redecl_capture {
+struct X {} x;
+struct S {
+  void gets_captured_by(X &x) [[clang::lifetime_capture_by(x)]];
+};
+
+void S::gets_captured_by(X &x) {
+  return;
+}
+
+void test() {
+  {
+    S s;
+    s.gets_captured_by(x);   // cfg-warning {{local variable 's' does not live 
long enough}}
+  }                          // cfg-note {{local variable 's' is destroyed 
here}}
+  
+  use(x);                   // cfg-note {{later used here}}
+}
+} // namespace method_redecl_capture
+
+namespace template_method_redecl_capture {
+struct X {} x;
+
+template<typename T>
+struct S {
+  void gets_captured_by(X &x);
+};
+
+template<typename T>
+void S<T>::gets_captured_by(X &x)  [[clang::lifetime_capture_by(x)]] {
+}
+
+void test() {
+  {
+    S<int> s;
+    s.gets_captured_by(x);    // cfg-warning {{local variable 's' does not 
live long enough}}
+  }                           // cfg-note {{local variable 's' is destroyed 
here}}
+  
+  use(x);                    // cfg-note {{later used here}}
+}
+
+} // template_method_redecl_capture
+
 namespace temporary_capturing_object {
 struct S {
   void add(const int& x [[clang::lifetime_capture_by_this]]);


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

Reply via email to