//===----------------------------------------------------------------------===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// #include "UseStructuredBindingCheck.h" #include "../utils/DeclRefExprUtils.h" #include "clang/Lex/Lexer.h" using namespace clang::ast_matchers; namespace clang::tidy::modernize { static constexpr StringRef PairDeclName = "PairVarD"; static constexpr StringRef PairVarTypeName = "PairVarType"; static constexpr StringRef FirstVarDeclName = "FirstVarDecl"; static constexpr StringRef SecondVarDeclName = "SecondVarDecl"; static constexpr StringRef BeginDeclStmtName = "BeginDeclStmt"; static constexpr StringRef EndDeclStmtName = "EndDeclStmt"; static constexpr StringRef FirstTypeName = "FirstType"; static constexpr StringRef SecondTypeName = "SecondType"; static constexpr StringRef ScopeBlockName = "ScopeBlock"; static constexpr StringRef StdTieAssignStmtName = "StdTieAssign"; static constexpr StringRef StdTieExprName = "StdTieExpr"; static constexpr StringRef ForRangeStmtName = "ForRangeStmt"; static constexpr StringRef InitExprName = "init_expr"; /// Matches a sequence of VarDecls matching the inner matchers, starting from /// the \p Iter to \p EndIter and set bindings for the first DeclStmt and the /// last DeclStmt if matched. /// /// \p Backwards indicates whether to match the VarDecls in reverse order. template static bool matchNVarDeclStartingWith( Iterator Iter, const Iterator &EndIter, ArrayRef> InnerMatchers, ast_matchers::internal::ASTMatchFinder *Finder, ast_matchers::internal::BoundNodesTreeBuilder *Builder, bool Backwards = false) { const DeclStmt *BeginDS = nullptr; const DeclStmt *EndDS = nullptr; const size_t N = InnerMatchers.size(); size_t Count = 0; auto Matches = [&](const Decl *VD) { // We don't want redundant decls in DeclStmt. if (Count == N) return false; if (const auto *Var = dyn_cast(VD); Var && InnerMatchers[Backwards ? N - Count - 1 : Count].matches( *Var, Finder, Builder)) { ++Count; return true; } return false; }; for (; Iter != EndIter; ++Iter) { EndDS = dyn_cast(*Iter); if (!EndDS) break; if (!BeginDS) BeginDS = EndDS; for (const auto *VD : llvm::reverse_conditionally(EndDS->decls(), Backwards)) { if (!Matches(VD)) return false; } // All the matchers is satisfied in those DeclStmts. if (Count == N) { if (Backwards) std::swap(BeginDS, EndDS); Builder->setBinding(BeginDeclStmtName, DynTypedNode::create(*BeginDS)); Builder->setBinding(EndDeclStmtName, DynTypedNode::create(*EndDS)); return true; } } return false; } namespace { /// What qualifiers and specifiers are used to create structured binding /// declaration, it only supports the following four cases now. enum TransferType : uint8_t { TT_ByVal, TT_ByConstVal, TT_ByRef, TT_ByConstRef }; /// Matches a Stmt whose parent is a CompoundStmt, and which is directly /// following two VarDecls matching the inner matcher. AST_MATCHER_P(Stmt, hasPreTwoVarDecl, SmallVector>, InnerMatchers) { const DynTypedNodeList Parents = Finder->getASTContext().getParents(Node); if (Parents.size() != 1) return false; const auto *C = Parents[0].get(); if (!C) return false; const auto It = llvm::find(llvm::reverse(C->body()), &Node); assert(It != C->body_rend() && "C is parent of Node"); return matchNVarDeclStartingWith(It + 1, C->body_rend(), InnerMatchers, Finder, Builder, true); } /// Matches a Stmt whose parent is a CompoundStmt, and which is directly /// followed by two VarDecls matching the inner matcher. AST_MATCHER_P(Stmt, hasNextTwoVarDecl, SmallVector>, InnerMatchers) { const DynTypedNodeList Parents = Finder->getASTContext().getParents(Node); if (Parents.size() != 1) return false; const auto *C = Parents[0].get(); if (!C) return false; const auto *It = llvm::find(C->body(), &Node); assert(It != C->body_end() && "C is parent of Node"); return matchNVarDeclStartingWith(It + 1, C->body_end(), InnerMatchers, Finder, Builder); } /// Matches a CompoundStmt which has two VarDecls matching the inner matcher in /// the beginning. AST_MATCHER_P(CompoundStmt, hasFirstTwoVarDecl, SmallVector>, InnerMatchers) { return matchNVarDeclStartingWith(Node.body_begin(), Node.body_end(), InnerMatchers, Finder, Builder); } /// It's not very common to have specifiers for variables used to decompose a /// pair, so we ignore these cases. AST_MATCHER(VarDecl, hasAnySpecifiersShouldBeIgnored) { return Node.isStaticLocal() || Node.isConstexpr() || Node.hasAttrs() || Node.isInlineSpecified() || Node.getStorageClass() != SC_None || Node.getTSCSpec() != TSCS_unspecified; } // Ignore nodes inside macros. AST_POLYMORPHIC_MATCHER(isInMacro, AST_POLYMORPHIC_SUPPORTED_TYPES(Stmt, Decl)) { return Node.getBeginLoc().isMacroID() || Node.getEndLoc().isMacroID(); } AST_MATCHER_P(Expr, ignoringCopyCtorAndImplicitCast, ast_matchers::internal::Matcher, InnerMatcher) { if (const auto *CtorE = dyn_cast(&Node)) { if (const CXXConstructorDecl *CtorD = CtorE->getConstructor(); CtorD->isCopyConstructor() && CtorE->getNumArgs() == 1) { return InnerMatcher.matches(*CtorE->getArg(0)->IgnoreImpCasts(), Finder, Builder); } } return InnerMatcher.matches(*Node.IgnoreImpCasts(), Finder, Builder); } AST_MATCHER(CXXRecordDecl, isPairType) { return llvm::all_of(Node.fields(), [](const FieldDecl *FD) { return FD->getAccess() == AS_public && (FD->getName() == "first" || FD->getName() == "second"); }); } AST_MATCHER(VarDecl, isDirectInitialization) { return Node.getInitStyle() != VarDecl::InitializationStyle::CInit; } } // namespace static auto getVarInitWithMemberMatcher( StringRef PairName, StringRef MemberName, StringRef TypeName, StringRef BindingName, const ast_matchers::internal::Matcher &ExtraMatcher) { return varDecl(ExtraMatcher, hasInitializer(ignoringCopyCtorAndImplicitCast(memberExpr( hasObjectExpression(ignoringImpCasts(declRefExpr( to(equalsBoundNode(std::string(PairName)))))), member(fieldDecl(hasName(MemberName), hasType(qualType().bind(TypeName)))))))) .bind(BindingName); } static auto typeOrLValueReferenceTo( const ast_matchers::internal::Matcher &TypeMatcher) { return qualType( anyOf(TypeMatcher, lValueReferenceType(pointee(TypeMatcher)))); } void UseStructuredBindingCheck::registerMatchers(MatchFinder *Finder) { auto PairType = qualType(unless(isVolatileQualified()), hasUnqualifiedDesugaredType(recordType( hasDeclaration(cxxRecordDecl(isPairType()))))); auto UnlessShouldBeIgnored = unless(anyOf(hasAnySpecifiersShouldBeIgnored(), isInMacro())); auto VarInitWithFirstMember = getVarInitWithMemberMatcher(PairDeclName, "first", FirstTypeName, FirstVarDeclName, UnlessShouldBeIgnored); auto VarInitWithSecondMember = getVarInitWithMemberMatcher(PairDeclName, "second", SecondTypeName, SecondVarDeclName, UnlessShouldBeIgnored); auto RefToBindName = [&UnlessShouldBeIgnored](const StringRef &Name) { return declRefExpr(to(varDecl(UnlessShouldBeIgnored).bind(Name))); }; auto HasAnyLambdaCaptureThisVar = [](const ast_matchers::internal::Matcher &VDMatcher) { return compoundStmt(hasDescendant( lambdaExpr(hasAnyCapture(capturesVar(varDecl(VDMatcher)))))); }; // Captured structured bindings are a C++20 extension auto UnlessFirstVarOrSecondVarIsCapturedByLambda = getLangOpts().CPlusPlus20 ? compoundStmt() : compoundStmt(unless(HasAnyLambdaCaptureThisVar( anyOf(equalsBoundNode(std::string(FirstVarDeclName)), equalsBoundNode(std::string(SecondVarDeclName)))))); // X x; // Y y; // std::tie(x, y) = ...; Finder->addMatcher( exprWithCleanups( unless(isInMacro()), has(cxxOperatorCallExpr( hasOverloadedOperatorName("="), hasLHS(ignoringImplicit( callExpr(callee(functionDecl(isInStdNamespace(), hasName("tie"))), hasArgument(0, RefToBindName(FirstVarDeclName)), hasArgument(1, RefToBindName(SecondVarDeclName))) .bind(StdTieExprName))), hasRHS(expr(hasType(PairType)))) .bind(StdTieAssignStmtName)), hasPreTwoVarDecl( SmallVector>{ varDecl(equalsBoundNode(std::string(FirstVarDeclName))), varDecl(equalsBoundNode(std::string(SecondVarDeclName)))}), hasParent(compoundStmt(UnlessFirstVarOrSecondVarIsCapturedByLambda) .bind(ScopeBlockName))), this); // pair p = ...; // X x = p.first; // Y y = p.second; Finder->addMatcher( declStmt( unless(isInMacro()), hasSingleDecl(varDecl(UnlessShouldBeIgnored, unless(isDirectInitialization()), hasType(typeOrLValueReferenceTo(PairType).bind( PairVarTypeName)), hasInitializer(ignoringCopyCtorAndImplicitCast( expr().bind(InitExprName)))) .bind(PairDeclName)), hasNextTwoVarDecl( SmallVector>{ VarInitWithFirstMember, VarInitWithSecondMember}), hasParent(compoundStmt(UnlessFirstVarOrSecondVarIsCapturedByLambda) .bind(ScopeBlockName))), this); // for (pair p : map) { // X x = p.first; // Y y = p.second; // } Finder->addMatcher( cxxForRangeStmt( unless(isInMacro()), hasLoopVariable( varDecl(hasType(typeOrLValueReferenceTo(PairType).bind( PairVarTypeName)), hasInitializer(ignoringCopyCtorAndImplicitCast( expr().bind(InitExprName)))) .bind(PairDeclName)), hasBody(compoundStmt( hasFirstTwoVarDecl( SmallVector>{ VarInitWithFirstMember, VarInitWithSecondMember}), UnlessFirstVarOrSecondVarIsCapturedByLambda) .bind(ScopeBlockName))) .bind(ForRangeStmtName), this); } static std::optional getTransferType(const ASTContext &Ctx, QualType ResultType, QualType OriginType) { ResultType = ResultType.getCanonicalType(); OriginType = OriginType.getCanonicalType(); if (ResultType == Ctx.getLValueReferenceType(OriginType.withConst())) return TT_ByConstRef; if (ResultType == Ctx.getLValueReferenceType(OriginType)) return TT_ByRef; if (ResultType == OriginType.withConst()) return TT_ByConstVal; if (ResultType == OriginType) return TT_ByVal; return std::nullopt; } void UseStructuredBindingCheck::check(const MatchFinder::MatchResult &Result) { const auto *FirstVar = Result.Nodes.getNodeAs(FirstVarDeclName); const auto *SecondVar = Result.Nodes.getNodeAs(SecondVarDeclName); const auto *BeginDS = Result.Nodes.getNodeAs(BeginDeclStmtName); const auto *EndDS = Result.Nodes.getNodeAs(EndDeclStmtName); const auto *ScopeBlock = Result.Nodes.getNodeAs(ScopeBlockName); const auto *CFRS = Result.Nodes.getNodeAs(ForRangeStmtName); auto DiagAndFix = [&BeginDS, &EndDS, &FirstVar, &SecondVar, &CFRS, this](SourceLocation DiagLoc, SourceRange ReplaceRange, TransferType TT = TT_ByVal) { const auto Prefix = [&TT]() -> StringRef { switch (TT) { case TT_ByVal: return "auto"; case TT_ByConstVal: return "const auto"; case TT_ByRef: return "auto&"; case TT_ByConstRef: return "const auto&"; } llvm_unreachable("Unhandled TransferType enum"); }(); const std::string ReplacementText = (Twine(Prefix) + " [" + FirstVar->getNameAsString() + ", " + SecondVar->getNameAsString() + "]" + (CFRS ? " :" : "")) .str(); diag(DiagLoc, "use a structured binding to decompose a pair") << FixItHint::CreateReplacement(ReplaceRange, ReplacementText) << FixItHint::CreateRemoval( SourceRange{BeginDS->getBeginLoc(), EndDS->getEndLoc()}); }; if (const auto *COCE = Result.Nodes.getNodeAs(StdTieAssignStmtName)) { DiagAndFix(COCE->getBeginLoc(), Result.Nodes.getNodeAs(StdTieExprName)->getSourceRange()); return; } // Check whether PairVar, FirstVar and SecondVar have the same transfer type, // so they can be combined to structured binding. const auto *PairVar = Result.Nodes.getNodeAs(PairDeclName); const std::optional PairCaptureType = getTransferType(*Result.Context, PairVar->getType(), Result.Nodes.getNodeAs(InitExprName)->getType()); const std::optional FirstVarCaptureType = getTransferType(*Result.Context, FirstVar->getType(), *Result.Nodes.getNodeAs(FirstTypeName)); const std::optional SecondVarCaptureType = getTransferType(*Result.Context, SecondVar->getType(), *Result.Nodes.getNodeAs(SecondTypeName)); if (!PairCaptureType || !FirstVarCaptureType || !SecondVarCaptureType || *PairCaptureType != *FirstVarCaptureType || *FirstVarCaptureType != *SecondVarCaptureType) return; // Check PairVar is not used except for assignment members to firstVar and // SecondVar. if (auto AllRef = utils::decl_ref_expr::allDeclRefExprs(*PairVar, *ScopeBlock, *Result.Context); AllRef.size() != 2) return; DiagAndFix(PairVar->getBeginLoc(), CFRS ? PairVar->getSourceRange() : SourceRange(PairVar->getBeginLoc(), Lexer::getLocForEndOfToken( PairVar->getLocation(), 0, Result.Context->getSourceManager(), Result.Context->getLangOpts())), *PairCaptureType); } } // namespace clang::tidy::modernize