[clang-tools-extra] [clang-tidy] use-after-move: Support null_after_move annotations (PR #186903)
via cfe-commits
cfe-commits at lists.llvm.org
Mon Mar 16 15:51:05 PDT 2026
https://github.com/juanvazquez updated https://github.com/llvm/llvm-project/pull/186903
>From 23b5fd1cda6d17b541c58c7eb5b79a5fe91474e3 Mon Sep 17 00:00:00 2001
From: Juan Vazquez <juvazq at google.com>
Date: Mon, 16 Mar 2026 22:44:32 +0100
Subject: [PATCH] [clang-tidy] use-after-move: Support null_after_move
annotations
Extend the bugprone-use-after-move check to recognize user defined
smart-pointer-like types that make guarantees on the state of a moved-from
object, leaving it in a valid and specified state that matches the standard
smart pointer's moved-from state (nullptr), where it is safe to use but not
dereference.
Following the RFC discussion:
* Use [[clang::annotate]] to mark the types.
* Use an schema for the [[clang::annotate]] annotation and arguments to help
avoid conflicts with other users of the attribute.
* The annotation will identify the tool ("clang-tidy") and the arguments the
plugin ("bugprone-use-after-move"), and the behavior of the type
("null_after_move"). E.g.:
[[clang::annotate("clang-tidy", "bugprone-use-after-move", "null_after_move")]]
RFC: https://discourse.llvm.org/t/rfc-add-a-class-attribute-clang-null-after-move-for-use-after-move-analysis/89760
---
.../clang-tidy/bugprone/UseAfterMoveCheck.cpp | 47 +++++++-
.../checks/bugprone/use-after-move.rst | 7 +-
.../checkers/bugprone/use-after-move.cpp | 101 ++++++++++++++++++
3 files changed, 149 insertions(+), 6 deletions(-)
diff --git a/clang-tools-extra/clang-tidy/bugprone/UseAfterMoveCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/UseAfterMoveCheck.cpp
index 8e9f48ee0cd21..91f8bc06bf002 100644
--- a/clang-tools-extra/clang-tidy/bugprone/UseAfterMoveCheck.cpp
+++ b/clang-tools-extra/clang-tidy/bugprone/UseAfterMoveCheck.cpp
@@ -8,11 +8,14 @@
#include "UseAfterMoveCheck.h"
+#include "clang/AST/Attr.h"
#include "clang/AST/Expr.h"
#include "clang/AST/ExprCXX.h"
+#include "clang/AST/Type.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Analysis/Analyses/CFGReachabilityAnalysis.h"
#include "clang/Analysis/CFG.h"
+#include "clang/Basic/LLVM.h"
#include "clang/Lex/Lexer.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/SmallPtrSet.h"
@@ -323,7 +326,32 @@ void UseAfterMoveFinder::getUsesAndReinits(
});
}
-static bool isStandardSmartPointer(const ValueDecl *VD) {
+static std::optional<StringRef> getStringLiteral(const Expr *E) {
+ if (!E)
+ return std::nullopt;
+ if (const auto *SL = dyn_cast<StringLiteral>(E->IgnoreParenImpCasts()))
+ return SL->getString();
+ return std::nullopt;
+}
+
+// User defined types can use [[clang::annotate]] to mark smart-pointer-like
+// types with a specified move from state that matches the standard smart
+// pointer's moved-from state (nullptr).
+static bool isNullAfterMoveAnnotate(const AnnotateAttr *Attr) {
+ if (Attr->getAnnotation() != "clang-tidy")
+ return false;
+
+ if (Attr->args_size() != 2)
+ return false;
+
+ std::optional<StringRef> Plugin = getStringLiteral(Attr->args_begin()[0]);
+ std::optional<StringRef> Annotation = getStringLiteral(Attr->args_begin()[1]);
+
+ return Plugin && Annotation && *Plugin == "bugprone-use-after-move" &&
+ *Annotation == "null_after_move";
+}
+
+static bool isSpecifiedAfterMove(const ValueDecl *VD) {
const Type *TheType = VD->getType().getNonReferenceType().getTypePtrOrNull();
if (!TheType)
return false;
@@ -332,6 +360,16 @@ static bool isStandardSmartPointer(const ValueDecl *VD) {
if (!RecordDecl)
return false;
+ // Use the definition for the declaration, as it is the expected place to add
+ // the annotations.
+ const CXXRecordDecl *DefinitionDecl = RecordDecl->getDefinition();
+ if (DefinitionDecl != nullptr) {
+ for (const auto *Attr : DefinitionDecl->specific_attrs<AnnotateAttr>())
+ if (isNullAfterMoveAnnotate(Attr))
+ return true;
+ }
+
+ // Standard smart pointers have a well-specified moved-from state (nullptr).
const IdentifierInfo *ID = RecordDecl->getIdentifier();
if (!ID)
return false;
@@ -358,9 +396,10 @@ void UseAfterMoveFinder::getDeclRefs(
const auto *DeclRef = Match.getNodeAs<DeclRefExpr>("declref");
const auto *Operator = Match.getNodeAs<CXXOperatorCallExpr>("operator");
if (DeclRef && BlockMap->blockContainingStmt(DeclRef) == Block) {
- // Ignore uses of a standard smart pointer that don't dereference the
- // pointer.
- if (Operator || !isStandardSmartPointer(DeclRef->getDecl()))
+ // Ignore uses of a standard smart pointer or classes annotated as
+ // "null_after_move" (smart-pointer-like behavior) that don't
+ // dereference the pointer.
+ if (Operator || !isSpecifiedAfterMove(DeclRef->getDecl()))
DeclRefs->insert(DeclRef);
}
}
diff --git a/clang-tools-extra/docs/clang-tidy/checks/bugprone/use-after-move.rst b/clang-tools-extra/docs/clang-tidy/checks/bugprone/use-after-move.rst
index 3218b32ce2c58..787b3a98026e7 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/bugprone/use-after-move.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/bugprone/use-after-move.rst
@@ -195,8 +195,6 @@ Use
Any occurrence of the moved variable that is not a reinitialization (see below)
is considered to be a use.
-An exception to this are objects of type ``std::unique_ptr``,
-``std::shared_ptr``, ``std::weak_ptr``, ``std::optional``, and ``std::any``.
An exception to this are objects of type ``std::unique_ptr``,
``std::shared_ptr``, ``std::weak_ptr``, ``std::optional``, and ``std::any``,
which can be reinitialized via ``reset``. For smart pointers specifically, the
@@ -204,6 +202,11 @@ moved-from objects have a well-defined state of being ``nullptr``s, and only
``operator*``, ``operator->`` and ``operator[]`` are considered bad accesses as
they would be dereferencing a ``nullptr``.
+User-defined types can be annotated as having the same semantics as standard
+smart pointers with ``[[clang::annotate("clang-tidy", "bugprone-use-after-move",
+"null_after_move")]]``. This expresses that a moved-from object of this type is
+a null pointer.
+
If multiple uses occur after a move, only the first of these is flagged.
Reinitialization
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/use-after-move.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/use-after-move.cpp
index 5d95c44fc318f..6a7b4a5614b88 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/use-after-move.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/use-after-move.cpp
@@ -1023,6 +1023,107 @@ void reinitAnnotation() {
}
}
+////////////////////////////////////////////////////////////////////////////////
+// Tests for annotations on smart-pointer-like types
+
+namespace null_after_move {
+
+template <typename T>
+class [[clang::annotate("clang-tidy",
+ "bugprone-use-after-move",
+ "null_after_move")]] SmartPtrAlike {
+public:
+ SmartPtrAlike();
+ ~SmartPtrAlike();
+ SmartPtrAlike(const SmartPtrAlike&) = delete;
+ SmartPtrAlike &operator=(const SmartPtrAlike&) = delete;
+ SmartPtrAlike(SmartPtrAlike&& other);
+ SmartPtrAlike &operator=(SmartPtrAlike&& other);
+ T* get() const;
+ T& operator*() const;
+};
+
+// Don't flag uses of smart-pointer-like types correctly annotated, unless it's
+// a dereference.
+void smartPointerLikeTypeUseAfterMove() {
+ SmartPtrAlike<int> ptr;
+ ptr.get();
+ SmartPtrAlike<int> other_ptr = std::move(ptr);
+ ptr.get();
+ int inv_value = *ptr;
+ // CHECK-NOTES: [[@LINE-1]]:20: warning: 'ptr' used after it was moved
+ // CHECK-NOTES: [[@LINE-4]]:34: note: move occurred here
+}
+
+class [[clang::annotate("other"),
+ clang::annotate("clang-tidy",
+ "bugprone-use-after-move",
+ "null_after_move")]] MultipleAnnotationsType {
+public:
+ MultipleAnnotationsType();
+ ~MultipleAnnotationsType();
+ MultipleAnnotationsType(const MultipleAnnotationsType&) = delete;
+ MultipleAnnotationsType &operator=(const MultipleAnnotationsType&) = delete;
+ MultipleAnnotationsType(MultipleAnnotationsType&& other);
+ MultipleAnnotationsType &operator=(MultipleAnnotationsType&& other);
+ int* get() const;
+ int& operator*() const;
+};
+
+// Handle smart-pointer-like types correctly annotated, even in the case of
+// multiple annotations.
+void typeWithMultipleAnnotations() {
+ MultipleAnnotationsType ptr;
+ ptr.get();
+ MultipleAnnotationsType other_ptr = std::move(ptr);
+ ptr.get();
+ int inv_value = *ptr;
+ // CHECK-NOTES: [[@LINE-1]]:20: warning: 'ptr' used after it was moved
+ // CHECK-NOTES: [[@LINE-4]]:39: note: move occurred here
+}
+
+class [[clang::annotate("null_after_move")]] BadAnnotation {
+public:
+ BadAnnotation();
+ ~BadAnnotation();
+ BadAnnotation(const BadAnnotation&) = delete;
+ BadAnnotation &operator=(const BadAnnotation&) = delete;
+ BadAnnotation(BadAnnotation&& other);
+ BadAnnotation &operator=(BadAnnotation&& other);
+ int* get() const;
+};
+
+// Flag uses of smart-pointer-like types with incorrect annotate.
+void badUseAfterMoveAnnotationIgnored() {
+ BadAnnotation ptr;
+ BadAnnotation other_ptr = std::move(ptr);
+ ptr.get();
+ // CHECK-NOTES: [[@LINE-1]]:3: warning: 'ptr' used after it was moved
+ // CHECK-NOTES: [[@LINE-3]]:29: note: move occurred here
+}
+
+class [[clang::annotate("clang-tidy", "null_after_move")]] BadAnnotationArgs {
+public:
+ BadAnnotationArgs();
+ ~BadAnnotationArgs();
+ BadAnnotationArgs(const BadAnnotationArgs&) = delete;
+ BadAnnotationArgs &operator=(const BadAnnotationArgs&) = delete;
+ BadAnnotationArgs(BadAnnotationArgs&& other);
+ BadAnnotationArgs &operator=(BadAnnotationArgs&& other);
+ int* get() const;
+};
+
+// Flag uses of smart-pointer-like types with wrong annotate arguments.
+void UseAfterMoveAnnotationWithBadArgsIgnored() {
+ BadAnnotationArgs ptr;
+ BadAnnotationArgs other_ptr = std::move(ptr);
+ ptr.get();
+ // CHECK-NOTES: [[@LINE-1]]:3: warning: 'ptr' used after it was moved
+ // CHECK-NOTES: [[@LINE-3]]:33: note: move occurred here
+}
+
+} // namespace null_after_move
+
////////////////////////////////////////////////////////////////////////////////
// Tests related to order of evaluation within expressions
More information about the cfe-commits
mailing list