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