Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixes

- Calls on built-in and external TypeScript/JavaScript receivers (such as `Map.get`, `Map.set`, `Map.has`) no longer resolve to unrelated same-named project methods; static and call-result receiver context is preserved so proven project members still resolve, while computed or otherwise unproven receivers stay unlinked — re-index after upgrading. (#1566)


## [1.6.0] - 2026-08-26

Expand Down
44 changes: 44 additions & 0 deletions __tests__/extraction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11918,4 +11918,48 @@ describe('C/C++ kernel-port preParse blanks (R7a)', () => {
expect(result.nodes.some((n) => n.kind === 'class' && n.name === 'Widget')).toBe(true);
expect(result.nodes.some((n) => n.kind === 'method' && n.name === 'size')).toBe(true);
});

describe('TypeScript/JavaScript nested receiver extraction (#1566)', () => {
it('preserves static receiver chains and leaves dynamic expressions silent', () => {
const src = `
export class TestClass {
private store = new Map<string, string>();
testMethod(holder: any) {
// Simple local receiver
const values = new Map<string, string>();
values.get("key");

// Nested member chains
holder.values.get("key");
this.store.get("key");
this.mailer.send("msg");
a.b.c.d("call");

// Direct this call
this.testMethod(null);

// Dynamic / computed / call-result expressions
holder[key].get("key");
factory().get("key");
}
}
`;
const result = extractFromSource('test.ts', src, 'typescript');
const refs = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls');
const refNames = refs.map((r) => r.referenceName);

expect(refNames).toContain('values.get');
expect(refNames).toContain('holder.values.get');
expect(refNames).toContain('this.store.get');
expect(refNames).toContain('this.mailer.send');
expect(refNames).toContain('a.b.c.d');
expect(refNames).toContain('testMethod'); // direct this.testMethod -> bare method

// Dynamic / computed expressions do not emit bare methodName refs (#1566/#647)
expect(refNames).toContain('factory().get');
expect(refNames).toContain('factory');
expect(refNames).not.toContain('get');
expect(refNames.some((r) => r.includes('key]'))).toBe(false);
});
});
});
42 changes: 41 additions & 1 deletion __tests__/kernel-tsjs-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,47 @@ describe.skipIf(!kernelBuilt)('kernel TS/JS extraction parity', () => {

it('torture fixture (tsx): components, stores, RTK, fn-refs, value-refs, decorators', () => {
const file = path.join(FIXTURE_DIR, 'torture.tsx');
assertParity('fixtures/torture.tsx', fs.readFileSync(file, 'utf8'), 'tsx');
const content = fs.readFileSync(file, 'utf8');
assertParity('fixtures/torture.tsx', content, 'tsx');

// Semantic check (#1566): BaseService::list preserves `this.cache.get` instead of bare `get`
const wasmRes = extractFromSource('fixtures/torture.tsx', content, 'tsx');
const kernelRes = tryKernelExtract('fixtures/torture.tsx', content, 'tsx')!;
for (const res of [wasmRes, kernelRes]) {
const calls = res.unresolvedReferences.filter((r) => r.referenceKind === 'calls');
expect(calls.map((c) => c.referenceName)).toContain('this.cache.get');
expect(calls.map((c) => c.referenceName)).not.toContain('get');
}
});

it('dynamic receiver extraction parity and silence (#1566/#647)', () => {
const src = `
function factory() { return { get: () => 1 }; }
export function dynamic(holder: any, key: string) {
holder[key].get("x");
factory().get("x");
}
export function staticChain(holder: any) {
holder.values.get("x");
}
export function storeCall(useStore: any) {
useStore.getState().reset();
}
`;
assertParity('fixtures/dynamic-parity.ts', src, 'typescript');
const wasmRes = extractFromSource('fixtures/dynamic-parity.ts', src, 'typescript');
const kernelRes = tryKernelExtract('fixtures/dynamic-parity.ts', src, 'typescript')!;
for (const res of [wasmRes, kernelRes]) {
const calls = res.unresolvedReferences.filter((r) => r.referenceKind === 'calls');
const names = calls.map((c) => c.referenceName);
expect(names).toContain('holder.values.get');
expect(names).toContain('factory().get');
expect(names).toContain('factory');
expect(names).toContain('useStore.getState().reset');
expect(names).not.toContain('get');
expect(names).not.toContain('reset');
expect(names.some((n) => n.includes('key]'))).toBe(false);
}
});

it('torture fixture (js): field methods, wrappers, vuex module shape', () => {
Expand Down
137 changes: 134 additions & 3 deletions __tests__/object-literal-methods.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,17 @@ describe('object-literal method extraction', () => {
expect(fnNames).toContain('switchOrganization');
expect(fnNames).toContain('reset');

// Each action's body was walked: fetchUser references its sibling `reset`,
// Each action's body was walked: fetchUser references its sibling `reset` via `get().reset`,
// so an in-store calls edge will resolve once the pipeline runs.
const fetchUser = result.nodes.find((n) => n.name === 'fetchUser')!;
const fetchUserRefs = result.unresolvedReferences.filter((r) => r.fromNodeId === fetchUser.id);
expect(fetchUserRefs.map((r) => r.referenceName)).toContain('reset');
expect(fetchUserRefs.map((r) => r.referenceName)).toContain('get().reset');

// The action's body wasn't mis-attributed to the file scope (the reason we
// skip the generic body-visit for the store-factory call).
const fileNode = result.nodes.find((n) => n.kind === 'file')!;
const fileRefs = result.unresolvedReferences.filter((r) => r.fromNodeId === fileNode.id);
expect(fileRefs.map((r) => r.referenceName)).not.toContain('reset');
expect(fileRefs.map((r) => r.referenceName)).not.toContain('get().reset');
});

it('extracts actions through a middleware wrapper (create(persist(...)))', () => {
Expand Down Expand Up @@ -173,4 +173,135 @@ describe('object-literal method resolution (end-to-end)', () => {

cg.close();
});

it('isolates store actions from top-level and sibling store decoys (#1566/#647)', async () => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-store-decoy-'));
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name":"t","dependencies":{"zustand":"^4"}}\n');
fs.writeFileSync(
path.join(tmpDir, 'store.ts'),
`import { create } from 'zustand'\n` +
`export function reset() { return 'top-level decoy'; }\n` +
`export const otherStore = create<any>(() => ({\n` +
` reset: () => {},\n` +
`}))\n` +
`export const useStore = create<any>((set, get) => ({\n` +
` fetchUser: async () => { get().reset() },\n` +
` reset: () => set({}),\n` +
`}))\n`
);
fs.writeFileSync(
path.join(tmpDir, 'caller.ts'),
`import { useStore } from './store'\n` +
`export function hardReset() {\n` +
` useStore.getState().reset()\n` +
`}\n`
);

const cg = CodeGraph.initSync(tmpDir);
await cg.indexAll();

const fns = cg.getNodesByKind('function');
const storeFns = fns.filter((n) => n.filePath.endsWith('store.ts'));
const topLevelReset = storeFns.find((n) => n.name === 'reset' && n.startLine === 2);
const otherStoreReset = storeFns.find((n) => n.name === 'reset' && n.startLine === 4);
const useStoreReset = storeFns.find((n) => n.name === 'reset' && n.startLine === 8);

expect(topLevelReset).toBeDefined();
expect(otherStoreReset).toBeDefined();
expect(useStoreReset).toBeDefined();

const useStoreResetCallers = cg.getCallers(useStoreReset!.id).map((c) => c.node.name);
expect(useStoreResetCallers).toContain('hardReset');
expect(useStoreResetCallers).toContain('fetchUser');

const topLevelCallers = cg.getCallers(topLevelReset!.id).map((c) => c.node.name);
expect(topLevelCallers).not.toContain('hardReset');
expect(topLevelCallers).not.toContain('fetchUser');

const otherStoreCallers = cg.getCallers(otherStoreReset!.id).map((c) => c.node.name);
expect(otherStoreCallers).not.toContain('hardReset');
expect(otherStoreCallers).not.toContain('fetchUser');

cg.close();
});

it('isolates declared factory inside store action from sibling actions (#1566/#647)', async () => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-store-factory-'));
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name":"t","dependencies":{"zustand":"^4"}}\n');
fs.writeFileSync(
path.join(tmpDir, 'store.ts'),
`import { create } from 'zustand'\n` +
`function factory() {\n` +
` return { reset() {} }\n` +
`}\n` +
`export const useStore = create<any>((set, get) => ({\n` +
` reset: () => set({}),\n` +
` run: () => {\n` +
` factory().reset()\n` +
` },\n` +
`}))\n`
);

const cg = CodeGraph.initSync(tmpDir);
await cg.indexAll();

const fns = cg.getNodesByKind('function');
const storeReset = fns.find((n) => n.name === 'reset' && n.startLine === 6);
const runFn = fns.find((n) => n.name === 'run');
const factoryFn = fns.find((n) => n.name === 'factory');

expect(storeReset).toBeDefined();
expect(runFn).toBeDefined();
expect(factoryFn).toBeDefined();

// run calls factory()
const factoryCallers = cg.getCallers(factoryFn!.id).map((c) => c.node.name);
expect(factoryCallers).toContain('run');

// run does NOT call useStore.reset
const storeResetCallers = cg.getCallers(storeReset!.id).map((c) => c.node.name);
expect(storeResetCallers).not.toContain('run');

cg.close();
});

it('isolates generic call-result receivers without store container (#1566/#647)', async () => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-generic-call-result-'));
fs.writeFileSync(
path.join(tmpDir, 'service.ts'),
`class Decoy {\n` +
` get() { return 1; }\n` +
` reset() {}\n` +
`}\n` +
`function factory() {\n` +
` return {};\n` +
`}\n` +
`export function useFactory() {\n` +
` factory().get();\n` +
`}\n`
);

const cg = CodeGraph.initSync(tmpDir);
await cg.indexAll();

const fns = cg.getNodesByKind('function');
const methods = cg.getNodesByKind('method');
const useFactory = fns.find((n) => n.name === 'useFactory');
const factory = fns.find((n) => n.name === 'factory');
const decoyGet = methods.find((n) => n.name === 'get');

expect(useFactory).toBeDefined();
expect(factory).toBeDefined();
expect(decoyGet).toBeDefined();

// useFactory calls factory()
const factoryCallers = cg.getCallers(factory!.id).map((c) => c.node.name);
expect(factoryCallers).toContain('useFactory');

// useFactory does NOT call Decoy.get
const decoyGetCallers = cg.getCallers(decoyGet!.id).map((c) => c.node.name);
expect(decoyGetCallers).not.toContain('useFactory');

cg.close();
});
});
Loading