Skip to content

Commit eb61150

Browse files
committed
fix: resolve merge conflicts with main
2 parents f313268 + 117ac5b commit eb61150

27 files changed

Lines changed: 674 additions & 152 deletions

File tree

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: 13 additions & 13 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: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -584,7 +584,11 @@ function buildCallEdges(
584584
}
585585
}
586586
if (targets.length === 0) {
587-
const sameFile = lookup.byNameAndFile(call.name, relPath);
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');
588592
if (sameFile.length > 0) {
589593
targets = [...sameFile];
590594
}

src/domain/graph/builder/stages/build-edges.ts

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {
1717
ClassRelation,
1818
Definition,
1919
ExtractorOutput,
20+
FnRefBinding,
2021
Import,
2122
NativeAddon,
2223
NodeRow,
@@ -709,6 +710,78 @@ function buildFnRefBindingsPtsPostPass(
709710
}
710711
}
711712

713+
/**
714+
* this-rebinding post-pass for the native call-edge path.
715+
*
716+
* When `fn.call(namedCtx, ...)` or `fn.apply(namedCtx, ...)` is extracted by the
717+
* WASM layer, `thisCallBindings` records `{ callee: 'fn', thisArg: 'namedCtx' }`.
718+
* The native Rust engine has no knowledge of these bindings, so `this()` calls
719+
* inside `fn` remain unresolved. This JS post-pass adds the missing edges by
720+
* resolving `this()` calls inside each `fn` that has a thisCallBinding.
721+
*/
722+
function buildThisCallBindingsPtsPostPass(
723+
ctx: PipelineContext,
724+
getNodeIdStmt: NodeIdStmt,
725+
allEdgeRows: EdgeRowTuple[],
726+
sharedLookup?: CallNodeLookup,
727+
): void {
728+
const filesWithBindings = [...ctx.fileSymbols].filter(
729+
([, symbols]) => symbols.thisCallBindings && symbols.thisCallBindings.length > 0,
730+
);
731+
if (filesWithBindings.length === 0) return;
732+
733+
const seenByPair = new Set<string>();
734+
for (const [srcId, tgtId] of allEdgeRows) {
735+
seenByPair.add(`${srcId}|${tgtId}`);
736+
}
737+
738+
const { barrelOnlyFiles, rootDir } = ctx;
739+
const lookup = sharedLookup ?? makeContextLookup(ctx, getNodeIdStmt);
740+
741+
for (const [relPath, symbols] of filesWithBindings) {
742+
if (barrelOnlyFiles.has(relPath)) continue;
743+
const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
744+
if (!fileNodeRow) continue;
745+
746+
const importedNames = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
747+
const typeMap: Map<string, TypeMapEntry | string> = symbols.typeMap || new Map();
748+
const ptsMap = buildPointsToMapForFile(symbols, importedNames);
749+
if (!ptsMap) continue;
750+
751+
// Only process calls named 'this' (callee-not-receiver usage)
752+
for (const call of symbols.calls) {
753+
if (call.name !== 'this' || call.receiver) continue;
754+
755+
const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
756+
if (caller.callerName == null) continue;
757+
758+
const scopedKey = `${caller.callerName}::this`;
759+
if (!ptsMap.has(scopedKey)) continue;
760+
761+
for (const alias of resolveViaPointsTo(scopedKey, ptsMap)) {
762+
const { targets: aliasTargets, importedFrom: aliasFrom } = resolveCallTargets(
763+
lookup,
764+
{ name: alias },
765+
relPath,
766+
importedNames,
767+
typeMap as Map<string, unknown>,
768+
);
769+
for (const t of aliasTargets) {
770+
const edgeKey = `${caller.id}|${t.id}`;
771+
if (t.id !== caller.id && !seenByPair.has(edgeKey)) {
772+
const conf =
773+
computeConfidence(relPath, t.file, aliasFrom ?? null) - PROPAGATION_HOP_PENALTY;
774+
if (conf > 0) {
775+
seenByPair.add(edgeKey);
776+
allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'points-to']);
777+
}
778+
}
779+
}
780+
}
781+
}
782+
}
783+
}
784+
712785
/**
713786
* Phase 8.3f post-pass for the native call-edge path.
714787
*
@@ -1144,6 +1217,7 @@ function buildPointsToMapForFile(
11441217
symbols: ExtractorOutput,
11451218
importedNames: Map<string, string>,
11461219
): PointsToMap | null {
1220+
const hasThisCallBindings = !!symbols.thisCallBindings?.length;
11471221
if (
11481222
!symbols.fnRefBindings?.length &&
11491223
!symbols.paramBindings?.length &&
@@ -1152,7 +1226,8 @@ function buildPointsToMapForFile(
11521226
!symbols.forOfBindings?.length &&
11531227
!symbols.arrayCallbackBindings?.length &&
11541228
!symbols.objectRestParamBindings?.length &&
1155-
!symbols.objectPropBindings?.length
1229+
!symbols.objectPropBindings?.length &&
1230+
!hasThisCallBindings
11561231
)
11571232
return null;
11581233
const defNames = new Set(
@@ -1161,8 +1236,21 @@ function buildPointsToMapForFile(
11611236
.map((d) => d.name),
11621237
);
11631238
const definitionParams = buildDefinitionParamsMap(symbols.definitions);
1239+
1240+
// Convert thisCallBindings into scoped fnRefBindings: `fn::this → namedCtx`.
1241+
// The scoped key `fn::this` is looked up when `this()` calls are resolved inside
1242+
// function `fn` — caller.callerName='fn', call.name='this' → scopedPtsKey='fn::this'.
1243+
let allFnRefBindings: readonly FnRefBinding[] = symbols.fnRefBindings ?? [];
1244+
if (hasThisCallBindings) {
1245+
const extra: FnRefBinding[] = (symbols.thisCallBindings ?? []).map((b) => ({
1246+
lhs: `${b.callee}::this`,
1247+
rhs: b.thisArg,
1248+
}));
1249+
allFnRefBindings = [...allFnRefBindings, ...extra];
1250+
}
1251+
11641252
return buildPointsToMap(
1165-
symbols.fnRefBindings ?? [],
1253+
allFnRefBindings,
11661254
defNames,
11671255
importedNames,
11681256
symbols.paramBindings,
@@ -1816,6 +1904,9 @@ export async function buildEdges(ctx: PipelineContext): Promise<void> {
18161904
// (e.g. `const f = fn.bind(ctx)`), so calls to bind-created aliases are
18171905
// not resolved to their original function on the native path.
18181906
buildFnRefBindingsPtsPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup);
1907+
// this-rebinding post-pass: resolve `this()` calls inside functions that
1908+
// were invoked via `.call(namedCtx, ...)` / `.apply(namedCtx, ...)`.
1909+
buildThisCallBindingsPtsPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup);
18191910
// Phase 8.3f post-pass: augment native call edges with object rest-param
18201911
// receiver resolution — typeMap[restName] → argName → typeMap[argName.method].
18211912
buildObjectRestParamPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup);

0 commit comments

Comments
 (0)