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