Skip to content

Commit

Permalink
Rewrite with Babylon
Browse files Browse the repository at this point in the history
  • Loading branch information
TimothyGu authored and ForbesLindesay committed Oct 20, 2016
1 parent 0c8a374 commit d9b4c71
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 69 deletions.
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# constantinople

Determine whether a JavaScript expression evaluates to a constant (using acorn). Here it is assumed to be safe to underestimate how constant something is.
Determine whether a JavaScript expression evaluates to a constant (using Babylon). Here it is assumed to be safe to underestimate how constant something is.

[![Build Status](https://img.shields.io/travis/ForbesLindesay/constantinople/master.svg)](https://travis-ci.org/ForbesLindesay/constantinople)
[![Dependency Status](https://img.shields.io/david/ForbesLindesay/constantinople.svg)](https://david-dm.org/ForbesLindesay/constantinople)
[![Build Status](https://img.shields.io/travis/pugjs/constantinople/master.svg)](https://travis-ci.org/pugjs/constantinople)
[![Dependency Status](https://img.shields.io/david/pugjs/constantinople.svg)](https://david-dm.org/pugjs/constantinople)
[![NPM version](https://img.shields.io/npm/v/constantinople.svg)](https://www.npmjs.org/package/constantinople)

## Installation
Expand All @@ -25,18 +25,22 @@ if (isConstant('Math.floor(10.5)', {Math: Math})) {

## API

### isConstant(src, [constants])
### isConstant(src, [constants, [options]])

Returns `true` if `src` evaluates to a constant, `false` otherwise. It will also return `false` if there is a syntax error, which makes it safe to use on potentially ES6 code.

Constants is an object mapping strings to values, where those values should be treated as constants. Note that this makes it a pretty bad idea to have `Math` in there if the user might make use of `Math.random` and a pretty bad idea to have `Date` in there.

### toConstant(src, [constants])
Options are directly passed-through to [Babylon](https://github.com/babel/babylon#options).

### toConstant(src, [constants, [options]])

Returns the value resulting from evaluating `src`. This method throws an error if the expression is not constant. e.g. `toConstant("Math.random()")` would throw an error.

Constants is an object mapping strings to values, where those values should be treated as constants. Note that this makes it a pretty bad idea to have `Math` in there if the user might make use of `Math.random` and a pretty bad idea to have `Date` in there.

Options are directly passed-through to [Babylon](https://github.com/babel/babylon#options).

## License

MIT
160 changes: 99 additions & 61 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
'use strict'

var acorn = require('acorn');
var walk = require('acorn/dist/walk');
var isExpression = require('is-expression');
var walk = require('babylon-walk');
var getExpression = require('is-expression-babylon').getExpression;
var t = require('babel-types');

var lastSRC = '(null)';
var lastRes = true;
Expand All @@ -12,80 +12,118 @@ var STATEMENT_WHITE_LIST = {
'EmptyStatement': true,
'ExpressionStatement': true,
};
// See require('babel-types').EXPRESSION_TYPES
var EXPRESSION_WHITE_LIST = {
'ParenthesizedExpression': true,
'ArrayExpression': true,
'ObjectExpression': true,
'SequenceExpression': true,
'TemplateLiteral': true,
'UnaryExpression': true,
'BinaryExpression': true,
'LogicalExpression': true,
'ConditionalExpression': true,
'Identifier': true,
'Literal': true,
'ComprehensionExpression': true,
'TaggedTemplateExpression': true,
'MemberExpression': true,
'CallExpression': true,
'NewExpression': true,
ArrayExpression: true,
// AssignmentExpression: false,
BinaryExpression: true,
CallExpression: true,
ConditionalExpression: true,
// FunctionExpression: false,
Identifier: true,
StringLiteral: true,
NumericLiteral: true,
NullLiteral: true,
BooleanLiteral: true,
RegExpLiteral: true,
LogicalExpression: true,
MemberExpression: true,
NewExpression: true,
ObjectExpression: true,
SequenceExpression: true,
// ThisExpression: false,
UnaryExpression: true,
// UpdateExpression: false,
// ArrowFunctionExpression: false,
// ClassExpression: false,
// MetaProperty: false,
// Super: false,
TaggedTemplateExpression: true,
TemplateLiteral: true,
// YieldExpression: false,
TypeCastExpression: true,
JSXElement: true,
JSXEmptyExpression: true,
JSXIdentifier: true,
JSXMemberExpression: true,
ParenthesizedExpression: true,
// AwaitExpression: false,
BindExpression: true,
// DoExpression: false,
};
var visitors = {
Statement: function (node, state) {
if (!state.stop && !STATEMENT_WHITE_LIST[node.type]) {
state.stop = true;
}
},
Expression: function (node, state) {
if (!state.stop && !EXPRESSION_WHITE_LIST[node.type]) {
state.stop = true;
}
},
'MemberExpression|JSXMemberExpression': function (node, state) {
if (state.stop) return;
if (node.computed) return state.stop = true;
else if (node.property.name[0] === '_') return state.stop = true;
},
'Identifier|JSXIdentifier': function (node, state, parents) {
if (state.stop) return;
var lastParent = parents[parents.length - 2];
if (lastParent && !isReferenced(node, lastParent)) return;
if (!(state.constants && node.name in state.constants)) {
state.stop = true;
}
},
};
module.exports = isConstant;
function isConstant(src, constants) {
src = '(' + src + ')';
function isConstant(src, constants, options) {
if (lastSRC === src && lastConstants === constants) return lastRes;
lastSRC = src;
lastConstants = constants;
if (!isExpression(src)) return lastRes = false;
var ast;
try {
ast = acorn.parse(src, {
ecmaVersion: 6,
allowReturnOutsideFunction: true,
allowImportExportEverywhere: true,
allowHashBang: true
});
ast = getExpression(src, options);
} catch (ex) {
return lastRes = false;
}
var isConstant = true;
walk.simple(ast, {
Statement: function (node) {
if (isConstant) {
if (STATEMENT_WHITE_LIST[node.type] !== true) {
isConstant = false;
}
}
},
Expression: function (node) {
if (isConstant) {
if (EXPRESSION_WHITE_LIST[node.type] !== true) {
isConstant = false;
}
}
},
MemberExpression: function (node) {
if (isConstant) {
if (node.computed) isConstant = false;
else if (node.property.name[0] === '_') isConstant = false;
}
},
Identifier: function (node) {
if (isConstant) {
if (!constants || !(node.name in constants)) {
isConstant = false;
}
}
},
});
return lastRes = isConstant;
var state = {
constants: constants,
stop: false
};
walk.ancestor(ast, visitors, state);

return lastRes = !state.stop;
}
isConstant.isConstant = isConstant;

isConstant.toConstant = toConstant;
function toConstant(src, constants) {
if (!isConstant(src, constants)) throw new Error(JSON.stringify(src) + ' is not constant.');
function toConstant(src, constants, options) {
if (!isConstant(src, constants, options)) throw new Error(JSON.stringify(src) + ' is not constant.');
return Function(Object.keys(constants || {}).join(','), 'return (' + src + ')').apply(null, Object.keys(constants || {}).map(function (key) {
return constants[key];
}));
}

function isReferenced(node, parent) {
switch (parent.type) {
// yes: { [NODE]: '' }
// yes: { NODE }
// no: { NODE: '' }
case 'ObjectProperty':
return parent.value === node || parent.computed;

// no: break NODE;
// no: continue NODE;
case 'BreakStatement':
case 'ContinueStatement':
return false;

// yes: left = NODE;
// yes: NODE = right;
case 'AssignmentExpression':
return true;
}

return t.isReferenced(node, parent);
}
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
"tooling"
],
"dependencies": {
"acorn": "^3.1.0",
"is-expression": "^2.0.1"
"babel-types": "^6.16.0",
"babylon-walk": "^1.0.2",
"is-expression-babylon": "^1.1.0"
},
"devDependencies": {
"mocha": "*"
Expand All @@ -23,4 +24,4 @@
},
"author": "ForbesLindesay",
"license": "MIT"
}
}

0 comments on commit d9b4c71

Please sign in to comment.