| /** |
| * @author Toru Nagashima |
| * @copyright 2015 Toru Nagashima. All rights reserved. |
| * See LICENSE file in root directory for full license. |
| */ |
| "use strict" |
| |
| //------------------------------------------------------------------------------ |
| // Requirements |
| //------------------------------------------------------------------------------ |
| |
| const semver = require("semver") |
| const features = require("../util/features") |
| const getPackageJson = require("../util/get-package-json") |
| const getValueIfString = require("../util/get-value-if-string") |
| |
| //------------------------------------------------------------------------------ |
| // Helpers |
| //------------------------------------------------------------------------------ |
| |
| const VERSION_MAP = new Map([ |
| [0.1, "0.10.0"], |
| [0.12, "0.12.0"], |
| [4, "4.0.0"], |
| [5, "5.0.0"], |
| [6, "6.0.0"], |
| [7, "7.0.0"], |
| [7.6, "7.6.0"], |
| [8, "8.0.0"], |
| ]) |
| const VERSION_SCHEMA = { |
| anyOf: [ |
| {enum: Array.from(VERSION_MAP.keys())}, |
| { |
| type: "string", |
| pattern: "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$", |
| }, |
| ], |
| } |
| const DEFAULT_VERSION = "4.0.0" |
| const MINIMUM_VERSION = "0.0.0" |
| const OPTIONS = Object.keys(features) |
| const FUNC_TYPE = /^(?:Arrow)?Function(?:Declaration|Expression)$/ |
| const CLASS_TYPE = /^Class(?:Declaration|Expression)$/ |
| const DESTRUCTURING_PARENT_TYPE = /^(?:Function(?:Declaration|Expression)|ArrowFunctionExpression|AssignmentExpression|VariableDeclarator)$/ |
| const TOPLEVEL_SCOPE_TYPE = /^(?:global|function|module)$/ |
| const BINARY_NUMBER = /^0[bB]/ |
| const OCTAL_NUMBER = /^0[oO]/ |
| const UNICODE_ESC = /(\\+)u\{[0-9a-fA-F]+?\}/g |
| const GET_OR_SET = /^(?:g|s)et$/ |
| const NEW_BUILTIN_TYPES = [ |
| "Int8Array", "Uint8Array", "Uint8ClampedArray", "Int16Array", "Uint16Array", |
| "Int32Array", "Uint32Array", "Float32Array", "Float64Array", "DataView", |
| "Map", "Set", "WeakMap", "WeakSet", "Proxy", "Reflect", "Promise", "Symbol", |
| "SharedArrayBuffer", "Atomics", |
| ] |
| const SUBCLASSING_TEST_TARGETS = [ |
| "Array", "RegExp", "Function", "Promise", "Boolean", "Number", "String", |
| "Map", "Set", |
| ] |
| const PROPERTY_TEST_TARGETS = { |
| Object: [ |
| "assign", "is", "getOwnPropertySymbols", "setPrototypeOf", "values", |
| "entries", "getOwnPropertyDescriptors", |
| ], |
| String: ["raw", "fromCodePoint"], |
| Array: ["from", "of"], |
| Number: [ |
| "isFinite", "isInteger", "isSafeInteger", "isNaN", "EPSILON", |
| "MIN_SAFE_INTEGER", "MAX_SAFE_INTEGER", |
| ], |
| Math: [ |
| "clz32", "imul", "sign", "log10", "log2", "log1p", "expm1", "cosh", |
| "sinh", "tanh", "acosh", "asinh", "atanh", "trunc", "fround", "cbrt", |
| "hypot", |
| ], |
| Symbol: [ |
| "hasInstance", "isConcatSpreadablec", "iterator", "species", "replace", |
| "search", "split", "match", "toPrimitive", "toStringTag", "unscopables", |
| ], |
| Atomics: [ |
| "add", "and", "compareExchange", "exchange", "wait", "wake", |
| "isLockFree", "load", "or", "store", "sub", "xor", |
| ], |
| } |
| |
| /** |
| * Get the smaller value of the given 2 semvers. |
| * @param {string|null} a A semver to compare. |
| * @param {string} b Another semver to compare. |
| * @returns {string} The smaller value. |
| */ |
| function min(a, b) { |
| return ( |
| a == null ? b : |
| semver.lt(a, b) ? a : |
| /* otherwise */ b |
| ) |
| } |
| |
| /** |
| * Get the larger value of the given 2 semvers. |
| * @param {string|null} a A semver to compare. |
| * @param {string} b Another semver to compare. |
| * @returns {string} The larger value. |
| */ |
| function max(a, b) { |
| return ( |
| a == null ? b : |
| semver.gt(a, b) ? a : |
| /* otherwise */ b |
| ) |
| } |
| |
| /** |
| * Gets default version configuration of this rule. |
| * |
| * This finds and reads 'package.json' file, then parses 'engines.node' field. |
| * If it's nothing, this returns '4'. |
| * |
| * @param {string} filename - The file name of the current linting file. |
| * @returns {string} The default version configuration. |
| */ |
| function getDefaultVersion(filename) { |
| const info = getPackageJson(filename) |
| const nodeVersion = info && info.engines && info.engines.node |
| |
| try { |
| const range = new semver.Range(nodeVersion) |
| const comparators = Array.prototype.concat.apply([], range.set) |
| const ret = comparators.reduce( |
| (lu, comparator) => { |
| const op = comparator.operator |
| const v = comparator.semver |
| |
| if (op === "" || op === ">=") { |
| lu.lower = min(lu.lower, `${v.major}.${v.minor}.${v.patch}`) |
| } |
| else if (op === ">") { |
| lu.lower = min(lu.lower, `${v.major}.${v.minor}.${v.patch + 1}`) |
| } |
| |
| if (op === "" || op === "<=" || op === "<") { |
| lu.upper = max(lu.upper, `${v.major}.${v.minor}.${v.patch}`) |
| } |
| |
| return lu |
| }, |
| {lower: null, upper: null} |
| ) |
| |
| if (ret.lower == null && ret.upper != null) { |
| return MINIMUM_VERSION |
| } |
| return ret.lower || DEFAULT_VERSION |
| } |
| catch (_err) { |
| return DEFAULT_VERSION |
| } |
| } |
| |
| /** |
| * Gets values of the `ignores` option. |
| * |
| * @returns {string[]} Values of the `ignores` option. |
| */ |
| function getIgnoresEnum() { |
| return Object.keys(OPTIONS.reduce( |
| (retv, key) => { |
| for (const alias of features[key].alias) { |
| retv[alias] = true |
| } |
| retv[key] = true |
| return retv |
| }, |
| Object.create(null) |
| )) |
| } |
| |
| /** |
| * Checks whether a given key should be ignored or not. |
| * |
| * @param {string} key - A key to check. |
| * @param {string[]} ignores - An array of keys and aliases to be ignored. |
| * @returns {boolean} `true` if the key should be ignored. |
| */ |
| function isIgnored(key, ignores) { |
| return ( |
| ignores.indexOf(key) !== -1 || |
| features[key].alias.some(alias => ignores.indexOf(alias) !== -1) |
| ) |
| } |
| |
| /** |
| * Parses the options. |
| * |
| * @param {number|string|object|undefined} options - An option object to parse. |
| * @param {number} defaultVersion - The default version to use if the version option was omitted. |
| * @returns {object} Parsed value. |
| */ |
| function parseOptions(options, defaultVersion) { |
| let version = defaultVersion |
| let ignores = [] |
| |
| if (typeof options === "number") { |
| version = VERSION_MAP.get(options) |
| } |
| else if (typeof options === "string") { |
| version = options |
| } |
| else if (typeof options === "object") { |
| version = (typeof options.version === "number") |
| ? VERSION_MAP.get(options.version) |
| : options.version || defaultVersion |
| ignores = options.ignores || [] |
| } |
| |
| return Object.freeze({ |
| version, |
| features: Object.freeze(OPTIONS.reduce( |
| (retv, key) => { |
| const feature = features[key] |
| |
| if (isIgnored(key, ignores)) { |
| retv[key] = Object.freeze({ |
| name: feature.name, |
| singular: Boolean(feature.singular), |
| supported: true, |
| supportedInStrict: true, |
| }) |
| } |
| else if (typeof feature.node === "string") { |
| retv[key] = Object.freeze({ |
| name: feature.name, |
| singular: Boolean(feature.singular), |
| supported: semver.gte(version, feature.node), |
| supportedInStrict: semver.gte(version, feature.node), |
| }) |
| } |
| else { |
| retv[key] = Object.freeze({ |
| name: feature.name, |
| singular: Boolean(feature.singular), |
| supported: |
| feature.node != null && |
| feature.node.sloppy != null && |
| semver.gte(version, feature.node.sloppy), |
| supportedInStrict: |
| feature.node != null && |
| feature.node.strict != null && |
| semver.gte(version, feature.node.strict), |
| }) |
| } |
| |
| return retv |
| }, |
| Object.create(null) |
| )), |
| }) |
| } |
| |
| /** |
| * Checks whether or not the current configure has a special lexical environment. |
| * If it's modules or globalReturn then it has a special lexical environment. |
| * |
| * @param {RuleContext} context - A context to check. |
| * @returns {boolean} `true` if the current configure is modules or globalReturn. |
| */ |
| function checkSpecialLexicalEnvironment(context) { |
| const parserOptions = context.parserOptions |
| const ecmaFeatures = parserOptions.ecmaFeatures |
| return Boolean( |
| parserOptions.sourceType === "module" || |
| (ecmaFeatures && ecmaFeatures.globalReturn) |
| ) |
| } |
| |
| /** |
| * Gets the name of a given node. |
| * |
| * @param {ASTNode} node - An Identifier node to get. |
| * @returns {string} The name of the node. |
| */ |
| function getIdentifierName(node) { |
| return node.name |
| } |
| |
| /** |
| * Checks whether the given string has `\u{90ABCDEF}`-like escapes. |
| * |
| * @param {string} raw - The string to check. |
| * @returns {boolean} `true` if the string has Unicode code point escapes. |
| */ |
| function hasUnicodeCodePointEscape(raw) { |
| let match = null |
| |
| UNICODE_ESC.lastIndex = 0 |
| while ((match = UNICODE_ESC.exec(raw)) != null) { |
| if (match[1].length % 2 === 1) { |
| return true |
| } |
| } |
| |
| return false |
| } |
| |
| /** |
| * The definition of this rule. |
| * |
| * @param {RuleContext} context - The rule context to check. |
| * @returns {object} The definition of this rule. |
| */ |
| function create(context) { |
| const sourceCode = context.getSourceCode() |
| const supportInfo = parseOptions( |
| context.options[0], |
| getDefaultVersion(context.getFilename()) |
| ) |
| const hasSpecialLexicalEnvironment = checkSpecialLexicalEnvironment(context) |
| |
| /** |
| * Gets the references of the specified global variables. |
| * |
| * @param {string[]} names - Variable names to get. |
| * @returns {void} |
| */ |
| function* getReferences(names) { |
| const globalScope = context.getScope() |
| |
| for (const name of names) { |
| const variable = globalScope.set.get(name) |
| |
| if (variable && variable.defs.length === 0) { |
| yield* variable.references |
| } |
| } |
| } |
| |
| /** |
| * Checks whether or not the current scope is strict mode. |
| * |
| * @returns {boolean} |
| * `true` if the current scope is strict mode. Otherwise `false`. |
| */ |
| function isStrict() { |
| let scope = context.getScope() |
| if (scope.type === "global" && hasSpecialLexicalEnvironment) { |
| scope = scope.childScopes[0] |
| } |
| return scope.isStrict |
| } |
| |
| /** |
| * Checks whether the given function has trailing commas or not. |
| * |
| * @param {ASTNode} node - The function node to check. |
| * @returns {boolean} `true` if the function has trailing commas. |
| */ |
| function hasTrailingCommaForFunction(node) { |
| const length = node.params.length |
| |
| return ( |
| length >= 1 && |
| sourceCode.getTokenAfter(node.params[length - 1]).value === "," |
| ) |
| } |
| |
| /** |
| * Checks whether the given call expression has trailing commas or not. |
| * |
| * @param {ASTNode} node - The call expression node to check. |
| * @returns {boolean} `true` if the call expression has trailing commas. |
| */ |
| function hasTrailingCommaForCall(node) { |
| return ( |
| node.arguments.length >= 1 && |
| sourceCode.getLastToken(node, 1).value === "," |
| ) |
| } |
| |
| /** |
| * Checks whether the given class extends from null or not. |
| * |
| * @param {ASTNode} node - The class node to check. |
| * @returns {boolean} `true` if the class extends from null. |
| */ |
| function extendsNull(node) { |
| return ( |
| node.superClass != null && |
| node.superClass.type === "Literal" && |
| node.superClass.value === null |
| ) |
| } |
| |
| /** |
| * Reports a given node if the specified feature is not supported. |
| * |
| * @param {ASTNode} node - A node to be reported. |
| * @param {string} key - A feature name to report. |
| * @returns {void} |
| */ |
| function report(node, key) { |
| const version = supportInfo.version |
| const feature = supportInfo.features[key] |
| if (feature.supported) { |
| return |
| } |
| |
| if (!feature.supportedInStrict) { |
| context.report({ |
| node, |
| message: "{{feature}} {{be}} not supported yet on Node {{version}}.", |
| data: { |
| feature: feature.name, |
| be: feature.singular ? "is" : "are", |
| version, |
| }, |
| }) |
| } |
| else if (!isStrict()) { |
| context.report({ |
| node, |
| message: "{{feature}} {{be}} not supported yet on Node {{version}}.", |
| data: { |
| feature: `${feature.name} in non-strict mode`, |
| be: feature.singular ? "is" : "are", |
| version, |
| }, |
| }) |
| } |
| } |
| |
| return { |
| //---------------------------------------------------------------------- |
| // Program |
| //---------------------------------------------------------------------- |
| |
| //eslint-disable-next-line complexity |
| "Program:exit"() { |
| // Check new global variables. |
| for (const name of NEW_BUILTIN_TYPES) { |
| for (const reference of getReferences([name])) { |
| // Ignore if it's using new static methods. |
| const node = reference.identifier |
| const parentNode = node.parent |
| const properties = PROPERTY_TEST_TARGETS[name] |
| if (properties && parentNode.type === "MemberExpression") { |
| const propertyName = (parentNode.computed ? getValueIfString : getIdentifierName)(parentNode.property) |
| if (properties.indexOf(propertyName) !== -1) { |
| continue |
| } |
| } |
| |
| report(reference.identifier, name) |
| } |
| } |
| |
| // Check static methods. |
| for (const reference of getReferences(Object.keys(PROPERTY_TEST_TARGETS))) { |
| const node = reference.identifier |
| const parentNode = node.parent |
| if (parentNode.type !== "MemberExpression" || |
| parentNode.object !== node |
| ) { |
| continue |
| } |
| |
| const objectName = node.name |
| const properties = PROPERTY_TEST_TARGETS[objectName] |
| const propertyName = (parentNode.computed ? getValueIfString : getIdentifierName)(parentNode.property) |
| if (propertyName && properties.indexOf(propertyName) !== -1) { |
| report(parentNode, `${objectName}.${propertyName}`) |
| } |
| } |
| |
| // Check subclassing |
| for (const reference of getReferences(SUBCLASSING_TEST_TARGETS)) { |
| const node = reference.identifier |
| const parentNode = node.parent |
| if (CLASS_TYPE.test(parentNode.type) && |
| parentNode.superClass === node |
| ) { |
| report(node, `extends${node.name}`) |
| } |
| } |
| }, |
| |
| //---------------------------------------------------------------------- |
| // Functions |
| //---------------------------------------------------------------------- |
| |
| "ArrowFunctionExpression"(node) { |
| report(node, "arrowFunctions") |
| if (node.async) { |
| report(node, "asyncAwait") |
| } |
| if (hasTrailingCommaForFunction(node)) { |
| report(node, "trailingCommasInFunctions") |
| } |
| }, |
| |
| "AssignmentPattern"(node) { |
| if (FUNC_TYPE.test(node.parent.type)) { |
| report(node, "defaultParameters") |
| } |
| }, |
| |
| "FunctionDeclaration"(node) { |
| const scope = context.getScope().upper |
| if (!TOPLEVEL_SCOPE_TYPE.test(scope.type)) { |
| report(node, "blockScopedFunctions") |
| } |
| if (node.generator) { |
| report(node, "generatorFunctions") |
| } |
| if (node.async) { |
| report(node, "asyncAwait") |
| } |
| if (hasTrailingCommaForFunction(node)) { |
| report(node, "trailingCommasInFunctions") |
| } |
| }, |
| |
| "FunctionExpression"(node) { |
| if (node.generator) { |
| report(node, "generatorFunctions") |
| } |
| if (node.async) { |
| report(node, "asyncAwait") |
| } |
| if (hasTrailingCommaForFunction(node)) { |
| report(node, "trailingCommasInFunctions") |
| } |
| }, |
| |
| "MetaProperty"(node) { |
| const meta = node.meta.name || node.meta |
| const property = node.property.name || node.property |
| if (meta === "new" && property === "target") { |
| report(node, "new.target") |
| } |
| }, |
| |
| "RestElement"(node) { |
| if (FUNC_TYPE.test(node.parent.type)) { |
| report(node, "restParameters") |
| } |
| }, |
| |
| //---------------------------------------------------------------------- |
| // Classes |
| //---------------------------------------------------------------------- |
| |
| "ClassDeclaration"(node) { |
| report(node, "classes") |
| |
| if (extendsNull(node)) { |
| report(node, "extendsNull") |
| } |
| }, |
| |
| "ClassExpression"(node) { |
| report(node, "classes") |
| |
| if (extendsNull(node)) { |
| report(node, "extendsNull") |
| } |
| }, |
| |
| //---------------------------------------------------------------------- |
| // Statements |
| //---------------------------------------------------------------------- |
| |
| "ForOfStatement"(node) { |
| report(node, "forOf") |
| }, |
| |
| "VariableDeclaration"(node) { |
| if (node.kind === "const") { |
| report(node, "const") |
| } |
| else if (node.kind === "let") { |
| report(node, "let") |
| } |
| }, |
| |
| //---------------------------------------------------------------------- |
| // Expressions |
| //---------------------------------------------------------------------- |
| |
| "ArrayPattern"(node) { |
| if (DESTRUCTURING_PARENT_TYPE.test(node.parent.type)) { |
| report(node, "destructuring") |
| } |
| }, |
| |
| "AssignmentExpression"(node) { |
| if (node.operator === "**=") { |
| report(node, "exponentialOperators") |
| } |
| }, |
| |
| "AwaitExpression"(node) { |
| report(node, "asyncAwait") |
| }, |
| |
| "BinaryExpression"(node) { |
| if (node.operator === "**") { |
| report(node, "exponentialOperators") |
| } |
| }, |
| |
| "CallExpression"(node) { |
| if (hasTrailingCommaForCall(node)) { |
| report(node, "trailingCommasInFunctions") |
| } |
| }, |
| |
| "Identifier"(node) { |
| const raw = sourceCode.getText(node) |
| if (hasUnicodeCodePointEscape(raw)) { |
| report(node, "unicodeCodePointEscapes") |
| } |
| }, |
| |
| "Literal"(node) { |
| if (typeof node.value === "number") { |
| if (BINARY_NUMBER.test(node.raw)) { |
| report(node, "binaryNumberLiterals") |
| } |
| else if (OCTAL_NUMBER.test(node.raw)) { |
| report(node, "octalNumberLiterals") |
| } |
| } |
| else if (typeof node.value === "string") { |
| if (hasUnicodeCodePointEscape(node.raw)) { |
| report(node, "unicodeCodePointEscapes") |
| } |
| } |
| else if (node.regex) { |
| if (node.regex.flags.indexOf("y") !== -1) { |
| report(node, "regexpY") |
| } |
| if (node.regex.flags.indexOf("u") !== -1) { |
| report(node, "regexpU") |
| } |
| } |
| }, |
| |
| "NewExpression"(node) { |
| if (node.callee.type === "Identifier" && |
| node.callee.name === "RegExp" && |
| node.arguments.length === 2 && |
| node.arguments[1].type === "Literal" && |
| typeof node.arguments[1].value === "string" |
| ) { |
| if (node.arguments[1].value.indexOf("y") !== -1) { |
| report(node, "regexpY") |
| } |
| if (node.arguments[1].value.indexOf("u") !== -1) { |
| report(node, "regexpU") |
| } |
| } |
| if (hasTrailingCommaForCall(node)) { |
| report(node, "trailingCommasInFunctions") |
| } |
| }, |
| |
| "ObjectPattern"(node) { |
| if (DESTRUCTURING_PARENT_TYPE.test(node.parent.type)) { |
| report(node, "destructuring") |
| } |
| }, |
| |
| "Property"(node) { |
| if (node.parent.type === "ObjectExpression" && |
| (node.computed || node.shorthand || node.method) |
| ) { |
| if (node.shorthand && GET_OR_SET.test(node.key.name)) { |
| report(node, "objectPropertyShorthandOfGetSet") |
| } |
| else { |
| report(node, "objectLiteralExtensions") |
| } |
| } |
| }, |
| |
| "SpreadElement"(node) { |
| report(node, "spreadOperators") |
| }, |
| |
| "TemplateLiteral"(node) { |
| report(node, "templateStrings") |
| }, |
| |
| //---------------------------------------------------------------------- |
| // Modules |
| //---------------------------------------------------------------------- |
| |
| "ExportAllDeclaration"(node) { |
| report(node, "modules") |
| }, |
| |
| "ExportDefaultDeclaration"(node) { |
| report(node, "modules") |
| }, |
| |
| "ExportNamedDeclaration"(node) { |
| report(node, "modules") |
| }, |
| |
| "ImportDeclaration"(node) { |
| report(node, "modules") |
| }, |
| } |
| } |
| |
| //------------------------------------------------------------------------------ |
| // Rule Definition |
| //------------------------------------------------------------------------------ |
| |
| module.exports = { |
| create, |
| meta: { |
| docs: { |
| description: "disallow unsupported ECMAScript features on the specified version", |
| category: "Possible Errors", |
| recommended: true, |
| }, |
| fixable: false, |
| schema: [ |
| { |
| anyOf: [ |
| VERSION_SCHEMA.anyOf[0], |
| VERSION_SCHEMA.anyOf[1], |
| { |
| type: "object", |
| properties: { |
| version: VERSION_SCHEMA, |
| ignores: { |
| type: "array", |
| items: {enum: getIgnoresEnum()}, |
| uniqueItems: true, |
| }, |
| }, |
| additionalProperties: false, |
| }, |
| ], |
| }, |
| ], |
| }, |
| } |