// heavily based on https://github.com/davidbonnet/astring // released under MIT license https://github.com/davidbonnet/astring/blob/master/LICENSE import { re } from '../utils/id.js'; import { push_array } from '../utils/push_array.js'; /** @typedef {import('estree').ArrowFunctionExpression} ArrowFunctionExpression */ /** @typedef {import('estree').BinaryExpression} BinaryExpression */ /** @typedef {import('estree').CallExpression} CallExpression */ /** @typedef {import('estree').Comment} Comment */ /** @typedef {import('estree').ExportSpecifier} ExportSpecifier */ /** @typedef {import('estree').Expression} Expression */ /** @typedef {import('estree').FunctionDeclaration} FunctionDeclaration */ /** @typedef {import('estree').ImportDeclaration} ImportDeclaration */ /** @typedef {import('estree').ImportSpecifier} ImportSpecifier */ /** @typedef {import('estree').Literal} Literal */ /** @typedef {import('estree').LogicalExpression} LogicalExpression */ /** @typedef {import('estree').NewExpression} NewExpression */ /** @typedef {import('estree').Node} Node */ /** @typedef {import('estree').ObjectExpression} ObjectExpression */ /** @typedef {import('estree').Pattern} Pattern */ /** @typedef {import('estree').Property} Property */ /** @typedef {import('estree').PropertyDefinition} PropertyDefinition */ /** @typedef {import('estree').SequenceExpression} SequenceExpression */ /** @typedef {import('estree').SimpleCallExpression} SimpleCallExpression */ /** @typedef {import('estree').SwitchStatement} SwitchStatement */ /** @typedef {import('estree').VariableDeclaration} VariableDeclaration */ /** @typedef {import('estree').StaticBlock} StaticBlock */ /** @typedef {import('estree').PrivateIdentifier} PrivateIdenifier*/ /** * @typedef {{ * content: string; * loc?: { * start: { line: number; column: number; }; * end: { line: number; column: number; }; * }; * has_newline: boolean; * }} Chunk */ /** * @typedef {(node: any, state: State) => Chunk[]} Handler */ /** * @typedef {{ * indent: string; * scope: any; // TODO import from periscopic * scope_map: WeakMap; * getName: (name: string) => string; * deconflicted: WeakMap>; * comments: Comment[]; * }} State */ /** * @param {Node} node * @param {State} state * @returns {Chunk[]} */ export function handle(node, state) { const handler = handlers[node.type]; if (!handler) { throw new Error(`Not implemented ${node.type}`); } const result = handler(node, state); if (node.leadingComments) { result.unshift( c( node.leadingComments .map((comment) => comment.type === 'Block' ? `/*${comment.value}*/${ /** @type {any} */ (comment).has_trailing_newline ? `\n${state.indent}` : ` ` }` : `//${comment.value}${ /** @type {any} */ (comment).has_trailing_newline ? `\n${state.indent}` : ` ` }` ) .join(``) ) ); } if (node.trailingComments) { state.comments.push(node.trailingComments[0]); // there is only ever one } return result; } /** * @param {string} content * @param {Node} [node] * @returns {Chunk} */ function c(content, node) { return { content, loc: node && node.loc, has_newline: /\n/.test(content) }; } const OPERATOR_PRECEDENCE = { '||': 2, '&&': 3, '??': 4, '|': 5, '^': 6, '&': 7, '==': 8, '!=': 8, '===': 8, '!==': 8, '<': 9, '>': 9, '<=': 9, '>=': 9, in: 9, instanceof: 9, '<<': 10, '>>': 10, '>>>': 10, '+': 11, '-': 11, '*': 12, '%': 12, '/': 12, '**': 13 }; /** @type {Record} */ const EXPRESSIONS_PRECEDENCE = { ArrayExpression: 20, TaggedTemplateExpression: 20, ThisExpression: 20, Identifier: 20, Literal: 18, TemplateLiteral: 20, Super: 20, SequenceExpression: 20, MemberExpression: 19, CallExpression: 19, NewExpression: 19, AwaitExpression: 17, ClassExpression: 17, FunctionExpression: 17, ObjectExpression: 17, UpdateExpression: 16, UnaryExpression: 15, BinaryExpression: 14, LogicalExpression: 13, ConditionalExpression: 4, ArrowFunctionExpression: 3, AssignmentExpression: 3, YieldExpression: 2, RestElement: 1 }; /** * * @param {Expression} node * @param {BinaryExpression | LogicalExpression} parent * @param {boolean} is_right * @returns */ function needs_parens(node, parent, is_right) { // special case where logical expressions and coalesce expressions cannot be mixed, // either of them need to be wrapped with parentheses if ( node.type === 'LogicalExpression' && parent.type === 'LogicalExpression' && ((parent.operator === '??' && node.operator !== '??') || (parent.operator !== '??' && node.operator === '??')) ) { return true; } const precedence = EXPRESSIONS_PRECEDENCE[node.type]; const parent_precedence = EXPRESSIONS_PRECEDENCE[parent.type]; if (precedence !== parent_precedence) { // Different node types return ( (!is_right && precedence === 15 && parent_precedence === 14 && parent.operator === '**') || precedence < parent_precedence ); } if (precedence !== 13 && precedence !== 14) { // Not a `LogicalExpression` or `BinaryExpression` return false; } if ( /** @type {BinaryExpression} */ (node).operator === '**' && parent.operator === '**' ) { // Exponentiation operator has right-to-left associativity return !is_right; } if (is_right) { // Parenthesis are used if both operators have the same precedence return ( OPERATOR_PRECEDENCE[/** @type {BinaryExpression} */ (node).operator] <= OPERATOR_PRECEDENCE[parent.operator] ); } return ( OPERATOR_PRECEDENCE[/** @type {BinaryExpression} */ (node).operator] < OPERATOR_PRECEDENCE[parent.operator] ); } /** @param {Node} node */ function has_call_expression(node) { while (node) { if (node.type[0] === 'CallExpression') { return true; } else if (node.type === 'MemberExpression') { node = node.object; } else { return false; } } } /** @param {Chunk[]} chunks */ const has_newline = (chunks) => { for (let i = 0; i < chunks.length; i += 1) { if (chunks[i].has_newline) return true; } return false; }; /** @param {Chunk[]} chunks */ const get_length = (chunks) => { let total = 0; for (let i = 0; i < chunks.length; i += 1) { total += chunks[i].content.length; } return total; }; /** * @param {number} a * @param {number} b */ const sum = (a, b) => a + b; /** * @param {Chunk[][]} nodes * @param {Chunk} separator * @returns {Chunk[]} */ const join = (nodes, separator) => { if (nodes.length === 0) return []; const joined = [...nodes[0]]; for (let i = 1; i < nodes.length; i += 1) { joined.push(separator); push_array(joined, nodes[i]); } return joined; }; /** * @param {(node: any, state: State) => Chunk[]} fn */ const scoped = (fn) => { /** * @param {any} node * @param {State} state */ const scoped_fn = (node, state) => { return fn(node, { ...state, scope: state.scope_map.get(node) }); }; return scoped_fn; }; /** * @param {string} name * @param {Set} names */ const deconflict = (name, names) => { const original = name; let i = 1; while (names.has(name)) { name = `${original}$${i++}`; } return name; }; /** * @param {Node[]} nodes * @param {State} state */ const handle_body = (nodes, state) => { const chunks = []; const body = nodes.map((statement) => { const chunks = handle(statement, { ...state, indent: state.indent }); let add_newline = false; while (state.comments.length) { const comment = state.comments.shift(); const prefix = add_newline ? `\n${state.indent}` : ` `; chunks.push( c( comment.type === 'Block' ? `${prefix}/*${comment.value}*/` : `${prefix}//${comment.value}` ) ); add_newline = comment.type === 'Line'; } return chunks; }); let needed_padding = false; for (let i = 0; i < body.length; i += 1) { const needs_padding = has_newline(body[i]); if (i > 0) { chunks.push( c( needs_padding || needed_padding ? `\n\n${state.indent}` : `\n${state.indent}` ) ); } push_array(chunks, body[i]); needed_padding = needs_padding; } return chunks; }; /** * @param {VariableDeclaration} node * @param {State} state */ const handle_var_declaration = (node, state) => { const chunks = [c(`${node.kind} `)]; const declarators = node.declarations.map((d) => handle(d, { ...state, indent: state.indent + (node.declarations.length === 1 ? '' : '\t') }) ); const multiple_lines = declarators.some(has_newline) || declarators.map(get_length).reduce(sum, 0) + (state.indent.length + declarators.length - 1) * 2 > 80; const separator = c(multiple_lines ? `,\n${state.indent}\t` : ', '); push_array(chunks, join(declarators, separator)); return chunks; }; /** @type {Record} */ const handlers = { Program(node, state) { return handle_body(node.body, state); }, BlockStatement: scoped((node, state) => { return [ c(`{\n${state.indent}\t`), ...handle_body(node.body, { ...state, indent: state.indent + '\t' }), c(`\n${state.indent}}`) ]; }), EmptyStatement(node, state) { return [c(';')]; }, ParenthesizedExpression(node, state) { return handle(node.expression, state); }, ExpressionStatement(node, state) { if ( node.expression.type === 'AssignmentExpression' && node.expression.left.type === 'ObjectPattern' ) { // is an AssignmentExpression to an ObjectPattern return [c('('), ...handle(node.expression, state), c(');')]; } return [...handle(node.expression, state), c(';')]; }, IfStatement(node, state) { const chunks = [ c('if ('), ...handle(node.test, state), c(') '), ...handle(node.consequent, state) ]; if (node.alternate) { chunks.push(c(' else ')); push_array(chunks, handle(node.alternate, state)); } return chunks; }, LabeledStatement(node, state) { return [...handle(node.label, state), c(': '), ...handle(node.body, state)]; }, BreakStatement(node, state) { return node.label ? [c('break '), ...handle(node.label, state), c(';')] : [c('break;')]; }, ContinueStatement(node, state) { return node.label ? [c('continue '), ...handle(node.label, state), c(';')] : [c('continue;')]; }, WithStatement(node, state) { return [ c('with ('), ...handle(node.object, state), c(') '), ...handle(node.body, state) ]; }, SwitchStatement(/** @type {SwitchStatement} */ node, state) { const chunks = [ c('switch ('), ...handle(node.discriminant, state), c(') {') ]; node.cases.forEach((block) => { if (block.test) { chunks.push(c(`\n${state.indent}\tcase `)); push_array( chunks, handle(block.test, { ...state, indent: `${state.indent}\t` }) ); chunks.push(c(':')); } else { chunks.push(c(`\n${state.indent}\tdefault:`)); } block.consequent.forEach((statement) => { chunks.push(c(`\n${state.indent}\t\t`)); push_array( chunks, handle(statement, { ...state, indent: `${state.indent}\t\t` }) ); }); }); chunks.push(c(`\n${state.indent}}`)); return chunks; }, ReturnStatement(node, state) { if (node.argument) { const contains_comment = node.argument.leadingComments && node.argument.leadingComments.some( ( /** @type import('../utils/comments.js').CommentWithLocation */ comment ) => comment.has_trailing_newline ); return [ c(contains_comment ? 'return (' : 'return '), ...handle(node.argument, state), c(contains_comment ? ');' : ';') ]; } else { return [c('return;')]; } }, ThrowStatement(node, state) { return [c('throw '), ...handle(node.argument, state), c(';')]; }, TryStatement(node, state) { const chunks = [c('try '), ...handle(node.block, state)]; if (node.handler) { if (node.handler.param) { chunks.push(c(' catch(')); push_array(chunks, handle(node.handler.param, state)); chunks.push(c(') ')); } else { chunks.push(c(' catch ')); } push_array(chunks, handle(node.handler.body, state)); } if (node.finalizer) { chunks.push(c(' finally ')); push_array(chunks, handle(node.finalizer, state)); } return chunks; }, WhileStatement(node, state) { return [ c('while ('), ...handle(node.test, state), c(') '), ...handle(node.body, state) ]; }, DoWhileStatement(node, state) { return [ c('do '), ...handle(node.body, state), c(' while ('), ...handle(node.test, state), c(');') ]; }, ForStatement: scoped((node, state) => { const chunks = [c('for (')]; if (node.init) { if (node.init.type === 'VariableDeclaration') { push_array(chunks, handle_var_declaration(node.init, state)); } else { push_array(chunks, handle(node.init, state)); } } chunks.push(c('; ')); if (node.test) push_array(chunks, handle(node.test, state)); chunks.push(c('; ')); if (node.update) push_array(chunks, handle(node.update, state)); chunks.push(c(') ')); push_array(chunks, handle(node.body, state)); return chunks; }), ForInStatement: scoped((node, state) => { const chunks = [c(`for ${node.await ? 'await ' : ''}(`)]; if (node.left.type === 'VariableDeclaration') { push_array(chunks, handle_var_declaration(node.left, state)); } else { push_array(chunks, handle(node.left, state)); } chunks.push(c(node.type === 'ForInStatement' ? ` in ` : ` of `)); push_array(chunks, handle(node.right, state)); chunks.push(c(') ')); push_array(chunks, handle(node.body, state)); return chunks; }), DebuggerStatement(node, state) { return [c('debugger', node), c(';')]; }, FunctionDeclaration: scoped( (/** @type {FunctionDeclaration} */ node, state) => { const chunks = []; if (node.async) chunks.push(c('async ')); chunks.push(c(node.generator ? 'function* ' : 'function ')); if (node.id) push_array(chunks, handle(node.id, state)); chunks.push(c('(')); const params = node.params.map((p) => handle(p, { ...state, indent: state.indent + '\t' }) ); const multiple_lines = params.some(has_newline) || params.map(get_length).reduce(sum, 0) + (state.indent.length + params.length - 1) * 2 > 80; const separator = c(multiple_lines ? `,\n${state.indent}` : ', '); if (multiple_lines) { chunks.push(c(`\n${state.indent}\t`)); push_array(chunks, join(params, separator)); chunks.push(c(`\n${state.indent}`)); } else { push_array(chunks, join(params, separator)); } chunks.push(c(') ')); push_array(chunks, handle(node.body, state)); return chunks; } ), VariableDeclaration(node, state) { return handle_var_declaration(node, state).concat(c(';')); }, VariableDeclarator(node, state) { if (node.init) { return [...handle(node.id, state), c(' = '), ...handle(node.init, state)]; } else { return handle(node.id, state); } }, ClassDeclaration(node, state) { const chunks = [c('class ')]; if (node.id) { push_array(chunks, handle(node.id, state)); chunks.push(c(' ')); } if (node.superClass) { chunks.push(c('extends ')); push_array(chunks, handle(node.superClass, state)); chunks.push(c(' ')); } push_array(chunks, handle(node.body, state)); return chunks; }, ImportDeclaration(/** @type {ImportDeclaration} */ node, state) { const chunks = [c('import ')]; const { length } = node.specifiers; const source = handle(node.source, state); if (length > 0) { let i = 0; while (i < length) { if (i > 0) { chunks.push(c(', ')); } const specifier = node.specifiers[i]; if (specifier.type === 'ImportDefaultSpecifier') { chunks.push(c(specifier.local.name, specifier)); i += 1; } else if (specifier.type === 'ImportNamespaceSpecifier') { chunks.push(c('* as ' + specifier.local.name, specifier)); i += 1; } else { break; } } if (i < length) { // we have named specifiers const specifiers = node.specifiers .slice(i) .map((/** @type {ImportSpecifier} */ specifier) => { const name = handle(specifier.imported, state)[0]; const as = handle(specifier.local, state)[0]; if (name.content === as.content) { return [as]; } return [name, c(' as '), as]; }); const width = get_length(chunks) + specifiers.map(get_length).reduce(sum, 0) + 2 * specifiers.length + 6 + get_length(source); if (width > 80) { chunks.push(c(`{\n\t`)); push_array(chunks, join(specifiers, c(',\n\t'))); chunks.push(c('\n}')); } else { chunks.push(c(`{ `)); push_array(chunks, join(specifiers, c(', '))); chunks.push(c(' }')); } } chunks.push(c(' from ')); } push_array(chunks, source); chunks.push(c(';')); return chunks; }, ImportExpression(node, state) { return [c('import('), ...handle(node.source, state), c(')')]; }, ExportDefaultDeclaration(node, state) { const chunks = [c(`export default `), ...handle(node.declaration, state)]; if (node.declaration.type !== 'FunctionDeclaration') { chunks.push(c(';')); } return chunks; }, ExportNamedDeclaration(node, state) { const chunks = [c('export ')]; if (node.declaration) { push_array(chunks, handle(node.declaration, state)); } else { const specifiers = node.specifiers.map( (/** @type {ExportSpecifier} */ specifier) => { const name = handle(specifier.local, state)[0]; const as = handle(specifier.exported, state)[0]; if (name.content === as.content) { return [name]; } return [name, c(' as '), as]; } ); const width = 7 + specifiers.map(get_length).reduce(sum, 0) + 2 * specifiers.length; if (width > 80) { chunks.push(c('{\n\t')); push_array(chunks, join(specifiers, c(',\n\t'))); chunks.push(c('\n}')); } else { chunks.push(c('{ ')); push_array(chunks, join(specifiers, c(', '))); chunks.push(c(' }')); } if (node.source) { chunks.push(c(' from ')); push_array(chunks, handle(node.source, state)); } } chunks.push(c(';')); return chunks; }, ExportAllDeclaration(node, state) { return [c(`export * from `), ...handle(node.source, state), c(`;`)]; }, MethodDefinition(node, state) { const chunks = []; if (node.static) { chunks.push(c('static ')); } if (node.kind === 'get' || node.kind === 'set') { // Getter or setter chunks.push(c(node.kind + ' ')); } if (node.value.async) { chunks.push(c('async ')); } if (node.value.generator) { chunks.push(c('*')); } if (node.computed) { chunks.push(c('[')); push_array(chunks, handle(node.key, state)); chunks.push(c(']')); } else { push_array(chunks, handle(node.key, state)); } chunks.push(c('(')); const { params } = node.value; for (let i = 0; i < params.length; i += 1) { push_array(chunks, handle(params[i], state)); if (i < params.length - 1) chunks.push(c(', ')); } chunks.push(c(') ')); push_array(chunks, handle(node.value.body, state)); return chunks; }, ArrowFunctionExpression: scoped( (/** @type {ArrowFunctionExpression} */ node, state) => { const chunks = []; if (node.async) chunks.push(c('async ')); if (node.params.length === 1 && node.params[0].type === 'Identifier') { push_array(chunks, handle(node.params[0], state)); } else { const params = node.params.map((param) => handle(param, { ...state, indent: state.indent + '\t' }) ); chunks.push(c('(')); push_array(chunks, join(params, c(', '))); chunks.push(c(')')); } chunks.push(c(' => ')); if ( node.body.type === 'ObjectExpression' || (node.body.type === 'AssignmentExpression' && node.body.left.type === 'ObjectPattern') ) { chunks.push(c('(')); push_array(chunks, handle(node.body, state)); chunks.push(c(')')); } else { push_array(chunks, handle(node.body, state)); } return chunks; } ), ThisExpression(node, state) { return [c('this', node)]; }, Super(node, state) { return [c('super', node)]; }, RestElement(node, state) { return [c('...'), ...handle(node.argument, state)]; }, YieldExpression(node, state) { if (node.argument) { return [ c(node.delegate ? `yield* ` : `yield `), ...handle(node.argument, state) ]; } return [c(node.delegate ? `yield*` : `yield`)]; }, AwaitExpression(node, state) { if (node.argument) { const precedence = EXPRESSIONS_PRECEDENCE[node.argument.type]; if (precedence && precedence < EXPRESSIONS_PRECEDENCE.AwaitExpression) { return [c('await ('), ...handle(node.argument, state), c(')')]; } else { return [c('await '), ...handle(node.argument, state)]; } } return [c('await')]; }, TemplateLiteral(node, state) { const chunks = [c('`')]; const { quasis, expressions } = node; for (let i = 0; i < expressions.length; i++) { chunks.push(c(quasis[i].value.raw), c('${')); push_array(chunks, handle(expressions[i], state)); chunks.push(c('}')); } chunks.push(c(quasis[quasis.length - 1].value.raw), c('`')); return chunks; }, TaggedTemplateExpression(node, state) { return handle(node.tag, state).concat(handle(node.quasi, state)); }, ArrayExpression(node, state) { const chunks = [c('[')]; /** @type {Chunk[][]} */ const elements = []; /** @type {Chunk[]} */ let sparse_commas = []; for (let i = 0; i < node.elements.length; i += 1) { // can't use map/forEach because of sparse arrays const element = node.elements[i]; if (element) { elements.push([ ...sparse_commas, ...handle(element, { ...state, indent: state.indent + '\t' }) ]); sparse_commas = []; } else { sparse_commas.push(c(',')); } } const multiple_lines = elements.some(has_newline) || elements.map(get_length).reduce(sum, 0) + (state.indent.length + elements.length - 1) * 2 > 80; if (multiple_lines) { chunks.push(c(`\n${state.indent}\t`)); push_array(chunks, join(elements, c(`,\n${state.indent}\t`))); chunks.push(c(`\n${state.indent}`)); push_array(chunks, sparse_commas); } else { push_array(chunks, join(elements, c(', '))); push_array(chunks, sparse_commas); } chunks.push(c(']')); return chunks; }, ObjectExpression(/** @type {ObjectExpression} */ node, state) { if (node.properties.length === 0) { return [c('{}')]; } let has_inline_comment = false; /** @type {Chunk[]} */ const chunks = []; const separator = c(', '); node.properties.forEach((p, i) => { push_array( chunks, handle(p, { ...state, indent: state.indent + '\t' }) ); if (state.comments.length) { // TODO generalise this, so it works with ArrayExpressions and other things. // At present, stuff will just get appended to the closest statement/declaration chunks.push(c(', ')); while (state.comments.length) { const comment = state.comments.shift(); chunks.push( c( comment.type === 'Block' ? `/*${comment.value}*/\n${state.indent}\t` : `//${comment.value}\n${state.indent}\t` ) ); if (comment.type === 'Line') { has_inline_comment = true; } } } else { if (i < node.properties.length - 1) { chunks.push(separator); } } }); const multiple_lines = has_inline_comment || has_newline(chunks) || get_length(chunks) > 40; if (multiple_lines) { separator.content = `,\n${state.indent}\t`; } return [ c(multiple_lines ? `{\n${state.indent}\t` : `{ `), ...chunks, c(multiple_lines ? `\n${state.indent}}` : ` }`) ]; }, Property(node, state) { const value = handle(node.value, state); if (node.key === node.value) { return value; } // special case if ( !node.computed && node.value.type === 'AssignmentPattern' && node.value.left.type === 'Identifier' && node.value.left.name === node.key.name ) { return value; } if ( !node.computed && node.value.type === 'Identifier' && ((node.key.type === 'Identifier' && node.key.name === value[0].content) || (node.key.type === 'Literal' && node.key.value === value[0].content)) ) { return value; } const key = handle(node.key, state); if (node.value.type === 'FunctionExpression' && !node.value.id) { state = { ...state, scope: state.scope_map.get(node.value) }; const chunks = node.kind !== 'init' ? [c(`${node.kind} `)] : []; if (node.value.async) { chunks.push(c('async ')); } if (node.value.generator) { chunks.push(c('*')); } push_array(chunks, node.computed ? [c('['), ...key, c(']')] : key); chunks.push(c('(')); push_array( chunks, join( node.value.params.map((/** @type {Pattern} */ param) => handle(param, state) ), c(', ') ) ); chunks.push(c(') ')); push_array(chunks, handle(node.value.body, state)); return chunks; } if (node.computed) { return [c('['), ...key, c(']: '), ...value]; } return [...key, c(': '), ...value]; }, ObjectPattern(node, state) { const chunks = [c('{ ')]; for (let i = 0; i < node.properties.length; i += 1) { push_array(chunks, handle(node.properties[i], state)); if (i < node.properties.length - 1) chunks.push(c(', ')); } chunks.push(c(' }')); return chunks; }, SequenceExpression(/** @type {SequenceExpression} */ node, state) { const expressions = node.expressions.map((e) => handle(e, state)); return [c('('), ...join(expressions, c(', ')), c(')')]; }, UnaryExpression(node, state) { const chunks = [c(node.operator)]; if (node.operator.length > 1) { chunks.push(c(' ')); } if ( EXPRESSIONS_PRECEDENCE[node.argument.type] < EXPRESSIONS_PRECEDENCE.UnaryExpression ) { chunks.push(c('(')); push_array(chunks, handle(node.argument, state)); chunks.push(c(')')); } else { push_array(chunks, handle(node.argument, state)); } return chunks; }, UpdateExpression(node, state) { return node.prefix ? [c(node.operator), ...handle(node.argument, state)] : [...handle(node.argument, state), c(node.operator)]; }, AssignmentExpression(node, state) { return [ ...handle(node.left, state), c(` ${node.operator || '='} `), ...handle(node.right, state) ]; }, BinaryExpression(node, state) { /** * @type any[] */ const chunks = []; // TODO // const is_in = node.operator === 'in'; // if (is_in) { // // Avoids confusion in `for` loops initializers // chunks.push(c('(')); // } if (needs_parens(node.left, node, false)) { chunks.push(c('(')); push_array(chunks, handle(node.left, state)); chunks.push(c(')')); } else { push_array(chunks, handle(node.left, state)); } chunks.push(c(` ${node.operator} `)); if (needs_parens(node.right, node, true)) { chunks.push(c('(')); push_array(chunks, handle(node.right, state)); chunks.push(c(')')); } else { push_array(chunks, handle(node.right, state)); } return chunks; }, ConditionalExpression(node, state) { /** * @type any[] */ const chunks = []; if ( EXPRESSIONS_PRECEDENCE[node.test.type] > EXPRESSIONS_PRECEDENCE.ConditionalExpression ) { push_array(chunks, handle(node.test, state)); } else { chunks.push(c('(')); push_array(chunks, handle(node.test, state)); chunks.push(c(')')); } const child_state = { ...state, indent: state.indent + '\t' }; const consequent = handle(node.consequent, child_state); const alternate = handle(node.alternate, child_state); const multiple_lines = has_newline(consequent) || has_newline(alternate) || get_length(chunks) + get_length(consequent) + get_length(alternate) > 50; if (multiple_lines) { chunks.push(c(`\n${state.indent}? `)); push_array(chunks, consequent); chunks.push(c(`\n${state.indent}: `)); push_array(chunks, alternate); } else { chunks.push(c(` ? `)); push_array(chunks, consequent); chunks.push(c(` : `)); push_array(chunks, alternate); } return chunks; }, NewExpression(/** @type {NewExpression} */ node, state) { const chunks = [c('new ')]; if ( EXPRESSIONS_PRECEDENCE[node.callee.type] < EXPRESSIONS_PRECEDENCE.CallExpression || has_call_expression(node.callee) ) { chunks.push(c('(')); push_array(chunks, handle(node.callee, state)); chunks.push(c(')')); } else { push_array(chunks, handle(node.callee, state)); } // TODO this is copied from CallExpression — DRY it out const args = node.arguments.map((arg) => handle(arg, { ...state, indent: state.indent + '\t' }) ); const separator = args.some(has_newline) // TODO or length exceeds 80 ? c(',\n' + state.indent) : c(', '); chunks.push(c('(')); push_array(chunks, join(args, separator)); chunks.push(c(')')); return chunks; }, ChainExpression(node, state) { return handle(node.expression, state); }, CallExpression(/** @type {CallExpression} */ node, state) { /** * @type any[] */ const chunks = []; if ( EXPRESSIONS_PRECEDENCE[node.callee.type] < EXPRESSIONS_PRECEDENCE.CallExpression ) { chunks.push(c('(')); push_array(chunks, handle(node.callee, state)); chunks.push(c(')')); } else { push_array(chunks, handle(node.callee, state)); } if (/** @type {SimpleCallExpression} */ (node).optional) { chunks.push(c('?.')); } let has_inline_comment = false; let arg_chunks = []; outer: for (const arg of node.arguments) { const chunks = []; while (state.comments.length) { const comment = state.comments.shift(); if (comment.type === 'Line') { has_inline_comment = true; break outer; } chunks.push( c( comment.type === 'Block' ? `/*${comment.value}*/ ` : `//${comment.value}` ) ); } push_array(chunks, handle(arg, state)); arg_chunks.push(chunks); } const multiple_lines = has_inline_comment || arg_chunks.slice(0, -1).some(has_newline); // TODO or length exceeds 80 if (multiple_lines) { // need to handle args again. TODO find alternative approach? const args = node.arguments.map((arg, i) => { const chunks = handle(arg, { ...state, indent: `${state.indent}\t` }); if (i < node.arguments.length - 1) chunks.push(c(',')); while (state.comments.length) { const comment = state.comments.shift(); chunks.push( c( comment.type === 'Block' ? ` /*${comment.value}*/ ` : ` //${comment.value}` ) ); } return chunks; }); chunks.push(c(`(\n${state.indent}\t`)); push_array(chunks, join(args, c(`\n${state.indent}\t`))); chunks.push(c(`\n${state.indent})`)); } else { chunks.push(c('(')); push_array(chunks, join(arg_chunks, c(', '))); chunks.push(c(')')); } return chunks; }, MemberExpression(node, state) { /** * @type any[] */ const chunks = []; if ( EXPRESSIONS_PRECEDENCE[node.object.type] < EXPRESSIONS_PRECEDENCE.MemberExpression ) { chunks.push(c('(')); push_array(chunks, handle(node.object, state)); chunks.push(c(')')); } else { push_array(chunks, handle(node.object, state)); } if (node.computed) { if (node.optional) { chunks.push(c('?.')); } chunks.push(c('[')); push_array(chunks, handle(node.property, state)); chunks.push(c(']')); } else { chunks.push(c(node.optional ? '?.' : '.')); push_array(chunks, handle(node.property, state)); } return chunks; }, MetaProperty(node, state) { return [ ...handle(node.meta, state), c('.'), ...handle(node.property, state) ]; }, Identifier(node, state) { let name = node.name; if (name[0] === '@') { name = state.getName(name.slice(1)); } else if (node.name[0] === '#') { const owner = state.scope.find_owner(node.name); if (!owner) { throw new Error(`Could not find owner for node`); } if (!state.deconflicted.has(owner)) { state.deconflicted.set(owner, new Map()); } const deconflict_map = state.deconflicted.get(owner); if (!deconflict_map.has(node.name)) { deconflict_map.set( node.name, deconflict(node.name.slice(1), owner.references) ); } name = deconflict_map.get(node.name); } return [c(name, node)]; }, Literal(/** @type {Literal} */ node, state) { if (typeof node.value === 'string') { return [ // TODO do we need to handle weird unicode characters somehow? // str.replace(/\\u(\d{4})/g, (m, n) => String.fromCharCode(+n)) c( (node.raw || JSON.stringify(node.value)).replace( re, (_m, _i, at, hash, name) => { if (at) return '@' + name; if (hash) return '#' + name; throw new Error(`this shouldn't happen`); } ), node ) ]; } return [c(node.raw || String(node.value), node)]; }, PropertyDefinition(/** @type {PropertyDefinition} */ node, state) { const chunks = []; if (node.static) { chunks.push(c('static ')); } if (node.computed) { chunks.push(c('['), ...handle(node.key, state), c(']')); } else { chunks.push(...handle(node.key, state)); } if (node.value) { chunks.push(c(' = ')); chunks.push(...handle(node.value, state)); } chunks.push(c(';')); return chunks; }, StaticBlock(/** @type {StaticBlock} */ node, state) { const chunks = [c('static ')]; push_array(chunks, handlers.BlockStatement(node, state)); return chunks; }, PrivateIdentifier(/** @type {PrivateIdenifier} */ node, state) { const chunks = [c('#')]; push_array(chunks, [c(node.name, node)]); return chunks; } }; handlers.ForOfStatement = handlers.ForInStatement; handlers.FunctionExpression = handlers.FunctionDeclaration; handlers.ClassExpression = handlers.ClassDeclaration; handlers.ClassBody = handlers.BlockStatement; handlers.SpreadElement = handlers.RestElement; handlers.ArrayPattern = handlers.ArrayExpression; handlers.LogicalExpression = handlers.BinaryExpression; handlers.AssignmentPattern = handlers.AssignmentExpression;