clang-tools  8.0.0
ArgumentCommentCheck.cpp
Go to the documentation of this file.
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 
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 = utils::lexer::getPreviousToken(
95  Loc, Ctx->getSourceManager(), Ctx->getLangOpts(),
96  /*SkipComments=*/false);
97  if (Tok.isNot(tok::comment))
98  break;
99  Loc = Tok.getLocation();
100  Comments.emplace_back(
101  Loc,
102  Lexer::getSourceText(CharSourceRange::getCharRange(
103  Loc, Loc.getLocWithOffset(Tok.getLength())),
104  Ctx->getSourceManager(), Ctx->getLangOpts()));
105  }
106  return Comments;
107 }
108 
109 static bool isLikelyTypo(llvm::ArrayRef<ParmVarDecl *> Params,
110  StringRef ArgName, unsigned ArgIndex) {
111  std::string ArgNameLowerStr = ArgName.lower();
112  StringRef ArgNameLower = ArgNameLowerStr;
113  // The threshold is arbitrary.
114  unsigned UpperBound = (ArgName.size() + 2) / 3 + 1;
115  unsigned ThisED = ArgNameLower.edit_distance(
116  Params[ArgIndex]->getIdentifier()->getName().lower(),
117  /*AllowReplacements=*/true, UpperBound);
118  if (ThisED >= UpperBound)
119  return false;
120 
121  for (unsigned I = 0, E = Params.size(); I != E; ++I) {
122  if (I == ArgIndex)
123  continue;
124  IdentifierInfo *II = Params[I]->getIdentifier();
125  if (!II)
126  continue;
127 
128  const unsigned Threshold = 2;
129  // Other parameters must be an edit distance at least Threshold more away
130  // from this parameter. This gives us greater confidence that this is a typo
131  // of this parameter and not one with a similar name.
132  unsigned OtherED = ArgNameLower.edit_distance(II->getName().lower(),
133  /*AllowReplacements=*/true,
134  ThisED + Threshold);
135  if (OtherED < ThisED + Threshold)
136  return false;
137  }
138 
139  return true;
140 }
141 
142 static bool sameName(StringRef InComment, StringRef InDecl, bool StrictMode) {
143  if (StrictMode)
144  return InComment == InDecl;
145  InComment = InComment.trim('_');
146  InDecl = InDecl.trim('_');
147  // FIXME: compare_lower only works for ASCII.
148  return InComment.compare_lower(InDecl) == 0;
149 }
150 
151 static bool looksLikeExpectMethod(const CXXMethodDecl *Expect) {
152  return Expect != nullptr && Expect->getLocation().isMacroID() &&
153  Expect->getNameInfo().getName().isIdentifier() &&
154  Expect->getName().startswith("gmock_");
155 }
156 static bool areMockAndExpectMethods(const CXXMethodDecl *Mock,
157  const CXXMethodDecl *Expect) {
158  assert(looksLikeExpectMethod(Expect));
159  return Mock != nullptr && Mock->getNextDeclInContext() == Expect &&
160  Mock->getNumParams() == Expect->getNumParams() &&
161  Mock->getLocation().isMacroID() &&
162  Mock->getNameInfo().getName().isIdentifier() &&
163  Mock->getName() == Expect->getName().substr(strlen("gmock_"));
164 }
165 
166 // This uses implementation details of MOCK_METHODx_ macros: for each mocked
167 // method M it defines M() with appropriate signature and a method used to set
168 // up expectations - gmock_M() - with each argument's type changed the
169 // corresponding matcher. This function returns M when given either M or
170 // gmock_M.
171 static const CXXMethodDecl *findMockedMethod(const CXXMethodDecl *Method) {
172  if (looksLikeExpectMethod(Method)) {
173  const DeclContext *Ctx = Method->getDeclContext();
174  if (Ctx == nullptr || !Ctx->isRecord())
175  return nullptr;
176  for (const auto *D : Ctx->decls()) {
177  if (D->getNextDeclInContext() == Method) {
178  const auto *Previous = dyn_cast<CXXMethodDecl>(D);
179  return areMockAndExpectMethods(Previous, Method) ? Previous : nullptr;
180  }
181  }
182  return nullptr;
183  }
184  if (const auto *Next = dyn_cast_or_null<CXXMethodDecl>(
185  Method->getNextDeclInContext())) {
186  if (looksLikeExpectMethod(Next) && areMockAndExpectMethods(Method, Next))
187  return Method;
188  }
189  return nullptr;
190 }
191 
192 // For gmock expectation builder method (the target of the call generated by
193 // `EXPECT_CALL(obj, Method(...))`) tries to find the real method being mocked
194 // (returns nullptr, if the mock method doesn't override anything). For other
195 // functions returns the function itself.
196 static const FunctionDecl *resolveMocks(const FunctionDecl *Func) {
197  if (const auto *Method = dyn_cast<CXXMethodDecl>(Func)) {
198  if (const auto *MockedMethod = findMockedMethod(Method)) {
199  // If mocked method overrides the real one, we can use its parameter
200  // names, otherwise we're out of luck.
201  if (MockedMethod->size_overridden_methods() > 0) {
202  return *MockedMethod->begin_overridden_methods();
203  }
204  return nullptr;
205  }
206  }
207  return Func;
208 }
209 
210 void ArgumentCommentCheck::checkCallArgs(ASTContext *Ctx,
211  const FunctionDecl *OriginalCallee,
212  SourceLocation ArgBeginLoc,
213  llvm::ArrayRef<const Expr *> Args) {
214  const FunctionDecl *Callee = resolveMocks(OriginalCallee);
215  if (!Callee)
216  return;
217 
218  Callee = Callee->getFirstDecl();
219  unsigned NumArgs = std::min<unsigned>(Args.size(), Callee->getNumParams());
220  if (NumArgs == 0)
221  return;
222 
223  auto makeFileCharRange = [Ctx](SourceLocation Begin, SourceLocation End) {
224  return Lexer::makeFileCharRange(CharSourceRange::getCharRange(Begin, End),
225  Ctx->getSourceManager(),
226  Ctx->getLangOpts());
227  };
228 
229  for (unsigned I = 0; I < NumArgs; ++I) {
230  const ParmVarDecl *PVD = Callee->getParamDecl(I);
231  IdentifierInfo *II = PVD->getIdentifier();
232  if (!II)
233  continue;
234  if (auto Template = Callee->getTemplateInstantiationPattern()) {
235  // Don't warn on arguments for parameters instantiated from template
236  // parameter packs. If we find more arguments than the template
237  // definition has, it also means that they correspond to a parameter
238  // pack.
239  if (Template->getNumParams() <= I ||
240  Template->getParamDecl(I)->isParameterPack()) {
241  continue;
242  }
243  }
244 
245  CharSourceRange BeforeArgument =
246  makeFileCharRange(ArgBeginLoc, Args[I]->getBeginLoc());
247  ArgBeginLoc = Args[I]->getEndLoc();
248 
249  std::vector<std::pair<SourceLocation, StringRef>> Comments;
250  if (BeforeArgument.isValid()) {
251  Comments = getCommentsInRange(Ctx, BeforeArgument);
252  } else {
253  // Fall back to parsing back from the start of the argument.
254  CharSourceRange ArgsRange = makeFileCharRange(
255  Args[I]->getBeginLoc(), Args[NumArgs - 1]->getEndLoc());
256  Comments = getCommentsBeforeLoc(Ctx, ArgsRange.getBegin());
257  }
258 
259  for (auto Comment : Comments) {
260  llvm::SmallVector<StringRef, 2> Matches;
261  if (IdentRE.match(Comment.second, &Matches) &&
262  !sameName(Matches[2], II->getName(), StrictMode)) {
263  {
264  DiagnosticBuilder Diag =
265  diag(Comment.first, "argument name '%0' in comment does not "
266  "match parameter name %1")
267  << Matches[2] << II;
268  if (isLikelyTypo(Callee->parameters(), Matches[2], I)) {
269  Diag << FixItHint::CreateReplacement(
270  Comment.first, (Matches[1] + II->getName() + Matches[3]).str());
271  }
272  }
273  diag(PVD->getLocation(), "%0 declared here", DiagnosticIDs::Note) << II;
274  if (OriginalCallee != Callee) {
275  diag(OriginalCallee->getLocation(),
276  "actual callee (%0) is declared here", DiagnosticIDs::Note)
277  << OriginalCallee;
278  }
279  }
280  }
281  }
282 }
283 
284 void ArgumentCommentCheck::check(const MatchFinder::MatchResult &Result) {
285  const auto *E = Result.Nodes.getNodeAs<Expr>("expr");
286  if (const auto *Call = dyn_cast<CallExpr>(E)) {
287  const FunctionDecl *Callee = Call->getDirectCallee();
288  if (!Callee)
289  return;
290 
291  checkCallArgs(Result.Context, Callee, Call->getCallee()->getEndLoc(),
292  llvm::makeArrayRef(Call->getArgs(), Call->getNumArgs()));
293  } else {
294  const auto *Construct = cast<CXXConstructExpr>(E);
295  if (Construct->getNumArgs() == 1 &&
296  Construct->getArg(0)->getSourceRange() == Construct->getSourceRange()) {
297  // Ignore implicit construction.
298  return;
299  }
300  checkCallArgs(
301  Result.Context, Construct->getConstructor(),
302  Construct->getParenOrBraceRange().getBegin(),
303  llvm::makeArrayRef(Construct->getArgs(), Construct->getNumArgs()));
304  }
305 }
306 
307 } // namespace bugprone
308 } // namespace tidy
309 } // namespace clang
SourceLocation Loc
&#39;#&#39; location in the include directive
void storeOptions(ClangTidyOptions::OptionMap &Opts) override
Should store all options supported by this check with their current values or default values for opti...
void store(ClangTidyOptions::OptionMap &Options, StringRef LocalName, StringRef Value) const
Stores an option with the check-local name LocalName with string value Value to Options.
Definition: ClangTidy.cpp:473
static bool looksLikeExpectMethod(const CXXMethodDecl *Expect)
static const FunctionDecl * resolveMocks(const FunctionDecl *Func)
Token getPreviousToken(SourceLocation Location, const SourceManager &SM, const LangOptions &LangOpts, bool SkipComments)
Returns previous token or tok::unknown if not found.
Definition: LexerUtils.cpp:17
static std::vector< std::pair< SourceLocation, StringRef > > getCommentsBeforeLoc(ASTContext *Ctx, SourceLocation Loc)
void check(const ast_matchers::MatchFinder::MatchResult &Result) override
ClangTidyChecks that register ASTMatchers should do the actual work in here.
Base class for all clang-tidy checks.
Definition: ClangTidy.h:127
static bool areMockAndExpectMethods(const CXXMethodDecl *Mock, const CXXMethodDecl *Expect)
static bool isLikelyTypo(llvm::ArrayRef< ParmVarDecl *> Params, StringRef ArgName, unsigned ArgIndex)
static std::vector< std::pair< SourceLocation, StringRef > > getCommentsInRange(ASTContext *Ctx, CharSourceRange Range)
Context Ctx
void registerMatchers(ast_matchers::MatchFinder *Finder) override
Override this to register AST matchers with Finder.
static constexpr llvm::StringLiteral Name
std::map< std::string, std::string > OptionMap
const Decl * D
Definition: XRefs.cpp:79
llvm::Optional< llvm::Expected< tooling::AtomicChanges > > Result
static char lower(char C)
Definition: FuzzyMatch.cpp:69
static bool sameName(StringRef InComment, StringRef InDecl, bool StrictMode)
===– Representation.cpp - ClangDoc Representation --------—*- C++ -*-===//
static const CXXMethodDecl * findMockedMethod(const CXXMethodDecl *Method)
CharSourceRange Range
SourceRange for the file name.
Every ClangTidyCheck reports errors through a DiagnosticsEngine provided by this context.
DiagnosticBuilder diag(SourceLocation Loc, StringRef Description, DiagnosticIDs::Level Level=DiagnosticIDs::Warning)
Add a diagnostic with the check&#39;s name.
Definition: ClangTidy.cpp:438