//===-- 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 #include #include #include #include #include #include #include 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 strToNoLintType(StringRef Str) { auto Type = llvm::StringSwitch>(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 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 &Checks) : Type(Type), Pos(Pos), ChecksGlob(std::make_unique( 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 checks() const { return Checks; } // Whether this NOLINT applies to the provided check. bool suppresses(StringRef Check) const { return ChecksGlob->contains(Check); } private: std::optional Checks; std::unique_ptr ChecksGlob; }; } // namespace // Consume the entire buffer and return all `NoLintToken`s that were found. static SmallVector getNoLints(StringRef Buffer) { static constexpr llvm::StringLiteral NOLINT = "NOLINT"; SmallVector 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 = strToNoLintType(Buffer.slice(NoLintPos, Pos)); if (!NoLintType) continue; // Get checks, if specified. std::optional 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 formNoLintBlocks(SmallVector NoLints, SmallVectorImpl &UnmatchedTokens) { SmallVector CompletedBlocks; SmallVector 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 &NoLintErrors, bool AllowIO, bool EnableNoLintBlocks); private: bool diagHasNoLintInMacro(const Diagnostic &Diag, StringRef DiagName, SmallVectorImpl &NoLintErrors, bool AllowIO, bool EnableNoLintBlocks); bool diagHasNoLint(StringRef DiagName, SourceLocation DiagLoc, const SourceManager &SrcMgr, SmallVectorImpl &NoLintErrors, bool AllowIO, bool EnableNoLintBlocks); void generateCache(const SourceManager &SrcMgr, StringRef FileName, FileID File, StringRef Buffer, SmallVectorImpl &NoLintErrors); llvm::StringMap> Cache; }; bool NoLintDirectiveHandler::Impl::shouldSuppress( DiagnosticsEngine::Level DiagLevel, const Diagnostic &Diag, StringRef DiagName, SmallVectorImpl &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 &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 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 LineStartAndEnd, NoLintType Type, StringRef DiagName) { // Get all NOLINTs on the line. Buffer = Buffer.slice(LineStartAndEnd.first, LineStartAndEnd.second); SmallVector 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 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 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 &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 . std::optional FileName = SrcMgr.getNonBuiltinFilenameForID(File); if (!FileName) return false; // Get file contents. std::optional 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 &NoLintErrors) { // Read entire file to get all NOLINTs. SmallVector NoLints = getNoLints(Buffer); // Match each BEGIN with its corresponding END. SmallVector 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()) {} NoLintDirectiveHandler::~NoLintDirectiveHandler() = default; bool NoLintDirectiveHandler::shouldSuppress( DiagnosticsEngine::Level DiagLevel, const Diagnostic &Diag, StringRef DiagName, SmallVectorImpl &NoLintErrors, bool AllowIO, bool EnableNoLintBlocks) { return PImpl->shouldSuppress(DiagLevel, Diag, DiagName, NoLintErrors, AllowIO, EnableNoLintBlocks); } } // namespace clang::tidy