Skip to content

Commit e490b8f

Browse files
committed
fix: resolve merge conflicts with main
2 parents 9840bb2 + 25ada09 commit e490b8f

13 files changed

Lines changed: 766 additions & 42 deletions

File tree

.claude/skills/resolve/SKILL.md

Lines changed: 527 additions & 0 deletions
Large diffs are not rendered by default.

crates/codegraph-core/src/domain/graph/builder/stages/build_edges.rs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -672,9 +672,9 @@ fn is_top_level_binding_kind(kind: &str) -> bool {
672672
/// Two-pass strategy (mirrors the updated `findCaller` in call-resolver.ts):
673673
/// Pass 1 — narrowest enclosing function/method. Local variable declarations
674674
/// inside a function body must not shadow the enclosing function.
675-
/// Pass 2 — narrowest enclosing variable/constant binding. Used as fallback
676-
/// when no function/method encloses the call (e.g. Haskell top-level
677-
/// `main = do …` is a `bind` node with kind `variable`).
675+
/// Pass 2 — widest (outermost) enclosing variable/constant binding. Used as
676+
/// fallback when no function/method encloses the call (e.g. Haskell
677+
/// top-level `main = do …` is a `bind` node with kind `variable`).
678678
///
679679
/// Returns `(caller_id, caller_name)` — `caller_name` is `""` when the call
680680
/// falls back to file scope.
@@ -765,20 +765,21 @@ fn resolve_call_targets<'a>(
765765
// Phase 8.3f: callee-scoped rest-param key (`callee::restName`) avoids
766766
// same-name rest-binding collisions across functions in the same file (#1358).
767767
let rest_param_key = format!("{}::{}", caller_name, effective_receiver);
768-
// Class-scoped key (`ClassName.prop`) seeded by `this.prop = new Ctor()`
769-
// property writes — prevents false edges when multiple classes define the
770-
// same property name (issue #1323). Only consulted for `this.` receivers.
768+
// Class-scoped key (`ClassName.prop`) seeded by `this.prop = new Ctor()` and
769+
// field annotations — prevents false edges when multiple classes define the same
770+
// property name (issues #1323, #1458). Consulted first for `this.` receivers so
771+
// bare fallback keys (confidence 0.6) don't shadow the correct per-class entry.
771772
let class_scoped_key = if receiver.starts_with("this.") && !caller_name.is_empty() {
772773
caller_name
773774
.rfind('.')
774775
.map(|dot| format!("{}.{}", &caller_name[..dot], effective_receiver))
775776
} else {
776777
None
777778
};
778-
let type_lookup = type_map.get(effective_receiver)
779+
let type_lookup = class_scoped_key.as_deref().and_then(|k| type_map.get(k))
780+
.or_else(|| type_map.get(effective_receiver))
779781
.or_else(|| type_map.get(receiver.as_str()))
780-
.or_else(|| if caller_name.is_empty() { None } else { type_map.get(rest_param_key.as_str()) })
781-
.or_else(|| class_scoped_key.as_deref().and_then(|k| type_map.get(k)));
782+
.or_else(|| if caller_name.is_empty() { None } else { type_map.get(rest_param_key.as_str()) });
782783
// Inline new-expression receiver: `(new Foo).bar()` — extract the constructor name
783784
// when no typeMap entry exists for the complex receiver expression.
784785
// Mirrors the regex `/^\(?\s*new\s+([A-Z_$][A-Za-z0-9_$]*)/` in call-resolver.ts.

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,10 @@ fn infer_address_of_composite(
398398
type_map: &mut Vec<TypeMapEntry>,
399399
) -> bool {
400400
if rhs.kind() != "unary_expression" { return false; }
401+
// Verify the operator is `&` — guards against any other unary operator
402+
// applied to a composite literal on a raw AST.
403+
let Some(op_node) = rhs.child(0) else { return false };
404+
if node_text(&op_node, source) != "&" { return false; }
401405
// The operand of `&` is a composite_literal.
402406
let Some(operand) = rhs.child_by_field_name("operand") else { return false };
403407
if operand.kind() != "composite_literal" { return false; }

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2975,6 +2975,15 @@ mod tests {
29752975
JsExtractor.extract(&tree, code.as_bytes(), "test.js")
29762976
}
29772977

2978+
fn parse_ts(code: &str) -> FileSymbols {
2979+
let mut parser = Parser::new();
2980+
parser
2981+
.set_language(&tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into())
2982+
.unwrap();
2983+
let tree = parser.parse(code.as_bytes(), None).unwrap();
2984+
JsExtractor.extract(&tree, code.as_bytes(), "test.ts")
2985+
}
2986+
29782987
#[test]
29792988
fn finds_function_declaration() {
29802989
let s = parse_js("function greet(name) { return name; }");
@@ -3598,6 +3607,38 @@ mod tests {
35983607
assert_eq!(tm.unwrap().type_name, "HttpClient");
35993608
}
36003609

3610+
/// Issue #1458: two classes with identically-named field annotations must
3611+
/// produce separate class-scoped typeMap keys, not overwrite each other.
3612+
/// Mirrors the TS `prevents cross-class collision` test.
3613+
#[test]
3614+
fn field_annotation_multi_class_seeds_separate_scoped_keys() {
3615+
let s = parse_ts(
3616+
"class OrderService {\n\
3617+
private repo: OrderRepository;\n\
3618+
}\n\
3619+
class UserService {\n\
3620+
private repo: UserRepository;\n\
3621+
}",
3622+
);
3623+
let order_entry = s.type_map.iter().find(|t| t.name == "OrderService.repo");
3624+
assert!(
3625+
order_entry.is_some(),
3626+
"type_map should contain 'OrderService.repo'; got: {:?}",
3627+
s.type_map.iter().map(|e| &e.name).collect::<Vec<_>>()
3628+
);
3629+
assert_eq!(order_entry.unwrap().type_name, "OrderRepository");
3630+
assert_eq!(order_entry.unwrap().confidence, 0.9);
3631+
3632+
let user_entry = s.type_map.iter().find(|t| t.name == "UserService.repo");
3633+
assert!(
3634+
user_entry.is_some(),
3635+
"type_map should contain 'UserService.repo'; got: {:?}",
3636+
s.type_map.iter().map(|e| &e.name).collect::<Vec<_>>()
3637+
);
3638+
assert_eq!(user_entry.unwrap().type_name, "UserRepository");
3639+
assert_eq!(user_entry.unwrap().confidence, 0.9);
3640+
}
3641+
36013642
/// Issue #1453 (edge 4): `const f = fn.bind(ctx)` must record a
36023643
/// fnRefBinding f → fn so later `f()` calls resolve through pts.
36033644
#[test]

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

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -147,22 +147,25 @@ export function resolveByMethodOrGlobal(
147147
const effectiveReceiver = call.receiver.startsWith('this.')
148148
? call.receiver.slice('this.'.length)
149149
: call.receiver;
150-
// For this.prop receivers, also try the class-scoped key (ClassName.prop) seeded by
151-
// handlePropWriteTypeMap — prevents false edges when multiple classes define the same
152-
// property name (issue #1323).
153-
let typeEntry =
154-
typeMap.get(effectiveReceiver) ??
155-
typeMap.get(call.receiver) ??
156-
// Phase 8.3f: callee-scoped rest-param key (`callee::restName`) to avoid
157-
// same-name rest-binding collision across functions in the same file (#1358).
158-
(callerName ? typeMap.get(`${callerName}::${effectiveReceiver}`) : undefined);
159-
if (!typeEntry && call.receiver.startsWith('this.') && callerName) {
150+
// For this.prop receivers, prefer the class-scoped key (ClassName.prop) seeded by
151+
// handlePropWriteTypeMap / handleFieldDefTypeMap — prevents false edges when multiple
152+
// classes define the same property name (issues #1323, #1458).
153+
// Class-scoped lookup runs first so bare fallback keys (confidence 0.6) don't shadow
154+
// the correct per-class entry when callerName is available.
155+
let typeEntry: unknown;
156+
if (call.receiver.startsWith('this.') && callerName) {
160157
const dotIdx = callerName.lastIndexOf('.');
161158
if (dotIdx > -1) {
162159
const callerClass = callerName.slice(0, dotIdx);
163160
typeEntry = typeMap.get(`${callerClass}.${effectiveReceiver}`);
164161
}
165162
}
163+
typeEntry ??=
164+
typeMap.get(effectiveReceiver) ??
165+
typeMap.get(call.receiver) ??
166+
// Phase 8.3f: callee-scoped rest-param key (`callee::restName`) to avoid
167+
// same-name rest-binding collision across functions in the same file (#1358).
168+
(callerName ? typeMap.get(`${callerName}::${effectiveReceiver}`) : undefined);
166169
let typeName = typeEntry
167170
? typeof typeEntry === 'string'
168171
? typeEntry

src/domain/graph/builder/helpers.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ export const BUILTIN_RECEIVERS: Set<string> = new Set([
4848
'require',
4949
]);
5050

51+
/** Phase 8.5: confidence penalty applied to CHA-dispatch edges. */
52+
export const CHA_DISPATCH_PENALTY = 0.1;
53+
/** Phase 8.5: fixed confidence for typed-receiver (interface/CHA) dispatch edges.
54+
* File proximity is not meaningful for virtual dispatch — all three engine paths
55+
* (WASM inline, WASM post-pass, native post-pass) must agree on this value. */
56+
export const CHA_TYPED_DISPATCH_CONFIDENCE = 0.8;
57+
5158
/** Check if a directory entry should be skipped (ignored dirs, dotfiles). */
5259
function shouldSkipEntry(entry: fs.Dirent, extraIgnore: Set<string> | null): boolean {
5360
if (entry.name.startsWith('.') && entry.name !== '.') {
@@ -361,6 +368,9 @@ export function batchInsertEdges(db: BetterSqlite3Database, rows: unknown[][]):
361368
}
362369
}
363370

371+
/** Confidence assigned to CHA-expanded interface/abstract dispatch edges. */
372+
export const CHA_DISPATCH_CONFIDENCE = 0.8;
373+
364374
/**
365375
* CHA (Class Hierarchy Analysis) post-pass.
366376
*
@@ -514,7 +524,14 @@ export function runChaPostPass(db: BetterSqlite3Database): number {
514524
const key = `${source_id}|${methodNode.id}`;
515525
if (seen.has(key)) continue;
516526
seen.add(key);
517-
newEdges.push([source_id, methodNode.id, 'calls', 0.8, 0, 'cha']);
527+
newEdges.push([
528+
source_id,
529+
methodNode.id,
530+
'calls',
531+
CHA_TYPED_DISPATCH_CONFIDENCE,
532+
0,
533+
'cha',
534+
]);
518535
}
519536
}
520537

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

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,13 @@ import {
4545
import type { ChaContext } from '../cha.js';
4646
import { buildChaContext, resolveChaTargets, resolveThisDispatch } from '../cha.js';
4747
import type { PipelineContext } from '../context.js';
48-
import { BUILTIN_RECEIVERS, batchInsertEdges, runChaPostPass } from '../helpers.js';
48+
import {
49+
BUILTIN_RECEIVERS,
50+
batchInsertEdges,
51+
CHA_DISPATCH_PENALTY,
52+
CHA_TYPED_DISPATCH_CONFIDENCE,
53+
runChaPostPass,
54+
} from '../helpers.js';
4955
import { getResolved, isBarrelFile, resolveBarrelExportCached } from './resolve-imports.js';
5056

5157
// ── Local types ──────────────────────────────────────────────────────────
@@ -101,9 +107,6 @@ interface NativeEdge {
101107
dynamic: number;
102108
}
103109

104-
/** Phase 8.5: confidence penalty applied to CHA-dispatch edges. */
105-
export const CHA_DISPATCH_PENALTY = 0.1;
106-
107110
// ── Node lookup setup ───────────────────────────────────────────────────
108111

109112
function makeGetNodeIdStmt(db: BetterSqlite3Database): NodeIdStmt {
@@ -735,13 +738,12 @@ function buildChaPostPass(
735738
for (const t of chaTargets) {
736739
const edgeKey = `${caller.id}|${t.id}`;
737740
if (t.id !== caller.id && !seenByPair.has(edgeKey)) {
738-
// Typed-receiver (interface/CHA) dispatch: use the same hardcoded 0.8 that
739-
// runChaPostPass (helpers.ts) and runPostNativeCha (native-orchestrator.ts)
740-
// use — file proximity is not meaningful for virtual dispatch confidence.
741+
// Typed-receiver (interface/CHA) dispatch: use CHA_TYPED_DISPATCH_CONFIDENCE
742+
// — file proximity is not meaningful for virtual dispatch confidence.
741743
// this/super dispatch keeps computeConfidence-based proximity scoring to
742744
// match runPostNativeThisDispatch (native-orchestrator.ts).
743745
const conf = isTypedReceiverDispatch
744-
? 0.8
746+
? CHA_TYPED_DISPATCH_CONFIDENCE
745747
: computeConfidence(relPath, t.file, null) - CHA_DISPATCH_PENALTY;
746748
if (conf > 0) {
747749
seenByPair.add(edgeKey);
@@ -1327,13 +1329,12 @@ function buildFileCallEdges(
13271329
for (const t of chaTargets) {
13281330
const edgeKey = `${caller.id}|${t.id}`;
13291331
if (t.id !== caller.id && !seenCallEdges.has(edgeKey) && !ptsEdgeRows.has(edgeKey)) {
1330-
// Typed-receiver (interface/CHA) dispatch: use the same hardcoded 0.8 that
1331-
// runChaPostPass (helpers.ts) and runPostNativeCha (native-orchestrator.ts)
1332-
// use — file proximity is not meaningful for virtual dispatch confidence.
1332+
// Typed-receiver (interface/CHA) dispatch: use CHA_TYPED_DISPATCH_CONFIDENCE
1333+
// — file proximity is not meaningful for virtual dispatch confidence.
13331334
// this/super dispatch keeps computeConfidence-based proximity scoring to
1334-
// match runPostNativeThisDispatch (native-orchestrator.ts line 906).
1335+
// match runPostNativeThisDispatch (native-orchestrator.ts).
13351336
const conf = isTypedReceiverDispatch
1336-
? 0.8
1337+
? CHA_TYPED_DISPATCH_CONFIDENCE
13371338
: computeConfidence(relPath, t.file, null) - CHA_DISPATCH_PENALTY;
13381339
if (conf > 0) {
13391340
seenCallEdges.add(edgeKey);

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,14 @@ import type { PipelineContext } from '../context.js';
4949
import {
5050
batchInsertEdges,
5151
batchInsertNodes,
52+
CHA_DISPATCH_PENALTY,
53+
CHA_TYPED_DISPATCH_CONFIDENCE,
5254
collectFiles as collectFilesUtil,
5355
fileHash,
5456
fileStat,
5557
readFileSafe,
5658
} from '../helpers.js';
5759
import { NativeDbProxy } from '../native-db-proxy.js';
58-
import { CHA_DISPATCH_PENALTY } from './build-edges.js';
5960
import { closeNativeDb } from './native-db-lifecycle.js';
6061

6162
// ── Native orchestrator types ──────────────────────────────────────────
@@ -570,8 +571,8 @@ function runPostNativeCha(
570571
}
571572

572573
// Find existing call edges targeting qualified methods (e.g., 'IWorker.doWork').
573-
// Include the caller node's file so confidence can be computed file-pair-aware,
574-
// matching the WASM path's computeConfidence(callerFile, targetFile, null) - CHA_DISPATCH_PENALTY formula.
574+
// Include caller_file and method_file so affectedFiles can be populated for
575+
// incremental role reclassification; confidence uses CHA_TYPED_DISPATCH_CONFIDENCE matching runChaPostPass.
575576
// When scopeToChangedFiles is true, restrict to call sites in the changed files
576577
// (safe because no hierarchy or RTA evidence changed outside those files).
577578
let callToMethods: Array<{ source_id: number; method_name: string; caller_file: string | null }>;
@@ -667,10 +668,7 @@ function runPostNativeCha(
667668
const key = `${source_id}|${methodNode.id}`;
668669
if (seen.has(key)) continue;
669670
seen.add(key);
670-
// Use the same hardcoded 0.8 that runChaPostPass (helpers.ts) uses for
671-
// DB-level CHA dispatch edges. This aligns the native orchestrator path
672-
// with the WASM and hybrid paths, which both go through runChaPostPass.
673-
const conf = 0.8;
671+
const conf = CHA_TYPED_DISPATCH_CONFIDENCE;
674672
newEdges.push([source_id, methodNode.id, 'calls', conf, 0, 'cha']);
675673
newEdgeCount++;
676674
if (caller_file) affectedFiles.add(caller_file);

src/extractors/cpp.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,10 @@ function handleCppDeclaration(node: TreeSitterNode, ctx: ExtractorOutput): void
229229
} else if (kind === 'identifier') {
230230
nameNode = child;
231231
}
232+
// Note: pointer_declarator / reference_declarator children (e.g. `UserService *svc;`)
233+
// are intentionally skipped here — they are also skipped by the native Rust
234+
// match_c_family_type_map helper, which only handles 'init_declarator' and
235+
// 'identifier' children. Both engines have the same scope for this case.
232236
if (!nameNode) continue;
233237
const varName = unwrapCppDeclaratorName(nameNode);
234238
if (varName) {

src/extractors/cuda.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,10 @@ function handleCudaDeclaration(node: TreeSitterNode, ctx: ExtractorOutput): void
229229
} else if (kind === 'identifier') {
230230
nameNode = child;
231231
}
232+
// Note: pointer_declarator / reference_declarator children (e.g. `UserService *svc;`)
233+
// are intentionally skipped here — they are also skipped by the native Rust
234+
// match_c_family_type_map helper, which only handles 'init_declarator' and
235+
// 'identifier' children. Both engines have the same scope for this case.
232236
if (!nameNode) continue;
233237
const varName = extractCudaFieldName(nameNode);
234238
if (varName) {

0 commit comments

Comments
 (0)