177 lines
5.2 KiB
JavaScript
177 lines
5.2 KiB
JavaScript
|
|
const {Parser: AcornParser, isNewLine: acornIsNewLine, getLineInfo: acornGetLineInfo} = require('acorn');
|
|
const {full: acornWalkFull} = require('acorn-walk');
|
|
|
|
const INTERNAL_STATE_NAME = 'VM2_INTERNAL_STATE_DO_NOT_USE_OR_PROGRAM_WILL_FAIL';
|
|
|
|
function assertType(node, type) {
|
|
if (!node) throw new Error(`None existent node expected '${type}'`);
|
|
if (node.type !== type) throw new Error(`Invalid node type '${node.type}' expected '${type}'`);
|
|
return node;
|
|
}
|
|
|
|
function makeNiceSyntaxError(message, code, filename, location, tokenizer) {
|
|
const loc = acornGetLineInfo(code, location);
|
|
let end = location;
|
|
while (end < code.length && !acornIsNewLine(code.charCodeAt(end))) {
|
|
end++;
|
|
}
|
|
let markerEnd = tokenizer.start === location ? tokenizer.end : location + 1;
|
|
if (!markerEnd || markerEnd > end) markerEnd = end;
|
|
let markerLen = markerEnd - location;
|
|
if (markerLen <= 0) markerLen = 1;
|
|
if (message === 'Unexpected token') {
|
|
const type = tokenizer.type;
|
|
if (type.label === 'name' || type.label === 'privateId') {
|
|
message = 'Unexpected identifier';
|
|
} else if (type.label === 'eof') {
|
|
message = 'Unexpected end of input';
|
|
} else if (type.label === 'num') {
|
|
message = 'Unexpected number';
|
|
} else if (type.label === 'string') {
|
|
message = 'Unexpected string';
|
|
} else if (type.label === 'regexp') {
|
|
message = 'Unexpected token \'/\'';
|
|
markerLen = 1;
|
|
} else {
|
|
const token = tokenizer.value || type.label;
|
|
message = `Unexpected token '${token}'`;
|
|
}
|
|
}
|
|
const error = new SyntaxError(message);
|
|
if (!filename) return error;
|
|
const line = code.slice(location - loc.column, end);
|
|
const marker = line.slice(0, loc.column).replace(/\S/g, ' ') + '^'.repeat(markerLen);
|
|
error.stack = `${filename}:${loc.line}\n${line}\n${marker}\n\n${error.stack}`;
|
|
return error;
|
|
}
|
|
|
|
function transformer(args, body, isAsync, isGenerator, filename) {
|
|
let code;
|
|
let argsOffset;
|
|
if (args === null) {
|
|
code = body;
|
|
} else {
|
|
code = isAsync ? '(async function' : '(function';
|
|
if (isGenerator) code += '*';
|
|
code += ' anonymous(';
|
|
code += args;
|
|
argsOffset = code.length;
|
|
code += '\n) {\n';
|
|
code += body;
|
|
code += '\n})';
|
|
}
|
|
|
|
const parser = new AcornParser({
|
|
__proto__: null,
|
|
ecmaVersion: 2022,
|
|
allowAwaitOutsideFunction: args === null && isAsync,
|
|
allowReturnOutsideFunction: args === null
|
|
}, code);
|
|
let ast;
|
|
try {
|
|
ast = parser.parse();
|
|
} catch (e) {
|
|
// Try to generate a nicer error message.
|
|
if (e instanceof SyntaxError && e.pos !== undefined) {
|
|
let message = e.message;
|
|
const match = message.match(/^(.*) \(\d+:\d+\)$/);
|
|
if (match) message = match[1];
|
|
e = makeNiceSyntaxError(message, code, filename, e.pos, parser);
|
|
}
|
|
throw e;
|
|
}
|
|
|
|
if (args !== null) {
|
|
const pBody = assertType(ast, 'Program').body;
|
|
if (pBody.length !== 1) throw new SyntaxError('Single function literal required');
|
|
const expr = pBody[0];
|
|
if (expr.type !== 'ExpressionStatement') throw new SyntaxError('Single function literal required');
|
|
const func = expr.expression;
|
|
if (func.type !== 'FunctionExpression') throw new SyntaxError('Single function literal required');
|
|
if (func.body.start !== argsOffset + 3) throw new SyntaxError('Unexpected end of arg string');
|
|
}
|
|
|
|
const insertions = [];
|
|
let hasAsync = false;
|
|
|
|
const TO_LEFT = -100;
|
|
const TO_RIGHT = 100;
|
|
|
|
let internStateValiable = undefined;
|
|
|
|
acornWalkFull(ast, (node, state, type) => {
|
|
if (type === 'Function') {
|
|
if (node.async) hasAsync = true;
|
|
}
|
|
const nodeType = node.type;
|
|
if (nodeType === 'CatchClause') {
|
|
const param = node.param;
|
|
if (param) {
|
|
const name = assertType(param, 'Identifier').name;
|
|
const cBody = assertType(node.body, 'BlockStatement');
|
|
if (cBody.body.length > 0) {
|
|
insertions.push({
|
|
__proto__: null,
|
|
pos: cBody.body[0].start,
|
|
order: TO_LEFT,
|
|
code: `${name}=${INTERNAL_STATE_NAME}.handleException(${name});`
|
|
});
|
|
}
|
|
}
|
|
} else if (nodeType === 'WithStatement') {
|
|
insertions.push({
|
|
__proto__: null,
|
|
pos: node.object.start,
|
|
order: TO_LEFT,
|
|
code: INTERNAL_STATE_NAME + '.wrapWith('
|
|
});
|
|
insertions.push({
|
|
__proto__: null,
|
|
pos: node.object.end,
|
|
order: TO_RIGHT,
|
|
code: ')'
|
|
});
|
|
} else if (nodeType === 'Identifier') {
|
|
if (node.name === INTERNAL_STATE_NAME) {
|
|
if (internStateValiable === undefined || internStateValiable.start > node.start) {
|
|
internStateValiable = node;
|
|
}
|
|
}
|
|
} else if (nodeType === 'ImportExpression') {
|
|
insertions.push({
|
|
__proto__: null,
|
|
pos: node.start,
|
|
order: TO_RIGHT,
|
|
code: INTERNAL_STATE_NAME + '.'
|
|
});
|
|
}
|
|
});
|
|
|
|
if (internStateValiable) {
|
|
throw makeNiceSyntaxError('Use of internal vm2 state variable', code, filename, internStateValiable.start, {
|
|
__proto__: null,
|
|
start: internStateValiable.start,
|
|
end: internStateValiable.end
|
|
});
|
|
}
|
|
|
|
if (insertions.length === 0) return {__proto__: null, code, hasAsync};
|
|
|
|
insertions.sort((a, b) => (a.pos == b.pos ? a.order - b.order : a.pos - b.pos));
|
|
|
|
let ncode = '';
|
|
let curr = 0;
|
|
for (let i = 0; i < insertions.length; i++) {
|
|
const change = insertions[i];
|
|
ncode += code.substring(curr, change.pos) + change.code;
|
|
curr = change.pos;
|
|
}
|
|
ncode += code.substring(curr);
|
|
|
|
return {__proto__: null, code: ncode, hasAsync};
|
|
}
|
|
|
|
exports.INTERNAL_STATE_NAME = INTERNAL_STATE_NAME;
|
|
exports.transformer = transformer;
|