| //===- 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 |