From 53d98f1175d69c8117168bfcb30b9285c414dd8b Mon Sep 17 00:00:00 2001 From: Sebastian McKenzie Date: Wed, 10 Dec 2014 18:59:24 +1100 Subject: [PATCH 01/14] Make semicolon optional in do-while in >=ES6 --- acorn.js | 5 ++++- test/tests-harmony.js | 46 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/acorn.js b/acorn.js index 87956399db..e39c3261e1 100644 --- a/acorn.js +++ b/acorn.js @@ -1605,7 +1605,10 @@ labels.pop(); expect(_while); node.test = parseParenExpression(); - semicolon(); + if (options.ecmaVersion >= 6) + eat(_semi); + else + semicolon(); return finishNode(node, "DoWhileStatement"); } diff --git a/test/tests-harmony.js b/test/tests-harmony.js index 50fe007316..9180e29abd 100644 --- a/test/tests-harmony.js +++ b/test/tests-harmony.js @@ -13605,6 +13605,52 @@ test("/[a-z]/u", { ecmaVersion: 6 }); + +test("do {} while (false) foo();", { + type: "Program", + start: 0, + end: 26, + body: [ + { + type: "DoWhileStatement", + start: 0, + end: 19, + body: { + type: "BlockStatement", + start: 3, + end: 5, + body: [] + }, + test: { + type: "Literal", + start: 13, + end: 18, + value: false, + raw: "false" + } + }, + { + type: "ExpressionStatement", + start: 20, + end: 26, + expression: { + type: "CallExpression", + start: 20, + end: 25, + callee: { + type: "Identifier", + start: 20, + end: 23, + name: "foo" + }, + arguments: [] + } + } + ] +}, { + ecmaVersion: 6 +}); + // Harmony Invalid syntax testFail("0o", "Expected number in radix 8 (1:2)", {ecmaVersion: 6}); From 2cb3dbcb4191a9ec60e4c6ed0fc3d7ee13fd6b5d Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Thu, 11 Dec 2014 14:11:39 +0100 Subject: [PATCH 02/14] Change token structure of template literals Fix various template parsing issues, makes tokenizer useable from outside the parser. Closes #169 Closes #173 --- acorn.js | 123 +++++++++++++++---------------- test/tests-harmony.js | 163 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 200 insertions(+), 86 deletions(-) diff --git a/acorn.js b/acorn.js index e39c3261e1..d32eaf2bac 100644 --- a/acorn.js +++ b/acorn.js @@ -302,11 +302,12 @@ var metParenL; - // This is used by parser for detecting if it's inside ES6 - // Template String. If it is, it should treat '$' as prefix before - // '{expression}' and everything else as string literals. + // This is used by the tokenizer to track the template strings it is + // inside, and count the amount of open braces seen inside them, to + // be able to switch back to a template token when the } to match ${ + // is encountered. It will hold an array of integers. - var inTemplate; + var templates; function initParserState() { lastStart = lastEnd = tokPos; @@ -409,7 +410,7 @@ var _braceR = {type: "}"}, _parenL = {type: "(", beforeExpr: true}, _parenR = {type: ")"}; var _comma = {type: ",", beforeExpr: true}, _semi = {type: ";", beforeExpr: true}; var _colon = {type: ":", beforeExpr: true}, _dot = {type: "."}, _question = {type: "?", beforeExpr: true}; - var _arrow = {type: "=>", beforeExpr: true}, _bquote = {type: "`"}, _dollarBraceL = {type: "${", beforeExpr: true}; + var _arrow = {type: "=>", beforeExpr: true}, _template = {type: "template"}, _templateContinued = {type: "templateContinued"}; var _ellipsis = {type: "...", prefix: true, beforeExpr: true}; // Operators. These carry several kinds of properties to help the @@ -452,8 +453,8 @@ parenL: _parenL, parenR: _parenR, comma: _comma, semi: _semi, colon: _colon, dot: _dot, ellipsis: _ellipsis, question: _question, slash: _slash, eq: _eq, name: _name, eof: _eof, num: _num, regexp: _regexp, string: _string, - arrow: _arrow, bquote: _bquote, dollarBraceL: _dollarBraceL, star: _star, - assign: _assign}; + arrow: _arrow, template: _template, templateContinued: _templateContinued, star: _star, + assign: _assign}; for (var kw in keywordTypes) exports.tokTypes["_" + kw] = keywordTypes[kw]; // This is a trick taken from Esprima. It turns out that, on @@ -548,6 +549,10 @@ var newline = /[\n\r\u2028\u2029]/; + function isNewLine(code) { + return code === 10 || code === 13 || code === 0x2028 || code == 0x2029; + } + // Matches a whole line break (where CRLF is considered a single // line break). Used to count lines. @@ -598,7 +603,7 @@ } tokRegexpAllowed = true; metParenL = 0; - inTemplate = false; + templates = []; } // Called at the end of every token. Sets `tokEnd`, `tokVal`, and @@ -788,25 +793,6 @@ return finishOp(code === 61 ? _eq : _prefix, 1); } - // Get token inside ES6 template (special rules work there). - - function getTemplateToken(code) { - // '`' and '${' have special meanings, but they should follow - // string (can be empty) - if (tokType === _string) { - if (code === 96) { // '`' - ++tokPos; - return finishToken(_bquote); - } else - if (code === 36 && input.charCodeAt(tokPos + 1) === 123) { // '${' - tokPos += 2; - return finishToken(_dollarBraceL); - } - } - // anything else is considered string literal - return readTmplString(); - } - function getTokenFromCode(code) { switch (code) { // The interpretation of a dot depends on whether it is followed @@ -821,15 +807,23 @@ case 44: ++tokPos; return finishToken(_comma); case 91: ++tokPos; return finishToken(_bracketL); case 93: ++tokPos; return finishToken(_bracketR); - case 123: ++tokPos; return finishToken(_braceL); - case 125: ++tokPos; return finishToken(_braceR); + case 123: + ++tokPos; + if (templates.length) ++templates[templates.length - 1]; + return finishToken(_braceL); + case 125: + ++tokPos; + if (templates.length && --templates[templates.length - 1] === 0) + return readTemplateString(_templateContinued); + else + return finishToken(_braceR); case 58: ++tokPos; return finishToken(_colon); case 63: ++tokPos; return finishToken(_question); case 96: // '`' if (options.ecmaVersion >= 6) { ++tokPos; - return finishToken(_bquote, undefined, false); + return readTemplateString(_template); } case 48: // '0' @@ -890,8 +884,6 @@ var code = input.charCodeAt(tokPos); - if (inTemplate) return getTemplateToken(code); - // Identifier or keyword. '\uXXXX' sequences are allowed in // identifiers, so '\' also dispatches to that. if (isIdentifierStart(code) || code === 92 /* '\' */) return readWord(); @@ -1074,28 +1066,34 @@ } } - function readTmplString() { - var out = ""; + function readTemplateString(type) { + if (type == _templateContinued) templates.pop(); + var out = "", start = tokPos;; for (;;) { - if (tokPos >= inputLen) raise(tokStart, "Unterminated string constant"); - var ch = input.charCodeAt(tokPos); - if (ch === 96 || ch === 36 && input.charCodeAt(tokPos + 1) === 123) // '`', '${' - return finishToken(_string, out); - if (ch === 92) { // '\' + if (tokPos >= inputLen) raise(tokStart, "Unterminated template"); + var ch = input.charAt(tokPos); + if (ch === "`" || ch === "$" && input.charCodeAt(tokPos + 1) === 123) { // '`', '${' + var raw = input.slice(start, tokPos); + ++tokPos; + if (ch == "$") { ++tokPos; templates.push(1); } + return finishToken(type, {cooked: out, raw: raw}); + } + + if (ch === "\\") { // '\' out += readEscapedChar(); } else { ++tokPos; - if (newline.test(String.fromCharCode(ch))) { - if (ch === 13 && input.charCodeAt(tokPos) === 10) { + if (newline.test(ch)) { + if (ch === "\r" && input.charCodeAt(tokPos) === 10) { ++tokPos; - ch = 10; + ch = "\n"; } if (options.locations) { ++tokCurLine; tokLineStart = tokPos; } } - out += String.fromCharCode(ch); // '\' + out += ch; } } } @@ -2010,7 +2008,7 @@ node.callee = base; node.arguments = parseExprList(_parenR, false); return parseSubscripts(finishNode(node, "CallExpression"), start, noCalls); - } else if (tokType === _bquote) { + } else if (tokType === _template) { var node = startNodeAt(start); node.tag = base; node.quasi = parseTemplate(); @@ -2125,7 +2123,7 @@ case _new: return parseNew(); - case _bquote: + case _template: return parseTemplate(); default: @@ -2149,33 +2147,24 @@ // Parse template expression. + function parseTemplateElement() { + var elem = startNode(); + elem.value = tokVal; + elem.tail = input.charCodeAt(tokEnd - 1) !== 123; // '{' + next(); + return finishNode(elem, "TemplateElement"); + } + function parseTemplate() { - var oldInTemplate = inTemplate; - inTemplate = true; var node = startNode(); node.expressions = []; - node.quasis = []; - next(); - for (;;) { - var elem = startNode(); - elem.value = {cooked: tokVal, raw: input.slice(tokStart, tokEnd)}; - elem.tail = false; - next(); - node.quasis.push(finishNode(elem, "TemplateElement")); - if (tokType === _bquote) { // '`', end of template - elem.tail = true; - break; - } - inTemplate = false; - expect(_dollarBraceL); + var curElt = parseTemplateElement(); + node.quasis = [curElt]; + while (!curElt.tail) { node.expressions.push(parseExpression()); - inTemplate = true; - // hack to include previously skipped space - tokPos = tokEnd; - expect(_braceR); + if (tokType !== _templateContinued) unexpected(); + node.quasis.push(curElt = parseTemplateElement()); } - inTemplate = oldInTemplate; - next(); return finishNode(node, "TemplateLiteral"); } diff --git a/test/tests-harmony.js b/test/tests-harmony.js index 9180e29abd..834979bcff 100644 --- a/test/tests-harmony.js +++ b/test/tests-harmony.js @@ -628,8 +628,8 @@ test("`42`", { value: {raw: "42", cooked: "42"}, tail: true, loc: { - start: {line: 1, column: 1}, - end: {line: 1, column: 3} + start: {line: 1, column: 0}, + end: {line: 1, column:4} } }], expressions: [], @@ -674,8 +674,8 @@ test("raw`42`", { value: {raw: "42", cooked: "42"}, tail: true, loc: { - start: {line: 1, column: 4}, - end: {line: 1, column: 6} + start: {line: 1, column: 3}, + end: {line: 1, column: 7} } }], expressions: [], @@ -726,8 +726,8 @@ test("raw`hello ${name}`", { value: {raw: "hello ", cooked: "hello "}, tail: false, loc: { - start: {line: 1, column: 4}, - end: {line: 1, column: 10} + start: {line: 1, column: 3}, + end: {line: 1, column: 12} } }, { @@ -735,8 +735,8 @@ test("raw`hello ${name}`", { value: {raw: "", cooked: ""}, tail: true, loc: { - start: {line: 1, column: 17}, - end: {line: 1, column: 17} + start: {line: 1, column: 16}, + end: {line: 1, column: 18} } } ], @@ -784,8 +784,8 @@ test("`$`", { value: {raw: "$", cooked: "$"}, tail: true, loc: { - start: {line: 1, column: 1}, - end: {line: 1, column: 2} + start: {line: 1, column: 0}, + end: {line: 1, column: 3} } }], expressions: [], @@ -820,8 +820,8 @@ test("`\\n\\r\\b\\v\\t\\f\\\n\\\r\n`", { value: {raw: "\\n\\r\\b\\v\\t\\f\\\n\\\r\n", cooked: "\n\r\b\u000b\t\f"}, tail: true, loc: { - start: {line: 1, column: 1}, - end: {line: 3, column: 0} + start: {line: 1, column: 0}, + end: {line: 3, column: 1} } }], expressions: [], @@ -856,8 +856,8 @@ test("`\n\r\n`", { value: {raw: "\n\r\n", cooked: "\n\n"}, tail: true, loc: { - start: {line: 1, column: 1}, - end: {line: 3, column: 0} + start: {line: 1, column: 0}, + end: {line: 3, column: 1} } }], expressions: [], @@ -892,8 +892,8 @@ test("`\\u{000042}\\u0042\\x42u0\\102\\A`", { value: {raw: "\\u{000042}\\u0042\\x42u0\\102\\A", cooked: "BBBu0BA"}, tail: true, loc: { - start: {line: 1, column: 1}, - end: {line: 1, column: 29} + start: {line: 1, column: 0}, + end: {line: 1, column: 30} } }], expressions: [], @@ -940,8 +940,8 @@ test("new raw`42`", { value: {raw: "42", cooked: "42"}, tail: true, loc: { - start: {line: 1, column: 8}, - end: {line: 1, column: 10} + start: {line: 1, column: 7}, + end: {line: 1, column: 11} } }], expressions: [], @@ -976,6 +976,131 @@ test("new raw`42`", { locations: true }); +test("`outer${{x: {y: 10}}}bar${`nested${function(){return 1;}}endnest`}end`",{ + type: "Program", + body: [ + { + type: "ExpressionStatement", + expression: { + type: "TemplateLiteral", + expressions: [ + { + type: "ObjectExpression", + properties: [ + { + type: "Property", + method: false, + shorthand: false, + computed: false, + key: { + type: "Identifier", + name: "x" + }, + value: { + type: "ObjectExpression", + properties: [ + { + type: "Property", + method: false, + shorthand: false, + computed: false, + key: { + type: "Identifier", + name: "y" + }, + value: { + type: "Literal", + value: 10, + raw: "10" + }, + kind: "init" + } + ] + }, + kind: "init" + } + ] + }, + { + type: "TemplateLiteral", + expressions: [ + { + type: "FunctionExpression", + id: null, + params: [], + defaults: [], + rest: null, + generator: false, + body: { + type: "BlockStatement", + body: [ + { + type: "ReturnStatement", + argument: { + type: "Literal", + value: 1, + raw: "1" + } + } + ] + }, + expression: false + } + ], + quasis: [ + { + type: "TemplateElement", + value: { + cooked: "nested", + raw: "nested" + }, + tail: false + }, + { + type: "TemplateElement", + value: { + cooked: "endnest", + raw: "endnest" + }, + tail: true + } + ] + } + ], + quasis: [ + { + type: "TemplateElement", + value: { + cooked: "outer", + raw: "outer" + }, + tail: false + }, + { + type: "TemplateElement", + value: { + cooked: "bar", + raw: "bar" + }, + tail: false + }, + { + type: "TemplateElement", + value: { + cooked: "end", + raw: "end" + }, + tail: true + } + ] + } + } + ] +}, { + ecmaVersion: 6 +}); + + // ES6: Switch Case Declaration test("switch (answer) { case 42: let t = 42; break; }", { @@ -13959,7 +14084,7 @@ testFail("class A extends yield B { }", "Unexpected token (1:22)", {ecmaVersion: testFail("class default", "Unexpected token (1:6)", {ecmaVersion: 6}); -testFail("`test", "Unterminated string constant (1:1)", {ecmaVersion: 6}); +testFail("`test", "Unterminated template (1:0)", {ecmaVersion: 6}); testFail("switch `test`", "Unexpected token (1:7)", {ecmaVersion: 6}); From 91e5ac0fddca6b8ca646a578e44ce16804af6019 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Thu, 11 Dec 2014 14:30:24 +0100 Subject: [PATCH 03/14] Make loose parser parse template strings --- acorn_loose.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ test/run.js | 1 - 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/acorn_loose.js b/acorn_loose.js index 649aec6241..fe11e5b114 100644 --- a/acorn_loose.js +++ b/acorn_loose.js @@ -100,6 +100,10 @@ var re = input.slice(e.pos, pos); try { re = new RegExp(re); } catch(e) {} replace = {start: e.pos, end: pos, type: tt.regexp, value: re}; + } else if (/template/.test(msg)) { + replace = {start: e.pos, end: pos, + type: input.charAt(e.pos) == "`" ? tt.template : tt.templateContinued, + value: input.slice(e.pos + 1, pos)}; } else { replace = false; } @@ -681,6 +685,11 @@ node.callee = base; node.arguments = parseExprList(tt.parenR); base = finishNode(node, "CallExpression"); + } else if (token.type == tt.template) { + var node = startNodeAt(start); + node.tag = base; + node.quasi = parseTemplate(); + base = finishNode(node, "TaggedTemplateExpression"); } else { return base; } @@ -769,6 +778,9 @@ } return finishNode(node, "YieldExpression"); + case tt.template: + return parseTemplate(); + default: return dummyIdent(); } @@ -788,6 +800,38 @@ return finishNode(node, "NewExpression"); } + function parseTemplateElement() { + var elem = startNode(); + elem.value = token.value; + elem.tail = input.charCodeAt(token.end - 1) !== 123; // '{' + next(); + return finishNode(elem, "TemplateElement"); + } + + function parseTemplate() { + var node = startNode(); + node.expressions = []; + var curElt = parseTemplateElement(); + node.quasis = [curElt]; + while (!curElt.tail) { + var next = parseExpression(); + if (isDummy(next)) { + node.quasis[node.quasis.length - 1].tail = true; + break; + } + node.expressions.push(next); + if (token.type === tt.templateContinued) { + node.quasis.push(curElt = parseTemplateElement()); + } else { + curElt = startNode(); + curElt.value = {cooked: "", raw: ""}; + curElt.tail = true; + node.quasis.push(curElt); + } + } + return finishNode(node, "TemplateLiteral"); + } + function parseObj(isClass, isStatement) { var node = startNode(); if (isClass) { diff --git a/test/run.js b/test/run.js index e896397100..04f752dc3b 100644 --- a/test/run.js +++ b/test/run.js @@ -55,7 +55,6 @@ parse: (typeof require === "undefined" ? window.acorn : require("../acorn_loose")).parse_dammit, loose: true, filter: function (test) { - if (/`/.test(test.code)) return false; // FIXME remove this when the loose parse supports template strings var opts = test.options || {}; if (opts.loose === false) return false; return (opts.ecmaVersion || 5) <= 6; From c989857aa5ec4713ff6bde4706105dc2cd2b2f40 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Thu, 11 Dec 2014 14:48:03 +0100 Subject: [PATCH 04/14] Add allowImportExportEverywhere option Closes #174 --- README.md | 4 ++++ acorn.js | 14 ++++++++++---- test/tests-harmony.js | 2 ++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 44d75747ea..58627b0275 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,10 @@ object referring to that same position. the top level raises an error. Set this to `true` to accept such code. +- **allowImportExportEverywhere**: By default, `import` and `export` + declarations can only appear at a program's top level. Setting this + option to `true` allows them anywhere where a statement is allowed. + - **locations**: When `true`, each node has a `loc` object attached with `start` and `end` subobjects, each of which contains the one-based line and zero-based column numbers in `{line, column}` diff --git a/acorn.js b/acorn.js index d32eaf2bac..22b32f1577 100644 --- a/acorn.js +++ b/acorn.js @@ -71,6 +71,9 @@ // When enabled, a return at the top level is not considered an // error. allowReturnOutsideFunction: false, + // When enabled, import/export statements are not constrained to + // appearing at the top of the program. + allowImportExportEverywhere: false, // When `locations` is on, `loc` properties holding objects with // `start` and `end` properties in `{line, column}` form (with // line being 1-based and column 0-based) will be attached to the @@ -1503,7 +1506,7 @@ var first = true; if (!node.body) node.body = []; while (tokType !== _eof) { - var stmt = parseStatement(); + var stmt = parseStatement(true); node.body.push(stmt); if (first && isUseStrict(stmt)) setStrict(true); first = false; @@ -1524,7 +1527,7 @@ // `if (foo) /blah/.exec(foo);`, where looking at the previous token // does not help. - function parseStatement() { + function parseStatement(topLevel) { if (tokType === _slash || tokType === _assign && tokVal == "/=") readToken(true); @@ -1551,8 +1554,11 @@ case _with: return parseWithStatement(node); case _braceL: return parseBlock(); // no point creating a function for this case _semi: return parseEmptyStatement(node); - case _export: return parseExport(node); - case _import: return parseImport(node); + case _export: + case _import: + if (!topLevel && !options.allowImportExportEverywhere) + raise(tokStart, "'import' and 'export' may only appear at the top level"); + return starttype === _import ? parseImport(node) : parseExport(node); // If the statement does not start with a statement keyword or a // brace, it's an ExpressionStatement or LabeledStatement. We diff --git a/test/tests-harmony.js b/test/tests-harmony.js index 834979bcff..9723073690 100644 --- a/test/tests-harmony.js +++ b/test/tests-harmony.js @@ -14138,6 +14138,8 @@ testFail("({ t(eval) { \"use strict\"; } });", "Defining 'eval' in strict mode ( testFail("\"use strict\"; `${test}\\02`;", "Octal literal in strict mode (1:22)", {ecmaVersion: 6}); +testFail("if (1) import \"acorn\";", "'import' and 'export' may only appear at the top level (1:7)", {ecmaVersion: 6}); + test("[...a, ] = b", { type: "Program", loc: { From 0a812b6020e89bc375dea94f144f68851dc8813f Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Thu, 11 Dec 2014 14:51:45 +0100 Subject: [PATCH 05/14] Mark release 0.10.0 --- AUTHORS | 3 +++ acorn.js | 2 +- package.json | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index a9e6016d75..799a063354 100644 --- a/AUTHORS +++ b/AUTHORS @@ -21,9 +21,12 @@ Mathias Bynens Mathieu 'p01' Henri Max Schaefer Mihai Bazon +Mike Rennie Oskar Schöldström Paul Harper Peter Rust PlNG r-e-d +Rich Harris +Sebastian McKenzie zsjforcn diff --git a/acorn.js b/acorn.js index 22b32f1577..ddc5abb3df 100644 --- a/acorn.js +++ b/acorn.js @@ -28,7 +28,7 @@ })(this, function(exports) { "use strict"; - exports.version = "0.9.1"; + exports.version = "0.10.0"; // The main exported interface (under `self.acorn` when in the // browser) is a `parse` function that takes a code string and diff --git a/package.json b/package.json index 77c0d1915b..179c1ccfff 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "ECMAScript parser", "homepage": "http://marijnhaverbeke.nl/acorn/", "main": "acorn.js", - "version": "0.9.1", + "version": "0.10.0", "engines": {"node": ">=0.4.0"}, "maintainers": [{"name": "Marijn Haverbeke", "email": "marijnh@gmail.com", From c671bcfaf7aa3f97222e668e168d92b1bf4cfb2f Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Thu, 11 Dec 2014 14:52:32 +0100 Subject: [PATCH 06/14] Bump version number post-0.10 --- acorn.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/acorn.js b/acorn.js index ddc5abb3df..2a21b4cd26 100644 --- a/acorn.js +++ b/acorn.js @@ -28,7 +28,7 @@ })(this, function(exports) { "use strict"; - exports.version = "0.10.0"; + exports.version = "0.10.1"; // The main exported interface (under `self.acorn` when in the // browser) is a `parse` function that takes a code string and diff --git a/package.json b/package.json index 179c1ccfff..87c3d6f456 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "ECMAScript parser", "homepage": "http://marijnhaverbeke.nl/acorn/", "main": "acorn.js", - "version": "0.10.0", + "version": "0.10.1", "engines": {"node": ">=0.4.0"}, "maintainers": [{"name": "Marijn Haverbeke", "email": "marijnh@gmail.com", From 6915519498661a1924da832f7186b216760c834b Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Thu, 11 Dec 2014 17:44:45 +0100 Subject: [PATCH 07/14] Give TemplateElements a narrower range Issue #169 --- acorn.js | 44 ++++++++++++++++++++++++++++++------------- acorn_loose.js | 14 ++++++++++++-- test/tests-harmony.js | 36 +++++++++++++++++------------------ 3 files changed, 61 insertions(+), 33 deletions(-) diff --git a/acorn.js b/acorn.js index 2a21b4cd26..d7a73623b9 100644 --- a/acorn.js +++ b/acorn.js @@ -43,7 +43,7 @@ input = String(inpt); inputLen = input.length; setOptions(opts); initTokenState(); - var startPos = options.locations ? [tokPos, new Position] : tokPos; + var startPos = options.locations ? [tokPos, curPosition()] : tokPos; initParserState(); return parseTopLevel(options.program || startNodeAt(startPos)); }; @@ -314,7 +314,7 @@ function initParserState() { lastStart = lastEnd = tokPos; - if (options.locations) lastEndLoc = new Position; + if (options.locations) lastEndLoc = curPosition(); inFunction = inGenerator = strict = false; labels = []; skipSpace(); @@ -588,9 +588,17 @@ // These are used when `options.locations` is on, for the // `tokStartLoc` and `tokEndLoc` properties. - function Position() { - this.line = tokCurLine; - this.column = tokPos - tokLineStart; + function Position(line, col) { + this.line = line; + this.column = col; + } + + Position.prototype.offset = function(n) { + return new Position(this.line, this.column + n); + } + + function curPosition() { + return new Position(tokCurLine, tokPos - tokLineStart); } // Reset the token state. Used at the start of a parse. @@ -615,7 +623,7 @@ function finishToken(type, val, shouldSkipSpace) { tokEnd = tokPos; - if (options.locations) tokEndLoc = new Position; + if (options.locations) tokEndLoc = curPosition(); tokType = type; if (shouldSkipSpace !== false) skipSpace(); tokVal = val; @@ -626,7 +634,7 @@ } function skipBlockComment() { - var startLoc = options.onComment && options.locations && new Position; + var startLoc = options.onComment && options.locations && curPosition(); var start = tokPos, end = input.indexOf("*/", tokPos += 2); if (end === -1) raise(tokPos - 2, "Unterminated comment"); tokPos = end + 2; @@ -640,12 +648,12 @@ } if (options.onComment) options.onComment(true, input.slice(start + 2, end), start, tokPos, - startLoc, options.locations && new Position); + startLoc, options.locations && curPosition()); } function skipLineComment(startSkip) { var start = tokPos; - var startLoc = options.onComment && options.locations && new Position; + var startLoc = options.onComment && options.locations && curPosition(); var ch = input.charCodeAt(tokPos+=startSkip); while (tokPos < inputLen && ch !== 10 && ch !== 13 && ch !== 8232 && ch !== 8233) { ++tokPos; @@ -653,7 +661,7 @@ } if (options.onComment) options.onComment(false, input.slice(start + startSkip, tokPos), start, tokPos, - startLoc, options.locations && new Position); + startLoc, options.locations && curPosition()); } // Called at the start of the parse and after every token. Skips @@ -881,7 +889,7 @@ function readToken(forceRegexp) { if (!forceRegexp) tokStart = tokPos; else tokPos = tokStart + 1; - if (options.locations) tokStartLoc = new Position; + if (options.locations) tokStartLoc = curPosition(); if (forceRegexp) return readRegexp(); if (tokPos >= inputLen) return finishToken(_eof); @@ -1303,6 +1311,15 @@ return node; } + function finishNodeAt(node, type, pos) { + if (options.locations) { node.loc.end = pos[1]; pos = pos[0]; } + node.type = type; + node.end = pos; + if (options.ranges) + node.range[1] = pos; + return node; + } + // Test whether a statement node is the string literal `"use strict"`. function isUseStrict(stmt) { @@ -2154,11 +2171,12 @@ // Parse template expression. function parseTemplateElement() { - var elem = startNode(); + var elem = startNodeAt(options.locations ? [tokStart + 1, tokStartLoc.offset(1)] : tokStart + 1); elem.value = tokVal; elem.tail = input.charCodeAt(tokEnd - 1) !== 123; // '{' next(); - return finishNode(elem, "TemplateElement"); + var endOff = elem.tail ? 1 : 2; + return finishNodeAt(elem, "TemplateElement", options.locations ? [lastEnd - endOff, lastEndLoc.offset(-endOff)] : lastEnd - endOff); } function parseTemplate() { diff --git a/acorn_loose.js b/acorn_loose.js index fe11e5b114..16176d7da8 100644 --- a/acorn_loose.js +++ b/acorn_loose.js @@ -254,6 +254,14 @@ return node; } + function finishNodeAt(node, type, pos) { + if (options.locations) { node.loc.end = pos[1]; pos = pos[0]; } + node.type = type; + node.end = pos; + if (options.ranges) node.range[1] = pos; + return node; + } + function dummyIdent() { var dummy = startNode(); dummy.name = "✖"; @@ -801,11 +809,13 @@ } function parseTemplateElement() { - var elem = startNode(); + var elem = startNodeAt(options.locations ? [token.start + 1, token.startLoc.offset(1)] : token.start + 1); elem.value = token.value; elem.tail = input.charCodeAt(token.end - 1) !== 123; // '{' + var endOff = elem.tail ? 1 : 2; + var endPos = options.locations ? [token.end - endOff, token.endLoc.offset(-endOff)] : token.end - endOff; next(); - return finishNode(elem, "TemplateElement"); + return finishNodeAt(elem, "TemplateElement", endPos); } function parseTemplate() { diff --git a/test/tests-harmony.js b/test/tests-harmony.js index 9723073690..fbe33bee07 100644 --- a/test/tests-harmony.js +++ b/test/tests-harmony.js @@ -628,8 +628,8 @@ test("`42`", { value: {raw: "42", cooked: "42"}, tail: true, loc: { - start: {line: 1, column: 0}, - end: {line: 1, column:4} + start: {line: 1, column: 1}, + end: {line: 1, column: 3} } }], expressions: [], @@ -674,8 +674,8 @@ test("raw`42`", { value: {raw: "42", cooked: "42"}, tail: true, loc: { - start: {line: 1, column: 3}, - end: {line: 1, column: 7} + start: {line: 1, column: 4}, + end: {line: 1, column: 6} } }], expressions: [], @@ -726,8 +726,8 @@ test("raw`hello ${name}`", { value: {raw: "hello ", cooked: "hello "}, tail: false, loc: { - start: {line: 1, column: 3}, - end: {line: 1, column: 12} + start: {line: 1, column: 4}, + end: {line: 1, column: 10} } }, { @@ -735,8 +735,8 @@ test("raw`hello ${name}`", { value: {raw: "", cooked: ""}, tail: true, loc: { - start: {line: 1, column: 16}, - end: {line: 1, column: 18} + start: {line: 1, column: 17}, + end: {line: 1, column: 17} } } ], @@ -784,8 +784,8 @@ test("`$`", { value: {raw: "$", cooked: "$"}, tail: true, loc: { - start: {line: 1, column: 0}, - end: {line: 1, column: 3} + start: {line: 1, column: 1}, + end: {line: 1, column: 2} } }], expressions: [], @@ -820,8 +820,8 @@ test("`\\n\\r\\b\\v\\t\\f\\\n\\\r\n`", { value: {raw: "\\n\\r\\b\\v\\t\\f\\\n\\\r\n", cooked: "\n\r\b\u000b\t\f"}, tail: true, loc: { - start: {line: 1, column: 0}, - end: {line: 3, column: 1} + start: {line: 1, column: 1}, + end: {line: 3, column: 0} } }], expressions: [], @@ -856,8 +856,8 @@ test("`\n\r\n`", { value: {raw: "\n\r\n", cooked: "\n\n"}, tail: true, loc: { - start: {line: 1, column: 0}, - end: {line: 3, column: 1} + start: {line: 1, column: 1}, + end: {line: 3, column: 0} } }], expressions: [], @@ -892,8 +892,8 @@ test("`\\u{000042}\\u0042\\x42u0\\102\\A`", { value: {raw: "\\u{000042}\\u0042\\x42u0\\102\\A", cooked: "BBBu0BA"}, tail: true, loc: { - start: {line: 1, column: 0}, - end: {line: 1, column: 30} + start: {line: 1, column: 1}, + end: {line: 1, column: 29} } }], expressions: [], @@ -940,8 +940,8 @@ test("new raw`42`", { value: {raw: "42", cooked: "42"}, tail: true, loc: { - start: {line: 1, column: 7}, - end: {line: 1, column: 11} + start: {line: 1, column: 8}, + end: {line: 1, column: 10} } }], expressions: [], From 75b58c07d48b6bef5fa7188bc50a29adcde93ff0 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Mon, 15 Dec 2014 17:32:38 +0100 Subject: [PATCH 08/14] [loose parser] Improve autoclosing of expression lists --- acorn_loose.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/acorn_loose.js b/acorn_loose.js index 16176d7da8..39157f310e 100644 --- a/acorn_loose.js +++ b/acorn_loose.js @@ -1135,10 +1135,9 @@ } function parseExprList(close, allowEmpty) { - var indent = curIndent, line = curLineStart, elts = [], continuedLine = nextLineStart; + var indent = curIndent, line = curLineStart, elts = []; next(); // Opening bracket - if (curLineStart > continuedLine) continuedLine = curLineStart; - while (!closes(close, indent + (curLineStart <= continuedLine ? 1 : 0), line)) { + while (!closes(close, indent + 1, line)) { if (eat(tt.comma)) { elts.push(allowEmpty ? null : dummyIdent()); continue; From 97f4e9a0268ca1bc5c4a1a18618878fd0dc668ef Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Wed, 17 Dec 2014 11:53:20 +0100 Subject: [PATCH 09/14] [loose parser] Make unclosed objects / lists span to the start of the next node That way, whitespace at their end is considered part of them, and Tern can recognize when the cursor is inside of them. --- acorn_loose.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/acorn_loose.js b/acorn_loose.js index 39157f310e..97a3df603a 100644 --- a/acorn_loose.js +++ b/acorn_loose.js @@ -905,7 +905,12 @@ } } popCx(); - eat(tt.braceR); + if (!eat(tt.braceR)) { + // If there is no closing brace, make the node span to the start + // of the next token (this is useful for Tern) + lastEnd = token.start; + if (options.locations) lastEndLoc = token.startLoc; + } if (isClass) { semicolon(); finishNode(node.body, "ClassBody"); @@ -1152,7 +1157,12 @@ eat(tt.comma); } popCx(); - eat(close); + if (!eat(close)) { + // If there is no closing brace, make the node span to the start + // of the next token (this is useful for Tern) + lastEnd = token.start; + if (options.locations) lastEndLoc = token.startLoc; + } return elts; } }); From 78e1d7ada684868d92d09555408ae2df1812b5ae Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Wed, 17 Dec 2014 11:54:09 +0100 Subject: [PATCH 10/14] Mark version 0.11.0 --- acorn.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/acorn.js b/acorn.js index d7a73623b9..d8ae3cc4c8 100644 --- a/acorn.js +++ b/acorn.js @@ -28,7 +28,7 @@ })(this, function(exports) { "use strict"; - exports.version = "0.10.1"; + exports.version = "0.11.0"; // The main exported interface (under `self.acorn` when in the // browser) is a `parse` function that takes a code string and diff --git a/package.json b/package.json index 87c3d6f456..71298fc0ad 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "ECMAScript parser", "homepage": "http://marijnhaverbeke.nl/acorn/", "main": "acorn.js", - "version": "0.10.1", + "version": "0.11.0", "engines": {"node": ">=0.4.0"}, "maintainers": [{"name": "Marijn Haverbeke", "email": "marijnh@gmail.com", From 8e84aa02f4485ca557d635806c3b8f1d193391b1 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Wed, 17 Dec 2014 11:54:40 +0100 Subject: [PATCH 11/14] Bump version number post-0.11 --- acorn.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/acorn.js b/acorn.js index d8ae3cc4c8..2e8fa7cd0a 100644 --- a/acorn.js +++ b/acorn.js @@ -28,7 +28,7 @@ })(this, function(exports) { "use strict"; - exports.version = "0.11.0"; + exports.version = "0.11.1"; // The main exported interface (under `self.acorn` when in the // browser) is a `parse` function that takes a code string and diff --git a/package.json b/package.json index 71298fc0ad..e8d244ea07 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "ECMAScript parser", "homepage": "http://marijnhaverbeke.nl/acorn/", "main": "acorn.js", - "version": "0.11.0", + "version": "0.11.1", "engines": {"node": ">=0.4.0"}, "maintainers": [{"name": "Marijn Haverbeke", "email": "marijnh@gmail.com", From e37c07248e7ad553b6b4df451c6ba1a935cb379a Mon Sep 17 00:00:00 2001 From: Ingvar Stepanyan Date: Wed, 17 Dec 2014 19:58:38 +0200 Subject: [PATCH 12/14] Added optional support for hashbang directives. Fixes #180. --- acorn.js | 6 ++++++ acorn_loose.js | 1 - test/tests.js | 11 +++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/acorn.js b/acorn.js index 2e8fa7cd0a..291f7b414c 100644 --- a/acorn.js +++ b/acorn.js @@ -74,6 +74,9 @@ // When enabled, import/export statements are not constrained to // appearing at the top of the program. allowImportExportEverywhere: false, + // When enabled, hashbang directive in the beginning of file + // is allowed and treated as a line comment. + allowHashBang: false, // When `locations` is on, `loc` properties holding objects with // `start` and `end` properties in `{line, column}` form (with // line being 1-based and column 0-based) will be attached to the @@ -615,6 +618,9 @@ tokRegexpAllowed = true; metParenL = 0; templates = []; + if (tokPos === 0 && options.allowHashBang && input.slice(0, 2) === '#!') { + skipLineComment(2); + } } // Called at the end of every token. Sets `tokEnd`, `tokVal`, and diff --git a/acorn_loose.js b/acorn_loose.js index 97a3df603a..6262162382 100644 --- a/acorn_loose.js +++ b/acorn_loose.js @@ -45,7 +45,6 @@ exports.parse_dammit = function(inpt, opts) { if (!opts) opts = {}; input = String(inpt); - if (/^#!.*/.test(input)) input = "//" + input.slice(2); fetchToken = acorn.tokenize(input, opts); options = fetchToken.options; sourceFile = options.sourceFile || null; diff --git a/test/tests.js b/test/tests.js index 72b9340307..f6df2edc5e 100644 --- a/test/tests.js +++ b/test/tests.js @@ -28834,3 +28834,14 @@ test('var x = (1 + 2)', {}, { }); test("function f(f) { 'use strict'; }", {}); + +// https://github.com/marijnh/acorn/issues/180 +test("#!/usr/bin/node\n;", {}, { + allowHashBang: true, + onComment: [{ + type: "Line", + value: "/usr/bin/node", + start: 0, + end: 15 + }] +}); From adec9f4b8ee4cac6ba682330b83540a0e988e73b Mon Sep 17 00:00:00 2001 From: Ingvar Stepanyan Date: Wed, 17 Dec 2014 20:37:00 +0200 Subject: [PATCH 13/14] Provide better error on member expression in binding's left side. --- acorn.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/acorn.js b/acorn.js index 291f7b414c..3f15583c13 100644 --- a/acorn.js +++ b/acorn.js @@ -1489,14 +1489,12 @@ switch (expr.type) { case "Identifier": if (strict && (isStrictBadIdWord(expr.name) || isStrictReservedWord(expr.name))) - raise(expr.start, isBinding - ? "Binding " + expr.name + " in strict mode" - : "Assigning to " + expr.name + " in strict mode" - ); + raise(expr.start, (isBinding ? "Binding " : "Assigning to ") + expr.name + " in strict mode"); break; case "MemberExpression": - if (!isBinding) break; + if (isBinding) raise(expr.start, "Binding to member expression"); + break; case "ObjectPattern": for (var i = 0; i < expr.properties.length; i++) From 5512e26ac0b257e28221dc43bbf54e824f98143c Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Thu, 18 Dec 2014 10:19:22 +0100 Subject: [PATCH 14/14] Note allowHashBang option in README Issue #180 --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 58627b0275..6cac0f0e46 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,10 @@ object referring to that same position. declarations can only appear at a program's top level. Setting this option to `true` allows them anywhere where a statement is allowed. +- **allowHashBang**: When this is enabled (off by default), if the + code starts with the characters `#!` (as in a shellscript), the + first line will be treated as a comment. + - **locations**: When `true`, each node has a `loc` object attached with `start` and `end` subobjects, each of which contains the one-based line and zero-based column numbers in `{line, column}`