Files
oxc-project__oxc/packages/codegen/src-js/print/literal.ts
overlookmotel d50c985fc2 refactor(packages/codegen): split categories into separate file (#26083)
Pure refactor. Move the category consts (`CAT_IDENT` etc) into a separate file.

2 reasons:

1. `write.ts` had got too large.
2. `index.ts`, which is not part of printer builds, imports `CAT_OTHER`. Importing it from a file containing only consts avoids any problems with minifier not being able to recognise dead code and remove it.
2026-08-25 13:46:38 +00:00

207 lines
7.6 KiB
TypeScript

// Literals.
import { typeAssertIs } from "../asserts.ts";
import {
CAT_CLOSE_BRACKET,
CAT_IDENT,
CAT_INT_DIGIT,
CAT_OP_UN_NEG,
CAT_OTHER,
CAT_REGEX_SLASH,
} from "./categories.ts";
import { write, writeIdent, writeNoLast, writeWithMap, writeWithMapNoLast } from "./write.ts";
import { printSpaceBeforeIdentifier, printSpaceBeforeOperator } from "./space.ts";
import { printNonNegativeFloat } from "./number.ts";
import { CTX_TYPESCRIPT } from "./operators.ts";
import { PREC_PREFIX } from "./precedence.ts";
import { printString } from "./string.ts";
import type { State } from "../state.ts";
import type { LiteralExtras } from "./types.ts";
import type * as ESTree from "../../../../npm/oxc-types/types.d.ts";
/**
* The literal types, which all share `type: "Literal"` and are told apart by `typeof value`.
*/
type LiteralNode =
| ESTree.BooleanLiteral
| ESTree.NullLiteral
| ESTree.NumericLiteral
| ESTree.BigIntLiteral
| ESTree.RegExpLiteral
| ESTree.StringLiteral;
/**
* Print any ESTree `Literal`, which is one node type covering strings, numbers, booleans, `null`,
* regexes and bigints.
*
* The kind is told apart by the type of `value` and by which extra property is present,
* since the node itself does not say.
*/
export function printLiteral(
node: LiteralNode,
state: State,
precedence: number,
ctx: number,
): void {
// Ordered for the hot path - `value` is a `RegExp` / `null` for regexes and a `BigInt` / `null` for bigints,
// so a string or number `value` identifies a plain string/numeric literal without touching `regex` / `bigint` first.
const { value } = node;
switch (typeof value) {
case "string":
printString(state, value, node.start, node.end, node);
break;
case "number":
typeAssertIs<ESTree.NumericLiteral>(node);
printNumericLiteral(node, state, precedence, ctx);
break;
case "boolean":
printSpaceBeforeIdentifier(state);
writeWithMap(state, value ? "true" : "false", CAT_IDENT, node.start, node.end, node);
break;
default:
typeAssertIs<LiteralExtras>(node);
if (node.regex != null) {
typeAssertIs<ESTree.RegExpLiteral>(node);
printRegExpLiteral(node, state);
} else if (node.bigint != null) {
typeAssertIs<ESTree.BigIntLiteral>(node);
printBigIntLiteral(node, state, precedence);
} else {
// `null`
printSpaceBeforeIdentifier(state);
writeWithMap(state, "null", CAT_IDENT, node.start, node.end, node);
}
break;
}
}
/**
* Print a numeric literal, parenthesizing a negative one where the position needs it.
*
* A negative number is a unary minus applied to a positive literal, so it needs a space
* or parentheses in the places any other unary minus would, and `-0` has to keep its sign.
*
* In TypeScript positions the source text is printed back rather than the shortest form,
* so a type keeps the digits it was written with.
*/
function printNumericLiteral(
node: ESTree.NumericLiteral,
state: State,
precedence: number,
ctx: number,
): void {
const { value } = node;
if ((ctx & CTX_TYPESCRIPT) !== 0 && node.raw != null) {
// A number's raw text can only end 3 ways: a digit, a hex letter, or a trailing `.` as in `1.`.
// The first two are identifier characters and the third is not, so testing for the `.` decides it -
// no character classification needed.
// `CAT_IDENT` rather than `CAT_INT_DIGIT` - raw text only prints inside a TS type,
// where no member `.` can follow, so there is no `0 .toExponential()` hazard to separate.
const { raw } = node;
writeWithMap(
state,
raw,
raw[raw.length - 1] === "." ? CAT_OTHER : CAT_IDENT,
node.start,
node.end,
node,
);
return;
}
if (value > 0 && value < Infinity) {
// Finite and positive: the common case, and the same branch the chain
// below would reach, without the two builtin calls.
printSpaceBeforeIdentifier(state);
printNonNegativeFloat(state, value, node.start, node.end, node);
} else if (Number.isNaN(value)) {
printSpaceBeforeIdentifier(state);
writeWithMap(state, "NaN", CAT_IDENT, node.start, node.end, node);
} else if (!Number.isFinite(value)) {
const negative = value < 0;
const wrap = negative && precedence >= PREC_PREFIX;
if (wrap) write(state, "(", CAT_OTHER);
if (negative) {
printSpaceBeforeOperator(state, CAT_OP_UN_NEG);
writeNoLast(state, "-");
} else {
printSpaceBeforeIdentifier(state);
}
writeWithMap(state, "Infinity", CAT_IDENT, node.start, node.end, node);
if (wrap) write(state, ")", CAT_CLOSE_BRACKET);
} else if (Object.is(value, 0)) {
// +0 exactly - `Object.is` rejects `-0`, which the negative paths below print as `-0`
printSpaceBeforeIdentifier(state);
writeWithMap(state, "0", CAT_INT_DIGIT, node.start, node.end, node);
} else if (precedence >= PREC_PREFIX) {
writeNoLast(state, "(-");
printNonNegativeFloat(state, -value, node.start, node.end, node);
write(state, ")", CAT_CLOSE_BRACKET);
} else {
printSpaceBeforeOperator(state, CAT_OP_UN_NEG);
writeNoLast(state, "-");
printNonNegativeFloat(state, -value, node.start, node.end, node);
}
}
/**
* Print a regex literal from its pattern and flags.
*
* A regex with no flags ends in `/`, which is recorded as its own category, because a `/`
* immediately after it would open a comment.
*/
function printRegExpLiteral(node: ESTree.RegExpLiteral, state: State): void {
// Neither of the separating spaces below can be needed in pretty mode, so the check is not made.
// Both guard against a regex being written immediately after something, and nothing can be
// immediately before a regex here: every operator which could put one after a `/` or a `<` is
// written space padded, so `last` is always the space.
//
// A minified mode would stop padding operators and make both reachable again, so this code would
// need to be restored. `CAT_REGEX_SLASH` is left in place for that - the code is still written
// after a flagless regex, and it still sits in the range `printSpaceBeforeIdentifier` tests,
// which costs nothing and is what keeps this a 4 line restoration.
//
// debugAssertLastFresh(state);
// const { last } = state;
// if (last === CAT_REGEX_SLASH || (last === CAT_LT && /^script/i.test(pattern.slice(0, 6)))) {
// write(state, " ", CAT_OTHER);
// }
//
// * `last === CAT_REGEX_SLASH` keeps `/a//b/` from lexing as a line comment
// * `CAT_LT` arm keeps `<` followed by `/script...` from closing a host `<script>` element.
writeWithMapNoLast(state, "/", node.start, node.end, node);
writeNoLast(state, node.regex.pattern);
// `CAT_REGEX_SLASH` rather than `CAT_OTHER`. It means "a regex just closed", which is what the
// commented-out check above would read. With flags, the flags are what `last` describes instead.
const { flags } = node.regex;
if (flags === "") {
write(state, "/", CAT_REGEX_SLASH);
} else {
writeNoLast(state, "/");
writeIdent(state, flags);
}
}
/**
* Print a bigint literal, parenthesizing a negative one where the position needs it,
* same as `printNumericLiteral` does.
*/
function printBigIntLiteral(node: ESTree.BigIntLiteral, state: State, precedence: number): void {
printSpaceBeforeIdentifier(state);
const value = node.bigint;
if (value.startsWith("-") && precedence >= PREC_PREFIX) {
writeWithMapNoLast(state, "(", node.start, node.end, node);
writeNoLast(state, value);
write(state, "n)", CAT_CLOSE_BRACKET);
} else {
writeWithMapNoLast(state, value, node.start, node.end, node);
writeIdent(state, "n");
}
}