-
Notifications
You must be signed in to change notification settings - Fork 14
fix(native): seed typeMap entries for let/var object-literal methods #1555
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0618660
087015c
0320b97
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -138,6 +138,17 @@ fn match_js_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _dep | |
| if value_n.kind() == "call_expression" { | ||
| seed_object_create_entries(var_name, &value_n, source, symbols); | ||
| } | ||
| // Phase 8.3f parity: seed composite typeMap keys for ALL object-literal | ||
| // declarations (`const`, `let`, `var`) when at non-function scope. | ||
| // Mirrors WASM handleVarDeclaratorTypeMap (no isConst guard there). | ||
| // For `const`, extract_object_literal_functions already seeds these entries; | ||
| // dedup_type_map collapses any duplicates at equal confidence. | ||
| if value_n.kind() == "object" && find_parent_of_types(node, &[ | ||
| "function_declaration", "arrow_function", "function_expression", | ||
| "method_definition", "generator_function_declaration", "generator_function", | ||
| ]).is_none() { | ||
| seed_objlit_type_map_entries(var_name, &value_n, source, symbols); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -555,16 +566,100 @@ fn extract_object_literal_functions( | |
| } | ||
| } | ||
|
|
||
| /// Seed composite typeMap keys from an object literal for ALL declaration kinds | ||
| /// (`const`, `let`, `var`) at non-function scope. | ||
| /// | ||
| /// Mirrors WASM `handleVarDeclaratorTypeMap`'s object-literal branch (no `isConst` guard). | ||
| /// Called from `match_js_type_map` so that `let obj = { f() {} }` and | ||
| /// `var routes = { get: handler }` resolve correctly just like `const` variants. | ||
| /// | ||
| /// For `const` declarations this produces the same entries as `extract_object_literal_functions`, | ||
| /// but `dedup_type_map` collapses duplicates at equal confidence. | ||
| fn seed_objlit_type_map_entries(var_name: &str, obj_node: &Node, source: &[u8], symbols: &mut FileSymbols) { | ||
| for i in 0..obj_node.child_count() { | ||
| let Some(child) = obj_node.child(i) else { continue }; | ||
| match child.kind() { | ||
| "shorthand_property_identifier" => { | ||
| let prop_name = node_text(&child, source); | ||
| symbols.type_map.push(TypeMapEntry { | ||
| name: format!("{}.{}", var_name, prop_name), | ||
| type_name: prop_name.to_string(), | ||
| confidence: 0.85, | ||
| }); | ||
| } | ||
| "pair" => { | ||
| let Some(key_n) = child.child_by_field_name("key") else { continue }; | ||
| let Some(val_n) = child.child_by_field_name("value") else { continue }; | ||
| let key = if key_n.kind() == "string" { | ||
| extract_string_fragment(&key_n, source).map(|s| s.to_string()) | ||
| } else { | ||
| Some(node_text(&key_n, source).to_string()) | ||
| }; | ||
| let Some(key) = key else { continue }; | ||
| let qualified = format!("{}.{}", var_name, key); | ||
| match val_n.kind() { | ||
| "arrow_function" | "function_expression" | "function" => { | ||
| // Store qualified name as value so the resolver finds the qualified def. | ||
| // Mirrors WASM: setTypeMapEntry(typeMap, qualifiedKey, qualifiedKey, 0.85). | ||
| // For `const`, `extract_object_literal_functions` creates the matching definition. | ||
| // For `let`/`var`, `match_js_objlit_qualified_method_defs` creates it in its | ||
| // deferred second pass (now covers all declaration kinds, not just `const`). | ||
| symbols.type_map.push(TypeMapEntry { | ||
| name: qualified.clone(), | ||
| type_name: qualified, | ||
| confidence: 0.85, | ||
| }); | ||
| } | ||
|
Comment on lines
+601
to
+612
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed — same root cause as the sibling comment. Extended match_js_objlit_qualified_method_defs (commit 0320b97) to emit qualified definitions for let/var pair+arrow/function values. The typeMap entry 'api.save' → 'api.save' (qualified→qualified) now resolves correctly because the matching 'api.save' definition is created by the deferred second-pass walker. The s_let_arrow test now asserts definition existence and end-to-end call-edge creation. |
||
| "identifier" => { | ||
| let target = node_text(&val_n, source); | ||
| symbols.type_map.push(TypeMapEntry { | ||
| name: qualified, | ||
| type_name: target.to_string(), | ||
| confidence: 0.85, | ||
| }); | ||
| } | ||
| _ => {} | ||
| } | ||
| } | ||
| "method_definition" => { | ||
| // Method shorthand: `let obj = { baz() {} }` → typeMap['obj.baz'] = 'baz' | ||
| // Points to the bare-name definition so the two-step accessor dispatch resolves | ||
| // via the bare node. `handle_method_def` always creates a bare definition for | ||
| // method_definition nodes; `match_js_objlit_qualified_method_defs` (which now | ||
| // covers all declaration kinds) adds the qualified definition in its deferred | ||
| // second pass. Using the bare name here keeps resolution consistent across all | ||
| // declaration kinds (const/let/var). | ||
| let Some(method_name) = resolve_method_def_name(&child, source) else { continue }; | ||
| let qualified = format!("{}.{}", var_name, method_name); | ||
| symbols.type_map.push(TypeMapEntry { | ||
| name: qualified, | ||
| type_name: method_name.to_string(), | ||
| confidence: 0.85, | ||
| }); | ||
|
Comment on lines
+624
to
+638
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed — changed type_name from the qualified name to the bare method name, matching the const path in extract_object_literal_functions. For let/var, no qualified definition exists (match_js_objlit_qualified_method_defs is const-only), so pointing at 'obj.baz' would leave resolution broken. |
||
| } | ||
| _ => {} | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Second-pass walker: emit qualified `obj.method(function)` definitions for | ||
| /// `method_definition` children of object literals. | ||
| /// `method_definition` and (for `let`/`var`) `pair+arrow/function` children of object literals. | ||
| /// | ||
| /// **method_definition** (all declaration kinds — `const`, `let`, `var`): | ||
| /// This must run AFTER the main `match_js_node` walk so that the bare `f(method)` node | ||
| /// created by `handle_method_def` appears BEFORE the qualified `obj.f(function)` node | ||
| /// in `symbols.definitions`. `findCaller` picks the narrowest-span enclosing definition; | ||
| /// when spans are equal it keeps the first inserted one (strict `<`), so `f(method)` wins | ||
| /// and call-edge attribution matches WASM (which runs `handleMethodCapture` via the query | ||
| /// path before `extractObjectLiteralFunctions` via `runCollectorWalk`). | ||
| /// | ||
| /// **pair + arrow_function / function_expression / function** (`let`/`var` only): | ||
| /// For `const`, `extract_object_literal_functions` already creates the qualified definition; | ||
| /// repeating it here would produce a duplicate. For `let`/`var`, no other pass emits the | ||
| /// qualified definition, so we must emit it here. Without the definition, the typeMap entry | ||
| /// seeded by `seed_objlit_type_map_entries` (`"api.save" → "api.save"`) dead-ends: the | ||
| /// resolver finds the typeMap entry but then fails to locate a node named `"api.save"`. | ||
| /// | ||
| /// WASM produces both nodes — the qualified one via `extractObjectLiteralFunctions` and the | ||
| /// bare one via `handleMethodCapture`. This pass mirrors that by adding only the qualified | ||
| /// definitions, deferred so ordering is correct. | ||
|
|
@@ -583,7 +678,6 @@ fn match_js_objlit_qualified_method_defs( | |
| return; | ||
| } | ||
| let is_const = node.child(0).map(|c| node_text(&c, source) == "const").unwrap_or(false); | ||
| if !is_const { return; } | ||
| for i in 0..node.child_count() { | ||
| let Some(declarator) = node.child(i) else { continue }; | ||
| if declarator.kind() != "variable_declarator" { continue; } | ||
|
|
@@ -593,22 +687,54 @@ fn match_js_objlit_qualified_method_defs( | |
| let var_name = node_text(&name_n, source); | ||
| for j in 0..value_n.child_count() { | ||
| let Some(child) = value_n.child(j) else { continue }; | ||
| if child.kind() != "method_definition" { continue; } | ||
| // Use resolve_method_def_name to strip brackets from computed string keys | ||
| // (e.g. ['foo'] → "foo") and skip non-string computed keys ([Symbol.iterator]). | ||
| let Some(method_name) = resolve_method_def_name(&child, source) else { continue }; | ||
| let qualified = format!("{}.{}", var_name, method_name); | ||
| let body = child.child_by_field_name("body"); | ||
| symbols.definitions.push(Definition { | ||
| name: qualified, | ||
| kind: "function".to_string(), | ||
| line: start_line(&child), | ||
| end_line: Some(end_line(&child)), | ||
| decorators: None, | ||
| complexity: body.and_then(|b| compute_all_metrics(&b, source, "javascript")), | ||
| cfg: body.and_then(|b| build_function_cfg(&b, "javascript", source)), | ||
| children: None, | ||
| }); | ||
| match child.kind() { | ||
| "method_definition" => { | ||
| // Emit qualified definition for ALL declaration kinds. | ||
| // Use resolve_method_def_name to strip brackets from computed string keys | ||
| // (e.g. ['foo'] → "foo") and skip non-string computed keys ([Symbol.iterator]). | ||
| let Some(method_name) = resolve_method_def_name(&child, source) else { continue }; | ||
| let qualified = format!("{}.{}", var_name, method_name); | ||
| let body = child.child_by_field_name("body"); | ||
| symbols.definitions.push(Definition { | ||
| name: qualified, | ||
| kind: "function".to_string(), | ||
| line: start_line(&child), | ||
| end_line: Some(end_line(&child)), | ||
| decorators: None, | ||
| complexity: body.and_then(|b| compute_all_metrics(&b, source, "javascript")), | ||
| cfg: body.and_then(|b| build_function_cfg(&b, "javascript", source)), | ||
| children: None, | ||
| }); | ||
| } | ||
| "pair" if !is_const => { | ||
| // Emit qualified definition for `let`/`var` pair+arrow/function values only. | ||
| // For `const`, `extract_object_literal_functions` already creates this definition; | ||
| // creating it again here would be a duplicate. | ||
| let Some(key_n) = child.child_by_field_name("key") else { continue }; | ||
| let Some(val_n) = child.child_by_field_name("value") else { continue }; | ||
| if !matches!(val_n.kind(), "arrow_function" | "function_expression" | "function") { | ||
| continue; | ||
| } | ||
| let key = if key_n.kind() == "string" { | ||
| extract_string_fragment(&key_n, source).map(|s| s.to_string()) | ||
| } else { | ||
| Some(node_text(&key_n, source).to_string()) | ||
| }; | ||
| let Some(key) = key else { continue }; | ||
| let qualified = format!("{}.{}", var_name, key); | ||
| symbols.definitions.push(Definition { | ||
| name: qualified, | ||
| kind: "function".to_string(), | ||
| line: start_line(&child), | ||
| end_line: Some(end_line(&val_n)), | ||
| decorators: None, | ||
| complexity: compute_all_metrics(&val_n, source, "javascript"), | ||
| cfg: build_function_cfg(&val_n, "javascript", source), | ||
| children: None, | ||
| }); | ||
| } | ||
| _ => {} | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -4108,6 +4234,82 @@ mod tests { | |
| assert_eq!(tm_f.unwrap().type_name, "f"); | ||
| } | ||
|
|
||
| /// Issue #1551: `let` and `var` object-literal declarations must seed composite typeMap keys | ||
| /// just like `const` declarations. Regression test for the parity gap where native bailed | ||
| /// early for non-`const` declarations in the object-literal typeMap walk. | ||
| #[test] | ||
| fn let_var_objlit_seeds_type_map_entries() { | ||
| // Method shorthand: `let obj = { f() {} }` → typeMap['obj.f'] present | ||
| let s_let_method = parse_js( | ||
| "let obj = { f() { return 1; } };\n\ | ||
| obj.f();", | ||
| ); | ||
| let tm = s_let_method.type_map.iter().find(|e| e.name == "obj.f"); | ||
| assert!(tm.is_some(), "let obj method: typeMap 'obj.f' missing; got: {:?}", | ||
| s_let_method.type_map.iter().map(|e| &e.name).collect::<Vec<_>>()); | ||
| assert_eq!(tm.unwrap().type_name, "f", | ||
| "typeMap 'obj.f' must point at bare name 'f', not the qualified key"); | ||
| let call = s_let_method.calls.iter().find(|c| c.name == "f" && c.receiver.as_deref() == Some("obj")); | ||
| assert!(call.is_some(), | ||
| "calls must contain obj.f() with receiver='obj'; got: {:?}", | ||
| s_let_method.calls.iter().map(|c| (&c.name, &c.receiver)).collect::<Vec<_>>()); | ||
|
|
||
| // Shorthand property: `var obj = { e4 }` → typeMap['obj.e4'] = 'e4' | ||
| let s_var_shorthand = parse_js( | ||
| "function e4() {}\n\ | ||
| var obj = { e4 };", | ||
| ); | ||
| let tm2 = s_var_shorthand.type_map.iter().find(|e| e.name == "obj.e4"); | ||
| assert!(tm2.is_some(), "var obj shorthand: typeMap 'obj.e4' missing; got: {:?}", | ||
| s_var_shorthand.type_map.iter().map(|e| &e.name).collect::<Vec<_>>()); | ||
| assert_eq!(tm2.unwrap().type_name, "e4"); | ||
|
|
||
| // Pair with identifier value: `var routes = { get: handler }` → typeMap['routes.get'] = 'handler' | ||
| let s_var_pair = parse_js( | ||
| "function handler() {}\n\ | ||
| var routes = { get: handler };", | ||
| ); | ||
| let tm3 = s_var_pair.type_map.iter().find(|e| e.name == "routes.get"); | ||
| assert!(tm3.is_some(), "var routes pair: typeMap 'routes.get' missing; got: {:?}", | ||
| s_var_pair.type_map.iter().map(|e| &e.name).collect::<Vec<_>>()); | ||
| assert_eq!(tm3.unwrap().type_name, "handler"); | ||
|
|
||
| // Pair with arrow value: `let api = { save: () => {} }` → typeMap['api.save'] = 'api.save' | ||
| // and a qualified definition 'api.save' must exist (emitted by the deferred | ||
| // match_js_objlit_qualified_method_defs pass for non-const pair+arrow/function). | ||
| let s_let_arrow = parse_js( | ||
| "let api = { save: () => {} };\n\ | ||
| api.save();", | ||
| ); | ||
| let tm4 = s_let_arrow.type_map.iter().find(|e| e.name == "api.save"); | ||
| assert!(tm4.is_some(), "let api arrow: typeMap 'api.save' missing; got: {:?}", | ||
| s_let_arrow.type_map.iter().map(|e| &e.name).collect::<Vec<_>>()); | ||
| assert_eq!(tm4.unwrap().type_name, "api.save", | ||
| "typeMap 'api.save' must point at the qualified name 'api.save' (qualified definition exists)"); | ||
| assert!( | ||
| s_let_arrow.definitions.iter().any(|d| d.name == "api.save"), | ||
| "let api arrow: qualified definition 'api.save' missing; got: {:?}", | ||
| s_let_arrow.definitions.iter().map(|d| &d.name).collect::<Vec<_>>() | ||
| ); | ||
| let call4 = s_let_arrow.calls.iter().find(|c| c.name == "save" && c.receiver.as_deref() == Some("api")); | ||
| assert!(call4.is_some(), | ||
| "calls must contain api.save() with receiver='api'; got: {:?}", | ||
| s_let_arrow.calls.iter().map(|c| (&c.name, &c.receiver)).collect::<Vec<_>>()); | ||
|
|
||
| // Scope guard: object literal inside a function body must NOT seed module-level typeMap. | ||
| let s_scoped = parse_js( | ||
| "function init() {\n\ | ||
| let local = { run() {} };\n\ | ||
| local.run();\n\ | ||
| }", | ||
| ); | ||
| assert!( | ||
| s_scoped.type_map.iter().all(|e| e.name != "local.run"), | ||
| "function-scoped let obj must not pollute typeMap; got: {:?}", | ||
| s_scoped.type_map.iter().map(|e| &e.name).collect::<Vec<_>>() | ||
| ); | ||
| } | ||
|
|
||
| /// Phase 8.3e: call receiver is correctly recorded for obj.f() inside defProp body. | ||
| #[test] | ||
| fn call_receiver_for_define_property() { | ||
|
Comment on lines
+4240
to
4315
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The five new unit tests assert that Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed — added assertions for both type_name value ('f', not 'obj.f') and call-edge creation (calls contains name='f' with receiver='obj'). The type_name assertion directly catches the regression fixed in the sibling comment. |
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pair+functionarm seeds typeMap but omits the Definition, breaking call resolutionFor
let api = { save: () => {} }, this arm seedsTypeMapEntry { name: "api.save", type_name: "api.save" }but never creates the matchingDefinition { name: "api.save", ... }. Theconstpath inextract_object_literal_functions(line 519–534) creates both the definition and the typeMap entry. When the resolver followstypeMap["api.save"] → "api.save"and then searchessymbols.definitionsfor a node named"api.save", it finds nothing and drops the call edge.Before this PR
typeMap["api.save"]didn't exist, soapi.save()took a direct-call path. After this PR the resolver enters the two-step dispatch and dead-ends on the missing definition, which is a regression forlet/varobjects with arrow/function-expression property values. The tests_let_arrowincludesapi.save()in the source but has no call-edge assertion, masking the broken state — unlikes_let_methodwhich was correctly upgraded to assert call-edge creation.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed — extended match_js_objlit_qualified_method_defs to emit a qualified definition for let/var pair+arrow/function values (commit 0320b97). For const, extract_object_literal_functions already creates the definition, so const pairs are excluded to avoid duplicates. The test now asserts both that the qualified definition 'api.save' exists in definitions and that api.save() produces a call edge with receiver='api'.