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