diff --git a/.github/workflows/node-4+.yml b/.github/workflows/node-4+.yml index f2dad098c..323c2ad54 100644 --- a/.github/workflows/node-4+.yml +++ b/.github/workflows/node-4+.yml @@ -36,6 +36,7 @@ jobs: - macos-latest node-version: ${{ fromJson(needs.matrix.outputs.latest) }} eslint: + - 9 - 8 - 7 - 6 @@ -63,34 +64,58 @@ jobs: env: TS_PARSER: 2 exclude: + - node-version: 16 + eslint: 9 + - node-version: 15 + eslint: 9 - node-version: 15 eslint: 8 + - node-version: 14 + eslint: 9 + - node-version: 13 + eslint: 9 - node-version: 13 eslint: 8 + - node-version: 12 + eslint: 9 + - node-version: 11 + eslint: 9 - node-version: 11 eslint: 8 + - node-version: 10 + eslint: 9 - node-version: 10 eslint: 8 + - node-version: 9 + eslint: 9 - node-version: 9 eslint: 8 - node-version: 9 eslint: 7 + - node-version: 8 + eslint: 9 - node-version: 8 eslint: 8 - node-version: 8 eslint: 7 + - node-version: 7 + eslint: 9 - node-version: 7 eslint: 8 - node-version: 7 eslint: 7 - node-version: 7 eslint: 6 + - node-version: 6 + eslint: 9 - node-version: 6 eslint: 8 - node-version: 6 eslint: 7 - node-version: 6 eslint: 6 + - node-version: 5 + eslint: 9 - node-version: 5 eslint: 8 - node-version: 5 @@ -99,6 +124,8 @@ jobs: eslint: 6 - node-version: 5 eslint: 5 + - node-version: 4 + eslint: 9 - node-version: 4 eslint: 8 - node-version: 4 diff --git a/.nycrc b/.nycrc index 5d75e2157..c5396cb18 100644 --- a/.nycrc +++ b/.nycrc @@ -14,6 +14,7 @@ "resolvers/*/test", "scripts", "memo-parser", - "lib" + "lib", + "examples" ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 9022dc887..ab57069c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,31 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ## [Unreleased] +## [2.31.0] - 2024-10-03 + +### Added +- support eslint v9 ([#2996], thanks [@G-Rath] [@michaelfaith]) +- [`order`]: allow validating named imports ([#3043], thanks [@manuth]) +- [`extensions`]: add the `checkTypeImports` option ([#2817], thanks [@phryneas]) + +### Fixed +- `ExportMap` / flat config: include `languageOptions` in context ([#3052], thanks [@michaelfaith]) +- [`no-named-as-default`]: Allow using an identifier if the export is both a named and a default export ([#3032], thanks [@akwodkiewicz]) +- [`export`]: False positive for exported overloaded functions in TS ([#3065], thanks [@liuxingbaoyu]) +- `exportMap`: export map cache is tainted by unreliable parse results ([#3062], thanks [@michaelfaith]) +- `exportMap`: improve cacheKey when using flat config ([#3072], thanks [@michaelfaith]) +- adjust "is source type module" checks for flat config ([#2996], thanks [@G-Rath]) + +### Changed +- [Docs] [`no-relative-packages`]: fix typo ([#3066], thanks [@joshuaobrien]) +- [Performance] [`no-cycle`]: dont scc for each linted file ([#3068], thanks [@soryy708]) +- [Docs] [`no-cycle`]: add `disableScc` to docs ([#3070], thanks [@soryy708]) +- [Tests] use re-exported `RuleTester` ([#3071], thanks [@G-Rath]) +- [Docs] [`no-restricted-paths`]: fix grammar ([#3073], thanks [@unbeauvoyage]) +- [Tests] [`no-default-export`], [`no-named-export`]: add test case (thanks [@G-Rath]) + +## [2.30.0] - 2024-09-02 + ### Added - [`dynamic-import-chunkname`]: add `allowEmpty` option to allow empty leading comments ([#2942], thanks [@JiangWeixian]) - [`dynamic-import-chunkname`]: Allow empty chunk name when webpackMode: 'eager' is set; add suggestions to remove name in eager mode ([#3004], thanks [@amsardesai]) @@ -20,11 +45,11 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange - [`order`]: do not compare first path segment for relative paths ([#2682]) ([#2885], thanks [@mihkeleidast]) ### Changed -- [Docs] `no-extraneous-dependencies`: Make glob pattern description more explicit ([#2944], thanks [@mulztob]) +- [Docs] [`no-extraneous-dependencies`]: Make glob pattern description more explicit ([#2944], thanks [@mulztob]) - [`no-unused-modules`]: add console message to help debug [#2866] - [Refactor] `ExportMap`: make procedures static instead of monkeypatching exportmap ([#2982], thanks [@soryy708]) - [Refactor] `ExportMap`: separate ExportMap instance from its builder logic ([#2985], thanks [@soryy708]) -- [Docs] `order`: Add a quick note on how unbound imports and --fix ([#2640], thanks [@minervabot]) +- [Docs] [`order`]: Add a quick note on how unbound imports and --fix ([#2640], thanks [@minervabot]) - [Tests] appveyor -> GHA (run tests on Windows in both pwsh and WSL + Ubuntu) ([#2987], thanks [@joeyguerra]) - [actions] migrate OSX tests to GHA ([ljharb#37], thanks [@aks-]) - [Refactor] `exportMapBuilder`: avoid hoisting ([#2989], thanks [@soryy708]) @@ -60,7 +85,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange - [`no-duplicates`]: remove duplicate identifiers in duplicate imports ([#2577], thanks [@joe-matsec]) - [`consistent-type-specifier-style`]: fix accidental removal of comma in certain cases ([#2754], thanks [@bradzacher]) - [Perf] `ExportMap`: Improve `ExportMap.for` performance on larger codebases ([#2756], thanks [@leipert]) -- [`no-extraneous-dependencies`]/TypeScript: do not error when importing inline type from dev dependencies ([#1820], thanks [@andyogo]) +- [`no-extraneous-dependencies`]/TypeScript: do not error when importing inline type from dev dependencies ([#2735], thanks [@andyogo]) - [`newline-after-import`]/TypeScript: do not error when re-exporting a namespaced import ([#2832], thanks [@laurens-dg]) - [`order`]: partial fix for [#2687] (thanks [@ljharb]) - [`no-duplicates`]: Detect across type and regular imports ([#2835], thanks [@benkrejci]) @@ -316,7 +341,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange - [`no-webpack-loader-syntax`]/TypeScript: avoid crash on missing name ([#1947], thanks [@leonardodino]) - [`no-extraneous-dependencies`]: Add package.json cache ([#1948], thanks [@fa93hws]) - [`prefer-default-export`]: handle empty array destructuring ([#1965], thanks [@ljharb]) -- [`no-unused-modules`]: make type imports mark a module as used (fixes #1924) ([#1974], thanks [@cherryblossom000]) +- [`no-unused-modules`]: make type imports mark a module as used (fixes [#1924]) ([#1974], thanks [@cherryblossom000]) - [`no-cycle`]: fix perf regression ([#1944], thanks [@Blasz]) - [`first`]: fix handling of `import = require` ([#1963], thanks [@MatthiasKunnen]) - [`no-cycle`]/[`extensions`]: fix isExternalModule usage ([#1696], thanks [@paztis]) @@ -344,7 +369,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange - [`dynamic-import-chunkname`]: allow single quotes to match Webpack support ([#1848], thanks [@straub]) ### Changed -- [`export`]: add tests for a name collision with `export * from` ([#1704], thanks @tomprats) +- [`export`]: add tests for a name collision with `export * from` ([#1704], thanks [@tomprats]) ## [2.22.0] - 2020-06-26 @@ -1127,13 +1152,25 @@ for info on changes for earlier releases. [`memo-parser`]: ./memo-parser/README.md +[#3073]: https://github.com/import-js/eslint-plugin-import/pull/3073 +[#3072]: https://github.com/import-js/eslint-plugin-import/pull/3072 +[#3071]: https://github.com/import-js/eslint-plugin-import/pull/3071 +[#3070]: https://github.com/import-js/eslint-plugin-import/pull/3070 +[#3068]: https://github.com/import-js/eslint-plugin-import/pull/3068 +[#3066]: https://github.com/import-js/eslint-plugin-import/pull/3066 +[#3065]: https://github.com/import-js/eslint-plugin-import/pull/3065 +[#3062]: https://github.com/import-js/eslint-plugin-import/pull/3062 +[#3052]: https://github.com/import-js/eslint-plugin-import/pull/3052 +[#3043]: https://github.com/import-js/eslint-plugin-import/pull/3043 [#3036]: https://github.com/import-js/eslint-plugin-import/pull/3036 [#3033]: https://github.com/import-js/eslint-plugin-import/pull/3033 +[#3032]: https://github.com/import-js/eslint-plugin-import/pull/3032 [#3018]: https://github.com/import-js/eslint-plugin-import/pull/3018 [#3012]: https://github.com/import-js/eslint-plugin-import/pull/3012 [#3011]: https://github.com/import-js/eslint-plugin-import/pull/3011 [#3004]: https://github.com/import-js/eslint-plugin-import/pull/3004 [#2998]: https://github.com/import-js/eslint-plugin-import/pull/2998 +[#2996]: https://github.com/import-js/eslint-plugin-import/pull/2996 [#2993]: https://github.com/import-js/eslint-plugin-import/pull/2993 [#2991]: https://github.com/import-js/eslint-plugin-import/pull/2991 [#2989]: https://github.com/import-js/eslint-plugin-import/pull/2989 @@ -1153,6 +1190,7 @@ for info on changes for earlier releases. [#2842]: https://github.com/import-js/eslint-plugin-import/pull/2842 [#2835]: https://github.com/import-js/eslint-plugin-import/pull/2835 [#2832]: https://github.com/import-js/eslint-plugin-import/pull/2832 +[#2817]: https://github.com/import-js/eslint-plugin-import/pull/2817 [#2778]: https://github.com/import-js/eslint-plugin-import/pull/2778 [#2756]: https://github.com/import-js/eslint-plugin-import/pull/2756 [#2754]: https://github.com/import-js/eslint-plugin-import/pull/2754 @@ -1467,10 +1505,7 @@ for info on changes for earlier releases. [#297]: https://github.com/import-js/eslint-plugin-import/pull/297 [#296]: https://github.com/import-js/eslint-plugin-import/pull/296 [#290]: https://github.com/import-js/eslint-plugin-import/pull/290 -[#289]: https://github.com/import-js/eslint-plugin-import/pull/289 [#288]: https://github.com/import-js/eslint-plugin-import/pull/288 -[#287]: https://github.com/import-js/eslint-plugin-import/pull/287 -[#278]: https://github.com/import-js/eslint-plugin-import/pull/278 [#261]: https://github.com/import-js/eslint-plugin-import/pull/261 [#256]: https://github.com/import-js/eslint-plugin-import/pull/256 [#254]: https://github.com/import-js/eslint-plugin-import/pull/254 @@ -1482,7 +1517,6 @@ for info on changes for earlier releases. [#239]: https://github.com/import-js/eslint-plugin-import/pull/239 [#228]: https://github.com/import-js/eslint-plugin-import/pull/228 [#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 [ljharb#37]: https://github.com/ljharb/eslint-plugin-import/pull/37 @@ -1595,7 +1629,6 @@ for info on changes for earlier releases. [#313]: https://github.com/import-js/eslint-plugin-import/issues/313 [#311]: https://github.com/import-js/eslint-plugin-import/issues/311 [#306]: https://github.com/import-js/eslint-plugin-import/issues/306 -[#286]: https://github.com/import-js/eslint-plugin-import/issues/286 [#283]: https://github.com/import-js/eslint-plugin-import/issues/283 [#281]: https://github.com/import-js/eslint-plugin-import/issues/281 [#275]: https://github.com/import-js/eslint-plugin-import/issues/275 @@ -1615,7 +1648,9 @@ 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.29.1...HEAD +[Unreleased]: https://github.com/import-js/eslint-plugin-import/compare/v2.31.0...HEAD +[2.31.0]: https://github.com/import-js/eslint-plugin-import/compare/v2.30.0...v2.31.0 +[2.30.0]: https://github.com/import-js/eslint-plugin-import/compare/v2.29.1...v2.30.0 [2.29.1]: https://github.com/import-js/eslint-plugin-import/compare/v2.29.0...v2.29.1 [2.29.0]: https://github.com/import-js/eslint-plugin-import/compare/v2.28.1...v2.29.0 [2.28.1]: https://github.com/import-js/eslint-plugin-import/compare/v2.28.0...v2.28.1 @@ -1644,10 +1679,9 @@ for info on changes for earlier releases. [2.22.0]: https://github.com/import-js/eslint-plugin-import/compare/v2.21.1...v2.22.0 [2.21.2]: https://github.com/import-js/eslint-plugin-import/compare/v2.21.1...v2.21.2 [2.21.1]: https://github.com/import-js/eslint-plugin-import/compare/v2.21.0...v2.21.1 -[2.21.0]: https://github.com/import-js/eslint-plugin-import/compare/v2.20.2...v2.21.0 -[2.20.1]: https://github.com/import-js/eslint-plugin-import/compare/v2.20.1...v2.20.2 -[2.20.0]: https://github.com/import-js/eslint-plugin-import/compare/v2.20.0...v2.20.1 -[2.19.1]: https://github.com/import-js/eslint-plugin-import/compare/v2.19.1...v2.20.0 +[2.21.0]: https://github.com/import-js/eslint-plugin-import/compare/v2.20.1...v2.21.0 +[2.20.1]: https://github.com/import-js/eslint-plugin-import/compare/v2.20.0...v2.20.1 +[2.20.0]: https://github.com/import-js/eslint-plugin-import/compare/v2.19.1...v2.20.0 [2.19.1]: https://github.com/import-js/eslint-plugin-import/compare/v2.19.0...v2.19.1 [2.19.0]: https://github.com/import-js/eslint-plugin-import/compare/v2.18.2...v2.19.0 [2.18.2]: https://github.com/import-js/eslint-plugin-import/compare/v2.18.1...v2.18.2 @@ -1721,6 +1755,7 @@ for info on changes for earlier releases. [@AdriAt360]: https://github.com/AdriAt360 [@ai]: https://github.com/ai [@aks-]: https://github.com/aks- +[@akwodkiewicz]: https://github.com/akwodkiewicz [@aladdin-add]: https://github.com/aladdin-add [@alex-page]: https://github.com/alex-page [@alexgorbatchev]: https://github.com/alexgorbatchev @@ -1796,6 +1831,7 @@ for info on changes for earlier releases. [@fsmaia]: https://github.com/fsmaia [@fson]: https://github.com/fson [@futpib]: https://github.com/futpib +[@G-Rath]: https://github.com/G-Rath [@gajus]: https://github.com/gajus [@gausie]: https://github.com/gausie [@gavriguy]: https://github.com/gavriguy @@ -1832,6 +1868,7 @@ for info on changes for earlier releases. [@johnthagen]: https://github.com/johnthagen [@jonboiser]: https://github.com/jonboiser [@josh]: https://github.com/josh +[@joshuaobrien]: https://github.com/joshuaobrien [@JounQin]: https://github.com/JounQin [@jquense]: https://github.com/jquense [@jseminck]: https://github.com/jseminck @@ -1862,6 +1899,7 @@ for info on changes for earlier releases. [@lilling]: https://github.com/lilling [@ljharb]: https://github.com/ljharb [@ljqx]: https://github.com/ljqx +[@liuxingbaoyu]: https://github.com/liuxingbaoyu [@lo1tuma]: https://github.com/lo1tuma [@loganfsmyth]: https://github.com/loganfsmyth [@luczsoma]: https://github.com/luczsoma @@ -1910,6 +1948,7 @@ for info on changes for earlier releases. [@pcorpet]: https://github.com/pcorpet [@Pearce-Ropion]: https://github.com/Pearce-Ropion [@Pessimistress]: https://github.com/Pessimistress +[@phryneas]: https://github.com/phryneas [@pmcelhaney]: https://github.com/pmcelhaney [@preco21]: https://github.com/preco21 [@pri1311]: https://github.com/pri1311 @@ -1959,7 +1998,6 @@ for info on changes for earlier releases. [@sveyret]: https://github.com/sveyret [@swernerx]: https://github.com/swernerx [@syymza]: https://github.com/syymza -[@taion]: https://github.com/taion [@TakeScoop]: https://github.com/TakeScoop [@tapayne88]: https://github.com/tapayne88 [@Taranys]: https://github.com/Taranys @@ -1972,6 +2010,7 @@ for info on changes for earlier releases. [@tomprats]: https://github.com/tomprats [@TrevorBurnham]: https://github.com/TrevorBurnham [@ttmarek]: https://github.com/ttmarek +[@unbeauvoyage]: https://github.com/unbeauvoyage [@vikr01]: https://github.com/vikr01 [@wenfangdu]: https://github.com/wenfangdu [@wKich]: https://github.com/wKich diff --git a/docs/rules/dynamic-import-chunkname.md b/docs/rules/dynamic-import-chunkname.md index de554148e..d9ee8d15e 100644 --- a/docs/rules/dynamic-import-chunkname.md +++ b/docs/rules/dynamic-import-chunkname.md @@ -15,7 +15,7 @@ You can also configure the regex format you'd like to accept for the webpackChun ```javascript { - "dynamic-import-chunkname": [2, { + "import/dynamic-import-chunkname": [2, { importFunctions: ["dynamicImport"], webpackChunknameFormat: "[a-zA-Z0-57-9-/_]+", allowEmpty: false diff --git a/docs/rules/extensions.md b/docs/rules/extensions.md index 946ccb7bf..5d15e93f1 100644 --- a/docs/rules/extensions.md +++ b/docs/rules/extensions.md @@ -56,6 +56,8 @@ For example, `["error", "never", { "svg": "always" }]` would require that all ex In that case, if you still want to specify extensions, you can do so inside the **pattern** property. Default value of `ignorePackages` is `false`. +By default, `import type` and `export type` style imports/exports are ignored. If you want to check them as well, you can set the `checkTypeImports` option to `true`. + ### Exception When disallowing the use of certain extensions this rule makes an exception and allows the use of extension when the file would not be resolvable without extension. @@ -104,6 +106,14 @@ import express from 'express/index'; import * as path from 'path'; ``` +The following patterns are considered problems when the configuration is set to "never" and the option "checkTypeImports" is set to `true`: + +```js +import type { Foo } from './foo.ts'; + +export type { Foo } from './foo.ts'; +``` + The following patterns are considered problems when configuration set to "always": ```js @@ -167,6 +177,14 @@ import express from 'express'; import foo from '@/foo'; ``` +The following patterns are considered problems when the configuration is set to "always" and the option "checkTypeImports" is set to `true`: + +```js +import type { Foo } from './foo'; + +export type { Foo } from './foo'; +``` + ## When Not To Use It If you are not concerned about a consistent usage of file extension. diff --git a/docs/rules/no-cycle.md b/docs/rules/no-cycle.md index 76e96f95f..898b75330 100644 --- a/docs/rules/no-cycle.md +++ b/docs/rules/no-cycle.md @@ -94,6 +94,14 @@ export function getBar() { return import('./bar'); } > Cyclic dependency are **always** a dangerous anti-pattern as discussed extensively in [#2265](https://github.com/import-js/eslint-plugin-import/issues/2265). Please be extra careful about using this option. +#### `disableScc` + +This option disables a pre-processing step that calculates [Strongly Connected Components](https://en.wikipedia.org/wiki/Strongly_connected_component), which are used for avoiding unnecessary work checking files in different SCCs for cycles. + +However, under some configurations, this pre-processing may be more expensive than the time it saves. + +When this option is `true`, we don't calculate any SCC graph, and check all files for cycles (leading to higher time-complexity). Default is `false`. + ## When Not To Use It This rule is comparatively computationally expensive. If you are pressed for lint diff --git a/docs/rules/no-relative-packages.md b/docs/rules/no-relative-packages.md index 4014ed985..ed724a9eb 100644 --- a/docs/rules/no-relative-packages.md +++ b/docs/rules/no-relative-packages.md @@ -6,8 +6,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. +It's useful in Yarn/Lerna workspaces, where it's possible to import a sibling package using `../package` relative path, while direct `package` is the correct one. ## Examples diff --git a/docs/rules/no-restricted-paths.md b/docs/rules/no-restricted-paths.md index 293f3ba00..a905226c2 100644 --- a/docs/rules/no-restricted-paths.md +++ b/docs/rules/no-restricted-paths.md @@ -5,7 +5,7 @@ Some projects contain files which are not always meant to be executed in the same environment. For example consider a web application that contains specific code for the server and some specific code for the browser/client. In this case you don’t want to import server-only files in your client code. -In order to prevent such scenarios this rule allows you to define restricted zones where you can forbid files from imported if they match a specific path. +In order to prevent such scenarios this rule allows you to define restricted zones where you can forbid files from being imported if they match a specific path. ## Rule Details diff --git a/docs/rules/order.md b/docs/rules/order.md index 67849bb7e..676279953 100644 --- a/docs/rules/order.md +++ b/docs/rules/order.md @@ -285,6 +285,78 @@ import index from './'; import sibling from './foo'; ``` +### `named: true|false|{ enabled: true|false, import: true|false, export: true|false, require: true|false, cjsExports: true|false, types: mixed|types-first|types-last }` + +Enforce ordering of names within imports and exports: + + - If set to `true`, named imports must be ordered according to the `alphabetize` options + - If set to `false`, named imports can occur in any order + +`enabled` enables the named ordering for all expressions by default. +Use `import`, `export` and `require` and `cjsExports` to override the enablement for the following kind of expressions: + + - `import`: + + ```ts + import { Readline } from "readline"; + ``` + + - `export`: + + ```ts + export { Readline }; + // and + export { Readline } from "readline"; + ``` + + - `require` + + ```ts + const { Readline } = require("readline"); + ``` + + - `cjsExports` + + ```ts + module.exports.Readline = Readline; + // and + module.exports = { Readline }; + ``` + +The `types` option allows you to specify the order of `import`s and `export`s of `type` specifiers. +Following values are possible: + + - `types-first`: forces `type` specifiers to occur first + - `types-last`: forces value specifiers to occur first + - `mixed`: sorts all specifiers in alphabetical order + +The default value is `false`. + +Example setting: + +```ts +{ + named: true, + alphabetize: { + order: 'asc' + } +} +``` + +This will fail the rule check: + +```ts +/* eslint import/order: ["error", {"named": true, "alphabetize": {"order": "asc"}}] */ +import { compose, apply } from 'xcompose'; +``` + +While this will pass: + +```ts +/* eslint import/order: ["error", {"named": true, "alphabetize": {"order": "asc"}}] */ +import { apply, compose } from 'xcompose'; +``` + ### `alphabetize: {order: asc|desc|ignore, orderImportKind: asc|desc|ignore, caseInsensitive: true|false}` Sort the order within each group in alphabetical manner based on **import path**: diff --git a/examples/flat/eslint.config.mjs b/examples/flat/eslint.config.mjs index 370514a65..143265265 100644 --- a/examples/flat/eslint.config.mjs +++ b/examples/flat/eslint.config.mjs @@ -20,6 +20,7 @@ export default [ 'import/no-dynamic-require': 'warn', 'import/no-nodejs-modules': 'warn', 'import/no-unused-modules': ['warn', { unusedExports: true }], + 'import/no-cycle': 'warn', }, }, ]; diff --git a/examples/flat/src/depth-zero.js b/examples/flat/src/depth-zero.js new file mode 100644 index 000000000..8cfde9979 --- /dev/null +++ b/examples/flat/src/depth-zero.js @@ -0,0 +1,3 @@ +import { foo } from "./es6/depth-one-dynamic"; + +foo(); diff --git a/examples/flat/src/es6/depth-one-dynamic.js b/examples/flat/src/es6/depth-one-dynamic.js new file mode 100644 index 000000000..ca129fd62 --- /dev/null +++ b/examples/flat/src/es6/depth-one-dynamic.js @@ -0,0 +1,3 @@ +export function foo() {} + +export const bar = () => import("../depth-zero").then(({foo}) => foo); diff --git a/examples/legacy/.eslintrc.cjs b/examples/legacy/.eslintrc.cjs index e3cec097f..90e065c9d 100644 --- a/examples/legacy/.eslintrc.cjs +++ b/examples/legacy/.eslintrc.cjs @@ -20,5 +20,6 @@ module.exports = { 'import/no-dynamic-require': 'warn', 'import/no-nodejs-modules': 'warn', 'import/no-unused-modules': ['warn', { unusedExports: true }], + 'import/no-cycle': 'warn', }, }; diff --git a/examples/legacy/src/depth-zero.js b/examples/legacy/src/depth-zero.js new file mode 100644 index 000000000..8cfde9979 --- /dev/null +++ b/examples/legacy/src/depth-zero.js @@ -0,0 +1,3 @@ +import { foo } from "./es6/depth-one-dynamic"; + +foo(); diff --git a/examples/legacy/src/es6/depth-one-dynamic.js b/examples/legacy/src/es6/depth-one-dynamic.js new file mode 100644 index 000000000..cda7091cd --- /dev/null +++ b/examples/legacy/src/es6/depth-one-dynamic.js @@ -0,0 +1,3 @@ +export function foo() {} + +export const bar = () => import("../depth-zero").then(({ foo }) => foo); diff --git a/package.json b/package.json index fff8d85c3..a513514e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@exodus/eslint-plugin-import", - "version": "2.29.1-exodus-1", + "version": "2.31.0", "description": "Import with sanity.", "engines": { "node": ">=4" @@ -76,7 +76,7 @@ "chai": "^4.3.10", "cross-env": "^4.0.0", "escope": "^3.6.0", - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8", + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9", "eslint-doc-generator": "^1.6.1", "eslint-import-resolver-node": "file:./resolvers/node", "eslint-import-resolver-typescript": "^1.0.2 || ^1.1.1", @@ -106,7 +106,7 @@ "typescript-eslint-parser": "^15 || ^20 || ^22" }, "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" }, "dependencies": { "@rtsao/scc": "^1.1.0", @@ -117,7 +117,7 @@ "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.2", + "eslint-module-utils": "^2.12.0", "hasown": "^2.0.2", "is-core-module": "^2.15.1", "is-glob": "^4.0.3", @@ -126,6 +126,7 @@ "object.groupby": "^1.0.3", "object.values": "^1.2.0", "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" } } diff --git a/resolvers/webpack/CHANGELOG.md b/resolvers/webpack/CHANGELOG.md index cd49cc3f4..1d23bf6fd 100644 --- a/resolvers/webpack/CHANGELOG.md +++ b/resolvers/webpack/CHANGELOG.md @@ -4,8 +4,12 @@ This project adheres to [Semantic Versioning](https://semver.org/). This change log adheres to standards from [Keep a CHANGELOG](https://keepachangelog.com). ## Unreleased +- [new] add cache option ([#3100], thanks [@seiyab]) +## 0.13.9 - 2024-09-02 - [refactor] simplify loop ([#3029], thanks [@fregante]) +- [meta] add `repository.directory` field +- [refactor] avoid hoisting, misc cleanup ## 0.13.8 - 2023-10-22 - [refactor] use `hasown` instead of `has` @@ -179,6 +183,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ### Added - `interpret` configs (such as `.babel.js`). Thanks to [@gausie] for the initial PR ([#164], ages ago! 😅) and [@jquense] for tests ([#278]). +[#3100]: https://github.com/import-js/eslint-plugin-import/pull/3100 [#3029]: https://github.com/import-js/eslint-plugin-import/pull/3029 [#2287]: https://github.com/import-js/eslint-plugin-import/pull/2287 [#2023]: https://github.com/import-js/eslint-plugin-import/pull/2023 @@ -244,6 +249,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange [@Rogeres]: https://github.com/Rogeres [@Satyam]: https://github.com/Satyam [@Schweinepriester]: https://github.com/Schweinepriester +[@seiyab]: https://github.com/seiyab [@SkeLLLa]: https://github.com/SkeLLLa [@taion]: https://github.com/taion [@toshafed]: https://github.com/toshafed diff --git a/resolvers/webpack/README.md b/resolvers/webpack/README.md index 06513ba14..9b0139689 100644 --- a/resolvers/webpack/README.md +++ b/resolvers/webpack/README.md @@ -94,6 +94,17 @@ settings: production: true ``` +If your config is set as a function, it will be evaluated at every resolution. You have an option to prevent this by caching it using the `cache` parameter: + +```yaml +--- +settings: + import/resolver: + webpack: + config: 'webpack.config.js' + cache: true +``` + ## Support [Get supported eslint-import-resolver-webpack with the Tidelift Subscription](https://tidelift.com/subscription/pkg/npm-eslint-import-resolver-webpack?utm_source=npm-eslint-import-resolver-webpack&utm_medium=referral&utm_campaign=readme) diff --git a/resolvers/webpack/index.js b/resolvers/webpack/index.js index 83297cd18..ae736abe7 100644 --- a/resolvers/webpack/index.js +++ b/resolvers/webpack/index.js @@ -311,6 +311,22 @@ function getResolveSync(configPath, webpackConfig, cwd) { return cached.value; } +const _evalCache = new Map(); +function evaluateFunctionConfigCached(configPath, webpackConfig, env, argv) { + const cacheKey = JSON.stringify({ configPath, args: [env, argv] }); + if (_evalCache.has(cacheKey)) { + return _evalCache.get(cacheKey); + } + const cached = webpackConfig(env, argv); + _evalCache.set(cacheKey, cached); + + while (_evalCache.size > MAX_CACHE) { + // remove oldest item + _evalCache.delete(_evalCache.keys().next().value); + } + return cached; +} + /** * Find the full path to 'source', given 'file' as a full reference path. * @@ -354,6 +370,7 @@ exports.resolve = function (source, file, settings) { const configIndex = settings && settings['config-index']; const env = settings && settings.env; const argv = settings && typeof settings.argv !== 'undefined' ? settings.argv : {}; + const shouldCacheFunctionConfig = settings && settings.cache; let packageDir; let configPath = typeof _configPath === 'string' && _configPath.startsWith('.') @@ -398,7 +415,9 @@ exports.resolve = function (source, file, settings) { } if (typeof webpackConfig === 'function') { - webpackConfig = webpackConfig(env, argv); + webpackConfig = shouldCacheFunctionConfig + ? evaluateFunctionConfigCached(configPath, webpackConfig, env, argv) + : webpackConfig(env, argv); } if (isArray(webpackConfig)) { diff --git a/resolvers/webpack/package.json b/resolvers/webpack/package.json index 38465bcde..60e5c900f 100644 --- a/resolvers/webpack/package.json +++ b/resolvers/webpack/package.json @@ -1,6 +1,6 @@ { "name": "eslint-import-resolver-webpack", - "version": "0.13.8", + "version": "0.13.9", "description": "Resolve paths to dependencies, given a webpack.config.js. Plugin for eslint-plugin-import.", "main": "index.js", "scripts": { diff --git a/resolvers/webpack/test/cache.js b/resolvers/webpack/test/cache.js new file mode 100644 index 000000000..04d6de057 --- /dev/null +++ b/resolvers/webpack/test/cache.js @@ -0,0 +1,48 @@ +'use strict'; + +const chai = require('chai'); +const expect = chai.expect; +const path = require('path'); + +const resolve = require('../index').resolve; + +const file = path.join(__dirname, 'files', 'src', 'jsx', 'dummy.js'); + +describe('cache', function () { + it('can distinguish different config files', function () { + const setting1 = { + config: require(path.join(__dirname, './files/webpack.function.config.js')), + argv: { + mode: 'test', + }, + cache: true, + }; + expect(resolve('baz', file, setting1)).to.have.property('path') + .and.equal(path.join(__dirname, 'files', 'some', 'bar', 'bar.js')); + const setting2 = { + config: require(path.join(__dirname, './files/webpack.function.config.multiple.js')), + cache: true, + }; + expect(resolve('baz', file, setting2)).to.have.property('path') + .and.equal(path.join(__dirname, 'files', 'some', 'goofy', 'path', 'foo.js')); + }); + + it('can distinguish different config', function () { + const setting1 = { + config: require(path.join(__dirname, './files/webpack.function.config.js')), + env: { + dummy: true, + }, + cache: true, + }; + expect(resolve('bar', file, setting1)).to.have.property('path') + .and.equal(path.join(__dirname, 'files', 'some', 'goofy', 'path', 'bar.js')); + const setting2 = { + config: require(path.join(__dirname, './files/webpack.function.config.multiple.js')), + cache: true, + }; + const result = resolve('bar', file, setting2); + expect(result).not.to.have.property('path'); + expect(result).to.have.property('found').to.be.false; + }); +}); diff --git a/resolvers/webpack/test/files/webpack.function.config.multiple.js b/resolvers/webpack/test/files/webpack.function.config.multiple.js index 4dbc94bbc..8ab982bbc 100644 --- a/resolvers/webpack/test/files/webpack.function.config.multiple.js +++ b/resolvers/webpack/test/files/webpack.function.config.multiple.js @@ -7,6 +7,7 @@ module.exports = [function(env) { alias: { 'foo': path.join(__dirname, 'some', 'goofy', 'path', 'foo.js'), 'bar': env ? path.join(__dirname, 'some', 'goofy', 'path', 'bar.js') : undefined, + 'baz': path.join(__dirname, 'some', 'goofy', 'path', 'foo.js'), 'some-alias': path.join(__dirname, 'some'), }, modules: [ diff --git a/src/core/packagePath.js b/src/core/packagePath.js index 142f44aa4..f45f54326 100644 --- a/src/core/packagePath.js +++ b/src/core/packagePath.js @@ -1,4 +1,5 @@ import { dirname } from 'path'; +import { getPhysicalFilename } from 'eslint-module-utils/contextCompat'; import pkgUp from 'eslint-module-utils/pkgUp'; import readPkgUp from 'eslint-module-utils/readPkgUp'; @@ -8,7 +9,7 @@ export function getFilePackagePath(filePath) { } export function getContextPackagePath(context) { - return getFilePackagePath(context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename()); + return getFilePackagePath(getPhysicalFilename(context)); } export function getFilePackageName(filePath) { diff --git a/src/core/sourceType.js b/src/core/sourceType.js new file mode 100644 index 000000000..5ff92edc9 --- /dev/null +++ b/src/core/sourceType.js @@ -0,0 +1,12 @@ +/** + * @param {import('eslint').Rule.RuleContext} context + * @returns 'module' | 'script' | 'commonjs' | undefined + */ +export default function sourceType(context) { + if ('sourceType' in context.parserOptions) { + return context.parserOptions.sourceType; + } + if ('languageOptions' in context && context.languageOptions) { + return context.languageOptions.sourceType; + } +} diff --git a/src/exportMap/builder.js b/src/exportMap/builder.js index 5348dba37..f7b9006ef 100644 --- a/src/exportMap/builder.js +++ b/src/exportMap/builder.js @@ -92,7 +92,11 @@ export default class ExportMapBuilder { exportMap.mtime = stats.mtime; - exportCache.set(cacheKey, exportMap); + // If the visitor keys were not populated, then we shouldn't save anything to the cache, + // since the parse results may not be reliable. + if (exportMap.visitorKeys) { + exportCache.set(cacheKey, exportMap); + } return exportMap; } diff --git a/src/exportMap/childContext.js b/src/exportMap/childContext.js index 5f82b8e57..8994ac206 100644 --- a/src/exportMap/childContext.js +++ b/src/exportMap/childContext.js @@ -1,32 +1,56 @@ import { hashObject } from 'eslint-module-utils/hash'; -let parserOptionsHash = ''; -let prevParserOptions = ''; +let optionsHash = ''; +let prevOptions = ''; let settingsHash = ''; let prevSettings = ''; +// Replacer function helps us with serializing the parser nested within `languageOptions`. +function stringifyReplacerFn(_, value) { + if (typeof value === 'function') { + return String(value); + } + return value; +} + /** * don't hold full context object in memory, just grab what we need. * also calculate a cacheKey, where parts of the cacheKey hash are memoized */ export default function childContext(path, context) { - const { settings, parserOptions, parserPath } = context; + const { settings, parserOptions, parserPath, languageOptions } = context; if (JSON.stringify(settings) !== prevSettings) { settingsHash = hashObject({ settings }).digest('hex'); prevSettings = JSON.stringify(settings); } - if (JSON.stringify(parserOptions) !== prevParserOptions) { - parserOptionsHash = hashObject({ parserOptions }).digest('hex'); - prevParserOptions = JSON.stringify(parserOptions); + // We'll use either a combination of `parserOptions` and `parserPath` or `languageOptions` + // to construct the cache key, depending on whether this is using a flat config or not. + let optionsToken; + if (!parserPath && languageOptions) { + if (JSON.stringify(languageOptions, stringifyReplacerFn) !== prevOptions) { + optionsHash = hashObject({ languageOptions }).digest('hex'); + prevOptions = JSON.stringify(languageOptions, stringifyReplacerFn); + } + // For languageOptions, we're just using the hashed options as the options token + optionsToken = optionsHash; + } else { + if (JSON.stringify(parserOptions) !== prevOptions) { + optionsHash = hashObject({ parserOptions }).digest('hex'); + prevOptions = JSON.stringify(parserOptions); + } + // When not using flat config, we use a combination of the hashed parserOptions + // and parserPath as the token + optionsToken = String(parserPath) + optionsHash; } return { - cacheKey: String(parserPath) + parserOptionsHash + settingsHash + String(path), + cacheKey: optionsToken + settingsHash + String(path), settings, parserOptions, parserPath, path, + languageOptions, }; } diff --git a/src/importDeclaration.js b/src/importDeclaration.js index 0d5e1870a..49446b260 100644 --- a/src/importDeclaration.js +++ b/src/importDeclaration.js @@ -1,4 +1,6 @@ -export default function importDeclaration(context) { - const ancestors = context.getAncestors(); +import { getAncestors } from 'eslint-module-utils/contextCompat'; + +export default function importDeclaration(context, node) { + const ancestors = getAncestors(context, node); return ancestors[ancestors.length - 1]; } diff --git a/src/rules/consistent-type-specifier-style.js b/src/rules/consistent-type-specifier-style.js index 9119976b1..84c33ecd8 100644 --- a/src/rules/consistent-type-specifier-style.js +++ b/src/rules/consistent-type-specifier-style.js @@ -1,9 +1,17 @@ +import { getSourceCode } from 'eslint-module-utils/contextCompat'; + import docsUrl from '../docsUrl'; function isComma(token) { return token.type === 'Punctuator' && token.value === ','; } +/** + * @param {import('eslint').Rule.Fix[]} fixes + * @param {import('eslint').Rule.RuleFixer} fixer + * @param {import('eslint').SourceCode.SourceCode} sourceCode + * @param {(ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier)[]} specifiers + * */ function removeSpecifiers(fixes, fixer, sourceCode, specifiers) { for (const specifier of specifiers) { // remove the trailing comma @@ -15,6 +23,7 @@ function removeSpecifiers(fixes, fixer, sourceCode, specifiers) { } } +/** @type {(node: import('estree').Node, sourceCode: import('eslint').SourceCode.SourceCode, specifiers: (ImportSpecifier | ImportNamespaceSpecifier)[], kind: 'type' | 'typeof') => string} */ function getImportText( node, sourceCode, @@ -36,6 +45,7 @@ function getImportText( return `import ${kind} {${names.join(', ')}} from ${sourceString};`; } +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { type: 'suggestion', @@ -55,7 +65,7 @@ module.exports = { }, create(context) { - const sourceCode = context.getSourceCode(); + const sourceCode = getSourceCode(context); if (context.options[0] === 'prefer-inline') { return { @@ -100,6 +110,7 @@ module.exports = { // prefer-top-level return { + /** @param {import('estree').ImportDeclaration} node */ ImportDeclaration(node) { if ( // already top-level is valid @@ -118,9 +129,13 @@ module.exports = { return; } + /** @type {typeof node.specifiers} */ const typeSpecifiers = []; + /** @type {typeof node.specifiers} */ const typeofSpecifiers = []; + /** @type {typeof node.specifiers} */ const valueSpecifiers = []; + /** @type {typeof node.specifiers[number]} */ let defaultSpecifier = null; for (const specifier of node.specifiers) { if (specifier.type === 'ImportDefaultSpecifier') { @@ -142,6 +157,7 @@ module.exports = { const newImports = `${typeImport}\n${typeofImport}`.trim(); if (typeSpecifiers.length + typeofSpecifiers.length === node.specifiers.length) { + /** @type {('type' | 'typeof')[]} */ // all specifiers have inline specifiers - so we replace the entire import const kind = [].concat( typeSpecifiers.length > 0 ? 'type' : [], @@ -160,7 +176,7 @@ module.exports = { }); } else { // remove specific specifiers and insert new imports for them - for (const specifier of typeSpecifiers.concat(typeofSpecifiers)) { + typeSpecifiers.concat(typeofSpecifiers).forEach((specifier) => { context.report({ node: specifier, message: 'Prefer using a top-level {{kind}}-only import instead of inline {{kind}} specifiers.', @@ -168,6 +184,7 @@ module.exports = { kind: specifier.importKind, }, fix(fixer) { + /** @type {import('eslint').Rule.Fix[]} */ const fixes = []; // if there are no value specifiers, then the other report fixer will be called, not this one @@ -213,7 +230,7 @@ module.exports = { ); }, }); - } + }); } }, }; diff --git a/src/rules/dynamic-import-chunkname.js b/src/rules/dynamic-import-chunkname.js index a72b04d12..12a765008 100644 --- a/src/rules/dynamic-import-chunkname.js +++ b/src/rules/dynamic-import-chunkname.js @@ -1,4 +1,6 @@ +import { getSourceCode } from 'eslint-module-utils/contextCompat'; import vm from 'vm'; + import docsUrl from '../docsUrl'; module.exports = { @@ -43,7 +45,7 @@ module.exports = { const eagerModeRegex = new RegExp(eagerModeFormat); function run(node, arg) { - const sourceCode = context.getSourceCode(); + const sourceCode = getSourceCode(context); const leadingComments = sourceCode.getCommentsBefore ? sourceCode.getCommentsBefore(arg) // This method is available in ESLint >= 4. : sourceCode.getComments(arg).leading; // This method is deprecated in ESLint 7. diff --git a/src/rules/export.js b/src/rules/export.js index 197a0eb51..fbbc39d75 100644 --- a/src/rules/export.js +++ b/src/rules/export.js @@ -2,7 +2,6 @@ import ExportMapBuilder from '../exportMap/builder'; import recursivePatternCapture from '../exportMap/patternCapture'; import docsUrl from '../docsUrl'; import includes from 'array-includes'; -import flatMap from 'array.prototype.flatmap'; /* Notes on TypeScript namespaces aka TSModuleDeclaration: @@ -27,42 +26,25 @@ const rootProgram = 'root'; const tsTypePrefix = 'type:'; /** - * Detect function overloads like: + * remove function overloads like: * ```ts * export function foo(a: number); * export function foo(a: string); - * export function foo(a: number|string) { return a; } * ``` * @param {Set} nodes - * @returns {boolean} */ -function isTypescriptFunctionOverloads(nodes) { - const nodesArr = Array.from(nodes); - - const idents = flatMap( - nodesArr, - (node) => node.declaration && ( - node.declaration.type === 'TSDeclareFunction' // eslint 6+ - || node.declaration.type === 'TSEmptyBodyFunctionDeclaration' // eslint 4-5 - ) - ? node.declaration.id.name - : [], - ); - if (new Set(idents).size !== idents.length) { - return true; - } - - const types = new Set(nodesArr.map((node) => node.parent.type)); - if (!types.has('TSDeclareFunction')) { - return false; - } - if (types.size === 1) { - return true; - } - if (types.size === 2 && types.has('FunctionDeclaration')) { - return true; - } - return false; +function removeTypescriptFunctionOverloads(nodes) { + nodes.forEach((node) => { + const declType = node.type === 'ExportDefaultDeclaration' ? node.declaration.type : node.parent.type; + if ( + // eslint 6+ + declType === 'TSDeclareFunction' + // eslint 4-5 + || declType === 'TSEmptyBodyFunctionDeclaration' + ) { + nodes.delete(node); + } + }); } /** @@ -227,9 +209,11 @@ module.exports = { 'Program:exit'() { for (const [, named] of namespace) { for (const [name, nodes] of named) { + removeTypescriptFunctionOverloads(nodes); + if (nodes.size <= 1) { continue; } - if (isTypescriptFunctionOverloads(nodes) || isTypescriptNamespaceMerging(nodes)) { continue; } + if (isTypescriptNamespaceMerging(nodes)) { continue; } for (const node of nodes) { if (shouldSkipTypescriptNamespace(node, nodes)) { continue; } diff --git a/src/rules/extensions.js b/src/rules/extensions.js index b1e5c6d9f..c2c03a2b1 100644 --- a/src/rules/extensions.js +++ b/src/rules/extensions.js @@ -14,6 +14,7 @@ const properties = { type: 'object', properties: { pattern: patternProperties, + checkTypeImports: { type: 'boolean' }, ignorePackages: { type: 'boolean' }, }, }; @@ -35,7 +36,7 @@ function buildProperties(context) { } // If this is not the new structure, transfer all props to result.pattern - if (obj.pattern === undefined && obj.ignorePackages === undefined) { + if (obj.pattern === undefined && obj.ignorePackages === undefined && obj.checkTypeImports === undefined) { Object.assign(result.pattern, obj); return; } @@ -49,6 +50,10 @@ function buildProperties(context) { if (obj.ignorePackages !== undefined) { result.ignorePackages = obj.ignorePackages; } + + if (obj.checkTypeImports !== undefined) { + result.checkTypeImports = obj.checkTypeImports; + } }); if (result.defaultConfig === 'ignorePackages') { @@ -168,7 +173,7 @@ module.exports = { if (!extension || !importPath.endsWith(`.${extension}`)) { // ignore type-only imports and exports - if (node.importKind === 'type' || node.exportKind === 'type') { return; } + if (!props.checkTypeImports && (node.importKind === 'type' || node.exportKind === 'type')) { return; } const extensionRequired = isUseOfExtensionRequired(extension, isPackage); const extensionForbidden = isUseOfExtensionForbidden(extension); if (extensionRequired && !extensionForbidden) { diff --git a/src/rules/first.js b/src/rules/first.js index f8cc273a3..e7df26ac9 100644 --- a/src/rules/first.js +++ b/src/rules/first.js @@ -1,3 +1,5 @@ +import { getDeclaredVariables, getSourceCode } from 'eslint-module-utils/contextCompat'; + import docsUrl from '../docsUrl'; function getImportValue(node) { @@ -38,7 +40,7 @@ module.exports = { } const absoluteFirst = context.options[0] === 'absolute-first'; const message = 'Import in body of module; reorder to top.'; - const sourceCode = context.getSourceCode(); + const sourceCode = getSourceCode(context); const originSourceCode = sourceCode.getText(); let nonImportCount = 0; let anyExpressions = false; @@ -66,7 +68,7 @@ module.exports = { } } if (nonImportCount > 0) { - for (const variable of context.getDeclaredVariables(node)) { + for (const variable of getDeclaredVariables(context, node)) { if (!shouldSort) { break; } const references = variable.references; if (references.length) { diff --git a/src/rules/named.js b/src/rules/named.js index ed7e5e018..ab5f3103f 100644 --- a/src/rules/named.js +++ b/src/rules/named.js @@ -1,4 +1,6 @@ import * as path from 'path'; +import { getFilename, getPhysicalFilename } from 'eslint-module-utils/contextCompat'; + import ExportMapBuilder from '../exportMap/builder'; import docsUrl from '../docsUrl'; @@ -67,7 +69,7 @@ module.exports = { if (!deepLookup.found) { if (deepLookup.path.length > 1) { const deepPath = deepLookup.path - .map((i) => path.relative(path.dirname(context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename()), i.path)) + .map((i) => path.relative(path.dirname(getPhysicalFilename(context)), i.path)) .join(' -> '); context.report(im[key], `${name} not found via ${deepPath}`); @@ -121,7 +123,7 @@ module.exports = { if (!deepLookup.found) { if (deepLookup.path.length > 1) { const deepPath = deepLookup.path - .map((i) => path.relative(path.dirname(context.getFilename()), i.path)) + .map((i) => path.relative(path.dirname(getFilename(context)), i.path)) .join(' -> '); context.report(im.key, `${im.key.name} not found via ${deepPath}`); diff --git a/src/rules/namespace.js b/src/rules/namespace.js index 60a4220de..b2de7f225 100644 --- a/src/rules/namespace.js +++ b/src/rules/namespace.js @@ -87,7 +87,7 @@ module.exports = { // same as above, but does not add names to local map ExportNamespaceSpecifier(namespace) { - const declaration = importDeclaration(context); + const declaration = importDeclaration(context, namespace); const imports = ExportMapBuilder.get(declaration.source.value, context); if (imports == null) { return null; } @@ -110,7 +110,7 @@ module.exports = { MemberExpression(dereference) { if (dereference.object.type !== 'Identifier') { return; } if (!namespaces.has(dereference.object.name)) { return; } - if (declaredScope(context, dereference.object.name) !== 'module') { return; } + if (declaredScope(context, dereference.object.name, dereference) !== 'module') { return; } if (dereference.parent.type === 'AssignmentExpression' && dereference.parent.left === dereference) { context.report( @@ -158,7 +158,7 @@ module.exports = { if (!namespaces.has(init.name)) { return; } // check for redefinition in intermediate scopes - if (declaredScope(context, init.name) !== 'module') { return; } + if (declaredScope(context, init.name, init) !== 'module') { return; } // DFS traverse child namespaces function testKey(pattern, namespace, path = [init.name]) { diff --git a/src/rules/newline-after-import.js b/src/rules/newline-after-import.js index d10b87d78..c645d2bc6 100644 --- a/src/rules/newline-after-import.js +++ b/src/rules/newline-after-import.js @@ -3,6 +3,8 @@ * @author Radek Benkel */ +import { getPhysicalFilename, getScope } from 'eslint-module-utils/contextCompat'; + import isStaticRequire from '../core/staticRequire'; import docsUrl from '../docsUrl'; @@ -192,9 +194,9 @@ module.exports = { requireCalls.push(node); } }, - 'Program:exit'() { - log('exit processing for', context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename()); - const scopeBody = getScopeBody(context.getScope()); + 'Program:exit'(node) { + log('exit processing for', getPhysicalFilename(context)); + const scopeBody = getScopeBody(getScope(context, node)); log('got scope:', scopeBody); requireCalls.forEach((node, index) => { diff --git a/src/rules/no-absolute-path.js b/src/rules/no-absolute-path.js index 04f67383f..0dbd8cb86 100644 --- a/src/rules/no-absolute-path.js +++ b/src/rules/no-absolute-path.js @@ -1,5 +1,7 @@ import path from 'path'; +import { getPhysicalFilename } from 'eslint-module-utils/contextCompat'; import moduleVisitor, { makeOptionsSchema } from 'eslint-module-utils/moduleVisitor'; + import { isAbsolute } from '../core/importType'; import docsUrl from '../docsUrl'; @@ -22,9 +24,8 @@ module.exports = { node: source, message: 'Do not import modules using an absolute path', fix(fixer) { - const resolvedContext = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename(); // node.js and web imports work with posix style paths ("/") - let relativePath = path.posix.relative(path.dirname(resolvedContext), source.value); + let relativePath = path.posix.relative(path.dirname(getPhysicalFilename(context)), source.value); if (!relativePath.startsWith('.')) { relativePath = `./${relativePath}`; } diff --git a/src/rules/no-amd.js b/src/rules/no-amd.js index 5edfe3e69..05ed0a521 100644 --- a/src/rules/no-amd.js +++ b/src/rules/no-amd.js @@ -3,6 +3,8 @@ * @author Jamund Ferguson */ +import { getScope } from 'eslint-module-utils/contextCompat'; + import docsUrl from '../docsUrl'; //------------------------------------------------------------------------------ @@ -23,7 +25,7 @@ module.exports = { create(context) { return { CallExpression(node) { - if (context.getScope().type !== 'module') { return; } + if (getScope(context, node).type !== 'module') { return; } if (node.callee.type !== 'Identifier') { return; } if (node.callee.name !== 'require' && node.callee.name !== 'define') { return; } diff --git a/src/rules/no-commonjs.js b/src/rules/no-commonjs.js index dde509222..33b77da59 100644 --- a/src/rules/no-commonjs.js +++ b/src/rules/no-commonjs.js @@ -3,6 +3,8 @@ * @author Jamund Ferguson */ +import { getScope } from 'eslint-module-utils/contextCompat'; + import docsUrl from '../docsUrl'; const EXPORT_MESSAGE = 'Expected "export" or "export default"'; @@ -107,7 +109,7 @@ module.exports = { // exports. if (node.object.name === 'exports') { - const isInScope = context.getScope() + const isInScope = getScope(context, node) .variables .some((variable) => variable.name === 'exports'); if (!isInScope) { @@ -117,7 +119,7 @@ module.exports = { }, CallExpression(call) { - if (!validateScope(context.getScope())) { return; } + if (!validateScope(getScope(context, call))) { return; } if (call.callee.type !== 'Identifier') { return; } if (call.callee.name !== 'require') { return; } diff --git a/src/rules/no-cycle.js b/src/rules/no-cycle.js index be8c288dd..713503d9f 100644 --- a/src/rules/no-cycle.js +++ b/src/rules/no-cycle.js @@ -3,11 +3,13 @@ * @author Ben Mosher */ +import { getPhysicalFilename } from 'eslint-module-utils/contextCompat'; +import moduleVisitor, { makeOptionsSchema } from 'eslint-module-utils/moduleVisitor'; import resolve from 'eslint-module-utils/resolve'; + import ExportMapBuilder from '../exportMap/builder'; import StronglyConnectedComponentsBuilder from '../scc'; import { isExternalModule } from '../core/importType'; -import moduleVisitor, { makeOptionsSchema } from 'eslint-module-utils/moduleVisitor'; import docsUrl from '../docsUrl'; const traversed = new Set(); @@ -57,7 +59,7 @@ module.exports = { }, create(context) { - const myPath = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename(); + const myPath = getPhysicalFilename(context); if (myPath === '') { return {}; } // can't cycle-check a non-file const options = context.options[0] || {}; @@ -150,9 +152,9 @@ module.exports = { */ if (path === myPath && toTraverse.length > 0) { return true; } if (route.length + 1 < maxDepth) { - for (const { source } of toTraverse) { + toTraverse.forEach(({ source }) => { untraversed.push({ mget: getter, route: route.concat(source) }); - } + }); } } } diff --git a/src/rules/no-default-export.js b/src/rules/no-default-export.js index dabbae543..d18f0c48f 100644 --- a/src/rules/no-default-export.js +++ b/src/rules/no-default-export.js @@ -1,4 +1,7 @@ +import { getSourceCode } from 'eslint-module-utils/contextCompat'; + import docsUrl from '../docsUrl'; +import sourceType from '../core/sourceType'; module.exports = { meta: { @@ -13,7 +16,7 @@ module.exports = { create(context) { // ignore non-modules - if (context.parserOptions.sourceType !== 'module') { + if (sourceType(context) !== 'module') { return {}; } @@ -22,7 +25,7 @@ module.exports = { return { ExportDefaultDeclaration(node) { - const { loc } = context.getSourceCode().getFirstTokens(node)[1] || {}; + const { loc } = getSourceCode(context).getFirstTokens(node)[1] || {}; context.report({ node, message: preferNamed, loc }); }, @@ -30,7 +33,7 @@ module.exports = { node.specifiers .filter((specifier) => (specifier.exported.name || specifier.exported.value) === 'default') .forEach((specifier) => { - const { loc } = context.getSourceCode().getFirstTokens(node)[1] || {}; + const { loc } = getSourceCode(context).getFirstTokens(node)[1] || {}; if (specifier.type === 'ExportDefaultSpecifier') { context.report({ node, message: preferNamed, loc }); } else if (specifier.type === 'ExportSpecifier') { diff --git a/src/rules/no-deprecated.js b/src/rules/no-deprecated.js index b4299a51d..9559046b9 100644 --- a/src/rules/no-deprecated.js +++ b/src/rules/no-deprecated.js @@ -98,7 +98,7 @@ module.exports = { if (!deprecated.has(node.name)) { return; } - if (declaredScope(context, node.name) !== 'module') { return; } + if (declaredScope(context, node.name, node) !== 'module') { return; } context.report({ node, message: message(deprecated.get(node.name)), @@ -109,7 +109,7 @@ module.exports = { if (dereference.object.type !== 'Identifier') { return; } if (!namespaces.has(dereference.object.name)) { return; } - if (declaredScope(context, dereference.object.name) !== 'module') { return; } + if (declaredScope(context, dereference.object.name, dereference) !== 'module') { return; } // go deep let namespace = namespaces.get(dereference.object.name); diff --git a/src/rules/no-duplicates.js b/src/rules/no-duplicates.js index d9fb1a130..b8c8d848c 100644 --- a/src/rules/no-duplicates.js +++ b/src/rules/no-duplicates.js @@ -1,3 +1,4 @@ +import { getSourceCode } from 'eslint-module-utils/contextCompat'; import resolve from 'eslint-module-utils/resolve'; import semver from 'semver'; import flatMap from 'array.prototype.flatmap'; @@ -73,6 +74,7 @@ function hasProblematicComments(node, sourceCode) { ); } +/** @type {(first: import('estree').ImportDeclaration, rest: import('estree').ImportDeclaration[], sourceCode: import('eslint').SourceCode.SourceCode, context: import('eslint').Rule.RuleContext) => import('eslint').Rule.ReportFixer | undefined} */ function getFix(first, rest, sourceCode, context) { // Sorry ESLint <= 3 users, no autofix for you. Autofixing duplicate imports // requires multiple `fixer.whatever()` calls in the `fix`: We both need to @@ -122,7 +124,7 @@ function getFix(first, rest, sourceCode, context) { isEmpty: !hasSpecifiers(node), }; }) - .filter(Boolean); + .filter((x) => !!x); const unnecessaryImports = restWithoutComments.filter((node) => !hasSpecifiers(node) && !hasNamespace(node) @@ -138,6 +140,7 @@ function getFix(first, rest, sourceCode, context) { return undefined; } + /** @type {import('eslint').Rule.ReportFixer} */ return (fixer) => { const tokens = sourceCode.getTokens(first); const openBrace = tokens.find((token) => isPunctuator(token, '{')); @@ -184,6 +187,7 @@ function getFix(first, rest, sourceCode, context) { ['', !firstHasTrailingComma && !firstIsEmpty, firstExistingIdentifiers], ); + /** @type {import('eslint').Rule.Fix[]} */ const fixes = []; if (shouldAddSpecifiers && preferInline && first.importKind === 'type') { @@ -227,7 +231,7 @@ function getFix(first, rest, sourceCode, context) { } // Remove imports whose specifiers have been moved into the first import. - for (const specifier of specifiers) { + specifiers.forEach((specifier) => { const importNode = specifier.importNode; fixes.push(fixer.remove(importNode)); @@ -236,12 +240,12 @@ function getFix(first, rest, sourceCode, context) { if (charAfterImport === '\n') { fixes.push(fixer.removeRange(charAfterImportRange)); } - } + }); // Remove imports whose default import has been moved to the first import, // and side-effect-only imports that are unnecessary due to the first // import. - for (const node of unnecessaryImports) { + unnecessaryImports.forEach((node) => { fixes.push(fixer.remove(node)); const charAfterImportRange = [node.range[1], node.range[1] + 1]; @@ -249,18 +253,19 @@ function getFix(first, rest, sourceCode, context) { if (charAfterImport === '\n') { fixes.push(fixer.removeRange(charAfterImportRange)); } - } + }); return fixes; }; } +/** @type {(imported: Map, context: import('eslint').Rule.RuleContext) => void} */ function checkImports(imported, context) { for (const [module, nodes] of imported.entries()) { if (nodes.length > 1) { const message = `'${module}' imported multiple times.`; const [first, ...rest] = nodes; - const sourceCode = context.getSourceCode(); + const sourceCode = getSourceCode(context); const fix = getFix(first, rest, sourceCode, context); context.report({ @@ -269,16 +274,17 @@ function checkImports(imported, context) { fix, // Attach the autofix (if any) to the first import. }); - for (const node of rest) { + rest.forEach((node) => { context.report({ node: node.source, message, }); - } + }); } } } +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { type: 'problem', @@ -304,10 +310,13 @@ module.exports = { ], }, + /** @param {import('eslint').Rule.RuleContext} context */ create(context) { + /** @type {boolean} */ // Prepare the resolver from options. - const considerQueryStringOption = context.options[0] - && context.options[0].considerQueryString; + const considerQueryStringOption = context.options[0] && context.options[0].considerQueryString; + /** @type {boolean} */ + const preferInline = context.options[0] && context.options[0]['prefer-inline']; const defaultResolver = (sourcePath) => resolve(sourcePath, context) || sourcePath; const resolver = considerQueryStringOption ? (sourcePath) => { const parts = sourcePath.match(/^([^?]*)\?(.*)$/); @@ -317,11 +326,14 @@ module.exports = { return `${defaultResolver(parts[1])}?${parts[2]}`; } : defaultResolver; + /** @type {Map, nsImported: Map, defaultTypesImported: Map, namedTypesImported: Map}>} */ const moduleMaps = new Map(); + /** @param {import('estree').ImportDeclaration} n */ + /** @returns {typeof moduleMaps[keyof typeof moduleMaps]} */ function getImportMap(n) { if (!moduleMaps.has(n.parent)) { - moduleMaps.set(n.parent, { + moduleMaps.set(n.parent, /** @type {typeof moduleMaps} */ { imported: new Map(), nsImported: new Map(), defaultTypesImported: new Map(), @@ -329,7 +341,6 @@ module.exports = { }); } const map = moduleMaps.get(n.parent); - const preferInline = context.options[0] && context.options[0]['prefer-inline']; if (!preferInline && n.importKind === 'type') { return n.specifiers.length > 0 && n.specifiers[0].type === 'ImportDefaultSpecifier' ? map.defaultTypesImported : map.namedTypesImported; } @@ -341,7 +352,9 @@ module.exports = { } return { + /** @param {import('estree').ImportDeclaration} n */ ImportDeclaration(n) { + /** @type {string} */ // resolved path will cover aliased duplicates const resolvedPath = resolver(n.source.value); const importMap = getImportMap(n); diff --git a/src/rules/no-empty-named-blocks.js b/src/rules/no-empty-named-blocks.js index 3ec1501b8..d68ecee38 100644 --- a/src/rules/no-empty-named-blocks.js +++ b/src/rules/no-empty-named-blocks.js @@ -1,3 +1,5 @@ +import { getSourceCode } from 'eslint-module-utils/contextCompat'; + import docsUrl from '../docsUrl'; function getEmptyBlockRange(tokens, index) { @@ -72,7 +74,7 @@ module.exports = { fix(fixer) { // Remove the empty block and the 'from' token, leaving the import only for its side // effects, e.g. `import 'mod'` - const sourceCode = context.getSourceCode(); + const sourceCode = getSourceCode(context); const fromToken = program.tokens.find((t) => t.value === 'from'); const importToken = program.tokens.find((t) => t.value === 'import'); const hasSpaceAfterFrom = sourceCode.isSpaceBetween(fromToken, sourceCode.getTokenAfter(fromToken)); diff --git a/src/rules/no-extraneous-dependencies.js b/src/rules/no-extraneous-dependencies.js index 0fe42f56f..bf0a1ed47 100644 --- a/src/rules/no-extraneous-dependencies.js +++ b/src/rules/no-extraneous-dependencies.js @@ -1,9 +1,11 @@ import path from 'path'; import fs from 'fs'; -import pkgUp from 'eslint-module-utils/pkgUp'; import minimatch from 'minimatch'; +import { getPhysicalFilename } from 'eslint-module-utils/contextCompat'; +import pkgUp from 'eslint-module-utils/pkgUp'; import resolve from 'eslint-module-utils/resolve'; import moduleVisitor from 'eslint-module-utils/moduleVisitor'; + import importType from '../core/importType'; import { getFilePackageName } from '../core/packagePath'; import docsUrl from '../docsUrl'; @@ -84,7 +86,7 @@ function getDependencies(context, packageDir) { }); } else { const packageJsonPath = pkgUp({ - cwd: context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename(), + cwd: getPhysicalFilename(context), normalize: false, }); @@ -283,7 +285,7 @@ module.exports = { create(context) { const options = context.options[0] || {}; - const filename = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename(); + const filename = getPhysicalFilename(context); const deps = getDependencies(context, options.packageDir) || extractDepFields({}); const depsOptions = { diff --git a/src/rules/no-import-module-exports.js b/src/rules/no-import-module-exports.js index bc4605c39..bf6fba61b 100644 --- a/src/rules/no-import-module-exports.js +++ b/src/rules/no-import-module-exports.js @@ -1,9 +1,10 @@ import minimatch from 'minimatch'; import path from 'path'; +import { getPhysicalFilename, getSourceCode } from 'eslint-module-utils/contextCompat'; import pkgUp from 'eslint-module-utils/pkgUp'; function getEntryPoint(context) { - const pkgPath = pkgUp({ cwd: context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename() }); + const pkgPath = pkgUp({ cwd: getPhysicalFilename(context) }); try { return require.resolve(path.dirname(pkgPath)); } catch (error) { @@ -14,7 +15,7 @@ function getEntryPoint(context) { } function findScope(context, identifier) { - const { scopeManager } = context.getSourceCode(); + const { scopeManager } = getSourceCode(context); return scopeManager && scopeManager.scopes.slice().reverse().find((scope) => scope.variables.some((variable) => variable.identifiers.some((node) => node.name === identifier))); } @@ -50,7 +51,7 @@ module.exports = { let alreadyReported = false; function report(node) { - const fileName = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename(); + const fileName = getPhysicalFilename(context); const isEntryPoint = entryPoint === fileName; const isIdentifier = node.object.type === 'Identifier'; const hasKeywords = (/^(module|exports)$/).test(node.object.name); diff --git a/src/rules/no-mutable-exports.js b/src/rules/no-mutable-exports.js index 433d64e16..0a0e128dc 100644 --- a/src/rules/no-mutable-exports.js +++ b/src/rules/no-mutable-exports.js @@ -1,5 +1,8 @@ +import { getScope } from 'eslint-module-utils/contextCompat'; + import docsUrl from '../docsUrl'; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { type: 'suggestion', @@ -19,41 +22,41 @@ module.exports = { } } + /** @type {(scope: import('eslint').Scope.Scope, name: string) => void} */ function checkDeclarationsInScope({ variables }, name) { - for (const variable of variables) { - if (variable.name === name) { - for (const def of variable.defs) { - if (def.type === 'Variable' && def.parent) { + variables + .filter((variable) => variable.name === name) + .forEach((variable) => { + variable.defs + .filter((def) => def.type === 'Variable' && def.parent) + .forEach((def) => { checkDeclaration(def.parent); - } - } - } - } - } - - function handleExportDefault(node) { - const scope = context.getScope(); - - if (node.declaration.name) { - checkDeclarationsInScope(scope, node.declaration.name); - } + }); + }); } - function handleExportNamed(node) { - const scope = context.getScope(); + return { + /** @param {import('estree').ExportDefaultDeclaration} node */ + ExportDefaultDeclaration(node) { + const scope = getScope(context, node); - if (node.declaration) { - checkDeclaration(node.declaration); - } else if (!node.source) { - for (const specifier of node.specifiers) { - checkDeclarationsInScope(scope, specifier.local.name); + if ('name' in node.declaration && node.declaration.name) { + checkDeclarationsInScope(scope, node.declaration.name); } - } - } - - return { - ExportDefaultDeclaration: handleExportDefault, - ExportNamedDeclaration: handleExportNamed, + }, + + /** @param {import('estree').ExportNamedDeclaration} node */ + ExportNamedDeclaration(node) { + const scope = getScope(context, node); + + if ('declaration' in node && node.declaration) { + checkDeclaration(node.declaration); + } else if (!('source' in node) || !node.source) { + node.specifiers.forEach((specifier) => { + checkDeclarationsInScope(scope, specifier.local.name); + }); + } + }, }; }, }; diff --git a/src/rules/no-named-as-default-member.js b/src/rules/no-named-as-default-member.js index 54bec64a2..c6abc46a8 100644 --- a/src/rules/no-named-as-default-member.js +++ b/src/rules/no-named-as-default-member.js @@ -35,7 +35,7 @@ module.exports = { return { ImportDefaultSpecifier(node) { - const declaration = importDeclaration(context); + const declaration = importDeclaration(context, node); const exportMap = ExportMapBuilder.get(declaration.source.value, context); if (exportMap == null) { return; } diff --git a/src/rules/no-named-as-default.js b/src/rules/no-named-as-default.js index 5b24f8e88..dacd294f4 100644 --- a/src/rules/no-named-as-default.js +++ b/src/rules/no-named-as-default.js @@ -15,28 +15,71 @@ module.exports = { create(context) { function checkDefault(nameKey, defaultSpecifier) { + /** + * For ImportDefaultSpecifier we're interested in the "local" name (`foo` for `import {bar as foo} ...`) + * For ExportDefaultSpecifier we're interested in the "exported" name (`foo` for `export {bar as foo} ...`) + */ + const analyzedName = defaultSpecifier[nameKey].name; + // #566: default is a valid specifier - if (defaultSpecifier[nameKey].name === 'default') { return; } + if (analyzedName === 'default') { return; } - const declaration = importDeclaration(context); + const declaration = importDeclaration(context, defaultSpecifier); + /** @type {import('../exportMap').default | null} */ + const importedModule = ExportMapBuilder.get(declaration.source.value, context); + if (importedModule == null) { return; } - const imports = ExportMapBuilder.get(declaration.source.value, context); - if (imports == null) { return; } + if (importedModule.errors.length > 0) { + importedModule.reportErrors(context, declaration); + return; + } - if (imports.errors.length) { - imports.reportErrors(context, declaration); + if (!importedModule.hasDefault) { + // The rule is triggered for default imports/exports, so if the imported module has no default + // this means we're dealing with incorrect source code anyway return; } - if (imports.has('default') && imports.has(defaultSpecifier[nameKey].name)) { + if (!importedModule.has(analyzedName)) { + // The name used locally for the default import was not even used in the imported module. + return; + } - context.report( - defaultSpecifier, - `Using exported name '${defaultSpecifier[nameKey].name}' as identifier for default export.`, - ); + /** + * FIXME: We can verify if a default and a named export are pointing to the same symbol only + * if they are both `reexports`. In case one of the symbols is not a re-export, but defined + * in the file, the ExportMap structure has no info about what actually is being exported -- + * the value in the `namespace` Map is an empty object. + * + * To solve this, it would require not relying on the ExportMap, but on some other way of + * accessing the imported module and its exported values. + * + * Additionally, although `ExportMap.get` is a unified way to get info from both `reexports` + * and `namespace` maps, it does not return valid output we need here, and I think this is + * related to the "cycle safeguards" in the `get` function. + */ + if (importedModule.reexports.has(analyzedName) && importedModule.reexports.has('default')) { + const thingImportedWithNamedImport = importedModule.reexports.get(analyzedName).getImport(); + const thingImportedWithDefaultImport = importedModule.reexports.get('default').getImport(); + + // Case: both imports point to the same file and they both refer to the same symbol in this file. + if ( + thingImportedWithNamedImport.path === thingImportedWithDefaultImport.path + && thingImportedWithNamedImport.local === thingImportedWithDefaultImport.local + ) { + // #1594: the imported module exports the same thing via a default export and a named export + return; + } } + + context.report( + defaultSpecifier, + `Using exported name '${defaultSpecifier[nameKey].name}' as identifier for default ${nameKey === 'local' ? `import` : `export`}.`, + ); + } + return { ImportDefaultSpecifier: checkDefault.bind(null, 'local'), ExportDefaultSpecifier: checkDefault.bind(null, 'exported'), diff --git a/src/rules/no-named-export.js b/src/rules/no-named-export.js index efaf9dc4c..fc9b2c48d 100644 --- a/src/rules/no-named-export.js +++ b/src/rules/no-named-export.js @@ -1,3 +1,4 @@ +import sourceType from '../core/sourceType'; import docsUrl from '../docsUrl'; module.exports = { @@ -13,7 +14,7 @@ module.exports = { create(context) { // ignore non-modules - if (context.parserOptions.sourceType !== 'module') { + if (sourceType(context) !== 'module') { return {}; } diff --git a/src/rules/no-namespace.js b/src/rules/no-namespace.js index 3c6617a41..7ab60bd21 100644 --- a/src/rules/no-namespace.js +++ b/src/rules/no-namespace.js @@ -4,6 +4,8 @@ */ import minimatch from 'minimatch'; +import { getScope, getSourceCode } from 'eslint-module-utils/contextCompat'; + import docsUrl from '../docsUrl'; /** @@ -108,7 +110,7 @@ module.exports = { return; } - const scopeVariables = context.getScope().variables; + const scopeVariables = getScope(context, node).variables; const namespaceVariable = scopeVariables.find((variable) => variable.defs[0].node === node); const namespaceReferences = namespaceVariable.references; const namespaceIdentifiers = namespaceReferences.map((reference) => reference.identifier); @@ -118,7 +120,7 @@ module.exports = { node, message: `Unexpected namespace import.`, fix: canFix && ((fixer) => { - const scopeManager = context.getSourceCode().scopeManager; + const { scopeManager } = getSourceCode(context); const fixes = []; // Pass 1: Collect variable names that are already in scope for each reference we want diff --git a/src/rules/no-relative-packages.js b/src/rules/no-relative-packages.js index 1d215519f..ebc280ff9 100644 --- a/src/rules/no-relative-packages.js +++ b/src/rules/no-relative-packages.js @@ -1,6 +1,7 @@ import path from 'path'; import readPkgUp from 'eslint-module-utils/readPkgUp'; +import { getPhysicalFilename } from 'eslint-module-utils/contextCompat'; import resolve from 'eslint-module-utils/resolve'; import moduleVisitor, { makeOptionsSchema } from 'eslint-module-utils/moduleVisitor'; import importType from '../core/importType'; @@ -26,7 +27,7 @@ function checkImportForRelativePackage(context, importPath, node) { } const resolvedImport = resolve(importPath, context); - const resolvedContext = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename(); + const resolvedContext = getPhysicalFilename(context); if (!resolvedImport || !resolvedContext) { return; diff --git a/src/rules/no-relative-parent-imports.js b/src/rules/no-relative-parent-imports.js index decd2ef7d..94972d3dd 100644 --- a/src/rules/no-relative-parent-imports.js +++ b/src/rules/no-relative-parent-imports.js @@ -1,9 +1,10 @@ -import moduleVisitor, { makeOptionsSchema } from 'eslint-module-utils/moduleVisitor'; -import docsUrl from '../docsUrl'; import { basename, dirname, relative } from 'path'; +import { getPhysicalFilename } from 'eslint-module-utils/contextCompat'; +import moduleVisitor, { makeOptionsSchema } from 'eslint-module-utils/moduleVisitor'; import resolve from 'eslint-module-utils/resolve'; import importType from '../core/importType'; +import docsUrl from '../docsUrl'; module.exports = { meta: { @@ -17,7 +18,7 @@ module.exports = { }, create: function noRelativePackages(context) { - const myPath = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename(); + const myPath = getPhysicalFilename(context); if (myPath === '') { return {}; } // can't check a non-file function checkSourceValue(sourceNode) { diff --git a/src/rules/no-restricted-paths.js b/src/rules/no-restricted-paths.js index 75952dd05..2e1bc608c 100644 --- a/src/rules/no-restricted-paths.js +++ b/src/rules/no-restricted-paths.js @@ -1,11 +1,12 @@ import path from 'path'; - +import { getPhysicalFilename } from 'eslint-module-utils/contextCompat'; import resolve from 'eslint-module-utils/resolve'; import moduleVisitor from 'eslint-module-utils/moduleVisitor'; import isGlob from 'is-glob'; import { Minimatch } from 'minimatch'; -import docsUrl from '../docsUrl'; + import importType from '../core/importType'; +import docsUrl from '../docsUrl'; const containsPath = (filepath, target) => { const relative = path.relative(target, filepath); @@ -85,7 +86,7 @@ module.exports = { const options = context.options[0] || {}; const restrictedPaths = options.zones || []; const basePath = options.basePath || process.cwd(); - const currentFilename = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename(); + const currentFilename = getPhysicalFilename(context); const matchingZones = restrictedPaths.filter( (zone) => [].concat(zone.target) .map((target) => path.resolve(basePath, target)) diff --git a/src/rules/no-self-import.js b/src/rules/no-self-import.js index 0ba0f6669..99c534270 100644 --- a/src/rules/no-self-import.js +++ b/src/rules/no-self-import.js @@ -3,12 +3,14 @@ * @author Gio d'Amelio */ +import { getPhysicalFilename } from 'eslint-module-utils/contextCompat'; import resolve from 'eslint-module-utils/resolve'; import moduleVisitor from 'eslint-module-utils/moduleVisitor'; + import docsUrl from '../docsUrl'; function isImportingSelf(context, node, requireName) { - const filePath = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename(); + const filePath = getPhysicalFilename(context); // If the input is from stdin, this test can't fail if (filePath !== '' && filePath === resolve(requireName, context)) { diff --git a/src/rules/no-unassigned-import.js b/src/rules/no-unassigned-import.js index 0af9f2e9f..fec232afe 100644 --- a/src/rules/no-unassigned-import.js +++ b/src/rules/no-unassigned-import.js @@ -1,5 +1,6 @@ import path from 'path'; import minimatch from 'minimatch'; +import { getPhysicalFilename } from 'eslint-module-utils/contextCompat'; import isStaticRequire from '../core/staticRequire'; import docsUrl from '../docsUrl'; @@ -31,7 +32,7 @@ function testIsAllow(globs, filename, source) { function create(context) { const options = context.options[0] || {}; - const filename = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename(); + const filename = getPhysicalFilename(context); const isAllow = (source) => testIsAllow(options.allow, filename, source); return { diff --git a/src/rules/no-unused-modules.js b/src/rules/no-unused-modules.js index ccb5932cb..d6822cccd 100644 --- a/src/rules/no-unused-modules.js +++ b/src/rules/no-unused-modules.js @@ -4,6 +4,7 @@ * @author René Fermann */ +import { getPhysicalFilename } from 'eslint-module-utils/contextCompat'; import { getFileExtensions } from 'eslint-module-utils/ignore'; import resolve from 'eslint-module-utils/resolve'; import visit from 'eslint-module-utils/visit'; @@ -609,7 +610,7 @@ module.exports = { doPreparation(src, ignoreExports, context); } - const file = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename(); + const file = getPhysicalFilename(context); const checkExportPresence = (node) => { if (!missingExports) { diff --git a/src/rules/no-useless-path-segments.js b/src/rules/no-useless-path-segments.js index 390a7546d..2d8dd3526 100644 --- a/src/rules/no-useless-path-segments.js +++ b/src/rules/no-useless-path-segments.js @@ -3,6 +3,7 @@ * @author Thomas Grainger */ +import { getPhysicalFilename } from 'eslint-module-utils/contextCompat'; import { getFileExtensions } from 'eslint-module-utils/ignore'; import moduleVisitor from 'eslint-module-utils/moduleVisitor'; import resolve from 'eslint-module-utils/resolve'; @@ -60,7 +61,7 @@ module.exports = { }, create(context) { - const currentDir = path.dirname(context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename()); + const currentDir = path.dirname(getPhysicalFilename(context)); const options = context.options[0]; function checkSourceValue(source) { diff --git a/src/rules/order.js b/src/rules/order.js index 1b25273c6..d6f25ddd3 100644 --- a/src/rules/order.js +++ b/src/rules/order.js @@ -3,19 +3,25 @@ import minimatch from 'minimatch'; import includes from 'array-includes'; import groupBy from 'object.groupby'; +import { getScope, getSourceCode } from 'eslint-module-utils/contextCompat'; +import trimEnd from 'string.prototype.trimend'; import importType from '../core/importType'; import isStaticRequire from '../core/staticRequire'; import docsUrl from '../docsUrl'; +const categories = { + named: 'named', + import: 'import', + exports: 'exports', +}; + const defaultGroups = ['builtin', 'external', 'parent', 'sibling', 'index']; // REPORTING AND FIXING function reverse(array) { - return array.map(function (v) { - return { ...v, rank: -v.rank }; - }).reverse(); + return array.map((v) => ({ ...v, rank: -v.rank })).reverse(); } function getTokensOrCommentsAfter(sourceCode, node, count) { @@ -130,6 +136,26 @@ function findStartOfLineWithComments(sourceCode, node) { return result; } +function findSpecifierStart(sourceCode, node) { + let token; + + do { + token = sourceCode.getTokenBefore(node); + } while (token.value !== ',' && token.value !== '{'); + + return token.range[1]; +} + +function findSpecifierEnd(sourceCode, node) { + let token; + + do { + token = sourceCode.getTokenAfter(node); + } while (token.value !== ',' && token.value !== '}'); + + return token.range[0]; +} + function isRequireExpression(expr) { return expr != null && expr.type === 'CallExpression' @@ -169,6 +195,49 @@ function isPlainImportEquals(node) { return node.type === 'TSImportEqualsDeclaration' && node.moduleReference.expression; } +function isCJSExports(context, node) { + if ( + node.type === 'MemberExpression' + && node.object.type === 'Identifier' + && node.property.type === 'Identifier' + && node.object.name === 'module' + && node.property.name === 'exports' + ) { + return getScope(context, node).variables.findIndex((variable) => variable.name === 'module') === -1; + } + if ( + node.type === 'Identifier' + && node.name === 'exports' + ) { + return getScope(context, node).variables.findIndex((variable) => variable.name === 'exports') === -1; + } +} + +function getNamedCJSExports(context, node) { + if (node.type !== 'MemberExpression') { + return; + } + const result = []; + let root = node; + let parent = null; + while (root.type === 'MemberExpression') { + if (root.property.type !== 'Identifier') { + return; + } + result.unshift(root.property.name); + parent = root; + root = root.object; + } + + if (isCJSExports(context, root)) { + return result; + } + + if (isCJSExports(context, parent)) { + return result.slice(1); + } +} + function canCrossNodeWhileReorder(node) { return isSupportedRequireModule(node) || isPlainImportModule(node) || isPlainImportEquals(node); } @@ -189,6 +258,12 @@ function canReorderItems(firstNode, secondNode) { } function makeImportDescription(node) { + if (node.type === 'export') { + if (node.node.exportKind === 'type') { + return 'type export'; + } + return 'export'; + } if (node.node.importKind === 'type') { return 'type import'; } @@ -198,58 +273,123 @@ function makeImportDescription(node) { return 'import'; } -function fixOutOfOrder(context, firstNode, secondNode, order) { - const sourceCode = context.getSourceCode(); - - const firstRoot = findRootNode(firstNode.node); - const firstRootStart = findStartOfLineWithComments(sourceCode, firstRoot); - const firstRootEnd = findEndOfLineWithComments(sourceCode, firstRoot); +function fixOutOfOrder(context, firstNode, secondNode, order, category) { + const isNamed = category === categories.named; + const isExports = category === categories.exports; + const sourceCode = getSourceCode(context); + + const { + firstRoot, + secondRoot, + } = isNamed ? { + firstRoot: firstNode.node, + secondRoot: secondNode.node, + } : { + firstRoot: findRootNode(firstNode.node), + secondRoot: findRootNode(secondNode.node), + }; - const secondRoot = findRootNode(secondNode.node); - const secondRootStart = findStartOfLineWithComments(sourceCode, secondRoot); - const secondRootEnd = findEndOfLineWithComments(sourceCode, secondRoot); - const canFix = canReorderItems(firstRoot, secondRoot); + const { + firstRootStart, + firstRootEnd, + secondRootStart, + secondRootEnd, + } = isNamed ? { + firstRootStart: findSpecifierStart(sourceCode, firstRoot), + firstRootEnd: findSpecifierEnd(sourceCode, firstRoot), + secondRootStart: findSpecifierStart(sourceCode, secondRoot), + secondRootEnd: findSpecifierEnd(sourceCode, secondRoot), + } : { + firstRootStart: findStartOfLineWithComments(sourceCode, firstRoot), + firstRootEnd: findEndOfLineWithComments(sourceCode, firstRoot), + secondRootStart: findStartOfLineWithComments(sourceCode, secondRoot), + secondRootEnd: findEndOfLineWithComments(sourceCode, secondRoot), + }; - let newCode = sourceCode.text.substring(secondRootStart, secondRootEnd); - if (newCode[newCode.length - 1] !== '\n') { - newCode = `${newCode}\n`; + if (firstNode.displayName === secondNode.displayName) { + if (firstNode.alias) { + firstNode.displayName = `${firstNode.displayName} as ${firstNode.alias}`; + } + if (secondNode.alias) { + secondNode.displayName = `${secondNode.displayName} as ${secondNode.alias}`; + } } const firstImport = `${makeImportDescription(firstNode)} of \`${firstNode.displayName}\``; const secondImport = `\`${secondNode.displayName}\` ${makeImportDescription(secondNode)}`; const message = `${secondImport} should occur ${order} ${firstImport}`; - if (order === 'before') { - context.report({ - node: secondNode.node, - message, - fix: canFix && ((fixer) => fixer.replaceTextRange( - [firstRootStart, secondRootEnd], - newCode + sourceCode.text.substring(firstRootStart, secondRootStart), - )), - }); - } else if (order === 'after') { - context.report({ - node: secondNode.node, - message, - fix: canFix && ((fixer) => fixer.replaceTextRange( - [secondRootStart, firstRootEnd], - sourceCode.text.substring(secondRootEnd, firstRootEnd) + newCode, - )), - }); + if (isNamed) { + const firstCode = sourceCode.text.slice(firstRootStart, firstRoot.range[1]); + const firstTrivia = sourceCode.text.slice(firstRoot.range[1], firstRootEnd); + const secondCode = sourceCode.text.slice(secondRootStart, secondRoot.range[1]); + const secondTrivia = sourceCode.text.slice(secondRoot.range[1], secondRootEnd); + + if (order === 'before') { + const trimmedTrivia = trimEnd(secondTrivia); + const gapCode = sourceCode.text.slice(firstRootEnd, secondRootStart - 1); + const whitespaces = secondTrivia.slice(trimmedTrivia.length); + context.report({ + node: secondNode.node, + message, + fix: (fixer) => fixer.replaceTextRange( + [firstRootStart, secondRootEnd], + `${secondCode},${trimmedTrivia}${firstCode}${firstTrivia}${gapCode}${whitespaces}`, + ), + }); + } else if (order === 'after') { + const trimmedTrivia = trimEnd(firstTrivia); + const gapCode = sourceCode.text.slice(secondRootEnd + 1, firstRootStart); + const whitespaces = firstTrivia.slice(trimmedTrivia.length); + context.report({ + node: secondNode.node, + message, + fix: (fixes) => fixes.replaceTextRange( + [secondRootStart, firstRootEnd], + `${gapCode}${firstCode},${trimmedTrivia}${secondCode}${whitespaces}`, + ), + }); + } + } else { + const canFix = isExports || canReorderItems(firstRoot, secondRoot); + let newCode = sourceCode.text.substring(secondRootStart, secondRootEnd); + + if (newCode[newCode.length - 1] !== '\n') { + newCode = `${newCode}\n`; + } + + if (order === 'before') { + context.report({ + node: secondNode.node, + message, + fix: canFix && ((fixer) => fixer.replaceTextRange( + [firstRootStart, secondRootEnd], + newCode + sourceCode.text.substring(firstRootStart, secondRootStart), + )), + }); + } else if (order === 'after') { + context.report({ + node: secondNode.node, + message, + fix: canFix && ((fixer) => fixer.replaceTextRange( + [secondRootStart, firstRootEnd], + sourceCode.text.substring(secondRootEnd, firstRootEnd) + newCode, + )), + }); + } } } -function reportOutOfOrder(context, imported, outOfOrder, order) { +function reportOutOfOrder(context, imported, outOfOrder, order, category) { outOfOrder.forEach(function (imp) { const found = imported.find(function hasHigherRank(importedItem) { return importedItem.rank > imp.rank; }); - fixOutOfOrder(context, found, imp, order); + fixOutOfOrder(context, found, imp, order, category); }); } -function makeOutOfOrderReport(context, imported) { +function makeOutOfOrderReport(context, imported, category) { const outOfOrder = findOutOfOrder(imported); if (!outOfOrder.length) { return; @@ -259,10 +399,10 @@ function makeOutOfOrderReport(context, imported) { const reversedImported = reverse(imported); const reversedOrder = findOutOfOrder(reversedImported); if (reversedOrder.length < outOfOrder.length) { - reportOutOfOrder(context, reversedImported, reversedOrder, 'after'); + reportOutOfOrder(context, reversedImported, reversedOrder, 'after', category); return; } - reportOutOfOrder(context, imported, outOfOrder, 'before'); + reportOutOfOrder(context, imported, outOfOrder, 'before', category); } const compareString = (a, b) => { @@ -499,7 +639,10 @@ function convertPathGroupsForRanks(pathGroups) { function fixNewLineAfterImport(context, previousImport) { const prevRoot = findRootNode(previousImport.node); const tokensToEndOfLine = takeTokensAfterWhile( - context.getSourceCode(), prevRoot, commentOnSameLineAs(prevRoot)); + getSourceCode(context), + prevRoot, + commentOnSameLineAs(prevRoot), + ); let endOfLine = prevRoot.range[1]; if (tokensToEndOfLine.length > 0) { @@ -509,7 +652,7 @@ function fixNewLineAfterImport(context, previousImport) { } function removeNewLineAfterImport(context, currentImport, previousImport) { - const sourceCode = context.getSourceCode(); + const sourceCode = getSourceCode(context); const prevRoot = findRootNode(previousImport.node); const currRoot = findRootNode(currentImport.node); const rangeToRemove = [ @@ -524,7 +667,7 @@ function removeNewLineAfterImport(context, currentImport, previousImport) { function makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, distinctGroup) { const getNumberOfEmptyLinesBetween = (currentImport, previousImport) => { - const linesBetweenImports = context.getSourceCode().lines.slice( + const linesBetweenImports = getSourceCode(context).lines.slice( previousImport.node.loc.end.line, currentImport.node.loc.start.line - 1, ); @@ -638,6 +781,30 @@ module.exports = { 'never', ], }, + named: { + default: false, + oneOf: [{ + type: 'boolean', + }, { + type: 'object', + properties: { + enabled: { type: 'boolean' }, + import: { type: 'boolean' }, + export: { type: 'boolean' }, + require: { type: 'boolean' }, + cjsExports: { type: 'boolean' }, + types: { + type: 'string', + enum: [ + 'mixed', + 'types-first', + 'types-last', + ], + }, + }, + additionalProperties: false, + }], + }, alphabetize: { type: 'object', properties: { @@ -666,10 +833,28 @@ module.exports = { ], }, - create: function importOrderRule(context) { + create(context) { const options = context.options[0] || {}; const newlinesBetweenImports = options['newlines-between'] || 'ignore'; const pathGroupsExcludedImportTypes = new Set(options.pathGroupsExcludedImportTypes || ['builtin', 'external', 'object']); + + const named = { + types: 'mixed', + ...typeof options.named === 'object' ? { + ...options.named, + import: 'import' in options.named ? options.named.import : options.named.enabled, + export: 'export' in options.named ? options.named.export : options.named.enabled, + require: 'require' in options.named ? options.named.require : options.named.enabled, + cjsExports: 'cjsExports' in options.named ? options.named.cjsExports : options.named.enabled, + } : { + import: options.named, + export: options.named, + require: options.named, + cjsExports: options.named, + }, + }; + + const namedGroups = named.types === 'mixed' ? [] : named.types === 'types-last' ? ['value'] : ['type']; const alphabetize = getAlphabetizeConfig(options); const distinctGroup = options.distinctGroup == null ? defaultDistinctGroup : !!options.distinctGroup; let ranks; @@ -692,6 +877,7 @@ module.exports = { }; } const importMap = new Map(); + const exportMap = new Map(); function getBlockImports(node) { if (!importMap.has(node)) { @@ -700,8 +886,38 @@ module.exports = { return importMap.get(node); } + function getBlockExports(node) { + if (!exportMap.has(node)) { + exportMap.set(node, []); + } + return exportMap.get(node); + } + + function makeNamedOrderReport(context, namedImports) { + if (namedImports.length > 1) { + const imports = namedImports.map( + (namedImport) => { + const kind = namedImport.kind || 'value'; + const rank = namedGroups.findIndex((entry) => [].concat(entry).indexOf(kind) > -1); + + return { + displayName: namedImport.value, + rank: rank === -1 ? namedGroups.length : rank, + ...namedImport, + value: `${namedImport.value}:${namedImport.alias || ''}`, + }; + }); + + if (alphabetize.order !== 'ignore') { + mutateRanksToAlphabetize(imports, alphabetize); + } + + makeOutOfOrderReport(context, imports, categories.named); + } + } + return { - ImportDeclaration: function handleImports(node) { + ImportDeclaration(node) { // Ignoring unassigned imports unless warnOnUnassignedImports is set if (node.specifiers.length || options.warnOnUnassignedImports) { const name = node.source.value; @@ -717,25 +933,45 @@ module.exports = { getBlockImports(node.parent), pathGroupsExcludedImportTypes, ); + + if (named.import) { + makeNamedOrderReport( + context, + node.specifiers.filter( + (specifier) => specifier.type === 'ImportSpecifier').map( + (specifier) => ({ + node: specifier, + value: specifier.imported.name, + type: 'import', + kind: specifier.importKind, + ...specifier.local.range[0] !== specifier.imported.range[0] && { + alias: specifier.local.name, + }, + }), + ), + ); + } } }, - TSImportEqualsDeclaration: function handleImports(node) { - let displayName; - let value; - let type; + TSImportEqualsDeclaration(node) { // skip "export import"s if (node.isExport) { return; } + + let displayName; + let value; + let type; if (node.moduleReference.type === 'TSExternalModuleReference') { value = node.moduleReference.expression.value; displayName = value; type = 'import'; } else { value = ''; - displayName = context.getSourceCode().getText(node.moduleReference); + displayName = getSourceCode(context).getText(node.moduleReference); type = 'import:object'; } + registerNode( context, { @@ -749,7 +985,7 @@ module.exports = { pathGroupsExcludedImportTypes, ); }, - CallExpression: function handleRequires(node) { + CallExpression(node) { if (!isStaticRequire(node)) { return; } @@ -771,7 +1007,90 @@ module.exports = { pathGroupsExcludedImportTypes, ); }, - 'Program:exit': function reportAndReset() { + ...named.require && { + VariableDeclarator(node) { + if (node.id.type === 'ObjectPattern' && isRequireExpression(node.init)) { + for (let i = 0; i < node.id.properties.length; i++) { + if ( + node.id.properties[i].key.type !== 'Identifier' + || node.id.properties[i].value.type !== 'Identifier' + ) { + return; + } + } + makeNamedOrderReport( + context, + node.id.properties.map((prop) => ({ + node: prop, + value: prop.key.name, + type: 'require', + ...prop.key.range[0] !== prop.value.range[0] && { + alias: prop.value.name, + }, + })), + ); + } + }, + }, + ...named.export && { + ExportNamedDeclaration(node) { + makeNamedOrderReport( + context, + node.specifiers.map((specifier) => ({ + node: specifier, + value: specifier.local.name, + type: 'export', + kind: specifier.exportKind, + ...specifier.local.range[0] !== specifier.exported.range[0] && { + alias: specifier.exported.name, + }, + })), + ); + }, + }, + ...named.cjsExports && { + AssignmentExpression(node) { + if (node.parent.type === 'ExpressionStatement') { + if (isCJSExports(context, node.left)) { + if (node.right.type === 'ObjectExpression') { + for (let i = 0; i < node.right.properties.length; i++) { + if ( + node.right.properties[i].key.type !== 'Identifier' + || node.right.properties[i].value.type !== 'Identifier' + ) { + return; + } + } + + makeNamedOrderReport( + context, + node.right.properties.map((prop) => ({ + node: prop, + value: prop.key.name, + type: 'export', + ...prop.key.range[0] !== prop.value.range[0] && { + alias: prop.value.name, + }, + })), + ); + } + } else { + const nameParts = getNamedCJSExports(context, node.left); + if (nameParts && nameParts.length > 0) { + const name = nameParts.join('.'); + getBlockExports(node.parent.parent).push({ + node, + value: name, + displayName: name, + type: 'export', + rank: 0, + }); + } + } + } + }, + }, + 'Program:exit'() { importMap.forEach((imported) => { if (newlinesBetweenImports !== 'ignore') { makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, distinctGroup); @@ -781,10 +1100,18 @@ module.exports = { mutateRanksToAlphabetize(imported, alphabetize); } - makeOutOfOrderReport(context, imported); + makeOutOfOrderReport(context, imported, categories.import); + }); + + exportMap.forEach((exported) => { + if (alphabetize.order !== 'ignore') { + mutateRanksToAlphabetize(exported, alphabetize); + makeOutOfOrderReport(context, exported, categories.exports); + } }); importMap.clear(); + exportMap.clear(); }, }; }, diff --git a/src/rules/unambiguous.js b/src/rules/unambiguous.js index 91152ea2a..2491fad3e 100644 --- a/src/rules/unambiguous.js +++ b/src/rules/unambiguous.js @@ -5,6 +5,7 @@ import { isModule } from 'eslint-module-utils/unambiguous'; import docsUrl from '../docsUrl'; +import sourceType from '../core/sourceType'; module.exports = { meta: { @@ -19,7 +20,7 @@ module.exports = { create(context) { // ignore non-modules - if (context.parserOptions.sourceType !== 'module') { + if (sourceType(context) !== 'module') { return {}; } diff --git a/src/scc.js b/src/scc.js index 44c818bbe..c2b2c637d 100644 --- a/src/scc.js +++ b/src/scc.js @@ -18,12 +18,18 @@ export default class StronglyConnectedComponentsBuilder { } static for(context) { - const cacheKey = context.cacheKey || hashObject(context).digest('hex'); + const settingsHash = hashObject({ + settings: context.settings, + parserOptions: context.parserOptions, + parserPath: context.parserPath, + }).digest('hex'); + const cacheKey = context.path + settingsHash; if (cache.has(cacheKey)) { return cache.get(cacheKey); } const scc = StronglyConnectedComponentsBuilder.calculate(context); - cache.set(cacheKey, scc); + const visitedFiles = Object.keys(scc); + visitedFiles.forEach((filePath) => cache.set(filePath + settingsHash, scc)); return scc; } diff --git a/tests/files/issue210.config.flat.js b/tests/files/issue210.config.flat.js new file mode 100644 index 000000000..c894376f4 --- /dev/null +++ b/tests/files/issue210.config.flat.js @@ -0,0 +1,3 @@ +exports.languageOptions = { + sourceType: 'module', +} diff --git a/tests/files/just-json-files/eslint.config.js b/tests/files/just-json-files/eslint.config.js new file mode 100644 index 000000000..b1bf2070b --- /dev/null +++ b/tests/files/just-json-files/eslint.config.js @@ -0,0 +1,28 @@ +var jsonPlugin = require('eslint-plugin-json'); + +if (!jsonPlugin.processors.json) { + jsonPlugin.processors.json = jsonPlugin.processors['.json']; +} + +module.exports = [ + { + files: ['tests/files/just-json-files/*.json'], + plugins:{ + json: jsonPlugin, + }, + processor: 'json/json', + rules: Object.assign( + {}, + { + 'import/no-unused-modules': [ + 'error', + { + 'missingExports': false, + 'unusedExports': true, + }, + ], + }, + jsonPlugin.configs.recommended.rules + ) + }, +]; diff --git a/tests/files/no-named-as-default/exports.js b/tests/files/no-named-as-default/exports.js new file mode 100644 index 000000000..62402634f --- /dev/null +++ b/tests/files/no-named-as-default/exports.js @@ -0,0 +1,4 @@ +const variable = 1; + +export { variable }; +export default variable; diff --git a/tests/files/no-named-as-default/misleading-re-exports.js b/tests/files/no-named-as-default/misleading-re-exports.js new file mode 100644 index 000000000..8d36a0866 --- /dev/null +++ b/tests/files/no-named-as-default/misleading-re-exports.js @@ -0,0 +1,2 @@ +export { variable as something } from './exports'; +export { something as default } from './something'; diff --git a/tests/files/no-named-as-default/no-default-export.js b/tests/files/no-named-as-default/no-default-export.js new file mode 100644 index 000000000..db3074797 --- /dev/null +++ b/tests/files/no-named-as-default/no-default-export.js @@ -0,0 +1 @@ +export const foobar = 4; diff --git a/tests/files/no-named-as-default/re-exports.js b/tests/files/no-named-as-default/re-exports.js new file mode 100644 index 000000000..20306c182 --- /dev/null +++ b/tests/files/no-named-as-default/re-exports.js @@ -0,0 +1,2 @@ +export { something as default } from "./something"; +export { something } from "./something"; diff --git a/tests/files/no-named-as-default/something.js b/tests/files/no-named-as-default/something.js new file mode 100644 index 000000000..d8fd6851b --- /dev/null +++ b/tests/files/no-named-as-default/something.js @@ -0,0 +1 @@ +export const something = 42; diff --git a/tests/src/cli.js b/tests/src/cli.js index 8a7345487..60b8382d0 100644 --- a/tests/src/cli.js +++ b/tests/src/cli.js @@ -15,17 +15,29 @@ describe('CLI regression tests', function () { let cli; before(function () { if (ESLint) { - eslint = new ESLint({ - useEslintrc: false, - overrideConfigFile: './tests/files/issue210.config.js', - rulePaths: ['./src/rules'], - overrideConfig: { - rules: { - named: 2, + if (semver.satisfies(eslintPkg.version, '>= 9')) { + eslint = new ESLint({ + overrideConfigFile: './tests/files/issue210.config.flat.js', + overrideConfig: { + rules: { + 'import/named': 2, + }, }, - }, - plugins: { 'eslint-plugin-import': importPlugin }, - }); + plugins: { 'eslint-plugin-import': importPlugin }, + }); + } else { + eslint = new ESLint({ + useEslintrc: false, + overrideConfigFile: './tests/files/issue210.config.js', + rulePaths: ['./src/rules'], + overrideConfig: { + rules: { + named: 2, + }, + }, + plugins: { 'eslint-plugin-import': importPlugin }, + }); + } } else { cli = new CLIEngine({ useEslintrc: false, @@ -56,13 +68,20 @@ describe('CLI regression tests', function () { this.skip(); } else { if (ESLint) { - eslint = new ESLint({ - useEslintrc: false, - overrideConfigFile: './tests/files/just-json-files/.eslintrc.json', - rulePaths: ['./src/rules'], - ignore: false, - plugins: { 'eslint-plugin-import': importPlugin }, - }); + if (semver.satisfies(eslintPkg.version, '>= 9')) { + eslint = new ESLint({ + overrideConfigFile: './tests/files/just-json-files/eslint.config.js', + plugins: { 'eslint-plugin-import': importPlugin }, + }); + } else { + eslint = new ESLint({ + useEslintrc: false, + overrideConfigFile: './tests/files/just-json-files/.eslintrc.json', + rulePaths: ['./src/rules'], + ignore: false, + plugins: { 'eslint-plugin-import': importPlugin }, + }); + } } else { cli = new CLIEngine({ useEslintrc: false, diff --git a/tests/src/core/getExports.js b/tests/src/core/getExports.js index 76003410d..f11a26131 100644 --- a/tests/src/core/getExports.js +++ b/tests/src/core/getExports.js @@ -1,15 +1,18 @@ import { expect } from 'chai'; +import fs from 'fs'; import semver from 'semver'; import sinon from 'sinon'; import eslintPkg from 'eslint/package.json'; +import { test as testUnambiguous } from 'eslint-module-utils/unambiguous'; import typescriptPkg from 'typescript/package.json'; import * as tsConfigLoader from 'tsconfig-paths/lib/tsconfig-loader'; -import ExportMapBuilder from '../../../src/exportMap/builder'; - -import * as fs from 'fs'; +import ExportMapBuilder from '../../../src/exportMap/builder'; import { getFilename } from '../utils'; -import { test as testUnambiguous } from 'eslint-module-utils/unambiguous'; + +const babelPath = require.resolve('babel-eslint'); +const hypotheticalLocation = babelPath.replace('index.js', 'visitor-keys.js'); +const isVisitorKeysSupported = fs.existsSync(hypotheticalLocation); describe('ExportMap', function () { const fakeContext = Object.assign( @@ -21,7 +24,7 @@ describe('ExportMap', function () { }, { settings: {}, - parserPath: 'babel-eslint', + parserPath: require.resolve('babel-eslint'), }, ); @@ -36,11 +39,20 @@ describe('ExportMap', function () { }); - it('returns a cached copy on subsequent requests', function () { + (isVisitorKeysSupported ? it : it.skip)('returns a cached copy on subsequent requests', function () { expect(ExportMapBuilder.get('./named-exports', fakeContext)) .to.exist.and.equal(ExportMapBuilder.get('./named-exports', fakeContext)); }); + it('does not return a cached copy if the parse does not yield a visitor keys', function () { + const mockContext = { + ...fakeContext, + parserPath: 'not-real', + }; + expect(ExportMapBuilder.get('./named-exports', mockContext)) + .to.exist.and.not.equal(ExportMapBuilder.get('./named-exports', mockContext)); + }); + it('does not return a cached copy after modification', (done) => { const firstAccess = ExportMapBuilder.get('./mutator', fakeContext); expect(firstAccess).to.exist; diff --git a/tests/src/core/parse.js b/tests/src/core/parse.js index 275b93982..b21326890 100644 --- a/tests/src/core/parse.js +++ b/tests/src/core/parse.js @@ -138,4 +138,18 @@ describe('parse(content, { settings, ecmaFeatures })', function () { parseStubParser.parse = parseSpy; expect(parse.bind(null, path, content, { settings: {}, parserPath: 'espree', languageOptions: { parserOptions: { sourceType: 'module', ecmaVersion: 2015, ecmaFeatures: { jsx: true } } }, parserOptions: { sourceType: 'script' } })).not.to.throw(Error); }); + + it('passes ecmaVersion and sourceType from languageOptions to parser', () => { + const parseSpy = sinon.spy(); + const languageOptions = { ecmaVersion: 'latest', sourceType: 'module', parserOptions: { ecmaFeatures: { jsx: true } } }; + parseStubParser.parse = parseSpy; + parse(path, content, { settings: {}, parserPath: parseStubParserPath, languageOptions }); + expect(parseSpy.args[0][1], 'custom parser to clone the parserOptions object').to.not.equal(languageOptions); + expect(parseSpy.args[0][1], 'custom parser to get ecmaFeatures in parserOptions which is a clone of ecmaFeatures passed in') + .to.have.property('ecmaFeatures') + .that.is.eql(languageOptions.parserOptions.ecmaFeatures) + .and.is.not.equal(languageOptions.parserOptions.ecmaFeatures); + expect(parseSpy.args[0][1], 'custom parser to get ecmaVersion in parserOptions from languageOptions').to.have.property('ecmaVersion', languageOptions.ecmaVersion); + expect(parseSpy.args[0][1], 'custom parser to get sourceType in parserOptions from languageOptions').to.have.property('sourceType', languageOptions.sourceType); + }); }); diff --git a/tests/src/exportMap/childContext.js b/tests/src/exportMap/childContext.js new file mode 100644 index 000000000..5bc53fdb0 --- /dev/null +++ b/tests/src/exportMap/childContext.js @@ -0,0 +1,121 @@ +import { expect } from 'chai'; +import { hashObject } from 'eslint-module-utils/hash'; + +import childContext from '../../../src/exportMap/childContext'; + +describe('childContext', () => { + const settings = { + setting1: true, + setting2: false, + }; + const parserOptions = { + ecmaVersion: 'latest', + sourceType: 'module', + }; + const parserPath = 'path/to/parser'; + const path = 'path/to/src/file'; + const languageOptions = { + ecmaVersion: 2024, + sourceType: 'module', + parser: { + parseForESLint() { return 'parser1'; }, + }, + }; + const languageOptionsHash = hashObject({ languageOptions }).digest('hex'); + const parserOptionsHash = hashObject({ parserOptions }).digest('hex'); + const settingsHash = hashObject({ settings }).digest('hex'); + + // https://github.com/import-js/eslint-plugin-import/issues/3051 + it('should pass context properties through, if present', () => { + const mockContext = { + settings, + parserOptions, + parserPath, + languageOptions, + }; + + const result = childContext(path, mockContext); + + expect(result.settings).to.deep.equal(settings); + expect(result.parserOptions).to.deep.equal(parserOptions); + expect(result.parserPath).to.equal(parserPath); + expect(result.languageOptions).to.deep.equal(languageOptions); + }); + + it('should add path and cacheKey to context', () => { + const mockContext = { + settings, + parserOptions, + parserPath, + }; + + const result = childContext(path, mockContext); + + expect(result.path).to.equal(path); + expect(result.cacheKey).to.be.a('string'); + }); + + it('should construct cache key out of languageOptions if present', () => { + const mockContext = { + settings, + languageOptions, + }; + + const result = childContext(path, mockContext); + + expect(result.cacheKey).to.equal(languageOptionsHash + settingsHash + path); + }); + + it('should use the same cache key upon multiple calls', () => { + const mockContext = { + settings, + languageOptions, + }; + + let result = childContext(path, mockContext); + + const expectedCacheKey = languageOptionsHash + settingsHash + path; + expect(result.cacheKey).to.equal(expectedCacheKey); + + result = childContext(path, mockContext); + expect(result.cacheKey).to.equal(expectedCacheKey); + }); + + it('should update cacheKey if different languageOptions are passed in', () => { + const mockContext = { + settings, + languageOptions, + }; + + let result = childContext(path, mockContext); + + const firstCacheKey = languageOptionsHash + settingsHash + path; + expect(result.cacheKey).to.equal(firstCacheKey); + + // Second run with different parser function + mockContext.languageOptions = { + ...languageOptions, + parser: { + parseForESLint() { return 'parser2'; }, + }, + }; + + result = childContext(path, mockContext); + + const secondCacheKey = hashObject({ languageOptions: mockContext.languageOptions }).digest('hex') + settingsHash + path; + expect(result.cacheKey).to.not.equal(firstCacheKey); + expect(result.cacheKey).to.equal(secondCacheKey); + }); + + it('should construct cache key out of parserOptions and parserPath if no languageOptions', () => { + const mockContext = { + settings, + parserOptions, + parserPath, + }; + + const result = childContext(path, mockContext); + + expect(result.cacheKey).to.equal(String(parserPath) + parserOptionsHash + settingsHash + path); + }); +}); diff --git a/tests/src/rule-tester.js b/tests/src/rule-tester.js new file mode 100644 index 000000000..103f2fd6f --- /dev/null +++ b/tests/src/rule-tester.js @@ -0,0 +1,47 @@ +import { RuleTester } from 'eslint'; +import { version as eslintVersion } from 'eslint/package.json'; +import semver from 'semver'; + +export const usingFlatConfig = semver.major(eslintVersion) >= 9; + +export function withoutAutofixOutput(test) { + return { ...test, ...usingFlatConfig || { output: test.code } }; +} + +class FlatCompatRuleTester { + constructor(testerConfig = { parserOptions: { sourceType: 'script' } }) { + this._tester = new RuleTester(FlatCompatRuleTester._flatCompat(testerConfig)); + } + + run(ruleName, rule, tests) { + this._tester.run(ruleName, rule, { + valid: tests.valid.map((t) => FlatCompatRuleTester._flatCompat(t)), + invalid: tests.invalid.map((t) => FlatCompatRuleTester._flatCompat(t)), + }); + } + + static _flatCompat(config) { + if (!config || !usingFlatConfig || typeof config !== 'object') { + return config; + } + + const { parser, parserOptions = {}, languageOptions = {}, ...remainingConfig } = config; + const { ecmaVersion, sourceType, ...remainingParserOptions } = parserOptions; + const parserObj = typeof parser === 'string' ? require(parser) : parser; + + return { + ...remainingConfig, + languageOptions: { + ...languageOptions, + ...parserObj ? { parser: parserObj } : {}, + ...ecmaVersion ? { ecmaVersion } : {}, + ...sourceType ? { sourceType } : {}, + parserOptions: { + ...remainingParserOptions, + }, + }, + }; + } +} + +export { FlatCompatRuleTester as RuleTester }; diff --git a/tests/src/rules/consistent-type-specifier-style.js b/tests/src/rules/consistent-type-specifier-style.js index 7799238c3..139457ff6 100644 --- a/tests/src/rules/consistent-type-specifier-style.js +++ b/tests/src/rules/consistent-type-specifier-style.js @@ -1,4 +1,4 @@ -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; import { test, parsers, tsVersionSatisfies, eslintVersionSatisfies, typescriptEslintParserSatisfies } from '../utils'; const rule = require('rules/consistent-type-specifier-style'); diff --git a/tests/src/rules/default.js b/tests/src/rules/default.js index eb2028c71..1df57a23a 100644 --- a/tests/src/rules/default.js +++ b/tests/src/rules/default.js @@ -1,6 +1,6 @@ import path from 'path'; import { test, testVersion, SYNTAX_CASES, getTSParsers, parsers } from '../utils'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; import semver from 'semver'; import { version as tsEslintVersion } from 'typescript-eslint-parser/package.json'; diff --git a/tests/src/rules/dynamic-import-chunkname.js b/tests/src/rules/dynamic-import-chunkname.js index 6afd834ab..e8f97475d 100644 --- a/tests/src/rules/dynamic-import-chunkname.js +++ b/tests/src/rules/dynamic-import-chunkname.js @@ -1,5 +1,5 @@ import { SYNTAX_CASES, getTSParsers, parsers } from '../utils'; -import { RuleTester } from 'eslint'; +import { RuleTester, withoutAutofixOutput } from '../rule-tester'; import semver from 'semver'; const rule = require('rules/dynamic-import-chunkname'); @@ -423,225 +423,172 @@ ruleTester.run('dynamic-import-chunkname', rule, { ], invalid: [ - { + withoutAutofixOutput({ code: `import( // webpackChunkName: "someModule" 'someModule' )`, options, parser, - output: `import( - // webpackChunkName: "someModule" - 'someModule' - )`, errors: [{ message: nonBlockCommentError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: 'import(\'test\')', options, parser, - output: 'import(\'test\')', errors: [{ message: noLeadingCommentError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackChunkName: someModule */ 'someModule' )`, options, parser, - output: `import( - /* webpackChunkName: someModule */ - 'someModule' - )`, errors: [{ message: invalidSyntaxCommentError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackChunkName: "someModule' */ 'someModule' )`, options, parser, - output: `import( - /* webpackChunkName: "someModule' */ - 'someModule' - )`, errors: [{ message: invalidSyntaxCommentError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackChunkName: 'someModule" */ 'someModule' )`, options, parser, - output: `import( - /* webpackChunkName: 'someModule" */ - 'someModule' - )`, errors: [{ message: invalidSyntaxCommentError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackChunkName "someModule" */ 'someModule' )`, options, parser, - output: `import( - /* webpackChunkName "someModule" */ - 'someModule' - )`, errors: [{ message: invalidSyntaxCommentError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackChunkName:"someModule" */ 'someModule' )`, options, parser, - output: `import( - /* webpackChunkName:"someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackChunkName: true */ 'someModule' )`, options, parser, - output: `import( - /* webpackChunkName: true */ - 'someModule' - )`, errors: [{ message: chunkNameFormatError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackChunkName: "my-module-[id]" */ 'someModule' )`, options, parser, - output: `import( - /* webpackChunkName: "my-module-[id]" */ - 'someModule' - )`, errors: [{ message: chunkNameFormatError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackChunkName: ["request"] */ 'someModule' )`, options, parser, - output: `import( - /* webpackChunkName: ["request"] */ - 'someModule' - )`, errors: [{ message: chunkNameFormatError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `import( /*webpackChunkName: "someModule"*/ 'someModule' )`, options, parser, - output: `import( - /*webpackChunkName: "someModule"*/ - 'someModule' - )`, errors: [{ message: noPaddingCommentError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackChunkName : "someModule" */ 'someModule' )`, options, parser, - output: `import( - /* webpackChunkName : "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackChunkName: "someModule" ; */ 'someModule' )`, options, parser, - output: `import( - /* webpackChunkName: "someModule" ; */ - 'someModule' - )`, errors: [{ message: invalidSyntaxCommentError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* totally not webpackChunkName: "someModule" */ 'someModule' )`, options, parser, - output: `import( - /* totally not webpackChunkName: "someModule" */ - 'someModule' - )`, errors: [{ message: invalidSyntaxCommentError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackPrefetch: true */ /* webpackChunk: "someModule" */ @@ -649,339 +596,253 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, options, parser, - output: `import( - /* webpackPrefetch: true */ - /* webpackChunk: "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackPrefetch: true, webpackChunk: "someModule" */ 'someModule' )`, options, parser, - output: `import( - /* webpackPrefetch: true, webpackChunk: "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackChunkName: "someModule123" */ 'someModule' )`, options: pickyCommentOptions, parser, - output: `import( - /* webpackChunkName: "someModule123" */ - 'someModule' - )`, errors: [{ message: pickyChunkNameFormatError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackPrefetch: "module", webpackChunkName: "someModule" */ 'someModule' )`, options, parser, - output: `import( - /* webpackPrefetch: "module", webpackChunkName: "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackPreload: "module", webpackChunkName: "someModule" */ 'someModule' )`, options, parser, - output: `import( - /* webpackPreload: "module", webpackChunkName: "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackIgnore: "no", webpackChunkName: "someModule" */ 'someModule' )`, options, parser, - output: `import( - /* webpackIgnore: "no", webpackChunkName: "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackInclude: "someModule", webpackChunkName: "someModule" */ 'someModule' )`, options, parser, - output: `import( - /* webpackInclude: "someModule", webpackChunkName: "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackInclude: true, webpackChunkName: "someModule" */ 'someModule' )`, options, parser, - output: `import( - /* webpackInclude: true, webpackChunkName: "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackExclude: "someModule", webpackChunkName: "someModule" */ 'someModule' )`, options, parser, - output: `import( - /* webpackExclude: "someModule", webpackChunkName: "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackExclude: true, webpackChunkName: "someModule" */ 'someModule' )`, options, parser, - output: `import( - /* webpackExclude: true, webpackChunkName: "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackMode: "fast", webpackChunkName: "someModule" */ 'someModule' )`, options, parser, - output: `import( - /* webpackMode: "fast", webpackChunkName: "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackMode: true, webpackChunkName: "someModule" */ 'someModule' )`, options, parser, - output: `import( - /* webpackMode: true, webpackChunkName: "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackExports: true, webpackChunkName: "someModule" */ 'someModule' )`, options, parser, - output: `import( - /* webpackExports: true, webpackChunkName: "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackExports: /default/, webpackChunkName: "someModule" */ 'someModule' )`, options, parser, - output: `import( - /* webpackExports: /default/, webpackChunkName: "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `dynamicImport( /* webpackChunkName "someModule" */ 'someModule' )`, options: multipleImportFunctionOptions, - output: `dynamicImport( - /* webpackChunkName "someModule" */ - 'someModule' - )`, errors: [{ message: invalidSyntaxCommentError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `definitelyNotStaticImport( /* webpackChunkName "someModule" */ 'someModule' )`, options: multipleImportFunctionOptions, - output: `definitelyNotStaticImport( - /* webpackChunkName "someModule" */ - 'someModule' - )`, errors: [{ message: invalidSyntaxCommentError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `dynamicImport( // webpackChunkName: "someModule" 'someModule' )`, options, - output: `dynamicImport( - // webpackChunkName: "someModule" - 'someModule' - )`, errors: [{ message: nonBlockCommentError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: 'dynamicImport(\'test\')', options, - output: 'dynamicImport(\'test\')', errors: [{ message: noLeadingCommentError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `dynamicImport( /* webpackChunkName: someModule */ 'someModule' )`, options, - output: `dynamicImport( - /* webpackChunkName: someModule */ - 'someModule' - )`, errors: [{ message: invalidSyntaxCommentError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `dynamicImport( /* webpackChunkName "someModule" */ 'someModule' )`, options, - output: `dynamicImport( - /* webpackChunkName "someModule" */ - 'someModule' - )`, errors: [{ message: invalidSyntaxCommentError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `dynamicImport( /* webpackChunkName:"someModule" */ 'someModule' )`, options, - output: `dynamicImport( - /* webpackChunkName:"someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `dynamicImport( /* webpackChunkName: "someModule123" */ 'someModule' )`, options: pickyCommentOptions, - output: `dynamicImport( - /* webpackChunkName: "someModule123" */ - 'someModule' - )`, errors: [{ message: pickyChunkNameFormatError, type: 'CallExpression', }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackChunkName: "someModule" */ /* webpackMode: "eager" */ @@ -989,11 +850,6 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, options, parser, - output: `import( - /* webpackChunkName: "someModule" */ - /* webpackMode: "eager" */ - 'someModule' - )`, errors: [{ message: eagerModeError, type: 'CallExpression', @@ -1001,7 +857,7 @@ ruleTester.run('dynamic-import-chunkname', rule, { { desc: 'Remove webpackChunkName', output: `import( - + ${''} /* webpackMode: "eager" */ 'someModule' )`, @@ -1010,13 +866,13 @@ ruleTester.run('dynamic-import-chunkname', rule, { desc: 'Remove webpackMode', output: `import( /* webpackChunkName: "someModule" */ - + ${''} 'someModule' )`, }, ], }], - }, + }), ], }); @@ -1343,177 +1199,136 @@ context('TypeScript', () => { }, ], invalid: [ - { + withoutAutofixOutput({ code: `import( // webpackChunkName: "someModule" 'someModule' )`, options, parser: typescriptParser, - output: `import( - // webpackChunkName: "someModule" - 'someModule' - )`, errors: [{ message: nonBlockCommentError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: 'import(\'test\')', options, parser: typescriptParser, - output: 'import(\'test\')', errors: [{ message: noLeadingCommentError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackChunkName: someModule */ 'someModule' )`, options, parser: typescriptParser, - output: `import( - /* webpackChunkName: someModule */ - 'someModule' - )`, errors: [{ message: invalidSyntaxCommentError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackChunkName "someModule' */ 'someModule' )`, options, parser: typescriptParser, - output: `import( - /* webpackChunkName "someModule' */ - 'someModule' - )`, errors: [{ message: invalidSyntaxCommentError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackChunkName 'someModule" */ 'someModule' )`, options, parser: typescriptParser, - output: `import( - /* webpackChunkName 'someModule" */ - 'someModule' - )`, errors: [{ message: invalidSyntaxCommentError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackChunkName "someModule" */ 'someModule' )`, options, parser: typescriptParser, - output: `import( - /* webpackChunkName "someModule" */ - 'someModule' - )`, errors: [{ message: invalidSyntaxCommentError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackChunkName:"someModule" */ 'someModule' )`, options, parser: typescriptParser, - output: `import( - /* webpackChunkName:"someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: `import( /*webpackChunkName: "someModule"*/ 'someModule' )`, options, parser: typescriptParser, - output: `import( - /*webpackChunkName: "someModule"*/ - 'someModule' - )`, errors: [{ message: noPaddingCommentError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackChunkName : "someModule" */ 'someModule' )`, options, parser: typescriptParser, - output: `import( - /* webpackChunkName : "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackChunkName: "someModule" ; */ 'someModule' )`, options, parser: typescriptParser, - output: `import( - /* webpackChunkName: "someModule" ; */ - 'someModule' - )`, errors: [{ message: invalidSyntaxCommentError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* totally not webpackChunkName: "someModule" */ 'someModule' )`, options, parser: typescriptParser, - output: `import( - /* totally not webpackChunkName: "someModule" */ - 'someModule' - )`, errors: [{ message: invalidSyntaxCommentError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackPrefetch: true */ /* webpackChunk: "someModule" */ @@ -1521,283 +1336,210 @@ context('TypeScript', () => { )`, options, parser: typescriptParser, - output: `import( - /* webpackPrefetch: true */ - /* webpackChunk: "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackPrefetch: true, webpackChunk: "someModule" */ 'someModule' )`, options, parser: typescriptParser, - output: `import( - /* webpackPrefetch: true, webpackChunk: "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackChunkName: true */ 'someModule' )`, options, parser: typescriptParser, - output: `import( - /* webpackChunkName: true */ - 'someModule' - )`, errors: [{ message: chunkNameFormatError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackChunkName: "my-module-[id]" */ 'someModule' )`, options, parser: typescriptParser, - output: `import( - /* webpackChunkName: "my-module-[id]" */ - 'someModule' - )`, errors: [{ message: chunkNameFormatError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackChunkName: ["request"] */ 'someModule' )`, options, parser: typescriptParser, - output: `import( - /* webpackChunkName: ["request"] */ - 'someModule' - )`, errors: [{ message: chunkNameFormatError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackChunkName: "someModule123" */ 'someModule' )`, options: pickyCommentOptions, parser: typescriptParser, - output: `import( - /* webpackChunkName: "someModule123" */ - 'someModule' - )`, errors: [{ message: pickyChunkNameFormatError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackPrefetch: "module", webpackChunkName: "someModule" */ 'someModule' )`, options, parser: typescriptParser, - output: `import( - /* webpackPrefetch: "module", webpackChunkName: "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackPreload: "module", webpackChunkName: "someModule" */ 'someModule' )`, options, parser: typescriptParser, - output: `import( - /* webpackPreload: "module", webpackChunkName: "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackIgnore: "no", webpackChunkName: "someModule" */ 'someModule' )`, options, parser: typescriptParser, - output: `import( - /* webpackIgnore: "no", webpackChunkName: "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackInclude: "someModule", webpackChunkName: "someModule" */ 'someModule' )`, options, parser: typescriptParser, - output: `import( - /* webpackInclude: "someModule", webpackChunkName: "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackInclude: true, webpackChunkName: "someModule" */ 'someModule' )`, options, parser: typescriptParser, - output: `import( - /* webpackInclude: true, webpackChunkName: "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackExclude: "someModule", webpackChunkName: "someModule" */ 'someModule' )`, options, parser: typescriptParser, - output: `import( - /* webpackExclude: "someModule", webpackChunkName: "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackExclude: true, webpackChunkName: "someModule" */ 'someModule' )`, options, parser: typescriptParser, - output: `import( - /* webpackExclude: true, webpackChunkName: "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackMode: "fast", webpackChunkName: "someModule" */ 'someModule' )`, options, parser: typescriptParser, - output: `import( - /* webpackMode: "fast", webpackChunkName: "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackMode: true, webpackChunkName: "someModule" */ 'someModule' )`, options, parser: typescriptParser, - output: `import( - /* webpackMode: true, webpackChunkName: "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackExports: true, webpackChunkName: "someModule" */ 'someModule' )`, options, parser: typescriptParser, - output: `import( - /* webpackExports: true, webpackChunkName: "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackExports: /default/, webpackChunkName: "someModule" */ 'someModule' )`, options, parser: typescriptParser, - output: `import( - /* webpackExports: /default/, webpackChunkName: "someModule" */ - 'someModule' - )`, errors: [{ message: commentFormatError, type: nodeType, }], - }, - { + }), + withoutAutofixOutput({ code: `import( /* webpackChunkName: "someModule", webpackMode: "eager" */ 'someModule' )`, options, parser: typescriptParser, - output: `import( - /* webpackChunkName: "someModule", webpackMode: "eager" */ - 'someModule' - )`, errors: [{ message: eagerModeError, type: nodeType, @@ -1818,8 +1560,8 @@ context('TypeScript', () => { }, ], }], - }, - { + }), + withoutAutofixOutput({ code: ` import( /* webpackMode: "eager", webpackChunkName: "someModule" */ @@ -1828,12 +1570,6 @@ context('TypeScript', () => { `, options, parser: typescriptParser, - output: ` - import( - /* webpackMode: "eager", webpackChunkName: "someModule" */ - 'someModule' - ) - `, errors: [{ message: eagerModeError, type: nodeType, @@ -1858,8 +1594,8 @@ context('TypeScript', () => { }, ], }], - }, - { + }), + withoutAutofixOutput({ code: ` import( /* webpackMode: "eager", webpackPrefetch: true, webpackChunkName: "someModule" */ @@ -1868,12 +1604,6 @@ context('TypeScript', () => { `, options, parser: typescriptParser, - output: ` - import( - /* webpackMode: "eager", webpackPrefetch: true, webpackChunkName: "someModule" */ - 'someModule' - ) - `, errors: [{ message: eagerModeError, type: nodeType, @@ -1898,8 +1628,8 @@ context('TypeScript', () => { }, ], }], - }, - { + }), + withoutAutofixOutput({ code: ` import( /* webpackChunkName: "someModule" */ @@ -1909,13 +1639,6 @@ context('TypeScript', () => { `, options, parser: typescriptParser, - output: ` - import( - /* webpackChunkName: "someModule" */ - /* webpackMode: "eager" */ - 'someModule' - ) - `, errors: [{ message: eagerModeError, type: nodeType, @@ -1942,7 +1665,7 @@ context('TypeScript', () => { }, ], }], - }, + }), ], }); }); diff --git a/tests/src/rules/export.js b/tests/src/rules/export.js index a7f2bec12..338501511 100644 --- a/tests/src/rules/export.js +++ b/tests/src/rules/export.js @@ -1,6 +1,6 @@ import { test, testFilePath, SYNTAX_CASES, getTSParsers, testVersion } from '../utils'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; import eslintPkg from 'eslint/package.json'; import semver from 'semver'; import { version as tsEslintVersion } from 'typescript-eslint-parser/package.json'; @@ -56,6 +56,15 @@ ruleTester.run('export', rule, { `, parser, })), + getTSParsers().map((parser) => ({ + code: ` + export default function foo(param: string): boolean; + export default function foo(param: string, param1?: number): boolean { + return param && param1; + } + `, + parser, + })), ), invalid: [].concat( @@ -154,6 +163,19 @@ ruleTester.run('export', rule, { ecmaVersion: 2022, }, })), + + getTSParsers().map((parser) => ({ + code: ` + export default function a(): void; + export default function a() {} + export { x as default }; + `, + errors: [ + 'Multiple default exports.', + 'Multiple default exports.', + ], + parser, + })), ), }); @@ -510,7 +532,7 @@ context('TypeScript', function () { }), test({ code: ` - export function Foo(); + export function Foo() { }; export class Foo { } export namespace Foo { } `, @@ -529,7 +551,7 @@ context('TypeScript', function () { test({ code: ` export const Foo = 'bar'; - export function Foo(); + export function Foo() { }; export namespace Foo { } `, errors: [ diff --git a/tests/src/rules/exports-last.js b/tests/src/rules/exports-last.js index d7122e9a0..a676ae044 100644 --- a/tests/src/rules/exports-last.js +++ b/tests/src/rules/exports-last.js @@ -1,6 +1,6 @@ import { test } from '../utils'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; import rule from 'rules/exports-last'; const ruleTester = new RuleTester(); diff --git a/tests/src/rules/extensions.js b/tests/src/rules/extensions.js index 14d84eaa6..883dfab65 100644 --- a/tests/src/rules/extensions.js +++ b/tests/src/rules/extensions.js @@ -1,8 +1,17 @@ -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; import rule from 'rules/extensions'; import { getTSParsers, test, testFilePath, parsers } from '../utils'; const ruleTester = new RuleTester(); +const ruleTesterWithTypeScriptImports = new RuleTester({ + settings: { + 'import/resolver': { + typescript: { + alwaysTryTypes: true, + }, + }, + }, +}); ruleTester.run('extensions', rule, { valid: [ @@ -689,6 +698,64 @@ describe('TypeScript', () => { ], parser, }), + test({ + code: 'import type T from "./typescript-declare";', + errors: ['Missing file extension for "./typescript-declare"'], + options: [ + 'always', + { ts: 'never', tsx: 'never', js: 'never', jsx: 'never', checkTypeImports: true }, + ], + parser, + }), + test({ + code: 'export type { MyType } from "./typescript-declare";', + errors: ['Missing file extension for "./typescript-declare"'], + options: [ + 'always', + { ts: 'never', tsx: 'never', js: 'never', jsx: 'never', checkTypeImports: true }, + ], + parser, + }), + ], + }); + ruleTesterWithTypeScriptImports.run(`${parser}: (with TS resolver) extensions are enforced for type imports/export when checkTypeImports is set`, rule, { + valid: [ + test({ + code: 'import type { MyType } from "./typescript-declare.ts";', + options: [ + 'always', + { checkTypeImports: true }, + ], + parser, + }), + test({ + code: 'export type { MyType } from "./typescript-declare.ts";', + options: [ + 'always', + { checkTypeImports: true }, + ], + parser, + }), + ], + invalid: [ + test({ + code: 'import type { MyType } from "./typescript-declare";', + errors: ['Missing file extension "ts" for "./typescript-declare"'], + options: [ + 'always', + { checkTypeImports: true }, + ], + parser, + }), + test({ + code: 'export type { MyType } from "./typescript-declare";', + errors: ['Missing file extension "ts" for "./typescript-declare"'], + options: [ + 'always', + { checkTypeImports: true }, + ], + parser, + }), ], }); }); diff --git a/tests/src/rules/first.js b/tests/src/rules/first.js index f34f227b2..52b71db86 100644 --- a/tests/src/rules/first.js +++ b/tests/src/rules/first.js @@ -2,7 +2,7 @@ import { test, getTSParsers, testVersion } from '../utils'; import fs from 'fs'; import path from 'path'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; const ruleTester = new RuleTester(); const rule = require('rules/first'); diff --git a/tests/src/rules/group-exports.js b/tests/src/rules/group-exports.js index c3d07046f..6f05bc866 100644 --- a/tests/src/rules/group-exports.js +++ b/tests/src/rules/group-exports.js @@ -1,5 +1,5 @@ import { test } from '../utils'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; import rule from 'rules/group-exports'; import { resolve } from 'path'; import { default as babelPresetFlow } from 'babel-preset-flow'; diff --git a/tests/src/rules/max-dependencies.js b/tests/src/rules/max-dependencies.js index 982a4b427..959ee68de 100644 --- a/tests/src/rules/max-dependencies.js +++ b/tests/src/rules/max-dependencies.js @@ -1,6 +1,6 @@ import { test, getTSParsers, parsers } from '../utils'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; const ruleTester = new RuleTester(); const rule = require('rules/max-dependencies'); diff --git a/tests/src/rules/named.js b/tests/src/rules/named.js index 227bffc80..51a76c129 100644 --- a/tests/src/rules/named.js +++ b/tests/src/rules/named.js @@ -1,5 +1,5 @@ import { test, SYNTAX_CASES, getTSParsers, testFilePath, testVersion, parsers } from '../utils'; -import { RuleTester } from 'eslint'; +import { RuleTester, usingFlatConfig } from '../rule-tester'; import path from 'path'; import { CASE_SENSITIVE_FS } from 'eslint-module-utils/resolve'; @@ -32,7 +32,7 @@ ruleTester.run('named', rule, { settings: { 'import/resolve': { extensions: ['.js', '.jsx'] } } }), // validate that eslint-disable-line silences this properly - test({ code: 'import {a, b, d} from "./common"; // eslint-disable-line named' }), + test({ code: `import {a, b, d} from "./common"; // eslint-disable-line ${usingFlatConfig ? 'rule-to-test/' : ''}named` }), test({ code: 'import { foo, bar } from "./re-export-names"' }), diff --git a/tests/src/rules/namespace.js b/tests/src/rules/namespace.js index 1475ae9b7..2a31d57e1 100644 --- a/tests/src/rules/namespace.js +++ b/tests/src/rules/namespace.js @@ -1,8 +1,8 @@ import { test, SYNTAX_CASES, getTSParsers, testVersion, testFilePath, parsers } from '../utils'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; import flatMap from 'array.prototype.flatmap'; -const ruleTester = new RuleTester({ env: { es6: true } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); const rule = require('rules/namespace'); function error(name, namespace) { @@ -336,10 +336,10 @@ const invalid = [].concat( test({ parser, code: `import { b } from "./${folder}/a"; console.log(b.c.d.e)` }), test({ parser, code: `import * as a from "./${folder}/a"; console.log(a.b.c.d.e.f)` }), test({ parser, code: `import * as a from "./${folder}/a"; var {b:{c:{d:{e}}}} = a` }), - test({ parser, code: `import { b } from "./${folder}/a"; var {c:{d:{e}}} = b` })); - - // deep namespaces should include explicitly exported defaults - test({ parser, code: `import * as a from "./${folder}/a"; console.log(a.b.default)` }), + test({ parser, code: `import { b } from "./${folder}/a"; var {c:{d:{e}}} = b` }), + // deep namespaces should include explicitly exported defaults + test({ parser, code: `import * as a from "./${folder}/a"; console.log(a.b.default)` }), + ); invalid.push( test({ @@ -371,7 +371,8 @@ const invalid = [].concat( parser, code: `import * as a from "./${folder}/a"; var {b:{c:{ e }}} = a`, errors: ["'e' not found in deeply imported namespace 'a.b.c'."], - })); + }), + ); }); ruleTester.run('namespace', rule, { valid, invalid }); diff --git a/tests/src/rules/newline-after-import.js b/tests/src/rules/newline-after-import.js index b78e891a3..984e89855 100644 --- a/tests/src/rules/newline-after-import.js +++ b/tests/src/rules/newline-after-import.js @@ -1,4 +1,4 @@ -import { RuleTester } from 'eslint'; +import { RuleTester, withoutAutofixOutput } from '../rule-tester'; import flatMap from 'array.prototype.flatmap'; import semver from 'semver'; import { version as tsEslintVersion } from 'typescript-eslint-parser/package.json'; @@ -710,9 +710,8 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { }], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, }, - { + withoutAutofixOutput({ code: `import foo from 'foo';\n\n\n\nexport default function() {};`, - output: `import foo from 'foo';\n\n\n\nexport default function() {};`, options: [{ count: 2, exactCount: true }], errors: [{ line: 1, @@ -720,10 +719,9 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { message: IMPORT_ERROR_MESSAGE_MULTIPLE(2), }], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, - }, - { + }), + withoutAutofixOutput({ code: `import foo from 'foo';\n\n\n\n\nexport default function() {};`, - output: `import foo from 'foo';\n\n\n\n\nexport default function() {};`, options: [{ count: 2, exactCount: true }], errors: [{ line: 1, @@ -731,7 +729,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { message: IMPORT_ERROR_MESSAGE_MULTIPLE(2), }], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, - }, + }), { code: `import foo from 'foo';\n// some random comment\nexport default function() {};`, output: `import foo from 'foo';\n\n// some random comment\nexport default function() {};`, @@ -743,9 +741,8 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { }], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, }, - { + withoutAutofixOutput({ code: `import foo from 'foo';\n// some random comment\n\n\nexport default function() {};`, - output: `import foo from 'foo';\n// some random comment\n\n\nexport default function() {};`, options: [{ count: 2, exactCount: true }], errors: [{ line: 1, @@ -753,10 +750,9 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { message: IMPORT_ERROR_MESSAGE_MULTIPLE(2), }], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, - }, - { + }), + withoutAutofixOutput({ code: `import foo from 'foo';\n// some random comment\n\n\n\nexport default function() {};`, - output: `import foo from 'foo';\n// some random comment\n\n\n\nexport default function() {};`, options: [{ count: 2, exactCount: true }], errors: [{ line: 1, @@ -764,7 +760,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { message: IMPORT_ERROR_MESSAGE_MULTIPLE(2), }], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, - }, + }), { code: `import foo from 'foo';\n// some random comment\nexport default function() {};`, output: `import foo from 'foo';\n\n\n// some random comment\nexport default function() {};`, @@ -787,9 +783,8 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { }], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, }, - { + withoutAutofixOutput({ code: `import foo from 'foo';\n\n\n\n// some random comment\nexport default function() {};`, - output: `import foo from 'foo';\n\n\n\n// some random comment\nexport default function() {};`, options: [{ count: 2, exactCount: true, considerComments: true }], errors: [{ line: 1, @@ -797,19 +792,12 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { message: IMPORT_ERROR_MESSAGE_MULTIPLE(2), }], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, - }, - { + }), + withoutAutofixOutput({ code: ` import foo from 'foo'; - // Some random single line comment - var bar = 42; - `, - output: ` - import foo from 'foo'; - - // Some random single line comment var bar = 42; `, @@ -820,7 +808,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { }], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, options: [{ considerComments: true, count: 1, exactCount: true }], - }, + }), { code: `import foo from 'foo';export default function() {};`, output: `import foo from 'foo';\n\nexport default function() {};`, @@ -832,9 +820,8 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { }], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, }, - { + withoutAutofixOutput({ code: `const foo = require('foo');\n\n\n\nconst bar = function() {};`, - output: `const foo = require('foo');\n\n\n\nconst bar = function() {};`, options: [{ count: 2, exactCount: true }], errors: [{ line: 1, @@ -842,10 +829,9 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { message: REQUIRE_ERROR_MESSAGE_MULTIPLE(2), }], parserOptions: { ecmaVersion: 2015 }, - }, - { + }), + withoutAutofixOutput({ code: `const foo = require('foo');\n\n\n\n// some random comment\nconst bar = function() {};`, - output: `const foo = require('foo');\n\n\n\n// some random comment\nconst bar = function() {};`, options: [{ count: 2, exactCount: true }], errors: [{ line: 1, @@ -853,7 +839,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { message: REQUIRE_ERROR_MESSAGE_MULTIPLE(2), }], parserOptions: { ecmaVersion: 2015 }, - }, + }), { code: `import foo from 'foo';// some random comment\nexport default function() {};`, output: `import foo from 'foo';\n\n// some random comment\nexport default function() {};`, diff --git a/tests/src/rules/no-absolute-path.js b/tests/src/rules/no-absolute-path.js index bfa08465c..bcf215137 100644 --- a/tests/src/rules/no-absolute-path.js +++ b/tests/src/rules/no-absolute-path.js @@ -1,6 +1,6 @@ import { test } from '../utils'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; const ruleTester = new RuleTester(); const rule = require('rules/no-absolute-path'); diff --git a/tests/src/rules/no-amd.js b/tests/src/rules/no-amd.js index 5317aa8fd..6b66578df 100644 --- a/tests/src/rules/no-amd.js +++ b/tests/src/rules/no-amd.js @@ -1,4 +1,4 @@ -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; import eslintPkg from 'eslint/package.json'; import semver from 'semver'; diff --git a/tests/src/rules/no-anonymous-default-export.js b/tests/src/rules/no-anonymous-default-export.js index 53b2fc6fb..37b3009f0 100644 --- a/tests/src/rules/no-anonymous-default-export.js +++ b/tests/src/rules/no-anonymous-default-export.js @@ -1,6 +1,6 @@ import { test, testVersion, SYNTAX_CASES } from '../utils'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; const ruleTester = new RuleTester(); const rule = require('rules/no-anonymous-default-export'); diff --git a/tests/src/rules/no-commonjs.js b/tests/src/rules/no-commonjs.js index b7c0aa803..3211c085a 100644 --- a/tests/src/rules/no-commonjs.js +++ b/tests/src/rules/no-commonjs.js @@ -1,4 +1,4 @@ -import { RuleTester } from 'eslint'; +import { RuleTester, withoutAutofixOutput } from '../rule-tester'; import eslintPkg from 'eslint/package.json'; import semver from 'semver'; @@ -69,47 +69,41 @@ ruleTester.run('no-commonjs', require('rules/no-commonjs'), { // imports ...semver.satisfies(eslintPkg.version, '< 4.0.0') ? [] : [ - { code: 'var x = require("x")', output: 'var x = require("x")', errors: [{ message: IMPORT_MESSAGE }] }, - { code: 'x = require("x")', output: 'x = require("x")', errors: [{ message: IMPORT_MESSAGE }] }, - { code: 'require("x")', output: 'require("x")', errors: [{ message: IMPORT_MESSAGE }] }, - { code: 'require(`x`)', + withoutAutofixOutput({ code: 'var x = require("x")', errors: [{ message: IMPORT_MESSAGE }] }), + withoutAutofixOutput({ code: 'x = require("x")', errors: [{ message: IMPORT_MESSAGE }] }), + withoutAutofixOutput({ code: 'require("x")', errors: [{ message: IMPORT_MESSAGE }] }), + withoutAutofixOutput({ code: 'require(`x`)', parserOptions: { ecmaVersion: 2015 }, - output: 'require(`x`)', errors: [{ message: IMPORT_MESSAGE }], - }, + }), - { code: 'if (typeof window !== "undefined") require("x")', + withoutAutofixOutput({ code: 'if (typeof window !== "undefined") require("x")', options: [{ allowConditionalRequire: false }], - output: 'if (typeof window !== "undefined") require("x")', errors: [{ message: IMPORT_MESSAGE }], - }, - { code: 'if (typeof window !== "undefined") { require("x") }', + }), + withoutAutofixOutput({ code: 'if (typeof window !== "undefined") { require("x") }', options: [{ allowConditionalRequire: false }], - output: 'if (typeof window !== "undefined") { require("x") }', errors: [{ message: IMPORT_MESSAGE }], - }, - { code: 'try { require("x") } catch (error) {}', + }), + withoutAutofixOutput({ code: 'try { require("x") } catch (error) {}', options: [{ allowConditionalRequire: false }], - output: 'try { require("x") } catch (error) {}', errors: [{ message: IMPORT_MESSAGE }], - }, + }), ], // exports - { code: 'exports.face = "palm"', output: 'exports.face = "palm"', errors: [{ message: EXPORT_MESSAGE }] }, - { code: 'module.exports.face = "palm"', output: 'module.exports.face = "palm"', errors: [{ message: EXPORT_MESSAGE }] }, - { code: 'module.exports = face', output: 'module.exports = face', errors: [{ message: EXPORT_MESSAGE }] }, - { code: 'exports = module.exports = {}', output: 'exports = module.exports = {}', errors: [{ message: EXPORT_MESSAGE }] }, - { code: 'var x = module.exports = {}', output: 'var x = module.exports = {}', errors: [{ message: EXPORT_MESSAGE }] }, - { code: 'module.exports = {}', + withoutAutofixOutput({ code: 'exports.face = "palm"', errors: [{ message: EXPORT_MESSAGE }] }), + withoutAutofixOutput({ code: 'module.exports.face = "palm"', errors: [{ message: EXPORT_MESSAGE }] }), + withoutAutofixOutput({ code: 'module.exports = face', errors: [{ message: EXPORT_MESSAGE }] }), + withoutAutofixOutput({ code: 'exports = module.exports = {}', errors: [{ message: EXPORT_MESSAGE }] }), + withoutAutofixOutput({ code: 'var x = module.exports = {}', errors: [{ message: EXPORT_MESSAGE }] }), + withoutAutofixOutput({ code: 'module.exports = {}', options: ['allow-primitive-modules'], - output: 'module.exports = {}', errors: [{ message: EXPORT_MESSAGE }], - }, - { code: 'var x = module.exports', + }), + withoutAutofixOutput({ code: 'var x = module.exports', options: ['allow-primitive-modules'], - output: 'var x = module.exports', errors: [{ message: EXPORT_MESSAGE }], - }, + }), ], }); diff --git a/tests/src/rules/no-cycle.js b/tests/src/rules/no-cycle.js index efc0fb6eb..ae4baab66 100644 --- a/tests/src/rules/no-cycle.js +++ b/tests/src/rules/no-cycle.js @@ -1,6 +1,6 @@ import { parsers, test as _test, testFilePath, testVersion as _testVersion } from '../utils'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; import flatMap from 'array.prototype.flatmap'; const ruleTester = new RuleTester(); @@ -8,11 +8,13 @@ const rule = require('rules/no-cycle'); const error = (message) => ({ message }); -const test = (def) => _test(Object.assign(def, { +const test = (def) => _test({ filename: testFilePath('./cycles/depth-zero.js'), -})); -const testVersion = (specifier, t) => _testVersion(specifier, () => Object.assign(t(), { + ...def, +}); +const testVersion = (specifier, t) => _testVersion(specifier, () => ({ filename: testFilePath('./cycles/depth-zero.js'), + ...t(), })); const testDialects = ['es6']; @@ -232,16 +234,6 @@ const cases = { errors: [error(`Dependency cycle via ./depth-two:1=>./depth-one:1`)], parser: parsers.BABEL_OLD, }), - test({ - code: `import { foo } from "./${testDialect}/depth-two"`, - options: [{ maxDepth: Infinity }], - errors: [error(`Dependency cycle via ./depth-one:1`)], - }), - test({ - code: `import { foo } from "./${testDialect}/depth-two"`, - options: [{ maxDepth: '∞' }], - errors: [error(`Dependency cycle via ./depth-one:1`)], - }), test({ code: `function bar(){ return import("./${testDialect}/depth-one"); } // #2265 5`, errors: [error(`Dependency cycle detected.`)], diff --git a/tests/src/rules/no-default-export.js b/tests/src/rules/no-default-export.js index 6c1a85a1d..8434ee148 100644 --- a/tests/src/rules/no-default-export.js +++ b/tests/src/rules/no-default-export.js @@ -1,12 +1,21 @@ import { parsers, test, testVersion } from '../utils'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; const ruleTester = new RuleTester(); const rule = require('rules/no-default-export'); ruleTester.run('no-default-export', rule, { valid: [ + test({ + code: 'module.exports = function foo() {}', + parserOptions: { + sourceType: 'script', + }, + }), + test({ + code: 'module.exports = function foo() {}', + }), test({ code: ` export const foo = 'foo'; diff --git a/tests/src/rules/no-deprecated.js b/tests/src/rules/no-deprecated.js index 318ea7c36..ad51d23c2 100644 --- a/tests/src/rules/no-deprecated.js +++ b/tests/src/rules/no-deprecated.js @@ -1,6 +1,6 @@ import { test, SYNTAX_CASES, getTSParsers } from '../utils'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; const ruleTester = new RuleTester(); const rule = require('rules/no-deprecated'); diff --git a/tests/src/rules/no-duplicates.js b/tests/src/rules/no-duplicates.js index e682f2235..cf57a3d59 100644 --- a/tests/src/rules/no-duplicates.js +++ b/tests/src/rules/no-duplicates.js @@ -2,7 +2,7 @@ import * as path from 'path'; import { test as testUtil, getNonDefaultParsers, parsers, tsVersionSatisfies, typescriptEslintParserSatisfies } from '../utils'; import jsxConfig from '../../../config/react'; -import { RuleTester } from 'eslint'; +import { RuleTester, withoutAutofixOutput } from '../rule-tester'; import eslintPkg from 'eslint/package.json'; import semver from 'semver'; import flatMap from 'array.prototype.flatmap'; @@ -96,15 +96,14 @@ ruleTester.run('no-duplicates', rule, { }), // #86: duplicate unresolved modules should be flagged - test({ + // Autofix bail because of different default import names. + test(withoutAutofixOutput({ code: "import foo from 'non-existent'; import bar from 'non-existent';", - // Autofix bail because of different default import names. - output: "import foo from 'non-existent'; import bar from 'non-existent';", errors: [ "'non-existent' imported multiple times.", "'non-existent' imported multiple times.", ], - }), + })), test({ code: "import type { x } from './foo'; import type { y } from './foo'", @@ -227,12 +226,11 @@ ruleTester.run('no-duplicates', rule, { errors: ['\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.'], }), - test({ + // Autofix bail because cannot merge namespace imports. + test(withoutAutofixOutput({ code: "import * as ns1 from './foo'; import * as ns2 from './foo'", - // Autofix bail because cannot merge namespace imports. - output: "import * as ns1 from './foo'; import * as ns2 from './foo'", errors: ['\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.'], - }), + })), test({ code: "import * as ns from './foo'; import {x} from './foo'; import {y} from './foo'", @@ -248,89 +246,57 @@ ruleTester.run('no-duplicates', rule, { errors: ['\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.'], }), - test({ + // Autofix bail because of comment. + test(withoutAutofixOutput({ code: ` // some-tool-disable-next-line import {x} from './foo' import {//y\ny} from './foo' `, - // Autofix bail because of comment. - output: ` - // some-tool-disable-next-line - import {x} from './foo' - import {//y\ny} from './foo' - `, errors: ['\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.'], - }), - - test({ + })), + // Autofix bail because of comment. + test(withoutAutofixOutput({ code: ` import {x} from './foo' // some-tool-disable-next-line import {y} from './foo' `, - // Autofix bail because of comment. - output: ` - import {x} from './foo' - // some-tool-disable-next-line - import {y} from './foo' - `, errors: ['\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.'], - }), - - test({ + })), + // Autofix bail because of comment. + test(withoutAutofixOutput({ code: ` import {x} from './foo' // some-tool-disable-line import {y} from './foo' `, - // Autofix bail because of comment. - output: ` - import {x} from './foo' // some-tool-disable-line - import {y} from './foo' - `, errors: ['\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.'], - }), - - test({ + })), + // Autofix bail because of comment. + test(withoutAutofixOutput({ code: ` import {x} from './foo' import {y} from './foo' // some-tool-disable-line `, - // Autofix bail because of comment. - output: ` - import {x} from './foo' - import {y} from './foo' // some-tool-disable-line - `, errors: ['\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.'], - }), - - test({ + })), + // Autofix bail because of comment. + test(withoutAutofixOutput({ code: ` import {x} from './foo' /* comment */ import {y} from './foo' `, - // Autofix bail because of comment. - output: ` - import {x} from './foo' - /* comment */ import {y} from './foo' - `, errors: ['\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.'], - }), - - test({ + })), + // Autofix bail because of comment. + test(withoutAutofixOutput({ code: ` import {x} from './foo' import {y} from './foo' /* comment multiline */ `, - // Autofix bail because of comment. - output: ` - import {x} from './foo' - import {y} from './foo' /* comment - multiline */ - `, errors: ['\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.'], - }), + })), test({ code: ` @@ -361,75 +327,48 @@ import {x,y} from './foo' `, errors: ['\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.'], }), - - test({ + // Autofix bail because of comment. + test(withoutAutofixOutput({ code: ` import {x} from './foo' import/* comment */{y} from './foo' `, - // Autofix bail because of comment. - output: ` - import {x} from './foo' - import/* comment */{y} from './foo' - `, errors: ['\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.'], - }), - - test({ + })), + // Autofix bail because of comment. + test(withoutAutofixOutput({ code: ` import {x} from './foo' import/* comment */'./foo' `, - // Autofix bail because of comment. - output: ` - import {x} from './foo' - import/* comment */'./foo' - `, errors: ['\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.'], - }), - - test({ + })), + // Autofix bail because of comment. + test(withoutAutofixOutput({ code: ` import {x} from './foo' import{y}/* comment */from './foo' `, - // Autofix bail because of comment. - output: ` - import {x} from './foo' - import{y}/* comment */from './foo' - `, errors: ['\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.'], - }), - - test({ + })), + // Autofix bail because of comment. + test(withoutAutofixOutput({ code: ` import {x} from './foo' import{y}from/* comment */'./foo' `, - // Autofix bail because of comment. - output: ` - import {x} from './foo' - import{y}from/* comment */'./foo' - `, errors: ['\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.'], - }), - - test({ + })), + // Autofix bail because of comment. + test(withoutAutofixOutput({ code: ` import {x} from // some-tool-disable-next-line './foo' import {y} from './foo' `, - // Autofix bail because of comment. - output: ` - import {x} from - // some-tool-disable-next-line - './foo' - import {y} from './foo' - `, errors: ['\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.'], - }), + })), // #2027 long import list generate empty lines test({ @@ -455,28 +394,28 @@ import {x,y} from './foo' import { BULK_ACTIONS_ENABLED } from '../constants'; - + ${''} const TestComponent = () => { return
; } - + ${''} export default TestComponent; `, output: ` import { DEFAULT_FILTER_KEYS, BULK_DISABLED, - + ${''} BULK_ACTIONS_ENABLED } from '../constants'; import React from 'react'; - + ${''} const TestComponent = () => { return
; } - + ${''} export default TestComponent; `, errors: ["'../constants' imported multiple times.", "'../constants' imported multiple times."], @@ -616,9 +555,8 @@ context('TypeScript', function () { ]); const invalid = [ - test({ + test(withoutAutofixOutput({ code: "import type x from './foo'; import type y from './foo'", - output: "import type x from './foo'; import type y from './foo'", ...parserConfig, errors: [ { @@ -632,7 +570,7 @@ context('TypeScript', function () { message: "'./foo' imported multiple times.", }, ], - }), + })), test({ code: "import type x from './foo'; import type x from './foo'", output: "import type x from './foo'; ", diff --git a/tests/src/rules/no-dynamic-require.js b/tests/src/rules/no-dynamic-require.js index e316470ec..fc7cf2b06 100644 --- a/tests/src/rules/no-dynamic-require.js +++ b/tests/src/rules/no-dynamic-require.js @@ -1,6 +1,6 @@ import { parsers, test, testVersion } from '../utils'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; import flatMap from 'array.prototype.flatmap'; const ruleTester = new RuleTester(); diff --git a/tests/src/rules/no-empty-named-blocks.js b/tests/src/rules/no-empty-named-blocks.js index f65e5a204..d9514a845 100644 --- a/tests/src/rules/no-empty-named-blocks.js +++ b/tests/src/rules/no-empty-named-blocks.js @@ -1,6 +1,6 @@ import { parsers, test } from '../utils'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; const ruleTester = new RuleTester(); const rule = require('rules/no-empty-named-blocks'); @@ -10,6 +10,7 @@ function generateSuggestionsTestCases(cases, parser) { code, parser, errors: [{ + message: 'Unexpected empty named import block', suggestions: [ { desc: 'Remove unused import', diff --git a/tests/src/rules/no-extraneous-dependencies.js b/tests/src/rules/no-extraneous-dependencies.js index 4b221de35..4a465eb39 100644 --- a/tests/src/rules/no-extraneous-dependencies.js +++ b/tests/src/rules/no-extraneous-dependencies.js @@ -3,7 +3,7 @@ import typescriptConfig from '../../../config/typescript'; import path from 'path'; import fs from 'fs'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; import flatMap from 'array.prototype.flatmap'; const ruleTester = new RuleTester(); @@ -43,11 +43,9 @@ ruleTester.run('no-extraneous-dependencies', rule, { test({ code: `export { foo } from "${pkg}"` }), test({ code: `export * from "${pkg}"` }), ]), - test({ code: 'import "eslint"' }), test({ code: 'import "eslint/lib/api"' }), test({ code: 'import "fs"' }), test({ code: 'import "./foo"' }), - test({ code: 'import "@org/package"' }), test({ code: 'import "electron"', settings: { 'import/core-modules': ['electron'] } }), test({ @@ -386,15 +384,6 @@ ruleTester.run('no-extraneous-dependencies', rule, { }], }), - test({ - code: 'import "not-a-dependency"', - filename: path.join(packageDirMonoRepoRoot, 'foo.js'), - options: [{ packageDir: packageDirMonoRepoRoot }], - errors: [{ - message: `'not-a-dependency' should be listed in the project's dependencies. Run 'npm i -S not-a-dependency' to add it`, - }], - }), - test({ code: 'import "esm-package-not-in-pkg-json/esm-module";', errors: [{ diff --git a/tests/src/rules/no-import-module-exports.js b/tests/src/rules/no-import-module-exports.js index c2bf7ed13..5738f8c52 100644 --- a/tests/src/rules/no-import-module-exports.js +++ b/tests/src/rules/no-import-module-exports.js @@ -1,5 +1,5 @@ import path from 'path'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; import { eslintVersionSatisfies, test, testVersion } from '../utils'; @@ -74,13 +74,13 @@ ruleTester.run('no-import-module-exports', rule, { import fs from 'fs/promises'; const subscriptions = new Map(); - + ${''} export default async (client) => { /** * loads all modules and their subscriptions */ const modules = await fs.readdir('./src/modules'); - + ${''} await Promise.all( modules.map(async (moduleName) => { // Loads the module @@ -97,7 +97,7 @@ ruleTester.run('no-import-module-exports', rule, { } }) ); - + ${''} /** * Setting up all events. * binds all events inside the subscriptions map to call all functions provided diff --git a/tests/src/rules/no-internal-modules.js b/tests/src/rules/no-internal-modules.js index c1c301545..9fa91ea3d 100644 --- a/tests/src/rules/no-internal-modules.js +++ b/tests/src/rules/no-internal-modules.js @@ -1,4 +1,4 @@ -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; import flatMap from 'array.prototype.flatmap'; import rule from 'rules/no-internal-modules'; diff --git a/tests/src/rules/no-mutable-exports.js b/tests/src/rules/no-mutable-exports.js index 1171443c4..ff9643b0d 100644 --- a/tests/src/rules/no-mutable-exports.js +++ b/tests/src/rules/no-mutable-exports.js @@ -1,5 +1,5 @@ import { parsers, test, testVersion } from '../utils'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; import rule from 'rules/no-mutable-exports'; const ruleTester = new RuleTester(); diff --git a/tests/src/rules/no-named-as-default-member.js b/tests/src/rules/no-named-as-default-member.js index 1773176f4..5c00224ed 100644 --- a/tests/src/rules/no-named-as-default-member.js +++ b/tests/src/rules/no-named-as-default-member.js @@ -1,5 +1,5 @@ import { test, testVersion, SYNTAX_CASES } from '../utils'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; import rule from 'rules/no-named-as-default-member'; const ruleTester = new RuleTester(); diff --git a/tests/src/rules/no-named-as-default.js b/tests/src/rules/no-named-as-default.js index c6646a4f0..349372067 100644 --- a/tests/src/rules/no-named-as-default.js +++ b/tests/src/rules/no-named-as-default.js @@ -1,5 +1,5 @@ import { test, testVersion, SYNTAX_CASES, parsers } from '../utils'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; const ruleTester = new RuleTester(); const rule = require('rules/no-named-as-default'); @@ -12,14 +12,20 @@ ruleTester.run('no-named-as-default', rule, { test({ code: 'import bar, { foo } from "./empty-folder";' }), // es7 - test({ code: 'export bar, { foo } from "./bar";', - parser: parsers.BABEL_OLD }), - test({ code: 'export bar from "./bar";', - parser: parsers.BABEL_OLD }), + test({ + code: 'export bar, { foo } from "./bar";', + parser: parsers.BABEL_OLD, + }), + test({ + code: 'export bar from "./bar";', + parser: parsers.BABEL_OLD, + }), // #566: don't false-positive on `default` itself - test({ code: 'export default from "./bar";', - parser: parsers.BABEL_OLD }), + test({ + code: 'export default from "./bar";', + parser: parsers.BABEL_OLD, + }), // es2022: Arbitrary module namespae identifier names testVersion('>= 8.7', () => ({ @@ -27,6 +33,48 @@ ruleTester.run('no-named-as-default', rule, { parserOptions: { ecmaVersion: 2022 }, })), + // #1594: Allow importing as default if object is exported both as default and named + test({ code: 'import something from "./no-named-as-default/re-exports.js";' }), + test({ + code: 'import { something } from "./no-named-as-default/re-exports.js";', + }), + test({ + code: 'import myOwnNameForVariable from "./no-named-as-default/exports.js";', + }), + test({ + code: 'import { variable } from "./no-named-as-default/exports.js";', + }), + test({ + code: 'import variable from "./no-named-as-default/misleading-re-exports.js";', + }), + test({ + // incorrect import + code: 'import foobar from "./no-named-as-default/no-default-export.js";', + }), + // same tests, but for exports + test({ + code: 'export something from "./no-named-as-default/re-exports.js";', + parser: parsers.BABEL_OLD, + }), + test({ + code: 'export { something } from "./no-named-as-default/re-exports.js";', + }), + test({ + code: 'export myOwnNameForVariable from "./no-named-as-default/exports.js";', + parser: parsers.BABEL_OLD, + }), + test({ + code: 'export { variable } from "./no-named-as-default/exports.js";', + }), + test({ + code: 'export variable from "./no-named-as-default/misleading-re-exports.js";', + parser: parsers.BABEL_OLD, + }), + test({ + code: 'export foobar from "./no-named-as-default/no-default-export.js";', + parser: parsers.BABEL_OLD, + }), + ...SYNTAX_CASES, ), @@ -34,13 +82,17 @@ ruleTester.run('no-named-as-default', rule, { test({ code: 'import foo from "./bar";', errors: [{ - message: 'Using exported name \'foo\' as identifier for default export.', - type: 'ImportDefaultSpecifier' }] }), + message: 'Using exported name \'foo\' as identifier for default import.', + type: 'ImportDefaultSpecifier', + }], + }), test({ code: 'import foo, { foo as bar } from "./bar";', errors: [{ - message: 'Using exported name \'foo\' as identifier for default export.', - type: 'ImportDefaultSpecifier' }] }), + message: 'Using exported name \'foo\' as identifier for default import.', + type: 'ImportDefaultSpecifier', + }], + }), // es7 test({ @@ -48,13 +100,17 @@ ruleTester.run('no-named-as-default', rule, { parser: parsers.BABEL_OLD, errors: [{ message: 'Using exported name \'foo\' as identifier for default export.', - type: 'ExportDefaultSpecifier' }] }), + type: 'ExportDefaultSpecifier', + }], + }), test({ code: 'export foo, { foo as bar } from "./bar";', parser: parsers.BABEL_OLD, errors: [{ message: 'Using exported name \'foo\' as identifier for default export.', - type: 'ExportDefaultSpecifier' }] }), + type: 'ExportDefaultSpecifier', + }], + }), test({ code: 'import foo from "./malformed.js"', @@ -68,7 +124,7 @@ ruleTester.run('no-named-as-default', rule, { testVersion('>= 8.7', () => ({ code: 'import foo from "./export-default-string-and-named"', errors: [{ - message: 'Using exported name \'foo\' as identifier for default export.', + message: 'Using exported name \'foo\' as identifier for default import.', type: 'ImportDefaultSpecifier', }], parserOptions: { ecmaVersion: 2022 }, @@ -76,10 +132,36 @@ ruleTester.run('no-named-as-default', rule, { 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.', + message: 'Using exported name \'foo\' as identifier for default import.', type: 'ImportDefaultSpecifier', }], parserOptions: { ecmaVersion: 2022 }, })), + + // #1594: Allow importing as default if object is exported both as default and named + test({ + code: 'import something from "./no-named-as-default/misleading-re-exports.js";', + parser: parsers.BABEL_OLD, + errors: [{ + message: 'Using exported name \'something\' as identifier for default import.', + type: 'ImportDefaultSpecifier', + }], + }), + // The only cases that are not covered by the fix + test({ + code: 'import variable from "./no-named-as-default/exports.js";', + errors: [{ + message: 'Using exported name \'variable\' as identifier for default import.', + type: 'ImportDefaultSpecifier', + }], + }), + test({ + code: 'export variable from "./no-named-as-default/exports.js";', + parser: parsers.BABEL_OLD, + errors: [{ + message: 'Using exported name \'variable\' as identifier for default export.', + type: 'ExportDefaultSpecifier', + }], + }), ), }); diff --git a/tests/src/rules/no-named-default.js b/tests/src/rules/no-named-default.js index 191c9c6ce..d36e26c44 100644 --- a/tests/src/rules/no-named-default.js +++ b/tests/src/rules/no-named-default.js @@ -1,5 +1,5 @@ import { test, testVersion, SYNTAX_CASES, parsers } from '../utils'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; const ruleTester = new RuleTester(); const rule = require('rules/no-named-default'); diff --git a/tests/src/rules/no-named-export.js b/tests/src/rules/no-named-export.js index 58b5da2f8..41f8e8f02 100644 --- a/tests/src/rules/no-named-export.js +++ b/tests/src/rules/no-named-export.js @@ -1,4 +1,4 @@ -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; import { parsers, test, testVersion } from '../utils'; const ruleTester = new RuleTester(); @@ -6,6 +6,15 @@ const rule = require('rules/no-named-export'); ruleTester.run('no-named-export', rule, { valid: [].concat( + test({ + code: 'module.export.foo = function () {}', + parserOptions: { + sourceType: 'script', + }, + }), + test({ + code: 'module.export.foo = function () {}', + }), test({ code: 'export default function bar() {};', }), diff --git a/tests/src/rules/no-namespace.js b/tests/src/rules/no-namespace.js index 03a23e3dd..f5cd967a2 100644 --- a/tests/src/rules/no-namespace.js +++ b/tests/src/rules/no-namespace.js @@ -1,4 +1,4 @@ -import { RuleTester } from 'eslint'; +import { RuleTester, withoutAutofixOutput } from '../rule-tester'; import eslintPkg from 'eslint/package.json'; import semver from 'semver'; import { test } from '../utils'; @@ -82,33 +82,30 @@ ruleTester.run('no-namespace', require('rules/no-namespace'), { ], invalid: [ - test({ + test(withoutAutofixOutput({ code: 'import * as foo from \'foo\';', - output: 'import * as foo from \'foo\';', errors: [{ line: 1, column: 8, message: ERROR_MESSAGE, }], - }), - test({ + })), + test(withoutAutofixOutput({ code: 'import defaultExport, * as foo from \'foo\';', - output: 'import defaultExport, * as foo from \'foo\';', errors: [{ line: 1, column: 23, message: ERROR_MESSAGE, }], - }), - test({ + })), + test(withoutAutofixOutput({ code: 'import * as foo from \'./foo\';', - output: 'import * as foo from \'./foo\';', errors: [{ line: 1, column: 8, message: ERROR_MESSAGE, }], - }), + })), ...FIX_TESTS, ], }); diff --git a/tests/src/rules/no-nodejs-modules.js b/tests/src/rules/no-nodejs-modules.js index b25eb0ce8..cf131ffee 100644 --- a/tests/src/rules/no-nodejs-modules.js +++ b/tests/src/rules/no-nodejs-modules.js @@ -1,6 +1,6 @@ import { test } from '../utils'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; const isCore = require('is-core-module'); const ruleTester = new RuleTester(); diff --git a/tests/src/rules/no-relative-packages.js b/tests/src/rules/no-relative-packages.js index 6104aeb9c..f47c6816b 100644 --- a/tests/src/rules/no-relative-packages.js +++ b/tests/src/rules/no-relative-packages.js @@ -1,4 +1,4 @@ -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; import rule from 'rules/no-relative-packages'; import { normalize } from 'path'; @@ -73,11 +73,11 @@ ruleTester.run('no-relative-packages', rule, { code: 'import bar from "../bar"', filename: testFilePath('./package-named/index.js'), errors: [{ - message: `Relative import from another package is not allowed. Use \`${normalize('eslint-plugin-import/tests/files/bar')}\` instead of \`../bar\``, + message: `Relative import from another package is not allowed. Use \`${normalize('@exodus/eslint-plugin-import/tests/files/bar')}\` instead of \`../bar\``, line: 1, column: 17, }], - output: `import bar from "eslint-plugin-import/tests/files/bar"`, + output: `import bar from "@exodus/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 1af9b8cf8..93c8b97aa 100644 --- a/tests/src/rules/no-relative-parent-imports.js +++ b/tests/src/rules/no-relative-parent-imports.js @@ -1,4 +1,4 @@ -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; import rule from 'rules/no-relative-parent-imports'; import { parsers, test as _test, testFilePath } from '../utils'; diff --git a/tests/src/rules/no-restricted-paths.js b/tests/src/rules/no-restricted-paths.js index a83a804a0..c3382ad08 100644 --- a/tests/src/rules/no-restricted-paths.js +++ b/tests/src/rules/no-restricted-paths.js @@ -1,4 +1,4 @@ -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; import rule from 'rules/no-restricted-paths'; import { getTSParsers, test, testFilePath } from '../utils'; diff --git a/tests/src/rules/no-self-import.js b/tests/src/rules/no-self-import.js index ff1248b43..dd2ea1bf2 100644 --- a/tests/src/rules/no-self-import.js +++ b/tests/src/rules/no-self-import.js @@ -1,6 +1,6 @@ import { test, testFilePath } from '../utils'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; const ruleTester = new RuleTester(); const rule = require('rules/no-self-import'); diff --git a/tests/src/rules/no-unassigned-import.js b/tests/src/rules/no-unassigned-import.js index f96808cbc..b73246ac0 100644 --- a/tests/src/rules/no-unassigned-import.js +++ b/tests/src/rules/no-unassigned-import.js @@ -1,7 +1,7 @@ import { test } from '../utils'; import * as path from 'path'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; const ruleTester = new RuleTester(); const rule = require('rules/no-unassigned-import'); diff --git a/tests/src/rules/no-unresolved.js b/tests/src/rules/no-unresolved.js index 04a53d887..c6e300c5d 100644 --- a/tests/src/rules/no-unresolved.js +++ b/tests/src/rules/no-unresolved.js @@ -4,7 +4,7 @@ import { getTSParsers, test, SYNTAX_CASES, testVersion, parsers } from '../utils import { CASE_SENSITIVE_FS } from 'eslint-module-utils/resolve'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; const ruleTester = new RuleTester(); const rule = require('rules/no-unresolved'); @@ -136,15 +136,6 @@ function runResolverTests(resolver) { ], }), - rest({ - code: "import bar from './baz';", - errors: [ - { - message: "Unable to resolve path to module './baz'.", - type: 'Literal', - }, - ], - }), rest({ code: "import bar from './baz';", errors: [ diff --git a/tests/src/rules/no-unused-modules.js b/tests/src/rules/no-unused-modules.js index 80bd70227..d86f40622 100644 --- a/tests/src/rules/no-unused-modules.js +++ b/tests/src/rules/no-unused-modules.js @@ -2,7 +2,7 @@ import { test, testVersion, testFilePath, getTSParsers, parsers } from '../utils import jsxConfig from '../../../config/react'; import typescriptConfig from '../../../config/typescript'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; import fs from 'fs'; import eslintPkg from 'eslint/package.json'; import semver from 'semver'; @@ -288,8 +288,8 @@ describe('dynamic imports', function () { // test for unused exports with `import()` ruleTester.run('no-unused-modules', rule, { - valid: [ - test({ + valid: [].concat( + testVersion('< 9', () => ({ options: unusedExportsOptions, code: ` export const a = 10 @@ -300,10 +300,10 @@ describe('dynamic imports', function () { `, parser: parsers.BABEL_OLD, filename: testFilePath('./no-unused-modules/exports-for-dynamic-js.js'), - }), - ], - invalid: [ - test({ + })), + ), + invalid: [].concat( + testVersion('< 9', () => ({ options: unusedExportsOptions, code: ` export const a = 10 @@ -319,8 +319,8 @@ describe('dynamic imports', function () { error(`exported declaration 'b' not used within other modules`), error(`exported declaration 'c' not used within other modules`), error(`exported declaration 'default' not used within other modules`), - ] }), - ], + ] })), + ), }); typescriptRuleTester.run('no-unused-modules', rule, { valid: [ diff --git a/tests/src/rules/no-useless-path-segments.js b/tests/src/rules/no-useless-path-segments.js index d6d0395de..87f7a73e9 100644 --- a/tests/src/rules/no-useless-path-segments.js +++ b/tests/src/rules/no-useless-path-segments.js @@ -1,5 +1,5 @@ import { parsers, test } from '../utils'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; const ruleTester = new RuleTester(); const rule = require('rules/no-useless-path-segments'); diff --git a/tests/src/rules/no-webpack-loader-syntax.js b/tests/src/rules/no-webpack-loader-syntax.js index 05ad242f5..86114b36c 100644 --- a/tests/src/rules/no-webpack-loader-syntax.js +++ b/tests/src/rules/no-webpack-loader-syntax.js @@ -1,6 +1,6 @@ import { test, getTSParsers, parsers } from '../utils'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; import semver from 'semver'; const ruleTester = new RuleTester(); diff --git a/tests/src/rules/order.js b/tests/src/rules/order.js index c2d659f83..ea62cec71 100644 --- a/tests/src/rules/order.js +++ b/tests/src/rules/order.js @@ -1,6 +1,6 @@ import { test, getTSParsers, getNonDefaultParsers, testFilePath, parsers } from '../utils'; -import { RuleTester } from 'eslint'; +import { RuleTester, withoutAutofixOutput } from '../rule-tester'; import eslintPkg from 'eslint/package.json'; import semver from 'semver'; import flatMap from 'array.prototype.flatmap'; @@ -21,10 +21,6 @@ const flowRuleTester = new RuleTester({ }); const rule = require('rules/order'); -function withoutAutofixOutput(test) { - return { ...test, output: test.code }; -} - ruleTester.run('order', rule, { valid: [ // Default order using require @@ -1133,6 +1129,140 @@ ruleTester.run('order', rule, { }, ], }), + // named import order + test({ + code: ` + import { a, B as C, Z } from './Z'; + const { D, n: c, Y } = require('./Z'); + export { C, D }; + export { A, B, C as default } from "./Z"; + + const { ["ignore require-statements with non-identifier imports"]: z, d } = require("./Z"); + exports = { ["ignore exports statements with non-identifiers"]: Z, D }; + `, + options: [{ + named: true, + alphabetize: { order: 'asc', caseInsensitive: true }, + }], + }), + test({ + code: ` + const { b, A } = require('./Z'); + `, + options: [{ + named: true, + alphabetize: { order: 'desc' }, + }], + }), + test({ + code: ` + import { A, B } from "./Z"; + export { Z, A } from "./Z"; + export { N, P } from "./Z"; + const { X, Y } = require("./Z"); + `, + options: [{ + named: { + require: true, + import: true, + export: false, + }, + }], + }), + test({ + code: ` + import { B, A } from "./Z"; + const { D, C } = require("./Z"); + export { B, A } from "./Z"; + `, + options: [{ + named: { + require: false, + import: false, + export: false, + }, + }], + }), + test({ + code: ` + import { B, A, R } from "foo"; + const { D, O, G } = require("tunes"); + export { B, A, Z } from "foo"; + `, + options: [{ + named: { enabled: false }, + }], + }), + test({ + code: ` + import { A as A, A as B, A as C } from "./Z"; + const { a, a: b, a: c } = require("./Z"); + `, + options: [{ + named: true, + }], + }), + test({ + code: ` + import { A, B, C } from "./Z"; + exports = { A, B, C }; + module.exports = { a: A, b: B, c: C }; + `, + options: [{ + named: { + cjsExports: true, + }, + alphabetize: { order: 'asc' }, + }], + }), + test({ + code: ` + module.exports.A = { }; + module.exports.A.B = { }; + module.exports.B = { }; + exports.C = { }; + `, + options: [{ + named: { + cjsExports: true, + }, + alphabetize: { order: 'asc' }, + }], + }), + // ensure other assignments are untouched + test({ + code: ` + var exports = null; + var module = null; + exports = { }; + module = { }; + module.exports = { }; + module.exports.U = { }; + module.exports.N = { }; + module.exports.C = { }; + exports.L = { }; + exports.E = { }; + `, + options: [{ + named: { + cjsExports: true, + }, + alphabetize: { order: 'asc' }, + }], + }), + test({ + code: ` + exports["B"] = { }; + exports["C"] = { }; + exports["A"] = { }; + `, + options: [{ + named: { + cjsExports: true, + }, + alphabetize: { order: 'asc' }, + }], + }), ], invalid: [ // builtin before external module (require) @@ -1772,19 +1902,13 @@ ruleTester.run('order', rule, { ], }), // Cannot fix newlines-between with multiline comment after - test({ + test(withoutAutofixOutput({ code: ` var fs = require('fs'); /* multiline comment */ var index = require('./'); `, - output: ` - var fs = require('fs'); /* multiline - comment */ - - var index = require('./'); - `, options: [ { groups: [['builtin'], ['index']], @@ -1797,7 +1921,7 @@ ruleTester.run('order', rule, { message: 'There should be no empty line between import groups', }, ], - }), + })), // Option newlines-between: 'always' - should report lack of newline between groups test({ code: ` @@ -1883,7 +2007,7 @@ ruleTester.run('order', rule, { }), // Option newlines-between: 'never' with unassigned imports and warnOnUnassignedImports disabled // newline is preserved to match existing behavior - test({ + test(withoutAutofixOutput({ code: ` import path from 'path'; import 'loud-rejection'; @@ -1891,13 +2015,6 @@ ruleTester.run('order', rule, { import 'something-else'; import _ from 'lodash'; `, - output: ` - import path from 'path'; - import 'loud-rejection'; - - import 'something-else'; - import _ from 'lodash'; - `, options: [{ 'newlines-between': 'never', warnOnUnassignedImports: false }], errors: [ { @@ -1905,7 +2022,7 @@ ruleTester.run('order', rule, { message: 'There should be no empty line between import groups', }, ], - }), + })), // Option newlines-between: 'never' with unassigned imports and warnOnUnassignedImports enabled test({ code: ` @@ -1930,7 +2047,7 @@ ruleTester.run('order', rule, { ], }), // Option newlines-between: 'never' cannot fix if there are other statements between imports - test({ + test(withoutAutofixOutput({ code: ` import path from 'path'; export const abc = 123; @@ -1938,13 +2055,6 @@ ruleTester.run('order', rule, { import 'something-else'; import _ from 'lodash'; `, - output: ` - import path from 'path'; - export const abc = 123; - - import 'something-else'; - import _ from 'lodash'; - `, options: [{ 'newlines-between': 'never' }], errors: [ { @@ -1952,7 +2062,7 @@ ruleTester.run('order', rule, { message: 'There should be no empty line between import groups', }, ], - }), + })), // Option newlines-between: 'always' should report missing empty lines when using not assigned imports test({ code: ` @@ -2036,7 +2146,7 @@ ruleTester.run('order', rule, { ], }), // reorder fix cannot cross function call on moving below #1 - test({ + test(withoutAutofixOutput({ code: ` const local = require('./local'); @@ -2047,22 +2157,12 @@ ruleTester.run('order', rule, { fn_call(); `, - output: ` - const local = require('./local'); - - fn_call(); - - const global1 = require('global1'); - const global2 = require('global2'); - - fn_call(); - `, errors: [{ message: '`./local` import should occur after import of `global2`', }], - }), + })), // reorder fix cannot cross function call on moving below #2 - test({ + test(withoutAutofixOutput({ code: ` const local = require('./local'); fn_call(); @@ -2071,20 +2171,12 @@ ruleTester.run('order', rule, { fn_call(); `, - output: ` - const local = require('./local'); - fn_call(); - const global1 = require('global1'); - const global2 = require('global2'); - - fn_call(); - `, errors: [{ message: '`./local` import should occur after import of `global2`', }], - }), + })), // reorder fix cannot cross function call on moving below #3 - test({ + test(withoutAutofixOutput({ code: ` const local1 = require('./local1'); const local2 = require('./local2'); @@ -2098,26 +2190,13 @@ ruleTester.run('order', rule, { const global5 = require('global5'); fn_call(); `, - output: ` - const local1 = require('./local1'); - const local2 = require('./local2'); - const local3 = require('./local3'); - const local4 = require('./local4'); - fn_call(); - const global1 = require('global1'); - const global2 = require('global2'); - const global3 = require('global3'); - const global4 = require('global4'); - const global5 = require('global5'); - fn_call(); - `, errors: [ '`./local1` import should occur after import of `global5`', '`./local2` import should occur after import of `global5`', '`./local3` import should occur after import of `global5`', '`./local4` import should occur after import of `global5`', ], - }), + })), // reorder fix cannot cross function call on moving below test(withoutAutofixOutput({ code: ` @@ -2427,7 +2506,7 @@ ruleTester.run('order', rule, { }], })), // reorder fix cannot cross function call on moving below (from #1252) - test({ + test(withoutAutofixOutput({ code: ` const env = require('./config'); @@ -2438,20 +2517,10 @@ ruleTester.run('order', rule, { http.createServer(express()); `, - output: ` - const env = require('./config'); - - Object.keys(env); - - const http = require('http'); - const express = require('express'); - - http.createServer(express()); - `, errors: [{ message: '`./config` import should occur after import of `express`', }], - }), + })), // reorder cannot cross non plain requires test(withoutAutofixOutput({ code: ` @@ -2742,6 +2811,205 @@ ruleTester.run('order', rule, { message: 'There should be no empty line within import group', }], }), + // named import order + test({ + code: ` + var { B, A: R } = require("./Z"); + import { O as G, D } from "./Z"; + import { K, L, J } from "./Z"; + export { Z, X, Y } from "./Z"; + `, + output: ` + var { A: R, B } = require("./Z"); + import { D, O as G } from "./Z"; + import { J, K, L } from "./Z"; + export { X, Y, Z } from "./Z"; + `, + options: [{ + named: true, + alphabetize: { order: 'asc' }, + }], + errors: [{ + message: '`A` import should occur before import of `B`', + }, { + message: '`D` import should occur before import of `O`', + }, { + message: '`J` import should occur before import of `K`', + }, { + message: '`Z` export should occur after export of `Y`', + }], + }), + test({ + code: ` + import { D, C } from "./Z"; + var { B, A } = require("./Z"); + export { B, A }; + `, + output: ` + import { C, D } from "./Z"; + var { B, A } = require("./Z"); + export { A, B }; + `, + options: [{ + named: { + require: false, + import: true, + export: true, + }, + alphabetize: { order: 'asc' }, + }], + errors: [{ + message: '`C` import should occur before import of `D`', + }, { + message: '`A` export should occur before export of `B`', + }], + }), + test({ + code: ` + import { A as B, A as C, A } from "./Z"; + export { A, A as D, A as B, A as C } from "./Z"; + const { a: b, a: c, a } = require("./Z"); + `, + output: ` + import { A, A as B, A as C } from "./Z"; + export { A, A as B, A as C, A as D } from "./Z"; + const { a, a: b, a: c } = require("./Z"); + `, + options: [{ + named: true, + alphabetize: { order: 'asc' }, + }], + errors: [{ + message: '`A` import should occur before import of `A as B`', + }, { + message: '`A as D` export should occur after export of `A as C`', + }, { + message: '`a` import should occur before import of `a as b`', + }], + }), + test({ + code: ` + import { A, B, C } from "./Z"; + exports = { B, C, A }; + module.exports = { c: C, a: A, b: B }; + `, + output: ` + import { A, B, C } from "./Z"; + exports = { A, B, C }; + module.exports = { a: A, b: B, c: C }; + `, + options: [{ + named: { + cjsExports: true, + }, + alphabetize: { order: 'asc' }, + }], + errors: [{ + message: '`A` export should occur before export of `B`', + }, { + message: '`c` export should occur after export of `b`', + }], + }), + test({ + code: ` + exports.B = { }; + module.exports.A = { }; + module.exports.C = { }; + `, + output: ` + module.exports.A = { }; + exports.B = { }; + module.exports.C = { }; + `, + options: [{ + named: { + cjsExports: true, + }, + alphabetize: { order: 'asc' }, + }], + errors: [{ + message: '`A` export should occur before export of `B`', + }], + }), + test({ + code: ` + exports.A.C = { }; + module.exports.A.A = { }; + exports.A.B = { }; + `, + output: ` + module.exports.A.A = { }; + exports.A.B = { }; + exports.A.C = { }; + `, + options: [{ + named: { + cjsExports: true, + }, + alphabetize: { order: 'asc' }, + }], + errors: [{ + message: '`A.C` export should occur after export of `A.B`', + }], + }), + // multiline named specifiers & trailing commas + test({ + code: ` + const { + F: O, + O: B, + /* Hello World */ + A: R + } = require("./Z"); + import { + Y, + X, + } from "./Z"; + export { + Z, A, + B + } from "./Z"; + module.exports = { + a: A, o: O, + b: B + }; + `, + output: ` + const { + /* Hello World */ + A: R, + F: O, + O: B + } = require("./Z"); + import { + X, + Y, + } from "./Z"; + export { A, + B, + Z + } from "./Z"; + module.exports = { + a: A, + b: B, o: O + }; + `, + options: [{ + named: { + enabled: true, + }, + alphabetize: { order: 'asc' }, + }], + errors: [{ + message: '`A` import should occur before import of `F`', + }, { + message: '`X` import should occur before import of `Y`', + }, { + message: '`Z` export should occur after export of `B`', + }, { + message: '`b` export should occur before export of `o`', + }], + }), // Alphabetize with require ...semver.satisfies(eslintPkg.version, '< 3.0.0') ? [] : [ test({ @@ -2772,6 +3040,9 @@ context('TypeScript', function () { // Type-only imports were added in TypeScript ESTree 2.23.0 .filter((parser) => parser !== parsers.TS_OLD) .forEach((parser) => { + const supportsTypeSpecifiers = semver.satisfies(require('@typescript-eslint/parser/package.json').version, '>= 5'); + const supportsImportTypeSpecifiers = parser !== parsers.TS_NEW || supportsTypeSpecifiers; + const supportsExportTypeSpecifiers = parser === parsers.TS_NEW && supportsTypeSpecifiers; const parserConfig = { parser, settings: { @@ -3161,19 +3432,13 @@ context('TypeScript', function () { ], }), // warns for out of order unassigned imports (warnOnUnassignedImports enabled) - test({ + test(withoutAutofixOutput({ code: ` import './local1'; import global from 'global1'; import local from './local2'; import 'global2'; `, - output: ` - import './local1'; - import global from 'global1'; - import local from './local2'; - import 'global2'; - `, errors: [ { message: '`global1` import should occur before import of `./local1`', @@ -3183,9 +3448,9 @@ context('TypeScript', function () { }, ], options: [{ warnOnUnassignedImports: true }], - }), + })), // fix cannot move below unassigned import (warnOnUnassignedImports enabled) - test({ + test(withoutAutofixOutput({ code: ` import local from './local'; @@ -3194,19 +3459,11 @@ context('TypeScript', function () { import global2 from 'global2'; import global3 from 'global3'; `, - output: ` - import local from './local'; - - import 'global1'; - - import global2 from 'global2'; - import global3 from 'global3'; - `, errors: [{ message: '`./local` import should occur after import of `global3`', }], options: [{ warnOnUnassignedImports: true }], - }), + })), // Imports inside module declaration test({ code: ` @@ -3238,6 +3495,108 @@ context('TypeScript', function () { }, ], }), + // named import order + test({ + code: ` + import { type Z, A } from "./Z"; + import type N, { E, D } from "./Z"; + import type { L, G } from "./Z"; + `, + output: ` + import { A, type Z } from "./Z"; + import type N, { D, E } from "./Z"; + import type { G, L } from "./Z"; + `, + ...parserConfig, + options: [{ + named: true, + alphabetize: { order: 'asc' }, + }], + errors: [ + { message: `\`A\` import should occur before${supportsImportTypeSpecifiers ? ' type' : ''} import of \`Z\`` }, + { message: '`D` import should occur before import of `E`' }, + { message: '`G` import should occur before import of `L`' }, + ], + }), + test({ + code: ` + const { B, /* Hello World */ A } = require("./Z"); + export { B, A } from "./Z"; + `, + output: ` + const { /* Hello World */ A, B } = require("./Z"); + export { A, B } from "./Z"; + `, + ...parserConfig, + options: [{ + named: true, + alphabetize: { order: 'asc' }, + }], + errors: [{ + message: '`A` import should occur before import of `B`', + }, { + message: '`A` export should occur before export of `B`', + }], + }), + + supportsExportTypeSpecifiers ? [ + test({ + code: ` + export { type B, A }; + `, + output: ` + export { A, type B }; + `, + ...parserConfig, + options: [{ + named: { + enabled: true, + types: 'mixed', + }, + alphabetize: { order: 'asc' }, + }], + errors: [{ + message: '`A` export should occur before type export of `B`', + }], + }), + test({ + code: ` + import { type B, A, default as C } from "./Z"; + `, + output: ` + import { A, default as C, type B } from "./Z"; + `, + ...parserConfig, + options: [{ + named: { + import: true, + types: 'types-last', + }, + alphabetize: { order: 'asc' }, + }], + errors: [{ + message: '`B` type import should occur after import of `default`', + }], + }), + test({ + code: ` + export { A, type Z } from "./Z"; + `, + output: ` + export { type Z, A } from "./Z"; + `, + ...parserConfig, + options: [{ + named: { + enabled: true, + types: 'types-first', + }, + }], + errors: [ + { message: '`Z` type export should occur before export of `A`' }, + ], + }), + ] : [], isCoreModule('node:child_process') && isCoreModule('node:fs/promises') ? [ test({ diff --git a/tests/src/rules/prefer-default-export.js b/tests/src/rules/prefer-default-export.js index a7310445b..8e459873f 100644 --- a/tests/src/rules/prefer-default-export.js +++ b/tests/src/rules/prefer-default-export.js @@ -1,6 +1,6 @@ import { test, testVersion, getNonDefaultParsers, parsers } from '../utils'; -import { RuleTester } from 'eslint'; +import { RuleTester } from '../rule-tester'; import semver from 'semver'; import { version as tsEslintVersion } from 'typescript-eslint-parser/package.json'; diff --git a/tests/src/rules/unambiguous.js b/tests/src/rules/unambiguous.js index 8cef69625..15c67470e 100644 --- a/tests/src/rules/unambiguous.js +++ b/tests/src/rules/unambiguous.js @@ -1,4 +1,4 @@ -import { RuleTester } from 'eslint'; +import { RuleTester, withoutAutofixOutput } from '../rule-tester'; import { parsers } from '../utils'; const ruleTester = new RuleTester(); @@ -48,11 +48,10 @@ ruleTester.run('unambiguous', rule, { }, ], invalid: [ - { + withoutAutofixOutput({ code: 'function x() {}', parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, - output: 'function x() {}', errors: ['This module could be parsed as a valid script.'], - }, + }), ], }); diff --git a/utils/CHANGELOG.md b/utils/CHANGELOG.md index 43bd0e022..bb9372512 100644 --- a/utils/CHANGELOG.md +++ b/utils/CHANGELOG.md @@ -5,6 +5,36 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ## Unreleased +### Changed +- [refactor] `parse`: avoid using a regex here (thanks [@ljharb]) + +## v2.12.0 - 2024-09-26 + +### Added +- `hash`: add support for hashing functions ([#3072], thanks [@michaelfaith]) + +## v2.11.1 - 2024-09-23 + +### Fixed +- `parse`: remove unneeded extra backticks ([#3057], thanks [@G-Rath]) +- `parse`: espree parser isn't working with flat config ([#3061], thanks [@michaelfaith]) +- `parse`: add `ecmaVersion` and `sourceType` to `parserOptions` ([#3061], thanks [@michaelfaith]) + +## v2.11.0 - 2024-09-05 + +### New +- `declaredScope`: take a `node` for modern eslint versions (thanks [@michaelfaith]) + +## v2.10.0 - 2024-09-05 + +### New +- add context compatibility helpers ([#3049], thanks [@michaelfaith]) + +## v2.9.0 - 2024-09-02 + +### New +- add support for Flat Config ([#3018], thanks [@michaelfaith]) + ## v2.8.2 - 2024-08-25 ### Fixed @@ -150,7 +180,12 @@ Yanked due to critical issue with cache key resulting from #839. ### Fixed - `unambiguous.test()` regex is now properly in multiline mode +[#3072]: https://github.com/import-js/eslint-plugin-import/pull/3072 +[#3061]: https://github.com/import-js/eslint-plugin-import/pull/3061 +[#3057]: https://github.com/import-js/eslint-plugin-import/pull/3057 +[#3049]: https://github.com/import-js/eslint-plugin-import/pull/3049 [#3039]: https://github.com/import-js/eslint-plugin-import/pull/3039 +[#3018]: https://github.com/import-js/eslint-plugin-import/pull/3018 [#2963]: https://github.com/import-js/eslint-plugin-import/pull/2963 [#2755]: https://github.com/import-js/eslint-plugin-import/pull/2755 [#2714]: https://github.com/import-js/eslint-plugin-import/pull/2714 @@ -187,6 +222,7 @@ Yanked due to critical issue with cache key resulting from #839. [@brettz9]: https://github.com/brettz9 [@christophercurrie]: https://github.com/christophercurrie [@DMartens]: https://github.com/DMartens +[@G-Rath]: https://github.com/G-Rath [@hulkish]: https://github.com/hulkish [@Hypnosphi]: https://github.com/Hypnosphi [@iamnapo]: https://github.com/iamnapo @@ -197,6 +233,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 +[@michaelfaith]: https://github.com/michaelfaith [@Mysak0CZ]: https://github.com/Mysak0CZ [@nicolo-ribaudo]: https://github.com/nicolo-ribaudo [@pmcelhaney]: https://github.com/pmcelhaney diff --git a/utils/contextCompat.d.ts b/utils/contextCompat.d.ts new file mode 100644 index 000000000..43fe0a91b --- /dev/null +++ b/utils/contextCompat.d.ts @@ -0,0 +1,38 @@ +import { Scope, SourceCode, Rule } from 'eslint'; +import * as ESTree from 'estree'; + +type LegacyContext = { + getFilename: () => string, + getPhysicalFilename: () => string, + getSourceCode: () => SourceCode, + getScope: never, + getAncestors: never, + getDeclaredVariables: never, +}; + +type NewContext = { + filename: string, + sourceCode: SourceCode, + getPhysicalFilename?: () => string, + getScope: () => Scope.Scope, + getAncestors: () => ESTree.Node[], + getDeclaredVariables: (node: ESTree.Node) => Scope.Variable[], +}; + +export type Context = LegacyContext | NewContext | Rule.RuleContext; + +declare function getAncestors(context: Context, node: ESTree.Node): ESTree.Node[]; +declare function getDeclaredVariables(context: Context, node: ESTree.Node): Scope.Variable[]; +declare function getFilename(context: Context): string; +declare function getPhysicalFilename(context: Context): string; +declare function getScope(context: Context, node: ESTree.Node): Scope.Scope; +declare function getSourceCode(context: Context): SourceCode; + +export { + getAncestors, + getDeclaredVariables, + getFilename, + getPhysicalFilename, + getScope, + getSourceCode, +}; diff --git a/utils/contextCompat.js b/utils/contextCompat.js new file mode 100644 index 000000000..b1bdc598e --- /dev/null +++ b/utils/contextCompat.js @@ -0,0 +1,72 @@ +'use strict'; + +exports.__esModule = true; + +/** @type {import('./contextCompat').getAncestors} */ +function getAncestors(context, node) { + const sourceCode = getSourceCode(context); + + if (sourceCode && sourceCode.getAncestors) { + return sourceCode.getAncestors(node); + } + + return context.getAncestors(); +} + +/** @type {import('./contextCompat').getDeclaredVariables} */ +function getDeclaredVariables(context, node) { + const sourceCode = getSourceCode(context); + + if (sourceCode && sourceCode.getDeclaredVariables) { + return sourceCode.getDeclaredVariables(node); + } + + return context.getDeclaredVariables(node); +} + +/** @type {import('./contextCompat').getFilename} */ +function getFilename(context) { + if ('filename' in context) { + return context.filename; + } + + return context.getFilename(); +} + +/** @type {import('./contextCompat').getPhysicalFilename} */ +function getPhysicalFilename(context) { + if (context.getPhysicalFilename) { + return context.getPhysicalFilename(); + } + + return getFilename(context); +} + +/** @type {import('./contextCompat').getScope} */ +function getScope(context, node) { + const sourceCode = getSourceCode(context); + + if (sourceCode && sourceCode.getScope) { + return sourceCode.getScope(node); + } + + return context.getScope(); +} + +/** @type {import('./contextCompat').getSourceCode} */ +function getSourceCode(context) { + if ('sourceCode' in context) { + return context.sourceCode; + } + + return context.getSourceCode(); +} + +module.exports = { + getAncestors, + getDeclaredVariables, + getFilename, + getPhysicalFilename, + getScope, + getSourceCode, +}; diff --git a/utils/declaredScope.d.ts b/utils/declaredScope.d.ts index e37200d87..90053e8e7 100644 --- a/utils/declaredScope.d.ts +++ b/utils/declaredScope.d.ts @@ -1,8 +1,10 @@ import { Rule, Scope } from 'eslint'; +import * as ESTree from 'estree'; declare function declaredScope( context: Rule.RuleContext, - name: string + name: string, + node?: ESTree.Node, ): Scope.Scope['type'] | undefined; export default declaredScope; diff --git a/utils/declaredScope.js b/utils/declaredScope.js index 0f0a3d945..aa3e38b47 100644 --- a/utils/declaredScope.js +++ b/utils/declaredScope.js @@ -2,9 +2,11 @@ exports.__esModule = true; +const { getScope } = require('./contextCompat'); + /** @type {import('./declaredScope').default} */ -exports.default = function declaredScope(context, name) { - const references = context.getScope().references; +exports.default = function declaredScope(context, name, node) { + const references = (node ? getScope(context, node) : context.getScope()).references; const reference = references.find((x) => x.identifier.name === name); if (!reference || !reference.resolved) { return undefined; } return reference.resolved.scope.type; diff --git a/utils/hash.js b/utils/hash.js index b3ce618b5..21ed524a9 100644 --- a/utils/hash.js +++ b/utils/hash.js @@ -17,6 +17,8 @@ function hashify(value, hash) { if (Array.isArray(value)) { hashArray(value, hash); + } else if (typeof value === 'function') { + hash.update(String(value)); } else if (value instanceof Object) { hashObject(value, hash); } else { diff --git a/utils/package.json b/utils/package.json index 6d69e2414..017eb7192 100644 --- a/utils/package.json +++ b/utils/package.json @@ -1,12 +1,13 @@ { "name": "eslint-module-utils", - "version": "2.8.2", + "version": "2.12.0", "description": "Core utilities to support eslint-plugin-import and other module-related plugins.", "engines": { "node": ">=4" }, "main": false, "exports": { + "./contextCompat": "./contextCompat.js", "./ModuleCache": "./ModuleCache.js", "./ModuleCache.js": "./ModuleCache.js", "./declaredScope": "./declaredScope.js", diff --git a/utils/parse.js b/utils/parse.js index 75d527b00..793e37152 100644 --- a/utils/parse.js +++ b/utils/parse.js @@ -29,12 +29,18 @@ function keysFromParser(parserPath, parserInstance, parsedResult) { if (parsedResult && parsedResult.visitorKeys) { return parsedResult.visitorKeys; } - if (typeof parserPath === 'string' && (/.*espree.*/).test(parserPath)) { - return parserInstance.VisitorKeys; - } - if (typeof parserPath === 'string' && (/.*babel-eslint.*/).test(parserPath)) { + // The old babel parser doesn't have a `parseForESLint` eslint function, so we don't end + // up with a `parsedResult` here. It also doesn't expose the visitor keys on the parser itself, + // so we have to try and infer the visitor-keys module from the parserPath. + // This is NOT supported in flat config! + if (typeof parserPath === 'string' && parserPath.indexOf('babel-eslint') > -1) { return getBabelEslintVisitorKeys(parserPath); } + // The espree parser doesn't have the `parseForESLint` function, so we don't end up with a + // `parsedResult` here, but it does expose the visitor keys on the parser instance that we can use. + if (parserInstance && parserInstance.VisitorKeys) { + return parserInstance.VisitorKeys; + } return null; } @@ -107,7 +113,8 @@ exports.default = function parse(path, content, context) { if (context == null) { throw new Error('need context to parse properly'); } // ESLint in "flat" mode only sets context.languageOptions.parserOptions - let parserOptions = context.languageOptions && context.languageOptions.parserOptions || context.parserOptions; + const languageOptions = context.languageOptions; + let parserOptions = languageOptions && languageOptions.parserOptions || context.parserOptions; const parserOrPath = getParser(path, context); if (!parserOrPath) { throw new Error('parserPath or languageOptions.parser is required!'); } @@ -138,6 +145,17 @@ exports.default = function parse(path, content, context) { delete parserOptions.project; delete parserOptions.projects; + // If this is a flat config, we need to add ecmaVersion and sourceType (if present) from languageOptions + if (languageOptions && languageOptions.ecmaVersion) { + parserOptions.ecmaVersion = languageOptions.ecmaVersion; + } + if (languageOptions && languageOptions.sourceType) { + // @ts-expect-error languageOptions is from the flatConfig Linter type in 8.57 while parserOptions is not. + // Non-flat config parserOptions.sourceType doesn't have "commonjs" in the type. Once upgraded to v9 types, + // they'll be the same and this expect-error should be removed. + parserOptions.sourceType = languageOptions.sourceType; + } + // require the parser relative to the main module (i.e., ESLint) const parser = typeof parserOrPath === 'string' ? moduleRequire(parserOrPath) : parserOrPath; @@ -161,7 +179,7 @@ exports.default = function parse(path, content, context) { if (!ast || typeof ast !== 'object') { console.warn( // Can only be invalid for custom parser per imports/parser - '`parseForESLint` from parser `' + (typeof parserOrPath === 'string' ? parserOrPath : '`context.languageOptions.parser`') + '` is invalid and will just be ignored' + '`parseForESLint` from parser `' + (typeof parserOrPath === 'string' ? parserOrPath : 'context.languageOptions.parser') + '` is invalid and will just be ignored' ); } else { // @ts-expect-error TODO: FIXME diff --git a/utils/resolve.js b/utils/resolve.js index 5a3084351..b332d2ec2 100644 --- a/utils/resolve.js +++ b/utils/resolve.js @@ -5,6 +5,7 @@ exports.__esModule = true; const fs = require('fs'); const Module = require('module'); const path = require('path'); +const { getPhysicalFilename } = require('./contextCompat'); const hashObject = require('./hash').hashObject; const ModuleCache = require('./ModuleCache').default; @@ -229,7 +230,7 @@ const erroredContexts = new Set(); */ function resolve(p, context) { try { - return relative(p, context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename(), context.settings); + return relative(p, getPhysicalFilename(context), context.settings); } catch (err) { if (!erroredContexts.has(context)) { // The `err.stack` string starts with `err.name` followed by colon and `err.message`.