Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
6f4c52e
refactor(native): extract magic numbers to named constants
carlos-alm Apr 4, 2026
74980eb
refactor: extract shared node-role classification from structure.ts
carlos-alm Apr 4, 2026
41f7dfd
refactor: unify duplicate dataflow result builders
carlos-alm Apr 4, 2026
8a08153
refactor(native): extract shared barrel resolution into common module
carlos-alm Apr 4, 2026
ac28911
refactor(native): flatten deeply nested extractor match arms
carlos-alm Apr 4, 2026
7be28ce
refactor(native): decompose cpp and scala node matchers
carlos-alm Apr 4, 2026
faa63c3
refactor(native): decompose louvain_impl into init/move/aggregate phases
carlos-alm Apr 4, 2026
8f14f42
refactor(native): split extract_param_names_strategy into per-languag…
carlos-alm Apr 4, 2026
dea81ca
refactor(native): decompose run_pipeline into stage functions
carlos-alm Apr 4, 2026
5988439
refactor: decompose buildComplexityMetrics into native/wasm/merge sub…
carlos-alm Apr 4, 2026
3f8537b
refactor: continue buildGraph decomposition into pipeline stages
carlos-alm Apr 4, 2026
f51fe4b
refactor: split presentation formatters into sub-renderers
carlos-alm Apr 4, 2026
6d521cd
refactor: extract watcher debounce and journal logic
carlos-alm Apr 4, 2026
c9433ed
refactor: reduce complexity in TS extractors and file-utils
carlos-alm Apr 4, 2026
b11b075
refactor: simplify AST store visitor and engine setup
carlos-alm Apr 4, 2026
8347867
refactor(native): improve helper and barrel resolution quality
carlos-alm Apr 4, 2026
9eacf7e
fix: resolve +1 function cycle regression in barrel resolution
carlos-alm Apr 4, 2026
d4a746c
docs: add Titan v3.9.0 report and sync Cargo.lock
carlos-alm Apr 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

191 changes: 191 additions & 0 deletions crates/codegraph-core/src/barrel_resolution.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
//! Shared barrel-file resolution logic.
//!
//! Both `edge_builder.rs` (napi-driven) and `import_edges.rs` (SQLite-driven)
//! need to recursively resolve a symbol through barrel reexport chains.
//! This module extracts the common algorithm so both callers share a single
//! implementation.

use std::collections::HashSet;

/// Minimal view of a single reexport entry, borrowed from the caller's data.
pub struct ReexportRef<'a> {
pub source: &'a str,
pub names: &'a [String],
pub wildcard_reexport: bool,
}

/// Trait that abstracts over the different context types in `edge_builder` and
/// `import_edges`. Each implementor provides access to its own reexport map
/// and definition index so the resolution algorithm stays generic.
pub trait BarrelContext {
/// Return the reexport entries for `barrel_path`, or `None` if the path
/// has no reexports.
fn reexports_for(&self, barrel_path: &str) -> Option<Vec<ReexportRef<'_>>>;

/// Return `true` if `file_path` contains a definition named `symbol`.
fn has_definition(&self, file_path: &str, symbol: &str) -> bool;
}

/// Recursively resolve a symbol through barrel reexport chains.
///
/// Mirrors `resolveBarrelExport()` in `resolve-imports.ts`.
/// The caller provides a `visited` set to prevent infinite loops on circular
/// reexport chains.
pub fn resolve_barrel_export<'a, C: BarrelContext>(
ctx: &'a C,
barrel_path: &str,
symbol_name: &str,
visited: &mut HashSet<String>,
) -> Option<String> {
if visited.contains(barrel_path) {
return None;
}
visited.insert(barrel_path.to_string());

let reexports = ctx.reexports_for(barrel_path)?;

for re in &reexports {
// Named reexports (non-wildcard)
if !re.names.is_empty() && !re.wildcard_reexport {
if re.names.iter().any(|n| n == symbol_name) {
if ctx.has_definition(re.source, symbol_name) {
return Some(re.source.to_string());
}
let deeper = resolve_barrel_export(ctx, re.source, symbol_name, visited);
if deeper.is_some() {
return deeper;
}
// Fallback: return source even if no definition found
return Some(re.source.to_string());
}
continue;
}

// Wildcard or empty-names reexports
if re.wildcard_reexport || re.names.is_empty() {
if ctx.has_definition(re.source, symbol_name) {
return Some(re.source.to_string());
}
let deeper = resolve_barrel_export(ctx, re.source, symbol_name, visited);
if deeper.is_some() {
return deeper;
}
}
}

None
}

#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;

struct TestContext {
reexports: HashMap<String, Vec<(String, Vec<String>, bool)>>,
definitions: HashMap<String, HashSet<String>>,
}

impl BarrelContext for TestContext {
fn reexports_for(&self, barrel_path: &str) -> Option<Vec<ReexportRef<'_>>> {
self.reexports.get(barrel_path).map(|entries| {
entries
.iter()
.map(|(source, names, wildcard)| ReexportRef {
source: source.as_str(),
names: names.as_slice(),
wildcard_reexport: *wildcard,
})
.collect()
})
}

fn has_definition(&self, file_path: &str, symbol: &str) -> bool {
self.definitions
.get(file_path)
.map_or(false, |defs| defs.contains(symbol))
}
}

#[test]
fn resolves_named_reexport() {
let mut reexports = HashMap::new();
reexports.insert(
"src/index.ts".to_string(),
vec![("src/utils.ts".to_string(), vec!["foo".to_string()], false)],
);
let mut definitions = HashMap::new();
definitions.insert(
"src/utils.ts".to_string(),
HashSet::from(["foo".to_string()]),
);

let ctx = TestContext { reexports, definitions };
let mut visited = HashSet::new();
let result = resolve_barrel_export(&ctx, "src/index.ts", "foo", &mut visited);
assert_eq!(result.as_deref(), Some("src/utils.ts"));
}

#[test]
fn resolves_wildcard_reexport() {
let mut reexports = HashMap::new();
reexports.insert(
"src/index.ts".to_string(),
vec![("src/utils.ts".to_string(), vec![], true)],
);
let mut definitions = HashMap::new();
definitions.insert(
"src/utils.ts".to_string(),
HashSet::from(["bar".to_string()]),
);

let ctx = TestContext { reexports, definitions };
let mut visited = HashSet::new();
let result = resolve_barrel_export(&ctx, "src/index.ts", "bar", &mut visited);
assert_eq!(result.as_deref(), Some("src/utils.ts"));
}

#[test]
fn resolves_transitive_chain() {
let mut reexports = HashMap::new();
reexports.insert(
"src/index.ts".to_string(),
vec![("src/mid.ts".to_string(), vec![], true)],
);
reexports.insert(
"src/mid.ts".to_string(),
vec![("src/deep.ts".to_string(), vec!["baz".to_string()], false)],
);
let mut definitions = HashMap::new();
definitions.insert(
"src/deep.ts".to_string(),
HashSet::from(["baz".to_string()]),
);

let ctx = TestContext { reexports, definitions };
let mut visited = HashSet::new();
let result = resolve_barrel_export(&ctx, "src/index.ts", "baz", &mut visited);
assert_eq!(result.as_deref(), Some("src/deep.ts"));
}

#[test]
fn prevents_circular_reexport() {
let mut reexports = HashMap::new();
reexports.insert(
"src/a.ts".to_string(),
vec![("src/b.ts".to_string(), vec![], true)],
);
reexports.insert(
"src/b.ts".to_string(),
vec![("src/a.ts".to_string(), vec![], true)],
);

let ctx = TestContext {
reexports,
definitions: HashMap::new(),
};
let mut visited = HashSet::new();
let result = resolve_barrel_export(&ctx, "src/a.ts", "missing", &mut visited);
assert_eq!(result, None);
}
}
Loading
Loading