#include #include #include #include #include #include #include #include #include #include #include "fold/symbolic/encoder_callee_names.hpp" namespace { std::unique_ptr parse(const std::string& code) { return clang::tooling::buildASTFromCodeWithArgs(code, {"-std=c++20"}, "input.cpp"); } const clang::FunctionDecl* functionNamed(clang::ASTUnit& unit, const std::string& qname) { const clang::FunctionDecl* found = nullptr; struct Visitor : clang::RecursiveASTVisitor { const std::string& target; const clang::FunctionDecl*& out; Visitor(const std::string& t, const clang::FunctionDecl*& o) : target(t), out(o) {} bool VisitFunctionDecl(clang::FunctionDecl* fd) { if (fd->hasBody() && fd->getQualifiedNameAsString() == target) { out = fd; return false; } return true; } } visitor(qname, found); visitor.TraverseDecl(unit.getASTContext().getTranslationUnitDecl()); return found; } std::vector collectCalls(clang::ASTUnit& unit, const std::string& in_func) { std::vector calls; const clang::FunctionDecl* fd = functionNamed(unit, in_func); if (fd == nullptr || fd->getBody() == nullptr) return calls; struct Visitor : clang::RecursiveASTVisitor { std::vector& out; explicit Visitor(std::vector& o) : out(o) {} bool VisitCallExpr(clang::CallExpr* ce) { out.push_back(ce); return true; } } visitor(calls); visitor.TraverseStmt(fd->getBody()); return calls; } } // namespace TEST_CASE("calleeName returns the unqualified function name for a direct call", "[encoder][callee_names]") { auto unit = parse(R"cpp( void target(int); void caller() { target(1); } )cpp"); REQUIRE(unit); auto calls = collectCalls(*unit, "caller"); REQUIRE(calls.size() == 1); REQUIRE(fold::calleeName(calls[0]) == "target"); } TEST_CASE("calleeName strips namespaces from the unqualified form", "[encoder][callee_names]") { auto unit = parse(R"cpp( namespace ns { void inner(); } void caller() { ns::inner(); } )cpp"); REQUIRE(unit); auto calls = collectCalls(*unit, "caller"); REQUIRE(calls.size() == 1); REQUIRE(fold::calleeName(calls[0]) == "inner"); } TEST_CASE("calleeName extracts the method name from a C++ member call", "[encoder][callee_names]") { auto unit = parse(R"cpp( struct S { void method() {} }; void caller() { S s; s.method(); } )cpp"); REQUIRE(unit); auto calls = collectCalls(*unit, "caller"); REQUIRE(!calls.empty()); bool found_method_call = false; for (const auto* ce : calls) { if (fold::calleeName(ce) == "method") found_method_call = true; } REQUIRE(found_method_call); } TEST_CASE("calleeName returns empty for null", "[encoder][callee_names]") { REQUIRE(fold::calleeName(nullptr).empty()); } TEST_CASE("qualifiedFunctionName returns the fully-qualified C++ name", "[encoder][callee_names][qualified]") { auto unit = parse(R"cpp( namespace outer { namespace inner { void target() {} } } )cpp"); REQUIRE(unit); const clang::FunctionDecl* fd = functionNamed(*unit, "outer::inner::target"); REQUIRE(fd); // External linkage: no @file suffix expected. const std::string name = fold::qualifiedFunctionName(fd); REQUIRE(name == "outer::inner::target"); } TEST_CASE("qualifiedFunctionName appends file suffix for internal-linkage statics", "[encoder][callee_names][qualified][linkage]") { auto unit = parse(R"cpp( static int helper() { return 0; } )cpp"); REQUIRE(unit); const clang::FunctionDecl* fd = functionNamed(*unit, "helper"); REQUIRE(fd); const std::string name = fold::qualifiedFunctionName(fd); // Should be "helper@" — the @-suffix is what disambiguates // same-named statics across TUs. REQUIRE(name.starts_with("helper@")); REQUIRE(name.find("input.cpp") != std::string::npos); } TEST_CASE("qualifiedFunctionName returns empty for null", "[encoder][callee_names][qualified]") { REQUIRE(fold::qualifiedFunctionName(nullptr).empty()); } TEST_CASE("qualifiedCalleeName resolves through a direct CallExpr", "[encoder][callee_names][qualified]") { auto unit = parse(R"cpp( namespace ns { void target() {} } void caller() { ns::target(); } )cpp"); REQUIRE(unit); auto calls = collectCalls(*unit, "caller"); REQUIRE(calls.size() == 1); REQUIRE(fold::qualifiedCalleeName(calls[0]) == "ns::target"); } TEST_CASE("qualifiedCalleeName returns empty for null", "[encoder][callee_names][qualified]") { REQUIRE(fold::qualifiedCalleeName(nullptr).empty()); } TEST_CASE("isCLibraryCallee: only C library functions, never members or namespaced lookalikes", "[encoder-callee-names][library]") { auto unit = parse("extern \"C\" void free(void*);\n" "namespace std { using ::free; }\n" "namespace mine { void free(void*) {} }\n" "struct Pool { void free(void*) {} };\n" "void global_helper(void*) {}\n" "void f(void* p, Pool& pool, void (*fp)(void*)) {\n" " free(p);\n" " std::free(p);\n" " mine::free(p);\n" " pool.free(p);\n" " global_helper(p);\n" " fp(p);\n" "}\n"); REQUIRE(unit); const auto calls = collectCalls(*unit, "f"); REQUIRE(calls.size() == 6); CHECK(fold::isCLibraryCallee(calls[0])); // ::free (extern "C") CHECK(fold::isCLibraryCallee(calls[1])); // std::free resolves to ::free CHECK_FALSE(fold::isCLibraryCallee(calls[2])); // mine::free CHECK_FALSE(fold::isCLibraryCallee(calls[3])); // Pool::free CHECK_FALSE(fold::isCLibraryCallee(calls[4])); // global C++ function, not extern "C" CHECK_FALSE(fold::isCLibraryCallee(calls[5])); // indirect call CHECK_FALSE(fold::isCLibraryCallee(nullptr)); }