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