
This commit tweaks the interface of `CheckerRegistry::addChecker` to make it more practical for plugins and tests: - The parameter `IsHidden` now defaults to `false` even in the non-templated overload (because setting it to true is unusual, especially in plugins). - The parameter `DocsUri` defaults to the dummy placeholder string `"NoDocsUri"` because (as of now) nothing queries its value from the checker registry (it's only used by the logic that generates the clang-tidy documentation, but that loads it directly from `Checkers.td` without involving the `CheckerRegistry`), so there is no reason to demand specifying this value. In addition to propagating these changes, this commit clarifies, corrects and extends lots of comments and performs various minor code quality improvements in the code of unit tests and example plugins. I originally wrote the bulk of this commit when I was planning to add an extra parameter to `addChecker` in order to implement some technical details of the CheckerFamily framework. At the end I decided against adding that extra parameter, so this cleanup was left out of the PR https://github.com/llvm/llvm-project/pull/139256 and I'm merging it now as a separate commit (after minor tweaks). This commit is mostly NFC: the only functional change is that the analyzer will be compatible with plugins that rely on the default argument values and don't specify `IsHidden` or `DocsUri`. (But existing plugin code will remain valid as well.)
370 lines
12 KiB
C++
370 lines
12 KiB
C++
//===- unittests/StaticAnalyzer/BlockEntranceCallbackTest.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
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#include "CheckerRegistration.h"
|
|
#include "clang/Analysis/AnalysisDeclContext.h"
|
|
#include "clang/Analysis/ProgramPoint.h"
|
|
#include "clang/StaticAnalyzer/Core/BugReporter/BugReporter.h"
|
|
#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h"
|
|
#include "clang/StaticAnalyzer/Core/Checker.h"
|
|
#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"
|
|
#include "clang/StaticAnalyzer/Core/PathSensitive/ProgramState_Fwd.h"
|
|
#include "clang/StaticAnalyzer/Frontend/AnalysisConsumer.h"
|
|
#include "clang/StaticAnalyzer/Frontend/CheckerRegistry.h"
|
|
#include "llvm/ADT/STLExtras.h"
|
|
#include "llvm/ADT/StringExtras.h"
|
|
#include "llvm/ADT/StringRef.h"
|
|
#include "llvm/ADT/Twine.h"
|
|
#include "llvm/Support/FormatVariadic.h"
|
|
#include "llvm/Support/raw_ostream.h"
|
|
#include "gtest/gtest.h"
|
|
|
|
using namespace clang;
|
|
using namespace ento;
|
|
|
|
namespace {
|
|
|
|
class BlockEntranceCallbackTester final : public Checker<check::BlockEntrance> {
|
|
const BugType Bug{this, "BlockEntranceTester"};
|
|
|
|
public:
|
|
void checkBlockEntrance(const BlockEntrance &Entrance,
|
|
CheckerContext &C) const {
|
|
ExplodedNode *Node = C.generateNonFatalErrorNode(C.getState());
|
|
if (!Node)
|
|
return;
|
|
|
|
const auto *FD =
|
|
cast<FunctionDecl>(C.getLocationContext()->getStackFrame()->getDecl());
|
|
|
|
std::string Description = llvm::formatv(
|
|
"Within '{0}' B{1} -> B{2}", FD->getIdentifier()->getName(),
|
|
Entrance.getPreviousBlock()->getBlockID(),
|
|
Entrance.getBlock()->getBlockID());
|
|
auto Report =
|
|
std::make_unique<PathSensitiveBugReport>(Bug, Description, Node);
|
|
C.emitReport(std::move(Report));
|
|
}
|
|
};
|
|
|
|
class BranchConditionCallbackTester final
|
|
: public Checker<check::BranchCondition> {
|
|
const BugType Bug{this, "BranchConditionCallbackTester"};
|
|
|
|
public:
|
|
void checkBranchCondition(const Stmt *Condition, CheckerContext &C) const {
|
|
ExplodedNode *Node = C.generateNonFatalErrorNode(C.getState());
|
|
if (!Node)
|
|
return;
|
|
const auto *FD =
|
|
cast<FunctionDecl>(C.getLocationContext()->getStackFrame()->getDecl());
|
|
|
|
std::string Buffer =
|
|
(llvm::Twine("Within '") + FD->getIdentifier()->getName() +
|
|
"': branch condition '")
|
|
.str();
|
|
llvm::raw_string_ostream OS(Buffer);
|
|
Condition->printPretty(OS, /*Helper=*/nullptr,
|
|
C.getASTContext().getPrintingPolicy());
|
|
OS << "'";
|
|
auto Report = std::make_unique<PathSensitiveBugReport>(Bug, Buffer, Node);
|
|
C.emitReport(std::move(Report));
|
|
|
|
C.addTransition();
|
|
}
|
|
};
|
|
|
|
template <typename Checker> void registerChecker(CheckerManager &Mgr) {
|
|
Mgr.registerChecker<Checker>();
|
|
}
|
|
|
|
bool shouldAlwaysRegister(const CheckerManager &) { return true; }
|
|
|
|
void addBlockEntranceTester(AnalysisASTConsumer &AnalysisConsumer,
|
|
AnalyzerOptions &AnOpts) {
|
|
AnOpts.CheckersAndPackages.emplace_back("test.BlockEntranceTester", true);
|
|
AnalysisConsumer.AddCheckerRegistrationFn([](CheckerRegistry &Registry) {
|
|
Registry.addChecker(®isterChecker<BlockEntranceCallbackTester>,
|
|
&shouldAlwaysRegister, "test.BlockEntranceTester",
|
|
"EmptyDescription");
|
|
});
|
|
}
|
|
|
|
void addBranchConditionTester(AnalysisASTConsumer &AnalysisConsumer,
|
|
AnalyzerOptions &AnOpts) {
|
|
AnOpts.CheckersAndPackages.emplace_back("test.BranchConditionTester", true);
|
|
AnalysisConsumer.AddCheckerRegistrationFn([](CheckerRegistry &Registry) {
|
|
Registry.addChecker(®isterChecker<BranchConditionCallbackTester>,
|
|
&shouldAlwaysRegister, "test.BranchConditionTester",
|
|
"EmptyDescription");
|
|
});
|
|
}
|
|
|
|
llvm::SmallVector<StringRef> parseEachDiag(StringRef Diags) {
|
|
llvm::SmallVector<StringRef> Fragments;
|
|
llvm::SplitString(Diags, Fragments, "\n");
|
|
// Drop the prefix like "test.BlockEntranceTester: " from each fragment.
|
|
for (StringRef &Fragment : Fragments) {
|
|
Fragment = Fragment.drop_until([](char Ch) { return Ch == ' '; });
|
|
Fragment.consume_front(" ");
|
|
}
|
|
llvm::sort(Fragments);
|
|
return Fragments;
|
|
}
|
|
|
|
template <AddCheckerFn Fn = addBlockEntranceTester, AddCheckerFn... Fns>
|
|
bool runChecker(const std::string &Code, std::string &Diags) {
|
|
std::string RawDiags;
|
|
bool Res = runCheckerOnCode<Fn, Fns...>(Code, RawDiags,
|
|
/*OnlyEmitWarnings=*/true);
|
|
llvm::raw_string_ostream OS(Diags);
|
|
llvm::interleave(parseEachDiag(RawDiags), OS, "\n");
|
|
return Res;
|
|
}
|
|
|
|
[[maybe_unused]] void dumpCFGAndEgraph(AnalysisASTConsumer &AnalysisConsumer,
|
|
AnalyzerOptions &AnOpts) {
|
|
AnOpts.CheckersAndPackages.emplace_back("debug.DumpCFG", true);
|
|
AnOpts.CheckersAndPackages.emplace_back("debug.ViewExplodedGraph", true);
|
|
}
|
|
|
|
/// Use this instead of \c runChecker to enable the debugging a test case.
|
|
template <AddCheckerFn... Fns>
|
|
[[maybe_unused]] bool debugChecker(const std::string &Code,
|
|
std::string &Diags) {
|
|
return runChecker<dumpCFGAndEgraph, Fns...>(Code, Diags);
|
|
}
|
|
|
|
std::string expected(SmallVector<StringRef> Diags) {
|
|
llvm::sort(Diags);
|
|
std::string Result;
|
|
llvm::raw_string_ostream OS(Result);
|
|
llvm::interleave(Diags, OS, "\n");
|
|
return Result;
|
|
}
|
|
|
|
TEST(BlockEntranceTester, FromEntryToExit) {
|
|
constexpr auto Code = R"cpp(
|
|
void top() {
|
|
// empty
|
|
})cpp";
|
|
|
|
std::string Diags;
|
|
// Use "debugChecker" instead of "runChecker" for debugging.
|
|
EXPECT_TRUE(runChecker(Code, Diags));
|
|
EXPECT_EQ(expected({"Within 'top' B1 -> B0"}), Diags);
|
|
}
|
|
|
|
TEST(BlockEntranceTester, SingleOpaqueIfCondition) {
|
|
constexpr auto Code = R"cpp(
|
|
bool coin();
|
|
int glob;
|
|
void top() {
|
|
if (coin()) {
|
|
glob = 1;
|
|
} else {
|
|
glob = 2;
|
|
}
|
|
glob = 3;
|
|
})cpp";
|
|
|
|
std::string Diags;
|
|
// Use "debugChecker" instead of "runChecker" for debugging.
|
|
EXPECT_TRUE(runChecker(Code, Diags));
|
|
EXPECT_EQ(expected({
|
|
"Within 'top' B1 -> B0",
|
|
"Within 'top' B2 -> B1",
|
|
"Within 'top' B3 -> B1",
|
|
"Within 'top' B4 -> B2",
|
|
"Within 'top' B4 -> B3",
|
|
"Within 'top' B5 -> B4",
|
|
}),
|
|
Diags);
|
|
// entry true exit
|
|
// B5 -------> B4 --> B2 --> B1 --> B0
|
|
// | ^
|
|
// | false |
|
|
// v |
|
|
// B3 -----------------------+
|
|
}
|
|
|
|
TEST(BlockEntranceTester, TrivialIfCondition) {
|
|
constexpr auto Code = R"cpp(
|
|
bool coin();
|
|
int glob;
|
|
void top() {
|
|
int cond = true;
|
|
if (cond) {
|
|
glob = 1;
|
|
} else {
|
|
glob = 2;
|
|
}
|
|
glob = 3;
|
|
})cpp";
|
|
|
|
std::string Diags;
|
|
// Use "debugChecker" instead of "runChecker" for debugging.
|
|
EXPECT_TRUE(runChecker(Code, Diags));
|
|
EXPECT_EQ(expected({
|
|
"Within 'top' B1 -> B0",
|
|
"Within 'top' B3 -> B1",
|
|
"Within 'top' B4 -> B3",
|
|
"Within 'top' B5 -> B4",
|
|
}),
|
|
Diags);
|
|
// entry true exit
|
|
// B5 ----------> B4 --> B3 --> B1 --> B0
|
|
}
|
|
|
|
TEST(BlockEntranceTester, AcrossFunctions) {
|
|
constexpr auto Code = R"cpp(
|
|
bool coin();
|
|
int glob;
|
|
void nested() { glob = 1; }
|
|
void top() {
|
|
glob = 0;
|
|
nested();
|
|
glob = 2;
|
|
})cpp";
|
|
|
|
std::string Diags;
|
|
// Use "debugChecker" instead of "runChecker" for debugging.
|
|
EXPECT_TRUE(runChecker(Code, Diags));
|
|
EXPECT_EQ(
|
|
expected({
|
|
// Going from the "top" entry artificial node to the "top" body.
|
|
// Ideally, we shouldn't observe this edge because it's artificial.
|
|
"Within 'top' B2 -> B1",
|
|
|
|
// We encounter the call to "nested()" in the "top" body, thus we have
|
|
// a "CallEnter" node, but importantly, we also elide the transition
|
|
// to the "entry" node of "nested()".
|
|
// We only see the edge from the "nested()" entry to the "nested()"
|
|
// body:
|
|
"Within 'nested' B2 -> B1",
|
|
|
|
// Once we return from "nested()", we transition to the "exit" node of
|
|
// "nested()":
|
|
"Within 'nested' B1 -> B0",
|
|
|
|
// We will eventually return to the "top" body, thus we transition to
|
|
// its "exit" node:
|
|
"Within 'top' B1 -> B0",
|
|
}),
|
|
Diags);
|
|
}
|
|
|
|
TEST(BlockEntranceTester, ShortCircuitingLogicalOperator) {
|
|
constexpr auto Code = R"cpp(
|
|
bool coin();
|
|
void top(int x) {
|
|
int v = 0;
|
|
if (coin() && (v = x)) {
|
|
v = 2;
|
|
}
|
|
v = 3;
|
|
})cpp";
|
|
// coin(): false
|
|
// +--------------------------------+
|
|
// entry | v exit
|
|
// +----+ +----+ +----+ +----+ +----+ +----+
|
|
// | B5 | --> | B4 | --> | B3 | --> | B2 | --> | B1 | --> | B0 |
|
|
// +----+ +----+ +----+ +----+ +----+ +----+
|
|
// | ^
|
|
// +---------------------+
|
|
// (v = x): false
|
|
|
|
std::string Diags;
|
|
// Use "debugChecker" instead of "runChecker" for debugging.
|
|
EXPECT_TRUE(runChecker(Code, Diags));
|
|
EXPECT_EQ(expected({
|
|
"Within 'top' B1 -> B0",
|
|
"Within 'top' B2 -> B1",
|
|
"Within 'top' B3 -> B1",
|
|
"Within 'top' B3 -> B2",
|
|
"Within 'top' B4 -> B1",
|
|
"Within 'top' B4 -> B3",
|
|
"Within 'top' B5 -> B4",
|
|
}),
|
|
Diags);
|
|
}
|
|
|
|
TEST(BlockEntranceTester, Switch) {
|
|
constexpr auto Code = R"cpp(
|
|
bool coin();
|
|
int top(int x) {
|
|
int v = 0;
|
|
switch (x) {
|
|
case 1: v = 10; break;
|
|
case 2: v = 20; break;
|
|
default: v = 30; break;
|
|
}
|
|
return v;
|
|
})cpp";
|
|
// +----+
|
|
// | B5 | -------------------------+
|
|
// +----+ |
|
|
// ^ [case 1] |
|
|
// entry | v exit
|
|
// +----+ +----+ [default] +----+ +----+ +----+
|
|
// | B6 | --> | B2 | ----------> | B3 | --> | B1 | --> | B0 |
|
|
// +----+ +----+ +----+ +----+ +----+
|
|
// | ^
|
|
// v [case 2] |
|
|
// +----+ |
|
|
// | B4 | -------------------------+
|
|
// +----+
|
|
|
|
std::string Diags;
|
|
// Use "debugChecker" instead of "runChecker" for debugging.
|
|
EXPECT_TRUE(runChecker(Code, Diags));
|
|
EXPECT_EQ(expected({
|
|
"Within 'top' B1 -> B0",
|
|
"Within 'top' B2 -> B3",
|
|
"Within 'top' B2 -> B4",
|
|
"Within 'top' B2 -> B5",
|
|
"Within 'top' B3 -> B1",
|
|
"Within 'top' B4 -> B1",
|
|
"Within 'top' B5 -> B1",
|
|
"Within 'top' B6 -> B2",
|
|
}),
|
|
Diags);
|
|
}
|
|
|
|
TEST(BlockEntranceTester, BlockEntranceVSBranchCondition) {
|
|
constexpr auto Code = R"cpp(
|
|
bool coin();
|
|
int top(int x) {
|
|
int v = 0;
|
|
switch (x) {
|
|
default: v = 30; break;
|
|
}
|
|
if (x == 6) {
|
|
v = 40;
|
|
}
|
|
return v;
|
|
})cpp";
|
|
std::string Diags;
|
|
// Use "debugChecker" instead of "runChecker" for debugging.
|
|
EXPECT_TRUE((runChecker<addBlockEntranceTester, addBranchConditionTester>(
|
|
Code, Diags)));
|
|
EXPECT_EQ(expected({
|
|
"Within 'top' B1 -> B0",
|
|
"Within 'top' B2 -> B1",
|
|
"Within 'top' B3 -> B1",
|
|
"Within 'top' B3 -> B2",
|
|
"Within 'top' B4 -> B5",
|
|
"Within 'top' B5 -> B3",
|
|
"Within 'top' B6 -> B4",
|
|
"Within 'top': branch condition 'x == 6'",
|
|
}),
|
|
Diags);
|
|
}
|
|
|
|
} // namespace
|