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 | |
17 | using namespace clang::ast_matchers; |
18 | |
19 | namespace clang::tidy::bugprone { |
20 | namespace { |
21 | AST_MATCHER(Decl, ) { |
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 | |
32 | 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 | |
47 | void ArgumentCommentCheck::(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 | |
59 | void ArgumentCommentCheck::(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 | |
80 | static std::vector<std::pair<SourceLocation, StringRef>> |
81 | (ASTContext *Ctx, CharSourceRange Range) { |
82 | std::vector<std::pair<SourceLocation, StringRef>> ; |
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> = |
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 | |
124 | static std::vector<std::pair<SourceLocation, StringRef>> |
125 | (ASTContext *Ctx, SourceLocation Loc) { |
126 | std::vector<std::pair<SourceLocation, StringRef>> ; |
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 | |
143 | static 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 | |
176 | static bool sameName(StringRef , 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 | |
185 | static bool looksLikeExpectMethod(const CXXMethodDecl *Expect) { |
186 | return Expect != nullptr && Expect->getLocation().isMacroID() && |
187 | Expect->getNameInfo().getName().isIdentifier() && |
188 | Expect->getName().starts_with("gmock_" ); |
189 | } |
190 | static 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. |
205 | static 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. |
230 | static 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. |
246 | bool ArgumentCommentCheck::(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 | |
261 | void ArgumentCommentCheck::(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>> ; |
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 : 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 = |
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 | |
346 | void ArgumentCommentCheck::(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 | |