Skip to content

Commit f313268

Browse files
committed
fix: resolve merge conflicts with main [docs check acknowledged]
2 parents 85ec995 + 0754216 commit f313268

13 files changed

Lines changed: 518 additions & 26 deletions

File tree

.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

package-lock.json

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

src/domain/graph/builder/incremental.ts

Lines changed: 74 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,59 @@ 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+
const sameFile = lookup.byNameAndFile(call.name, relPath);
588+
if (sameFile.length > 0) {
589+
targets = [...sameFile];
590+
}
591+
}
592+
}
593+
}
521594

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

src/domain/parser.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,14 +168,20 @@ const COMMON_QUERY_PATTERNS: string[] = [
168168
'(expression_statement (assignment_expression left: (member_expression) @assign_left right: (_) @assign_right)) @assign_node',
169169
];
170170

171-
// JS: class name is (identifier)
172-
const JS_CLASS_PATTERN: string = '(class_declaration name: (identifier) @cls_name) @cls_node';
171+
// JS: class name is (identifier) — declarations and expressions
172+
const JS_CLASS_PATTERNS: string[] = [
173+
'(class_declaration name: (identifier) @cls_name) @cls_node',
174+
// class expressions: `return class Foo extends Bar { ... }` or `const X = class Foo { ... }`
175+
'(class name: (identifier) @cls_name) @cls_node',
176+
];
173177

174178
// TS/TSX: class name is (type_identifier), plus interface and type alias
175179
// abstract_class_declaration is a separate node type in tree-sitter-typescript
176180
const TS_EXTRA_PATTERNS: string[] = [
177181
'(class_declaration name: (type_identifier) @cls_name) @cls_node',
178182
'(abstract_class_declaration name: (type_identifier) @cls_name) @cls_node',
183+
// class expressions: `return class Foo extends Bar { ... }`
184+
'(class name: (type_identifier) @cls_name) @cls_node',
179185
'(interface_declaration name: (type_identifier) @iface_name) @iface_node',
180186
'(type_alias_declaration name: (type_identifier) @type_name) @type_node',
181187
];
@@ -206,7 +212,7 @@ async function doLoadLanguage(entry: LanguageRegistryEntry): Promise<void> {
206212
const isTS = entry.id === 'typescript' || entry.id === 'tsx';
207213
const patterns = isTS
208214
? [...COMMON_QUERY_PATTERNS, ...TS_EXTRA_PATTERNS]
209-
: [...COMMON_QUERY_PATTERNS, JS_CLASS_PATTERN];
215+
: [...COMMON_QUERY_PATTERNS, ...JS_CLASS_PATTERNS];
210216
_queryCache.set(entry.id, new Query(lang, patterns.join('\n')));
211217
}
212218
} catch (e: unknown) {

src/extractors/javascript.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,8 @@ function walkJavaScriptNode(node: TreeSitterNode, ctx: ExtractorOutput): void {
734734
break;
735735
case 'class_declaration':
736736
case 'abstract_class_declaration':
737+
// class expressions: `return class Foo extends Bar { ... }` or `const X = class Foo { ... }`
738+
case 'class':
737739
handleClassDecl(node, ctx);
738740
break;
739741
case 'method_definition':
@@ -874,7 +876,7 @@ function handleStaticBlock(node: TreeSitterNode, definitions: Definition[]): voi
874876
if (!className) return;
875877
definitions.push({
876878
name: `${className}.<static>`,
877-
kind: 'function',
879+
kind: 'method',
878880
line: nodeStartLine(node),
879881
endLine: nodeEndLine(node),
880882
});
@@ -2141,6 +2143,31 @@ function extractParamBindingsWalk(rootNode: TreeSitterNode, paramBindings: Param
21412143
if (ct === ',' || ct === '(' || ct === ')') continue;
21422144
if (ct === 'identifier' && !BUILTIN_GLOBALS.has(child.text)) {
21432145
paramBindings.push({ callee: fn.text, argIndex: argIdx, argName: child.text });
2146+
} else if (ct === 'spread_element') {
2147+
// f(...[a, b]) — inline array literal: expand each element as a direct param binding.
2148+
const inner =
2149+
child.childForFieldName('argument') ?? (child.childCount > 1 ? child.child(1) : null);
2150+
if (inner?.type === 'array') {
2151+
let elemCount = 0;
2152+
for (let j = 0; j < inner.childCount; j++) {
2153+
const elem = inner.child(j);
2154+
if (!elem) continue;
2155+
if (elem.type === ',' || elem.type === '[' || elem.type === ']') continue;
2156+
if (elem.type === 'identifier' && !BUILTIN_GLOBALS.has(elem.text)) {
2157+
paramBindings.push({
2158+
callee: fn.text,
2159+
argIndex: argIdx + elemCount,
2160+
argName: elem.text,
2161+
});
2162+
}
2163+
elemCount++;
2164+
}
2165+
// Advance by the exact number of slots this spread occupies and skip
2166+
// the unconditional argIdx++ below so that zero-element spreads (...[])
2167+
// do not shift subsequent argument indices.
2168+
argIdx += elemCount;
2169+
continue;
2170+
}
21442171
}
21452172
argIdx++;
21462173
}

src/infrastructure/native.ts

Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
* to the existing WASM pipeline.
77
*/
88

9+
import { existsSync } from 'node:fs';
910
import { createRequire } from 'node:module';
1011
import os from 'node:os';
12+
import { fileURLToPath } from 'node:url';
1113
import { EngineError, toErrorMessage } from '../shared/errors.js';
1214
import type { NativeAddon } from '../types.js';
13-
import { debug } from './logger.js';
15+
import { debug, warn } from './logger.js';
1416

1517
let _cached: NativeAddon | null | undefined; // undefined = not yet tried, null = failed, NativeAddon = module
1618
let _loadError: Error | null = null;
@@ -44,47 +46,107 @@ const PLATFORM_PACKAGES: Record<string, string> = {
4446
'win32-x64': '@optave/codegraph-win32-x64-msvc',
4547
};
4648

49+
/**
50+
* Map of (platform-arch[-libc]) → locally compiled binary filename.
51+
* Checked before the npm package so that locally compiled Rust changes
52+
* are picked up immediately in development without publishing a new release.
53+
*/
54+
const PLATFORM_LOCAL_BINARIES: Record<string, string> = {
55+
'linux-x64-gnu': 'codegraph-core.linux-x64-gnu.node',
56+
'linux-x64-musl': 'codegraph-core.linux-x64-musl.node',
57+
'linux-arm64-gnu': 'codegraph-core.linux-arm64-gnu.node',
58+
'linux-arm64-musl': 'codegraph-core.linux-arm64-musl.node',
59+
'darwin-arm64': 'codegraph-core.darwin-arm64.node',
60+
'darwin-x64': 'codegraph-core.darwin-x64.node',
61+
'win32-x64': 'codegraph-core.win32-x64-msvc.node',
62+
};
63+
64+
/** Compute the platform key used to index PLATFORM_PACKAGES / PLATFORM_LOCAL_BINARIES. */
65+
function resolvePlatformKey(): string {
66+
const platform = os.platform();
67+
const arch = os.arch();
68+
return platform === 'linux' ? `${platform}-${arch}-${detectLibc()}` : `${platform}-${arch}`;
69+
}
70+
4771
/**
4872
* Resolve the platform-specific npm package name for the native addon.
4973
* Returns null if the current platform is not supported.
5074
*/
5175
function resolvePlatformPackage(): string | null {
52-
const platform = os.platform();
53-
const arch = os.arch();
54-
const key = platform === 'linux' ? `${platform}-${arch}-${detectLibc()}` : `${platform}-${arch}`;
55-
return PLATFORM_PACKAGES[key] || null;
76+
return PLATFORM_PACKAGES[resolvePlatformKey()] ?? null;
5677
}
5778

5879
/**
5980
* Try to load the native napi addon.
6081
* Returns the module on success, null on failure.
6182
*
62-
* Dev override: CODEGRAPH_NATIVE_ADDON_PATH can point to a locally built
63-
* .node file (e.g. crates/codegraph-core/index.node from `cargo build`).
64-
* Only honoured when set explicitly — never falls back to it implicitly.
83+
* Load order:
84+
* 1. NAPI_RS_NATIVE_LIBRARY_PATH env var (explicit override)
85+
* 2. locally compiled binary in crates/codegraph-core/ (dev mode — preferred
86+
* over the npm package so that Rust changes are picked up immediately
87+
* without publishing a new release)
88+
* 3. npm platform package (production path)
6589
*/
6690
export function loadNative(): NativeAddon | null {
6791
if (_cached !== undefined) return _cached;
6892

69-
const devOverride = process.env.CODEGRAPH_NATIVE_ADDON_PATH;
70-
if (devOverride) {
93+
const platformKey = resolvePlatformKey();
94+
95+
// 1. Explicit path override — highest priority. Failure is fatal: if the
96+
// operator set this variable, silently loading a different binary would
97+
// be harder to diagnose than an explicit error.
98+
const envPath = process.env.NAPI_RS_NATIVE_LIBRARY_PATH;
99+
if (envPath) {
71100
try {
72-
_cached = _require(devOverride) as NativeAddon;
101+
_cached = _require(envPath) as NativeAddon;
102+
debug(`loadNative: loaded from NAPI_RS_NATIVE_LIBRARY_PATH: ${envPath}`);
73103
return _cached;
74104
} catch (err) {
75105
_loadError = err as Error;
106+
warn(
107+
`loadNative: NAPI_RS_NATIVE_LIBRARY_PATH is set but failed to load "${envPath}": ${toErrorMessage(err as Error)}`,
108+
);
76109
_cached = null;
77110
return null;
78111
}
79112
}
80113

114+
// 2. Locally compiled dev binary — preferred over npm package so that Rust
115+
// changes are visible without publishing. Only used when the file exists.
116+
// If the file exists but fails to load (e.g. stale ABI), we warn and halt
117+
// rather than silently falling through to the npm package — that would
118+
// defeat the purpose of this priority order.
119+
const localFile = PLATFORM_LOCAL_BINARIES[platformKey];
120+
if (localFile) {
121+
const localPath = fileURLToPath(
122+
new URL(`../../crates/codegraph-core/${localFile}`, import.meta.url),
123+
);
124+
if (existsSync(localPath)) {
125+
try {
126+
_cached = _require(localPath) as NativeAddon;
127+
debug(`loadNative: loaded local dev binary: ${localPath}`);
128+
return _cached;
129+
} catch (err) {
130+
_loadError = err as Error;
131+
warn(
132+
`loadNative: local dev binary exists but failed to load "${localPath}": ${toErrorMessage(err as Error)}`,
133+
);
134+
_cached = null;
135+
return null;
136+
}
137+
}
138+
}
139+
140+
// 3. Published npm platform package — production path.
81141
const pkg = resolvePlatformPackage();
82142
if (pkg) {
83143
try {
84144
_cached = _require(pkg) as NativeAddon;
145+
debug(`loadNative: loaded npm package: ${pkg}`);
85146
return _cached;
86147
} catch (err) {
87148
_loadError = err as Error;
149+
debug(`loadNative: npm package ${pkg} not available: ${toErrorMessage(err as Error)}`);
88150
}
89151
} else {
90152
_loadError = new Error(`Unsupported platform: ${os.platform()}-${os.arch()}`);
@@ -104,6 +166,10 @@ export function isNativeAvailable(): boolean {
104166
/**
105167
* Read the version from the platform-specific npm package.json.
106168
* Returns null if the package is not installed or has no version.
169+
*
170+
* Note: always reports the npm package version. When the local dev binary or
171+
* NAPI_RS_NATIVE_LIBRARY_PATH is loaded instead, this version may not match
172+
* the running binary.
107173
*/
108174
export function getNativePackageVersion(): string | null {
109175
const pkg = resolvePlatformPackage();

0 commit comments

Comments
 (0)