diff --git a/eslint/babel-eslint-parser/acorn-to-esprima.js b/eslint/babel-eslint-parser/acorn-to-esprima.js index 756b82e974..b562711bcc 100644 --- a/eslint/babel-eslint-parser/acorn-to-esprima.js +++ b/eslint/babel-eslint-parser/acorn-to-esprima.js @@ -58,10 +58,82 @@ exports.toAST = function (ast) { traverse(ast, astTransformVisitor); }; +exports.toTokens = function (tokens) { + // transform tokens to type "Template" + convertTemplateType(tokens); + + return tokens.map(exports.toToken); +}; + function isCompatTag(tagName) { return tagName && /^[a-z]|\-/.test(tagName); } +function convertTemplateType(tokens) { + var startingToken = 0; + var currentToken = 0; + var numBraces = 0; + + function isTemplateStarter(token) { + return tokens[token].type === tt.backQuote || + tokens[token].type === tt.braceR; + } + + function isTemplateEnder(token) { + return tokens[token].type === tt.dollarBraceL || + tokens[token].type === tt.backQuote; + } + + function createTemplateValue(start, end) { + var value = ""; + while (start <= end) { + if (tokens[start].value) { + value += tokens[start].value; + } else if (tokens[start].type !== tt.template) { + value += tokens[start].type.label; + } + start++; + } + return value; + } + + function replaceWithTemplateType(start, end) { + var templateToken = { + type: 'Template', + value: createTemplateValue(start, end), + range: [tokens[start].start, tokens[end].end], + loc: { + start: tokens[start].loc.start, + end: tokens[end].loc.end + } + } + tokens.splice(start, end - start + 1, templateToken); + } + + function checkNumBraces(token) { + if (tokens[token].type === tt.braceL) { + numBraces++; + } else if (tokens[token].type === tt.braceR) { + numBraces--; + } + } + + while (startingToken < tokens.length) { + if (isTemplateStarter(startingToken) && numBraces === 0) { + currentToken = startingToken + 1; + while (currentToken < tokens.length - 1 && !isTemplateEnder(currentToken)) { + checkNumBraces(currentToken); + currentToken++; + } + replaceWithTemplateType(startingToken, currentToken); + startingToken++; + } else { + checkNumBraces(startingToken); + startingToken++; + } + } +} + var astTransformVisitor = { noScope: true, exit: function (node, parent) { @@ -117,5 +189,24 @@ var astTransformVisitor = { node.delegate = node.all; delete node.all; } + + // template strings + + if (t.isTemplateLiteral(node)) { + node.quasis.forEach(function (q) { + q.range[0] -= 1; + if (q.tail) { + q.range[1] += 1; + } else { + q.range[1] += 2; + } + q.loc.start.column -= 1; + if (q.tail) { + q.loc.end.column += 1; + } else { + q.loc.end.column += 2; + } + }); + } } }; diff --git a/eslint/babel-eslint-parser/index.js b/eslint/babel-eslint-parser/index.js index c5426b672d..2002b68fd9 100644 --- a/eslint/babel-eslint-parser/index.js +++ b/eslint/babel-eslint-parser/index.js @@ -56,7 +56,7 @@ function monkeypatch() { }; } -exports.attachComments = function(ast, comments, tokens) { +exports.attachComments = function (ast, comments, tokens) { estraverse.attachComments(ast, comments, tokens); if (comments.length) { @@ -82,7 +82,7 @@ exports.attachComments = function(ast, comments, tokens) { node.leadingComments = []; var firstTokenStart = token.range[0]; var len = comments.length; - for(var i = 0; i < len && comments[i].start < firstTokenStart; i++ ) { + for (var i = 0; i < len && comments[i].start < firstTokenStart; i++) { node.leadingComments.push(comments[i]); } } @@ -138,7 +138,7 @@ exports.parse = function (code) { tokens.pop(); // convert tokens - ast.tokens = tokens.map(acornToEsprima.toToken); + ast.tokens = acornToEsprima.toTokens(tokens); // add comments ast.comments = comments; diff --git a/eslint/babel-eslint-parser/test/babel-eslint.js b/eslint/babel-eslint-parser/test/babel-eslint.js index 7c7c45c5b2..123745f516 100644 --- a/eslint/babel-eslint-parser/test/babel-eslint.js +++ b/eslint/babel-eslint-parser/test/babel-eslint.js @@ -20,7 +20,7 @@ function assertImplementsAST(target, source, path) { error("have different types (" + typeA + " !== " + typeB + ")"); } else if (typeA === "object") { var keysTarget = Object.keys(target); - for(var i in keysTarget) { + for (var i in keysTarget) { var key = keysTarget[i]; path.push(key); assertImplementsAST(target[key], source[key], path); @@ -34,6 +34,7 @@ function assertImplementsAST(target, source, path) { function parseAndAssertSame(code) { var esAST = espree.parse(code, { ecmaFeatures: { + templateStrings: true, modules: true, classes: true, jsx: true @@ -50,14 +51,60 @@ function parseAndAssertSame(code) { } catch(err) { err.message += "\nespree:\n" + - util.inspect(esAST, {depth: err.depth}) + + util.inspect(esAST, {depth: err.depth, colors: true}) + "\nbabel-eslint:\n" + - util.inspect(acornAST, {depth: err.depth}); + util.inspect(acornAST, {depth: err.depth, colors: true}); throw err; } } describe("acorn-to-esprima", function () { + describe("templates", function () { + it("empty template string", function () { + parseAndAssertSame("``"); + }); + + it("template string", function () { + parseAndAssertSame("`test`"); + }); + + it("template string using $", function () { + parseAndAssertSame("`$`"); + }); + + it("template string with expression", function () { + parseAndAssertSame("`${a}`"); + }); + + it("template string with multiple expressions", function () { + parseAndAssertSame("`${a}${b}${c}`"); + }); + + it("template string with expression and strings", function () { + parseAndAssertSame("`a${a}a`"); + }); + + it("template string with binary expression", function () { + parseAndAssertSame("`a${a + b}a`"); + }); + + it("tagged template", function () { + parseAndAssertSame("jsx``"); + }); + + it("tagged template with expression", function () { + parseAndAssertSame("jsx``"); + }); + + it("tagged template with new operator", function () { + parseAndAssertSame("new raw`42`"); + }); + + it("template with nested function/object", function () { + parseAndAssertSame("`outer${{x: {y: 10}}}bar${`nested${function(){return 1;}}endnest`}end`"); + }); + }); + it("simple expression", function () { parseAndAssertSame("a = 1"); }); diff --git a/eslint/babel-eslint-parser/test/non-regression.js b/eslint/babel-eslint-parser/test/non-regression.js index 3e4b3d9bf3..1ce55b0577 100644 --- a/eslint/babel-eslint-parser/test/non-regression.js +++ b/eslint/babel-eslint-parser/test/non-regression.js @@ -139,4 +139,14 @@ describe("verify", function () { [] ); }); + + it("template strings #31", function () { + verifyAndAssertMessages( + "console.log(`${a}, b`);", + { "comma-spacing": 1 }, + [] + ); + }); + + });