2025-03-24 22:56:10 +01:00

1562 lines
38 KiB
JavaScript

/** @import { TSESTree } from '@typescript-eslint/types' */
/** @import { Chunk, Command, Dedent, Handlers, Indent, Newline, NodeWithComments, Sequence, State, TypeAnnotationNodes } from './types' */
/** @type {Newline} */
const newline = { type: 'Newline' };
/** @type {Indent} */
const indent = { type: 'Indent' };
/** @type {Dedent} */
const dedent = { type: 'Dedent' };
/**
* @param {Command[]} children
* @returns {Sequence}
*/
function create_sequence(...children) {
return { type: 'Sequence', children };
}
/**
* Rough estimate of the combined width of a group of commands
* @param {Command[]} commands
* @param {number} from
* @param {number} to
*/
function measure(commands, from, to = commands.length) {
let total = 0;
for (let i = from; i < to; i += 1) {
const command = commands[i];
if (typeof command === 'string') {
total += command.length;
} else if (command.type === 'Chunk') {
total += command.content.length;
} else if (command.type === 'Sequence') {
// assume this is ', '
total += 2;
}
}
return total;
}
/**
* @param {TSESTree.Node} node
* @param {State} state
*/
export function handle(node, state) {
const node_with_comments = /** @type {NodeWithComments} */ (node);
const handler = handlers[node.type];
if (!handler) {
throw new Error(`Not implemented ${node.type}`);
}
if (node_with_comments.leadingComments) {
prepend_comments(node_with_comments.leadingComments, state, false);
}
// @ts-expect-error
handler(node, state);
if (node_with_comments.trailingComments) {
state.comments.push(node_with_comments.trailingComments[0]); // there is only ever one
}
}
/**
* @param {string} content
* @param {TSESTree.Node} node
* @returns {Chunk}
*/
function c(content, node) {
return {
type: 'Chunk',
content,
loc: node?.loc ?? null
};
}
/**
* @param {TSESTree.Comment[]} comments
* @param {State} state
* @param {boolean} newlines
*/
function prepend_comments(comments, state, newlines) {
for (const comment of comments) {
state.commands.push({ type: 'Comment', comment });
if (newlines || comment.type === 'Line' || /\n/.test(comment.value)) {
state.commands.push(newline);
} else {
state.commands.push(' ');
}
}
}
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<TSESTree.Expression['type'] | 'Super' | 'RestElement', number>} */
const EXPRESSIONS_PRECEDENCE = {
JSXFragment: 20,
JSXElement: 20,
ArrayPattern: 20,
ObjectPattern: 20,
ArrayExpression: 20,
TaggedTemplateExpression: 20,
ThisExpression: 20,
Identifier: 20,
TemplateLiteral: 20,
Super: 20,
SequenceExpression: 20,
MemberExpression: 19,
MetaProperty: 19,
CallExpression: 19,
ChainExpression: 19,
ImportExpression: 19,
NewExpression: 19,
Literal: 18,
TSSatisfiesExpression: 18,
TSInstantiationExpression: 18,
TSNonNullExpression: 18,
TSTypeAssertion: 18,
AwaitExpression: 17,
ClassExpression: 17,
FunctionExpression: 17,
ObjectExpression: 17,
TSAsExpression: 16,
UpdateExpression: 16,
UnaryExpression: 15,
BinaryExpression: 14,
LogicalExpression: 13,
ConditionalExpression: 4,
ArrowFunctionExpression: 3,
AssignmentExpression: 3,
YieldExpression: 2,
RestElement: 1
};
/**
*
* @param {TSESTree.Expression | TSESTree.PrivateIdentifier} node
* @param {TSESTree.BinaryExpression | TSESTree.LogicalExpression} parent
* @param {boolean} is_right
* @returns
*/
function needs_parens(node, parent, is_right) {
if (node.type === 'PrivateIdentifier') return false;
// 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 {TSESTree.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 {TSESTree.BinaryExpression} */ (node).operator] <=
OPERATOR_PRECEDENCE[parent.operator]
);
}
return (
OPERATOR_PRECEDENCE[/** @type {TSESTree.BinaryExpression} */ (node).operator] <
OPERATOR_PRECEDENCE[parent.operator]
);
}
/** @param {TSESTree.Node} node */
function has_call_expression(node) {
while (node) {
if (node.type === 'CallExpression') {
return true;
} else if (node.type === 'MemberExpression') {
node = node.object;
} else {
return false;
}
}
}
const grouped_expression_types = [
'ImportDeclaration',
'VariableDeclaration',
'ExportDefaultDeclaration',
'ExportNamedDeclaration'
];
/**
* @param {TSESTree.Node[]} nodes
* @param {State} state
*/
const handle_body = (nodes, state) => {
let last_statement = /** @type {TSESTree.Node} */ ({
type: 'EmptyStatement'
});
let first = true;
let needs_margin = false;
for (const statement of nodes) {
if (statement.type === 'EmptyStatement') continue;
const margin = create_sequence();
if (!first) state.commands.push(margin, newline);
first = false;
const statement_with_comments = /** @type {NodeWithComments} */ (statement);
const leading_comments = statement_with_comments.leadingComments;
delete statement_with_comments.leadingComments;
if (leading_comments && leading_comments.length > 0) {
prepend_comments(leading_comments, state, true);
}
const child_state = { ...state, multiline: false };
handle(statement, child_state);
if (
child_state.multiline ||
needs_margin ||
((grouped_expression_types.includes(statement.type) ||
grouped_expression_types.includes(last_statement.type)) &&
last_statement.type !== statement.type)
) {
margin.children.push('\n');
}
let add_newline = false;
while (state.comments.length) {
const comment = /** @type {TSESTree.Comment} */ (state.comments.shift());
state.commands.push(add_newline ? newline : ' ', { type: 'Comment', comment });
add_newline = comment.type === 'Line';
}
needs_margin = child_state.multiline;
last_statement = statement;
}
};
/**
* @param {TSESTree.VariableDeclaration} node
* @param {State} state
*/
const handle_var_declaration = (node, state) => {
const index = state.commands.length;
const open = create_sequence();
const join = create_sequence();
const child_state = { ...state, multiline: false };
state.commands.push(`${node.kind} `, open);
let first = true;
for (const d of node.declarations) {
if (!first) state.commands.push(join);
first = false;
handle(d, child_state);
}
const multiline =
child_state.multiline || (node.declarations.length > 1 && measure(state.commands, index) > 50);
if (multiline) {
state.multiline = true;
if (node.declarations.length > 1) open.children.push(indent);
join.children.push(',', newline);
if (node.declarations.length > 1) state.commands.push(dedent);
} else {
join.children.push(', ');
}
};
/**
* @template {TSESTree.Node} T
* @param {Array<T | null>} nodes
* @param {State} state
* @param {boolean} spaces
* @param {(node: T, state: State) => void} fn
*/
function sequence(nodes, state, spaces, fn, separator = ',') {
if (nodes.length === 0) return;
const index = state.commands.length;
const open = create_sequence();
const join = create_sequence();
const close = create_sequence();
state.commands.push(open);
const child_state = { ...state, multiline: false };
let prev;
for (let i = 0; i < nodes.length; i += 1) {
const node = nodes[i];
const is_first = i === 0;
const is_last = i === nodes.length - 1;
if (node) {
if (!is_first && !prev) {
state.commands.push(join);
}
fn(node, child_state);
if (!is_last) {
state.commands.push(separator);
}
if (state.comments.length > 0) {
state.commands.push(' ');
while (state.comments.length) {
const comment = /** @type {TSESTree.Comment} */ (state.comments.shift());
state.commands.push({ type: 'Comment', comment });
if (!is_last) state.commands.push(join);
}
child_state.multiline = true;
} else {
if (!is_last) state.commands.push(join);
}
} else {
// This is only used for ArrayPattern and ArrayExpression, but
// it makes more sense to have the logic here than there, because
// otherwise we'd duplicate a lot more stuff
state.commands.push(separator);
}
prev = node;
}
state.commands.push(close);
const multiline = child_state.multiline || measure(state.commands, index) > 50;
if (multiline) {
state.multiline = true;
open.children.push(indent, newline);
join.children.push(newline);
close.children.push(dedent, newline);
} else {
if (spaces) open.children.push(' ');
join.children.push(' ');
if (spaces) close.children.push(' ');
}
}
/**
* @param {TypeAnnotationNodes} node
* @param {State} state
*/
function handle_type_annotation(node, state) {
switch (node.type) {
case 'TSNumberKeyword':
state.commands.push('number');
break;
case 'TSStringKeyword':
state.commands.push('string');
break;
case 'TSBooleanKeyword':
state.commands.push('boolean');
break;
case 'TSAnyKeyword':
state.commands.push('any');
break;
case 'TSVoidKeyword':
state.commands.push('void');
break;
case 'TSUnknownKeyword':
state.commands.push('unknown');
break;
case 'TSNeverKeyword':
state.commands.push('never');
break;
case 'TSArrayType':
handle_type_annotation(node.elementType, state);
state.commands.push('[]');
break;
case 'TSTypeAnnotation':
state.commands.push(': ');
handle_type_annotation(node.typeAnnotation, state);
break;
case 'TSTypeLiteral':
state.commands.push('{ ');
sequence(node.members, state, false, handle_type_annotation, ';');
state.commands.push(' }');
break;
case 'TSPropertySignature':
handle(node.key, state);
if (node.optional) state.commands.push('?');
if (node.typeAnnotation) handle_type_annotation(node.typeAnnotation, state);
break;
case 'TSTypeReference':
handle(node.typeName, state);
// @ts-expect-error `acorn-typescript` and `@typescript-esling/types` have slightly different type definitions
if (node.typeParameters) handle_type_annotation(node.typeParameters, state);
break;
case 'TSTypeParameterInstantiation':
case 'TSTypeParameterDeclaration':
state.commands.push('<');
for (let i = 0; i < node.params.length; i++) {
handle_type_annotation(node.params[i], state);
if (i != node.params.length - 1) state.commands.push(', ');
}
state.commands.push('>');
break;
case 'TSTypeParameter':
// @ts-expect-error `acorn-typescript` and `@typescript-esling/types` have slightly different type definitions
state.commands.push(node.name);
if (node.constraint) {
state.commands.push(' extends ');
handle_type_annotation(node.constraint, state);
}
break;
case 'TSTypeQuery':
state.commands.push('typeof ');
handle(node.exprName, state);
break;
case 'TSEnumMember':
handle(node.id, state);
if (node.initializer) {
state.commands.push(' = ');
handle(node.initializer, state);
}
break;
case 'TSFunctionType':
if (node.typeParameters) handle_type_annotation(node.typeParameters, state);
// @ts-expect-error `acorn-typescript` and `@typescript-esling/types` have slightly different type definitions
const parameters = node.parameters;
state.commands.push('(');
sequence(parameters, state, false, handle);
state.commands.push(') => ');
// @ts-expect-error `acorn-typescript` and `@typescript-esling/types` have slightly different type definitions
handle_type_annotation(node.typeAnnotation.typeAnnotation, state);
break;
case 'TSIndexSignature':
const indexParameters = node.parameters;
state.commands.push('[');
sequence(indexParameters, state, false, handle);
state.commands.push(']');
// @ts-expect-error `acorn-typescript` and `@typescript-esling/types` have slightly different type definitions
handle_type_annotation(node.typeAnnotation, state);
break;
case 'TSMethodSignature':
handle(node.key, state);
// @ts-expect-error `acorn-typescript` and `@typescript-esling/types` have slightly different type definitions
const parametersSignature = node.parameters;
state.commands.push('(');
sequence(parametersSignature, state, false, handle);
state.commands.push(')');
// @ts-expect-error `acorn-typescript` and `@typescript-esling/types` have slightly different type definitions
handle_type_annotation(node.typeAnnotation, state);
break;
case 'TSExpressionWithTypeArguments':
handle(node.expression, state);
break;
case 'TSTupleType':
state.commands.push('[');
sequence(node.elementTypes, state, false, handle_type_annotation);
state.commands.push(']');
break;
case 'TSNamedTupleMember':
handle(node.label, state);
state.commands.push(': ');
handle_type_annotation(node.elementType, state);
break;
case 'TSUnionType':
sequence(node.types, state, false, handle_type_annotation, ' |');
break;
case 'TSIntersectionType':
sequence(node.types, state, false, handle_type_annotation, ' &');
break;
case 'TSLiteralType':
handle(node.literal, state);
break;
case 'TSConditionalType':
handle_type_annotation(node.checkType, state);
state.commands.push(' extends ');
handle_type_annotation(node.extendsType, state);
state.commands.push(' ? ');
handle_type_annotation(node.trueType, state);
state.commands.push(' : ');
handle_type_annotation(node.falseType, state);
break;
default:
throw new Error(`Not implemented type annotation ${node.type}`);
}
}
/** @satisfies {Record<string, (node: any, state: State) => undefined>} */
const shared = {
/**
* @param {TSESTree.ArrayExpression | TSESTree.ArrayPattern} node
* @param {State} state
*/
'ArrayExpression|ArrayPattern': (node, state) => {
state.commands.push('[');
sequence(/** @type {TSESTree.Node[]} */ (node.elements), state, false, handle);
state.commands.push(']');
},
/**
* @param {TSESTree.BinaryExpression | TSESTree.LogicalExpression} node
* @param {State} state
*/
'BinaryExpression|LogicalExpression': (node, state) => {
// 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)) {
state.commands.push('(');
handle(node.left, state);
state.commands.push(')');
} else {
handle(node.left, state);
}
state.commands.push(` ${node.operator} `);
if (needs_parens(node.right, node, true)) {
state.commands.push('(');
handle(node.right, state);
state.commands.push(')');
} else {
handle(node.right, state);
}
},
/**
* @param {TSESTree.BlockStatement | TSESTree.ClassBody} node
* @param {State} state
*/
'BlockStatement|ClassBody': (node, state) => {
if (node.body.length === 0) {
state.commands.push('{}');
return;
}
state.multiline = true;
state.commands.push('{', indent, newline);
handle_body(node.body, state);
state.commands.push(dedent, newline, '}');
},
/**
* @param {TSESTree.CallExpression | TSESTree.NewExpression} node
* @param {State} state
*/
'CallExpression|NewExpression': (node, state) => {
if (node.type === 'NewExpression') {
state.commands.push('new ');
}
const needs_parens =
EXPRESSIONS_PRECEDENCE[node.callee.type] < EXPRESSIONS_PRECEDENCE.CallExpression ||
(node.type === 'NewExpression' && has_call_expression(node.callee));
if (needs_parens) {
state.commands.push('(');
handle(node.callee, state);
state.commands.push(')');
} else {
handle(node.callee, state);
}
if (/** @type {TSESTree.CallExpression} */ (node).optional) {
state.commands.push('?.');
}
// @ts-expect-error
if (node.typeParameters) handle_type_annotation(node.typeParameters, state);
const open = create_sequence();
const join = create_sequence();
const close = create_sequence();
state.commands.push('(', open);
// if the final argument is multiline, it doesn't need to force all the
// other arguments to also be multiline
const child_state = { ...state, multiline: false };
const final_state = { ...state, multiline: false };
for (let i = 0; i < node.arguments.length; i += 1) {
if (i > 0) {
if (state.comments.length > 0) {
state.commands.push(', ');
while (state.comments.length) {
const comment = /** @type {TSESTree.Comment} */ (state.comments.shift());
state.commands.push({ type: 'Comment', comment });
if (comment.type === 'Line') {
child_state.multiline = true;
state.commands.push(newline);
} else {
state.commands.push(' ');
}
}
} else {
state.commands.push(join);
}
}
const p = node.arguments[i];
handle(p, i === node.arguments.length - 1 ? final_state : child_state);
}
state.commands.push(close, ')');
const multiline = child_state.multiline;
if (multiline || final_state.multiline) {
state.multiline = true;
}
if (multiline) {
open.children.push(indent, newline);
join.children.push(',', newline);
close.children.push(dedent, newline);
} else {
join.children.push(', ');
}
},
/**
* @param {TSESTree.ClassDeclaration | TSESTree.ClassExpression} node
* @param {State} state
*/
'ClassDeclaration|ClassExpression': (node, state) => {
state.commands.push('class ');
if (node.id) {
handle(node.id, state);
state.commands.push(' ');
}
if (node.superClass) {
state.commands.push('extends ');
handle(node.superClass, state);
state.commands.push(' ');
}
if (node.implements) {
state.commands.push('implements ');
sequence(node.implements, state, false, handle_type_annotation);
}
handle(node.body, state);
},
/**
* @param {TSESTree.ForInStatement | TSESTree.ForOfStatement} node
* @param {State} state
*/
'ForInStatement|ForOfStatement': (node, state) => {
state.commands.push('for ');
if (node.type === 'ForOfStatement' && node.await) state.commands.push('await ');
state.commands.push('(');
if (node.left.type === 'VariableDeclaration') {
handle_var_declaration(node.left, state);
} else {
handle(node.left, state);
}
state.commands.push(node.type === 'ForInStatement' ? ` in ` : ` of `);
handle(node.right, state);
state.commands.push(') ');
handle(node.body, state);
},
/**
* @param {TSESTree.FunctionDeclaration | TSESTree.FunctionExpression} node
* @param {State} state
*/
'FunctionDeclaration|FunctionExpression': (node, state) => {
if (node.async) state.commands.push('async ');
state.commands.push(node.generator ? 'function* ' : 'function ');
if (node.id) handle(node.id, state);
if (node.typeParameters) {
handle_type_annotation(node.typeParameters, state);
}
state.commands.push('(');
sequence(node.params, state, false, handle);
state.commands.push(')');
if (node.returnType) handle_type_annotation(node.returnType, state);
state.commands.push(' ');
handle(node.body, state);
},
/**
* @param {TSESTree.RestElement | TSESTree.SpreadElement} node
* @param {State} state
*/
'RestElement|SpreadElement': (node, state) => {
state.commands.push('...');
handle(node.argument, state);
// @ts-expect-error `acorn-typescript` and `@typescript-esling/types` have slightly different type definitions
if (node.typeAnnotation) handle_type_annotation(node.typeAnnotation, state);
}
};
/** @type {Handlers} */
const handlers = {
ArrayExpression: shared['ArrayExpression|ArrayPattern'],
ArrayPattern: shared['ArrayExpression|ArrayPattern'],
ArrowFunctionExpression: (node, state) => {
if (node.async) state.commands.push('async ');
state.commands.push('(');
sequence(node.params, state, false, handle);
state.commands.push(') => ');
if (
node.body.type === 'ObjectExpression' ||
(node.body.type === 'AssignmentExpression' && node.body.left.type === 'ObjectPattern') ||
(node.body.type === 'LogicalExpression' && node.body.left.type === 'ObjectExpression') ||
(node.body.type === 'ConditionalExpression' && node.body.test.type === 'ObjectExpression')
) {
state.commands.push('(');
handle(node.body, state);
state.commands.push(')');
} else {
handle(node.body, state);
}
},
AssignmentExpression(node, state) {
handle(node.left, state);
state.commands.push(` ${node.operator} `);
handle(node.right, state);
},
AssignmentPattern(node, state) {
handle(node.left, state);
state.commands.push(' = ');
handle(node.right, state);
},
AwaitExpression(node, state) {
if (node.argument) {
const precedence = EXPRESSIONS_PRECEDENCE[node.argument.type];
if (precedence && precedence < EXPRESSIONS_PRECEDENCE.AwaitExpression) {
state.commands.push('await (');
handle(node.argument, state);
state.commands.push(')');
} else {
state.commands.push('await ');
handle(node.argument, state);
}
} else {
state.commands.push('await');
}
},
BinaryExpression: shared['BinaryExpression|LogicalExpression'],
BlockStatement: shared['BlockStatement|ClassBody'],
BreakStatement(node, state) {
if (node.label) {
state.commands.push('break ');
handle(node.label, state);
state.commands.push(';');
} else {
state.commands.push('break;');
}
},
CallExpression: shared['CallExpression|NewExpression'],
ChainExpression(node, state) {
handle(node.expression, state);
},
ClassBody: shared['BlockStatement|ClassBody'],
ClassDeclaration: shared['ClassDeclaration|ClassExpression'],
ClassExpression: shared['ClassDeclaration|ClassExpression'],
ConditionalExpression(node, state) {
if (EXPRESSIONS_PRECEDENCE[node.test.type] > EXPRESSIONS_PRECEDENCE.ConditionalExpression) {
handle(node.test, state);
} else {
state.commands.push('(');
handle(node.test, state);
state.commands.push(')');
}
const if_true = create_sequence();
const if_false = create_sequence();
const child_state = { ...state, multiline: false };
state.commands.push(if_true);
handle(node.consequent, child_state);
state.commands.push(if_false);
handle(node.alternate, child_state);
const multiline = child_state.multiline;
if (multiline) {
if_true.children.push(indent, newline, '? ');
if_false.children.push(newline, ': ');
state.commands.push(dedent);
} else {
if_true.children.push(' ? ');
if_false.children.push(' : ');
}
},
ContinueStatement(node, state) {
if (node.label) {
state.commands.push('continue ');
handle(node.label, state);
state.commands.push(';');
} else {
state.commands.push('continue;');
}
},
DebuggerStatement(node, state) {
state.commands.push(c('debugger', node), ';');
},
Decorator(node, state) {
state.commands.push('@');
handle(node.expression, state);
state.commands.push(newline);
},
DoWhileStatement(node, state) {
state.commands.push('do ');
handle(node.body, state);
state.commands.push(' while (');
handle(node.test, state);
state.commands.push(');');
},
EmptyStatement(node, state) {
state.commands.push(';');
},
ExportAllDeclaration(node, state) {
state.commands.push('export * from ');
handle(node.source, state);
state.commands.push(';');
},
ExportDefaultDeclaration(node, state) {
state.commands.push('export default ');
handle(node.declaration, state);
if (node.declaration.type !== 'FunctionDeclaration') {
state.commands.push(';');
}
},
ExportNamedDeclaration(node, state) {
state.commands.push('export ');
if (node.declaration) {
handle(node.declaration, state);
return;
}
state.commands.push('{');
sequence(node.specifiers, state, true, (s, state) => {
handle(s.local, state);
if (s.local.name !== s.exported.name) {
state.commands.push(' as ');
handle(s.exported, state);
}
});
state.commands.push('}');
if (node.source) {
state.commands.push(' from ');
handle(node.source, state);
}
state.commands.push(';');
},
ExpressionStatement(node, state) {
if (
node.expression.type === 'ObjectExpression' ||
(node.expression.type === 'AssignmentExpression' &&
node.expression.left.type === 'ObjectPattern')
) {
// is an AssignmentExpression to an ObjectPattern
state.commands.push('(');
handle(node.expression, state);
state.commands.push(');');
return;
}
handle(node.expression, state);
state.commands.push(';');
},
ForStatement: (node, state) => {
state.commands.push('for (');
if (node.init) {
if (node.init.type === 'VariableDeclaration') {
handle_var_declaration(node.init, state);
} else {
handle(node.init, state);
}
}
state.commands.push('; ');
if (node.test) handle(node.test, state);
state.commands.push('; ');
if (node.update) handle(node.update, state);
state.commands.push(') ');
handle(node.body, state);
},
ForInStatement: shared['ForInStatement|ForOfStatement'],
ForOfStatement: shared['ForInStatement|ForOfStatement'],
FunctionDeclaration: shared['FunctionDeclaration|FunctionExpression'],
FunctionExpression: shared['FunctionDeclaration|FunctionExpression'],
Identifier(node, state) {
let name = node.name;
state.commands.push(c(name, node));
if (node.typeAnnotation) handle_type_annotation(node.typeAnnotation, state);
},
IfStatement(node, state) {
state.commands.push('if (');
handle(node.test, state);
state.commands.push(') ');
handle(node.consequent, state);
if (node.alternate) {
state.commands.push(' else ');
handle(node.alternate, state);
}
},
ImportDeclaration(node, state) {
if (node.specifiers.length === 0) {
state.commands.push('import ');
handle(node.source, state);
state.commands.push(';');
return;
}
/** @type {TSESTree.ImportNamespaceSpecifier | null} */
let namespace_specifier = null;
/** @type {TSESTree.ImportDefaultSpecifier | null} */
let default_specifier = null;
/** @type {TSESTree.ImportSpecifier[]} */
const named_specifiers = [];
for (const s of node.specifiers) {
if (s.type === 'ImportNamespaceSpecifier') {
namespace_specifier = s;
} else if (s.type === 'ImportDefaultSpecifier') {
default_specifier = s;
} else {
named_specifiers.push(s);
}
}
state.commands.push('import ');
if (node.importKind == 'type') state.commands.push('type ');
if (default_specifier) {
state.commands.push(c(default_specifier.local.name, default_specifier));
if (namespace_specifier || named_specifiers.length > 0) state.commands.push(', ');
}
if (namespace_specifier) {
state.commands.push(c('* as ' + namespace_specifier.local.name, namespace_specifier));
}
if (named_specifiers.length > 0) {
state.commands.push('{');
sequence(named_specifiers, state, true, (s, state) => {
if (s.local.name !== s.imported.name) {
handle(s.imported, state);
state.commands.push(' as ');
}
if (s.importKind == 'type') state.commands.push('type ');
handle(s.local, state);
});
state.commands.push('}');
}
state.commands.push(' from ');
handle(node.source, state);
state.commands.push(';');
},
ImportExpression(node, state) {
state.commands.push('import(');
handle(node.source, state);
state.commands.push(')');
},
LabeledStatement(node, state) {
handle(node.label, state);
state.commands.push(': ');
handle(node.body, state);
},
Literal(node, state) {
// TODO do we need to handle weird unicode characters somehow?
// str.replace(/\\u(\d{4})/g, (m, n) => String.fromCharCode(+n))
let value = node.raw;
if (!value)
value = typeof node.value === 'string' ? JSON.stringify(node.value) : String(node.value);
state.commands.push(c(value, node));
},
LogicalExpression: shared['BinaryExpression|LogicalExpression'],
MemberExpression(node, state) {
if (EXPRESSIONS_PRECEDENCE[node.object.type] < EXPRESSIONS_PRECEDENCE.MemberExpression) {
state.commands.push('(');
handle(node.object, state);
state.commands.push(')');
} else {
handle(node.object, state);
}
if (node.computed) {
if (node.optional) {
state.commands.push('?.');
}
state.commands.push('[');
handle(node.property, state);
state.commands.push(']');
} else {
state.commands.push(node.optional ? '?.' : '.');
handle(node.property, state);
}
},
MetaProperty(node, state) {
handle(node.meta, state);
state.commands.push('.');
handle(node.property, state);
},
MethodDefinition(node, state) {
if (node.decorators) {
for (const decorator of node.decorators) {
handle(decorator, state);
}
}
if (node.static) {
state.commands.push('static ');
}
if (node.kind === 'get' || node.kind === 'set') {
// Getter or setter
state.commands.push(node.kind + ' ');
}
if (node.value.async) {
state.commands.push('async ');
}
if (node.value.generator) {
state.commands.push('*');
}
if (node.computed) state.commands.push('[');
handle(node.key, state);
if (node.computed) state.commands.push(']');
state.commands.push('(');
sequence(node.value.params, state, false, handle);
state.commands.push(') ');
if (node.value.body) handle(node.value.body, state);
},
NewExpression: shared['CallExpression|NewExpression'],
ObjectExpression(node, state) {
state.commands.push('{');
sequence(node.properties, state, true, (p, state) => {
if (p.type === 'Property' && p.value.type === 'FunctionExpression') {
const fn = /** @type {TSESTree.FunctionExpression} */ (p.value);
if (p.kind === 'get' || p.kind === 'set') {
state.commands.push(p.kind + ' ');
} else {
if (fn.async) state.commands.push('async ');
if (fn.generator) state.commands.push('*');
}
if (p.computed) state.commands.push('[');
handle(p.key, state);
if (p.computed) state.commands.push(']');
state.commands.push('(');
sequence(fn.params, state, false, handle);
state.commands.push(') ');
handle(fn.body, state);
} else {
handle(p, state);
}
});
state.commands.push('}');
},
ObjectPattern(node, state) {
state.commands.push('{');
sequence(node.properties, state, true, handle);
state.commands.push('}');
if (node.typeAnnotation) handle_type_annotation(node.typeAnnotation, state);
},
// @ts-expect-error this isn't a real node type, but Acorn produces it
ParenthesizedExpression(node, state) {
return handle(node.expression, state);
},
PrivateIdentifier(node, state) {
state.commands.push('#', c(node.name, node));
},
Program(node, state) {
handle_body(node.body, state);
},
Property(node, state) {
const value = node.value.type === 'AssignmentPattern' ? node.value.left : node.value;
const shorthand =
!node.computed &&
node.kind === 'init' &&
node.key.type === 'Identifier' &&
value.type === 'Identifier' &&
node.key.name === value.name;
if (shorthand) {
handle(node.value, state);
return;
}
if (node.computed) state.commands.push('[');
handle(node.key, state);
state.commands.push(node.computed ? ']: ' : ': ');
handle(node.value, state);
},
PropertyDefinition(node, state) {
if (node.accessibility) {
state.commands.push(node.accessibility, ' ');
}
if (node.static) {
state.commands.push('static ');
}
if (node.computed) {
state.commands.push('[');
handle(node.key, state);
state.commands.push(']');
} else {
handle(node.key, state);
}
if (node.typeAnnotation) {
state.commands.push(': ');
handle_type_annotation(node.typeAnnotation.typeAnnotation, state);
}
if (node.value) {
state.commands.push(' = ');
handle(node.value, state);
}
state.commands.push(';');
},
RestElement: shared['RestElement|SpreadElement'],
ReturnStatement(node, state) {
if (node.argument) {
const argumentWithComment = /** @type {NodeWithComments} */ (node.argument);
const contains_comment =
argumentWithComment.leadingComments &&
argumentWithComment.leadingComments.some((comment) => comment.type === 'Line');
state.commands.push(contains_comment ? 'return (' : 'return ');
handle(node.argument, state);
state.commands.push(contains_comment ? ');' : ';');
} else {
state.commands.push('return;');
}
},
SequenceExpression(node, state) {
state.commands.push('(');
sequence(node.expressions, state, false, handle);
state.commands.push(')');
},
SpreadElement: shared['RestElement|SpreadElement'],
StaticBlock(node, state) {
state.commands.push(indent, 'static {', newline);
handle_body(node.body, state);
state.commands.push(dedent, newline, '}');
},
Super(node, state) {
state.commands.push(c('super', node));
},
SwitchStatement(node, state) {
state.commands.push('switch (');
handle(node.discriminant, state);
state.commands.push(') {', indent);
let first = true;
for (const block of node.cases) {
if (!first) state.commands.push('\n');
first = false;
if (block.test) {
state.commands.push(newline, `case `);
handle(block.test, state);
state.commands.push(':');
} else {
state.commands.push(newline, `default:`);
}
state.commands.push(indent);
for (const statement of block.consequent) {
state.commands.push(newline);
handle(statement, state);
}
state.commands.push(dedent);
}
state.commands.push(dedent, newline, `}`);
},
TaggedTemplateExpression(node, state) {
handle(node.tag, state);
handle(node.quasi, state);
},
TemplateLiteral(node, state) {
state.commands.push('`');
const { quasis, expressions } = node;
for (let i = 0; i < expressions.length; i++) {
const raw = quasis[i].value.raw;
state.commands.push(raw, '${');
handle(expressions[i], state);
state.commands.push('}');
if (/\n/.test(raw)) state.multiline = true;
}
const raw = quasis[quasis.length - 1].value.raw;
state.commands.push(raw, '`');
if (/\n/.test(raw)) state.multiline = true;
},
ThisExpression(node, state) {
state.commands.push(c('this', node));
},
ThrowStatement(node, state) {
state.commands.push('throw ');
if (node.argument) handle(node.argument, state);
state.commands.push(';');
},
TryStatement(node, state) {
state.commands.push('try ');
handle(node.block, state);
if (node.handler) {
if (node.handler.param) {
state.commands.push(' catch(');
handle(node.handler.param, state);
state.commands.push(') ');
} else {
state.commands.push(' catch ');
}
handle(node.handler.body, state);
}
if (node.finalizer) {
state.commands.push(' finally ');
handle(node.finalizer, state);
}
},
TSAsExpression(node, state) {
if (node.expression) {
const needs_parens =
EXPRESSIONS_PRECEDENCE[node.expression.type] < EXPRESSIONS_PRECEDENCE.TSAsExpression;
if (needs_parens) {
state.commands.push('(');
handle(node.expression, state);
state.commands.push(')');
} else {
handle(node.expression, state);
}
}
state.commands.push(' as ');
handle_type_annotation(node.typeAnnotation, state);
},
TSEnumDeclaration(node, state) {
state.commands.push('enum ');
handle(node.id, state);
state.commands.push(' {', indent, newline);
sequence(node.members, state, false, handle_type_annotation);
state.commands.push(dedent, newline, '}', newline);
},
TSNonNullExpression(node, state) {
handle(node.expression, state);
state.commands.push('!');
},
TSInterfaceBody(node, state) {
sequence(node.body, state, false, handle_type_annotation, ';');
},
TSInterfaceDeclaration(node, state) {
state.commands.push('interface ');
handle(node.id, state);
if (node.typeParameters) handle_type_annotation(node.typeParameters, state);
if (node.extends) {
state.commands.push(' extends ');
sequence(node.extends, state, false, handle_type_annotation);
}
state.commands.push(' {');
handle(node.body, state);
state.commands.push('}');
},
TSSatisfiesExpression(node, state) {
if (node.expression) {
const needs_parens =
EXPRESSIONS_PRECEDENCE[node.expression.type] < EXPRESSIONS_PRECEDENCE.TSSatisfiesExpression;
if (needs_parens) {
state.commands.push('(');
handle(node.expression, state);
state.commands.push(')');
} else {
handle(node.expression, state);
}
}
state.commands.push(' satisfies ');
handle_type_annotation(node.typeAnnotation, state);
},
TSTypeAliasDeclaration(node, state) {
state.commands.push('type ');
handle(node.id, state);
if (node.typeParameters) handle_type_annotation(node.typeParameters, state);
state.commands.push(' = ');
handle_type_annotation(node.typeAnnotation, state);
state.commands.push(';');
},
TSQualifiedName(node, state) {
handle(node.left, state);
state.commands.push('.');
handle(node.right, state);
},
UnaryExpression(node, state) {
state.commands.push(node.operator);
if (node.operator.length > 1) {
state.commands.push(' ');
}
if (EXPRESSIONS_PRECEDENCE[node.argument.type] < EXPRESSIONS_PRECEDENCE.UnaryExpression) {
state.commands.push('(');
handle(node.argument, state);
state.commands.push(')');
} else {
handle(node.argument, state);
}
},
UpdateExpression(node, state) {
if (node.prefix) {
state.commands.push(node.operator);
handle(node.argument, state);
} else {
handle(node.argument, state);
state.commands.push(node.operator);
}
},
VariableDeclaration(node, state) {
handle_var_declaration(node, state);
state.commands.push(';');
},
VariableDeclarator(node, state) {
handle(node.id, state);
if (node.init) {
state.commands.push(' = ');
handle(node.init, state);
}
},
WhileStatement(node, state) {
state.commands.push('while (');
handle(node.test, state);
state.commands.push(') ');
handle(node.body, state);
},
WithStatement(node, state) {
state.commands.push('with (');
handle(node.object, state);
state.commands.push(') ');
handle(node.body, state);
},
YieldExpression(node, state) {
if (node.argument) {
state.commands.push(node.delegate ? `yield* ` : `yield `);
handle(node.argument, state);
} else {
state.commands.push(node.delegate ? `yield*` : `yield`);
}
}
};