diff --git a/.eslintrc b/.eslintrc index 12fb8b7a23..2cbfd59cea 100644 --- a/.eslintrc +++ b/.eslintrc @@ -102,6 +102,16 @@ "es6": false, }, }, + { + "files": "resolvers/webpack/**", + "rules": { + "import/no-extraneous-dependencies": 1, + "no-console": 1, + }, + "env": { + "es6": true, + }, + }, { "files": [ "resolvers/*/test/**/*", @@ -127,5 +137,15 @@ "no-console": 1, }, }, + { + "files": "tests/**", + "env": { + "mocha": true, + }, + "rules": { + "max-len": 0, + "import/default": 0, + }, + }, ], } diff --git a/.github/workflows/node-4+.yml b/.github/workflows/node-4+.yml index 3af06b3bc3..6762bf0bbd 100644 --- a/.github/workflows/node-4+.yml +++ b/.github/workflows/node-4+.yml @@ -34,6 +34,11 @@ jobs: - 3 - 2 include: + - node-version: 'lts/*' + eslint: 7 + ts-parser: 4 + env: + TS_PARSER: 4 - node-version: 'lts/*' eslint: 7 ts-parser: 3 diff --git a/CHANGELOG.md b/CHANGELOG.md index c38f68659d..da3cac482e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,15 +6,39 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ## [Unreleased] +### Added +- [`no-named-default`, `no-default-export`, `prefer-default-export`, `no-named-export`, `export`, `named`, `namespace`, `no-unused-modules`]: support arbitrary module namespace names ([#2358], thanks [@sosukesuzuki]) +- [`no-dynamic-require`]: support dynamic import with espree ([#2371], thanks [@sosukesuzuki]) +- [`no-relative-packages`]: add fixer ([#2381], thanks [@forivall]) + +### Fixed +- [`default`]: `typescript-eslint-parser`: avoid a crash on exporting as namespace (thanks [@ljharb]) +- [`export`]/TypeScript: false positive for typescript namespace merging ([#1964], thanks [@magarcia]) +- [`no-duplicates`]: ignore duplicate modules in different TypeScript module declarations ([#2378], thanks [@remcohaszing]) +- [`no-unused-modules`]: avoid a crash when processing re-exports ([#2388], thanks [@ljharb]) + +### Changed +- [Tests] `no-nodejs-modules`: add tests for node protocol URL ([#2367], thanks [@sosukesuzuki]) +- [Tests] `default`, `no-anonymous-default-export`, `no-mutable-exports`, `no-named-as-default-member`, `no-named-as-default`: add tests for arbitrary module namespace names ([#2358], thanks [@sosukesuzuki]) +- [Docs] [`no-unresolved`]: Fix RegExp escaping in readme ([#2332], thanks [@stephtr]) +- [Refactor] `namespace`: try to improve performance ([#2340], thanks [@ljharb]) +- [Docs] make rule doc titles consistent ([#2393], thanks [@TheJaredWilcurt]) +- [Docs] `order`: TS code examples should use TS code blocks ([#2411], thanks [@MM25Zamanian]) +- [Docs] `no-unresolved`: fix link ([#2417], thanks [@kylemh]) + +## [2.25.4] - 2022-01-02 + ### Fixed - `importType`: avoid crashing on a non-string' ([#2305], thanks [@ljharb]) - [`first`]: prevent crash when parsing angular templates ([#2210], thanks [@ljharb]) - `importType`: properly resolve `@/*`-aliased imports as internal ([#2334], thanks [@ombene]) +- [`named`]/`ExportMap`: handle named imports from CJS modules that use dynamic import ([#2341], thanks [@ludofischer]) - [`dynamic-import-chunkname`]: prevent false report on a valid webpack magic comment ([#2330], thanks [@mhmadhamster]) ### Changed - [`no-default-import`]: report on the token "default" instead of the entire node ([#2299], thanks [@pmcelhaney]) - [Docs] [`order`]: Remove duplicate mention of default ([#2280], thanks [@johnthagen]) +- [Deps] update `eslint-module-utils` ## [2.25.3] - 2021-11-09 @@ -953,6 +977,17 @@ for info on changes for earlier releases. [`memo-parser`]: ./memo-parser/README.md [#2330]: https://github.com/import-js/eslint-plugin-import/pull/2330 +[#2417]: https://github.com/import-js/eslint-plugin-import/pull/2417 +[#2411]: https://github.com/import-js/eslint-plugin-import/pull/2411 +[#2393]: https://github.com/import-js/eslint-plugin-import/pull/2393 +[#2388]: https://github.com/import-js/eslint-plugin-import/pull/2388 +[#2381]: https://github.com/import-js/eslint-plugin-import/pull/2381 +[#2378]: https://github.com/import-js/eslint-plugin-import/pull/2378 +[#2371]: https://github.com/import-js/eslint-plugin-import/pull/2371 +[#2367]: https://github.com/import-js/eslint-plugin-import/pull/2367 +[#2332]: https://github.com/import-js/eslint-plugin-import/pull/2332 +[#2358]: https://github.com/import-js/eslint-plugin-import/pull/2358 +[#2341]: https://github.com/import-js/eslint-plugin-import/pull/2341 [#2334]: https://github.com/import-js/eslint-plugin-import/pull/2334 [#2305]: https://github.com/import-js/eslint-plugin-import/pull/2305 [#2299]: https://github.com/import-js/eslint-plugin-import/pull/2299 @@ -1231,6 +1266,7 @@ for info on changes for earlier releases. [#211]: https://github.com/import-js/eslint-plugin-import/pull/211 [#164]: https://github.com/import-js/eslint-plugin-import/pull/164 [#157]: https://github.com/import-js/eslint-plugin-import/pull/157 +[#2340]: https://github.com/import-js/eslint-plugin-import/issues/2340 [#2255]: https://github.com/import-js/eslint-plugin-import/issues/2255 [#2210]: https://github.com/import-js/eslint-plugin-import/issues/2210 [#2201]: https://github.com/import-js/eslint-plugin-import/issues/2201 @@ -1344,7 +1380,8 @@ for info on changes for earlier releases. [#119]: https://github.com/import-js/eslint-plugin-import/issues/119 [#89]: https://github.com/import-js/eslint-plugin-import/issues/89 -[Unreleased]: https://github.com/import-js/eslint-plugin-import/compare/v2.25.3...HEAD +[Unreleased]: https://github.com/import-js/eslint-plugin-import/compare/v2.25.4...HEAD +[2.25.4]: https://github.com/import-js/eslint-plugin-import/compare/v2.25.3...v2.25.4 [2.25.3]: https://github.com/import-js/eslint-plugin-import/compare/v2.25.2...v2.25.3 [2.25.2]: https://github.com/import-js/eslint-plugin-import/compare/v2.25.1...v2.25.2 [2.25.1]: https://github.com/import-js/eslint-plugin-import/compare/v2.25.0...v2.25.1 @@ -1538,6 +1575,7 @@ for info on changes for earlier releases. [@kmui2]: https://github.com/kmui2 [@knpwrs]: https://github.com/knpwrs [@KostyaZgara]: https://github.com/KostyaZgara +[@kylemh]: https://github.com/kylemh [@laysent]: https://github.com/laysent [@le0nik]: https://github.com/le0nik [@lemonmade]: https://github.com/lemonmade @@ -1551,8 +1589,10 @@ for info on changes for earlier releases. [@lo1tuma]: https://github.com/lo1tuma [@loganfsmyth]: https://github.com/loganfsmyth [@luczsoma]: https://github.com/luczsoma +[@ludofischer]: https://github.com/ludofischer [@lukeapage]: https://github.com/lukeapage [@lydell]: https://github.com/lydell +[@magarcia]: https://github.com/magarcia [@Mairu]: https://github.com/Mairu [@malykhinvi]: https://github.com/malykhinvi [@manovotny]: https://github.com/manovotny @@ -1614,9 +1654,11 @@ for info on changes for earlier releases. [@skyrpex]: https://github.com/skyrpex [@sompylasar]: https://github.com/sompylasar [@soryy708]: https://github.com/soryy708 +[@sosukesuzuki]: https://github.com/sosukesuzuki [@spalger]: https://github.com/spalger [@st-sloth]: https://github.com/st-sloth [@stekycz]: https://github.com/stekycz +[@stephtr]: https://github.com/stephtr [@straub]: https://github.com/straub [@strawbrary]: https://github.com/strawbrary [@stropho]: https://github.com/stropho @@ -1629,6 +1671,7 @@ for info on changes for earlier releases. [@Taranys]: https://github.com/Taranys [@taye]: https://github.com/taye [@TheCrueltySage]: https://github.com/TheCrueltySage +[@TheJaredWilcurt]: https://github.com/TheJaredWilcurt [@tihonove]: https://github.com/tihonove [@timkraut]: https://github.com/timkraut [@tizmagik]: https://github.com/tizmagik diff --git a/docs/rules/dynamic-import-chunkname.md b/docs/rules/dynamic-import-chunkname.md index d29c06bbaa..6b43074f19 100644 --- a/docs/rules/dynamic-import-chunkname.md +++ b/docs/rules/dynamic-import-chunkname.md @@ -1,4 +1,4 @@ -# dynamic imports require a leading comment with a webpackChunkName (dynamic-import-chunkname) +# import/dynamic-import-chunkname This rule reports any dynamic imports without a webpackChunkName specified in a leading block comment in the proper format. diff --git a/docs/rules/imports-first.md b/docs/rules/imports-first.md index 7dadffa684..4b90f04ea8 100644 --- a/docs/rules/imports-first.md +++ b/docs/rules/imports-first.md @@ -1,3 +1,3 @@ -# imports-first +# import/imports-first This rule was **deprecated** in eslint-plugin-import v2.0.0. Please use the corresponding rule [`first`](https://github.com/import-js/eslint-plugin-import/blob/HEAD/docs/rules/first.md). diff --git a/docs/rules/no-import-module-exports.md b/docs/rules/no-import-module-exports.md index 8131fd5f78..d658deb566 100644 --- a/docs/rules/no-import-module-exports.md +++ b/docs/rules/no-import-module-exports.md @@ -1,4 +1,4 @@ -# no-import-module-exports +# import/no-import-module-exports Reports the use of import declarations with CommonJS exports in any module except for the [main module](https://docs.npmjs.com/files/package.json#main). diff --git a/docs/rules/no-relative-packages.md b/docs/rules/no-relative-packages.md index d5a0684932..a989c12a23 100644 --- a/docs/rules/no-relative-packages.md +++ b/docs/rules/no-relative-packages.md @@ -5,6 +5,7 @@ Use this rule to prevent importing packages through relative paths. It's useful in Yarn/Lerna workspaces, were it's possible to import a sibling package using `../package` relative path, while direct `package` is the correct one. ++(fixable) The `--fix` option on the [command line] automatically fixes problems reported by this rule. ### Examples diff --git a/docs/rules/no-unresolved.md b/docs/rules/no-unresolved.md index 89d00b9301..08522deb4c 100644 --- a/docs/rules/no-unresolved.md +++ b/docs/rules/no-unresolved.md @@ -60,7 +60,7 @@ This rule has its own ignore list, separate from [`import/ignore`]. This is beca To suppress errors from files that may not be properly resolved by your [resolver settings](../../README.md#resolver-plugins), you may add an `ignore` key with an array of `RegExp` pattern strings: ```js -/*eslint import/no-unresolved: [2, { ignore: ['\.img$'] }]*/ +/*eslint import/no-unresolved: [2, { ignore: ['\\.img$'] }]*/ import { x } from './mod' // may be reported, if not resolved to a module @@ -98,7 +98,7 @@ If you're using a module bundler other than Node or Webpack, you may end up with ## Further Reading -- [Resolver plugins](../../README.md#resolver-plugins) +- [Resolver plugins](../../README.md#resolvers) - [Node resolver](https://npmjs.com/package/eslint-import-resolver-node) (default) - [Webpack resolver](https://npmjs.com/package/eslint-import-resolver-webpack) - [`import/ignore`] global setting diff --git a/docs/rules/order.md b/docs/rules/order.md index 437467e244..f6e1ddbeb1 100644 --- a/docs/rules/order.md +++ b/docs/rules/order.md @@ -5,7 +5,7 @@ Enforce a convention in the order of `require()` / `import` statements. With the [`groups`](#groups-array) option set to `["builtin", "external", "internal", "parent", "sibling", "index", "object", "type"]` the order is as shown in the following example: -```js +```ts // 1. node "builtin" modules import fs from 'fs'; import path from 'path'; @@ -36,7 +36,7 @@ Statements using the ES6 `import` syntax must appear before any `require()` stat ## Fail -```js +```ts import _ from 'lodash'; import path from 'path'; // `path` import should occur before import of `lodash` @@ -54,7 +54,7 @@ import foo from './foo'; // `import` statements must be before `require` stateme ## Pass -```js +```ts import path from 'path'; import _ from 'lodash'; @@ -85,7 +85,7 @@ This rule supports the following options: How groups are defined, and the order to respect. `groups` must be an array of `string` or [`string`]. The only allowed `string`s are: `"builtin"`, `"external"`, `"internal"`, `"unknown"`, `"parent"`, `"sibling"`, `"index"`, `"object"`, `"type"`. The enforced order is the same as the order of each element in a group. Omitted types are implicitly grouped together as the last element. Example: -```js +```ts [ 'builtin', // Built-in types are first ['sibling', 'parent'], // Then sibling and parent types. They can be mingled together @@ -98,7 +98,7 @@ The default value is `["builtin", "external", "parent", "sibling", "index"]`. You can set the options like this: -```js +```ts "import/order": ["error", {"groups": ["index", "sibling", "parent", "internal", "external", "builtin", "object", "type"]}] ``` @@ -184,7 +184,7 @@ The default value is `"ignore"`. With the default group setting, the following will be invalid: -```js +```ts /* eslint import/order: ["error", {"newlines-between": "always"}] */ import fs from 'fs'; import path from 'path'; @@ -192,7 +192,7 @@ import index from './'; import sibling from './foo'; ``` -```js +```ts /* eslint import/order: ["error", {"newlines-between": "always-and-inside-groups"}] */ import fs from 'fs'; @@ -201,7 +201,7 @@ import index from './'; import sibling from './foo'; ``` -```js +```ts /* eslint import/order: ["error", {"newlines-between": "never"}] */ import fs from 'fs'; import path from 'path'; @@ -213,7 +213,7 @@ import sibling from './foo'; while those will be valid: -```js +```ts /* eslint import/order: ["error", {"newlines-between": "always"}] */ import fs from 'fs'; import path from 'path'; @@ -223,7 +223,7 @@ import index from './'; import sibling from './foo'; ``` -```js +```ts /* eslint import/order: ["error", {"newlines-between": "always-and-inside-groups"}] */ import fs from 'fs'; @@ -234,7 +234,7 @@ import index from './'; import sibling from './foo'; ``` -```js +```ts /* eslint import/order: ["error", {"newlines-between": "never"}] */ import fs from 'fs'; import path from 'path'; @@ -250,7 +250,7 @@ Sort the order within each group in alphabetical manner based on **import path** - `caseInsensitive`: use `true` to ignore case, and `false` to consider case (default: `false`). Example setting: -```js +```ts alphabetize: { order: 'asc', /* sort in ascending order. Options: ['ignore', 'asc', 'desc'] */ caseInsensitive: true /* ignore case. Options: [true, false] */ @@ -259,7 +259,7 @@ alphabetize: { This will fail the rule check: -```js +```ts /* eslint import/order: ["error", {"alphabetize": {"order": "asc", "caseInsensitive": true}}] */ import React, { PureComponent } from 'react'; import aTypes from 'prop-types'; @@ -270,7 +270,7 @@ import blist from 'BList'; While this will pass: -```js +```ts /* eslint import/order: ["error", {"alphabetize": {"order": "asc", "caseInsensitive": true}}] */ import blist from 'BList'; import * as classnames from 'classnames'; @@ -290,7 +290,7 @@ way that is safe. This will fail the rule check: -```js +```ts /* eslint import/order: ["error", {"warnOnUnassignedImports": true}] */ import fs from 'fs'; import './styles.css'; @@ -299,7 +299,7 @@ import path from 'path'; While this will pass: -```js +```ts /* eslint import/order: ["error", {"warnOnUnassignedImports": true}] */ import fs from 'fs'; import path from 'path'; diff --git a/memo-parser/.eslintrc.yml b/memo-parser/.eslintrc.yml deleted file mode 100644 index e7e6b3d341..0000000000 --- a/memo-parser/.eslintrc.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -rules: - import/no-extraneous-dependencies: 1 diff --git a/package.json b/package.json index a48f86b225..186c5cb57e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-import", - "version": "2.25.3", + "version": "2.25.4", "description": "Import with sanity.", "engines": { "node": ">=4" @@ -53,10 +53,10 @@ }, "homepage": "https://github.com/import-js/eslint-plugin-import", "devDependencies": { - "@angular-eslint/template-parser": "^13.0.1", + "@angular-eslint/template-parser": "^13.1.0", "@eslint/import-test-order-redirect-scoped": "file:./tests/files/order-redirect-scoped", "@test-scope/some-module": "file:./tests/files/symlinked-module", - "@typescript-eslint/parser": "^2.23.0 || ^3.3.0 || ^4.29.3", + "@typescript-eslint/parser": "^2.23.0 || ^3.3.0 || ^4.29.3 || ^5.10.0", "array.prototype.flatmap": "^1.2.5", "babel-cli": "^6.26.0", "babel-core": "^6.26.3", @@ -67,7 +67,7 @@ "babel-preset-flow": "^6.23.0", "babel-register": "^6.26.0", "babylon": "^6.18.0", - "chai": "^4.3.4", + "chai": "^4.3.6", "cross-env": "^4.0.0", "escope": "^3.6.0", "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8", @@ -89,11 +89,11 @@ "nyc": "^11.9.0", "redux": "^3.7.2", "rimraf": "^2.7.1", - "safe-publish-latest": "^1.1.4", + "safe-publish-latest": "^2.0.0", "semver": "^6.3.0", "sinon": "^2.4.1", "typescript": "^2.8.1 || ~3.9.5", - "typescript-eslint-parser": "^15 || ^22.0.0" + "typescript-eslint-parser": "^15 || ^20 || ^22" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" @@ -104,13 +104,13 @@ "debug": "^2.6.9", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.6", - "eslint-module-utils": "^2.7.1", + "eslint-module-utils": "^2.7.3", "has": "^1.0.3", - "is-core-module": "^2.8.0", + "is-core-module": "^2.8.1", "is-glob": "^4.0.3", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "object.values": "^1.1.5", - "resolve": "^1.20.0", - "tsconfig-paths": "^3.12.0" + "resolve": "^1.22.0", + "tsconfig-paths": "^3.13.0" } } diff --git a/resolvers/webpack/.eslintrc b/resolvers/webpack/.eslintrc deleted file mode 100644 index 544167c4bb..0000000000 --- a/resolvers/webpack/.eslintrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "rules": { - "import/no-extraneous-dependencies": 1, - "no-console": 1, - }, - "env": { - "es6": true, - }, -} diff --git a/src/ExportMap.js b/src/ExportMap.js index d818fa6ca8..d75c7ecd47 100644 --- a/src/ExportMap.js +++ b/src/ExportMap.js @@ -43,6 +43,10 @@ export default class ExportMap { */ this.imports = new Map(); this.errors = []; + /** + * type {'ambiguous' | 'Module' | 'Script'} + */ + this.parseGoal = 'ambiguous'; } get hasDefault() { return this.get('default') != null; } // stronger than this.has @@ -406,7 +410,8 @@ ExportMap.parse = function (path, content, context) { }, }); - if (!unambiguous.isModule(ast) && !hasDynamicImports) return null; + const unambiguouslyESM = unambiguous.isModule(ast); + if (!unambiguouslyESM && !hasDynamicImports) return null; const docstyle = (context.settings && context.settings['import/docstyle']) || ['jsdoc']; const docStyleParsers = {}; @@ -474,11 +479,11 @@ ExportMap.parse = function (path, content, context) { })); return; case 'ExportAllDeclaration': - m.namespace.set(s.exported.name, addNamespace(exportMeta, s.source.value)); + m.namespace.set(s.exported.name || s.exported.value, addNamespace(exportMeta, s.source.value)); return; case 'ExportSpecifier': if (!n.source) { - m.namespace.set(s.exported.name, addNamespace(exportMeta, s.local)); + m.namespace.set(s.exported.name || s.exported.value, addNamespace(exportMeta, s.local)); return; } // else falls through @@ -588,7 +593,7 @@ ExportMap.parse = function (path, content, context) { importedSpecifiers.add(specifier.type); } if (specifier.type === 'ImportSpecifier') { - importedSpecifiers.add(specifier.imported.name); + importedSpecifiers.add(specifier.imported.name || specifier.imported.value); } // import { type Foo } (Flow) @@ -640,7 +645,7 @@ ExportMap.parse = function (path, content, context) { // This doesn't declare anything, but changes what's being exported. if (includes(exports, n.type)) { const exportedName = n.type === 'TSNamespaceExportDeclaration' - ? n.id.name + ? (n.id || n.name).name : (n.expression && n.expression.name || (n.expression.id && n.expression.id.name) || null); const declTypes = [ 'VariableDeclaration', @@ -710,6 +715,9 @@ ExportMap.parse = function (path, content, context) { m.namespace.set('default', {}); // add default export } + if (unambiguouslyESM) { + m.parseGoal = 'Module'; + } return m; }; diff --git a/src/rules/export.js b/src/rules/export.js index e0b2c57857..4cae107402 100644 --- a/src/rules/export.js +++ b/src/rules/export.js @@ -36,13 +36,59 @@ const tsTypePrefix = 'type:'; */ function isTypescriptFunctionOverloads(nodes) { const types = new Set(Array.from(nodes, node => node.parent.type)); - return ( - types.has('TSDeclareFunction') && - ( - types.size === 1 || - (types.size === 2 && types.has('FunctionDeclaration')) - ) - ); + return types.has('TSDeclareFunction') + && ( + types.size === 1 + || (types.size === 2 && types.has('FunctionDeclaration')) + ); +} + +/** + * Detect merging Namespaces with Classes, Functions, or Enums like: + * ```ts + * export class Foo { } + * export namespace Foo { } + * ``` + * @param {Set} nodes + * @returns {boolean} + */ +function isTypescriptNamespaceMerging(nodes) { + const types = new Set(Array.from(nodes, node => node.parent.type)); + const noNamespaceNodes = Array.from(nodes).filter((node) => node.parent.type !== 'TSModuleDeclaration'); + + return types.has('TSModuleDeclaration') + && ( + types.size === 1 + // Merging with functions + || (types.size === 2 && (types.has('FunctionDeclaration') || types.has('TSDeclareFunction'))) + || (types.size === 3 && types.has('FunctionDeclaration') && types.has('TSDeclareFunction')) + // Merging with classes or enums + || (types.size === 2 && (types.has('ClassDeclaration') || types.has('TSEnumDeclaration')) && noNamespaceNodes.length === 1) + ); +} + +/** + * Detect if a typescript namespace node should be reported as multiple export: + * ```ts + * export class Foo { } + * export function Foo(); + * export namespace Foo { } + * ``` + * @param {Object} node + * @param {Set} nodes + * @returns {boolean} + */ +function shouldSkipTypescriptNamespace(node, nodes) { + const types = new Set(Array.from(nodes, node => node.parent.type)); + + return !isTypescriptNamespaceMerging(nodes) + && node.parent.type === 'TSModuleDeclaration' + && ( + types.has('TSEnumDeclaration') + || types.has('ClassDeclaration') + || types.has('FunctionDeclaration') + || types.has('TSDeclareFunction') + ); } module.exports = { @@ -85,15 +131,19 @@ module.exports = { } return { - 'ExportDefaultDeclaration': (node) => addNamed('default', node, getParent(node)), + ExportDefaultDeclaration(node) { + addNamed('default', node, getParent(node)); + }, - 'ExportSpecifier': (node) => addNamed( - node.exported.name, - node.exported, - getParent(node.parent), - ), + ExportSpecifier(node) { + addNamed( + node.exported.name || node.exported.value, + node.exported, + getParent(node.parent), + ); + }, - 'ExportNamedDeclaration': function (node) { + ExportNamedDeclaration(node) { if (node.declaration == null) return; const parent = getParent(node); @@ -119,7 +169,7 @@ module.exports = { } }, - 'ExportAllDeclaration': function (node) { + ExportAllDeclaration(node) { if (node.source == null) return; // not sure if this is ever true // `export * as X from 'path'` does not conflict @@ -156,9 +206,11 @@ module.exports = { for (const [name, nodes] of named) { if (nodes.size <= 1) continue; - if (isTypescriptFunctionOverloads(nodes)) continue; + if (isTypescriptFunctionOverloads(nodes) || isTypescriptNamespaceMerging(nodes)) continue; for (const node of nodes) { + if (shouldSkipTypescriptNamespace(node, nodes)) continue; + if (name === 'default') { context.report(node, 'Multiple default exports.'); } else { diff --git a/src/rules/named.js b/src/rules/named.js index 24e6bc0ac5..ad1b5e1728 100644 --- a/src/rules/named.js +++ b/src/rules/named.js @@ -40,7 +40,7 @@ module.exports = { } const imports = Exports.get(node.source.value, context); - if (imports == null) { + if (imports == null || imports.parseGoal === 'ambiguous') { return; } @@ -58,7 +58,9 @@ module.exports = { return; } - const deepLookup = imports.hasDeep(im[key].name); + const name = im[key].name || im[key].value; + + const deepLookup = imports.hasDeep(name); if (!deepLookup.found) { if (deepLookup.path.length > 1) { @@ -66,9 +68,9 @@ module.exports = { .map(i => path.relative(path.dirname(context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename()), i.path)) .join(' -> '); - context.report(im[key], `${im[key].name} not found via ${deepPath}`); + context.report(im[key], `${name} not found via ${deepPath}`); } else { - context.report(im[key], im[key].name + ' not found in \'' + node.source.value + '\''); + context.report(im[key], name + ' not found in \'' + node.source.value + '\''); } } }); @@ -97,6 +99,7 @@ module.exports = { // return if it's not a string source || source.type !== 'Literal' || variableExports == null + || variableExports.parseGoal === 'ambiguous' ) { return; } diff --git a/src/rules/namespace.js b/src/rules/namespace.js index 6325b88ba2..405c415cea 100644 --- a/src/rules/namespace.js +++ b/src/rules/namespace.js @@ -3,6 +3,44 @@ import Exports from '../ExportMap'; import importDeclaration from '../importDeclaration'; import docsUrl from '../docsUrl'; +function processBodyStatement(context, namespaces, declaration) { + if (declaration.type !== 'ImportDeclaration') return; + + if (declaration.specifiers.length === 0) return; + + const imports = Exports.get(declaration.source.value, context); + if (imports == null) return null; + + if (imports.errors.length > 0) { + imports.reportErrors(context, declaration); + return; + } + + declaration.specifiers.forEach((specifier) => { + switch (specifier.type) { + case 'ImportNamespaceSpecifier': + if (!imports.size) { + context.report( + specifier, + `No exported names found in module '${declaration.source.value}'.`, + ); + } + namespaces.set(specifier.local.name, imports); + break; + case 'ImportDefaultSpecifier': + case 'ImportSpecifier': { + const meta = imports.get( + // default to 'default' for default https://i.imgur.com/nj6qAWy.jpg + specifier.imported ? (specifier.imported.name || specifier.imported.value) : 'default', + ); + if (!meta || !meta.namespace) { break; } + namespaces.set(specifier.local.name, meta.namespace); + break; + } + } + }); +} + module.exports = { meta: { type: 'problem', @@ -41,44 +79,7 @@ module.exports = { return { // pick up all imports at body entry time, to properly respect hoisting Program({ body }) { - function processBodyStatement(declaration) { - if (declaration.type !== 'ImportDeclaration') return; - - if (declaration.specifiers.length === 0) return; - - const imports = Exports.get(declaration.source.value, context); - if (imports == null) return null; - - if (imports.errors.length) { - imports.reportErrors(context, declaration); - return; - } - - for (const specifier of declaration.specifiers) { - switch (specifier.type) { - case 'ImportNamespaceSpecifier': - if (!imports.size) { - context.report( - specifier, - `No exported names found in module '${declaration.source.value}'.`, - ); - } - namespaces.set(specifier.local.name, imports); - break; - case 'ImportDefaultSpecifier': - case 'ImportSpecifier': { - const meta = imports.get( - // default to 'default' for default https://i.imgur.com/nj6qAWy.jpg - specifier.imported ? specifier.imported.name : 'default', - ); - if (!meta || !meta.namespace) { break; } - namespaces.set(specifier.local.name, meta.namespace); - break; - } - } - } - } - body.forEach(processBodyStatement); + body.forEach(x => processBodyStatement(context, namespaces, x)); }, // same as above, but does not add names to local map @@ -120,7 +121,6 @@ module.exports = { const namepath = [dereference.object.name]; // while property is namespace and parent is member expression, keep validating while (namespace instanceof Exports && dereference.type === 'MemberExpression') { - if (dereference.computed) { if (!allowComputed) { context.report( @@ -147,7 +147,6 @@ module.exports = { namespace = exported.namespace; dereference = dereference.parent; } - }, VariableDeclarator({ id, init }) { diff --git a/src/rules/no-default-export.js b/src/rules/no-default-export.js index a17428c563..ed1aaf8db6 100644 --- a/src/rules/no-default-export.js +++ b/src/rules/no-default-export.js @@ -25,7 +25,7 @@ module.exports = { }, ExportNamedDeclaration(node) { - node.specifiers.filter(specifier => specifier.exported.name === 'default').forEach(specifier => { + node.specifiers.filter(specifier => (specifier.exported.name || specifier.exported.value) === 'default').forEach(specifier => { const { loc } = context.getSourceCode().getFirstTokens(node)[1] || {}; if (specifier.type === 'ExportDefaultSpecifier') { context.report({ node, message: preferNamed, loc }); diff --git a/src/rules/no-duplicates.js b/src/rules/no-duplicates.js index 43c2c5b201..efd9583fbc 100644 --- a/src/rules/no-duplicates.js +++ b/src/rules/no-duplicates.js @@ -274,17 +274,23 @@ module.exports = { return defaultResolver(parts[1]) + '?' + parts[2]; }) : defaultResolver; - const imported = new Map(); - const nsImported = new Map(); - const defaultTypesImported = new Map(); - const namedTypesImported = new Map(); + const moduleMaps = new Map(); function getImportMap(n) { + if (!moduleMaps.has(n.parent)) { + moduleMaps.set(n.parent, { + imported: new Map(), + nsImported: new Map(), + defaultTypesImported: new Map(), + namedTypesImported: new Map(), + }); + } + const map = moduleMaps.get(n.parent); if (n.importKind === 'type') { - return n.specifiers.length > 0 && n.specifiers[0].type === 'ImportDefaultSpecifier' ? defaultTypesImported : namedTypesImported; + return n.specifiers.length > 0 && n.specifiers[0].type === 'ImportDefaultSpecifier' ? map.defaultTypesImported : map.namedTypesImported; } - return hasNamespace(n) ? nsImported : imported; + return hasNamespace(n) ? map.nsImported : map.imported; } return { @@ -301,10 +307,12 @@ module.exports = { }, 'Program:exit': function () { - checkImports(imported, context); - checkImports(nsImported, context); - checkImports(defaultTypesImported, context); - checkImports(namedTypesImported, context); + for (const map of moduleMaps.values()) { + checkImports(map.imported, context); + checkImports(map.nsImported, context); + checkImports(map.defaultTypesImported, context); + checkImports(map.namedTypesImported, context); + } }, }; }, diff --git a/src/rules/no-dynamic-require.js b/src/rules/no-dynamic-require.js index 8267fd26e9..27e9a957a7 100644 --- a/src/rules/no-dynamic-require.js +++ b/src/rules/no-dynamic-require.js @@ -19,6 +19,8 @@ function isStaticValue(arg) { (arg.type === 'TemplateLiteral' && arg.expressions.length === 0); } +const dynamicImportErrorMessage = 'Calls to import() should use string literals'; + module.exports = { meta: { type: 'suggestion', @@ -55,10 +57,19 @@ module.exports = { if (options.esmodule && isDynamicImport(node)) { return context.report({ node, - message: 'Calls to import() should use string literals', + message: dynamicImportErrorMessage, }); } }, + ImportExpression(node) { + if (!options.esmodule || isStaticValue(node.source)) { + return; + } + return context.report({ + node, + message: dynamicImportErrorMessage, + }); + }, }; }, }; diff --git a/src/rules/no-named-default.js b/src/rules/no-named-default.js index 116c89cf57..6a5c1db703 100644 --- a/src/rules/no-named-default.js +++ b/src/rules/no-named-default.js @@ -17,7 +17,7 @@ module.exports = { return; } - if (im.type === 'ImportSpecifier' && im.imported.name === 'default') { + if (im.type === 'ImportSpecifier' && (im.imported.name || im.imported.value) === 'default') { context.report({ node: im.local, message: `Use default import syntax to import '${im.local.name}'.` }); diff --git a/src/rules/no-named-export.js b/src/rules/no-named-export.js index bb586ead00..6c92ad9cae 100644 --- a/src/rules/no-named-export.js +++ b/src/rules/no-named-export.js @@ -25,7 +25,7 @@ module.exports = { return context.report({ node, message }); } - const someNamed = node.specifiers.some(specifier => specifier.exported.name !== 'default'); + const someNamed = node.specifiers.some(specifier => (specifier.exported.name || specifier.exported.value) !== 'default'); if (someNamed) { context.report({ node, message }); } diff --git a/src/rules/no-relative-packages.js b/src/rules/no-relative-packages.js index 17406e80eb..7bf1ce5cea 100644 --- a/src/rules/no-relative-packages.js +++ b/src/rules/no-relative-packages.js @@ -6,6 +6,11 @@ import moduleVisitor, { makeOptionsSchema } from 'eslint-module-utils/moduleVisi import importType from '../core/importType'; import docsUrl from '../docsUrl'; +/** @param {string} filePath */ +function toPosixPath(filePath) { + return filePath.replace(/\\/g, '/'); +} + function findNamedPackage(filePath) { const found = readPkgUp({ cwd: filePath }); if (found.pkg && !found.pkg.name) { @@ -42,6 +47,8 @@ function checkImportForRelativePackage(context, importPath, node) { context.report({ node, message: `Relative import from another package is not allowed. Use \`${properImport}\` instead of \`${importPath}\``, + fix: fixer => fixer.replaceText(node, JSON.stringify(toPosixPath(properImport))) + , }); } } @@ -52,6 +59,7 @@ module.exports = { docs: { url: docsUrl('no-relative-packages'), }, + fixable: 'code', schema: [makeOptionsSchema()], }, diff --git a/src/rules/no-unused-modules.js b/src/rules/no-unused-modules.js index 9891d4097e..5feb319036 100644 --- a/src/rules/no-unused-modules.js +++ b/src/rules/no-unused-modules.js @@ -273,8 +273,10 @@ const prepareImportsAndExports = (srcFiles, context) => { exportAll.forEach((value, key) => { value.forEach(val => { const currentExports = exportList.get(val); - const currentExport = currentExports.get(EXPORT_ALL_DECLARATION); - currentExport.whereUsed.add(key); + if (currentExports) { + const currentExport = currentExports.get(EXPORT_ALL_DECLARATION); + currentExport.whereUsed.add(key); + } }); }); }; @@ -604,7 +606,7 @@ module.exports = { if (specifiers.length > 0) { specifiers.forEach(specifier => { if (specifier.exported) { - newExportIdentifiers.add(specifier.exported.name); + newExportIdentifiers.add(specifier.exported.name || specifier.exported.value); } }); } @@ -715,8 +717,8 @@ module.exports = { if (astNode.source) { resolvedPath = resolve(astNode.source.raw.replace(/('|")/g, ''), context); astNode.specifiers.forEach(specifier => { - const name = specifier.local.name; - if (specifier.local.name === DEFAULT) { + const name = specifier.local.name || specifier.local.value; + if (name === DEFAULT) { newDefaultImports.add(resolvedPath); } else { newImports.set(name, resolvedPath); @@ -753,7 +755,7 @@ module.exports = { specifier.type === IMPORT_NAMESPACE_SPECIFIER) { return; } - newImports.set(specifier.imported.name, resolvedPath); + newImports.set(specifier.imported.name || specifier.imported.value, resolvedPath); }); } }); @@ -942,7 +944,7 @@ module.exports = { }, 'ExportNamedDeclaration': node => { node.specifiers.forEach(specifier => { - checkUsage(node, specifier.exported.name); + checkUsage(node, specifier.exported.name || specifier.exported.value); }); forEachDeclarationIdentifier(node.declaration, (name) => { checkUsage(node, name); diff --git a/src/rules/prefer-default-export.js b/src/rules/prefer-default-export.js index a8c52bb1a0..230efad12f 100644 --- a/src/rules/prefer-default-export.js +++ b/src/rules/prefer-default-export.js @@ -40,7 +40,7 @@ module.exports = { }, 'ExportSpecifier': function (node) { - if (node.exported.name === 'default') { + if ((node.exported.name || node.exported.value) === 'default') { hasDefaultExport = true; } else { specifierExportCount++; diff --git a/tests/.eslintrc b/tests/.eslintrc deleted file mode 100644 index 5720f3fcac..0000000000 --- a/tests/.eslintrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "env": { - "mocha": true, - }, - "rules": { - "max-len": 0, - "import/default": 0, - }, -} diff --git a/tests/dep-time-travel.sh b/tests/dep-time-travel.sh index 82681b38f3..665ca1ccf1 100755 --- a/tests/dep-time-travel.sh +++ b/tests/dep-time-travel.sh @@ -21,6 +21,8 @@ elif [[ "$ESLINT_VERSION" -lt "5" ]]; then # completely remove the new TypeScrip npm uninstall --no-save @typescript-eslint/parser elif [[ "$TRAVIS_NODE_VERSION" -lt "10" ]]; then # TS parser 3 requires node 10+ npm i --no-save "@typescript-eslint/parser@3" +elif [[ "$TRAVIS_NODE_VERSION" -lt "12" ]]; then # TS parser 4 requires node 12+ + npm i --no-save "@typescript-eslint/parser@4" fi # use these alternate TypeScript dependencies for ESLint < v4 @@ -30,6 +32,9 @@ if [[ "$ESLINT_VERSION" -lt "4" ]]; then echo "Downgrading TypeScript dependencies..." npm i --no-save typescript-eslint-parser@15 typescript@2.8.1 +elif [[ "$ESLINT_VERSION" -lt "7" ]]; then + echo "Downgrading TypeScript dependencies..." + npm i --no-save typescript-eslint-parser@20 fi # typescript-eslint-parser 1.1.1+ is not compatible with node 6 diff --git a/tests/files/.eslintrc b/tests/files/.eslintrc.js similarity index 85% rename from tests/files/.eslintrc rename to tests/files/.eslintrc.js index 6d36c133b3..6f25298010 100644 --- a/tests/files/.eslintrc +++ b/tests/files/.eslintrc.js @@ -1,4 +1,9 @@ -{ +const eslintPkg = require('eslint/package.json'); +const semver = require('semver'); + +const supportsArbitraryModuleNamespaceIdentifierNames = semver.satisfies(eslintPkg.version, '>= 8.7'); + +const config = { "parser": "babel-eslint", "parserOptions": { "sourceType": "module", @@ -281,5 +286,36 @@ "import/no-duplicates": 0, "import/no-extraneous-dependencies": 0, "import/unambiguous": 0 - } + }, + ignorePatterns: [ + "default-export-namespace-string.js", + "default-export-string.js", + "export-default-string-and-named.js", + "no-unused-modules/arbitrary-module-namespace-identifier-name-a.js", + "no-unused-modules/arbitrary-module-namespace-identifier-name-b.js", + "no-unused-modules/arbitrary-module-namespace-identifier-name-c.js" + ], +} + +if (supportsArbitraryModuleNamespaceIdentifierNames) { + config.ignorePatterns = []; + config.overrides = [ + // For parsing arbitrary module namespace names + { + "files": [ + "default-export-namespace-string.js", + "default-export-string.js", + "export-default-string-and-named.js", + "no-unused-modules/arbitrary-module-namespace-identifier-name-a.js", + "no-unused-modules/arbitrary-module-namespace-identifier-name-b.js", + "no-unused-modules/arbitrary-module-namespace-identifier-name-c.js" + ], + "parser": "espree", + "parserOptions": { + "ecmaVersion": 2022 + } + } + ]; } + +module.exports = config; diff --git a/tests/files/default-export-namespace-string.js b/tests/files/default-export-namespace-string.js new file mode 100644 index 0000000000..5b4a01ab72 --- /dev/null +++ b/tests/files/default-export-namespace-string.js @@ -0,0 +1 @@ +export * as "default" from "./named-exports"; diff --git a/tests/files/default-export-string.js b/tests/files/default-export-string.js new file mode 100644 index 0000000000..4f68b517e6 --- /dev/null +++ b/tests/files/default-export-string.js @@ -0,0 +1,3 @@ +function foo() { return 'bar' } + +export { foo as "default" } diff --git a/tests/files/dynamic-import-in-commonjs.js b/tests/files/dynamic-import-in-commonjs.js new file mode 100644 index 0000000000..259feb4cd9 --- /dev/null +++ b/tests/files/dynamic-import-in-commonjs.js @@ -0,0 +1,5 @@ +async function doSomething() { + await import('./bar.js'); +} + +exports.something = 'hello'; diff --git a/tests/files/export-default-string-and-named.js b/tests/files/export-default-string-and-named.js new file mode 100644 index 0000000000..62c7d13c4f --- /dev/null +++ b/tests/files/export-default-string-and-named.js @@ -0,0 +1,4 @@ +const bar = "bar"; +export function foo() {} + +export { bar as "default" } diff --git a/tests/files/no-unused-modules/arbitrary-module-namespace-identifier-name-a.js b/tests/files/no-unused-modules/arbitrary-module-namespace-identifier-name-a.js new file mode 100644 index 0000000000..7ad810de86 --- /dev/null +++ b/tests/files/no-unused-modules/arbitrary-module-namespace-identifier-name-a.js @@ -0,0 +1,2 @@ +const foo = 333 +export { foo as "foo" } diff --git a/tests/files/no-unused-modules/arbitrary-module-namespace-identifier-name-b.js b/tests/files/no-unused-modules/arbitrary-module-namespace-identifier-name-b.js new file mode 100644 index 0000000000..fa7652725e --- /dev/null +++ b/tests/files/no-unused-modules/arbitrary-module-namespace-identifier-name-b.js @@ -0,0 +1 @@ +import { "foo" as foo } from "./arbitrary-module-namespace-identifier-name-a.js" diff --git a/tests/files/no-unused-modules/arbitrary-module-namespace-identifier-name-c.js b/tests/files/no-unused-modules/arbitrary-module-namespace-identifier-name-c.js new file mode 100644 index 0000000000..7ad810de86 --- /dev/null +++ b/tests/files/no-unused-modules/arbitrary-module-namespace-identifier-name-c.js @@ -0,0 +1,2 @@ +const foo = 333 +export { foo as "foo" } diff --git a/tests/files/unused-modules-reexport-crash/src/App.tsx b/tests/files/unused-modules-reexport-crash/src/App.tsx new file mode 100644 index 0000000000..c797a976cb --- /dev/null +++ b/tests/files/unused-modules-reexport-crash/src/App.tsx @@ -0,0 +1,5 @@ +import { hello } from './magic/test' + +hello(); + +export default function App() {}; diff --git a/tests/files/unused-modules-reexport-crash/src/index.tsx b/tests/files/unused-modules-reexport-crash/src/index.tsx new file mode 100644 index 0000000000..124b1745d2 --- /dev/null +++ b/tests/files/unused-modules-reexport-crash/src/index.tsx @@ -0,0 +1,3 @@ +import App from './App'; + +export const x = App \ No newline at end of file diff --git a/tests/files/unused-modules-reexport-crash/src/magic/index.js b/tests/files/unused-modules-reexport-crash/src/magic/index.js new file mode 100644 index 0000000000..ac3f46bb1b --- /dev/null +++ b/tests/files/unused-modules-reexport-crash/src/magic/index.js @@ -0,0 +1 @@ +export * from './test' diff --git a/tests/files/unused-modules-reexport-crash/src/magic/test.js b/tests/files/unused-modules-reexport-crash/src/magic/test.js new file mode 100644 index 0000000000..a6d74afd9b --- /dev/null +++ b/tests/files/unused-modules-reexport-crash/src/magic/test.js @@ -0,0 +1,7 @@ +export function hello() { + console.log('hello!!'); +} + +export function unused() { + console.log('im unused!!'); +} \ No newline at end of file diff --git a/tests/src/cli.js b/tests/src/cli.js index 5b7e705b0e..e6afd8e441 100644 --- a/tests/src/cli.js +++ b/tests/src/cli.js @@ -104,6 +104,9 @@ describe('CLI regression tests', function () { fixableErrorCount: 0, fixableWarningCount: 0, source: results[0].source, // NewLine-characters might differ depending on git-settings + ...(semver.satisfies(eslintPkg.version, '>= 8.8') && { + suppressedMessages: [], + }), usedDeprecatedRules: results[0].usedDeprecatedRules, // we don't care about this one }, ], diff --git a/tests/src/core/getExports.js b/tests/src/core/getExports.js index 604ae5cf28..867644bc19 100644 --- a/tests/src/core/getExports.js +++ b/tests/src/core/getExports.js @@ -343,7 +343,6 @@ describe('ExportMap', function () { }); context('alternate parsers', function () { - const configs = [ // ['string form', { 'typescript-eslint-parser': '.ts' }], ]; diff --git a/tests/src/rules/default.js b/tests/src/rules/default.js index 0274e43745..eb2028c71a 100644 --- a/tests/src/rules/default.js +++ b/tests/src/rules/default.js @@ -1,6 +1,8 @@ import path from 'path'; -import { test, SYNTAX_CASES, getTSParsers } from '../utils'; +import { test, testVersion, SYNTAX_CASES, getTSParsers, parsers } from '../utils'; import { RuleTester } from 'eslint'; +import semver from 'semver'; +import { version as tsEslintVersion } from 'typescript-eslint-parser/package.json'; import { CASE_SENSITIVE_FS } from 'eslint-module-utils/resolve'; @@ -8,7 +10,7 @@ const ruleTester = new RuleTester(); const rule = require('rules/default'); ruleTester.run('default', rule, { - valid: [ + valid: [].concat( test({ code: 'import "./malformed.js"' }), test({ code: 'import foo from "./empty-folder";' }), @@ -29,19 +31,19 @@ ruleTester.run('default', rule, { // es7 export syntax test({ code: 'export bar from "./bar"', - parser: require.resolve('babel-eslint') }), + parser: parsers.BABEL_OLD }), test({ code: 'export { default as bar } from "./bar"' }), test({ code: 'export bar, { foo } from "./bar"', - parser: require.resolve('babel-eslint') }), + parser: parsers.BABEL_OLD }), test({ code: 'export { default as bar, foo } from "./bar"' }), test({ code: 'export bar, * as names from "./bar"', - parser: require.resolve('babel-eslint') }), + parser: parsers.BABEL_OLD }), // sanity check test({ code: 'export {a} from "./named-exports"' }), test({ code: 'import twofer from "./trampoline"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), // jsx @@ -69,31 +71,39 @@ ruleTester.run('default', rule, { // from no-errors test({ code: "import Foo from './jsx/FooES7.js';", - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), // #545: more ES7 cases test({ code: "import bar from './default-export-from.js';", - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: "import bar from './default-export-from-named.js';", - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: "import bar from './default-export-from-ignored.js';", settings: { 'import/ignore': ['common'] }, - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: "export bar from './default-export-from-ignored.js';", settings: { 'import/ignore': ['common'] }, - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), + // es2022: Arbitrary module namespace identifier names + testVersion('>= 8.7', () => ({ + code: 'export { "default" as bar } from "./bar"', + parserOptions: { + ecmaVersion: 2022, + }, + })), + ...SYNTAX_CASES, - ], + ), invalid: [ test({ @@ -109,23 +119,23 @@ ruleTester.run('default', rule, { // es7 export syntax test({ code: 'export baz from "./named-exports"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, errors: ['No default export found in imported module "./named-exports".'], }), test({ code: 'export baz, { bar } from "./named-exports"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, errors: ['No default export found in imported module "./named-exports".'], }), test({ code: 'export baz, * as names from "./named-exports"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, errors: ['No default export found in imported module "./named-exports".'], }), // exports default from a module with no default test({ code: 'import twofer from "./broken-trampoline"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, errors: ['No default export found in imported module "./broken-trampoline".'], }), @@ -157,7 +167,7 @@ if (!CASE_SENSITIVE_FS) { context('TypeScript', function () { getTSParsers().forEach((parser) => { ruleTester.run(`default`, rule, { - valid: [ + valid: [].concat( test({ code: `import foobar from "./typescript-default"`, parser, @@ -182,14 +192,14 @@ context('TypeScript', function () { 'import/resolver': { 'eslint-import-resolver-typescript': true }, }, }), - test({ + semver.satisfies(tsEslintVersion, '>= 22') ? test({ code: `import foobar from "./typescript-export-assign-mixed"`, parser, settings: { 'import/parsers': { [parser]: ['.ts'] }, 'import/resolver': { 'eslint-import-resolver-typescript': true }, }, - }), + }) : [], test({ code: `import foobar from "./typescript-export-assign-default-reexport"`, parser, @@ -250,7 +260,7 @@ context('TypeScript', function () { 'import/resolver': { 'eslint-import-resolver-typescript': true }, }, }), - ], + ), invalid: [ test({ diff --git a/tests/src/rules/dynamic-import-chunkname.js b/tests/src/rules/dynamic-import-chunkname.js index 4c5c2f4716..46a1b97afe 100644 --- a/tests/src/rules/dynamic-import-chunkname.js +++ b/tests/src/rules/dynamic-import-chunkname.js @@ -1,4 +1,4 @@ -import { SYNTAX_CASES, getTSParsers } from '../utils'; +import { SYNTAX_CASES, getTSParsers, parsers } from '../utils'; import { RuleTester } from 'eslint'; import semver from 'semver'; @@ -15,7 +15,7 @@ const pickyCommentOptions = [{ const multipleImportFunctionOptions = [{ importFunctions: ['dynamicImport', 'definitelyNotStaticImport'], }]; -const parser = require.resolve('babel-eslint'); +const parser = parsers.BABEL_OLD; const noLeadingCommentError = 'dynamic imports require a leading comment with the webpack chunkname'; const nonBlockCommentError = 'dynamic imports require a /* foo */ style comment, not a // foo comment'; @@ -969,7 +969,7 @@ ruleTester.run('dynamic-import-chunkname', rule, { context('TypeScript', () => { getTSParsers().forEach((typescriptParser) => { - const nodeType = typescriptParser.includes('typescript-eslint-parser') || (typescriptParser.includes('@typescript-eslint/parser') && semver.satisfies(require('@typescript-eslint/parser/package.json').version, '^2')) + const nodeType = typescriptParser === parsers.TS_OLD || (typescriptParser === parsers.TS_NEW && semver.satisfies(require('@typescript-eslint/parser/package.json').version, '^2')) ? 'CallExpression' : 'ImportExpression'; diff --git a/tests/src/rules/export.js b/tests/src/rules/export.js index 29fae41015..5996e9fa3f 100644 --- a/tests/src/rules/export.js +++ b/tests/src/rules/export.js @@ -3,6 +3,7 @@ import { test, testFilePath, SYNTAX_CASES, getTSParsers, testVersion } from '../ import { RuleTester } from 'eslint'; import eslintPkg from 'eslint/package.json'; import semver from 'semver'; +import { version as tsEslintVersion } from 'typescript-eslint-parser/package.json'; const ruleTester = new RuleTester(); const rule = require('rules/export'); @@ -25,7 +26,7 @@ ruleTester.run('export', rule, { // #328: "export * from" does not export a default test({ code: 'export default foo; export * from "./bar"' }), - ...SYNTAX_CASES, + SYNTAX_CASES, test({ code: ` @@ -46,7 +47,7 @@ ruleTester.run('export', rule, { })) || [], ), - invalid: [ + invalid: [].concat( // multiple defaults // test({ // code: 'export default foo; export default bar', @@ -81,22 +82,31 @@ ruleTester.run('export', rule, { // }), test({ code: 'let foo; export { foo }; export * from "./export-all"', - errors: ['Multiple exports of name \'foo\'.', - 'Multiple exports of name \'foo\'.'], + errors: [ + 'Multiple exports of name \'foo\'.', + 'Multiple exports of name \'foo\'.', + ], }), - // test({ code: 'export * from "./default-export"' - // , errors: [{ message: 'No named exports found in module ' + - // '\'./default-export\'.' - // , type: 'Literal' }] }), + // test({ + // code: 'export * from "./default-export"', + // errors: [ + // { + // message: 'No named exports found in module \'./default-export\'.', + // type: 'Literal', + // }, + // ], + // }), // note: Espree bump to Acorn 4+ changed this test's error message. // `npm up` first if it's failing. test({ code: 'export * from "./malformed.js"', - errors: [{ - message: "Parse errors in imported module './malformed.js': 'return' outside of function (1:1)", - type: 'Literal', - }], + errors: [ + { + message: "Parse errors in imported module './malformed.js': 'return' outside of function (1:1)", + type: 'Literal', + }, + ], }), // test({ @@ -122,7 +132,19 @@ ruleTester.run('export', rule, { code: 'export * from "./default-export"', errors: [`No named exports found in module './default-export'.`], }), - ], + + // es2022: Arbitrary module namespace identifier names + testVersion('>= 8.7', () => ({ + code: 'let foo; export { foo as "foo" }; export * from "./export-all"', + errors: [ + 'Multiple exports of name \'foo\'.', + 'Multiple exports of name \'foo\'.', + ], + parserOptions: { + ecmaVersion: 2022, + }, + })), + ), }); @@ -137,54 +159,60 @@ context('TypeScript', function () { }; ruleTester.run('export', rule, { - valid: [ + valid: [].concat( // type/value name clash - test(Object.assign({ + test({ code: ` export const Foo = 1; export type Foo = number; `, - }, parserConfig)), - test(Object.assign({ + ...parserConfig, + }), + test({ code: ` export const Foo = 1; export interface Foo {} `, - }, parserConfig)), + ...parserConfig, + }), - test(Object.assign({ + semver.satisfies(tsEslintVersion, '>= 22') ? test({ code: ` export function fff(a: string); export function fff(a: number); `, - }, parserConfig)), + ...parserConfig, + }) : [], - test(Object.assign({ + semver.satisfies(tsEslintVersion, '>= 22') ? test({ code: ` export function fff(a: string); export function fff(a: number); export function fff(a: string|number) {}; `, - }, parserConfig)), + ...parserConfig, + }) : [], // namespace - test(Object.assign({ + test({ code: ` export const Bar = 1; export namespace Foo { export const Bar = 1; } `, - }, parserConfig)), - test(Object.assign({ + ...parserConfig, + }), + test({ code: ` export type Bar = string; export namespace Foo { export type Bar = string; } `, - }, parserConfig)), - test(Object.assign({ + ...parserConfig, + }), + test({ code: ` export const Bar = 1; export type Bar = string; @@ -193,8 +221,9 @@ context('TypeScript', function () { export type Bar = string; } `, - }, parserConfig)), - test(Object.assign({ + ...parserConfig, + }), + test({ code: ` export namespace Foo { export const Foo = 1; @@ -206,13 +235,56 @@ context('TypeScript', function () { } } `, - }, parserConfig)), - test(Object.assign({ + ...parserConfig, + }), + semver.satisfies(eslintPkg.version, '>= 6') ? [ + test({ + code: ` + export class Foo { } + export namespace Foo { } + export namespace Foo { + export class Bar {} + } + `, + ...parserConfig, + }), + test({ + code: ` + export function Foo(); + export namespace Foo { } + `, + ...parserConfig, + }), + test({ + code: ` + export function Foo(a: string); + export namespace Foo { } + `, + ...parserConfig, + }), + test({ + code: ` + export function Foo(a: string); + export function Foo(a: number); + export namespace Foo { } + `, + ...parserConfig, + }), + test({ + code: ` + export enum Foo { } + export namespace Foo { } + `, + ...parserConfig, + }), + ] : [], + test({ code: 'export * from "./file1.ts"', filename: testFilePath('typescript-d-ts/file-2.ts'), - }, parserConfig)), + ...parserConfig, + }), - ...(semver.satisfies(eslintPkg.version, '< 6') ? [] : [ + semver.satisfies(eslintPkg.version, '>= 6') ? [ test({ code: ` export * as A from './named-export-collision/a'; @@ -220,10 +292,10 @@ context('TypeScript', function () { `, parser, }), - ]), + ] : [], // Exports in ambient modules - test(Object.assign({ + test({ code: ` declare module "a" { const Foo = 1; @@ -234,8 +306,9 @@ context('TypeScript', function () { export {Bar as default}; } `, - }, parserConfig)), - test(Object.assign({ + ...parserConfig, + }), + test({ code: ` declare module "a" { const Foo = 1; @@ -244,9 +317,10 @@ context('TypeScript', function () { const Bar = 2; export {Bar as default}; `, - }, parserConfig)), + ...parserConfig, + }), - ...(semver.satisfies(process.version, '< 8') && semver.satisfies(eslintPkg.version, '< 6') ? [] : test({ + semver.satisfies(process.version, '< 8') && semver.satisfies(eslintPkg.version, '< 6') ? [] : test({ ...parserConfig, code: ` export * from './module'; @@ -256,11 +330,11 @@ context('TypeScript', function () { ...parserConfig.settings, 'import/extensions': ['.js', '.ts', '.jsx'], }, - })), - ], - invalid: [ + }), + ), + invalid: [].concat( // type/value name clash - test(Object.assign({ + test({ code: ` export type Foo = string; export type Foo = number; @@ -275,10 +349,11 @@ context('TypeScript', function () { line: 3, }, ], - }, parserConfig)), + ...parserConfig, + }), // namespace - test(Object.assign({ + test({ code: ` export const a = 1 export namespace Foo { @@ -296,8 +371,9 @@ context('TypeScript', function () { line: 5, }, ], - }, parserConfig)), - test(Object.assign({ + ...parserConfig, + }), + test({ code: ` declare module 'foo' { const Foo = 1; @@ -315,8 +391,9 @@ context('TypeScript', function () { line: 5, }, ], - }, parserConfig)), - test(Object.assign({ + ...parserConfig, + }), + test({ code: ` export namespace Foo { export namespace Bar { @@ -347,10 +424,138 @@ context('TypeScript', function () { line: 9, }, ], - }, parserConfig)), + ...parserConfig, + }), + semver.satisfies(eslintPkg.version, '< 6') ? [] : [ + test({ + code: ` + export class Foo { } + export class Foo { } + export namespace Foo { } + `, + errors: [ + { + message: `Multiple exports of name 'Foo'.`, + line: 2, + }, + { + message: `Multiple exports of name 'Foo'.`, + line: 3, + }, + ], + ...parserConfig, + }), + test({ + code: ` + export enum Foo { } + export enum Foo { } + export namespace Foo { } + `, + errors: [ + { + message: `Multiple exports of name 'Foo'.`, + line: 2, + }, + { + message: `Multiple exports of name 'Foo'.`, + line: 3, + }, + ], + ...parserConfig, + }), + test({ + code: ` + export enum Foo { } + export class Foo { } + export namespace Foo { } + `, + errors: [ + { + message: `Multiple exports of name 'Foo'.`, + line: 2, + }, + { + message: `Multiple exports of name 'Foo'.`, + line: 3, + }, + ], + ...parserConfig, + }), + test({ + code: ` + export const Foo = 'bar'; + export class Foo { } + export namespace Foo { } + `, + errors: [ + { + message: `Multiple exports of name 'Foo'.`, + line: 2, + }, + { + message: `Multiple exports of name 'Foo'.`, + line: 3, + }, + ], + ...parserConfig, + }), + test({ + code: ` + export function Foo(); + export class Foo { } + export namespace Foo { } + `, + errors: [ + { + message: `Multiple exports of name 'Foo'.`, + line: 2, + }, + { + message: `Multiple exports of name 'Foo'.`, + line: 3, + }, + ], + ...parserConfig, + }), + test({ + code: ` + export const Foo = 'bar'; + export function Foo(); + export namespace Foo { } + `, + errors: [ + { + message: `Multiple exports of name 'Foo'.`, + line: 2, + }, + { + message: `Multiple exports of name 'Foo'.`, + line: 3, + }, + ], + ...parserConfig, + }), + test({ + code: ` + export const Foo = 'bar'; + export namespace Foo { } + `, + errors: [ + { + message: `Multiple exports of name 'Foo'.`, + line: 2, + }, + { + message: `Multiple exports of name 'Foo'.`, + line: 3, + }, + ], + ...parserConfig, + }), + ], // Exports in ambient modules - test(Object.assign({ + test({ code: ` declare module "a" { const Foo = 1; @@ -371,8 +576,9 @@ context('TypeScript', function () { line: 9, }, ], - }, parserConfig)), - ], + ...parserConfig, + }), + ), }); }); }); diff --git a/tests/src/rules/extensions.js b/tests/src/rules/extensions.js index 93ebc28f8e..cf93fac9f4 100644 --- a/tests/src/rules/extensions.js +++ b/tests/src/rules/extensions.js @@ -1,6 +1,6 @@ import { RuleTester } from 'eslint'; import rule from 'rules/extensions'; -import { getTSParsers, test, testFilePath } from '../utils'; +import { getTSParsers, test, testFilePath, parsers } from '../utils'; const ruleTester = new RuleTester(); @@ -601,7 +601,7 @@ ruleTester.run('extensions', rule, { describe('TypeScript', () => { getTSParsers() // Type-only imports were added in TypeScript ESTree 2.23.0 - .filter((parser) => parser !== require.resolve('typescript-eslint-parser')) + .filter((parser) => parser !== parsers.TS_OLD) .forEach((parser) => { ruleTester.run(`${parser}: extensions ignore type-only`, rule, { valid: [ diff --git a/tests/src/rules/max-dependencies.js b/tests/src/rules/max-dependencies.js index 6d80bbf046..982a4b427a 100644 --- a/tests/src/rules/max-dependencies.js +++ b/tests/src/rules/max-dependencies.js @@ -1,4 +1,4 @@ -import { test, getTSParsers } from '../utils'; +import { test, getTSParsers, parsers } from '../utils'; import { RuleTester } from 'eslint'; @@ -66,7 +66,7 @@ ruleTester.run('max-dependencies', rule, { test({ code: 'import type { x } from \'./foo\'; import type { y } from \'./bar\'', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, options: [{ max: 1, }], @@ -77,7 +77,7 @@ ruleTester.run('max-dependencies', rule, { test({ code: 'import type { x } from \'./foo\'; import type { y } from \'./bar\'; import type { z } from \'./baz\'', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, options: [{ max: 2, ignoreTypeImports: false, @@ -92,7 +92,7 @@ ruleTester.run('max-dependencies', rule, { describe('TypeScript', () => { getTSParsers() // Type-only imports were added in TypeScript ESTree 2.23.0 - .filter((parser) => parser !== require.resolve('typescript-eslint-parser')) + .filter((parser) => parser !== parsers.TS_OLD) .forEach((parser) => { ruleTester.run(`max-dependencies (${parser.replace(process.cwd(), '.')})`, rule, { valid: [ diff --git a/tests/src/rules/named.js b/tests/src/rules/named.js index 992baa0fd0..b5500a6d31 100644 --- a/tests/src/rules/named.js +++ b/tests/src/rules/named.js @@ -1,4 +1,4 @@ -import { test, SYNTAX_CASES, getTSParsers, testFilePath, testVersion } from '../utils'; +import { test, SYNTAX_CASES, getTSParsers, testFilePath, testVersion, parsers } from '../utils'; import { RuleTester } from 'eslint'; import { CASE_SENSITIVE_FS } from 'eslint-module-utils/resolve'; @@ -7,9 +7,8 @@ import { CASE_SENSITIVE_FS } from 'eslint-module-utils/resolve'; const ruleTester = new RuleTester(); const rule = require('rules/named'); -function error(name, module) { - return { message: name + ' not found in \'' + module + '\'', - type: 'Identifier' }; +function error(name, module, type = 'Identifier') { + return { message: name + ' not found in \'' + module + '\'', type }; } ruleTester.run('named', rule, { @@ -55,11 +54,11 @@ ruleTester.run('named', rule, { // es7 test({ code: 'export bar, { foo } from "./bar"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: 'import { foo, bar } from "./named-trampoline"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), // regression tests @@ -74,43 +73,43 @@ ruleTester.run('named', rule, { // should ignore imported/exported flow types, even if they don’t exist test({ code: 'import type { MissingType } from "./flowtypes"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: 'import typeof { MissingType } from "./flowtypes"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: 'import type { MyOpaqueType } from "./flowtypes"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: 'import typeof { MyOpaqueType } from "./flowtypes"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: 'import { type MyOpaqueType, MyClass } from "./flowtypes"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: 'import { typeof MyOpaqueType, MyClass } from "./flowtypes"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: 'import typeof MissingType from "./flowtypes"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: 'import typeof * as MissingType from "./flowtypes"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: 'export type { MissingType } from "./flowtypes"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: 'export type { MyOpaqueType } from "./flowtypes"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), // jsnext @@ -190,10 +189,25 @@ ruleTester.run('named', rule, { sourceType: 'module', ecmaVersion: 2020, }, - })) || []), + })), + + testVersion('>=7.8.0', () =>({ code: 'const { something } = require("./dynamic-import-in-commonjs")', + parserOptions: { ecmaVersion: 2021 }, + options: [{ commonjs: true }], + })), + + testVersion('>=7.8.0', () => ({ code: 'import { something } from "./dynamic-import-in-commonjs"', + parserOptions: { ecmaVersion: 2021 } })), + + // es2022: Arbitrary module namespace identifier names + testVersion('>= 8.7', () => ({ + code: 'import { "foo" as foo } from "./bar"', parserOptions: { ecmaVersion: 2022 } })), + testVersion('>= 8.7', () => ({ + code: 'import { "foo" as foo } from "./empty-module"', parserOptions: { ecmaVersion: 2022 } })), + ), ], - invalid: [ + invalid: [].concat( test({ code: 'import { somethingElse } from "./test-module"', errors: [ error('somethingElse', './test-module') ] }), @@ -230,17 +244,17 @@ ruleTester.run('named', rule, { // es7 test({ code: 'export bar2, { bar } from "./bar"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, errors: ["bar not found in './bar'"], }), test({ code: 'import { foo, bar, baz } from "./named-trampoline"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, errors: ["baz not found in './named-trampoline'"], }), test({ code: 'import { baz } from "./broken-trampoline"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, errors: ['baz not found via broken-trampoline.js -> named-exports.js'], }), @@ -280,7 +294,7 @@ ruleTester.run('named', rule, { test({ code: 'import { type MyOpaqueType, MyMissingClass } from "./flowtypes"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, errors: ["MyMissingClass not found in './flowtypes'"], }), @@ -314,7 +328,24 @@ ruleTester.run('named', rule, { code: 'import { default as barDefault } from "./re-export"', errors: [`default not found in './re-export'`], }), - ], + + // es2022: Arbitrary module namespace identifier names + testVersion('>= 8.7', () => ({ + code: 'import { "somethingElse" as somethingElse } from "./test-module"', + errors: [ error('somethingElse', './test-module', 'Literal') ], + parserOptions: { ecmaVersion: 2022 }, + })), + testVersion('>= 8.7', () => ({ + code: 'import { "baz" as baz, "bop" as bop } from "./bar"', + errors: [error('baz', './bar', 'Literal'), error('bop', './bar', 'Literal')], + parserOptions: { ecmaVersion: 2022 }, + })), + testVersion('>= 8.7', () => ({ + code: 'import { "default" as barDefault } from "./re-export"', + errors: [`default not found in './re-export'`], + parserOptions: { ecmaVersion: 2022 }, + })), + ), }); // #311: import of mismatched case diff --git a/tests/src/rules/namespace.js b/tests/src/rules/namespace.js index 826637b970..1465d21363 100644 --- a/tests/src/rules/namespace.js +++ b/tests/src/rules/namespace.js @@ -1,4 +1,4 @@ -import { test, SYNTAX_CASES, getTSParsers, testVersion, testFilePath } from '../utils'; +import { test, SYNTAX_CASES, getTSParsers, testVersion, testFilePath, parsers } from '../utils'; import { RuleTester } from 'eslint'; import flatMap from 'array.prototype.flatmap'; @@ -57,16 +57,16 @@ const valid = [ // es7 // ///////// test({ code: 'export * as names from "./named-exports"', - parser: require.resolve('babel-eslint') }), + parser: parsers.BABEL_OLD }), test({ code: 'export defport, * as names from "./named-exports"', - parser: require.resolve('babel-eslint') }), + parser: parsers.BABEL_OLD }), // non-existent is handled by no-unresolved test({ code: 'export * as names from "./does-not-exist"', - parser: require.resolve('babel-eslint') }), + parser: parsers.BABEL_OLD }), test({ code: 'import * as Endpoints from "./issue-195/Endpoints"; console.log(Endpoints.Users)', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), // respect hoisting @@ -81,11 +81,11 @@ const valid = [ test({ code: "import * as names from './default-export'; console.log(names.default)" }), test({ code: 'export * as names from "./default-export"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: 'export defport, * as names from "./default-export"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), // #456: optionally ignore computed references @@ -103,7 +103,7 @@ const valid = [ }), test({ code: `import * as names from './named-exports'; const {a, b, ...rest} = names;`, - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), // #1144: should handle re-export CommonJS as namespace @@ -183,10 +183,35 @@ const valid = [ parserOptions: { ecmaVersion: 2020, }, - })) || []), + })), + // es2022: Arbitrary module namespace identifier names + testVersion('>= 8.7', () => ({ + code: "import * as names from './default-export-string';", + parserOptions: { ecmaVersion: 2022 }, + })), + testVersion('>= 8.7', () => ({ + code: "import * as names from './default-export-string'; console.log(names.default)", + parserOptions: { ecmaVersion: 2022 }, + })), + testVersion('>= 8.7', () => ({ + code: "import * as names from './default-export-namespace-string';", + parserOptions: { ecmaVersion: 2022 }, + })), + testVersion('>= 8.7', () => ({ + code: "import * as names from './default-export-namespace-string'; console.log(names.default)", + parserOptions: { ecmaVersion: 2022 }, + })), + testVersion('>= 8.7', () => ({ + code: `import { "b" as b } from "./deep/a"; console.log(b.c.d.e)`, + parserOptions: { ecmaVersion: 2022 }, + })), + testVersion('>= 8.7', () => ({ + code: `import { "b" as b } from "./deep/a"; var {c:{d:{e}}} = b`, + parserOptions: { ecmaVersion: 2022 }, + }))), ]; -const invalid = [ +const invalid = [].concat( test({ code: "import * as names from './named-exports'; " + ' console.log(names.c);', errors: [error('c', 'names')] }), @@ -226,7 +251,7 @@ const invalid = [ test({ code: 'import * as Endpoints from "./issue-195/Endpoints"; console.log(Endpoints.Foo)', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, errors: ["'Foo' not found in imported namespace 'Endpoints'."], }), @@ -275,12 +300,23 @@ const invalid = [ }, }), -] + // es2022: Arbitrary module namespace identifier names + testVersion('>= 8.7', () => ({ + code: `import { "b" as b } from "./deep/a"; console.log(b.e)`, + errors: [ "'e' not found in imported namespace 'b'." ], + parserOptions: { ecmaVersion: 2022 }, + })), + testVersion('>= 8.7', () => ({ + code: `import { "b" as b } from "./deep/a"; console.log(b.c.e)`, + errors: [ "'e' not found in deeply imported namespace 'b.c'." ], + parserOptions: { ecmaVersion: 2022 }, + })), +) /////////////////////// // deep dereferences // ////////////////////// -;[['deep', require.resolve('espree')], ['deep-es7', require.resolve('babel-eslint')]].forEach(function ([folder, parser]) { // close over params +;[['deep', require.resolve('espree')], ['deep-es7', parsers.BABEL_OLD]].forEach(function ([folder, parser]) { // close over params valid.push( test({ parser, code: `import * as a from "./${folder}/a"; console.log(a.b.c.d.e)` }), test({ parser, code: `import { b } from "./${folder}/a"; console.log(b.c.d.e)` }), diff --git a/tests/src/rules/newline-after-import.js b/tests/src/rules/newline-after-import.js index a00e86900f..80cc076ced 100644 --- a/tests/src/rules/newline-after-import.js +++ b/tests/src/rules/newline-after-import.js @@ -1,7 +1,9 @@ import { RuleTester } from 'eslint'; import flatMap from 'array.prototype.flatmap'; +import semver from 'semver'; +import { version as tsEslintVersion } from 'typescript-eslint-parser/package.json'; -import { getTSParsers, testVersion } from '../utils'; +import { getTSParsers, parsers, testVersion } from '../utils'; const IMPORT_ERROR_MESSAGE = 'Expected 1 empty line after import statement not followed by another import.'; const IMPORT_ERROR_MESSAGE_MULTIPLE = (count) => { @@ -12,7 +14,7 @@ const REQUIRE_ERROR_MESSAGE = 'Expected 1 empty line after require statement not const ruleTester = new RuleTester(); ruleTester.run('newline-after-import', require('rules/newline-after-import'), { - valid: [ + valid: [].concat( `var path = require('path');\nvar foo = require('foo');\n`, `require('foo');`, `switch ('foo') { case 'bar': require('baz'); }`, @@ -161,24 +163,24 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { class App {} `, parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }, { code: `var foo = require('foo');\n\n@SomeDecorator(foo)\nclass Foo {}`, parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }, { code : `// issue 1004\nimport foo from 'foo';\n\n@SomeDecorator(foo)\nexport default class Test {}`, parserOptions: { sourceType: 'module' }, - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }, { code : `// issue 1004\nconst foo = require('foo');\n\n@SomeDecorator(foo)\nexport default class Test {}`, parserOptions: { sourceType: 'module' }, - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }, - ...flatMap(getTSParsers(), (parser) => [ + flatMap(getTSParsers(), (parser) => [].concat( { code: ` import { ExecaReturnValue } from 'execa'; @@ -213,14 +215,14 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { parser, parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, }, - { + parser !== parsers.TS_OLD || semver.satisfies(tsEslintVersion, '>= 22') ? { code: ` export import a = obj;\nf(a); `, parser, parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, - }, - { + } : [], + parser !== parsers.TS_OLD || semver.satisfies(tsEslintVersion, '>= 22') ? { code: ` import { a } from "./a"; @@ -230,7 +232,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { }`, parser, parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, - }, + } : [], { code: ` import stub from './stub'; @@ -242,7 +244,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { parser, parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, }, - ]), + )), { code: ` import stub from './stub'; @@ -253,7 +255,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { `, parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, }, - ], + ), invalid: [].concat( { @@ -415,7 +417,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { message: IMPORT_ERROR_MESSAGE, } ], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }, { code: `var foo = require('foo');\n@SomeDecorator(foo)\nclass Foo {}`, @@ -426,7 +428,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { message: REQUIRE_ERROR_MESSAGE, } ], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }, { code: `// issue 10042\nimport foo from 'foo';\n@SomeDecorator(foo)\nexport default class Test {}`, @@ -437,7 +439,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { message: IMPORT_ERROR_MESSAGE, } ], parserOptions: { sourceType: 'module' }, - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }, { code: `// issue 1004\nconst foo = require('foo');\n@SomeDecorator(foo)\nexport default class Test {}`, @@ -448,7 +450,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { message: REQUIRE_ERROR_MESSAGE, } ], parserOptions: { sourceType: 'module' }, - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }, testVersion('>= 6', () => ({ code: ` @@ -472,7 +474,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { }, ], parserOptions: { sourceType: 'module' }, - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, })) || [], ), }); diff --git a/tests/src/rules/no-anonymous-default-export.js b/tests/src/rules/no-anonymous-default-export.js index 231f1b667d..0428ee1b99 100644 --- a/tests/src/rules/no-anonymous-default-export.js +++ b/tests/src/rules/no-anonymous-default-export.js @@ -1,4 +1,4 @@ -import { test, SYNTAX_CASES } from '../utils'; +import { test, testVersion, SYNTAX_CASES } from '../utils'; import { RuleTester } from 'eslint'; @@ -6,7 +6,7 @@ const ruleTester = new RuleTester(); const rule = require('rules/no-anonymous-default-export'); ruleTester.run('no-anonymous-default-export', rule, { - valid: [ + valid: [].concat( // Exports with identifiers are valid test({ code: 'const foo = 123\nexport default foo' }), test({ code: 'export default function foo() {}' }), @@ -31,12 +31,17 @@ ruleTester.run('no-anonymous-default-export', rule, { test({ code: 'export * from \'foo\'' }), test({ code: 'const foo = 123\nexport { foo }' }), test({ code: 'const foo = 123\nexport { foo as default }' }), + // es2022: Arbitrary module namespace identifier names + testVersion('>= 8.7', () => ({ + code: 'const foo = 123\nexport { foo as "default" }', + parserOptions: { ecmaVersion: 2022 }, + })), // Allow call expressions by default for backwards compatibility test({ code: 'export default foo(bar)' }), ...SYNTAX_CASES, - ], + ), invalid: [ test({ code: 'export default []', errors: [{ message: 'Assign array to a variable before exporting as module default' }] }), diff --git a/tests/src/rules/no-cycle.js b/tests/src/rules/no-cycle.js index c4e3235e78..22e097dd2c 100644 --- a/tests/src/rules/no-cycle.js +++ b/tests/src/rules/no-cycle.js @@ -1,4 +1,4 @@ -import { test as _test, testFilePath } from '../utils'; +import { parsers, test as _test, testFilePath } from '../utils'; import { RuleTester } from 'eslint'; import flatMap from 'array.prototype.flatmap'; @@ -63,36 +63,36 @@ ruleTester.run('no-cycle', rule, { test({ code: `import("./${testDialect}/depth-two").then(function({ foo }) {})`, options: [{ maxDepth: 1 }], - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: `import type { FooType } from "./${testDialect}/depth-one"`, - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: `import type { FooType, BarType } from "./${testDialect}/depth-one"`, - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), ]), test({ code: 'import { bar } from "./flow-types"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: 'import { bar } from "./flow-types-only-importing-type"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: 'import { bar } from "./flow-types-only-importing-multiple-types"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), ), invalid: [].concat( test({ code: 'import { bar } from "./flow-types-some-type-imports"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, errors: [error(`Dependency cycle detected.`)], }), test({ @@ -166,17 +166,17 @@ ruleTester.run('no-cycle', rule, { test({ code: `import { bar } from "./${testDialect}/depth-three-indirect"`, errors: [error(`Dependency cycle via ./depth-two:1=>./depth-one:1`)], - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: `import("./${testDialect}/depth-three-star")`, errors: [error(`Dependency cycle via ./depth-two:1=>./depth-one:1`)], - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: `import("./${testDialect}/depth-three-indirect")`, errors: [error(`Dependency cycle via ./depth-two:1=>./depth-one:1`)], - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: `import { foo } from "./${testDialect}/depth-two"`, @@ -192,7 +192,7 @@ ruleTester.run('no-cycle', rule, { test({ code: 'import { bar } from "./flow-types-depth-one"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, errors: [error(`Dependency cycle via ./flow-types-depth-two:4=>./es6/depth-one:1`)], }), ), diff --git a/tests/src/rules/no-default-export.js b/tests/src/rules/no-default-export.js index 61e55d9593..6c1a85a1d5 100644 --- a/tests/src/rules/no-default-export.js +++ b/tests/src/rules/no-default-export.js @@ -1,4 +1,4 @@ -import { test, testVersion } from '../utils'; +import { parsers, test, testVersion } from '../utils'; import { RuleTester } from 'eslint'; @@ -58,7 +58,7 @@ ruleTester.run('no-default-export', rule, { }), test({ code: 'export { a, b } from "foo.js"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), // no exports at all @@ -74,15 +74,15 @@ ruleTester.run('no-default-export', rule, { test({ code: `export type UserId = number;`, - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: 'export foo from "foo.js"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: `export Memory, { MemoryValue } from './Memory'`, - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), ], invalid: [].concat( @@ -154,7 +154,7 @@ ruleTester.run('no-default-export', rule, { }), test({ code: 'export default from "foo.js"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, errors: [ { type: 'ExportNamedDeclaration', @@ -162,5 +162,16 @@ ruleTester.run('no-default-export', rule, { }, ], }), + // es2022: Arbitrary module namespae identifier names + testVersion('>= 8.7', () => ({ + code: 'let foo; export { foo as "default" }', + errors: [ + { + type: 'ExportNamedDeclaration', + message: 'Do not alias `foo` as `default`. Just export `foo` itself instead.', + }, + ], + parserOptions: { ecmaVersion: 2022 }, + })), ), }); diff --git a/tests/src/rules/no-duplicates.js b/tests/src/rules/no-duplicates.js index ad39543f81..cde41b3a07 100644 --- a/tests/src/rules/no-duplicates.js +++ b/tests/src/rules/no-duplicates.js @@ -1,5 +1,5 @@ import * as path from 'path'; -import { test as testUtil, getNonDefaultParsers } from '../utils'; +import { test as testUtil, getNonDefaultParsers, parsers } from '../utils'; import { RuleTester } from 'eslint'; import eslintPkg from 'eslint/package.json'; @@ -26,7 +26,7 @@ ruleTester.run('no-duplicates', rule, { // #225: ignore duplicate if is a flow type import test({ code: "import { x } from './foo'; import type { y } from './foo'", - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), // #1107: Using different query strings that trigger different webpack loaders. @@ -107,7 +107,7 @@ ruleTester.run('no-duplicates', rule, { test({ code: "import type { x } from './foo'; import type { y } from './foo'", output: "import type { x , y } from './foo'; ", - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, errors: ['\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.'], }), @@ -418,7 +418,7 @@ import {x,y} from './foo' context('TypeScript', function () { getNonDefaultParsers() // Type-only imports were added in TypeScript ESTree 2.23.0 - .filter((parser) => parser !== require.resolve('typescript-eslint-parser')) + .filter((parser) => parser !== parsers.TS_OLD) .forEach((parser) => { const parserConfig = { parser, @@ -454,6 +454,20 @@ context('TypeScript', function () { `, ...parserConfig, }), + test({ + code: ` + import type { Identifier } from 'module'; + + declare module 'module2' { + import type { Identifier } from 'module'; + } + + declare module 'module3' { + import type { Identifier } from 'module'; + } + `, + ...parserConfig, + }), ], invalid: [ test({ diff --git a/tests/src/rules/no-dynamic-require.js b/tests/src/rules/no-dynamic-require.js index 4a70e7bc2b..0b141ccd76 100644 --- a/tests/src/rules/no-dynamic-require.js +++ b/tests/src/rules/no-dynamic-require.js @@ -1,6 +1,7 @@ -import { test } from '../utils'; +import { parsers, test, testVersion } from '../utils'; import { RuleTester } from 'eslint'; +import flatMap from 'array.prototype.flatmap'; const ruleTester = new RuleTester(); const rule = require('rules/no-dynamic-require'); @@ -28,56 +29,93 @@ ruleTester.run('no-dynamic-require', rule, { test({ code: 'var foo = require("@scope/foo")' }), //dynamic import - test({ - code: 'import("foo")', - parser: require.resolve('babel-eslint'), - options: [{ esmodule: true }], - }), - test({ - code: 'import(`foo`)', - parser: require.resolve('babel-eslint'), - options: [{ esmodule: true }], - }), - test({ - code: 'import("./foo")', - parser: require.resolve('babel-eslint'), - options: [{ esmodule: true }], - }), - test({ - code: 'import("@scope/foo")', - parser: require.resolve('babel-eslint'), - options: [{ esmodule: true }], - }), - test({ - code: 'var foo = import("foo")', - parser: require.resolve('babel-eslint'), - options: [{ esmodule: true }], - }), - test({ - code: 'var foo = import(`foo`)', - parser: require.resolve('babel-eslint'), - options: [{ esmodule: true }], - }), - test({ - code: 'var foo = import("./foo")', - parser: require.resolve('babel-eslint'), - options: [{ esmodule: true }], - }), - test({ - code: 'var foo = import("@scope/foo")', - parser: require.resolve('babel-eslint'), - options: [{ esmodule: true }], - }), - test({ - code: 'import("../" + name)', - errors: [dynamicImportError], - parser: require.resolve('babel-eslint'), - options: [{ esmodule: false }], - }), - test({ - code: 'import(`../${name}`)', - errors: [dynamicImportError], - parser: require.resolve('babel-eslint'), + ...flatMap([parsers.ESPREE, parsers.BABEL_OLD], (parser) => { + const _test = + parser === parsers.ESPREE + ? (testObj) => testVersion('>= 6.2.0', () => testObj) + : (testObj) => test(testObj); + return [].concat( + _test({ + code: 'import("foo")', + options: [{ esmodule: true }], + parser, + parserOptions: { + ecmaVersion: 2020, + }, + }), + _test({ + code: 'import(`foo`)', + options: [{ esmodule: true }], + parser, + parserOptions: { + ecmaVersion: 2020, + }, + }), + _test({ + code: 'import("./foo")', + options: [{ esmodule: true }], + parser, + parserOptions: { + ecmaVersion: 2020, + }, + }), + _test({ + code: 'import("@scope/foo")', + options: [{ esmodule: true }], + parser, + parserOptions: { + ecmaVersion: 2020, + }, + }), + _test({ + code: 'var foo = import("foo")', + options: [{ esmodule: true }], + parser, + parserOptions: { + ecmaVersion: 2020, + }, + }), + _test({ + code: 'var foo = import(`foo`)', + options: [{ esmodule: true }], + parser, + parserOptions: { + ecmaVersion: 2020, + }, + }), + _test({ + code: 'var foo = import("./foo")', + options: [{ esmodule: true }], + parser, + parserOptions: { + ecmaVersion: 2020, + }, + }), + _test({ + code: 'var foo = import("@scope/foo")', + options: [{ esmodule: true }], + parser, + parserOptions: { + ecmaVersion: 2020, + }, + }), + _test({ + code: 'import("../" + name)', + errors: [dynamicImportError], + parser, + parserOptions: { + ecmaVersion: 2020, + }, + }), + _test({ + code: 'import(`../${name}`)', + errors: [dynamicImportError], + parser, + parserOptions: { + ecmaVersion: 2020, + }, + }), + ); }), ], invalid: [ @@ -104,29 +142,49 @@ ruleTester.run('no-dynamic-require', rule, { }), // dynamic import - test({ - code: 'import("../" + name)', - errors: [dynamicImportError], - parser: require.resolve('babel-eslint'), - options: [{ esmodule: true }], - }), - test({ - code: 'import(`../${name}`)', - errors: [dynamicImportError], - parser: require.resolve('babel-eslint'), - options: [{ esmodule: true }], - }), - test({ - code: 'import(name)', - errors: [dynamicImportError], - parser: require.resolve('babel-eslint'), - options: [{ esmodule: true }], - }), - test({ - code: 'import(name())', - errors: [dynamicImportError], - parser: require.resolve('babel-eslint'), - options: [{ esmodule: true }], + ...flatMap([parsers.ESPREE, parsers.BABEL_OLD], (parser) => { + const _test = + parser === parsers.ESPREE + ? (testObj) => testVersion('>= 6.2.0', () => testObj) + : (testObj) => test(testObj); + return [].concat( + _test({ + code: 'import("../" + name)', + errors: [dynamicImportError], + options: [{ esmodule: true }], + parser, + parserOptions: { + ecmaVersion: 2020, + }, + }), + _test({ + code: 'import(`../${name}`)', + errors: [dynamicImportError], + options: [{ esmodule: true }], + parser, + parserOptions: { + ecmaVersion: 2020, + }, + }), + _test({ + code: 'import(name)', + errors: [dynamicImportError], + options: [{ esmodule: true }], + parser, + parserOptions: { + ecmaVersion: 2020, + }, + }), + _test({ + code: 'import(name())', + errors: [dynamicImportError], + options: [{ esmodule: true }], + parser, + parserOptions: { + ecmaVersion: 2020, + }, + }), + ); }), test({ code: 'require(`foo${x}`)', diff --git a/tests/src/rules/no-extraneous-dependencies.js b/tests/src/rules/no-extraneous-dependencies.js index d4c9f6d7f5..d4e3886bed 100644 --- a/tests/src/rules/no-extraneous-dependencies.js +++ b/tests/src/rules/no-extraneous-dependencies.js @@ -1,4 +1,4 @@ -import { getTSParsers, test, testFilePath } from '../utils'; +import { getTSParsers, parsers, test, testFilePath } from '../utils'; import typescriptConfig from '../../../config/typescript'; import path from 'path'; import fs from 'fs'; @@ -82,7 +82,7 @@ ruleTester.run('no-extraneous-dependencies', rule, { test({ code: 'import type MyType from "myflowtyped";', options: [{ packageDir: packageDirWithFlowTyped }], - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: ` @@ -90,7 +90,7 @@ ruleTester.run('no-extraneous-dependencies', rule, { import typeof TypeScriptModule from 'typescript'; `, options: [{ packageDir: packageDirWithFlowTyped }], - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: 'import react from "react";', @@ -398,7 +398,7 @@ ruleTester.run('no-extraneous-dependencies', rule, { describe('TypeScript', () => { getTSParsers() // Type-only imports were added in TypeScript ESTree 2.23.0 - .filter((parser) => parser !== require.resolve('typescript-eslint-parser')) + .filter((parser) => parser !== parsers.TS_OLD) .forEach((parser) => { const parserConfig = { parser, @@ -433,12 +433,12 @@ typescriptRuleTester.run('no-extraneous-dependencies typescript type imports', r test({ code: 'import type MyType from "not-a-dependency";', filename: testFilePath('./no-unused-modules/typescript/file-ts-a.ts'), - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: 'import type { MyType } from "not-a-dependency";', filename: testFilePath('./no-unused-modules/typescript/file-ts-a.ts'), - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), ], invalid: [ diff --git a/tests/src/rules/no-mutable-exports.js b/tests/src/rules/no-mutable-exports.js index 2ecae48cdb..1171443c4a 100644 --- a/tests/src/rules/no-mutable-exports.js +++ b/tests/src/rules/no-mutable-exports.js @@ -1,11 +1,11 @@ -import { test } from '../utils'; +import { parsers, test, testVersion } from '../utils'; import { RuleTester } from 'eslint'; import rule from 'rules/no-mutable-exports'; const ruleTester = new RuleTester(); ruleTester.run('no-mutable-exports', rule, { - valid: [ + valid: [].concat( test({ code: 'export const count = 1' }), test({ code: 'export function getCount() {}' }), test({ code: 'export class Counter {}' }), @@ -25,15 +25,19 @@ ruleTester.run('no-mutable-exports', rule, { test({ code: 'class Counter {}\nexport default Counter' }), test({ code: 'class Counter {}\nexport { Counter as default }' }), test({ - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, code: 'export Something from "./something";', }), test({ - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, code: 'type Foo = {}\nexport type {Foo}', }), - ], - invalid: [ + // es2022: Arbitrary module namespace identifier names + testVersion('>= 8.7', () => ({ + code: 'const count = 1\nexport { count as "counter" }', parserOptions: { ecmaVersion: 2022 }, + })), + ), + invalid: [].concat( test({ code: 'export let count = 1', errors: ['Exporting mutable \'let\' binding, use \'const\' instead.'], @@ -66,6 +70,12 @@ ruleTester.run('no-mutable-exports', rule, { code: 'var count = 1\nexport default count', errors: ['Exporting mutable \'var\' binding, use \'const\' instead.'], }), + // es2022: Arbitrary module namespace identifier names + testVersion('>= 8.7', () => ({ + code: 'let count = 1\nexport { count as "counter" }', + errors: ['Exporting mutable \'let\' binding, use \'const\' instead.'], + parserOptions: { ecmaVersion: 2022 }, + })), // todo: undeclared globals // test({ @@ -76,5 +86,5 @@ ruleTester.run('no-mutable-exports', rule, { // code: 'count = 1\nexport default count', // errors: ['Exporting mutable global binding, use \'const\' instead.'], // }), - ], + ), }); diff --git a/tests/src/rules/no-named-as-default-member.js b/tests/src/rules/no-named-as-default-member.js index b4f3cf5896..53cba230ba 100644 --- a/tests/src/rules/no-named-as-default-member.js +++ b/tests/src/rules/no-named-as-default-member.js @@ -1,21 +1,27 @@ -import { test, SYNTAX_CASES } from '../utils'; +import { test, testVersion, SYNTAX_CASES } from '../utils'; import { RuleTester } from 'eslint'; import rule from 'rules/no-named-as-default-member'; const ruleTester = new RuleTester(); ruleTester.run('no-named-as-default-member', rule, { - valid: [ + valid: [].concat( test({ code: 'import bar, {foo} from "./bar";' }), test({ code: 'import bar from "./bar"; const baz = bar.baz' }), test({ code: 'import {foo} from "./bar"; const baz = foo.baz;' }), test({ code: 'import * as named from "./named-exports"; const a = named.a' }), test({ code: 'import foo from "./default-export-default-property"; const a = foo.default' }), + // es2022: Arbitrary module namespace identifier names + testVersion('>= 8.7', () => ({ + code: 'import bar, { foo } from "./export-default-string-and-named"', + parserOptions: { ecmaVersion: 2022 }, + })), + ...SYNTAX_CASES, - ], + ), - invalid: [ + invalid: [].concat( test({ code: 'import bar from "./bar"; const foo = bar.foo;', errors: [{ @@ -56,5 +62,17 @@ ruleTester.run('no-named-as-default-member', rule, { type: 'Identifier', }], }), - ], + // es2022: Arbitrary module namespace identifier names + testVersion('>= 8.7', () => ({ + code: 'import bar from "./export-default-string-and-named"; const foo = bar.foo;', + errors: [{ + message: ( + 'Caution: `bar` also has a named export `foo`. ' + + 'Check if you meant to write `import {foo} from \'./export-default-string-and-named\'` instead.' + ), + type: 'MemberExpression', + }], + parserOptions: { ecmaVersion: 2022 }, + })), + ), }); diff --git a/tests/src/rules/no-named-as-default.js b/tests/src/rules/no-named-as-default.js index 57b2f53bd8..04ec28e615 100644 --- a/tests/src/rules/no-named-as-default.js +++ b/tests/src/rules/no-named-as-default.js @@ -1,11 +1,11 @@ -import { test, SYNTAX_CASES } from '../utils'; +import { test, testVersion, SYNTAX_CASES, parsers } from '../utils'; import { RuleTester } from 'eslint'; const ruleTester = new RuleTester(); const rule = require('rules/no-named-as-default'); ruleTester.run('no-named-as-default', rule, { - valid: [ + valid: [].concat( test({ code: 'import "./malformed.js"' }), test({ code: 'import bar, { foo } from "./bar";' }), @@ -13,18 +13,24 @@ ruleTester.run('no-named-as-default', rule, { // es7 test({ code: 'export bar, { foo } from "./bar";', - parser: require.resolve('babel-eslint') }), + parser: parsers.BABEL_OLD }), test({ code: 'export bar from "./bar";', - parser: require.resolve('babel-eslint') }), + parser: parsers.BABEL_OLD }), // #566: don't false-positive on `default` itself test({ code: 'export default from "./bar";', - parser: require.resolve('babel-eslint') }), + parser: parsers.BABEL_OLD }), + + // es2022: Arbitrary module namespae identifier names + testVersion('>= 8.7', () => ({ + code: 'import bar, { foo } from "./export-default-string-and-named"', + parserOptions: { ecmaVersion: 2022 }, + })), ...SYNTAX_CASES, - ], + ), - invalid: [ + invalid: [].concat( test({ code: 'import foo from "./bar";', errors: [ { @@ -39,13 +45,13 @@ ruleTester.run('no-named-as-default', rule, { // es7 test({ code: 'export foo from "./bar";', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, errors: [ { message: 'Using exported name \'foo\' as identifier for default export.', type: 'ExportDefaultSpecifier' } ] }), test({ code: 'export foo, { foo as bar } from "./bar";', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, errors: [ { message: 'Using exported name \'foo\' as identifier for default export.', type: 'ExportDefaultSpecifier' } ] }), @@ -57,5 +63,23 @@ ruleTester.run('no-named-as-default', rule, { type: 'Literal', }], }), - ], + + // es2022: Arbitrary module namespae identifier names + testVersion('>= 8.7', () => ({ + code: 'import foo from "./export-default-string-and-named"', + errors: [{ + message: 'Using exported name \'foo\' as identifier for default export.', + type: 'ImportDefaultSpecifier', + }], + parserOptions: { ecmaVersion: 2022 }, + })), + testVersion('>= 8.7', () => ({ + code: 'import foo, { foo as bar } from "./export-default-string-and-named"', + errors: [{ + message: 'Using exported name \'foo\' as identifier for default export.', + type: 'ImportDefaultSpecifier', + }], + parserOptions: { ecmaVersion: 2022 }, + })), + ), }); diff --git a/tests/src/rules/no-named-default.js b/tests/src/rules/no-named-default.js index 56470f2bac..191c9c6ce9 100644 --- a/tests/src/rules/no-named-default.js +++ b/tests/src/rules/no-named-default.js @@ -1,4 +1,4 @@ -import { test, SYNTAX_CASES } from '../utils'; +import { test, testVersion, SYNTAX_CASES, parsers } from '../utils'; import { RuleTester } from 'eslint'; const ruleTester = new RuleTester(); @@ -12,24 +12,24 @@ ruleTester.run('no-named-default', rule, { // Should ignore imported flow types test({ code: 'import { type default as Foo } from "./bar";', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: 'import { typeof default as Foo } from "./bar";', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), ...SYNTAX_CASES, ], - invalid: [ + invalid: [].concat( /*test({ code: 'import { default } from "./bar";', errors: [{ message: 'Use default import syntax to import \'default\'.', type: 'Identifier', }], - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }),*/ test({ code: 'import { default as bar } from "./bar";', @@ -45,5 +45,17 @@ ruleTester.run('no-named-default', rule, { type: 'Identifier', }], }), - ], + + // es2022: Arbitrary module namespae identifier names + testVersion('>= 8.7', () => ({ + code: 'import { "default" as bar } from "./bar";', + errors: [{ + message: 'Use default import syntax to import \'bar\'.', + type: 'Identifier', + }], + parserOptions: { + ecmaVersion: 2022, + }, + })) || [], + ), }); diff --git a/tests/src/rules/no-named-export.js b/tests/src/rules/no-named-export.js index 41d0fcd7cf..58b5da2f85 100644 --- a/tests/src/rules/no-named-export.js +++ b/tests/src/rules/no-named-export.js @@ -1,11 +1,11 @@ import { RuleTester } from 'eslint'; -import { test } from '../utils'; +import { parsers, test, testVersion } from '../utils'; const ruleTester = new RuleTester(); const rule = require('rules/no-named-export'); ruleTester.run('no-named-export', rule, { - valid: [ + valid: [].concat( test({ code: 'export default function bar() {};', }), @@ -14,7 +14,7 @@ ruleTester.run('no-named-export', rule, { }), test({ code: 'export default from "foo.js"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), // no exports at all @@ -27,7 +27,13 @@ ruleTester.run('no-named-export', rule, { test({ code: `import {default as foo} from './foo';`, }), - ], + + // es2022: Arbitrary module namespae identifier names + testVersion('>= 8.7', () => ({ + code: 'let foo; export { foo as "default" }', + parserOptions: { ecmaVersion: 2022 }, + })), + ), invalid: [ test({ code: ` @@ -146,7 +152,7 @@ ruleTester.run('no-named-export', rule, { }), test({ code: 'export { a, b } from "foo.js"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, errors: [{ type: 'ExportNamedDeclaration', message: 'Named exports are not allowed.', @@ -154,7 +160,7 @@ ruleTester.run('no-named-export', rule, { }), test({ code: `export type UserId = number;`, - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, errors: [{ type: 'ExportNamedDeclaration', message: 'Named exports are not allowed.', @@ -162,7 +168,7 @@ ruleTester.run('no-named-export', rule, { }), test({ code: 'export foo from "foo.js"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, errors: [{ type: 'ExportNamedDeclaration', message: 'Named exports are not allowed.', @@ -170,7 +176,7 @@ ruleTester.run('no-named-export', rule, { }), test({ code: `export Memory, { MemoryValue } from './Memory'`, - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, errors: [{ type: 'ExportNamedDeclaration', message: 'Named exports are not allowed.', diff --git a/tests/src/rules/no-nodejs-modules.js b/tests/src/rules/no-nodejs-modules.js index 3587a71dca..9be605709a 100644 --- a/tests/src/rules/no-nodejs-modules.js +++ b/tests/src/rules/no-nodejs-modules.js @@ -1,6 +1,7 @@ import { test } from '../utils'; import { RuleTester } from 'eslint'; +const isCore = require('is-core-module'); const ruleTester = new RuleTester(); const rule = require('rules/no-nodejs-modules'); @@ -10,7 +11,7 @@ const error = message => ({ }); ruleTester.run('no-nodejs-modules', rule, { - valid: [ + valid: [].concat( test({ code: 'import _ from "lodash"' }), test({ code: 'import find from "lodash.find"' }), test({ code: 'import foo from "./foo"' }), @@ -55,8 +56,42 @@ ruleTester.run('no-nodejs-modules', rule, { allow: ['path', 'events'], }], }), - ], - invalid: [ + isCore('node:events') ? [ + test({ + code: 'import events from "node:events"', + options: [{ + allow: ['node:events'], + }], + }), + test({ + code: 'var events = require("node:events")', + options: [{ + allow: ['node:events'], + }], + }), + ]: [], + isCore('node:path') ? [ + test({ + code: 'import path from "node:path"', + options: [{ + allow: ['node:path'], + }], + }), + test({ + code: 'var path = require("node:path")', + options: [{ + allow: ['node:path'], + }], + }), + ] : [], + isCore('node:path') && isCore('node:events') ? test({ + code: 'import path from "node:path";import events from "node:events"', + options: [{ + allow: ['node:path', 'node:events'], + }], + }) : [], + ), + invalid: [].concat( test({ code: 'import path from "path"', errors: [error('Do not import Node.js builtin module "path"')], @@ -80,5 +115,32 @@ ruleTester.run('no-nodejs-modules', rule, { }], errors: [error('Do not import Node.js builtin module "fs"')], }), - ], + isCore('node:path') ? [ + test({ + code: 'import path from "node:path"', + errors: [error('Do not import Node.js builtin module "node:path"')], + }), + test({ + code: 'var path = require("node:path")', + errors: [error('Do not import Node.js builtin module "node:path"')], + }), + ] : [], + isCore('node:fs') ? [ + test({ + code: 'import fs from "node:fs"', + errors: [error('Do not import Node.js builtin module "node:fs"')], + }), + test({ + code: 'var fs = require("node:fs")', + errors: [error('Do not import Node.js builtin module "node:fs"')], + }), + test({ + code: 'import fs from "node:fs"', + options: [{ + allow: ['node:path'], + }], + errors: [error('Do not import Node.js builtin module "node:fs"')], + }), + ] : [], + ), }); diff --git a/tests/src/rules/no-relative-packages.js b/tests/src/rules/no-relative-packages.js index 1a706387c0..2d27bcc91e 100644 --- a/tests/src/rules/no-relative-packages.js +++ b/tests/src/rules/no-relative-packages.js @@ -47,6 +47,7 @@ ruleTester.run('no-relative-packages', rule, { line: 1, column: 17, } ], + output: 'import foo from "package-named"', }), test({ code: 'import foo from "../package-named"', @@ -56,6 +57,7 @@ ruleTester.run('no-relative-packages', rule, { line: 1, column: 17, } ], + output: 'import foo from "package-named"', }), test({ code: 'import foo from "../package-scoped"', @@ -65,6 +67,7 @@ ruleTester.run('no-relative-packages', rule, { line: 1, column: 17, } ], + output: `import foo from "@scope/package-named"`, }), test({ code: 'import bar from "../bar"', @@ -74,6 +77,7 @@ ruleTester.run('no-relative-packages', rule, { line: 1, column: 17, } ], + output: `import bar from "eslint-plugin-import/tests/files/bar"`, }), ], }); diff --git a/tests/src/rules/no-relative-parent-imports.js b/tests/src/rules/no-relative-parent-imports.js index d6a47ae373..3050498026 100644 --- a/tests/src/rules/no-relative-parent-imports.js +++ b/tests/src/rules/no-relative-parent-imports.js @@ -1,10 +1,10 @@ import { RuleTester } from 'eslint'; import rule from 'rules/no-relative-parent-imports'; -import { test as _test, testFilePath } from '../utils'; +import { parsers, test as _test, testFilePath } from '../utils'; const test = def => _test(Object.assign(def, { filename: testFilePath('./internal-modules/plugins/plugin2/index.js'), - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, })); const ruleTester = new RuleTester(); diff --git a/tests/src/rules/no-unresolved.js b/tests/src/rules/no-unresolved.js index 71efc128e3..c0252ad19d 100644 --- a/tests/src/rules/no-unresolved.js +++ b/tests/src/rules/no-unresolved.js @@ -1,6 +1,6 @@ -import * as path from 'path'; +import path from 'path'; -import { getTSParsers, test, SYNTAX_CASES, testVersion } from '../utils'; +import { getTSParsers, test, SYNTAX_CASES, testVersion, parsers } from '../utils'; import { CASE_SENSITIVE_FS } from 'eslint-module-utils/resolve'; @@ -30,7 +30,7 @@ function runResolverTests(resolver) { rest({ code: "import {someThing} from './test-module';" }), rest({ code: "import fs from 'fs';" }), rest({ code: "import('fs');", - parser: require.resolve('babel-eslint') }), + parser: parsers.BABEL_OLD }), // check with eslint parser testVersion('>= 7', () => rest({ @@ -46,9 +46,9 @@ function runResolverTests(resolver) { // stage 1 proposal for export symmetry, rest({ code: 'export * as bar from "./bar"', - parser: require.resolve('babel-eslint') }), + parser: parsers.BABEL_OLD }), rest({ code: 'export bar from "./bar"', - parser: require.resolve('babel-eslint') }), + parser: parsers.BABEL_OLD }), rest({ code: 'import foo from "./jsx/MyUnCoolComponent.jsx"' }), // commonjs setting @@ -127,7 +127,7 @@ function runResolverTests(resolver) { message: 'Unable to resolve path to module \'in-alternate-root\'.', type: 'Literal', }], - parser: require.resolve('babel-eslint') }), + parser: parsers.BABEL_OLD }), rest({ code: 'export { foo } from "./does-not-exist"', errors: ["Unable to resolve path to module './does-not-exist'."] }), @@ -148,11 +148,11 @@ function runResolverTests(resolver) { // export symmetry proposal rest({ code: 'export * as bar from "./does-not-exist"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, errors: ["Unable to resolve path to module './does-not-exist'."], }), rest({ code: 'export bar from "./does-not-exist"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, errors: ["Unable to resolve path to module './does-not-exist'."], }), @@ -444,7 +444,7 @@ ruleTester.run('import() with built-in parser', rule, { context('TypeScript', () => { // Type-only imports were added in TypeScript ESTree 2.23.0 - getTSParsers().filter(x => x !== require.resolve('typescript-eslint-parser')).forEach((parser) => { + getTSParsers().filter(x => x !== parsers.TS_OLD).forEach((parser) => { ruleTester.run(`${parser}: no-unresolved ignore type-only`, rule, { valid: [ test({ diff --git a/tests/src/rules/no-unused-modules.js b/tests/src/rules/no-unused-modules.js index 6f87058c4a..38db2ef43d 100644 --- a/tests/src/rules/no-unused-modules.js +++ b/tests/src/rules/no-unused-modules.js @@ -1,4 +1,4 @@ -import { test, testFilePath, getTSParsers } from '../utils'; +import { test, testVersion, testFilePath, getTSParsers, parsers } from '../utils'; import jsxConfig from '../../../config/react'; import typescriptConfig from '../../../config/typescript'; @@ -105,56 +105,56 @@ ruleTester.run('no-unused-modules', rule, { }); -// tests for exports +// tests for exports ruleTester.run('no-unused-modules', rule, { valid: [ test({ options: unusedExportsOptions, code: 'import { o2 } from "./file-o";export default () => 12', filename: testFilePath('./no-unused-modules/file-a.js'), - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ options: unusedExportsOptions, code: 'export const b = 2', filename: testFilePath('./no-unused-modules/file-b.js'), - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ options: unusedExportsOptions, code: 'const c1 = 3; function c2() { return 3 }; export { c1, c2 }', filename: testFilePath('./no-unused-modules/file-c.js'), - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ options: unusedExportsOptions, code: 'export function d() { return 4 }', filename: testFilePath('./no-unused-modules/file-d.js'), - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ options: unusedExportsOptions, code: 'export class q { q0() {} }', filename: testFilePath('./no-unused-modules/file-q.js'), - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ options: unusedExportsOptions, code: 'const e0 = 5; export { e0 as e }', filename: testFilePath('./no-unused-modules/file-e.js'), - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ options: unusedExportsOptions, code: 'const l0 = 5; const l = 10; export { l0 as l1, l }; export default () => {}', filename: testFilePath('./no-unused-modules/file-l.js'), - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ options: unusedExportsOptions, code: 'const o0 = 0; const o1 = 1; export { o0, o1 as o2 }; export default () => {}', filename: testFilePath('./no-unused-modules/file-o.js'), - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), ], invalid: [ @@ -263,7 +263,7 @@ describe('dynamic imports', () => { const d = 40 export default d `, - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, filename: testFilePath('./no-unused-modules/exports-for-dynamic-js.js'), }), ], @@ -277,7 +277,7 @@ describe('dynamic imports', () => { const d = 40 export default d `, - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, filename: testFilePath('./no-unused-modules/exports-for-dynamic-js-2.js'), errors: [ error(`exported declaration 'a' not used within other modules`), @@ -298,9 +298,20 @@ describe('dynamic imports', () => { const ts_d = 40 export default ts_d `, - parser: require.resolve('@typescript-eslint/parser'), + parser: parsers.TS_NEW, filename: testFilePath('./no-unused-modules/typescript/exports-for-dynamic-ts.ts'), }), + test({ + code: ` + import App from './App'; + `, + filename: testFilePath('./unused-modules-reexport-crash/src/index.tsx'), + parser: parsers.TS_NEW, + options: [{ + unusedExports: true, + ignoreExports: ['**/magic/**'], + }], + }), ], invalid: [ ], @@ -1177,7 +1188,7 @@ describe('correctly work with JSX only files', () => { test({ options: unusedExportsJsxOptions, code: 'import a from "file-jsx-a";', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, filename: testFilePath('./no-unused-modules/jsx/file-jsx-a.jsx'), }), ], @@ -1185,7 +1196,7 @@ describe('correctly work with JSX only files', () => { test({ options: unusedExportsJsxOptions, code: `export const b = 2;`, - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, filename: testFilePath('./no-unused-modules/jsx/file-jsx-b.jsx'), errors: [ error(`exported declaration 'b' not used within other modules`), @@ -1201,7 +1212,7 @@ describe('ignore flow types', () => { test({ options: unusedExportsOptions, code: 'import { type FooType, type FooInterface } from "./flow-2";', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, filename: testFilePath('./no-unused-modules/flow/flow-0.js'), }), test({ @@ -1210,13 +1221,13 @@ describe('ignore flow types', () => { export type FooType = string; export interface FooInterface {}; `, - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, filename: testFilePath('./no-unused-modules/flow/flow-2.js'), }), test({ options: unusedExportsOptions, code: 'import type { FooType, FooInterface } from "./flow-4";', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, filename: testFilePath('./no-unused-modules/flow/flow-3.js'), }), test({ @@ -1225,7 +1236,7 @@ describe('ignore flow types', () => { export type FooType = string; export interface FooInterface {}; `, - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, filename: testFilePath('./no-unused-modules/flow/flow-4.js'), }), test({ @@ -1234,7 +1245,7 @@ describe('ignore flow types', () => { export type Bar = number; export interface BarInterface {}; `, - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, filename: testFilePath('./no-unused-modules/flow/flow-1.js'), }), ], @@ -1248,16 +1259,46 @@ describe('support (nested) destructuring assignment', () => { test({ options: unusedExportsOptions, code: 'import {a, b} from "./destructuring-b";', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, filename: testFilePath('./no-unused-modules/destructuring-a.js'), }), test({ options: unusedExportsOptions, code: 'const obj = {a: 1, dummy: {b: 2}}; export const {a, dummy: {b}} = obj;', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, filename: testFilePath('./no-unused-modules/destructuring-b.js'), }), ], invalid: [], }); }); + +describe('support ES2022 Arbitrary module namespace identifier names', () => { + ruleTester.run('no-unused-module', rule, { + valid: [].concat( + testVersion('>= 8.7', () => ({ + options: unusedExportsOptions, + code: `import { "foo" as foo } from "./arbitrary-module-namespace-identifier-name-a"`, + parserOptions: { ecmaVersion: 2022 }, + filename: testFilePath('./no-unused-modules/arbitrary-module-namespace-identifier-name-b.js'), + })), + testVersion('>= 8.7', () => ({ + options: unusedExportsOptions, + code: 'const foo = 333;\nexport { foo as "foo" }', + parserOptions: { ecmaVersion: 2022 }, + filename: testFilePath('./no-unused-modules/arbitrary-module-namespace-identifier-name-a.js'), + })), + ), + invalid: [].concat( + testVersion('>= 8.7', () => ({ + options: unusedExportsOptions, + code: 'const foo = 333\nexport { foo as "foo" }', + parserOptions: { ecmaVersion: 2022 }, + filename: testFilePath('./no-unused-modules/arbitrary-module-namespace-identifier-name-c.js'), + errors: [ + error(`exported declaration 'foo' not used within other modules`), + ], + })), + ), + }); +}); diff --git a/tests/src/rules/no-useless-path-segments.js b/tests/src/rules/no-useless-path-segments.js index 21d9a5f704..f960953503 100644 --- a/tests/src/rules/no-useless-path-segments.js +++ b/tests/src/rules/no-useless-path-segments.js @@ -1,4 +1,4 @@ -import { test } from '../utils'; +import { parsers, test } from '../utils'; import { RuleTester } from 'eslint'; const ruleTester = new RuleTester(); @@ -29,11 +29,11 @@ function runResolverTests(resolver) { test({ code: 'import "./importType"', options: [{ noUselessIndex: true }] }), // ./importType.js does not exist test({ code: 'import(".")', - parser: require.resolve('babel-eslint') }), + parser: parsers.BABEL_OLD }), test({ code: 'import("..")', - parser: require.resolve('babel-eslint') }), + parser: parsers.BABEL_OLD }), test({ code: 'import("fs").then(function(fs) {})', - parser: require.resolve('babel-eslint') }), + parser: parsers.BABEL_OLD }), ], invalid: [ @@ -232,19 +232,19 @@ function runResolverTests(resolver) { code: 'import("./")', output: 'import(".")', errors: [ 'Useless path segments for "./", should be "."'], - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: 'import("../")', output: 'import("..")', errors: [ 'Useless path segments for "../", should be ".."'], - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: 'import("./deep//a")', output: 'import("./deep/a")', errors: [ 'Useless path segments for "./deep//a", should be "./deep/a"'], - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), ], }); diff --git a/tests/src/rules/no-webpack-loader-syntax.js b/tests/src/rules/no-webpack-loader-syntax.js index a8aa0dd2b0..2b841e18a3 100644 --- a/tests/src/rules/no-webpack-loader-syntax.js +++ b/tests/src/rules/no-webpack-loader-syntax.js @@ -1,6 +1,7 @@ -import { test, getTSParsers } from '../utils'; +import { test, getTSParsers, parsers } from '../utils'; import { RuleTester } from 'eslint'; +import semver from 'semver'; const ruleTester = new RuleTester(); const rule = require('rules/no-webpack-loader-syntax'); @@ -82,16 +83,20 @@ context('TypeScript', function () { 'import/resolver': { 'eslint-import-resolver-typescript': true }, }, }; - ruleTester.run('no-webpack-loader-syntax', rule, { - valid: [ - test(Object.assign({ - code: 'import { foo } from\nalert()', - }, parserConfig)), - test(Object.assign({ - code: 'import foo from\nalert()', - }, parserConfig)), - ], - invalid: [], - }); + // @typescript-eslint/parser@5+ throw error for invalid module specifiers at parsing time. + // https://github.com/typescript-eslint/typescript-eslint/releases/tag/v5.0.0 + if (!(parser === parsers.TS_NEW && semver.satisfies(require('@typescript-eslint/parser/package.json').version, '>= 5'))) { + ruleTester.run('no-webpack-loader-syntax', rule, { + valid: [ + test(Object.assign({ + code: 'import { foo } from\nalert()', + }, parserConfig)), + test(Object.assign({ + code: 'import foo from\nalert()', + }, parserConfig)), + ], + invalid: [], + }); + } }); }); diff --git a/tests/src/rules/order.js b/tests/src/rules/order.js index 79426c4c43..e552c9a853 100644 --- a/tests/src/rules/order.js +++ b/tests/src/rules/order.js @@ -1,4 +1,4 @@ -import { test, getTSParsers, getNonDefaultParsers, testFilePath } from '../utils'; +import { test, getTSParsers, getNonDefaultParsers, testFilePath, parsers } from '../utils'; import { RuleTester } from 'eslint'; import eslintPkg from 'eslint/package.json'; @@ -2321,7 +2321,7 @@ ruleTester.run('order', rule, { context('TypeScript', function () { getNonDefaultParsers() // Type-only imports were added in TypeScript ESTree 2.23.0 - .filter((parser) => parser !== require.resolve('typescript-eslint-parser')) + .filter((parser) => parser !== parsers.TS_OLD) .forEach((parser) => { const parserConfig = { parser, diff --git a/tests/src/rules/prefer-default-export.js b/tests/src/rules/prefer-default-export.js index 36205b1935..6a36f08bbf 100644 --- a/tests/src/rules/prefer-default-export.js +++ b/tests/src/rules/prefer-default-export.js @@ -1,12 +1,14 @@ -import { test, getNonDefaultParsers } from '../utils'; +import { test, testVersion, getNonDefaultParsers, parsers } from '../utils'; import { RuleTester } from 'eslint'; +import semver from 'semver'; +import { version as tsEslintVersion } from 'typescript-eslint-parser/package.json'; const ruleTester = new RuleTester(); const rule = require('../../../src/rules/prefer-default-export'); ruleTester.run('prefer-default-export', rule, { - valid: [ + valid: [].concat( test({ code: ` export const foo = 'foo'; @@ -64,7 +66,7 @@ ruleTester.run('prefer-default-export', rule, { }), test({ code: `export Memory, { MemoryValue } from './Memory'`, - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), // no exports at all @@ -75,26 +77,31 @@ ruleTester.run('prefer-default-export', rule, { test({ code: `export type UserId = number;`, - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), // issue #653 test({ code: 'export default from "foo.js"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), test({ code: 'export { a, b } from "foo.js"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, }), // ...SYNTAX_CASES, test({ code: ` export const [CounterProvider,, withCounter] = func();; `, - parser: require.resolve('babel-eslint'), - }), - ], + parser: parsers.BABEL_OLD, + }), + // es2022: Arbitrary module namespae identifier names + testVersion('>= 8.7', () => ({ + code: 'let foo; export { foo as "default" };', + parserOptions: { ecmaVersion: 2022 }, + })), + ), invalid: [ test({ code: ` @@ -159,28 +166,24 @@ context('TypeScript', function () { }; ruleTester.run('prefer-default-export', rule, { - valid: [ + valid: [].concat( // Exporting types - test({ + semver.satisfies(tsEslintVersion, '>= 22') ? test({ code: ` export type foo = string; export type bar = number;`, ...parserConfig, - }), + }) : [], test({ code: ` export type foo = string; export type bar = number;`, ...parserConfig, }), - test({ - code: 'export type foo = string', - ...parserConfig, - }), - test({ + semver.satisfies(tsEslintVersion, '>= 22') ? test({ code: 'export type foo = string', ...parserConfig, - }), + }) : [], test({ code: 'export interface foo { bar: string; }', ...parserConfig, @@ -189,7 +192,7 @@ context('TypeScript', function () { code: 'export interface foo { bar: string; }; export function goo() {}', ...parserConfig, }), - ], + ), invalid: [], }); }); diff --git a/tests/src/rules/unambiguous.js b/tests/src/rules/unambiguous.js index 72e6b10828..8cef69625f 100644 --- a/tests/src/rules/unambiguous.js +++ b/tests/src/rules/unambiguous.js @@ -1,4 +1,5 @@ import { RuleTester } from 'eslint'; +import { parsers } from '../utils'; const ruleTester = new RuleTester(); const rule = require('rules/unambiguous'); @@ -38,7 +39,7 @@ ruleTester.run('unambiguous', rule, { }, { code: 'function x() {}; export * as y from "z"', - parser: require.resolve('babel-eslint'), + parser: parsers.BABEL_OLD, parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, }, { diff --git a/tests/src/utils.js b/tests/src/utils.js index 012c3a7c7b..b66ecf9c66 100644 --- a/tests/src/utils.js +++ b/tests/src/utils.js @@ -5,24 +5,26 @@ import semver from 'semver'; // warms up the module cache. this import takes a while (>500ms) import 'babel-eslint'; +export const parsers = { + ESPREE: require.resolve('espree'), + TS_OLD: semver.satisfies(eslintPkg.version, '>=4.0.0 <6.0.0') && require.resolve('typescript-eslint-parser'), + TS_NEW: semver.satisfies(eslintPkg.version, '>5.0.0') && require.resolve('@typescript-eslint/parser'), + BABEL_OLD: require.resolve('babel-eslint'), +}; + export function testFilePath(relativePath) { return path.join(process.cwd(), './tests/files', relativePath); } export function getTSParsers() { - const parsers = []; - if (semver.satisfies(eslintPkg.version, '>=4.0.0 <6.0.0')) { - parsers.push(require.resolve('typescript-eslint-parser')); - } - - if (semver.satisfies(eslintPkg.version, '>5.0.0')) { - parsers.push(require.resolve('@typescript-eslint/parser')); - } - return parsers; + return [ + parsers.TS_OLD, + parsers.TS_NEW, + ].filter(Boolean); } export function getNonDefaultParsers() { - return getTSParsers().concat(require.resolve('babel-eslint')); + return getTSParsers().concat(parsers.BABEL_OLD).filter(Boolean); } export const FILENAME = testFilePath('foo.js'); @@ -65,7 +67,7 @@ export const SYNTAX_CASES = [ test({ code: 'for (let [ foo, bar ] of baz) {}' }), test({ code: 'const { x, y } = bar' }), - test({ code: 'const { x, y, ...z } = bar', parser: require.resolve('babel-eslint') }), + test({ code: 'const { x, y, ...z } = bar', parser: parsers.BABEL_OLD }), // all the exports test({ code: 'let x; export { x }' }), @@ -73,7 +75,7 @@ export const SYNTAX_CASES = [ // not sure about these since they reference a file // test({ code: 'export { x } from "./y.js"'}), - // test({ code: 'export * as y from "./y.js"', parser: require.resolve('babel-eslint')}), + // test({ code: 'export * as y from "./y.js"', parser: parsers.BABEL_OLD}), test({ code: 'export const x = null' }), test({ code: 'export var x = null' }), diff --git a/utils/CHANGELOG.md b/utils/CHANGELOG.md index 65d67a35c7..193fc141e0 100644 --- a/utils/CHANGELOG.md +++ b/utils/CHANGELOG.md @@ -5,10 +5,23 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ## Unreleased +## v2.7.3 - 2022-01-26 + +### Fixed +- [Fix] `parse`: restore compatibility by making the return value `ast` again ([#2350], thanks [@ljharb]) + +## v2.7.2 - 2022-01-01 + +### Fixed +- [patch] Fix `@babel/eslint-parser` 8 compatibility ([#2343], thanks [@nicolo-ribaudo]) + +### Changed +- [Refactor] inline `pkgDir` implementation; remove `pkg-dir` + ## v2.7.1 - 2021-10-13 ### Fixed - - fixed SyntaxError in node <= 6: Unexpected token ) in parse.js ([#2261], thanks [@VitusFW]) +- fixed SyntaxError in node <= 6: Unexpected token ) in parse.js ([#2261], thanks [@VitusFW]) ## v2.7.0 - 2021-10-11 @@ -102,6 +115,8 @@ Yanked due to critical issue with cache key resulting from #839. ### Fixed - `unambiguous.test()` regex is now properly in multiline mode +[#2350]: https://github.com/import-js/eslint-plugin-import/issues/2350 +[#2343]: https://github.com/import-js/eslint-plugin-import/pull/2343 [#2261]: https://github.com/import-js/eslint-plugin-import/pull/2261 [#2212]: https://github.com/import-js/eslint-plugin-import/pull/2212 [#2160]: https://github.com/import-js/eslint-plugin-import/pull/2160 @@ -138,6 +153,7 @@ Yanked due to critical issue with cache key resulting from #839. [@manuth]: https://github.com/manuth [@maxkomarychev]: https://github.com/maxkomarychev [@mgwalker]: https://github.com/mgwalker +[@nicolo-ribaudo]: https://github.com/nicolo-ribaudo [@pmcelhaney]: https://github.com/pmcelhaney [@sergei-startsev]: https://github.com/sergei-startsev [@sompylasar]: https://github.com/sompylasar diff --git a/utils/package.json b/utils/package.json index 2c4c53bd47..2e348d07f2 100644 --- a/utils/package.json +++ b/utils/package.json @@ -1,6 +1,6 @@ { "name": "eslint-module-utils", - "version": "2.7.1", + "version": "2.7.3", "description": "Core utilities to support eslint-plugin-import and other module-related plugins.", "engines": { "node": ">=4" @@ -27,7 +27,6 @@ "homepage": "https://github.com/import-js/eslint-plugin-import#readme", "dependencies": { "debug": "^3.2.7", - "find-up": "^2.1.0", - "pkg-dir": "^2.0.0" + "find-up": "^2.1.0" } } diff --git a/utils/parse.js b/utils/parse.js index a771544ea7..98e8215992 100644 --- a/utils/parse.js +++ b/utils/parse.js @@ -7,38 +7,41 @@ const fs = require('fs'); const log = require('debug')('eslint-plugin-import:parse'); -function getBabelVisitorKeys(parserPath) { +function getBabelEslintVisitorKeys(parserPath) { if (parserPath.endsWith('index.js')) { const hypotheticalLocation = parserPath.replace('index.js', 'visitor-keys.js'); if (fs.existsSync(hypotheticalLocation)) { const keys = moduleRequire(hypotheticalLocation); return keys.default || keys; } - } else if (parserPath.endsWith('index.cjs')) { - const hypotheticalLocation = parserPath.replace('index.cjs', 'worker/ast-info.cjs'); - if (fs.existsSync(hypotheticalLocation)) { - const astInfo = moduleRequire(hypotheticalLocation); - return astInfo.getVisitorKeys(); - } } return null; } function keysFromParser(parserPath, parserInstance, parsedResult) { + // Exposed by @typescript-eslint/parser and @babel/eslint-parser + if (parsedResult && parsedResult.visitorKeys) { + return parsedResult.visitorKeys; + } if (/.*espree.*/.test(parserPath)) { return parserInstance.VisitorKeys; } - if (/.*(babel-eslint|@babel\/eslint-parser).*/.test(parserPath)) { - return getBabelVisitorKeys(parserPath); - } - if (/.*@typescript-eslint\/parser/.test(parserPath)) { - if (parsedResult) { - return parsedResult.visitorKeys; - } + if (/.*babel-eslint.*/.test(parserPath)) { + return getBabelEslintVisitorKeys(parserPath); } return null; } +// this exists to smooth over the unintentional breaking change in v2.7. +// TODO, semver-major: avoid mutating `ast` and return a plain object instead. +function makeParseReturn(ast, visitorKeys) { + if (ast) { + ast.visitorKeys = visitorKeys; + ast.ast = ast; + } + return ast; +} + exports.default = function parse(path, content, context) { if (context == null) throw new Error('need context to parse properly'); @@ -80,10 +83,7 @@ exports.default = function parse(path, content, context) { try { const parserRaw = parser.parseForESLint(content, parserOptions); ast = parserRaw.ast; - return { - ast, - visitorKeys: keysFromParser(parserPath, parser, parserRaw), - }; + return makeParseReturn(ast, keysFromParser(parserPath, parser, parserRaw)); } catch (e) { console.warn(); console.warn('Error while parsing ' + parserOptions.filePath); @@ -96,18 +96,12 @@ exports.default = function parse(path, content, context) { '` is invalid and will just be ignored' ); } else { - return { - ast, - visitorKeys: keysFromParser(parserPath, parser, undefined), - }; + return makeParseReturn(ast, keysFromParser(parserPath, parser, undefined)); } } - const keys = keysFromParser(parserPath, parser, undefined); - return { - ast: parser.parse(content, parserOptions), - visitorKeys: keys, - }; + const ast = parser.parse(content, parserOptions); + return makeParseReturn(ast, keysFromParser(parserPath, parser, undefined)); }; function getParserPath(path, context) { diff --git a/utils/pkgDir.js b/utils/pkgDir.js new file mode 100644 index 0000000000..34412202f1 --- /dev/null +++ b/utils/pkgDir.js @@ -0,0 +1,11 @@ +'use strict'; + +const path = require('path'); +const pkgUp = require('./pkgUp').default; + +exports.__esModule = true; + +exports.default = function (cwd) { + const fp = pkgUp({ cwd }); + return fp ? path.dirname(fp) : null; +}; diff --git a/utils/resolve.js b/utils/resolve.js index 31fb659079..4a35c6a472 100644 --- a/utils/resolve.js +++ b/utils/resolve.js @@ -1,14 +1,13 @@ 'use strict'; exports.__esModule = true; -const pkgDir = require('pkg-dir'); - const fs = require('fs'); const Module = require('module'); const path = require('path'); const hashObject = require('./hash').hashObject; const ModuleCache = require('./ModuleCache').default; +const pkgDir = require('./pkgDir').default; const CASE_SENSITIVE_FS = !fs.existsSync(path.join(__dirname.toUpperCase(), 'reSOLVE.js')); exports.CASE_SENSITIVE_FS = CASE_SENSITIVE_FS; @@ -175,7 +174,7 @@ function resolverReducer(resolvers, map) { } function getBaseDir(sourceFile) { - return pkgDir.sync(sourceFile) || process.cwd(); + return pkgDir(sourceFile) || process.cwd(); } function requireResolver(name, sourceFile) { // Try to resolve package with conventional name