mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
f91b7739a3
Pure refactor. Re-order `write*` functions, so `*NoLast` versions are grouped together.
408 lines
16 KiB
TypeScript
408 lines
16 KiB
TypeScript
// Writing to the output, and the categories which describe what was written last.
|
|
//
|
|
// These live together because the categories exist only to be stored by `write` -
|
|
// `state.last` records what was written last by category, not the last character itself.
|
|
|
|
import { debugAssert, typeAssertIs } from "../asserts.ts";
|
|
|
|
import type { MappableNode } from "./types.ts";
|
|
import type { State } from "../state.ts";
|
|
|
|
// `state.last` records what was written last, by category, not the last character itself.
|
|
//
|
|
// Reading last character of `state.output` is disastrous for performance. `state.output` is a "cons string",
|
|
// a concatenation of many smaller strings. Reading any character of it causes the string to be flattened,
|
|
// which is costly. Given how often it's required to know what was printed last, this produces a 4x slow-down.
|
|
//
|
|
// No reader needs the character - only its category - so the category is defined statically in `write` calls
|
|
// and stored as a small integer, which costs no write barrier to store and one compare to test.
|
|
//
|
|
// Three properties of the layout are load bearing, so keep them if you add a code:
|
|
//
|
|
// 1. `CAT_IDENT` through `CAT_REGEX_SLASH` are the classes needing a space before a following identifier,
|
|
// and they are the lowest codes, so `printSpaceBeforeIdentifier` is one compare against `CAT_REGEX_SLASH`.
|
|
// The unary and update operators are likewise contiguous, and the highest codes, so `printSpaceBeforeOperator`
|
|
// is one compare too.
|
|
//
|
|
// 2. The `CAT_START_OF_*` codes say where the output has reached, not what was written last, and they sit between
|
|
// those two ranges. Both space checks therefore read them as "nothing to separate", which is what the `CAT_OTHER`
|
|
// they displace meant - every position one of them marks follows whitespace or the start of the output.
|
|
// See `state.ts` for what they are for.
|
|
//
|
|
// 3. `CAT_START_OF_STMT` is odd, with the other two marks either side of it.
|
|
// Each of the two pairs a reader asks about is then one `|`-and-compare instead of two compares.
|
|
//
|
|
// The whole numbering:
|
|
//
|
|
// Group 0 to 2: A following identifier needs a space (`printSpaceBeforeIdentifier` checks `last <= CAT_REGEX_SLASH`)
|
|
// 0 CAT_IDENT Identifier part - letters, digits, `_`, `$`, ID_Continue
|
|
// 1 CAT_INT_DIGIT Numeric literal of plain digits (`0 .toExponential()`)
|
|
// 2 CAT_REGEX_SLASH Regex closed with no flags
|
|
//
|
|
// Group 3 to 9: Checked individually
|
|
// 3 CAT_OTHER Anything else not covered by another category - punctuation, whitespace
|
|
// 4 CAT_LT `<`
|
|
// 5 CAT_QUESTION `?`
|
|
// 6 CAT_START_OF_DEFAULT_EXPORT `export default` expression is about to be printed
|
|
// 7 CAT_START_OF_STMT Expression statement's expression is about to be printed
|
|
// 8 CAT_START_OF_ARROW_EXPR Concise arrow body is about to be printed
|
|
// 9 CAT_OP_UN_NOT `!`
|
|
//
|
|
// Group 10 to 14: Operators `printSpaceBeforeOperatorSlow` needs to tell apart (`last >= CAT_OP_UN_NOT_AFTER_LT`)
|
|
// 10 CAT_OP_UN_NOT_AFTER_LT `!` written straight after a `<`
|
|
// 11 CAT_OP_UN_PLUS `+`
|
|
// 12 CAT_OP_UPD_INC `++`
|
|
// 13 CAT_OP_UN_NEG `-`
|
|
// 14 CAT_OP_UPD_DEC `--`
|
|
|
|
/**
|
|
* A category code. One of the `CAT_*` constants below, and nothing else -
|
|
* the range checks which read `state.last` are only sound over exactly this set.
|
|
*/
|
|
export type Category =
|
|
| typeof CAT_IDENT
|
|
| typeof CAT_INT_DIGIT
|
|
| typeof CAT_REGEX_SLASH
|
|
| typeof CAT_OTHER
|
|
| typeof CAT_LT
|
|
| typeof CAT_QUESTION
|
|
| typeof CAT_START_OF_STMT
|
|
| typeof CAT_START_OF_ARROW_EXPR
|
|
| typeof CAT_START_OF_DEFAULT_EXPORT
|
|
| typeof CAT_OP_UN_NOT
|
|
| typeof CAT_OP_UN_NOT_AFTER_LT
|
|
| typeof CAT_OP_UN_PLUS
|
|
| typeof CAT_OP_UPD_INC
|
|
| typeof CAT_OP_UN_NEG
|
|
| typeof CAT_OP_UPD_DEC;
|
|
|
|
/** Identifier part - letters, digits, `_`, `$`, Unicode `ID_Continue`. */
|
|
export const CAT_IDENT = 0;
|
|
|
|
/**
|
|
* A numeric literal whose text is plain digits, so a following `.` would be read as part of the number -
|
|
* `0 .toExponential()` needs the space, `1e3.x` and `.5.x` do not.
|
|
*/
|
|
export const CAT_INT_DIGIT = 1;
|
|
|
|
/**
|
|
* A regex's closing `/`, written only where the regex has no flags.
|
|
*
|
|
* In the identifier range because `/a/ in x` needs the space just as much as `x in y` does -
|
|
* without it the `in` would be read as regex flags. With flags, `CAT_IDENT` says the same thing.
|
|
*/
|
|
export const CAT_REGEX_SLASH = 2;
|
|
|
|
/** Anything not covered by another category - punctuation, whitespace, a quote. */
|
|
export const CAT_OTHER = 3;
|
|
|
|
/** `<`, which a following `!` must not merge with into `<!--`. */
|
|
export const CAT_LT = 4;
|
|
|
|
/** `?`, which must not merge with a following `?` into `??` - see `TSJSDocNullableType`. */
|
|
export const CAT_QUESTION = 5;
|
|
|
|
/**
|
|
* An `export default` expression is about to be printed, and nothing has been written since.
|
|
*
|
|
* Read by the function and class printers, which parenthesize themselves here so the declaration forms
|
|
* (`export default function f() {}`) stay distinguishable from the expression ones.
|
|
*
|
|
* Immediately below `CAT_START_OF_STMT`, which is what makes their shared test one `|`.
|
|
*/
|
|
export const CAT_START_OF_DEFAULT_EXPORT = 6;
|
|
|
|
/**
|
|
* An expression statement's expression is about to be printed, and nothing has been written since.
|
|
*
|
|
* A node which finds this in `last` is the leftmost token of the statement, however deeply nested it is,
|
|
* so an object literal or an object destructuring assignment there parenthesizes itself.
|
|
*
|
|
* The only mark both readers ask about, so it is the odd one, with the other two either side.
|
|
*/
|
|
export const CAT_START_OF_STMT = 7;
|
|
|
|
/**
|
|
* A concise arrow body is about to be printed, and nothing has been written since.
|
|
*
|
|
* Read by the same two printers as `CAT_START_OF_STMT` - `x => ({ a: 1 })` needs the parens for
|
|
* the same reason a statement does, because a leading `{` would be read as a block.
|
|
*
|
|
* Immediately above `CAT_START_OF_STMT`, which is what makes their shared test one `|`.
|
|
*/
|
|
export const CAT_START_OF_ARROW_EXPR = 8;
|
|
|
|
/**
|
|
* `!`, written anywhere other than straight after a `<` - both the unary operator and TS's
|
|
* postfix `!` (non-null assertion, definite assignment), which postfix position keeps off a `<`.
|
|
*
|
|
* An operator code, but deliberately below the range `printSpaceBeforeOperator` gates on -
|
|
* no following operator merges with a plain `!`, so storing this never costs the slow path a call.
|
|
*/
|
|
export const CAT_OP_UN_NOT = 9;
|
|
|
|
/**
|
|
* `!` written immediately after a `<`, which is the `<!--` hazard.
|
|
* Folding the check on the preceding character into the code saves tracking the second-last character.
|
|
*
|
|
* The first of the operators `printSpaceBeforeOperator` gates on - writing one of these
|
|
* is what records it, so no separate field tracks which operator came last.
|
|
*/
|
|
export const CAT_OP_UN_NOT_AFTER_LT = 10;
|
|
|
|
/** `+`, which must not merge with a following `+` or `++`. */
|
|
export const CAT_OP_UN_PLUS = 11;
|
|
|
|
/** `++`, which must not follow a `+` without a space. */
|
|
export const CAT_OP_UPD_INC = 12;
|
|
|
|
/** `-`, which must not merge with a following `-` or `--`. */
|
|
export const CAT_OP_UN_NEG = 13;
|
|
|
|
/** `--`, which must not follow a `-`, nor the `!` of a `<!`. */
|
|
export const CAT_OP_UPD_DEC = 14;
|
|
|
|
/**
|
|
* Every category, in numbering order.
|
|
*
|
|
* Only the debug checks which prove the range compares still match the sets they mean read this,
|
|
* so release builds drop it entirely.
|
|
*/
|
|
export const ALL_CATEGORIES: Category[] = [
|
|
CAT_IDENT,
|
|
CAT_INT_DIGIT,
|
|
CAT_REGEX_SLASH,
|
|
CAT_OTHER,
|
|
CAT_LT,
|
|
CAT_QUESTION,
|
|
CAT_START_OF_DEFAULT_EXPORT,
|
|
CAT_START_OF_STMT,
|
|
CAT_START_OF_ARROW_EXPR,
|
|
CAT_OP_UN_NOT,
|
|
CAT_OP_UN_NOT_AFTER_LT,
|
|
CAT_OP_UN_PLUS,
|
|
CAT_OP_UPD_INC,
|
|
CAT_OP_UN_NEG,
|
|
CAT_OP_UPD_DEC,
|
|
];
|
|
|
|
// The five marker readers select a pair of marks by position rather than comparing each in turn.
|
|
// `printFunction`, `printClass` and `printCallExpression` ask "statement or `export default`",
|
|
// `printObjectExpression` and `printAssignmentExpression` ask "statement or concise arrow body":
|
|
//
|
|
// (last | 1) === CAT_START_OF_STMT // Statement or `export default`
|
|
// ((last - 1) | 1) === CAT_START_OF_STMT // Statement or concise arrow body
|
|
//
|
|
// Both hold only while `CAT_START_OF_STMT` is odd and its two neighbours sit either side of it.
|
|
// Check each against the set it is meant to select, over every category and no others.
|
|
if (DEBUG) {
|
|
for (const category of ALL_CATEGORIES) {
|
|
debugAssert(
|
|
((category | 1) === CAT_START_OF_STMT) ===
|
|
(category === CAT_START_OF_STMT || category === CAT_START_OF_DEFAULT_EXPORT),
|
|
`Category ${category} disagrees with \`(last | 1) === CAT_START_OF_STMT\``,
|
|
);
|
|
debugAssert(
|
|
(((category - 1) | 1) === CAT_START_OF_STMT) ===
|
|
(category === CAT_START_OF_STMT || category === CAT_START_OF_ARROW_EXPR),
|
|
`Category ${category} disagrees with \`((last - 1) | 1) === CAT_START_OF_STMT\``,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Append `code` to the output, and record what it ends with.
|
|
*
|
|
* @param code - Text to append, never empty
|
|
* @param last - Category of the last character of `code`
|
|
*/
|
|
export function write(state: State, code: string, last: Category): void {
|
|
debugAssert(code.length > 0, "`code` should not be an empty string");
|
|
debugAssertCategoryMatches(state, code, last);
|
|
|
|
state.last = last;
|
|
state.output += code;
|
|
|
|
if (DEBUG) {
|
|
state.lastIsStale = false;
|
|
state.lastCharWritten = code[code.length - 1];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Append `code` to the output, record what it ends with, and record a source mapping for `node`.
|
|
*
|
|
* The mapping is only recorded where the caller asked for source maps and `node` carries a `loc` -
|
|
* Oxc's own AST records byte spans instead, so the check is on the node, not the build.
|
|
*
|
|
* Builds without source map support have no use for this. For those builds, TSDown plugin rewrites every call
|
|
* into `write` and drops the `node` argument, leaving this unreferenced for the minifier to remove.
|
|
*
|
|
* @param code - Text to append, never empty
|
|
* @param last - Category of the last character of `code`
|
|
* @param node - Node this text came from
|
|
*/
|
|
export function writeWithMap(state: State, code: string, last: Category, node: MappableNode): void {
|
|
debugAssert(code.length > 0, "`code` should not be an empty string");
|
|
debugAssertCategoryMatches(state, code, last);
|
|
|
|
if (SOURCEMAPS && node.loc != null) {
|
|
debugAssert(
|
|
state.mapOffsets !== null && state.mapPositions !== null && state.mapNames !== null,
|
|
"Source map arrays should exist when source maps are enabled",
|
|
);
|
|
|
|
typeAssertIs<{ name?: string }>(node);
|
|
state.mapOffsets.push(state.output.length);
|
|
state.mapPositions.push(node.loc.start);
|
|
state.mapNames.push(node.name);
|
|
}
|
|
|
|
state.last = last;
|
|
state.output += code;
|
|
|
|
if (DEBUG) {
|
|
state.lastIsStale = false;
|
|
state.lastCharWritten = code[code.length - 1];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Append `code` to the output, leaving `state.last` describing whatever came before it.
|
|
*
|
|
* Only sound where the value of `last` is provably dead - another `write` must follow before anything reads it.
|
|
* The readers are `printSpaceBeforeIdentifier` and the `CAT_LT`, `CAT_REGEX_SLASH` and `CAT_QUESTION`
|
|
* adjacency checks, all of which run at the start of printing a construct.
|
|
*
|
|
* In practice that means using it for all but the final fragment when one token is written in pieces.
|
|
* Debug builds track the rule and throw if a reader sees a stale `last`.
|
|
*
|
|
* @param code - Text to append, which unlike `write` may be empty
|
|
*/
|
|
export function writeNoLast(state: State, code: string): void {
|
|
state.output += code;
|
|
|
|
if (DEBUG) {
|
|
state.lastIsStale = true;
|
|
if (code.length > 0) state.lastCharWritten = code[code.length - 1];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Append `code` and record a source mapping for `node`, leaving `state.last` alone.
|
|
*
|
|
* `writeNoLast`'s rule about `last` applies here too - another write must follow before anything reads it.
|
|
*
|
|
* Builds without source map support have no use for this. For those builds, TSDown plugin rewrites every call
|
|
* into `write` and drops the `node` argument, leaving this unreferenced for the minifier to remove.
|
|
*
|
|
* @param code - Text to append, which unlike `writeWithMap` may be empty
|
|
* @param node - Node this text came from
|
|
*/
|
|
export function writeWithMapNoLast(state: State, code: string, node: MappableNode): void {
|
|
if (SOURCEMAPS && node.loc != null) {
|
|
debugAssert(
|
|
state.mapOffsets !== null && state.mapPositions !== null && state.mapNames !== null,
|
|
"Source map arrays should exist when source maps are enabled",
|
|
);
|
|
|
|
typeAssertIs<{ name?: string }>(node);
|
|
state.mapOffsets.push(state.output.length);
|
|
state.mapPositions.push(node.loc.start);
|
|
state.mapNames.push(node.name);
|
|
}
|
|
|
|
state.output += code;
|
|
|
|
if (DEBUG) {
|
|
state.lastIsStale = true;
|
|
if (code.length > 0) state.lastCharWritten = code[code.length - 1];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assert that `state.last` still describes what was written last.
|
|
*
|
|
* Every reader of `last` calls this, so a `writeNoLast` whose caller broke the rule is caught by
|
|
* the conformance suites rather than silently incorrectly spacing one construct.
|
|
*
|
|
* Debug builds only - the call and its argument are removed from release builds entirely.
|
|
*/
|
|
export function debugAssertLastFresh(state: State): void {
|
|
debugAssert(!state.lastIsStale, "`last` was read after `writeNoLast` left it stale");
|
|
}
|
|
|
|
/** Matches a character which can continue an identifier. */
|
|
const IDENT_CONTINUE_REGEX = /[\p{ID_Continue}$\u200C\u200D]/u;
|
|
|
|
/**
|
|
* Assert that `last` truthfully describes the end of `code`.
|
|
*
|
|
* A category encodes the merge hazard of the trailing token, not the trailing character -
|
|
* a digit legitimately ends both a `CAT_IDENT` and a `CAT_INT_DIGIT` write - so each final character
|
|
* has a set of permitted categories rather than one right answer.
|
|
*
|
|
* Debug builds only. Removed by minifier in release builds.
|
|
*/
|
|
function debugAssertCategoryMatches(state: State, code: string, last: Category): void {
|
|
if (!DEBUG) return;
|
|
|
|
const ch = code.at(-1)!;
|
|
|
|
let ok;
|
|
if (ch >= "0" && ch <= "9") {
|
|
// A digit ends an identifier (`x1`), a number some other character already separates
|
|
// (`1e3`, `0x1f`, `.5`) or a written fragment of one (the digits of `15e-8`) - all `CAT_IDENT`.
|
|
// Only a numeric literal which is plain digits throughout is `CAT_INT_DIGIT`.
|
|
// The dangerous converse - a whole plain-digit literal claiming `CAT_IDENT`, which loses the space
|
|
// in `0 .toExponential()` - looks exactly like a fragment write, so it cannot be caught here.
|
|
// The AST fixtures pin the number printer's category decisions instead.
|
|
ok = last === CAT_IDENT || (last === CAT_INT_DIGIT && /^[0-9]+$/.test(code));
|
|
} else if (ch === "+") {
|
|
if (code.endsWith("++")) {
|
|
ok = last === CAT_OP_UPD_INC;
|
|
} else {
|
|
ok = last === CAT_OP_UN_PLUS;
|
|
}
|
|
} else if (ch === "-") {
|
|
if (code.endsWith("--")) {
|
|
ok = last === CAT_OP_UPD_DEC;
|
|
} else {
|
|
ok = last === CAT_OP_UN_NEG;
|
|
}
|
|
} else if (ch === "!") {
|
|
// The two operator categories are told apart by the character before the `!`, which also proves
|
|
// the `CAT_OP_UN_NOT_AFTER_LT` branch in `printUnaryExpression` fires exactly when it should
|
|
if (last === CAT_OP_UN_NOT_AFTER_LT) {
|
|
ok = state.lastCharWritten === "<";
|
|
} else {
|
|
ok = last === CAT_OP_UN_NOT && state.lastCharWritten !== "<";
|
|
}
|
|
} else if (ch === "<") {
|
|
ok = last === CAT_LT;
|
|
} else if (ch === "?") {
|
|
ok = last === CAT_QUESTION;
|
|
} else if (ch === "/") {
|
|
ok = last === CAT_REGEX_SLASH;
|
|
} else {
|
|
// The final character, whole - for an astral character `ch` is only its low surrogate,
|
|
// which no Unicode property matches
|
|
const isLowSurrogate = ch >= "\uDC00" && ch <= "\uDFFF";
|
|
const finalChar = isLowSurrogate && code.length >= 2 ? code.slice(-2) : ch;
|
|
|
|
if (IDENT_CONTINUE_REGEX.test(finalChar)) {
|
|
ok = last === CAT_IDENT;
|
|
} else if (ch <= "\x7F") {
|
|
// ASCII
|
|
ok = last === CAT_OTHER;
|
|
} else {
|
|
// Not an identifier character by this runtime's Unicode tables, but the parser's tables
|
|
// may be a newer Unicode version which does count it, so both claims are believed -
|
|
// test262 holds identifiers whose final characters are new enough to hit this
|
|
ok = last === CAT_IDENT || last === CAT_OTHER;
|
|
}
|
|
}
|
|
|
|
debugAssert(ok, () => `Category ${last} does not describe the end of ${JSON.stringify(code)}`);
|
|
}
|