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