diff --git a/docs/rules/no-unsupported-features.md b/docs/rules/no-unsupported-features.md index 6b2d9108..27ee5072 100644 --- a/docs/rules/no-unsupported-features.md +++ b/docs/rules/no-unsupported-features.md @@ -1,9 +1,9 @@ # Disallow unsupported ECMAScript features on the specified version (no-unsupported-features) Node.js doesn't support all ECMAScript standard features. -This rule reports when you used unsupported ECMAScript 2015-2017 features on the specified Node.js version. +This rule reports when you used unsupported ECMAScript 2015-2018 features on the specified Node.js version. -> ※ About ECMAScript 2017, this rule reports only features which have arrived at stage 4 until 2017-06-01. +> ※ About ECMAScript 2018, this rule reports only features which have arrived at stage 4 until 2018-02-01. > It needs a major version bump in order to cover newer features. ## Rule Details @@ -13,7 +13,7 @@ This rule reports when you used unsupported ECMAScript 2015-2017 features on the ```json { "env": {"es6": true}, - "parserOptions": {"ecmaVersion": 2017} + "parserOptions": {"ecmaVersion": 2018} } ``` @@ -27,7 +27,7 @@ For example of `package.json`: "name": "your-module", "version": "1.0.0", "engines": { - "node": ">=4.0.0" + "node": ">=6.0.0" } } ``` @@ -125,9 +125,13 @@ The `version` option accepts the following version number: - `4` - `5` - `6` -- `7` -- `7.6` ... supports async functions. -- `8` ... supports trailing commas in functions. +- `6.5` ... `Symbol.hasInstance` and `Symbol.species`. +- `7` ... Exponential operators, `Object.values`, `Object.entries`, and `Object.getOwnPropertyDescriptors`. +- `7.6` ... Async functions. +- `8` ... Trailing commas in functions. +- `8.3` ... Rest/Spread proeprties. +- `9.0` ... Illegal escape sequences in taggled templates, RegExp 's' flags, RegExp lookbehind assertions, `SharedArrayBuffer`, and `Atomics`. +- `10.0` ... RegExp named capture groups, RegExp Unicode property escapes, Async generators, and `for-await-of` loops. ### ignores @@ -159,6 +163,15 @@ The `"ignores"` option accepts an array of the following strings. - `"exponentialOperators"` - `"asyncAwait"` - `"trailingCommasInFunctions"` + - `"templateLiteralRevision"` + - `"regexpS"` + - `"regexpNamedCaptureGroups"` + - `"regexpLookbehind"` + - `"regexpUnicodeProperties"` + - `"restProperties"` + - `"spreadProperties"` + - `"asyncGenerators"` + - `"forAwaitOf"` - `"runtime"` (group) - `"globalObjects"` (group) - `"typedArrays"` (group) @@ -282,6 +295,5 @@ E.g., a use of instance methods. ## Further Reading - http://node.green/ -- http://kangax.github.io/compat-table/es6/ [engines]: https://docs.npmjs.com/files/package.json#engines diff --git a/lib/rules/no-unsupported-features.js b/lib/rules/no-unsupported-features.js index 0ed1493d..989ada24 100644 --- a/lib/rules/no-unsupported-features.js +++ b/lib/rules/no-unsupported-features.js @@ -25,9 +25,13 @@ const VERSION_MAP = new Map([ [4, "4.0.0"], [5, "5.0.0"], [6, "6.0.0"], + [6.5, "6.5.0"], [7, "7.0.0"], [7.6, "7.6.0"], [8, "8.0.0"], + [8.3, "8.3.0"], + [9, "9.0.0"], + [10, "10.0.0"], ]) const VERSION_SCHEMA = { anyOf: [ @@ -83,6 +87,9 @@ const PROPERTY_TEST_TARGETS = { "isLockFree", "load", "or", "store", "sub", "xor", ], } +const REGEXP_NAMED_GROUP = /(\\*)\(\?<[_$\w]/ +const REGEXP_LOOKBEHIND = /(\\*)\(\?<[=!]/ +const REGEXP_UNICODE_PROPERTY = /(\\*)\\[pP]{.+?}/ /** * Gets default version configuration of this rule. @@ -252,6 +259,17 @@ function hasUnicodeCodePointEscape(raw) { return false } +/** + * Check a given string has a given pattern. + * @param {string} s A string to check. + * @param {RegExp} pattern A RegExp object to check. + * @returns {boolean} `true` if the string has the pattern. + */ +function hasPattern(s, pattern) { + const m = pattern.exec(s) + return m != null && ((m[1] || "").length % 2) === 0 +} + /** * The definition of this rule. * @@ -378,6 +396,60 @@ function create(context) { } } + /** + * Validate RegExp syntax. + * @param {string} pattern A RegExp pattern to check. + * @param {string} flags A RegExp flags to check. + * @param {ASTNode} node A node to report. + * @returns {void} + */ + function validateRegExp(pattern, flags, node) { + if (typeof pattern === "string") { + if (hasPattern(pattern, REGEXP_NAMED_GROUP)) { + report(node, "regexpNamedCaptureGroups") + } + if (hasPattern(pattern, REGEXP_LOOKBEHIND)) { + report(node, "regexpLookbehind") + } + if (hasPattern(pattern, REGEXP_UNICODE_PROPERTY)) { + report(node, "regexpUnicodeProperties") + } + } + if (typeof flags === "string") { + if (flags.indexOf("y") !== -1) { + report(node, "regexpY") + } + if (flags.indexOf("u") !== -1) { + report(node, "regexpU") + } + if (flags.indexOf("s") !== -1) { + report(node, "regexpS") + } + } + } + + /** + * Validate RegExp syntax in a RegExp literal. + * @param {ASTNode} node A Literal node to check. + * @returns {void} + */ + function validateRegExpLiteral(node) { + validateRegExp(node.regex.pattern, node.regex.flags, node) + } + + /** + * Validate RegExp syntax in the first argument of `new RegExp()`. + * @param {ASTNode} node A NewExpression node to check. + * @returns {void} + */ + function validateRegExpString(node) { + const patternNode = node.arguments[0] + const flagsNode = node.arguments[1] + const pattern = (patternNode && patternNode.type === "Literal" && typeof patternNode.value === "string") ? patternNode.value : null + const flags = (flagsNode && flagsNode.type === "Literal" && typeof flagsNode.value === "string") ? flagsNode.value : null + validateRegExp(pattern, flags, node) + } + return { //---------------------------------------------------------------------- // Program @@ -467,6 +539,9 @@ function create(context) { if (hasTrailingCommaForFunction(node)) { report(node, "trailingCommasInFunctions") } + if (node.async && node.generator) { + report(node, "asyncGenerators") + } }, "FunctionExpression"(node) { @@ -479,6 +554,9 @@ function create(context) { if (hasTrailingCommaForFunction(node)) { report(node, "trailingCommasInFunctions") } + if (node.async && node.generator) { + report(node, "asyncGenerators") + } }, "MetaProperty"(node) { @@ -489,12 +567,6 @@ function create(context) { } }, - "RestElement"(node) { - if (FUNC_TYPE.test(node.parent.type)) { - report(node, "restParameters") - } - }, - //---------------------------------------------------------------------- // Classes //---------------------------------------------------------------------- @@ -521,6 +593,9 @@ function create(context) { "ForOfStatement"(node) { report(node, "forOf") + if (node.await) { + report(node, "forAwaitOf") + } }, "VariableDeclaration"(node) { @@ -586,28 +661,13 @@ function create(context) { } } else if (node.regex) { - if (node.regex.flags.indexOf("y") !== -1) { - report(node, "regexpY") - } - if (node.regex.flags.indexOf("u") !== -1) { - report(node, "regexpU") - } + validateRegExpLiteral(node) } }, "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 (node.callee.type === "Identifier" && node.callee.name === "RegExp") { + validateRegExpString(node) } if (hasTrailingCommaForCall(node)) { report(node, "trailingCommasInFunctions") @@ -633,14 +693,54 @@ function create(context) { } }, + "RestElement"(node) { + if (FUNC_TYPE.test(node.parent.type)) { + report(node, "restParameters") + } + else if (node.parent.type === "ObjectPattern") { + report(node, "restProperties") + } + }, + "SpreadElement"(node) { - report(node, "spreadOperators") + if (node.parent.type === "ObjectExpression") { + report(node, "spreadProperties") + } + else { + report(node, "spreadOperators") + } + }, + + "TemplateElement"(node) { + if (node.value.cooked == null) { + report(node, "templateLiteralRevision") + } }, "TemplateLiteral"(node) { report(node, "templateStrings") }, + //---------------------------------------------------------------------- + // Legacy + //---------------------------------------------------------------------- + + "ExperimentalRestProperty"(node) { + report(node, "restProperties") + }, + + "ExperimentalSpreadProperty"(node) { + report(node, "spreadProperties") + }, + + "RestProperty"(node) { + report(node, "restProperties") + }, + + "SpreadProperty"(node) { + report(node, "spreadProperties") + }, + //---------------------------------------------------------------------- // Modules //---------------------------------------------------------------------- diff --git a/lib/util/features.js b/lib/util/features.js index 759baadb..874e6408 100644 --- a/lib/util/features.js +++ b/lib/util/features.js @@ -142,11 +142,56 @@ module.exports = { node: "7.6.0", }, "trailingCommasInFunctions": { - // trailingCommasInFunctionSyntax is for backward compatibility. - alias: ["syntax", "trailingCommasInFunctionSyntax"], + alias: ["syntax"], name: "Trailing commas in functions", node: "8.0.0", }, + //------------------------------------------ + "templateLiteralRevision": { + alias: ["syntax"], + name: "Illegal escape sequences in taggled templates", + node: "9.0.0", + }, + "regexpS": { + alias: ["syntax"], + name: "RegExp 's' flags", + node: "9.0.0", + }, + "regexpNamedCaptureGroups": { + alias: ["syntax"], + name: "RegExp named capture groups", + node: "10.0.0", + }, + "regexpLookbehind": { + alias: ["syntax"], + name: "RegExp lookbehind assertions", + node: "9.0.0", + }, + "regexpUnicodeProperties": { + alias: ["syntax"], + name: "RegExp Unicode property escapes", + node: "10.0.0", + }, + "restProperties": { + alias: ["syntax"], + name: "Rest properties", + node: "8.3.0", + }, + "spreadProperties": { + alias: ["syntax"], + name: "Spread properties", + node: "8.3.0", + }, + "asyncGenerators": { + alias: ["syntax"], + name: "Async generators", + node: "10.0.0", + }, + "forAwaitOf": { + alias: ["syntax"], + name: "for-await-of loops", + node: "10.0.0", + }, //-------------------------------------------------------------------------- // Runtime @@ -264,13 +309,13 @@ module.exports = { alias: ["runtime", "globalObjects"], name: "'SharedArrayBuffer'", singular: true, - node: null, + node: "9.0.0", }, "Atomics": { alias: ["runtime", "globalObjects"], name: "'Atomics'", singular: true, - node: null, + node: "9.0.0", }, "Object.assign": { @@ -492,7 +537,7 @@ module.exports = { alias: ["runtime", "staticMethods", "Symbol.*"], name: "'Symbol.hasInstance'", singular: true, - node: null, + node: "6.5.0", }, "Symbol.isConcatSpreadablec": { alias: ["runtime", "staticMethods", "Symbol.*"], @@ -510,7 +555,7 @@ module.exports = { alias: ["runtime", "staticMethods", "Symbol.*"], name: "'Symbol.species'", singular: true, - node: null, + node: "6.5.0", }, "Symbol.replace": { alias: ["runtime", "staticMethods", "Symbol.*"], @@ -559,73 +604,73 @@ module.exports = { alias: ["runtime", "staticMethods", "Atomics.*"], name: "'Atomics.add'", singular: true, - node: null, + node: "9.0.0", }, "Atomics.and": { alias: ["runtime", "staticMethods", "Atomics.*"], name: "'Atomics.and'", singular: true, - node: null, + node: "9.0.0", }, "Atomics.compareExchange": { alias: ["runtime", "staticMethods", "Atomics.*"], name: "'Atomics.compareExchange'", singular: true, - node: null, + node: "9.0.0", }, "Atomics.exchange": { alias: ["runtime", "staticMethods", "Atomics.*"], name: "'Atomics.exchange'", singular: true, - node: null, + node: "9.0.0", }, "Atomics.wait": { alias: ["runtime", "staticMethods", "Atomics.*"], name: "'Atomics.wait'", singular: true, - node: null, + node: "9.0.0", }, "Atomics.wake": { alias: ["runtime", "staticMethods", "Atomics.*"], name: "'Atomics.wake'", singular: true, - node: null, + node: "9.0.0", }, "Atomics.isLockFree": { alias: ["runtime", "staticMethods", "Atomics.*"], name: "'Atomics.isLockFree'", singular: true, - node: null, + node: "9.0.0", }, "Atomics.load": { alias: ["runtime", "staticMethods", "Atomics.*"], name: "'Atomics.load'", singular: true, - node: null, + node: "9.0.0", }, "Atomics.or": { alias: ["runtime", "staticMethods", "Atomics.*"], name: "'Atomics.or'", singular: true, - node: null, + node: "9.0.0", }, "Atomics.store": { alias: ["runtime", "staticMethods", "Atomics.*"], name: "'Atomics.store'", singular: true, - node: null, + node: "9.0.0", }, "Atomics.sub": { alias: ["runtime", "staticMethods", "Atomics.*"], name: "'Atomics.sub'", singular: true, - node: null, + node: "9.0.0", }, "Atomics.xor": { alias: ["runtime", "staticMethods", "Atomics.*"], name: "'Atomics.xor'", singular: true, - node: null, + node: "9.0.0", }, "extendsArray": { diff --git a/tests/lib/rules/no-unsupported-features.js b/tests/lib/rules/no-unsupported-features.js index a9435ede..dff1e356 100644 --- a/tests/lib/rules/no-unsupported-features.js +++ b/tests/lib/rules/no-unsupported-features.js @@ -23,9 +23,13 @@ const VERSION_MAP = new Map([ [4, "4.0.0"], [5, "5.0.0"], [6, "6.0.0"], + [6.5, "6.5.0"], [7, "7.0.0"], [7.6, "7.6.0"], [8, "8.0.0"], + [8.3, "8.3.0"], + [9, "9.0.0"], + [10, "10.0.0"], ]) /** @@ -75,7 +79,7 @@ function convertPattern(retv, pattern) { globals: { SharedArrayBuffer: false, Atomics: false }, options: [version], parserOptions: { - ecmaVersion: 8, + ecmaVersion: 2018, sourceType: pattern.modules ? "module" : "script", }, }) @@ -88,7 +92,7 @@ function convertPattern(retv, pattern) { globals: { SharedArrayBuffer: false, Atomics: false }, options: [{ version, ignores: [key] }], parserOptions: { - ecmaVersion: 8, + ecmaVersion: 2018, sourceType: pattern.modules ? "module" : "script", }, }))) @@ -100,7 +104,7 @@ function convertPattern(retv, pattern) { globals: { SharedArrayBuffer: false, Atomics: false }, options: [version], parserOptions: { - ecmaVersion: 8, + ecmaVersion: 2018, sourceType: pattern.modules ? "module" : "script", }, errors: errors.map(message => `${message + versionText}.`), @@ -378,7 +382,7 @@ ruleTester.run("no-unsupported-features", rule, [ ignores: [0.10, 0.12, 4, 5], }, { - keys: ["trailingCommasInFunctionSyntax", "trailingCommasInFunctions", "syntax"], + keys: ["trailingCommasInFunctions", "syntax"], name: "Trailing commas in functions", code: [ "function foo(a,) {}", @@ -393,6 +397,105 @@ ruleTester.run("no-unsupported-features", rule, [ supported: 8, ignores: [0.10, 0.12, 4, 5], }, + { + keys: ["templateLiteralRevision", "syntax"], + name: "Illegal escape sequences in taggled templates", + code: [ + //eslint-disable-next-line no-template-curly-in-string + "tag`\\01\\1\\xg\\xAg\\u0\\u0g\\u00g\\u000g\\u{g\\u{0\\u{110000}${0}\\0`", + ].join("\n"), + errors: 1, + supported: 9, + ignores: [0.10, 0.12, 4, 5], + }, + { + keys: ["regexpS", "syntax"], + name: "RegExp 's' flags", + code: "new RegExp('', 's'); (/a/s)", + errors: 2, + supported: 9, + }, + { + keys: ["regexpNamedCaptureGroups", "syntax"], + name: "RegExp named capture groups", + code: [ + "new RegExp('(?b)');", + //TODO: Espree has not supported this syntax yet. + // ";(/(?b)/)", + ].join("\n"), + errors: 1, + supported: 10, + }, + { + keys: ["regexpLookbehind", "syntax"], + name: "RegExp lookbehind assertions", + code: [ + "new RegExp('(?<=a)b')", + "new RegExp('(?