import { describe, expect, it } from 'vitest'; import { pageSizeSiteId, scanPageSizeSites, type PageSizeSite } from './pageSizeScan'; /** * Fixture test for `scanPageSizeSites` itself — NOT a scan of the real repo (that's * `pageSizeCallSites.guard.test.ts`). This is what actually protects the SCANNER going forward: * a prior hand-rolled regex/bracket-tracking version passed the guard test against unmodified * source at both #650 commits while still being defeated by every case below, because the guard * only ever exercised today's snapshot of real call sites — it never proved the scanner handles * the INPUT CLASSES that expose a text-level scanner's blind spots. Pinning the exact discovered * set against synthetic source strings closes that gap. * * Not every fixture here is a REGRESSION pin against the prior (round-1, `b90f8a3b`) bracket- * tracking scanner — a round-3 review found that round 1's simple `pageSize:\s*value` regex * already handled a bare URL-string or a bare `??` context correctly on its own (a `//` inside a * string, or the token immediately before `{`, only mattered to round 1's OWN heuristics, not to * a plain regex match). Those two are labelled CONTRACT fixtures below — they pin the documented * behavior going forward, not a fix. The fixtures that genuinely fail against round 1 (verified) * are: the string CONTAINING the literal text `pageSize: 100`, the template-literal * interpolation, the same-line ternary identity/multiplicity, the JSX shorthand container, * parameter destructuring, nested destructuring, and the type-literal declaration — plus the * combined multi-case fixture, which fails round 1 for several of those reasons at once. */ function ids(sites: PageSizeSite[]): string[] { return sites.map(pageSizeSiteId); } describe('scanPageSizeSites', () => { it('finds a literal pageSize: property in a plain object-literal call argument', () => { const source = `getFoo({ pageSize: 100, query });\n`; const sites = scanPageSizeSites(source, 'fixture.ts'); expect(ids(sites)).toEqual(['1:10:literal:100']); }); it('finds the ES6 shorthand pageSize property in a plain object-literal call argument', () => { const source = `getFoo({ pageSize, query });\n`; const sites = scanPageSizeSites(source, 'fixture.ts'); expect(ids(sites)).toEqual(['1:10:shorthand:pageSize']); }); it('is NOT fooled by a "//" inside a string literal — CONTRACT fixture, not a round-1 regression pin (M-3)', () => { // NOTE: round 1's unconditional literal regex (`pageSize:\s*(\d+|identifier)`) already // matched this exact input correctly on its own — a `//` inside a string never confused THAT // narrower pattern. This pins the AST scanner's documented contract going forward; it is the // COMBINED multi-case fixture below (and the M-3-shaped case buried inside it — a literal // `//` immediately preceding a real call site on the SAME conceptual scan) that actually // fails against round 1's comment-stripping step, not this input in isolation. const source = [`const endpoint = 'https://example.test';`, `getFoo({ pageSize: 100 });`, ''].join('\n'); const sites = scanPageSizeSites(source, 'fixture.ts'); expect(ids(sites)).toEqual(['2:10:literal:100']); }); it('does NOT match a string literal that merely CONTAINS the text "pageSize: 100" (L-7)', () => { const source = `const label = "pageSize: 100";\n`; expect(scanPageSizeSites(source, 'fixture.ts')).toEqual([]); }); it('finds an object literal passed inside a template-literal interpolation (M-5)', () => { const source = 'const url = `${await getFoo({ pageSize })}`;\n'; const sites = scanPageSizeSites(source, 'fixture.ts'); expect(ids(sites)).toEqual(['1:31:shorthand:pageSize']); }); it('finds an object literal in each branch of a ternary, even on the SAME line (M-4, M-6)', () => { const source = `return ok ? getA({ pageSize }) : getB({ pageSize });\n`; const sites = scanPageSizeSites(source, 'fixture.ts'); // Two distinct occurrences on one line get distinct identities (different columns) — a // single registry entry cannot silently cover both. expect(ids(sites)).toEqual(['1:20:shorthand:pageSize', '1:41:shorthand:pageSize']); expect(sites[0].column).not.toBe(sites[1].column); }); it('finds an object literal on the right-hand side of ?? — CONTRACT fixture, not a round-1 regression pin (M-4)', () => { // NOTE: like the URL fixture above, round 1's literal-form regex already matched this exact // `pageSize: 50` text correctly on its own — `??` doesn't change what characters precede the // match on the line. This pins the documented contract, not a round-1 regression. const source = `getFoo(options ?? { pageSize: 50 });\n`; const sites = scanPageSizeSites(source, 'fixture.ts'); expect(ids(sites)).toEqual(['1:21:literal:50']); }); it('finds an object literal inside a JSX expression container attribute (M-4)', () => { const source = `const el = ;\n`; const sites = scanPageSizeSites(source, 'fixture.tsx'); expect(ids(sites)).toEqual(['1:34:shorthand:pageSize']); }); it('does NOT match a parameter destructuring pattern (L-7)', () => { const source = `function f({ pageSize }: { pageSize: number }) {}\n`; // The destructured PARAMETER `{ pageSize }` is an ObjectBindingPattern, not an // ObjectLiteralExpression — excluded by node kind. Its TYPE annotation `{ pageSize: number }` // is a TypeLiteral (PropertySignature), also excluded by node kind — never an object literal. expect(scanPageSizeSites(source, 'fixture.ts')).toEqual([]); }); it('does NOT match a nested destructuring pattern (L-7)', () => { const source = `const { nested: { pageSize } } = input;\n`; expect(scanPageSizeSites(source, 'fixture.ts')).toEqual([]); }); it('does NOT match a type-literal declaration (L-7)', () => { const source = `type P = { pageSize: 100 };\n`; expect(scanPageSizeSites(source, 'fixture.ts')).toEqual([]); }); it('does NOT match an interface property declaration', () => { const source = `interface Params {\n pageSize?: number;\n}\n`; expect(scanPageSizeSites(source, 'fixture.ts')).toEqual([]); }); it('does NOT match a forwarded call expression (a dynamic passthrough, not a fixed value)', () => { const source = `getFoo({ pageSize: String(pageSize) });\n`; expect(scanPageSizeSites(source, 'fixture.ts')).toEqual([]); }); it('is not confused by a pageSize reference inside a comment', () => { const source = [`// pageSize: 999 — this is just prose, not code`, `getFoo({ query });`, ''].join('\n'); expect(scanPageSizeSites(source, 'fixture.ts')).toEqual([]); }); it('is not confused by a pageSize reference inside a block/JSDoc comment', () => { const source = ['/**', ' * Uses `pageSize` under the hood — see also `{ pageSize: 100 }`.', ' */', 'getFoo({ query });', ''].join( '\n' ); expect(scanPageSizeSites(source, 'fixture.ts')).toEqual([]); }); it('does NOT match a React dependency array containing pageSize', () => { const source = `useCallback(load, [pageNum, pageSize, sortField]);\n`; expect(scanPageSizeSites(source, 'fixture.ts')).toEqual([]); }); it('finds a const-identifier literal value (not just a numeric literal)', () => { const source = `getFoo({ pageSize: LIBRARY_BROWSE_PAGE_CAP });\n`; const sites = scanPageSizeSites(source, 'fixture.ts'); expect(ids(sites)).toEqual(['1:10:literal:LIBRARY_BROWSE_PAGE_CAP']); }); it('covers every case above together in one multi-line fixture and pins the exact discovered set', () => { const source = [ `const endpoint = 'https://example.test';`, // M-3: not a comment `const label = "pageSize: 100";`, // L-7: string contents, not code `// pageSize: 999 in a line comment`, // not code `/** block comment mentioning \`pageSize\` */`, // not code `type P = { pageSize: 100 };`, // L-7: type literal, not a value `interface Q { pageSize?: number; }`, // not a value `function f({ pageSize }: { pageSize: number }) {}`, // L-7: destructuring + its type `const { nested: { pageSize } } = input;`, // L-7: nested destructuring `useCallback(load, [pageNum, pageSize]);`, // dependency array, not an object literal `getFoo({ pageSize: String(pageSize) });`, // forwarded call, not a fixed value `getFoo({ pageSize: 100 });`, // REAL: literal `getBar({ pageSize });`, // REAL: shorthand `getBaz(options ?? { pageSize: 50 });`, // REAL: ?? context (M-4) `const url = \`\${await getQux({ pageSize })}\`;`, // REAL: template interpolation (M-5) `return ok ? getA({ pageSize }) : getB({ pageSize });` // REAL x2: ternary, same line (M-4/M-6) ].join('\n'); const sites = scanPageSizeSites(source, 'fixture.ts'); expect(ids(sites)).toEqual([ '11:10:literal:100', '12:10:shorthand:pageSize', '13:21:literal:50', '14:31:shorthand:pageSize', '15:20:shorthand:pageSize', '15:41:shorthand:pageSize' ]); // Anti-vacuity: the fixture packs in 10 non-matching traps ahead of the 6 real sites — a // scanner that matched everything (or nothing) would fail this count, not just the ids above. expect(sites.length).toBe(6); }); it('scans .tsx source using the TSX script kind (JSX does not parse under plain .ts rules)', () => { const source = `export function C() {\n return
;\n}\n`; const sites = scanPageSizeSites(source, 'fixture.tsx'); expect(ids(sites)).toEqual(['2:23:literal:10']); }); // ---- round-3 MEDIUM finding: transparent TS wrappers around the initializer ----------------- it('finds a literal wrapped in "as const" (transparent to the runtime value)', () => { const source = `getFoo({ pageSize: 100 as const });\n`; const sites = scanPageSizeSites(source, 'fixture.ts'); expect(ids(sites)).toEqual(['1:10:literal:100']); }); it('finds a literal wrapped in "satisfies number" (transparent to the runtime value)', () => { const source = `getFoo({ pageSize: 100 satisfies number });\n`; const sites = scanPageSizeSites(source, 'fixture.ts'); expect(ids(sites)).toEqual(['1:10:literal:100']); }); it('finds a parenthesized literal', () => { const source = `getFoo({ pageSize: (100) });\n`; const sites = scanPageSizeSites(source, 'fixture.ts'); expect(ids(sites)).toEqual(['1:10:literal:100']); }); it('finds a const identifier through a chain of "as"/"satisfies"/parens wrappers', () => { const source = `getFoo({ pageSize: ((LIBRARY_BROWSE_PAGE_CAP as number) satisfies number) });\n`; const sites = scanPageSizeSites(source, 'fixture.ts'); expect(ids(sites)).toEqual(['1:10:literal:LIBRARY_BROWSE_PAGE_CAP']); }); it('still rejects a forwarded call expression even when wrapped in "as"', () => { const source = `getFoo({ pageSize: String(pageSize) as string });\n`; expect(scanPageSizeSites(source, 'fixture.ts')).toEqual([]); }); // ---- round-3 MEDIUM finding: non-Identifier property names ----------------------------------- it('finds a quoted string property key ("pageSize": 100)', () => { const source = `getFoo({ 'pageSize': 100 });\n`; const sites = scanPageSizeSites(source, 'fixture.ts'); expect(ids(sites)).toEqual(['1:10:literal:100']); }); it('finds a statically-resolvable computed property key (["pageSize"]: 100)', () => { const source = `getFoo({ ['pageSize']: 100 });\n`; const sites = scanPageSizeSites(source, 'fixture.ts'); expect(ids(sites)).toEqual(['1:10:literal:100']); }); it('does NOT match a computed property key that cannot be resolved statically', () => { const source = `const key = getKey();\ngetFoo({ [key]: 100 });\n`; expect(scanPageSizeSites(source, 'fixture.ts')).toEqual([]); }); it('does NOT match a quoted string key for a DIFFERENT property name', () => { const source = `getFoo({ 'pageSizeLimit': 100 });\n`; expect(scanPageSizeSites(source, 'fixture.ts')).toEqual([]); }); // ---- round-3 MEDIUM finding: .mts/.cts are never silently skipped ----------------------------- it('scans .mts source (parses as plain TS, no JSX grammar)', () => { const source = `export function loadPage() {\n return getFoo({ pageSize: 100 });\n}\n`; const sites = scanPageSizeSites(source, 'fixture.mts'); expect(ids(sites)).toEqual(['2:19:literal:100']); }); it('scans .cts source (parses as plain TS, no JSX grammar)', () => { const source = `getFoo({ pageSize });\n`; const sites = scanPageSizeSites(source, 'fixture.cts'); expect(ids(sites)).toEqual(['1:10:shorthand:pageSize']); }); });