fix(codegen): validate starting indent level (#25550)

## Summary

Validate `startingIndentLevel` before printing to prevent unbounded indentation-cache growth.

- Reject non-finite, fractional, negative, and unsafe integer values with a `RangeError`.
- Document the option as a non-negative integer.
This commit is contained in:
camc314 committed 2026-08-12 15:46:26 +00:00
1 parent b2f8fdf230
commit 59e8895162
4 files changed
+42 -4

No files matched your search

+1 -1
View File
@@ -47,7 +47,7 @@ Returns a string containing the code for the AST `node`.
| Option | Type | Default | Description |
| :-------------------- | :------------------- | :------ | :---------------------------------------------- |
| `indent` | `string` | `"\t"` | Indentation - spaces and/or tabs only |
| `startingIndentLevel` | `number` | `0` | Indent level to start from |
| `startingIndentLevel` | `number` | `0` | Starting indent level, from `0` to `1000` |
| `jsx` | `boolean` | `false` | `.tsx` mode - lone type parameters print `<T,>` |
| `ts` | `boolean` | `false` | AST may contain TypeScript syntax |
| `sourceMap` | `SourceMapGenerator` | - | If present, source mappings are emitted into it |
+1 -1
View File
@@ -49,7 +49,7 @@ export interface Options {
indent?: string;
/**
* Indent level to start from, defaults to `0`.
* Non-negative integer indent level to start from, from `0` to `1000`. Defaults to `0`.
*/
startingIndentLevel?: number;
+15 -2
View File
@@ -34,6 +34,9 @@ const indents = [""];
*/
const INDENT_REGEX = /^[ \t]+$/;
/** Upper bound for the process-wide indentation cache. */
const MAX_STARTING_INDENT_LEVEL = 1_000;
export class State {
// Current output.
// A string which is appended to as the printing process proceeds.
@@ -85,8 +88,18 @@ export class State {
constructor(options: Options) {
this.output = "";
let indentLevel = options.startingIndentLevel;
if (typeof indentLevel !== "number") indentLevel = 0;
let { startingIndentLevel: indentLevel } = options;
if (indentLevel === undefined) {
indentLevel = 0;
} else if (
!Number.isSafeInteger(indentLevel) ||
indentLevel < 0 ||
indentLevel > MAX_STARTING_INDENT_LEVEL
) {
throw new RangeError(
"`startingIndentLevel` must be a non-negative safe integer no greater than 1000",
);
}
this.indentLevel = indentLevel;
// The `indent` option is validated here, not in the printer, and changing of it discards
+25
View File
@@ -107,6 +107,31 @@ function checkCases(cases: Case[]): void {
// --- Tests ----------------------------------------------------------------------------------
describe("starting indent level", () => {
const ast = e(id("x"));
test("indents from a valid level", () => {
expect(printSync(ast, { startingIndentLevel: 1 })).toBe("\tx;\n");
});
test("accepts the maximum level", () => {
expect(printSync(ast, { startingIndentLevel: 1000 })).toBe(`${"\t".repeat(1000)}x;\n`);
});
test.each([
["infinity", Infinity],
["negative infinity", -Infinity],
["not a number", NaN],
["fraction", 0.5],
["negative", -1],
["above the maximum", 1001],
])("rejects %s", (_name, startingIndentLevel) => {
expect(() => printSync(ast, { startingIndentLevel })).toThrow(
"`startingIndentLevel` must be a non-negative safe integer no greater than 1000",
);
});
});
// A numeric literal followed by `.` needs a space when the number is plain digits, because
// `0.toExponential()` would lex the `.` as part of the number. This is the `needSpaceBeforeDot`
// path, and every case below is unreachable from a parsed fixture in at least one respect.