Skip to content

Commit

Permalink
[[FIX]] Correct parsing of let token
Browse files Browse the repository at this point in the history
Tolerate the use of `let` as an Identifier outside of strict mode.

This change makes error messages less helpful when the input code
contains the non-standard "`let` expression" language extension but
omits the required `moz` option. However, this change does not effect
JSHint's ability to parse that language extension and is therefore
compatible with prior releases of JSHint.
  • Loading branch information
jugglinmike committed Jan 14, 2019
1 parent 0d87e1e commit 030d6b4
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 81 deletions.
83 changes: 73 additions & 10 deletions src/jshint.js
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,7 @@ var JSHINT = (function() {
function expression(rbp, context) {
var left, isArray = false, isObject = false;
var initial = context & prodParams.initial;
var curr;

context &= ~prodParams.initial;

Expand All @@ -928,7 +929,9 @@ var JSHINT = (function() {
state.tokens.curr.beginsStmt = true;
}

if (initial && state.tokens.curr.fud) {
curr = state.tokens.curr;

if (initial && curr.fud && (!curr.useFud || curr.useFud(context))) {
left = state.tokens.curr.fud(context);
} else {
if (state.tokens.curr.nud) {
Expand Down Expand Up @@ -4140,10 +4143,7 @@ var JSHINT = (function() {
warning("W104", state.tokens.curr, type, "6");
}

if (isLet && state.tokens.next.value === "(") {
if (!state.inMoz()) {
warning("W118", state.tokens.next, "let block");
}
if (isLet && isMozillaLet()) {
advance("(");
state.funct["(scope)"].stack();
letblock = true;
Expand Down Expand Up @@ -4174,6 +4174,13 @@ var JSHINT = (function() {
for (var t in tokens) {
if (tokens.hasOwnProperty(t)) {
t = tokens[t];

// It is a Syntax Error if the BoundNames of BindingList contains
// "let".
if (t.id === "let") {
warning("W024", t.token, t.id);
}

if (state.funct["(scope)"].block.isGlobal()) {
if (predefined[t.id] === false) {
warning("W079", t.token, t.id);
Expand Down Expand Up @@ -4246,25 +4253,74 @@ var JSHINT = (function() {
conststatement.exps = true;
conststatement.declaration = true;


/**
* Determine if the current `let` token designates the beginning of a "let
* block" or "let expression" as implemented in the Mozilla SpiderMonkey
* engine.
*
* This function will only return `true` if Mozilla extensions have been
* enabled. It would be preferable to detect the language feature regardless
* of the parser's state because this would allow JSHint to instruct users to
* enable the `moz` option where necessary. This is not possible because the
* language extension is not compatible with standard JavaScript. For
* example, the following program code may describe a "let block" or a
* function invocation:
*
* let(x)
* {
* typeof x;
* }
*
* @returns {boolean}
*/
function isMozillaLet() {
return state.tokens.next.id === "(" && state.inMoz();
}
var letstatement = stmt("let", function(context) {
return blockVariableStatement("let", this, context);
});
letstatement.nud = function(context, rbp) {
if (state.tokens.next.value === "(") {
if (!state.inMoz()) {
warning("W118", state.tokens.next, "let expressions");
}
if (isMozillaLet()) {
// create a new block scope we use only for the current expression
state.funct["(scope)"].stack();
advance("(");
state.tokens.prev.fud(context);
advance(")");
expression(rbp, context);
state.funct["(scope)"].unstack();
} else {
this.exps = false;
return state.syntax["(identifier)"].nud.apply(this, arguments);
}
};
letstatement.meta = { es5: true, isFutureReservedWord: true, strictOnly: true };
letstatement.exps = true;
letstatement.declaration = true;
letstatement.useFud = function() {
var next = state.tokens.next;
var nextIsBindingName;

if (this.line !== next.line && !state.inES6()) {
return false;
}

// JSHint generally interprets `let` as a reserved word even though it is
// not considered as such by the ECMAScript specification because doing so
// simplifies parsing logic. It is special-cased here so that code such as
//
// let
// let
//
// is correctly interpreted as an invalid LexicalBinding. (Without this
// consideration, the code above would be parsed as two
// IdentifierReferences.)
nextIsBindingName = next.identifier && (!isReserved(next) ||
next.id === "let");

return nextIsBindingName || checkPunctuators(next, ["{", "["]) ||
isMozillaLet();
};

var varstatement = stmt("var", function(context) {
var noin = context & prodParams.noin;
Expand Down Expand Up @@ -4756,6 +4812,7 @@ var JSHINT = (function() {
var targets;
var target;
var decl;
var afterNext = peek();

var headContext = context | prodParams.noin;

Expand All @@ -4764,7 +4821,13 @@ var JSHINT = (function() {
decl = state.tokens.curr.fud(headContext);
comma = decl.hasComma ? decl : null;
initializer = decl.hasInitializer ? decl : null;
} else if (state.tokens.next.id === "let" || state.tokens.next.id === "const") {
} else if (state.tokens.next.id === "const" ||
// The "let" keyword only signals a lexical binding if it is followed by
// an identifier, `{`, or `[`. Otherwise, it should be parsed as an
// IdentifierReference (i.e. in a subsquent branch).
(state.tokens.next.id === "let" &&
((afterNext.identifier && afterNext.id !== "in") ||
checkPunctuators(afterNext, ["{", "["])))) {
advance(state.tokens.next.id);
// create a new block scope
letscope = true;
Expand Down
38 changes: 1 addition & 37 deletions src/lex.js
Original file line number Diff line number Diff line change
Expand Up @@ -1883,38 +1883,6 @@ Lexer.prototype = {
var checks = asyncTrigger();
var token;


function isReserved(token, isProperty) {
// At present all current identifiers have reserved set.
// Preserving check anyway, for future-proofing.
/* istanbul ignore if */
if (!token.reserved) {
return false;
}
var meta = token.meta;

if (meta && meta.isFutureReservedWord && state.inES5()) {
// ES3 FutureReservedWord in an ES5 environment.
if (!meta.es5) {
return false;
}

// Some ES5 FutureReservedWord identifiers are active only
// within a strict mode environment.
if (meta.strictOnly) {
if (!state.option.strict && !state.isStrict()) {
return false;
}
}

if (isProperty) {
return false;
}
}

return true;
}

// Produce a token object.
var create = function(type, value, isProperty, token) {
/*jshint validthis:true */
Expand Down Expand Up @@ -1950,11 +1918,7 @@ Lexer.prototype = {
}

if (_.has(state.syntax, value)) {
obj = Object.create(state.syntax[value]);
// If this can't be a reserved keyword, reset the object.
if (!isReserved(obj, isProperty && type === "(identifier)")) {
obj = null;
}
obj = Object.create(state.syntax[value] || state.syntax["(error)"]);
}
}

Expand Down
5 changes: 0 additions & 5 deletions tests/test262/expectations.txt
Original file line number Diff line number Diff line change
Expand Up @@ -605,8 +605,6 @@ test/language/expressions/assignment/dstr-obj-prop-elem-target-yield-expr.js(str
test/language/expressions/assignment/dstr-obj-prop-elem-target-yield-ident-valid.js(default)
test/language/expressions/assignment/dstr-obj-prop-nested-array-yield-ident-valid.js(default)
test/language/expressions/assignment/dstr-obj-prop-nested-obj-yield-ident-valid.js(default)
test/language/expressions/object/let-non-strict-access.js(default)
test/language/expressions/object/let-non-strict-syntax.js(default)
test/language/expressions/object/scope-gen-meth-param-rest-elem-var-close.js(default)
test/language/expressions/object/scope-gen-meth-param-rest-elem-var-open.js(default)
test/language/expressions/object/scope-meth-param-rest-elem-var-close.js(default)
Expand Down Expand Up @@ -686,7 +684,6 @@ test/language/statements/for-in/head-const-bound-names-fordecl-tdz.js(strict mod
test/language/statements/for-in/head-let-bound-names-fordecl-tdz.js(default)
test/language/statements/for-in/head-let-bound-names-fordecl-tdz.js(strict mode)
test/language/statements/for-in/head-lhs-let.js(default)
test/language/statements/for-in/head-var-bound-names-let.js(default)
test/language/statements/for/decl-cls.js(default)
test/language/statements/for/decl-cls.js(strict mode)
test/language/statements/for/head-lhs-let.js(default)
Expand Down Expand Up @@ -838,7 +835,6 @@ test/language/statements/for-of/head-const-bound-names-fordecl-tdz.js(default)
test/language/statements/for-of/head-const-bound-names-fordecl-tdz.js(strict mode)
test/language/statements/for-of/head-let-bound-names-fordecl-tdz.js(default)
test/language/statements/for-of/head-let-bound-names-fordecl-tdz.js(strict mode)
test/language/statements/for-of/head-var-bound-names-let.js(default)
test/language/statements/switch/S12.11_A2_T1.js(default)
test/language/statements/switch/S12.11_A2_T1.js(strict mode)
test/language/statements/while/decl-cls.js(default)
Expand Down Expand Up @@ -876,7 +872,6 @@ test/language/statements/let/global-use-before-initialization-in-declaration-sta
test/language/statements/let/global-use-before-initialization-in-declaration-statement.js(strict mode)
test/language/statements/let/global-use-before-initialization-in-prior-statement.js(default)
test/language/statements/let/global-use-before-initialization-in-prior-statement.js(strict mode)
test/language/statements/let/syntax/identifier-let-allowed-as-lefthandside-expression-non-strict.js(default)
test/language/statements/let/syntax/with-initialisers-in-statement-positions-label-statement.js(default)
test/language/statements/let/syntax/with-initialisers-in-statement-positions-label-statement.js(strict mode)
test/language/statements/let/syntax/without-initialisers-in-statement-positions-label-statement.js(default)
Expand Down
1 change: 0 additions & 1 deletion tests/unit/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,6 @@ exports.testReserved = function (test) {
.test(src, {es3: true});

TestRun(test)
.addError(5, 5, "Expected an identifier and instead saw 'let' (a reserved word).")
.addError(10, 7, "Expected an identifier and instead saw 'let' (a reserved word).")
.test(src, {}); // es5

Expand Down

0 comments on commit 030d6b4

Please sign in to comment.