Skip to content

Commit

Permalink
[[FEAT]] parse and lint tagged template literals
Browse files Browse the repository at this point in the history
  • Loading branch information
caitp committed Feb 26, 2015
1 parent cc8ccd7 commit 4816dbd
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 40 deletions.
89 changes: 60 additions & 29 deletions src/jshint.js
Original file line number Diff line number Diff line change
Expand Up @@ -875,10 +875,6 @@ var JSHINT = (function() {
if (state.tokens.next.id === "(end)")
error("E006", state.tokens.curr);

if (state.tokens.next.type === "(template)") {
doTemplateLiteral();
}

var isDangerous =
state.option.asi &&
state.tokens.prev.line !== startLine(state.tokens.curr) &&
Expand All @@ -904,7 +900,9 @@ var JSHINT = (function() {
error("E030", state.tokens.curr, state.tokens.curr.id);
}

while (rbp < state.tokens.next.lbp && !isEndOfExpr()) {
// TODO: use pratt mechanics rather than special casing template tokens
while ((rbp < state.tokens.next.lbp || state.tokens.next.type === "(template)") &&
!isEndOfExpr()) {
isArray = state.tokens.curr.value === "Array";
isObject = state.tokens.curr.value === "Object";

Expand Down Expand Up @@ -1491,6 +1489,8 @@ var JSHINT = (function() {

function parseFinalSemicolon() {
if (state.tokens.next.id !== ";") {
// don't complain about unclosed templates / strings
if (state.tokens.next.isUnclosed) return advance();
if (!state.option.asi) {
// If this is the last statement in a block that ends on
// the same line *and* option lastsemic is on, ignore the warning.
Expand Down Expand Up @@ -1985,24 +1985,37 @@ var JSHINT = (function() {
}
};

state.syntax["(template)"] = {
type: "(template)",
var baseTemplateSyntax = {
lbp: 0,
identifier: false,
nud: doTemplateLiteral
template: true,
};

type("(template middle)", function() {
return this;
});

type("(template tail)", function() {
return this;
});

type("(no subst template)", function() {
return this;
});
state.syntax["(template)"] = _.extend({
type: "(template)",
nud: doTemplateLiteral,
led: doTemplateLiteral,
noSubst: false
}, baseTemplateSyntax);

state.syntax["(template middle)"] = _.extend({
type: "(template middle)",
middle: true,
noSubst: false
}, baseTemplateSyntax);

state.syntax["(template tail)"] = _.extend({
type: "(template tail)",
tail: true,
noSubst: false
}, baseTemplateSyntax);

state.syntax["(no subst template)"] = _.extend({
type: "(template)",
nud: doTemplateLiteral,
led: doTemplateLiteral,
noSubst: true,
tail: true // mark as tail, since it's always the last component
}, baseTemplateSyntax);

type("(regexp)", function() {
return this;
Expand Down Expand Up @@ -2903,20 +2916,38 @@ var JSHINT = (function() {
return "(scope)" in token;
}

function doTemplateLiteral() {
while (state.tokens.next.type !== "(template tail)" && state.tokens.next.id !== "(end)") {
advance();
if (state.tokens.next.type === "(template tail)") {
break;
} else if (state.tokens.next.type !== "(template middle)" &&
state.tokens.next.type !== "(end)") {
expression(10); // should probably have different rbp?
function doTemplateLiteral(left) {
// ASSERT: this.type === "(template)"
// jshint validthis: true
var ctx = this.context;
var noSubst = this.noSubst;
var depth = this.depth;

if (!noSubst) {
while (!end() && state.tokens.next.id !== "(end)") {
if (!state.tokens.next.template || state.tokens.next.depth > depth) {
expression(0); // should probably have different rbp?
} else {
// skip template start / middle
advance();
}
}
}

return {
id: "(template)",
type: "(template)"
type: "(template)",
tag: left
};

function end() {
if (state.tokens.curr.template && state.tokens.curr.tail &&
state.tokens.curr.context === ctx) return true;
var complete = (state.tokens.next.template && state.tokens.next.tail &&
state.tokens.next.context === ctx);
if (complete) advance();
return complete || state.tokens.next.isUnclosed;
}
}

/**
Expand Down
53 changes: 44 additions & 9 deletions src/lex.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,23 @@ Lexer.prototype = {
_lines: [],

inContext: function(ctxType) {
return this.context.length > 0 && this.context[this.context.length - 1] === ctxType;
return this.context.length > 0 && this.context[this.context.length - 1].type === ctxType;
},

pushContext: function(ctxType) {
this.context.push({ type: ctxType });
},

popContext: function() {
return this.context.pop();
},

isContext: function(context) {
return this.context.length > 0 && this.context[this.context.length - 1] === context;
},

currentContext: function() {
return this.context.length > 0 && this.context[this.context.length - 1];
},

getLines: function() {
Expand Down Expand Up @@ -237,7 +253,7 @@ Lexer.prototype = {

// A block/object opener
case "{":
this.context.push(Context.Block);
this.pushContext(Context.Block);
return {
type: Token.Punctuator,
value: ch1
Expand All @@ -246,7 +262,7 @@ Lexer.prototype = {
// A block/object closer
case "}":
if (this.inContext(Context.Block)) {
this.context.pop();
this.popContext();
}
return {
type: Token.Punctuator,
Expand Down Expand Up @@ -1014,6 +1030,7 @@ Lexer.prototype = {
var ch;
var startLine = this.line;
var startChar = this.char;
var depth = this.templateStarts.length;

if (!state.option.esnext) {
// Only lex template strings in ESNext mode.
Expand All @@ -1022,8 +1039,9 @@ Lexer.prototype = {
// Template must start with a backtick.
tokenType = Token.TemplateHead;
this.templateStarts.push({ line: this.line, char: this.char });
depth = this.templateStarts.length;
this.skip(1);
this.context.push(Context.Template);
this.pushContext(Context.Template);
} else if (this.inContext(Context.Template) && this.peek() === "}") {
// If we're in a template context, and we have a '}', lex a TemplateMiddle.
tokenType = Token.TemplateMiddle;
Expand All @@ -1037,7 +1055,6 @@ Lexer.prototype = {
value += "\n";
if (!this.nextLine()) {
// Unclosed template literal --- point to the starting "`"
this.context.pop();
var startPos = this.templateStarts.pop();
this.trigger("error", {
code: "E052",
Expand All @@ -1049,7 +1066,9 @@ Lexer.prototype = {
value: value,
startLine: startLine,
startChar: startChar,
isUnclosed: true
isUnclosed: true,
depth: depth,
context: this.popContext()
};
}
}
Expand All @@ -1062,7 +1081,9 @@ Lexer.prototype = {
value: value,
startLine: startLine,
startChar: startChar,
isUnclosed: false
isUnclosed: false,
depth: depth,
context: this.currentContext()
};
} else if (ch === '\\') {
var escape = this.scanEscapeSequence(checks);
Expand All @@ -1078,15 +1099,16 @@ Lexer.prototype = {
// Final value is either NoSubstTemplate or TemplateTail
tokenType = tokenType === Token.TemplateHead ? Token.NoSubstTemplate : Token.TemplateTail;
this.skip(1);
this.context.pop();
this.templateStarts.pop();

return {
type: tokenType,
value: value,
startLine: startLine,
startChar: startChar,
isUnclosed: false
isUnclosed: false,
depth: depth,
context: this.popContext()
};
},

Expand Down Expand Up @@ -1620,6 +1642,18 @@ Lexer.prototype = {
if (token && token.startLine && token.startLine !== this.line) {
obj.startLine = token.startLine;
}
if (token && token.context) {
// Context of current token
obj.context = token.context;
}
if (token && token.depth) {
// Nested template depth
obj.depth = token.depth;
}
if (token && token.isUnclosed) {
// Mark token as unclosed string / template literal
obj.isUnclosed = token.isUnclosed;
}

if (isProperty && obj.identifier) {
obj.isProperty = isProperty;
Expand Down Expand Up @@ -1795,3 +1829,4 @@ Lexer.prototype = {
};

exports.Lexer = Lexer;
exports.Context = Context;
67 changes: 65 additions & 2 deletions tests/unit/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -847,8 +847,15 @@ exports.testES6TemplateLiterals = function (test) {
TestRun(test)
.addError(14, "Octal literals are not allowed in strict mode.")
.addError(21, "Unclosed template literal.")
.addError(22, "Expected an identifier and instead saw '(end)'.")
.addError(22, "Missing semicolon.")
.test(src, { esnext: true });
test.done();
};

exports.testES6TaggedTemplateLiterals = function (test) {
var src = fs.readFileSync(__dirname + "/fixtures/es6-template-literal-tagged.js", "utf8");
TestRun(test)
.addError(16, "Octal literals are not allowed in strict mode.")
.addError(23, "Unclosed template literal.")
.test(src, { esnext: true });
test.done();
};
Expand All @@ -864,6 +871,19 @@ exports.testES6TemplateLiteralsUnused = function (test) {
test.done();
};

exports.testES6TaggedTemplateLiteralsUnused = function (test) {
var src = [
"function tag() {}",
"var a = 'hello';",
"alert(tag`${a} world`);"
];
TestRun(test)
.test(src, { esnext: true, unused: true });

test.done();
};


exports.testES6TemplateLiteralsUndef = function (test) {
var src = [
"/* global alert */",
Expand All @@ -876,6 +896,21 @@ exports.testES6TemplateLiteralsUndef = function (test) {
test.done();
};


exports.testES6TaggedTemplateLiteralsUndef = function (test) {
var src = [
"/* global alert */",
"alert(tag`${a} world`);"
];
TestRun(test)
.addError(2, "'tag' is not defined.")
.addError(2, "'a' is not defined.")
.test(src, { esnext: true, undef: true });

test.done();
};


exports.testES6TemplateLiteralMultiline = function (test) {
var src = [
'let multiline = `',
Expand Down Expand Up @@ -961,6 +996,34 @@ exports.testES6TemplateLiteralMultilineReturnValue = function (test) {
test.done();
};


exports.testES6TaggedTemplateLiteralMultilineReturnValue = function (test) {
var src = [
'function tag() {}',
'function sayHello(to) {',
' return tag`Hello, ',
' ${to}!`;',
'}',
'print(sayHello("George"));'
];

TestRun(test).test(src, { esnext: true });

var src = [
'function tag() {}',
'function* sayHello(to) {',
' yield tag`Hello, ',
' ${to}!`;',
'}',
'print(sayHello("George"));'
];

TestRun(test).test(src, { esnext: true });

test.done();
};


exports.testMultilineReturnValueStringLiteral = function (test) {
var src = [
'function sayHello(to) {',
Expand Down
23 changes: 23 additions & 0 deletions tests/unit/fixtures/es6-template-literal-tagged.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
function tag() {}

var one = 1, two = 2;

var string = tag` ${one} + ${two}
= ${one + two}`;

var literal = tag`because I
can`;

var escaped = tag`one = \`${one}\``;

function octal_strictmode() {
"use strict";

var test = tag`\033\t`;
}

var nested = tag`Look and ${ tag`Nested ${ tag`whoaaa` } template` } listen`;

var innerobj = tag`Template with ${ {obj: "literal"} } inside`;

var unterminated = tag`${one}

0 comments on commit 4816dbd

Please sign in to comment.