| 1 | //===--- StandaloneEmptyCheck.cpp - clang-tidy ----------------------------===// |
| 2 | // |
| 3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| 4 | // See https://llvm.org/LICENSE.txt for license information. |
| 5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| 6 | // |
| 7 | //===----------------------------------------------------------------------===// |
| 8 | |
| 9 | #include "StandaloneEmptyCheck.h" |
| 10 | #include "clang/AST/ASTContext.h" |
| 11 | #include "clang/AST/Decl.h" |
| 12 | #include "clang/AST/DeclBase.h" |
| 13 | #include "clang/AST/DeclCXX.h" |
| 14 | #include "clang/AST/Expr.h" |
| 15 | #include "clang/AST/ExprCXX.h" |
| 16 | #include "clang/AST/Stmt.h" |
| 17 | #include "clang/AST/Type.h" |
| 18 | #include "clang/ASTMatchers/ASTMatchFinder.h" |
| 19 | #include "clang/ASTMatchers/ASTMatchers.h" |
| 20 | #include "clang/Basic/Diagnostic.h" |
| 21 | #include "clang/Basic/SourceLocation.h" |
| 22 | #include "clang/Lex/Lexer.h" |
| 23 | #include "clang/Sema/HeuristicResolver.h" |
| 24 | #include "llvm/Support/Casting.h" |
| 25 | |
| 26 | namespace clang::tidy::bugprone { |
| 27 | |
| 28 | using ast_matchers::BoundNodes; |
| 29 | using ast_matchers::callee; |
| 30 | using ast_matchers::callExpr; |
| 31 | using ast_matchers::classTemplateDecl; |
| 32 | using ast_matchers::cxxMemberCallExpr; |
| 33 | using ast_matchers::cxxMethodDecl; |
| 34 | using ast_matchers::expr; |
| 35 | using ast_matchers::functionDecl; |
| 36 | using ast_matchers::hasAncestor; |
| 37 | using ast_matchers::hasName; |
| 38 | using ast_matchers::hasParent; |
| 39 | using ast_matchers::ignoringImplicit; |
| 40 | using ast_matchers::ignoringParenImpCasts; |
| 41 | using ast_matchers::MatchFinder; |
| 42 | using ast_matchers::optionally; |
| 43 | using ast_matchers::returns; |
| 44 | using ast_matchers::stmt; |
| 45 | using ast_matchers::stmtExpr; |
| 46 | using ast_matchers::unless; |
| 47 | using ast_matchers::voidType; |
| 48 | |
| 49 | static const Expr *getCondition(const BoundNodes &Nodes, |
| 50 | const StringRef NodeId) { |
| 51 | const auto *If = Nodes.getNodeAs<IfStmt>(ID: NodeId); |
| 52 | if (If != nullptr) |
| 53 | return If->getCond(); |
| 54 | |
| 55 | const auto *For = Nodes.getNodeAs<ForStmt>(ID: NodeId); |
| 56 | if (For != nullptr) |
| 57 | return For->getCond(); |
| 58 | |
| 59 | const auto *While = Nodes.getNodeAs<WhileStmt>(ID: NodeId); |
| 60 | if (While != nullptr) |
| 61 | return While->getCond(); |
| 62 | |
| 63 | const auto *Do = Nodes.getNodeAs<DoStmt>(ID: NodeId); |
| 64 | if (Do != nullptr) |
| 65 | return Do->getCond(); |
| 66 | |
| 67 | const auto *Switch = Nodes.getNodeAs<SwitchStmt>(ID: NodeId); |
| 68 | if (Switch != nullptr) |
| 69 | return Switch->getCond(); |
| 70 | |
| 71 | return nullptr; |
| 72 | } |
| 73 | |
| 74 | void StandaloneEmptyCheck::registerMatchers(ast_matchers::MatchFinder *Finder) { |
| 75 | // Ignore empty calls in a template definition which fall under callExpr |
| 76 | // non-member matcher even if they are methods. |
| 77 | const auto NonMemberMatcher = expr(ignoringImplicit(InnerMatcher: ignoringParenImpCasts( |
| 78 | InnerMatcher: callExpr( |
| 79 | hasParent(stmt(optionally(hasParent(stmtExpr().bind(ID: "stexpr" )))) |
| 80 | .bind(ID: "parent" )), |
| 81 | unless(hasAncestor(classTemplateDecl())), |
| 82 | callee(InnerMatcher: functionDecl(hasName(Name: "empty" ), unless(returns(InnerMatcher: voidType()))))) |
| 83 | .bind(ID: "empty" )))); |
| 84 | const auto MemberMatcher = |
| 85 | expr(ignoringImplicit(InnerMatcher: ignoringParenImpCasts(InnerMatcher: cxxMemberCallExpr( |
| 86 | hasParent(stmt(optionally(hasParent(stmtExpr().bind(ID: "stexpr" )))) |
| 87 | .bind(ID: "parent" )), |
| 88 | callee(InnerMatcher: cxxMethodDecl(hasName(Name: "empty" ), |
| 89 | unless(returns(InnerMatcher: voidType())))))))) |
| 90 | .bind(ID: "empty" ); |
| 91 | |
| 92 | Finder->addMatcher(NodeMatch: MemberMatcher, Action: this); |
| 93 | Finder->addMatcher(NodeMatch: NonMemberMatcher, Action: this); |
| 94 | } |
| 95 | |
| 96 | void StandaloneEmptyCheck::check(const MatchFinder::MatchResult &Result) { |
| 97 | // Skip if the parent node is Expr. |
| 98 | if (Result.Nodes.getNodeAs<Expr>(ID: "parent" )) |
| 99 | return; |
| 100 | |
| 101 | const auto *PParentStmtExpr = Result.Nodes.getNodeAs<Expr>(ID: "stexpr" ); |
| 102 | const auto *ParentCompStmt = Result.Nodes.getNodeAs<CompoundStmt>(ID: "parent" ); |
| 103 | const auto *ParentCond = getCondition(Nodes: Result.Nodes, NodeId: "parent" ); |
| 104 | const auto *ParentReturnStmt = Result.Nodes.getNodeAs<ReturnStmt>(ID: "parent" ); |
| 105 | |
| 106 | if (const auto *MemberCall = |
| 107 | Result.Nodes.getNodeAs<CXXMemberCallExpr>(ID: "empty" )) { |
| 108 | // Skip if it's a condition of the parent statement. |
| 109 | if (ParentCond == MemberCall->getExprStmt()) |
| 110 | return; |
| 111 | // Skip if it's the last statement in the GNU extension |
| 112 | // statement expression. |
| 113 | if (PParentStmtExpr && ParentCompStmt && |
| 114 | ParentCompStmt->body_back() == MemberCall->getExprStmt()) |
| 115 | return; |
| 116 | // Skip if it's a return statement |
| 117 | if (ParentReturnStmt) |
| 118 | return; |
| 119 | |
| 120 | SourceLocation MemberLoc = MemberCall->getBeginLoc(); |
| 121 | SourceLocation ReplacementLoc = MemberCall->getExprLoc(); |
| 122 | SourceRange ReplacementRange = SourceRange(ReplacementLoc, ReplacementLoc); |
| 123 | |
| 124 | ASTContext &Context = MemberCall->getRecordDecl()->getASTContext(); |
| 125 | DeclarationName Name = |
| 126 | Context.DeclarationNames.getIdentifier(ID: &Context.Idents.get(Name: "clear" )); |
| 127 | |
| 128 | auto Candidates = HeuristicResolver(Context).lookupDependentName( |
| 129 | MemberCall->getRecordDecl(), Name, [](const NamedDecl *ND) { |
| 130 | return isa<CXXMethodDecl>(ND) && |
| 131 | llvm::cast<CXXMethodDecl>(ND)->getMinRequiredArguments() == |
| 132 | 0 && |
| 133 | !llvm::cast<CXXMethodDecl>(ND)->isConst(); |
| 134 | }); |
| 135 | |
| 136 | bool HasClear = !Candidates.empty(); |
| 137 | if (HasClear) { |
| 138 | const auto *Clear = llvm::cast<CXXMethodDecl>(Candidates.at(0)); |
| 139 | QualType RangeType = MemberCall->getImplicitObjectArgument()->getType(); |
| 140 | bool QualifierIncompatible = |
| 141 | (!Clear->isVolatile() && RangeType.isVolatileQualified()) || |
| 142 | RangeType.isConstQualified(); |
| 143 | if (!QualifierIncompatible) { |
| 144 | diag(Loc: MemberLoc, |
| 145 | Description: "ignoring the result of 'empty()'; did you mean 'clear()'? " ) |
| 146 | << FixItHint::CreateReplacement(RemoveRange: ReplacementRange, Code: "clear" ); |
| 147 | return; |
| 148 | } |
| 149 | } |
| 150 | |
| 151 | diag(Loc: MemberLoc, Description: "ignoring the result of 'empty()'" ); |
| 152 | |
| 153 | } else if (const auto *NonMemberCall = |
| 154 | Result.Nodes.getNodeAs<CallExpr>(ID: "empty" )) { |
| 155 | if (ParentCond == NonMemberCall->getExprStmt()) |
| 156 | return; |
| 157 | if (PParentStmtExpr && ParentCompStmt && |
| 158 | ParentCompStmt->body_back() == NonMemberCall->getExprStmt()) |
| 159 | return; |
| 160 | if (ParentReturnStmt) |
| 161 | return; |
| 162 | if (NonMemberCall->getNumArgs() != 1) |
| 163 | return; |
| 164 | |
| 165 | SourceLocation NonMemberLoc = NonMemberCall->getExprLoc(); |
| 166 | SourceLocation NonMemberEndLoc = NonMemberCall->getEndLoc(); |
| 167 | |
| 168 | const Expr *Arg = NonMemberCall->getArg(Arg: 0); |
| 169 | CXXRecordDecl *ArgRecordDecl = Arg->getType()->getAsCXXRecordDecl(); |
| 170 | if (ArgRecordDecl == nullptr) |
| 171 | return; |
| 172 | |
| 173 | ASTContext &Context = ArgRecordDecl->getASTContext(); |
| 174 | DeclarationName Name = |
| 175 | Context.DeclarationNames.getIdentifier(ID: &Context.Idents.get(Name: "clear" )); |
| 176 | |
| 177 | auto Candidates = HeuristicResolver(Context).lookupDependentName( |
| 178 | ArgRecordDecl, Name, [](const NamedDecl *ND) { |
| 179 | return isa<CXXMethodDecl>(ND) && |
| 180 | llvm::cast<CXXMethodDecl>(ND)->getMinRequiredArguments() == |
| 181 | 0 && |
| 182 | !llvm::cast<CXXMethodDecl>(ND)->isConst(); |
| 183 | }); |
| 184 | |
| 185 | bool HasClear = !Candidates.empty(); |
| 186 | |
| 187 | if (HasClear) { |
| 188 | const auto *Clear = llvm::cast<CXXMethodDecl>(Candidates.at(0)); |
| 189 | bool QualifierIncompatible = |
| 190 | (!Clear->isVolatile() && Arg->getType().isVolatileQualified()) || |
| 191 | Arg->getType().isConstQualified(); |
| 192 | if (!QualifierIncompatible) { |
| 193 | std::string ReplacementText = |
| 194 | std::string(Lexer::getSourceText( |
| 195 | Range: CharSourceRange::getTokenRange(Arg->getSourceRange()), |
| 196 | SM: *Result.SourceManager, LangOpts: getLangOpts())) + |
| 197 | ".clear()" ; |
| 198 | SourceRange ReplacementRange = |
| 199 | SourceRange(NonMemberLoc, NonMemberEndLoc); |
| 200 | diag(Loc: NonMemberLoc, |
| 201 | Description: "ignoring the result of '%0'; did you mean 'clear()'?" ) |
| 202 | << llvm::dyn_cast<NamedDecl>(Val: NonMemberCall->getCalleeDecl()) |
| 203 | ->getQualifiedNameAsString() |
| 204 | << FixItHint::CreateReplacement(RemoveRange: ReplacementRange, Code: ReplacementText); |
| 205 | return; |
| 206 | } |
| 207 | } |
| 208 | |
| 209 | diag(Loc: NonMemberLoc, Description: "ignoring the result of '%0'" ) |
| 210 | << llvm::dyn_cast<NamedDecl>(Val: NonMemberCall->getCalleeDecl()) |
| 211 | ->getQualifiedNameAsString(); |
| 212 | } |
| 213 | } |
| 214 | |
| 215 | } // namespace clang::tidy::bugprone |
| 216 | |