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 |
Definitions
- NoLintType
- strToNoLintType
- trimWhitespace
- NoLintToken
- NoLintToken
- checks
- suppresses
- getNoLints
- NoLintBlockToken
- NoLintBlockToken
- suppresses
- formNoLintBlocks
- Impl
- shouldSuppress
- diagHasNoLintInMacro
- getLineStartAndEnd
- lineHasNoLint
- withinNoLintBlock
- getBuffer
- diagHasNoLint
- makeNoLintError
- generateCache
- NoLintDirectiveHandler
- ~NoLintDirectiveHandler
Improve your Profiling and Debugging skills
Find out more