├── .gitignore ├── .travis.yml ├── README.md ├── analyze.js ├── index.js ├── package.json ├── replacement ├── array.js ├── expression.js ├── index.js ├── math.js ├── memberAccess.js ├── primitives.js └── variable.js ├── test └── index.js └── util.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | *.bin 10 | 11 | # Packages # 12 | ############ 13 | # it's better to unpack these files and commit the raw source 14 | # git has its own built in compression methods 15 | *.7z 16 | *.dmg 17 | *.gz 18 | *.iso 19 | *.jar 20 | *.rar 21 | *.tar 22 | *.zip 23 | 24 | # Logs and databases # 25 | ###################### 26 | *.log 27 | *.sql 28 | *.sqlite 29 | 30 | # OS generated files # 31 | ###################### 32 | .DS_Store 33 | .DS_Store? 34 | *._* 35 | .Spotlight-V100 36 | .Trashes 37 | Icon? 38 | ehthumbs.db 39 | Thumbs.db 40 | 41 | # My extension # 42 | ################ 43 | *.lock 44 | *.bak 45 | lsn 46 | *.dump 47 | *.beam 48 | *.[0-9] 49 | *._[0-9] 50 | *.ns 51 | Scripting_* 52 | docs 53 | *.pdf 54 | *.pak 55 | 56 | 57 | design 58 | instances 59 | *node_modules 60 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.11" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ast-eval [![Build Status](https://travis-ci.org/dfcreative/ast-eval.svg?branch=master)](https://travis-ci.org/dfcreative/ast-eval) [![experimental](http://badges.github.io/stability-badges/dist/experimental.svg)](http://github.com/badges/stability-badges) 2 | 3 | Statically evaluate expressions in AST, also known as [constants folding](http://en.wikipedia.org/wiki/Constant_folding). Useful for precompilation tasks. 4 | 5 | 6 | ## Use 7 | 8 | ```sh 9 | npm install --save ast-eval 10 | ``` 11 | 12 | ```js 13 | var esprima = require('esprima'); 14 | var gen = require('escodegen').generate; 15 | var astEval = require('ast-eval'); 16 | 17 | var ast = esprima.parse('[1, 2 === "2", 3+4*10, [2] === 2]'); 18 | ast = astEval(ast); 19 | 20 | gen(ast); //'[1, false, 43, false]' 21 | ``` 22 | 23 | 24 | ## API 25 | 26 | ### preeval(Node, options) → Node 27 | 28 | Evaluate expressions in a Node, return a new Node with optimized shorten expression nodes. 29 | 30 | 39 | 40 | 41 | ## Features 42 | 43 | * [x] Fold expressions 44 | * [x] Binary expressions: `1000 * 60 * 60` → `36e6` 45 | * [x] Logical expressions: `{a:1} && {b:2}` → `true` 46 | * [x] Math expressions: `Math.sin(Math.Pi / 2 )` → `1` 47 | 48 | * [x] Fold arrays 49 | * [x] Safe methods: `[1,2,3].concat(4, [5])` → `[1,2,3,4,5]` 50 | * [x] Unsafe methods: `[1,2,3].map(function(x){ return x*2})` → `[2,4,6]` 51 | * [ ] Static methods: `Array.from([1, 2, 3], function(x){ return x*2})` → `[2,4,6]` 52 | * [ ] Prototype methods: `Array.prototype.slice.call([1,2,3], 1,2)` → `[2]` 53 | 54 | * [ ] Fold static globals 55 | 56 | * [x] Decompute object access (optionally) 57 | * [x] `a['x'] = 1` → `a.x = 1` 58 | 59 | * [x] Fold strings 60 | * [x] `'a b c'.split(' ')` → `['a', 'b', 'c']` 61 | 62 | * [ ] [Propagate constants](http://en.wikipedia.org/wiki/Constant_folding#Constant_propagation) 63 | * [ ] Simple flow analysis: `var x = 1; x + 2;` → `3;` 64 | * [ ] Scope analysis 65 | * [ ] Method substitution: `var slice = Array.prototype.slice; var x = [1,2,3]; var y = slice(x)'` 66 | 67 | * [ ] Fold loops 68 | * [ ] `var x = []; for (var i = 0; i < 10; i++) {x[i] = 10*i;}` 69 | 70 | * [ ] Fold proxy functions 71 | 72 | * [ ] Remove unused props 73 | 74 | * [ ] Undead code 75 | * [ ] Empty isolated functions 76 | * [ ] Remove unused variables (after enabling constants) 77 | * [ ] Remove unused functions 78 | * [ ] Remove unused properties 79 | 80 | * [ ] Fold clone-code 81 | * `a.x`×3 → `var _a = a; _a.x` 82 | 83 | * [ ] Data-flow analysis 84 | * [ ] Precall functions 85 | * [ ] Substitute variables 86 | 87 | * [ ] Provide exports 88 | 89 | * [ ] Fold primitives 90 | * [ ] new Array([1,2,3,...]) 91 | * [ ] [1,2,3,...] 92 | 93 | * [ ] Rearrange things 94 | * [ ] Hoist functions (place after first use) 95 | * [ ] Fold variable declarations 96 | 97 | 98 | 99 | ## References 100 | 101 | * [List of compiler optimizations](http://en.wikipedia.org/wiki/Optimizing_compiler) — ideas of folding. 102 | * Substack’s [static-eval](https://github.com/substack/static-eval) — evaluate static expressions. 103 | * [esmangle](https://github.com/estools/esmangle) 104 | 105 | 106 | [![NPM](https://nodei.co/npm/ast-eval.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/ast-eval/) -------------------------------------------------------------------------------- /analyze.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Analyze AST, associate data with nodes. 3 | * 4 | * @module ast-eval/data 5 | */ 6 | 7 | var isObject = require('is-object'); 8 | var extend = require('xtend/mutable'); 9 | var types = require('ast-types'); 10 | var escope = require('escope'); 11 | var visit = types.visit; 12 | var n = types.namedTypes; 13 | 14 | 15 | /** Cache of data associated with nodes */ 16 | var nodeCache = new WeakMap(); 17 | 18 | 19 | 20 | /** Default data associated with node */ 21 | var defaultNodeData = { 22 | //self 23 | node: null, 24 | 25 | //parent node 26 | parent: null, 27 | 28 | //scope from escope 29 | scope: null, 30 | 31 | //reference this node resolves to (from scope) 32 | reference: null 33 | }; 34 | 35 | 36 | /** 37 | * Analyze ast, save info for each node within 38 | * Variable info 39 | * Scope 40 | * Infered type 41 | * Mutability 42 | */ 43 | function analyze(node){ 44 | var scopeMan = escope.analyze(node, { 45 | ignoreEval: true, 46 | optimistic: true 47 | }); 48 | 49 | visit(node, { 50 | visitNode: function(path){ 51 | var node = path.node; 52 | 53 | //save parent 54 | setData(node, 'parent', path.parent && path.parent.node); 55 | 56 | //find out & save current scope 57 | //FIXME: for undeclared global vars scope is undefined 58 | var scopeNode = path.scope.node; 59 | var scope = scopeMan.scopes.find(function(scope){ 60 | return scope.block === scopeNode; 61 | }); 62 | setData(node, 'scope', scope); 63 | 64 | 65 | //find out and save variable with it’s own scope 66 | if (n.Identifier.check(node)) { 67 | 68 | //find variable in global list of variables 69 | var reference = findReference(node); 70 | 71 | if (reference) { 72 | setData(node, 'reference', reference); 73 | } 74 | } 75 | 76 | this.traverse(path); 77 | } 78 | }); 79 | 80 | 81 | /** Find reference of a node within the scopeManager (all scopes for the node) */ 82 | function findReference (node) { 83 | var result; 84 | 85 | n.Identifier.assert(node); 86 | 87 | //go through all scopes 88 | scopeMan.scopes.some(function (scope) { 89 | //if scope contains reference to the identifier passed - get it’s variable 90 | return scope.references.some(function(reference){ 91 | if (reference.identifier === node) { 92 | return result = reference; 93 | } 94 | }); 95 | }); 96 | 97 | return result; 98 | } 99 | } 100 | 101 | 102 | 103 | /** 104 | * Set node data 105 | */ 106 | function setData (node, key, value) { 107 | var nodeData; 108 | if (nodeCache.has(node)) { 109 | nodeData = nodeCache.get(node); 110 | } else { 111 | nodeData = extend({}, defaultNodeData, {node: node}); 112 | nodeCache.set(node, nodeData); 113 | } 114 | 115 | //set multiple data 116 | if (isObject(key)) { 117 | nodeData = extend(nodeData, key); 118 | } 119 | 120 | //set single value 121 | else { 122 | nodeData[key] = value; 123 | } 124 | 125 | return nodeData; 126 | } 127 | 128 | 129 | /** 130 | * Retrieve node data 131 | */ 132 | function getData(node, key){ 133 | if (arguments.length === 1) { 134 | return nodeCache.get(node); 135 | } 136 | else { 137 | return nodeCache.get(node)[key]; 138 | } 139 | } 140 | 141 | 142 | module.exports = analyze; 143 | module.exports.getData = getData; 144 | module.exports.setData = setData; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Eval expressions in AST. 3 | * Calls all sub-evals. 4 | * 5 | * @module ast-eval 6 | */ 7 | 8 | var types = require('ast-types'); 9 | var n = types.namedTypes, b = types.builders; 10 | var gen = require('escodegen').generate; 11 | var u = require('./util'); 12 | var parse = require('esprima').parse; 13 | var uneval = require('tosource'); 14 | var analyzeAst = require('./analyze'); 15 | var extend = require('xtend/mutable'); 16 | var hoist = require('ast-redeclare'); 17 | var r = require('./replacement'); 18 | 19 | 20 | /** 21 | * Init replacements. 22 | * Simple expressions go last to let more complex patterns trigger first. 23 | */ 24 | 25 | [].push.apply(r, require('./replacement/array')); 26 | [].push.apply(r, require('./replacement/math')); 27 | 28 | r.push( 29 | require('./replacement/memberAccess'), 30 | require('./replacement/primitives'), 31 | require('./replacement/variable') 32 | ); 33 | 34 | [].push.apply(r, require('./replacement/expression')); 35 | 36 | 37 | /** Default options */ 38 | var defaults = { 39 | optimize: false, 40 | computeProps: false, 41 | externs: {} 42 | }; 43 | 44 | 45 | /** 46 | * Eval AST with options passed 47 | */ 48 | function evalAst (ast, options) { 49 | options = extend({}, defaults, options); 50 | 51 | //fold variable declarations (so that only one entry declaration for a var). 52 | ast = hoist(ast); 53 | 54 | //analyze nodes 55 | analyzeAst(ast); 56 | 57 | //eval simple expressions 58 | types.visit(ast, { 59 | // catch entry nodes to eval 60 | visitNode: function (path) { 61 | //go leafs first to be able to eval simple things first (easier to check) 62 | this.traverse(path); 63 | 64 | var node = path.node; 65 | 66 | //if node is evaluable 67 | if (r.test(node)) { 68 | 69 | var evaledNode = r.eval(node); 70 | 71 | //ignore unchanged node 72 | if (types.astNodesAreEquivalent(node, evaledNode)) return false; 73 | 74 | //compare optimized node result, ignore if it is lengthen 75 | if (options.optimize) { 76 | //TODO 77 | } 78 | 79 | //FIXME: analyze updated subtree 80 | 81 | return evaledNode; 82 | } 83 | } 84 | }); 85 | 86 | return ast; 87 | } 88 | 89 | 90 | evalAst.defaults = defaults; 91 | module.exports = evalAst; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ast-eval", 3 | "version": "0.8.0", 4 | "description": "Statically evaluate AST", 5 | "main": "index.js", 6 | "files": [ 7 | "*.js" 8 | ], 9 | "directories": { 10 | "test": "test" 11 | }, 12 | "scripts": { 13 | "test": "mocha --harmony" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "http://github.com/dfcreative/ast-eval" 18 | }, 19 | "keywords": [ 20 | "static-eval", 21 | "ecmascript", 22 | "esprima", 23 | "ast", 24 | "es-trim", 25 | "estrim", 26 | "espurify", 27 | "ast-eval", 28 | "static", 29 | "static-analysis", 30 | "estools", 31 | "eval", 32 | "es-eval", 33 | "eseval", 34 | "preeval", 35 | "pre-eval", 36 | "fold", 37 | "constants", 38 | "compiler", 39 | "optimizing", 40 | "dataflow", 41 | "data-flow", 42 | "analysis" 43 | ], 44 | "author": "Deema Ywanov ", 45 | "license": "Unlicensed", 46 | "bugs": { 47 | "url": "https://github.com/dfcreative/ast-eval/issues" 48 | }, 49 | "homepage": "https://github.com/dfcreative/ast-eval", 50 | "devDependencies": { 51 | "chai": "^2.0.0", 52 | "mocha": "^2.1.0" 53 | }, 54 | "dependencies": { 55 | "ast-redeclare": "^1.0.4", 56 | "ast-replace": "^1.1.3", 57 | "ast-test": "^1.1.1", 58 | "ast-types": "^0.6.12", 59 | "clone": "^1.0.0", 60 | "escodegen": "^1.6.1", 61 | "escope": "^2.0.4", 62 | "esprima": "^2.0.0", 63 | "esquery": "^0.3.0", 64 | "is-object": "~1.0.1", 65 | "is-primitive": "^1.0.0", 66 | "tosource": "^0.1.3", 67 | "xtend": "^4.0.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /replacement/array.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Array mutators 3 | */ 4 | var u = require('../util'); 5 | var types = require('ast-types'); 6 | var n = types.namedTypes; 7 | var r = require('./'); 8 | var clone = require('clone'); 9 | var gen = require('escodegen').generate; 10 | 11 | /** 12 | * List of non-evaling array methods 13 | */ 14 | var mutators = [ 15 | 'concat', 16 | 'includes', 17 | 'indexOf', 18 | 'join', 19 | 'lastIndexOf', 20 | 'pop', 21 | 'push', 22 | 'reverse', 23 | 'shift', 24 | 'slice', 25 | 'splice', 26 | 'toSource', 27 | 'toString', 28 | 'unshift' 29 | ]; 30 | 31 | 32 | //FIXME: generalize builtin method calling 33 | //FIXME: treat unsafe arguments for associative objects 34 | 35 | 36 | /** Test whether safe method is called */ 37 | function testMutatorMethod (node) { 38 | if (n.MemberExpression.check(node.callee) && 39 | ( 40 | n.ArrayExpression.check(u.getMemberExpressionSource(node.callee)) || 41 | u.isString(u.getMemberExpressionSource(node.callee)) 42 | ) && 43 | r.test(u.getMemberExpressionSource(node.callee)) 44 | ) { 45 | var callName = u.getCallName(node); 46 | //method, accepting simple arguments 47 | var isSafe = mutators.indexOf(callName) >= 0; 48 | if (isSafe && 49 | ( 50 | (callName in Array.prototype) || 51 | (callName in String.prototype) 52 | ) && 53 | u.getCallArguments(node).every(function (node) { 54 | //harmless methods (non-callable) may accept any functions 55 | //as far they’re evaled unchanged 56 | if (n.FunctionExpression.check(node)) return true; 57 | 58 | //it may mash up newexpressions as well 59 | if (n.NewExpression.check(node)) return true; 60 | 61 | //else - check that all arguments are known 62 | return r.test(node); 63 | }) 64 | ) { 65 | return true; 66 | } 67 | } 68 | return false; 69 | } 70 | 71 | 72 | module.exports = [ 73 | //associative methods, ~ `[].concat(a, b) === [].concat(a).concat(b)` 74 | { 75 | match: 'CallExpression', 76 | 77 | test: function (node) { 78 | var callName = u.getCallName(node); 79 | 80 | if (!n.MemberExpression.check(node.callee)) return false; 81 | 82 | var source = u.getMemberExpressionSource(node.callee); 83 | 84 | if (!n.ArrayExpression.check(source)) return false; 85 | 86 | //test each array element to be whether new or evaluable 87 | if (!source.elements.every(function (node) { 88 | return u.isTransferable(node) || r.test(node); 89 | })) return false; 90 | 91 | var callName = u.getCallName(node); 92 | 93 | if ([ 94 | 'concat' 95 | ].indexOf(callName) < 0) return false; 96 | 97 | var callArgs = u.getCallArguments(node); 98 | 99 | if (!callArgs.every(function (node) { 100 | return u.isTransferable(node) || r.test(node); 101 | })) return false; 102 | 103 | return true; 104 | }, 105 | 106 | eval: function (node) { 107 | var callName = u.getCallName(node); 108 | var args = u.getCallArguments(node); 109 | var expSource = u.getMemberExpressionSource(node.callee); 110 | 111 | var resultArray = expSource; 112 | 113 | //for each expression from the arguments do single operation 114 | args.forEach(function (arg) { 115 | var evalResult = arg; 116 | 117 | //if transferable - just bring as is 118 | if (u.isTransferable(arg)) { 119 | resultArray.elements.push(evalResult); 120 | } 121 | //else eval 122 | else { 123 | //do [].method(singleArg); 124 | var cloned = clone(node); 125 | var clonedArgs = u.getCallArguments(cloned); 126 | clonedArgs.length = 0; 127 | clonedArgs.push(arg); 128 | var clonedArr = u.getMemberExpressionSource(cloned.callee); 129 | clonedArr.elements.length = 0; 130 | 131 | evalResult = u.evalNode(cloned); 132 | 133 | resultArray.elements = resultArray.elements.concat(evalResult.elements); 134 | } 135 | 136 | }); 137 | 138 | return resultArray; 139 | } 140 | }, 141 | 142 | //mapping methods 143 | { 144 | match: 'CallExpression', 145 | 146 | test: function (node) { 147 | if (n.MemberExpression.check(node.callee) && 148 | ( 149 | n.ArrayExpression.check(u.getMemberExpressionSource(node.callee)) || 150 | u.isString(u.getMemberExpressionSource(node.callee)) 151 | ) && 152 | r.test(u.getMemberExpressionSource(node.callee)) 153 | ) { 154 | var callName = u.getCallName(node); 155 | 156 | if ( 157 | ( 158 | (callName in Array.prototype) || 159 | (callName in String.prototype) 160 | ) && 161 | u.getCallArguments(node).every(r.test) 162 | ) { 163 | return true; 164 | } 165 | } 166 | }, 167 | 168 | eval: u.evalNode 169 | }, 170 | 171 | //default safe methods 172 | { 173 | match: 'CallExpression', 174 | 175 | test: testMutatorMethod, 176 | 177 | eval: u.evalNode 178 | }, 179 | 180 | ]; -------------------------------------------------------------------------------- /replacement/expression.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple evaluable expressions 3 | */ 4 | 5 | var u = require('../util'); 6 | var r = require('./'); 7 | 8 | module.exports = [ 9 | { 10 | match: 'ArrayExpression', 11 | test: function (node) { 12 | return node.elements.every(r.test); 13 | }, 14 | eval: u.evalNode 15 | }, 16 | 17 | { 18 | match: 'UnaryExpression', 19 | test: function (node) { 20 | return r.test(node.argument); 21 | }, 22 | eval: u.evalNode 23 | }, 24 | 25 | { 26 | match: 'LogicalExpression', 27 | test: function (node) { 28 | return (u.isObject(node.left) || r.test(node.left)) && 29 | (u.isObject(node.right) || r.test(node.left)); 30 | }, 31 | eval: u.evalNode 32 | }, 33 | 34 | //calls .valueOf or .toString on objects 35 | { 36 | match: 'BinaryExpression', 37 | test: function (node) { 38 | return r.test(node.left) && r.test(node.right); 39 | }, 40 | eval: u.evalNode 41 | }, 42 | 43 | { 44 | match: 'ConditionalExpression', 45 | test: function (node) { 46 | return r.test(node.test) && r.test(node.alternate) && r.test(node.consequent); 47 | }, 48 | eval: u.evalNode 49 | }, 50 | 51 | { 52 | match: 'SequenceExpression', 53 | test: function (node) { 54 | return node.expressions.every(test); 55 | }, 56 | eval: u.evalNode 57 | }, 58 | 59 | { 60 | match: 'ObjectExpression', 61 | test: function (node) { 62 | return node.properties.every(function(prop){ 63 | return r.test(prop.value); 64 | }); 65 | }, 66 | eval: u.evalNode 67 | }, 68 | 69 | { 70 | match: 'UpdateExpression', 71 | test: function (node) { 72 | return r.test(node.argument); 73 | }, 74 | eval: u.evalNode 75 | }, 76 | 77 | { 78 | match: 'FunctionExpression', 79 | test: function (node) { 80 | return u.isIsolated(node); 81 | } 82 | }, 83 | 84 | //default safe fallback for call expressions 85 | { 86 | match: 'CallExpression', 87 | test: function (node) { 88 | //check that both callee is callable and all arguments are ok 89 | return r.test(node.callee) && node.arguments.every(r.test); 90 | }, 91 | 92 | eval: u.evalNode 93 | } 94 | 95 | 96 | 97 | //FIXME: try to adopt `new Date` etc, to work with concat 98 | // if (n.NewExpression.check(node)) { 99 | // return r.test(node.callee) || n.Identifier.check(node.callee) && node.arguments.every(test); 100 | // } 101 | ]; -------------------------------------------------------------------------------- /replacement/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * List of replacement rules 3 | * 4 | * @module ast-eval/replacement/index 5 | */ 6 | 7 | 8 | var replacements = []; 9 | 10 | 11 | /** 12 | * Run every replacement, test whether at least one is passed. 13 | * 14 | * @param {Node} node AST Node to test 15 | * 16 | * @return {Boolean} Test result 17 | */ 18 | function test (node) { 19 | if (node === null) return true; 20 | 21 | var matchedReplacements = getMatchedRules (node); 22 | 23 | //if no match registered - return false 24 | if (!matchedReplacements.length) return false; 25 | 26 | for (var i = 0, l = matchedReplacements.length; i < l; i++) { 27 | if (matchedReplacements[i].test === true || matchedReplacements[i].test(node)) return true; 28 | } 29 | 30 | return false; 31 | } 32 | 33 | 34 | /** 35 | * Match node, find proper evaluation for it. 36 | */ 37 | function evalNode (node) { 38 | var matchedReplacements = getMatchedRules (node); 39 | if (!matchedReplacements.length) return node; 40 | 41 | for (var i = 0, l = matchedReplacements.length, replacement; i < l; i++) { 42 | var replacement = matchedReplacements[i]; 43 | 44 | //FIXME: print matched expression 45 | if (replacement.test === true || replacement.test(node)) { 46 | //use specific eval or default eval 47 | return replacement.eval ? replacement.eval(node) : node; 48 | } 49 | } 50 | } 51 | 52 | 53 | /** Get list of matched rules for a node */ 54 | function getMatchedRules (node) { 55 | var matchedReplacements = replacements.filter(function (replacement) { 56 | return replacement.match === node.type; 57 | }); 58 | 59 | return matchedReplacements; 60 | } 61 | 62 | 63 | replacements.eval = evalNode; 64 | replacements.test = test; 65 | 66 | 67 | module.exports = replacements; -------------------------------------------------------------------------------- /replacement/math.js: -------------------------------------------------------------------------------- 1 | var n = require('ast-types').namedTypes; 2 | var isEvaluable = require('./').test; 3 | var u = require('../util'); 4 | var isPrimitive = require('is-primitive'); 5 | var r = require('./'); 6 | 7 | module.exports = [ 8 | //Math.const 9 | { 10 | match: 'MemberExpression', 11 | 12 | test: function (node) { 13 | var source = u.getMemberExpressionSource(node); 14 | 15 | if (source.name !== 'Math') return false; 16 | 17 | var propName = u.getPropertyName(node); 18 | 19 | return propName !== undefined && propName in Math && isPrimitive(Math[propName]); 20 | }, 21 | 22 | eval: u.evalNode 23 | }, 24 | 25 | //Math.fun() 26 | { 27 | match: 'CallExpression', 28 | test: function (node) { 29 | if (n.MemberExpression.check(node.callee)) { 30 | var source = u.getMemberExpressionSource(node.callee); 31 | 32 | if (source.name !== 'Math') return false; 33 | } 34 | 35 | var propName = u.getPropertyName(node.callee); 36 | 37 | return propName in Math && node.arguments.every(r.test); 38 | }, 39 | 40 | eval: u.evalNode 41 | } 42 | ]; -------------------------------------------------------------------------------- /replacement/memberAccess.js: -------------------------------------------------------------------------------- 1 | var u = require('../util'); 2 | var n = require('ast-types').namedTypes; 3 | var test = require('./').test; 4 | 5 | module.exports = { 6 | match: 'MemberExpression', 7 | 8 | test: function (node) { 9 | //`{a:1}.a`, but not `[].x` 10 | if (test(node.object)) { 11 | //doesn’t call object method 12 | if ( 13 | n.ObjectExpression.check(node.object) && !(node.property.name in Object.prototype) 14 | ) return true; 15 | 16 | //doesn’t call array method 17 | if ( 18 | n.ArrayExpression.check(node.object) && !(node.property.name in Array.prototype) 19 | ) return true; 20 | 21 | //doesn’t call string method 22 | if ( 23 | u.isString(node.object) && !(node.property.name in String.prototype) 24 | ) return true; 25 | 26 | //doesn’t call number method 27 | if ( 28 | u.isNumber(node.object) && !(node.property.name in Number.prototype) 29 | ) return true; 30 | 31 | //doesn’t call array method 32 | if ( 33 | n.ObjectExpression.check(node.object) && !(node.property.name in Array.prototype) 34 | ) return true; 35 | 36 | //doesn’t call function method 37 | if ( 38 | n.FunctionExpression.check(node.object) && !(node.property.name in Function.prototype) 39 | ) return true; 40 | } 41 | }, 42 | 43 | eval: u.evalNode 44 | }; -------------------------------------------------------------------------------- /replacement/primitives.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Literals etc 3 | */ 4 | module.exports = { 5 | //literal's just passed as is 6 | match: 'Literal', 7 | test: true 8 | }; -------------------------------------------------------------------------------- /replacement/variable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Variables evaluation 3 | */ 4 | 5 | module.exports = { 6 | match: 'Identifier', 7 | test: function (node) { 8 | //known (global) identifiers 9 | // return node.name in global; 10 | 11 | //if statically calculable variable 12 | // return isCalculableVar(node); 13 | return false; 14 | } 15 | }; -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests are borrowed from 3 | * https://github.com/substack/static-eval/blob/master/test/eval.js 4 | */ 5 | var assert = require('chai').assert; 6 | var parse = require('esprima').parse; 7 | var gen = require('escodegen').generate; 8 | var astEval = require('../'); 9 | var u = require('../util'); 10 | 11 | 12 | // var src = '[1, 2, 3+4*10+n, 3+4*10+(n||6), beep.boop(3+5), obj[""+"x"].y, 1===2+3-16/4, [2]==2, [2]!==2, [2]!==[2]]'; 13 | 14 | describe('Expressions', function(){ 15 | it('boolean', function(){ 16 | var src = '[1 && true, 1===2+3-16/4, [2]==2, [2]!==2, [2]!==[2]]'; 17 | var ast = parse(src); 18 | 19 | ast = astEval(ast); 20 | var out = gen(ast, {format: {indent: {style: ''}, newline: ''}}); 21 | 22 | assert.deepEqual(out, '[true,true,true,true,true];'); 23 | }); 24 | 25 | it.skip('unresolved', function(){ 26 | var src = '[1,2,3+4*10*z+n,foo(3+5),obj[""+"x"].y]'; 27 | var ast = parse(src); 28 | 29 | ast = astEval(ast); 30 | var out = gen(ast); 31 | console.log(out) 32 | 33 | // assert.deepEqual(eval(out), [true, true, true, true]); 34 | }); 35 | 36 | it('resolved', function(){ 37 | var src = '[1,2=="2",3.1+4*10+(2.14||6),""+"x", 2 > 4, [] + []]'; 38 | var ast = parse(src); 39 | 40 | ast = astEval(ast); 41 | var out = gen(ast, {format: {indent: {style: ''}, newline: ''}}); 42 | 43 | assert.deepEqual(out, "[1,true,45.24,'x',false,''];"); 44 | }); 45 | 46 | it('proper order', function(){ 47 | var src1 = '1 + 2 * 3'; 48 | var ast1 = parse(src1); 49 | ast1 = astEval(ast1); 50 | var out1 = gen(ast1, {format: {indent: {style: ''}, newline: ''}}); 51 | 52 | var src2 = '2 * 3 + 1'; 53 | var ast2 = parse(src2); 54 | ast2 = astEval(ast2); 55 | var out2 = gen(ast1, {format: {indent: {style: ''}, newline: ''}}); 56 | 57 | assert.equal(out1, out2); 58 | }); 59 | 60 | 61 | it('unary operator', function(){ 62 | var src = '-1 + 2'; 63 | var ast = parse(src); 64 | ast = astEval(ast); 65 | var out = gen(ast, {format: {indent: {style: ''}, newline: ''}}); 66 | 67 | assert.equal(out, '1;'); 68 | }); 69 | 70 | it('Math primitives', function(){ 71 | var src = '- Math.PI + Math.PI'; 72 | var ast = parse(src); 73 | ast = astEval(ast); 74 | var out = gen(ast, {format: {indent: {style: ''}, newline: ''}}); 75 | 76 | assert.equal(out, '0;'); 77 | }); 78 | 79 | it.skip('Ignore math non-primitives', function(){ 80 | 'Math.sin + 1'; 81 | }); 82 | 83 | 84 | it.skip('property getter', function(){ 85 | var src = '[1,2=="2",3.1+4*10+(2.14||6),""+"x"]'; 86 | var ast = parse(src); 87 | 88 | ast = astEval(ast); 89 | var out = gen(ast); 90 | 91 | assert.deepEqual(eval(out), [1, true, 45.24, 'x']); 92 | }); 93 | 94 | it('maths', function(){ 95 | var src = 'Math.sin(Math.PI / 2)'; 96 | }); 97 | 98 | it('dates'); 99 | 100 | it('decalc props'); 101 | 102 | 103 | it.skip('scoped variables', function(){ 104 | '[1,2,3+4*10+(n||6),foo(3+5),obj[""+"x"].y]'; 105 | '(function(){var x = 2; return x;})() + 1'; 106 | }); 107 | }); 108 | 109 | 110 | 111 | describe('Array', function(){ 112 | it('map', function(){ 113 | var src = '[1, 2, 3].map(function(n) {return n * 2 })'; 114 | var ast = parse(src); 115 | 116 | ast = astEval(ast); 117 | var out = gen(ast, {format: {indent: {style: ''}, newline: ''}}); 118 | 119 | assert.deepEqual(out, "[2,4,6];"); 120 | }); 121 | 122 | it('methods access', function () { 123 | var src = '[1,2,,3].concat;'; 124 | var ast = parse(src); 125 | 126 | ast = astEval(ast); 127 | var out = gen(ast, {format: {indent: {style: ''}, newline: ''}}); 128 | 129 | assert.deepEqual(out, src); 130 | }); 131 | 132 | it('concat', function(){ 133 | var src = '[1, 2,, 3].concat(4, [5], {}, {a: 2}, function(){}, function(){x + 1})'; 134 | var ast = parse(src); 135 | 136 | ast = astEval(ast); 137 | var out = gen(ast, {format: {indent: {style: ''}, newline: ''}}); 138 | 139 | assert.deepEqual(out, "[1,2,,3,4,5,{},{ a: 2 },function () {},function () {x + 1;}];"); 140 | }); 141 | 142 | it('concat special objects', function(){ 143 | var src = '[new A,1, 2,, 3].concat(4, [5], {}, {a: 2}, new Date, function(){}, function(){x + 1})'; 144 | var ast = parse(src); 145 | 146 | ast = astEval(ast); 147 | var out = gen(ast, {format: {indent: {style: ''}, newline: ''}}); 148 | 149 | assert.deepEqual(out, "[new A(),1,2,,3,4,5,{},{ a: 2 },new Date(),function () {},function () {x + 1;}];"); 150 | }); 151 | 152 | it.skip('unresolvable transforms', function(){ 153 | '[1,2,x].map(function(n){return n;}'; 154 | '[1,2,3].map(function(n){window; return n;}'; 155 | }); 156 | 157 | it('mutators', function(){ 158 | var src = '[1, 2, 3].concat(4, [5])'; 159 | var ast = parse(src); 160 | 161 | ast = astEval(ast); 162 | var out = gen(ast, {format: {indent: {style: ''}, newline: ''}}); 163 | 164 | assert.deepEqual(out, "[1,2,3,4,5];"); 165 | }); 166 | 167 | it.skip('unresolvable mutators', function(){ 168 | '[1, 2, x].concat(4, [5])' 169 | '[1, 2, 3].concat(4, [x])' 170 | }); 171 | 172 | it('join', function(){ 173 | var src = '["a", "b", "c"].join(" ")'; 174 | var ast = parse(src); 175 | 176 | ast = astEval(ast); 177 | var out = gen(ast, {format: {indent: {style: ''}, newline: ''}}); 178 | 179 | assert.deepEqual(out, "'a b c';"); 180 | }); 181 | 182 | }); 183 | 184 | 185 | describe('Decompute', function(){ 186 | it('decompute', function(){ 187 | var src = 'a["x"] = 1;'; 188 | var ast = parse(src); 189 | 190 | ast = u.decompute(ast); 191 | var out = gen(ast, {format: {indent: {style: ''}, newline: ''}}); 192 | 193 | assert.deepEqual(out, "a.x = 1;"); 194 | }); 195 | }); 196 | 197 | 198 | describe('Math', function(){ 199 | it('functions', function(){ 200 | var src = 'Math.sin(Math.PI / 2)'; 201 | var ast = parse(src); 202 | 203 | ast = astEval(ast); 204 | var out = gen(ast, {format: {indent: {style: ''}, newline: ''}}); 205 | 206 | assert.deepEqual(out, "1;"); 207 | }); 208 | }); 209 | 210 | 211 | describe('Other', function(){ 212 | it.skip('global variables', function(){ 213 | //TODO 214 | }); 215 | }); 216 | 217 | 218 | describe('String', function(){ 219 | it('prototype methods', function(){ 220 | var src = '"a_b_c".split("_")'; 221 | var ast = parse(src); 222 | 223 | ast = astEval(ast); 224 | var out = gen(ast, {format: {indent: {style: ''}, newline: ''}}); 225 | 226 | assert.deepEqual(out, "['a','b','c'];"); 227 | }); 228 | 229 | it.skip('refuse methods', function(){ 230 | '"a".badMethod' 231 | }); 232 | 233 | it('call/apply method', function(){ 234 | var src = '"".split.call("a_b_c", "_")'; 235 | var ast = parse(src); 236 | 237 | ast = astEval(ast); 238 | var out = gen(ast, {format: {indent: {style: ''}, newline: ''}}); 239 | 240 | assert.deepEqual(out, "['a','b','c'];"); 241 | }); 242 | 243 | it('static methods'); 244 | 245 | it.skip('prototype methods', function(){ 246 | var src = 'String.prototype.split.call("a_b_c", "_")'; 247 | var ast = parse(src); 248 | 249 | ast = astEval(ast); 250 | var out = gen(ast, {format: {indent: {style: ''}, newline: ''}}); 251 | 252 | assert.deepEqual(out, "['a','b','c'];"); 253 | }); 254 | }); 255 | 256 | describe('Loops', function(){ 257 | 258 | }); 259 | 260 | 261 | describe.skip('Variables', function(){ 262 | it('same scope call', function(){ 263 | var src = 'var a = 1; var b = a + 1; var c = a > 1 ? 0 : a + b + 1;'; 264 | var ast = parse(src); 265 | 266 | ast = astEval(ast); 267 | var out = gen(ast, {format: {indent: {style: ''}, newline: ''}}); 268 | 269 | assert.deepEqual(out, "var a = 1; var b = 2; var c = 4;"); 270 | }); 271 | 272 | it('proper scopes', function(){ 273 | 'var x = 1; (function(){x++});'; 274 | }); 275 | 276 | it.skip('ignore substitution', function(){ 277 | 278 | }); 279 | 280 | it.skip('diff scopes operations', function(){ 281 | 'function x () { var b = a + 1; }; var a = 1;'; 282 | 'function x () { var b = 2;}; var a = 1;'; 283 | }); 284 | }); 285 | 286 | 287 | describe('Functions', function(){ 288 | it.skip('isolated definition & call', function(){ 289 | }); 290 | 291 | it('precalc', function(){ 292 | 'function x (c) { var b = 2; return a + b + c; }; var a = 1; var c = x(3);' 293 | 'function x (c) { var b = 2; return 3 + c; }; var a = 1; var c = 6;' 294 | }); 295 | }); 296 | 297 | 298 | 299 | 300 | assert.equal(); -------------------------------------------------------------------------------- /util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Some helpers 3 | * @module ast-eval/util 4 | */ 5 | 6 | var analyzeScopes = require('escope').analyze; 7 | var q = require('esquery'); 8 | var types = require('ast-types'); 9 | var n = types.namedTypes, b = types.builders; 10 | var evalAst = require('./'); 11 | var parse = require('esprima').parse; 12 | var uneval = require('tosource'); 13 | var gen = require('escodegen').generate; 14 | var a = require('./analyze'); 15 | var r = require('./replacement'); 16 | 17 | 18 | /** 19 | * Get an identifier 20 | * check whether it is variable 21 | * and if it is statically evaluable 22 | */ 23 | function isCalculableVar (node) { 24 | // var nodeData = a.getData(node); 25 | // var scope = nodeData.scope; 26 | 27 | // var reference = nodeData.reference; 28 | // var variable = reference.resolved; 29 | 30 | // console.log('node:', node); 31 | // console.log('var:', variable); 32 | // console.log('reference:', reference); 33 | 34 | // console.log(nodeData) 35 | //how to analyze variables? 36 | //via lifecycle: declaration → [operations] → current use 37 | //detect which other variables this one depends on in operations 38 | //assess whether it is statically calculable to calculate each other variable in operations (they’re independent) 39 | //and if it is, statically calculate 40 | //via `calcVar()` and `isCalculableVar()` 41 | 42 | //go by each reference from declaration up to current one, 43 | 44 | return false; 45 | } 46 | 47 | 48 | /** Test whether literal is a string */ 49 | function isString (node) { 50 | if (n.Literal.check(node) && typeof node.value === 'string') return true; 51 | } 52 | /** Test whether literal is a string */ 53 | function isNumber (node) { 54 | if (n.Literal.check(node) && typeof node.value === 'number') return true; 55 | } 56 | 57 | 58 | /** Test whether node is an object */ 59 | function isObject (node) { 60 | if (n.ArrayExpression.check(node)) return true; 61 | if (n.ObjectExpression.check(node)) return true; 62 | if (n.FunctionExpression.check(node)) return true; 63 | if (n.ThisExpression.check(node)) return true; 64 | } 65 | 66 | 67 | /** Check whether function doesn’t use external variables */ 68 | //FIXME: actually check that function has no assignment expressions to external vars, or doesn’t use them in any way. 69 | function isIsolated (node) { 70 | //refuse non-fn nodes 71 | if (!n.FunctionExpression.check(node)) return; 72 | 73 | //if node refers to external vars - not isolated 74 | var scope = analyzeScopes(node).scopes[0]; 75 | if (scope.through.length) return; 76 | 77 | //also if it includes `ThisExpression` - ignore it, as far `this` isn’t clear 78 | if (q(node, 'ThisExpression').length) return; 79 | 80 | return true; 81 | } 82 | 83 | 84 | /** Test whether node is transferable as is (with no eval) */ 85 | function isTransferable (node) { 86 | if (n.FunctionExpression.check(node)) return true; 87 | if (n.NewExpression.check(node)) return true; 88 | } 89 | 90 | 91 | /** Return name of prop in call expression */ 92 | function getCallName (node) { 93 | if (!n.CallExpression.check(node)) return; 94 | if (!n.MemberExpression.check(node.callee)) return; 95 | 96 | //a.b() 97 | if ( 98 | !n.MemberExpression.check(node.callee.object) && 99 | !node.callee.computed 100 | ) 101 | return node.callee.property.name; 102 | 103 | //a.b.call() 104 | if ( 105 | n.MemberExpression.check(node.callee.object) && 106 | node.callee.property.name === 'call' 107 | ) 108 | return node.callee.object.property.name; 109 | 110 | //a.b.apply() 111 | if ( 112 | n.MemberExpression.check(node.callee.object) && 113 | node.callee.property.name === 'apply' 114 | ) 115 | 116 | return node.callee.object.property.name; 117 | } 118 | 119 | 120 | /** Return arguments of a call */ 121 | function getCallArguments (node) { 122 | if (!n.CallExpression.check(node)) return; 123 | if (!n.MemberExpression.check(node.callee)) return; 124 | 125 | //a.b(1,2,3) 126 | if ( 127 | !n.MemberExpression.check(node.callee.object) && 128 | !node.callee.computed 129 | ) 130 | return node.arguments; 131 | 132 | //a.b.call(ctx, 1,2,3) 133 | if ( 134 | n.MemberExpression.check(node.callee.object) && 135 | node.callee.property.name === 'call' 136 | ) 137 | return node.arguments.slice(1); 138 | 139 | //a.b.apply(ctx, [1,2,3]) 140 | if ( 141 | n.MemberExpression.check(node.callee.object) && 142 | node.callee.property.name === 'apply' && 143 | n.ArrayExpression.check(node.arguments[1]) 144 | ) 145 | 146 | return node.arguments[1].elements; 147 | } 148 | 149 | 150 | /** Get member expression initial node */ 151 | function getMemberExpressionSource (node) { 152 | if (!n.MemberExpression.check(node)) return; 153 | 154 | //go deep 155 | while (n.MemberExpression.check(node)) { 156 | node = node.object; 157 | } 158 | 159 | return node; 160 | } 161 | 162 | 163 | /** Get node’s computed property, if node is computed */ 164 | function getPropertyName (node) { 165 | if (!n.MemberExpression.check(node)) return; 166 | 167 | //Math['P' + 'I'] 168 | if (node.computed) { 169 | if (r.test(node.property)) { 170 | return r.eval(node.property); 171 | } 172 | 173 | else return; 174 | } 175 | 176 | //Math.PI 177 | else { 178 | return node.property.name; 179 | } 180 | } 181 | 182 | 183 | /** Return member expression with decalculated properties, if possible */ 184 | function decompute (node) { 185 | types.visit(node, { 186 | visitMemberExpression: function(path){ 187 | //resolve deep first 188 | this.traverse(path); 189 | var node = path.node; 190 | 191 | if (node.computed && 192 | isString(node.property) 193 | ) { 194 | node.computed = false; 195 | path.get('property').replace(b.identifier(node.property.value)); 196 | } 197 | } 198 | }); 199 | 200 | return node; 201 | } 202 | 203 | 204 | /** 205 | * Get evaluated node (shrunk) 206 | * 207 | * @param {Node} node Simple node 208 | */ 209 | function evalNode (node) { 210 | //wrap object expression: `{}` → `({})` 211 | // if (n.ObjectExpression.check(node)) 212 | node = b.expressionStatement(node); 213 | 214 | //wrap uneval result to expression so to avoid bad uneval, like `{}` → `({})` 215 | return parse('(' + uneval(eval(gen(node))) + ')').body[0].expression; 216 | } 217 | 218 | 219 | 220 | module.exports = { 221 | getMemberExpressionSource: getMemberExpressionSource, 222 | getCallName: getCallName, 223 | getPropertyName: getPropertyName, 224 | getCallArguments: getCallArguments, 225 | isString: isString, 226 | isObject: isObject, 227 | isIsolated: isIsolated, 228 | isTransferable: isTransferable, 229 | isNumber: isNumber, 230 | decompute: decompute, 231 | evalNode: evalNode 232 | }; --------------------------------------------------------------------------------