Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
38 changes: 30 additions & 8 deletions crates/codegraph-core/src/domain/graph/builder/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1277,38 +1277,48 @@ fn load_file_node_id_map(conn: &Connection) -> HashMap<String, u32> {
/// Resolve a file's imports to the list of `ImportedName` entries the edge
/// builder consumes. Walks barrel chains to the ultimate definition file so
/// the edge builder's name-lookup can find the right target (#976 P1).
///
/// For renamed specifiers (`import { X as Y }`), `ImportedName.imported`
/// carries the original name (X) so `resolve_call_targets` can look it up in
/// the target file instead of the local alias (Y), which only exists in the
/// importing file (#1730).
fn collect_imported_names_for_file(
abs_str: &str,
symbols: &FileSymbols,
import_ctx: &crate::domain::graph::builder::stages::import_edges::ImportEdgeContext,
) -> Vec<crate::domain::graph::builder::stages::build_edges::ImportedName> {
use crate::domain::graph::builder::stages::build_edges::ImportedName;
use crate::domain::graph::builder::stages::import_edges::import_name_pairs;
let mut imported_names: Vec<ImportedName> = Vec::new();
for imp in &symbols.imports {
let resolved_path = import_ctx.get_resolved(abs_str, &imp.source);
for name in &imp.names {
let clean_name = name.strip_prefix("* as ").unwrap_or(name).to_string();
for (local, original) in import_name_pairs(imp) {
// CJS require bindings are included in imported_names so the receiver-edge
// resolver treats them as import artifacts (not locally-defined symbols).
// We use an empty target_file so the import-aware call-target lookup
// (`nodes_by_name_and_file.get(&(name, ""))`) always misses and falls
// through to the same-file shadow node — matching WASM call-resolution
// behaviour where CJS bindings are not in importedNamesMap (#1678).
if imp.cjs_require.unwrap_or(false) {
imported_names.push(ImportedName { name: clean_name, file: String::new() });
imported_names.push(ImportedName {
name: local,
file: String::new(),
imported: None,
});
continue;
}
let mut target_file = resolved_path.clone();
if import_ctx.is_barrel_file(&resolved_path) {
let mut visited = HashSet::new();
if let Some(actual) =
import_ctx.resolve_barrel_export(&resolved_path, &clean_name, &mut visited)
import_ctx.resolve_barrel_export(&resolved_path, &original, &mut visited)
{
target_file = actual;
}
}
imported_names.push(ImportedName {
name: clean_name,
imported: if original != local { Some(original) } else { None },
name: local,
file: target_file,
});
}
Expand Down Expand Up @@ -1411,8 +1421,14 @@ fn inject_return_types_for_file(
let imported_names = collect_imported_names_for_file(abs_str, symbols, import_ctx);
// Later entries overwrite earlier ones on duplicate names — same as the
// HashMap collect in build_call_edges.
let imported_map: HashMap<String, String> =
imported_names.into_iter().map(|e| (e.name, e.file)).collect();
let mut imported_map: HashMap<String, String> = HashMap::new();
let mut imported_original_map: HashMap<String, String> = HashMap::new();
for e in imported_names {
if let Some(original) = e.imported {
imported_original_map.insert(e.name.clone(), original);
}
imported_map.insert(e.name, e.file);
}

let mut injections: Vec<TypeMapEntry> = Vec::new();
let mut injected: HashSet<String> = HashSet::new();
Expand All @@ -1429,7 +1445,13 @@ fn inject_return_types_for_file(
global_return_types.get(&format!("{receiver}.{}", ca.callee_name))
}
None => imported_map.get(&ca.callee_name).and_then(|from| {
return_type_index.get(from).and_then(|m| m.get(&ca.callee_name))
// The return-type index for the imported file is keyed by the
// function's own declared name — use the original (pre-rename)
// name when the callee is a renamed import binding (#1730).
let callee_original_name = imported_original_map
.get(&ca.callee_name)
.unwrap_or(&ca.callee_name);
return_type_index.get(from).and_then(|m| m.get(callee_original_name))
}),
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ pub struct CallInfo {
pub struct ImportedName {
pub name: String,
pub file: String,
/// For renamed specifiers (`import { X as Y }`): the original name
/// exported by `file` (X), when it differs from `name` (the local
/// binding Y). `resolve_call_targets` looks this up in `file` instead of
/// `name` — the renamed local alias only exists in the importing file,
/// not in `file` itself (#1730).
pub imported: Option<String>,
}

#[napi(object)]
Expand Down Expand Up @@ -384,6 +390,7 @@ struct PtsAliasCtx<'a> {
is_dynamic: u32,
rel_path: &'a str,
imported_names: &'a HashMap<&'a str, &'a str>,
imported_original_names: &'a HashMap<&'a str, &'a str>,
type_map: &'a HashMap<&'a str, (&'a str, f64)>,
}

Expand All @@ -399,6 +406,7 @@ fn emit_pts_alias_edges<'a>(
) {
for alias in resolve_via_points_to(alias_ctx.lookup_name, alias_ctx.pts) {
let alias_imported_from = alias_ctx.imported_names.get(alias).copied();
let alias_imported_original_name = alias_ctx.imported_original_names.get(alias).copied();
let alias_call = CallInfo {
name: alias.to_string(),
line: alias_ctx.call_line,
Expand All @@ -409,6 +417,7 @@ fn emit_pts_alias_edges<'a>(
};
let mut alias_targets = resolve_call_targets(
ctx, &alias_call, alias_ctx.rel_path, alias_imported_from, alias_ctx.type_map, alias_ctx.caller_name,
alias_imported_original_name,
);
sort_targets_by_confidence(&mut alias_targets, alias_ctx.rel_path, alias_imported_from);
for t in &alias_targets {
Expand Down Expand Up @@ -458,6 +467,11 @@ struct FileContext<'a> {
rel_path: &'a str,
file_node_id: u32,
imported_names: HashMap<&'a str, &'a str>,
/// Local import alias -> original exported name, for renamed specifiers
/// (`import { X as Y }`) only — entries where local === original are
/// omitted. Consulted by `resolve_call_targets` so a call to the local
/// alias resolves against the correct exported symbol (#1730).
imported_original_names: HashMap<&'a str, &'a str>,
type_map: HashMap<&'a str, (&'a str, f64)>,
defs_with_ids: Vec<DefWithId<'a>>,
pts_map: Option<HashMap<String, HashSet<String>>>,
Expand Down Expand Up @@ -567,6 +581,10 @@ fn build_file_context<'a>(
.imported_names.iter()
.map(|im| (im.name.as_str(), im.file.as_str()))
.collect();
let imported_original_names: HashMap<&str, &str> = file_input
.imported_names.iter()
.filter_map(|im| im.imported.as_deref().map(|orig| (im.name.as_str(), orig)))
.collect();
let type_map = build_type_map(file_input);
let file_nodes: Vec<&NodeInfo> = all_nodes.iter().filter(|n| n.file == rel_path).collect();
let defs_with_ids: Vec<DefWithId> = file_input.definitions.iter().map(|d| {
Expand All @@ -590,6 +608,7 @@ fn build_file_context<'a>(
rel_path,
file_node_id: file_input.file_node_id,
imported_names,
imported_original_names,
type_map,
defs_with_ids,
pts_map,
Expand Down Expand Up @@ -655,6 +674,7 @@ fn emit_no_receiver_pts_edges<'a>(
is_dynamic,
rel_path: fc.rel_path,
imported_names: &fc.imported_names,
imported_original_names: &fc.imported_original_names,
type_map: &fc.type_map,
},
seen_edges,
Expand Down Expand Up @@ -697,6 +717,7 @@ fn emit_receiver_pts_edges<'a>(
is_dynamic,
rel_path: fc.rel_path,
imported_names: &fc.imported_names,
imported_original_names: &fc.imported_original_names,
type_map: &fc.type_map,
},
seen_edges,
Expand Down Expand Up @@ -734,8 +755,11 @@ fn process_file<'a>(
let (caller_id, caller_name) = find_enclosing_caller(&fc.defs_with_ids, call.line, fc.file_node_id);
let is_dynamic = if call.dynamic.unwrap_or(false) { 1u32 } else { 0u32 };
let imported_from = fc.imported_names.get(call.name.as_str()).copied();
let imported_original_name = fc.imported_original_names.get(call.name.as_str()).copied();

let mut targets = resolve_call_targets(ctx, call, fc.rel_path, imported_from, &fc.type_map, caller_name);
let mut targets = resolve_call_targets(
ctx, call, fc.rel_path, imported_from, &fc.type_map, caller_name, imported_original_name,
);
sort_targets_by_confidence(&mut targets, fc.rel_path, imported_from);
emit_call_edges(&targets, caller_id, is_dynamic, fc.rel_path, imported_from, &mut seen_edges, &mut pts_edge_map, edges);

Expand Down Expand Up @@ -882,17 +906,23 @@ fn resolve_call_targets<'a>(
imported_from: Option<&str>,
type_map: &HashMap<&str, (&str, f64)>,
caller_name: &str,
imported_original_name: Option<&str>,
) -> Vec<&'a NodeInfo> {
// Flagged dynamic calls use synthetic names like "<dynamic:eval>". Short-circuit
// so they never accidentally match a real symbol via name lookup.
if call.name.starts_with("<dynamic:") {
return vec![];
}

// When the call site uses a renamed import binding (`import { X as Y }`),
// the imported file's actual symbol is declared under the *original* name
// (X) — look that up instead of the local alias the call site wrote (#1730).
let target_name = imported_original_name.unwrap_or(call.name.as_str());

// 1. Import-aware resolution
if let Some(imp_file) = imported_from {
let targets = ctx.nodes_by_name_and_file
.get(&(call.name.as_str(), imp_file))
.get(&(target_name, imp_file))
.cloned().unwrap_or_default();
if !targets.is_empty() { return targets; }
}
Expand Down Expand Up @@ -2081,7 +2111,7 @@ mod call_edge_tests {
);
// Mark `Calculator` as an imported name so the resolver treats the
// same-file kind="function" node as an import artifact and falls through.
file.imported_names = vec![ImportedName { name: "Calculator".to_string(), file: "utils.js".to_string() }];
file.imported_names = vec![ImportedName { name: "Calculator".to_string(), file: "utils.js".to_string(), imported: None }];

let edges = build_call_edges(vec![file], all_nodes, vec![]);

Expand Down Expand Up @@ -2464,7 +2494,7 @@ mod call_edge_tests {
vec![],
vec![],
);
file.imported_names = vec![ImportedName { name: "e4".to_string(), file: "other.js".to_string() }];
file.imported_names = vec![ImportedName { name: "e4".to_string(), file: "other.js".to_string(), imported: None }];
file.param_bindings = Some(vec![ParamBinding {
callee: "f3".to_string(),
arg_index: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,29 @@ impl BarrelContext for ImportEdgeContext {
}
}

/// Pairs each locally-bound name from an import statement with its original
/// (pre-rename) exported name — identical to the local name unless the
/// specifier renames a binding (`import { X as Y }`). Barrel tracing and
/// target-file symbol lookups must search using the *original* name — the
/// renamed local alias only exists in the importing file, not in the file
/// being imported from (#1730). Mirrors `importNamePairs` in build-edges.ts.
pub(crate) fn import_name_pairs(imp: &crate::types::Import) -> Vec<(String, String)> {
let mut original_name_for: HashMap<&str, &str> = HashMap::new();
if let Some(renamed) = &imp.renamed_imports {
for r in renamed {
original_name_for.insert(&r.local, &r.imported);
}
}
imp.names
.iter()
.map(|name| {
let local = name.strip_prefix("* as ").unwrap_or(name);
let original = original_name_for.get(local).copied().unwrap_or(local);
(local.to_string(), original.to_string())
})
.collect()
}

/// Build the reexport map from parsed file symbols.
pub fn build_reexport_map(ctx: &ImportEdgeContext) -> HashMap<String, Vec<ReexportEntry>> {
let mut reexport_map = HashMap::new();
Expand Down Expand Up @@ -249,18 +272,17 @@ fn collect_type_only_lookup_pairs(ctx: &ImportEdgeContext) -> HashSet<(String, S
continue;
}
let resolved_path = ctx.get_resolved(abs_str, &imp.source);
for name in &imp.names {
let clean_name = name.strip_prefix("* as ").unwrap_or(name);
for (_local, original) in import_name_pairs(imp) {
let mut target_file = resolved_path.clone();
if ctx.is_barrel_file(&resolved_path) {
let mut visited = HashSet::new();
if let Some(actual) =
ctx.resolve_barrel_export(&resolved_path, clean_name, &mut visited)
ctx.resolve_barrel_export(&resolved_path, &original, &mut visited)
{
target_file = actual;
}
}
pairs.insert((clean_name.to_string(), target_file));
pairs.insert((original, target_file));
}
}
}
Expand Down Expand Up @@ -303,18 +325,16 @@ fn emit_type_only_symbol_rows(
if !imp.type_only.unwrap_or(false) {
return;
}
for name in &imp.names {
let clean_name = name.strip_prefix("* as ").unwrap_or(name);
for (_local, original) in import_name_pairs(imp) {
let mut target_file = resolved_path.to_string();
if ctx.is_barrel_file(resolved_path) {
let mut visited = HashSet::new();
if let Some(actual) =
ctx.resolve_barrel_export(resolved_path, clean_name, &mut visited)
if let Some(actual) = ctx.resolve_barrel_export(resolved_path, &original, &mut visited)
{
target_file = actual;
}
}
if let Some(&sym_id) = symbol_node_ids.get(&(clean_name.to_string(), target_file)) {
if let Some(&sym_id) = symbol_node_ids.get(&(original, target_file)) {
edges.push(EdgeRow {
source_id: file_node_id,
target_id: sym_id,
Expand Down Expand Up @@ -347,14 +367,13 @@ fn emit_barrel_through_rows(
_ => "imports",
};
let mut resolved_sources: HashSet<String> = HashSet::new();
for name in &imp.names {
let clean_name = name.strip_prefix("* as ").unwrap_or(name);
for (_local, original) in import_name_pairs(imp) {
let mut visited = HashSet::new();
let actual_source =
match ctx.resolve_barrel_export(resolved_path, clean_name, &mut visited) {
Some(s) => s,
None => continue,
};
let actual_source = match ctx.resolve_barrel_export(resolved_path, &original, &mut visited)
{
Some(s) => s,
None => continue,
};
if actual_source == resolved_path || !resolved_sources.insert(actual_source.clone()) {
continue;
}
Expand Down
Loading
Loading