Skip to content

Commit 96932d8

Browse files
authored
Merge branch 'main' into fix/native-this-dispatch-1413
2 parents fb94a1a + 117ac5b commit 96932d8

34 files changed

Lines changed: 1192 additions & 161 deletions

.github/workflows/claude.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949

5050
- name: Run Automated AI Review
5151
id: automated-review
52-
uses: anthropics/claude-code-action@64de744025ca9e24df2b88204b4f1e968f39f009
52+
uses: anthropics/claude-code-action@28f83620103c48a57093dcc2837eec89e036bb9f
5353
with:
5454
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
5555
model: claude-sonnet-4-6
@@ -208,7 +208,7 @@ jobs:
208208

209209
- name: Run Interactive AI Assistant
210210
id: interactive-claude
211-
uses: anthropics/claude-code-action@64de744025ca9e24df2b88204b4f1e968f39f009
211+
uses: anthropics/claude-code-action@28f83620103c48a57093dcc2837eec89e036bb9f
212212
with:
213213
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
214214
model: claude-sonnet-4-6

crates/codegraph-core/src/edge_builder.rs

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

506-
// For this/self/super: prefer class-scoped exact lookup (e.g. `this.area()` in
507-
// `Shape.describe` → try `Shape.area` first). This avoids false edges to unrelated
508-
// classes that happen to have a method with the same name in the same file.
509-
// Fall back to the broader same-file suffix scan only when the class-scoped lookup
510-
// finds nothing (e.g. when the caller is at module scope or the name is unknown).
511-
if call.receiver.is_some() {
512-
// Extract the class prefix from the enclosing caller name (e.g. "Shape" from "Shape.describe").
513-
if let Some(dot_pos) = caller_name.find('.') {
514-
let class_prefix = &caller_name[..dot_pos];
515-
let qualified = format!("{}.{}", class_prefix, call.name);
516-
let class_scoped: Vec<&NodeInfo> = ctx.nodes_by_name
517-
.get(qualified.as_str())
518-
.map(|v| v.iter().filter(|n| n.kind == "method").copied().collect())
519-
.unwrap_or_default();
520-
if !class_scoped.is_empty() { return class_scoped; }
521-
}
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; }
523+
}
522524

523-
// Broader fallback: same-file suffix scan. Always restrict to the caller's
524-
// own class prefix — regardless of how many matches are found — to avoid
525-
// false-positive edges to unrelated classes in the same file.
526-
// (e.g. this.area() inside Shape.describe must never yield Calculator.area,
527-
// even when Calculator.area is the only method with that name in the file.)
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).
530+
if call.receiver.is_some() {
528531
let suffix = format!(".{}", call.name);
529532
if let Some(file_nodes) = ctx.nodes_by_file.get(rel_path) {
530533
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();

package-lock.json

Lines changed: 16 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

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

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

src/domain/graph/builder/incremental.ts

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,34 @@ function buildCallEdges(
502502
]),
503503
)
504504
: new Map();
505+
506+
// Phase 8.3f: seed typeMap[callee::restName] = { type: argName } from
507+
// objectRestParamBindings × paramBindings, mirroring buildObjectRestParamPostPass.
508+
// Scoped keys prevent same-name rest-param collisions when two functions in
509+
// the same file both use `...rest` (#1358). The unscoped key is also seeded
510+
// when only one callee uses a given rest name, preserving resolution when
511+
// callerName is null (findCaller couldn't identify the enclosing function).
512+
if (symbols.objectRestParamBindings?.length && symbols.paramBindings?.length) {
513+
const restNameCallees = new Map<string, Set<string>>();
514+
for (const orpb of symbols.objectRestParamBindings) {
515+
if (!restNameCallees.has(orpb.restName)) restNameCallees.set(orpb.restName, new Set());
516+
restNameCallees.get(orpb.restName)!.add(orpb.callee);
517+
}
518+
for (const orpb of symbols.objectRestParamBindings) {
519+
for (const pb of symbols.paramBindings) {
520+
if (pb.callee === orpb.callee && pb.argIndex === orpb.argIndex) {
521+
const scopedKey = `${orpb.callee}::${orpb.restName}`;
522+
if (!typeMap.has(scopedKey)) {
523+
typeMap.set(scopedKey, { type: pb.argName, confidence: 0.65 });
524+
if (restNameCallees.get(orpb.restName)!.size === 1 && !typeMap.has(orpb.restName)) {
525+
typeMap.set(orpb.restName, { type: pb.argName, confidence: 0.65 });
526+
}
527+
}
528+
}
529+
}
530+
}
531+
}
532+
505533
const seenCallEdges = new Set<string>();
506534
const lookup = makeIncrementalLookup(db, stmts);
507535
let edgesAdded = 0;
@@ -510,14 +538,63 @@ function buildCallEdges(
510538
if (call.receiver && BUILTIN_RECEIVERS.has(call.receiver)) continue;
511539

512540
const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
513-
const { targets, importedFrom } = resolveCallTargets(
541+
const { targets: initialTargets, importedFrom } = resolveCallTargets(
514542
lookup,
515543
call,
516544
relPath,
517545
importedNames,
518546
typeMap,
519547
caller.callerName,
520548
);
549+
let targets = initialTargets;
550+
551+
if (targets.length === 0 && call.receiver === 'this' && caller.callerName != null) {
552+
const dotIdx = caller.callerName.indexOf('.');
553+
if (dotIdx > 0) {
554+
const className = caller.callerName.slice(0, dotIdx);
555+
const qualifiedName = `${className}.${call.name}`;
556+
const qualified = lookup
557+
.byNameAndFile(qualifiedName, relPath)
558+
.filter((n) => n.kind === 'method');
559+
if (qualified.length > 0) {
560+
targets = qualified;
561+
}
562+
}
563+
}
564+
565+
if (
566+
targets.length === 0 &&
567+
call.receiver === 'this' &&
568+
caller.callerName != null &&
569+
symbols.definePropertyReceivers
570+
) {
571+
const receiverVarName = symbols.definePropertyReceivers.get(caller.callerName);
572+
if (receiverVarName) {
573+
const typeEntry = typeMap.get(receiverVarName);
574+
const typeName = typeEntry
575+
? typeof typeEntry === 'string'
576+
? typeEntry
577+
: (typeEntry as { type?: string }).type
578+
: null;
579+
if (typeName) {
580+
const qualifiedName = `${typeName}.${call.name}`;
581+
const qualified = lookup.byNameAndFile(qualifiedName, relPath);
582+
if (qualified.length > 0) {
583+
targets = [...qualified];
584+
}
585+
}
586+
if (targets.length === 0) {
587+
// Narrow to function/method kinds only to avoid matching unrelated
588+
// variables or classes that share a name in the same file.
589+
const sameFile = lookup
590+
.byNameAndFile(call.name, relPath)
591+
.filter((n) => n.kind === 'function' || n.kind === 'method');
592+
if (sameFile.length > 0) {
593+
targets = [...sameFile];
594+
}
595+
}
596+
}
597+
}
521598

522599
for (const t of targets) {
523600
const edgeKey = `${caller.id}|${t.id}`;

0 commit comments

Comments
 (0)