import * as ts from 'typescript'; /** * #650 follow-up: an AST-based scanner for every `pageSize` property that appears inside a real * object LITERAL expression. Extracted into its own module so both the enumerating guard * (`pageSizeCallSites.guard.test.ts`, which scans the real repo) and a fixture test * (`pageSizeScan.test.ts`, which scans synthetic source strings and does NOT touch the repo) can * exercise the exact same scanning logic. * * A prior hand-rolled regex/bracket-tracking version of this scan was replaced after a review * found it defeated by comments-in-strings, template-literal interpolations, ternary/`??` * contexts, JSX containers, and same-line duplicates — each a DIFFERENT input class a text-level * lexer has to special-case one at a time. The TypeScript compiler API sidesteps the whole * category: comments and string/template CONTENTS are trivia/literal text the parser never * revisits as code, and a real object-literal expression (`ObjectLiteralExpression`) is a * structurally different AST node from a type literal (`type X = { pageSize: number }`, * `PropertySignature` inside a `TypeLiteralNode`/`InterfaceDeclaration`) or a destructuring * pattern (`ObjectBindingPattern`, e.g. `function f({ pageSize }) {}` or * `const { pageSize } = x`) — so those are excluded by NODE KIND, not by a preceding-character * heuristic that can be fooled by an unrelated `{`/`(`/`,`. */ export interface PageSizeSite { /** 1-based source line of the `pageSize` property (name), matching editor line numbers. */ line: number; /** 1-based source column of the `pageSize` property (name). */ column: number; kind: 'literal' | 'shorthand'; /** * For `kind: 'literal'`: the numeric-literal text or the referenced const identifier's name. * For `kind: 'shorthand'`: always the literal string `'pageSize'` (the shorthand form only ever * forwards whatever `pageSize` binding is in scope — there is no separate "value" to name). */ value: string; } function scriptKindFor(fileName: string): ts.ScriptKind { return fileName.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS; } export function scanPageSizeSites(sourceText: string, fileName: string): PageSizeSite[] { const sourceFile = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, true, scriptKindFor(fileName)); const sites: PageSizeSite[] = []; function positionOf(node: ts.Node): { line: number; column: number } { const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)); return { line: line + 1, column: character + 1 }; } function visit(node: ts.Node): void { if (ts.isObjectLiteralExpression(node)) { for (const property of node.properties) { if (ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && property.name.text === 'pageSize') { const initializer = property.initializer; // Only a numeric literal or a bare identifier (a const/variable reference) counts as a // fixed value baked into THIS call site. A forwarded expression — `String(pageSize)`, a // ternary, a template, a function call — is a dynamic passthrough of whatever the // caller supplied, not a literal this site chose; it is deliberately not recorded here // (see the module doc comment on `loadAllPages`/positional-argument residual gaps). if (ts.isNumericLiteral(initializer)) { const { line, column } = positionOf(property.name); sites.push({ line, column, kind: 'literal', value: initializer.text }); } else if (ts.isIdentifier(initializer)) { const { line, column } = positionOf(property.name); sites.push({ line, column, kind: 'literal', value: initializer.text }); } } else if (ts.isShorthandPropertyAssignment(property) && property.name.text === 'pageSize') { const { line, column } = positionOf(property.name); sites.push({ line, column, kind: 'shorthand', value: 'pageSize' }); } } } ts.forEachChild(node, visit); } visit(sourceFile); return sites.sort((a, b) => (a.line === b.line ? a.column - b.column : a.line - b.line)); } export function pageSizeSiteId(site: { line: number; column: number; kind: string; value: string }): string { return `${site.line}:${site.column}:${site.kind}:${site.value}`; }