UNPKG

jshint

Version:

Static analysis tool for JavaScript

1,719 lines (1,485 loc) 198 kB
/*! * 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