https://github.com/aviralg updated https://github.com/llvm/llvm-project/pull/147410
>From be9fc41e5b1b52cf3f0bfb6acc4debae5561575a Mon Sep 17 00:00:00 2001 From: Aviral Goel <agoe...@apple.com> Date: Mon, 7 Jul 2025 14:38:00 -0700 Subject: [PATCH] [clang-tidy] bugprone-infinite-loop: Add support for tuple structured bindings Tuple structured bindings introduce implicit variable declarations under `BindingDecl` nodes which are currently ignored by the infinite loop checker. This PR adds support for these bindings to this checker through the following changes: 1. The pattern matcher in `ExprMutationAnalyzer` has been updated to match against `DeclRefExpr` nodes that refer to these implicit variables via `BindingDecl` nodes. 2. Enumeration of a loop's condition's variables for mutation analysis has been updated to recognize these implicit variables so they can be checked for mutation. 3. Enumeration of the names of a loop's condition's variables for error reporting has been similarly updated. The changes have been tested against a mock tuple implementation lifted from `clang/unittests/Analysis/FlowSensitive/TransferTest.cpp` --- .../clang-tidy/bugprone/InfiniteLoopCheck.cpp | 61 +++++++++++++++---- .../checkers/bugprone/infinite-loop.cpp | 47 ++++++++++++++ .../Analysis/Analyses/ExprMutationAnalyzer.h | 2 + clang/lib/Analysis/ExprMutationAnalyzer.cpp | 39 ++++++++---- 4 files changed, 126 insertions(+), 23 deletions(-) diff --git a/clang-tools-extra/clang-tidy/bugprone/InfiniteLoopCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/InfiniteLoopCheck.cpp index 3c3024d538785..a0011eab4251b 100644 --- a/clang-tools-extra/clang-tidy/bugprone/InfiniteLoopCheck.cpp +++ b/clang-tools-extra/clang-tidy/bugprone/InfiniteLoopCheck.cpp @@ -64,24 +64,53 @@ static bool isChanged(const Stmt *LoopStmt, const VarDecl *Var, return ExprMutationAnalyzer(*LoopStmt, *Context).isMutated(Var); } +bool isVolatileOrNonIntegerType(QualType QT) { + + if (QT.isVolatileQualified()) + return true; + + const Type *T = QT.getTypePtr(); + + if (T->isIntegerType()) + return false; + + if (T->isRValueReferenceType()) { + QT = dyn_cast<RValueReferenceType>(T)->getPointeeType(); + return isVolatileOrNonIntegerType(QT); + } + + return true; +} + +static bool isVarDeclPossiblyChanged(const Decl *Func, const Stmt *LoopStmt, + const VarDecl *Var, ASTContext *Context) { + if (!Var->isLocalVarDeclOrParm()) + return true; + + if (isVolatileOrNonIntegerType(Var->getType())) + return true; + + return hasPtrOrReferenceInFunc(Func, Var) || + isChanged(LoopStmt, Var, Context); +} + /// Return whether `Cond` is a variable that is possibly changed in `LoopStmt`. static bool isVarThatIsPossiblyChanged(const Decl *Func, const Stmt *LoopStmt, const Stmt *Cond, ASTContext *Context) { if (const auto *DRE = dyn_cast<DeclRefExpr>(Cond)) { - if (const auto *Var = dyn_cast<VarDecl>(DRE->getDecl())) { - if (!Var->isLocalVarDeclOrParm()) - return true; + const ValueDecl *VD = DRE->getDecl(); - if (Var->getType().isVolatileQualified()) - return true; - - if (!Var->getType().getTypePtr()->isIntegerType()) - return true; + if (const auto *Var = dyn_cast<VarDecl>(VD)) { + return isVarDeclPossiblyChanged(Func, LoopStmt, Var, Context); + } - return hasPtrOrReferenceInFunc(Func, Var) || - isChanged(LoopStmt, Var, Context); - // FIXME: Track references. + if (const auto *BD = dyn_cast<BindingDecl>(VD)) { + if (const auto *Var = BD->getHoldingVar()) { + return isVarDeclPossiblyChanged(Func, LoopStmt, Var, Context); + } } + + // FIXME: Track references. } else if (isa<MemberExpr, CallExpr, ObjCIvarRefExpr, ObjCPropertyRefExpr, ObjCMessageExpr>(Cond)) { // FIXME: Handle MemberExpr. @@ -121,8 +150,16 @@ static bool isAtLeastOneCondVarChanged(const Decl *Func, const Stmt *LoopStmt, /// Return the variable names in `Cond`. static std::string getCondVarNames(const Stmt *Cond) { if (const auto *DRE = dyn_cast<DeclRefExpr>(Cond)) { - if (const auto *Var = dyn_cast<VarDecl>(DRE->getDecl())) + const ValueDecl *VD = DRE->getDecl(); + + if (const auto *Var = dyn_cast<VarDecl>(VD)) return std::string(Var->getName()); + + if (const auto *BD = dyn_cast<BindingDecl>(VD)) { + if (const auto *Var = BD->getHoldingVar()) { + return Var->getName().str(); + } + } } std::string Result; diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/infinite-loop.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/infinite-loop.cpp index bc14ece3f332c..0895b91f77d9b 100644 --- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/infinite-loop.cpp +++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/infinite-loop.cpp @@ -592,6 +592,53 @@ void test_structured_bindings_bad() { } } +namespace std { + using size_t = int; + template <class> struct tuple_size; + template <std::size_t, class> struct tuple_element; + template <class...> class tuple; + +namespace { + template <class T, T v> + struct size_helper { static const T value = v; }; +} // namespace + +template <class... T> +struct tuple_size<tuple<T...>> : size_helper<std::size_t, sizeof...(T)> {}; + +template <std::size_t I, class... T> +struct tuple_element<I, tuple<T...>> { + using type = __type_pack_element<I, T...>; +}; + +template <class...> class tuple {}; + +template <std::size_t I, class... T> +typename tuple_element<I, tuple<T...>>::type get(tuple<T...>); +} // namespace std + +std::tuple<int*, int> &get_chunk(); + +void test_structured_bindings_tuple() { + auto [buffer, size ] = get_chunk(); + int maxLen = 8; + + while (size < maxLen) { + // No warning. The loop is finite because 'size' is being incremented in each iteration and compared against 'maxLen' for termination + buffer[size++] = 2; + } +} + +void test_structured_bindings_tuple_ref() { + auto& [buffer, size ] = get_chunk(); + int maxLen = 8; + + while (size < maxLen) { + // No warning. The loop is finite because 'size' is being incremented in each iteration and compared against 'maxLen' for termination + buffer[size++] = 2; + } +} + void test_volatile_cast() { // This is a no-op cast. Clang ignores the qualifier, we should too. for (int i = 0; (volatile int)i < 10;) { diff --git a/clang/include/clang/Analysis/Analyses/ExprMutationAnalyzer.h b/clang/include/clang/Analysis/Analyses/ExprMutationAnalyzer.h index 3344959072c22..441bc56b29de8 100644 --- a/clang/include/clang/Analysis/Analyses/ExprMutationAnalyzer.h +++ b/clang/include/clang/Analysis/Analyses/ExprMutationAnalyzer.h @@ -54,6 +54,8 @@ class ExprMutationAnalyzer { const Stmt *findMutationMemoized(const Expr *Exp, llvm::ArrayRef<MutationFinder> Finders, Memoized::ResultMap &MemoizedResults); + const ast_matchers::internal::BindableMatcher<Stmt> + makeDeclRefExprMatcher(const Decl *Dec); const Stmt *tryEachDeclRef(const Decl *Dec, MutationFinder Finder); const Stmt *findExprMutation(ArrayRef<ast_matchers::BoundNodes> Matches); diff --git a/clang/lib/Analysis/ExprMutationAnalyzer.cpp b/clang/lib/Analysis/ExprMutationAnalyzer.cpp index 823d7543f085f..fb36d9a4cea64 100644 --- a/clang/lib/Analysis/ExprMutationAnalyzer.cpp +++ b/clang/lib/Analysis/ExprMutationAnalyzer.cpp @@ -113,6 +113,14 @@ AST_MATCHER_P(Stmt, canResolveToExpr, const Stmt *, Inner) { return canExprResolveTo(Exp, Target); } +AST_MATCHER_P(BindingDecl, hasHoldingVar, + ast_matchers::internal::Matcher<VarDecl>, InnerMatcher) { + if (const VarDecl *HoldingVar = Node.getHoldingVar()) { + return InnerMatcher.matches(*HoldingVar, Finder, Builder); + } + return false; +} + // use class member to store data can reduce stack usage to avoid stack overflow // when recursive call. class ExprPointeeResolve { @@ -310,21 +318,30 @@ const Stmt *ExprMutationAnalyzer::Analyzer::findMutationMemoized( return nullptr; } +const ast_matchers::internal::BindableMatcher<Stmt> +ExprMutationAnalyzer::Analyzer::makeDeclRefExprMatcher(const Decl *Dec) { + + // For VarDecl created implicitly via structured bindings, create a matcher + // for DeclRefExpr nodes which refer to this VarDecl via BindingDecl nodes. + if (const auto *VD = dyn_cast<VarDecl>(Dec)) { + if (VD->isImplicit()) { + return declRefExpr(to(bindingDecl(hasHoldingVar(equalsNode(VD))))); + } + } + + return declRefExpr(to( + // `Dec` or a binding if `Dec` is a decomposition. + anyOf(equalsNode(Dec), bindingDecl(forDecomposition(equalsNode(Dec)))))); +} + const Stmt * ExprMutationAnalyzer::Analyzer::tryEachDeclRef(const Decl *Dec, MutationFinder Finder) { - const auto Refs = match( - findAll( - declRefExpr(to( - // `Dec` or a binding if `Dec` is a decomposition. - anyOf(equalsNode(Dec), - bindingDecl(forDecomposition(equalsNode(Dec)))) - // - )) - .bind(NodeID<Expr>::value)), - Stm, Context); + const auto matcher = makeDeclRefExprMatcher(Dec); + const auto nodeId = NodeID<Expr>::value; + const auto Refs = match(findAll(matcher.bind(nodeId)), Stm, Context); for (const auto &RefNodes : Refs) { - const auto *E = RefNodes.getNodeAs<Expr>(NodeID<Expr>::value); + const auto *E = RefNodes.getNodeAs<Expr>(nodeId); if ((this->*Finder)(E)) return E; } _______________________________________________ cfe-commits mailing list cfe-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits