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