brintos

brintos / llvm-project-archived public Read only

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