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
