llvm-project/clang/unittests/StaticAnalyzer/CallDescriptionTest.cpp

627 lines
19 KiB
C++

//===- unittests/StaticAnalyzer/CallDescriptionTest.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 "Reusables.h"
#include "clang/AST/ExprCXX.h"
#include "clang/Analysis/PathDiagnostic.h"
#include "clang/StaticAnalyzer/Core/BugReporter/CommonBugCategories.h"
#include "clang/StaticAnalyzer/Core/Checker.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/CallDescription.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"
#include "clang/StaticAnalyzer/Frontend/AnalysisConsumer.h"
#include "clang/StaticAnalyzer/Frontend/CheckerRegistry.h"
#include "clang/Tooling/Tooling.h"
#include "gtest/gtest.h"
#include <type_traits>
namespace clang {
namespace ento {
namespace {
// A wrapper around CallDescriptionMap<bool> that allows verifying that
// all functions have been found. This is needed because CallDescriptionMap
// isn't supposed to support iteration.
class ResultMap {
size_t Found, Total;
CallDescriptionMap<bool> Impl;
public:
ResultMap(std::initializer_list<std::pair<CallDescription, bool>> Data)
: Found(0),
Total(std::count_if(Data.begin(), Data.end(),
[](const std::pair<CallDescription, bool> &Pair) {
return Pair.second == true;
})),
Impl(std::move(Data)) {}
const bool *lookup(const CallEvent &Call) {
const bool *Result = Impl.lookup(Call);
// If it's a function we expected to find, remember that we've found it.
if (Result && *Result)
++Found;
return Result;
}
// Fail the test if we haven't found all the true-calls we were looking for.
~ResultMap() { EXPECT_EQ(Found, Total); }
};
// Scan the code body for call expressions and see if we find all calls that
// we were supposed to find ("true" in the provided ResultMap) and that we
// don't find the ones that we weren't supposed to find
// ("false" in the ResultMap).
template <typename MatchedExprT>
class CallDescriptionConsumer : public ExprEngineConsumer {
ResultMap &RM;
void performTest(const Decl *D) {
using namespace ast_matchers;
using T = MatchedExprT;
if (!D->hasBody())
return;
const StackFrameContext *SFC =
Eng.getAnalysisDeclContextManager().getStackFrame(D);
const ProgramStateRef State = Eng.getInitialState(SFC);
// FIXME: Maybe use std::variant and std::visit for these.
const auto MatcherCreator = []() {
if (std::is_same<T, CallExpr>::value)
return callExpr();
if (std::is_same<T, CXXConstructExpr>::value)
return cxxConstructExpr();
if (std::is_same<T, CXXMemberCallExpr>::value)
return cxxMemberCallExpr();
if (std::is_same<T, CXXOperatorCallExpr>::value)
return cxxOperatorCallExpr();
llvm_unreachable("Only these expressions are supported for now.");
};
const Expr *E = findNode<T>(D, MatcherCreator());
CallEventManager &CEMgr = Eng.getStateManager().getCallEventManager();
CallEventRef<> Call = [=, &CEMgr]() -> CallEventRef<CallEvent> {
if (std::is_base_of<CallExpr, T>::value)
return CEMgr.getCall(E, State, SFC);
if (std::is_same<T, CXXConstructExpr>::value)
return CEMgr.getCXXConstructorCall(cast<CXXConstructExpr>(E),
/*Target=*/nullptr, State, SFC);
llvm_unreachable("Only these expressions are supported for now.");
}();
// If the call actually matched, check if we really expected it to match.
const bool *LookupResult = RM.lookup(*Call);
EXPECT_TRUE(!LookupResult || *LookupResult);
// ResultMap is responsible for making sure that we've found *all* calls.
}
public:
CallDescriptionConsumer(CompilerInstance &C,
ResultMap &RM)
: ExprEngineConsumer(C), RM(RM) {}
bool HandleTopLevelDecl(DeclGroupRef DG) override {
for (const auto *D : DG)
performTest(D);
return true;
}
};
template <typename MatchedExprT = CallExpr>
class CallDescriptionAction : public ASTFrontendAction {
ResultMap RM;
public:
CallDescriptionAction(
std::initializer_list<std::pair<CallDescription, bool>> Data)
: RM(Data) {}
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &Compiler,
StringRef File) override {
return std::make_unique<CallDescriptionConsumer<MatchedExprT>>(Compiler,
RM);
}
};
TEST(CallDescription, SimpleNameMatching) {
EXPECT_TRUE(tooling::runToolOnCode(
std::unique_ptr<FrontendAction>(new CallDescriptionAction<>({
{{"bar"}, false}, // false: there's no call to 'bar' in this code.
{{"foo"}, true}, // true: there's a call to 'foo' in this code.
})),
"void foo(); void bar() { foo(); }"));
}
TEST(CallDescription, RequiredArguments) {
EXPECT_TRUE(tooling::runToolOnCode(
std::unique_ptr<FrontendAction>(new CallDescriptionAction<>({
{{"foo", 1}, true},
{{"foo", 2}, false},
})),
"void foo(int); void foo(int, int); void bar() { foo(1); }"));
}
TEST(CallDescription, LackOfRequiredArguments) {
EXPECT_TRUE(tooling::runToolOnCode(
std::unique_ptr<FrontendAction>(new CallDescriptionAction<>({
{{"foo", None}, true},
{{"foo", 2}, false},
})),
"void foo(int); void foo(int, int); void bar() { foo(1); }"));
}
constexpr StringRef MockStdStringHeader = R"code(
namespace std { inline namespace __1 {
template<typename T> class basic_string {
class Allocator {};
public:
basic_string();
explicit basic_string(const char*, const Allocator & = Allocator());
~basic_string();
T *c_str();
};
} // namespace __1
using string = __1::basic_string<char>;
} // namespace std
)code";
TEST(CallDescription, QualifiedNames) {
constexpr StringRef AdditionalCode = R"code(
void foo() {
using namespace std;
basic_string<char> s;
s.c_str();
})code";
const std::string Code = (Twine{MockStdStringHeader} + AdditionalCode).str();
EXPECT_TRUE(tooling::runToolOnCode(
std::unique_ptr<FrontendAction>(new CallDescriptionAction<>({
{{{"std", "basic_string", "c_str"}}, true},
})),
Code));
}
TEST(CallDescription, MatchConstructor) {
constexpr StringRef AdditionalCode = R"code(
void foo() {
using namespace std;
basic_string<char> s("hello");
})code";
const std::string Code = (Twine{MockStdStringHeader} + AdditionalCode).str();
EXPECT_TRUE(tooling::runToolOnCode(
std::unique_ptr<FrontendAction>(
new CallDescriptionAction<CXXConstructExpr>({
{{{"std", "basic_string", "basic_string"}, 2, 2}, true},
})),
Code));
}
// FIXME: Test matching destructors: {"std", "basic_string", "~basic_string"}
// This feature is actually implemented, but the test infra is not yet
// sophisticated enough for testing this. To do that, we will need to
// implement a much more advanced dispatching mechanism using the CFG for
// the implicit destructor events.
TEST(CallDescription, MatchConversionOperator) {
constexpr StringRef Code = R"code(
namespace aaa {
namespace bbb {
struct Bar {
operator int();
};
} // bbb
} // aaa
void foo() {
aaa::bbb::Bar x;
int tmp = x;
})code";
EXPECT_TRUE(tooling::runToolOnCode(
std::unique_ptr<FrontendAction>(new CallDescriptionAction<>({
{{{"aaa", "bbb", "Bar", "operator int"}}, true},
})),
Code));
}
TEST(CallDescription, RejectOverQualifiedNames) {
constexpr auto Code = R"code(
namespace my {
namespace std {
struct container {
const char *data() const;
};
} // namespace std
} // namespace my
void foo() {
using namespace my;
std::container v;
v.data();
})code";
// FIXME: We should **not** match.
EXPECT_TRUE(tooling::runToolOnCode(
std::unique_ptr<FrontendAction>(new CallDescriptionAction<>({
{{{"std", "container", "data"}}, true},
})),
Code));
}
TEST(CallDescription, DontSkipNonInlineNamespaces) {
constexpr auto Code = R"code(
namespace my {
/*not inline*/ namespace v1 {
void bar();
} // namespace v1
} // namespace my
void foo() {
my::v1::bar();
})code";
{
SCOPED_TRACE("my v1 bar");
EXPECT_TRUE(tooling::runToolOnCode(
std::unique_ptr<FrontendAction>(new CallDescriptionAction<>({
{{{"my", "v1", "bar"}}, true},
})),
Code));
}
{
// FIXME: We should **not** skip non-inline namespaces.
SCOPED_TRACE("my bar");
EXPECT_TRUE(tooling::runToolOnCode(
std::unique_ptr<FrontendAction>(new CallDescriptionAction<>({
{{{"my", "bar"}}, true},
})),
Code));
}
}
TEST(CallDescription, SkipTopInlineNamespaces) {
constexpr auto Code = R"code(
inline namespace my {
namespace v1 {
void bar();
} // namespace v1
} // namespace my
void foo() {
using namespace v1;
bar();
})code";
{
SCOPED_TRACE("my v1 bar");
EXPECT_TRUE(tooling::runToolOnCode(
std::unique_ptr<FrontendAction>(new CallDescriptionAction<>({
{{{"my", "v1", "bar"}}, true},
})),
Code));
}
{
SCOPED_TRACE("v1 bar");
EXPECT_TRUE(tooling::runToolOnCode(
std::unique_ptr<FrontendAction>(new CallDescriptionAction<>({
{{{"v1", "bar"}}, true},
})),
Code));
}
}
TEST(CallDescription, SkipAnonimousNamespaces) {
constexpr auto Code = R"code(
namespace {
namespace std {
namespace {
inline namespace {
struct container {
const char *data() const { return nullptr; };
};
} // namespace inline anonymous
} // namespace anonymous
} // namespace std
} // namespace anonymous
void foo() {
std::container v;
v.data();
})code";
EXPECT_TRUE(tooling::runToolOnCode(
std::unique_ptr<FrontendAction>(new CallDescriptionAction<>({
{{{"std", "container", "data"}}, true},
})),
Code));
}
TEST(CallDescription, AliasNames) {
constexpr StringRef AliasNamesCode = R"code(
namespace std {
struct container {
const char *data() const;
};
using cont = container;
} // std
)code";
constexpr StringRef UseAliasInSpelling = R"code(
void foo() {
std::cont v;
v.data();
})code";
constexpr StringRef UseStructNameInSpelling = R"code(
void foo() {
std::container v;
v.data();
})code";
const std::string UseAliasInSpellingCode =
(Twine{AliasNamesCode} + UseAliasInSpelling).str();
const std::string UseStructNameInSpellingCode =
(Twine{AliasNamesCode} + UseStructNameInSpelling).str();
// Test if the code spells the alias, wile we match against the struct name,
// and again matching against the alias.
{
SCOPED_TRACE("Using alias in spelling");
{
SCOPED_TRACE("std container data");
EXPECT_TRUE(tooling::runToolOnCode(
std::unique_ptr<FrontendAction>(new CallDescriptionAction<>({
{{{"std", "container", "data"}}, true},
})),
UseAliasInSpellingCode));
}
{
// FIXME: We should be able to see-through aliases.
SCOPED_TRACE("std cont data");
EXPECT_TRUE(tooling::runToolOnCode(
std::unique_ptr<FrontendAction>(new CallDescriptionAction<>({
{{{"std", "cont", "data"}}, false},
})),
UseAliasInSpellingCode));
}
}
// Test if the code spells the struct name, wile we match against the struct
// name, and again matching against the alias.
{
SCOPED_TRACE("Using struct name in spelling");
{
SCOPED_TRACE("std container data");
EXPECT_TRUE(tooling::runToolOnCode(
std::unique_ptr<FrontendAction>(new CallDescriptionAction<>({
{{{"std", "container", "data"}}, true},
})),
UseAliasInSpellingCode));
}
{
// FIXME: We should be able to see-through aliases.
SCOPED_TRACE("std cont data");
EXPECT_TRUE(tooling::runToolOnCode(
std::unique_ptr<FrontendAction>(new CallDescriptionAction<>({
{{{"std", "cont", "data"}}, false},
})),
UseAliasInSpellingCode));
}
}
}
TEST(CallDescription, AliasSingleNamespace) {
constexpr StringRef Code = R"code(
namespace aaa {
namespace bbb {
namespace ccc {
void bar();
}} // namespace bbb::ccc
namespace bbb_alias = bbb;
} // namespace aaa
void foo() {
aaa::bbb_alias::ccc::bar();
})code";
{
SCOPED_TRACE("aaa bbb ccc bar");
EXPECT_TRUE(tooling::runToolOnCode(
std::unique_ptr<FrontendAction>(new CallDescriptionAction<>({
{{{"aaa", "bbb", "ccc", "bar"}}, true},
})),
Code));
}
{
// FIXME: We should be able to see-through namespace aliases.
SCOPED_TRACE("aaa bbb_alias ccc bar");
EXPECT_TRUE(tooling::runToolOnCode(
std::unique_ptr<FrontendAction>(new CallDescriptionAction<>({
{{{"aaa", "bbb_alias", "ccc", "bar"}}, false},
})),
Code));
}
}
TEST(CallDescription, AliasMultipleNamespaces) {
constexpr StringRef Code = R"code(
namespace aaa {
namespace bbb {
namespace ccc {
void bar();
}}} // namespace aaa::bbb::ccc
namespace aaa_bbb_ccc = aaa::bbb::ccc;
void foo() {
using namespace aaa_bbb_ccc;
bar();
})code";
{
SCOPED_TRACE("aaa bbb ccc bar");
EXPECT_TRUE(tooling::runToolOnCode(
std::unique_ptr<FrontendAction>(new CallDescriptionAction<>({
{{{"aaa", "bbb", "ccc", "bar"}}, true},
})),
Code));
}
{
// FIXME: We should be able to see-through namespace aliases.
SCOPED_TRACE("aaa_bbb_ccc bar");
EXPECT_TRUE(tooling::runToolOnCode(
std::unique_ptr<FrontendAction>(new CallDescriptionAction<>({
{{{"aaa_bbb_ccc", "bar"}}, false},
})),
Code));
}
}
TEST(CallDescription, NegativeMatchQualifiedNames) {
EXPECT_TRUE(tooling::runToolOnCode(
std::unique_ptr<FrontendAction>(new CallDescriptionAction<>({
{{{"foo", "bar"}}, false},
{{{"bar", "foo"}}, false},
{{"foo"}, true},
})),
"void foo(); struct bar { void foo(); }; void test() { foo(); }"));
}
TEST(CallDescription, MatchBuiltins) {
// Test CDF_MaybeBuiltin - a flag that allows matching weird builtins.
EXPECT_TRUE(tooling::runToolOnCode(
std::unique_ptr<FrontendAction>(new CallDescriptionAction<>(
{{{"memset", 3}, false}, {{CDF_MaybeBuiltin, "memset", 3}, true}})),
"void foo() {"
" int x;"
" __builtin___memset_chk(&x, 0, sizeof(x),"
" __builtin_object_size(&x, 0));"
"}"));
{
SCOPED_TRACE("multiple similar builtins");
EXPECT_TRUE(tooling::runToolOnCode(
std::unique_ptr<FrontendAction>(new CallDescriptionAction<>(
{{{CDF_MaybeBuiltin, "memcpy", 3}, false},
{{CDF_MaybeBuiltin, "wmemcpy", 3}, true}})),
R"(void foo(wchar_t *x, wchar_t *y) {
__builtin_wmemcpy(x, y, sizeof(wchar_t));
})"));
}
{
SCOPED_TRACE("multiple similar builtins reversed order");
EXPECT_TRUE(tooling::runToolOnCode(
std::unique_ptr<FrontendAction>(new CallDescriptionAction<>(
{{{CDF_MaybeBuiltin, "wmemcpy", 3}, true},
{{CDF_MaybeBuiltin, "memcpy", 3}, false}})),
R"(void foo(wchar_t *x, wchar_t *y) {
__builtin_wmemcpy(x, y, sizeof(wchar_t));
})"));
}
{
SCOPED_TRACE("lookbehind and lookahead mismatches");
EXPECT_TRUE(tooling::runToolOnCode(
std::unique_ptr<FrontendAction>(
new CallDescriptionAction<>({{{CDF_MaybeBuiltin, "func"}, false}})),
R"(
void funcXXX();
void XXXfunc();
void XXXfuncXXX();
void test() {
funcXXX();
XXXfunc();
XXXfuncXXX();
})"));
}
{
SCOPED_TRACE("lookbehind and lookahead matches");
EXPECT_TRUE(tooling::runToolOnCode(
std::unique_ptr<FrontendAction>(
new CallDescriptionAction<>({{{CDF_MaybeBuiltin, "func"}, true}})),
R"(
void func();
void func_XXX();
void XXX_func();
void XXX_func_XXX();
void test() {
func(); // exact match
func_XXX();
XXX_func();
XXX_func_XXX();
})"));
}
}
//===----------------------------------------------------------------------===//
// Testing through a checker interface.
//
// Above, the static analyzer isn't run properly, only the bare minimum to
// create CallEvents. This causes CallEvents through function pointers to not
// refer to the pointee function, but this works fine if we run
// AnalysisASTConsumer.
//===----------------------------------------------------------------------===//
class CallDescChecker
: public Checker<check::PreCall, check::PreStmt<CallExpr>> {
CallDescriptionSet Set = {{"bar", 0}};
public:
void checkPreCall(const CallEvent &Call, CheckerContext &C) const {
if (Set.contains(Call)) {
C.getBugReporter().EmitBasicReport(
Call.getDecl(), this, "CallEvent match", categories::LogicError,
"CallEvent match",
PathDiagnosticLocation{Call.getDecl(), C.getSourceManager()});
}
}
void checkPreStmt(const CallExpr *CE, CheckerContext &C) const {
if (Set.containsAsWritten(*CE)) {
C.getBugReporter().EmitBasicReport(
CE->getCalleeDecl(), this, "CallExpr match", categories::LogicError,
"CallExpr match",
PathDiagnosticLocation{CE->getCalleeDecl(), C.getSourceManager()});
}
}
};
void addCallDescChecker(AnalysisASTConsumer &AnalysisConsumer,
AnalyzerOptions &AnOpts) {
AnOpts.CheckersAndPackages = {{"test.CallDescChecker", true}};
AnalysisConsumer.AddCheckerRegistrationFn([](CheckerRegistry &Registry) {
Registry.addChecker<CallDescChecker>("test.CallDescChecker", "Description",
"");
});
}
TEST(CallDescription, CheckCallExprMatching) {
// Imprecise matching shouldn't catch the call to bar, because its obscured
// by a function pointer.
constexpr StringRef FnPtrCode = R"code(
void bar();
void foo() {
void (*fnptr)() = bar;
fnptr();
})code";
std::string Diags;
EXPECT_TRUE(runCheckerOnCode<addCallDescChecker>(FnPtrCode.str(), Diags,
/*OnlyEmitWarnings*/ true));
EXPECT_EQ("test.CallDescChecker: CallEvent match\n", Diags);
// This should be caught properly by imprecise matching, as the call is done
// purely through syntactic means.
constexpr StringRef Code = R"code(
void bar();
void foo() {
bar();
})code";
Diags.clear();
EXPECT_TRUE(runCheckerOnCode<addCallDescChecker>(Code.str(), Diags,
/*OnlyEmitWarnings*/ true));
EXPECT_EQ("test.CallDescChecker: CallEvent match\n"
"test.CallDescChecker: CallExpr match\n",
Diags);
}
} // namespace
} // namespace ento
} // namespace clang