Step 259: cross-file symbol resolution and import graph

getInScopeSymbols gains crossFile parameter to include exported symbols from
all open buffers. Import graph tracks file dependencies, auto-maintained on
openFile/closeFile. Text-based fallback for parsers without Import AST nodes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bill
2026-02-11 20:06:46 +00:00
parent ac5b7e21c8
commit f0417bccce
6 changed files with 695 additions and 1 deletions

View File

@@ -9,8 +9,15 @@
#include <vector>
#include <map>
#include <set>
#include <sstream>
#include <filesystem>
#include "FileTree.h"
#include "ast/ASTNode.h"
#include "ast/Module.h"
#include "ast/Function.h"
#include "ast/Variable.h"
#include "ast/Import.h"
#include "CompactAST.h" // getNodeName
namespace fs = std::filesystem;
@@ -136,11 +143,134 @@ private:
}
};
// -----------------------------------------------------------------------
// Step 259: ImportGraph — tracks which files import which
// -----------------------------------------------------------------------
class ImportGraph {
public:
// Record that `fromFile` imports `moduleName`
void addEdge(const std::string& fromFile,
const std::string& moduleName) {
edges_[fromFile].insert(moduleName);
}
// Clear edges for a file (call before re-scanning)
void clearFile(const std::string& filePath) {
edges_.erase(filePath);
}
// Get modules imported by a file
std::set<std::string> importsOf(const std::string& filePath) const {
auto it = edges_.find(filePath);
if (it != edges_.end()) return it->second;
return {};
}
// Get files that import a given module name
std::vector<std::string> importedBy(
const std::string& moduleName) const {
std::vector<std::string> result;
for (const auto& [file, imports] : edges_) {
if (imports.count(moduleName)) result.push_back(file);
}
return result;
}
// Rebuild import edges from an AST's Import nodes
void updateFromAST(const std::string& filePath, Module* ast) {
clearFile(filePath);
if (!ast) return;
for (auto* child : ast->allChildren()) {
if (child->conceptType == "Import") {
auto* imp = static_cast<const Import*>(child);
if (!imp->moduleName.empty())
addEdge(filePath, imp->moduleName);
}
}
}
// Rebuild import edges from source text (fallback for parsers
// that don't generate Import AST nodes, e.g. Python)
void updateFromSource(const std::string& filePath,
const std::string& source) {
// Simple line-by-line scan for import statements
std::istringstream iss(source);
std::string line;
while (std::getline(iss, line)) {
// Python: "import foo" or "from foo import bar"
if (line.substr(0, 7) == "import ") {
std::string mod = line.substr(7);
// Trim whitespace
while (!mod.empty() && mod.back() == ' ')
mod.pop_back();
// Handle "import foo, bar"
size_t comma = mod.find(',');
if (comma != std::string::npos)
mod = mod.substr(0, comma);
if (!mod.empty()) addEdge(filePath, mod);
} else if (line.substr(0, 5) == "from ") {
size_t space = line.find(' ', 5);
if (space != std::string::npos) {
std::string mod = line.substr(5, space - 5);
if (!mod.empty()) addEdge(filePath, mod);
}
}
// JS/TS: "import ... from 'foo'" handled by AST
// C++: "#include" handled by AST
}
}
const std::map<std::string, std::set<std::string>>& edges() const {
return edges_;
}
private:
// filePath -> set of imported module names
std::map<std::string, std::set<std::string>> edges_;
};
// -----------------------------------------------------------------------
// ExportedSymbol — a symbol visible across files
// -----------------------------------------------------------------------
struct ExportedSymbol {
std::string name;
std::string kind; // "function", "variable", "class"
std::string nodeId;
std::string filePath; // source file
};
// Collect exported (module-level) symbols from an AST
inline std::vector<ExportedSymbol> collectExportedSymbols(
Module* ast, const std::string& filePath) {
std::vector<ExportedSymbol> symbols;
if (!ast) return symbols;
for (auto* child : ast->getChildren("functions")) {
if (child->conceptType == "Function") {
auto* fn = static_cast<const Function*>(child);
symbols.push_back({fn->name, "function", fn->id, filePath});
}
}
for (auto* child : ast->getChildren("variables")) {
if (child->conceptType == "Variable") {
auto* v = static_cast<const Variable*>(child);
symbols.push_back({v->name, "variable", v->id, filePath});
}
}
for (auto* child : ast->getChildren("classes")) {
if (child->conceptType == "Class") {
symbols.push_back({getNodeName(child), "class",
child->id, filePath});
}
}
return symbols;
}
// -----------------------------------------------------------------------
// ProjectState — aggregates workspace index and open buffer tracking
// -----------------------------------------------------------------------
struct ProjectState {
WorkspaceIndex index;
ImportGraph importGraph;
void scanWorkspace(const std::string& workspaceRoot) {
index.scan(workspaceRoot);