UNPKG

babel-plugin-transform-decimal

Version:

A plugin for babel to transform `x * y` into something like `JSBD.multiply(x, y)` to support decimals.

505 lines (494 loc) 17.9 kB
const syntaxDecimal = require('@babel/plugin-syntax-decimal').default; // https://github.com/yukinotech/JSBD#usage const binaryExpressionToFunction = new Map([ ['+', 'add'], ['-', 'subtract'], ['*', 'multiply'], ['/', 'divide'], ['%', 'remainder'], ['**', 'pow'], ['===', 'equal'], ['!==', 'notEqual'], ['<', 'lessThan'], ['>', 'greaterThan'], ['<=', 'lessThanOrEqual'], ['>=', 'greaterThanOrEqual'], ['==', 'equal'], ['!=', 'notEqual'], ]); // https://github.com/yukinotech/JSBD#usage const unaryExpressionToFunction = new Map([ /* TODO */ ]); // https://github.com/yukinotech/JSBD#usage const updateExpressionToFunction = new Map([ ['++', 'add'], ['--', 'subtract'], ]); // https://github.com/yukinotech/JSBD#usage const staticMethods = new Set(['round']); module.exports = function (babel) { const t = babel.types; const visited = new Map(); const canBeDecimal = function (path) { if (visited.has(path)) { return visited.get(path); } visited.set(path, maybeJSBD); const result = canBeDecimalInternal(path); if (result === maybeJSBD) { visited.delete(path); } else { visited.set(path, result); } return result; }; const and = function (a, b) { if (a === maybeJSBD) { return b; } if (b === maybeJSBD) { return a; } if (a === JSBD && b === JSBD) { return JSBD; } return false; }; const canBeDecimalInternal = function (path) { if (path.node.type === 'DecimalLiteral') { return JSBD; } if (path.node.type === 'NumericLiteral') { return false; } if (path.node.type === 'StringLiteral') { return false; } if (path.node.type === 'UnaryExpression') { if (path.node.operator === '+') { return false; // +0m is not allowed } return canBeDecimal(path.get('argument')); } if (path.node.type === 'BinaryExpression') { return and(canBeDecimal(path.get('left')), canBeDecimal(path.get('right'))); } if (path.node.type === 'AssignmentExpression') { return and(canBeDecimal(path.get('left')), canBeDecimal(path.get('right'))); } if (path.node.type === 'Identifier') { const binding = path.scope.getBinding(path.node.name); if (binding != null) { if (binding.path.node.type === 'VariableDeclarator') { const x = binding.path.get('init'); if (x.node != null && canBeDecimal(x) === false) { return false; } } for (const path of binding.referencePaths) { // The next code causes infinite recursion, seems: // if ( // path.parentPath.node.type === 'BinaryExpression' && // binaryExpressionToFunction.has(path.parentPath.node.operator) && // canBeBigDecimal(path.parentPath) === false // ) { // return false; // } } } else if (path.node.name === 'undefined') { return false; } if (binding != null && binding.constant) { const ifStatement = path.findParent((path) => path.isIfStatement()); const variableName = path.node.name; if (ifStatement != null) { const tmp = ifStatement.get('test'); if (tmp.node.operator === '&&') { const checkTypeOf = function (node) { if (node.type === 'BinaryExpression' && /===?/.test(node.operator)) { if (node.left.type === 'UnaryExpression' && node.left.operator === 'typeof') { if ( node.left.argument.type === 'Identifier' && node.left.argument.name === variableName ) { if (node.right.type === 'StringLiteral' && node.right.value === 'number') { return true; } } } } return false; }; if (checkTypeOf(tmp.node.left)) { return false; } if (checkTypeOf(tmp.node.right)) { return false; } } } } return maybeJSBD; } if (path.node.type === 'ConditionalExpression') { return canBeDecimal(path.get('consequent')) !== false || canBeDecimal(path.get('alternate')) !== false ? maybeJSBD : false; } if (path.node.type === 'FunctionExpression') { return false; } if (path.node.type === 'NewExpression') { return false; } if (path.node.type === 'NullLiteral') { return false; } if (path.node.type === 'LogicalExpression') { return false; // TODO } if (path.node.type === 'ObjectProperty') { return false; // TODO } if (path.node.type === 'CallExpression') { if ( path.node.callee.type === 'MemberExpression' && path.node.callee.object.type === 'Identifier' && path.node.callee.object.name === 'Math' ) { return false; } if (path.node.callee.type === 'Identifier' && path.node.callee.name === 'Number') { return false; } if (path.node.callee.type === 'Identifier' && path.node.callee.name === 'BigDecimal') { return JSBD; } // if ( // path.node.callee.type === 'MemberExpression' && // path.node.callee.object.type === 'Identifier' && // path.node.callee.object.name === 'BigDecimal' // ) { // return JSBD; // } } if (path.node.type === 'CallExpression') { if (path.node.callee.type === 'Identifier') { const binding = path.scope.getBinding(path.node.callee.name); if (binding != null) { if (binding.path.node.type === 'FunctionDeclaration') { const statements = binding.path.get('body').get('body'); for (const statement of statements) { if (statement.type === 'ReturnStatement') { if (canBeDecimal(statement.get('argument')) === false) { return false; } } } } } } } if (path.node.type === 'UpdateExpression') { return canBeDecimal(path.get('argument')); } return maybeJSBD; }; const JSBD = 'JSBD'; const maybeJSBD = 'maybeJSBD'; const IMPORT_PATH = 'jsbd'; const maybeJSBDCode = ` var maybeJSBD = { add: function add(a, b) { return typeof a === "object" && typeof b === "object" ? JSBD.add(a, b) : a + b; }, subtract: function subtract(a, b) { return typeof a === "object" && typeof b === "object" ? JSBD.subtract(a, b) : a - b; }, multiply: function multiply(a, b) { return typeof a === "object" && typeof b === "object" ? JSBD.multiply(a, b) : a * b; }, divide: function divide(a, b) { return typeof a === "object" && typeof b === "object" ? JSBD.divide(a, b) : a / b; }, remainder: function remainder(a, b) { return typeof a === "object" && typeof b === "object" ? JSBD.remainder(a, b) : a % b; }, pow: function pow(a, b) { return typeof a === "object" && typeof b === "object" ? JSBD.pow(a, b) : (typeof a === "bigint" && typeof b === "bigint" ? new Function("a**b", "a", "b")(a, b) : Math.pow(a, b)); }, lessThan: function lessThan(a, b) { return typeof a === "object" && typeof b === "object" ? JSBD.lessThan(a, b) : a < b; }, greaterThan: function greaterThan(a, b) { return typeof a === "object" && typeof b === "object" ? JSBD.greaterThan(a, b) : a > b; }, lessThanOrEqual: function lessThanOrEqual(a, b) { return typeof a === "object" && typeof b === "object" ? JSBD.lessThanOrEqual(a, b) : a <= b; }, greaterThanOrEqual: function greaterThanOrEqual(a, b) { return typeof a === "object" && typeof b === "object" ? JSBD.greaterThanOrEqual(a, b) : a >= b; }, equal: function equal(a, b) { return typeof a === "object" && typeof b === "object" ? JSBD.equal(a, b) : a === b; }, notEqual: function notEqual(a, b) { return typeof a === "object" && typeof b === "object" ? JSBD.notEqual(a, b) : a !== b; }, }; `; return { name: 'transform-bigdecimal', inherits: syntaxDecimal, visitor: { CallExpression: function (path) { if (path.node.callee.name === 'BigDecimal') { path.replaceWith( t.callExpression( t.memberExpression(t.identifier(JSBD), t.identifier('BigDecimal')), path.node.arguments ) ); this.shouldInjectHelperCode = true; } if (path.node.callee.type === 'MemberExpression') { if (path.node.callee.object.name === 'BigDecimal') { if (staticMethods.has(path.node.callee.property.name)) { path.replaceWith( t.callExpression( t.memberExpression( t.identifier(JSBD), t.identifier(path.node.callee.property.name) ), path.node.arguments ) ); this.shouldInjectHelperCode = true; } } } }, DecimalLiteral: function (path) { const value = path.node.value; path.replaceWith( t.callExpression(t.memberExpression(t.identifier(JSBD), t.identifier('BigDecimal')), [ t.stringLiteral(value), ]) ); this.shouldInjectHelperCode = true; }, BinaryExpression: function (path) { const JSBD = canBeDecimal(path); if (JSBD !== false) { const operator = path.node.operator; if (binaryExpressionToFunction.has(operator)) { const functionName = binaryExpressionToFunction.get(operator); // x * y -> JSBD.multiply(x, y) path.replaceWith( t.callExpression(t.memberExpression(t.identifier(JSBD), t.identifier(functionName)), [ path.node.left, path.node.right, ]) ); this.shouldInjectHelperCode = true; } } }, UnaryExpression: function (path) { const JSBD = canBeDecimal(path); if (JSBD !== false) { if (unaryExpressionToFunction.has(path.node.operator)) { const functionName = unaryExpressionToFunction.get(path.node.operator); // -x -> JSBD.unaryMinus(x) path.replaceWith( t.callExpression(t.memberExpression(t.identifier(JSBD), t.identifier(functionName)), [ path.node.argument, ]) ); this.shouldInjectHelperCode = true; } } }, UpdateExpression: function (path) { const JSBD = canBeDecimal(path); if (JSBD !== false) { const operator = path.node.operator; const prefix = path.node.prefix; if (updateExpressionToFunction.has(operator)) { const functionName = updateExpressionToFunction.get(operator); const one = t.callExpression( t.memberExpression(t.identifier(JSBD), t.identifier('BigDecimal')), [t.numericLiteral(1)] ); const argument = path.node.argument; if (t.isMemberExpression(argument)) { if (prefix) { const x = path.scope.generateUidIdentifier('x'); path.scope.push({ id: x }); const y = path.scope.generateUidIdentifier('y'); path.scope.push({ id: y }); // ++object[property] -> (x = object, y = property, x[y] = x[y] + 1) path.replaceWith( t.sequenceExpression([ t.assignmentExpression('=', x, argument.object), t.assignmentExpression( '=', y, argument.computed ? argument.property : t.StringLiteral(argument.property.name) ), t.assignmentExpression( '=', t.memberExpression(x, y, true), t.callExpression( t.memberExpression(t.identifier(JSBD), t.identifier(functionName)), [t.memberExpression(x, y, true), one] ) ), ]) ); } else { const x = path.scope.generateUidIdentifier('x'); path.scope.push({ id: x }); const y = path.scope.generateUidIdentifier('y'); path.scope.push({ id: y }); const z = path.scope.generateUidIdentifier('z'); path.scope.push({ id: z }); // object[property]++ -> (x = object, y = property, z = x[y], x[y] = x[y] + 1, z) path.replaceWith( t.sequenceExpression([ t.assignmentExpression('=', x, argument.object), t.assignmentExpression( '=', y, argument.computed ? argument.property : t.StringLiteral(argument.property.name) ), t.assignmentExpression('=', z, t.memberExpression(x, y, true)), t.assignmentExpression( '=', t.memberExpression(x, y, true), t.callExpression( t.memberExpression(t.identifier(JSBD), t.identifier(functionName)), [z, one] ) ), z, ]) ); } } else if (prefix) { // ++argument -> (argument = argument + 1) path.replaceWith( t.assignmentExpression( '=', argument, t.callExpression( t.memberExpression(t.identifier(JSBD), t.identifier(functionName)), [argument, one] ) ) ); } else { const x = path.scope.generateUidIdentifier('x'); path.scope.push({ id: x }); // argument++ -> (x = argument, argument = argument + 1, x) path.replaceWith( t.sequenceExpression([ t.assignmentExpression('=', x, argument), t.assignmentExpression( '=', argument, t.callExpression( t.memberExpression(t.identifier(JSBD), t.identifier(functionName)), [argument, one] ) ), x, ]) ); } this.shouldInjectHelperCode = true; } } }, AssignmentExpression: function (path) { const D = canBeDecimal(path); if (D !== false) { const operator = path.node.operator; if (operator.endsWith('=')) { const functionName = binaryExpressionToFunction.get(operator.slice(0, -'='.length)); if (functionName != null) { const left = path.node.left; const right = path.node.right; if (t.isMemberExpression(left)) { const x = path.scope.generateUidIdentifier('x'); path.scope.push({ id: x }); const y = path.scope.generateUidIdentifier('y'); path.scope.push({ id: y }); // object[property] += right -> (x = object, y = property, x[y] = x[y] + right) path.replaceWith( t.sequenceExpression([ t.assignmentExpression('=', x, left.object), t.assignmentExpression( '=', y, left.computed ? left.property : t.StringLiteral(left.property.name) ), t.assignmentExpression( '=', t.memberExpression(x, y, true), t.callExpression( t.memberExpression(t.identifier(D), t.identifier(functionName)), [t.memberExpression(x, y, true), right] ) ), ]) ); } else { // left += right -> (left = left + right) path.replaceWith( t.assignmentExpression( '=', left, t.callExpression( t.memberExpression(t.identifier(D), t.identifier(functionName)), [left, right] ) ) ); } this.shouldInjectHelperCode = true; } } } }, Program: { exit: function (path) { if (this.shouldInjectHelperCode) { // https://stackoverflow.com/a/35994497 const identifier = t.identifier(JSBD); const importDefaultSpecifier = t.importDefaultSpecifier(identifier); const importDeclaration = t.importDeclaration( [importDefaultSpecifier], t.stringLiteral(IMPORT_PATH) ); path.unshiftContainer('body', importDeclaration); } }, }, }, pre: function () { this.shouldInjectHelperCode = false; }, post: function (state) { if (this.shouldInjectHelperCode) { state.ast.program.body.unshift(babel.parse(maybeJSBDCode).program.body[0]); } }, }; };