1 //===--- ArgumentCommentCheck.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 "ArgumentCommentCheck.h" 10 #include "clang/AST/ASTContext.h" 11 #include "clang/ASTMatchers/ASTMatchFinder.h" 12 #include "clang/Lex/Lexer.h" 13 #include "clang/Lex/Token.h" 14 15 #include "../utils/LexerUtils.h" 16 17 using namespace clang::ast_matchers; 18 19 namespace clang { 20 namespace tidy { 21 namespace bugprone { 22 23 ArgumentCommentCheck::ArgumentCommentCheck(StringRef Name, 24 ClangTidyContext *Context) 25 : ClangTidyCheck(Name, Context), 26 StrictMode(Options.getLocalOrGlobal("StrictMode", 0) != 0), 27 CommentBoolLiterals(Options.getLocalOrGlobal("CommentBoolLiterals", 0) != 28 0), 29 CommentIntegerLiterals( 30 Options.getLocalOrGlobal("CommentIntegerLiterals", 0) != 0), 31 CommentFloatLiterals( 32 Options.getLocalOrGlobal("CommentFloatLiterals", 0) != 0), 33 CommentStringLiterals( 34 Options.getLocalOrGlobal("CommentStringLiterals", 0) != 0), 35 CommentUserDefinedLiterals( 36 Options.getLocalOrGlobal("CommentUserDefinedLiterals", 0) != 0), 37 CommentCharacterLiterals( 38 Options.getLocalOrGlobal("CommentCharacterLiterals", 0) != 0), 39 CommentNullPtrs(Options.getLocalOrGlobal("CommentNullPtrs", 0) != 0), 40 IdentRE("^(/\\* *)([_A-Za-z][_A-Za-z0-9]*)( *= *\\*/)$") {} 41 42 void ArgumentCommentCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) { 43 Options.store(Opts, "StrictMode", StrictMode); 44 Options.store(Opts, "CommentBoolLiterals", CommentBoolLiterals); 45 Options.store(Opts, "CommentIntegerLiterals", CommentIntegerLiterals); 46 Options.store(Opts, "CommentFloatLiterals", CommentFloatLiterals); 47 Options.store(Opts, "CommentStringLiterals", CommentStringLiterals); 48 Options.store(Opts, "CommentUserDefinedLiterals", CommentUserDefinedLiterals); 49 Options.store(Opts, "CommentCharacterLiterals", CommentCharacterLiterals); 50 Options.store(Opts, "CommentNullPtrs", CommentNullPtrs); 51 } 52 53 void ArgumentCommentCheck::registerMatchers(MatchFinder *Finder) { 54 Finder->addMatcher( 55 callExpr(unless(cxxOperatorCallExpr()), 56 // NewCallback's arguments relate to the pointed function, 57 // don't check them against NewCallback's parameter names. 58 // FIXME: Make this configurable. 59 unless(hasDeclaration(functionDecl( 60 hasAnyName("NewCallback", "NewPermanentCallback"))))) 61 .bind("expr"), 62 this); 63 Finder->addMatcher(cxxConstructExpr().bind("expr"), this); 64 } 65 66 static std::vector<std::pair<SourceLocation, StringRef>> 67 getCommentsInRange(ASTContext *Ctx, CharSourceRange Range) { 68 std::vector<std::pair<SourceLocation, StringRef>> Comments; 69 auto &SM = Ctx->getSourceManager(); 70 std::pair<FileID, unsigned> BeginLoc = SM.getDecomposedLoc(Range.getBegin()), 71 EndLoc = SM.getDecomposedLoc(Range.getEnd()); 72 73 if (BeginLoc.first != EndLoc.first) 74 return Comments; 75 76 bool Invalid = false; 77 StringRef Buffer = SM.getBufferData(BeginLoc.first, &Invalid); 78 if (Invalid) 79 return Comments; 80 81 const char *StrData = Buffer.data() + BeginLoc.second; 82 83 Lexer TheLexer(SM.getLocForStartOfFile(BeginLoc.first), Ctx->getLangOpts(), 84 Buffer.begin(), StrData, Buffer.end()); 85 TheLexer.SetCommentRetentionState(true); 86 87 while (true) { 88 Token Tok; 89 if (TheLexer.LexFromRawLexer(Tok)) 90 break; 91 if (Tok.getLocation() == Range.getEnd() || Tok.is(tok::eof)) 92 break; 93 94 if (Tok.is(tok::comment)) { 95 std::pair<FileID, unsigned> CommentLoc = 96 SM.getDecomposedLoc(Tok.getLocation()); 97 assert(CommentLoc.first == BeginLoc.first); 98 Comments.emplace_back( 99 Tok.getLocation(), 100 StringRef(Buffer.begin() + CommentLoc.second, Tok.getLength())); 101 } else { 102 // Clear comments found before the different token, e.g. comma. 103 Comments.clear(); 104 } 105 } 106 107 return Comments; 108 } 109 110 static std::vector<std::pair<SourceLocation, StringRef>> 111 getCommentsBeforeLoc(ASTContext *Ctx, SourceLocation Loc) { 112 std::vector<std::pair<SourceLocation, StringRef>> Comments; 113 while (Loc.isValid()) { 114 clang::Token Tok = utils::lexer::getPreviousToken( 115 Loc, Ctx->getSourceManager(), Ctx->getLangOpts(), 116 /*SkipComments=*/false); 117 if (Tok.isNot(tok::comment)) 118 break; 119 Loc = Tok.getLocation(); 120 Comments.emplace_back( 121 Loc, 122 Lexer::getSourceText(CharSourceRange::getCharRange( 123 Loc, Loc.getLocWithOffset(Tok.getLength())), 124 Ctx->getSourceManager(), Ctx->getLangOpts())); 125 } 126 return Comments; 127 } 128 129 static bool isLikelyTypo(llvm::ArrayRef<ParmVarDecl *> Params, 130 StringRef ArgName, unsigned ArgIndex) { 131 std::string ArgNameLowerStr = ArgName.lower(); 132 StringRef ArgNameLower = ArgNameLowerStr; 133 // The threshold is arbitrary. 134 unsigned UpperBound = (ArgName.size() + 2) / 3 + 1; 135 unsigned ThisED = ArgNameLower.edit_distance( 136 Params[ArgIndex]->getIdentifier()->getName().lower(), 137 /*AllowReplacements=*/true, UpperBound); 138 if (ThisED >= UpperBound) 139 return false; 140 141 for (unsigned I = 0, E = Params.size(); I != E; ++I) { 142 if (I == ArgIndex) 143 continue; 144 IdentifierInfo *II = Params[I]->getIdentifier(); 145 if (!II) 146 continue; 147 148 const unsigned Threshold = 2; 149 // Other parameters must be an edit distance at least Threshold more away 150 // from this parameter. This gives us greater confidence that this is a 151 // typo of this parameter and not one with a similar name. 152 unsigned OtherED = ArgNameLower.edit_distance(II->getName().lower(), 153 /*AllowReplacements=*/true, 154 ThisED + Threshold); 155 if (OtherED < ThisED + Threshold) 156 return false; 157 } 158 159 return true; 160 } 161 162 static bool sameName(StringRef InComment, StringRef InDecl, bool StrictMode) { 163 if (StrictMode) 164 return InComment == InDecl; 165 InComment = InComment.trim('_'); 166 InDecl = InDecl.trim('_'); 167 // FIXME: compare_lower only works for ASCII. 168 return InComment.compare_lower(InDecl) == 0; 169 } 170 171 static bool looksLikeExpectMethod(const CXXMethodDecl *Expect) { 172 return Expect != nullptr && Expect->getLocation().isMacroID() && 173 Expect->getNameInfo().getName().isIdentifier() && 174 Expect->getName().startswith("gmock_"); 175 } 176 static bool areMockAndExpectMethods(const CXXMethodDecl *Mock, 177 const CXXMethodDecl *Expect) { 178 assert(looksLikeExpectMethod(Expect)); 179 return Mock != nullptr && Mock->getNextDeclInContext() == Expect && 180 Mock->getNumParams() == Expect->getNumParams() && 181 Mock->getLocation().isMacroID() && 182 Mock->getNameInfo().getName().isIdentifier() && 183 Mock->getName() == Expect->getName().substr(strlen("gmock_")); 184 } 185 186 // This uses implementation details of MOCK_METHODx_ macros: for each mocked 187 // method M it defines M() with appropriate signature and a method used to set 188 // up expectations - gmock_M() - with each argument's type changed the 189 // corresponding matcher. This function returns M when given either M or 190 // gmock_M. 191 static const CXXMethodDecl *findMockedMethod(const CXXMethodDecl *Method) { 192 if (looksLikeExpectMethod(Method)) { 193 const DeclContext *Ctx = Method->getDeclContext(); 194 if (Ctx == nullptr || !Ctx->isRecord()) 195 return nullptr; 196 for (const auto *D : Ctx->decls()) { 197 if (D->getNextDeclInContext() == Method) { 198 const auto *Previous = dyn_cast<CXXMethodDecl>(D); 199 return areMockAndExpectMethods(Previous, Method) ? Previous : nullptr; 200 } 201 } 202 return nullptr; 203 } 204 if (const auto *Next = 205 dyn_cast_or_null<CXXMethodDecl>(Method->getNextDeclInContext())) { 206 if (looksLikeExpectMethod(Next) && areMockAndExpectMethods(Method, Next)) 207 return Method; 208 } 209 return nullptr; 210 } 211 212 // For gmock expectation builder method (the target of the call generated by 213 // `EXPECT_CALL(obj, Method(...))`) tries to find the real method being mocked 214 // (returns nullptr, if the mock method doesn't override anything). For other 215 // functions returns the function itself. 216 static const FunctionDecl *resolveMocks(const FunctionDecl *Func) { 217 if (const auto *Method = dyn_cast<CXXMethodDecl>(Func)) { 218 if (const auto *MockedMethod = findMockedMethod(Method)) { 219 // If mocked method overrides the real one, we can use its parameter 220 // names, otherwise we're out of luck. 221 if (MockedMethod->size_overridden_methods() > 0) { 222 return *MockedMethod->begin_overridden_methods(); 223 } 224 return nullptr; 225 } 226 } 227 return Func; 228 } 229 230 // Given the argument type and the options determine if we should 231 // be adding an argument comment. 232 bool ArgumentCommentCheck::shouldAddComment(const Expr *Arg) const { 233 if (Arg->getExprLoc().isMacroID()) 234 return false; 235 Arg = Arg->IgnoreImpCasts(); 236 return (CommentBoolLiterals && isa<CXXBoolLiteralExpr>(Arg)) || 237 (CommentIntegerLiterals && isa<IntegerLiteral>(Arg)) || 238 (CommentFloatLiterals && isa<FloatingLiteral>(Arg)) || 239 (CommentUserDefinedLiterals && isa<UserDefinedLiteral>(Arg)) || 240 (CommentCharacterLiterals && isa<CharacterLiteral>(Arg)) || 241 (CommentStringLiterals && isa<StringLiteral>(Arg)) || 242 (CommentNullPtrs && isa<CXXNullPtrLiteralExpr>(Arg)); 243 } 244 245 void ArgumentCommentCheck::checkCallArgs(ASTContext *Ctx, 246 const FunctionDecl *OriginalCallee, 247 SourceLocation ArgBeginLoc, 248 llvm::ArrayRef<const Expr *> Args) { 249 const FunctionDecl *Callee = resolveMocks(OriginalCallee); 250 if (!Callee) 251 return; 252 253 Callee = Callee->getFirstDecl(); 254 unsigned NumArgs = std::min<unsigned>(Args.size(), Callee->getNumParams()); 255 if (NumArgs == 0) 256 return; 257 258 auto MakeFileCharRange = [Ctx](SourceLocation Begin, SourceLocation End) { 259 return Lexer::makeFileCharRange(CharSourceRange::getCharRange(Begin, End), 260 Ctx->getSourceManager(), 261 Ctx->getLangOpts()); 262 }; 263 264 for (unsigned I = 0; I < NumArgs; ++I) { 265 const ParmVarDecl *PVD = Callee->getParamDecl(I); 266 IdentifierInfo *II = PVD->getIdentifier(); 267 if (!II) 268 continue; 269 if (auto Template = Callee->getTemplateInstantiationPattern()) { 270 // Don't warn on arguments for parameters instantiated from template 271 // parameter packs. If we find more arguments than the template 272 // definition has, it also means that they correspond to a parameter 273 // pack. 274 if (Template->getNumParams() <= I || 275 Template->getParamDecl(I)->isParameterPack()) { 276 continue; 277 } 278 } 279 280 CharSourceRange BeforeArgument = 281 MakeFileCharRange(ArgBeginLoc, Args[I]->getBeginLoc()); 282 ArgBeginLoc = Args[I]->getEndLoc(); 283 284 std::vector<std::pair<SourceLocation, StringRef>> Comments; 285 if (BeforeArgument.isValid()) { 286 Comments = getCommentsInRange(Ctx, BeforeArgument); 287 } else { 288 // Fall back to parsing back from the start of the argument. 289 CharSourceRange ArgsRange = MakeFileCharRange( 290 Args[I]->getBeginLoc(), Args[NumArgs - 1]->getEndLoc()); 291 Comments = getCommentsBeforeLoc(Ctx, ArgsRange.getBegin()); 292 } 293 294 for (auto Comment : Comments) { 295 llvm::SmallVector<StringRef, 2> Matches; 296 if (IdentRE.match(Comment.second, &Matches) && 297 !sameName(Matches[2], II->getName(), StrictMode)) { 298 { 299 DiagnosticBuilder Diag = 300 diag(Comment.first, "argument name '%0' in comment does not " 301 "match parameter name %1") 302 << Matches[2] << II; 303 if (isLikelyTypo(Callee->parameters(), Matches[2], I)) { 304 Diag << FixItHint::CreateReplacement( 305 Comment.first, (Matches[1] + II->getName() + Matches[3]).str()); 306 } 307 } 308 diag(PVD->getLocation(), "%0 declared here", DiagnosticIDs::Note) << II; 309 if (OriginalCallee != Callee) { 310 diag(OriginalCallee->getLocation(), 311 "actual callee (%0) is declared here", DiagnosticIDs::Note) 312 << OriginalCallee; 313 } 314 } 315 } 316 317 // If the argument comments are missing for literals add them. 318 if (Comments.empty() && shouldAddComment(Args[I])) { 319 std::string ArgComment = 320 (llvm::Twine("/*") + II->getName() + "=*/").str(); 321 DiagnosticBuilder Diag = 322 diag(Args[I]->getBeginLoc(), 323 "argument comment missing for literal argument %0") 324 << II 325 << FixItHint::CreateInsertion(Args[I]->getBeginLoc(), ArgComment); 326 } 327 } 328 } // namespace bugprone 329 330 void ArgumentCommentCheck::check(const MatchFinder::MatchResult &Result) { 331 const auto *E = Result.Nodes.getNodeAs<Expr>("expr"); 332 if (const auto *Call = dyn_cast<CallExpr>(E)) { 333 const FunctionDecl *Callee = Call->getDirectCallee(); 334 if (!Callee) 335 return; 336 337 checkCallArgs(Result.Context, Callee, Call->getCallee()->getEndLoc(), 338 llvm::makeArrayRef(Call->getArgs(), Call->getNumArgs())); 339 } else { 340 const auto *Construct = cast<CXXConstructExpr>(E); 341 if (Construct->getNumArgs() == 1 && 342 Construct->getArg(0)->getSourceRange() == Construct->getSourceRange()) { 343 // Ignore implicit construction. 344 return; 345 } 346 checkCallArgs( 347 Result.Context, Construct->getConstructor(), 348 Construct->getParenOrBraceRange().getBegin(), 349 llvm::makeArrayRef(Construct->getArgs(), Construct->getNumArgs())); 350 } 351 } 352 353 } // namespace bugprone 354 } // namespace tidy 355 } // namespace clang 356