brintos

brintos / llvm-project-archived public Read only

0
0
Text · 15.6 KiB · a74ab00 Raw
407 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///  \file This file implements the NoLintDirectiveHandler class, which is used10///  to locate NOLINT comments in the file being analyzed, to decide whether a11///  diagnostic should be suppressed.12///13//===----------------------------------------------------------------------===//14 15#include "NoLintDirectiveHandler.h"16#include "GlobList.h"17#include "clang/Basic/LLVM.h"18#include "clang/Basic/SourceLocation.h"19#include "clang/Basic/SourceManager.h"20#include "clang/Tooling/Core/Diagnostic.h"21#include "llvm/ADT/STLExtras.h"22#include "llvm/ADT/SmallVector.h"23#include "llvm/ADT/StringExtras.h"24#include "llvm/ADT/StringMap.h"25#include "llvm/ADT/StringSwitch.h"26#include <cassert>27#include <cstddef>28#include <memory>29#include <optional>30#include <string>31#include <utility>32 33namespace clang::tidy {34 35//===----------------------------------------------------------------------===//36// NoLintType37//===----------------------------------------------------------------------===//38 39// The type - one of NOLINT[NEXTLINE/BEGIN/END].40enum class NoLintType { NoLint, NoLintNextLine, NoLintBegin, NoLintEnd };41 42// Convert a string like "NOLINTNEXTLINE" to its enum `Type::NoLintNextLine`.43// Return `std::nullopt` if the string is unrecognized.44static std::optional<NoLintType> strToNoLintType(StringRef Str) {45  auto Type = llvm::StringSwitch<std::optional<NoLintType>>(Str)46                  .Case("NOLINT", NoLintType::NoLint)47                  .Case("NOLINTNEXTLINE", NoLintType::NoLintNextLine)48                  .Case("NOLINTBEGIN", NoLintType::NoLintBegin)49                  .Case("NOLINTEND", NoLintType::NoLintEnd)50                  .Default(std::nullopt);51  return Type;52}53 54//===----------------------------------------------------------------------===//55// NoLintToken56//===----------------------------------------------------------------------===//57 58// Whitespace within a NOLINT's check list shall be ignored.59// "NOLINT( check1, check2 )" is equivalent to "NOLINT(check1,check2)".60// Return the check list with all extraneous whitespace removed.61static std::string trimWhitespace(StringRef Checks) {62  SmallVector<StringRef> Split;63  Checks.split(Split, ',');64  for (StringRef &Check : Split)65    Check = Check.trim();66  return llvm::join(Split, ",");67}68 69namespace {70 71// Record the presence of a NOLINT comment - its type, location, checks -72// as parsed from the file's character contents.73class NoLintToken {74public:75  // \param Checks:76  // - If unspecified (i.e. `None`) then ALL checks are suppressed - equivalent77  //   to NOLINT(*).78  // - An empty string means nothing is suppressed - equivalent to NOLINT().79  // - Negative globs ignored (which would effectively disable the suppression).80  NoLintToken(NoLintType Type, size_t Pos,81              const std::optional<StringRef> &Checks)82      : Type(Type), Pos(Pos), ChecksGlob(std::make_unique<CachedGlobList>(83                                  Checks.value_or("*"),84                                  /*KeepNegativeGlobs=*/false)) {85    if (Checks)86      this->Checks = trimWhitespace(*Checks);87  }88 89  // The type - one of NOLINT[NEXTLINE/BEGIN/END].90  NoLintType Type;91 92  // The location of the first character, "N", in "NOLINT".93  size_t Pos;94 95  // A glob of the checks this NOLINT token disables.96  std::unique_ptr<CachedGlobList> ChecksGlob;97 98  // If this NOLINT specifies checks, return the checks.99  const std::optional<std::string> &checks() const { return Checks; }100 101  // Whether this NOLINT applies to the provided check.102  bool suppresses(StringRef Check) const { return ChecksGlob->contains(Check); }103 104private:105  std::optional<std::string> Checks;106};107 108} // namespace109 110// Consume the entire buffer and return all `NoLintToken`s that were found.111static SmallVector<NoLintToken> getNoLints(StringRef Buffer) {112  static constexpr llvm::StringLiteral NOLINT = "NOLINT";113  SmallVector<NoLintToken> NoLints;114 115  size_t Pos = 0;116  while (Pos < Buffer.size()) {117    // Find NOLINT:118    const size_t NoLintPos = Buffer.find(NOLINT, Pos);119    if (NoLintPos == StringRef::npos)120      break; // Buffer exhausted121 122    // Read [A-Z] characters immediately after "NOLINT", e.g. the "NEXTLINE" in123    // "NOLINTNEXTLINE".124    Pos = NoLintPos + NOLINT.size();125    while (Pos < Buffer.size() && llvm::isAlpha(Buffer[Pos]))126      ++Pos;127 128    // Is this a recognized NOLINT type?129    const std::optional<NoLintType> NoLintType =130        strToNoLintType(Buffer.slice(NoLintPos, Pos));131    if (!NoLintType)132      continue;133 134    // Get checks, if specified.135    std::optional<StringRef> Checks;136    if (Pos < Buffer.size() && Buffer[Pos] == '(') {137      const size_t ClosingBracket = Buffer.find_first_of("\n)", ++Pos);138      if (ClosingBracket != StringRef::npos && Buffer[ClosingBracket] == ')') {139        Checks = Buffer.slice(Pos, ClosingBracket);140        Pos = ClosingBracket + 1;141      }142    }143 144    NoLints.emplace_back(*NoLintType, NoLintPos, Checks);145  }146 147  return NoLints;148}149 150//===----------------------------------------------------------------------===//151// NoLintBlockToken152//===----------------------------------------------------------------------===//153 154namespace {155 156// Represents a source range within a pair of NOLINT(BEGIN/END) comments.157class NoLintBlockToken {158public:159  NoLintBlockToken(size_t BeginPos, size_t EndPos,160                   std::unique_ptr<CachedGlobList> ChecksGlob)161      : BeginPos(BeginPos), EndPos(EndPos), ChecksGlob(std::move(ChecksGlob)) {}162 163  // Whether the provided diagnostic is within and is suppressible by this block164  // of NOLINT(BEGIN/END) comments.165  bool suppresses(size_t DiagPos, StringRef DiagName) const {166    return (BeginPos < DiagPos) && (DiagPos < EndPos) &&167           ChecksGlob->contains(DiagName);168  }169 170private:171  size_t BeginPos;172  size_t EndPos;173  std::unique_ptr<CachedGlobList> ChecksGlob;174};175 176} // namespace177 178// Construct a [clang-tidy-nolint] diagnostic to do with the unmatched179// NOLINT(BEGIN/END) pair.180static tooling::Diagnostic makeNoLintError(const SourceManager &SrcMgr,181                                           FileID File,182                                           const NoLintToken &NoLint) {183  tooling::Diagnostic Error;184  Error.DiagLevel = tooling::Diagnostic::Error;185  Error.DiagnosticName = "clang-tidy-nolint";186  const StringRef Message =187      (NoLint.Type == NoLintType::NoLintBegin)188          ? ("unmatched 'NOLINTBEGIN' comment without a subsequent 'NOLINT"189             "END' comment")190          : ("unmatched 'NOLINTEND' comment without a previous 'NOLINT"191             "BEGIN' comment");192  const SourceLocation Loc = SrcMgr.getComposedLoc(File, NoLint.Pos);193  Error.Message = tooling::DiagnosticMessage(Message, SrcMgr, Loc);194  return Error;195}196 197// Match NOLINTBEGINs with their corresponding NOLINTENDs and move them into198// `NoLintBlockToken`s. If any BEGINs or ENDs are left over, a diagnostic is199// written to `NoLintErrors`.200static SmallVector<NoLintBlockToken>201formNoLintBlocks(SmallVector<NoLintToken> NoLints, const SourceManager &SrcMgr,202                 FileID File,203                 SmallVectorImpl<tooling::Diagnostic> &NoLintErrors) {204  SmallVector<NoLintBlockToken> CompletedBlocks;205  SmallVector<NoLintToken> Stack;206 207  // Nested blocks must be fully contained within their parent block. What this208  // means is that when you have a series of nested BEGIN tokens, the END tokens209  // shall appear in the reverse order, starting with the closing of the210  // inner-most block first, then the next level up, and so on. This is211  // essentially a last-in-first-out/stack system.212  for (NoLintToken &NoLint : NoLints) {213    if (NoLint.Type == NoLintType::NoLintBegin)214      // A new block is being started. Add it to the stack.215      Stack.emplace_back(std::move(NoLint));216    else if (NoLint.Type == NoLintType::NoLintEnd) {217      if (!Stack.empty() && Stack.back().checks() == NoLint.checks()) {218        // The previous block is being closed. Pop one element off the stack.219        CompletedBlocks.emplace_back(Stack.back().Pos, NoLint.Pos,220                                     std::move(Stack.back().ChecksGlob));221        Stack.pop_back();222      } else223        // Trying to close the wrong block.224        NoLintErrors.emplace_back(makeNoLintError(SrcMgr, File, NoLint));225    }226  }227 228  for (const NoLintToken &NoLint : Stack)229    NoLintErrors.emplace_back(makeNoLintError(SrcMgr, File, NoLint));230 231  return CompletedBlocks;232}233 234//===----------------------------------------------------------------------===//235// NoLintDirectiveHandler::Impl236//===----------------------------------------------------------------------===//237 238class NoLintDirectiveHandler::Impl {239public:240  bool shouldSuppress(DiagnosticsEngine::Level DiagLevel,241                      const Diagnostic &Diag, StringRef DiagName,242                      SmallVectorImpl<tooling::Diagnostic> &NoLintErrors,243                      bool AllowIO, bool EnableNoLintBlocks);244 245private:246  bool diagHasNoLintInMacro(const Diagnostic &Diag, StringRef DiagName,247                            SmallVectorImpl<tooling::Diagnostic> &NoLintErrors,248                            bool AllowIO, bool EnableNoLintBlocks);249 250  bool diagHasNoLint(StringRef DiagName, SourceLocation DiagLoc,251                     const SourceManager &SrcMgr,252                     SmallVectorImpl<tooling::Diagnostic> &NoLintErrors,253                     bool AllowIO, bool EnableNoLintBlocks);254 255  void generateCache(const SourceManager &SrcMgr, StringRef FileName,256                     FileID File, StringRef Buffer,257                     SmallVectorImpl<tooling::Diagnostic> &NoLintErrors);258 259  llvm::StringMap<SmallVector<NoLintBlockToken>> Cache;260};261 262bool NoLintDirectiveHandler::Impl::shouldSuppress(263    DiagnosticsEngine::Level DiagLevel, const Diagnostic &Diag,264    StringRef DiagName, SmallVectorImpl<tooling::Diagnostic> &NoLintErrors,265    bool AllowIO, bool EnableNoLintBlocks) {266  if (DiagLevel >= DiagnosticsEngine::Error)267    return false;268  return diagHasNoLintInMacro(Diag, DiagName, NoLintErrors, AllowIO,269                              EnableNoLintBlocks);270}271 272// Look at the macro's spelling location for a NOLINT. If none is found, keep273// looking up the call stack.274bool NoLintDirectiveHandler::Impl::diagHasNoLintInMacro(275    const Diagnostic &Diag, StringRef DiagName,276    SmallVectorImpl<tooling::Diagnostic> &NoLintErrors, bool AllowIO,277    bool EnableNoLintBlocks) {278  SourceLocation DiagLoc = Diag.getLocation();279  if (DiagLoc.isInvalid())280    return false;281  const SourceManager &SrcMgr = Diag.getSourceManager();282  while (true) {283    if (diagHasNoLint(DiagName, DiagLoc, SrcMgr, NoLintErrors, AllowIO,284                      EnableNoLintBlocks))285      return true;286    if (!DiagLoc.isMacroID())287      return false;288    DiagLoc = SrcMgr.getImmediateExpansionRange(DiagLoc).getBegin();289  }290  return false;291}292 293// Look behind and ahead for '\n' characters. These mark the start and end of294// this line.295static std::pair<size_t, size_t> getLineStartAndEnd(StringRef Buffer,296                                                    size_t From) {297  const size_t StartPos = Buffer.find_last_of('\n', From) + 1;298  const size_t EndPos = std::min(Buffer.find('\n', From), Buffer.size());299  return {StartPos, EndPos};300}301 302// Whether the line has a NOLINT of type = `Type` that can suppress the303// diagnostic `DiagName`.304static bool lineHasNoLint(StringRef Buffer,305                          std::pair<size_t, size_t> LineStartAndEnd,306                          NoLintType Type, StringRef DiagName) {307  // Get all NOLINTs on the line.308  Buffer = Buffer.slice(LineStartAndEnd.first, LineStartAndEnd.second);309  SmallVector<NoLintToken> NoLints = getNoLints(Buffer);310 311  // Do any of these NOLINTs match the desired type and diag name?312  return llvm::any_of(NoLints, [&](const NoLintToken &NoLint) {313    return NoLint.Type == Type && NoLint.suppresses(DiagName);314  });315}316 317// Whether the provided diagnostic is located within and is suppressible by a318// block of NOLINT(BEGIN/END) comments.319static bool withinNoLintBlock(ArrayRef<NoLintBlockToken> NoLintBlocks,320                              size_t DiagPos, StringRef DiagName) {321  return llvm::any_of(NoLintBlocks, [&](const NoLintBlockToken &NoLintBlock) {322    return NoLintBlock.suppresses(DiagPos, DiagName);323  });324}325 326// Get the file contents as a string.327static std::optional<StringRef> getBuffer(const SourceManager &SrcMgr,328                                          FileID File, bool AllowIO) {329  return AllowIO ? SrcMgr.getBufferDataOrNone(File)330                 : SrcMgr.getBufferDataIfLoaded(File);331}332 333// We will check for NOLINTs and NOLINTNEXTLINEs first. Checking for these is334// not so expensive (just need to parse the current and previous lines). Only if335// that fails do we look for NOLINT(BEGIN/END) blocks (which requires reading336// the entire file).337bool NoLintDirectiveHandler::Impl::diagHasNoLint(338    StringRef DiagName, SourceLocation DiagLoc, const SourceManager &SrcMgr,339    SmallVectorImpl<tooling::Diagnostic> &NoLintErrors, bool AllowIO,340    bool EnableNoLintBlocks) {341  // Translate the diagnostic's SourceLocation to a raw file + offset pair.342  const auto [File, Pos] = SrcMgr.getDecomposedSpellingLoc(DiagLoc);343 344  // We will only see NOLINTs in user-authored sources. No point reading the345  // file if it is a <built-in>.346  std::optional<StringRef> FileName = SrcMgr.getNonBuiltinFilenameForID(File);347  if (!FileName)348    return false;349 350  // Get file contents.351  std::optional<StringRef> Buffer = getBuffer(SrcMgr, File, AllowIO);352  if (!Buffer)353    return false;354 355  // Check if there's a NOLINT on this line.356  auto ThisLine = getLineStartAndEnd(*Buffer, Pos);357  if (lineHasNoLint(*Buffer, ThisLine, NoLintType::NoLint, DiagName))358    return true;359 360  // Check if there's a NOLINTNEXTLINE on the previous line.361  if (ThisLine.first > 0) {362    auto PrevLine = getLineStartAndEnd(*Buffer, ThisLine.first - 1);363    if (lineHasNoLint(*Buffer, PrevLine, NoLintType::NoLintNextLine, DiagName))364      return true;365  }366 367  // Check if this line is within a NOLINT(BEGIN/END) block.368  if (!EnableNoLintBlocks)369    return false;370 371  // Do we have cached NOLINT block locations for this file?372  if (!Cache.contains(*FileName))373    // Warning: heavy operation - need to read entire file.374    generateCache(SrcMgr, *FileName, File, *Buffer, NoLintErrors);375 376  return withinNoLintBlock(Cache[*FileName], Pos, DiagName);377}378 379// Find all NOLINT(BEGIN/END) blocks in a file and store in the cache.380void NoLintDirectiveHandler::Impl::generateCache(381    const SourceManager &SrcMgr, StringRef FileName, FileID File,382    StringRef Buffer, SmallVectorImpl<tooling::Diagnostic> &NoLintErrors) {383  // Read entire file to get all NOLINTs and match each BEGIN with its384  // corresponding END, raising errors for any BEGIN or END that is unmatched.385  Cache.try_emplace(FileName, formNoLintBlocks(getNoLints(Buffer), SrcMgr, File,386                                               NoLintErrors));387}388 389//===----------------------------------------------------------------------===//390// NoLintDirectiveHandler391//===----------------------------------------------------------------------===//392 393NoLintDirectiveHandler::NoLintDirectiveHandler()394    : PImpl(std::make_unique<Impl>()) {}395 396NoLintDirectiveHandler::~NoLintDirectiveHandler() = default;397 398bool NoLintDirectiveHandler::shouldSuppress(399    DiagnosticsEngine::Level DiagLevel, const Diagnostic &Diag,400    StringRef DiagName, SmallVectorImpl<tooling::Diagnostic> &NoLintErrors,401    bool AllowIO, bool EnableNoLintBlocks) {402  return PImpl->shouldSuppress(DiagLevel, Diag, DiagName, NoLintErrors, AllowIO,403                               EnableNoLintBlocks);404}405 406} // namespace clang::tidy407