diff --git a/README.md b/README.md index 7798e74a4..c3181e632 100644 --- a/README.md +++ b/README.md @@ -220,20 +220,6 @@ console.log(escodegen.generate(ast, {comment: true})); [escodegen]: https://github.com/Constellation/escodegen -#### Using Acorn in an environment with a Content Security Policy - -Some contexts, such as Chrome Web Apps, disallow run-time code evaluation. -Acorn uses `new Function` to generate fast functions that test whether -a word is in a given set, and will trigger a security error when used -in a context with such a -[Content Security Policy](http://www.html5rocks.com/en/tutorials/security/content-security-policy/#eval-too) -(see [#90](https://github.com/marijnh/acorn/issues/90) and -[#123](https://github.com/marijnh/acorn/issues/123)). - -The `dist/acorn_csp.js` file in the distribution (which is built -by the `bin/without_eval` script) has the generated code inlined, and -can thus run without evaluating anything. - ### dist/acorn_loose.js ### This file implements an error-tolerant parser. It exposes a single diff --git a/bin/without_eval b/bin/without_eval deleted file mode 100755 index 4331e7070..000000000 --- a/bin/without_eval +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env node - -var fs = require("fs") - -var acornSrc = fs.readFileSync(require.resolve("../dist/acorn"), "utf8") -var acorn = require("../dist/acorn"), walk = require("../dist/walk") - -var ast = acorn.parse(acornSrc) -var touchups = [], uses = [] - -var makePred - -walk.simple(ast, { - FunctionDeclaration: function(node) { - if (node.id.name == "makePredicate") { - makePred = node - touchups.push({text: "// Removed to create an eval-free library", from: node.start, to: node.end}) - } - }, - ObjectExpression: function(node) { - node.properties.forEach(function(prop) { - if (prop.value.type == "CallExpression" && - prop.value.callee.name == "makePredicate") - uses.push(prop.value) - }) - } -}) - -var results = [] -var dryRun = acornSrc.slice(0, makePred.end) + "; makePredicate = (function(mp) {" + - "return function(words) { var r = mp(words); predicates.push(r); return r }})(makePredicate);" + - acornSrc.slice(makePred.end) -;(new Function("predicates", dryRun))(results) - -uses.forEach(function (node, i) { - touchups.push({text: results[i].toString(), from: node.start, to: node.end}) -}) - -var result = "", pos = 0 -touchups.sort(function(a, b) { return a.from - b.from }) -touchups.forEach(function(touchup) { - result += acornSrc.slice(pos, touchup.from) - result += touchup.text - pos = touchup.to -}) -result += acornSrc.slice(pos) - -process.stdout.write(result) diff --git a/dist/acorn_csp.js b/dist/acorn_csp.js new file mode 120000 index 000000000..c44020a9c --- /dev/null +++ b/dist/acorn_csp.js @@ -0,0 +1 @@ +acorn.js \ No newline at end of file diff --git a/package.json b/package.json index 381af3949..0e475179a 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ }, "license": "MIT", "scripts": { - "prepublish": "node bin/build-acorn.js && node bin/without_eval > dist/acorn_csp.js", + "prepublish": "node bin/build-acorn.js", "test": "node test/run.js" }, "bin": { diff --git a/src/expression.js b/src/expression.js index 6b27b2a09..b6f24d804 100644 --- a/src/expression.js +++ b/src/expression.js @@ -18,7 +18,6 @@ import {types as tt} from "./tokentype" import {Parser} from "./state" -import {reservedWords} from "./identifier" import {has} from "./util" const pp = Parser.prototype @@ -510,9 +509,8 @@ pp.parsePropertyValue = function(prop, isPattern, isGenerator, startPos, startLo } else if (this.options.ecmaVersion >= 6 && !prop.computed && prop.key.type === "Identifier") { prop.kind = "init" if (isPattern) { - if (this.isKeyword(prop.key.name) || - (this.strict && (reservedWords.strictBind(prop.key.name) || reservedWords.strict(prop.key.name))) || - (!this.options.allowReserved && this.isReservedWord(prop.key.name))) + if (this.keywords.test(prop.key.name) || + (this.strict ? this.reservedWordsStrictBind : this.reservedWords).test(prop.key.name)) this.raise(prop.key.start, "Binding " + prop.key.name) prop.value = this.parseMaybeDefault(startPos, startLoc, prop.key) } else if (this.type === tt.eq && refShorthandDefaultPos) { @@ -641,10 +639,9 @@ pp.parseIdent = function(liberal) { if (liberal && this.options.allowReserved == "never") liberal = false if (this.type === tt.name) { if (!liberal && - ((!this.options.allowReserved && this.isReservedWord(this.value)) || - (this.strict && reservedWords.strict(this.value)) && - (this.options.ecmaVersion >= 6 || - this.input.slice(this.start, this.end).indexOf("\\") == -1))) + (this.strict ? this.reservedWordsStrict : this.reservedWords).test(this.value) && + (this.options.ecmaVersion >= 6 || + this.input.slice(this.start, this.end).indexOf("\\") == -1)) this.raise(this.start, "The keyword '" + this.value + "' is reserved") node.name = this.value } else if (liberal && this.type.keyword) { diff --git a/src/identifier.js b/src/identifier.js index ad2fe0393..e4926f795 100644 --- a/src/identifier.js +++ b/src/identifier.js @@ -7,53 +7,14 @@ // // It starts by sorting the words by length. -function makePredicate(words) { - words = words.split(" ") - let f = "", cats = [] - out: for (let i = 0; i < words.length; ++i) { - for (let j = 0; j < cats.length; ++j) - if (cats[j][0].length == words[i].length) { - cats[j].push(words[i]) - continue out - } - cats.push([words[i]]) - } - function compareTo(arr) { - if (arr.length == 1) return f += "return str === " + JSON.stringify(arr[0]) + ";" - f += "switch(str){" - for (let i = 0; i < arr.length; ++i) f += "case " + JSON.stringify(arr[i]) + ":" - f += "return true}return false;" - } - - // When there are more than three length categories, an outer - // switch first dispatches on the lengths, to save on comparisons. - - if (cats.length > 3) { - cats.sort((a, b) => b.length - a.length) - f += "switch(str.length){" - for (let i = 0; i < cats.length; ++i) { - let cat = cats[i] - f += "case " + cat[0].length + ":" - compareTo(cat) - } - f += "}" - - // Otherwise, simply generate a flat `switch` statement. - - } else { - compareTo(words) - } - return new Function("str", f) -} - // Reserved word lists for various dialects of the language export const reservedWords = { - 3: makePredicate("abstract boolean byte char class double enum export extends final float goto implements import int interface long native package private protected public short static super synchronized throws transient volatile"), - 5: makePredicate("class enum extends super const export import"), - 6: makePredicate("enum await"), - strict: makePredicate("implements interface let package private protected public static yield"), - strictBind: makePredicate("eval arguments") + 3: "abstract boolean byte char class double enum export extends final float goto implements import int interface long native package private protected public short static super synchronized throws transient volatile", + 5: "class enum extends super const export import", + 6: "enum await", + strict: "implements interface let package private protected public static yield", + strictBind: "eval arguments" } // And the keywords @@ -61,8 +22,8 @@ export const reservedWords = { var ecma5AndLessKeywords = "break case catch continue debugger default do else finally for function if return switch throw try var while with null true false instanceof typeof void delete new in this" export const keywords = { - 5: makePredicate(ecma5AndLessKeywords), - 6: makePredicate(ecma5AndLessKeywords + " let const class extends export import yield super") + 5: ecma5AndLessKeywords, + 6: ecma5AndLessKeywords + " let const class extends export import yield super" } // ## Character categories diff --git a/src/lval.js b/src/lval.js index 4cf79b5bc..80828aef8 100644 --- a/src/lval.js +++ b/src/lval.js @@ -1,6 +1,5 @@ import {types as tt} from "./tokentype" import {Parser} from "./state" -import {reservedWords} from "./identifier" import {has} from "./util" const pp = Parser.prototype @@ -161,7 +160,7 @@ pp.parseMaybeDefault = function(startPos, startLoc, left) { pp.checkLVal = function(expr, isBinding, checkClashes) { switch (expr.type) { case "Identifier": - if (this.strict && (reservedWords.strictBind(expr.name) || reservedWords.strict(expr.name))) + if (this.strict && this.reservedWordsStrictBind.test(expr.name)) // FIXME match shorter regexp? this.raise(expr.start, (isBinding ? "Binding " : "Assigning to ") + expr.name + " in strict mode") if (checkClashes) { if (has(checkClashes, expr.name)) diff --git a/src/state.js b/src/state.js index 542da9e2e..a06cd7d96 100644 --- a/src/state.js +++ b/src/state.js @@ -6,12 +6,20 @@ import {getOptions} from "./options" // Registered plugins export const plugins = {} +function keywordRegexp(words) { + return new RegExp("^(" + words.replace(/ /g, "|") + ")$") +} + export class Parser { constructor(options, input, startPos) { this.options = getOptions(options) this.sourceFile = this.options.sourceFile - this.isKeyword = keywords[this.options.ecmaVersion >= 6 ? 6 : 5] - this.isReservedWord = reservedWords[this.options.ecmaVersion] + this.keywords = keywordRegexp(keywords[this.options.ecmaVersion >= 6 ? 6 : 5]) + let reserved = this.options.allowReserved ? "" : reservedWords[this.options.ecmaVersion] + this.reservedWords = keywordRegexp(reserved) + let reservedStrict = (reserved ? reserved + " " : "") + reservedWords.strict + this.reservedWordsStrict = keywordRegexp(reservedStrict) + this.reservedWordsStrictBind = keywordRegexp(reservedStrict + " " + reservedWords.strictBind) this.input = String(input) // Used to signal to callers of `readWord1` whether the word @@ -71,6 +79,10 @@ export class Parser { this.skipLineComment(2) } + // DEPRECATED Kept for backwards compatibility until 3.0 in case a plugin uses them + isKeyword(word) { return this.keywords.test(word) } + isReservedWord(word) { return this.reservedWords.test(word) } + extend(name, f) { this[name] = f(this[name]) } diff --git a/src/tokenize.js b/src/tokenize.js index 0409f887f..b02c7a7e3 100644 --- a/src/tokenize.js +++ b/src/tokenize.js @@ -676,7 +676,7 @@ pp.readWord1 = function() { pp.readWord = function() { let word = this.readWord1() let type = tt.name - if ((this.options.ecmaVersion >= 6 || !this.containsEsc) && this.isKeyword(word)) + if ((this.options.ecmaVersion >= 6 || !this.containsEsc) && this.keywords.test(word)) type = keywordTypes[word] return this.finishToken(type, word) }