Files
codegraph/__tests__/destructured-call-result.test.ts
Colby MchenryandClaude Opus 5.5 cf449fdad7 fix(resolution): a name destructured from a composable reaches what it returns (#2163)
`const { getDefaultActivityRoute } = useDefaultActivity(); getDefaultActivityRoute()`
bound the name locally, which ruled out every cross-file candidate, so the
call resolved to nothing. A bare JS-family call through a name destructured
(`{ key: alias }` included) from a call's result now resolves the callee —
through the file's imports, else the same file, else the project's one
function of that name (Nuxt's `~/` alias) — checks its source returns the
key, and takes the function declared in its body, else the module-level one
it returns. A later declaration at the call's scope shadows the binding.
Returned object literals now count as exporting their names for #2162's
unexported-binding rule.

A/B edges added (removed): mealie +176 (-4), excalidraw +156 (`const { t } =
useI18n()`), halo +58, kit +34 (-14, `validate` now the validator's own),
elk +19; trpc byte-identical.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:15:50 +00:00

71 lines
2.5 KiB
TypeScript

/**
* A call through a name destructured from a call's result — a composable or
* a custom hook — reaches the function that callee returns under that key:
* `const { getDefaultActivityRoute } = useDefaultActivity()` (mealie, where the
* function is a module-level one the composable returns), `const { t } =
* useI18n()`, `const { login } = useAuth()` (declared in the hook's body).
* The local binding used to rule out every cross-file candidate.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-destructured-'));
const files: Record<string, string> = {
'package.json': JSON.stringify({ name: 'app', dependencies: { vue: '^3' } }),
'src/composables/use-default-activity.ts': `function getDefaultActivityRoute(key?: string): string {
return key ?? '/';
}
export default function useDefaultActivity() {
return { getDefaultActivityRoute };
}
`,
'src/hooks/useAuth.ts': `export function useAuth() {
function login(user: string) {
return user;
}
const logout = () => null;
return { login, signOut: logout };
}
`,
'src/pages/index.ts': `import useDefaultActivity from '../composables/use-default-activity';
import { useAuth } from '../hooks/useAuth';
export function go() {
const { getDefaultActivityRoute } = useDefaultActivity();
const { login, signOut: leave } = useAuth();
login('ada');
leave();
return getDefaultActivityRoute('x');
}
`,
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
describe('names destructured from a call', () => {
it('reach what the callee returns under that key', () => {
const go = cg.getNodesInFile('src/pages/index.ts').find((n) => n.name === 'go')!;
const targets = cg
.getOutgoingEdges(go.id)
.filter((e) => e.kind === 'calls')
.map((e) => `${cg.getNode(e.target)!.filePath}:${cg.getNode(e.target)!.name}`);
expect(targets).toContain('src/composables/use-default-activity.ts:getDefaultActivityRoute');
expect(targets).toContain('src/hooks/useAuth.ts:login');
});
});