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
34namespace clang::tidy {
35
36//===----------------------------------------------------------------------===//
37// NoLintType
38//===----------------------------------------------------------------------===//
39
40// The type - one of NOLINT[NEXTLINE/BEGIN/END].
41enum 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.
45static 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.
62static 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
70namespace {
71
72// Record the presence of a NOLINT comment - its type, location, checks -
73// as parsed from the file's character contents.
74class NoLintToken {
75public:
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
102private:
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.
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(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
153namespace {
154
155// Represents a source range within a pair of NOLINT(BEGIN/END) comments.
156class NoLintBlockToken {
157public:
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
173private:
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`.
183static SmallVector<NoLintBlockToken>
184formNoLintBlocks(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
216class NoLintDirectiveHandler::Impl {
217public:
218 bool shouldSuppress(DiagnosticsEngine::Level DiagLevel,
219 const Diagnostic &Diag, StringRef DiagName,
220 SmallVectorImpl<tooling::Diagnostic> &NoLintErrors,
221 bool AllowIO, bool EnableNoLintBlocks);
222
223private:
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
240bool 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.
252bool 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.
273static 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`.
282static 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.
297static 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.
305static 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).
315bool 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.
361static 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.
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.
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
398NoLintDirectiveHandler::NoLintDirectiveHandler()
399 : PImpl(std::make_unique<Impl>()) {}
400
401NoLintDirectiveHandler::~NoLintDirectiveHandler() = default;
402
403bool 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

Provided by KDAB

Privacy Policy
Improve your Profiling and Debugging skills
Find out more

source code of clang-tools-extra/clang-tidy/NoLintDirectiveHandler.cpp