123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413 |
- //===-- 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/ArrayRef.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 <type_traits>
- #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
|