mfuntowicz's picture
mfuntowicz HF Staff
Upload folder using huggingface_hub
04ec17f verified
raw
history blame
53.2 kB
/** @import { TSESTree } from '@typescript-eslint/types' */
/** @import { Visitors } from '../../types.js' */
/** @import { TSOptions, Comment } from '../types.js' */
import { Context } from 'esrap';
/** @typedef {TSESTree.Node} Node */
/** @type {Record<TSESTree.Expression['type'] | 'Super' | 'RestElement', number>} */
export 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
};
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
};
/**
* @param {Comment} comment
* @param {Context} context
*/
function write_comment(comment, context) {
if (comment.type === 'Line') {
context.write(`//${comment.value}`);
} else {
context.write('/*');
const lines = comment.value.split('\n');
for (let i = 0; i < lines.length; i += 1) {
if (i > 0) context.newline();
context.write(lines[i]);
}
context.write('*/');
}
}
/**
* @param {TSOptions} [options]
* @returns {Visitors<TSESTree.Node>}
*/
export default (options = {}) => {
const quote_char = options.quotes === 'double' ? '"' : "'";
const comments = options.comments ?? [];
let comment_index = 0;
/**
* Set `comment_index` to be the first comment after `start`.
* Most of the time this is already correct, but if nodes
* have been moved around we may need to search for it
* @param {TSESTree.Node} node
*/
function reset_comment_index(node) {
if (!node.loc) {
comment_index = comments.length;
return;
}
let previous = comments[comment_index - 1];
let comment = comments[comment_index];
if (
comment &&
comment.loc &&
!before(comment.loc.start, node.loc.start) &&
(!previous || (previous.loc && before(previous.loc.start, node.loc.start)))
) {
return;
}
// TODO use a binary search here, account for synthetic nodes (without `loc`)
comment_index = comments.findIndex(
(comment) => comment.loc && node.loc && !before(comment.loc.start, node.loc.start)
);
if (comment_index === -1) comment_index = comments.length;
}
/**
* @param {Context} context
* @param {{ line: number, column: number } | null} prev
* @param {{ line: number, column: number } | null} next
*/
function flush_trailing_comments(context, prev, next) {
while (comment_index < comments.length) {
const comment = comments[comment_index];
if (
comment &&
prev &&
comment.loc.start.line === prev.line &&
(next === null || before(comment.loc.end, next))
) {
context.write(' ');
write_comment(comment, context);
comment_index += 1;
if (comment.type === 'Line') {
context.newline();
} else {
continue;
}
}
break;
}
}
/**
* @param {Context} context
* @param {{ line: number, column: number } | null} from
* @param {{ line: number, column: number }} to
* @param {boolean} pad
*/
function flush_comments_until(context, from, to, pad) {
let first = true;
while (comment_index < comments.length) {
const comment = comments[comment_index];
if (comment && comment.loc && to && before(comment.loc.start, to)) {
if (first && from !== null && comment.loc.start.line > from.line) {
context.margin();
context.newline();
}
first = false;
write_comment(comment, context);
if (comment.loc.end.line < to.line) {
context.newline();
} else if (pad) {
context.write(' ');
}
comment_index += 1;
} else {
break;
}
}
}
/**
* @param {Context} context
* @param {TSESTree.Node[]} nodes
* @param {{ line: number, column: number }} until
* @param {boolean} pad
*/
function sequence(context, nodes, until, pad, separator = ',') {
let multiline = false;
let length = -1;
/** @type {boolean[]} */
const multiline_nodes = [];
const children = nodes.map((child, i) => {
const child_context = context.new();
if (child) child_context.visit(child);
multiline_nodes[i] = child_context.multiline;
if (i < nodes.length - 1 || !child) {
child_context.write(separator);
}
const next = i === nodes.length - 1 ? until : nodes[i + 1]?.loc?.start || null;
flush_trailing_comments(child_context, child?.loc?.end || null, next);
length += child_context.measure() + 1;
multiline ||= child_context.multiline;
return child_context;
});
multiline ||= length > 60;
if (multiline) {
context.indent();
context.newline();
} else if (pad && length > 0) {
context.write(' ');
}
/** @type {Context | null} */
let prev = null;
for (let i = 0; i < nodes.length; i += 1) {
const child = children[i];
if (prev !== null) {
if (multiline_nodes[i - 1] || multiline_nodes[i]) {
context.margin();
}
if (nodes[i]) {
if (multiline) {
context.newline();
} else {
context.write(' ');
}
}
}
context.append(child);
prev = child;
}
flush_comments_until(context, nodes[nodes.length - 1]?.loc?.end ?? null, until, false);
if (multiline) {
context.dedent();
context.newline();
} else if (pad && length > 0) {
context.write(' ');
}
}
/**
* Push a sequence of nodes onto separate lines, separating them with
* an extra newline where appropriate
* @param {Context} context
* @param {TSESTree.Node & { body: TSESTree.Node[] }} node
*/
function body(context, node) {
reset_comment_index(node);
/** @type {string | null} */
let prev_type = null;
let prev_multiline = false;
for (let i = 0; i < node.body.length; i += 1) {
const child = node.body[i];
if (child.type === 'EmptyStatement') continue;
const child_context = context.new();
child_context.visit(child);
if (prev_type !== null) {
if (child_context.multiline || prev_multiline || child.type !== prev_type) {
context.margin();
}
context.newline();
}
context.append(child_context);
flush_trailing_comments(
context,
child.loc?.end || null,
node.body[i + 1]?.loc?.end ?? node.loc?.end ?? null
);
prev_type = child.type;
prev_multiline = child_context.multiline;
}
if (node.loc) {
context.newline();
flush_comments_until(
context,
node.body[node.body.length - 1]?.loc?.end ?? null,
node.loc.end,
false
);
}
}
const shared = {
/**
* @param {TSESTree.ArrayExpression | TSESTree.ArrayPattern} node
* @param {Context} context
*/
'ArrayExpression|ArrayPattern': (node, context) => {
context.write('[');
sequence(
context,
/** @type {TSESTree.Node[]} */ (node.elements),
node.loc?.end ?? null,
false
);
context.write(']');
},
/**
* @param {TSESTree.BinaryExpression | TSESTree.LogicalExpression} node
* @param {Context} context
*/
'BinaryExpression|LogicalExpression': (node, context) => {
// TODO
// const is_in = node.operator === 'in';
// if (is_in) {
// // Avoids confusion in `for` loops initializers
// chunks.write('(');
// }
if (needs_parens(node.left, node, false)) {
context.write('(');
context.visit(node.left);
context.write(')');
} else {
context.visit(node.left);
}
context.write(` ${node.operator} `);
if (needs_parens(node.right, node, true)) {
context.write('(');
context.visit(node.right);
context.write(')');
} else {
context.visit(node.right);
}
},
/**
* @param {TSESTree.BlockStatement | TSESTree.ClassBody} node
* @param {Context} context
*/
'BlockStatement|ClassBody': (node, context) => {
if (node.loc) {
const { line, column } = node.loc.start;
context.location(line, column);
context.write('{');
context.location(line, column + 1);
} else {
context.write('{');
}
const child_context = context.new();
body(child_context, node);
if (!child_context.empty()) {
context.indent();
context.newline();
context.append(child_context);
context.dedent();
context.newline();
}
if (node.loc) {
const { line, column } = node.loc.end;
context.location(line, column - 1);
context.write('}');
context.location(line, column);
} else {
context.write('}');
}
},
/**
* @param {TSESTree.CallExpression | TSESTree.NewExpression} node
* @param {Context} context
*/
'CallExpression|NewExpression': (node, context) => {
if (node.type === 'NewExpression') {
context.write('new ');
}
const needs_parens =
EXPRESSIONS_PRECEDENCE[node.callee.type] < EXPRESSIONS_PRECEDENCE.CallExpression ||
(node.type === 'NewExpression' && has_call_expression(node.callee));
if (needs_parens) {
context.write('(');
context.visit(node.callee);
context.write(')');
} else {
context.visit(node.callee);
}
if (/** @type {TSESTree.CallExpression} */ (node).optional) {
context.write('?.');
}
if (node.typeArguments) context.visit(node.typeArguments);
const open = context.new();
const join = context.new();
context.write('(');
context.append(open);
// if the final argument is multiline, it doesn't need to force all the
// other arguments to also be multiline
const child_context = context.new();
const final_context = context.new();
context.append(child_context);
context.append(final_context);
for (let i = 0; i < node.arguments.length; i += 1) {
const is_last = i === node.arguments.length - 1;
const context = is_last ? final_context : child_context;
const arg = node.arguments[i];
// special case — if final argument has a comment above it,
// we make the whole sequence multiline
if (
is_last &&
arg.loc &&
comments[comment_index] &&
comments[comment_index].loc &&
comments[comment_index].loc.start.line < arg.loc.start.line
) {
child_context.multiline = true;
}
context.visit(arg);
if (!is_last) context.write(',');
const next = is_last
? (node.loc?.end ?? null)
: (node.arguments[i + 1]?.loc?.start ?? null);
flush_trailing_comments(context, arg.loc?.end ?? null, next);
if (!is_last) context.append(join);
}
context.multiline ||= child_context.multiline || final_context.multiline;
if (child_context.multiline) {
open.indent();
open.newline();
join.newline();
context.dedent();
context.newline();
} else {
join.write(' ');
}
context.write(')');
},
/**
* @param {TSESTree.ClassDeclaration | TSESTree.ClassExpression} node
* @param {Context} context
*/
'ClassDeclaration|ClassExpression': (node, context) => {
if (node.decorators) {
for (const decorator of node.decorators) {
context.visit(decorator);
}
}
if (node.declare) {
context.write('declare ');
}
if (node.abstract) context.write('abstract ');
context.write('class ');
if (node.id) {
context.visit(node.id);
context.write(' ');
}
if (node.superClass) {
context.write('extends ');
context.visit(node.superClass);
context.write(' ');
}
if (node.implements && node.implements.length > 0) {
context.write('implements');
sequence(context, node.implements, node.body.loc?.start ?? null, true);
}
context.visit(node.body);
},
/**
* @param {TSESTree.ForInStatement | TSESTree.ForOfStatement} node
* @param {Context} context
*/
'ForInStatement|ForOfStatement': (node, context) => {
context.write('for ');
if (node.type === 'ForOfStatement' && node.await) context.write('await ');
context.write('(');
if (node.left.type === 'VariableDeclaration') {
handle_var_declaration(node.left, context);
} else {
context.visit(node.left);
}
context.write(node.type === 'ForInStatement' ? ' in ' : ' of ');
context.visit(node.right);
context.write(') ');
context.visit(node.body);
},
/**
* @param {TSESTree.FunctionDeclaration | TSESTree.FunctionExpression} node
* @param {Context} context
*/
'FunctionDeclaration|FunctionExpression': (node, context) => {
if (node.async) context.write('async ');
context.write(node.generator ? 'function* ' : 'function ');
if (node.id) context.visit(node.id);
if (node.typeParameters) {
context.visit(node.typeParameters);
}
context.write('(');
sequence(context, node.params, (node.returnType ?? node.body).loc?.start ?? null, false);
context.write(')');
if (node.returnType) context.visit(node.returnType);
context.write(' ');
context.visit(node.body);
},
/**
* @param {TSESTree.MethodDefinition | TSESTree.TSAbstractMethodDefinition} node
* @param {Context} context
*/
'MethodDefinition|TSAbstractMethodDefinition': (node, context) => {
if (node.decorators) {
for (const decorator of node.decorators) {
context.visit(decorator);
}
}
// @ts-expect-error `acorn-typescript` and `@typescript-eslint/types` have slightly different type definitions
if (node.abstract || node.type === 'TSAbstractMethodDefinition') {
context.write('abstract ');
}
if (node.static) {
context.write('static ');
}
if (node.kind === 'get' || node.kind === 'set') {
// Getter or setter
context.write(node.kind + ' ');
}
if (node.value.async) {
context.write('async ');
}
if (node.value.generator) {
context.write('*');
}
if (node.computed) context.write('[');
context.visit(node.key);
if (node.computed) context.write(']');
context.write('(');
sequence(
context,
node.value.params,
(node.value.returnType ?? node.value.body)?.loc?.start ?? node.loc?.end ?? null,
false
);
context.write(')');
if (node.value.returnType) context.visit(node.value.returnType);
context.write(' ');
if (node.value.body) context.visit(node.value.body);
},
/**
* @param {TSESTree.PropertyDefinition | TSESTree.TSAbstractPropertyDefinition | TSESTree.AccessorProperty | TSESTree.TSAbstractAccessorProperty} node
* @param {Context} context
*/
'PropertyDefinition|TSAbstractPropertyDefinition|AccessorProperty|TSAbstractAccessorProperty': (
node,
context
) => {
if (node.decorators) {
for (const decorator of node.decorators) {
context.visit(decorator);
}
}
if (node.accessibility) {
context.write(node.accessibility + ' ');
}
if (
// @ts-expect-error `acorn-typescript` and `@typescript-eslint/types` have slightly different type definitions
node.abstract ||
node.type === 'TSAbstractPropertyDefinition' ||
node.type === 'TSAbstractAccessorProperty'
) {
context.write('abstract ');
}
if (node.static) {
context.write('static ');
}
if (
// @ts-expect-error `acorn-typescript` and `@typescript-eslint/types` have slightly different type definitions
node.accessor ||
node.type === 'AccessorProperty' ||
node.type === 'TSAbstractAccessorProperty'
) {
context.write('accessor ');
}
if (node.computed) {
context.write('[');
context.visit(node.key);
context.write(']');
} else {
context.visit(node.key);
}
if (node.typeAnnotation) {
if (node.type === 'AccessorProperty' || node.type === 'TSAbstractAccessorProperty') {
context.visit(node.typeAnnotation);
} else {
context.write(': ');
context.visit(node.typeAnnotation.typeAnnotation);
}
}
if (node.value) {
context.write(' = ');
context.visit(node.value);
}
context.write(';');
flush_trailing_comments(
context,
(node.value ?? node.typeAnnotation ?? node.key).loc?.end ?? null,
null
);
},
/**
* @param {TSESTree.RestElement | TSESTree.SpreadElement} node
* @param {Context} context
*/
'RestElement|SpreadElement': (node, context) => {
context.write('...');
context.visit(node.argument);
// @ts-expect-error `acorn-typescript` and `@typescript-eslint/types` have slightly different type definitions
if (node.typeAnnotation) context.visit(node.typeAnnotation);
},
/**
* @param {TSESTree.TSConstructSignatureDeclaration | TSESTree.TSCallSignatureDeclaration} node
* @param {Context} context
*/
'TSConstructSignatureDeclaration|TSCallSignatureDeclaration': (node, context) => {
if (node.type === 'TSConstructSignatureDeclaration') context.write('new');
if (node.typeParameters) {
context.visit(node.typeParameters);
}
context.write('(');
sequence(
context,
// @ts-expect-error `acorn-typescript` and `@typescript-eslint/types` have slightly different type definitions
node.parameters ?? node.params,
// @ts-expect-error `acorn-typescript` and `@typescript-eslint/types` have slightly different type definitions
(node.typeAnnotation ?? node.returnType)?.loc?.start ?? null,
false
);
context.write(')');
// @ts-expect-error `acorn-typescript` and `@typescript-eslint/types` have slightly different type definitions
if (node.typeAnnotation || node.returnType) {
// @ts-expect-error `acorn-typescript` and `@typescript-eslint/types` have slightly different type definitions
context.visit(node.typeAnnotation ?? node.returnType);
}
},
/**
* @param {TSESTree.TSFunctionType | TSESTree.TSConstructorType} node
* @param {Context} context
*/
'TSFunctionType|TSConstructorType': (node, context) => {
if (node.type === 'TSConstructorType') context.write('new ');
if (node.typeParameters) context.visit(node.typeParameters);
context.write('(');
sequence(
context,
// @ts-expect-error `acorn-typescript` and `@typescript-eslint/types` have slightly different type definitions
node.parameters ?? node.params,
// @ts-expect-error `acorn-typescript` and `@typescript-eslint/types` have slightly different type definitions
node.typeAnnotation?.typeAnnotation?.loc?.start ??
node.returnType?.typeAnnotation?.loc?.start ??
null,
false
);
context.write(') => ');
// @ts-expect-error `acorn-typescript` and `@typescript-eslint/types` have slightly different type definitions
context.visit(node.typeAnnotation?.typeAnnotation ?? node.returnType?.typeAnnotation);
}
};
return {
_(node, context, visit) {
if (node.loc) {
flush_comments_until(context, null, node.loc.start, true);
}
visit(node);
},
AccessorProperty:
shared[
'PropertyDefinition|TSAbstractPropertyDefinition|AccessorProperty|TSAbstractAccessorProperty'
],
ArrayExpression: shared['ArrayExpression|ArrayPattern'],
ArrayPattern: shared['ArrayExpression|ArrayPattern'],
ArrowFunctionExpression: (node, context) => {
if (node.async) context.write('async ');
context.write('(');
sequence(context, node.params, node.body.loc?.start ?? null, false);
context.write(') => ');
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')
) {
context.write('(');
context.visit(node.body);
context.write(')');
} else {
context.visit(node.body);
}
},
AssignmentExpression(node, context) {
context.visit(node.left);
context.write(` ${node.operator} `);
context.visit(node.right);
},
AssignmentPattern(node, context) {
context.visit(node.left);
context.write(' = ');
context.visit(node.right);
},
AwaitExpression(node, context) {
if (node.argument) {
const precedence = EXPRESSIONS_PRECEDENCE[node.argument.type];
if (precedence && precedence < EXPRESSIONS_PRECEDENCE.AwaitExpression) {
context.write('await (');
context.visit(node.argument);
context.write(')');
} else {
context.write('await ');
context.visit(node.argument);
}
} else {
context.write('await');
}
},
BinaryExpression: shared['BinaryExpression|LogicalExpression'],
BlockStatement: shared['BlockStatement|ClassBody'],
BreakStatement(node, context) {
if (node.label) {
context.write('break ');
context.visit(node.label);
context.write(';');
} else {
context.write('break;');
}
},
CallExpression: shared['CallExpression|NewExpression'],
ChainExpression(node, context) {
context.visit(node.expression);
},
ClassBody: shared['BlockStatement|ClassBody'],
ClassDeclaration: shared['ClassDeclaration|ClassExpression'],
ClassExpression: shared['ClassDeclaration|ClassExpression'],
ConditionalExpression(node, context) {
if (EXPRESSIONS_PRECEDENCE[node.test.type] > EXPRESSIONS_PRECEDENCE.ConditionalExpression) {
context.visit(node.test);
} else {
context.write('(');
context.visit(node.test);
context.write(')');
}
const consequent = context.new();
const alternate = context.new();
// TODO flush comments here, rather than in visitors
consequent.visit(node.consequent);
alternate.visit(node.alternate);
if (
consequent.multiline ||
alternate.multiline ||
consequent.measure() + alternate.measure() > 50
) {
context.indent();
context.newline();
context.write('? ');
context.append(consequent);
context.newline();
context.write(': ');
context.append(alternate);
context.dedent();
} else {
context.write(' ? ');
context.append(consequent);
context.write(' : ');
context.append(alternate);
}
},
ContinueStatement(node, context) {
if (node.label) {
context.write('continue ');
context.visit(node.label);
context.write(';');
} else {
context.write('continue;');
}
},
DebuggerStatement(node, context) {
context.write('debugger', node);
context.write(';');
},
Decorator(node, context) {
context.write('@');
context.visit(node.expression);
context.newline();
},
DoWhileStatement(node, context) {
context.write('do ');
context.visit(node.body);
context.write(' while (');
context.visit(node.test);
context.write(');');
},
EmptyStatement(node, context) {
context.write(';');
},
ExportAllDeclaration(node, context) {
context.write(node.exportKind === 'type' ? 'export type * ' : 'export * ');
if (node.exported) {
context.write('as ');
context.visit(node.exported);
}
context.write(' from ');
context.visit(node.source);
context.write(';');
},
ExportDefaultDeclaration(node, context) {
context.write('export default ');
context.visit(node.declaration);
if (node.declaration.type !== 'FunctionDeclaration') {
context.write(';');
}
},
ExportNamedDeclaration(node, context) {
if (node.declaration) {
// Check if declaration has decorators (ClassDeclaration, ClassExpression can have them)
const decl = /** @type {any} */ (node.declaration);
if (decl.decorators && decl.decorators.length > 0) {
for (const decorator of decl.decorators) {
context.visit(decorator);
}
context.write('export ');
// Temporarily remove decorators so ClassDeclaration doesn't print them again
const savedDecorators = decl.decorators;
decl.decorators = [];
context.visit(node.declaration);
decl.decorators = savedDecorators;
} else {
context.write('export ');
context.visit(node.declaration);
}
return;
}
context.write('export ');
if (node.exportKind === 'type') {
context.write('type ');
}
context.write('{');
sequence(context, node.specifiers, node.source?.loc?.start ?? node.loc?.end ?? null, true);
context.write('}');
if (node.source) {
context.write(' from ');
context.visit(node.source);
}
context.write(';');
},
ExportSpecifier(node, context) {
if (node.exportKind === 'type') {
context.write('type ');
}
context.visit(node.local);
if (
node.local.type === 'Identifier' &&
node.exported.type === 'Identifier' &&
node.local.name !== node.exported.name
) {
context.write(' as ');
context.visit(node.exported);
}
},
ExpressionStatement(node, context) {
if (
node.expression.type === 'ObjectExpression' ||
(node.expression.type === 'AssignmentExpression' &&
node.expression.left.type === 'ObjectPattern') ||
node.expression.type === 'FunctionExpression'
) {
// is an AssignmentExpression to an ObjectPattern
context.write('(');
context.visit(node.expression);
context.write(');');
return;
}
context.visit(node.expression);
context.write(';');
},
ForStatement: (node, context) => {
context.write('for (');
if (node.init) {
if (node.init.type === 'VariableDeclaration') {
handle_var_declaration(node.init, context);
} else {
context.visit(node.init);
}
}
context.write('; ');
if (node.test) context.visit(node.test);
context.write('; ');
if (node.update) context.visit(node.update);
context.write(') ');
context.visit(node.body);
},
ForInStatement: shared['ForInStatement|ForOfStatement'],
ForOfStatement: shared['ForInStatement|ForOfStatement'],
FunctionDeclaration: shared['FunctionDeclaration|FunctionExpression'],
FunctionExpression: shared['FunctionDeclaration|FunctionExpression'],
Identifier(node, context) {
let name = node.name;
context.write(name, node);
if (node.typeAnnotation) context.visit(node.typeAnnotation);
},
IfStatement(node, context) {
context.write('if (');
context.visit(node.test);
context.write(') ');
context.visit(node.consequent);
if (node.alternate) {
context.space();
context.write('else ');
context.visit(node.alternate);
}
},
ImportDeclaration(node, context) {
if (node.specifiers.length === 0) {
context.write('import ');
context.visit(node.source);
context.write(';');
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);
}
}
context.write('import ');
if (node.importKind == 'type') context.write('type ');
if (default_specifier) {
context.write(default_specifier.local.name, default_specifier);
if (namespace_specifier || named_specifiers.length > 0) context.write(', ');
}
if (namespace_specifier) {
context.write('* as ' + namespace_specifier.local.name, namespace_specifier);
}
if (named_specifiers.length > 0) {
context.write('{');
sequence(context, named_specifiers, node.source.loc?.start ?? null, true);
context.write('}');
}
context.write(' from ');
context.visit(node.source);
if (node.attributes && node.attributes.length > 0) {
context.write(' with { ');
for (let index = 0; index < node.attributes.length; index++) {
const { key, value } = node.attributes[index];
context.visit(key);
context.write(': ');
context.visit(value);
if (index + 1 !== node.attributes.length) {
context.write(', ');
}
}
context.write(' }');
}
context.write(';');
},
ImportExpression(node, context) {
context.write('import(');
context.visit(node.source);
//@ts-expect-error for some reason the types haven't been updated
if (node.arguments) {
//@ts-expect-error
for (let index = 0; index < node.arguments.length; index++) {
context.write(', ');
//@ts-expect-error
context.visit(node.arguments[index]);
}
}
if (node.options) {
context.write(', ');
context.visit(node.options);
}
context.write(')');
},
ImportSpecifier(node, context) {
if (
node.local.type === 'Identifier' &&
node.imported.type === 'Identifier' &&
node.local.name !== node.imported.name
) {
context.visit(node.imported);
context.write(' as ');
}
if (node.importKind == 'type') context.write('type ');
context.visit(node.local);
},
LabeledStatement(node, context) {
context.visit(node.label);
context.write(': ');
context.visit(node.body);
},
Literal(node, context) {
// TODO do we need to handle weird unicode characters somehow?
// str.replace(/\\u(\d{4})/g, (m, n) => String.fromCharCode(+n))
const value =
node.raw ||
(typeof node.value === 'string' ? quote(node.value, quote_char) : String(node.value));
context.write(value, node);
},
LogicalExpression: shared['BinaryExpression|LogicalExpression'],
MemberExpression(node, context) {
if (EXPRESSIONS_PRECEDENCE[node.object.type] < EXPRESSIONS_PRECEDENCE.MemberExpression) {
context.write('(');
context.visit(node.object);
context.write(')');
} else {
context.visit(node.object);
}
if (node.computed) {
if (node.optional) {
context.write('?.');
}
context.write('[');
context.visit(node.property);
context.write(']');
} else {
context.write(node.optional ? '?.' : '.');
context.visit(node.property);
}
},
MetaProperty(node, context) {
context.visit(node.meta);
context.write('.');
context.visit(node.property);
},
MethodDefinition: shared['MethodDefinition|TSAbstractMethodDefinition'],
NewExpression: shared['CallExpression|NewExpression'],
ObjectExpression(node, context) {
context.write('{');
sequence(context, node.properties, node.loc?.end ?? null, true);
context.write('}');
},
ObjectPattern(node, context) {
context.write('{');
sequence(context, node.properties, node.loc?.end ?? null, true);
context.write('}');
if (node.typeAnnotation) context.visit(node.typeAnnotation);
},
// @ts-expect-error this isn't a real node type, but Acorn produces it
ParenthesizedExpression(node, context) {
context.write('(');
context.visit(node.expression);
context.write(')');
},
PrivateIdentifier(node, context) {
context.write('#');
context.write(node.name, node);
},
Program(node, context) {
body(context, node);
},
Property(node, context) {
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) {
context.visit(node.value);
return;
}
// shorthand methods
if (node.value.type === 'FunctionExpression') {
if (node.kind !== 'init') context.write(node.kind + ' ');
if (node.value.async) context.write('async ');
if (node.value.generator) context.write('*');
if (node.computed) context.write('[');
context.visit(node.key);
if (node.computed) context.write(']');
context.write('(');
sequence(
context,
node.value.params,
(node.value.returnType ?? node.value.body).loc?.start ?? null,
false
);
context.write(')');
if (node.value.returnType) context.visit(node.value.returnType);
context.write(' ');
context.visit(node.value.body);
} else {
if (node.computed) context.write('[');
if (node.kind === 'get' || node.kind === 'set') context.write(node.kind + ' ');
context.visit(node.key);
context.write(node.computed ? ']: ' : ': ');
context.visit(node.value);
}
},
PropertyDefinition:
shared[
'PropertyDefinition|TSAbstractPropertyDefinition|AccessorProperty|TSAbstractAccessorProperty'
],
RestElement: shared['RestElement|SpreadElement'],
ReturnStatement(node, context) {
if (node.argument) {
const contains_comment =
comments[comment_index] &&
comments[comment_index].loc &&
node.argument.loc &&
before(comments[comment_index].loc.start, node.argument.loc.start);
context.write(contains_comment ? 'return (' : 'return ');
context.visit(node.argument);
context.write(contains_comment ? ');' : ';');
} else {
context.write('return;');
}
},
SequenceExpression(node, context) {
context.write('(');
sequence(context, node.expressions, node.loc?.end ?? null, false);
context.write(')');
},
SpreadElement: shared['RestElement|SpreadElement'],
StaticBlock(node, context) {
context.write('static {');
context.indent();
context.newline();
body(context, node);
context.dedent();
context.newline();
context.write('}');
},
Super(node, context) {
context.write('super', node);
},
SwitchStatement(node, context) {
context.write('switch (');
context.visit(node.discriminant);
context.write(') {');
context.indent();
let first = true;
for (const block of node.cases) {
if (!first) {
context.margin();
}
first = false;
if (block.test) {
context.newline();
context.write('case ');
context.visit(block.test);
context.write(':');
} else {
context.newline();
context.write('default:');
}
context.indent();
for (const statement of block.consequent) {
context.newline();
context.visit(statement);
}
context.dedent();
}
context.dedent();
context.newline();
context.write('}');
},
TaggedTemplateExpression(node, context) {
context.visit(node.tag);
context.visit(node.quasi);
},
TemplateLiteral(node, context) {
context.write('`');
const { quasis, expressions } = node;
for (let i = 0; i < expressions.length; i++) {
const raw = quasis[i].value.raw;
context.write(raw + '${');
context.visit(expressions[i]);
context.write('}');
if (/\n/.test(raw)) context.multiline = true;
}
const raw = quasis[quasis.length - 1].value.raw;
context.write(raw + '`');
if (/\n/.test(raw)) context.multiline = true;
},
ThisExpression(node, context) {
context.write('this', node);
},
ThrowStatement(node, context) {
context.write('throw ');
if (node.argument) context.visit(node.argument);
context.write(';');
},
TryStatement(node, context) {
context.write('try ');
context.visit(node.block);
if (node.handler) {
if (node.handler.param) {
context.write(' catch(');
context.visit(node.handler.param);
context.write(') ');
} else {
context.write(' catch ');
}
context.visit(node.handler.body);
}
if (node.finalizer) {
context.write(' finally ');
context.visit(node.finalizer);
}
},
UnaryExpression(node, context) {
context.write(node.operator);
if (node.operator.length > 1) {
context.write(' ');
}
if (EXPRESSIONS_PRECEDENCE[node.argument.type] < EXPRESSIONS_PRECEDENCE.UnaryExpression) {
context.write('(');
context.visit(node.argument);
context.write(')');
} else {
context.visit(node.argument);
}
},
UpdateExpression(node, context) {
if (node.prefix) {
context.write(node.operator);
context.visit(node.argument);
} else {
context.visit(node.argument);
context.write(node.operator);
}
},
VariableDeclaration(node, context) {
handle_var_declaration(node, context);
context.write(';');
},
VariableDeclarator(node, context) {
context.visit(node.id);
if (node.init) {
context.write(' = ');
context.visit(node.init);
}
},
WhileStatement(node, context) {
context.write('while (');
context.visit(node.test);
context.write(') ');
context.visit(node.body);
},
WithStatement(node, context) {
context.write('with (');
context.visit(node.object);
context.write(') ');
context.visit(node.body);
},
YieldExpression(node, context) {
if (node.argument) {
context.write(node.delegate ? `yield* ` : `yield `);
context.visit(node.argument);
} else {
context.write(node.delegate ? `yield*` : `yield`);
}
},
TSAbstractMethodDefinition: shared['MethodDefinition|TSAbstractMethodDefinition'],
TSAbstractAccessorProperty:
shared[
'PropertyDefinition|TSAbstractPropertyDefinition|AccessorProperty|TSAbstractAccessorProperty'
],
TSAbstractPropertyDefinition:
shared[
'PropertyDefinition|TSAbstractPropertyDefinition|AccessorProperty|TSAbstractAccessorProperty'
],
TSDeclareFunction(node, context) {
context.write('declare ');
if (node.async) {
context.write('async ');
}
context.write('function');
if (node.generator) {
context.write('*');
}
if (node.id) {
context.write(' ');
context.visit(node.id);
}
if (node.typeParameters) {
context.visit(node.typeParameters);
}
context.write('(');
sequence(context, node.params, node.returnType?.loc?.start ?? node.loc?.end ?? null, false);
context.write(')');
if (node.returnType) {
context.visit(node.returnType);
}
context.write(';');
},
TSNumberKeyword(node, context) {
context.write('number', node);
},
TSStringKeyword(node, context) {
context.write('string', node);
},
TSBooleanKeyword(node, context) {
context.write('boolean', node);
},
TSAnyKeyword(node, context) {
context.write('any', node);
},
TSVoidKeyword(node, context) {
context.write('void', node);
},
TSUnknownKeyword(node, context) {
context.write('unknown', node);
},
TSNeverKeyword(node, context) {
context.write('never', node);
},
TSSymbolKeyword(node, context) {
context.write('symbol', node);
},
TSNullKeyword(node, context) {
context.write('null', node);
},
TSUndefinedKeyword(node, context) {
context.write('undefined', node);
},
TSObjectKeyword(node, context) {
context.write('object', node);
},
TSBigIntKeyword(node, context) {
context.write('bigint', node);
},
TSIntrinsicKeyword(node, context) {
context.write('intrinsic', node);
},
TSArrayType(node, context) {
context.visit(node.elementType);
context.write('[]');
},
TSTypeAnnotation(node, context) {
context.write(': ');
context.visit(node.typeAnnotation);
},
TSTypeLiteral(node, context) {
context.write('{ ');
sequence(context, node.members, node.loc?.end ?? null, false, ';');
context.write(' }');
},
TSPropertySignature(node, context) {
context.visit(node.key);
if (node.optional) context.write('?');
if (node.typeAnnotation) context.visit(node.typeAnnotation);
},
TSTypeReference(node, context) {
context.visit(node.typeName);
if (node.typeArguments) {
context.visit(node.typeArguments);
}
},
TSTypeOperator(node, context) {
context.write(node.operator + ' ');
if (node.typeAnnotation) {
context.visit(node.typeAnnotation);
}
},
TSTemplateLiteralType(node, context) {
context.write('`');
const { quasis, types } = node;
for (let i = 0; i < types.length; i++) {
const raw = quasis[i].value.raw;
context.write(raw + '${');
context.visit(types[i]);
context.write('}');
if (/\n/.test(raw)) context.multiline = true;
}
context.write('`');
},
TSParameterProperty(node, context) {
if (node.accessibility) {
context.write(node.accessibility + ' ');
}
if (node.readonly) {
context.write('readonly ');
}
context.visit(node.parameter);
},
TSExportAssignment(node, context) {
context.write('export = ');
context.visit(node.expression);
context.write(';');
},
TSNamespaceExportDeclaration(node, context) {
context.write('export as namespace ');
context.visit(node.id);
context.write(';');
},
//@ts-expect-error I don't know why, but this is relied upon in the tests, but doesn't exist in the TSESTree types
TSExpressionWithTypeArguments(node, context) {
context.visit(node.expression);
},
TSTypeAssertion(node, context) {
context.write('<');
context.visit(node.typeAnnotation);
context.write('>');
if (EXPRESSIONS_PRECEDENCE[node.expression.type] < EXPRESSIONS_PRECEDENCE.TSTypeAssertion) {
context.write('(');
context.visit(node.expression);
context.write(')');
} else {
context.visit(node.expression);
}
},
TSTypeParameterInstantiation(node, context) {
context.write('<');
for (let i = 0; i < node.params.length; i++) {
context.visit(node.params[i]);
if (i != node.params.length - 1) context.write(', ');
}
context.write('>');
},
TSTypeParameterDeclaration(node, context) {
context.write('<');
for (let i = 0; i < node.params.length; i++) {
context.visit(node.params[i]);
if (i != node.params.length - 1) context.write(', ');
}
context.write('>');
},
TSTypeParameter(node, context) {
if (node.name && node.name.type) context.visit(node.name);
// @ts-expect-error type mismatch TSESTree and acorn-typescript?
else context.write(node.name, node);
if (node.constraint) {
context.write(' extends ');
context.visit(node.constraint);
}
},
TSTypePredicate(node, context) {
if (node.parameterName) {
context.visit(node.parameterName);
} else if (node.typeAnnotation) {
context.visit(node.typeAnnotation);
}
if (node.asserts) {
context.write(' asserts ');
} else {
context.write(' is ');
}
if (node.typeAnnotation) {
context.visit(node.typeAnnotation.typeAnnotation);
}
},
TSTypeQuery(node, context) {
context.write('typeof ');
context.visit(node.exprName);
},
TSClassImplements(node, context) {
if (node.expression) {
context.visit(node.expression);
}
},
TSEnumMember(node, context) {
context.visit(node.id);
if (node.initializer) {
context.write(' = ');
context.visit(node.initializer);
}
},
TSFunctionType: shared['TSFunctionType|TSConstructorType'],
TSIndexSignature(node, context) {
context.write('[');
// @ts-expect-error `acorn-typescript` and `@typescript-eslint/types` have slightly different type definitions
sequence(context, node.parameters, node.typeAnnotation?.loc?.start ?? null, false);
context.write(']');
// @ts-expect-error `acorn-typescript` and `@typescript-eslint/types` have slightly different type definitions
context.visit(node.typeAnnotation);
},
TSMappedType(node, context) {
context.write('{[');
if (node.typeParameter) {
context.visit(node.typeParameter);
} else {
context.visit(node.key);
context.write(' in ');
context.visit(node.constraint);
}
context.write(']');
if (node.typeAnnotation) {
context.write(': ');
context.visit(node.typeAnnotation);
}
context.write('}');
},
TSMethodSignature(node, context) {
context.visit(node.key);
context.write('(');
sequence(
context,
// @ts-expect-error `acorn-typescript` and `@typescript-eslint/types` have slightly different type definitions
node.parameters ?? node.params,
// @ts-expect-error `acorn-typescript` and `@typescript-eslint/types` have slightly different type definitions
(node.typeAnnotation ?? node.returnType)?.loc?.start ?? null,
false
);
context.write(')');
// @ts-expect-error `acorn-typescript` and `@typescript-eslint/types` have slightly different type definitions
if (node.typeAnnotation || node.returnType) {
// @ts-expect-error `acorn-typescript` and `@typescript-eslint/types` have slightly different type definitions
context.visit(node.typeAnnotation ?? node.returnType);
}
},
TSTupleType(node, context) {
context.write('[');
sequence(context, node.elementTypes, node.loc?.end ?? null, false);
context.write(']');
},
TSNamedTupleMember(node, context) {
context.visit(node.label);
context.write(': ');
context.visit(node.elementType);
},
TSUnionType(node, context) {
sequence(context, node.types, node.loc?.end ?? null, false, ' |');
},
TSIntersectionType(node, context) {
sequence(context, node.types, node.loc?.end ?? null, false, ' &');
},
TSInferType(node, context) {
context.write('infer ');
context.visit(node.typeParameter);
},
TSLiteralType(node, context) {
context.visit(node.literal);
},
TSCallSignatureDeclaration:
shared['TSConstructSignatureDeclaration|TSCallSignatureDeclaration'],
TSConditionalType(node, context) {
context.visit(node.checkType);
context.write(' extends ');
context.visit(node.extendsType);
context.write(' ? ');
context.visit(node.trueType);
context.write(' : ');
context.visit(node.falseType);
},
TSConstructSignatureDeclaration:
shared['TSConstructSignatureDeclaration|TSCallSignatureDeclaration'],
TSConstructorType: shared['TSFunctionType|TSConstructorType'],
TSExternalModuleReference(node, context) {
context.write('require(');
context.visit(node.expression);
context.write(');');
},
TSIndexedAccessType(node, context) {
context.visit(node.objectType);
context.write('[');
context.visit(node.indexType);
context.write(']');
},
TSImportEqualsDeclaration(node, context) {
context.write('import ');
context.visit(node.id);
context.write(' = ');
context.visit(node.moduleReference);
},
TSImportType(node, context) {
context.write('import(');
context.visit(node.argument);
context.write(')');
if (node.qualifier) {
context.write('.');
context.visit(node.qualifier);
}
},
TSOptionalType(node, context) {
context.visit(node.typeAnnotation);
context.write('?');
},
TSRestType(node, context) {
context.write('...');
context.visit(node.typeAnnotation);
},
TSThisType(node, context) {
context.write('this', node);
},
TSAsExpression(node, context) {
if (node.expression) {
const needs_parens =
EXPRESSIONS_PRECEDENCE[node.expression.type] < EXPRESSIONS_PRECEDENCE.TSAsExpression;
if (needs_parens) {
context.write('(');
context.visit(node.expression);
context.write(')');
} else {
context.visit(node.expression);
}
}
context.write(' as ');
context.visit(node.typeAnnotation);
},
TSEnumDeclaration(node, context) {
context.write('enum ');
context.visit(node.id);
context.write(' {');
context.indent();
context.newline();
sequence(context, node.members ?? node.body.members, node.loc?.end ?? null, false);
context.dedent();
context.newline();
context.write('}');
},
TSModuleBlock(node, context) {
context.write(' {');
context.indent();
context.newline();
body(context, node);
context.dedent();
context.newline();
context.write('}');
},
TSModuleDeclaration(node, context) {
if (node.declare) context.write('declare ');
else context.write('namespace ');
context.visit(node.id);
if (!node.body) return;
context.visit(node.body);
},
TSNonNullExpression(node, context) {
context.visit(node.expression);
context.write('!');
},
TSInterfaceBody(node, context) {
sequence(context, node.body, node.loc?.end ?? null, true, ';');
},
TSInterfaceDeclaration(node, context) {
context.write('interface ');
context.visit(node.id);
if (node.typeParameters) context.visit(node.typeParameters);
if (node.extends && node.extends.length > 0) {
context.write(' extends ');
sequence(context, node.extends, node.body.loc?.start ?? null, false);
}
context.write(' {');
context.visit(node.body);
context.write('}');
},
TSInstantiationExpression(node, context) {
context.visit(node.expression);
context.visit(node.typeArguments);
},
TSInterfaceHeritage(node, context) {
if (node.expression) {
context.visit(node.expression);
}
},
//@ts-expect-error I don't know why, but this is relied upon in the tests, but doesn't exist in the TSESTree types
TSParenthesizedType(node, context) {
context.write('(');
context.visit(node.typeAnnotation);
context.write(')');
},
TSSatisfiesExpression(node, context) {
if (node.expression) {
const needs_parens =
EXPRESSIONS_PRECEDENCE[node.expression.type] <
EXPRESSIONS_PRECEDENCE.TSSatisfiesExpression;
if (needs_parens) {
context.write('(');
context.visit(node.expression);
context.write(')');
} else {
context.visit(node.expression);
}
}
context.write(' satisfies ');
context.visit(node.typeAnnotation);
},
TSTypeAliasDeclaration(node, context) {
context.write('type ');
context.visit(node.id);
if (node.typeParameters) context.visit(node.typeParameters);
context.write(' = ');
context.visit(node.typeAnnotation);
context.write(';');
},
TSQualifiedName(node, context) {
context.visit(node.left);
context.write('.');
context.visit(node.right);
}
};
};
/** @satisfies {Visitors} */
/**
*
* @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;
}
}
}
/**
* @param {TSESTree.VariableDeclaration} node
* @param {Context} context
*/
function handle_var_declaration(node, context) {
const open = context.new();
const join = context.new();
const child_context = context.new();
context.append(child_context);
if (node.declare) {
child_context.write('declare ');
}
child_context.write(`${node.kind} `);
child_context.append(open);
let first = true;
for (const d of node.declarations) {
if (!first) child_context.append(join);
first = false;
child_context.visit(d);
}
const length = child_context.measure() + 2 * (node.declarations.length - 1);
const multiline = child_context.multiline || (node.declarations.length > 1 && length > 50);
if (multiline) {
context.multiline = true;
if (node.declarations.length > 1) open.indent();
join.write(',');
join.newline();
if (node.declarations.length > 1) context.dedent();
} else {
join.write(', ');
}
}
/**
* @param {string} string
* @param {string} char
*/
function quote(string, char) {
let out = char;
for (const c of string) {
if (c === '\\') {
out += '\\\\';
} else if (c === char) {
out += '\\' + c;
} else if (c === '\n') {
out += '\\n';
} else if (c === '\r') {
out += '\\r';
} else {
out += c;
}
}
return out + char;
}
/**
*
* @param {{ line: number, column: number }} a
* @param {{ line: number, column: number }} b
*/
function before(a, b) {
if (a.line < b.line) return true;
if (a.line > b.line) return false;
return a.column < b.column;
}