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 CommentBoolLiterals(Options.get("CommentBoolLiterals", false)),
40 CommentIntegerLiterals(Options.get("CommentIntegerLiterals", false)),
41 CommentFloatLiterals(Options.get("CommentFloatLiterals", false)),
42 CommentStringLiterals(Options.get("CommentStringLiterals", false)),
43 CommentUserDefinedLiterals(
44 Options.get("CommentUserDefinedLiterals", false)),
45 CommentCharacterLiterals(Options.get("CommentCharacterLiterals", false)),
46 CommentNullPtrs(Options.get("CommentNullPtrs", false)),
47 IdentRE("^(/\\* *)([_A-Za-z][_A-Za-z0-9]*)( *= *\\*/)$") {}
48
50 Options.store(Opts, "StrictMode", StrictMode);
51 Options.store(Opts, "IgnoreSingleArgument", IgnoreSingleArgument);
52 Options.store(Opts, "CommentBoolLiterals", CommentBoolLiterals);
53 Options.store(Opts, "CommentIntegerLiterals", CommentIntegerLiterals);
54 Options.store(Opts, "CommentFloatLiterals", CommentFloatLiterals);
55 Options.store(Opts, "CommentStringLiterals", CommentStringLiterals);
56 Options.store(Opts, "CommentUserDefinedLiterals", CommentUserDefinedLiterals);
57 Options.store(Opts, "CommentCharacterLiterals", CommentCharacterLiterals);
58 Options.store(Opts, "CommentNullPtrs", CommentNullPtrs);
59}
60
61void ArgumentCommentCheck::registerMatchers(MatchFinder *Finder) {
62 Finder->addMatcher(
63 callExpr(unless(cxxOperatorCallExpr()), unless(userDefinedLiteral()),
64 // NewCallback's arguments relate to the pointed function,
65 // don't check them against NewCallback's parameter names.
66 // FIXME: Make this configurable.
67 unless(hasDeclaration(functionDecl(
68 hasAnyName("NewCallback", "NewPermanentCallback")))),
69 // Ignore APIs from the standard library, since their names are
70 // not specified by the standard, and standard library
71 // implementations in practice have to use reserved names to
72 // avoid conflicts with same-named macros.
73 unless(hasDeclaration(isFromStdNamespaceOrSystemHeader())))
74 .bind("expr"),
75 this);
76 Finder->addMatcher(cxxConstructExpr(unless(hasDeclaration(
77 isFromStdNamespaceOrSystemHeader())))
78 .bind("expr"),
79 this);
80}
81
82static std::vector<CommentToken> getCommentsBeforeLoc(ASTContext *Ctx,
83 SourceLocation Loc) {
84 std::vector<CommentToken> Comments;
85 while (Loc.isValid()) {
86 const std::optional<Token> Tok = utils::lexer::getPreviousToken(
87 Loc, Ctx->getSourceManager(), Ctx->getLangOpts(),
88 /*SkipComments=*/false);
89 if (!Tok || Tok->isNot(tok::comment))
90 break;
91 Loc = Tok->getLocation();
92 Comments.emplace_back(CommentToken{
93 Loc,
94 Lexer::getSourceText(CharSourceRange::getCharRange(
95 Loc, Loc.getLocWithOffset(Tok->getLength())),
96 Ctx->getSourceManager(), Ctx->getLangOpts()),
97 });
98 }
99 return Comments;
100}
101
102template <typename NamedDeclRange>
103static bool isLikelyTypo(const NamedDeclRange &Candidates, StringRef ArgName,
104 StringRef TargetName) {
105 const std::string ArgNameLowerStr = ArgName.lower();
106 const StringRef ArgNameLower = ArgNameLowerStr;
107 // The threshold is arbitrary.
108 const unsigned UpperBound = ((ArgName.size() + 2) / 3) + 1;
109 const unsigned ThisED =
110 ArgNameLower.edit_distance(TargetName.lower(),
111 /*AllowReplacements=*/true, UpperBound);
112 if (ThisED >= UpperBound)
113 return false;
114
115 return llvm::all_of(Candidates, [&](const auto &Candidate) {
116 const IdentifierInfo *II = Candidate->getIdentifier();
117 if (!II)
118 return true;
119
120 // Skip the target itself.
121 if (II->getName() == TargetName)
122 return true;
123
124 const unsigned Threshold = 2;
125 // Other candidates must be an edit distance at least Threshold more away
126 // from this candidate. This gives us greater confidence that this is a
127 // typo of this candidate and not one with a similar name.
128 const unsigned OtherED = ArgNameLower.edit_distance(
129 II->getName().lower(),
130 /*AllowReplacements=*/true, ThisED + Threshold);
131 return OtherED >= ThisED + Threshold;
132 });
133}
134
135static bool sameName(StringRef InComment, StringRef InDecl, bool StrictMode) {
136 if (StrictMode)
137 return InComment == InDecl;
138 InComment = InComment.trim('_');
139 InDecl = InDecl.trim('_');
140 // FIXME: compare_insensitive only works for ASCII.
141 return InComment.compare_insensitive(InDecl) == 0;
142}
143
144static bool looksLikeExpectMethod(const CXXMethodDecl *Expect) {
145 return Expect != nullptr && Expect->getLocation().isMacroID() &&
146 Expect->getNameInfo().getName().isIdentifier() &&
147 Expect->getName().starts_with("gmock_");
148}
149static bool areMockAndExpectMethods(const CXXMethodDecl *Mock,
150 const CXXMethodDecl *Expect) {
151 assert(looksLikeExpectMethod(Expect));
152 return Mock != nullptr && Mock->getNextDeclInContext() == Expect &&
153 Mock->getNumParams() == Expect->getNumParams() &&
154 Mock->getLocation().isMacroID() &&
155 Mock->getNameInfo().getName().isIdentifier() &&
156 Mock->getName() == Expect->getName().substr(strlen("gmock_"));
157}
158
159// This uses implementation details of MOCK_METHODx_ macros: for each mocked
160// method M it defines M() with appropriate signature and a method used to set
161// up expectations - gmock_M() - with each argument's type changed the
162// corresponding matcher. This function returns M when given either M or
163// gmock_M.
164static const CXXMethodDecl *findMockedMethod(const CXXMethodDecl *Method) {
165 if (looksLikeExpectMethod(Method)) {
166 const DeclContext *Ctx = Method->getDeclContext();
167 if (Ctx == nullptr || !Ctx->isRecord())
168 return nullptr;
169 for (const auto *D : Ctx->decls()) {
170 if (D->getNextDeclInContext() == Method) {
171 const auto *Previous = dyn_cast<CXXMethodDecl>(D);
172 return areMockAndExpectMethods(Previous, Method) ? Previous : nullptr;
173 }
174 }
175 return nullptr;
176 }
177 if (const auto *Next =
178 dyn_cast_or_null<CXXMethodDecl>(Method->getNextDeclInContext())) {
179 if (looksLikeExpectMethod(Next) && areMockAndExpectMethods(Method, Next))
180 return Method;
181 }
182 return nullptr;
183}
184
185// For gmock expectation builder method (the target of the call generated by
186// `EXPECT_CALL(obj, Method(...))`) tries to find the real method being mocked
187// (returns nullptr, if the mock method doesn't override anything). For other
188// functions returns the function itself.
189static const FunctionDecl *resolveMocks(const FunctionDecl *Func) {
190 if (const auto *Method = dyn_cast<CXXMethodDecl>(Func)) {
191 if (const auto *MockedMethod = findMockedMethod(Method)) {
192 // If mocked method overrides the real one, we can use its parameter
193 // names, otherwise we're out of luck.
194 if (MockedMethod->size_overridden_methods() > 0)
195 return *MockedMethod->begin_overridden_methods();
196 return nullptr;
197 }
198 }
199 return Func;
200}
201
202// Given the argument type and the options determine if we should
203// be adding an argument comment.
204bool ArgumentCommentCheck::shouldAddComment(const Expr *Arg) const {
205 Arg = Arg->IgnoreImpCasts();
206 if (isa<UnaryOperator>(Arg))
207 Arg = cast<UnaryOperator>(Arg)->getSubExpr();
208 if (Arg->getExprLoc().isMacroID())
209 return false;
210 return (CommentBoolLiterals && isa<CXXBoolLiteralExpr>(Arg)) ||
211 (CommentIntegerLiterals && isa<IntegerLiteral>(Arg)) ||
212 (CommentFloatLiterals && isa<FloatingLiteral>(Arg)) ||
213 (CommentUserDefinedLiterals && isa<UserDefinedLiteral>(Arg)) ||
214 (CommentCharacterLiterals && isa<CharacterLiteral>(Arg)) ||
215 (CommentStringLiterals && isa<StringLiteral>(Arg)) ||
216 (CommentNullPtrs && isa<CXXNullPtrLiteralExpr>(Arg));
217}
218
219void ArgumentCommentCheck::checkCallArgs(ASTContext *Ctx,
220 const FunctionDecl *OriginalCallee,
221 SourceLocation ArgBeginLoc,
222 llvm::ArrayRef<const Expr *> Args) {
223 const FunctionDecl *Callee = resolveMocks(OriginalCallee);
224 if (!Callee)
225 return;
226
227 Callee = Callee->getFirstDecl();
228 if (const auto *Ctor = dyn_cast<CXXConstructorDecl>(Callee);
229 Ctor && Ctor->isInheritingConstructor()) {
230 if (const auto *BaseCtor = Ctor->getInheritedConstructor().getConstructor())
231 Callee = BaseCtor->getFirstDecl();
232 }
233 const unsigned NumArgs =
234 std::min<unsigned>(Args.size(), Callee->getNumParams());
235 if ((NumArgs == 0) || (IgnoreSingleArgument && NumArgs == 1))
236 return;
237
238 auto MakeFileCharRange = [Ctx](SourceLocation Begin, SourceLocation End) {
239 return Lexer::makeFileCharRange(CharSourceRange::getCharRange(Begin, End),
240 Ctx->getSourceManager(),
241 Ctx->getLangOpts());
242 };
243
244 for (unsigned I = 0; I < NumArgs; ++I) {
245 const ParmVarDecl *PVD = Callee->getParamDecl(I);
246 const IdentifierInfo *II = PVD->getIdentifier();
247 if (!II)
248 continue;
249 if (FunctionDecl *Template = Callee->getTemplateInstantiationPattern()) {
250 // Don't warn on arguments for parameters instantiated from template
251 // parameter packs. If we find more arguments than the template
252 // definition has, it also means that they correspond to a parameter
253 // pack.
254 if (Template->getNumParams() <= I ||
255 Template->getParamDecl(I)->isParameterPack()) {
256 continue;
257 }
258 }
259
260 const CharSourceRange BeforeArgument =
261 MakeFileCharRange(ArgBeginLoc, Args[I]->getBeginLoc());
262 ArgBeginLoc = Args[I]->getEndLoc();
263
264 std::vector<CommentToken> Comments;
265 if (BeforeArgument.isValid()) {
267 BeforeArgument, Ctx->getSourceManager(), Ctx->getLangOpts());
268 } else {
269 // Fall back to parsing back from the start of the argument.
270 const CharSourceRange ArgsRange =
271 MakeFileCharRange(Args[I]->getBeginLoc(), Args[I]->getEndLoc());
272 Comments = getCommentsBeforeLoc(Ctx, ArgsRange.getBegin());
273 }
274
275 for (const auto &Comment : Comments) {
276 llvm::SmallVector<StringRef, 2> Matches;
277 if (IdentRE.match(Comment.Text, &Matches) &&
278 !sameName(Matches[2], II->getName(), StrictMode)) {
279 {
280 const DiagnosticBuilder Diag =
281 diag(Comment.Loc, "argument name '%0' in comment does not "
282 "match parameter name %1")
283 << Matches[2] << II;
284 if (isLikelyTypo(Callee->parameters(), Matches[2], II->getName())) {
285 Diag << FixItHint::CreateReplacement(
286 Comment.Loc, (Matches[1] + II->getName() + Matches[3]).str());
287 }
288 }
289 diag(PVD->getLocation(), "%0 declared here", DiagnosticIDs::Note) << II;
290 if (OriginalCallee != Callee) {
291 diag(OriginalCallee->getLocation(),
292 "actual callee (%0) is declared here", DiagnosticIDs::Note)
293 << OriginalCallee;
294 }
295 }
296 }
297
298 // If the argument comments are missing for literals add them.
299 if (Comments.empty() && shouldAddComment(Args[I])) {
300 llvm::SmallString<32> ArgComment;
301 (llvm::Twine("/*") + II->getName() + "=*/").toStringRef(ArgComment);
302 const DiagnosticBuilder Diag =
303 diag(Args[I]->getBeginLoc(),
304 "argument comment missing for literal argument %0")
305 << II
306 << FixItHint::CreateInsertion(Args[I]->getBeginLoc(), ArgComment);
307 }
308 }
309}
310
311void ArgumentCommentCheck::check(const MatchFinder::MatchResult &Result) {
312 const auto *E = Result.Nodes.getNodeAs<Expr>("expr");
313 if (const auto *Call = dyn_cast<CallExpr>(E)) {
314 const FunctionDecl *Callee = Call->getDirectCallee();
315 if (!Callee)
316 return;
317
318 checkCallArgs(Result.Context, Callee, Call->getCallee()->getEndLoc(),
319 llvm::ArrayRef(Call->getArgs(), Call->getNumArgs()));
320 } else {
321 const auto *Construct = cast<CXXConstructExpr>(E);
322 if (Construct->getNumArgs() > 0 &&
323 Construct->getArg(0)->getSourceRange() == Construct->getSourceRange()) {
324 // Ignore implicit construction.
325 return;
326 }
327 checkCallArgs(
328 Result.Context, Construct->getConstructor(),
329 Construct->getParenOrBraceRange().getBegin(),
330 llvm::ArrayRef(Construct->getArgs(), Construct->getNumArgs()));
331 }
332}
333
334} // 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 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[]