forked from OSchip/llvm-project
416 lines
16 KiB
C++
416 lines
16 KiB
C++
//===-- clang-tools-extra/clang-tidy/NoLintDirectiveHandler.cpp -----------===//
|
|
//
|
|
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
// See https://llvm.org/LICENSE.txt for license information.
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
///
|
|
/// \file This file implements the NoLintDirectiveHandler class, which is used
|
|
/// to locate NOLINT comments in the file being analyzed, to decide whether a
|
|
/// diagnostic should be suppressed.
|
|
///
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#include "NoLintDirectiveHandler.h"
|
|
#include "GlobList.h"
|
|
#include "clang/Basic/LLVM.h"
|
|
#include "clang/Basic/SourceLocation.h"
|
|
#include "clang/Basic/SourceManager.h"
|
|
#include "clang/Tooling/Core/Diagnostic.h"
|
|
#include "llvm/ADT/ArrayRef.h"
|
|
#include "llvm/ADT/None.h"
|
|
#include "llvm/ADT/Optional.h"
|
|
#include "llvm/ADT/STLExtras.h"
|
|
#include "llvm/ADT/SmallVector.h"
|
|
#include "llvm/ADT/StringExtras.h"
|
|
#include "llvm/ADT/StringMap.h"
|
|
#include "llvm/ADT/StringSwitch.h"
|
|
#include <cassert>
|
|
#include <cstddef>
|
|
#include <iterator>
|
|
#include <string>
|
|
#include <tuple>
|
|
#include <type_traits>
|
|
#include <utility>
|
|
|
|
namespace clang {
|
|
namespace tidy {
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// NoLintType
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
// The type - one of NOLINT[NEXTLINE/BEGIN/END].
|
|
enum class NoLintType { NoLint, NoLintNextLine, NoLintBegin, NoLintEnd };
|
|
|
|
// Convert a string like "NOLINTNEXTLINE" to its enum `Type::NoLintNextLine`.
|
|
// Return `None` if the string is unrecognized.
|
|
static Optional<NoLintType> strToNoLintType(StringRef Str) {
|
|
auto Type = llvm::StringSwitch<Optional<NoLintType>>(Str)
|
|
.Case("NOLINT", NoLintType::NoLint)
|
|
.Case("NOLINTNEXTLINE", NoLintType::NoLintNextLine)
|
|
.Case("NOLINTBEGIN", NoLintType::NoLintBegin)
|
|
.Case("NOLINTEND", NoLintType::NoLintEnd)
|
|
.Default(None);
|
|
return Type;
|
|
}
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// NoLintToken
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
// Whitespace within a NOLINT's check list shall be ignored.
|
|
// "NOLINT( check1, check2 )" is equivalent to "NOLINT(check1,check2)".
|
|
// Return the check list with all extraneous whitespace removed.
|
|
static std::string trimWhitespace(StringRef Checks) {
|
|
SmallVector<StringRef> Split;
|
|
Checks.split(Split, ',');
|
|
for (StringRef &Check : Split)
|
|
Check = Check.trim();
|
|
return llvm::join(Split, ",");
|
|
}
|
|
|
|
namespace {
|
|
|
|
// Record the presence of a NOLINT comment - its type, location, checks -
|
|
// as parsed from the file's character contents.
|
|
class NoLintToken {
|
|
public:
|
|
// \param Checks:
|
|
// - If unspecified (i.e. `None`) then ALL checks are suppressed - equivalent
|
|
// to NOLINT(*).
|
|
// - An empty string means nothing is suppressed - equivalent to NOLINT().
|
|
// - Negative globs ignored (which would effectively disable the suppression).
|
|
NoLintToken(NoLintType Type, size_t Pos, const Optional<std::string> &Checks)
|
|
: Type(Type), Pos(Pos), ChecksGlob(std::make_unique<CachedGlobList>(
|
|
Checks.getValueOr("*"),
|
|
/*KeepNegativeGlobs=*/false)) {
|
|
if (Checks)
|
|
this->Checks = trimWhitespace(*Checks);
|
|
}
|
|
|
|
// The type - one of NOLINT[NEXTLINE/BEGIN/END].
|
|
NoLintType Type;
|
|
|
|
// The location of the first character, "N", in "NOLINT".
|
|
size_t Pos;
|
|
|
|
// If this NOLINT specifies checks, return the checks.
|
|
Optional<std::string> checks() const { return Checks; }
|
|
|
|
// Whether this NOLINT applies to the provided check.
|
|
bool suppresses(StringRef Check) const { return ChecksGlob->contains(Check); }
|
|
|
|
private:
|
|
Optional<std::string> Checks;
|
|
std::unique_ptr<CachedGlobList> ChecksGlob;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
// Consume the entire buffer and return all `NoLintToken`s that were found.
|
|
static SmallVector<NoLintToken> getNoLints(StringRef Buffer) {
|
|
static constexpr llvm::StringLiteral NOLINT = "NOLINT";
|
|
SmallVector<NoLintToken> NoLints;
|
|
|
|
size_t Pos = 0;
|
|
while (Pos < Buffer.size()) {
|
|
// Find NOLINT:
|
|
const size_t NoLintPos = Buffer.find(NOLINT, Pos);
|
|
if (NoLintPos == StringRef::npos)
|
|
break; // Buffer exhausted
|
|
|
|
// Read [A-Z] characters immediately after "NOLINT", e.g. the "NEXTLINE" in
|
|
// "NOLINTNEXTLINE".
|
|
Pos = NoLintPos + NOLINT.size();
|
|
while (Pos < Buffer.size() && llvm::isAlpha(Buffer[Pos]))
|
|
++Pos;
|
|
|
|
// Is this a recognized NOLINT type?
|
|
const Optional<NoLintType> NoLintType =
|
|
strToNoLintType(Buffer.slice(NoLintPos, Pos));
|
|
if (!NoLintType)
|
|
continue;
|
|
|
|
// Get checks, if specified.
|
|
Optional<std::string> Checks;
|
|
if (Pos < Buffer.size() && Buffer[Pos] == '(') {
|
|
size_t ClosingBracket = Buffer.find_first_of("\n)", ++Pos);
|
|
if (ClosingBracket != StringRef::npos && Buffer[ClosingBracket] == ')') {
|
|
Checks = Buffer.slice(Pos, ClosingBracket).str();
|
|
Pos = ClosingBracket + 1;
|
|
}
|
|
}
|
|
|
|
NoLints.emplace_back(*NoLintType, NoLintPos, Checks);
|
|
}
|
|
|
|
return NoLints;
|
|
}
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// NoLintBlockToken
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
namespace {
|
|
|
|
// Represents a source range within a pair of NOLINT(BEGIN/END) comments.
|
|
class NoLintBlockToken {
|
|
public:
|
|
NoLintBlockToken(NoLintToken Begin, const NoLintToken &End)
|
|
: Begin(std::move(Begin)), EndPos(End.Pos) {
|
|
assert(this->Begin.Type == NoLintType::NoLintBegin);
|
|
assert(End.Type == NoLintType::NoLintEnd);
|
|
assert(this->Begin.Pos < End.Pos);
|
|
assert(this->Begin.checks() == End.checks());
|
|
}
|
|
|
|
// Whether the provided diagnostic is within and is suppressible by this block
|
|
// of NOLINT(BEGIN/END) comments.
|
|
bool suppresses(size_t DiagPos, StringRef DiagName) const {
|
|
return (Begin.Pos < DiagPos) && (DiagPos < EndPos) &&
|
|
Begin.suppresses(DiagName);
|
|
}
|
|
|
|
private:
|
|
NoLintToken Begin;
|
|
size_t EndPos;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
// Match NOLINTBEGINs with their corresponding NOLINTENDs and move them into
|
|
// `NoLintBlockToken`s. If any BEGINs or ENDs are left over, they are moved to
|
|
// `UnmatchedTokens`.
|
|
static SmallVector<NoLintBlockToken>
|
|
formNoLintBlocks(SmallVector<NoLintToken> NoLints,
|
|
SmallVectorImpl<NoLintToken> &UnmatchedTokens) {
|
|
SmallVector<NoLintBlockToken> CompletedBlocks;
|
|
SmallVector<NoLintToken> Stack;
|
|
|
|
// Nested blocks must be fully contained within their parent block. What this
|
|
// means is that when you have a series of nested BEGIN tokens, the END tokens
|
|
// shall appear in the reverse order, starting with the closing of the
|
|
// inner-most block first, then the next level up, and so on. This is
|
|
// essentially a last-in-first-out/stack system.
|
|
for (NoLintToken &NoLint : NoLints) {
|
|
if (NoLint.Type == NoLintType::NoLintBegin)
|
|
// A new block is being started. Add it to the stack.
|
|
Stack.emplace_back(std::move(NoLint));
|
|
else if (NoLint.Type == NoLintType::NoLintEnd) {
|
|
if (!Stack.empty() && Stack.back().checks() == NoLint.checks())
|
|
// The previous block is being closed. Pop one element off the stack.
|
|
CompletedBlocks.emplace_back(Stack.pop_back_val(), NoLint);
|
|
else
|
|
// Trying to close the wrong block.
|
|
UnmatchedTokens.emplace_back(std::move(NoLint));
|
|
}
|
|
}
|
|
|
|
llvm::move(Stack, std::back_inserter(UnmatchedTokens));
|
|
return CompletedBlocks;
|
|
}
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// NoLintDirectiveHandler::Impl
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
class NoLintDirectiveHandler::Impl {
|
|
public:
|
|
bool shouldSuppress(DiagnosticsEngine::Level DiagLevel,
|
|
const Diagnostic &Diag, StringRef DiagName,
|
|
SmallVectorImpl<tooling::Diagnostic> &NoLintErrors,
|
|
bool AllowIO, bool EnableNoLintBlocks);
|
|
|
|
private:
|
|
bool diagHasNoLintInMacro(const Diagnostic &Diag, StringRef DiagName,
|
|
SmallVectorImpl<tooling::Diagnostic> &NoLintErrors,
|
|
bool AllowIO, bool EnableNoLintBlocks);
|
|
|
|
bool diagHasNoLint(StringRef DiagName, SourceLocation DiagLoc,
|
|
const SourceManager &SrcMgr,
|
|
SmallVectorImpl<tooling::Diagnostic> &NoLintErrors,
|
|
bool AllowIO, bool EnableNoLintBlocks);
|
|
|
|
void generateCache(const SourceManager &SrcMgr, StringRef FileName,
|
|
FileID File, StringRef Buffer,
|
|
SmallVectorImpl<tooling::Diagnostic> &NoLintErrors);
|
|
|
|
llvm::StringMap<SmallVector<NoLintBlockToken>> Cache;
|
|
};
|
|
|
|
bool NoLintDirectiveHandler::Impl::shouldSuppress(
|
|
DiagnosticsEngine::Level DiagLevel, const Diagnostic &Diag,
|
|
StringRef DiagName, SmallVectorImpl<tooling::Diagnostic> &NoLintErrors,
|
|
bool AllowIO, bool EnableNoLintBlocks) {
|
|
if (DiagLevel >= DiagnosticsEngine::Error)
|
|
return false;
|
|
return diagHasNoLintInMacro(Diag, DiagName, NoLintErrors, AllowIO,
|
|
EnableNoLintBlocks);
|
|
}
|
|
|
|
// Look at the macro's spelling location for a NOLINT. If none is found, keep
|
|
// looking up the call stack.
|
|
bool NoLintDirectiveHandler::Impl::diagHasNoLintInMacro(
|
|
const Diagnostic &Diag, StringRef DiagName,
|
|
SmallVectorImpl<tooling::Diagnostic> &NoLintErrors, bool AllowIO,
|
|
bool EnableNoLintBlocks) {
|
|
SourceLocation DiagLoc = Diag.getLocation();
|
|
if (DiagLoc.isInvalid())
|
|
return false;
|
|
const SourceManager &SrcMgr = Diag.getSourceManager();
|
|
while (true) {
|
|
if (diagHasNoLint(DiagName, DiagLoc, SrcMgr, NoLintErrors, AllowIO,
|
|
EnableNoLintBlocks))
|
|
return true;
|
|
if (!DiagLoc.isMacroID())
|
|
return false;
|
|
DiagLoc = SrcMgr.getImmediateMacroCallerLoc(DiagLoc);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Look behind and ahead for '\n' characters. These mark the start and end of
|
|
// this line.
|
|
static std::pair<size_t, size_t> getLineStartAndEnd(StringRef Buffer,
|
|
size_t From) {
|
|
size_t StartPos = Buffer.find_last_of('\n', From) + 1;
|
|
size_t EndPos = std::min(Buffer.find('\n', From), Buffer.size());
|
|
return std::make_pair(StartPos, EndPos);
|
|
}
|
|
|
|
// Whether the line has a NOLINT of type = `Type` that can suppress the
|
|
// diagnostic `DiagName`.
|
|
static bool lineHasNoLint(StringRef Buffer,
|
|
std::pair<size_t, size_t> LineStartAndEnd,
|
|
NoLintType Type, StringRef DiagName) {
|
|
// Get all NOLINTs on the line.
|
|
Buffer = Buffer.slice(LineStartAndEnd.first, LineStartAndEnd.second);
|
|
SmallVector<NoLintToken> NoLints = getNoLints(Buffer);
|
|
|
|
// Do any of these NOLINTs match the desired type and diag name?
|
|
return llvm::any_of(NoLints, [&](const NoLintToken &NoLint) {
|
|
return NoLint.Type == Type && NoLint.suppresses(DiagName);
|
|
});
|
|
}
|
|
|
|
// Whether the provided diagnostic is located within and is suppressible by a
|
|
// block of NOLINT(BEGIN/END) comments.
|
|
static bool withinNoLintBlock(ArrayRef<NoLintBlockToken> NoLintBlocks,
|
|
size_t DiagPos, StringRef DiagName) {
|
|
return llvm::any_of(NoLintBlocks, [&](const NoLintBlockToken &NoLintBlock) {
|
|
return NoLintBlock.suppresses(DiagPos, DiagName);
|
|
});
|
|
}
|
|
|
|
// Get the file contents as a string.
|
|
static Optional<StringRef> getBuffer(const SourceManager &SrcMgr, FileID File,
|
|
bool AllowIO) {
|
|
return AllowIO ? SrcMgr.getBufferDataOrNone(File)
|
|
: SrcMgr.getBufferDataIfLoaded(File);
|
|
}
|
|
|
|
// We will check for NOLINTs and NOLINTNEXTLINEs first. Checking for these is
|
|
// not so expensive (just need to parse the current and previous lines). Only if
|
|
// that fails do we look for NOLINT(BEGIN/END) blocks (which requires reading
|
|
// the entire file).
|
|
bool NoLintDirectiveHandler::Impl::diagHasNoLint(
|
|
StringRef DiagName, SourceLocation DiagLoc, const SourceManager &SrcMgr,
|
|
SmallVectorImpl<tooling::Diagnostic> &NoLintErrors, bool AllowIO,
|
|
bool EnableNoLintBlocks) {
|
|
// Translate the diagnostic's SourceLocation to a raw file + offset pair.
|
|
FileID File;
|
|
unsigned int Pos = 0;
|
|
std::tie(File, Pos) = SrcMgr.getDecomposedSpellingLoc(DiagLoc);
|
|
|
|
// We will only see NOLINTs in user-authored sources. No point reading the
|
|
// file if it is a <built-in>.
|
|
Optional<StringRef> FileName = SrcMgr.getNonBuiltinFilenameForID(File);
|
|
if (!FileName)
|
|
return false;
|
|
|
|
// Get file contents.
|
|
Optional<StringRef> Buffer = getBuffer(SrcMgr, File, AllowIO);
|
|
if (!Buffer)
|
|
return false;
|
|
|
|
// Check if there's a NOLINT on this line.
|
|
auto ThisLine = getLineStartAndEnd(*Buffer, Pos);
|
|
if (lineHasNoLint(*Buffer, ThisLine, NoLintType::NoLint, DiagName))
|
|
return true;
|
|
|
|
// Check if there's a NOLINTNEXTLINE on the previous line.
|
|
if (ThisLine.first > 0) {
|
|
auto PrevLine = getLineStartAndEnd(*Buffer, ThisLine.first - 1);
|
|
if (lineHasNoLint(*Buffer, PrevLine, NoLintType::NoLintNextLine, DiagName))
|
|
return true;
|
|
}
|
|
|
|
// Check if this line is within a NOLINT(BEGIN/END) block.
|
|
if (!EnableNoLintBlocks)
|
|
return false;
|
|
|
|
// Do we have cached NOLINT block locations for this file?
|
|
if (Cache.count(*FileName) == 0)
|
|
// Warning: heavy operation - need to read entire file.
|
|
generateCache(SrcMgr, *FileName, File, *Buffer, NoLintErrors);
|
|
|
|
return withinNoLintBlock(Cache[*FileName], Pos, DiagName);
|
|
}
|
|
|
|
// Construct a [clang-tidy-nolint] diagnostic to do with the unmatched
|
|
// NOLINT(BEGIN/END) pair.
|
|
static tooling::Diagnostic makeNoLintError(const SourceManager &SrcMgr,
|
|
FileID File,
|
|
const NoLintToken &NoLint) {
|
|
tooling::Diagnostic Error;
|
|
Error.DiagLevel = tooling::Diagnostic::Error;
|
|
Error.DiagnosticName = "clang-tidy-nolint";
|
|
StringRef Message =
|
|
(NoLint.Type == NoLintType::NoLintBegin)
|
|
? ("unmatched 'NOLINTBEGIN' comment without a subsequent 'NOLINT"
|
|
"END' comment")
|
|
: ("unmatched 'NOLINTEND' comment without a previous 'NOLINT"
|
|
"BEGIN' comment");
|
|
SourceLocation Loc = SrcMgr.getComposedLoc(File, NoLint.Pos);
|
|
Error.Message = tooling::DiagnosticMessage(Message, SrcMgr, Loc);
|
|
return Error;
|
|
}
|
|
|
|
// Find all NOLINT(BEGIN/END) blocks in a file and store in the cache.
|
|
void NoLintDirectiveHandler::Impl::generateCache(
|
|
const SourceManager &SrcMgr, StringRef FileName, FileID File,
|
|
StringRef Buffer, SmallVectorImpl<tooling::Diagnostic> &NoLintErrors) {
|
|
// Read entire file to get all NOLINTs.
|
|
SmallVector<NoLintToken> NoLints = getNoLints(Buffer);
|
|
|
|
// Match each BEGIN with its corresponding END.
|
|
SmallVector<NoLintToken> UnmatchedTokens;
|
|
Cache[FileName] = formNoLintBlocks(std::move(NoLints), UnmatchedTokens);
|
|
|
|
// Raise error for any BEGIN/END left over.
|
|
for (const NoLintToken &NoLint : UnmatchedTokens)
|
|
NoLintErrors.emplace_back(makeNoLintError(SrcMgr, File, NoLint));
|
|
}
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// NoLintDirectiveHandler
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
NoLintDirectiveHandler::NoLintDirectiveHandler()
|
|
: PImpl(std::make_unique<Impl>()) {}
|
|
|
|
NoLintDirectiveHandler::~NoLintDirectiveHandler() = default;
|
|
|
|
bool NoLintDirectiveHandler::shouldSuppress(
|
|
DiagnosticsEngine::Level DiagLevel, const Diagnostic &Diag,
|
|
StringRef DiagName, SmallVectorImpl<tooling::Diagnostic> &NoLintErrors,
|
|
bool AllowIO, bool EnableNoLintBlocks) {
|
|
return PImpl->shouldSuppress(DiagLevel, Diag, DiagName, NoLintErrors, AllowIO,
|
|
EnableNoLintBlocks);
|
|
}
|
|
|
|
} // namespace tidy
|
|
} // namespace clang
|