Skip to content

Commit fe804b3

Browse files
authored
feat(resolver): resolve super.method() dispatch via class expression + static block + field def (#1399)
* feat(resolver): resolve super.method() dispatch via class expression + static block + field def - Add `(class name: ...)` query patterns for JS/TS class expressions so that `return class Foo extends Bar { ... }` records the extends relationship in ctx.classes — previously only class_declaration was captured, leaving class expressions invisible to resolveThisDispatch. - Add `class_static_block` → `ClassName.<static>` synthetic method definition in both query path (extractClassMembersWalk) and walk path (walkJavaScriptNode). Calls inside `static { super.f(); }` blocks are now attributed to a method-kind node so the CHA parents map can resolve `super.f()` to the parent class. - Add `field_definition`/`public_field_definition` → `ClassName.fieldName` method definition when the field value is an arrow function or function expression. `static f = () => { ... }` becomes a resolvable `A.f` node so `resolveThisDispatch` can emit the `B.<static> → A.f` edge. - Mirror all three changes in the native Rust extractor for parity. - Add 6 parser unit tests and import Jelly micro-test fixtures for super, super2, super3, super4, super5 as ground-truth benchmarks. Benchmark result: super fixture 31% → 38% recall (B.<static> → A.f now resolved). docs check acknowledged Closes #1377 * fix(extractor): guard handleFieldDef to only emit callable field definitions (#1399) Impact: 2 functions changed, 18 affected * fix(extractor): remove unreachable duplicate match arms in Rust match_js_node (#1399) The PR moved class_static_block and field_definition earlier in the match block (lines 771/773) but left identical arms below (lines 786/787) as dead code. Removing them eliminates Rust unreachable_patterns warnings.
1 parent 0754216 commit fe804b3

5 files changed

Lines changed: 107 additions & 63 deletions

File tree

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

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -763,10 +763,14 @@ fn match_js_call_assignments(node: &Node, source: &[u8], symbols: &mut FileSymbo
763763
fn match_js_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: usize) {
764764
match node.kind() {
765765
"function_declaration" | "generator_function_declaration" => handle_function_decl(node, source, symbols),
766-
"class_declaration" | "abstract_class_declaration" => {
766+
"class_declaration" | "abstract_class_declaration"
767+
// class expressions: `return class Foo extends Bar { ... }` or `const X = class Foo { ... }`
768+
| "class" => {
767769
handle_class_decl(node, source, symbols)
768770
}
771+
"class_static_block" => handle_static_block(node, source, symbols),
769772
"method_definition" => handle_method_def(node, source, symbols),
773+
"field_definition" | "public_field_definition" => handle_field_def(node, source, symbols),
770774
"interface_declaration" => handle_interface_decl(node, source, symbols),
771775
"type_alias_declaration" => handle_type_alias(node, source, symbols),
772776
"enum_declaration" => handle_enum_decl(node, source, symbols),
@@ -776,11 +780,6 @@ fn match_js_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth:
776780
"import_statement" => handle_import_stmt(node, source, symbols),
777781
"export_statement" => handle_export_stmt(node, source, symbols),
778782
"expression_statement" => handle_expr_stmt(node, source, symbols),
779-
// Synthetic definitions for class field initializers and static blocks.
780-
// These give `findCaller` a narrower span with a kind that passes the SQL
781-
// call-edge filter (`kind IN ('function','method')`), matching WASM behaviour.
782-
"field_definition" | "public_field_definition" => handle_field_def(node, source, symbols),
783-
"class_static_block" => handle_static_block(node, source, symbols),
784783
_ => {}
785784
}
786785
}
@@ -864,6 +863,29 @@ fn handle_method_def(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
864863
}
865864
}
866865

866+
/// Create a synthetic `ClassName.<static:L:C>` definition for a class static block
867+
/// so that calls inside the block are attributed to a method-kind node and
868+
/// `super.method()` dispatch can walk up to the parent class.
869+
///
870+
/// The start line and column are appended to the name to ensure uniqueness when a
871+
/// class has multiple `static { }` blocks (each has a distinct start position even
872+
/// if on the same line).
873+
fn handle_static_block(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
874+
let Some(class_name) = find_parent_class(node, source) else { return };
875+
let line = start_line(node);
876+
let col = node.start_position().column;
877+
symbols.definitions.push(Definition {
878+
name: format!("{}.<static:{}:{}>", class_name, line, col),
879+
kind: "method".to_string(),
880+
line,
881+
end_line: Some(end_line(node)),
882+
decorators: None,
883+
complexity: None,
884+
cfg: None,
885+
children: None,
886+
});
887+
}
888+
867889
/// Emit a `ClassName.fieldName` synthetic definition for each `class { field = ... }` node.
868890
/// Only fired when a value node is present (skips bare `x;` declarations), mirroring the WASM
869891
/// `handleFieldDef` guard. The synthetic definition has `kind = "method"` so that the SQL
@@ -881,7 +903,12 @@ fn handle_field_def(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
881903
return;
882904
}
883905
// Skip uninitialised fields (`class C { x; }`) — must have a value node.
884-
let Some(_value_node) = node.child_by_field_name("value") else { return };
906+
let Some(value_node) = node.child_by_field_name("value") else { return };
907+
// Only emit a callable definition when the initializer is a function/arrow expression.
908+
// Scalar fields like `static x = 42` should not appear as method-kind nodes.
909+
if !matches!(value_node.kind(), "arrow_function" | "function_expression" | "generator_function") {
910+
return;
911+
}
885912
let field_name = node_text(&name_node, source);
886913
if field_name.is_empty() { return; }
887914
let Some(class_name) = find_parent_class(node, source) else { return };
@@ -897,23 +924,6 @@ fn handle_field_def(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
897924
});
898925
}
899926

900-
/// Emit a `ClassName.<static>` synthetic definition for each `static { }` block.
901-
/// Enables `findCaller` to attribute calls inside static initializer blocks to this
902-
/// synthetic node rather than to the enclosing class node, matching WASM behaviour.
903-
fn handle_static_block(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
904-
let Some(class_name) = find_parent_class(node, source) else { return };
905-
symbols.definitions.push(Definition {
906-
name: format!("{}.<static>", class_name),
907-
kind: "function".to_string(),
908-
line: start_line(node),
909-
end_line: Some(end_line(node)),
910-
decorators: None,
911-
complexity: None,
912-
cfg: None,
913-
children: None,
914-
});
915-
}
916-
917927
fn handle_interface_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
918928
let Some(name_node) = node.child_by_field_name("name") else { return };
919929
let iface_name = node_text(&name_node, source).to_string();

src/extractors/javascript.ts

Lines changed: 51 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,13 @@ function walkJavaScriptNode(node: TreeSitterNode, ctx: ExtractorOutput): void {
738738
case 'class':
739739
handleClassDecl(node, ctx);
740740
break;
741+
case 'class_static_block':
742+
handleStaticBlock(node, ctx.definitions);
743+
break;
744+
case 'field_definition':
745+
case 'public_field_definition':
746+
handleFieldDef(node, ctx.definitions);
747+
break;
741748
case 'method_definition':
742749
handleMethodDef(node, ctx);
743750
break;
@@ -769,13 +776,6 @@ function walkJavaScriptNode(node: TreeSitterNode, ctx: ExtractorOutput): void {
769776
case 'expression_statement':
770777
handleExpressionStmt(node, ctx);
771778
break;
772-
case 'field_definition':
773-
case 'public_field_definition':
774-
handleFieldDef(node, ctx.definitions);
775-
break;
776-
case 'class_static_block':
777-
handleStaticBlock(node, ctx.definitions);
778-
break;
779779
}
780780

781781
for (let i = 0; i < node.childCount; i++) {
@@ -843,39 +843,63 @@ function handleMethodDef(node: TreeSitterNode, ctx: ExtractorOutput): void {
843843
}
844844
}
845845

846+
/**
847+
* Create a synthetic `ClassName.<static:L:C>` definition for a class static block
848+
* so that calls inside the block can be attributed to a method-kind node and
849+
* `resolveThisDispatch` can walk up to the parent class for `super.method()`.
850+
*
851+
* The start line and column are appended to the name to ensure uniqueness when a
852+
* class has multiple `static { }` blocks (each has a distinct start position even
853+
* if on the same line).
854+
*
855+
* Tree-sitter uses `class_static_block` (not `static_block`) for `static { ... }`.
856+
*/
857+
function handleStaticBlock(node: TreeSitterNode, definitions: Definition[]): void {
858+
const parentClass = findParentClass(node);
859+
if (!parentClass) return;
860+
const line = nodeStartLine(node);
861+
const col = node.startPosition.column;
862+
definitions.push({
863+
name: `${parentClass}.<static:${line}:${col}>`,
864+
kind: 'method',
865+
line,
866+
endLine: nodeEndLine(node),
867+
});
868+
}
869+
846870
/**
847871
* Emit a `ClassName.fieldName` definition for class fields that have an initializer.
848872
* This lets `findCaller` attribute calls inside field initializers (e.g. static field
849873
* side-effects) to the field rather than the enclosing class.
874+
*
875+
* JS `field_definition` uses the `'property'` field name; TS
876+
* `public_field_definition` uses `'name'`. As a third fallback (Rust/TS parity) we
877+
* also check for a positional `property_identifier` child.
850878
*/
879+
const CALLABLE_FIELD_TYPES = new Set([
880+
'arrow_function',
881+
'function_expression',
882+
'generator_function',
883+
]);
884+
851885
function handleFieldDef(node: TreeSitterNode, definitions: Definition[]): void {
852886
// JS field_definition uses 'property' field; TS public_field_definition uses 'name' field
853-
const nameNode = node.childForFieldName('name') || node.childForFieldName('property');
887+
const nameNode =
888+
node.childForFieldName('name') ||
889+
node.childForFieldName('property') ||
890+
findChild(node, 'property_identifier');
854891
const valueNode = node.childForFieldName('value');
855892
if (!nameNode || !valueNode) return;
856893
if (nameNode.type === 'computed_property_name') return;
894+
// Only emit a callable definition when the initializer is a function/arrow expression.
895+
// Scalar fields like `static x = 42` should not appear as method-kind nodes.
896+
if (!CALLABLE_FIELD_TYPES.has(valueNode.type)) return;
857897
const fieldName = nameNode.text;
858898
if (!fieldName) return;
859-
const className = findParentClass(node);
860-
if (!className) return;
861-
definitions.push({
862-
name: `${className}.${fieldName}`,
863-
kind: 'method',
864-
line: nodeStartLine(node),
865-
endLine: nodeEndLine(node),
866-
});
867-
}
868-
869-
/**
870-
* Emit a `ClassName.<static>` definition for each `static { }` block.
871-
* Enables `findCaller` to attribute calls inside static initializer blocks to
872-
* this synthetic node rather than to the enclosing class node.
873-
*/
874-
function handleStaticBlock(node: TreeSitterNode, definitions: Definition[]): void {
875-
const className = findParentClass(node);
876-
if (!className) return;
899+
const parentClass = findParentClass(node);
900+
if (!parentClass) return;
877901
definitions.push({
878-
name: `${className}.<static>`,
902+
name: `${parentClass}.${fieldName}`,
879903
kind: 'method',
880904
line: nodeStartLine(node),
881905
endLine: nodeEndLine(node),

tests/benchmarks/resolution/fixtures/jelly-micro/classes/expected-edges.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@
366366
},
367367
{
368368
"source": {
369-
"name": "C6.<static>",
369+
"name": "C6.<static:75:2>",
370370
"file": "classes.js"
371371
},
372372
"target": {
@@ -378,7 +378,7 @@
378378
},
379379
{
380380
"source": {
381-
"name": "C6.<static>",
381+
"name": "C6.<static:78:2>",
382382
"file": "classes.js"
383383
},
384384
"target": {

tests/benchmarks/resolution/fixtures/jelly-micro/super/expected-edges.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
},
7979
{
8080
"source": {
81-
"name": "B.<static>",
81+
"name": "B.<static:36:2>",
8282
"file": "super.js"
8383
},
8484
"target": {

tests/parsers/javascript.test.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -99,18 +99,27 @@ describe('JavaScript parser', () => {
9999
});
100100

101101
it('extracts static class field definitions as method definitions', () => {
102-
const symbols = parseJS(`class C6 { static staticProperty = (f1(), function() {}); }`);
102+
const symbols = parseJS(`class C6 { static staticProperty = function() {}; }`);
103103
expect(symbols.definitions).toContainEqual(
104104
expect.objectContaining({ name: 'C6.staticProperty', kind: 'method' }),
105105
);
106106
});
107107

108-
it('extracts static blocks as method definitions', () => {
108+
it('does not extract scalar static field definitions as method definitions', () => {
109+
const symbols = parseJS(`class C7 { static x = 42; }`);
110+
const names = symbols.definitions.map((d: { name: string }) => d.name);
111+
expect(names).not.toContain('C7.x');
112+
});
113+
114+
it('extracts static blocks as method definitions with unique names', () => {
109115
const symbols = parseJS(`class C6 { static { f1(); } static { f2(); } }`);
110-
const staticDefs = symbols.definitions.filter((d) => d.name === 'C6.<static>');
116+
// Each static block gets a unique name with line:column suffix to avoid collisions
117+
const staticDefs = symbols.definitions.filter((d) => d.name.startsWith('C6.<static:'));
111118
expect(staticDefs).toHaveLength(2);
112119
expect(staticDefs[0]).toMatchObject({ kind: 'method' });
113120
expect(staticDefs[1]).toMatchObject({ kind: 'method' });
121+
// Names must be distinct even on the same line
122+
expect(staticDefs[0].name).not.toBe(staticDefs[1].name);
114123
});
115124

116125
it('extracts import statements', () => {
@@ -1166,16 +1175,17 @@ describe('JavaScript parser', () => {
11661175
expect(superCall).toBeDefined();
11671176
});
11681177

1169-
it('creates ClassName.<static> definition for class static block', () => {
1178+
it('creates ClassName.<static:L:C> definition for class static block', () => {
11701179
const symbols = parseJS(`class A extends B {\n static {\n super.init();\n }\n}`);
1171-
expect(symbols.definitions).toContainEqual(
1172-
expect.objectContaining({ name: 'A.<static>', kind: 'method' }),
1173-
);
1180+
// Name includes line:column suffix for uniqueness
1181+
const staticDef = symbols.definitions.find((d) => d.name.startsWith('A.<static:'));
1182+
expect(staticDef).toBeDefined();
1183+
expect(staticDef).toMatchObject({ kind: 'method' });
11741184
});
11751185

1176-
it('attributes super.method() call inside static block to ClassName.<static>', () => {
1186+
it('attributes super.method() call inside static block to ClassName.<static:L:C>', () => {
11771187
const symbols = parseJS(`class A extends B {\n static {\n super.init();\n }\n}`);
1178-
const staticDef = symbols.definitions.find((d) => d.name === 'A.<static>');
1188+
const staticDef = symbols.definitions.find((d) => d.name.startsWith('A.<static:'));
11791189
expect(staticDef).toBeDefined();
11801190
const superCall = symbols.calls.find((c) => c.name === 'init' && c.receiver === 'super');
11811191
expect(superCall).toBeDefined();

0 commit comments

Comments
 (0)