Files
codegraph/__tests__/store-exported-later.test.ts
Colby Mchenry 5f5b8db620 fix(js,ts): find a store exported by an export { } list when its name starts or ends with $ (#2384)
TreeSitterExtractor.isExportedLater and its kernel mirror is_exported_later
bounded the name in the `export { ... }` branch with `\b`, which takes a `$`
for a separator. It never fires beside a leading or trailing `$`, so
`const items$ = create(...)` + `export { items$ };` (or `export { $items as
default };` for a `$items` store) never matched, and the store's actions
were not extracted on either engine. It also matched inside a longer name:
`export { useStore$ }` or `export { $useStore }` counted as exporting a
store named `useStore`.

The clause branch now bounds the name with a character that can't continue
an identifier. The regex crate has no look-around, so the bounds consume,
and both sides spell the class in ASCII, since the crate's `\w` is Unicode
while JS's is ASCII. Plain names decide as before apart from the two false
positives. A non-ASCII letter written against the name (`export {
éuseStore }`) used to split the engines (the kernel's Unicode `\b` said not
exported, the wasm path exported); both now give the wasm answer.

Adds kernel/wasm parity cases (ts/tsx/js/jsx) inside 'a store exported by a
later statement', and the same cases in store-exported-later.test.ts, for
`items$`, `$items as default` and both false-positive shapes. They fail with
the unfixed kernel and with the unfixed TS on the wasm path, and pass with
both fixes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 15:31:37 +00:00

82 lines
2.7 KiB
TypeScript

/**
* A store exported by a LATER statement — `const useStore = create(…)` then
* `export default useStore` — is exported, and its actions are extracted like
* an `export const` store's (object-literal-methods.test.ts covers that
* form). The scope rule that keeps inline-object noise out still holds: a
* store nothing exports stays a constant.
*/
import { describe, it, expect, beforeAll } from 'vitest';
import { extractFromSource } from '../src/extraction';
import { initGrammars, loadAllGrammars } from '../src/extraction/grammars';
beforeAll(async () => {
await initGrammars();
await loadAllGrammars();
});
const fnNames = (code: string, file = 'store.ts') =>
extractFromSource(file, code)
.nodes.filter((n) => n.kind === 'function')
.map((n) => n.name);
describe('store actions on a later-exported const', () => {
it('export default NAME', () => {
const code = `
import { create } from 'zustand'
const useCaptureStorage = create<State>((set, get) => ({
object: null,
setSettings: (settings: Settings) => {
set({ settings })
},
reset: () => set({ object: null }),
}))
export default useCaptureStorage
`;
expect(fnNames(code)).toEqual(expect.arrayContaining(['setSettings', 'reset']));
});
it('export { NAME } and export { NAME as default }', () => {
const named = `
const useStore = create((set) => ({ bump: () => set({}) }))
export { useStore }
`;
const asDefault = `
const useStore = create((set) => ({ bump: () => set({}) }))
export { useStore as default }
`;
expect(fnNames(named)).toContain('bump');
expect(fnNames(asDefault)).toContain('bump');
});
it.each([
['items$', 'export { items$ };', true],
['$items', 'export { $items as default };', true],
['items', 'export { items$ };', false],
['items', 'export { $items };', false],
] as const)('an export clause names %s only whole: `%s`', (name, exportLine, kept) => {
// A `$` belongs to the name: `items$` is exported, `items` is not.
const code = `
const ${name} = create((set) => ({ inc: () => set({}) }))
${exportLine}
`;
expect(fnNames(code).includes('inc')).toBe(kept);
});
it('a const nothing exports keeps its members out of the graph', () => {
const code = `
const useStore = create((set) => ({ bump: () => set({}) }))
export const other = 1
`;
expect(fnNames(code)).not.toContain('bump');
});
it('is not fooled by a different name in the export', () => {
const code = `
const useStoreInternal = create((set) => ({ bump: () => set({}) }))
const useStore = 1
export default useStore
`;
expect(fnNames(code)).not.toContain('bump');
});
});