diff --git a/README.md b/README.md index a6d7c0b..110532b 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/index.js b/index.js index 22bc018..5e0f19c 100644 --- a/index.js +++ b/index.js @@ -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; @@ -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); +} diff --git a/package.json b/package.json index 2bffec9..143c921 100644 --- a/package.json +++ b/package.json @@ -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": "*" @@ -23,4 +24,4 @@ }, "author": "ForbesLindesay", "license": "MIT" -} \ No newline at end of file +}