//===-- clang-tools-extra/clang-tidy/NoLintDirectiveHandler.cpp -----------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
///
///  \file This file implements the NoLintDirectiveHandler class, which is used
///  to locate NOLINT comments in the file being analyzed, to decide whether a
///  diagnostic should be suppressed.
///
//===----------------------------------------------------------------------===//

#include "NoLintDirectiveHandler.h"
#include "GlobList.h"
#include "clang/Basic/LLVM.h"
#include "clang/Basic/SourceLocation.h"
#include "clang/Basic/SourceManager.h"
#include "clang/Tooling/Core/Diagnostic.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/ADT/StringMap.h"
#include "llvm/ADT/StringSwitch.h"
#include <cassert>
#include <cstddef>
#include <iterator>
#include <optional>
#include <string>
#include <tuple>
#include <utility>

namespace clang::tidy {

//===----------------------------------------------------------------------===//
// NoLintType
//===----------------------------------------------------------------------===//

// The type - one of NOLINT[NEXTLINE/BEGIN/END].
enum class NoLintType { NoLint, NoLintNextLine, NoLintBegin, NoLintEnd };

// Convert a string like "NOLINTNEXTLINE" to its enum `Type::NoLintNextLine`.
// Return `std::nullopt` if the string is unrecognized.
static std::optional<NoLintType> strToNoLintType(StringRef Str) {
  auto Type = llvm::StringSwitch<std::optional<NoLintType>>(Str)
                  .Case("NOLINT", NoLintType::NoLint)
                  .Case("NOLINTNEXTLINE", NoLintType::NoLintNextLine)
                  .Case("NOLINTBEGIN", NoLintType::NoLintBegin)
                  .Case("NOLINTEND", NoLintType::NoLintEnd)
                  .Default(std::nullopt);
  return Type;
}

//===----------------------------------------------------------------------===//
// NoLintToken
//===----------------------------------------------------------------------===//

// Whitespace within a NOLINT's check list shall be ignored.
// "NOLINT( check1, check2 )" is equivalent to "NOLINT(check1,check2)".
// Return the check list with all extraneous whitespace removed.
static std::string trimWhitespace(StringRef Checks) {
  SmallVector<StringRef> Split;
  Checks.split(Split, ',');
  for (StringRef &Check : Split)
    Check = Check.trim();
  return llvm::join(Split, ",");
}

namespace {

// Record the presence of a NOLINT comment - its type, location, checks -
// as parsed from the file's character contents.
class NoLintToken {
public:
  // \param Checks:
  // - If unspecified (i.e. `None`) then ALL checks are suppressed - equivalent
  //   to NOLINT(*).
  // - An empty string means nothing is suppressed - equivalent to NOLINT().
  // - Negative globs ignored (which would effectively disable the suppression).
  NoLintToken(NoLintType Type, size_t Pos,
              const std::optional<std::string> &Checks)
      : Type(Type), Pos(Pos), ChecksGlob(std::make_unique<CachedGlobList>(
                                  Checks.value_or("*"),
                                  /*KeepNegativeGlobs=*/false)) {
    if (Checks)
      this->Checks = trimWhitespace(*Checks);
  }

  // The type - one of NOLINT[NEXTLINE/BEGIN/END].
  NoLintType Type;

  // The location of the first character, "N", in "NOLINT".
  size_t Pos;

  // If this NOLINT specifies checks, return the checks.
  std::optional<std::string> checks() const { return Checks; }

  // Whether this NOLINT applies to the provided check.
  bool suppresses(StringRef Check) const { return ChecksGlob->contains(Check); }

private:
  std::optional<std::string> Checks;
  std::unique_ptr<CachedGlobList> ChecksGlob;
};

} // namespace

// Consume the entire buffer and return all `NoLintToken`s that were found.
static SmallVector<NoLintToken> getNoLints(StringRef Buffer) {
  static constexpr llvm::StringLiteral NOLINT = "NOLINT";
  SmallVector<NoLintToken> NoLints;

  size_t Pos = 0;
  while (Pos < Buffer.size()) {
    // Find NOLINT:
    const size_t NoLintPos = Buffer.find(NOLINT, Pos);
    if (NoLintPos == StringRef::npos)
      break; // Buffer exhausted

    // Read [A-Z] characters immediately after "NOLINT", e.g. the "NEXTLINE" in
    // "NOLINTNEXTLINE".
    Pos = NoLintPos + NOLINT.size();
    while (Pos < Buffer.size() && llvm::isAlpha(Buffer[Pos]))
      ++Pos;

    // Is this a recognized NOLINT type?
    const std::optional<NoLintType> NoLintType =
        strToNoLintType(Buffer.slice(NoLintPos, Pos));
    if (!NoLintType)
      continue;

    // Get checks, if specified.
    std::optional<std::string> Checks;
    if (Pos < Buffer.size() && Buffer[Pos] == '(') {
      size_t ClosingBracket = Buffer.find_first_of("\n)", ++Pos);
      if (ClosingBracket != StringRef::npos && Buffer[ClosingBracket] == ')') {
        Checks = Buffer.slice(Pos, ClosingBracket).str();
        Pos = ClosingBracket + 1;
      }
    }

    NoLints.emplace_back(*NoLintType, NoLintPos, Checks);
  }

  return NoLints;
}

//===----------------------------------------------------------------------===//
// NoLintBlockToken
//===----------------------------------------------------------------------===//

namespace {

// Represents a source range within a pair of NOLINT(BEGIN/END) comments.
class NoLintBlockToken {
public:
  NoLintBlockToken(NoLintToken Begin, const NoLintToken &End)
      : Begin(std::move(Begin)), EndPos(End.Pos) {
    assert(this->Begin.Type == NoLintType::NoLintBegin);
    assert(End.Type == NoLintType::NoLintEnd);
    assert(this->Begin.Pos < End.Pos);
    assert(this->Begin.checks() == End.checks());
  }

  // Whether the provided diagnostic is within and is suppressible by this block
  // of NOLINT(BEGIN/END) comments.
  bool suppresses(size_t DiagPos, StringRef DiagName) const {
    return (Begin.Pos < DiagPos) && (DiagPos < EndPos) &&
           Begin.suppresses(DiagName);
  }

private:
  NoLintToken Begin;
  size_t EndPos;
};

} // namespace

// Match NOLINTBEGINs with their corresponding NOLINTENDs and move them into
// `NoLintBlockToken`s. If any BEGINs or ENDs are left over, they are moved to
// `UnmatchedTokens`.
static SmallVector<NoLintBlockToken>
formNoLintBlocks(SmallVector<NoLintToken> NoLints,
                 SmallVectorImpl<NoLintToken> &UnmatchedTokens) {
  SmallVector<NoLintBlockToken> CompletedBlocks;
  SmallVector<NoLintToken> Stack;

  // Nested blocks must be fully contained within their parent block. What this
  // means is that when you have a series of nested BEGIN tokens, the END tokens
  // shall appear in the reverse order, starting with the closing of the
  // inner-most block first, then the next level up, and so on. This is
  // essentially a last-in-first-out/stack system.
  for (NoLintToken &NoLint : NoLints) {
    if (NoLint.Type == NoLintType::NoLintBegin)
      // A new block is being started. Add it to the stack.
      Stack.emplace_back(std::move(NoLint));
    else if (NoLint.Type == NoLintType::NoLintEnd) {
      if (!Stack.empty() && Stack.back().checks() == NoLint.checks())
        // The previous block is being closed. Pop one element off the stack.
        CompletedBlocks.emplace_back(Stack.pop_back_val(), NoLint);
      else
        // Trying to close the wrong block.
        UnmatchedTokens.emplace_back(std::move(NoLint));
    }
  }

  llvm::move(Stack, std::back_inserter(UnmatchedTokens));
  return CompletedBlocks;
}

//===----------------------------------------------------------------------===//
// NoLintDirectiveHandler::Impl
//===----------------------------------------------------------------------===//

class NoLintDirectiveHandler::Impl {
public:
  bool shouldSuppress(DiagnosticsEngine::Level DiagLevel,
                      const Diagnostic &Diag, StringRef DiagName,
                      SmallVectorImpl<tooling::Diagnostic> &NoLintErrors,
                      bool AllowIO, bool EnableNoLintBlocks);

private:
  bool diagHasNoLintInMacro(const Diagnostic &Diag, StringRef DiagName,
                            SmallVectorImpl<tooling::Diagnostic> &NoLintErrors,
                            bool AllowIO, bool EnableNoLintBlocks);

  bool diagHasNoLint(StringRef DiagName, SourceLocation DiagLoc,
                     const SourceManager &SrcMgr,
                     SmallVectorImpl<tooling::Diagnostic> &NoLintErrors,
                     bool AllowIO, bool EnableNoLintBlocks);

  void generateCache(const SourceManager &SrcMgr, StringRef FileName,
                     FileID File, StringRef Buffer,
                     SmallVectorImpl<tooling::Diagnostic> &NoLintErrors);

  llvm::StringMap<SmallVector<NoLintBlockToken>> Cache;
};

bool NoLintDirectiveHandler::Impl::shouldSuppress(
    DiagnosticsEngine::Level DiagLevel, const Diagnostic &Diag,
    StringRef DiagName, SmallVectorImpl<tooling::Diagnostic> &NoLintErrors,
    bool AllowIO, bool EnableNoLintBlocks) {
  if (DiagLevel >= DiagnosticsEngine::Error)
    return false;
  return diagHasNoLintInMacro(Diag, DiagName, NoLintErrors, AllowIO,
                              EnableNoLintBlocks);
}

// Look at the macro's spelling location for a NOLINT. If none is found, keep
// looking up the call stack.
bool NoLintDirectiveHandler::Impl::diagHasNoLintInMacro(
    const Diagnostic &Diag, StringRef DiagName,
    SmallVectorImpl<tooling::Diagnostic> &NoLintErrors, bool AllowIO,
    bool EnableNoLintBlocks) {
  SourceLocation DiagLoc = Diag.getLocation();
  if (DiagLoc.isInvalid())
    return false;
  const SourceManager &SrcMgr = Diag.getSourceManager();
  while (true) {
    if (diagHasNoLint(DiagName, DiagLoc, SrcMgr, NoLintErrors, AllowIO,
                      EnableNoLintBlocks))
      return true;
    if (!DiagLoc.isMacroID())
      return false;
    DiagLoc = SrcMgr.getImmediateExpansionRange(DiagLoc).getBegin();
  }
  return false;
}

// Look behind and ahead for '\n' characters. These mark the start and end of
// this line.
static std::pair<size_t, size_t> getLineStartAndEnd(StringRef Buffer,
                                                    size_t From) {
  size_t StartPos = Buffer.find_last_of('\n', From) + 1;
  size_t EndPos = std::min(Buffer.find('\n', From), Buffer.size());
  return std::make_pair(StartPos, EndPos);
}

// Whether the line has a NOLINT of type = `Type` that can suppress the
// diagnostic `DiagName`.
static bool lineHasNoLint(StringRef Buffer,
                          std::pair<size_t, size_t> LineStartAndEnd,
                          NoLintType Type, StringRef DiagName) {
  // Get all NOLINTs on the line.
  Buffer = Buffer.slice(LineStartAndEnd.first, LineStartAndEnd.second);
  SmallVector<NoLintToken> NoLints = getNoLints(Buffer);

  // Do any of these NOLINTs match the desired type and diag name?
  return llvm::any_of(NoLints, [&](const NoLintToken &NoLint) {
    return NoLint.Type == Type && NoLint.suppresses(DiagName);
  });
}

// Whether the provided diagnostic is located within and is suppressible by a
// block of NOLINT(BEGIN/END) comments.
static bool withinNoLintBlock(ArrayRef<NoLintBlockToken> NoLintBlocks,
                              size_t DiagPos, StringRef DiagName) {
  return llvm::any_of(NoLintBlocks, [&](const NoLintBlockToken &NoLintBlock) {
    return NoLintBlock.suppresses(DiagPos, DiagName);
  });
}

// Get the file contents as a string.
static std::optional<StringRef> getBuffer(const SourceManager &SrcMgr,
                                          FileID File, bool AllowIO) {
  return AllowIO ? SrcMgr.getBufferDataOrNone(File)
                 : SrcMgr.getBufferDataIfLoaded(File);
}

// We will check for NOLINTs and NOLINTNEXTLINEs first. Checking for these is
// not so expensive (just need to parse the current and previous lines). Only if
// that fails do we look for NOLINT(BEGIN/END) blocks (which requires reading
// the entire file).
bool NoLintDirectiveHandler::Impl::diagHasNoLint(
    StringRef DiagName, SourceLocation DiagLoc, const SourceManager &SrcMgr,
    SmallVectorImpl<tooling::Diagnostic> &NoLintErrors, bool AllowIO,
    bool EnableNoLintBlocks) {
  // Translate the diagnostic's SourceLocation to a raw file + offset pair.
  FileID File;
  unsigned int Pos = 0;
  std::tie(File, Pos) = SrcMgr.getDecomposedSpellingLoc(DiagLoc);

  // We will only see NOLINTs in user-authored sources. No point reading the
  // file if it is a <built-in>.
  std::optional<StringRef> FileName = SrcMgr.getNonBuiltinFilenameForID(File);
  if (!FileName)
    return false;

  // Get file contents.
  std::optional<StringRef> Buffer = getBuffer(SrcMgr, File, AllowIO);
  if (!Buffer)
    return false;

  // Check if there's a NOLINT on this line.
  auto ThisLine = getLineStartAndEnd(*Buffer, Pos);
  if (lineHasNoLint(*Buffer, ThisLine, NoLintType::NoLint, DiagName))
    return true;

  // Check if there's a NOLINTNEXTLINE on the previous line.
  if (ThisLine.first > 0) {
    auto PrevLine = getLineStartAndEnd(*Buffer, ThisLine.first - 1);
    if (lineHasNoLint(*Buffer, PrevLine, NoLintType::NoLintNextLine, DiagName))
      return true;
  }

  // Check if this line is within a NOLINT(BEGIN/END) block.
  if (!EnableNoLintBlocks)
    return false;

  // Do we have cached NOLINT block locations for this file?
  if (Cache.count(*FileName) == 0)
    // Warning: heavy operation - need to read entire file.
    generateCache(SrcMgr, *FileName, File, *Buffer, NoLintErrors);

  return withinNoLintBlock(Cache[*FileName], Pos, DiagName);
}

// Construct a [clang-tidy-nolint] diagnostic to do with the unmatched
// NOLINT(BEGIN/END) pair.
static tooling::Diagnostic makeNoLintError(const SourceManager &SrcMgr,
                                           FileID File,
                                           const NoLintToken &NoLint) {
  tooling::Diagnostic Error;
  Error.DiagLevel = tooling::Diagnostic::Error;
  Error.DiagnosticName = "clang-tidy-nolint";
  StringRef Message =
      (NoLint.Type == NoLintType::NoLintBegin)
          ? ("unmatched 'NOLINTBEGIN' comment without a subsequent 'NOLINT"
             "END' comment")
          : ("unmatched 'NOLINTEND' comment without a previous 'NOLINT"
             "BEGIN' comment");
  SourceLocation Loc = SrcMgr.getComposedLoc(File, NoLint.Pos);
  Error.Message = tooling::DiagnosticMessage(Message, SrcMgr, Loc);
  return Error;
}

// Find all NOLINT(BEGIN/END) blocks in a file and store in the cache.
void NoLintDirectiveHandler::Impl::generateCache(
    const SourceManager &SrcMgr, StringRef FileName, FileID File,
    StringRef Buffer, SmallVectorImpl<tooling::Diagnostic> &NoLintErrors) {
  // Read entire file to get all NOLINTs.
  SmallVector<NoLintToken> NoLints = getNoLints(Buffer);

  // Match each BEGIN with its corresponding END.
  SmallVector<NoLintToken> UnmatchedTokens;
  Cache[FileName] = formNoLintBlocks(std::move(NoLints), UnmatchedTokens);

  // Raise error for any BEGIN/END left over.
  for (const NoLintToken &NoLint : UnmatchedTokens)
    NoLintErrors.emplace_back(makeNoLintError(SrcMgr, File, NoLint));
}

//===----------------------------------------------------------------------===//
// NoLintDirectiveHandler
//===----------------------------------------------------------------------===//

NoLintDirectiveHandler::NoLintDirectiveHandler()
    : PImpl(std::make_unique<Impl>()) {}

NoLintDirectiveHandler::~NoLintDirectiveHandler() = default;

bool NoLintDirectiveHandler::shouldSuppress(
    DiagnosticsEngine::Level DiagLevel, const Diagnostic &Diag,
    StringRef DiagName, SmallVectorImpl<tooling::Diagnostic> &NoLintErrors,
    bool AllowIO, bool EnableNoLintBlocks) {
  return PImpl->shouldSuppress(DiagLevel, Diag, DiagName, NoLintErrors, AllowIO,
                               EnableNoLintBlocks);
}

} // namespace clang::tidy
