[clang-tools-extra] [clang-tidy] Add `ReportAccessOnlyUseForTypes` option to `bugprone-use-after-move` (PR #225804)
via cfe-commits
cfe-commits at lists.llvm.org
Mon Sep 28 04:58:58 PDT 2026
https://github.com/Harald-R updated https://github.com/llvm/llvm-project/pull/225804
>From e44365b2fc0b511037f27e0153356b7c6086427c Mon Sep 17 00:00:00 2001
From: Harald-R <rotuna.razvan at gmail.com>
Date: Tue, 4 Aug 2026 18:04:19 +0300
Subject: [PATCH] Add support for ReportAccessOnlyUseForTypes
---
.../clang-tidy/bugprone/UseAfterMoveCheck.cpp | 93 +++++++-
.../clang-tidy/bugprone/UseAfterMoveCheck.h | 1 +
clang-tools-extra/docs/ReleaseNotes.md | 7 +
.../checks/bugprone/use-after-move.md | 11 +
.../checkers/bugprone/use-after-move.cpp | 199 +++++++++++++++++-
5 files changed, 298 insertions(+), 13 deletions(-)
diff --git a/clang-tools-extra/clang-tidy/bugprone/UseAfterMoveCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/UseAfterMoveCheck.cpp
index b13367e25dcb2c..6a39591a07a9da 100644
--- a/clang-tools-extra/clang-tidy/bugprone/UseAfterMoveCheck.cpp
+++ b/clang-tools-extra/clang-tidy/bugprone/UseAfterMoveCheck.cpp
@@ -67,6 +67,7 @@ class UseAfterMoveFinder {
UseAfterMoveFinder(ASTContext *TheContext,
llvm::ArrayRef<StringRef> InvalidationFunctions,
llvm::ArrayRef<StringRef> ReinitializationFunctions,
+ llvm::ArrayRef<StringRef> ReportAccessOnlyUseForTypes,
const CXXRecordDecl *MovedAs);
// Within the given code block, finds the first use of 'MovedVariable' that
@@ -92,6 +93,7 @@ class UseAfterMoveFinder {
ASTContext *Context;
llvm::ArrayRef<StringRef> InvalidationFunctions;
llvm::ArrayRef<StringRef> ReinitializationFunctions;
+ llvm::ArrayRef<StringRef> ReportAccessOnlyUseForTypes;
const CXXRecordDecl *MovedAs;
std::unique_ptr<ExprSequence> Sequence;
std::unique_ptr<StmtToBlockMap> BlockMap;
@@ -205,9 +207,12 @@ static StatementMatcher inDecltypeOrTemplateArg() {
UseAfterMoveFinder::UseAfterMoveFinder(
ASTContext *TheContext, llvm::ArrayRef<StringRef> InvalidationFunctions,
llvm::ArrayRef<StringRef> ReinitializationFunctions,
+ llvm::ArrayRef<StringRef> ReportAccessOnlyUseForTypes,
const CXXRecordDecl *MovedAs)
: Context(TheContext), InvalidationFunctions(InvalidationFunctions),
- ReinitializationFunctions(ReinitializationFunctions), MovedAs(MovedAs) {}
+ ReinitializationFunctions(ReinitializationFunctions),
+ ReportAccessOnlyUseForTypes(ReportAccessOnlyUseForTypes),
+ MovedAs(MovedAs) {}
std::optional<UseAfterMove>
UseAfterMoveFinder::find(Stmt *CodeBlock, const Expr *MovingCall,
@@ -404,32 +409,74 @@ static bool isSpecifiedAfterMove(const ValueDecl *VD) {
return RecordDecl->getDeclContext()->isStdNamespace();
}
+// Returns true if the check must report a use of `VD` only when the code
+// accesses the object. An access is a member access or a dereference. The
+// check does not report other references to the variable. The check does this
+// when the type of `VD` is one of these:
+// - a pointer to a type in the ReportAccessOnlyUseForTypes option;
+// - a class that is the same as or derived from such a type.
+static bool
+reportAccessOnlyUse(const ValueDecl *VD, ASTContext &Context,
+ llvm::ArrayRef<StringRef> ReportAccessOnlyUseForTypes) {
+ if (ReportAccessOnlyUseForTypes.empty())
+ return false;
+
+ const auto RecordIsListed = cxxRecordDecl(isSameOrDerivedFrom(
+ matchers::matchesAnyListedRegexName(ReportAccessOnlyUseForTypes)));
+ const auto TypeMatcher =
+ qualType(anyOf(pointsTo(RecordIsListed), hasDeclaration(RecordIsListed)));
+
+ const QualType QT = VD->getType().getNonReferenceType().getCanonicalType();
+ return !match(TypeMatcher, QT, Context).empty();
+}
+
void UseAfterMoveFinder::getDeclRefs(
const CFGBlock *Block, const Decl *MovedVariable,
llvm::SmallPtrSetImpl<const DeclRefExpr *> *DeclRefs) {
DeclRefs->clear();
+
+ // For the types in the ReportAccessOnlyUseForTypes option, only an access
+ // counts as a use. An access is a member access or a dereference of the
+ // object. A different reference to the variable does not count as a use.
+ // Examples of a different reference are an argument, a comparison, or a copy.
+ const auto *MovedValueDecl = dyn_cast<ValueDecl>(MovedVariable);
+ const bool ReportAccessOnly =
+ MovedValueDecl && reportAccessOnlyUse(MovedValueDecl, *Context,
+ ReportAccessOnlyUseForTypes);
+
for (const auto &Elem : *Block) {
std::optional<CFGStmt> S = Elem.getAs<CFGStmt>();
if (!S)
continue;
- const auto AddDeclRefs = [this, Block,
- DeclRefs](const ArrayRef<BoundNodes> Matches) {
+ const auto AddDeclRefs = [this, Block, DeclRefs, ReportAccessOnly](
+ const ArrayRef<BoundNodes> Matches) {
for (const auto &Match : Matches) {
const auto *DeclRef = Match.getNodeAs<DeclRefExpr>("declref");
const auto *Member = Match.getNodeAs<MemberExpr>("member-expr");
const auto *Operator = Match.getNodeAs<CXXOperatorCallExpr>("operator");
+ const auto *Access = Match.getNodeAs<Expr>("access");
// Non-moved member as the move only implies a base class.
if (Member && MovedAs && !isa<CXXMethodDecl>(Member->getMemberDecl()) &&
!MovedAs->hasMemberName(Member->getMemberDecl()->getIdentifier())) {
continue;
}
- if (DeclRef && BlockMap->blockContainingStmt(DeclRef) == Block &&
- (Operator || !isSpecifiedAfterMove(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.
- DeclRefs->insert(DeclRef);
+ if (DeclRef && BlockMap->blockContainingStmt(DeclRef) == Block) {
+ if (ReportAccessOnly) {
+ // Report the use only if the code accesses the object. An access is
+ // one of these:
+ // - a member access (`op->m`, `op.m`);
+ // - an overloaded dereference operator;
+ // - a built-in dereference (`*op`) or subscript (`op[i]`).
+ if (Member || Operator || Access)
+ DeclRefs->insert(DeclRef);
+ } else if (Operator || !isSpecifiedAfterMove(DeclRef->getDecl())) {
+ // Ignore uses of a standard smart pointer, or of a class with the
+ // "null_after_move" annotation, that do not dereference the
+ // pointer.
+ DeclRefs->insert(DeclRef);
+ }
+ }
}
};
@@ -449,6 +496,24 @@ void UseAfterMoveFinder::getDeclRefs(
hasArgument(0, DeclRefMatcher))
.bind("operator")),
*S->getStmt(), *Context));
+ // A built-in dereference (`*op`) or a subscript (`op[i]`) of the variable
+ // is also an access. The types in the ReportAccessOnlyUseForTypes option
+ // need these matchers. For example, raw pointers need them because these
+ // operators are not overloaded calls. Thus, the "member-expr" and
+ // "operator" matchers do not match these operators.
+ AddDeclRefs(match(
+ traverse(TK_AsIs,
+ findAll(unaryOperator(hasOperatorName("*"),
+ hasUnaryOperand(ignoringParenImpCasts(
+ DeclRefMatcher)))
+ .bind("access"))),
+ *S->getStmt(), *Context));
+ AddDeclRefs(match(
+ traverse(TK_AsIs,
+ findAll(arraySubscriptExpr(
+ hasBase(ignoringParenImpCasts(DeclRefMatcher)))
+ .bind("access"))),
+ *S->getStmt(), *Context));
}
}
@@ -541,13 +606,18 @@ UseAfterMoveCheck::UseAfterMoveCheck(StringRef Name, ClangTidyContext *Context)
InvalidationFunctions(utils::options::parseStringList(
Options.get("InvalidationFunctions", ""))),
ReinitializationFunctions(utils::options::parseStringList(
- Options.get("ReinitializationFunctions", ""))) {}
+ Options.get("ReinitializationFunctions", ""))),
+ ReportAccessOnlyUseForTypes(utils::options::parseStringList(
+ Options.get("ReportAccessOnlyUseForTypes", ""))) {}
void UseAfterMoveCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
Options.store(Opts, "InvalidationFunctions",
utils::options::serializeStringList(InvalidationFunctions));
Options.store(Opts, "ReinitializationFunctions",
utils::options::serializeStringList(ReinitializationFunctions));
+ Options.store(
+ Opts, "ReportAccessOnlyUseForTypes",
+ utils::options::serializeStringList(ReportAccessOnlyUseForTypes));
}
void UseAfterMoveCheck::registerMatchers(MatchFinder *Finder) {
@@ -656,7 +726,8 @@ void UseAfterMoveCheck::check(const MatchFinder::MatchResult &Result) {
for (Stmt *CodeBlock : CodeBlocks) {
UseAfterMoveFinder Finder(Result.Context, InvalidationFunctions,
- ReinitializationFunctions, MovedAs);
+ ReinitializationFunctions,
+ ReportAccessOnlyUseForTypes, MovedAs);
if (auto Use = Finder.find(CodeBlock, MovingCall, Arg))
emitDiagnostic(MovingCall, Arg, *Use, this, Result.Context,
determineMoveType(MoveDecl), MoveDecl);
diff --git a/clang-tools-extra/clang-tidy/bugprone/UseAfterMoveCheck.h b/clang-tools-extra/clang-tidy/bugprone/UseAfterMoveCheck.h
index fff1c2621867db..ae7bf34511a9c8 100644
--- a/clang-tools-extra/clang-tidy/bugprone/UseAfterMoveCheck.h
+++ b/clang-tools-extra/clang-tidy/bugprone/UseAfterMoveCheck.h
@@ -31,6 +31,7 @@ class UseAfterMoveCheck : public ClangTidyCheck {
private:
std::vector<StringRef> InvalidationFunctions;
std::vector<StringRef> ReinitializationFunctions;
+ std::vector<StringRef> ReportAccessOnlyUseForTypes;
};
} // namespace clang::tidy::bugprone
diff --git a/clang-tools-extra/docs/ReleaseNotes.md b/clang-tools-extra/docs/ReleaseNotes.md
index 3d9e34cc4d44e7..bb0212956ce19e 100644
--- a/clang-tools-extra/docs/ReleaseNotes.md
+++ b/clang-tools-extra/docs/ReleaseNotes.md
@@ -194,6 +194,13 @@ infrastructure are described first, followed by tool-specific sections.
<clang-tidy/checks/bugprone/std-namespace-modification>` when checking
lambda closure types used as template arguments.
+- Improved {doc}`bugprone-use-after-move
+ <clang-tidy/checks/bugprone/use-after-move>` check by adding the
+ {option}`ReportAccessOnlyUseForTypes` option, which restricts diagnostics to
+ uses that access the object (a member access, a dereference, or a subscript)
+ for the configured pointer-like types, ignoring other references such as
+ passing, comparing, or copying the moved-from variable.
+
- Improved {doc}`cppcoreguidelines-missing-std-forward
<clang-tidy/checks/cppcoreguidelines/missing-std-forward>` check by diagnosing
unforwarded `auto&&` parameters in C++20 abbreviated function templates.
diff --git a/clang-tools-extra/docs/clang-tidy/checks/bugprone/use-after-move.md b/clang-tools-extra/docs/clang-tidy/checks/bugprone/use-after-move.md
index e766283c77ca84..f4d10f84eff49d 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/bugprone/use-after-move.md
+++ b/clang-tools-extra/docs/clang-tidy/checks/bugprone/use-after-move.md
@@ -269,3 +269,14 @@ argument (`*this`) is considered to be reinitialized. For non-member or
static member functions, the first argument is considered to be
reinitialized. Default value is an empty string.
```
+
+```{option} ReportAccessOnlyUseForTypes
+A semicolon-separated list of regular expressions matching names of types for
+which only an access of the object counts as a use. For a variable whose type
+is a pointer to a listed type, or a class that is the same as or derived from a
+listed type, the check reports a use only when the code accesses the object,
+that is, a member access (`p->m`, `p.m`), an overloaded dereference, a built-in
+dereference (`*p`), or a subscript (`p[i]`). Other references to the variable
+(such as passing it as an argument, comparing it, or copying it) are not
+reported. Default value is an empty string.
+```
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 1dd67941bd84cf..9611062fb1bbe1 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
@@ -1,13 +1,15 @@
// RUN: %check_clang_tidy -std=c++11,c++14 -check-suffixes=,CXX11 %s bugprone-use-after-move %t -- \
// RUN: -config='{CheckOptions: { \
// RUN: bugprone-use-after-move.InvalidationFunctions: "::Database<>::StaticCloseConnection;Database<>::CloseConnection;FriendCloseConnection;FreeCloseConnection", \
-// RUN: bugprone-use-after-move.ReinitializationFunctions: "::Database<>::Reset;::Database<>::StaticReset;::FriendReset;::RegularReset" \
+// RUN: bugprone-use-after-move.ReinitializationFunctions: "::Database<>::Reset;::Database<>::StaticReset;::FriendReset;::RegularReset", \
+// RUN: bugprone-use-after-move.ReportAccessOnlyUseForTypes: "::report_access_only::AccessOnly;::report_access_only::HandleBase;::report_access_only::SmartHandle" \
// RUN: }}' -- \
// RUN: -fno-delayed-template-parsing
// RUN: %check_clang_tidy -std=c++17-or-later %s bugprone-use-after-move %t -- \
// RUN: -config='{CheckOptions: { \
// RUN: bugprone-use-after-move.InvalidationFunctions: "::Database<>::StaticCloseConnection;Database<>::CloseConnection;FriendCloseConnection;FreeCloseConnection", \
-// RUN: bugprone-use-after-move.ReinitializationFunctions: "::Database<>::Reset;::Database<>::StaticReset;::FriendReset;::RegularReset" \
+// RUN: bugprone-use-after-move.ReinitializationFunctions: "::Database<>::Reset;::Database<>::StaticReset;::FriendReset;::RegularReset", \
+// RUN: bugprone-use-after-move.ReportAccessOnlyUseForTypes: "::report_access_only::AccessOnly;::report_access_only::HandleBase;::report_access_only::SmartHandle" \
// RUN: }}' -- \
// RUN: -fno-delayed-template-parsing
@@ -1998,3 +2000,196 @@ void callPartialForwardTemplate(Derived &&d) {
partialForwardTemplate<Derived>(std::forward<Derived>(d));
}
} // namespace GH63202
+
+////////////////////////////////////////////////////////////////////////////////
+// Tests for the ReportAccessOnlyUseForTypes option
+//
+// For the types in this option, only an access counts as a use. An access is a
+// member access or a dereference of the object. A different reference to the
+// variable does not count as a use. Examples of a different reference are an
+// argument, a comparison, or a copy. This behavior applies to a pointer to a
+// listed type. It also applies to a class that is the same as or derived from
+// a listed type.
+
+namespace report_access_only {
+
+struct AccessOnly {
+ void foo() const;
+ int bar;
+};
+
+void takePointer(AccessOnly *);
+
+void pointerMethodAccessIsUse() {
+ AccessOnly *p = nullptr;
+ std::move(p);
+ p->foo();
+ // CHECK-NOTES: [[@LINE-1]]:3: warning: 'p' used after it was moved
+ // CHECK-NOTES: [[@LINE-3]]:3: note: move occurred here
+}
+
+void pointerMemberDataAccessIsUse() {
+ AccessOnly *p = nullptr;
+ std::move(p);
+ int i = p->bar;
+ (void)i;
+ // CHECK-NOTES: [[@LINE-2]]:11: warning: 'p' used after it was moved
+ // CHECK-NOTES: [[@LINE-4]]:3: note: move occurred here
+}
+
+void pointerDerefIsUse() {
+ AccessOnly *p = nullptr;
+ std::move(p);
+ *p;
+ // CHECK-NOTES: [[@LINE-1]]:4: warning: 'p' used after it was moved
+ // CHECK-NOTES: [[@LINE-3]]:3: note: move occurred here
+}
+
+void pointerDerefThenMemberIsUse() {
+ AccessOnly *p = nullptr;
+ std::move(p);
+ (*p).foo();
+ // CHECK-NOTES: [[@LINE-1]]:5: warning: 'p' used after it was moved
+ // CHECK-NOTES: [[@LINE-3]]:3: note: move occurred here
+}
+
+void pointerSubscriptIsUse() {
+ AccessOnly *p = nullptr;
+ std::move(p);
+ p[0];
+ // CHECK-NOTES: [[@LINE-1]]:3: warning: 'p' used after it was moved
+ // CHECK-NOTES: [[@LINE-3]]:3: note: move occurred here
+}
+
+void pointerPassIsNotUse() {
+ AccessOnly *p = nullptr;
+ std::move(p);
+ takePointer(p);
+}
+
+void pointerCompareIsNotUse() {
+ AccessOnly *p = nullptr;
+ std::move(p);
+ if (p == nullptr) {
+ }
+}
+
+void pointerCopyIsNotUse() {
+ AccessOnly *p = nullptr;
+ std::move(p);
+ AccessOnly *p2 = p;
+ (void)p2;
+}
+
+struct DerivedAccess : AccessOnly {};
+
+void derivedPointerAccessIsUse() {
+ DerivedAccess *p = nullptr;
+ std::move(p);
+ p->foo();
+ // CHECK-NOTES: [[@LINE-1]]:3: warning: 'p' used after it was moved
+ // CHECK-NOTES: [[@LINE-3]]:3: note: move occurred here
+}
+
+void derivedPointerPassIsNotUse() {
+ DerivedAccess *p = nullptr;
+ std::move(p);
+ takePointer(p);
+}
+
+struct HandleBase {};
+struct Handle : HandleBase {
+ void foo() const;
+};
+
+void takeHandle(Handle);
+
+void handleMemberAccessIsUse() {
+ Handle h;
+ std::move(h);
+ h.foo();
+ // CHECK-NOTES: [[@LINE-1]]:3: warning: 'h' used after it was moved
+ // CHECK-NOTES: [[@LINE-3]]:3: note: move occurred here
+}
+
+void handlePassIsNotUse() {
+ Handle h;
+ std::move(h);
+ takeHandle(h);
+}
+
+void handleCopyIsNotUse() {
+ Handle h;
+ std::move(h);
+ Handle h2 = h;
+ (void)h2;
+}
+
+struct NotListedInConfig {
+ void foo() const;
+};
+
+void nonListedPointerCompareIsUse() {
+ NotListedInConfig *p = nullptr;
+ std::move(p);
+ if (p == nullptr) {
+ }
+ // CHECK-NOTES: [[@LINE-2]]:7: warning: 'p' used after it was moved
+ // CHECK-NOTES: [[@LINE-4]]:3: note: move occurred here
+}
+
+// A smart-pointer-like class with overloaded dereference operators. An access
+// to the pointee through an overloaded operator counts as a use. Other
+// references to the variable do not count as a use.
+struct Pointee {
+ void foo() const;
+};
+
+struct SmartHandle {
+ Pointee *operator->();
+ Pointee &operator*();
+ Pointee &operator[](int);
+};
+
+void takeSmartHandle(SmartHandle);
+bool operator==(const SmartHandle &, const SmartHandle &);
+
+void smartHandleArrowIsUse() {
+ SmartHandle s;
+ std::move(s);
+ s->foo();
+ // CHECK-NOTES: [[@LINE-1]]:3: warning: 's' used after it was moved
+ // CHECK-NOTES: [[@LINE-3]]:3: note: move occurred here
+}
+
+void smartHandleDerefIsUse() {
+ SmartHandle s;
+ std::move(s);
+ (*s).foo();
+ // CHECK-NOTES: [[@LINE-1]]:5: warning: 's' used after it was moved
+ // CHECK-NOTES: [[@LINE-3]]:3: note: move occurred here
+}
+
+void smartHandleSubscriptIsUse() {
+ SmartHandle s;
+ std::move(s);
+ s[0];
+ // CHECK-NOTES: [[@LINE-1]]:3: warning: 's' used after it was moved
+ // CHECK-NOTES: [[@LINE-3]]:3: note: move occurred here
+}
+
+void smartHandlePassIsNotUse() {
+ SmartHandle s;
+ std::move(s);
+ takeSmartHandle(s);
+}
+
+void smartHandleCompareIsNotUse() {
+ SmartHandle s1;
+ SmartHandle s2;
+ std::move(s1);
+ if (s1 == s2) {
+ }
+}
+
+} // namespace report_access_only
More information about the cfe-commits
mailing list