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