Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions crates/codegraph-core/src/build_pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,7 @@ fn build_and_insert_call_edges(
.map(|t| TypeMapInput {
name: t.name.clone(),
type_name: t.type_name.clone(),
confidence: t.confidence,
})
.collect();

Expand Down
31 changes: 23 additions & 8 deletions crates/codegraph-core/src/edge_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ pub struct TypeMapInput {
pub name: String,
#[napi(js_name = "typeName")]
pub type_name: String,
/// Confidence: 0.9 = type annotation, 1.0 = constructor, 0.7 = factory.
#[napi(default = 0.9)]
pub confidence: f64,
}

#[napi(object)]
Expand Down Expand Up @@ -153,10 +156,22 @@ fn process_file<'a>(
.map(|im| (im.name.as_str(), im.file.as_str()))
.collect();

let type_map: HashMap<&str, &str> = file_input
.type_map.iter()
.map(|tm| (tm.name.as_str(), tm.type_name.as_str()))
.collect();
// Build type map keeping the highest-confidence entry per name
// (first-wins on tie), matching the JS setTypeMapEntry behaviour.
let mut type_map: HashMap<&str, (&str, f64)> = HashMap::new();
for tm in &file_input.type_map {
let entry = type_map.entry(tm.name.as_str());
match entry {
std::collections::hash_map::Entry::Vacant(e) => {
e.insert((tm.type_name.as_str(), tm.confidence));
}
std::collections::hash_map::Entry::Occupied(mut e) => {
if tm.confidence > e.get().1 {
e.insert((tm.type_name.as_str(), tm.confidence));
}
}
}
}

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 Down Expand Up @@ -210,7 +225,7 @@ fn resolve_call_targets<'a>(
call: &CallInfo,
rel_path: &str,
imported_from: Option<&str>,
type_map: &HashMap<&str, &str>,
type_map: &HashMap<&str, (&str, f64)>,
) -> Vec<&'a NodeInfo> {
// 1. Import-aware resolution
if let Some(imp_file) = imported_from {
Expand All @@ -236,7 +251,7 @@ fn resolve_call_targets<'a>(

// 4. Type-aware resolution via receiver → type map
if let Some(ref receiver) = call.receiver {
if let Some(type_name) = type_map.get(receiver.as_str()) {
if let Some(&(type_name, _conf)) = type_map.get(receiver.as_str()) {
let qualified = format!("{}.{}", type_name, call.name);
let typed: Vec<&NodeInfo> = ctx.nodes_by_name
.get(qualified.as_str())
Expand Down Expand Up @@ -296,15 +311,15 @@ fn emit_call_edges(
/// Emit a receiver edge from caller to the receiver's type node (if applicable).
fn emit_receiver_edge(
ctx: &EdgeContext, call: &CallInfo, caller_id: u32, rel_path: &str,
type_map: &HashMap<&str, &str>,
type_map: &HashMap<&str, (&str, f64)>,
seen_edges: &mut HashSet<u64>, edges: &mut Vec<ComputedEdge>,
) {
let Some(ref receiver) = call.receiver else { return };
if ctx.builtin_set.contains(receiver.as_str())
|| receiver == "this" || receiver == "self" || receiver == "super"
{ return; }

let effective_receiver = type_map.get(receiver.as_str()).copied().unwrap_or(receiver.as_str());
let effective_receiver = type_map.get(receiver.as_str()).map(|&(t, _)| t).unwrap_or(receiver.as_str());
let type_resolved = effective_receiver != receiver.as_str();

let samefile = ctx.nodes_by_name_and_file
Expand Down
2 changes: 2 additions & 0 deletions crates/codegraph-core/src/extractors/c.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ fn match_c_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _dept
symbols.type_map.push(TypeMapEntry {
name: final_name,
type_name: type_name.to_string(),
confidence: 0.9,
});
}
}
Expand All @@ -55,6 +56,7 @@ fn match_c_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _dept
symbols.type_map.push(TypeMapEntry {
name,
type_name: node_text(&type_node, source).to_string(),
confidence: 0.9,
});
}
}
Expand Down
2 changes: 2 additions & 0 deletions crates/codegraph-core/src/extractors/cpp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ fn match_cpp_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _de
symbols.type_map.push(TypeMapEntry {
name: final_name,
type_name: type_name.to_string(),
confidence: 0.9,
});
}
}
Expand All @@ -54,6 +55,7 @@ fn match_cpp_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _de
symbols.type_map.push(TypeMapEntry {
name,
type_name: node_text(&type_node, source).to_string(),
confidence: 0.9,
});
}
}
Expand Down
2 changes: 2 additions & 0 deletions crates/codegraph-core/src/extractors/csharp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ fn match_csharp_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols,
symbols.type_map.push(TypeMapEntry {
name: node_text(&name_node, source).to_string(),
type_name: type_name.to_string(),
confidence: 0.9,
});
}
}
Expand All @@ -445,6 +446,7 @@ fn match_csharp_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols,
symbols.type_map.push(TypeMapEntry {
name: node_text(&name_node, source).to_string(),
type_name: type_name.to_string(),
confidence: 0.9,
});
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/codegraph-core/src/extractors/go.rs
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ fn collect_go_typed_identifiers(node: &Node, source: &[u8], type_map: &mut Vec<T
type_map.push(TypeMapEntry {
name: node_text(&child, source).to_string(),
type_name: type_name.to_string(),
confidence: 0.9,
});
}
}
Expand Down
2 changes: 2 additions & 0 deletions crates/codegraph-core/src/extractors/java.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ fn match_java_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _d
symbols.type_map.push(TypeMapEntry {
name: node_text(&name_node, source).to_string(),
type_name: type_name.to_string(),
confidence: 0.9,
});
}
}
Expand All @@ -54,6 +55,7 @@ fn match_java_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _d
symbols.type_map.push(TypeMapEntry {
name: node_text(&name_node, source).to_string(),
type_name: type_name.to_string(),
confidence: 0.9,
});
}
}
Expand Down
8 changes: 5 additions & 3 deletions crates/codegraph-core/src/extractors/javascript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,23 +58,24 @@ fn match_js_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _dep
if let Some(name_n) = node.child_by_field_name("name") {
if name_n.kind() == "identifier" {
let var_name = node_text(&name_n, source);
// Type annotation takes priority
// Type annotation: confidence 0.9
if let Some(type_anno) = find_child(node, "type_annotation") {
if let Some(type_name) = extract_simple_type_name(&type_anno, source) {
symbols.type_map.push(TypeMapEntry {
name: var_name.to_string(),
type_name: type_name.to_string(),
confidence: 0.9,
});
return; // Skip new_expression check — annotation wins
}
}
// Fall back to new expression inference
// Constructor: confidence 1.0 (overrides annotation in edge builder)
if let Some(value_n) = node.child_by_field_name("value") {
if value_n.kind() == "new_expression" {
if let Some(type_name) = extract_new_expr_type_name(&value_n, source) {
symbols.type_map.push(TypeMapEntry {
name: var_name.to_string(),
type_name: type_name.to_string(),
confidence: 1.0,
});
}
}
Expand All @@ -93,6 +94,7 @@ fn match_js_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _dep
symbols.type_map.push(TypeMapEntry {
name: node_text(&name_node, source).to_string(),
type_name: type_name.to_string(),
confidence: 0.9,
});
}
}
Expand Down
2 changes: 2 additions & 0 deletions crates/codegraph-core/src/extractors/kotlin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ fn match_kotlin_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols,
symbols.type_map.push(TypeMapEntry {
name,
type_name: type_name.to_string(),
confidence: 0.9,
});
}
}
Expand All @@ -44,6 +45,7 @@ fn match_kotlin_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols,
symbols.type_map.push(TypeMapEntry {
name: node_text(&name_node, source).to_string(),
type_name: node_text(&type_node, source).to_string(),
confidence: 0.9,
});
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/codegraph-core/src/extractors/php.rs
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ fn match_php_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _de
symbols.type_map.push(TypeMapEntry {
name: node_text(&name_node, source).to_string(),
type_name: type_name.to_string(),
confidence: 0.9,
});
}
}
Expand Down
2 changes: 2 additions & 0 deletions crates/codegraph-core/src/extractors/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ fn match_python_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols,
symbols.type_map.push(TypeMapEntry {
name: name.to_string(),
type_name: type_name.to_string(),
confidence: 0.9,
});
}
}
Expand All @@ -352,6 +353,7 @@ fn match_python_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols,
symbols.type_map.push(TypeMapEntry {
name: node_text(&name_node, source).to_string(),
type_name: type_name.to_string(),
confidence: 0.9,
});
}
}
Expand Down
2 changes: 2 additions & 0 deletions crates/codegraph-core/src/extractors/rust_lang.rs
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ fn match_rust_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _d
symbols.type_map.push(TypeMapEntry {
name: node_text(&pattern, source).to_string(),
type_name: type_name.to_string(),
confidence: 0.9,
});
}
}
Expand All @@ -410,6 +411,7 @@ fn match_rust_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _d
symbols.type_map.push(TypeMapEntry {
name: name.to_string(),
type_name: type_name.to_string(),
confidence: 0.9,
});
}
}
Expand Down
2 changes: 2 additions & 0 deletions crates/codegraph-core/src/extractors/scala.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ fn match_scala_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _
symbols.type_map.push(TypeMapEntry {
name: node_text(&pat, source).to_string(),
type_name: node_text(&type_node, source).to_string(),
confidence: 0.9,
});
}
}
Expand All @@ -43,6 +44,7 @@ fn match_scala_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _
symbols.type_map.push(TypeMapEntry {
name: node_text(&name_node, source).to_string(),
type_name: node_text(&type_node, source).to_string(),
confidence: 0.9,
});
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/codegraph-core/src/extractors/swift.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ fn match_swift_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _
symbols.type_map.push(TypeMapEntry {
name: name.to_string(),
type_name: type_name.to_string(),
confidence: 0.9,
});
}
}
Expand Down
4 changes: 4 additions & 0 deletions crates/codegraph-core/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,10 @@ pub struct TypeMapEntry {
pub name: String,
#[napi(js_name = "typeName")]
pub type_name: String,
/// Confidence: 0.9 = type annotation, 1.0 = constructor, 0.7 = factory.
/// Used to resolve conflicts when the same name appears multiple times.
#[napi(default = 0.9)]
pub confidence: f64,
}

#[napi(object)]
Expand Down
13 changes: 12 additions & 1 deletion src/domain/graph/builder/stages/build-edges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ function buildCallEdgesNative(
if (!fileNodeRow) continue;

const importedNames = buildImportedNamesForNative(ctx, relPath, symbols, rootDir);
const typeMap: Array<{ name: string; typeName: string; confidence: number }> =
const typeMapRaw: Array<{ name: string; typeName: string; confidence: number }> =
symbols.typeMap instanceof Map
? [...symbols.typeMap.entries()].map(([name, entry]) => ({
name,
Expand All @@ -352,6 +352,17 @@ function buildCallEdgesNative(
: Array.isArray(symbols.typeMap)
? (symbols.typeMap as Array<{ name: string; typeName: string; confidence: number }>)
: [];
// Deduplicate: keep highest-confidence entry per name (first-wins on tie),
// matching JS setTypeMapEntry semantics. This ensures parity even when
// the native edge builder's HashMap would otherwise use last-wins.
const typeMapDedup = new Map<string, { name: string; typeName: string; confidence: number }>();
for (const entry of typeMapRaw) {
const existing = typeMapDedup.get(entry.name);
if (!existing || entry.confidence > existing.confidence) {
typeMapDedup.set(entry.name, entry);
}
}
const typeMap = [...typeMapDedup.values()];
Comment on lines 345 to +367
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Dedup is a no-op for the Map path

When symbols.typeMap instanceof Map (the WASM/JS extraction path), setTypeMapEntry already enforces highest-confidence-wins at insertion time, so the resulting array has no duplicate names — the dedup loop adds no correctness value for that branch. The fix is only needed for the Array.isArray branch. Consider scoping the dedup to that branch, or add a comment clarifying this is intentional belt-and-suspenders for pre-rebuilt-addon builds.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — expanded the comment to clarify that the dedup is a no-op for the Map path (already deduped by setTypeMapEntry) and only needed for the Array branch (pre-rebuilt native addon). Kept it unconditional as intentional belt-and-suspenders since it's a cheap O(n) pass.

nativeFiles.push({
file: relPath,
fileNodeId: fileNodeRow.id,
Expand Down
Loading