fix(codegen): reject invalid indent options (#25807)

## Summary

 - throw a TypeError when indent is empty, contains characters other than spaces or tabs, or is not a string
 - preserve the existing default tab indentation and unchanged printIndent hot path
 - update API documentation and validation coverage
This commit is contained in:
camc314 committed 2026-08-17 14:22:56 +00:00
1 parent f6b22654d2
commit 4cc7ea4595
4 files changed
+31 -34

No files matched your search

+2 -1
View File
@@ -44,7 +44,8 @@ export interface Position {
export interface Options {
/**
* String to use for indentation, defaults to `"\t"`.
* Must consist only of spaces and/or tabs - anything else falls back to a tab.
* Must be a non-empty string consisting only of spaces and/or tabs.
* Throws a `TypeError` otherwise.
*/
indent?: string;
+8 -6
View File
@@ -28,10 +28,7 @@ let indentString = "\t";
*/
const indents = [""];
/**
* The `indent` option must be made up of only spaces and tabs, and not empty string.
* Anything else falls back to a tab.
*/
/** The `indent` option must be a non-empty string made up of only spaces and tabs. */
const INDENT_REGEX = /^[ \t]+$/;
/** Upper bound for the process-wide indentation cache. */
@@ -105,8 +102,13 @@ export class State {
// The `indent` option is validated here, not in the printer, and changing of it discards
// the cache grown for the old `indentString`.
// That should be rare - most users have an indent style they prefer, and use it consistently.
let { indent } = options;
if (typeof indent !== "string" || !INDENT_REGEX.test(indent)) indent = "\t";
const { indent: indentOption } = options;
let indent = indentOption;
if (indent === undefined) {
indent = "\t";
} else if (typeof indent !== "string" || !INDENT_REGEX.test(indent)) {
throw new TypeError("`indent` must be a non-empty string containing only spaces and tabs");
}
if (indent !== indentString) {
indentString = indent;
indents.length = 1;
+16
View File
@@ -107,6 +107,22 @@ function checkCases(cases: Case[]): void {
// --- Tests ----------------------------------------------------------------------------------
describe("indent", () => {
const ast = e(id("x"));
test.each(["", "x", "\n", "\r\n", " x "])("rejects %j", (indent) => {
expect(() => printSync(ast, { indent })).toThrowError(
new TypeError("`indent` must be a non-empty string containing only spaces and tabs"),
);
});
test.each([4, null, {}])("rejects non-string value %j", (indent) => {
expect(() => printSync(ast, { indent: indent as unknown as string })).toThrowError(
new TypeError("`indent` must be a non-empty string containing only spaces and tabs"),
);
});
});
describe("starting indent level", () => {
const ast = e(id("x"));
+5 -27
View File
@@ -329,31 +329,9 @@ for (const fixture of FIXTURES) {
// --- Indentation ----------------------------------------------------------------------------
// `printIndent` goes through the same write path as everything else, so the `indent` option and
// the mappings have to agree: indentation is honoured (or falls back to a tab), and no mapping
// ever points into the middle of an indent run.
//
// The invalid values are typed `unknown` because they are what an untyped JS caller can pass -
// that they fall back to a tab rather than corrupting the output is the point of testing them.
const INDENT_CASES: unknown[] = [
// Valid - used as given
undefined,
"\t",
" ",
" ",
"\t\t",
" \t",
"\t ",
// Invalid - must fall back to a tab
"",
"x",
"\n",
"\r\n",
" x ",
" ",
4,
null,
{},
];
// the mappings have to agree: indentation is honoured, and no mapping ever points into the middle
// of an indent run.
const INDENT_CASES = [undefined, "\t", " ", " ", "\t\t", " \t", "\t "];
describe("indent option", () => {
test.each(
@@ -362,11 +340,11 @@ describe("indent option", () => {
const program = parseWithLocs("indent.js", INLINE_JS);
const collector = new Collector("indent.js");
const out = printSync(program, {
indent: indent as string | undefined,
indent,
sourceMap: collector,
}).code;
const expectedIndent = typeof indent === "string" && /^[ \t]+$/.test(indent) ? indent : "\t";
const expectedIndent = indent ?? "\t";
const lines = out.split("\n");
const indented = lines.filter((line) => line.startsWith("\t") || line.startsWith(" "));
expect(indented.length).toBeGreaterThan(0);