/jslint.js
http://github.com/douglascrockford/JSLint · JavaScript · 4988 lines · 4235 code · 382 blank · 371 comment · 1009 complexity · f4a6727d4e59e18bbf8023a841c562e3 MD5 · raw file
Large files are truncated click here to view the full file
- // jslint.js
- // 2020-03-28
- // Copyright (c) 2015 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 shall be used for Good, not Evil.
- // 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.
- // jslint(source, option_object, global_array) is a function that takes 3
- // arguments. The second two arguments are optional.
- // source A text to analyze, a string or an array of strings.
- // option_object An object whose keys correspond to option names.
- // global_array An array of strings containing global variables that
- // the file is allowed readonly access.
- // jslint returns an object containing its results. The object contains a lot
- // of valuable information. It can be used to generate reports. The object
- // contains:
- // directives: an array of directive comment tokens.
- // edition: the version of JSLint that did the analysis.
- // exports: the names exported from the module.
- // froms: an array of strings representing each of the imports.
- // functions: an array of objects that represent all of the functions
- // declared in the file.
- // global: an object representing the global object. Its .context property
- // is an object containing a property for each global variable.
- // id: "(JSLint)"
- // json: true if the file is a JSON text.
- // lines: an array of strings, the source.
- // module: true if an import or export statement was used.
- // ok: true if no warnings were generated. This is what you want.
- // option: the option argument.
- // property: a property object.
- // stop: true if JSLint was unable to finish. You don't want this.
- // tokens: an array of objects representing the tokens in the file.
- // tree: the token objects arranged in a tree.
- // warnings: an array of warning objects. A warning object can contain:
- // name: "JSLintError"
- // column: A column number in the file.
- // line: A line number in the file.
- // code: A warning code string.
- // message: The warning message string.
- // a: Exhibit A.
- // b: Exhibit B.
- // c: Exhibit C.
- // d: Exhibit D.
- // jslint works in several phases. In any of these phases, errors might be
- // found. Sometimes JSLint is able to recover from an error and continue
- // parsing. In some cases, it cannot and will stop early. If that should happen,
- // repair your code and try again.
- // Phases:
- // 1. If the source is a single string, split it into an array of strings.
- // 2. Turn the source into an array of tokens.
- // 3. Furcate the tokens into a parse tree.
- // 4. Walk the tree, traversing all of the nodes of the tree. It is a
- // recursive traversal. Each node may be processed on the way down
- // (preaction) and on the way up (postaction).
- // 5. Check the whitespace between the tokens.
- // jslint can also examine JSON text. It decides that a file is JSON text if
- // the first token is "[" or "{". Processing of JSON text is much simpler than
- // the processing of JavaScript programs. Only the first three phases are
- // required.
- // WARNING: JSLint will hurt your feelings.
- /*property
- a, and, arity, assign, b, bad_assignment_a, bad_directive_a, bad_get,
- bad_module_name_a, bad_option_a, bad_property_a, bad_set, bitwise, block,
- body, browser, c, calls, catch, charCodeAt, closer, closure, code, column,
- concat, constant, context, convert, couch, create, d, dead, default, devel,
- directive, directives, disrupt, dot, duplicate_a, edition, ellipsis, else,
- empty_block, escape_mega, eval, every, expected_a, expected_a_at_b_c,
- expected_a_b, expected_a_b_from_c_d, expected_a_before_b,
- expected_a_next_at_b, expected_digits_after_a, expected_four_digits,
- expected_identifier_a, expected_line_break_a_b, expected_regexp_factor_a,
- expected_space_a_b, expected_statements_a, expected_string_a,
- expected_type_string_a, exports, expression, extra, finally, flag, for,
- forEach, free, freeze, freeze_exports, from, froms, fud, fudge,
- function_in_loop, functions, g, getset, global, i, id, identifier, import,
- inc, indexOf, infix_in, init, initial, isArray, isNaN, join, json, keys,
- label, label_a, lbp, led, length, level, line, lines, live, long, loop, m,
- margin, match, message, misplaced_a, misplaced_directive_a, missing_browser,
- missing_m, module, naked_block, name, names, nested_comment, new, node,
- not_label_a, nr, nud, number_isNaN, ok, open, opening, option,
- out_of_scope_a, parameters, parent, pop, property, push, quote,
- redefinition_a_b, replace, required_a_optional_b, reserved_a, role, search,
- shebang, signature, single, slice, some, sort, split, startsWith, statement,
- stop, subscript_a, switch, test, this, thru, toString, todo_comment,
- tokens, too_long, too_many_digits, tree, try, type, u, unclosed_comment,
- unclosed_mega, unclosed_string, undeclared_a, unexpected_a,
- unexpected_a_after_b, unexpected_a_before_b, unexpected_at_top_level_a,
- unexpected_char_a, unexpected_comment, unexpected_directive_a,
- unexpected_expression_a, unexpected_label_a, unexpected_parens,
- unexpected_space_a_b, unexpected_statement_a, unexpected_trailing_space,
- unexpected_typeof_a, uninitialized_a, unreachable_a,
- unregistered_property_a, unsafe, unused_a, use_double, use_open, use_spaces,
- used, value, var_loop, var_switch, variable, warning, warnings,
- weird_condition_a, weird_expression_a, weird_loop, weird_relation_a, white,
- wrap_condition, wrap_immediate, wrap_parameter, wrap_regexp, wrap_unary,
- wrapped, writable, y
- */
- function empty() {
- // The empty function produces a new empty object that inherits nothing. This is
- // much better than '{}' because confusions around accidental method names like
- // 'constructor' are completely avoided.
- return Object.create(null);
- }
- function populate(array, object = empty(), value = true) {
- // Augment an object by taking property names from an array of strings.
- array.forEach(function (name) {
- object[name] = value;
- });
- return object;
- }
- const allowed_option = {
- // These are the options that are recognized in the option object or that may
- // appear in a /*jslint*/ directive. Most options will have a boolean value,
- // usually true. Some options will also predefine some number of global
- // variables.
- bitwise: true,
- browser: [
- "caches", "CharacterData", "clearInterval", "clearTimeout", "document",
- "DocumentType", "DOMException", "Element", "Event", "event", "fetch",
- "FileReader", "FontFace", "FormData", "history", "IntersectionObserver",
- "localStorage", "location", "MutationObserver", "name", "navigator",
- "screen", "sessionStorage", "setInterval", "setTimeout", "Storage",
- "TextDecoder", "TextEncoder", "URL", "window", "Worker",
- "XMLHttpRequest"
- ],
- couch: [
- "emit", "getRow", "isArray", "log", "provides", "registerType",
- "require", "send", "start", "sum", "toJSON"
- ],
- convert: true,
- devel: [
- "alert", "confirm", "console", "prompt"
- ],
- eval: true,
- for: true,
- fudge: true,
- getset: true,
- long: true,
- node: [
- "Buffer", "clearImmediate", "clearInterval", "clearTimeout",
- "console", "exports", "module", "process", "require",
- "setImmediate", "setInterval", "setTimeout", "TextDecoder",
- "TextEncoder", "URL", "URLSearchParams", "__dirname", "__filename"
- ],
- single: true,
- this: true,
- white: true
- };
- const anticondition = populate([
- "?", "~", "&", "|", "^", "<<", ">>", ">>>", "+", "-", "*", "/", "%",
- "typeof", "(number)", "(string)"
- ]);
- // These are the bitwise operators.
- const bitwiseop = populate([
- "~", "^", "^=", "&", "&=", "|", "|=", "<<", "<<=", ">>", ">>=",
- ">>>", ">>>="
- ]);
- const escapeable = populate([
- "\\", "/", "`", "b", "f", "n", "r", "t"
- ]);
- const opener = {
- // The open and close pairs.
- "(": ")", // paren
- "[": "]", // bracket
- "{": "}", // brace
- "${": "}" // mega
- };
- // The relational operators.
- const relationop = populate([
- "!=", "!==", "==", "===", "<", "<=", ">", ">="
- ]);
- // This is the set of infix operators that require a space on each side.
- const spaceop = populate([
- "!=", "!==", "%", "%=", "&", "&=", "&&", "*", "*=", "+=", "-=", "/",
- "/=", "<", "<=", "<<", "<<=", "=", "==", "===", "=>", ">", ">=",
- ">>", ">>=", ">>>", ">>>=", "^", "^=", "|", "|=", "||"
- ]);
- const standard = [
- // These are the globals that are provided by the language standard.
- "Array", "ArrayBuffer", "Boolean", "DataView", "Date", "decodeURI",
- "decodeURIComponent", "encodeURI", "encodeURIComponent", "Error",
- "EvalError", "Float32Array", "Float64Array", "Generator",
- "GeneratorFunction", "Int8Array", "Int16Array", "Int32Array", "Intl",
- "JSON", "Map", "Math", "Number", "Object", "parseInt", "parseFloat",
- "Promise", "Proxy", "RangeError", "ReferenceError", "Reflect", "RegExp",
- "Set", "String", "Symbol", "SyntaxError", "System", "TypeError",
- "Uint8Array", "Uint8ClampedArray", "Uint16Array", "Uint32Array",
- "URIError", "WeakMap", "WeakSet"
- ];
- const bundle = {
- // The bundle contains the raw text messages that are generated by jslint. It
- // seems that they are all error messages and warnings. There are no "Atta
- // boy!" or "You are so awesome!" messages. There is no positive reinforcement
- // or encouragement. This relentless negativity can undermine self-esteem and
- // wound the inner child. But if you accept it as sound advice rather than as
- // personal criticism, it can make your programs better.
- and: "The '&&' subexpression should be wrapped in parens.",
- bad_assignment_a: "Bad assignment to '{a}'.",
- bad_directive_a: "Bad directive '{a}'.",
- bad_get: "A get function takes no parameters.",
- bad_module_name_a: "Bad module name '{a}'.",
- bad_option_a: "Bad option '{a}'.",
- bad_property_a: "Bad property name '{a}'.",
- bad_set: "A set function takes one parameter.",
- duplicate_a: "Duplicate '{a}'.",
- empty_block: "Empty block.",
- escape_mega: "Unexpected escapement in mega literal.",
- expected_a: "Expected '{a}'.",
- expected_a_at_b_c: "Expected '{a}' at column {b}, not column {c}.",
- expected_a_b: "Expected '{a}' and instead saw '{b}'.",
- expected_a_b_from_c_d: (
- "Expected '{a}' to match '{b}' from line {c} and instead saw '{d}'."
- ),
- expected_a_before_b: "Expected '{a}' before '{b}'.",
- expected_a_next_at_b: "Expected '{a}' at column {b} on the next line.",
- expected_digits_after_a: "Expected digits after '{a}'.",
- expected_four_digits: "Expected four digits after '\\u'.",
- expected_identifier_a: "Expected an identifier and instead saw '{a}'.",
- expected_line_break_a_b: "Expected a line break between '{a}' and '{b}'.",
- expected_regexp_factor_a: "Expected a regexp factor and instead saw '{a}'.",
- expected_space_a_b: "Expected one space between '{a}' and '{b}'.",
- expected_statements_a: "Expected statements before '{a}'.",
- expected_string_a: "Expected a string and instead saw '{a}'.",
- expected_type_string_a: "Expected a type string and instead saw '{a}'.",
- freeze_exports: (
- "Expected 'Object.freeze('. All export values should be frozen."
- ),
- function_in_loop: "Don't make functions within a loop.",
- infix_in: (
- "Unexpected 'in'. Compare with undefined, "
- + "or use the hasOwnProperty method instead."
- ),
- label_a: "'{a}' is a statement label.",
- misplaced_a: "Place '{a}' at the outermost level.",
- misplaced_directive_a: (
- "Place the '/*{a}*/' directive before the first statement."
- ),
- missing_browser: "/*global*/ requires the Assume a browser option.",
- missing_m: "Expected 'm' flag on a multiline regular expression.",
- naked_block: "Naked block.",
- nested_comment: "Nested comment.",
- not_label_a: "'{a}' is not a label.",
- number_isNaN: "Use Number.isNaN function to compare with NaN.",
- out_of_scope_a: "'{a}' is out of scope.",
- redefinition_a_b: "Redefinition of '{a}' from line {b}.",
- required_a_optional_b: (
- "Required parameter '{a}' after optional parameter '{b}'."
- ),
- reserved_a: "Reserved name '{a}'.",
- subscript_a: "['{a}'] is better written in dot notation.",
- todo_comment: "Unexpected TODO comment.",
- too_long: "Line is longer than 80 characters.",
- too_many_digits: "Too many digits.",
- unclosed_comment: "Unclosed comment.",
- unclosed_mega: "Unclosed mega literal.",
- unclosed_string: "Unclosed string.",
- undeclared_a: "Undeclared '{a}'.",
- unexpected_a: "Unexpected '{a}'.",
- unexpected_a_after_b: "Unexpected '{a}' after '{b}'.",
- unexpected_a_before_b: "Unexpected '{a}' before '{b}'.",
- unexpected_at_top_level_a: "Expected '{a}' to be in a function.",
- unexpected_char_a: "Unexpected character '{a}'.",
- unexpected_comment: "Unexpected comment.",
- unexpected_directive_a: "When using modules, don't use directive '/*{a}'.",
- unexpected_expression_a: (
- "Unexpected expression '{a}' in statement position."
- ),
- unexpected_label_a: "Unexpected label '{a}'.",
- unexpected_parens: "Don't wrap function literals in parens.",
- unexpected_space_a_b: "Unexpected space between '{a}' and '{b}'.",
- unexpected_statement_a: (
- "Unexpected statement '{a}' in expression position."
- ),
- unexpected_trailing_space: "Unexpected trailing space.",
- unexpected_typeof_a: (
- "Unexpected 'typeof'. Use '===' to compare directly with {a}."
- ),
- uninitialized_a: "Uninitialized '{a}'.",
- unreachable_a: "Unreachable '{a}'.",
- unregistered_property_a: "Unregistered property name '{a}'.",
- unsafe: "Unsafe character '{a}'.",
- unused_a: "Unused '{a}'.",
- use_double: "Use double quotes, not single quotes.",
- use_open: (
- "Wrap a ternary expression in parens, "
- + "with a line break after the left paren."
- ),
- use_spaces: "Use spaces, not tabs.",
- var_loop: "Don't declare variables in a loop.",
- var_switch: "Don't declare variables in a switch.",
- weird_condition_a: "Weird condition '{a}'.",
- weird_expression_a: "Weird expression '{a}'.",
- weird_loop: "Weird loop.",
- weird_relation_a: "Weird relation '{a}'.",
- wrap_condition: "Wrap the condition in parens.",
- wrap_immediate: (
- "Wrap an immediate function invocation in parentheses to assist "
- + "the reader in understanding that the expression is the result "
- + "of a function, and not the function itself."
- ),
- wrap_parameter: "Wrap the parameter in parens.",
- wrap_regexp: "Wrap this regexp in parens to avoid confusion.",
- wrap_unary: "Wrap the unary expression in parens."
- };
- // Regular expression literals:
- // supplant {variables}
- const rx_supplant = /\{([^{}]*)\}/g;
- // carriage return, carriage return linefeed, or linefeed
- const rx_crlf = /\n|\r\n?/;
- // unsafe characters that are silently deleted by one or more browsers
- const rx_unsafe = /[\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/;
- // identifier
- const rx_identifier = /^([a-zA-Z_$][a-zA-Z0-9_$]*)$/;
- const rx_module = /^[a-zA-Z0-9_$:.@\-\/]+$/;
- const rx_bad_property = /^_|\$|Sync\$|_$/;
- // star slash
- const rx_star_slash = /\*\//;
- // slash star
- const rx_slash_star = /\/\*/;
- // slash star or ending slash
- const rx_slash_star_or_slash = /\/\*|\/$/;
- // uncompleted work comment
- const rx_todo = /\b(?:todo|TO\s?DO|HACK)\b/;
- // tab
- const rx_tab = /\t/g;
- // directive
- const rx_directive = /^(jslint|property|global)\s+(.*)$/;
- const rx_directive_part = /^([a-zA-Z$_][a-zA-Z0-9$_]*)(?::\s*(true|false))?,?\s*(.*)$/;
- // token (sorry it is so long)
- const rx_token = /^((\s+)|([a-zA-Z_$][a-zA-Z0-9_$]*)|[(){}\[\],:;'"~`]|\?\.?|=(?:==?|>)?|\.+|[*\/][*\/=]?|\+[=+]?|-[=\-]?|[\^%]=?|&[&=]?|\|[|=]?|>{1,3}=?|<<?=?|!(?:!|==?)?|(0|[1-9][0-9]*))(.*)$/;
- const rx_digits = /^([0-9]+)(.*)$/;
- const rx_hexs = /^([0-9a-fA-F]+)(.*)$/;
- const rx_octals = /^([0-7]+)(.*)$/;
- const rx_bits = /^([01]+)(.*)$/;
- // mega
- const rx_mega = /[`\\]|\$\{/;
- // JSON number
- const rx_JSON_number = /^-?\d+(?:\.\d*)?(?:e[\-+]?\d+)?$/i;
- // initial cap
- const rx_cap = /^[A-Z]/;
- function is_letter(string) {
- return (
- (string >= "a" && string <= "z\uffff")
- || (string >= "A" && string <= "Z\uffff")
- );
- }
- function supplant(string, object) {
- return string.replace(rx_supplant, function (found, filling) {
- const replacement = object[filling];
- return (
- replacement !== undefined
- ? replacement
- : found
- );
- });
- }
- let anon; // The guessed name for anonymous functions.
- let blockage; // The current block.
- let block_stack; // The stack of blocks.
- let declared_globals; // The object containing the global declarations.
- let directives; // The directive comments.
- let directive_mode; // true if directives are still allowed.
- let early_stop; // true if JSLint cannot finish.
- let exports; // The exported names and values.
- let froms; // The array collecting all import-from strings.
- let fudge; // true if the natural numbers start with 1.
- let functionage; // The current function.
- let functions; // The array containing all of the functions.
- let global; // The global object; the outermost context.
- let json_mode; // true if parsing JSON.
- let lines; // The array containing source lines.
- let mega_mode; // true if currently parsing a megastring literal.
- let module_mode; // true if import or export was used.
- let next_token; // The next token to be examined in the parse.
- let option; // The options parameter.
- let property; // The object containing the tallied property names.
- let shebang; // true if a #! was seen on the first line.
- let stack; // The stack of functions.
- let syntax; // The object containing the parser.
- let token; // The current token being examined in the parse.
- let token_nr; // The number of the next token.
- let tokens; // The array of tokens.
- let tenure; // The predefined property registry.
- let tree; // The abstract parse tree.
- let var_mode; // "var" if using var; "let" if using let.
- let warnings; // The array collecting all generated warnings.
- // Error reportage functions:
- function artifact(the_token) {
- // Return a string representing an artifact.
- if (the_token === undefined) {
- the_token = next_token;
- }
- return (
- (the_token.id === "(string)" || the_token.id === "(number)")
- ? String(the_token.value)
- : the_token.id
- );
- }
- function artifact_line(the_token) {
- // Return the fudged line number of an artifact.
- if (the_token === undefined) {
- the_token = next_token;
- }
- return the_token.line + fudge;
- }
- function artifact_column(the_token) {
- // Return the fudged column number of an artifact.
- if (the_token === undefined) {
- the_token = next_token;
- }
- return the_token.from + fudge;
- }
- function warn_at(code, line, column, a, b, c, d) {
- // Report an error at some line and column of the program. The warning object
- // resembles an exception.
- const warning = { // ~~
- name: "JSLintError",
- column,
- line,
- code
- };
- if (a !== undefined) {
- warning.a = a;
- }
- if (b !== undefined) {
- warning.b = b;
- }
- if (c !== undefined) {
- warning.c = c;
- }
- if (d !== undefined) {
- warning.d = d;
- }
- warning.message = supplant(bundle[code] || code, warning);
- warnings.push(warning);
- return warning;
- }
- function stop_at(code, line, column, a, b, c, d) {
- // Same as warn_at, except that it stops the analysis.
- throw warn_at(code, line, column, a, b, c, d);
- }
- function warn(code, the_token, a, b, c, d) {
- // Same as warn_at, except the warning will be associated with a specific token.
- // If there is already a warning on this token, suppress the new one. It is
- // likely that the first warning will be the most meaningful.
- if (the_token === undefined) {
- the_token = next_token;
- }
- if (the_token.warning === undefined) {
- the_token.warning = warn_at(
- code,
- the_token.line,
- the_token.from,
- a || artifact(the_token),
- b,
- c,
- d
- );
- return the_token.warning;
- }
- }
- function stop(code, the_token, a, b, c, d) {
- // Similar to warn and stop_at. If the token already had a warning, that
- // warning will be replaced with this new one. It is likely that the stopping
- // warning will be the more meaningful.
- if (the_token === undefined) {
- the_token = next_token;
- }
- delete the_token.warning;
- throw warn(code, the_token, a, b, c, d);
- }
- // Tokenize:
- function tokenize(source) {
- // tokenize takes a source and produces from it an array of token objects.
- // JavaScript is notoriously difficult to tokenize because of the horrible
- // interactions between automatic semicolon insertion, regular expression
- // literals, and now megastring literals. JSLint benefits from eliminating
- // automatic semicolon insertion and nested megastring literals, which allows
- // full tokenization to precede parsing.
- // If the source is not an array, then it is split into lines at the
- // carriage return/linefeed.
- lines = (
- Array.isArray(source)
- ? source
- : source.split(rx_crlf)
- );
- tokens = [];
- let char; // a popular character
- let column = 0; // the column number of the next character
- let first; // the first token
- let from; // the starting column number of the token
- let line = -1; // the line number of the next character
- let nr = 0; // the next token number
- let previous = global; // the previous token including comments
- let prior = global; // the previous token excluding comments
- let mega_from; // the starting column of megastring
- let mega_line; // the starting line of megastring
- let regexp_seen; // regular expression literal seen on this line
- let snippet; // a piece of string
- let source_line = ""; // the remaining line source string
- let whole_line = ""; // the whole line source string
- if (lines[0].startsWith("#!")) {
- line = 0;
- shebang = true;
- }
- function next_line() {
- // Put the next line of source in source_line. If the line contains tabs,
- // replace them with spaces and give a warning. Also warn if the line contains
- // unsafe characters or is too damn long.
- let at;
- if (
- !option.long
- && whole_line.length > 80
- && !json_mode
- && first
- && !regexp_seen
- ) {
- warn_at("too_long", line, 80);
- }
- column = 0;
- line += 1;
- regexp_seen = false;
- source_line = lines[line];
- whole_line = source_line || "";
- if (source_line !== undefined) {
- at = source_line.search(rx_tab);
- if (at >= 0) {
- if (!option.white) {
- warn_at("use_spaces", line, at + 1);
- }
- source_line = source_line.replace(rx_tab, " ");
- }
- at = source_line.search(rx_unsafe);
- if (at >= 0) {
- warn_at(
- "unsafe",
- line,
- column + at,
- "U+" + source_line.charCodeAt(at).toString(16)
- );
- }
- if (!option.white && source_line.slice(-1) === " ") {
- warn_at(
- "unexpected_trailing_space",
- line,
- source_line.length - 1
- );
- }
- }
- return source_line;
- }
- // Most tokens, including the identifiers, operators, and punctuators, can be
- // found with a regular expression. Regular expressions cannot correctly match
- // regular expression literals, so we will match those the hard way. String
- // literals and number literals can be matched by regular expressions, but they
- // don't provide good warnings. The functions snip, next_char, prev_char,
- // some_digits, and escape help in the parsing of literals.
- function snip() {
- // Remove the last character from snippet.
- snippet = snippet.slice(0, -1);
- }
- function next_char(match) {
- // Get the next character from the source line. Remove it from the source_line,
- // and append it to the snippet. Optionally check that the previous character
- // matched an expected value.
- if (match !== undefined && char !== match) {
- return stop_at(
- (
- char === ""
- ? "expected_a"
- : "expected_a_b"
- ),
- line,
- column - 1,
- match,
- char
- );
- }
- if (source_line) {
- char = source_line[0];
- source_line = source_line.slice(1);
- snippet += char;
- } else {
- char = "";
- snippet += " ";
- }
- column += 1;
- return char;
- }
- function back_char() {
- // Back up one character by moving a character from the end of the snippet to
- // the front of the source_line.
- if (snippet) {
- char = snippet.slice(-1);
- source_line = char + source_line;
- column -= 1;
- snip();
- } else {
- char = "";
- }
- return char;
- }
- function some_digits(rx, quiet) {
- const result = source_line.match(rx);
- if (result) {
- char = result[1];
- column += char.length;
- source_line = result[2];
- snippet += char;
- } else {
- char = "";
- if (!quiet) {
- warn_at(
- "expected_digits_after_a",
- line,
- column,
- snippet
- );
- }
- }
- return char.length;
- }
- function escape(extra) {
- next_char("\\");
- if (escapeable[char] === true) {
- return next_char();
- }
- if (char === "") {
- return stop_at("unclosed_string", line, column);
- }
- if (char === "u") {
- if (next_char("u") === "{") {
- if (json_mode) {
- warn_at("unexpected_a", line, column - 1, char);
- }
- if (some_digits(rx_hexs) > 5) {
- warn_at("too_many_digits", line, column - 1);
- }
- if (next_char() !== "}") {
- stop_at("expected_a_before_b", line, column, "}", char);
- }
- return next_char();
- }
- back_char();
- if (some_digits(rx_hexs, true) < 4) {
- warn_at("expected_four_digits", line, column - 1);
- }
- return;
- }
- if (extra && extra.indexOf(char) >= 0) {
- return next_char();
- }
- warn_at("unexpected_a_before_b", line, column - 2, "\\", char);
- }
- function make(id, value, identifier) {
- // Make the token object and append it to the tokens list.
- const the_token = {
- from,
- id,
- identifier: Boolean(identifier),
- line,
- nr,
- thru: column
- };
- tokens[nr] = the_token;
- nr += 1;
- // Directives must appear before the first statement.
- if (id !== "(comment)" && id !== ";") {
- directive_mode = false;
- }
- // If the token is to have a value, give it one.
- if (value !== undefined) {
- the_token.value = value;
- }
- // If this token is an identifier that touches a preceding number, or
- // a "/", comment, or regular expression literal that touches a preceding
- // comment or regular expression literal, then give a missing space warning.
- // This warning is not suppressed by option.white.
- if (
- previous.line === line
- && previous.thru === from
- && (id === "(comment)" || id === "(regexp)" || id === "/")
- && (previous.id === "(comment)" || previous.id === "(regexp)")
- ) {
- warn(
- "expected_space_a_b",
- the_token,
- artifact(previous),
- artifact(the_token)
- );
- }
- if (previous.id === "." && id === "(number)") {
- warn("expected_a_before_b", previous, "0", ".");
- }
- if (prior.id === "." && the_token.identifier) {
- the_token.dot = true;
- }
- // The previous token is used to detect adjacency problems.
- previous = the_token;
- // The prior token is a previous token that was not a comment. The prior token
- // is used to disambiguate "/", which can mean division or regular expression
- // literal.
- if (previous.id !== "(comment)") {
- prior = previous;
- }
- return the_token;
- }
- function parse_directive(the_comment, body) {
- // JSLint recognizes three directives that can be encoded in comments. This
- // function processes one item, and calls itself recursively to process the
- // next one.
- const result = body.match(rx_directive_part);
- if (result) {
- let allowed;
- const name = result[1];
- const value = result[2];
- if (the_comment.directive === "jslint") {
- allowed = allowed_option[name];
- if (
- typeof allowed === "boolean"
- || typeof allowed === "object"
- ) {
- if (
- value === ""
- || value === "true"
- || value === undefined
- ) {
- option[name] = true;
- if (Array.isArray(allowed)) {
- populate(allowed, declared_globals, false);
- }
- } else if (value === "false") {
- option[name] = false;
- } else {
- warn("bad_option_a", the_comment, name + ":" + value);
- }
- } else {
- warn("bad_option_a", the_comment, name);
- }
- } else if (the_comment.directive === "property") {
- if (tenure === undefined) {
- tenure = empty();
- }
- tenure[name] = true;
- } else if (the_comment.directive === "global") {
- if (value) {
- warn("bad_option_a", the_comment, name + ":" + value);
- }
- declared_globals[name] = false;
- module_mode = the_comment;
- }
- return parse_directive(the_comment, result[3]);
- }
- if (body) {
- return stop("bad_directive_a", the_comment, body);
- }
- }
- function comment(snippet) {
- // Make a comment object. Comments are not allowed in JSON text. Comments can
- // include directives and notices of incompletion.
- const the_comment = make("(comment)", snippet);
- if (Array.isArray(snippet)) {
- snippet = snippet.join(" ");
- }
- if (!option.devel && rx_todo.test(snippet)) {
- warn("todo_comment", the_comment);
- }
- const result = snippet.match(rx_directive);
- if (result) {
- if (!directive_mode) {
- warn_at("misplaced_directive_a", line, from, result[1]);
- } else {
- the_comment.directive = result[1];
- parse_directive(the_comment, result[2]);
- }
- directives.push(the_comment);
- }
- return the_comment;
- }
- function regexp() {
- // Parse a regular expression literal.
- let multi_mode = false;
- let result;
- let value;
- regexp_seen = true;
- function quantifier() {
- // Match an optional quantifier.
- if (char === "?" || char === "*" || char === "+") {
- next_char();
- } else if (char === "{") {
- if (some_digits(rx_digits, true) === 0) {
- warn_at("expected_a", line, column, "0");
- }
- if (next_char() === ",") {
- some_digits(rx_digits, true);
- next_char();
- }
- next_char("}");
- } else {
- return;
- }
- if (char === "?") {
- next_char("?");
- }
- }
- function subklass() {
- // Match a character in a character class.
- if (char === "\\") {
- escape("BbDdSsWw-[]^");
- return true;
- }
- if (
- char === ""
- || char === "["
- || char === "]"
- || char === "/"
- || char === "^"
- || char === "-"
- ) {
- return false;
- }
- if (char === " ") {
- warn_at("expected_a_b", line, column, "\\u0020", " ");
- } else if (char === "`" && mega_mode) {
- warn_at("unexpected_a", line, column, "`");
- }
- next_char();
- return true;
- }
- function ranges() {
- // Match a range of subclasses.
- if (subklass()) {
- if (char === "-") {
- next_char("-");
- if (!subklass()) {
- return stop_at(
- "unexpected_a",
- line,
- column - 1,
- "-"
- );
- }
- }
- return ranges();
- }
- }
- function klass() {
- // Match a class.
- next_char("[");
- if (char === "^") {
- next_char("^");
- }
- (function classy() {
- ranges();
- if (char !== "]" && char !== "") {
- warn_at(
- "expected_a_before_b",
- line,
- column,
- "\\",
- char
- );
- next_char();
- return classy();
- }
- }());
- next_char("]");
- }
- function choice() {
- function group() {
- // Match a group that starts with left paren.
- next_char("(");
- if (char === "?") {
- next_char("?");
- if (char === "=" || char === "!") {
- next_char();
- } else {
- next_char(":");
- }
- } else if (char === ":") {
- warn_at("expected_a_before_b", line, column, "?", ":");
- }
- choice();
- next_char(")");
- }
- function factor() {
- if (
- char === ""
- || char === "/"
- || char === "]"
- || char === ")"
- ) {
- return false;
- }
- if (char === "(") {
- group();
- return true;
- }
- if (char === "[") {
- klass();
- return true;
- }
- if (char === "\\") {
- escape("BbDdSsWw^${}[]():=!.|*+?");
- return true;
- }
- if (
- char === "?"
- || char === "+"
- || char === "*"
- || char === "}"
- || char === "{"
- ) {
- warn_at(
- "expected_a_before_b",
- line,
- column - 1,
- "\\",
- char
- );
- } else if (char === "`") {
- if (mega_mode) {
- warn_at("unexpected_a", line, column - 1, "`");
- }
- } else if (char === " ") {
- warn_at(
- "expected_a_b",
- line,
- column - 1,
- "\\s",
- " "
- );
- } else if (char === "$") {
- if (source_line[0] !== "/") {
- multi_mode = true;
- }
- } else if (char === "^") {
- if (snippet !== "^") {
- multi_mode = true;
- }
- }
- next_char();
- return true;
- }
- function sequence(follow) {
- if (factor()) {
- quantifier();
- return sequence(true);
- }
- if (!follow) {
- warn_at("expected_regexp_factor_a", line, column, char);
- }
- }
- // Match a choice (a sequence that can be followed by | and another choice).
- sequence();
- if (char === "|") {
- next_char("|");
- return choice();
- }
- }
- // Scan the regexp literal. Give a warning if the first character is = because
- // /= looks like a division assignment operator.
- snippet = "";
- next_char();
- if (char === "=") {
- warn_at("expected_a_before_b", line, column, "\\", "=");
- }
- choice();
- // Make sure there is a closing slash.
- snip();
- value = snippet;
- next_char("/");
- // Process dangling flag letters.
- const allowed = {
- g: true,
- i: true,
- m: true,
- u: true,
- y: true
- };
- const flag = empty();
- (function make_flag() {
- if (is_letter(char)) {
- if (allowed[char] !== true) {
- warn_at("unexpected_a", line, column, char);
- }
- allowed[char] = false;
- flag[char] = true;
- next_char();
- return make_flag();
- }
- }());
- back_char();
- if (char === "/" || char === "*") {
- return stop_at("unexpected_a", line, from, char);
- }
- result = make("(regexp)", char);
- result.flag = flag;
- result.value = value;
- if (multi_mode && !flag.m) {
- warn_at("missing_m", line, column);
- }
- return result;
- }
- function string(quote) {
- // Make a string token.
- let the_token;
- snippet = "";
- next_char();
- return (function next() {
- if (char === quote) {
- snip();
- the_token = make("(string)", snippet);
- the_token.quote = quote;
- return the_token;
- }
- if (char === "") {
- return stop_at("unclosed_string", line, column);
- }
- if (char === "\\") {
- escape(quote);
- } else if (char === "`") {
- if (mega_mode) {
- warn_at("unexpected_a", line, column, "`");
- }
- next_char("`");
- } else {
- next_char();
- }
- return next();
- }());
- }
- function frack() {
- if (char === ".") {
- some_digits(rx_digits);
- next_char();
- }
- if (char === "E" || char === "e") {
- next_char();
- if (char !== "+" && char !== "-") {
- back_char();
- }
- some_digits(rx_digits);
- next_char();
- }
- }
- function number() {
- if (snippet === "0") {
- next_char();
- if (char === ".") {
- frack();
- } else if (char === "b") {
- some_digits(rx_bits);
- next_char();
- } else if (char === "o") {
- some_digits(rx_octals);
- next_char();
- } else if (char === "x") {
- some_digits(rx_hexs);
- next_char();
- }
- } else {
- next_char();
- frack();
- }
- // If the next character after a number is a digit or letter, then something
- // unexpected is going on.
- if (
- (char >= "0" && char <= "9")
- || (char >= "a" && char <= "z")
- || (char >= "A" && char <= "Z")
- ) {
- return stop_at(
- "unexpected_a_after_b",
- line,
- column - 1,
- snippet.slice(-1),
- snippet.slice(0, -1)
- );
- }
- back_char();
- return make("(number)", snippet);
- }
- function lex() {
- let array;
- let i = 0;
- let j = 0;
- let last;
- let result;
- let the_token;
- // This should properly be a tail recursive function, but sadly, conformant
- // implementations of ES6 are still rare. This is the ideal code:
- // if (!source_line) {
- // source_line = next_line();
- // from = 0;
- // return (
- // source_line === undefined
- // ? (
- // mega_mode
- // ? stop_at("unclosed_mega", mega_line, mega_from)
- // : make("(end)")
- // )
- // : lex()
- // );
- // }
- // Unfortunately, incompetent JavaScript engines will sometimes fail to execute
- // it correctly. So for now, we do it the old fashioned way.
- while (!source_line) {
- source_line = next_line();
- from = 0;
- if (source_line === undefined) {
- return (
- mega_mode
- ? stop_at("unclosed_mega", mega_line, mega_from)
- : make("(end)")
- );
- }
- }
- from = column;
- result = source_line.match(rx_token);
- // result[1] token
- // result[2] whitespace
- // result[3] identifier
- // result[4] number
- // result[5] rest
- if (!result) {
- return stop_at(
- "unexpected_char_a",
- line,
- column,
- source_line[0]
- );
- }
- snippet = result[1];
- column += snippet.length;
- source_line = result[5];
- // Whitespace was matched. Call lex again to get more.
- if (result[2]) {
- return lex();
- }
- // The token is an identifier.
- if (result[3]) {
- return make(snippet, undefined, true);
- }
- // The token is a number.
- if (result[4]) {
- return number(snippet);
- }
- // The token is a string.
- if (snippet === "\"") {
- return string(snippet);
- }
- if (snippet === "'") {
- if (!option.single) {
- warn_at("use_double", line, column);
- }
- return string(snippet);
- }
- // The token is a megastring. We don't allow any kind of mega nesting.
- if (snippet === "`") {
- if (mega_mode) {
- return stop_at("expected_a_b", line, column, "}", "`");
- }
- snippet = "";
- mega_from = from;
- mega_line = line;
- mega_mode = true;
- // Parsing a mega literal is tricky. First make a ` token.
- make("`");
- from += 1;
- // Then loop, building up a string, possibly from many lines, until seeing
- // the end of file, a closing `, or a ${ indicting an expression within the
- // string.
- (function part() {
- const at = source_line.search(rx_mega);
- // If neither ` nor ${ is seen, then the whole line joins the snippet.
- if (at < 0) {
- snippet += source_line + "\n";
- return (
- next_line() === undefined
- ? stop_at("unclosed_mega", mega_line, mega_from)
- : part()
- );
- }
- // if either ` or ${ was found, then the preceding joins the snippet to become
- // a string token.
- snippet += source_line.slice(0, at);
- column += at;
- source_line = source_line.slice(at);
- if (source_line[0] === "\\") {
- stop_at("escape_mega", line, at);
- }
- make("(string)", snippet).quote = "`";
- snippet = "";
- // If ${, then make tokens that will become part of an expression until
- // a } token is made.
- if (source_line[0] === "$") {
- column += 2;
- make("${");
- source_line = source_line.slice(2);
- (function expr() {
- const id = lex().id;
- if (id === "{") {
- return stop_at(
- "expected_a_b",
- line,
- column,
- "}",
- "{"
- );
- }
- if (id !== "}") {
- return expr();
- }
- }());
- return part();
- }
- }());
- source_line = source_line.slice(1);
- column += 1;
- mega_mode = false;
- return make("`");
- }
- // The token is a // comment.
- if (snippet === "//") {
- snippet = source_line;
- source_line = "";
- the_token = comment(snippet);
- if (mega_mode) {
- warn("unexpected_comment", the_token, "`");
- }
- return the_token;
- }
- // The token is a /* comment.
- if (snippet === "/*") {
- array = [];
- if (source_line[0] === "/") {
- warn_at("unexpected_a", line, column + i, "/");
- }
- (function next() {
- if (source_line > "") {
- i = source_line.search(rx_star_slash);
- if (i >= 0) {
- return;
- }
- j = source_line.search(rx_slash_star);
- if (j >= 0) {
- warn_at("nested_comment", line, column + j);
- }
- }
- array.push(source_line);
- source_line = next_line();
- if (source_line === undefined) {
- return stop_at("unclosed_comment", line, column);
- }
- return next();
- }());
- snippet = source_line.slice(0, i);
- j = snippet.search(rx_slash_star_or_slash);
- if (j >= 0) {
- warn_at("nested_comment", line, column + j);
- }
- array.push(snippet);
- column += i + 2;
- source_line = source_line.slice(i + 2);
- return comment(array);
- }
- // The token is a slash.
- if (snippet === "/") {
- // The / can be a division operator or the beginning of a regular expression
- // literal. It is not possible to know which without doing a complete parse.
- // We want to complete the tokenization before we begin to parse, so we will
- // estimate. This estimator can fail in some cases. For example, it cannot
- // know if "}" is ending a block or ending an object literal, so it can
- // behave incorrectly in that case; it is not meaningful to divide an
- // object, so it is likely that we can get away with it. We avoided the worst
- // cases by eliminating automatic semicolon insertion.
- if (prior.identifier) {…