Skip to content

Commit bc8732f

Browse files
authored
Merge branch 'main' into docs/phase8-language-analysis-reference-map
2 parents 5c82617 + c62e3b8 commit bc8732f

18 files changed

Lines changed: 1167 additions & 39 deletions

File tree

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

Lines changed: 231 additions & 6 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,13 +202,158 @@ 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`.
200353
/// Mirrors `extractReturnTypeMapWalk` in src/extractors/javascript.ts.
201354
fn match_js_return_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: usize) {
202355
match node.kind() {
203-
"function_declaration" => {
356+
"function_declaration" | "generator_function_declaration" => {
204357
let Some(name_n) = node.child_by_field_name("name") else { return };
205358
let fn_name = node_text(&name_n, source);
206359
if fn_name == "constructor" { return; }
@@ -228,9 +381,9 @@ fn match_js_return_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbol
228381
let Some(name_n) = node.child_by_field_name("name") else { return };
229382
if name_n.kind() != "identifier" { return; }
230383
let Some(value_n) = node.child_by_field_name("value") else { return };
231-
// Only arrow_function and function_expression match the TS reference;
384+
// Only arrow_function, function_expression and generator_function match the TS reference;
232385
// "function" is not a valid tree-sitter value-expression kind here.
233-
if !matches!(value_n.kind(), "arrow_function" | "function_expression") {
386+
if !matches!(value_n.kind(), "arrow_function" | "function_expression" | "generator_function") {
234387
return;
235388
}
236389
let var_name = node_text(&name_n, source);
@@ -334,7 +487,7 @@ fn match_js_call_assignments(node: &Node, source: &[u8], symbols: &mut FileSymbo
334487

335488
fn match_js_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: usize) {
336489
match node.kind() {
337-
"function_declaration" => handle_function_decl(node, source, symbols),
490+
"function_declaration" | "generator_function_declaration" => handle_function_decl(node, source, symbols),
338491
"class_declaration" | "abstract_class_declaration" => {
339492
handle_class_decl(node, source, symbols)
340493
}
@@ -497,7 +650,7 @@ fn handle_var_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
497650
let value_n = declarator.child_by_field_name("value");
498651
let (Some(name_n), Some(value_n)) = (name_n, value_n) else { continue };
499652
let vt = value_n.kind();
500-
if vt == "arrow_function" || vt == "function_expression" || vt == "function" {
653+
if vt == "arrow_function" || vt == "function_expression" || vt == "function" || vt == "generator_function" {
501654
let children = extract_js_parameters(&value_n, source);
502655
symbols.definitions.push(Definition {
503656
name: node_text(&name_n, source).to_string(),
@@ -651,7 +804,7 @@ fn handle_export_stmt(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
651804

652805
fn handle_export_declaration(node: &Node, decl: &Node, source: &[u8], symbols: &mut FileSymbols) {
653806
let (kind_str, field) = match decl.kind() {
654-
"function_declaration" => ("function", "name"),
807+
"function_declaration" | "generator_function_declaration" => ("function", "name"),
655808
"class_declaration" | "abstract_class_declaration" => ("class", "name"),
656809
"interface_declaration" => ("interface", "name"),
657810
"type_alias_declaration" => ("type", "name"),
@@ -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
}

docs/benchmarks/RESOLUTION-COMPARISON.md

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,35 @@ Established Java static call graph tools all require compiled bytecode:
177177
| [Soot](https://github.com/soot-oss/soot) | CHA / RTA / VTA / Spark | Needs compiled `.class` files |
178178
| [javacg-static](https://github.com/gousiosg/java-callgraph) | CHA | Lightweight, reads JARs |
179179

180-
The fixture contains raw `.java` source with no build system. Running these
181-
tools requires a `javac` compilation step (tracked in #1307).
180+
The fixture now includes a `Makefile` that compiles all `.java` sources into
181+
`fixture.jar`:
182182

183-
**Current codegraph Java metrics** (`scripts/resolution-benchmark.ts`):
183+
```bash
184+
cd tests/benchmarks/resolution/fixtures/java && make
185+
```
186+
187+
`scripts/compare-javacg.mjs` then runs
188+
[javacg-static](https://github.com/gousiosg/java-callgraph) on the compiled JAR.
189+
javacg-static uses CHA to enumerate all possible call targets at virtual,
190+
interface, and static call sites.
191+
192+
```bash
193+
node scripts/compare-javacg.mjs --jar /path/to/javacg-0.1-SNAPSHOT.jar
194+
# or: JAVACG_JAR=... node scripts/compare-javacg.mjs
195+
```
196+
197+
Download javacg-static from
198+
[github.com/gousiosg/java-callgraph/releases](https://github.com/gousiosg/java-callgraph/releases)
199+
or build with `mvn package -DskipTests`.
200+
201+
**Name mapping:** javacg-static uses `pkg.ClassName:method(JVM-descriptors)` form.
202+
`compare-javacg.mjs` maps this to `ClassName.method` and matches
203+
`ClassName.ClassName` (source constructor) / `ClassName` (target constructor)
204+
against the expected-edges.json convention. Only edges where both class names
205+
appear in the fixture source files are counted; JDK calls (`String`, `HashMap`,
206+
`System.out`, …) are filtered out.
207+
208+
**Codegraph Java metrics** (`scripts/resolution-benchmark.ts`):
184209

185210
| Mode | Codegraph |
186211
|------|:---------:|
@@ -192,6 +217,19 @@ tools requires a `javac` compilation step (tracked in #1307).
192217
| `class-inheritance` (3 edges) | 0/3 (0%) |
193218
| **Total** | **9/17 (53%)** · precision=100% |
194219

220+
**javacg-static comparison** — run `node scripts/compare-javacg.mjs` to populate:
221+
222+
| Tool | Precision | Recall | TP | FP | FN |
223+
|------|:---------:|:------:|---:|---:|---:|
224+
| Codegraph | 100% | 53% | 9 | 0 | 8 |
225+
| javacg-static (CHA) ||||||
226+
227+
javacg-static uses CHA and reads compiled bytecode, so it should resolve
228+
`class-inheritance` (inherited `log()` calls) and `interface-dispatched`
229+
(virtual dispatch via `UserRepository` interface) edges that codegraph
230+
currently misses at the source-level. `static` and `same-file` calls
231+
(`invokestatic` in bytecode) should also be fully captured.
232+
195233
---
196234

197235
## Conclusions
@@ -231,8 +269,9 @@ tools requires a `javac` compilation step (tracked in #1307).
231269
2 `class-inheritance` edges (+7 recall on TS fixture).
232270
2. **Property-assignment type tracking** (#1306) — Track `this.prop = new Foo()`
233271
writes. Recovers 3 JS `receiver-typed` FN.
234-
3. **Java comparison with javacg-static** (#1307) — Add `javac` compilation to
235-
the Java fixture so a bytecode-level tool can validate Java recall claims.
272+
3. **Java recall gaps**`same-file` (0/2) and `static` (0/2) are `invokestatic`
273+
patterns that javacg-static will expose; `class-inheritance` (0/3) requires
274+
tracking inherited method calls from superclass to subclass invocation site.
236275

237276
---
238277

@@ -300,6 +339,10 @@ npx tsx scripts/resolution-benchmark.ts | jq '{javascript, typescript, java}'
300339
npm install @cs-au-dk/jelly @persper/js-callgraph
301340
node scripts/compare-tools.mjs --all
302341

342+
# javacg-static comparison (Java)
343+
cd tests/benchmarks/resolution/fixtures/java && make && cd -
344+
node scripts/compare-javacg.mjs --jar /path/to/javacg-0.1-SNAPSHOT.jar
345+
303346
# Full resolution test suite
304347
npx vitest run tests/benchmarks/resolution/resolution-benchmark.test.ts
305348
```

0 commit comments

Comments
 (0)