Skip to content

Commit c3db002

Browse files
authored
Merge branch 'main' into chore/bench-javacg-static-1307
2 parents fab7780 + f0db64c commit c3db002

18 files changed

Lines changed: 1007 additions & 78 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
1010
1111
> **Never silently skip verification.** If tests, builds, or any verification step cannot run or fails for any reason (compilation errors, platform issues, missing dependencies), STOP and report the issue to the user immediately. Never silently proceed with unverified changes. Let the user decide whether to proceed — do not make that decision yourself.
1212
13-
> **Scope discipline — open issues, don't expand scope.** When you encounter a problem unrelated to the current task — pre-existing bugs, code that needs a bigger refactor, or any defect that does not directly affect the result of what you're doing — open a GitHub issue with `gh issue create` and keep going. Only fix it inline when it directly affects the correctness or outcome of the work in front of you. This keeps PRs focused (one concern per PR) while ensuring the finding is not lost.
13+
> **Scope discipline — open issues, don't expand scope.** When you encounter anything out of scope — a pre-existing bug, a refactor opportunity, a potential improvement, a missing feature, or any other finding that doesn't directly affect the correctness of the current task**immediately open a GitHub issue with `gh issue create` before continuing**. Do not hold the finding in memory or defer it to a comment. Only address it inline when it directly blocks the result of the work in front of you. This keeps PRs focused (one concern per PR) while ensuring no finding is lost.
1414
1515
> **Prioritize the best architecture, not the smallest diff.** Do not default to the simplest or most localized fix. Choose the approach that fits the codebase's architecture best, even when that means larger changes, moving code across modules, or restructuring an abstraction. Do not be afraid of bigger changes — a larger diff that leaves the design healthier is preferable to a small diff that entrenches a poor structure. Surface the architectural reasoning to the user; don't silently shrink the change to avoid the work.
1616

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

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,18 @@ fn match_js_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _dep
9393
});
9494
}
9595
}
96+
// Phase 8.3e: Object.create({ key: fn }) → composite pts key per property
97+
if value_n.kind() == "call_expression" {
98+
seed_object_create_entries(var_name, &value_n, source, symbols);
99+
}
96100
}
97101
}
98102
}
99103
}
104+
// Phase 8.3e: Object.defineProperty / defineProperties → composite pts key
105+
"call_expression" => {
106+
seed_define_property_entries(node, source, symbols);
107+
}
100108
"required_parameter" | "optional_parameter" => {
101109
let name_node = node.child_by_field_name("pattern")
102110
.or_else(|| node.child_by_field_name("left"))
@@ -194,6 +202,151 @@ fn is_js_builtin_global(name: &str) -> bool {
194202
)
195203
}
196204

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

199352
/// Walk the AST collecting function/method return types into `symbols.return_type_map`.
@@ -2292,4 +2445,76 @@ mod tests {
22922445
"compute call should have receiver='calc'"
22932446
);
22942447
}
2448+
2449+
/// Phase 8.3e: Object.defineProperty seeds composite type_map key.
2450+
#[test]
2451+
fn type_map_from_define_property() {
2452+
let s = parse_js(
2453+
"function f1() {}\n\
2454+
const obj = {};\n\
2455+
Object.defineProperty(obj, \"f\", { value: f1 });",
2456+
);
2457+
let entry = s.type_map.iter().find(|e| e.name == "obj.f");
2458+
assert!(entry.is_some(), "type_map should contain 'obj.f'; got: {:?}", s.type_map);
2459+
assert_eq!(entry.unwrap().type_name, "f1");
2460+
}
2461+
2462+
/// Phase 8.3e: Object.defineProperties seeds composite type_map keys.
2463+
#[test]
2464+
fn type_map_from_define_properties() {
2465+
let s = parse_js(
2466+
"function f1() {}\n\
2467+
function f2() {}\n\
2468+
const obj = {};\n\
2469+
Object.defineProperties(obj, {\n\
2470+
\"f1\": { value: f1 },\n\
2471+
\"f2\": { value: f2 },\n\
2472+
});",
2473+
);
2474+
let e1 = s.type_map.iter().find(|e| e.name == "obj.f1");
2475+
let e2 = s.type_map.iter().find(|e| e.name == "obj.f2");
2476+
assert!(e1.is_some(), "type_map should contain 'obj.f1'; got: {:?}", s.type_map);
2477+
assert!(e2.is_some(), "type_map should contain 'obj.f2'; got: {:?}", s.type_map);
2478+
assert_eq!(e1.unwrap().type_name, "f1");
2479+
assert_eq!(e2.unwrap().type_name, "f2");
2480+
}
2481+
2482+
/// Phase 8.3e: Object.create seeds composite type_map keys from shorthand proto.
2483+
#[test]
2484+
fn type_map_from_object_create() {
2485+
let s = parse_js(
2486+
"function f1() {}\n\
2487+
function f2() {}\n\
2488+
const obj = Object.create({ f1, f2 });",
2489+
);
2490+
let e1 = s.type_map.iter().find(|e| e.name == "obj.f1");
2491+
let e2 = s.type_map.iter().find(|e| e.name == "obj.f2");
2492+
assert!(e1.is_some(), "type_map should contain 'obj.f1'; got: {:?}", s.type_map);
2493+
assert!(e2.is_some(), "type_map should contain 'obj.f2'; got: {:?}", s.type_map);
2494+
assert_eq!(e1.unwrap().type_name, "f1");
2495+
assert_eq!(e2.unwrap().type_name, "f2");
2496+
}
2497+
2498+
/// Phase 8.3e: call receiver is correctly recorded for obj.f() inside defProp body.
2499+
#[test]
2500+
fn call_receiver_for_define_property() {
2501+
let s = parse_js(
2502+
"function f1() {}\n\
2503+
function defProp() {\n\
2504+
const obj = {};\n\
2505+
Object.defineProperty(obj, \"f\", { value: f1 });\n\
2506+
obj.f();\n\
2507+
}",
2508+
);
2509+
let tm = s.type_map.iter().find(|e| e.name == "obj.f");
2510+
assert!(tm.is_some(), "type_map should contain 'obj.f'; got: {:?}", s.type_map);
2511+
assert_eq!(tm.unwrap().type_name, "f1");
2512+
2513+
let call = s.calls.iter().find(|c| c.name == "f" && c.receiver.as_deref() == Some("obj"));
2514+
assert!(
2515+
call.is_some(),
2516+
"calls should contain obj.f() with receiver='obj'; got: {:?}",
2517+
s.calls.iter().map(|c| (&c.name, &c.receiver)).collect::<Vec<_>>()
2518+
);
2519+
}
22952520
}

src/domain/graph/builder/call-resolver.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,40 @@ export function resolveByMethodOrGlobal(
7373
? call.receiver.slice('this.'.length)
7474
: call.receiver;
7575
const typeEntry = typeMap.get(effectiveReceiver) ?? typeMap.get(call.receiver);
76-
const typeName = typeEntry
76+
let typeName = typeEntry
7777
? typeof typeEntry === 'string'
7878
? typeEntry
7979
: (typeEntry as { type?: string }).type
8080
: null;
81+
82+
// Handle inline new-expression receivers: `(new Foo).bar()` or `(new Foo()).bar()`.
83+
// extractReceiverName returns the raw node text for non-identifier nodes, so `(new A).t()`
84+
// produces receiver='(new A)'. Extract the constructor name directly.
85+
if (!typeName && call.receiver) {
86+
const m = /^\(?\s*new\s+([A-Z_$][A-Za-z0-9_$]*)/.exec(call.receiver);
87+
if (m?.[1]) typeName = m[1];
88+
}
89+
8190
if (typeName) {
8291
const typed = lookup.byName(`${typeName}.${call.name}`).filter((n) => n.kind === 'method');
8392
if (typed.length > 0) return typed;
93+
94+
// Prototype alias: `Foo.prototype.bar = identifier` seeds typeMap['Foo.bar'] = { type: identifier }.
95+
// Checked after the symbol-DB lookup so an actual method definition always wins.
96+
const protoEntry = typeMap.get(`${typeName}.${call.name}`);
97+
const protoTarget = protoEntry
98+
? typeof protoEntry === 'string'
99+
? protoEntry
100+
: (protoEntry as { type?: string }).type
101+
: null;
102+
if (protoTarget) {
103+
const resolved = lookup
104+
.byName(protoTarget)
105+
.filter((t) => computeConfidence(relPath, t.file, null) >= 0.5);
106+
if (resolved.length > 0) return resolved;
107+
}
84108
}
109+
85110
// Phase 8.3d: composite pts key — `obj.prop = fn` seeds typeMap['obj.prop'] = { type: 'fn' }.
86111
// When a call site references `obj.prop` as a callback, resolve directly to the target fn.
87112
const compositeEntry = typeMap.get(`${call.receiver}.${call.name}`);

0 commit comments

Comments
 (0)