1 //===-- clang-tools-extra/clang-tidy/NoLintDirectiveHandler.cpp -----------===//
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
7 //===----------------------------------------------------------------------===//
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.
13 //===----------------------------------------------------------------------===//
15 #include "NoLintDirectiveHandler.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/ArrayRef.h"
22 #include "llvm/ADT/STLExtras.h"
23 #include "llvm/ADT/SmallVector.h"
24 #include "llvm/ADT/StringExtras.h"
25 #include "llvm/ADT/StringMap.h"
26 #include "llvm/ADT/StringSwitch.h"
33 #include <type_traits>
36 namespace clang::tidy
{
38 //===----------------------------------------------------------------------===//
40 //===----------------------------------------------------------------------===//
42 // The type - one of NOLINT[NEXTLINE/BEGIN/END].
43 enum class NoLintType
{ NoLint
, NoLintNextLine
, NoLintBegin
, NoLintEnd
};
45 // Convert a string like "NOLINTNEXTLINE" to its enum `Type::NoLintNextLine`.
46 // Return `std::nullopt` if the string is unrecognized.
47 static 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
);
57 //===----------------------------------------------------------------------===//
59 //===----------------------------------------------------------------------===//
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.
64 static std::string
trimWhitespace(StringRef Checks
) {
65 SmallVector
<StringRef
> Split
;
66 Checks
.split(Split
, ',');
67 for (StringRef
&Check
: Split
)
69 return llvm::join(Split
, ",");
74 // Record the presence of a NOLINT comment - its type, location, checks -
75 // as parsed from the file's character contents.
79 // - If unspecified (i.e. `None`) then ALL checks are suppressed - equivalent
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
<std::string
> &Checks
)
85 : Type(Type
), Pos(Pos
), ChecksGlob(std::make_unique
<CachedGlobList
>(
87 /*KeepNegativeGlobs=*/false)) {
89 this->Checks
= trimWhitespace(*Checks
);
92 // The type - one of NOLINT[NEXTLINE/BEGIN/END].
95 // The location of the first character, "N", in "NOLINT".
98 // If this NOLINT specifies checks, return the checks.
99 std::optional
<std::string
> checks() const { return Checks
; }
101 // Whether this NOLINT applies to the provided check.
102 bool suppresses(StringRef Check
) const { return ChecksGlob
->contains(Check
); }
105 std::optional
<std::string
> Checks
;
106 std::unique_ptr
<CachedGlobList
> ChecksGlob
;
111 // Consume the entire buffer and return all `NoLintToken`s that were found.
112 static SmallVector
<NoLintToken
> getNoLints(StringRef Buffer
) {
113 static constexpr llvm::StringLiteral NOLINT
= "NOLINT";
114 SmallVector
<NoLintToken
> NoLints
;
117 while (Pos
< Buffer
.size()) {
119 const size_t NoLintPos
= Buffer
.find(NOLINT
, Pos
);
120 if (NoLintPos
== StringRef::npos
)
121 break; // Buffer exhausted
123 // Read [A-Z] characters immediately after "NOLINT", e.g. the "NEXTLINE" in
125 Pos
= NoLintPos
+ NOLINT
.size();
126 while (Pos
< Buffer
.size() && llvm::isAlpha(Buffer
[Pos
]))
129 // Is this a recognized NOLINT type?
130 const std::optional
<NoLintType
> NoLintType
=
131 strToNoLintType(Buffer
.slice(NoLintPos
, Pos
));
135 // Get checks, if specified.
136 std::optional
<std::string
> Checks
;
137 if (Pos
< Buffer
.size() && Buffer
[Pos
] == '(') {
138 size_t ClosingBracket
= Buffer
.find_first_of("\n)", ++Pos
);
139 if (ClosingBracket
!= StringRef::npos
&& Buffer
[ClosingBracket
] == ')') {
140 Checks
= Buffer
.slice(Pos
, ClosingBracket
).str();
141 Pos
= ClosingBracket
+ 1;
145 NoLints
.emplace_back(*NoLintType
, NoLintPos
, Checks
);
151 //===----------------------------------------------------------------------===//
153 //===----------------------------------------------------------------------===//
157 // Represents a source range within a pair of NOLINT(BEGIN/END) comments.
158 class NoLintBlockToken
{
160 NoLintBlockToken(NoLintToken Begin
, const NoLintToken
&End
)
161 : Begin(std::move(Begin
)), EndPos(End
.Pos
) {
162 assert(this->Begin
.Type
== NoLintType::NoLintBegin
);
163 assert(End
.Type
== NoLintType::NoLintEnd
);
164 assert(this->Begin
.Pos
< End
.Pos
);
165 assert(this->Begin
.checks() == End
.checks());
168 // Whether the provided diagnostic is within and is suppressible by this block
169 // of NOLINT(BEGIN/END) comments.
170 bool suppresses(size_t DiagPos
, StringRef DiagName
) const {
171 return (Begin
.Pos
< DiagPos
) && (DiagPos
< EndPos
) &&
172 Begin
.suppresses(DiagName
);
182 // Match NOLINTBEGINs with their corresponding NOLINTENDs and move them into
183 // `NoLintBlockToken`s. If any BEGINs or ENDs are left over, they are moved to
184 // `UnmatchedTokens`.
185 static SmallVector
<NoLintBlockToken
>
186 formNoLintBlocks(SmallVector
<NoLintToken
> NoLints
,
187 SmallVectorImpl
<NoLintToken
> &UnmatchedTokens
) {
188 SmallVector
<NoLintBlockToken
> CompletedBlocks
;
189 SmallVector
<NoLintToken
> Stack
;
191 // Nested blocks must be fully contained within their parent block. What this
192 // means is that when you have a series of nested BEGIN tokens, the END tokens
193 // shall appear in the reverse order, starting with the closing of the
194 // inner-most block first, then the next level up, and so on. This is
195 // essentially a last-in-first-out/stack system.
196 for (NoLintToken
&NoLint
: NoLints
) {
197 if (NoLint
.Type
== NoLintType::NoLintBegin
)
198 // A new block is being started. Add it to the stack.
199 Stack
.emplace_back(std::move(NoLint
));
200 else if (NoLint
.Type
== NoLintType::NoLintEnd
) {
201 if (!Stack
.empty() && Stack
.back().checks() == NoLint
.checks())
202 // The previous block is being closed. Pop one element off the stack.
203 CompletedBlocks
.emplace_back(Stack
.pop_back_val(), NoLint
);
205 // Trying to close the wrong block.
206 UnmatchedTokens
.emplace_back(std::move(NoLint
));
210 llvm::move(Stack
, std::back_inserter(UnmatchedTokens
));
211 return CompletedBlocks
;
214 //===----------------------------------------------------------------------===//
215 // NoLintDirectiveHandler::Impl
216 //===----------------------------------------------------------------------===//
218 class NoLintDirectiveHandler::Impl
{
220 bool shouldSuppress(DiagnosticsEngine::Level DiagLevel
,
221 const Diagnostic
&Diag
, StringRef DiagName
,
222 SmallVectorImpl
<tooling::Diagnostic
> &NoLintErrors
,
223 bool AllowIO
, bool EnableNoLintBlocks
);
226 bool diagHasNoLintInMacro(const Diagnostic
&Diag
, StringRef DiagName
,
227 SmallVectorImpl
<tooling::Diagnostic
> &NoLintErrors
,
228 bool AllowIO
, bool EnableNoLintBlocks
);
230 bool diagHasNoLint(StringRef DiagName
, SourceLocation DiagLoc
,
231 const SourceManager
&SrcMgr
,
232 SmallVectorImpl
<tooling::Diagnostic
> &NoLintErrors
,
233 bool AllowIO
, bool EnableNoLintBlocks
);
235 void generateCache(const SourceManager
&SrcMgr
, StringRef FileName
,
236 FileID File
, StringRef Buffer
,
237 SmallVectorImpl
<tooling::Diagnostic
> &NoLintErrors
);
239 llvm::StringMap
<SmallVector
<NoLintBlockToken
>> Cache
;
242 bool NoLintDirectiveHandler::Impl::shouldSuppress(
243 DiagnosticsEngine::Level DiagLevel
, const Diagnostic
&Diag
,
244 StringRef DiagName
, SmallVectorImpl
<tooling::Diagnostic
> &NoLintErrors
,
245 bool AllowIO
, bool EnableNoLintBlocks
) {
246 if (DiagLevel
>= DiagnosticsEngine::Error
)
248 return diagHasNoLintInMacro(Diag
, DiagName
, NoLintErrors
, AllowIO
,
252 // Look at the macro's spelling location for a NOLINT. If none is found, keep
253 // looking up the call stack.
254 bool NoLintDirectiveHandler::Impl::diagHasNoLintInMacro(
255 const Diagnostic
&Diag
, StringRef DiagName
,
256 SmallVectorImpl
<tooling::Diagnostic
> &NoLintErrors
, bool AllowIO
,
257 bool EnableNoLintBlocks
) {
258 SourceLocation DiagLoc
= Diag
.getLocation();
259 if (DiagLoc
.isInvalid())
261 const SourceManager
&SrcMgr
= Diag
.getSourceManager();
263 if (diagHasNoLint(DiagName
, DiagLoc
, SrcMgr
, NoLintErrors
, AllowIO
,
266 if (!DiagLoc
.isMacroID())
268 DiagLoc
= SrcMgr
.getImmediateExpansionRange(DiagLoc
).getBegin();
273 // Look behind and ahead for '\n' characters. These mark the start and end of
275 static std::pair
<size_t, size_t> getLineStartAndEnd(StringRef Buffer
,
277 size_t StartPos
= Buffer
.find_last_of('\n', From
) + 1;
278 size_t EndPos
= std::min(Buffer
.find('\n', From
), Buffer
.size());
279 return std::make_pair(StartPos
, EndPos
);
282 // Whether the line has a NOLINT of type = `Type` that can suppress the
283 // diagnostic `DiagName`.
284 static bool lineHasNoLint(StringRef Buffer
,
285 std::pair
<size_t, size_t> LineStartAndEnd
,
286 NoLintType Type
, StringRef DiagName
) {
287 // Get all NOLINTs on the line.
288 Buffer
= Buffer
.slice(LineStartAndEnd
.first
, LineStartAndEnd
.second
);
289 SmallVector
<NoLintToken
> NoLints
= getNoLints(Buffer
);
291 // Do any of these NOLINTs match the desired type and diag name?
292 return llvm::any_of(NoLints
, [&](const NoLintToken
&NoLint
) {
293 return NoLint
.Type
== Type
&& NoLint
.suppresses(DiagName
);
297 // Whether the provided diagnostic is located within and is suppressible by a
298 // block of NOLINT(BEGIN/END) comments.
299 static bool withinNoLintBlock(ArrayRef
<NoLintBlockToken
> NoLintBlocks
,
300 size_t DiagPos
, StringRef DiagName
) {
301 return llvm::any_of(NoLintBlocks
, [&](const NoLintBlockToken
&NoLintBlock
) {
302 return NoLintBlock
.suppresses(DiagPos
, DiagName
);
306 // Get the file contents as a string.
307 static std::optional
<StringRef
> getBuffer(const SourceManager
&SrcMgr
,
308 FileID File
, bool AllowIO
) {
309 return AllowIO
? SrcMgr
.getBufferDataOrNone(File
)
310 : SrcMgr
.getBufferDataIfLoaded(File
);
313 // We will check for NOLINTs and NOLINTNEXTLINEs first. Checking for these is
314 // not so expensive (just need to parse the current and previous lines). Only if
315 // that fails do we look for NOLINT(BEGIN/END) blocks (which requires reading
317 bool NoLintDirectiveHandler::Impl::diagHasNoLint(
318 StringRef DiagName
, SourceLocation DiagLoc
, const SourceManager
&SrcMgr
,
319 SmallVectorImpl
<tooling::Diagnostic
> &NoLintErrors
, bool AllowIO
,
320 bool EnableNoLintBlocks
) {
321 // Translate the diagnostic's SourceLocation to a raw file + offset pair.
323 unsigned int Pos
= 0;
324 std::tie(File
, Pos
) = SrcMgr
.getDecomposedSpellingLoc(DiagLoc
);
326 // We will only see NOLINTs in user-authored sources. No point reading the
327 // file if it is a <built-in>.
328 std::optional
<StringRef
> FileName
= SrcMgr
.getNonBuiltinFilenameForID(File
);
332 // Get file contents.
333 std::optional
<StringRef
> Buffer
= getBuffer(SrcMgr
, File
, AllowIO
);
337 // Check if there's a NOLINT on this line.
338 auto ThisLine
= getLineStartAndEnd(*Buffer
, Pos
);
339 if (lineHasNoLint(*Buffer
, ThisLine
, NoLintType::NoLint
, DiagName
))
342 // Check if there's a NOLINTNEXTLINE on the previous line.
343 if (ThisLine
.first
> 0) {
344 auto PrevLine
= getLineStartAndEnd(*Buffer
, ThisLine
.first
- 1);
345 if (lineHasNoLint(*Buffer
, PrevLine
, NoLintType::NoLintNextLine
, DiagName
))
349 // Check if this line is within a NOLINT(BEGIN/END) block.
350 if (!EnableNoLintBlocks
)
353 // Do we have cached NOLINT block locations for this file?
354 if (Cache
.count(*FileName
) == 0)
355 // Warning: heavy operation - need to read entire file.
356 generateCache(SrcMgr
, *FileName
, File
, *Buffer
, NoLintErrors
);
358 return withinNoLintBlock(Cache
[*FileName
], Pos
, DiagName
);
361 // Construct a [clang-tidy-nolint] diagnostic to do with the unmatched
362 // NOLINT(BEGIN/END) pair.
363 static tooling::Diagnostic
makeNoLintError(const SourceManager
&SrcMgr
,
365 const NoLintToken
&NoLint
) {
366 tooling::Diagnostic Error
;
367 Error
.DiagLevel
= tooling::Diagnostic::Error
;
368 Error
.DiagnosticName
= "clang-tidy-nolint";
370 (NoLint
.Type
== NoLintType::NoLintBegin
)
371 ? ("unmatched 'NOLINTBEGIN' comment without a subsequent 'NOLINT"
373 : ("unmatched 'NOLINTEND' comment without a previous 'NOLINT"
375 SourceLocation Loc
= SrcMgr
.getComposedLoc(File
, NoLint
.Pos
);
376 Error
.Message
= tooling::DiagnosticMessage(Message
, SrcMgr
, Loc
);
380 // Find all NOLINT(BEGIN/END) blocks in a file and store in the cache.
381 void NoLintDirectiveHandler::Impl::generateCache(
382 const SourceManager
&SrcMgr
, StringRef FileName
, FileID File
,
383 StringRef Buffer
, SmallVectorImpl
<tooling::Diagnostic
> &NoLintErrors
) {
384 // Read entire file to get all NOLINTs.
385 SmallVector
<NoLintToken
> NoLints
= getNoLints(Buffer
);
387 // Match each BEGIN with its corresponding END.
388 SmallVector
<NoLintToken
> UnmatchedTokens
;
389 Cache
[FileName
] = formNoLintBlocks(std::move(NoLints
), UnmatchedTokens
);
391 // Raise error for any BEGIN/END left over.
392 for (const NoLintToken
&NoLint
: UnmatchedTokens
)
393 NoLintErrors
.emplace_back(makeNoLintError(SrcMgr
, File
, NoLint
));
396 //===----------------------------------------------------------------------===//
397 // NoLintDirectiveHandler
398 //===----------------------------------------------------------------------===//
400 NoLintDirectiveHandler::NoLintDirectiveHandler()
401 : PImpl(std::make_unique
<Impl
>()) {}
403 NoLintDirectiveHandler::~NoLintDirectiveHandler() = default;
405 bool NoLintDirectiveHandler::shouldSuppress(
406 DiagnosticsEngine::Level DiagLevel
, const Diagnostic
&Diag
,
407 StringRef DiagName
, SmallVectorImpl
<tooling::Diagnostic
> &NoLintErrors
,
408 bool AllowIO
, bool EnableNoLintBlocks
) {
409 return PImpl
->shouldSuppress(DiagLevel
, Diag
, DiagName
, NoLintErrors
, AllowIO
,
413 } // namespace clang::tidy