Skip to content

Commit 3b8c66a

Browse files
committed
fix: resolve merge conflicts with main
2 parents 95d0234 + 6f7189e commit 3b8c66a

10 files changed

Lines changed: 149 additions & 117 deletions

File tree

crates/codegraph-core/src/edge_builder.rs

Lines changed: 22 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -503,48 +503,31 @@ fn resolve_call_targets<'a>(
503503
.unwrap_or_default();
504504
if !exact.is_empty() { return exact; }
505505

506-
// Bare-call same-class fallback: mirrors the WASM buildFileCallEdges same-class
507-
// bare-call fallback. When a no-receiver call can't be resolved globally, try the
508-
// caller's own class prefix: `IsValidEmail()` in `Validators.ValidateUser` →
509-
// `Validators.IsValidEmail`. Safe: only fires after the global exact lookup fails,
510-
// so module-level functions always take priority.
511-
if call.receiver.is_none() {
512-
if let Some(dot_pos) = caller_name.find('.') {
513-
let class_prefix = &caller_name[..dot_pos];
514-
let qualified = format!("{}.{}", class_prefix, call.name);
515-
let class_scoped: Vec<&NodeInfo> = ctx.nodes_by_name
516-
.get(qualified.as_str())
517-
.map(|v| v.iter()
518-
.filter(|n| n.kind == "method" && n.file == rel_path)
519-
.copied()
520-
.collect())
521-
.unwrap_or_default();
522-
if !class_scoped.is_empty() { return class_scoped; }
523-
}
506+
// Class-scoped exact lookup: prefer `ClassName.method` when the caller is a qualified
507+
// method (e.g. `this.area()` or plain `area()` in `Shape.describe` → try `Shape.area`).
508+
// Covers both this/self/super dispatch AND no-receiver static sibling calls (e.g.
509+
// `IsValidEmail()` inside `Validators.ValidateUser` → `Validators.IsValidEmail`).
510+
// This avoids false edges to unrelated classes that happen to have a method with the
511+
// same name in the same file.
512+
if let Some(dot_pos) = caller_name.find('.') {
513+
let class_prefix = &caller_name[..dot_pos];
514+
let qualified = format!("{}.{}", class_prefix, call.name);
515+
let class_scoped: Vec<&NodeInfo> = ctx.nodes_by_name
516+
.get(qualified.as_str())
517+
.map(|v| v.iter()
518+
.filter(|n| n.kind == "method"
519+
&& import_resolution::compute_confidence(rel_path, &n.file, None) >= 0.5)
520+
.copied().collect())
521+
.unwrap_or_default();
522+
if !class_scoped.is_empty() { return class_scoped; }
524523
}
525524

526-
// For this/self/super: prefer class-scoped exact lookup (e.g. `this.area()` in
527-
// `Shape.describe` → try `Shape.area` first). This avoids false edges to unrelated
528-
// classes that happen to have a method with the same name in the same file.
529-
// Fall back to the broader same-file suffix scan only when the class-scoped lookup
530-
// finds nothing (e.g. when the caller is at module scope or the name is unknown).
525+
// Broader fallback: same-file suffix scan. Only for this/self/super (not no-receiver
526+
// plain calls) to avoid false positives on global function calls inside class methods.
527+
// Always restricts to the caller's own class prefix to avoid false edges to unrelated
528+
// classes in the same file (e.g. this.area() inside Shape.describe must never yield
529+
// Calculator.area, even when Calculator.area is the only method with that name).
531530
if call.receiver.is_some() {
532-
// Extract the class prefix from the enclosing caller name (e.g. "Shape" from "Shape.describe").
533-
if let Some(dot_pos) = caller_name.find('.') {
534-
let class_prefix = &caller_name[..dot_pos];
535-
let qualified = format!("{}.{}", class_prefix, call.name);
536-
let class_scoped: Vec<&NodeInfo> = ctx.nodes_by_name
537-
.get(qualified.as_str())
538-
.map(|v| v.iter().filter(|n| n.kind == "method").copied().collect())
539-
.unwrap_or_default();
540-
if !class_scoped.is_empty() { return class_scoped; }
541-
}
542-
543-
// Broader fallback: same-file suffix scan. Always restrict to the caller's
544-
// own class prefix — regardless of how many matches are found — to avoid
545-
// false-positive edges to unrelated classes in the same file.
546-
// (e.g. this.area() inside Shape.describe must never yield Calculator.area,
547-
// even when Calculator.area is the only method with that name in the file.)
548531
let suffix = format!(".{}", call.name);
549532
if let Some(file_nodes) = ctx.nodes_by_file.get(rel_path) {
550533
let same_file_methods: Vec<&NodeInfo> = file_nodes.iter()

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/domain/graph/builder/call-resolver.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,12 @@ export function resolveByMethodOrGlobal(
196196
.filter((t) => computeConfidence(relPath, t.file, null) >= 0.5);
197197
if (exact.length > 0) return exact;
198198

199-
// For this/self/super receiver: try same-class method lookup via callerName.
199+
// Try same-class method lookup via callerName.
200200
// e.g. `this.area()` inside `Shape.describe` → try `Shape.area`.
201+
// Also covers no-receiver calls inside class methods, e.g. `IsValidEmail(x)` inside
202+
// `Validators.ValidateUser` → try `Validators.IsValidEmail` (C#/Java static siblings).
201203
// This seeds the initial edge that runChaPostPass later expands to subclass overrides.
202-
if (call.receiver && callerName) {
204+
if (callerName) {
203205
const dotIdx = callerName.lastIndexOf('.');
204206
if (dotIdx > -1) {
205207
// Extract only the segment immediately before the method name so that

src/domain/graph/builder/stages/native-orchestrator.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -402,16 +402,16 @@ async function runPostNativeAnalysis(
402402
* Note: `this`/`super` dispatch is handled separately by `runPostNativeThisDispatch`,
403403
* which WASM-re-parses JS/TS files to obtain raw call site receiver info.
404404
*
405-
* Returns the set of target node IDs for newly inserted CHA edges so the caller
406-
* can re-classify roles for the affected implementation files. An empty set
407-
* means no edges were added and role re-classification is unnecessary.
405+
* Returns the count of newly inserted CHA edges so the caller can determine
406+
* whether a full role re-classification is needed. Zero means no edges were
407+
* added and role re-classification is unnecessary.
408408
*/
409-
function runPostNativeCha(db: BetterSqlite3Database): Set<number> {
409+
function runPostNativeCha(db: BetterSqlite3Database): number {
410410
// Fast guard: no hierarchy edges → no CHA work
411411
const hasHierarchy = db
412412
.prepare(`SELECT 1 FROM edges WHERE kind IN ('extends', 'implements') LIMIT 1`)
413413
.get();
414-
if (!hasHierarchy) return new Set();
414+
if (!hasHierarchy) return 0;
415415

416416
// Build implementors map: parent/interface name → [child/implementing class names]
417417
const hierarchyRows = db
@@ -433,7 +433,7 @@ function runPostNativeCha(db: BetterSqlite3Database): Set<number> {
433433
}
434434
if (!list.includes(row.child_name)) list.push(row.child_name);
435435
}
436-
if (implementors.size === 0) return new Set();
436+
if (implementors.size === 0) return 0;
437437

438438
// RTA: collect class names that are actually instantiated via `new X()`.
439439
// Primary query targets `class`-kind nodes (the canonical schema).
@@ -506,7 +506,7 @@ function runPostNativeCha(db: BetterSqlite3Database): Set<number> {
506506
`SELECT id, file AS method_file FROM nodes WHERE name = ? AND kind = 'method'`,
507507
);
508508
const newEdges: Array<[number, number, string, number, number, string]> = [];
509-
const newTargetIds = new Set<number>();
509+
let newEdgeCount = 0;
510510

511511
for (const { source_id, method_name, caller_file } of callToMethods) {
512512
const dotIdx = method_name.indexOf('.');
@@ -545,7 +545,7 @@ function runPostNativeCha(db: BetterSqlite3Database): Set<number> {
545545
CHA_DISPATCH_PENALTY;
546546
if (conf <= 0) continue;
547547
newEdges.push([source_id, methodNode.id, 'calls', conf, 0, 'cha']);
548-
newTargetIds.add(methodNode.id);
548+
newEdgeCount++;
549549
}
550550
}
551551

@@ -558,7 +558,7 @@ function runPostNativeCha(db: BetterSqlite3Database): Set<number> {
558558
if (newEdges.length > 0) {
559559
db.transaction(() => batchInsertEdges(db, newEdges))();
560560
}
561-
return newTargetIds;
561+
return newEdgeCount;
562562
}
563563

564564
/**
@@ -1607,9 +1607,9 @@ export async function tryNativeOrchestrator(
16071607
}
16081608

16091609
// Phase 8.5: expand CHA call edges (interface dispatch → concrete implementations).
1610-
// Returns the target node IDs of newly inserted edges; used to determine whether
1610+
// Returns the count of newly inserted edges; used to determine whether
16111611
// a full role re-classification is needed after all edge-writing post-passes complete.
1612-
const chaTargetIds = runPostNativeCha(ctx.db as unknown as BetterSqlite3Database);
1612+
const chaEdgeCount = runPostNativeCha(ctx.db as unknown as BetterSqlite3Database);
16131613

16141614
// Function-as-object-property post-pass: the Rust engine does not yet recognise
16151615
// `fn.method = function() {}` patterns. Re-parse only those JS/TS files via
@@ -1636,7 +1636,7 @@ export async function tryNativeOrchestrator(
16361636
// this-dispatch) add edges, so the Rust-computed roles and the cached
16371637
// fan-out medians are stale. A full re-classification ensures the final
16381638
// roles reflect the true fan-in/out with all edges in place.
1639-
if (chaTargetIds.size > 0 || thisDispatchTargetIds.size > 0) {
1639+
if (chaEdgeCount > 0 || thisDispatchTargetIds.size > 0) {
16401640
try {
16411641
const { classifyNodeRoles } = (await import('../../../../features/structure.js')) as {
16421642
classifyNodeRoles: (

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": {

0 commit comments

Comments
 (0)