Skip to content

Commit f3f7913

Browse files
committed
fix: resolve merge conflicts with feat/prototype-resolver-1317
2 parents 92b58db + 194507c commit f3f7913

53 files changed

Lines changed: 1522 additions & 114 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

biome.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,13 @@
2525
"semicolons": "always",
2626
"trailingCommas": "all"
2727
}
28-
}
28+
},
29+
"overrides": [
30+
{
31+
"includes": ["tests/benchmarks/resolution/fixtures/**"],
32+
"linter": {
33+
"enabled": false
34+
}
35+
}
36+
]
2937
}

crates/codegraph-core/src/edge_builder.rs

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -483,15 +483,27 @@ fn resolve_call_targets<'a>(
483483
if !class_scoped.is_empty() { return class_scoped; }
484484
}
485485

486-
// Broader fallback: same-file suffix scan to pick up CHA-expanded targets
487-
// (subclasses that override the method).
486+
// Broader fallback: same-file suffix scan. Always restrict to the caller's
487+
// own class prefix — regardless of how many matches are found — to avoid
488+
// false-positive edges to unrelated classes in the same file.
489+
// (e.g. this.area() inside Shape.describe must never yield Calculator.area,
490+
// even when Calculator.area is the only method with that name in the file.)
488491
let suffix = format!(".{}", call.name);
489492
if let Some(file_nodes) = ctx.nodes_by_file.get(rel_path) {
490493
let same_file_methods: Vec<&NodeInfo> = file_nodes.iter()
491494
.filter(|n| n.kind == "method" && n.name.ends_with(&suffix))
492495
.copied()
493496
.collect();
494-
if !same_file_methods.is_empty() { return same_file_methods; }
497+
if !same_file_methods.is_empty() {
498+
if let Some(dot_pos) = caller_name.find('.') {
499+
let caller_prefix = format!("{}.", &caller_name[..dot_pos]);
500+
let caller_scoped: Vec<&NodeInfo> = same_file_methods.iter()
501+
.filter(|n| n.name.starts_with(&caller_prefix))
502+
.copied()
503+
.collect();
504+
if !caller_scoped.is_empty() { return caller_scoped; }
505+
}
506+
}
495507
}
496508
}
497509
return exact; // empty
@@ -1406,3 +1418,52 @@ mod call_edge_tests {
14061418
assert_eq!(receiver_edge.unwrap().target_id, 2);
14071419
}
14081420
}
1421+
1422+
#[cfg(test)]
1423+
mod inline_new_type_tests {
1424+
use super::extract_inline_new_type;
1425+
1426+
#[test]
1427+
fn parens_new_uppercase() {
1428+
assert_eq!(extract_inline_new_type("(new Foo)"), Some("Foo".to_string()));
1429+
}
1430+
1431+
#[test]
1432+
fn parens_new_with_args() {
1433+
// (new Foo('arg')) — parens and constructor args
1434+
assert_eq!(extract_inline_new_type("(new Foo('arg'))"), Some("Foo".to_string()));
1435+
}
1436+
1437+
#[test]
1438+
fn no_parens_new_uppercase() {
1439+
assert_eq!(extract_inline_new_type("new Bar"), Some("Bar".to_string()));
1440+
}
1441+
1442+
#[test]
1443+
fn underscore_prefix_accepted() {
1444+
assert_eq!(extract_inline_new_type("new _Factory"), Some("_Factory".to_string()));
1445+
}
1446+
1447+
#[test]
1448+
fn dollar_prefix_accepted() {
1449+
assert_eq!(extract_inline_new_type("new $Service"), Some("$Service".to_string()));
1450+
}
1451+
1452+
#[test]
1453+
fn lowercase_constructor_rejected() {
1454+
// `new foo()` — lowercase, should return None to avoid false positives
1455+
assert_eq!(extract_inline_new_type("new foo"), None);
1456+
}
1457+
1458+
#[test]
1459+
fn not_a_new_expression() {
1460+
// plain receiver name — no `new` keyword
1461+
assert_eq!(extract_inline_new_type("myVar"), None);
1462+
}
1463+
1464+
#[test]
1465+
fn new_without_whitespace_is_not_new_keyword() {
1466+
// `newFoo` — not a `new` keyword, just an identifier
1467+
assert_eq!(extract_inline_new_type("newFoo"), None);
1468+
}
1469+
}

crates/codegraph-core/src/extractors/javascript.rs

Lines changed: 232 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,18 @@ fn match_js_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _dep
9595
});
9696
}
9797
}
98+
// Phase 8.3e: Object.create({ key: fn }) → composite pts key per property
99+
if value_n.kind() == "call_expression" {
100+
seed_object_create_entries(var_name, &value_n, source, symbols);
101+
}
98102
}
99103
}
100104
}
101105
}
106+
// Phase 8.3e: Object.defineProperty / defineProperties → composite pts key
107+
"call_expression" => {
108+
seed_define_property_entries(node, source, symbols);
109+
}
102110
"required_parameter" | "optional_parameter" => {
103111
let name_node = node.child_by_field_name("pattern")
104112
.or_else(|| node.child_by_field_name("left"))
@@ -196,13 +204,158 @@ fn is_js_builtin_global(name: &str) -> bool {
196204
)
197205
}
198206

207+
// ── Phase 8.3e: Object.defineProperty / defineProperties / create ────────────
208+
209+
/// Seed composite pts keys for `Object.defineProperty(obj, "key", { value: fn })`
210+
/// and `Object.defineProperties(obj, { "key": { value: fn }, ... })`.
211+
fn seed_define_property_entries(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
212+
let Some(callee) = node.child_by_field_name("function") else { return };
213+
if callee.kind() != "member_expression" { return; }
214+
let Some(callee_obj) = callee.child_by_field_name("object") else { return };
215+
if node_text(&callee_obj, source) != "Object" { return; }
216+
let Some(callee_prop) = callee.child_by_field_name("property") else { return };
217+
let method = node_text(&callee_prop, source);
218+
if method != "defineProperty" && method != "defineProperties" { return; }
219+
220+
let args_node = node.child_by_field_name("arguments")
221+
.or_else(|| find_child(node, "arguments"));
222+
let Some(args_node) = args_node else { return };
223+
224+
// Collect non-punctuation argument nodes in order
225+
let mut args: Vec<Node> = Vec::new();
226+
for i in 0..args_node.child_count() {
227+
let Some(child) = args_node.child(i) else { continue };
228+
if !matches!(child.kind(), "(" | ")" | ",") {
229+
args.push(child);
230+
}
231+
}
232+
233+
if method == "defineProperty" {
234+
// Object.defineProperty(obj, "key", { value: fn })
235+
if args.len() < 3 { return; }
236+
if args[0].kind() != "identifier" { return; }
237+
let obj_name = node_text(&args[0], source);
238+
let Some(key) = extract_string_fragment(&args[1], source) else { return };
239+
let Some(target) = find_descriptor_value(&args[2], source) else { return };
240+
symbols.type_map.push(TypeMapEntry {
241+
name: format!("{}.{}", obj_name, key),
242+
type_name: target.to_string(),
243+
confidence: 0.85,
244+
});
245+
} else {
246+
// Object.defineProperties(obj, { "key": { value: fn }, ... })
247+
if args.len() < 2 { return; }
248+
if args[0].kind() != "identifier" { return; }
249+
let obj_name = node_text(&args[0], source).to_string();
250+
if args[1].kind() != "object" { return; }
251+
seed_descriptor_object(&obj_name, &args[1], source, symbols);
252+
}
253+
}
254+
255+
/// Seed composite pts keys from `const obj = Object.create({ f1, f2 })`.
256+
fn seed_object_create_entries(var_name: &str, call_node: &Node, source: &[u8], symbols: &mut FileSymbols) {
257+
let Some(callee) = call_node.child_by_field_name("function") else { return };
258+
if callee.kind() != "member_expression" { return; }
259+
let Some(callee_obj) = callee.child_by_field_name("object") else { return };
260+
if node_text(&callee_obj, source) != "Object" { return; }
261+
let Some(callee_prop) = callee.child_by_field_name("property") else { return };
262+
if node_text(&callee_prop, source) != "create" { return; }
263+
264+
let args_node = call_node.child_by_field_name("arguments")
265+
.or_else(|| find_child(call_node, "arguments"));
266+
let Some(args_node) = args_node else { return };
267+
268+
// First non-punctuation argument = prototype object
269+
let proto = (0..args_node.child_count())
270+
.filter_map(|i| args_node.child(i))
271+
.find(|n| !matches!(n.kind(), "(" | ")" | ","));
272+
let Some(proto) = proto else { return };
273+
if proto.kind() != "object" { return };
274+
275+
for i in 0..proto.child_count() {
276+
let Some(child) = proto.child(i) else { continue };
277+
match child.kind() {
278+
"shorthand_property_identifier" => {
279+
// { f1 } shorthand — property name equals value name
280+
let name = node_text(&child, source);
281+
symbols.type_map.push(TypeMapEntry {
282+
name: format!("{}.{}", var_name, name),
283+
type_name: name.to_string(),
284+
confidence: 0.85,
285+
});
286+
}
287+
"pair" => {
288+
let Some(key_n) = child.child_by_field_name("key") else { continue };
289+
let Some(val_n) = child.child_by_field_name("value") else { continue };
290+
if val_n.kind() != "identifier" { continue; }
291+
let key = if key_n.kind() == "string" {
292+
extract_string_fragment(&key_n, source).map(|s| s.to_string())
293+
} else {
294+
Some(node_text(&key_n, source).to_string())
295+
};
296+
let Some(key) = key else { continue };
297+
symbols.type_map.push(TypeMapEntry {
298+
name: format!("{}.{}", var_name, key),
299+
type_name: node_text(&val_n, source).to_string(),
300+
confidence: 0.85,
301+
});
302+
}
303+
_ => {}
304+
}
305+
}
306+
}
307+
308+
/// Iterate over the properties of a `defineProperties` descriptor object and seed the type_map.
309+
fn seed_descriptor_object(obj_name: &str, obj_node: &Node, source: &[u8], symbols: &mut FileSymbols) {
310+
for i in 0..obj_node.child_count() {
311+
let Some(child) = obj_node.child(i) else { continue };
312+
if child.kind() != "pair" { continue; }
313+
let Some(key_n) = child.child_by_field_name("key") else { continue };
314+
let Some(val_n) = child.child_by_field_name("value") else { continue };
315+
let key = if key_n.kind() == "string" {
316+
extract_string_fragment(&key_n, source).map(|s| s.to_string())
317+
} else {
318+
Some(node_text(&key_n, source).to_string())
319+
};
320+
let Some(key) = key else { continue };
321+
let Some(target) = find_descriptor_value(&val_n, source) else { continue };
322+
symbols.type_map.push(TypeMapEntry {
323+
name: format!("{}.{}", obj_name, key),
324+
type_name: target.to_string(),
325+
confidence: 0.85,
326+
});
327+
}
328+
}
329+
330+
/// Extract the text of the `string_fragment` child of a string node, i.e. content without quotes.
331+
fn extract_string_fragment<'a>(node: &Node<'a>, source: &'a [u8]) -> Option<&'a str> {
332+
if node.kind() != "string" { return None; }
333+
find_child(node, "string_fragment").map(|n| node_text(&n, source))
334+
}
335+
336+
/// Find the `value` identifier in a property descriptor object `{ value: fn }`.
337+
fn find_descriptor_value<'a>(node: &Node<'a>, source: &'a [u8]) -> Option<&'a str> {
338+
if node.kind() != "object" { return None; }
339+
for i in 0..node.child_count() {
340+
let Some(child) = node.child(i) else { continue };
341+
if child.kind() != "pair" { continue; }
342+
let Some(key) = child.child_by_field_name("key") else { continue };
343+
if node_text(&key, source) != "value" { continue; }
344+
let Some(val) = child.child_by_field_name("value") else { continue };
345+
if val.kind() == "identifier" {
346+
return Some(node_text(&val, source));
347+
}
348+
}
349+
None
350+
}
351+
199352
// ── Return-type map extraction (Phase 8.2 parity) ───────────────────────────
200353

201354
/// Walk the AST collecting function/method return types into `symbols.return_type_map`.
202355
/// Mirrors `extractReturnTypeMapWalk` in src/extractors/javascript.ts.
203356
fn match_js_return_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: usize) {
204357
match node.kind() {
205-
"function_declaration" => {
358+
"function_declaration" | "generator_function_declaration" => {
206359
let Some(name_n) = node.child_by_field_name("name") else { return };
207360
let fn_name = node_text(&name_n, source);
208361
if fn_name == "constructor" { return; }
@@ -230,9 +383,9 @@ fn match_js_return_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbol
230383
let Some(name_n) = node.child_by_field_name("name") else { return };
231384
if name_n.kind() != "identifier" { return; }
232385
let Some(value_n) = node.child_by_field_name("value") else { return };
233-
// Only arrow_function and function_expression match the TS reference;
386+
// Only arrow_function, function_expression and generator_function match the TS reference;
234387
// "function" is not a valid tree-sitter value-expression kind here.
235-
if !matches!(value_n.kind(), "arrow_function" | "function_expression") {
388+
if !matches!(value_n.kind(), "arrow_function" | "function_expression" | "generator_function") {
236389
return;
237390
}
238391
let var_name = node_text(&name_n, source);
@@ -479,7 +632,7 @@ fn match_js_call_assignments(node: &Node, source: &[u8], symbols: &mut FileSymbo
479632

480633
fn match_js_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: usize) {
481634
match node.kind() {
482-
"function_declaration" => handle_function_decl(node, source, symbols),
635+
"function_declaration" | "generator_function_declaration" => handle_function_decl(node, source, symbols),
483636
"class_declaration" | "abstract_class_declaration" => {
484637
handle_class_decl(node, source, symbols)
485638
}
@@ -642,7 +795,7 @@ fn handle_var_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
642795
let value_n = declarator.child_by_field_name("value");
643796
let (Some(name_n), Some(value_n)) = (name_n, value_n) else { continue };
644797
let vt = value_n.kind();
645-
if vt == "arrow_function" || vt == "function_expression" || vt == "function" {
798+
if vt == "arrow_function" || vt == "function_expression" || vt == "function" || vt == "generator_function" {
646799
let children = extract_js_parameters(&value_n, source);
647800
symbols.definitions.push(Definition {
648801
name: node_text(&name_n, source).to_string(),
@@ -796,7 +949,7 @@ fn handle_export_stmt(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
796949

797950
fn handle_export_declaration(node: &Node, decl: &Node, source: &[u8], symbols: &mut FileSymbols) {
798951
let (kind_str, field) = match decl.kind() {
799-
"function_declaration" => ("function", "name"),
952+
"function_declaration" | "generator_function_declaration" => ("function", "name"),
800953
"class_declaration" | "abstract_class_declaration" => ("class", "name"),
801954
"interface_declaration" => ("interface", "name"),
802955
"type_alias_declaration" => ("type", "name"),
@@ -2537,4 +2690,77 @@ mod tests {
25372690
let def = s.definitions.iter().find(|d| d.name.contains("Array"));
25382691
assert!(def.is_none(), "built-in prototype assignment should be ignored; got: {:?}", def);
25392692
}
2693+
2694+
2695+
/// Phase 8.3e: Object.defineProperty seeds composite type_map key.
2696+
#[test]
2697+
fn type_map_from_define_property() {
2698+
let s = parse_js(
2699+
"function f1() {}\n\
2700+
const obj = {};\n\
2701+
Object.defineProperty(obj, \"f\", { value: f1 });",
2702+
);
2703+
let entry = s.type_map.iter().find(|e| e.name == "obj.f");
2704+
assert!(entry.is_some(), "type_map should contain 'obj.f'; got: {:?}", s.type_map);
2705+
assert_eq!(entry.unwrap().type_name, "f1");
2706+
}
2707+
2708+
/// Phase 8.3e: Object.defineProperties seeds composite type_map keys.
2709+
#[test]
2710+
fn type_map_from_define_properties() {
2711+
let s = parse_js(
2712+
"function f1() {}\n\
2713+
function f2() {}\n\
2714+
const obj = {};\n\
2715+
Object.defineProperties(obj, {\n\
2716+
\"f1\": { value: f1 },\n\
2717+
\"f2\": { value: f2 },\n\
2718+
});",
2719+
);
2720+
let e1 = s.type_map.iter().find(|e| e.name == "obj.f1");
2721+
let e2 = s.type_map.iter().find(|e| e.name == "obj.f2");
2722+
assert!(e1.is_some(), "type_map should contain 'obj.f1'; got: {:?}", s.type_map);
2723+
assert!(e2.is_some(), "type_map should contain 'obj.f2'; got: {:?}", s.type_map);
2724+
assert_eq!(e1.unwrap().type_name, "f1");
2725+
assert_eq!(e2.unwrap().type_name, "f2");
2726+
}
2727+
2728+
/// Phase 8.3e: Object.create seeds composite type_map keys from shorthand proto.
2729+
#[test]
2730+
fn type_map_from_object_create() {
2731+
let s = parse_js(
2732+
"function f1() {}\n\
2733+
function f2() {}\n\
2734+
const obj = Object.create({ f1, f2 });",
2735+
);
2736+
let e1 = s.type_map.iter().find(|e| e.name == "obj.f1");
2737+
let e2 = s.type_map.iter().find(|e| e.name == "obj.f2");
2738+
assert!(e1.is_some(), "type_map should contain 'obj.f1'; got: {:?}", s.type_map);
2739+
assert!(e2.is_some(), "type_map should contain 'obj.f2'; got: {:?}", s.type_map);
2740+
assert_eq!(e1.unwrap().type_name, "f1");
2741+
assert_eq!(e2.unwrap().type_name, "f2");
2742+
}
2743+
2744+
/// Phase 8.3e: call receiver is correctly recorded for obj.f() inside defProp body.
2745+
#[test]
2746+
fn call_receiver_for_define_property() {
2747+
let s = parse_js(
2748+
"function f1() {}\n\
2749+
function defProp() {\n\
2750+
const obj = {};\n\
2751+
Object.defineProperty(obj, \"f\", { value: f1 });\n\
2752+
obj.f();\n\
2753+
}",
2754+
);
2755+
let tm = s.type_map.iter().find(|e| e.name == "obj.f");
2756+
assert!(tm.is_some(), "type_map should contain 'obj.f'; got: {:?}", s.type_map);
2757+
assert_eq!(tm.unwrap().type_name, "f1");
2758+
2759+
let call = s.calls.iter().find(|c| c.name == "f" && c.receiver.as_deref() == Some("obj"));
2760+
assert!(
2761+
call.is_some(),
2762+
"calls should contain obj.f() with receiver='obj'; got: {:?}",
2763+
s.calls.iter().map(|c| (&c.name, &c.receiver)).collect::<Vec<_>>()
2764+
);
2765+
}
25402766
}

0 commit comments

Comments
 (0)