Artem Dergachev 017675fff1
[attributes][analyzer] Generalize [[clang::suppress]] to declarations. (#80371)
The attribute is now allowed on an assortment of declarations, to
suppress warnings related to declarations themselves, or all warnings in
the lexical scope of the declaration.

I don't necessarily see a reason to have a list at all, but it does look
as if some of those more niche items aren't properly supported by the
compiler itself so let's maintain a short safe list for now.

The initial implementation raised a question whether the attribute
should apply to lexical declaration context vs. "actual" declaration
context. I'm using "lexical" here because it results in less warnings
suppressed, which is the conservative behavior: we can always expand it
later if we think this is wrong, without breaking any existing code. I
also think that this is the correct behavior that we will probably never
want to change, given that the user typically desires to keep the
suppressions as localized as possible.
2024-02-13 14:57:55 -08:00

192 lines
7.3 KiB
C++

//===- BugSuppression.cpp - Suppression interface -------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
#include "clang/StaticAnalyzer/Core/BugReporter/BugSuppression.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/StaticAnalyzer/Core/BugReporter/BugReporter.h"
using namespace clang;
using namespace ento;
namespace {
using Ranges = llvm::SmallVectorImpl<SourceRange>;
inline bool hasSuppression(const Decl *D) {
// FIXME: Implement diagnostic identifier arguments
// (checker names, "hashtags").
if (const auto *Suppression = D->getAttr<SuppressAttr>())
return !Suppression->isGSL() &&
(Suppression->diagnosticIdentifiers().empty());
return false;
}
inline bool hasSuppression(const AttributedStmt *S) {
// FIXME: Implement diagnostic identifier arguments
// (checker names, "hashtags").
return llvm::any_of(S->getAttrs(), [](const Attr *A) {
const auto *Suppression = dyn_cast<SuppressAttr>(A);
return Suppression && !Suppression->isGSL() &&
(Suppression->diagnosticIdentifiers().empty());
});
}
template <class NodeType> inline SourceRange getRange(const NodeType *Node) {
return Node->getSourceRange();
}
template <> inline SourceRange getRange(const AttributedStmt *S) {
// Begin location for attributed statement node seems to be ALWAYS invalid.
//
// It is unlikely that we ever report any warnings on suppression
// attribute itself, but even if we do, we wouldn't want that warning
// to be suppressed by that same attribute.
//
// Long story short, we can use inner statement and it's not going to break
// anything.
return getRange(S->getSubStmt());
}
inline bool isLessOrEqual(SourceLocation LHS, SourceLocation RHS,
const SourceManager &SM) {
// SourceManager::isBeforeInTranslationUnit tests for strict
// inequality, when we need a non-strict comparison (bug
// can be reported directly on the annotated note).
// For this reason, we use the following equivalence:
//
// A <= B <==> !(B < A)
//
return !SM.isBeforeInTranslationUnit(RHS, LHS);
}
inline bool fullyContains(SourceRange Larger, SourceRange Smaller,
const SourceManager &SM) {
// Essentially this means:
//
// Larger.fullyContains(Smaller)
//
// However, that method has a very trivial implementation and couldn't
// compare regular locations and locations from macro expansions.
// We could've converted everything into regular locations as a solution,
// but the following solution seems to be the most bulletproof.
return isLessOrEqual(Larger.getBegin(), Smaller.getBegin(), SM) &&
isLessOrEqual(Smaller.getEnd(), Larger.getEnd(), SM);
}
class CacheInitializer : public RecursiveASTVisitor<CacheInitializer> {
public:
static void initialize(const Decl *D, Ranges &ToInit) {
CacheInitializer(ToInit).TraverseDecl(const_cast<Decl *>(D));
}
bool VisitDecl(Decl *D) {
// Bug location could be somewhere in the init value of
// a freshly declared variable. Even though it looks like the
// user applied attribute to a statement, it will apply to a
// variable declaration, and this is where we check for it.
return VisitAttributedNode(D);
}
bool VisitAttributedStmt(AttributedStmt *AS) {
// When we apply attributes to statements, it actually creates
// a wrapper statement that only contains attributes and the wrapped
// statement.
return VisitAttributedNode(AS);
}
private:
template <class NodeType> bool VisitAttributedNode(NodeType *Node) {
if (hasSuppression(Node)) {
// TODO: In the future, when we come up with good stable IDs for checkers
// we can return a list of kinds to ignore, or all if no arguments
// were provided.
addRange(getRange(Node));
}
// We should keep traversing AST.
return true;
}
void addRange(SourceRange R) {
if (R.isValid()) {
Result.push_back(R);
}
}
CacheInitializer(Ranges &R) : Result(R) {}
Ranges &Result;
};
} // end anonymous namespace
// TODO: Introduce stable IDs for checkers and check for those here
// to be more specific. Attribute without arguments should still
// be considered as "suppress all".
// It is already much finer granularity than what we have now
// (i.e. removing the whole function from the analysis).
bool BugSuppression::isSuppressed(const BugReport &R) {
PathDiagnosticLocation Location = R.getLocation();
PathDiagnosticLocation UniqueingLocation = R.getUniqueingLocation();
const Decl *DeclWithIssue = R.getDeclWithIssue();
return isSuppressed(Location, DeclWithIssue, {}) ||
isSuppressed(UniqueingLocation, DeclWithIssue, {});
}
bool BugSuppression::isSuppressed(const PathDiagnosticLocation &Location,
const Decl *DeclWithIssue,
DiagnosticIdentifierList Hashtags) {
if (!Location.isValid())
return false;
if (!DeclWithIssue) {
// FIXME: This defeats the purpose of passing DeclWithIssue to begin with.
// If this branch is ever hit, we're re-doing all the work we've already
// done as well as perform a lot of work we'll never need.
// Gladly, none of our on-by-default checkers currently need it.
DeclWithIssue = ACtx.getTranslationUnitDecl();
} else {
// This is the fast path. However, we should still consider the topmost
// declaration that isn't TranslationUnitDecl, because we should respect
// attributes on the entire declaration chain.
while (true) {
// Use the "lexical" parent. Eg., if the attribute is on a class, suppress
// warnings in inline methods but not in out-of-line methods.
const Decl *Parent =
dyn_cast_or_null<Decl>(DeclWithIssue->getLexicalDeclContext());
if (Parent == nullptr || isa<TranslationUnitDecl>(Parent))
break;
DeclWithIssue = Parent;
}
}
// While some warnings are attached to AST nodes (mostly path-sensitive
// checks), others are simply associated with a plain source location
// or range. Figuring out the node based on locations can be tricky,
// so instead, we traverse the whole body of the declaration and gather
// information on ALL suppressions. After that we can simply check if
// any of those suppressions affect the warning in question.
//
// Traversing AST of a function is not a heavy operation, but for
// large functions with a lot of bugs it can make a dent in performance.
// In order to avoid this scenario, we cache traversal results.
auto InsertionResult = CachedSuppressionLocations.insert(
std::make_pair(DeclWithIssue, CachedRanges{}));
Ranges &SuppressionRanges = InsertionResult.first->second;
if (InsertionResult.second) {
// We haven't checked this declaration for suppressions yet!
CacheInitializer::initialize(DeclWithIssue, SuppressionRanges);
}
SourceRange BugRange = Location.asRange();
const SourceManager &SM = Location.getManager();
return llvm::any_of(SuppressionRanges,
[BugRange, &SM](SourceRange Suppression) {
return fullyContains(Suppression, BugRange, SM);
});
}