clang-tools 23.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
20
22namespace {
23AST_MATCHER(Decl, isFromStdNamespaceOrSystemHeader) {
24 if (const auto *D = Node.getDeclContext()->getEnclosingNamespaceContext())
25 if (D->isStdNamespace())
26 return true;
27 if (Node.getLocation().isInvalid())
28 return false;
29 return Node.getASTContext().getSourceManager().isInSystemHeader(
30 Node.getLocation());
31}
32} // namespace
33
35 ClangTidyContext *Context)
36 : ClangTidyCheck(Name, Context),
37 StrictMode(Options.get("StrictMode", false)),
38 IgnoreSingleArgument(Options.get("IgnoreSingleArgument", false)),
39 CommentAnonymousInitLists(
40 Options.get("CommentAnonymousInitLists", false)),
41 CommentBoolLiterals(Options.get("CommentBoolLiterals", false)),
42 CommentCharacterLiterals(Options.get("CommentCharacterLiterals", false)),
43 CommentFloatLiterals(Options.get("CommentFloatLiterals", false)),
44 CommentIntegerLiterals(Options.get("CommentIntegerLiterals", false)),
45 CommentNullPtrs(Options.get("CommentNullPtrs", false)),
46 CommentParenthesizedTemporaries(
47 Options.get("CommentParenthesizedTemporaries", false)),
48 CommentStringLiterals(Options.get("CommentStringLiterals", false)),
49 CommentTypedInitLists(Options.get("CommentTypedInitLists", false)),
50 CommentUserDefinedLiterals(
51 Options.get("CommentUserDefinedLiterals", false)),
52 IdentRE("^(/\\* *)([_A-Za-z][_A-Za-z0-9]*)( *= *\\*/)$") {}
53
55 Options.store(Opts, "StrictMode", StrictMode);
56 Options.store(Opts, "IgnoreSingleArgument", IgnoreSingleArgument);
57 Options.store(Opts, "CommentAnonymousInitLists", CommentAnonymousInitLists);
58 Options.store(Opts, "CommentBoolLiterals", CommentBoolLiterals);
59 Options.store(Opts, "CommentCharacterLiterals", CommentCharacterLiterals);
60 Options.store(Opts, "CommentFloatLiterals", CommentFloatLiterals);
61 Options.store(Opts, "CommentIntegerLiterals", CommentIntegerLiterals);
62 Options.store(Opts, "CommentNullPtrs", CommentNullPtrs);
63 Options.store(Opts, "CommentParenthesizedTemporaries",
64 CommentParenthesizedTemporaries);
65 Options.store(Opts, "CommentStringLiterals", CommentStringLiterals);
66 Options.store(Opts, "CommentTypedInitLists", CommentTypedInitLists);
67 Options.store(Opts, "CommentUserDefinedLiterals", CommentUserDefinedLiterals);
68}
69
70void ArgumentCommentCheck::registerMatchers(MatchFinder *Finder) {
71 Finder->addMatcher(
72 callExpr(unless(cxxOperatorCallExpr()), unless(userDefinedLiteral()),
73 // NewCallback's arguments relate to the pointed function,
74 // don't check them against NewCallback's parameter names.
75 // FIXME: Make this configurable.
76 unless(hasDeclaration(functionDecl(
77 hasAnyName("NewCallback", "NewPermanentCallback")))),
78 // Ignore APIs from the standard library, since their names are
79 // not specified by the standard, and standard library
80 // implementations in practice have to use reserved names to
81 // avoid conflicts with same-named macros.
82 unless(hasDeclaration(isFromStdNamespaceOrSystemHeader())))
83 .bind("expr"),
84 this);
85 Finder->addMatcher(cxxConstructExpr(unless(hasDeclaration(
86 isFromStdNamespaceOrSystemHeader())))
87 .bind("expr"),
88 this);
89}
90
91static std::vector<CommentToken> getCommentsBeforeLoc(ASTContext *Ctx,
92 SourceLocation Loc) {
93 std::vector<CommentToken> Comments;
94 while (Loc.isValid()) {
95 const std::optional<Token> Tok = utils::lexer::getPreviousToken(
96 Loc, Ctx->getSourceManager(), Ctx->getLangOpts(),
97 /*SkipComments=*/false);
98 if (!Tok || Tok->isNot(tok::comment))
99 break;
100 Loc = Tok->getLocation();
101 Comments.emplace_back(CommentToken{
102 Loc,
103 Lexer::getSourceText(CharSourceRange::getCharRange(
104 Loc, Loc.getLocWithOffset(Tok->getLength())),
105 Ctx->getSourceManager(), Ctx->getLangOpts()),
106 });
107 }
108 return Comments;
109}
110
111template <typename NamedDeclRange>
112static bool isLikelyTypo(const NamedDeclRange &Candidates, StringRef ArgName,
113 StringRef TargetName) {
114 const std::string ArgNameLowerStr = ArgName.lower();
115 const StringRef ArgNameLower = ArgNameLowerStr;
116 // The threshold is arbitrary.
117 const unsigned UpperBound = ((ArgName.size() + 2) / 3) + 1;
118 const unsigned ThisED =
119 ArgNameLower.edit_distance(TargetName.lower(),
120 /*AllowReplacements=*/true, UpperBound);
121 if (ThisED >= UpperBound)
122 return false;
123
124 return llvm::all_of(Candidates, [&](const auto &Candidate) {
125 const IdentifierInfo *II = Candidate->getIdentifier();
126 if (!II)
127 return true;
128
129 // Skip the target itself.
130 if (II->getName() == TargetName)
131 return true;
132
133 const unsigned Threshold = 2;
134 // Other candidates must be an edit distance at least Threshold more away
135 // from this candidate. This gives us greater confidence that this is a
136 // typo of this candidate and not one with a similar name.
137 const unsigned OtherED = ArgNameLower.edit_distance(
138 II->getName().lower(),
139 /*AllowReplacements=*/true, ThisED + Threshold);
140 return OtherED >= ThisED + Threshold;
141 });
142}
143
144static bool sameName(StringRef InComment, StringRef InDecl, bool StrictMode) {
145 if (StrictMode)
146 return InComment == InDecl;
147 InComment = InComment.trim('_');
148 InDecl = InDecl.trim('_');
149 // FIXME: compare_insensitive only works for ASCII.
150 return InComment.compare_insensitive(InDecl) == 0;
151}
152
153static bool looksLikeExpectMethod(const CXXMethodDecl *Expect) {
154 return Expect != nullptr && Expect->getLocation().isMacroID() &&
155 Expect->getNameInfo().getName().isIdentifier() &&
156 Expect->getName().starts_with("gmock_");
157}
158static bool areMockAndExpectMethods(const CXXMethodDecl *Mock,
159 const CXXMethodDecl *Expect) {
160 assert(looksLikeExpectMethod(Expect));
161 return Mock != nullptr && Mock->getNextDeclInContext() == Expect &&
162 Mock->getNumParams() == Expect->getNumParams() &&
163 Mock->getLocation().isMacroID() &&
164 Mock->getNameInfo().getName().isIdentifier() &&
165 Mock->getName() == Expect->getName().substr(strlen("gmock_"));
166}
167
168// This uses implementation details of MOCK_METHODx_ macros: for each mocked
169// method M it defines M() with appropriate signature and a method used to set
170// up expectations - gmock_M() - with each argument's type changed the
171// corresponding matcher. This function returns M when given either M or
172// gmock_M.
173static const CXXMethodDecl *findMockedMethod(const CXXMethodDecl *Method) {
174 if (looksLikeExpectMethod(Method)) {
175 const DeclContext *Ctx = Method->getDeclContext();
176 if (Ctx == nullptr || !Ctx->isRecord())
177 return nullptr;
178 for (const auto *D : Ctx->decls()) {
179 if (D->getNextDeclInContext() == Method) {
180 const auto *Previous = dyn_cast<CXXMethodDecl>(D);
181 return areMockAndExpectMethods(Previous, Method) ? Previous : nullptr;
182 }
183 }
184 return nullptr;
185 }
186 if (const auto *Next =
187 dyn_cast_or_null<CXXMethodDecl>(Method->getNextDeclInContext())) {
188 if (looksLikeExpectMethod(Next) && areMockAndExpectMethods(Method, Next))
189 return Method;
190 }
191 return nullptr;
192}
193
194// For gmock expectation builder method (the target of the call generated by
195// `EXPECT_CALL(obj, Method(...))`) tries to find the real method being mocked
196// (returns nullptr, if the mock method doesn't override anything). For other
197// functions returns the function itself.
198static const FunctionDecl *resolveMocks(const FunctionDecl *Func) {
199 if (const auto *Method = dyn_cast<CXXMethodDecl>(Func)) {
200 if (const auto *MockedMethod = findMockedMethod(Method)) {
201 // If mocked method overrides the real one, we can use its parameter
202 // names, otherwise we're out of luck.
203 if (MockedMethod->size_overridden_methods() > 0)
204 return *MockedMethod->begin_overridden_methods();
205 return nullptr;
206 }
207 }
208 return Func;
209}
210
211namespace {
212
213enum class InitListKind {
214 None,
215 Anonymous,
216 Typed,
217};
218
219} // namespace
220
221static InitListKind getInitListKind(const Expr *Arg) {
222 Arg = Arg->IgnoreUnlessSpelledInSource();
223
224 if (const auto *StdInit = dyn_cast<CXXStdInitializerListExpr>(Arg))
225 Arg = StdInit->getSubExpr()->IgnoreUnlessSpelledInSource();
226
227 if (isa<InitListExpr>(Arg))
228 return InitListKind::Anonymous;
229
230 if (const auto *Ctor = dyn_cast<CXXConstructExpr>(Arg)) {
231 if (!Ctor->isListInitialization())
232 return InitListKind::None;
233 // CXXTemporaryObjectExpr corresponds to explicit Type{...} syntax.
234 if (isa<CXXTemporaryObjectExpr>(Ctor))
235 return InitListKind::Typed;
236 // Other list-initialized constructions (for example '{}') have no
237 // explicit type at the call site.
238 return InitListKind::Anonymous;
239 }
240
241 // std::initializer_list<T>{...} is represented as a functional cast whose
242 // subexpression carries the list-initialization spelling.
243 if (const auto *FuncCast = dyn_cast<CXXFunctionalCastExpr>(Arg)) {
244 const Expr *SubExpr = FuncCast->getSubExpr()->IgnoreImplicit();
245 if (FuncCast->isListInitialization() ||
246 isa<CXXStdInitializerListExpr>(SubExpr))
247 return InitListKind::Typed;
248 }
249
250 return InitListKind::None;
251}
252
253static bool isParenthesizedTemporary(const Expr *Arg) {
254 Arg = Arg->IgnoreUnlessSpelledInSource();
255 if (const auto *TempObject = dyn_cast<CXXTemporaryObjectExpr>(Arg))
256 return !TempObject->isListInitialization();
257 // CXXFunctionalCastExpr with CXXParenListInitExpr corresponds to explicit
258 // Type(...) aggregate temporary initialization syntax.
259 const auto *FuncCast = dyn_cast<CXXFunctionalCastExpr>(Arg);
260 return FuncCast &&
261 isa<CXXParenListInitExpr>(FuncCast->getSubExpr()->IgnoreImplicit());
262}
263
264// Given the argument type and the options determine if we should be adding an
265// argument comment and which diagnostic wording to use.
266ArgumentCommentCheck::CommentKind
267ArgumentCommentCheck::shouldAddComment(const Expr *Arg) const {
268 const InitListKind Kind = getInitListKind(Arg);
269 const bool IsParenthesizedTemporary = isParenthesizedTemporary(Arg);
270
271 // Strip implicit wrappers so brace-init arguments bound to references still
272 // look like list-initialization at this point.
273 Arg = Arg->IgnoreImplicit();
274 if (const auto *UO = dyn_cast<UnaryOperator>(Arg))
275 Arg = UO->getSubExpr()->IgnoreImplicit();
276 if (Arg->getExprLoc().isMacroID())
277 return CommentKind::None;
278
279 if ((CommentAnonymousInitLists && Kind == InitListKind::Anonymous) ||
280 (CommentTypedInitLists && Kind == InitListKind::Typed) ||
281 (CommentParenthesizedTemporaries && IsParenthesizedTemporary)) {
282 return CommentKind::NonLiteral;
283 }
284
285 if ((CommentBoolLiterals && isa<CXXBoolLiteralExpr>(Arg)) ||
286 (CommentIntegerLiterals && isa<IntegerLiteral>(Arg)) ||
287 (CommentFloatLiterals && isa<FloatingLiteral>(Arg)) ||
288 (CommentUserDefinedLiterals && isa<UserDefinedLiteral>(Arg)) ||
289 (CommentCharacterLiterals && isa<CharacterLiteral>(Arg)) ||
290 (CommentStringLiterals && isa<StringLiteral>(Arg)) ||
291 (CommentNullPtrs && isa<CXXNullPtrLiteralExpr>(Arg))) {
292 return CommentKind::Literal;
293 }
294
295 return CommentKind::None;
296}
297
298void ArgumentCommentCheck::checkCallArgs(ASTContext *Ctx,
299 const FunctionDecl *OriginalCallee,
300 SourceLocation ArgBeginLoc,
301 llvm::ArrayRef<const Expr *> Args) {
302 const FunctionDecl *Callee = resolveMocks(OriginalCallee);
303 if (!Callee)
304 return;
305
306 Callee = Callee->getFirstDecl();
307 if (const auto *Ctor = dyn_cast<CXXConstructorDecl>(Callee);
308 Ctor && Ctor->isInheritingConstructor()) {
309 if (const auto *BaseCtor = Ctor->getInheritedConstructor().getConstructor())
310 Callee = BaseCtor->getFirstDecl();
311 }
312 const unsigned NumArgs =
313 std::min<unsigned>(Args.size(), Callee->getNumParams());
314 if ((NumArgs == 0) || (IgnoreSingleArgument && NumArgs == 1))
315 return;
316
317 auto MakeFileCharRange = [Ctx](SourceLocation Begin, SourceLocation End) {
318 return Lexer::makeFileCharRange(CharSourceRange::getCharRange(Begin, End),
319 Ctx->getSourceManager(),
320 Ctx->getLangOpts());
321 };
322
323 for (unsigned I = 0; I < NumArgs; ++I) {
324 const ParmVarDecl *PVD = Callee->getParamDecl(I);
325 const IdentifierInfo *II = PVD->getIdentifier();
326 if (!II)
327 continue;
328 if (FunctionDecl *Template = Callee->getTemplateInstantiationPattern()) {
329 // Don't warn on arguments for parameters instantiated from template
330 // parameter packs. If we find more arguments than the template
331 // definition has, it also means that they correspond to a parameter
332 // pack.
333 if (Template->getNumParams() <= I ||
334 Template->getParamDecl(I)->isParameterPack()) {
335 continue;
336 }
337 }
338
339 const CharSourceRange BeforeArgument =
340 MakeFileCharRange(ArgBeginLoc, Args[I]->getBeginLoc());
341 ArgBeginLoc = Args[I]->getEndLoc();
342
343 std::vector<CommentToken> Comments;
344 if (BeforeArgument.isValid()) {
346 BeforeArgument, Ctx->getSourceManager(), Ctx->getLangOpts());
347 } else {
348 // Fall back to parsing back from the start of the argument.
349 const CharSourceRange ArgsRange =
350 MakeFileCharRange(Args[I]->getBeginLoc(), Args[I]->getEndLoc());
351 Comments = getCommentsBeforeLoc(Ctx, ArgsRange.getBegin());
352 }
353
354 for (const auto &Comment : Comments) {
355 SmallVector<StringRef, 2> Matches;
356 if (IdentRE.match(Comment.Text, &Matches) &&
357 !sameName(Matches[2], II->getName(), StrictMode)) {
358 {
359 const DiagnosticBuilder Diag =
360 diag(Comment.Loc, "argument name '%0' in comment does not "
361 "match parameter name %1")
362 << Matches[2] << II;
363 if (isLikelyTypo(Callee->parameters(), Matches[2], II->getName())) {
364 Diag << FixItHint::CreateReplacement(
365 Comment.Loc,
366 llvm::Twine(Matches[1] + II->getName() + Matches[3]).str());
367 }
368 }
369 diag(PVD->getLocation(), "%0 declared here", DiagnosticIDs::Note) << II;
370 if (OriginalCallee != Callee) {
371 diag(OriginalCallee->getLocation(),
372 "actual callee (%0) is declared here", DiagnosticIDs::Note)
373 << OriginalCallee;
374 }
375 }
376 }
377
378 // If the argument comments are missing for configured argument kinds, add
379 // them.
380 const CommentKind Kind = shouldAddComment(Args[I]);
381 if (Comments.empty() && Kind != CommentKind::None) {
382 SmallString<32> ArgComment;
383 llvm::Twine(llvm::Twine("/*") + II->getName() + "=*/")
384 .toStringRef(ArgComment);
385 const DiagnosticBuilder Diag =
386 diag(Args[I]->getBeginLoc(),
387 "argument comment missing for %select{literal argument|"
388 "argument}0 %1")
389 << (Kind == CommentKind::Literal ? 0 : 1) << II
390 << FixItHint::CreateInsertion(Args[I]->getBeginLoc(), ArgComment);
391 }
392 }
393}
394
395void ArgumentCommentCheck::check(const MatchFinder::MatchResult &Result) {
396 const auto *E = Result.Nodes.getNodeAs<Expr>("expr");
397 if (const auto *Call = dyn_cast<CallExpr>(E)) {
398 const FunctionDecl *Callee = Call->getDirectCallee();
399 if (!Callee)
400 return;
401
402 checkCallArgs(Result.Context, Callee, Call->getCallee()->getEndLoc(),
403 llvm::ArrayRef(Call->getArgs(), Call->getNumArgs()));
404 } else {
405 const auto *Construct = cast<CXXConstructExpr>(E);
406 if (Construct->getNumArgs() > 0 &&
407 Construct->getArg(0)->getSourceRange() == Construct->getSourceRange()) {
408 // Ignore implicit construction.
409 return;
410 }
411 checkCallArgs(
412 Result.Context, Construct->getConstructor(),
413 Construct->getParenOrBraceRange().getBegin(),
414 llvm::ArrayRef(Construct->getArgs(), Construct->getNumArgs()));
415 }
416}
417
418} // 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 InitListKind getInitListKind(const Expr *Arg)
static bool sameName(StringRef InComment, StringRef InDecl, bool StrictMode)
static bool isParenthesizedTemporary(const Expr *Arg)
static bool isLikelyTypo(const NamedDeclRange &Candidates, StringRef ArgName, StringRef TargetName)
static bool looksLikeExpectMethod(const CXXMethodDecl *Expect)
static const CXXMethodDecl * findMockedMethod(const CXXMethodDecl *Method)
static const FunctionDecl * resolveMocks(const FunctionDecl *Func)
static std::vector< CommentToken > getCommentsBeforeLoc(ASTContext *Ctx, SourceLocation Loc)
static bool areMockAndExpectMethods(const CXXMethodDecl *Mock, const CXXMethodDecl *Expect)
std::optional< Token > getPreviousToken(SourceLocation Location, const SourceManager &SM, const LangOptions &LangOpts, bool SkipComments)
Returns previous token or std::nullopt if not found.
std::vector< CommentToken > getTrailingCommentsInRange(CharSourceRange Range, const SourceManager &SM, const LangOptions &LangOpts)
Returns comment tokens found in the given range. If a non-comment token is encountered,...
llvm::StringMap< ClangTidyValue > OptionMap
static constexpr const char ArgName[]