diff --git a/babel.config.js b/babel.config.js index 34831722f9b8..434bbe4d8963 100644 --- a/babel.config.js +++ b/babel.config.js @@ -162,7 +162,11 @@ module.exports = function (api) { presets: [ [ "@babel/preset-typescript", - { onlyRemoveTypeImports: true, allowDeclareFields: true }, + { + onlyRemoveTypeImports: true, + allowDeclareFields: true, + optimizeConstEnums: true, + }, ], ["@babel/env", envOpts], ["@babel/preset-flow", { allowDeclareFields: true }], @@ -200,6 +204,10 @@ module.exports = function (api) { ].map(normalize), plugins: ["babel-plugin-transform-charcodes"], }, + { + test: ["packages/babel-generator"].map(normalize), + plugins: [pluginGeneratorOptimization], + }, convertESM && { test: ["./packages/babel-node/src"].map(normalize), // Used to conditionally import kexec @@ -781,3 +789,37 @@ function pluginInjectNodeReexportsHints({ types: t, template }, { names }) { }, }; } + +/** + * @param {import("@babel/core")} pluginAPI + * @returns {import("@babel/core").PluginObj} + */ +function pluginGeneratorOptimization({ types: t }) { + return { + visitor: { + CallExpression: { + exit(path) { + const node = path.node; + if ( + t.isMemberExpression(node.callee) && + t.isThisExpression(node.callee.object) + ) { + const args = node.arguments; + + if ( + node.callee.property.name === "token" && + args.length === 1 && + t.isStringLiteral(args[0]) + ) { + const str = args[0].value; + if (str.length == 1) { + node.callee.property.name = "tokenChar"; + args[0] = t.numericLiteral(str.charCodeAt(0)); + } + } + } + }, + }, + }, + }; +} diff --git a/benchmark/babel-generator/real-case/jquery.mjs b/benchmark/babel-generator/real-case/jquery.mjs index 90d40d12d565..dc5c76f52f63 100644 --- a/benchmark/babel-generator/real-case/jquery.mjs +++ b/benchmark/babel-generator/real-case/jquery.mjs @@ -15,16 +15,20 @@ function createInput(length) { ); } +const inputs = [1, 4, 16, 64].map(length => ({ + tag: length, + body: createInput(length), +})); + function benchCases(name, implementation, options) { - for (const length of [1, 2]) { - const input = createInput(length); - suite.add(`${name} ${length} jquery 3.6`, () => { - implementation(input, options); + for (const input of inputs) { + suite.add(`${name} ${input.tag} jquery 3.6`, () => { + implementation(input.body, options); }); } } -benchCases("baseline", baseline.default); benchCases("current", current.default); +benchCases("baseline", baseline.default); suite.on("cycle", report).run(); diff --git a/packages/babel-generator/src/buffer.ts b/packages/babel-generator/src/buffer.ts index f097e4fe2070..6dba3a30aad3 100644 --- a/packages/babel-generator/src/buffer.ts +++ b/packages/babel-generator/src/buffer.ts @@ -18,6 +18,15 @@ type SourcePos = { filename: string | undefined; }; +type QueueItem = { + char: number; + repeat: number; + line: number | undefined; + column: number | undefined; + identifierName: string | undefined; + filename: string | undefined; +}; + function SourcePos(): SourcePos { return { identifierName: undefined, @@ -27,23 +36,78 @@ function SourcePos(): SourcePos { }; } -const SPACES_RE = /^[ \t]+$/; export default class Buffer { constructor(map?: SourceMap | null) { this._map = map; + + this._allocQueue(); } _map: SourceMap = null; _buf = ""; + _str = ""; + _appendCount = 0; _last = 0; - _queue: Parameters[] = []; + _queue: QueueItem[] = []; + _queueCursor = 0; _position = { line: 1, column: 0, }; _sourcePosition = SourcePos(); - _disallowedPop: SourcePos | null = null; + _disallowedPop: SourcePos & { objectReusable: boolean } = { + identifierName: undefined, + line: undefined, + column: undefined, + filename: undefined, + objectReusable: true, // To avoid deleting and re-creating objects, we reuse existing objects when they are not needed anymore. + }; + + _allocQueue() { + const queue = this._queue; + + for (let i = 0; i < 16; i++) { + queue.push({ + char: 0, + repeat: 1, + line: undefined, + column: undefined, + identifierName: undefined, + filename: "", + }); + } + } + + _pushQueue( + char: number, + repeat: number, + line: number | undefined, + column: number | undefined, + identifierName: string | undefined, + filename: string | undefined, + ) { + const cursor = this._queueCursor; + if (cursor === this._queue.length) { + this._allocQueue(); + } + const item = this._queue[cursor]; + item.char = char; + item.repeat = repeat; + item.line = line; + item.column = column; + item.identifierName = identifierName; + item.filename = filename; + + this._queueCursor++; + } + + _popQueue(): QueueItem { + if (this._queueCursor === 0) { + throw new Error("Cannot pop from empty queue"); + } + return this._queue[--this._queueCursor]; + } /** * Get the final string output from the buffer, along with the sourcemap if one exists. @@ -56,7 +120,7 @@ export default class Buffer { const result = { // Whatever trim is used here should not execute a regex against the // source string since it may be arbitrarily large after all transformations - code: this._buf.trimRight(), + code: (this._buf + this._str).trimRight(), // Decoded sourcemap is free to generate. decodedMap: map?.getDecoded(), @@ -87,50 +151,105 @@ export default class Buffer { * Add a string to the buffer that cannot be reverted. */ - append(str: string): void { + append(str: string, maybeNewline: boolean): void { this._flush(); - const { line, column, filename, identifierName } = this._sourcePosition; - this._append(str, line, column, identifierName, filename); + + this._append(str, this._sourcePosition, maybeNewline); + } + + appendChar(char: number): void { + this._flush(); + this._appendChar(char, 1, this._sourcePosition); } /** * Add a string to the buffer than can be reverted. */ - queue(str: string): void { + queue(char: number): void { // Drop trailing spaces when a newline is inserted. - if (str === "\n") { - while (this._queue.length > 0 && SPACES_RE.test(this._queue[0][0])) { - this._queue.shift(); + if (char === charcodes.lineFeed) { + while (this._queueCursor !== 0) { + const char = this._queue[this._queueCursor - 1].char; + if (char !== charcodes.space && char !== charcodes.tab) { + break; + } + + this._queueCursor--; } } - const { line, column, filename, identifierName } = this._sourcePosition; - this._queue.unshift([str, line, column, identifierName, filename]); + const sourcePosition = this._sourcePosition; + this._pushQueue( + char, + 1, + sourcePosition.line, + sourcePosition.column, + sourcePosition.identifierName, + sourcePosition.filename, + ); } /** * Same as queue, but this indentation will never have a sourcmap marker. */ - queueIndentation(str: string): void { - this._queue.unshift([str, undefined, undefined, undefined, undefined]); + queueIndentation(char: number, repeat: number): void { + this._pushQueue(char, repeat, undefined, undefined, undefined, undefined); } _flush(): void { - let item: Parameters; - while ((item = this._queue.pop())) { - this._append(...item); + const queueCursor = this._queueCursor; + const queue = this._queue; + for (let i = 0; i < queueCursor; i++) { + const item: QueueItem = queue[i]; + this._appendChar(item.char, item.repeat, item); } + this._queueCursor = 0; } - _append( - str: string, - line: number | undefined, - column: number | undefined, - identifierName: string | undefined, - filename: string | undefined, - ): void { - this._buf += str; - this._last = str.charCodeAt(str.length - 1); + _appendChar(char: number, repeat: number, sourcePos: SourcePos): void { + this._last = char; + + this._str += + repeat > 1 + ? String.fromCharCode(char).repeat(repeat) + : String.fromCharCode(char); + + if (char !== charcodes.lineFeed) { + this._mark( + sourcePos.line, + sourcePos.column, + sourcePos.identifierName, + sourcePos.filename, + ); + this._position.column += repeat; + } else { + this._position.line++; + this._position.column = 0; + } + } + + _append(str: string, sourcePos: SourcePos, maybeNewline: boolean): void { + const len = str.length; + + this._last = str.charCodeAt(len - 1); + + if (++this._appendCount > 4096) { + // @ts-ignore + +this._str; // Unexplainable huge performance boost. Ref: https://github.com/davidmarkclements/flatstr License: MIT + this._buf += this._str; + this._str = str; + this._appendCount = 0; + } else { + this._str += str; + } + + if (!maybeNewline && !this._map) { + this._position.column += len; + return; + } + + const { column, identifierName, filename } = sourcePos; + let line = sourcePos.line; // Search for newline chars. We search only for `\n`, since both `\r` and // `\r\n` are normalized to `\n` during parse. We exclude `\u2028` and @@ -171,26 +290,28 @@ export default class Buffer { } removeTrailingNewline(): void { - if (this._queue.length > 0 && this._queue[0][0] === "\n") { - this._queue.shift(); + const queueCursor = this._queueCursor; + if ( + queueCursor !== 0 && + this._queue[queueCursor - 1].char === charcodes.lineFeed + ) { + this._queueCursor--; } } removeLastSemicolon(): void { - if (this._queue.length > 0 && this._queue[0][0] === ";") { - this._queue.shift(); + const queueCursor = this._queueCursor; + if ( + queueCursor !== 0 && + this._queue[queueCursor - 1].char === charcodes.semicolon + ) { + this._queueCursor--; } } getLastChar(): number { - let last; - if (this._queue.length > 0) { - const str = this._queue[0][0]; - last = str.charCodeAt(0); - } else { - last = this._last; - } - return last; + const queueCursor = this._queueCursor; + return queueCursor !== 0 ? this._queue[queueCursor - 1].char : this._last; } /** @@ -201,14 +322,13 @@ export default class Buffer { */ endsWithCharAndNewline(): number { const queue = this._queue; - if (queue.length > 0) { - const last = queue[0][0]; + const queueCursor = this._queueCursor; + if (queueCursor !== 0) { // every element in queue is one-length whitespace string - const lastCp = last.charCodeAt(0); + const lastCp = queue[queueCursor - 1].char; if (lastCp !== charcodes.lineFeed) return; - if (queue.length > 1) { - const secondLast = queue[1][0]; - return secondLast.charCodeAt(0); + if (queueCursor > 1) { + return queue[queueCursor - 2].char; } else { return this._last; } @@ -218,7 +338,7 @@ export default class Buffer { } hasContent(): boolean { - return this._queue.length > 0 || !!this._last; + return this._queueCursor !== 0 || !!this._last; } /** @@ -245,6 +365,8 @@ export default class Buffer { * over "();", where previously it would have been a single mapping. */ exactSource(loc: Loc | undefined, cb: () => void) { + if (!this._map) return cb(); + this.source("start", loc); cb(); @@ -266,7 +388,7 @@ export default class Buffer { */ source(prop: "start" | "end", loc: Loc | undefined): void { - if (prop && !loc) return; + if (!loc) return; // Since this is called extremely often, we re-use the same _sourcePosition // object for the whole lifetime of the buffer. @@ -294,7 +416,7 @@ export default class Buffer { if ( // Verify if reactivating this specific position has been disallowed. - !this._disallowedPop || + this._disallowedPop.objectReusable || this._disallowedPop.line !== originalLine || this._disallowedPop.column !== originalColumn || this._disallowedPop.filename !== originalFilename @@ -303,7 +425,7 @@ export default class Buffer { this._sourcePosition.column = originalColumn; this._sourcePosition.filename = originalFilename; this._sourcePosition.identifierName = originalIdentifierName; - this._disallowedPop = null; + this._disallowedPop.objectReusable = true; } } @@ -313,42 +435,55 @@ export default class Buffer { * "end" location that they set is actually treated as the end position. */ _disallowPop(prop: "start" | "end", loc: Loc) { - if (prop && !loc) return; + if (!loc) return; + + const disallowedPop = this._disallowedPop; - this._disallowedPop = this._normalizePosition(prop, loc, SourcePos()); + this._normalizePosition(prop, loc, disallowedPop); + + disallowedPop.objectReusable = false; } - _normalizePosition( - prop: "start" | "end", - loc: Loc | undefined | null, - targetObj: SourcePos, - ) { - const pos = loc ? loc[prop] : null; + _normalizePosition(prop: "start" | "end", loc: Loc, targetObj: SourcePos) { + const pos = loc[prop]; targetObj.identifierName = - (prop === "start" && loc?.identifierName) || undefined; - targetObj.line = pos?.line; - targetObj.column = pos?.column; - targetObj.filename = loc?.filename; - - return targetObj; + (prop === "start" && loc.identifierName) || undefined; + if (pos) { + targetObj.line = pos.line; + targetObj.column = pos.column; + targetObj.filename = loc.filename; + } else { + targetObj.line = null; + targetObj.column = null; + targetObj.filename = null; + } } getCurrentColumn(): number { - const extra = this._queue.reduce((acc, item) => item[0] + acc, ""); - const lastIndex = extra.lastIndexOf("\n"); + const queue = this._queue; + + let lastIndex = -1; + let len = 0; + for (let i = 0; i < this._queueCursor; i++) { + const item = queue[i]; + if (item.char === charcodes.lineFeed) { + lastIndex = i; + len += item.repeat; + } + } - return lastIndex === -1 - ? this._position.column + extra.length - : extra.length - 1 - lastIndex; + return lastIndex === -1 ? this._position.column + len : len - 1 - lastIndex; } getCurrentLine(): number { - const extra = this._queue.reduce((acc, item) => item[0] + acc, ""); - let count = 0; - for (let i = 0; i < extra.length; i++) { - if (extra[i] === "\n") count++; + + const queue = this._queue; + for (let i = 0; i < this._queueCursor; i++) { + if (queue[i].char === charcodes.lineFeed) { + count++; + } } return this._position.line + count; diff --git a/packages/babel-generator/src/generators/base.ts b/packages/babel-generator/src/generators/base.ts index ae53b83d0fc1..fed810d55ab9 100644 --- a/packages/babel-generator/src/generators/base.ts +++ b/packages/babel-generator/src/generators/base.ts @@ -85,7 +85,7 @@ export function InterpreterDirective( this: Printer, node: t.InterpreterDirective, ) { - this.token(`#!${node.value}\n`); + this.token(`#!${node.value}\n`, true); } export function Placeholder(this: Printer, node: t.Placeholder) { diff --git a/packages/babel-generator/src/generators/template-literals.ts b/packages/babel-generator/src/generators/template-literals.ts index 48c9e5589ebb..694947adba00 100644 --- a/packages/babel-generator/src/generators/template-literals.ts +++ b/packages/babel-generator/src/generators/template-literals.ts @@ -20,7 +20,7 @@ export function TemplateElement( const value = (isFirst ? "`" : "}") + node.value.raw + (isLast ? "`" : "${"); - this.token(value); + this.token(value, true); } export function TemplateLiteral(this: Printer, node: t.TemplateLiteral) { diff --git a/packages/babel-generator/src/index.ts b/packages/babel-generator/src/index.ts index b64536d182ab..326c8df96b99 100644 --- a/packages/babel-generator/src/index.ts +++ b/packages/babel-generator/src/index.ts @@ -91,8 +91,8 @@ function normalizeOptions( format.shouldPrintComment || (value => format.comments || - value.indexOf("@license") >= 0 || - value.indexOf("@preserve") >= 0); + value.includes("@license") || + value.includes("@preserve")); } if (format.compact === "auto") { diff --git a/packages/babel-generator/src/node/index.ts b/packages/babel-generator/src/node/index.ts index 09e2bbdc79cd..079a311ff26c 100644 --- a/packages/babel-generator/src/node/index.ts +++ b/packages/babel-generator/src/node/index.ts @@ -8,7 +8,8 @@ import { isNewExpression, } from "@babel/types"; import type * as t from "@babel/types"; -import type { WhitespaceObject } from "./whitespace"; + +import type { WhitespaceFlag } from "./whitespace"; export type NodeHandlers = { [K in string]?: ( @@ -57,7 +58,6 @@ function expandAliases(obj: NodeHandlers) { // into concrete types so that the 'find' call below can be as fast as possible. const expandedParens = expandAliases(parens); const expandedWhitespaceNodes = expandAliases(whitespace.nodes); -const expandedWhitespaceList = expandAliases(whitespace.list); function find( obj: NodeHandlers, @@ -80,7 +80,7 @@ function isOrHasCallExpression(node: t.Node): boolean { export function needsWhitespace( node: t.Node, parent: t.Node, - type: "before" | "after", + type: WhitespaceFlag, ): boolean { if (!node) return false; @@ -88,35 +88,21 @@ export function needsWhitespace( node = node.expression; } - let linesInfo: WhitespaceObject | null | boolean = find( - expandedWhitespaceNodes, - node, - parent, - ); - - if (!linesInfo) { - const items = find(expandedWhitespaceList, node, parent); - if (items) { - for (let i = 0; i < items.length; i++) { - linesInfo = needsWhitespace(items[i], node, type); - if (linesInfo) break; - } - } - } + const flag = find(expandedWhitespaceNodes, node, parent); - if (typeof linesInfo === "object" && linesInfo !== null) { - return linesInfo[type] || false; + if (typeof flag === "number") { + return (flag & type) !== 0; } return false; } export function needsWhitespaceBefore(node: t.Node, parent: t.Node) { - return needsWhitespace(node, parent, "before"); + return needsWhitespace(node, parent, 1); } export function needsWhitespaceAfter(node: t.Node, parent: t.Node) { - return needsWhitespace(node, parent, "after"); + return needsWhitespace(node, parent, 2); } export function needsParens( diff --git a/packages/babel-generator/src/node/parentheses.ts b/packages/babel-generator/src/node/parentheses.ts index 89ad23b18479..4cf704caf481 100644 --- a/packages/babel-generator/src/node/parentheses.ts +++ b/packages/babel-generator/src/node/parentheses.ts @@ -80,6 +80,15 @@ const PRECEDENCE = { "**": 10, }; +const enum CheckParam { + expressionStatement = 1 << 0, + arrowBody = 1 << 1, + exportDefault = 1 << 2, + forHead = 1 << 3, + forInHead = 1 << 4, + forOfHead = 1 << 5, +} + const isClassExtendsClause = ( node: t.Node, parent: t.Node, @@ -107,6 +116,8 @@ export function FunctionTypeAnnotation( parent: t.Node, printStack: Array, ): boolean { + if (printStack.length < 3) return; + return ( // (() => A) | (() => B) isUnionTypeAnnotation(parent) || @@ -133,10 +144,10 @@ export function ObjectExpression( parent: t.Node, printStack: Array, ): boolean { - return isFirstInContext(printStack, { - expressionStatement: true, - arrowBody: true, - }); + return isFirstInContext( + printStack, + CheckParam.expressionStatement | CheckParam.arrowBody, + ); } export function DoExpression( @@ -146,7 +157,7 @@ export function DoExpression( ): boolean { // `async do` can start an expression statement return ( - !node.async && isFirstInContext(printStack, { expressionStatement: true }) + !node.async && isFirstInContext(printStack, CheckParam.expressionStatement) ); } @@ -305,10 +316,10 @@ export function ClassExpression( parent: t.Node, printStack: Array, ): boolean { - return isFirstInContext(printStack, { - expressionStatement: true, - exportDefault: true, - }); + return isFirstInContext( + printStack, + CheckParam.expressionStatement | CheckParam.exportDefault, + ); } export function UnaryLike( @@ -331,10 +342,10 @@ export function FunctionExpression( parent: t.Node, printStack: Array, ): boolean { - return isFirstInContext(printStack, { - expressionStatement: true, - exportDefault: true, - }); + return isFirstInContext( + printStack, + CheckParam.expressionStatement | CheckParam.exportDefault, + ); } export function ArrowFunctionExpression( @@ -433,12 +444,15 @@ export function Identifier( computed: true, optional: false, }); - return isFirstInContext(printStack, { - expressionStatement: isFollowedByBracket, - forHead: isFollowedByBracket, - forInHead: isFollowedByBracket, - forOfHead: true, - }); + return isFirstInContext( + printStack, + isFollowedByBracket + ? CheckParam.expressionStatement | + CheckParam.forHead | + CheckParam.forInHead | + CheckParam.forOfHead + : CheckParam.forOfHead, + ); } // ECMAScript specifically forbids a for-of loop from starting with the @@ -458,16 +472,17 @@ export function Identifier( // in a particular context. function isFirstInContext( printStack: Array, - { - expressionStatement = false, - arrowBody = false, - exportDefault = false, - forHead = false, - forInHead = false, - forOfHead = false, - }, + checkParam: CheckParam, ): boolean { + const expressionStatement = checkParam & CheckParam.expressionStatement; + const arrowBody = checkParam & CheckParam.arrowBody; + const exportDefault = checkParam & CheckParam.exportDefault; + const forHead = checkParam & CheckParam.forHead; + const forInHead = checkParam & CheckParam.forInHead; + const forOfHead = checkParam & CheckParam.forOfHead; + let i = printStack.length - 1; + if (i <= 0) return; let node = printStack[i]; i--; let parent = printStack[i]; @@ -486,12 +501,13 @@ function isFirstInContext( } if ( - (hasPostfixPart(node, parent) && !isNewExpression(parent)) || - (isSequenceExpression(parent) && parent.expressions[0] === node) || - (isUpdateExpression(parent) && !parent.prefix) || - isConditional(parent, { test: node }) || - isBinary(parent, { left: node }) || - isAssignmentExpression(parent, { left: node }) + i > 0 && + ((hasPostfixPart(node, parent) && !isNewExpression(parent)) || + (isSequenceExpression(parent) && parent.expressions[0] === node) || + (isUpdateExpression(parent) && !parent.prefix) || + isConditional(parent, { test: node }) || + isBinary(parent, { left: node }) || + isAssignmentExpression(parent, { left: node })) ) { node = parent; i--; diff --git a/packages/babel-generator/src/node/whitespace.ts b/packages/babel-generator/src/node/whitespace.ts index 8bd234b8045e..a58427a94c0a 100644 --- a/packages/babel-generator/src/node/whitespace.ts +++ b/packages/babel-generator/src/node/whitespace.ts @@ -14,55 +14,75 @@ import { isOptionalMemberExpression, isStringLiteral, } from "@babel/types"; +import * as charCodes from "charcodes"; import type { NodeHandlers } from "./index"; import type * as t from "@babel/types"; -export type WhitespaceObject = { - before?: boolean; - after?: boolean; -}; -/** - * Crawl a node to test if it contains a CallExpression, a Function, or a Helper. - * - * @example - * crawl(node) - * // { hasCall: false, hasFunction: true, hasHelper: false } - */ +const enum WhitespaceFlag { + before = 1 << 0, + after = 1 << 1, +} + +export type { WhitespaceFlag }; -function crawl( +function crawlInternal( node: t.Node, - state: { hasCall?: boolean; hasFunction?: boolean; hasHelper?: boolean } = {}, + state: { hasCall: boolean; hasFunction: boolean; hasHelper: boolean }, ) { + if (!node) return state; + if (isMemberExpression(node) || isOptionalMemberExpression(node)) { - crawl(node.object, state); - if (node.computed) crawl(node.property, state); + crawlInternal(node.object, state); + if (node.computed) crawlInternal(node.property, state); } else if (isBinary(node) || isAssignmentExpression(node)) { - crawl(node.left, state); - crawl(node.right, state); + crawlInternal(node.left, state); + crawlInternal(node.right, state); } else if (isCallExpression(node) || isOptionalCallExpression(node)) { state.hasCall = true; - crawl(node.callee, state); + crawlInternal(node.callee, state); } else if (isFunction(node)) { state.hasFunction = true; } else if (isIdentifier(node)) { - // @ts-expect-error todo(flow->ts): node.callee is not really expected here… - state.hasHelper = state.hasHelper || isHelper(node.callee); + state.hasHelper = + // @ts-expect-error todo(flow->ts): node.callee is not really expected here… + state.hasHelper || (node.callee && isHelper(node.callee)); } return state; } +/** + * Crawl a node to test if it contains a CallExpression, a Function, or a Helper. + * + * @example + * crawl(node) + * // { hasCall: false, hasFunction: true, hasHelper: false } + */ + +function crawl(node: t.Node) { + return crawlInternal(node, { + hasCall: false, + hasFunction: false, + hasHelper: false, + }); +} + /** * Test if a node is or has a helper. */ function isHelper(node: t.Node): boolean { + if (!node) return false; + if (isMemberExpression(node)) { return isHelper(node.object) || isHelper(node.property); } else if (isIdentifier(node)) { - return node.name === "require" || node.name[0] === "_"; + return ( + node.name === "require" || + node.name.charCodeAt(0) === charCodes.underscore + ); } else if (isCallExpression(node)) { return isHelper(node.callee); } else if (isBinary(node) || isAssignmentExpression(node)) { @@ -88,20 +108,17 @@ function isType(node: t.Node) { * Tests for node types that need whitespace. */ -export const nodes: NodeHandlers = { +export const nodes: NodeHandlers = { /** * Test if AssignmentExpression needs whitespace. */ - AssignmentExpression( - node: t.AssignmentExpression, - ): WhitespaceObject | undefined | null { + AssignmentExpression(node: t.AssignmentExpression): WhitespaceFlag { const state = crawl(node.right); if ((state.hasCall && state.hasHelper) || state.hasFunction) { - return { - before: state.hasFunction, - after: true, - }; + return state.hasFunction + ? WhitespaceFlag.before | WhitespaceFlag.after + : WhitespaceFlag.after; } }, @@ -109,24 +126,24 @@ export const nodes: NodeHandlers = { * Test if SwitchCase needs whitespace. */ - SwitchCase(node: t.SwitchCase, parent: t.SwitchStatement): WhitespaceObject { - return { - before: !!node.consequent.length || parent.cases[0] === node, - after: - !node.consequent.length && - parent.cases[parent.cases.length - 1] === node, - }; + SwitchCase(node: t.SwitchCase, parent: t.SwitchStatement): WhitespaceFlag { + return ( + (!!node.consequent.length || parent.cases[0] === node + ? WhitespaceFlag.before + : 0) | + (!node.consequent.length && parent.cases[parent.cases.length - 1] === node + ? WhitespaceFlag.after + : 0) + ); }, /** * Test if LogicalExpression needs whitespace. */ - LogicalExpression(node: t.LogicalExpression): WhitespaceObject | undefined { + LogicalExpression(node: t.LogicalExpression): WhitespaceFlag { if (isFunction(node.left) || isFunction(node.right)) { - return { - after: true, - }; + return WhitespaceFlag.after; } }, @@ -134,11 +151,9 @@ export const nodes: NodeHandlers = { * Test if Literal needs whitespace. */ - Literal(node: t.Literal): WhitespaceObject | undefined | null { + Literal(node: t.Literal): WhitespaceFlag { if (isStringLiteral(node) && node.value === "use strict") { - return { - after: true, - }; + return WhitespaceFlag.after; } }, @@ -146,23 +161,15 @@ export const nodes: NodeHandlers = { * Test if CallExpressionish needs whitespace. */ - CallExpression(node: t.CallExpression): WhitespaceObject | undefined | null { + CallExpression(node: t.CallExpression): WhitespaceFlag { if (isFunction(node.callee) || isHelper(node)) { - return { - before: true, - after: true, - }; + return WhitespaceFlag.before | WhitespaceFlag.after; } }, - OptionalCallExpression( - node: t.OptionalCallExpression, - ): WhitespaceObject | undefined | null { + OptionalCallExpression(node: t.OptionalCallExpression): WhitespaceFlag { if (isFunction(node.callee)) { - return { - before: true, - after: true, - }; + return WhitespaceFlag.before | WhitespaceFlag.after; } }, @@ -170,23 +177,18 @@ export const nodes: NodeHandlers = { * Test if VariableDeclaration needs whitespace. */ - VariableDeclaration( - node: t.VariableDeclaration, - ): WhitespaceObject | undefined | null { + VariableDeclaration(node: t.VariableDeclaration): WhitespaceFlag { for (let i = 0; i < node.declarations.length; i++) { const declar = node.declarations[i]; let enabled = isHelper(declar.id) && !isType(declar.init); - if (!enabled) { + if (!enabled && declar.init) { const state = crawl(declar.init); enabled = (isHelper(declar.init) && state.hasCall) || state.hasFunction; } if (enabled) { - return { - before: true, - after: true, - }; + return WhitespaceFlag.before | WhitespaceFlag.after; } } }, @@ -195,12 +197,9 @@ export const nodes: NodeHandlers = { * Test if IfStatement needs whitespace. */ - IfStatement(node: t.IfStatement): WhitespaceObject | undefined | null { + IfStatement(node: t.IfStatement): WhitespaceFlag { if (isBlockStatement(node.consequent)) { - return { - before: true, - after: true, - }; + return WhitespaceFlag.before | WhitespaceFlag.after; } }, }; @@ -215,86 +214,48 @@ nodes.ObjectProperty = function ( node: t.ObjectProperty | t.ObjectTypeProperty | t.ObjectMethod, parent: t.ObjectExpression, - ): WhitespaceObject | undefined | null { + ): WhitespaceFlag { if (parent.properties[0] === node) { - return { - before: true, - }; + return WhitespaceFlag.before; } }; nodes.ObjectTypeCallProperty = function ( node: t.ObjectTypeCallProperty, parent: t.ObjectTypeAnnotation, -): WhitespaceObject | undefined | null { +): WhitespaceFlag { if (parent.callProperties[0] === node && !parent.properties?.length) { - return { - before: true, - }; + return WhitespaceFlag.before; } }; nodes.ObjectTypeIndexer = function ( node: t.ObjectTypeIndexer, parent: t.ObjectTypeAnnotation, -): WhitespaceObject | undefined | null { +): WhitespaceFlag { if ( parent.indexers[0] === node && !parent.properties?.length && !parent.callProperties?.length ) { - return { - before: true, - }; + return WhitespaceFlag.before; } }; nodes.ObjectTypeInternalSlot = function ( node: t.ObjectTypeInternalSlot, parent: t.ObjectTypeAnnotation, -): WhitespaceObject | undefined | null { +): WhitespaceFlag { if ( parent.internalSlots[0] === node && !parent.properties?.length && !parent.callProperties?.length && !parent.indexers?.length ) { - return { - before: true, - }; + return WhitespaceFlag.before; } }; -/** - * Returns lists from node types that need whitespace. - */ - -export const list: NodeHandlers = { - /** - * Return VariableDeclaration declarations init properties. - */ - - VariableDeclaration(node: t.VariableDeclaration) { - return node.declarations.map(decl => decl.init); - }, - - /** - * Return VariableDeclaration elements. - */ - - ArrayExpression(node: t.ArrayExpression) { - return node.elements; - }, - - /** - * Return VariableDeclaration properties. - */ - - ObjectExpression(node: t.ObjectExpression) { - return node.properties; - }, -}; - /** * Add whitespace tests for nodes and their aliases. */ @@ -312,8 +273,7 @@ export const list: NodeHandlers = { [type as string] .concat(FLIPPED_ALIAS_KEYS[type] || []) .forEach(function (type) { - nodes[type] = function () { - return { after: amounts, before: amounts }; - }; + const ret = amounts ? WhitespaceFlag.before | WhitespaceFlag.after : 0; + nodes[type] = () => ret; }); }); diff --git a/packages/babel-generator/src/printer.ts b/packages/babel-generator/src/printer.ts index 857ca0c97034..30d82ddb3067 100644 --- a/packages/babel-generator/src/printer.ts +++ b/packages/babel-generator/src/printer.ts @@ -72,6 +72,9 @@ class Printer { constructor(format: Format, map: SourceMap) { this.format = format; this._buf = new Buffer(map); + + this._indentChar = format.indent.style.charCodeAt(0); + this._indentRepeat = format.indent.style.length; } declare format: Format; @@ -80,11 +83,13 @@ class Printer { declare _buf: Buffer; _printStack: Array = []; _indent: number = 0; + _indentChar: number = 0; + _indentRepeat: number = 0; _insideAux: boolean = false; _parenPushNewlineState: { printed: boolean } | null = null; _noLineTerminator: boolean = false; _printAuxAfterOnNextUserNode: boolean = false; - _printedComments = new WeakSet(); + _printedComments = new Set(); _endsWithInteger = false; _endsWithWord = false; @@ -121,7 +126,11 @@ class Printer { semicolon(force: boolean = false): void { this._maybeAddAuxComment(); - this._append(";", !force /* queue */); + if (force) { + this._appendChar(charCodes.semicolon); + } else { + this._queue(charCodes.semicolon); + } } /** @@ -160,13 +169,13 @@ class Printer { // prevent concatenating words and creating // comment out of division and regex if ( this._endsWithWord || - (this.endsWith(charCodes.slash) && str.charCodeAt(0) === charCodes.slash) + (str.charCodeAt(0) === charCodes.slash && this.endsWith(charCodes.slash)) ) { this._space(); } this._maybeAddAuxComment(); - this._append(str); + this._append(str, false); this._endsWithWord = true; } @@ -192,13 +201,13 @@ class Printer { * Writes a simple token. */ - token(str: string): void { + token(str: string, maybeNewline = false): void { // space is mandatory to avoid outputting