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/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// NoLintType
37//===----------------------------------------------------------------------===//
38
39// The type - one of NOLINT[NEXTLINE/BEGIN/END].
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// NoLintToken
56//===----------------------------------------------------------------------===//
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 - equivalent
77 // 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} // namespace
109
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 exhausted
121
122 // Read [A-Z] characters immediately after "NOLINT", e.g. the "NEXTLINE" in
123 // "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 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// NoLintBlockToken
152//===----------------------------------------------------------------------===//
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 block
164 // 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} // namespace
177
178// Construct a [clang-tidy-nolint] diagnostic to do with the unmatched
179// 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 StringRef Message =
188 ? ("unmatched 'NOLINTBEGIN' comment without a subsequent 'NOLINT"
189 "END' comment")
190 : ("unmatched 'NOLINTEND' comment without a previous 'NOLINT"
191 "BEGIN' comment");
192 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 into
198// `NoLintBlockToken`s. If any BEGINs or ENDs are left over, a diagnostic is
199// 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 this
208 // means is that when you have a series of nested BEGIN tokens, the END tokens
209 // shall appear in the reverse order, starting with the closing of the
210 // inner-most block first, then the next level up, and so on. This is
211 // essentially a last-in-first-out/stack system.
212 for (NoLintToken &NoLint : NoLints) {
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 } else
223 // 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::Impl
236//===----------------------------------------------------------------------===//
237
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
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, keep
273// 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 of
294// this line.
295static std::pair<size_t, size_t> getLineStartAndEnd(StringRef Buffer,
296 size_t From) {
297 size_t StartPos = Buffer.find_last_of('\n', From) + 1;
298 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 the
303// 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 a
318// 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 is
334// not so expensive (just need to parse the current and previous lines). Only if
335// that fails do we look for NOLINT(BEGIN/END) blocks (which requires reading
336// 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 the
345 // 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 its
384 // 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// NoLintDirectiveHandler
391//===----------------------------------------------------------------------===//
392
394 : PImpl(std::make_unique<Impl>()) {}
395
397
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::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)