jshint
Version:
Static analysis tool for JavaScript
1,719 lines (1,485 loc) • 198 kB
JavaScript
/*!
* JSHint, by JSHint Community.
*
* Licensed under the MIT license.
*
* JSHint is a derivative work of JSLint:
*
* Copyright (c) 2002 Douglas Crockford (www.JSLint.com)
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom
* the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
/*jshint quotmark:double */
/*exported console */
var _ = require("lodash");
var events = require("events");
var vars = require("./vars.js");
var messages = require("./messages.js");
var Lexer = require("./lex.js").Lexer;
var reg = require("./reg.js");
var state = require("./state.js").state;
var style = require("./style.js");
var options = require("./options.js");
var scopeManager = require("./scope-manager.js");
var prodParams = require("./prod-params.js");
// We need this module here because environments such as IE and Rhino
// don't necessarilly expose the 'console' API and browserify uses
// it to log things. It's a sad state of affair, really.
var console = require("console-browserify");
// We build the application inside a function so that we produce only a singleton
// variable. That function will be invoked immediately, and its return value is
// the JSHINT function itself.
var JSHINT = (function() {
"use strict";
var api, // Extension API
// These are operators that should not be used with the ! operator.
bang = {
"<" : true,
"<=" : true,
"==" : true,
"===": true,
"!==": true,
"!=" : true,
">" : true,
">=" : true,
"+" : true,
"-" : true,
"*" : true,
"/" : true,
"%" : true
},
declared, // Globals that were declared using /*global ... */ syntax.
functions, // All of the functions
inblock,
indent,
lookahead,
lex,
member,
membersOnly,
predefined, // Global variables defined by option
extraModules = [],
emitter = new events.EventEmitter();
function checkOption(name, isStable, t) {
var type, validNames;
if (isStable) {
type = "";
validNames = options.validNames;
} else {
type = "unstable ";
validNames = options.unstableNames;
}
name = name.trim();
if (/^[+-]W\d{3}$/g.test(name)) {
return true;
}
if (validNames.indexOf(name) === -1) {
if (t.type !== "jslint" && !_.has(options.removed, name)) {
error("E001", t, type, name);
return false;
}
}
return true;
}
function isString(obj) {
return Object.prototype.toString.call(obj) === "[object String]";
}
function isIdentifier(tkn, value) {
if (!tkn)
return false;
if (!tkn.identifier || tkn.value !== value)
return false;
return true;
}
/**
* ES3 defined a set of "FutureReservedWords" in order "to allow for the
* possibility of future adoption of [proposed] extensions."
*
* ES5 reduced the set of FutureReservedWords, in some cases by using them to
* define new syntactic forms (e.g. `class` and `const`) and in other cases
* by simply allowing their use as Identifiers (e.g. `int` and `goto`).
* Separately, ES5 introduced new restrictions on certain tokens, but limited
* the restriction to strict mode code (e.g. `let` and `yield`).
*
* This function determines if a given token describes a reserved word
* according to the current state of the parser.
*
* @param {number} context - the parsing context; see `prod-params.js` for
* more information
* @param {Token} token
*/
function isReserved(context, token) {
if (!token.reserved) {
return false;
}
var meta = token.meta;
if (meta && meta.isFutureReservedWord) {
if (state.inES5()) {
// ES3 FutureReservedWord in an ES5 environment.
if (!meta.es5) {
return false;
}
if (token.isProperty) {
return false;
}
}
} else if (meta && meta.es5 && !state.inES5()) {
return false;
}
// Some identifiers are reserved only within a strict mode environment.
if (meta && meta.strictOnly && state.inES5()) {
if (!state.option.strict && !state.isStrict()) {
return false;
}
}
if (token.id === "await" && (!(context & prodParams.async) && !state.option.module)) {
return false;
}
if (token.id === "yield" && (!(context & prodParams.yield))) {
return state.isStrict();
}
return true;
}
function supplant(str, data) {
return str.replace(/\{([^{}]*)\}/g, function(a, b) {
var r = data[b];
return typeof r === "string" || typeof r === "number" ? r : a;
});
}
function combine(dest, src) {
Object.keys(src).forEach(function(name) {
if (_.has(JSHINT.blacklist, name)) return;
dest[name] = src[name];
});
}
function processenforceall() {
if (state.option.enforceall) {
for (var enforceopt in options.bool.enforcing) {
if (state.option[enforceopt] === undefined &&
!options.noenforceall[enforceopt]) {
state.option[enforceopt] = true;
}
}
for (var relaxopt in options.bool.relaxing) {
if (state.option[relaxopt] === undefined) {
state.option[relaxopt] = false;
}
}
}
}
/**
* Apply all linting options according to the status of the `state` object.
*/
function applyOptions() {
var badESOpt = null;
processenforceall();
/**
* TODO: Remove in JSHint 3
*/
badESOpt = state.inferEsVersion();
if (badESOpt) {
quit("E059", state.tokens.next, "esversion", badESOpt);
}
if (state.inES5()) {
combine(predefined, vars.ecmaIdentifiers[5]);
}
if (state.inES6()) {
combine(predefined, vars.ecmaIdentifiers[6]);
}
if (state.inES8()) {
combine(predefined, vars.ecmaIdentifiers[8]);
}
if (state.inES11()) {
combine(predefined, vars.ecmaIdentifiers[11]);
}
/**
* Use `in` to check for the presence of any explicitly-specified value for
* `globalstrict` because both `true` and `false` should trigger an error.
*/
if (state.option.strict === "global" && "globalstrict" in state.option) {
quit("E059", state.tokens.next, "strict", "globalstrict");
}
if (state.option.module) {
/**
* TODO: Extend this restriction to *all* ES6-specific options.
*/
if (!state.inES6()) {
warning("W134", state.tokens.next, "module", 6);
}
}
if (state.option.regexpu) {
/**
* TODO: Extend this restriction to *all* ES6-specific options.
*/
if (!state.inES6()) {
warning("W134", state.tokens.next, "regexpu", 6);
}
}
if (state.option.couch) {
combine(predefined, vars.couch);
}
if (state.option.qunit) {
combine(predefined, vars.qunit);
}
if (state.option.rhino) {
combine(predefined, vars.rhino);
}
if (state.option.shelljs) {
combine(predefined, vars.shelljs);
combine(predefined, vars.node);
}
if (state.option.typed) {
combine(predefined, vars.typed);
}
if (state.option.phantom) {
combine(predefined, vars.phantom);
}
if (state.option.prototypejs) {
combine(predefined, vars.prototypejs);
}
if (state.option.node) {
combine(predefined, vars.node);
combine(predefined, vars.typed);
}
if (state.option.devel) {
combine(predefined, vars.devel);
}
if (state.option.dojo) {
combine(predefined, vars.dojo);
}
if (state.option.browser) {
combine(predefined, vars.browser);
combine(predefined, vars.typed);
}
if (state.option.browserify) {
combine(predefined, vars.browser);
combine(predefined, vars.typed);
combine(predefined, vars.browserify);
}
if (state.option.nonstandard) {
combine(predefined, vars.nonstandard);
}
if (state.option.jasmine) {
combine(predefined, vars.jasmine);
}
if (state.option.jquery) {
combine(predefined, vars.jquery);
}
if (state.option.mootools) {
combine(predefined, vars.mootools);
}
if (state.option.worker) {
combine(predefined, vars.worker);
}
if (state.option.wsh) {
combine(predefined, vars.wsh);
}
if (state.option.yui) {
combine(predefined, vars.yui);
}
if (state.option.mocha) {
combine(predefined, vars.mocha);
}
}
// Produce an error warning.
function quit(code, token, a, b) {
var percentage = Math.floor((token.line / state.lines.length) * 100);
var message = messages.errors[code].desc;
var exception = {
name: "JSHintError",
line: token.line,
character: token.from,
message: message + " (" + percentage + "% scanned).",
raw: message,
code: code,
a: a,
b: b
};
exception.reason = supplant(message, exception) + " (" + percentage +
"% scanned).";
throw exception;
}
function removeIgnoredMessages() {
var ignored = state.ignoredLines;
if (_.isEmpty(ignored)) return;
JSHINT.errors = _.reject(JSHINT.errors, function(err) { return ignored[err.line] });
}
function warning(code, t, a, b, c, d) {
var ch, l, w, msg;
if (/^W\d{3}$/.test(code)) {
if (state.ignored[code])
return;
msg = messages.warnings[code];
} else if (/E\d{3}/.test(code)) {
msg = messages.errors[code];
} else if (/I\d{3}/.test(code)) {
msg = messages.info[code];
}
t = t || state.tokens.next || {};
if (t.id === "(end)") { // `~
t = state.tokens.curr;
}
l = t.line;
ch = t.from;
w = {
id: "(error)",
raw: msg.desc,
code: msg.code,
evidence: state.lines[l - 1] || "",
line: l,
character: ch,
scope: JSHINT.scope,
a: a,
b: b,
c: c,
d: d
};
w.reason = supplant(msg.desc, w);
JSHINT.errors.push(w);
removeIgnoredMessages();
var errors = JSHINT.errors.filter(function(e) { return /E\d{3}/.test(e.code); });
if (errors.length >= state.option.maxerr) {
quit("E043", t);
}
return w;
}
function warningAt(m, l, ch, a, b, c, d) {
return warning(m, {
line: l,
from: ch
}, a, b, c, d);
}
function error(m, t, a, b, c, d) {
warning(m, t, a, b, c, d);
}
function errorAt(m, l, ch, a, b, c, d) {
return error(m, {
line: l,
from: ch
}, a, b, c, d);
}
// Tracking of "internal" scripts, like eval containing a static string
function addEvalCode(elem, token) {
JSHINT.internals.push({
id: "(internal)",
elem: elem,
token: token,
code: token.value.replace(/([^\\])(\\*)\2\\n/g, "$1\n")
});
}
/**
* Process an inline linting directive
*
* @param {Token} directiveToken - the directive-bearing comment token
* @param {Token} previous - the token that preceeds the directive
*/
function lintingDirective(directiveToken, previous) {
var body = directiveToken.body.split(",")
.map(function(s) { return s.trim(); });
var predef = {};
if (directiveToken.type === "falls through") {
previous.caseFallsThrough = true;
return;
}
if (directiveToken.type === "globals") {
body.forEach(function(item, idx) {
var parts = item.split(":");
var key = parts[0].trim();
if (key === "-" || !key.length) {
// Ignore trailing comma
if (idx > 0 && idx === body.length - 1) {
return;
}
error("E002", directiveToken);
return;
}
if (key.charAt(0) === "-") {
key = key.slice(1);
JSHINT.blacklist[key] = key;
delete predefined[key];
} else {
predef[key] = parts.length > 1 && parts[1].trim() === "true";
}
});
combine(predefined, predef);
for (var key in predef) {
if (_.has(predef, key)) {
declared[key] = directiveToken;
}
}
}
if (directiveToken.type === "exported") {
body.forEach(function(e, idx) {
if (!e.length) {
// Ignore trailing comma
if (idx > 0 && idx === body.length - 1) {
return;
}
error("E002", directiveToken);
return;
}
state.funct["(scope)"].addExported(e);
});
}
if (directiveToken.type === "members") {
membersOnly = membersOnly || {};
body.forEach(function(m) {
var ch1 = m.charAt(0);
var ch2 = m.charAt(m.length - 1);
if (ch1 === ch2 && (ch1 === "\"" || ch1 === "'")) {
m = m
.substr(1, m.length - 2)
.replace("\\\"", "\"");
}
membersOnly[m] = false;
});
}
var numvals = [
"maxstatements",
"maxparams",
"maxdepth",
"maxcomplexity",
"maxerr",
"maxlen",
"indent"
];
if (directiveToken.type === "jshint" || directiveToken.type === "jslint" ||
directiveToken.type === "jshint.unstable") {
body.forEach(function(item) {
var parts = item.split(":");
var key = parts[0].trim();
var val = parts.length > 1 ? parts[1].trim() : "";
var numberVal;
if (!checkOption(key, directiveToken.type !== "jshint.unstable", directiveToken)) {
return;
}
if (numvals.indexOf(key) >= 0) {
// GH988 - numeric options can be disabled by setting them to `false`
if (val !== "false") {
numberVal = +val;
if (typeof numberVal !== "number" || !isFinite(numberVal) ||
numberVal <= 0 || Math.floor(numberVal) !== numberVal) {
error("E032", directiveToken, val);
return;
}
state.option[key] = numberVal;
} else {
state.option[key] = key === "indent" ? 4 : false;
}
return;
}
if (key === "validthis") {
// `validthis` is valid only within a function scope.
if (state.funct["(global)"])
return void error("E009");
if (val !== "true" && val !== "false")
return void error("E002", directiveToken);
state.option.validthis = (val === "true");
return;
}
if (key === "quotmark") {
switch (val) {
case "true":
case "false":
state.option.quotmark = (val === "true");
break;
case "double":
case "single":
state.option.quotmark = val;
break;
default:
error("E002", directiveToken);
}
return;
}
if (key === "shadow") {
switch (val) {
case "true":
state.option.shadow = true;
break;
case "outer":
state.option.shadow = "outer";
break;
case "false":
case "inner":
state.option.shadow = "inner";
break;
default:
error("E002", directiveToken);
}
return;
}
if (key === "unused") {
switch (val) {
case "true":
state.option.unused = true;
break;
case "false":
state.option.unused = false;
break;
case "vars":
case "strict":
state.option.unused = val;
break;
default:
error("E002", directiveToken);
}
return;
}
if (key === "latedef") {
switch (val) {
case "true":
state.option.latedef = true;
break;
case "false":
state.option.latedef = false;
break;
case "nofunc":
state.option.latedef = "nofunc";
break;
default:
error("E002", directiveToken);
}
return;
}
if (key === "ignore") {
switch (val) {
case "line":
state.ignoredLines[directiveToken.line] = true;
removeIgnoredMessages();
break;
default:
error("E002", directiveToken);
}
return;
}
if (key === "strict") {
switch (val) {
case "true":
state.option.strict = true;
break;
case "false":
state.option.strict = false;
break;
case "global":
case "implied":
state.option.strict = val;
break;
default:
error("E002", directiveToken);
}
return;
}
if (key === "module") {
/**
* TODO: Extend this restriction to *all* "environmental" options.
*/
if (!hasParsedCode(state.funct)) {
error("E055", directiveToken, "module");
}
}
if (key === "esversion") {
switch (val) {
case "3":
case "5":
case "6":
case "7":
case "8":
case "9":
case "10":
case "11":
state.option.moz = false;
state.option.esversion = +val;
break;
case "2015":
case "2016":
case "2017":
case "2018":
case "2019":
case "2020":
state.option.moz = false;
// Translate specification publication year to version number.
state.option.esversion = +val - 2009;
break;
default:
error("E002", directiveToken);
}
if (!hasParsedCode(state.funct)) {
error("E055", directiveToken, "esversion");
}
return;
}
var match = /^([+-])(W\d{3})$/g.exec(key);
if (match) {
// ignore for -W..., unignore for +W...
state.ignored[match[2]] = (match[1] === "-");
return;
}
var tn;
if (val === "true" || val === "false") {
if (directiveToken.type === "jslint") {
tn = options.renamed[key] || key;
state.option[tn] = (val === "true");
if (options.inverted[tn] !== undefined) {
state.option[tn] = !state.option[tn];
}
} else if (directiveToken.type === "jshint.unstable") {
/* istanbul ignore next */
state.option.unstable[key] = (val === "true");
} else {
state.option[key] = (val === "true");
}
return;
}
error("E002", directiveToken);
});
applyOptions();
}
}
/**
* Return a token beyond the token available in `state.tokens.next`. If no
* such token exists, return the "(end)" token. This function is used to
* determine parsing strategies in cases where the value of the next token
* does not provide sufficient information, as is the case with `for` loops,
* e.g.:
*
* for ( var i in ...
*
* versus:
*
* for ( var i = ...
*
* @param {number} [p] - offset of desired token; defaults to 0
*
* @returns {token}
*/
function peek(p) {
var i = p || 0, j = lookahead.length, t;
if (i < j) {
return lookahead[i];
}
while (j <= i) {
t = lex.token();
// When the lexer is exhausted, this function should produce the "(end)"
// token, even in cases where the requested token is beyond the end of
// the input stream.
if (!t) {
// If the lookahead buffer is empty, the expected "(end)" token was
// already emitted by the most recent invocation of `advance` and is
// available as the next token.
if (!lookahead.length) {
return state.tokens.next;
}
return lookahead[j - 1];
}
lookahead[j] = t;
j += 1;
}
return t;
}
function peekIgnoreEOL() {
var i = 0;
var t;
do {
t = peek(i++);
} while (t.id === "(endline)");
return t;
}
/**
* Consume the next token.
*
* @param {string} [expected] - the expected value of the next token's `id`
* property (in the case of punctuators) or
* `value` property (in the case of identifiers
* and literals); if unspecified, any token will
* be accepted
* @param {object} [relatedToken] - the token that informed the expected
* value, if any (for example: the opening
* brace when a closing brace is expected);
* used to produce more meaningful errors
*/
function advance(expected, relatedToken) {
var nextToken = state.tokens.next;
if (expected && nextToken.id !== expected) {
if (relatedToken) {
if (nextToken.id === "(end)") {
error("E019", relatedToken, relatedToken.id);
} else {
error("E020", nextToken, expected, relatedToken.id,
relatedToken.line, nextToken.value);
}
} else if (nextToken.type !== "(identifier)" || nextToken.value !== expected) {
error("E021", nextToken, expected, nextToken.value);
}
}
state.tokens.prev = state.tokens.curr;
state.tokens.curr = state.tokens.next;
for (;;) {
state.tokens.next = lookahead.shift() || lex.token();
if (!state.tokens.next) { // No more tokens left, give up
quit("E041", state.tokens.curr);
}
if (state.tokens.next.id === "(end)" || state.tokens.next.id === "(error)") {
return;
}
if (state.tokens.next.check) {
state.tokens.next.check();
}
if (state.tokens.next.isSpecial) {
lintingDirective(state.tokens.next, state.tokens.curr);
} else {
if (state.tokens.next.id !== "(endline)") {
break;
}
}
}
}
/**
* Determine whether a given token is an operator.
*
* @param {token} token
*
* @returns {boolean}
*/
function isOperator(token) {
return token.first || token.right || token.left || token.id === "yield" || token.id === "await";
}
function isEndOfExpr(context, curr, next) {
if (arguments.length <= 1) {
curr = state.tokens.curr;
next = state.tokens.next;
}
if (next.id === "in" && context & prodParams.noin) {
return true;
}
if (next.id === ";" || next.id === "}" || next.id === ":") {
return true;
}
if (next.infix === curr.infix ||
// Infix operators which follow `yield` should only be consumed as part
// of the current expression if allowed by the syntactic grammar. In
// effect, this prevents automatic semicolon insertion when `yield` is
// followed by a newline and a comma operator (without enabling it when
// `yield` is followed by a newline and a `[` token).
(curr.id === "yield" && curr.rbp < next.rbp)) {
return !sameLine(curr, next);
}
return false;
}
/**
* The `expression` function is the heart of JSHint's parsing behaior. It is
* based on the Pratt parser, but it extends that model with a `fud` method.
* Short for "first null denotation," it it similar to the `nud` ("null
* denotation") function, but it is only used on the first token of a
* statement. This simplifies usage in statement-oriented languages like
* JavaScript.
*
* .nud Null denotation
* .fud First null denotation
* .led Left denotation
* lbp Left binding power
* rbp Right binding power
*
* They are elements of the parsing method called Top Down Operator Precedence.
*
* In addition to parsing, this function applies a number of linting patterns.
*
* @param {number} context - the parsing context (a bitfield describing
* conditions of the current parsing operation
* which can influence how the next tokens are
* interpreted); see `prod-params.js` for more
* detail)
* @param {number} rbp - the right-binding power of the token to be consumed
*/
function expression(context, rbp) {
var left, isArray = false, isObject = false;
var initial = context & prodParams.initial;
var curr;
context &= ~prodParams.initial;
state.nameStack.push();
if (state.tokens.next.id === "(end)")
error("E006", state.tokens.curr);
advance();
if (initial) {
state.funct["(verb)"] = state.tokens.curr.value;
state.tokens.curr.beginsStmt = true;
}
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) {
left = state.tokens.curr.nud(context, rbp);
} else {
error("E030", state.tokens.curr, state.tokens.curr.id);
}
while (rbp < state.tokens.next.lbp && !isEndOfExpr(context)) {
isArray = state.tokens.curr.value === "Array";
isObject = state.tokens.curr.value === "Object";
// #527, new Foo.Array(), Foo.Array(), new Foo.Object(), Foo.Object()
// Line breaks in IfStatement heads exist to satisfy the checkJSHint
// "Line too long." error.
if (left && (left.value || (left.first && left.first.value))) {
// If the left.value is not "new", or the left.first.value is a "."
// then safely assume that this is not "new Array()" and possibly
// not "new Object()"...
if (left.value !== "new" ||
(left.first && left.first.value && left.first.value === ".")) {
isArray = false;
// ...In the case of Object, if the left.value and state.tokens.curr.value
// are not equal, then safely assume that this not "new Object()"
if (left.value !== state.tokens.curr.value) {
isObject = false;
}
}
}
advance();
if (isArray && state.tokens.curr.id === "(" && state.tokens.next.id === ")") {
warning("W009", state.tokens.curr);
}
if (isObject && state.tokens.curr.id === "(" && state.tokens.next.id === ")") {
warning("W010", state.tokens.curr);
}
if (left && state.tokens.curr.led) {
left = state.tokens.curr.led(context, left);
} else {
error("E033", state.tokens.curr, state.tokens.curr.id);
}
}
}
state.nameStack.pop();
return left;
}
// Functions for conformance of style.
function sameLine(first, second) {
return first.line === (second.startLine || second.line);
}
function nobreaknonadjacent(left, right) {
if (!state.option.laxbreak && !sameLine(left, right)) {
warning("W014", right, right.value);
}
}
function nolinebreak(t) {
if (!sameLine(t, state.tokens.next)) {
warning("E022", t, t.value);
}
}
/**
* Validate the comma token in the "current" position of the token stream.
*
* @param {object} [opts]
* @param {boolean} [opts.property] - flag indicating whether the current
* comma token is situated directly within
* an object initializer
* @param {boolean} [opts.allowTrailing] - flag indicating whether the
* current comma token may appear
* directly before a delimiter
*
* @returns {boolean} flag indicating the validity of the current comma
* token; `false` if the token directly causes a syntax
* error, `true` otherwise
*/
function checkComma(opts) {
var prev = state.tokens.prev;
var curr = state.tokens.curr;
opts = opts || {};
if (!sameLine(prev, curr)) {
if (!state.option.laxcomma) {
if (checkComma.first) {
warning("I001", curr);
checkComma.first = false;
}
warning("W014", prev, curr.value);
}
}
if (state.tokens.next.identifier && !(opts.property && state.inES5())) {
// Keywords that cannot follow a comma operator.
switch (state.tokens.next.value) {
case "break":
case "case":
case "catch":
case "continue":
case "default":
case "do":
case "else":
case "finally":
case "for":
case "if":
case "in":
case "instanceof":
case "return":
case "switch":
case "throw":
case "try":
case "var":
case "let":
case "while":
case "with":
error("E024", state.tokens.next, state.tokens.next.value);
return false;
}
}
if (state.tokens.next.type === "(punctuator)") {
switch (state.tokens.next.value) {
case "}":
case "]":
case ",":
case ")":
if (opts.allowTrailing) {
return true;
}
error("E024", state.tokens.next, state.tokens.next.value);
return false;
}
}
return true;
}
/**
* Factory function for creating "symbols"--objects that will be inherited by
* tokens. The objects created by this function are stored in a symbol table
* and set as the prototype of the tokens generated by the lexer.
*
* Note that this definition of "symbol" describes an implementation detail
* of JSHint and is not related to the ECMAScript value type introduced in
* ES2015.
*
* @param {string} s - the name of the token; for keywords (e.g. `void`) and
* delimiters (e.g.. `[`), this is the token's text
* representation; for literals (e.g. numbers) and other
* "special" tokens (e.g. the end-of-file marker) this is
* a parenthetical value
* @param {number} p - the left-binding power of the token as used by the
* Pratt parsing semantics
*
* @returns {object} - the object describing the JSHint symbol (provided to
* support cases where further refinement is necessary)
*/
function symbol(s, p) {
var x = state.syntax[s];
if (!x || typeof x !== "object") {
state.syntax[s] = x = {
id: s,
lbp: p,
// Symbols that accept a right-hand side do so with a binding power
// that is commonly identical to their left-binding power. (This value
// is relevant when determining if the grouping operator is necessary
// to override the precedence of surrounding operators.) Because the
// exponentiation operator's left-binding power and right-binding power
// are distinct, the values must be encoded separately.
rbp: p,
value: s
};
}
return x;
}
/**
* Convenience function for defining delimiter symbols.
*
* @param {string} s - the name of the symbol
*
* @returns {object} - the object describing the JSHint symbol (provided to
* support cases where further refinement is necessary)
*/
function delim(s) {
var x = symbol(s, 0);
x.delim = true;
return x;
}
/**
* Convenience function for defining statement-denoting symbols.
*
* @param {string} s - the name of the symbol
* @param {function} f - the first null denotation function for the symbol;
* see the `expression` function for more detail
*
* @returns {object} - the object describing the JSHint symbol (provided to
* support cases where further refinement is necessary)
*/
function stmt(s, f) {
var x = delim(s);
x.identifier = x.reserved = true;
x.fud = f;
return x;
}
/**
* Convenience function for defining block-statement-denoting symbols.
*
* A block-statement-denoting symbol is one like 'if' or 'for', which will be
* followed by a block and will not have to end with a semicolon.
*
* @param {string} s - the name of the symbol
* @param {function} - the first null denotation function for the symbol; see
* the `expression` function for more detail
*
* @returns {object} - the object describing the JSHint symbol (provided to
* support cases where further refinement is necessary)
*/
function blockstmt(s, f) {
var x = stmt(s, f);
x.block = true;
return x;
}
/**
* Denote a given JSHint symbol as an identifier and a reserved keyword.
*
* @param {object} - a JSHint symbol value
*
* @returns {object} - the provided object
*/
function reserveName(x) {
var c = x.id.charAt(0);
if ((c >= "a" && c <= "z") || (c >= "A" && c <= "Z")) {
x.identifier = x.reserved = true;
}
return x;
}
/**
* Convenience function for defining "prefix" symbols--operators that accept
* expressions as a right-hand side.
*
* @param {string} s - the name of the symbol
* @param {function} [f] - the first null denotation function for the symbol;
* see the `expression` function for more detail
*
* @returns {object} - the object describing the JSHint symbol (provided to
* support cases where further refinement is necessary)
*/
function prefix(s, f) {
var x = symbol(s, 150);
reserveName(x);
x.nud = (typeof f === "function") ? f : function(context) {
this.arity = "unary";
this.right = expression(context, 150);
if (this.id === "++" || this.id === "--") {
if (state.option.plusplus) {
warning("W016", this, this.id);
}
if (this.right) {
checkLeftSideAssign(context, this.right, this);
}
}
return this;
};
return x;
}
/**
* Convenience function for defining "type" symbols--those that describe
* literal values.
*
* @param {string} s - the name of the symbol
* @param {function} f - the first null denotation function for the symbol;
* see the `expression` function for more detail
*
* @returns {object} - the object describing the JSHint symbol (provided to
* support cases where further refinement is necessary)
*/
function type(s, f) {
var x = symbol(s, 0);
x.type = s;
x.nud = f;
return x;
}
/**
* Convenience function for defining JSHint symbols for reserved
* keywords--those that are restricted from use as bindings (and as property
* names in ECMAScript 3 environments).
*
* @param {string} s - the name of the symbol
* @param {function} func - the first null denotation function for the
* symbol; see the `expression` function for more
* detail
*
* @returns {object} - the object describing the JSHint symbol (provided to
* support cases where further refinement is necessary)
*/
function reserve(name, func) {
var x = type(name, func);
x.identifier = true;
x.reserved = true;
return x;
}
/**
* Convenience function for defining JSHint symbols for keywords that are
* only reserved in some circumstances.
*
* @param {string} name - the name of the symbol
* @param {object} [meta] - a collection of optional arguments
* @param {function} [meta.nud] -the null denotation function for the symbol;
* see the `expression` function for more detail
* @param {boolean} [meta.es5] - `true` if the identifier is reserved
* in ECMAScript 5 or later
* @param {boolean} [meta.strictOnly] - `true` if the identifier is only
* reserved in strict mode code.
*
* @returns {object} - the object describing the JSHint symbol (provided to
* support cases where further refinement is necessary)
*/
function FutureReservedWord(name, meta) {
var x = type(name, state.syntax["(identifier)"].nud);
meta = meta || {};
meta.isFutureReservedWord = true;
x.value = name;
x.identifier = true;
x.reserved = true;
x.meta = meta;
return x;
}
/**
* Convenience function for defining "infix" symbols--operators that require
* operands as both "land-hand side" and "right-hand side".
*
* @param {string} s - the name of the symbol
* @param {function} [f] - a function to be invoked that consumes the
* right-hand side of the operator
* @param {number} p - the left-binding power of the token as used by the
* Pratt parsing semantics
* @param {boolean} [w] - if `true`
*
* @returns {object} - the object describing the JSHint symbol (provided to
* support cases where further refinement is necessary)
*/
function infix(s, f, p, w) {
var x = symbol(s, p);
reserveName(x);
x.infix = true;
x.led = function(context, left) {
if (!w) {
nobreaknonadjacent(state.tokens.prev, state.tokens.curr);
}
if ((s === "in" || s === "instanceof") && left.id === "!") {
warning("W018", left, "!");
}
if (typeof f === "function") {
return f(context, left, this);
} else {
this.left = left;
this.right = expression(context, p);
return this;
}
};
return x;
}
/**
* Convenience function for defining the `=>` token as used in arrow
* functions.
*
* @param {string} s - the name of the symbol
*
* @returns {object} - the object describing the JSHint symbol (provided to
* support cases where further refinement is necessary)
*/
function application(s) {
var x = symbol(s, 42);
x.infix = true;
x.led = function(context, left) {
nobreaknonadjacent(state.tokens.prev, state.tokens.curr);
this.left = left;
this.right = doFunction(context, { type: "arrow", loneArg: left });
return this;
};
return x;
}
/**
* Convenience function for defining JSHint symbols for relation operators.
*
* @param {string} s - the name of the symbol
* @param {function} [f] - a function to be invoked to enforce any additional
* linting rules.
*
* @returns {object} - the object describing the JSHint symbol (provided to
* support cases where further refinement is necessary)
*/
function relation(s, f) {
var x = symbol(s, 100);
x.infix = true;
x.led = function(context, left) {
nobreaknonadjacent(state.tokens.prev, state.tokens.curr);
this.left = left;
var right = this.right = expression(context, 100);
if (isIdentifier(left, "NaN") || isIdentifier(right, "NaN")) {
warning("W019", this);
} else if (f) {
f.apply(this, [context, left, right]);
}
if (!left || !right) {
quit("E041", state.tokens.curr);
}
if (left.id === "!") {
warning("W018", left, "!");
}
if (right.id === "!") {
warning("W018", right, "!");
}
return this;
};
return x;
}
/**
* Determine if a given token marks the beginning of a UnaryExpression.
*
* @param {object} token
*
* @returns {boolean}
*/
function beginsUnaryExpression(token) {
return token.arity === "unary" && token.id !== "++" && token.id !== "--";
}
var typeofValues = {};
typeofValues.legacy = [
// E4X extended the `typeof` operator to return "xml" for the XML and
// XMLList types it introduced.
// Ref: 11.3.2 The typeof Operator
// http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-357.pdf
"xml",
// IE<9 reports "unknown" when the `typeof` operator is applied to an
// object existing across a COM+ bridge. In lieu of official documentation
// (which does not exist), see:
// http://robertnyman.com/2005/12/21/what-is-typeof-unknown/
"unknown"
];
typeofValues.es3 = [
"undefined", "boolean", "number", "string", "function", "object",
];
typeofValues.es3 = typeofValues.es3.concat(typeofValues.legacy);
typeofValues.es6 = typeofValues.es3.concat("symbol", "bigint");
/**
* Validate comparisons between the result of a `typeof` expression and a
* string literal.
*
* @param {token} [left] - one of the values being compared
* @param {token} [right] - the other value being compared
* @param {object} state - the global state object (see `state.js`)
*
* @returns {boolean} - `false` if the second token describes a `typeof`
* expression and the first token is a string literal
* whose value is never returned by that operator;
* `true` otherwise
*/
function isTypoTypeof(left, right, state) {
var values;
if (state.option.notypeof)
return false;
if (!left || !right)
return false;
values = state.inES6() ? typeofValues.es6 : typeofValues.es3;
if (right.type === "(identifier)" && right.value === "typeof" && left.type === "(string)") {
if (left.value === "bigint") {
if (!state.inES11()) {
warning("W119", left, "BigInt", "11");
}
return false;
}
return !_.includes(values, left.value);
}
return false;
}
/**
* Determine if a given token describes the built-in `eval` function.
*
* @param {token} left
* @param {object} state - the global state object (see `state.js`)
*
* @returns {boolean}
*/
function isGlobalEval(left, state) {
var isGlobal = false;
// permit methods to refer to an "eval" key in their own context
if (left.type === "this" && state.funct["(context)"] === null) {
isGlobal = true;
}
// permit use of "eval" members of objects
else if (left.type === "(identifier)") {
if (state.option.node && left.value === "global") {
isGlobal = true;
}
else if (state.option.browser && (left.value === "window" || left.value === "document")) {
isGlobal = true;
}
}
return isGlobal;
}
/**
* Determine if a given token describes a property of a built-in object.
*
* @param {token} left
*
* @returns {boolean}
*/
function findNativePrototype(left) {
var natives = [
"Array", "ArrayBuffer", "Boolean", "Collator", "DataView", "Date",
"DateTimeFormat", "Error", "EvalError", "Float32Array", "Float64Array",
"Function", "Infinity", "Intl", "Int16Array", "Int32Array", "Int8Array",
"Iterator", "Number", "NumberFormat", "Object", "RangeError",
"ReferenceError", "RegExp", "StopIteration", "String", "SyntaxError",
"TypeError", "Uint16Array", "Uint32Array", "Uint8Array", "Uint8ClampedArray",
"URIError"
];
function walkPrototype(obj) {
if (typeof obj !== "object") return;
return obj.right === "prototype" ? obj : walkPrototype(obj.left);
}
function walkNative(obj) {
while (!obj.identifier && typeof obj.left === "object")
obj = obj.left;
if (obj.identifier && natives.indexOf(obj.value) >= 0 &&
state.funct["(scope)"].isPredefined(obj.value)) {
return obj.value;
}
}
var prototype = walkPrototype(left);
if (prototype) return walkNative(prototype);
}
/**
* Determine if the given token is a valid assignment target; emit errors
* and/or warnings as appropriate
*
* @param {number} context - the parsing context; see `prod-params.js` for
* more information
* @param {token} left - the left hand side of the assignment
* @param {token=} assignToken - the token for the assignment, used for
* reporting
* @param {object=} options - optional object
* @param {boolean} options.allowDestructuring - whether to allow
* destructuring binding
*
* @returns {boolean} Whether the left hand side is OK
*/
function checkLeftSideAssign(context, left, assignToken, options) {
var allowDestructuring = options && options.allowDestructuring;
assignToken = assignToken || left;
if (state.option.freeze) {
var nativeObject = findNativePrototype(left);
if (nativeObject)
warning("W121", left, nativeObject);
}
if (left.identifier && !left.isMetaProperty) {
// The `reassign` method also calls `modify`, but we are specific in
// order to catch function re-assignment and globals re-assignment
state.funct["(scope)"].block.reassign(left.value, left);
}
if (left.id === ".") {
if (!left.left || left.left.value === "arguments" && !state.isStrict()) {
warning("W143", assignToken);
}
state.nameStack.set(state.tokens.prev);
return true;
} else if (left.id === "{" || left.id === "[") {
if (!allowDestructuring || !left.destructAssign) {
if (left.id === "{" || !left.left) {
warning("E031", assignToken);
} else if (left.left.value === "arguments" && !state.isStrict()) {
warning("W143", assignToken);
}
}
if (left.id === "[") {
state.nameStack.set(left.right);
}
return true;
} else if (left.identifier && !isReserved(context, left) && !left.isMetaProperty) {
if (state.funct["(scope)"].bindingtype(left.value) === "exception") {
warning("W022", left);
}
if (left.value === "eval" && state.isStrict()) {
error("E031", assignToken);
return false;
} else if (left.value === "arguments") {
if (!state.isStrict()) {
warning("W143", assignToken);
} else {
error("E031", assignToken);
return false;
}
}
state.nameStack.set(left);
return true;
}
error("E031", assignToken);
return false;
}
/**
* Convenience function for defining JSHint symbols for assignment operators.
*
* @param {string} s - the name of the symbol
* @param {function} [f] - a function to be invoked that consumes the
* right-hand side of the operator (see the `infix`
* function)
*
* @returns {object} - the object describing the JSHint symbol (provided to
* support cases where further refinement is necessary)
*/
function assignop(s, f) {
var x = infix(s, typeof f === "function" ? f : function(context, left, that) {
that.left = left;
checkLeftSideAssign(context, left, that, { allowDestructuring: true });
that.right = expression(context, 10);
return that;
}, 20);
x.exps = true;
x.assign = true;
return x;
}
/**
* Convenience function for defining JSHint symbols for bitwise operators.
*
* @param {string} s - the name of the symbol
* @param {function} [f] - the left denotation function for the symbol; see
* the `expression` function for more detail
* @param {number} p - the left-binding power of the token as used by the
* Pratt parsing semantics
*
* @returns {object} - the object describing the JSHint symbol (provided to
* support cases where further refinement is necessary)
*/
function bitwise(s, f, p) {
var x = symbol(s, p);
reserveName(x);
x.infix = true;
x.led = (typeof f === "function") ? f : function(context, left) {
if (state.option.bitwise) {
warning("W016", this, this.id);
}
this.left = left;
this.right = expression(context, p);
return this;
};
return x;
}
/**
* Convenience function for defining JSHint symbols for bitwise assignment
* operators. See the `assignop` function for more detail.
*
* @param {string} s - the name of the symbol
*
* @returns {object} - the object describing the JSHint symbol (provided to
* support cases where further refinement is necessary)
*/
function bitwiseassignop(s) {
symbol(s, 20).exps = true;
return infix(s, function(context, left, that) {
if (state.option.bitwise) {
warning("W016", that, that.id);
}
checkLeftSideAssign(context, left, that);
that.right = expression(context, 10);
return that;
}, 20);
}
/**
* Convenience function f