You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							199 lines
						
					
					
						
							5.9 KiB
						
					
					
				
			
		
		
	
	
							199 lines
						
					
					
						
							5.9 KiB
						
					
					
				| 
 | |
| 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;
 | |
| 		// Note: Keywords are not allows to contain u escapes
 | |
| 		if (!/\b(?:catch|import|async)\b/.test(code)) {
 | |
| 			return {__proto__: null, code, hasAsync: false};
 | |
| 		}
 | |
| 	} 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;
 | |
| 	let tmpname = 'VM2_INTERNAL_TMPNAME';
 | |
| 
 | |
| 	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) {
 | |
| 				if (param.type === 'ObjectPattern') {
 | |
| 					insertions.push({
 | |
| 						__proto__: null,
 | |
| 						pos: node.start,
 | |
| 						order: TO_RIGHT,
 | |
| 						code: `catch($tmpname){try{throw ${INTERNAL_STATE_NAME}.handleException($tmpname);}`
 | |
| 					});
 | |
| 					insertions.push({
 | |
| 						__proto__: null,
 | |
| 						pos: node.body.end,
 | |
| 						order: TO_LEFT,
 | |
| 						code: `}`
 | |
| 					});
 | |
| 				} else {
 | |
| 					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 (node.name.startsWith(tmpname)) {
 | |
| 				tmpname = node.name + '_UNIQUE';
 | |
| 			}
 | |
| 		} 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.replace(/\$tmpname/g, tmpname);
 | |
| 		curr = change.pos;
 | |
| 	}
 | |
| 	ncode += code.substring(curr);
 | |
| 
 | |
| 	return {__proto__: null, code: ncode, hasAsync};
 | |
| }
 | |
| 
 | |
| exports.INTERNAL_STATE_NAME = INTERNAL_STATE_NAME;
 | |
| exports.transformer = transformer;
 |