#include #include #include #include #include #include #include #include #include #include #include "fold/cli/project_graph_orchestrator.hpp" #include "fold/cli/source_call_graph.hpp" #include "fold/config/configuration_profile.hpp" #include "fold/frontend/comp_db.hpp" #include "fold/index/store.hpp" #include "fold/ir/call_graph.hpp" namespace { struct TempDir { std::filesystem::path path; TempDir() { const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count(); path = std::filesystem::temp_directory_path() / ("fold-incremental-project-graph-" + std::to_string(stamp)); std::filesystem::create_directories(path); } ~TempDir() { std::filesystem::remove_all(path); } }; void writeFile(const std::filesystem::path& path, const std::string& content) { std::ofstream output(path); REQUIRE(output.good()); output << content; } void writeCompileDb(const std::filesystem::path& directory, const std::vector& files, const std::string& extra_flags = {}, const std::filesystem::path& command_directory = {}) { nlohmann::json commands = nlohmann::json::array(); for (const auto& file : files) { nlohmann::json arguments = {"clang++", "-std=c++20"}; if (!extra_flags.empty()) arguments.push_back(extra_flags); arguments.push_back("-c"); arguments.push_back(file.string()); commands.push_back( {{"directory", (command_directory.empty() ? directory : command_directory).string()}, {"file", file.string()}, {"arguments", arguments}}); } writeFile(directory / "compile_commands.json", commands.dump(2)); } std::unique_ptr loadDb(const std::filesystem::path& path) { std::string error; auto database = fold::loadCompilationDatabase(path.string(), error); INFO(error); REQUIRE(database); return database; } struct GraphSnapshot { std::vector, std::vector>> nodes; std::vector> edges; bool operator==(const GraphSnapshot&) const = default; }; GraphSnapshot snapshot(const fold::ir::ProjectCallGraph& graph) { GraphSnapshot result; for (const auto& node : graph.nodes()) { result.nodes.emplace_back( node.id, node.qualified_name, node.source_file, node.body_hash, node.scc_id, node.is_defined, node.escapes, std::vector(node.callees.begin(), node.callees.end()), std::vector(node.callers.begin(), node.callers.end())); } for (const auto& edge : graph.edges()) { result.edges.emplace_back(edge.caller_id, edge.callee_id, static_cast(edge.kind), edge.confidence); } return result; } fold::ir::ProjectCallGraph exhaustive(clang::tooling::CompilationDatabase& database, const std::vector& files) { fold::ir::ProjectCallGraph graph; fold::cli::buildProjectCallGraph(database, files, graph); return graph; } void checkMatchesExhaustive(clang::tooling::CompilationDatabase& database, const std::vector& files, const fold::ir::ProjectCallGraph& incremental) { const auto expected = exhaustive(database, files); CHECK(snapshot(incremental) == snapshot(expected)); } std::size_t countNamed(const fold::ir::ProjectCallGraph& graph, const std::string& name) { const auto nodes = graph.nodes(); return static_cast( std::count_if(nodes.begin(), nodes.end(), [&name](const auto& node) { return node.qualified_name == name; })); } std::size_t countNamePrefix(const fold::ir::ProjectCallGraph& graph, const std::string& prefix) { const auto nodes = graph.nodes(); return static_cast( std::count_if(nodes.begin(), nodes.end(), [&prefix](const auto& node) { return node.qualified_name.starts_with(prefix); })); } } // namespace TEST_CASE("warm project graph reuses unchanged TU contributions without parsing", "[project-graph][incremental][warm]") { TempDir directory; const auto header = directory.path / "shared.hpp"; const auto caller = directory.path / "caller.cpp"; const auto callee = directory.path / "callee.cpp"; writeFile(header, "static int header_local(){ return 3; }\n"); writeFile(caller, "#include \"shared.hpp\"\n" "int cross_tu(int); int overloaded(int); double overloaded(double);\n" "int entry(){ return cross_tu(1) + overloaded(1) + header_local(); }\n"); writeFile(callee, "#include \"shared.hpp\"\n" "int cross_tu(int x){ return x + header_local(); }\n" "int overloaded(int x){ return x; }\n" "double overloaded(double x){ return x; }\n"); writeCompileDb(directory.path, {caller, callee}); auto database = loadDb(directory.path); const std::vector files{caller.string(), callee.string()}; fold::index::ProjectIndex index((directory.path / "project.sqlite").string()); REQUIRE(index.isOpen()); fold::ir::ProjectCallGraph cold_graph; const auto cold = fold::cli::assembleProjectCallGraph(*database, files, &index, cold_graph); REQUIRE_FALSE(cold.exhaustive_fallback); CHECK(cold.parsed_tus == 2); checkMatchesExhaustive(*database, files, cold_graph); fold::ir::ProjectCallGraph warm_graph; const auto warm = fold::cli::assembleProjectCallGraph(*database, files, &index, warm_graph); REQUIRE_FALSE(warm.exhaustive_fallback); CHECK(warm.parsed_tus == 0); CHECK(warm.reused_tus == 2); CHECK(countNamed(warm_graph, "overloaded") == 2); CHECK(countNamePrefix(warm_graph, "header_local@") == 2); checkMatchesExhaustive(*database, files, warm_graph); } TEST_CASE("reachability-only assembly reuses unresolved contributions without proving closure", "[project-graph][incremental][production-reachability]") { TempDir directory; const auto source = directory.path / "indirect.cpp"; writeFile(source, "using Callback=void(*)(); static void target(){} " "static void invoke(Callback callback){ callback(); } " "int main(){ invoke(target); return 0; }\n"); writeCompileDb(directory.path, {source}); auto database = loadDb(directory.path); const std::vector files = {source.string()}; fold::index::ProjectIndex index((directory.path / "index.sqlite").string()); REQUIRE(index.isOpen()); fold::ir::ProjectCallGraph graph; fold::cli::ProjectGraphEvidence cold_evidence; const auto cold = fold::cli::assembleProjectCallGraph(*database, files, &index, graph, nullptr, &cold_evidence, true); CHECK_FALSE(cold.exhaustive_fallback); CHECK(cold.parsed_tus == 1); CHECK(cold_evidence.complete); CHECK_FALSE(cold_evidence.roots.closed_world); CHECK(cold_evidence.roots.has_unresolved_dynamic_calls); fold::cli::ProjectGraphEvidence warm_evidence; const auto warm = fold::cli::assembleProjectCallGraph(*database, files, &index, graph, nullptr, &warm_evidence, true); CHECK_FALSE(warm.exhaustive_fallback); CHECK(warm.parsed_tus == 0); CHECK(warm.reused_tus == 1); CHECK(warm_evidence.complete); CHECK_FALSE(warm_evidence.roots.closed_world); } TEST_CASE("production callback roots survive cross-TU declaration and definition merging", "[project-graph][incremental][production-roots]") { TempDir directory; const auto callback = directory.path / "callback.cpp"; const auto entry = directory.path / "entry.cpp"; writeFile(callback, "void callback() {}\n"); writeFile(entry, "void register_callback(void (*)()); void callback(); " "int main(){ register_callback(callback); return 0; }\n"); writeCompileDb(directory.path, {callback, entry}); auto database = loadDb(directory.path); const std::vector files = {callback.string(), entry.string()}; fold::index::ProjectIndex index((directory.path / "index.sqlite").string()); REQUIRE(index.isOpen()); fold::ir::ProjectCallGraph graph; const auto cold = fold::cli::assembleProjectCallGraph(*database, files, &index, graph); REQUIRE_FALSE(cold.exhaustive_fallback); CHECK(cold.production_roots == 2); CHECK(cold.root_inventory_closed_world); checkMatchesExhaustive(*database, files, graph); const auto loaded = index.loadTranslationUnitContributions(); REQUIRE(loaded.status == fold::index::ContributionLoadStatus::Ok); std::vector roots; for (const auto& contribution : loaded.contributions) roots.insert(roots.end(), contribution.production_roots.begin(), contribution.production_roots.end()); const auto callback_root = std::ranges::find_if(roots, [](const auto& root) { return root.qualified_name == "callback" && root.kind == fold::analysis::ProductionRootKind::RegisteredCallback; }); REQUIRE(callback_root != roots.end()); const auto* callback_node = graph.getNode(callback_root->function_usr); REQUIRE(callback_node != nullptr); CHECK(callback_node->is_defined); CHECK(callback_node->escapes); const auto warm = fold::cli::assembleProjectCallGraph(*database, files, &index, graph); CHECK(warm.parsed_tus == 0); CHECK(warm.reused_tus == 2); CHECK(warm.production_roots == 2); } TEST_CASE("configuration profiles persist projected callsite guards and invalidate reuse", "[project-graph][incremental][configuration]") { TempDir directory; const auto source = directory.path / "guarded.cpp"; writeFile(source, "bool feature; int runtime; void enabled(){} void disabled(){} " "void entry(){ if (feature) enabled(); else disabled(); " "if (feature + runtime) enabled(); if (runtime) disabled(); }\n"); writeCompileDb(directory.path, {source}); auto database = loadDb(directory.path); const std::vector files = {source.string()}; fold::index::ProjectIndex index((directory.path / "index.sqlite").string()); REQUIRE(index.isOpen()); const auto disabled = fold::config::parseConfigurationProfileText( R"({"version":1,"entrypoints":["entry"],"assumptions":[{"kind":"global","selector":"feature","value":false}]})"); REQUIRE(disabled); fold::ir::ProjectCallGraph graph; fold::cli::ProjectGraphEvidence cold_evidence; const auto cold = fold::cli::assembleProjectCallGraph(*database, files, &index, graph, &*disabled.profile, &cold_evidence); REQUIRE_FALSE(cold.exhaustive_fallback); CHECK(cold.parsed_tus == 1); CHECK(cold.guarded_callsites == 4); CHECK(cold.projected_guards == 2); CHECK(cold.unconditional_guards == 1); CHECK(cold.unknown_guards == 1); CHECK(cold.production_roots == 1); CHECK(cold.test_roots == 0); CHECK(cold.conditional_roots == 0); CHECK(cold.unresolved_explicit_roots == 0); CHECK(cold.root_inventory_closed_world); CHECK(cold_evidence.complete); CHECK(cold_evidence.roots.closed_world); CHECK(cold_evidence.guarded_calls.size() == 4); checkMatchesExhaustive(*database, files, graph); const auto loaded = index.loadTranslationUnitContributions(); REQUIRE(loaded.status == fold::index::ContributionLoadStatus::Ok); REQUIRE(loaded.contributions.size() == 1); REQUIRE(loaded.contributions.front().guarded_calls.size() == 4); REQUIRE(loaded.contributions.front().production_roots.size() == 1); CHECK(loaded.contributions.front().production_roots.front().kind == fold::analysis::ProductionRootKind::Explicit); std::size_t projected = 0; std::size_t unconditional = 0; std::size_t unknown = 0; for (const auto& callsite : loaded.contributions.front().guarded_calls) { const auto serialized = fold::ir::configGuardToJson(callsite.guard).dump(); CHECK(serialized.find("feature") == std::string::npos); projected += callsite.guard.status == fold::ir::ConfigGuardStatus::Projected; unconditional += callsite.guard.status == fold::ir::ConfigGuardStatus::Unconditional; unknown += callsite.guard.status == fold::ir::ConfigGuardStatus::Unknown; } CHECK(projected == 2); CHECK(unconditional == 1); CHECK(unknown == 1); fold::cli::ProjectGraphEvidence warm_evidence; const auto warm = fold::cli::assembleProjectCallGraph(*database, files, &index, graph, &*disabled.profile, &warm_evidence); CHECK(warm.parsed_tus == 0); CHECK(warm.reused_tus == 1); CHECK(warm_evidence.complete); CHECK(warm_evidence.guarded_calls == cold_evidence.guarded_calls); CHECK(warm_evidence.roots.production_roots == cold_evidence.roots.production_roots); const auto enabled = fold::config::parseConfigurationProfileText( R"({"version":1,"entrypoints":["entry"],"assumptions":[{"kind":"global","selector":"feature","value":true}]})"); REQUIRE(enabled); const auto changed = fold::cli::assembleProjectCallGraph(*database, files, &index, graph, &*enabled.profile); CHECK(changed.parsed_tus == 1); CHECK(changed.reused_tus == 0); checkMatchesExhaustive(*database, files, graph); } TEST_CASE("incremental project graph matches exhaustive after edits dependencies flags and removal", "[project-graph][incremental][invalidate]") { TempDir directory; const auto header = directory.path / "value.hpp"; const auto caller = directory.path / "caller.cpp"; const auto callee = directory.path / "callee.cpp"; writeFile(header, "#define HEADER_VALUE 1\n"); writeFile(caller, "#include \"value.hpp\"\nint callee(int); " "int caller(){ return callee(HEADER_VALUE); }\n"); writeFile(callee, "#include \"value.hpp\"\nint callee(int x){ return x + HEADER_VALUE; }\n"); writeCompileDb(directory.path, {caller, callee}, "-include" + header.string()); auto database = loadDb(directory.path); std::vector files{caller.string(), callee.string()}; fold::index::ProjectIndex index((directory.path / "project.sqlite").string()); fold::ir::ProjectCallGraph graph; REQUIRE(fold::cli::assembleProjectCallGraph(*database, files, &index, graph).parsed_tus == 2); writeFile(callee, "#include \"value.hpp\"\nint callee(int x){ return x + HEADER_VALUE + 1; }\n"); auto edited = fold::cli::assembleProjectCallGraph(*database, files, &index, graph); CHECK(edited.parsed_tus == 1); CHECK(edited.dirty_tus == 1); checkMatchesExhaustive(*database, files, graph); writeFile(header, "#define HEADER_VALUE 2\n"); auto header_changed = fold::cli::assembleProjectCallGraph(*database, files, &index, graph); CHECK(header_changed.parsed_tus == 2); CHECK(header_changed.dirty_tus == 2); checkMatchesExhaustive(*database, files, graph); writeCompileDb(directory.path, {caller, callee}, "-DGRAPH_FLAG=1"); database = loadDb(directory.path); auto flags_changed = fold::cli::assembleProjectCallGraph(*database, files, &index, graph); CHECK(flags_changed.parsed_tus == 2); CHECK(flags_changed.dirty_tus == 2); checkMatchesExhaustive(*database, files, graph); files = {caller.string()}; auto removed = fold::cli::assembleProjectCallGraph(*database, files, &index, graph); CHECK(removed.parsed_tus == 0); CHECK(removed.removed_tus == 1); checkMatchesExhaustive(*database, files, graph); const auto loaded = index.loadTranslationUnitContributions(); REQUIRE(loaded.status == fold::index::ContributionLoadStatus::Ok); REQUIRE(loaded.contributions.size() == 1); CHECK(loaded.contributions.front().fingerprint.source_path == std::filesystem::weakly_canonical(caller).string()); } TEST_CASE("compile-command working directory invalidates TU contribution reuse", "[project-graph][incremental][invalidate]") { TempDir directory; const auto first_working_directory = directory.path / "first"; const auto second_working_directory = directory.path / "second"; std::filesystem::create_directories(first_working_directory); std::filesystem::create_directories(second_working_directory); const auto source = directory.path / "entry.cpp"; writeFile(source, "int entry(){ return 1; }\n"); const std::vector source_paths{source}; const std::vector files{source.string()}; writeCompileDb(directory.path, source_paths, {}, first_working_directory); auto database = loadDb(directory.path); fold::index::ProjectIndex index((directory.path / "project.sqlite").string()); fold::ir::ProjectCallGraph graph; REQUIRE(fold::cli::assembleProjectCallGraph(*database, files, &index, graph).parsed_tus == 1); writeCompileDb(directory.path, source_paths, {}, second_working_directory); database = loadDb(directory.path); const auto changed = fold::cli::assembleProjectCallGraph(*database, files, &index, graph); CHECK(changed.dirty_tus == 1); CHECK(changed.parsed_tus == 1); } TEST_CASE("unresolved virtual function-pointer evidence resolves in contribution merge", "[project-graph][incremental][resolution]") { TempDir directory; const auto source = directory.path / "dynamic.cpp"; writeFile(source, "struct Base { virtual int run(); };\n" "struct Derived : Base { int run() override { return 1; } };\n" "int virtual_call(Base* value){ return value->run(); }\n" "int indirect(int (*function)(double)){ return function(1.0); }\n"); writeCompileDb(directory.path, {source}); auto database = loadDb(directory.path); const std::vector files{source.string()}; fold::index::ProjectIndex index((directory.path / "project.sqlite").string()); fold::ir::ProjectCallGraph cold_graph; const auto cold = fold::cli::assembleProjectCallGraph(*database, files, &index, cold_graph); REQUIRE_FALSE(cold.exhaustive_fallback); CHECK(cold.parsed_tus == 1); CHECK(cold_graph.edgeCount(fold::ir::CallGraphEdgeKind::Conservative) >= 1); CHECK(cold_graph.edgeCount(fold::ir::CallGraphEdgeKind::Unknown) >= 1); checkMatchesExhaustive(*database, files, cold_graph); // The warm run reuses the stored contribution: unresolved calls are // resolved from the persisted cross-TU resolution facts instead of // re-parsing every TU through the exhaustive fallback. fold::ir::ProjectCallGraph warm_graph; const auto warm = fold::cli::assembleProjectCallGraph(*database, files, &index, warm_graph); REQUIRE_FALSE(warm.exhaustive_fallback); CHECK(warm.parsed_tus == 0); CHECK(warm.reused_tus == 1); CHECK(warm_graph.edgeCount(fold::ir::CallGraphEdgeKind::Conservative) >= 1); CHECK(warm_graph.edgeCount(fold::ir::CallGraphEdgeKind::Unknown) >= 1); checkMatchesExhaustive(*database, files, warm_graph); }