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

source code of clang-tools-extra/clang-tidy/bugprone/ArgumentCommentCheck.cpp