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