clang-tools 22.0.0git
ArgumentCommentCheck.cpp
Go to the documentation of this file.
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-exception
6//
7//===----------------------------------------------------------------------===//
8
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
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} // namespace
31
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
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 are
68 // not specified by the standard, and standard library
69 // implementations in practice have to use reserved names to
70 // 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 away
166 // from this parameter. This gives us greater confidence that this is a
167 // 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 mocked
203// method M it defines M() with appropriate signature and a method used to set
204// up expectations - gmock_M() - with each argument's type changed the
205// corresponding matcher. This function returns M when given either M or
206// 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 by
229// `EXPECT_CALL(obj, Method(...))`) tries to find the real method being mocked
230// (returns nullptr, if the mock method doesn't override anything). For other
231// 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 parameter
236 // 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 should
247// 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 template
290 // parameter packs. If we find more arguments than the template
291 // definition has, it also means that they correspond to a parameter
292 // 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 << II
344 << 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::bugprone
Every ClangTidyCheck reports errors through a DiagnosticsEngine provided by this context.
ArgumentCommentCheck(StringRef Name, ClangTidyContext *Context)
void check(const ast_matchers::MatchFinder::MatchResult &Result) override
void storeOptions(ClangTidyOptions::OptionMap &Opts) override
void registerMatchers(ast_matchers::MatchFinder *Finder) override
static bool sameName(StringRef InComment, StringRef InDecl, bool StrictMode)
static SmallString< 64 > getName(const NamedDecl *ND)
Returns the diagnostic-friendly name of the node, or empty string.
static std::vector< std::pair< SourceLocation, StringRef > > getCommentsBeforeLoc(ASTContext *Ctx, SourceLocation Loc)
static std::vector< std::pair< SourceLocation, StringRef > > getCommentsInRange(ASTContext *Ctx, CharSourceRange Range)
static bool looksLikeExpectMethod(const CXXMethodDecl *Expect)
static const CXXMethodDecl * findMockedMethod(const CXXMethodDecl *Method)
static const FunctionDecl * resolveMocks(const FunctionDecl *Func)
static bool isLikelyTypo(llvm::ArrayRef< ParmVarDecl * > Params, StringRef ArgName, unsigned ArgIndex)
static bool areMockAndExpectMethods(const CXXMethodDecl *Mock, const CXXMethodDecl *Expect)
Token getPreviousToken(SourceLocation Location, const SourceManager &SM, const LangOptions &LangOpts, bool SkipComments)
Returns previous token or tok::unknown if not found.
llvm::StringMap< ClangTidyValue > OptionMap
static constexpr const char ArgName[]