NoLintDirectiveHandler.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. //===-- clang-tools-extra/clang-tidy/NoLintDirectiveHandler.cpp -----------===//
  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-exception
  6. //
  7. //===----------------------------------------------------------------------===//
  8. ///
  9. /// \file This file implements the NoLintDirectiveHandler class, which is used
  10. /// to locate NOLINT comments in the file being analyzed, to decide whether a
  11. /// diagnostic should be suppressed.
  12. ///
  13. //===----------------------------------------------------------------------===//
  14. #include "NoLintDirectiveHandler.h"
  15. #include "GlobList.h"
  16. #include "clang/Basic/LLVM.h"
  17. #include "clang/Basic/SourceLocation.h"
  18. #include "clang/Basic/SourceManager.h"
  19. #include "clang/Tooling/Core/Diagnostic.h"
  20. #include "llvm/ADT/ArrayRef.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 <iterator>
  29. #include <optional>
  30. #include <string>
  31. #include <tuple>
  32. #include <type_traits>
  33. #include <utility>
  34. namespace clang::tidy {
  35. //===----------------------------------------------------------------------===//
  36. // NoLintType
  37. //===----------------------------------------------------------------------===//
  38. // The type - one of NOLINT[NEXTLINE/BEGIN/END].
  39. enum class NoLintType { NoLint, NoLintNextLine, NoLintBegin, NoLintEnd };
  40. // Convert a string like "NOLINTNEXTLINE" to its enum `Type::NoLintNextLine`.
  41. // Return `std::nullopt` if the string is unrecognized.
  42. static std::optional<NoLintType> strToNoLintType(StringRef Str) {
  43. auto Type = llvm::StringSwitch<std::optional<NoLintType>>(Str)
  44. .Case("NOLINT", NoLintType::NoLint)
  45. .Case("NOLINTNEXTLINE", NoLintType::NoLintNextLine)
  46. .Case("NOLINTBEGIN", NoLintType::NoLintBegin)
  47. .Case("NOLINTEND", NoLintType::NoLintEnd)
  48. .Default(std::nullopt);
  49. return Type;
  50. }
  51. //===----------------------------------------------------------------------===//
  52. // NoLintToken
  53. //===----------------------------------------------------------------------===//
  54. // Whitespace within a NOLINT's check list shall be ignored.
  55. // "NOLINT( check1, check2 )" is equivalent to "NOLINT(check1,check2)".
  56. // Return the check list with all extraneous whitespace removed.
  57. static std::string trimWhitespace(StringRef Checks) {
  58. SmallVector<StringRef> Split;
  59. Checks.split(Split, ',');
  60. for (StringRef &Check : Split)
  61. Check = Check.trim();
  62. return llvm::join(Split, ",");
  63. }
  64. namespace {
  65. // Record the presence of a NOLINT comment - its type, location, checks -
  66. // as parsed from the file's character contents.
  67. class NoLintToken {
  68. public:
  69. // \param Checks:
  70. // - If unspecified (i.e. `None`) then ALL checks are suppressed - equivalent
  71. // to NOLINT(*).
  72. // - An empty string means nothing is suppressed - equivalent to NOLINT().
  73. // - Negative globs ignored (which would effectively disable the suppression).
  74. NoLintToken(NoLintType Type, size_t Pos,
  75. const std::optional<std::string> &Checks)
  76. : Type(Type), Pos(Pos), ChecksGlob(std::make_unique<CachedGlobList>(
  77. Checks.value_or("*"),
  78. /*KeepNegativeGlobs=*/false)) {
  79. if (Checks)
  80. this->Checks = trimWhitespace(*Checks);
  81. }
  82. // The type - one of NOLINT[NEXTLINE/BEGIN/END].
  83. NoLintType Type;
  84. // The location of the first character, "N", in "NOLINT".
  85. size_t Pos;
  86. // If this NOLINT specifies checks, return the checks.
  87. std::optional<std::string> checks() const { return Checks; }
  88. // Whether this NOLINT applies to the provided check.
  89. bool suppresses(StringRef Check) const { return ChecksGlob->contains(Check); }
  90. private:
  91. std::optional<std::string> Checks;
  92. std::unique_ptr<CachedGlobList> ChecksGlob;
  93. };
  94. } // namespace
  95. // Consume the entire buffer and return all `NoLintToken`s that were found.
  96. static SmallVector<NoLintToken> getNoLints(StringRef Buffer) {
  97. static constexpr llvm::StringLiteral NOLINT = "NOLINT";
  98. SmallVector<NoLintToken> NoLints;
  99. size_t Pos = 0;
  100. while (Pos < Buffer.size()) {
  101. // Find NOLINT:
  102. const size_t NoLintPos = Buffer.find(NOLINT, Pos);
  103. if (NoLintPos == StringRef::npos)
  104. break; // Buffer exhausted
  105. // Read [A-Z] characters immediately after "NOLINT", e.g. the "NEXTLINE" in
  106. // "NOLINTNEXTLINE".
  107. Pos = NoLintPos + NOLINT.size();
  108. while (Pos < Buffer.size() && llvm::isAlpha(Buffer[Pos]))
  109. ++Pos;
  110. // Is this a recognized NOLINT type?
  111. const std::optional<NoLintType> NoLintType =
  112. strToNoLintType(Buffer.slice(NoLintPos, Pos));
  113. if (!NoLintType)
  114. continue;
  115. // Get checks, if specified.
  116. std::optional<std::string> Checks;
  117. if (Pos < Buffer.size() && Buffer[Pos] == '(') {
  118. size_t ClosingBracket = Buffer.find_first_of("\n)", ++Pos);
  119. if (ClosingBracket != StringRef::npos && Buffer[ClosingBracket] == ')') {
  120. Checks = Buffer.slice(Pos, ClosingBracket).str();
  121. Pos = ClosingBracket + 1;
  122. }
  123. }
  124. NoLints.emplace_back(*NoLintType, NoLintPos, Checks);
  125. }
  126. return NoLints;
  127. }
  128. //===----------------------------------------------------------------------===//
  129. // NoLintBlockToken
  130. //===----------------------------------------------------------------------===//
  131. namespace {
  132. // Represents a source range within a pair of NOLINT(BEGIN/END) comments.
  133. class NoLintBlockToken {
  134. public:
  135. NoLintBlockToken(NoLintToken Begin, const NoLintToken &End)
  136. : Begin(std::move(Begin)), EndPos(End.Pos) {
  137. assert(this->Begin.Type == NoLintType::NoLintBegin);
  138. assert(End.Type == NoLintType::NoLintEnd);
  139. assert(this->Begin.Pos < End.Pos);
  140. assert(this->Begin.checks() == End.checks());
  141. }
  142. // Whether the provided diagnostic is within and is suppressible by this block
  143. // of NOLINT(BEGIN/END) comments.
  144. bool suppresses(size_t DiagPos, StringRef DiagName) const {
  145. return (Begin.Pos < DiagPos) && (DiagPos < EndPos) &&
  146. Begin.suppresses(DiagName);
  147. }
  148. private:
  149. NoLintToken Begin;
  150. size_t EndPos;
  151. };
  152. } // namespace
  153. // Match NOLINTBEGINs with their corresponding NOLINTENDs and move them into
  154. // `NoLintBlockToken`s. If any BEGINs or ENDs are left over, they are moved to
  155. // `UnmatchedTokens`.
  156. static SmallVector<NoLintBlockToken>
  157. formNoLintBlocks(SmallVector<NoLintToken> NoLints,
  158. SmallVectorImpl<NoLintToken> &UnmatchedTokens) {
  159. SmallVector<NoLintBlockToken> CompletedBlocks;
  160. SmallVector<NoLintToken> Stack;
  161. // Nested blocks must be fully contained within their parent block. What this
  162. // means is that when you have a series of nested BEGIN tokens, the END tokens
  163. // shall appear in the reverse order, starting with the closing of the
  164. // inner-most block first, then the next level up, and so on. This is
  165. // essentially a last-in-first-out/stack system.
  166. for (NoLintToken &NoLint : NoLints) {
  167. if (NoLint.Type == NoLintType::NoLintBegin)
  168. // A new block is being started. Add it to the stack.
  169. Stack.emplace_back(std::move(NoLint));
  170. else if (NoLint.Type == NoLintType::NoLintEnd) {
  171. if (!Stack.empty() && Stack.back().checks() == NoLint.checks())
  172. // The previous block is being closed. Pop one element off the stack.
  173. CompletedBlocks.emplace_back(Stack.pop_back_val(), NoLint);
  174. else
  175. // Trying to close the wrong block.
  176. UnmatchedTokens.emplace_back(std::move(NoLint));
  177. }
  178. }
  179. llvm::move(Stack, std::back_inserter(UnmatchedTokens));
  180. return CompletedBlocks;
  181. }
  182. //===----------------------------------------------------------------------===//
  183. // NoLintDirectiveHandler::Impl
  184. //===----------------------------------------------------------------------===//
  185. class NoLintDirectiveHandler::Impl {
  186. public:
  187. bool shouldSuppress(DiagnosticsEngine::Level DiagLevel,
  188. const Diagnostic &Diag, StringRef DiagName,
  189. SmallVectorImpl<tooling::Diagnostic> &NoLintErrors,
  190. bool AllowIO, bool EnableNoLintBlocks);
  191. private:
  192. bool diagHasNoLintInMacro(const Diagnostic &Diag, StringRef DiagName,
  193. SmallVectorImpl<tooling::Diagnostic> &NoLintErrors,
  194. bool AllowIO, bool EnableNoLintBlocks);
  195. bool diagHasNoLint(StringRef DiagName, SourceLocation DiagLoc,
  196. const SourceManager &SrcMgr,
  197. SmallVectorImpl<tooling::Diagnostic> &NoLintErrors,
  198. bool AllowIO, bool EnableNoLintBlocks);
  199. void generateCache(const SourceManager &SrcMgr, StringRef FileName,
  200. FileID File, StringRef Buffer,
  201. SmallVectorImpl<tooling::Diagnostic> &NoLintErrors);
  202. llvm::StringMap<SmallVector<NoLintBlockToken>> Cache;
  203. };
  204. bool NoLintDirectiveHandler::Impl::shouldSuppress(
  205. DiagnosticsEngine::Level DiagLevel, const Diagnostic &Diag,
  206. StringRef DiagName, SmallVectorImpl<tooling::Diagnostic> &NoLintErrors,
  207. bool AllowIO, bool EnableNoLintBlocks) {
  208. if (DiagLevel >= DiagnosticsEngine::Error)
  209. return false;
  210. return diagHasNoLintInMacro(Diag, DiagName, NoLintErrors, AllowIO,
  211. EnableNoLintBlocks);
  212. }
  213. // Look at the macro's spelling location for a NOLINT. If none is found, keep
  214. // looking up the call stack.
  215. bool NoLintDirectiveHandler::Impl::diagHasNoLintInMacro(
  216. const Diagnostic &Diag, StringRef DiagName,
  217. SmallVectorImpl<tooling::Diagnostic> &NoLintErrors, bool AllowIO,
  218. bool EnableNoLintBlocks) {
  219. SourceLocation DiagLoc = Diag.getLocation();
  220. if (DiagLoc.isInvalid())
  221. return false;
  222. const SourceManager &SrcMgr = Diag.getSourceManager();
  223. while (true) {
  224. if (diagHasNoLint(DiagName, DiagLoc, SrcMgr, NoLintErrors, AllowIO,
  225. EnableNoLintBlocks))
  226. return true;
  227. if (!DiagLoc.isMacroID())
  228. return false;
  229. DiagLoc = SrcMgr.getImmediateExpansionRange(DiagLoc).getBegin();
  230. }
  231. return false;
  232. }
  233. // Look behind and ahead for '\n' characters. These mark the start and end of
  234. // this line.
  235. static std::pair<size_t, size_t> getLineStartAndEnd(StringRef Buffer,
  236. size_t From) {
  237. size_t StartPos = Buffer.find_last_of('\n', From) + 1;
  238. size_t EndPos = std::min(Buffer.find('\n', From), Buffer.size());
  239. return std::make_pair(StartPos, EndPos);
  240. }
  241. // Whether the line has a NOLINT of type = `Type` that can suppress the
  242. // diagnostic `DiagName`.
  243. static bool lineHasNoLint(StringRef Buffer,
  244. std::pair<size_t, size_t> LineStartAndEnd,
  245. NoLintType Type, StringRef DiagName) {
  246. // Get all NOLINTs on the line.
  247. Buffer = Buffer.slice(LineStartAndEnd.first, LineStartAndEnd.second);
  248. SmallVector<NoLintToken> NoLints = getNoLints(Buffer);
  249. // Do any of these NOLINTs match the desired type and diag name?
  250. return llvm::any_of(NoLints, [&](const NoLintToken &NoLint) {
  251. return NoLint.Type == Type && NoLint.suppresses(DiagName);
  252. });
  253. }
  254. // Whether the provided diagnostic is located within and is suppressible by a
  255. // block of NOLINT(BEGIN/END) comments.
  256. static bool withinNoLintBlock(ArrayRef<NoLintBlockToken> NoLintBlocks,
  257. size_t DiagPos, StringRef DiagName) {
  258. return llvm::any_of(NoLintBlocks, [&](const NoLintBlockToken &NoLintBlock) {
  259. return NoLintBlock.suppresses(DiagPos, DiagName);
  260. });
  261. }
  262. // Get the file contents as a string.
  263. static std::optional<StringRef> getBuffer(const SourceManager &SrcMgr,
  264. FileID File, bool AllowIO) {
  265. return AllowIO ? SrcMgr.getBufferDataOrNone(File)
  266. : SrcMgr.getBufferDataIfLoaded(File);
  267. }
  268. // We will check for NOLINTs and NOLINTNEXTLINEs first. Checking for these is
  269. // not so expensive (just need to parse the current and previous lines). Only if
  270. // that fails do we look for NOLINT(BEGIN/END) blocks (which requires reading
  271. // the entire file).
  272. bool NoLintDirectiveHandler::Impl::diagHasNoLint(
  273. StringRef DiagName, SourceLocation DiagLoc, const SourceManager &SrcMgr,
  274. SmallVectorImpl<tooling::Diagnostic> &NoLintErrors, bool AllowIO,
  275. bool EnableNoLintBlocks) {
  276. // Translate the diagnostic's SourceLocation to a raw file + offset pair.
  277. FileID File;
  278. unsigned int Pos = 0;
  279. std::tie(File, Pos) = SrcMgr.getDecomposedSpellingLoc(DiagLoc);
  280. // We will only see NOLINTs in user-authored sources. No point reading the
  281. // file if it is a <built-in>.
  282. std::optional<StringRef> FileName = SrcMgr.getNonBuiltinFilenameForID(File);
  283. if (!FileName)
  284. return false;
  285. // Get file contents.
  286. std::optional<StringRef> Buffer = getBuffer(SrcMgr, File, AllowIO);
  287. if (!Buffer)
  288. return false;
  289. // Check if there's a NOLINT on this line.
  290. auto ThisLine = getLineStartAndEnd(*Buffer, Pos);
  291. if (lineHasNoLint(*Buffer, ThisLine, NoLintType::NoLint, DiagName))
  292. return true;
  293. // Check if there's a NOLINTNEXTLINE on the previous line.
  294. if (ThisLine.first > 0) {
  295. auto PrevLine = getLineStartAndEnd(*Buffer, ThisLine.first - 1);
  296. if (lineHasNoLint(*Buffer, PrevLine, NoLintType::NoLintNextLine, DiagName))
  297. return true;
  298. }
  299. // Check if this line is within a NOLINT(BEGIN/END) block.
  300. if (!EnableNoLintBlocks)
  301. return false;
  302. // Do we have cached NOLINT block locations for this file?
  303. if (Cache.count(*FileName) == 0)
  304. // Warning: heavy operation - need to read entire file.
  305. generateCache(SrcMgr, *FileName, File, *Buffer, NoLintErrors);
  306. return withinNoLintBlock(Cache[*FileName], Pos, DiagName);
  307. }
  308. // Construct a [clang-tidy-nolint] diagnostic to do with the unmatched
  309. // NOLINT(BEGIN/END) pair.
  310. static tooling::Diagnostic makeNoLintError(const SourceManager &SrcMgr,
  311. FileID File,
  312. const NoLintToken &NoLint) {
  313. tooling::Diagnostic Error;
  314. Error.DiagLevel = tooling::Diagnostic::Error;
  315. Error.DiagnosticName = "clang-tidy-nolint";
  316. StringRef Message =
  317. (NoLint.Type == NoLintType::NoLintBegin)
  318. ? ("unmatched 'NOLINTBEGIN' comment without a subsequent 'NOLINT"
  319. "END' comment")
  320. : ("unmatched 'NOLINTEND' comment without a previous 'NOLINT"
  321. "BEGIN' comment");
  322. SourceLocation Loc = SrcMgr.getComposedLoc(File, NoLint.Pos);
  323. Error.Message = tooling::DiagnosticMessage(Message, SrcMgr, Loc);
  324. return Error;
  325. }
  326. // Find all NOLINT(BEGIN/END) blocks in a file and store in the cache.
  327. void NoLintDirectiveHandler::Impl::generateCache(
  328. const SourceManager &SrcMgr, StringRef FileName, FileID File,
  329. StringRef Buffer, SmallVectorImpl<tooling::Diagnostic> &NoLintErrors) {
  330. // Read entire file to get all NOLINTs.
  331. SmallVector<NoLintToken> NoLints = getNoLints(Buffer);
  332. // Match each BEGIN with its corresponding END.
  333. SmallVector<NoLintToken> UnmatchedTokens;
  334. Cache[FileName] = formNoLintBlocks(std::move(NoLints), UnmatchedTokens);
  335. // Raise error for any BEGIN/END left over.
  336. for (const NoLintToken &NoLint : UnmatchedTokens)
  337. NoLintErrors.emplace_back(makeNoLintError(SrcMgr, File, NoLint));
  338. }
  339. //===----------------------------------------------------------------------===//
  340. // NoLintDirectiveHandler
  341. //===----------------------------------------------------------------------===//
  342. NoLintDirectiveHandler::NoLintDirectiveHandler()
  343. : PImpl(std::make_unique<Impl>()) {}
  344. NoLintDirectiveHandler::~NoLintDirectiveHandler() = default;
  345. bool NoLintDirectiveHandler::shouldSuppress(
  346. DiagnosticsEngine::Level DiagLevel, const Diagnostic &Diag,
  347. StringRef DiagName, SmallVectorImpl<tooling::Diagnostic> &NoLintErrors,
  348. bool AllowIO, bool EnableNoLintBlocks) {
  349. return PImpl->shouldSuppress(DiagLevel, Diag, DiagName, NoLintErrors, AllowIO,
  350. EnableNoLintBlocks);
  351. }
  352. } // namespace clang::tidy