From 284c5b53fcf7eb40ee5ef880699f13be5e487164 Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 9 Feb 2026 16:15:10 -0700 Subject: [PATCH] Step 128: parse dependency files into imports --- editor/CMakeLists.txt | 4 + editor/src/DependencyParser.h | 317 ++++++++++++++++++++++++++++++++++ editor/tests/step128_test.cpp | 61 +++++++ sprint5_plan.md | 2 +- 4 files changed, 383 insertions(+), 1 deletion(-) create mode 100644 editor/src/DependencyParser.h create mode 100644 editor/tests/step128_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 1e29710..b29128e 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -724,6 +724,10 @@ add_executable(step127_test tests/step127_test.cpp) target_include_directories(step127_test PRIVATE src) target_link_libraries(step127_test PRIVATE nlohmann_json::nlohmann_json) +add_executable(step128_test tests/step128_test.cpp) +target_include_directories(step128_test PRIVATE src) +target_link_libraries(step128_test PRIVATE nlohmann_json::nlohmann_json) + find_package(SDL2 CONFIG REQUIRED) find_package(OpenGL REQUIRED) find_package(glad CONFIG REQUIRED) diff --git a/editor/src/DependencyParser.h b/editor/src/DependencyParser.h new file mode 100644 index 0000000..0f55de8 --- /dev/null +++ b/editor/src/DependencyParser.h @@ -0,0 +1,317 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include "ast/Import.h" +#include "ast/Module.h" + +struct DependencySpec { + std::string name; + std::string version; + std::string source; // file or ecosystem hint +}; + +class DependencyParser { +public: + static std::vector parseFile(const std::string& path) { + std::filesystem::path p(path); + std::string filename = p.filename().string(); + if (filename == "requirements.txt") return parseRequirements(path); + if (filename == "package.json") return parsePackageJson(path); + if (filename == "Cargo.toml") return parseCargoToml(path); + if (filename == "go.mod") return parseGoMod(path); + if (filename == "pom.xml") return parsePomXml(path); + if (filename == "build.gradle") return parseGradle(path); + if (filename == "vcpkg.json") return parseVcpkgJson(path); + if (filename == "CMakeLists.txt") return parseCMakeLists(path); + if (filename == "setup.py" || filename == "pyproject.toml") return parsePythonMeta(path); + return {}; + } + + static void populateImports(Module* module, const std::vector& deps) { + if (!module) return; + for (const auto& dep : deps) { + if (dep.name.empty()) continue; + auto* imp = new Import("imp_" + dep.name, dep.name, "module"); + module->addChild("imports", imp); + } + } + +private: + static std::string trim(const std::string& s) { + size_t start = s.find_first_not_of(" \t\r\n"); + size_t end = s.find_last_not_of(" \t\r\n"); + if (start == std::string::npos || end == std::string::npos) return ""; + return s.substr(start, end - start + 1); + } + + static std::vector parseRequirements(const std::string& path) { + std::vector out; + std::ifstream in(path); + if (!in.is_open()) return out; + std::string line; + while (std::getline(in, line)) { + auto hash = line.find('#'); + if (hash != std::string::npos) line = line.substr(0, hash); + line = trim(line); + if (line.empty()) continue; + DependencySpec dep; + dep.source = "requirements.txt"; + std::string name = line; + std::string version; + size_t op = line.find("=="); + if (op == std::string::npos) op = line.find(">="); + if (op == std::string::npos) op = line.find("<="); + if (op != std::string::npos) { + name = trim(line.substr(0, op)); + version = trim(line.substr(op + 2)); + } + dep.name = name; + dep.version = version; + out.push_back(dep); + } + return out; + } + + static std::vector parsePackageJson(const std::string& path) { + std::vector out; + std::ifstream in(path); + if (!in.is_open()) return out; + nlohmann::json j; + try { in >> j; } catch (...) { return out; } + auto collect = [&](const std::string& key) { + if (!j.contains(key)) return; + for (auto it = j[key].begin(); it != j[key].end(); ++it) { + DependencySpec dep; + dep.name = it.key(); + dep.version = it.value().get(); + dep.source = "package.json"; + out.push_back(dep); + } + }; + collect("dependencies"); + collect("devDependencies"); + return out; + } + + static std::vector parseCargoToml(const std::string& path) { + std::vector out; + std::ifstream in(path); + if (!in.is_open()) return out; + std::string line; + bool inDeps = false; + while (std::getline(in, line)) { + line = trim(line); + if (line.empty() || line[0] == '#') continue; + if (line[0] == '[') { + inDeps = (line == "[dependencies]"); + continue; + } + if (!inDeps) continue; + auto eq = line.find('='); + if (eq == std::string::npos) continue; + std::string name = trim(line.substr(0, eq)); + std::string val = trim(line.substr(eq + 1)); + std::string version; + if (!val.empty() && val[0] == '"') { + auto end = val.find('"', 1); + if (end != std::string::npos) version = val.substr(1, end - 1); + } else { + auto pos = val.find("version"); + if (pos != std::string::npos) { + auto q1 = val.find('"', pos); + auto q2 = q1 == std::string::npos ? std::string::npos : val.find('"', q1 + 1); + if (q1 != std::string::npos && q2 != std::string::npos) { + version = val.substr(q1 + 1, q2 - q1 - 1); + } + } + } + DependencySpec dep; + dep.name = name; + dep.version = version; + dep.source = "Cargo.toml"; + out.push_back(dep); + } + return out; + } + + static std::vector parseGoMod(const std::string& path) { + std::vector out; + std::ifstream in(path); + if (!in.is_open()) return out; + std::string line; + bool inRequire = false; + while (std::getline(in, line)) { + line = trim(line); + if (line.empty()) continue; + if (line.rfind("require (", 0) == 0) { inRequire = true; continue; } + if (inRequire && line == ")") { inRequire = false; continue; } + if (line.rfind("require ", 0) == 0 && !inRequire) { + line = trim(line.substr(8)); + } else if (!inRequire) { + continue; + } + std::istringstream iss(line); + std::string name, version; + iss >> name >> version; + if (name.empty()) continue; + DependencySpec dep; + dep.name = name; + dep.version = version; + dep.source = "go.mod"; + out.push_back(dep); + } + return out; + } + + static std::vector parsePomXml(const std::string& path) { + std::vector out; + std::ifstream in(path); + if (!in.is_open()) return out; + std::string content((std::istreambuf_iterator(in)), + std::istreambuf_iterator()); + size_t pos = 0; + while ((pos = content.find("", pos)) != std::string::npos) { + size_t end = content.find("", pos); + if (end == std::string::npos) break; + std::string block = content.substr(pos, end - pos); + auto getTag = [&](const std::string& tag) { + std::string open = "<" + tag + ">"; + std::string close = ""; + size_t a = block.find(open); + size_t b = block.find(close); + if (a == std::string::npos || b == std::string::npos) return std::string(); + return trim(block.substr(a + open.size(), b - a - open.size())); + }; + std::string artifact = getTag("artifactId"); + std::string version = getTag("version"); + if (!artifact.empty()) { + DependencySpec dep; + dep.name = artifact; + dep.version = version; + dep.source = "pom.xml"; + out.push_back(dep); + } + pos = end + 1; + } + return out; + } + + static std::vector parseGradle(const std::string& path) { + std::vector out; + std::ifstream in(path); + if (!in.is_open()) return out; + std::string line; + while (std::getline(in, line)) { + line = trim(line); + if (line.find("implementation") == std::string::npos && + line.find("api") == std::string::npos) { + continue; + } + auto q1 = line.find('"'); + auto q2 = q1 == std::string::npos ? std::string::npos : line.find('"', q1 + 1); + if (q1 == std::string::npos || q2 == std::string::npos) continue; + std::string depStr = line.substr(q1 + 1, q2 - q1 - 1); + auto parts = split(depStr, ':'); + if (parts.size() >= 2) { + DependencySpec dep; + dep.name = parts[1]; + if (parts.size() >= 3) dep.version = parts[2]; + dep.source = "build.gradle"; + out.push_back(dep); + } + } + return out; + } + + static std::vector parseVcpkgJson(const std::string& path) { + std::vector out; + std::ifstream in(path); + if (!in.is_open()) return out; + nlohmann::json j; + try { in >> j; } catch (...) { return out; } + if (!j.contains("dependencies")) return out; + for (const auto& dep : j["dependencies"]) { + DependencySpec spec; + if (dep.is_string()) { + spec.name = dep.get(); + } else if (dep.is_object() && dep.contains("name")) { + spec.name = dep["name"].get(); + if (dep.contains("version")) spec.version = dep["version"].get(); + } + spec.source = "vcpkg.json"; + if (!spec.name.empty()) out.push_back(spec); + } + return out; + } + + static std::vector parseCMakeLists(const std::string& path) { + std::vector out; + std::ifstream in(path); + if (!in.is_open()) return out; + std::string line; + while (std::getline(in, line)) { + auto pos = line.find("find_package("); + if (pos == std::string::npos) continue; + line = line.substr(pos + strlen("find_package(")); + auto end = line.find(')'); + if (end != std::string::npos) line = line.substr(0, end); + std::istringstream iss(line); + std::string name; + iss >> name; + if (!name.empty()) { + DependencySpec dep; + dep.name = name; + dep.source = "CMakeLists.txt"; + out.push_back(dep); + } + } + return out; + } + + static std::vector parsePythonMeta(const std::string& path) { + std::vector out; + std::ifstream in(path); + if (!in.is_open()) return out; + std::string content((std::istreambuf_iterator(in)), + std::istreambuf_iterator()); + auto pos = content.find("install_requires"); + if (pos == std::string::npos) return out; + auto start = content.find('[', pos); + auto end = content.find(']', start); + if (start == std::string::npos || end == std::string::npos) return out; + std::string list = content.substr(start + 1, end - start - 1); + std::stringstream ss(list); + std::string item; + while (std::getline(ss, item, ',')) { + item = trim(item); + if (item.size() >= 2 && item.front() == '"' && item.back() == '"') { + item = item.substr(1, item.size() - 2); + } else if (item.size() >= 2 && item.front() == '\'' && item.back() == '\'') { + item = item.substr(1, item.size() - 2); + } + if (!item.empty()) { + DependencySpec dep; + dep.name = item; + dep.source = std::filesystem::path(path).filename().string(); + out.push_back(dep); + } + } + return out; + } + + static std::vector split(const std::string& s, char delim) { + std::vector parts; + std::stringstream ss(s); + std::string item; + while (std::getline(ss, item, delim)) { + parts.push_back(item); + } + return parts; + } +}; diff --git a/editor/tests/step128_test.cpp b/editor/tests/step128_test.cpp new file mode 100644 index 0000000..e7770f6 --- /dev/null +++ b/editor/tests/step128_test.cpp @@ -0,0 +1,61 @@ +// Step 128 TDD Test: Dependency parsing +#include "DependencyParser.h" +#include +#include + +static void expect(bool cond, const std::string& name, int& passed, int& failed) { + if (cond) { + std::cout << "Test " << (passed + failed + 1) << " PASS: " << name << "\n"; + ++passed; + } else { + std::cout << "Test " << (passed + failed + 1) << " FAIL: " << name << "\n"; + ++failed; + } +} + +int main() { + int passed = 0; + int failed = 0; + + std::filesystem::path dir = std::filesystem::current_path() / "dep_test_tmp"; + std::filesystem::create_directories(dir); + + // requirements.txt + { + std::ofstream out(dir / "requirements.txt"); + out << "numpy==1.26.0\n# comment\nrequests>=2.0\n"; + } + auto reqs = DependencyParser::parseFile((dir / "requirements.txt").string()); + expect(reqs.size() == 2, "requirements count", passed, failed); + if (reqs.size() >= 2) { + expect(reqs[0].name == "numpy", "requirements name", passed, failed); + expect(reqs[0].version == "1.26.0", "requirements version", passed, failed); + } + + // package.json + { + std::ofstream out(dir / "package.json"); + out << "{ \"dependencies\": { \"react\": \"^18.2.0\" }," + "\"devDependencies\": { \"vite\": \"^5.0.0\" } }"; + } + auto npm = DependencyParser::parseFile((dir / "package.json").string()); + expect(npm.size() == 2, "package.json count", passed, failed); + + // Cargo.toml + { + std::ofstream out(dir / "Cargo.toml"); + out << "[dependencies]\nserde = \"1.0\"\nanyhow = { version = \"1.0\" }\n"; + } + auto cargo = DependencyParser::parseFile((dir / "Cargo.toml").string()); + expect(cargo.size() == 2, "cargo count", passed, failed); + + // Populate imports + Module mod("m1", "Mod", "python"); + DependencyParser::populateImports(&mod, reqs); + expect(mod.getChildren("imports").size() == 2, "imports populated", passed, failed); + + std::filesystem::remove_all(dir); + + std::cout << "\n=== Step 128 Results: " << passed << " passed, " << failed << " failed ===\n"; + return failed == 0 ? 0 : 1; +} diff --git a/sprint5_plan.md b/sprint5_plan.md index a30ac5f..37f035e 100644 --- a/sprint5_plan.md +++ b/sprint5_plan.md @@ -36,7 +36,7 @@ The foundation: import libraries, parse their APIs, index their symbols. dependencies, homepage URL. *New:* `PackageRegistry.h` -- [ ] **Step 128: Dependency file parsing** +- [x] **Step 128: Dependency file parsing** Parse existing dependency files to discover what's already imported: - Python: `requirements.txt`, `pyproject.toml`, `setup.py` - JS/TS: `package.json`