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