1514 lines
34 KiB
JavaScript
Executable File
1514 lines
34 KiB
JavaScript
Executable File
// 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<Node, any>;
|
|
* getName: (name: string) => string;
|
|
* deconflicted: WeakMap<Node, Map<string, string>>;
|
|
* 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<string, number>} */
|
|
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<string>} 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<string, Handler>} */
|
|
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;
|