Skip to content

Commit 6021541

Browse files
committed
fix: resolve merge conflicts with main
2 parents 0ab5291 + 34988f8 commit 6021541

13 files changed

Lines changed: 416 additions & 48 deletions

File tree

.github/workflows/ci.yml

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -174,17 +174,11 @@ jobs:
174174
steps:
175175
- uses: actions/checkout@v6
176176

177-
- name: Setup Node.js
178-
uses: actions/setup-node@v6
179-
with:
180-
node-version: 22
181-
182-
- name: Install dependencies
183-
timeout-minutes: 20
184-
run: npm ci
185-
186177
- name: Audit production dependencies
187-
run: npm audit --omit=dev --audit-level=high
178+
# --package-lock-only reads the lock file directly — no npm install needed.
179+
# This avoids flaky binary downloads (e.g. onnxruntime-node) that would
180+
# abort the audit before it can report real vulnerabilities.
181+
run: npm audit --omit=dev --audit-level=high --package-lock-only
188182

189183
verify-imports:
190184
runs-on: ubuntu-latest

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
1010
1111
> **Never silently skip verification.** If tests, builds, or any verification step cannot run or fails for any reason (compilation errors, platform issues, missing dependencies), STOP and report the issue to the user immediately. Never silently proceed with unverified changes. Let the user decide whether to proceed — do not make that decision yourself.
1212
13-
> **Scope discipline — open issues, don't expand scope.** When you encounter a problem unrelated to the current task — pre-existing bugs, code that needs a bigger refactor, or any defect that does not directly affect the result of what you're doing — open a GitHub issue with `gh issue create` and keep going. Only fix it inline when it directly affects the correctness or outcome of the work in front of you. This keeps PRs focused (one concern per PR) while ensuring the finding is not lost.
13+
> **Scope discipline — open issues, don't expand scope.** When you encounter anything out of scope — a pre-existing bug, a refactor opportunity, a potential improvement, a missing feature, or any other finding that doesn't directly affect the correctness of the current task**immediately open a GitHub issue with `gh issue create` before continuing**. Do not hold the finding in memory or defer it to a comment. Only address it inline when it directly blocks the result of the work in front of you. This keeps PRs focused (one concern per PR) while ensuring no finding is lost.
1414
1515
> **Prioritize the best architecture, not the smallest diff.** Do not default to the simplest or most localized fix. Choose the approach that fits the codebase's architecture best, even when that means larger changes, moving code across modules, or restructuring an abstraction. Do not be afraid of bigger changes — a larger diff that leaves the design healthier is preferable to a small diff that entrenches a poor structure. Surface the architectural reasoning to the user; don't silently shrink the change to avoid the work.
1616

crates/codegraph-core/src/edge_builder.rs

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ pub struct ComputedEdge {
9696

9797
/// Internal struct for caller resolution (def line range → node ID).
9898
struct DefWithId<'a> {
99-
_name: &'a str,
99+
name: &'a str,
100100
line: u32,
101101
end_line: u32,
102102
node_id: Option<u32>,
@@ -106,6 +106,8 @@ struct DefWithId<'a> {
106106
struct EdgeContext<'a> {
107107
nodes_by_name: HashMap<&'a str, Vec<&'a NodeInfo>>,
108108
nodes_by_name_and_file: HashMap<(&'a str, &'a str), Vec<&'a NodeInfo>>,
109+
/// All nodes grouped by file — used for same-file method resolution (CHA this-dispatch).
110+
nodes_by_file: HashMap<&'a str, Vec<&'a NodeInfo>>,
109111
builtin_set: HashSet<&'a str>,
110112
receiver_kinds: HashSet<&'a str>,
111113
}
@@ -114,17 +116,19 @@ impl<'a> EdgeContext<'a> {
114116
fn new(all_nodes: &'a [NodeInfo], builtin_receivers: &'a [String]) -> Self {
115117
let mut nodes_by_name: HashMap<&str, Vec<&NodeInfo>> = HashMap::new();
116118
let mut nodes_by_name_and_file: HashMap<(&str, &str), Vec<&NodeInfo>> = HashMap::new();
119+
let mut nodes_by_file: HashMap<&str, Vec<&NodeInfo>> = HashMap::new();
117120
for node in all_nodes {
118121
nodes_by_name.entry(&node.name).or_default().push(node);
119122
nodes_by_name_and_file
120123
.entry((&node.name, &node.file))
121124
.or_default()
122125
.push(node);
126+
nodes_by_file.entry(&node.file).or_default().push(node);
123127
}
124128
let builtin_set: HashSet<&str> = builtin_receivers.iter().map(|s| s.as_str()).collect();
125129
let receiver_kinds: HashSet<&str> = ["class", "struct", "interface", "type", "module"]
126130
.iter().copied().collect();
127-
Self { nodes_by_name, nodes_by_name_and_file, builtin_set, receiver_kinds }
131+
Self { nodes_by_name, nodes_by_name_and_file, nodes_by_file, builtin_set, receiver_kinds }
128132
}
129133
}
130134

@@ -253,7 +257,7 @@ fn process_file<'a>(
253257
let node_id = file_nodes.iter()
254258
.find(|n| n.name == d.name && n.kind == d.kind && n.line == d.line)
255259
.map(|n| n.id);
256-
DefWithId { _name: &d.name, line: d.line, end_line: d.end_line.unwrap_or(u32::MAX), node_id }
260+
DefWithId { name: &d.name, line: d.line, end_line: d.end_line.unwrap_or(u32::MAX), node_id }
257261
}).collect();
258262

259263
// Phase 8.3: build pts map for alias resolution.
@@ -280,11 +284,11 @@ fn process_file<'a>(
280284
if ctx.builtin_set.contains(receiver.as_str()) { continue; }
281285
}
282286

283-
let caller_id = find_enclosing_caller(&defs_with_ids, call.line, file_node_id);
287+
let (caller_id, caller_name) = find_enclosing_caller(&defs_with_ids, call.line, file_node_id);
284288
let is_dynamic = if call.dynamic.unwrap_or(false) { 1u32 } else { 0u32 };
285289
let imported_from = imported_names.get(call.name.as_str()).copied();
286290

287-
let mut targets = resolve_call_targets(ctx, call, rel_path, imported_from, &type_map);
291+
let mut targets = resolve_call_targets(ctx, call, rel_path, imported_from, &type_map, caller_name);
288292
sort_targets_by_confidence(&mut targets, rel_path, imported_from);
289293
emit_call_edges(&targets, caller_id, is_dynamic, rel_path, imported_from, &mut seen_edges, &mut pts_edge_map, edges);
290294

@@ -307,7 +311,7 @@ fn process_file<'a>(
307311
receiver: None,
308312
};
309313
let mut alias_targets = resolve_call_targets(
310-
ctx, &alias_call, rel_path, alias_imported_from, &type_map,
314+
ctx, &alias_call, rel_path, alias_imported_from, &type_map, caller_name,
311315
);
312316
sort_targets_by_confidence(&mut alias_targets, rel_path, alias_imported_from);
313317
for t in &alias_targets {
@@ -339,30 +343,36 @@ fn process_file<'a>(
339343
}
340344

341345
/// Find the narrowest enclosing definition for a call at the given line.
342-
fn find_enclosing_caller(defs: &[DefWithId], call_line: u32, file_node_id: u32) -> u32 {
346+
/// Returns `(caller_id, caller_name)` — `caller_name` is `""` when the call is at file scope.
347+
fn find_enclosing_caller<'a>(defs: &[DefWithId<'a>], call_line: u32, file_node_id: u32) -> (u32, &'a str) {
343348
let mut caller_id = file_node_id;
349+
let mut caller_name = "";
344350
let mut caller_span = u32::MAX;
345351
for def in defs {
346352
if def.line <= call_line && call_line <= def.end_line {
347353
let span = def.end_line - def.line;
348354
if span < caller_span {
349355
if let Some(id) = def.node_id {
350356
caller_id = id;
357+
caller_name = def.name;
351358
caller_span = span;
352359
}
353360
}
354361
}
355362
}
356-
caller_id
363+
(caller_id, caller_name)
357364
}
358365

359366
/// Multi-strategy call target resolution: import-aware → same-file → method → type-aware → scoped.
367+
/// `caller_name` is the enclosing function/method name (e.g. `"Shape.describe"`) used to scope
368+
/// `this`/`self`/`super` dispatch to the caller's own class before falling back to a broader scan.
360369
fn resolve_call_targets<'a>(
361370
ctx: &EdgeContext<'a>,
362371
call: &CallInfo,
363372
rel_path: &str,
364373
imported_from: Option<&str>,
365374
type_map: &HashMap<&str, (&str, f64)>,
375+
caller_name: &str,
366376
) -> Vec<&'a NodeInfo> {
367377
// 1. Import-aware resolution
368378
if let Some(imp_file) = imported_from {
@@ -386,9 +396,18 @@ fn resolve_call_targets<'a>(
386396
.unwrap_or_default();
387397
if !method_candidates.is_empty() { return method_candidates; }
388398

389-
// 4. Type-aware resolution via receiver → type map
399+
// 4. Type-aware resolution via receiver → type map.
400+
// Strips "this." prefix so `this.repo.method()` resolves via typeMap["repo"]
401+
// or typeMap["this.repo"] (both seeded by the class-field extractor).
390402
if let Some(ref receiver) = call.receiver {
391-
if let Some(&(type_name, _conf)) = type_map.get(receiver.as_str()) {
403+
let effective_receiver = if receiver.starts_with("this.") {
404+
&receiver["this.".len()..]
405+
} else {
406+
receiver.as_str()
407+
};
408+
let type_lookup = type_map.get(effective_receiver)
409+
.or_else(|| type_map.get(receiver.as_str()));
410+
if let Some(&(type_name, _conf)) = type_lookup {
392411
let qualified = format!("{}.{}", type_name, call.name);
393412
let typed: Vec<&NodeInfo> = ctx.nodes_by_name
394413
.get(qualified.as_str())
@@ -415,12 +434,44 @@ fn resolve_call_targets<'a>(
415434
|| call.receiver.as_deref() == Some("self")
416435
|| call.receiver.as_deref() == Some("super")
417436
{
418-
return ctx.nodes_by_name
437+
// First try exact name match (e.g. an unqualified function named "area").
438+
let exact: Vec<&NodeInfo> = ctx.nodes_by_name
419439
.get(call.name.as_str())
420440
.map(|v| v.iter()
421441
.filter(|n| import_resolution::compute_confidence(rel_path, &n.file, None) >= 0.5)
422442
.copied().collect())
423443
.unwrap_or_default();
444+
if !exact.is_empty() { return exact; }
445+
446+
// For this/self/super: prefer class-scoped exact lookup (e.g. `this.area()` in
447+
// `Shape.describe` → try `Shape.area` first). This avoids false edges to unrelated
448+
// classes that happen to have a method with the same name in the same file.
449+
// Fall back to the broader same-file suffix scan only when the class-scoped lookup
450+
// finds nothing (e.g. when the caller is at module scope or the name is unknown).
451+
if call.receiver.is_some() {
452+
// Extract the class prefix from the enclosing caller name (e.g. "Shape" from "Shape.describe").
453+
if let Some(dot_pos) = caller_name.find('.') {
454+
let class_prefix = &caller_name[..dot_pos];
455+
let qualified = format!("{}.{}", class_prefix, call.name);
456+
let class_scoped: Vec<&NodeInfo> = ctx.nodes_by_name
457+
.get(qualified.as_str())
458+
.map(|v| v.iter().filter(|n| n.kind == "method").copied().collect())
459+
.unwrap_or_default();
460+
if !class_scoped.is_empty() { return class_scoped; }
461+
}
462+
463+
// Broader fallback: same-file suffix scan to pick up CHA-expanded targets
464+
// (subclasses that override the method).
465+
let suffix = format!(".{}", call.name);
466+
if let Some(file_nodes) = ctx.nodes_by_file.get(rel_path) {
467+
let same_file_methods: Vec<&NodeInfo> = file_nodes.iter()
468+
.filter(|n| n.kind == "method" && n.name.ends_with(&suffix))
469+
.copied()
470+
.collect();
471+
if !same_file_methods.is_empty() { return same_file_methods; }
472+
}
473+
}
474+
return exact; // empty
424475
}
425476

426477
Vec::new()

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,29 @@ fn match_js_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _dep
140140
}
141141
}
142142
}
143+
// TypeScript class field declarations: `private repo: Repository<User>`
144+
// Seeds both "repo" and "this.repo" so that `this.repo.method()` calls
145+
// can be resolved to the interface/class type via the type map.
146+
"public_field_definition" | "field_definition" => {
147+
let name_node = node.child_by_field_name("name")
148+
.or_else(|| node.child_by_field_name("property"))
149+
.or_else(|| find_child(node, "property_identifier"));
150+
if let Some(name_node) = name_node {
151+
let kind = name_node.kind();
152+
if kind == "property_identifier" || kind == "identifier"
153+
|| kind == "private_property_identifier"
154+
{
155+
let field_name = node_text(&name_node, source).to_string();
156+
if let Some(type_anno) = find_child(node, "type_annotation") {
157+
if let Some(type_name) = extract_simple_type_name(&type_anno, source) {
158+
push_type_map_entry(symbols, field_name.clone(), type_name.to_string());
159+
// "this.fieldName" key resolves `this.repo.method()` calls.
160+
push_type_map_entry(symbols, format!("this.{}", field_name), type_name.to_string());
161+
}
162+
}
163+
}
164+
}
165+
}
143166
_ => {}
144167
}
145168
}

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

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,15 @@ export function resolveByMethodOrGlobal(
6464
call: { name: string; receiver?: string | null },
6565
relPath: string,
6666
typeMap: Map<string, unknown>,
67+
callerName?: string | null,
6768
): ReadonlyArray<{ id: number; file: string }> {
6869
if (call.receiver) {
69-
const typeEntry = typeMap.get(call.receiver);
70+
// Strip "this." so `this.repo.method()` resolves via typeMap["repo"]
71+
// (or the "this.repo" key seeded directly by the TSC property-declaration enricher).
72+
const effectiveReceiver = call.receiver.startsWith('this.')
73+
? call.receiver.slice('this.'.length)
74+
: call.receiver;
75+
const typeEntry = typeMap.get(effectiveReceiver) ?? typeMap.get(call.receiver);
7076
let typeName = typeEntry
7177
? typeof typeEntry === 'string'
7278
? typeEntry
@@ -122,7 +128,26 @@ export function resolveByMethodOrGlobal(
122128
call.receiver === 'self' ||
123129
call.receiver === 'super'
124130
) {
125-
return lookup.byName(call.name).filter((t) => computeConfidence(relPath, t.file, null) >= 0.5);
131+
const exact = lookup
132+
.byName(call.name)
133+
.filter((t) => computeConfidence(relPath, t.file, null) >= 0.5);
134+
if (exact.length > 0) return exact;
135+
136+
// For this/self/super receiver: try same-class method lookup via callerName.
137+
// e.g. `this.area()` inside `Shape.describe` → try `Shape.area`.
138+
// This seeds the initial edge that runChaPostPass later expands to subclass overrides.
139+
if (call.receiver && callerName) {
140+
const dotIdx = callerName.lastIndexOf('.');
141+
if (dotIdx > -1) {
142+
const callerClass = callerName.slice(0, dotIdx);
143+
const qualifiedName = `${callerClass}.${call.name}`;
144+
const sameClass = lookup
145+
.byName(qualifiedName)
146+
.filter((t) => t.kind === 'method' && computeConfidence(relPath, t.file, null) >= 0.5);
147+
if (sameClass.length > 0) return sameClass;
148+
}
149+
}
150+
return exact; // empty
126151
}
127152
return [];
128153
}
@@ -133,6 +158,7 @@ export function resolveCallTargets(
133158
relPath: string,
134159
importedNames: Map<string, string>,
135160
typeMap: Map<string, unknown>,
161+
callerName?: string | null,
136162
): { targets: Array<{ id: number; file: string }>; importedFrom: string | undefined } {
137163
const importedFrom = importedNames.get(call.name);
138164
let targets: ReadonlyArray<{ id: number; file: string }> | undefined;
@@ -150,7 +176,7 @@ export function resolveCallTargets(
150176
if (!targets || targets.length === 0) {
151177
targets = lookup.byNameAndFile(call.name, relPath);
152178
if (targets.length === 0) {
153-
targets = resolveByMethodOrGlobal(lookup, call, relPath, typeMap);
179+
targets = resolveByMethodOrGlobal(lookup, call, relPath, typeMap, callerName);
154180
}
155181
}
156182

0 commit comments

Comments
 (0)