import * as acorn from 'acorn'; import { walk } from 'estree-walker'; import { id, re } from './utils/id.js'; import { get_comment_handlers } from './utils/comments.js'; /** @typedef {import('estree').Expression} Expression */ /** @typedef {import('estree').Node} Node */ /** @typedef {import('estree').ObjectExpression} ObjectExpression */ /** @typedef {import('estree').Property} Property */ /** @typedef {import('estree').SpreadElement} SpreadElement */ /** @typedef {import('./utils/comments').CommentWithLocation} CommentWithLocation */ /** @type {Record} */ const sigils = { '@': 'AT', '#': 'HASH' }; /** @param {TemplateStringsArray} strings */ const join = (strings) => { let str = strings[0]; for (let i = 1; i < strings.length; i += 1) { str += `_${id}_${i - 1}_${strings[i]}`; } return str.replace( /([@#])(\w+)/g, (_m, sigil, name) => `_${id}_${sigils[sigil]}_${name}` ); }; /** * @param {any[]} array * @param {any[]} target */ const flatten_body = (array, target) => { for (let i = 0; i < array.length; i += 1) { const statement = array[i]; if (Array.isArray(statement)) { flatten_body(statement, target); continue; } if (statement.type === 'ExpressionStatement') { if (statement.expression === EMPTY) continue; if (Array.isArray(statement.expression)) { // TODO this is hacktacular let node = statement.expression[0]; while (Array.isArray(node)) node = node[0]; if (node) node.leadingComments = statement.leadingComments; flatten_body(statement.expression, target); continue; } if (/(Expression|Literal)$/.test(statement.expression.type)) { target.push(statement); continue; } if (statement.leadingComments) statement.expression.leadingComments = statement.leadingComments; if (statement.trailingComments) statement.expression.trailingComments = statement.trailingComments; target.push(statement.expression); continue; } target.push(statement); } return target; }; /** * @param {any[]} array * @param {any[]} target */ const flatten_properties = (array, target) => { for (let i = 0; i < array.length; i += 1) { const property = array[i]; if (property.value === EMPTY) continue; if (property.key === property.value && Array.isArray(property.key)) { flatten_properties(property.key, target); continue; } target.push(property); } return target; }; /** * @param {any[]} nodes * @param {any[]} target */ const flatten = (nodes, target) => { for (let i = 0; i < nodes.length; i += 1) { const node = nodes[i]; if (node === EMPTY) continue; if (Array.isArray(node)) { flatten(node, target); continue; } target.push(node); } return target; }; const EMPTY = { type: 'Empty' }; /** * * @param {CommentWithLocation[]} comments * @param {string} raw * @returns {any} */ const acorn_opts = (comments, raw) => { const { onComment } = get_comment_handlers(comments, raw); return { ecmaVersion: 2022, sourceType: 'module', allowAwaitOutsideFunction: true, allowImportExportEverywhere: true, allowReturnOutsideFunction: true, onComment }; }; /** * @param {string} raw * @param {Node} node * @param {any[]} values * @param {CommentWithLocation[]} comments */ const inject = (raw, node, values, comments) => { comments.forEach((comment) => { comment.value = comment.value.replace(re, (m, i) => +i in values ? values[+i] : m ); }); const { enter, leave } = get_comment_handlers(comments, raw); return walk(node, { enter, /** @param {any} node */ leave(node) { if (node.type === 'Identifier') { re.lastIndex = 0; const match = re.exec(node.name); if (match) { if (match[1]) { if (+match[1] in values) { let value = values[+match[1]]; if (typeof value === 'string') { value = { type: 'Identifier', name: value, leadingComments: node.leadingComments, trailingComments: node.trailingComments }; } else if (typeof value === 'number') { value = { type: 'Literal', value, leadingComments: node.leadingComments, trailingComments: node.trailingComments }; } this.replace(value || EMPTY); } } else { node.name = `${match[2] ? `@` : `#`}${match[4]}`; } } } if (node.type === 'Literal') { if (typeof node.value === 'string') { re.lastIndex = 0; const new_value = /** @type {string} */ (node.value).replace( re, (m, i) => (+i in values ? values[+i] : m) ); const has_changed = new_value !== node.value; node.value = new_value; if (has_changed && node.raw) { // preserve the quotes node.raw = `${node.raw[0]}${JSON.stringify(node.value).slice( 1, -1 )}${node.raw[node.raw.length - 1]}`; } } } if (node.type === 'TemplateElement') { re.lastIndex = 0; node.value.raw = /** @type {string} */ (node.value.raw).replace( re, (m, i) => (+i in values ? values[+i] : m) ); } if (node.type === 'Program' || node.type === 'BlockStatement') { node.body = flatten_body(node.body, []); } if (node.type === 'ObjectExpression' || node.type === 'ObjectPattern') { node.properties = flatten_properties(node.properties, []); } if (node.type === 'ArrayExpression' || node.type === 'ArrayPattern') { node.elements = flatten(node.elements, []); } if ( node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration' || node.type === 'ArrowFunctionExpression' ) { node.params = flatten(node.params, []); } if (node.type === 'CallExpression' || node.type === 'NewExpression') { node.arguments = flatten(node.arguments, []); } if ( node.type === 'ImportDeclaration' || node.type === 'ExportNamedDeclaration' ) { node.specifiers = flatten(node.specifiers, []); } if (node.type === 'ForStatement') { node.init = node.init === EMPTY ? null : node.init; node.test = node.test === EMPTY ? null : node.test; node.update = node.update === EMPTY ? null : node.update; } leave(node); } }); }; /** * * @param {TemplateStringsArray} strings * @param {any[]} values * @returns {Node[]} */ export function b(strings, ...values) { const str = join(strings); /** @type {CommentWithLocation[]} */ const comments = []; try { let ast = /** @type {any} */ (acorn.parse(str, acorn_opts(comments, str))); ast = inject(str, ast, values, comments); return ast.body; } catch (err) { handle_error(str, err); } } /** * * @param {TemplateStringsArray} strings * @param {any[]} values * @returns {Expression & { start: Number, end: number }} */ export function x(strings, ...values) { const str = join(strings); /** @type {CommentWithLocation[]} */ const comments = []; try { let expression = /** @type {Expression & { start: Number, end: number }} */ ( acorn.parseExpressionAt(str, 0, acorn_opts(comments, str)) ); const match = /\S+/.exec(str.slice(expression.end)); if (match) { throw new Error(`Unexpected token '${match[0]}'`); } expression = /** @type {Expression & { start: Number, end: number }} */ ( inject(str, expression, values, comments) ); return expression; } catch (err) { handle_error(str, err); } } /** * * @param {TemplateStringsArray} strings * @param {any[]} values * @returns {(Property | SpreadElement) & { start: Number, end: number }} */ export function p(strings, ...values) { const str = `{${join(strings)}}`; /** @type {CommentWithLocation[]} */ const comments = []; try { let expression = /** @type {any} */ ( acorn.parseExpressionAt(str, 0, acorn_opts(comments, str)) ); expression = inject(str, expression, values, comments); return expression.properties[0]; } catch (err) { handle_error(str, err); } } /** * @param {string} str * @param {Error} err */ function handle_error(str, err) { // TODO location/code frame re.lastIndex = 0; str = str.replace(re, (m, i, at, hash, name) => { if (at) return `@${name}`; if (hash) return `#${name}`; return '${...}'; }); console.log(`failed to parse:\n${str}`); throw err; } export { print } from './print/index.js'; /** * @param {string} source * @param {any} opts */ export const parse = (source, opts) => { /** @type {CommentWithLocation[]} */ const comments = []; const { onComment, enter, leave } = get_comment_handlers(comments, source); const ast = /** @type {any} */ (acorn.parse(source, { onComment, ...opts })); walk(ast, { enter, leave }); return ast; }; /** * @param {string} source * @param {number} index * @param {any} opts */ export const parseExpressionAt = (source, index, opts) => { /** @type {CommentWithLocation[]} */ const comments = []; const { onComment, enter, leave } = get_comment_handlers(comments, source); const ast = /** @type {any} */ ( acorn.parseExpressionAt(source, index, { onComment, ...opts }) ); walk(ast, { enter, leave }); return ast; };