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']);
});
});