| //===-- 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 |