#include #include #include #include #include #include #include #include #include #include #include #include #include #include "fold/cli/incremental_fingerprint.hpp" #include "fold/cli/scan_options.hpp" #include "fold/cli/scan_router.hpp" #include "fold/cli/scan_runner.hpp" #include "fold/cli/source_call_graph.hpp" #include "fold/cli/source_summary_pipeline.hpp" #include "fold/detect/cross_tu_cache.hpp" #include "fold/detect/incremental_cache.hpp" #include "fold/detect/summary_store.hpp" #include "fold/frontend/comp_db.hpp" #include "fold/ir/call_graph.hpp" #include "fold/symbolic/encoder_callee_names.hpp" namespace { struct TempDir { std::filesystem::path path; TempDir() : path(std::filesystem::temp_directory_path() / ("fold-cross-tu-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()))) { std::filesystem::create_directories(path); } ~TempDir() { std::filesystem::remove_all(path); } }; // SummaryStore key (USR-based) of the function `name` defined in `path`, // computed from the same file the scan parsed. std::string summaryKeyIn(const std::filesystem::path& path, const std::string& name) { std::ifstream in(path); const std::string code((std::istreambuf_iterator(in)), std::istreambuf_iterator()); auto unit = clang::tooling::buildASTFromCodeWithArgs(code, {"-std=c++20"}, path.string()); if (!unit) return {}; struct Finder : clang::RecursiveASTVisitor { std::string name; const clang::FunctionDecl* found = nullptr; bool VisitFunctionDecl(clang::FunctionDecl* fd) { if (!found && fd->getNameAsString() == name) found = fd; return true; } } finder; finder.name = name; finder.TraverseDecl(unit->getASTContext().getTranslationUnitDecl()); return fold::summaryStoreKey(finder.found); } void writeFile(const std::filesystem::path& path, const std::string& content) { std::ofstream(path) << content; } void writeCompileDb(const std::filesystem::path& dir, const std::filesystem::path& caller, const std::filesystem::path& callee) { std::vector files{caller, callee}; nlohmann::json db = nlohmann::json::array(); for (const auto& file : files) { db.push_back({ {"directory", dir.string()}, {"command", std::string("clang++ -std=c++20 -c ") + file.string()}, {"file", file.string()}, }); } writeFile(dir / "compile_commands.json", db.dump()); } const fold::detect::IncrementalFunctionState* findFingerprintByName( const std::unordered_map& fingerprints, const std::string& qualified_name) { for (const auto& [_, state] : fingerprints) { if (state.fingerprint.qualified_name == qualified_name) return &state; } return nullptr; } void writeCompileDb(const std::filesystem::path& dir, const std::vector& files) { nlohmann::json db = nlohmann::json::array(); for (const auto& file : files) { db.push_back({ {"directory", dir.string()}, {"command", std::string("clang++ -std=c++20 -c ") + file.string()}, {"file", file.string()}, }); } writeFile(dir / "compile_commands.json", db.dump()); } } // namespace TEST_CASE("incremental fingerprints are deterministic and include transitive facts", "[scan][incremental][fingerprint]") { TempDir dir; const auto source = dir.path / "fingerprint.cpp"; writeFile(source, "int global_value;\n" "int leaf(int x){ global_value = x; return global_value; }\n" "int caller(int x){ return leaf(x); }\n"); writeCompileDb(dir.path, std::vector{source}); std::string err; auto db = fold::loadCompilationDatabase(dir.path.string(), err); REQUIRE(db); const auto first = fold::cli::collectFingerprints(*db, {source.string()}); const auto second = fold::cli::collectFingerprints(*db, {source.string()}); const auto* leaf = findFingerprintByName(first, "leaf"); const auto* caller = findFingerprintByName(first, "caller"); const auto* second_leaf = findFingerprintByName(second, "leaf"); const auto* second_caller = findFingerprintByName(second, "caller"); REQUIRE(leaf != nullptr); REQUIRE(caller != nullptr); REQUIRE(second_leaf != nullptr); REQUIRE(second_caller != nullptr); CHECK_FALSE(leaf->fingerprint.id.empty()); CHECK(leaf->fingerprint.source_hash != 0); CHECK(caller->fingerprint.callee_hash != 0); CHECK(caller->fingerprint.combined_hash == (caller->fingerprint.source_hash ^ caller->fingerprint.callee_hash)); CHECK(caller->summary.callees.contains(leaf->fingerprint.id)); CHECK(!leaf->summary.reads_globals.empty()); CHECK(!leaf->summary.writes_globals.empty()); CHECK(caller->summary.transitive_hash != 0); CHECK(second_leaf->fingerprint.combined_hash == leaf->fingerprint.combined_hash); CHECK(second_leaf->summary.transitive_hash == leaf->summary.transitive_hash); CHECK(second_caller->fingerprint.combined_hash == caller->fingerprint.combined_hash); CHECK(second_caller->summary.transitive_hash == caller->summary.transitive_hash); } TEST_CASE("incremental fingerprints preserve overloaded semantic identities", "[scan][incremental][fingerprint][overload]") { TempDir dir; const auto source = dir.path / "overload_fingerprint.cpp"; writeFile(source, "int overloaded(int x){ return x; }\n" "long overloaded(long x){ return x; }\n"); writeCompileDb(dir.path, std::vector{source}); std::string error; auto database = fold::loadCompilationDatabase(dir.path.string(), error); REQUIRE(database); const auto fingerprints = fold::cli::collectFingerprints(*database, {source.string()}); std::vector overload_ids; for (const auto& [function_id, state] : fingerprints) { if (state.fingerprint.qualified_name == "overloaded") { CHECK(function_id == state.fingerprint.id); overload_ids.push_back(function_id); } } REQUIRE(overload_ids.size() == 2); CHECK(overload_ids[0] != overload_ids[1]); } TEST_CASE("incremental identities separate internal functions from shared headers", "[scan][incremental][fingerprint][internal-header]") { TempDir dir; const auto header = dir.path / "shared_internal.hpp"; const auto first_source = dir.path / "first_internal.cpp"; const auto second_source = dir.path / "second_internal.cpp"; writeFile(header, "static int header_helper(){ return 1; }\n"); writeFile(first_source, "#include \"shared_internal.hpp\"\nint first_entry(){ return header_helper(); }\n"); writeFile(second_source, "#include \"shared_internal.hpp\"\nint second_entry(){ return header_helper(); }\n"); writeCompileDb(dir.path, first_source, second_source); std::string error; auto database = fold::loadCompilationDatabase(dir.path.string(), error); REQUIRE(database); const auto fingerprints = fold::cli::collectFingerprints(*database, {first_source.string(), second_source.string()}); std::vector helper_ids; for (const auto& [function_id, state] : fingerprints) { if (state.fingerprint.qualified_name == "header_helper") helper_ids.push_back(function_id); } REQUIRE(helper_ids.size() == 2); CHECK(helper_ids[0] != helper_ids[1]); } TEST_CASE("incremental identities connect cross-TU declarations and definitions", "[scan][incremental][fingerprint][cross-tu]") { TempDir dir; const auto caller_source = dir.path / "fingerprint_caller.cpp"; const auto callee_source = dir.path / "fingerprint_callee.cpp"; writeFile( caller_source, "int cross_tu_callee(int); int cross_tu_caller(int x){ return cross_tu_callee(x); }\n"); writeFile(callee_source, "int cross_tu_callee(int x){ return x + 1; }\n"); writeCompileDb(dir.path, caller_source, callee_source); std::string error; auto database = fold::loadCompilationDatabase(dir.path.string(), error); REQUIRE(database); const auto first = fold::cli::collectFingerprints(*database, {caller_source.string(), callee_source.string()}); const auto* first_caller = findFingerprintByName(first, "cross_tu_caller"); const auto* first_callee = findFingerprintByName(first, "cross_tu_callee"); REQUIRE(first_caller != nullptr); REQUIRE(first_callee != nullptr); CHECK(first_caller->summary.callees.contains(first_callee->fingerprint.id)); fold::ir::ProjectCallGraph graph; fold::cli::buildProjectCallGraph(*database, {caller_source.string(), callee_source.string()}, graph); const auto graph_callers = graph.getCallerIds(first_callee->fingerprint.id); CHECK(std::find(graph_callers.begin(), graph_callers.end(), first_caller->fingerprint.id) != graph_callers.end()); fold::detect::IncrementalCache cold; for (const auto& [_, state] : first) cold.insert(state); cold.save((dir.path / "cache").string()); writeFile(callee_source, "int cross_tu_callee(int x){ return x + 2; }\n"); const auto second = fold::cli::collectFingerprints(*database, {caller_source.string(), callee_source.string()}); const auto* second_caller = findFingerprintByName(second, "cross_tu_caller"); const auto* second_callee = findFingerprintByName(second, "cross_tu_callee"); REQUIRE(second_caller != nullptr); REQUIRE(second_callee != nullptr); auto warm = fold::detect::IncrementalCache::load((dir.path / "cache").string()); for (const auto& [_, state] : second) warm.insert(state); CHECK(warm.isStale(second_callee->fingerprint.id)); CHECK(warm.isStale(second_caller->fingerprint.id)); } TEST_CASE("incremental global dependencies use cross-TU semantic identity", "[scan][incremental][fingerprint][global]") { TempDir dir; const auto reader_source = dir.path / "global_reader.cpp"; const auto writer_source = dir.path / "global_writer.cpp"; writeFile(reader_source, "extern int shared_global; int global_reader(){ return shared_global; }\n"); writeFile(writer_source, "int shared_global; void global_writer(){ shared_global = 2; }\n"); writeCompileDb(dir.path, reader_source, writer_source); std::string error; auto database = fold::loadCompilationDatabase(dir.path.string(), error); REQUIRE(database); const auto first = fold::cli::collectFingerprints(*database, {reader_source.string(), writer_source.string()}); const auto* first_reader = findFingerprintByName(first, "global_reader"); const auto* first_writer = findFingerprintByName(first, "global_writer"); REQUIRE(first_reader != nullptr); REQUIRE(first_writer != nullptr); REQUIRE(first_reader->summary.reads_globals.size() == 1); CHECK(first_writer->summary.writes_globals.contains( *first_reader->summary.reads_globals.begin())); fold::detect::IncrementalCache cold; for (const auto& [_, state] : first) cold.insert(state); cold.save((dir.path / "cache").string()); writeFile(writer_source, "int shared_global; void global_writer(){ shared_global = 3; }\n"); const auto second = fold::cli::collectFingerprints(*database, {reader_source.string(), writer_source.string()}); const auto* second_reader = findFingerprintByName(second, "global_reader"); REQUIRE(second_reader != nullptr); auto warm = fold::detect::IncrementalCache::load((dir.path / "cache").string()); for (const auto& [_, state] : second) warm.insert(state); CHECK(warm.isStale(second_reader->fingerprint.id)); } TEST_CASE("incremental cache ignores mtime-only changes", "[scan][incremental][fingerprint][mtime]") { TempDir dir; fold::detect::IncrementalFunctionState state; state.fingerprint.id = "usr:stable"; state.fingerprint.file = "stable.cpp"; state.fingerprint.qualified_name = "stable"; state.fingerprint.source_hash = 11; state.fingerprint.callee_hash = 13; state.fingerprint.combined_hash = 6; state.fingerprint.last_modified = 17; fold::detect::IncrementalCache cold; cold.insert(state); cold.save(dir.path.string()); auto warm = fold::detect::IncrementalCache::load(dir.path.string()); state.fingerprint.last_modified = 19; warm.insert(state); CHECK_FALSE(warm.isStale(state.fingerprint.id)); } TEST_CASE("incremental cache rejects malformed versioned state", "[scan][incremental][fingerprint][corrupt]") { TempDir dir; const auto cache_dir = dir.path / "cache"; std::filesystem::create_directories(cache_dir); writeFile(cache_dir / "fingerprints.bin", "v3\nusr:broken\tbroken.cpp\tbroken\tnot-a-number\t0\t0\t0\t\t\t\t\t0\n"); fold::detect::IncrementalFunctionState current; current.fingerprint.id = "usr:current"; current.fingerprint.file = "current.cpp"; current.fingerprint.qualified_name = "current"; current.fingerprint.source_hash = 1; auto cache = fold::detect::IncrementalCache::load(cache_dir.string()); cache.insert(current); CHECK(cache.isStale(current.fingerprint.id)); writeFile(cache_dir / "fingerprints.bin", "v3\nusr:broken\tbroken.cpp\tbroken\t1\t0\t0\t0\t" "18446744073709551616:\t\t\t\t0\n"); auto overflowed = fold::detect::IncrementalCache::load(cache_dir.string()); overflowed.insert(current); CHECK(overflowed.isStale(current.fingerprint.id)); } TEST_CASE("incremental input changes outside function bodies invalidate semantic work", "[scan][incremental][fingerprint][macro]") { TempDir dir; const auto source = dir.path / "macro_change.cpp"; writeFile(source, "#define DIVISOR 1\nint macro_divide(){ return 10 / DIVISOR; }\n"); writeCompileDb(dir.path, std::vector{source}); std::string error; auto database = fold::loadCompilationDatabase(dir.path.string(), error); REQUIRE(database); fold::cli::ScanOptions options; options.detectors = {"div-by-zero"}; options.incremental = true; options.cache_dir = (dir.path / "cache").string(); options.profile = true; const auto cold = fold::cli::runDetectorsOnShard(*database, options, {source.string()}, true); CHECK(cold.findings.empty()); writeFile(source, "#define DIVISOR 0\nint macro_divide(){ return 10 / DIVISOR; }\n"); const auto changed = fold::cli::runDetectorsOnShard(*database, options, {source.string()}, true); REQUIRE(changed.profile.has_value()); CHECK(changed.profile->project_index_input_mismatches == 1); CHECK(changed.profile->functions_stale == 1); CHECK_FALSE(changed.findings.empty()); } TEST_CASE("dead-statement honors incremental function filtering", "[scan][incremental][dead-statement]") { TempDir dir; const auto source = dir.path / "dead_statement_incremental.cpp"; writeFile(source, "int stale_candidate(){ return 1; int unreachable = 2; return unreachable; }\n"); writeCompileDb(dir.path, std::vector{source}); std::string error; auto database = fold::loadCompilationDatabase(dir.path.string(), error); REQUIRE(database); fold::cli::ScanOptions options; options.detectors = {"dead-statement"}; options.incremental = true; options.cache_dir = (dir.path / "cache").string(); options.profile = true; const auto cold = fold::cli::runDetectorsOnShard(*database, options, {source.string()}, true); REQUIRE_FALSE(cold.findings.empty()); const auto warm = fold::cli::runDetectorsOnShard(*database, options, {source.string()}, true); CHECK(warm.findings.empty()); REQUIRE(warm.profile.has_value()); CHECK(warm.profile->functions_stale == 0); } TEST_CASE("scan_runner prepass supports cross-TU security lookup", "[scan][crosstu][security]") { TempDir dir; const auto caller = dir.path / "caller.cpp"; const auto callee = dir.path / "callee.cpp"; writeFile(caller, "int sink_null(int* p); int sink_div(int x, int y); int sink_of(int a, int b);\n" "int f(int x, int y, int a, int b){ int* p = nullptr; sink_null(p); sink_div(x, y); " "return sink_of(a, b); }\n"); writeFile(callee, "int sink_null(int* p){ return *p; }\n" "int sink_div(int x, int y){ return x / y; }\n" "int sink_of(int a, int b){ return a + b; }\n"); writeCompileDb(dir.path, caller, callee); std::string err; auto db = fold::loadCompilationDatabase(dir.path.string(), err); REQUIRE(db); fold::cli::ScanOptions opts; opts.compile_db = dir.path.string(); opts.detectors = {"null-deref", "div-by-zero", "int-overflow"}; auto result = fold::cli::runDetectorsOnShard(*db, opts, {caller.string(), callee.string()}, /*include_clone_clustering=*/false); bool saw_null = false; bool saw_div = false; bool saw_overflow = false; std::size_t cross_tu_calls = 0; std::size_t explicit_preconditions = 0; for (const auto& finding : result.findings) { saw_null = saw_null || finding.kind == fold::FindingKind::NullDeref; saw_div = saw_div || finding.kind == fold::FindingKind::DivByZero; saw_overflow = saw_overflow || finding.kind == fold::FindingKind::IntOverflow; if (finding.id.find(":call:") != std::string::npos) { ++cross_tu_calls; CHECK(finding.evidence.is_summary_backed); CHECK(finding.evidence.has_cross_tu_corroboration); CHECK(finding.evidence.proof_mode != "exact"); if (finding.evidence.proof_mode == "precondition") ++explicit_preconditions; } } REQUIRE(saw_null); REQUIRE(saw_div); REQUIRE(saw_overflow); REQUIRE(cross_tu_calls >= 3); REQUIRE(explicit_preconditions >= 2); } TEST_CASE("local security findings do not claim summary or cross-TU evidence", "[scan][evidence][local]") { TempDir dir; const auto source = dir.path / "local.cpp"; writeFile(source, "int divide_local(int x){ int zero = 0; return x / zero; }\n" "int overflow_local(int x){ return x + 1; }\n"); writeCompileDb(dir.path, std::vector{source}); std::string err; auto db = fold::loadCompilationDatabase(dir.path.string(), err); REQUIRE(db); fold::cli::ScanOptions opts; opts.compile_db = dir.path.string(); opts.detectors = {"div-by-zero", "int-overflow"}; auto result = fold::cli::runScanRouter(opts); REQUIRE_FALSE(result.findings.empty()); for (const auto& finding : result.findings) { CHECK_FALSE(finding.evidence.is_summary_backed); CHECK_FALSE(finding.evidence.has_cross_tu_corroboration); } } TEST_CASE("same-TU guarded division does not acquire a cached callsite warning", "[scan][crosstu][guarded][same-tu]") { TempDir dir; const auto source = dir.path / "guarded.cpp"; writeFile(source, "int divide(int x, int denominator){ if(denominator == 0) return 0; " "return x / denominator; }\n" "int entry(){ return divide(42, 0); }\n"); writeCompileDb(dir.path, std::vector{source}); fold::cli::ScanOptions opts; opts.compile_db = dir.path.string(); opts.detectors = {"div-by-zero"}; auto result = fold::cli::runScanRouter(opts); REQUIRE(result.findings.empty()); } TEST_CASE("symbolic summary priming reaches recursive SCC fixpoint", "[scan][summary][scc]") { fold::detect::SummaryStore::instance().clear(); TempDir dir; const auto caller = dir.path / "caller.cpp"; const auto callee = dir.path / "callee.cpp"; writeFile(caller, "int b(int);\n" "int a(int x){ return b(x); }\n" "int entry(int z){ if (z != 0) return a(z); return 0; }\n"); writeFile(callee, "int a(int);\n" "int b(int x){ return 10 / x + a(x - 1); }\n"); writeCompileDb(dir.path, caller, callee); std::string err; auto db = fold::loadCompilationDatabase(dir.path.string(), err); REQUIRE(db); fold::cli::ScanOptions opts; opts.compile_db = dir.path.string(); opts.detectors = {"div-by-zero"}; opts.build_summaries = "1"; auto result = fold::cli::runDetectorsOnShard(*db, opts, {caller.string(), callee.string()}, /*include_clone_clustering=*/false); const auto* a_summary = fold::detect::SummaryStore::instance().lookup(summaryKeyIn(caller, "a")); const auto* b_summary = fold::detect::SummaryStore::instance().lookup(summaryKeyIn(callee, "b")); REQUIRE(b_summary != nullptr); REQUIRE(a_summary != nullptr); REQUIRE(!a_summary->param_pre.empty()); bool saw_recursive_callsite = false; for (const auto& finding : result.findings) { saw_recursive_callsite = saw_recursive_callsite || (finding.kind == fold::FindingKind::DivByZero && finding.id.find("div-by-zero:call:") != std::string::npos && finding.message.find("`a`") != std::string::npos); } REQUIRE(saw_recursive_callsite); } TEST_CASE("symbolic summaries distinguish internal linkage functions by file", "[scan][summary][crosstu]") { fold::detect::SummaryStore::instance().clear(); TempDir dir; const auto unsafe = dir.path / "unsafe.cpp"; const auto safe = dir.path / "safe.cpp"; writeFile(unsafe, "static int require_nonzero(int x){ return 10 / x; }\n" "int unsafe_entry(int y){ return require_nonzero(y); }\n"); writeFile(safe, "static int require_nonzero(int x){ return x; }\n" "int safe_entry(int y){ return require_nonzero(y); }\n"); writeCompileDb(dir.path, std::vector{unsafe, safe}); std::string err; auto db = fold::loadCompilationDatabase(dir.path.string(), err); REQUIRE(db); fold::cli::ScanOptions opts; opts.compile_db = dir.path.string(); opts.detectors = {"div-by-zero"}; opts.build_summaries = "1"; (void)fold::cli::runDetectorsOnShard(*db, opts, {unsafe.string(), safe.string()}, /*include_clone_clustering=*/false); const auto* unsafe_summary = fold::detect::SummaryStore::instance().lookup(summaryKeyIn(unsafe, "require_nonzero")); const auto* safe_summary = fold::detect::SummaryStore::instance().lookup(summaryKeyIn(safe, "require_nonzero")); REQUIRE(unsafe_summary != nullptr); REQUIRE(safe_summary != nullptr); REQUIRE(!unsafe_summary->param_pre.empty()); REQUIRE(safe_summary->param_pre.empty()); } TEST_CASE("Clang call graph adapter records constructor edges", "[scan][callgraph][clang]") { TempDir dir; const auto source = dir.path / "ctor.cpp"; writeFile(source, "int sink(int x){ return 10 / x; }\n" "struct Wrapper { explicit Wrapper(int x){ sink(x); } };\n" "int entry(int z){ Wrapper w(z); return z; }\n"); writeCompileDb(dir.path, std::vector{source}); std::string err; auto db = fold::loadCompilationDatabase(dir.path.string(), err); REQUIRE(db); fold::ir::ProjectCallGraph graph; fold::cli::primeCrossTUProperties(*db, {source.string()}, {"div-by-zero"}, &graph, dir.path.string()); const auto entry_callees = graph.getCallees("entry"); bool saw_ctor = false; std::string ctor_name; for (const auto& callee : entry_callees) { if (callee.find("Wrapper") != std::string::npos) { saw_ctor = true; ctor_name = callee; } } REQUIRE(saw_ctor); const auto ctor_callees = graph.getCallees(ctor_name); bool saw_sink = false; for (const auto& callee : ctor_callees) saw_sink = saw_sink || callee == "sink"; REQUIRE(saw_sink); } TEST_CASE("source scans persist, reuse, invalidate, and fail closed on corrupt project graphs", "[scan][callgraph][persistent]") { TempDir dir; const auto source = dir.path / "persistent.cpp"; writeFile(source, "int divide(int x){ return 10 / x; }\n" "int entry(int x){ return divide(x); }\n"); writeCompileDb(dir.path, std::vector{source}); std::string error; auto database = fold::loadCompilationDatabase(dir.path.string(), error); REQUIRE(database); fold::cli::ScanOptions options; options.detectors = {"div-by-zero"}; options.incremental = true; options.cache_dir = (dir.path / "cache").string(); options.profile = true; const auto cold = fold::cli::runDetectorsOnShard(*database, options, {source.string()}, true); REQUIRE(cold.profile.has_value()); CHECK(cold.profile->project_index_loaded_nodes == 0); CHECK(cold.profile->project_index_changed_functions == 2); const auto warm = fold::cli::runDetectorsOnShard(*database, options, {source.string()}, true); REQUIRE(warm.profile.has_value()); CHECK(warm.profile->project_index_loaded_nodes >= 2); CHECK(warm.profile->project_index_reused_functions == 2); CHECK(warm.profile->project_index_changed_functions == 0); writeFile(source, "int divide(int x){ return 20 / x; }\n" "int entry(int x){ return divide(x); }\n"); const auto changed = fold::cli::runDetectorsOnShard(*database, options, {source.string()}, true); REQUIRE(changed.profile.has_value()); CHECK(changed.profile->project_index_changed_functions == 1); CHECK(changed.profile->project_index_invalidated_dependents >= 1); const auto index_path = dir.path / "cache" / "project_index.sqlite"; std::filesystem::remove(index_path); writeFile(index_path, "corrupt"); const auto recovered = fold::cli::runDetectorsOnShard(*database, options, {source.string()}, true); REQUIRE(recovered.profile.has_value()); CHECK(recovered.profile->project_index_fallbacks == 1); CHECK_FALSE(recovered.findings.empty()); const auto detector = std::find_if( recovered.profile->detectors.begin(), recovered.profile->detectors.end(), [](const fold::cli::DetectorProfile& profile) { return profile.name == "div-by-zero"; }); REQUIRE(detector != recovered.profile->detectors.end()); CHECK(detector->functions_analyzed == recovered.profile->num_functions); CHECK(detector->functions_skipped == 0); } TEST_CASE("summary targeting follows every semantic overload path", "[scan][summary][callgraph]") { TempDir dir; const auto source = dir.path / "overloads.cpp"; writeFile(source, R"cpp( int overloaded(int value) { return 10 / value; } int overloaded(long value) { return 20 / static_cast(value); } int caller_a(int value) { return overloaded(value); } int caller_b(long value) { return overloaded(value); } )cpp"); writeCompileDb(dir.path, std::vector{source}); std::string error; auto database = fold::loadCompilationDatabase(dir.path.string(), error); REQUIRE(database); fold::ir::ProjectCallGraph graph; fold::cli::buildProjectCallGraph(*database, {source.string()}, graph); auto& cache = fold::detect::CrossTUCache::instance(); cache.clear(); fold::detect::CalleeProperties properties; properties.divisor_params.insert(0); cache.insert("overloaded", properties); fold::detect::SummaryStore::instance().clear(); const auto telemetry = fold::cli::primeSymbolicSummaries(*database, {"div-by-zero"}, graph); CHECK(telemetry.target_functions == 3); // overloaded + both semantic callers cache.clear(); fold::detect::SummaryStore::instance().clear(); } TEST_CASE("project call graph resolves virtual and signature-compatible targets", "[scan][callgraph][precision]") { TempDir dir; const auto source = dir.path / "dispatch.cpp"; writeFile(source, "struct Base { virtual int check(int x){ return x; } };\n" "struct Derived : Base { int check(int x) override { return 10 / x; } };\n" "int virtual_entry(Base& value, int x){ return value.check(x); }\n" "int indirect_target(int x, int y){ return x / y; }\n" "using BinaryFn = int (*)(int, int);\n" "int indirect_entry(BinaryFn fn, int x, int y){ return fn(x, y); }\n" "using UnknownFn = long (*)(char*);\n" "long unknown_entry(UnknownFn fn, char* p){ return fn(p); }\n" "int overloaded(int x){ return x; }\n" "double overloaded(double x){ return x; }\n" "int overload_entry(){ return overloaded(1); }\n"); writeCompileDb(dir.path, std::vector{source}); std::string error; auto database = fold::loadCompilationDatabase(dir.path.string(), error); REQUIRE(database); fold::ir::ProjectCallGraph graph; fold::cli::buildProjectCallGraph(*database, {source.string()}, graph); const auto virtual_targets = graph.getCallees("virtual_entry"); CHECK(std::find(virtual_targets.begin(), virtual_targets.end(), "Base::check") != virtual_targets.end()); CHECK(std::find(virtual_targets.begin(), virtual_targets.end(), "Derived::check") != virtual_targets.end()); const auto indirect_targets = graph.getCallees("indirect_entry"); CHECK(std::find(indirect_targets.begin(), indirect_targets.end(), "indirect_target") != indirect_targets.end()); const auto unknown_ids = graph.getCalleeIds("unknown_entry"); REQUIRE(unknown_ids.size() == 1); CHECK(graph.getNode(unknown_ids.front())->qualified_name == ""); CHECK(graph.edgeCount(fold::ir::CallGraphEdgeKind::Unknown) == 1); CHECK(graph.getNode("overloaded") == nullptr); // overload lookup is intentionally ambiguous CHECK(graph.getCalleeIds("overload_entry").size() == 1); }