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