From 678cc943898d89270d397fe4ab363c07415d7a5e Mon Sep 17 00:00:00 2001 From: mulztob <49060581+mulztob@users.noreply.github.com> Date: Mon, 18 Dec 2023 08:43:34 +0100 Subject: [PATCH 01/50] [Docs] `no-extraneous-dependencies`: Make glob pattern description more explicit --- CHANGELOG.md | 7 ++++++- docs/rules/no-extraneous-dependencies.md | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b81ad61a61..13a201c90b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ## [Unreleased] +### Changed +- [Docs] `no-extraneous-dependencies`: Make glob pattern description more explicit ([#2944], thanks [@mulztob]) + ## [2.29.1] - 2023-12-14 ### Fixed @@ -1101,6 +1104,7 @@ for info on changes for earlier releases. [`memo-parser`]: ./memo-parser/README.md +[#2944]: https://github.com/import-js/eslint-plugin-import/pull/2944 [#2919]: https://github.com/import-js/eslint-plugin-import/pull/2919 [#2884]: https://github.com/import-js/eslint-plugin-import/pull/2884 [#2854]: https://github.com/import-js/eslint-plugin-import/pull/2854 @@ -1835,6 +1839,7 @@ for info on changes for earlier releases. [@mplewis]: https://github.com/mplewis [@mrmckeb]: https://github.com/mrmckeb [@msvab]: https://github.com/msvab +[@mulztob]: https://github.com/mulztob [@mx-bernhard]: https://github.com/mx-bernhard [@Nfinished]: https://github.com/Nfinished [@nickofthyme]: https://github.com/nickofthyme @@ -1843,9 +1848,9 @@ for info on changes for earlier releases. [@ntdb]: https://github.com/ntdb [@nwalters512]: https://github.com/nwalters512 [@ombene]: https://github.com/ombene -[@Pandemic1617]: https://github.com/Pandemic1617 [@ota-meshi]: https://github.com/ota-meshi [@OutdatedVersion]: https://github.com/OutdatedVersion +[@Pandemic1617]: https://github.com/Pandemic1617 [@panrafal]: https://github.com/panrafal [@paztis]: https://github.com/paztis [@pcorpet]: https://github.com/pcorpet diff --git a/docs/rules/no-extraneous-dependencies.md b/docs/rules/no-extraneous-dependencies.md index 547e5c2e57..848d5bb0da 100644 --- a/docs/rules/no-extraneous-dependencies.md +++ b/docs/rules/no-extraneous-dependencies.md @@ -32,7 +32,7 @@ You can also use an array of globs instead of literal booleans: "import/no-extraneous-dependencies": ["error", {"devDependencies": ["**/*.test.js", "**/*.spec.js"]}] ``` -When using an array of globs, the setting will be set to `true` (no errors reported) if the name of the file being linted matches a single glob in the array, and `false` otherwise. +When using an array of globs, the setting will be set to `true` (no errors reported) if the name of the file being linted (i.e. not the imported file/module) matches a single glob in the array, and `false` otherwise. There are 2 boolean options to opt into checking extra imports that are normally ignored: `includeInternal`, which enables the checking of internal modules, and `includeTypes`, which enables checking of type imports in TypeScript. From 6f0668c937a247d27c90cb391d4255fdfdd46a0f Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Mon, 18 Dec 2023 18:22:00 -0800 Subject: [PATCH 02/50] [Dev Deps] pin `markdownlint-cli` to v0.35, because v0.36+ depends on a `glob` that breaks CI See https://github.com/isaacs/jackspeak/issues/4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5c0af48543..dbe14f24c3 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "jackspeak": "=2.1.1", "linklocal": "^2.8.2", "lodash.isarray": "^4.0.0", - "markdownlint-cli": "^0.38.0", + "markdownlint-cli": "~0.35", "mocha": "^3.5.3", "npm-which": "^3.0.1", "nyc": "^11.9.0", From e9489d8f7378d7bf78a33898b963aead48efe186 Mon Sep 17 00:00:00 2001 From: JW Date: Sun, 17 Dec 2023 13:35:24 +0800 Subject: [PATCH 03/50] [New] `dynamic-import-chunkname`: add `allowEmpty` option to allow empty leading comments --- docs/rules/dynamic-import-chunkname.md | 35 ++++++++++++++++++++- src/rules/dynamic-import-chunkname.js | 9 ++++-- tests/src/rules/dynamic-import-chunkname.js | 30 ++++++++++++++++++ 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/docs/rules/dynamic-import-chunkname.md b/docs/rules/dynamic-import-chunkname.md index 35ae9df516..dd526c8913 100644 --- a/docs/rules/dynamic-import-chunkname.md +++ b/docs/rules/dynamic-import-chunkname.md @@ -15,7 +15,8 @@ You can also configure the regex format you'd like to accept for the webpackChun { "dynamic-import-chunkname": [2, { importFunctions: ["dynamicImport"], - webpackChunknameFormat: "[a-zA-Z0-57-9-/_]+" + webpackChunknameFormat: "[a-zA-Z0-57-9-/_]+", + allowEmpty: false }] } ``` @@ -87,6 +88,38 @@ The following patterns are valid: ); ``` +### `allowEmpty: true` + +If you want to allow dynamic imports without a webpackChunkName, you can set `allowEmpty: true` in the rule config. This will allow dynamic imports without a leading comment, or with a leading comment that does not contain a webpackChunkName. + +Given `{ "allowEmpty": true }`: + + +### valid + +The following patterns are valid: + +```javascript +import('someModule'); + +import( + /* webpackChunkName: "someModule" */ + 'someModule', +); +``` + +### invalid + +The following patterns are invalid: + +```javascript +// incorrectly formatted comment +import( + /*webpackChunkName:"someModule"*/ + 'someModule', +); +``` + ## When Not To Use It If you don't care that webpack will autogenerate chunk names and may blow up browser caches and bundle size reports. diff --git a/src/rules/dynamic-import-chunkname.js b/src/rules/dynamic-import-chunkname.js index 96ceff2e16..a62e5c6c12 100644 --- a/src/rules/dynamic-import-chunkname.js +++ b/src/rules/dynamic-import-chunkname.js @@ -19,6 +19,9 @@ module.exports = { type: 'string', }, }, + allowEmpty: { + type: 'boolean', + }, webpackChunknameFormat: { type: 'string', }, @@ -28,7 +31,7 @@ module.exports = { create(context) { const config = context.options[0]; - const { importFunctions = [] } = config || {}; + const { importFunctions = [], allowEmpty = false } = config || {}; const { webpackChunknameFormat = '([0-9a-zA-Z-_/.]|\\[(request|index)\\])+' } = config || {}; const paddedCommentRegex = /^ (\S[\s\S]+\S) $/; @@ -42,7 +45,7 @@ module.exports = { ? sourceCode.getCommentsBefore(arg) // This method is available in ESLint >= 4. : sourceCode.getComments(arg).leading; // This method is deprecated in ESLint 7. - if (!leadingComments || leadingComments.length === 0) { + if ((!leadingComments || leadingComments.length === 0) && !allowEmpty) { context.report({ node, message: 'dynamic imports require a leading comment with the webpack chunkname', @@ -94,7 +97,7 @@ module.exports = { } } - if (!isChunknamePresent) { + if (!isChunknamePresent && !allowEmpty) { context.report({ node, message: diff --git a/tests/src/rules/dynamic-import-chunkname.js b/tests/src/rules/dynamic-import-chunkname.js index 73617a6f36..c710507b26 100644 --- a/tests/src/rules/dynamic-import-chunkname.js +++ b/tests/src/rules/dynamic-import-chunkname.js @@ -12,6 +12,10 @@ const pickyCommentOptions = [{ importFunctions: ['dynamicImport'], webpackChunknameFormat: pickyCommentFormat, }]; +const allowEmptyOptions = [{ + importFunctions: ['dynamicImport'], + allowEmpty: true, +}]; const multipleImportFunctionOptions = [{ importFunctions: ['dynamicImport', 'definitelyNotStaticImport'], }]; @@ -83,6 +87,19 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, options, }, + { + code: `import('test')`, + options: allowEmptyOptions, + parser, + }, + { + code: `import( + /* webpackMode: "lazy" */ + 'test' + )`, + options: allowEmptyOptions, + parser, + }, { code: `import( /* webpackChunkName: "someModule" */ @@ -975,6 +992,19 @@ context('TypeScript', () => { ruleTester.run('dynamic-import-chunkname', rule, { valid: [ + { + code: `import('test')`, + options: allowEmptyOptions, + parser: typescriptParser, + }, + { + code: `import( + /* webpackMode: "lazy" */ + 'test' + )`, + options: allowEmptyOptions, + parser: typescriptParser, + }, { code: `import( /* webpackChunkName: "someModule" */ From 7a21f7e10f18c04473faadca94928af6b8e28009 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Fri, 29 Dec 2023 09:18:33 -0800 Subject: [PATCH 04/50] [meta] add missing changelog entry from #2942 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13a201c90b..3330dd331f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ## [Unreleased] +### Added +- [`dynamic-import-chunkname`]: add `allowEmpty` option to allow empty leading comments ([#2942], thanks [@JiangWeixian]) + ### Changed - [Docs] `no-extraneous-dependencies`: Make glob pattern description more explicit ([#2944], thanks [@mulztob]) @@ -1105,6 +1108,7 @@ for info on changes for earlier releases. [`memo-parser`]: ./memo-parser/README.md [#2944]: https://github.com/import-js/eslint-plugin-import/pull/2944 +[#2942]: https://github.com/import-js/eslint-plugin-import/pull/2942 [#2919]: https://github.com/import-js/eslint-plugin-import/pull/2919 [#2884]: https://github.com/import-js/eslint-plugin-import/pull/2884 [#2854]: https://github.com/import-js/eslint-plugin-import/pull/2854 @@ -1774,6 +1778,7 @@ for info on changes for earlier releases. [@jeffshaver]: https://github.com/jeffshaver [@jf248]: https://github.com/jf248 [@jfmengels]: https://github.com/jfmengels +[@JiangWeixian]: https://github.com/JiangWeixian [@jimbolla]: https://github.com/jimbolla [@jkimbo]: https://github.com/jkimbo [@joaovieira]: https://github.com/joaovieira From 1dc7fc66056c2b802aa9d72941846f08e1679544 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Wed, 14 Feb 2024 09:05:18 -0800 Subject: [PATCH 05/50] [Deps] update `array.prototype.findlastindex`, `hasown`, `object.groupby` --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index dbe14f24c3..7d6d63e718 100644 --- a/package.json +++ b/package.json @@ -104,19 +104,19 @@ }, "dependencies": { "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", + "array.prototype.findlastindex": "^1.2.4", "array.prototype.flat": "^1.3.2", "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", + "hasown": "^2.0.1", "is-core-module": "^2.13.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", + "object.groupby": "^1.0.2", "object.values": "^1.1.7", "semver": "^6.3.1", "tsconfig-paths": "^3.15.0" From 4d298b5ea61c359a39ac8b2c49f88b18070f4773 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Wed, 14 Feb 2024 09:10:10 -0800 Subject: [PATCH 06/50] [patch] `no-unused-modules`: add console message to help debug #2866 --- CHANGELOG.md | 2 ++ src/rules/no-unused-modules.js | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3330dd331f..06cdb922e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ### Changed - [Docs] `no-extraneous-dependencies`: Make glob pattern description more explicit ([#2944], thanks [@mulztob]) +- [`no-unused-modules`]: add console message to help debug [#2866] ## [2.29.1] - 2023-12-14 @@ -1111,6 +1112,7 @@ for info on changes for earlier releases. [#2942]: https://github.com/import-js/eslint-plugin-import/pull/2942 [#2919]: https://github.com/import-js/eslint-plugin-import/pull/2919 [#2884]: https://github.com/import-js/eslint-plugin-import/pull/2884 +[#2866]: https://github.com/import-js/eslint-plugin-import/pull/2866 [#2854]: https://github.com/import-js/eslint-plugin-import/pull/2854 [#2851]: https://github.com/import-js/eslint-plugin-import/pull/2851 [#2850]: https://github.com/import-js/eslint-plugin-import/pull/2850 diff --git a/src/rules/no-unused-modules.js b/src/rules/no-unused-modules.js index 8229d880ce..ec3425dacd 100644 --- a/src/rules/no-unused-modules.js +++ b/src/rules/no-unused-modules.js @@ -529,6 +529,10 @@ module.exports = { exports = exportList.get(file); + if (!exports) { + console.error(`file \`${file}\` has no exports. Please update to the latest, and if it still happens, report this on https://github.com/import-js/eslint-plugin-import/issues/2866!`); + } + // special case: export * from const exportAll = exports.get(EXPORT_ALL_DECLARATION); if (typeof exportAll !== 'undefined' && exportedValue !== IMPORT_DEFAULT_SPECIFIER) { From e55d05a85105769a563631ffcb55e818363ab8b6 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Wed, 14 Feb 2024 09:30:58 -0800 Subject: [PATCH 07/50] [Dev Deps] pin `jsonc-parser` due to a breaking change See https://github.com/microsoft/node-jsonc-parser/issues/85 --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 7d6d63e718..638942f97c 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "glob": "^7.2.3", "in-publish": "^2.0.1", "jackspeak": "=2.1.1", + "jsonc-parser": "=3.2.0", "linklocal": "^2.8.2", "lodash.isarray": "^4.0.0", "markdownlint-cli": "~0.35", From 20e3f297f9dd190eddb71e8277777ae7d583e7ef Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sat, 10 Feb 2024 17:44:15 -0500 Subject: [PATCH 08/50] [utils] [fix] `parse`: also delete `parserOptions.EXPERIMENTAL_useProjectService` --- utils/CHANGELOG.md | 5 +++++ utils/parse.js | 1 + 2 files changed, 6 insertions(+) diff --git a/utils/CHANGELOG.md b/utils/CHANGELOG.md index ae3588a390..9067cc1ef1 100644 --- a/utils/CHANGELOG.md +++ b/utils/CHANGELOG.md @@ -5,6 +5,9 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ## Unreleased +### Fixed +- `parse`: also delete `parserOptions.EXPERIMENTAL_useProjectService` ([#2963], thanks [@JoshuaKGoldberg]) + ## v2.8.0 - 2023-04-14 ### New @@ -131,6 +134,7 @@ Yanked due to critical issue with cache key resulting from #839. ### Fixed - `unambiguous.test()` regex is now properly in multiline mode +[#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 [#2523]: https://github.com/import-js/eslint-plugin-import/pull/2523 @@ -169,6 +173,7 @@ Yanked due to critical issue with cache key resulting from #839. [@hulkish]: https://github.com/hulkish [@Hypnosphi]: https://github.com/Hypnosphi [@iamnapo]: https://github.com/iamnapo +[@JoshuaKGoldberg]: https://github.com/JoshuaKGoldberg [@JounQin]: https://github.com/JounQin [@kaiyoma]: https://github.com/kaiyoma [@leipert]: https://github.com/leipert diff --git a/utils/parse.js b/utils/parse.js index 7646b3177c..bddd2d913d 100644 --- a/utils/parse.js +++ b/utils/parse.js @@ -81,6 +81,7 @@ exports.default = function parse(path, content, context) { // "project" or "projects" in parserOptions. Removing these options means the parser will // only parse one file in isolate mode, which is much, much faster. // https://github.com/import-js/eslint-plugin-import/issues/1408#issuecomment-509298962 + delete parserOptions.EXPERIMENTAL_useProjectService; delete parserOptions.project; delete parserOptions.projects; From 3c7f99062f57d879058f690c51e4866fbe635b8f Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Fri, 23 Feb 2024 16:43:11 -0800 Subject: [PATCH 09/50] [utils] add types --- .eslintrc | 4 +-- utils/.attw.json | 5 +++ utils/.npmignore | 1 + utils/CHANGELOG.md | 3 ++ utils/ModuleCache.d.ts | 22 +++++++++++++ utils/ModuleCache.js | 38 ++++++++++++----------- utils/declaredScope.d.ts | 8 +++++ utils/declaredScope.js | 3 +- utils/hash.d.ts | 14 +++++++++ utils/hash.js | 8 +++-- utils/ignore.d.ts | 12 ++++++++ utils/ignore.js | 12 ++++++-- utils/module-require.d.ts | 3 ++ utils/module-require.js | 5 +++ utils/moduleVisitor.d.ts | 26 ++++++++++++++++ utils/moduleVisitor.js | 51 ++++++++++++++++++------------ utils/package.json | 12 ++++++++ utils/parse.d.ts | 11 +++++++ utils/parse.js | 34 +++++++++++++++++--- utils/pkgDir.d.ts | 3 ++ utils/pkgDir.js | 1 + utils/pkgUp.d.ts | 3 ++ utils/pkgUp.js | 4 +++ utils/readPkgUp.d.ts | 5 +++ utils/readPkgUp.js | 2 ++ utils/resolve.d.ts | 30 ++++++++++++++++++ utils/resolve.js | 65 +++++++++++++++++++++++++-------------- utils/tsconfig.json | 49 +++++++++++++++++++++++++++++ utils/types.d.ts | 9 ++++++ utils/unambiguous.d.ts | 7 +++++ utils/unambiguous.js | 5 ++- utils/visit.d.ts | 9 ++++++ utils/visit.js | 13 +++++--- 33 files changed, 397 insertions(+), 80 deletions(-) create mode 100644 utils/.attw.json create mode 100644 utils/.npmignore create mode 100644 utils/ModuleCache.d.ts create mode 100644 utils/declaredScope.d.ts create mode 100644 utils/hash.d.ts create mode 100644 utils/ignore.d.ts create mode 100644 utils/module-require.d.ts create mode 100644 utils/moduleVisitor.d.ts create mode 100644 utils/parse.d.ts create mode 100644 utils/pkgDir.d.ts create mode 100644 utils/pkgUp.d.ts create mode 100644 utils/readPkgUp.d.ts create mode 100644 utils/resolve.d.ts create mode 100644 utils/tsconfig.json create mode 100644 utils/types.d.ts create mode 100644 utils/unambiguous.d.ts create mode 100644 utils/visit.d.ts diff --git a/.eslintrc b/.eslintrc index 3c9c658f2f..ddf7bc5628 100644 --- a/.eslintrc +++ b/.eslintrc @@ -209,10 +209,10 @@ "exports": "always-multiline", "functions": "never" }], - "prefer-destructuring": "warn", + "prefer-destructuring": "off", "prefer-object-spread": "off", "prefer-rest-params": "off", - "prefer-spread": "warn", + "prefer-spread": "off", "prefer-template": "off", } }, diff --git a/utils/.attw.json b/utils/.attw.json new file mode 100644 index 0000000000..45dd01e12f --- /dev/null +++ b/utils/.attw.json @@ -0,0 +1,5 @@ +{ + "ignoreRules": [ + "cjs-only-exports-default" + ] +} diff --git a/utils/.npmignore b/utils/.npmignore new file mode 100644 index 0000000000..366f3ebb6e --- /dev/null +++ b/utils/.npmignore @@ -0,0 +1 @@ +.attw.json diff --git a/utils/CHANGELOG.md b/utils/CHANGELOG.md index 9067cc1ef1..a0aa43da75 100644 --- a/utils/CHANGELOG.md +++ b/utils/CHANGELOG.md @@ -8,6 +8,9 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ### Fixed - `parse`: also delete `parserOptions.EXPERIMENTAL_useProjectService` ([#2963], thanks [@JoshuaKGoldberg]) +### Changed +- add types (thanks [@ljharb]) + ## v2.8.0 - 2023-04-14 ### New diff --git a/utils/ModuleCache.d.ts b/utils/ModuleCache.d.ts new file mode 100644 index 0000000000..72a72a0699 --- /dev/null +++ b/utils/ModuleCache.d.ts @@ -0,0 +1,22 @@ +import type { ESLintSettings } from "./types"; + +export type CacheKey = unknown; +export type CacheObject = { + result: unknown; + lastSeen: ReturnType; +}; + +declare class ModuleCache { + map: Map; + + constructor(map?: Map); + + get(cacheKey: CacheKey, settings: ESLintSettings): T | undefined; + + set(cacheKey: CacheKey, result: T): T; + + static getSettings(settings: ESLintSettings): { lifetime: number } & Omit; +} +export default ModuleCache; + +export type { ModuleCache } diff --git a/utils/ModuleCache.js b/utils/ModuleCache.js index 4b1edc0eff..24c76849dd 100644 --- a/utils/ModuleCache.js +++ b/utils/ModuleCache.js @@ -4,26 +4,26 @@ exports.__esModule = true; const log = require('debug')('eslint-module-utils:ModuleCache'); +/** @type {import('./ModuleCache').ModuleCache} */ class ModuleCache { + /** @param {typeof import('./ModuleCache').ModuleCache.prototype.map} map */ constructor(map) { - this.map = map || new Map(); + this.map = map || /** @type {{typeof import('./ModuleCache').ModuleCache.prototype.map} */ new Map(); } - /** - * returns value for returning inline - * @param {[type]} cacheKey [description] - * @param {[type]} result [description] - */ + /** @type {typeof import('./ModuleCache').ModuleCache.prototype.set} */ set(cacheKey, result) { this.map.set(cacheKey, { result, lastSeen: process.hrtime() }); log('setting entry for', cacheKey); return result; } + /** @type {typeof import('./ModuleCache').ModuleCache.prototype.get} */ get(cacheKey, settings) { if (this.map.has(cacheKey)) { const f = this.map.get(cacheKey); // check freshness + // @ts-expect-error TS can't narrow properly from `has` and `get` if (process.hrtime(f.lastSeen)[0] < settings.lifetime) { return f.result; } } else { log('cache miss for', cacheKey); @@ -32,19 +32,21 @@ class ModuleCache { return undefined; } -} - -ModuleCache.getSettings = function (settings) { - const cacheSettings = Object.assign({ - lifetime: 30, // seconds - }, settings['import/cache']); + /** @type {typeof import('./ModuleCache').ModuleCache.getSettings} */ + static getSettings(settings) { + /** @type {ReturnType} */ + const cacheSettings = Object.assign({ + lifetime: 30, // seconds + }, settings['import/cache']); + + // parse infinity + // @ts-expect-error the lack of type overlap is because we're abusing `cacheSettings` as a temporary object + if (cacheSettings.lifetime === '∞' || cacheSettings.lifetime === 'Infinity') { + cacheSettings.lifetime = Infinity; + } - // parse infinity - if (cacheSettings.lifetime === '∞' || cacheSettings.lifetime === 'Infinity') { - cacheSettings.lifetime = Infinity; + return cacheSettings; } - - return cacheSettings; -}; +} exports.default = ModuleCache; diff --git a/utils/declaredScope.d.ts b/utils/declaredScope.d.ts new file mode 100644 index 0000000000..e37200d870 --- /dev/null +++ b/utils/declaredScope.d.ts @@ -0,0 +1,8 @@ +import { Rule, Scope } from 'eslint'; + +declare function declaredScope( + context: Rule.RuleContext, + name: string +): Scope.Scope['type'] | undefined; + +export default declaredScope; diff --git a/utils/declaredScope.js b/utils/declaredScope.js index dd2a20149f..0f0a3d9458 100644 --- a/utils/declaredScope.js +++ b/utils/declaredScope.js @@ -2,9 +2,10 @@ exports.__esModule = true; +/** @type {import('./declaredScope').default} */ exports.default = function declaredScope(context, name) { const references = context.getScope().references; const reference = references.find((x) => x.identifier.name === name); - if (!reference) { return undefined; } + if (!reference || !reference.resolved) { return undefined; } return reference.resolved.scope.type; }; diff --git a/utils/hash.d.ts b/utils/hash.d.ts new file mode 100644 index 0000000000..5e4cf471bd --- /dev/null +++ b/utils/hash.d.ts @@ -0,0 +1,14 @@ +import type { Hash } from 'crypto'; + +declare function hashArray(value: Array, hash?: Hash): Hash; + +declare function hashObject(value: T, hash?: Hash): Hash; + +declare function hashify( + value: Array | object | unknown, + hash?: Hash, +): Hash; + +export default hashify; + +export { hashArray, hashObject }; diff --git a/utils/hash.js b/utils/hash.js index b9bff25bd9..b3ce618b54 100644 --- a/utils/hash.js +++ b/utils/hash.js @@ -11,6 +11,7 @@ const createHash = require('crypto').createHash; const stringify = JSON.stringify; +/** @type {import('./hash').default} */ function hashify(value, hash) { if (!hash) { hash = createHash('sha256'); } @@ -26,6 +27,7 @@ function hashify(value, hash) { } exports.default = hashify; +/** @type {import('./hash').hashArray} */ function hashArray(array, hash) { if (!hash) { hash = createHash('sha256'); } @@ -41,13 +43,15 @@ function hashArray(array, hash) { hashify.array = hashArray; exports.hashArray = hashArray; -function hashObject(object, hash) { - if (!hash) { hash = createHash('sha256'); } +/** @type {import('./hash').hashObject} */ +function hashObject(object, optionalHash) { + const hash = optionalHash || createHash('sha256'); hash.update('{'); Object.keys(object).sort().forEach((key) => { hash.update(stringify(key)); hash.update(':'); + // @ts-expect-error the key is guaranteed to exist on the object here hashify(object[key], hash); hash.update(','); }); diff --git a/utils/ignore.d.ts b/utils/ignore.d.ts new file mode 100644 index 0000000000..53953b33e9 --- /dev/null +++ b/utils/ignore.d.ts @@ -0,0 +1,12 @@ +import { Rule } from 'eslint'; +import type { ESLintSettings, Extension } from './types'; + +declare function ignore(path: string, context: Rule.RuleContext): boolean; + +declare function getFileExtensions(settings: ESLintSettings): Set; + +declare function hasValidExtension(path: string, context: Rule.RuleContext): path is `${string}${Extension}`; + +export default ignore; + +export { getFileExtensions, hasValidExtension } diff --git a/utils/ignore.js b/utils/ignore.js index 960538e706..59ac821eb8 100644 --- a/utils/ignore.js +++ b/utils/ignore.js @@ -7,7 +7,10 @@ const extname = require('path').extname; const log = require('debug')('eslint-plugin-import:utils:ignore'); // one-shot memoized -let cachedSet; let lastSettings; +/** @type {Set} */ let cachedSet; +/** @type {import('./types').ESLintSettings} */ let lastSettings; + +/** @type {(context: import('eslint').Rule.RuleContext) => Set} */ function validExtensions(context) { if (cachedSet && context.settings === lastSettings) { return cachedSet; @@ -18,8 +21,10 @@ function validExtensions(context) { return cachedSet; } +/** @type {import('./ignore').getFileExtensions} */ function makeValidExtensionSet(settings) { // start with explicit JS-parsed extensions + /** @type {Set} */ const exts = new Set(settings['import/extensions'] || ['.js']); // all alternate parser extensions are also valid @@ -37,6 +42,7 @@ function makeValidExtensionSet(settings) { } exports.getFileExtensions = makeValidExtensionSet; +/** @type {import('./ignore').default} */ exports.default = function ignore(path, context) { // check extension whitelist first (cheap) if (!hasValidExtension(path, context)) { return true; } @@ -55,7 +61,9 @@ exports.default = function ignore(path, context) { return false; }; +/** @type {import('./ignore').hasValidExtension} */ function hasValidExtension(path, context) { - return validExtensions(context).has(extname(path)); + // eslint-disable-next-line no-extra-parens + return validExtensions(context).has(/** @type {import('./types').Extension} */ (extname(path))); } exports.hasValidExtension = hasValidExtension; diff --git a/utils/module-require.d.ts b/utils/module-require.d.ts new file mode 100644 index 0000000000..91df90d616 --- /dev/null +++ b/utils/module-require.d.ts @@ -0,0 +1,3 @@ +declare function moduleRequire(p: string): T; + +export default moduleRequire; diff --git a/utils/module-require.js b/utils/module-require.js index 96ef82ba51..14006c5dc6 100644 --- a/utils/module-require.js +++ b/utils/module-require.js @@ -6,23 +6,28 @@ const Module = require('module'); const path = require('path'); // borrowed from babel-eslint +/** @type {(filename: string) => Module} */ function createModule(filename) { const mod = new Module(filename); mod.filename = filename; + // @ts-expect-error _nodeModulesPaths are undocumented mod.paths = Module._nodeModulePaths(path.dirname(filename)); return mod; } +/** @type {import('./module-require').default} */ exports.default = function moduleRequire(p) { try { // attempt to get espree relative to eslint const eslintPath = require.resolve('eslint'); const eslintModule = createModule(eslintPath); + // @ts-expect-error _resolveFilename is undocumented return require(Module._resolveFilename(p, eslintModule)); } catch (err) { /* ignore */ } try { // try relative to entry point + // @ts-expect-error TODO: figure out what this is return require.main.require(p); } catch (err) { /* ignore */ } diff --git a/utils/moduleVisitor.d.ts b/utils/moduleVisitor.d.ts new file mode 100644 index 0000000000..6f30186d71 --- /dev/null +++ b/utils/moduleVisitor.d.ts @@ -0,0 +1,26 @@ +import type { Rule } from 'eslint'; +import type { Node } from 'estree'; + +type Visitor = (source: Node, importer: unknown) => any; + +type Options = { + amd?: boolean; + commonjs?: boolean; + esmodule?: boolean; + ignore?: string[]; +}; + +declare function moduleVisitor( + visitor: Visitor, + options?: Options, +): object; + +export default moduleVisitor; + +export type Schema = NonNullable; + +declare function makeOptionsSchema(additionalProperties?: Partial): Schema + +declare const optionsSchema: Schema; + +export { makeOptionsSchema, optionsSchema }; diff --git a/utils/moduleVisitor.js b/utils/moduleVisitor.js index c312ca2d45..acdee6774f 100644 --- a/utils/moduleVisitor.js +++ b/utils/moduleVisitor.js @@ -2,50 +2,58 @@ exports.__esModule = true; +/** @typedef {import('estree').Node} Node */ +/** @typedef {{ arguments: import('estree').CallExpression['arguments'], callee: Node }} Call */ +/** @typedef {import('estree').ImportDeclaration | import('estree').ExportNamedDeclaration | import('estree').ExportAllDeclaration} Declaration */ + /** * Returns an object of node visitors that will call * 'visitor' with every discovered module path. * - * todo: correct function prototype for visitor - * @param {Function(String)} visitor [description] - * @param {[type]} options [description] - * @return {object} + * @type {(import('./moduleVisitor').default)} */ exports.default = function visitModules(visitor, options) { + const ignore = options && options.ignore; + const amd = !!(options && options.amd); + const commonjs = !!(options && options.commonjs); // if esmodule is not explicitly disabled, it is assumed to be enabled - options = Object.assign({ esmodule: true }, options); + const esmodule = !!Object.assign({ esmodule: true }, options).esmodule; - let ignoreRegExps = []; - if (options.ignore != null) { - ignoreRegExps = options.ignore.map((p) => new RegExp(p)); - } + const ignoreRegExps = ignore == null ? [] : ignore.map((p) => new RegExp(p)); + /** @type {(source: undefined | null | import('estree').Literal, importer: Parameters[1]) => void} */ function checkSourceValue(source, importer) { if (source == null) { return; } //? // handle ignore - if (ignoreRegExps.some((re) => re.test(source.value))) { return; } + if (ignoreRegExps.some((re) => re.test(String(source.value)))) { return; } // fire visitor visitor(source, importer); } // for import-y declarations + /** @type {(node: Declaration) => void} */ function checkSource(node) { checkSourceValue(node.source, node); } // for esmodule dynamic `import()` calls + /** @type {(node: import('estree').ImportExpression | import('estree').CallExpression) => void} */ function checkImportCall(node) { + /** @type {import('estree').Expression | import('estree').Literal | import('estree').CallExpression['arguments'][0]} */ let modulePath; // refs https://github.com/estree/estree/blob/HEAD/es2020.md#importexpression if (node.type === 'ImportExpression') { modulePath = node.source; } else if (node.type === 'CallExpression') { + // @ts-expect-error this structure is from an older version of eslint if (node.callee.type !== 'Import') { return; } if (node.arguments.length !== 1) { return; } modulePath = node.arguments[0]; + } else { + throw new TypeError('this should be unreachable'); } if (modulePath.type !== 'Literal') { return; } @@ -56,6 +64,7 @@ exports.default = function visitModules(visitor, options) { // for CommonJS `require` calls // adapted from @mctep: https://git.io/v4rAu + /** @type {(call: Call) => void} */ function checkCommon(call) { if (call.callee.type !== 'Identifier') { return; } if (call.callee.name !== 'require') { return; } @@ -68,6 +77,7 @@ exports.default = function visitModules(visitor, options) { checkSourceValue(modulePath, call); } + /** @type {(call: Call) => void} */ function checkAMD(call) { if (call.callee.type !== 'Identifier') { return; } if (call.callee.name !== 'require' && call.callee.name !== 'define') { return; } @@ -77,6 +87,7 @@ exports.default = function visitModules(visitor, options) { if (modules.type !== 'ArrayExpression') { return; } for (const element of modules.elements) { + if (!element) { continue; } if (element.type !== 'Literal') { continue; } if (typeof element.value !== 'string') { continue; } @@ -92,7 +103,7 @@ exports.default = function visitModules(visitor, options) { } const visitors = {}; - if (options.esmodule) { + if (esmodule) { Object.assign(visitors, { ImportDeclaration: checkSource, ExportNamedDeclaration: checkSource, @@ -102,12 +113,12 @@ exports.default = function visitModules(visitor, options) { }); } - if (options.commonjs || options.amd) { + if (commonjs || amd) { const currentCallExpression = visitors.CallExpression; - visitors.CallExpression = function (call) { + visitors.CallExpression = /** @type {(call: Call) => void} */ function (call) { if (currentCallExpression) { currentCallExpression(call); } - if (options.commonjs) { checkCommon(call); } - if (options.amd) { checkAMD(call); } + if (commonjs) { checkCommon(call); } + if (amd) { checkAMD(call); } }; } @@ -115,10 +126,11 @@ exports.default = function visitModules(visitor, options) { }; /** - * make an options schema for the module visitor, optionally - * adding extra fields. + * make an options schema for the module visitor, optionally adding extra fields. + * @type {import('./moduleVisitor').makeOptionsSchema} */ function makeOptionsSchema(additionalProperties) { + /** @type {import('./moduleVisitor').Schema} */ const base = { type: 'object', properties: { @@ -137,6 +149,7 @@ function makeOptionsSchema(additionalProperties) { if (additionalProperties) { for (const key in additionalProperties) { + // @ts-expect-error TS always has trouble with arbitrary object assignment/mutation base.properties[key] = additionalProperties[key]; } } @@ -146,8 +159,6 @@ function makeOptionsSchema(additionalProperties) { exports.makeOptionsSchema = makeOptionsSchema; /** - * json schema object for options parameter. can be used to build - * rule options schema object. - * @type {Object} + * json schema object for options parameter. can be used to build rule options schema object. */ exports.optionsSchema = makeOptionsSchema(); diff --git a/utils/package.json b/utils/package.json index d56c442b1a..04c338b1a8 100644 --- a/utils/package.json +++ b/utils/package.json @@ -7,6 +7,7 @@ }, "scripts": { "prepublishOnly": "cp ../{LICENSE,.npmrc} ./", + "tsc": "tsc -p .", "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { @@ -28,9 +29,20 @@ "dependencies": { "debug": "^3.2.7" }, + "devDependencies": { + "@types/debug": "^4.1.12", + "@types/eslint": "^8.56.3", + "@types/node": "^20.11.20", + "typescript": "next" + }, "peerDependenciesMeta": { "eslint": { "optional": true } + }, + "publishConfig": { + "ignore": [ + ".attw.json" + ] } } diff --git a/utils/parse.d.ts b/utils/parse.d.ts new file mode 100644 index 0000000000..f92ab3edc6 --- /dev/null +++ b/utils/parse.d.ts @@ -0,0 +1,11 @@ +import { AST, Rule } from 'eslint'; + + + +declare function parse( + path: string, + content: string, + context: Rule.RuleContext +): AST.Program | null | undefined; + +export default parse; diff --git a/utils/parse.js b/utils/parse.js index bddd2d913d..804186ca97 100644 --- a/utils/parse.js +++ b/utils/parse.js @@ -2,12 +2,16 @@ exports.__esModule = true; +/** @typedef {`.${string}`} Extension */ +/** @typedef {NonNullable & { 'import/extensions'?: Extension[], 'import/parsers'?: { [k: string]: Extension[] }, 'import/cache'?: { lifetime: number | '∞' | 'Infinity' } }} ESLintSettings */ + const moduleRequire = require('./module-require').default; const extname = require('path').extname; const fs = require('fs'); const log = require('debug')('eslint-plugin-import:parse'); +/** @type {(parserPath: NonNullable) => unknown} */ function getBabelEslintVisitorKeys(parserPath) { if (parserPath.endsWith('index.js')) { const hypotheticalLocation = parserPath.replace('index.js', 'visitor-keys.js'); @@ -19,6 +23,7 @@ function getBabelEslintVisitorKeys(parserPath) { return null; } +/** @type {(parserPath: import('eslint').Rule.RuleContext['parserPath'], parserInstance: { VisitorKeys: unknown }, parsedResult?: { visitorKeys?: unknown }) => unknown} */ function keysFromParser(parserPath, parserInstance, parsedResult) { // Exposed by @typescript-eslint/parser and @babel/eslint-parser if (parsedResult && parsedResult.visitorKeys) { @@ -35,22 +40,28 @@ function keysFromParser(parserPath, parserInstance, parsedResult) { // this exists to smooth over the unintentional breaking change in v2.7. // TODO, semver-major: avoid mutating `ast` and return a plain object instead. +/** @type {(ast: T, visitorKeys: unknown) => T} */ function makeParseReturn(ast, visitorKeys) { if (ast) { + // @ts-expect-error see TODO ast.visitorKeys = visitorKeys; + // @ts-expect-error see TODO ast.ast = ast; } return ast; } +/** @type {(text: string) => string} */ function stripUnicodeBOM(text) { return text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text; } +/** @type {(text: string) => string} */ function transformHashbang(text) { return text.replace(/^#!([^\r\n]+)/u, (_, captured) => `//${captured}`); } +/** @type {import('./parse').default} */ exports.default = function parse(path, content, context) { if (context == null) { throw new Error('need context to parse properly'); } @@ -97,10 +108,12 @@ exports.default = function parse(path, content, context) { try { const parserRaw = parser.parseForESLint(content, parserOptions); ast = parserRaw.ast; + // @ts-expect-error TODO: FIXME return makeParseReturn(ast, keysFromParser(parserOrPath, parser, parserRaw)); } catch (e) { console.warn(); console.warn('Error while parsing ' + parserOptions.filePath); + // @ts-expect-error e is almost certainly an Error here console.warn('Line ' + e.lineNumber + ', column ' + e.column + ': ' + e.message); } if (!ast || typeof ast !== 'object') { @@ -109,34 +122,45 @@ exports.default = function parse(path, content, context) { '`parseForESLint` from parser `' + (typeof parserOrPath === 'string' ? parserOrPath : '`context.languageOptions.parser`') + '` is invalid and will just be ignored' ); } else { + // @ts-expect-error TODO: FIXME return makeParseReturn(ast, keysFromParser(parserOrPath, parser, undefined)); } } const ast = parser.parse(content, parserOptions); + // @ts-expect-error TODO: FIXME return makeParseReturn(ast, keysFromParser(parserOrPath, parser, undefined)); }; +/** @type {(path: string, context: import('eslint').Rule.RuleContext) => string | null | (import('eslint').Linter.ParserModule)} */ function getParser(path, context) { const parserPath = getParserPath(path, context); if (parserPath) { return parserPath; } - const isFlat = context.languageOptions - && context.languageOptions.parser + if ( + !!context.languageOptions + && !!context.languageOptions.parser && typeof context.languageOptions.parser !== 'string' && ( + // @ts-expect-error TODO: figure out a better type typeof context.languageOptions.parser.parse === 'function' + // @ts-expect-error TODO: figure out a better type || typeof context.languageOptions.parser.parseForESLint === 'function' - ); + ) + ) { + return context.languageOptions.parser; + } - return isFlat ? context.languageOptions.parser : null; + return null; } +/** @type {(path: string, context: import('eslint').Rule.RuleContext & { settings?: ESLintSettings }) => import('eslint').Rule.RuleContext['parserPath']} */ function getParserPath(path, context) { const parsers = context.settings['import/parsers']; if (parsers != null) { - const extension = extname(path); + // eslint-disable-next-line no-extra-parens + const extension = /** @type {Extension} */ (extname(path)); for (const parserPath in parsers) { if (parsers[parserPath].indexOf(extension) > -1) { // use this alternate parser diff --git a/utils/pkgDir.d.ts b/utils/pkgDir.d.ts new file mode 100644 index 0000000000..af01e2e9bf --- /dev/null +++ b/utils/pkgDir.d.ts @@ -0,0 +1,3 @@ +declare function pkgDir(cwd: string): string | null; + +export default pkgDir; diff --git a/utils/pkgDir.js b/utils/pkgDir.js index 34412202f1..84c334680a 100644 --- a/utils/pkgDir.js +++ b/utils/pkgDir.js @@ -5,6 +5,7 @@ const pkgUp = require('./pkgUp').default; exports.__esModule = true; +/** @type {import('./pkgDir').default} */ exports.default = function (cwd) { const fp = pkgUp({ cwd }); return fp ? path.dirname(fp) : null; diff --git a/utils/pkgUp.d.ts b/utils/pkgUp.d.ts new file mode 100644 index 0000000000..6382457bec --- /dev/null +++ b/utils/pkgUp.d.ts @@ -0,0 +1,3 @@ +declare function pkgUp(opts?: { cwd?: string }): string | null; + +export default pkgUp; diff --git a/utils/pkgUp.js b/utils/pkgUp.js index 889f62265f..076e59fd76 100644 --- a/utils/pkgUp.js +++ b/utils/pkgUp.js @@ -31,10 +31,13 @@ const path = require('path'); * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ + +/** @type {(filename: string | string[], cwd?: string) => string | null} */ function findUp(filename, cwd) { let dir = path.resolve(cwd || ''); const root = path.parse(dir).root; + /** @type {string[]} */ // @ts-expect-error TS sucks with concat const filenames = [].concat(filename); // eslint-disable-next-line no-constant-condition @@ -52,6 +55,7 @@ function findUp(filename, cwd) { } } +/** @type {import('./pkgUp').default} */ exports.default = function pkgUp(opts) { return findUp('package.json', opts && opts.cwd); }; diff --git a/utils/readPkgUp.d.ts b/utils/readPkgUp.d.ts new file mode 100644 index 0000000000..5fc1668879 --- /dev/null +++ b/utils/readPkgUp.d.ts @@ -0,0 +1,5 @@ +import pkgUp from './pkgUp'; + +declare function readPkgUp(opts?: Parameters[0]): {} | { pkg: string, path: string }; + +export default readPkgUp; diff --git a/utils/readPkgUp.js b/utils/readPkgUp.js index d34fa6c818..08371931f2 100644 --- a/utils/readPkgUp.js +++ b/utils/readPkgUp.js @@ -5,6 +5,7 @@ exports.__esModule = true; const fs = require('fs'); const pkgUp = require('./pkgUp').default; +/** @type {(str: string) => string} */ function stripBOM(str) { return str.replace(/^\uFEFF/, ''); } @@ -35,6 +36,7 @@ function stripBOM(str) { * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ +/** @type {import('./readPkgUp').default} */ exports.default = function readPkgUp(opts) { const fp = pkgUp(opts); diff --git a/utils/resolve.d.ts b/utils/resolve.d.ts new file mode 100644 index 0000000000..bb885bcfaf --- /dev/null +++ b/utils/resolve.d.ts @@ -0,0 +1,30 @@ +import type { Rule } from 'eslint'; + +import type ModuleCache from './ModuleCache'; +import type { ESLintSettings } from './types'; + +export type ResultNotFound = { found: false, path?: undefined }; +export type ResultFound = { found: true, path: string | null }; +export type ResolvedResult = ResultNotFound | ResultFound; + +export type ResolverResolve = (modulePath: string, sourceFile:string, config: unknown) => ResolvedResult; +export type ResolverResolveImport = (modulePath: string, sourceFile:string, config: unknown) => string | undefined; +export type Resolver = { interfaceVersion?: 1 | 2, resolve: ResolverResolve, resolveImport: ResolverResolveImport }; + +declare function resolve( + p: string, + context: Rule.RuleContext, +): ResolvedResult['path']; + +export default resolve; + +declare function fileExistsWithCaseSync( + filepath: string | null, + cacheSettings: ESLintSettings, + strict: boolean +): boolean | ReturnType; + +declare function relative(modulePath: string, sourceFile: string, settings: ESLintSettings): ResolvedResult['path']; + + +export { fileExistsWithCaseSync, relative }; diff --git a/utils/resolve.js b/utils/resolve.js index 0ed5bdb0c9..05f7b35abf 100644 --- a/utils/resolve.js +++ b/utils/resolve.js @@ -19,16 +19,22 @@ const fileExistsCache = new ModuleCache(); // Polyfill Node's `Module.createRequireFromPath` if not present (added in Node v10.12.0) // Use `Module.createRequire` if available (added in Node v12.2.0) -const createRequire = Module.createRequire || Module.createRequireFromPath || function (filename) { - const mod = new Module(filename, null); - mod.filename = filename; - mod.paths = Module._nodeModulePaths(path.dirname(filename)); - - mod._compile(`module.exports = require;`, filename); - - return mod.exports; -}; - +const createRequire = Module.createRequire + // @ts-expect-error this only exists in older node + || Module.createRequireFromPath + || /** @type {(filename: string) => unknown} */ function (filename) { + const mod = new Module(filename, void null); + mod.filename = filename; + // @ts-expect-error _nodeModulePaths is undocumented + mod.paths = Module._nodeModulePaths(path.dirname(filename)); + + // @ts-expect-error _compile is undocumented + mod._compile(`module.exports = require;`, filename); + + return mod.exports; + }; + +/** @type {(target: T, sourceFile?: string | null | undefined) => undefined | ReturnType} */ function tryRequire(target, sourceFile) { let resolved; try { @@ -52,6 +58,7 @@ function tryRequire(target, sourceFile) { } // https://stackoverflow.com/a/27382838 +/** @type {import('./resolve').fileExistsWithCaseSync} */ exports.fileExistsWithCaseSync = function fileExistsWithCaseSync(filepath, cacheSettings, strict) { // don't care if the FS is case-sensitive if (CASE_SENSITIVE_FS) { return true; } @@ -80,12 +87,10 @@ exports.fileExistsWithCaseSync = function fileExistsWithCaseSync(filepath, cache return result; }; -function relative(modulePath, sourceFile, settings) { - return fullResolve(modulePath, sourceFile, settings).path; -} - +/** @type {import('./types').ESLintSettings | null} */ let prevSettings = null; let memoizedHash = ''; +/** @type {(modulePath: string, sourceFile: string, settings: import('./types').ESLintSettings) => import('./resolve').ResolvedResult} */ function fullResolve(modulePath, sourceFile, settings) { // check if this is a bonus core module const coreSet = new Set(settings['import/core-modules']); @@ -105,10 +110,12 @@ function fullResolve(modulePath, sourceFile, settings) { const cachedPath = fileExistsCache.get(cacheKey, cacheSettings); if (cachedPath !== undefined) { return { found: true, path: cachedPath }; } + /** @type {(resolvedPath: string | null) => void} */ function cache(resolvedPath) { fileExistsCache.set(cacheKey, resolvedPath); } + /** @type {(resolver: import('./resolve').Resolver, config: unknown) => import('./resolve').ResolvedResult} */ function withResolver(resolver, config) { if (resolver.interfaceVersion === 2) { return resolver.resolve(modulePath, sourceFile, config); @@ -145,8 +152,14 @@ function fullResolve(modulePath, sourceFile, settings) { // cache(undefined) return { found: false }; } + +/** @type {import('./resolve').relative} */ +function relative(modulePath, sourceFile, settings) { + return fullResolve(modulePath, sourceFile, settings).path; +} exports.relative = relative; +/** @type {>(resolvers: string[] | string | { [k: string]: string }, map: T) => T} */ function resolverReducer(resolvers, map) { if (Array.isArray(resolvers)) { resolvers.forEach((r) => resolverReducer(r, map)); @@ -170,9 +183,12 @@ function resolverReducer(resolvers, map) { throw err; } +/** @type {(sourceFile: string) => string} */ function getBaseDir(sourceFile) { return pkgDir(sourceFile) || process.cwd(); } + +/** @type {(name: string, sourceFile: string) => import('./resolve').Resolver} */ function requireResolver(name, sourceFile) { // Try to resolve package with conventional name const resolver = tryRequire(`eslint-import-resolver-${name}`, sourceFile) @@ -193,23 +209,23 @@ function requireResolver(name, sourceFile) { return resolver; } +/** @type {(resolver: object) => resolver is import('./resolve').Resolver} */ function isResolverValid(resolver) { - if (resolver.interfaceVersion === 2) { - return resolver.resolve && typeof resolver.resolve === 'function'; - } else { - return resolver.resolveImport && typeof resolver.resolveImport === 'function'; + if ('interfaceVersion' in resolver && resolver.interfaceVersion === 2) { + return 'resolve' in resolver && !!resolver.resolve && typeof resolver.resolve === 'function'; } + return 'resolveImport' in resolver && !!resolver.resolveImport && typeof resolver.resolveImport === 'function'; } +/** @type {Set} */ const erroredContexts = new Set(); /** * Given - * @param {string} p - module path - * @param {object} context - ESLint context - * @return {string} - the full module filesystem path; - * null if package is core; - * undefined if not found + * @param p - module path + * @param context - ESLint context + * @return - the full module filesystem path; null if package is core; undefined if not found + * @type {import('./resolve').default} */ function resolve(p, context) { try { @@ -218,8 +234,11 @@ function resolve(p, context) { if (!erroredContexts.has(context)) { // The `err.stack` string starts with `err.name` followed by colon and `err.message`. // We're filtering out the default `err.name` because it adds little value to the message. + // @ts-expect-error this might be an Error let errMessage = err.message; + // @ts-expect-error this might be an Error if (err.name !== ERROR_NAME && err.stack) { + // @ts-expect-error this might be an Error errMessage = err.stack.replace(/^Error: /, ''); } context.report({ diff --git a/utils/tsconfig.json b/utils/tsconfig.json new file mode 100644 index 0000000000..4d1c86efdf --- /dev/null +++ b/utils/tsconfig.json @@ -0,0 +1,49 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + + /* Language and Environment */ + "target": "ES2017", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": ["types"], /* Specify multiple folders that act like './node_modules/@types'. */ + "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + + /* JavaScript Support */ + "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + "maxNodeModuleJsDepth": 0, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + "declarationMap": true, /* Create sourcemaps for d.ts files. */ + "noEmit": true, /* Disable emitting files from a compilation. */ + + /* Interop Constraints */ + "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + + /* Completeness */ + //"skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "exclude": [ + "coverage" + ] +} diff --git a/utils/types.d.ts b/utils/types.d.ts new file mode 100644 index 0000000000..e0c4f5749d --- /dev/null +++ b/utils/types.d.ts @@ -0,0 +1,9 @@ +import type { Rule } from 'eslint'; + +export type Extension = `.${string}`; + +export type ESLintSettings = NonNullable & { + 'import/extensions'?: Extension[]; + 'import/parsers'?: { [k: string]: Extension[] }; + 'import/cache'?: { lifetime: number | '∞' | 'Infinity' }; +}; diff --git a/utils/unambiguous.d.ts b/utils/unambiguous.d.ts new file mode 100644 index 0000000000..1679224189 --- /dev/null +++ b/utils/unambiguous.d.ts @@ -0,0 +1,7 @@ +import type { AST } from 'eslint'; + +declare function isModule(ast: AST.Program): boolean; + +declare function test(content: string): boolean; + +export { isModule, test } diff --git a/utils/unambiguous.js b/utils/unambiguous.js index 24cb123157..20aabd1bd4 100644 --- a/utils/unambiguous.js +++ b/utils/unambiguous.js @@ -11,7 +11,7 @@ const pattern = /(^|;)\s*(export|import)((\s+\w)|(\s*[{*=]))|import\(/m; * * Not perfect, just a fast way to disqualify large non-ES6 modules and * avoid a parse. - * @type {RegExp} + * @type {import('./unambiguous').test} */ exports.test = function isMaybeUnambiguousModule(content) { return pattern.test(content); @@ -22,8 +22,7 @@ const unambiguousNodeType = /^(?:(?:Exp|Imp)ort.*Declaration|TSExportAssignment) /** * Given an AST, return true if the AST unambiguously represents a module. - * @param {Program node} ast - * @return {Boolean} + * @type {import('./unambiguous').isModule} */ exports.isModule = function isUnambiguousModule(ast) { return ast.body && ast.body.some((node) => unambiguousNodeType.test(node.type)); diff --git a/utils/visit.d.ts b/utils/visit.d.ts new file mode 100644 index 0000000000..50559aaab0 --- /dev/null +++ b/utils/visit.d.ts @@ -0,0 +1,9 @@ +import type { Node } from 'estree'; + +declare function visit( + node: Node, + keys: { [k in Node['type']]?: (keyof Node)[] }, + visitorSpec: { [k in Node['type'] | `${Node['type']}:Exit`]?: Function } +): void; + +export default visit; diff --git a/utils/visit.js b/utils/visit.js index 6178faeaa0..dd0c6248da 100644 --- a/utils/visit.js +++ b/utils/visit.js @@ -2,24 +2,29 @@ exports.__esModule = true; +/** @type {import('./visit').default} */ exports.default = function visit(node, keys, visitorSpec) { if (!node || !keys) { return; } const type = node.type; - if (typeof visitorSpec[type] === 'function') { - visitorSpec[type](node); + const visitor = visitorSpec[type]; + if (typeof visitor === 'function') { + visitor(node); } const childFields = keys[type]; if (!childFields) { return; } childFields.forEach((fieldName) => { + // @ts-expect-error TS sucks with concat [].concat(node[fieldName]).forEach((item) => { visit(item, keys, visitorSpec); }); }); - if (typeof visitorSpec[`${type}:Exit`] === 'function') { - visitorSpec[`${type}:Exit`](node); + + const exit = visitorSpec[`${type}:Exit`]; + if (typeof exit === 'function') { + exit(node); } }; From df751e0d004aacc34f975477163fb221485a85f6 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Mon, 26 Feb 2024 16:21:46 -0800 Subject: [PATCH 10/50] [utils] v2.8.1 --- utils/CHANGELOG.md | 2 ++ utils/package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/utils/CHANGELOG.md b/utils/CHANGELOG.md index a0aa43da75..b1344b9e0d 100644 --- a/utils/CHANGELOG.md +++ b/utils/CHANGELOG.md @@ -5,6 +5,8 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ## Unreleased +## v2.8.1 - 2024-02-26 + ### Fixed - `parse`: also delete `parserOptions.EXPERIMENTAL_useProjectService` ([#2963], thanks [@JoshuaKGoldberg]) diff --git a/utils/package.json b/utils/package.json index 04c338b1a8..275e364dca 100644 --- a/utils/package.json +++ b/utils/package.json @@ -1,6 +1,6 @@ { "name": "eslint-module-utils", - "version": "2.8.0", + "version": "2.8.1", "description": "Core utilities to support eslint-plugin-import and other module-related plugins.", "engines": { "node": ">=4" From b87746016dbb1aab7d5ec9331a39ccdabedfb1b7 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Sun, 10 Mar 2024 22:50:46 -0700 Subject: [PATCH 11/50] [utils] [types] use shared config --- utils/CHANGELOG.md | 3 +++ utils/package.json | 1 + utils/tsconfig.json | 56 ++++++++------------------------------------- 3 files changed, 13 insertions(+), 47 deletions(-) diff --git a/utils/CHANGELOG.md b/utils/CHANGELOG.md index b1344b9e0d..3e2f5a8997 100644 --- a/utils/CHANGELOG.md +++ b/utils/CHANGELOG.md @@ -5,6 +5,9 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ## Unreleased +### Changed +- [types] use shared config (thanks [@ljharb]) + ## v2.8.1 - 2024-02-26 ### Fixed diff --git a/utils/package.json b/utils/package.json index 275e364dca..df4871790b 100644 --- a/utils/package.json +++ b/utils/package.json @@ -30,6 +30,7 @@ "debug": "^3.2.7" }, "devDependencies": { + "@ljharb/tsconfig": "^0.2.0", "@types/debug": "^4.1.12", "@types/eslint": "^8.56.3", "@types/node": "^20.11.20", diff --git a/utils/tsconfig.json b/utils/tsconfig.json index 4d1c86efdf..9e6fbc5cc1 100644 --- a/utils/tsconfig.json +++ b/utils/tsconfig.json @@ -1,49 +1,11 @@ { - "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ - - /* Projects */ - - /* Language and Environment */ - "target": "ES2017", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - - /* Modules */ - "module": "commonjs", /* Specify what module code is generated. */ - "rootDir": "./", /* Specify the root folder within your source files. */ - "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": ["types"], /* Specify multiple folders that act like './node_modules/@types'. */ - "resolveJsonModule": true, /* Enable importing .json files. */ - // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ - - /* JavaScript Support */ - "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - "maxNodeModuleJsDepth": 0, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - - /* Emit */ - "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - "declarationMap": true, /* Create sourcemaps for d.ts files. */ - "noEmit": true, /* Disable emitting files from a compilation. */ - - /* Interop Constraints */ - "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ - - /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - - /* Completeness */ - //"skipLibCheck": true /* Skip type checking all .d.ts files. */ - }, - "exclude": [ - "coverage" - ] + "extends": "@ljharb/tsconfig", + "compilerOptions": { + "target": "ES2017", + "moduleResolution": "node", + "maxNodeModuleJsDepth": 0, + }, + "exclude": [ + "coverage", + ], } From d5ab2ccf4da82f6bc7a7daa586cea3fa1171d527 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Sun, 10 Mar 2024 22:56:37 -0700 Subject: [PATCH 12/50] [Docs] run `npm run update:eslint-docs --- README.md | 2 +- docs/rules/no-empty-named-blocks.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1baa0069b3..d6f107d1c9 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a ⌨️ Set in the `typescript` configuration.\ 🚸 Set in the `warnings` configuration.\ 🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\ -💡 Manually fixable by [editor suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).\ +💡 Manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).\ ❌ Deprecated. ### Helpful warnings diff --git a/docs/rules/no-empty-named-blocks.md b/docs/rules/no-empty-named-blocks.md index 85821d8afe..ad83c535f8 100644 --- a/docs/rules/no-empty-named-blocks.md +++ b/docs/rules/no-empty-named-blocks.md @@ -1,6 +1,6 @@ # import/no-empty-named-blocks -🔧💡 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix) and manually fixable by [editor suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions). +🔧💡 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix) and manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions). From 51185dd347a7c83904743433592b2f74d73e709d Mon Sep 17 00:00:00 2001 From: Ivan Rubinson Date: Wed, 13 Mar 2024 18:32:47 +0200 Subject: [PATCH 13/50] [Refactor] `ExportMap`: make procedures static instead of monkeypatching exportmap --- CHANGELOG.md | 2 + src/ExportMap.js | 908 +++++++++++++++++++++++------------------------ 2 files changed, 456 insertions(+), 454 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06cdb922e5..9552774dce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ### Changed - [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]) ## [2.29.1] - 2023-12-14 @@ -1108,6 +1109,7 @@ for info on changes for earlier releases. [`memo-parser`]: ./memo-parser/README.md +[#2982]: https://github.com/import-js/eslint-plugin-import/pull/2982 [#2944]: https://github.com/import-js/eslint-plugin-import/pull/2944 [#2942]: https://github.com/import-js/eslint-plugin-import/pull/2942 [#2919]: https://github.com/import-js/eslint-plugin-import/pull/2919 diff --git a/src/ExportMap.js b/src/ExportMap.js index f61d3c170a..9ee65a504f 100644 --- a/src/ExportMap.js +++ b/src/ExportMap.js @@ -26,6 +26,97 @@ const log = debug('eslint-plugin-import:ExportMap'); const exportCache = new Map(); const tsconfigCache = new Map(); +/** + * parse docs from the first node that has leading comments + */ +function captureDoc(source, docStyleParsers, ...nodes) { + const metadata = {}; + + // 'some' short-circuits on first 'true' + nodes.some((n) => { + try { + + let leadingComments; + + // n.leadingComments is legacy `attachComments` behavior + if ('leadingComments' in n) { + leadingComments = n.leadingComments; + } else if (n.range) { + leadingComments = source.getCommentsBefore(n); + } + + if (!leadingComments || leadingComments.length === 0) { return false; } + + for (const name in docStyleParsers) { + const doc = docStyleParsers[name](leadingComments); + if (doc) { + metadata.doc = doc; + } + } + + return true; + } catch (err) { + return false; + } + }); + + return metadata; +} + +/** + * parse JSDoc from leading comments + * @param {object[]} comments + * @return {{ doc: object }} + */ +function captureJsDoc(comments) { + let doc; + + // capture XSDoc + comments.forEach((comment) => { + // skip non-block comments + if (comment.type !== 'Block') { return; } + try { + doc = doctrine.parse(comment.value, { unwrap: true }); + } catch (err) { + /* don't care, for now? maybe add to `errors?` */ + } + }); + + return doc; +} + +/** + * parse TomDoc section from comments + */ +function captureTomDoc(comments) { + // collect lines up to first paragraph break + const lines = []; + for (let i = 0; i < comments.length; i++) { + const comment = comments[i]; + if (comment.value.match(/^\s*$/)) { break; } + lines.push(comment.value.trim()); + } + + // return doctrine-like object + const statusMatch = lines.join(' ').match(/^(Public|Internal|Deprecated):\s*(.+)/); + if (statusMatch) { + return { + description: statusMatch[2], + tags: [{ + title: statusMatch[1].toLowerCase(), + description: statusMatch[2], + }], + }; + } +} + +const availableDocStyleParsers = { + jsdoc: captureJsDoc, + tomdoc: captureTomDoc, +}; + +const supportedImportTypes = new Set(['ImportDefaultSpecifier', 'ImportNamespaceSpecifier']); + export default class ExportMap { constructor(path) { this.path = path; @@ -203,534 +294,443 @@ export default class ExportMap { message: `Parse errors in imported module '${declaration.source.value}': ${msg}`, }); } -} -/** - * parse docs from the first node that has leading comments - */ -function captureDoc(source, docStyleParsers, ...nodes) { - const metadata = {}; + static get(source, context) { + const path = resolve(source, context); + if (path == null) { return null; } - // 'some' short-circuits on first 'true' - nodes.some((n) => { - try { + return ExportMap.for(childContext(path, context)); + } - let leadingComments; + static for(context) { + const { path } = context; - // n.leadingComments is legacy `attachComments` behavior - if ('leadingComments' in n) { - leadingComments = n.leadingComments; - } else if (n.range) { - leadingComments = source.getCommentsBefore(n); - } + const cacheKey = context.cacheKey || hashObject(context).digest('hex'); + let exportMap = exportCache.get(cacheKey); - if (!leadingComments || leadingComments.length === 0) { return false; } + // return cached ignore + if (exportMap === null) { return null; } - for (const name in docStyleParsers) { - const doc = docStyleParsers[name](leadingComments); - if (doc) { - metadata.doc = doc; - } + const stats = fs.statSync(path); + if (exportMap != null) { + // date equality check + if (exportMap.mtime - stats.mtime === 0) { + return exportMap; } - - return true; - } catch (err) { - return false; + // future: check content equality? } - }); - - return metadata; -} - -const availableDocStyleParsers = { - jsdoc: captureJsDoc, - tomdoc: captureTomDoc, -}; - -/** - * parse JSDoc from leading comments - * @param {object[]} comments - * @return {{ doc: object }} - */ -function captureJsDoc(comments) { - let doc; - // capture XSDoc - comments.forEach((comment) => { - // skip non-block comments - if (comment.type !== 'Block') { return; } - try { - doc = doctrine.parse(comment.value, { unwrap: true }); - } catch (err) { - /* don't care, for now? maybe add to `errors?` */ + // check valid extensions first + if (!hasValidExtension(path, context)) { + exportCache.set(cacheKey, null); + return null; } - }); - - return doc; -} - -/** - * parse TomDoc section from comments - */ -function captureTomDoc(comments) { - // collect lines up to first paragraph break - const lines = []; - for (let i = 0; i < comments.length; i++) { - const comment = comments[i]; - if (comment.value.match(/^\s*$/)) { break; } - lines.push(comment.value.trim()); - } - - // return doctrine-like object - const statusMatch = lines.join(' ').match(/^(Public|Internal|Deprecated):\s*(.+)/); - if (statusMatch) { - return { - description: statusMatch[2], - tags: [{ - title: statusMatch[1].toLowerCase(), - description: statusMatch[2], - }], - }; - } -} - -const supportedImportTypes = new Set(['ImportDefaultSpecifier', 'ImportNamespaceSpecifier']); - -ExportMap.get = function (source, context) { - const path = resolve(source, context); - if (path == null) { return null; } - - return ExportMap.for(childContext(path, context)); -}; - -ExportMap.for = function (context) { - const { path } = context; - - const cacheKey = context.cacheKey || hashObject(context).digest('hex'); - let exportMap = exportCache.get(cacheKey); - // return cached ignore - if (exportMap === null) { return null; } - - const stats = fs.statSync(path); - if (exportMap != null) { - // date equality check - if (exportMap.mtime - stats.mtime === 0) { - return exportMap; + // check for and cache ignore + if (isIgnored(path, context)) { + log('ignored path due to ignore settings:', path); + exportCache.set(cacheKey, null); + return null; } - // future: check content equality? - } - - // check valid extensions first - if (!hasValidExtension(path, context)) { - exportCache.set(cacheKey, null); - return null; - } - // check for and cache ignore - if (isIgnored(path, context)) { - log('ignored path due to ignore settings:', path); - exportCache.set(cacheKey, null); - return null; - } - - const content = fs.readFileSync(path, { encoding: 'utf8' }); - - // check for and cache unambiguous modules - if (!unambiguous.test(content)) { - log('ignored path due to unambiguous regex:', path); - exportCache.set(cacheKey, null); - return null; - } + const content = fs.readFileSync(path, { encoding: 'utf8' }); - log('cache miss', cacheKey, 'for path', path); - exportMap = ExportMap.parse(path, content, context); + // check for and cache unambiguous modules + if (!unambiguous.test(content)) { + log('ignored path due to unambiguous regex:', path); + exportCache.set(cacheKey, null); + return null; + } - // ambiguous modules return null - if (exportMap == null) { - log('ignored path due to ambiguous parse:', path); - exportCache.set(cacheKey, null); - return null; - } + log('cache miss', cacheKey, 'for path', path); + exportMap = ExportMap.parse(path, content, context); - exportMap.mtime = stats.mtime; + // ambiguous modules return null + if (exportMap == null) { + log('ignored path due to ambiguous parse:', path); + exportCache.set(cacheKey, null); + return null; + } - exportCache.set(cacheKey, exportMap); - return exportMap; -}; + exportMap.mtime = stats.mtime; -ExportMap.parse = function (path, content, context) { - const m = new ExportMap(path); - const isEsModuleInteropTrue = isEsModuleInterop(); - - let ast; - let visitorKeys; - try { - const result = parse(path, content, context); - ast = result.ast; - visitorKeys = result.visitorKeys; - } catch (err) { - m.errors.push(err); - return m; // can't continue + exportCache.set(cacheKey, exportMap); + return exportMap; } - m.visitorKeys = visitorKeys; - - let hasDynamicImports = false; + static parse(path, content, context) { + const m = new ExportMap(path); + const isEsModuleInteropTrue = isEsModuleInterop(); - function processDynamicImport(source) { - hasDynamicImports = true; - if (source.type !== 'Literal') { - return null; - } - const p = remotePath(source.value); - if (p == null) { - return null; + let ast; + let visitorKeys; + try { + const result = parse(path, content, context); + ast = result.ast; + visitorKeys = result.visitorKeys; + } catch (err) { + m.errors.push(err); + return m; // can't continue } - const importedSpecifiers = new Set(); - importedSpecifiers.add('ImportNamespaceSpecifier'); - const getter = thunkFor(p, context); - m.imports.set(p, { - getter, - declarations: new Set([{ - source: { - // capturing actual node reference holds full AST in memory! - value: source.value, - loc: source.loc, - }, - importedSpecifiers, - dynamic: true, - }]), - }); - } - visit(ast, visitorKeys, { - ImportExpression(node) { - processDynamicImport(node.source); - }, - CallExpression(node) { - if (node.callee.type === 'Import') { - processDynamicImport(node.arguments[0]); - } - }, - }); + m.visitorKeys = visitorKeys; - const unambiguouslyESM = unambiguous.isModule(ast); - if (!unambiguouslyESM && !hasDynamicImports) { return null; } + let hasDynamicImports = false; - const docstyle = context.settings && context.settings['import/docstyle'] || ['jsdoc']; - const docStyleParsers = {}; - docstyle.forEach((style) => { - docStyleParsers[style] = availableDocStyleParsers[style]; - }); + function processDynamicImport(source) { + hasDynamicImports = true; + if (source.type !== 'Literal') { + return null; + } + const p = remotePath(source.value); + if (p == null) { + return null; + } + const importedSpecifiers = new Set(); + importedSpecifiers.add('ImportNamespaceSpecifier'); + const getter = thunkFor(p, context); + m.imports.set(p, { + getter, + declarations: new Set([{ + source: { + // capturing actual node reference holds full AST in memory! + value: source.value, + loc: source.loc, + }, + importedSpecifiers, + dynamic: true, + }]), + }); + } - // attempt to collect module doc - if (ast.comments) { - ast.comments.some((c) => { - if (c.type !== 'Block') { return false; } - try { - const doc = doctrine.parse(c.value, { unwrap: true }); - if (doc.tags.some((t) => t.title === 'module')) { - m.doc = doc; - return true; + visit(ast, visitorKeys, { + ImportExpression(node) { + processDynamicImport(node.source); + }, + CallExpression(node) { + if (node.callee.type === 'Import') { + processDynamicImport(node.arguments[0]); } - } catch (err) { /* ignore */ } - return false; + }, }); - } - const namespaces = new Map(); + const unambiguouslyESM = unambiguous.isModule(ast); + if (!unambiguouslyESM && !hasDynamicImports) { return null; } - function remotePath(value) { - return resolve.relative(value, path, context.settings); - } + const docstyle = context.settings && context.settings['import/docstyle'] || ['jsdoc']; + const docStyleParsers = {}; + docstyle.forEach((style) => { + docStyleParsers[style] = availableDocStyleParsers[style]; + }); - function resolveImport(value) { - const rp = remotePath(value); - if (rp == null) { return null; } - return ExportMap.for(childContext(rp, context)); - } + // attempt to collect module doc + if (ast.comments) { + ast.comments.some((c) => { + if (c.type !== 'Block') { return false; } + try { + const doc = doctrine.parse(c.value, { unwrap: true }); + if (doc.tags.some((t) => t.title === 'module')) { + m.doc = doc; + return true; + } + } catch (err) { /* ignore */ } + return false; + }); + } - function getNamespace(identifier) { - if (!namespaces.has(identifier.name)) { return; } + const namespaces = new Map(); - return function () { - return resolveImport(namespaces.get(identifier.name)); - }; - } + function remotePath(value) { + return resolve.relative(value, path, context.settings); + } - function addNamespace(object, identifier) { - const nsfn = getNamespace(identifier); - if (nsfn) { - Object.defineProperty(object, 'namespace', { get: nsfn }); + function resolveImport(value) { + const rp = remotePath(value); + if (rp == null) { return null; } + return ExportMap.for(childContext(rp, context)); } - return object; - } + function getNamespace(identifier) { + if (!namespaces.has(identifier.name)) { return; } - function processSpecifier(s, n, m) { - const nsource = n.source && n.source.value; - const exportMeta = {}; - let local; - - switch (s.type) { - case 'ExportDefaultSpecifier': - if (!nsource) { return; } - local = 'default'; - break; - case 'ExportNamespaceSpecifier': - m.namespace.set(s.exported.name, Object.defineProperty(exportMeta, 'namespace', { - get() { return resolveImport(nsource); }, - })); - return; - case 'ExportAllDeclaration': - m.namespace.set(s.exported.name || s.exported.value, addNamespace(exportMeta, s.source.value)); - return; - case 'ExportSpecifier': - if (!n.source) { - m.namespace.set(s.exported.name || s.exported.value, addNamespace(exportMeta, s.local)); - return; - } - // else falls through - default: - local = s.local.name; - break; + return function () { + return resolveImport(namespaces.get(identifier.name)); + }; } - // todo: JSDoc - m.reexports.set(s.exported.name, { local, getImport: () => resolveImport(nsource) }); - } - - function captureDependencyWithSpecifiers(n) { - // import type { Foo } (TS and Flow); import typeof { Foo } (Flow) - const declarationIsType = n.importKind === 'type' || n.importKind === 'typeof'; - // import './foo' or import {} from './foo' (both 0 specifiers) is a side effect and - // shouldn't be considered to be just importing types - let specifiersOnlyImportingTypes = n.specifiers.length > 0; - const importedSpecifiers = new Set(); - n.specifiers.forEach((specifier) => { - if (specifier.type === 'ImportSpecifier') { - importedSpecifiers.add(specifier.imported.name || specifier.imported.value); - } else if (supportedImportTypes.has(specifier.type)) { - importedSpecifiers.add(specifier.type); + function addNamespace(object, identifier) { + const nsfn = getNamespace(identifier); + if (nsfn) { + Object.defineProperty(object, 'namespace', { get: nsfn }); } - // import { type Foo } (Flow); import { typeof Foo } (Flow) - specifiersOnlyImportingTypes = specifiersOnlyImportingTypes - && (specifier.importKind === 'type' || specifier.importKind === 'typeof'); - }); - captureDependency(n, declarationIsType || specifiersOnlyImportingTypes, importedSpecifiers); - } + return object; + } - function captureDependency({ source }, isOnlyImportingTypes, importedSpecifiers = new Set()) { - if (source == null) { return null; } + function processSpecifier(s, n, m) { + const nsource = n.source && n.source.value; + const exportMeta = {}; + let local; + + switch (s.type) { + case 'ExportDefaultSpecifier': + if (!nsource) { return; } + local = 'default'; + break; + case 'ExportNamespaceSpecifier': + m.namespace.set(s.exported.name, Object.defineProperty(exportMeta, 'namespace', { + get() { return resolveImport(nsource); }, + })); + return; + case 'ExportAllDeclaration': + m.namespace.set(s.exported.name || s.exported.value, addNamespace(exportMeta, s.source.value)); + return; + case 'ExportSpecifier': + if (!n.source) { + m.namespace.set(s.exported.name || s.exported.value, addNamespace(exportMeta, s.local)); + return; + } + // else falls through + default: + local = s.local.name; + break; + } - const p = remotePath(source.value); - if (p == null) { return null; } + // todo: JSDoc + m.reexports.set(s.exported.name, { local, getImport: () => resolveImport(nsource) }); + } - const declarationMetadata = { - // capturing actual node reference holds full AST in memory! - source: { value: source.value, loc: source.loc }, - isOnlyImportingTypes, - importedSpecifiers, - }; + function captureDependencyWithSpecifiers(n) { + // import type { Foo } (TS and Flow); import typeof { Foo } (Flow) + const declarationIsType = n.importKind === 'type' || n.importKind === 'typeof'; + // import './foo' or import {} from './foo' (both 0 specifiers) is a side effect and + // shouldn't be considered to be just importing types + let specifiersOnlyImportingTypes = n.specifiers.length > 0; + const importedSpecifiers = new Set(); + n.specifiers.forEach((specifier) => { + if (specifier.type === 'ImportSpecifier') { + importedSpecifiers.add(specifier.imported.name || specifier.imported.value); + } else if (supportedImportTypes.has(specifier.type)) { + importedSpecifiers.add(specifier.type); + } - const existing = m.imports.get(p); - if (existing != null) { - existing.declarations.add(declarationMetadata); - return existing.getter; + // import { type Foo } (Flow); import { typeof Foo } (Flow) + specifiersOnlyImportingTypes = specifiersOnlyImportingTypes + && (specifier.importKind === 'type' || specifier.importKind === 'typeof'); + }); + captureDependency(n, declarationIsType || specifiersOnlyImportingTypes, importedSpecifiers); } - const getter = thunkFor(p, context); - m.imports.set(p, { getter, declarations: new Set([declarationMetadata]) }); - return getter; - } + function captureDependency({ source }, isOnlyImportingTypes, importedSpecifiers = new Set()) { + if (source == null) { return null; } - const source = makeSourceCode(content, ast); + const p = remotePath(source.value); + if (p == null) { return null; } - function readTsConfig(context) { - const tsconfigInfo = tsConfigLoader({ - cwd: context.parserOptions && context.parserOptions.tsconfigRootDir || process.cwd(), - getEnv: (key) => process.env[key], - }); - try { - if (tsconfigInfo.tsConfigPath !== undefined) { - // Projects not using TypeScript won't have `typescript` installed. - if (!ts) { ts = require('typescript'); } // eslint-disable-line import/no-extraneous-dependencies - - const configFile = ts.readConfigFile(tsconfigInfo.tsConfigPath, ts.sys.readFile); - return ts.parseJsonConfigFileContent( - configFile.config, - ts.sys, - dirname(tsconfigInfo.tsConfigPath), - ); - } - } catch (e) { - // Catch any errors - } + const declarationMetadata = { + // capturing actual node reference holds full AST in memory! + source: { value: source.value, loc: source.loc }, + isOnlyImportingTypes, + importedSpecifiers, + }; - return null; - } + const existing = m.imports.get(p); + if (existing != null) { + existing.declarations.add(declarationMetadata); + return existing.getter; + } - function isEsModuleInterop() { - const cacheKey = hashObject({ - tsconfigRootDir: context.parserOptions && context.parserOptions.tsconfigRootDir, - }).digest('hex'); - let tsConfig = tsconfigCache.get(cacheKey); - if (typeof tsConfig === 'undefined') { - tsConfig = readTsConfig(context); - tsconfigCache.set(cacheKey, tsConfig); + const getter = thunkFor(p, context); + m.imports.set(p, { getter, declarations: new Set([declarationMetadata]) }); + return getter; } - return tsConfig && tsConfig.options ? tsConfig.options.esModuleInterop : false; - } + const source = makeSourceCode(content, ast); - ast.body.forEach(function (n) { - if (n.type === 'ExportDefaultDeclaration') { - const exportMeta = captureDoc(source, docStyleParsers, n); - if (n.declaration.type === 'Identifier') { - addNamespace(exportMeta, n.declaration); + function readTsConfig(context) { + const tsconfigInfo = tsConfigLoader({ + cwd: context.parserOptions && context.parserOptions.tsconfigRootDir || process.cwd(), + getEnv: (key) => process.env[key], + }); + try { + if (tsconfigInfo.tsConfigPath !== undefined) { + // Projects not using TypeScript won't have `typescript` installed. + if (!ts) { ts = require('typescript'); } // eslint-disable-line import/no-extraneous-dependencies + + const configFile = ts.readConfigFile(tsconfigInfo.tsConfigPath, ts.sys.readFile); + return ts.parseJsonConfigFileContent( + configFile.config, + ts.sys, + dirname(tsconfigInfo.tsConfigPath), + ); + } + } catch (e) { + // Catch any errors } - m.namespace.set('default', exportMeta); - return; - } - if (n.type === 'ExportAllDeclaration') { - const getter = captureDependency(n, n.exportKind === 'type'); - if (getter) { m.dependencies.add(getter); } - if (n.exported) { - processSpecifier(n, n.exported, m); - } - return; + return null; } - // capture namespaces in case of later export - if (n.type === 'ImportDeclaration') { - captureDependencyWithSpecifiers(n); - - const ns = n.specifiers.find((s) => s.type === 'ImportNamespaceSpecifier'); - if (ns) { - namespaces.set(ns.local.name, n.source.value); + function isEsModuleInterop() { + const cacheKey = hashObject({ + tsconfigRootDir: context.parserOptions && context.parserOptions.tsconfigRootDir, + }).digest('hex'); + let tsConfig = tsconfigCache.get(cacheKey); + if (typeof tsConfig === 'undefined') { + tsConfig = readTsConfig(context); + tsconfigCache.set(cacheKey, tsConfig); } - return; + + return tsConfig && tsConfig.options ? tsConfig.options.esModuleInterop : false; } - if (n.type === 'ExportNamedDeclaration') { - captureDependencyWithSpecifiers(n); - - // capture declaration - if (n.declaration != null) { - switch (n.declaration.type) { - case 'FunctionDeclaration': - case 'ClassDeclaration': - case 'TypeAlias': // flowtype with babel-eslint parser - case 'InterfaceDeclaration': - case 'DeclareFunction': - case 'TSDeclareFunction': - case 'TSEnumDeclaration': - case 'TSTypeAliasDeclaration': - case 'TSInterfaceDeclaration': - case 'TSAbstractClassDeclaration': - case 'TSModuleDeclaration': - m.namespace.set(n.declaration.id.name, captureDoc(source, docStyleParsers, n)); - break; - case 'VariableDeclaration': - n.declaration.declarations.forEach((d) => { - recursivePatternCapture( - d.id, - (id) => m.namespace.set(id.name, captureDoc(source, docStyleParsers, d, n)), - ); - }); - break; - default: + ast.body.forEach(function (n) { + if (n.type === 'ExportDefaultDeclaration') { + const exportMeta = captureDoc(source, docStyleParsers, n); + if (n.declaration.type === 'Identifier') { + addNamespace(exportMeta, n.declaration); } + m.namespace.set('default', exportMeta); + return; } - n.specifiers.forEach((s) => processSpecifier(s, n, m)); - } + if (n.type === 'ExportAllDeclaration') { + const getter = captureDependency(n, n.exportKind === 'type'); + if (getter) { m.dependencies.add(getter); } + if (n.exported) { + processSpecifier(n, n.exported, m); + } + return; + } - const exports = ['TSExportAssignment']; - if (isEsModuleInteropTrue) { - exports.push('TSNamespaceExportDeclaration'); - } + // capture namespaces in case of later export + if (n.type === 'ImportDeclaration') { + captureDependencyWithSpecifiers(n); - // This doesn't declare anything, but changes what's being exported. - if (includes(exports, n.type)) { - const exportedName = n.type === 'TSNamespaceExportDeclaration' - ? (n.id || n.name).name - : n.expression && n.expression.name || n.expression.id && n.expression.id.name || null; - const declTypes = [ - 'VariableDeclaration', - 'ClassDeclaration', - 'TSDeclareFunction', - 'TSEnumDeclaration', - 'TSTypeAliasDeclaration', - 'TSInterfaceDeclaration', - 'TSAbstractClassDeclaration', - 'TSModuleDeclaration', - ]; - const exportedDecls = ast.body.filter(({ type, id, declarations }) => includes(declTypes, type) && ( - id && id.name === exportedName || declarations && declarations.find((d) => d.id.name === exportedName) - )); - if (exportedDecls.length === 0) { - // Export is not referencing any local declaration, must be re-exporting - m.namespace.set('default', captureDoc(source, docStyleParsers, n)); + const ns = n.specifiers.find((s) => s.type === 'ImportNamespaceSpecifier'); + if (ns) { + namespaces.set(ns.local.name, n.source.value); + } return; } - if ( - isEsModuleInteropTrue // esModuleInterop is on in tsconfig - && !m.namespace.has('default') // and default isn't added already - ) { - m.namespace.set('default', {}); // add default export - } - exportedDecls.forEach((decl) => { - if (decl.type === 'TSModuleDeclaration') { - if (decl.body && decl.body.type === 'TSModuleDeclaration') { - m.namespace.set(decl.body.id.name, captureDoc(source, docStyleParsers, decl.body)); - } else if (decl.body && decl.body.body) { - decl.body.body.forEach((moduleBlockNode) => { - // Export-assignment exports all members in the namespace, - // explicitly exported or not. - const namespaceDecl = moduleBlockNode.type === 'ExportNamedDeclaration' - ? moduleBlockNode.declaration - : moduleBlockNode; - - if (!namespaceDecl) { - // TypeScript can check this for us; we needn't - } else if (namespaceDecl.type === 'VariableDeclaration') { - namespaceDecl.declarations.forEach((d) => recursivePatternCapture(d.id, (id) => m.namespace.set( - id.name, - captureDoc(source, docStyleParsers, decl, namespaceDecl, moduleBlockNode), - )), + + if (n.type === 'ExportNamedDeclaration') { + captureDependencyWithSpecifiers(n); + + // capture declaration + if (n.declaration != null) { + switch (n.declaration.type) { + case 'FunctionDeclaration': + case 'ClassDeclaration': + case 'TypeAlias': // flowtype with babel-eslint parser + case 'InterfaceDeclaration': + case 'DeclareFunction': + case 'TSDeclareFunction': + case 'TSEnumDeclaration': + case 'TSTypeAliasDeclaration': + case 'TSInterfaceDeclaration': + case 'TSAbstractClassDeclaration': + case 'TSModuleDeclaration': + m.namespace.set(n.declaration.id.name, captureDoc(source, docStyleParsers, n)); + break; + case 'VariableDeclaration': + n.declaration.declarations.forEach((d) => { + recursivePatternCapture( + d.id, + (id) => m.namespace.set(id.name, captureDoc(source, docStyleParsers, d, n)), ); - } else { - m.namespace.set( - namespaceDecl.id.name, - captureDoc(source, docStyleParsers, moduleBlockNode)); - } - }); + }); + break; + default: } - } else { - // Export as default - m.namespace.set('default', captureDoc(source, docStyleParsers, decl)); } - }); - } - }); - if ( - isEsModuleInteropTrue // esModuleInterop is on in tsconfig - && m.namespace.size > 0 // anything is exported - && !m.namespace.has('default') // and default isn't added already - ) { - m.namespace.set('default', {}); // add default export - } + n.specifiers.forEach((s) => processSpecifier(s, n, m)); + } + + const exports = ['TSExportAssignment']; + if (isEsModuleInteropTrue) { + exports.push('TSNamespaceExportDeclaration'); + } + + // This doesn't declare anything, but changes what's being exported. + if (includes(exports, n.type)) { + const exportedName = n.type === 'TSNamespaceExportDeclaration' + ? (n.id || n.name).name + : n.expression && n.expression.name || n.expression.id && n.expression.id.name || null; + const declTypes = [ + 'VariableDeclaration', + 'ClassDeclaration', + 'TSDeclareFunction', + 'TSEnumDeclaration', + 'TSTypeAliasDeclaration', + 'TSInterfaceDeclaration', + 'TSAbstractClassDeclaration', + 'TSModuleDeclaration', + ]; + const exportedDecls = ast.body.filter(({ type, id, declarations }) => includes(declTypes, type) && ( + id && id.name === exportedName || declarations && declarations.find((d) => d.id.name === exportedName) + )); + if (exportedDecls.length === 0) { + // Export is not referencing any local declaration, must be re-exporting + m.namespace.set('default', captureDoc(source, docStyleParsers, n)); + return; + } + if ( + isEsModuleInteropTrue // esModuleInterop is on in tsconfig + && !m.namespace.has('default') // and default isn't added already + ) { + m.namespace.set('default', {}); // add default export + } + exportedDecls.forEach((decl) => { + if (decl.type === 'TSModuleDeclaration') { + if (decl.body && decl.body.type === 'TSModuleDeclaration') { + m.namespace.set(decl.body.id.name, captureDoc(source, docStyleParsers, decl.body)); + } else if (decl.body && decl.body.body) { + decl.body.body.forEach((moduleBlockNode) => { + // Export-assignment exports all members in the namespace, + // explicitly exported or not. + const namespaceDecl = moduleBlockNode.type === 'ExportNamedDeclaration' + ? moduleBlockNode.declaration + : moduleBlockNode; + + if (!namespaceDecl) { + // TypeScript can check this for us; we needn't + } else if (namespaceDecl.type === 'VariableDeclaration') { + namespaceDecl.declarations.forEach((d) => recursivePatternCapture(d.id, (id) => m.namespace.set( + id.name, + captureDoc(source, docStyleParsers, decl, namespaceDecl, moduleBlockNode), + )), + ); + } else { + m.namespace.set( + namespaceDecl.id.name, + captureDoc(source, docStyleParsers, moduleBlockNode)); + } + }); + } + } else { + // Export as default + m.namespace.set('default', captureDoc(source, docStyleParsers, decl)); + } + }); + } + }); - if (unambiguouslyESM) { - m.parseGoal = 'Module'; + if ( + isEsModuleInteropTrue // esModuleInterop is on in tsconfig + && m.namespace.size > 0 // anything is exported + && !m.namespace.has('default') // and default isn't added already + ) { + m.namespace.set('default', {}); // add default export + } + + if (unambiguouslyESM) { + m.parseGoal = 'Module'; + } + return m; } - return m; -}; +} /** * The creation of this closure is isolated from other scopes From 2d38b3367e90df2eabd6c6aad0a05d6a1fc96734 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Wed, 13 Mar 2024 14:23:09 -0700 Subject: [PATCH 14/50] [resolvers] [*] [refactor] avoid hoisting --- resolvers/node/index.js | 40 +-- resolvers/webpack/index.js | 588 ++++++++++++++++++------------------- 2 files changed, 314 insertions(+), 314 deletions(-) diff --git a/resolvers/node/index.js b/resolvers/node/index.js index 7f207fbf31..9e0e753cc7 100644 --- a/resolvers/node/index.js +++ b/resolvers/node/index.js @@ -8,26 +8,6 @@ const log = require('debug')('eslint-plugin-import:resolver:node'); exports.interfaceVersion = 2; -exports.resolve = function (source, file, config) { - log('Resolving:', source, 'from:', file); - let resolvedPath; - - if (isCoreModule(source)) { - log('resolved to core'); - return { found: true, path: null }; - } - - try { - const cachedFilter = function (pkg, dir) { return packageFilter(pkg, dir, config); }; - resolvedPath = resolve(source, opts(file, config, cachedFilter)); - log('Resolved to:', resolvedPath); - return { found: true, path: resolvedPath }; - } catch (err) { - log('resolve threw error:', err); - return { found: false }; - } -}; - function opts(file, config, packageFilter) { return Object.assign({ // more closely matches Node (#333) // plus 'mjs' for native modules! (#939) @@ -64,3 +44,23 @@ function packageFilter(pkg, dir, config) { } return pkg; } + +exports.resolve = function (source, file, config) { + log('Resolving:', source, 'from:', file); + let resolvedPath; + + if (isCoreModule(source)) { + log('resolved to core'); + return { found: true, path: null }; + } + + try { + const cachedFilter = function (pkg, dir) { return packageFilter(pkg, dir, config); }; + resolvedPath = resolve(source, opts(file, config, cachedFilter)); + log('Resolved to:', resolvedPath); + return { found: true, path: resolvedPath }; + } catch (err) { + log('resolve threw error:', err); + return { found: false }; + } +}; diff --git a/resolvers/webpack/index.js b/resolvers/webpack/index.js index 3ca2874dd8..da16eda593 100644 --- a/resolvers/webpack/index.js +++ b/resolvers/webpack/index.js @@ -16,203 +16,133 @@ const log = require('debug')('eslint-plugin-import:resolver:webpack'); exports.interfaceVersion = 2; -/** - * Find the full path to 'source', given 'file' as a full reference path. - * - * resolveImport('./foo', '/Users/ben/bar.js') => '/Users/ben/foo.js' - * @param {string} source - the module to resolve; i.e './some-module' - * @param {string} file - the importing file's full path; i.e. '/usr/local/bin/file.js' - * @param {object} settings - the webpack config file name, as well as cwd - * @example - * options: { - * // Path to the webpack config - * config: 'webpack.config.js', - * // Path to be used to determine where to resolve webpack from - * // (may differ from the cwd in some cases) - * cwd: process.cwd() - * } - * @return {string?} the resolved path to source, undefined if not resolved, or null - * if resolved to a non-FS resource (i.e. script tag at page load) - */ -exports.resolve = function (source, file, settings) { - - // strip loaders - const finalBang = source.lastIndexOf('!'); - if (finalBang >= 0) { - source = source.slice(finalBang + 1); - } - - // strip resource query - const finalQuestionMark = source.lastIndexOf('?'); - if (finalQuestionMark >= 0) { - source = source.slice(0, finalQuestionMark); - } - - let webpackConfig; - - const _configPath = settings && settings.config; - /** - * Attempt to set the current working directory. - * If none is passed, default to the `cwd` where the config is located. - */ - const cwd = settings && settings.cwd; - const configIndex = settings && settings['config-index']; - const env = settings && settings.env; - const argv = settings && typeof settings.argv !== 'undefined' ? settings.argv : {}; - let packageDir; - - let configPath = typeof _configPath === 'string' && _configPath.startsWith('.') - ? path.resolve(_configPath) - : _configPath; - - log('Config path from settings:', configPath); - - // see if we've got a config path, a config object, an array of config objects or a config function - if (!configPath || typeof configPath === 'string') { - - // see if we've got an absolute path - if (!configPath || !path.isAbsolute(configPath)) { - // if not, find ancestral package.json and use its directory as base for the path - packageDir = findRoot(path.resolve(file)); - if (!packageDir) { throw new Error('package not found above ' + file); } - } - - configPath = findConfigPath(configPath, packageDir); - - log('Config path resolved to:', configPath); - if (configPath) { - try { - webpackConfig = require(configPath); - } catch (e) { - console.log('Error resolving webpackConfig', e); - throw e; - } +function registerCompiler(moduleDescriptor) { + if (moduleDescriptor) { + if (typeof moduleDescriptor === 'string') { + require(moduleDescriptor); + } else if (!Array.isArray(moduleDescriptor)) { + moduleDescriptor.register(require(moduleDescriptor.module)); } else { - log('No config path found relative to', file, '; using {}'); - webpackConfig = {}; - } - - if (webpackConfig && webpackConfig.default) { - log('Using ES6 module "default" key instead of module.exports.'); - webpackConfig = webpackConfig.default; + for (let i = 0; i < moduleDescriptor.length; i++) { + try { + registerCompiler(moduleDescriptor[i]); + break; + } catch (e) { + log('Failed to register compiler for moduleDescriptor[]:', i, moduleDescriptor); + } + } } - - } else { - webpackConfig = configPath; - configPath = null; } +} - if (typeof webpackConfig === 'function') { - webpackConfig = webpackConfig(env, argv); - } +function findConfigPath(configPath, packageDir) { + const extensions = Object.keys(interpret.extensions).sort(function (a, b) { + return a === '.js' ? -1 : b === '.js' ? 1 : a.length - b.length; + }); + let extension; - if (Array.isArray(webpackConfig)) { - webpackConfig = webpackConfig.map((cfg) => { - if (typeof cfg === 'function') { - return cfg(env, argv); + if (configPath) { + // extensions is not reused below, so safe to mutate it here. + extensions.reverse(); + extensions.forEach(function (maybeExtension) { + if (extension) { + return; } - return cfg; + if (configPath.substr(-maybeExtension.length) === maybeExtension) { + extension = maybeExtension; + } }); - if (typeof configIndex !== 'undefined' && webpackConfig.length > configIndex) { - webpackConfig = webpackConfig[configIndex]; - } else { - webpackConfig = find(webpackConfig, function findFirstWithResolve(config) { - return !!config.resolve; - }); + // see if we've got an absolute path + if (!path.isAbsolute(configPath)) { + configPath = path.join(packageDir, configPath); } - } - - if (typeof webpackConfig.then === 'function') { - webpackConfig = {}; + } else { + extensions.forEach(function (maybeExtension) { + if (extension) { + return; + } - console.warn('Webpack config returns a `Promise`; that signature is not supported at the moment. Using empty object instead.'); + const maybePath = path.resolve( + path.join(packageDir, 'webpack.config' + maybeExtension) + ); + if (fs.existsSync(maybePath)) { + configPath = maybePath; + extension = maybeExtension; + } + }); } - if (webpackConfig == null) { - webpackConfig = {}; - - console.warn('No webpack configuration with a "resolve" field found. Using empty object instead.'); - } + registerCompiler(interpret.extensions[extension]); + return configPath; +} - log('Using config: ', webpackConfig); +function findExternal(source, externals, context, resolveSync) { + if (!externals) { return false; } - const resolveSync = getResolveSync(configPath, webpackConfig, cwd); + // string match + if (typeof externals === 'string') { return source === externals; } - // externals - if (findExternal(source, webpackConfig.externals, path.dirname(file), resolveSync)) { - return { found: true, path: null }; + // array: recurse + if (Array.isArray(externals)) { + return externals.some(function (e) { return findExternal(source, e, context, resolveSync); }); } - // otherwise, resolve "normally" - - try { - return { found: true, path: resolveSync(path.dirname(file), source) }; - } catch (err) { - if (isCore(source)) { - return { found: true, path: null }; - } - - log('Error during module resolution:', err); - return { found: false }; + if (isRegex(externals)) { + return externals.test(source); } -}; -const MAX_CACHE = 10; -const _cache = []; -function getResolveSync(configPath, webpackConfig, cwd) { - const cacheKey = { configPath, webpackConfig }; - let cached = find(_cache, function (entry) { return isEqual(entry.key, cacheKey); }); - if (!cached) { - cached = { - key: cacheKey, - value: createResolveSync(configPath, webpackConfig, cwd), + if (typeof externals === 'function') { + let functionExternalFound = false; + const callback = function (err, value) { + if (err) { + functionExternalFound = false; + } else { + functionExternalFound = findExternal(source, value, context, resolveSync); + } }; - // put in front and pop last item - if (_cache.unshift(cached) > MAX_CACHE) { - _cache.pop(); + // - for prior webpack 5, 'externals function' uses 3 arguments + // - for webpack 5, the count of arguments is less than 3 + if (externals.length === 3) { + externals.call(null, context, source, callback); + } else { + const ctx = { + context, + request: source, + contextInfo: { + issuer: '', + issuerLayer: null, + compiler: '', + }, + getResolve: () => (resolveContext, requestToResolve, cb) => { + if (cb) { + try { + cb(null, resolveSync(resolveContext, requestToResolve)); + } catch (e) { + cb(e); + } + } else { + log('getResolve without callback not supported'); + return Promise.reject(new Error('Not supported')); + } + }, + }; + const result = externals.call(null, ctx, callback); + // todo handling Promise object (using synchronous-promise package?) + if (result && typeof result.then === 'function') { + log('Asynchronous functions for externals not supported'); + } } - } - return cached.value; -} - -function createResolveSync(configPath, webpackConfig, cwd) { - let webpackRequire; - let basedir = null; - - if (typeof configPath === 'string') { - // This can be changed via the settings passed in when defining the resolver - basedir = cwd || path.dirname(configPath); - log(`Attempting to load webpack path from ${basedir}`); - } - - try { - // Attempt to resolve webpack from the given `basedir` - const webpackFilename = resolve('webpack', { basedir, preserveSymlinks: false }); - const webpackResolveOpts = { basedir: path.dirname(webpackFilename), preserveSymlinks: false }; - - webpackRequire = function (id) { - return require(resolve(id, webpackResolveOpts)); - }; - } catch (e) { - // Something has gone wrong (or we're in a test). Use our own bundled - // enhanced-resolve. - log('Using bundled enhanced-resolve.'); - webpackRequire = require; + return functionExternalFound; } - const enhancedResolvePackage = webpackRequire('enhanced-resolve/package.json'); - const enhancedResolveVersion = enhancedResolvePackage.version; - log('enhanced-resolve version:', enhancedResolveVersion); - - const resolveConfig = webpackConfig.resolve || {}; - - if (semver.major(enhancedResolveVersion) >= 2) { - return createWebpack2ResolveSync(webpackRequire, resolveConfig); + // else, vanilla object + for (const key in externals) { + if (!hasOwn(externals, key)) { continue; } + if (source === key) { return true; } } - - return createWebpack1ResolveSync(webpackRequire, resolveConfig, webpackConfig.plugins); + return false; } /** @@ -242,6 +172,22 @@ const webpack1DefaultMains = [ 'webpack', 'browser', 'web', 'browserify', ['jam', 'main'], 'main', ]; +/* eslint-disable */ +// from https://github.com/webpack/webpack/blob/v1.13.0/lib/WebpackOptionsApply.js#L365 +function makeRootPlugin(ModulesInRootPlugin, name, root) { + if (typeof root === 'string') { + return new ModulesInRootPlugin(name, root); + } else if (Array.isArray(root)) { + return function() { + root.forEach(function (root) { + this.apply(new ModulesInRootPlugin(name, root)); + }, this); + }; + } + return function () {}; +} +/* eslint-enable */ + // adapted from tests & // https://github.com/webpack/webpack/blob/v1.13.0/lib/WebpackOptionsApply.js#L322 function createWebpack1ResolveSync(webpackRequire, resolveConfig, plugins) { @@ -298,154 +244,208 @@ function createWebpack1ResolveSync(webpackRequire, resolveConfig, plugins) { }); } - resolver.apply.apply(resolver, resolvePlugins); - - return function () { - return resolver.resolveSync.apply(resolver, arguments); - }; + resolver.apply.apply(resolver, resolvePlugins); + + return function () { + return resolver.resolveSync.apply(resolver, arguments); + }; +} + +function createResolveSync(configPath, webpackConfig, cwd) { + let webpackRequire; + let basedir = null; + + if (typeof configPath === 'string') { + // This can be changed via the settings passed in when defining the resolver + basedir = cwd || path.dirname(configPath); + log(`Attempting to load webpack path from ${basedir}`); + } + + try { + // Attempt to resolve webpack from the given `basedir` + const webpackFilename = resolve('webpack', { basedir, preserveSymlinks: false }); + const webpackResolveOpts = { basedir: path.dirname(webpackFilename), preserveSymlinks: false }; + + webpackRequire = function (id) { + return require(resolve(id, webpackResolveOpts)); + }; + } catch (e) { + // Something has gone wrong (or we're in a test). Use our own bundled + // enhanced-resolve. + log('Using bundled enhanced-resolve.'); + webpackRequire = require; + } + + const enhancedResolvePackage = webpackRequire('enhanced-resolve/package.json'); + const enhancedResolveVersion = enhancedResolvePackage.version; + log('enhanced-resolve version:', enhancedResolveVersion); + + const resolveConfig = webpackConfig.resolve || {}; + + if (semver.major(enhancedResolveVersion) >= 2) { + return createWebpack2ResolveSync(webpackRequire, resolveConfig); + } + + return createWebpack1ResolveSync(webpackRequire, resolveConfig, webpackConfig.plugins); } -/* eslint-disable */ -// from https://github.com/webpack/webpack/blob/v1.13.0/lib/WebpackOptionsApply.js#L365 -function makeRootPlugin(ModulesInRootPlugin, name, root) { - if (typeof root === 'string') { - return new ModulesInRootPlugin(name, root); - } else if (Array.isArray(root)) { - return function() { - root.forEach(function (root) { - this.apply(new ModulesInRootPlugin(name, root)); - }, this); +const MAX_CACHE = 10; +const _cache = []; +function getResolveSync(configPath, webpackConfig, cwd) { + const cacheKey = { configPath, webpackConfig }; + let cached = find(_cache, function (entry) { return isEqual(entry.key, cacheKey); }); + if (!cached) { + cached = { + key: cacheKey, + value: createResolveSync(configPath, webpackConfig, cwd), }; + // put in front and pop last item + if (_cache.unshift(cached) > MAX_CACHE) { + _cache.pop(); + } } - return function () {}; + return cached.value; } -/* eslint-enable */ -function findExternal(source, externals, context, resolveSync) { - if (!externals) { return false; } - - // string match - if (typeof externals === 'string') { return source === externals; } +/** + * Find the full path to 'source', given 'file' as a full reference path. + * + * resolveImport('./foo', '/Users/ben/bar.js') => '/Users/ben/foo.js' + * @param {string} source - the module to resolve; i.e './some-module' + * @param {string} file - the importing file's full path; i.e. '/usr/local/bin/file.js' + * @param {object} settings - the webpack config file name, as well as cwd + * @example + * options: { + * // Path to the webpack config + * config: 'webpack.config.js', + * // Path to be used to determine where to resolve webpack from + * // (may differ from the cwd in some cases) + * cwd: process.cwd() + * } + * @return {string?} the resolved path to source, undefined if not resolved, or null + * if resolved to a non-FS resource (i.e. script tag at page load) + */ +exports.resolve = function (source, file, settings) { - // array: recurse - if (Array.isArray(externals)) { - return externals.some(function (e) { return findExternal(source, e, context, resolveSync); }); + // strip loaders + const finalBang = source.lastIndexOf('!'); + if (finalBang >= 0) { + source = source.slice(finalBang + 1); } - if (isRegex(externals)) { - return externals.test(source); + // strip resource query + const finalQuestionMark = source.lastIndexOf('?'); + if (finalQuestionMark >= 0) { + source = source.slice(0, finalQuestionMark); } - if (typeof externals === 'function') { - let functionExternalFound = false; - const callback = function (err, value) { - if (err) { - functionExternalFound = false; - } else { - functionExternalFound = findExternal(source, value, context, resolveSync); + let webpackConfig; + + const _configPath = settings && settings.config; + /** + * Attempt to set the current working directory. + * If none is passed, default to the `cwd` where the config is located. + */ + const cwd = settings && settings.cwd; + const configIndex = settings && settings['config-index']; + const env = settings && settings.env; + const argv = settings && typeof settings.argv !== 'undefined' ? settings.argv : {}; + let packageDir; + + let configPath = typeof _configPath === 'string' && _configPath.startsWith('.') + ? path.resolve(_configPath) + : _configPath; + + log('Config path from settings:', configPath); + + // see if we've got a config path, a config object, an array of config objects or a config function + if (!configPath || typeof configPath === 'string') { + + // see if we've got an absolute path + if (!configPath || !path.isAbsolute(configPath)) { + // if not, find ancestral package.json and use its directory as base for the path + packageDir = findRoot(path.resolve(file)); + if (!packageDir) { throw new Error('package not found above ' + file); } + } + + configPath = findConfigPath(configPath, packageDir); + + log('Config path resolved to:', configPath); + if (configPath) { + try { + webpackConfig = require(configPath); + } catch (e) { + console.log('Error resolving webpackConfig', e); + throw e; } - }; - // - for prior webpack 5, 'externals function' uses 3 arguments - // - for webpack 5, the count of arguments is less than 3 - if (externals.length === 3) { - externals.call(null, context, source, callback); } else { - const ctx = { - context, - request: source, - contextInfo: { - issuer: '', - issuerLayer: null, - compiler: '', - }, - getResolve: () => (resolveContext, requestToResolve, cb) => { - if (cb) { - try { - cb(null, resolveSync(resolveContext, requestToResolve)); - } catch (e) { - cb(e); - } - } else { - log('getResolve without callback not supported'); - return Promise.reject(new Error('Not supported')); - } - }, - }; - const result = externals.call(null, ctx, callback); - // todo handling Promise object (using synchronous-promise package?) - if (result && typeof result.then === 'function') { - log('Asynchronous functions for externals not supported'); - } + log('No config path found relative to', file, '; using {}'); + webpackConfig = {}; } - return functionExternalFound; - } - // else, vanilla object - for (const key in externals) { - if (!hasOwn(externals, key)) { continue; } - if (source === key) { return true; } + if (webpackConfig && webpackConfig.default) { + log('Using ES6 module "default" key instead of module.exports.'); + webpackConfig = webpackConfig.default; + } + + } else { + webpackConfig = configPath; + configPath = null; } - return false; -} -function findConfigPath(configPath, packageDir) { - const extensions = Object.keys(interpret.extensions).sort(function (a, b) { - return a === '.js' ? -1 : b === '.js' ? 1 : a.length - b.length; - }); - let extension; + if (typeof webpackConfig === 'function') { + webpackConfig = webpackConfig(env, argv); + } - if (configPath) { - // extensions is not reused below, so safe to mutate it here. - extensions.reverse(); - extensions.forEach(function (maybeExtension) { - if (extension) { - return; + if (Array.isArray(webpackConfig)) { + webpackConfig = webpackConfig.map((cfg) => { + if (typeof cfg === 'function') { + return cfg(env, argv); } - if (configPath.substr(-maybeExtension.length) === maybeExtension) { - extension = maybeExtension; - } + return cfg; }); - // see if we've got an absolute path - if (!path.isAbsolute(configPath)) { - configPath = path.join(packageDir, configPath); + if (typeof configIndex !== 'undefined' && webpackConfig.length > configIndex) { + webpackConfig = webpackConfig[configIndex]; + } else { + webpackConfig = find(webpackConfig, function findFirstWithResolve(config) { + return !!config.resolve; + }); } - } else { - extensions.forEach(function (maybeExtension) { - if (extension) { - return; - } + } - const maybePath = path.resolve( - path.join(packageDir, 'webpack.config' + maybeExtension) - ); - if (fs.existsSync(maybePath)) { - configPath = maybePath; - extension = maybeExtension; - } - }); + if (typeof webpackConfig.then === 'function') { + webpackConfig = {}; + + console.warn('Webpack config returns a `Promise`; that signature is not supported at the moment. Using empty object instead.'); } - registerCompiler(interpret.extensions[extension]); - return configPath; -} + if (webpackConfig == null) { + webpackConfig = {}; -function registerCompiler(moduleDescriptor) { - if (moduleDescriptor) { - if (typeof moduleDescriptor === 'string') { - require(moduleDescriptor); - } else if (!Array.isArray(moduleDescriptor)) { - moduleDescriptor.register(require(moduleDescriptor.module)); - } else { - for (let i = 0; i < moduleDescriptor.length; i++) { - try { - registerCompiler(moduleDescriptor[i]); - break; - } catch (e) { - log('Failed to register compiler for moduleDescriptor[]:', i, moduleDescriptor); - } - } + console.warn('No webpack configuration with a "resolve" field found. Using empty object instead.'); + } + + log('Using config: ', webpackConfig); + + const resolveSync = getResolveSync(configPath, webpackConfig, cwd); + + // externals + if (findExternal(source, webpackConfig.externals, path.dirname(file), resolveSync)) { + return { found: true, path: null }; + } + + // otherwise, resolve "normally" + + try { + return { found: true, path: resolveSync(path.dirname(file), source) }; + } catch (err) { + if (isCore(source)) { + return { found: true, path: null }; } + + log('Error during module resolution:', err); + return { found: false }; } -} +}; From 70ca58fac5d2b2c827caa06fb1925c623e3f4034 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Wed, 13 Mar 2024 14:23:38 -0700 Subject: [PATCH 15/50] [utils] [refactor] avoid hoisting --- utils/ignore.js | 36 +++++++-------- utils/parse.js | 82 ++++++++++++++++----------------- utils/resolve.js | 116 +++++++++++++++++++++++------------------------ 3 files changed, 117 insertions(+), 117 deletions(-) diff --git a/utils/ignore.js b/utils/ignore.js index 59ac821eb8..56f2ef7239 100644 --- a/utils/ignore.js +++ b/utils/ignore.js @@ -10,17 +10,6 @@ const log = require('debug')('eslint-plugin-import:utils:ignore'); /** @type {Set} */ let cachedSet; /** @type {import('./types').ESLintSettings} */ let lastSettings; -/** @type {(context: import('eslint').Rule.RuleContext) => Set} */ -function validExtensions(context) { - if (cachedSet && context.settings === lastSettings) { - return cachedSet; - } - - lastSettings = context.settings; - cachedSet = makeValidExtensionSet(context.settings); - return cachedSet; -} - /** @type {import('./ignore').getFileExtensions} */ function makeValidExtensionSet(settings) { // start with explicit JS-parsed extensions @@ -42,6 +31,24 @@ function makeValidExtensionSet(settings) { } exports.getFileExtensions = makeValidExtensionSet; +/** @type {(context: import('eslint').Rule.RuleContext) => Set} */ +function validExtensions(context) { + if (cachedSet && context.settings === lastSettings) { + return cachedSet; + } + + lastSettings = context.settings; + cachedSet = makeValidExtensionSet(context.settings); + return cachedSet; +} + +/** @type {import('./ignore').hasValidExtension} */ +function hasValidExtension(path, context) { + // eslint-disable-next-line no-extra-parens + return validExtensions(context).has(/** @type {import('./types').Extension} */ (extname(path))); +} +exports.hasValidExtension = hasValidExtension; + /** @type {import('./ignore').default} */ exports.default = function ignore(path, context) { // check extension whitelist first (cheap) @@ -60,10 +67,3 @@ exports.default = function ignore(path, context) { return false; }; - -/** @type {import('./ignore').hasValidExtension} */ -function hasValidExtension(path, context) { - // eslint-disable-next-line no-extra-parens - return validExtensions(context).has(/** @type {import('./types').Extension} */ (extname(path))); -} -exports.hasValidExtension = hasValidExtension; diff --git a/utils/parse.js b/utils/parse.js index 804186ca97..94aca1d0f8 100644 --- a/utils/parse.js +++ b/utils/parse.js @@ -61,6 +61,47 @@ function transformHashbang(text) { return text.replace(/^#!([^\r\n]+)/u, (_, captured) => `//${captured}`); } +/** @type {(path: string, context: import('eslint').Rule.RuleContext & { settings?: ESLintSettings }) => import('eslint').Rule.RuleContext['parserPath']} */ +function getParserPath(path, context) { + const parsers = context.settings['import/parsers']; + if (parsers != null) { + // eslint-disable-next-line no-extra-parens + const extension = /** @type {Extension} */ (extname(path)); + for (const parserPath in parsers) { + if (parsers[parserPath].indexOf(extension) > -1) { + // use this alternate parser + log('using alt parser:', parserPath); + return parserPath; + } + } + } + // default to use ESLint parser + return context.parserPath; +} + +/** @type {(path: string, context: import('eslint').Rule.RuleContext) => string | null | (import('eslint').Linter.ParserModule)} */ +function getParser(path, context) { + const parserPath = getParserPath(path, context); + if (parserPath) { + return parserPath; + } + if ( + !!context.languageOptions + && !!context.languageOptions.parser + && typeof context.languageOptions.parser !== 'string' + && ( + // @ts-expect-error TODO: figure out a better type + typeof context.languageOptions.parser.parse === 'function' + // @ts-expect-error TODO: figure out a better type + || typeof context.languageOptions.parser.parseForESLint === 'function' + ) + ) { + return context.languageOptions.parser; + } + + return null; +} + /** @type {import('./parse').default} */ exports.default = function parse(path, content, context) { if (context == null) { throw new Error('need context to parse properly'); } @@ -131,44 +172,3 @@ exports.default = function parse(path, content, context) { // @ts-expect-error TODO: FIXME return makeParseReturn(ast, keysFromParser(parserOrPath, parser, undefined)); }; - -/** @type {(path: string, context: import('eslint').Rule.RuleContext) => string | null | (import('eslint').Linter.ParserModule)} */ -function getParser(path, context) { - const parserPath = getParserPath(path, context); - if (parserPath) { - return parserPath; - } - if ( - !!context.languageOptions - && !!context.languageOptions.parser - && typeof context.languageOptions.parser !== 'string' - && ( - // @ts-expect-error TODO: figure out a better type - typeof context.languageOptions.parser.parse === 'function' - // @ts-expect-error TODO: figure out a better type - || typeof context.languageOptions.parser.parseForESLint === 'function' - ) - ) { - return context.languageOptions.parser; - } - - return null; -} - -/** @type {(path: string, context: import('eslint').Rule.RuleContext & { settings?: ESLintSettings }) => import('eslint').Rule.RuleContext['parserPath']} */ -function getParserPath(path, context) { - const parsers = context.settings['import/parsers']; - if (parsers != null) { - // eslint-disable-next-line no-extra-parens - const extension = /** @type {Extension} */ (extname(path)); - for (const parserPath in parsers) { - if (parsers[parserPath].indexOf(extension) > -1) { - // use this alternate parser - log('using alt parser:', parserPath); - return parserPath; - } - } - } - // default to use ESLint parser - return context.parserPath; -} diff --git a/utils/resolve.js b/utils/resolve.js index 05f7b35abf..5a3084351e 100644 --- a/utils/resolve.js +++ b/utils/resolve.js @@ -34,6 +34,14 @@ const createRequire = Module.createRequire return mod.exports; }; +/** @type {(resolver: object) => resolver is import('./resolve').Resolver} */ +function isResolverValid(resolver) { + if ('interfaceVersion' in resolver && resolver.interfaceVersion === 2) { + return 'resolve' in resolver && !!resolver.resolve && typeof resolver.resolve === 'function'; + } + return 'resolveImport' in resolver && !!resolver.resolveImport && typeof resolver.resolveImport === 'function'; +} + /** @type {(target: T, sourceFile?: string | null | undefined) => undefined | ReturnType} */ function tryRequire(target, sourceFile) { let resolved; @@ -57,6 +65,56 @@ function tryRequire(target, sourceFile) { return require(resolved); } +/** @type {>(resolvers: string[] | string | { [k: string]: string }, map: T) => T} */ +function resolverReducer(resolvers, map) { + if (Array.isArray(resolvers)) { + resolvers.forEach((r) => resolverReducer(r, map)); + return map; + } + + if (typeof resolvers === 'string') { + map.set(resolvers, null); + return map; + } + + if (typeof resolvers === 'object') { + for (const key in resolvers) { + map.set(key, resolvers[key]); + } + return map; + } + + const err = new Error('invalid resolver config'); + err.name = ERROR_NAME; + throw err; +} + +/** @type {(sourceFile: string) => string} */ +function getBaseDir(sourceFile) { + return pkgDir(sourceFile) || process.cwd(); +} + +/** @type {(name: string, sourceFile: string) => import('./resolve').Resolver} */ +function requireResolver(name, sourceFile) { + // Try to resolve package with conventional name + const resolver = tryRequire(`eslint-import-resolver-${name}`, sourceFile) + || tryRequire(name, sourceFile) + || tryRequire(path.resolve(getBaseDir(sourceFile), name)); + + if (!resolver) { + const err = new Error(`unable to load resolver "${name}".`); + err.name = ERROR_NAME; + throw err; + } + if (!isResolverValid(resolver)) { + const err = new Error(`${name} with invalid interface loaded as resolver`); + err.name = ERROR_NAME; + throw err; + } + + return resolver; +} + // https://stackoverflow.com/a/27382838 /** @type {import('./resolve').fileExistsWithCaseSync} */ exports.fileExistsWithCaseSync = function fileExistsWithCaseSync(filepath, cacheSettings, strict) { @@ -159,64 +217,6 @@ function relative(modulePath, sourceFile, settings) { } exports.relative = relative; -/** @type {>(resolvers: string[] | string | { [k: string]: string }, map: T) => T} */ -function resolverReducer(resolvers, map) { - if (Array.isArray(resolvers)) { - resolvers.forEach((r) => resolverReducer(r, map)); - return map; - } - - if (typeof resolvers === 'string') { - map.set(resolvers, null); - return map; - } - - if (typeof resolvers === 'object') { - for (const key in resolvers) { - map.set(key, resolvers[key]); - } - return map; - } - - const err = new Error('invalid resolver config'); - err.name = ERROR_NAME; - throw err; -} - -/** @type {(sourceFile: string) => string} */ -function getBaseDir(sourceFile) { - return pkgDir(sourceFile) || process.cwd(); -} - -/** @type {(name: string, sourceFile: string) => import('./resolve').Resolver} */ -function requireResolver(name, sourceFile) { - // Try to resolve package with conventional name - const resolver = tryRequire(`eslint-import-resolver-${name}`, sourceFile) - || tryRequire(name, sourceFile) - || tryRequire(path.resolve(getBaseDir(sourceFile), name)); - - if (!resolver) { - const err = new Error(`unable to load resolver "${name}".`); - err.name = ERROR_NAME; - throw err; - } - if (!isResolverValid(resolver)) { - const err = new Error(`${name} with invalid interface loaded as resolver`); - err.name = ERROR_NAME; - throw err; - } - - return resolver; -} - -/** @type {(resolver: object) => resolver is import('./resolve').Resolver} */ -function isResolverValid(resolver) { - if ('interfaceVersion' in resolver && resolver.interfaceVersion === 2) { - return 'resolve' in resolver && !!resolver.resolve && typeof resolver.resolve === 'function'; - } - return 'resolveImport' in resolver && !!resolver.resolveImport && typeof resolver.resolveImport === 'function'; -} - /** @type {Set} */ const erroredContexts = new Set(); From 2efdf79af9a543a4c4cb02265a56acc2b7ed0317 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Wed, 13 Mar 2024 14:23:50 -0700 Subject: [PATCH 16/50] [eslint] avoid hoisting --- .eslintrc | 10 ++ src/core/importType.js | 49 +++++----- src/core/packagePath.js | 8 +- src/rules/no-cycle.js | 8 +- src/rules/no-duplicates.js | 160 +++++++++++++++---------------- src/rules/no-namespace.js | 140 +++++++++++++-------------- src/rules/no-restricted-paths.js | 18 ++-- src/rules/order.js | 12 +-- tests/src/package.js | 14 +-- tests/src/utils.js | 8 +- 10 files changed, 216 insertions(+), 211 deletions(-) diff --git a/.eslintrc b/.eslintrc index ddf7bc5628..8dc0b2637a 100644 --- a/.eslintrc +++ b/.eslintrc @@ -96,6 +96,7 @@ "no-multiple-empty-lines": [2, { "max": 1, "maxEOF": 1, "maxBOF": 0 }], "no-return-assign": [2, "always"], "no-trailing-spaces": 2, + "no-use-before-define": [2, { "functions": true, "classes": true, "variables": true }], "no-var": 2, "object-curly-spacing": [2, "always"], "object-shorthand": ["error", "always", { @@ -225,6 +226,15 @@ "no-console": 1, }, }, + { + "files": [ + "utils/**", // TODO + "src/ExportMap.js", // TODO + ], + "rules": { + "no-use-before-define": "off", + }, + }, { "files": [ "resolvers/*/test/**/*", diff --git a/src/core/importType.js b/src/core/importType.js index 6a37d1bb14..32e200f1de 100644 --- a/src/core/importType.js +++ b/src/core/importType.js @@ -4,6 +4,11 @@ import isCoreModule from 'is-core-module'; import resolve from 'eslint-module-utils/resolve'; import { getContextPackagePath } from './packagePath'; +const scopedRegExp = /^@[^/]+\/?[^/]+/; +export function isScoped(name) { + return name && scopedRegExp.test(name); +} + function baseModule(name) { if (isScoped(name)) { const [scope, pkg] = name.split('/'); @@ -30,20 +35,6 @@ export function isBuiltIn(name, settings, path) { return isCoreModule(base) || extras.indexOf(base) > -1; } -export function isExternalModule(name, path, context) { - if (arguments.length < 3) { - throw new TypeError('isExternalModule: name, path, and context are all required'); - } - return (isModule(name) || isScoped(name)) && typeTest(name, context, path) === 'external'; -} - -export function isExternalModuleMain(name, path, context) { - if (arguments.length < 3) { - throw new TypeError('isExternalModule: name, path, and context are all required'); - } - return isModuleMain(name) && typeTest(name, context, path) === 'external'; -} - const moduleRegExp = /^\w/; function isModule(name) { return name && moduleRegExp.test(name); @@ -54,20 +45,9 @@ function isModuleMain(name) { return name && moduleMainRegExp.test(name); } -const scopedRegExp = /^@[^/]+\/?[^/]+/; -export function isScoped(name) { - return name && scopedRegExp.test(name); -} - -const scopedMainRegExp = /^@[^/]+\/?[^/]+$/; -export function isScopedMain(name) { - return name && scopedMainRegExp.test(name); -} - function isRelativeToParent(name) { return (/^\.\.$|^\.\.[\\/]/).test(name); } - const indexFiles = ['.', './', './index', './index.js']; function isIndex(name) { return indexFiles.indexOf(name) !== -1; @@ -123,6 +103,25 @@ function typeTest(name, context, path) { return 'unknown'; } +export function isExternalModule(name, path, context) { + if (arguments.length < 3) { + throw new TypeError('isExternalModule: name, path, and context are all required'); + } + return (isModule(name) || isScoped(name)) && typeTest(name, context, path) === 'external'; +} + +export function isExternalModuleMain(name, path, context) { + if (arguments.length < 3) { + throw new TypeError('isExternalModule: name, path, and context are all required'); + } + return isModuleMain(name) && typeTest(name, context, path) === 'external'; +} + +const scopedMainRegExp = /^@[^/]+\/?[^/]+$/; +export function isScopedMain(name) { + return name && scopedMainRegExp.test(name); +} + export default function resolveImportType(name, context) { return typeTest(name, context, resolve(name, context)); } diff --git a/src/core/packagePath.js b/src/core/packagePath.js index 1a7a28f4b4..142f44aa4d 100644 --- a/src/core/packagePath.js +++ b/src/core/packagePath.js @@ -2,15 +2,15 @@ import { dirname } from 'path'; import pkgUp from 'eslint-module-utils/pkgUp'; import readPkgUp from 'eslint-module-utils/readPkgUp'; -export function getContextPackagePath(context) { - return getFilePackagePath(context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename()); -} - export function getFilePackagePath(filePath) { const fp = pkgUp({ cwd: filePath }); return dirname(fp); } +export function getContextPackagePath(context) { + return getFilePackagePath(context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename()); +} + export function getFilePackageName(filePath) { const { pkg, path } = readPkgUp({ cwd: filePath, normalize: false }); if (pkg) { diff --git a/src/rules/no-cycle.js b/src/rules/no-cycle.js index 5b9d8c0709..11c2f44fc0 100644 --- a/src/rules/no-cycle.js +++ b/src/rules/no-cycle.js @@ -11,6 +11,10 @@ import docsUrl from '../docsUrl'; const traversed = new Set(); +function routeString(route) { + return route.map((s) => `${s.value}:${s.loc.start.line}`).join('=>'); +} + module.exports = { meta: { type: 'suggestion', @@ -151,7 +155,3 @@ module.exports = { }); }, }; - -function routeString(route) { - return route.map((s) => `${s.value}:${s.loc.start.line}`).join('=>'); -} diff --git a/src/rules/no-duplicates.js b/src/rules/no-duplicates.js index 6b4f4d559e..033e854e03 100644 --- a/src/rules/no-duplicates.js +++ b/src/rules/no-duplicates.js @@ -9,28 +9,68 @@ try { typescriptPkg = require('typescript/package.json'); // eslint-disable-line import/no-extraneous-dependencies } catch (e) { /**/ } -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 fix = getFix(first, rest, sourceCode, context); +function isPunctuator(node, value) { + return node.type === 'Punctuator' && node.value === value; +} - context.report({ - node: first.source, - message, - fix, // Attach the autofix (if any) to the first import. - }); +// Get the name of the default import of `node`, if any. +function getDefaultImportName(node) { + const defaultSpecifier = node.specifiers + .find((specifier) => specifier.type === 'ImportDefaultSpecifier'); + return defaultSpecifier != null ? defaultSpecifier.local.name : undefined; +} - for (const node of rest) { - context.report({ - node: node.source, - message, - }); - } - } - } +// Checks whether `node` has a namespace import. +function hasNamespace(node) { + const specifiers = node.specifiers + .filter((specifier) => specifier.type === 'ImportNamespaceSpecifier'); + return specifiers.length > 0; +} + +// Checks whether `node` has any non-default specifiers. +function hasSpecifiers(node) { + const specifiers = node.specifiers + .filter((specifier) => specifier.type === 'ImportSpecifier'); + return specifiers.length > 0; +} + +// Checks whether `node` has a comment (that ends) on the previous line or on +// the same line as `node` (starts). +function hasCommentBefore(node, sourceCode) { + return sourceCode.getCommentsBefore(node) + .some((comment) => comment.loc.end.line >= node.loc.start.line - 1); +} + +// Checks whether `node` has a comment (that starts) on the same line as `node` +// (ends). +function hasCommentAfter(node, sourceCode) { + return sourceCode.getCommentsAfter(node) + .some((comment) => comment.loc.start.line === node.loc.end.line); +} + +// Checks whether `node` has any comments _inside,_ except inside the `{...}` +// part (if any). +function hasCommentInsideNonSpecifiers(node, sourceCode) { + const tokens = sourceCode.getTokens(node); + const openBraceIndex = tokens.findIndex((token) => isPunctuator(token, '{')); + const closeBraceIndex = tokens.findIndex((token) => isPunctuator(token, '}')); + // Slice away the first token, since we're no looking for comments _before_ + // `node` (only inside). If there's a `{...}` part, look for comments before + // the `{`, but not before the `}` (hence the `+1`s). + const someTokens = openBraceIndex >= 0 && closeBraceIndex >= 0 + ? tokens.slice(1, openBraceIndex + 1).concat(tokens.slice(closeBraceIndex + 1)) + : tokens.slice(1); + return someTokens.some((token) => sourceCode.getCommentsBefore(token).length > 0); +} + +// It's not obvious what the user wants to do with comments associated with +// duplicate imports, so skip imports with comments when autofixing. +function hasProblematicComments(node, sourceCode) { + return ( + hasCommentBefore(node, sourceCode) + || hasCommentAfter(node, sourceCode) + || hasCommentInsideNonSpecifiers(node, sourceCode) + ); } function getFix(first, rest, sourceCode, context) { @@ -203,68 +243,28 @@ function getFix(first, rest, sourceCode, context) { }; } -function isPunctuator(node, value) { - return node.type === 'Punctuator' && node.value === value; -} - -// Get the name of the default import of `node`, if any. -function getDefaultImportName(node) { - const defaultSpecifier = node.specifiers - .find((specifier) => specifier.type === 'ImportDefaultSpecifier'); - return defaultSpecifier != null ? defaultSpecifier.local.name : undefined; -} - -// Checks whether `node` has a namespace import. -function hasNamespace(node) { - const specifiers = node.specifiers - .filter((specifier) => specifier.type === 'ImportNamespaceSpecifier'); - return specifiers.length > 0; -} - -// Checks whether `node` has any non-default specifiers. -function hasSpecifiers(node) { - const specifiers = node.specifiers - .filter((specifier) => specifier.type === 'ImportSpecifier'); - return specifiers.length > 0; -} - -// It's not obvious what the user wants to do with comments associated with -// duplicate imports, so skip imports with comments when autofixing. -function hasProblematicComments(node, sourceCode) { - return ( - hasCommentBefore(node, sourceCode) - || hasCommentAfter(node, sourceCode) - || hasCommentInsideNonSpecifiers(node, sourceCode) - ); -} - -// Checks whether `node` has a comment (that ends) on the previous line or on -// the same line as `node` (starts). -function hasCommentBefore(node, sourceCode) { - return sourceCode.getCommentsBefore(node) - .some((comment) => comment.loc.end.line >= node.loc.start.line - 1); -} +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 fix = getFix(first, rest, sourceCode, context); -// Checks whether `node` has a comment (that starts) on the same line as `node` -// (ends). -function hasCommentAfter(node, sourceCode) { - return sourceCode.getCommentsAfter(node) - .some((comment) => comment.loc.start.line === node.loc.end.line); -} + context.report({ + node: first.source, + message, + fix, // Attach the autofix (if any) to the first import. + }); -// Checks whether `node` has any comments _inside,_ except inside the `{...}` -// part (if any). -function hasCommentInsideNonSpecifiers(node, sourceCode) { - const tokens = sourceCode.getTokens(node); - const openBraceIndex = tokens.findIndex((token) => isPunctuator(token, '{')); - const closeBraceIndex = tokens.findIndex((token) => isPunctuator(token, '}')); - // Slice away the first token, since we're no looking for comments _before_ - // `node` (only inside). If there's a `{...}` part, look for comments before - // the `{`, but not before the `}` (hence the `+1`s). - const someTokens = openBraceIndex >= 0 && closeBraceIndex >= 0 - ? tokens.slice(1, openBraceIndex + 1).concat(tokens.slice(closeBraceIndex + 1)) - : tokens.slice(1); - return someTokens.some((token) => sourceCode.getCommentsBefore(token).length > 0); + for (const node of rest) { + context.report({ + node: node.source, + message, + }); + } + } + } } module.exports = { diff --git a/src/rules/no-namespace.js b/src/rules/no-namespace.js index d3e591876f..3c6617a41c 100644 --- a/src/rules/no-namespace.js +++ b/src/rules/no-namespace.js @@ -6,9 +6,74 @@ import minimatch from 'minimatch'; import docsUrl from '../docsUrl'; -//------------------------------------------------------------------------------ -// Rule Definition -//------------------------------------------------------------------------------ +/** + * @param {MemberExpression} memberExpression + * @returns {string} the name of the member in the object expression, e.g. the `x` in `namespace.x` + */ +function getMemberPropertyName(memberExpression) { + return memberExpression.property.type === 'Identifier' + ? memberExpression.property.name + : memberExpression.property.value; +} + +/** + * @param {ScopeManager} scopeManager + * @param {ASTNode} node + * @return {Set} + */ +function getVariableNamesInScope(scopeManager, node) { + let currentNode = node; + let scope = scopeManager.acquire(currentNode); + while (scope == null) { + currentNode = currentNode.parent; + scope = scopeManager.acquire(currentNode, true); + } + return new Set(scope.variables.concat(scope.upper.variables).map((variable) => variable.name)); +} + +/** + * + * @param {*} names + * @param {*} nameConflicts + * @param {*} namespaceName + */ +function generateLocalNames(names, nameConflicts, namespaceName) { + const localNames = {}; + names.forEach((name) => { + let localName; + if (!nameConflicts[name].has(name)) { + localName = name; + } else if (!nameConflicts[name].has(`${namespaceName}_${name}`)) { + localName = `${namespaceName}_${name}`; + } else { + for (let i = 1; i < Infinity; i++) { + if (!nameConflicts[name].has(`${namespaceName}_${name}_${i}`)) { + localName = `${namespaceName}_${name}_${i}`; + break; + } + } + } + localNames[name] = localName; + }); + return localNames; +} + +/** + * @param {Identifier[]} namespaceIdentifiers + * @returns {boolean} `true` if the namespace variable is more than just a glorified constant + */ +function usesNamespaceAsObject(namespaceIdentifiers) { + return !namespaceIdentifiers.every((identifier) => { + const parent = identifier.parent; + + // `namespace.x` or `namespace['x']` + return ( + parent + && parent.type === 'MemberExpression' + && (parent.property.type === 'Identifier' || parent.property.type === 'Literal') + ); + }); +} module.exports = { meta: { @@ -103,72 +168,3 @@ module.exports = { }; }, }; - -/** - * @param {Identifier[]} namespaceIdentifiers - * @returns {boolean} `true` if the namespace variable is more than just a glorified constant - */ -function usesNamespaceAsObject(namespaceIdentifiers) { - return !namespaceIdentifiers.every((identifier) => { - const parent = identifier.parent; - - // `namespace.x` or `namespace['x']` - return ( - parent - && parent.type === 'MemberExpression' - && (parent.property.type === 'Identifier' || parent.property.type === 'Literal') - ); - }); -} - -/** - * @param {MemberExpression} memberExpression - * @returns {string} the name of the member in the object expression, e.g. the `x` in `namespace.x` - */ -function getMemberPropertyName(memberExpression) { - return memberExpression.property.type === 'Identifier' - ? memberExpression.property.name - : memberExpression.property.value; -} - -/** - * @param {ScopeManager} scopeManager - * @param {ASTNode} node - * @return {Set} - */ -function getVariableNamesInScope(scopeManager, node) { - let currentNode = node; - let scope = scopeManager.acquire(currentNode); - while (scope == null) { - currentNode = currentNode.parent; - scope = scopeManager.acquire(currentNode, true); - } - return new Set(scope.variables.concat(scope.upper.variables).map((variable) => variable.name)); -} - -/** - * - * @param {*} names - * @param {*} nameConflicts - * @param {*} namespaceName - */ -function generateLocalNames(names, nameConflicts, namespaceName) { - const localNames = {}; - names.forEach((name) => { - let localName; - if (!nameConflicts[name].has(name)) { - localName = name; - } else if (!nameConflicts[name].has(`${namespaceName}_${name}`)) { - localName = `${namespaceName}_${name}`; - } else { - for (let i = 1; i < Infinity; i++) { - if (!nameConflicts[name].has(`${namespaceName}_${name}_${i}`)) { - localName = `${namespaceName}_${name}_${i}`; - break; - } - } - } - localNames[name] = localName; - }); - return localNames; -} diff --git a/src/rules/no-restricted-paths.js b/src/rules/no-restricted-paths.js index cd680a1946..75952dd058 100644 --- a/src/rules/no-restricted-paths.js +++ b/src/rules/no-restricted-paths.js @@ -12,6 +12,15 @@ const containsPath = (filepath, target) => { return relative === '' || !relative.startsWith('..'); }; +function isMatchingTargetPath(filename, targetPath) { + if (isGlob(targetPath)) { + const mm = new Minimatch(targetPath); + return mm.match(filename); + } + + return containsPath(filename, targetPath); +} + module.exports = { meta: { type: 'problem', @@ -83,15 +92,6 @@ module.exports = { .some((targetPath) => isMatchingTargetPath(currentFilename, targetPath)), ); - function isMatchingTargetPath(filename, targetPath) { - if (isGlob(targetPath)) { - const mm = new Minimatch(targetPath); - return mm.match(filename); - } - - return containsPath(filename, targetPath); - } - function isValidExceptionPath(absoluteFromPath, absoluteExceptionPath) { const relativeExceptionPath = path.relative(absoluteFromPath, absoluteExceptionPath); diff --git a/src/rules/order.js b/src/rules/order.js index 44d25be63c..7071513bf3 100644 --- a/src/rules/order.js +++ b/src/rules/order.js @@ -92,6 +92,12 @@ function findRootNode(node) { return parent; } +function commentOnSameLineAs(node) { + return (token) => (token.type === 'Block' || token.type === 'Line') + && token.loc.start.line === token.loc.end.line + && token.loc.end.line === node.loc.end.line; +} + function findEndOfLineWithComments(sourceCode, node) { const tokensToEndOfLine = takeTokensAfterWhile(sourceCode, node, commentOnSameLineAs(node)); const endOfTokens = tokensToEndOfLine.length > 0 @@ -111,12 +117,6 @@ function findEndOfLineWithComments(sourceCode, node) { return result; } -function commentOnSameLineAs(node) { - return (token) => (token.type === 'Block' || token.type === 'Line') - && token.loc.start.line === token.loc.end.line - && token.loc.end.line === node.loc.end.line; -} - function findStartOfLineWithComments(sourceCode, node) { const tokensToEndOfLine = takeTokensBeforeWhile(sourceCode, node, commentOnSameLineAs(node)); const startOfTokens = tokensToEndOfLine.length > 0 ? tokensToEndOfLine[0].range[0] : node.range[0]; diff --git a/tests/src/package.js b/tests/src/package.js index 08138084c6..c56bd1333d 100644 --- a/tests/src/package.js +++ b/tests/src/package.js @@ -45,6 +45,13 @@ describe('package', function () { }); }); + function getRulePath(ruleName) { + // 'require' does not work with dynamic paths because of the compilation step by babel + // (which resolves paths according to the root folder configuration) + // the usage of require.resolve on a static path gets around this + return path.resolve(require.resolve('rules/no-unresolved'), '..', ruleName); + } + it('has configs only for rules that exist', function () { for (const configFile in module.configs) { const preamble = 'import/'; @@ -54,13 +61,6 @@ describe('package', function () { .not.to.throw(Error); } } - - function getRulePath(ruleName) { - // 'require' does not work with dynamic paths because of the compilation step by babel - // (which resolves paths according to the root folder configuration) - // the usage of require.resolve on a static path gets around this - return path.resolve(require.resolve('rules/no-unresolved'), '..', ruleName); - } }); it('marks deprecated rules in their metadata', function () { diff --git a/tests/src/utils.js b/tests/src/utils.js index d5215b02e3..24d5504a71 100644 --- a/tests/src/utils.js +++ b/tests/src/utils.js @@ -42,10 +42,6 @@ export function eslintVersionSatisfies(specifier) { return semver.satisfies(eslintPkg.version, specifier); } -export function testVersion(specifier, t) { - return eslintVersionSatisfies(specifier) ? test(t()) : []; -} - export function test(t) { if (arguments.length !== 1) { throw new SyntaxError('`test` requires exactly one object argument'); @@ -61,6 +57,10 @@ export function test(t) { }; } +export function testVersion(specifier, t) { + return eslintVersionSatisfies(specifier) ? test(t()) : []; +} + export function testContext(settings) { return { getFilename() { return FILENAME; }, settings: settings || {} }; From c77c1a6ace311f99ed25878768c2dbee49c0512a Mon Sep 17 00:00:00 2001 From: Ivan Rubinson Date: Wed, 13 Mar 2024 18:41:22 +0200 Subject: [PATCH 17/50] [eslint] ignore some warnings --- .eslintrc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.eslintrc b/.eslintrc index 8dc0b2637a..224ffcb0e3 100644 --- a/.eslintrc +++ b/.eslintrc @@ -235,6 +235,16 @@ "no-use-before-define": "off", }, }, + { + "files": [ + "resolvers/webpack/index.js", + "resolvers/webpack/test/example.js", + "utils/parse.js", + ], + "rules": { + "no-console": "off", + }, + }, { "files": [ "resolvers/*/test/**/*", From 7a28ba23885dbb2bda98b5c6d7659134ea6d7777 Mon Sep 17 00:00:00 2001 From: Ivan Rubinson Date: Mon, 18 Mar 2024 21:11:25 +0200 Subject: [PATCH 18/50] [Refactor] `ExportMap`: separate ExportMap instance from its builder logic --- .eslintrc | 2 +- CHANGELOG.md | 2 + src/exportMap.js | 178 ++++++++++++++++++++ src/{ExportMap.js => exportMapBuilder.js} | 188 +--------------------- src/rules/default.js | 4 +- src/rules/export.js | 4 +- src/rules/named.js | 6 +- src/rules/namespace.js | 11 +- src/rules/no-cycle.js | 4 +- src/rules/no-deprecated.js | 7 +- src/rules/no-named-as-default-member.js | 4 +- src/rules/no-named-as-default.js | 4 +- src/rules/no-unused-modules.js | 4 +- tests/src/core/getExports.js | 60 +++---- 14 files changed, 242 insertions(+), 236 deletions(-) create mode 100644 src/exportMap.js rename src/{ExportMap.js => exportMapBuilder.js} (78%) diff --git a/.eslintrc b/.eslintrc index 224ffcb0e3..1bbbba0148 100644 --- a/.eslintrc +++ b/.eslintrc @@ -229,7 +229,7 @@ { "files": [ "utils/**", // TODO - "src/ExportMap.js", // TODO + "src/exportMapBuilder.js", // TODO ], "rules": { "no-use-before-define": "off", diff --git a/CHANGELOG.md b/CHANGELOG.md index 9552774dce..7ad93fbd30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange - [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]) ## [2.29.1] - 2023-12-14 @@ -1109,6 +1110,7 @@ for info on changes for earlier releases. [`memo-parser`]: ./memo-parser/README.md +[#2985]: https://github.com/import-js/eslint-plugin-import/pull/2985 [#2982]: https://github.com/import-js/eslint-plugin-import/pull/2982 [#2944]: https://github.com/import-js/eslint-plugin-import/pull/2944 [#2942]: https://github.com/import-js/eslint-plugin-import/pull/2942 diff --git a/src/exportMap.js b/src/exportMap.js new file mode 100644 index 0000000000..e4d61638c5 --- /dev/null +++ b/src/exportMap.js @@ -0,0 +1,178 @@ +export default class ExportMap { + constructor(path) { + this.path = path; + this.namespace = new Map(); + // todo: restructure to key on path, value is resolver + map of names + this.reexports = new Map(); + /** + * star-exports + * @type {Set<() => ExportMap>} + */ + this.dependencies = new Set(); + /** + * dependencies of this module that are not explicitly re-exported + * @type {Map ExportMap>} + */ + this.imports = new Map(); + this.errors = []; + /** + * type {'ambiguous' | 'Module' | 'Script'} + */ + this.parseGoal = 'ambiguous'; + } + + get hasDefault() { return this.get('default') != null; } // stronger than this.has + + get size() { + let size = this.namespace.size + this.reexports.size; + this.dependencies.forEach((dep) => { + const d = dep(); + // CJS / ignored dependencies won't exist (#717) + if (d == null) { return; } + size += d.size; + }); + return size; + } + + /** + * Note that this does not check explicitly re-exported names for existence + * in the base namespace, but it will expand all `export * from '...'` exports + * if not found in the explicit namespace. + * @param {string} name + * @return {boolean} true if `name` is exported by this module. + */ + has(name) { + if (this.namespace.has(name)) { return true; } + if (this.reexports.has(name)) { return true; } + + // default exports must be explicitly re-exported (#328) + if (name !== 'default') { + for (const dep of this.dependencies) { + const innerMap = dep(); + + // todo: report as unresolved? + if (!innerMap) { continue; } + + if (innerMap.has(name)) { return true; } + } + } + + return false; + } + + /** + * ensure that imported name fully resolves. + * @param {string} name + * @return {{ found: boolean, path: ExportMap[] }} + */ + hasDeep(name) { + if (this.namespace.has(name)) { return { found: true, path: [this] }; } + + if (this.reexports.has(name)) { + const reexports = this.reexports.get(name); + const imported = reexports.getImport(); + + // if import is ignored, return explicit 'null' + if (imported == null) { return { found: true, path: [this] }; } + + // safeguard against cycles, only if name matches + if (imported.path === this.path && reexports.local === name) { + return { found: false, path: [this] }; + } + + const deep = imported.hasDeep(reexports.local); + deep.path.unshift(this); + + return deep; + } + + // default exports must be explicitly re-exported (#328) + if (name !== 'default') { + for (const dep of this.dependencies) { + const innerMap = dep(); + if (innerMap == null) { return { found: true, path: [this] }; } + // todo: report as unresolved? + if (!innerMap) { continue; } + + // safeguard against cycles + if (innerMap.path === this.path) { continue; } + + const innerValue = innerMap.hasDeep(name); + if (innerValue.found) { + innerValue.path.unshift(this); + return innerValue; + } + } + } + + return { found: false, path: [this] }; + } + + get(name) { + if (this.namespace.has(name)) { return this.namespace.get(name); } + + if (this.reexports.has(name)) { + const reexports = this.reexports.get(name); + const imported = reexports.getImport(); + + // if import is ignored, return explicit 'null' + if (imported == null) { return null; } + + // safeguard against cycles, only if name matches + if (imported.path === this.path && reexports.local === name) { return undefined; } + + return imported.get(reexports.local); + } + + // default exports must be explicitly re-exported (#328) + if (name !== 'default') { + for (const dep of this.dependencies) { + const innerMap = dep(); + // todo: report as unresolved? + if (!innerMap) { continue; } + + // safeguard against cycles + if (innerMap.path === this.path) { continue; } + + const innerValue = innerMap.get(name); + if (innerValue !== undefined) { return innerValue; } + } + } + + return undefined; + } + + forEach(callback, thisArg) { + this.namespace.forEach((v, n) => { callback.call(thisArg, v, n, this); }); + + this.reexports.forEach((reexports, name) => { + const reexported = reexports.getImport(); + // can't look up meta for ignored re-exports (#348) + callback.call(thisArg, reexported && reexported.get(reexports.local), name, this); + }); + + this.dependencies.forEach((dep) => { + const d = dep(); + // CJS / ignored dependencies won't exist (#717) + if (d == null) { return; } + + d.forEach((v, n) => { + if (n !== 'default') { + callback.call(thisArg, v, n, this); + } + }); + }); + } + + // todo: keys, values, entries? + + reportErrors(context, declaration) { + const msg = this.errors + .map((e) => `${e.message} (${e.lineNumber}:${e.column})`) + .join(', '); + context.report({ + node: declaration.source, + message: `Parse errors in imported module '${declaration.source.value}': ${msg}`, + }); + } +} diff --git a/src/ExportMap.js b/src/exportMapBuilder.js similarity index 78% rename from src/ExportMap.js rename to src/exportMapBuilder.js index 9ee65a504f..f2b40e7b4c 100644 --- a/src/ExportMap.js +++ b/src/exportMapBuilder.js @@ -18,6 +18,7 @@ import * as unambiguous from 'eslint-module-utils/unambiguous'; import { tsConfigLoader } from 'tsconfig-paths/lib/tsconfig-loader'; import includes from 'array-includes'; +import ExportMap from './exportMap'; let ts; @@ -117,189 +118,12 @@ const availableDocStyleParsers = { const supportedImportTypes = new Set(['ImportDefaultSpecifier', 'ImportNamespaceSpecifier']); -export default class ExportMap { - constructor(path) { - this.path = path; - this.namespace = new Map(); - // todo: restructure to key on path, value is resolver + map of names - this.reexports = new Map(); - /** - * star-exports - * @type {Set} of () => ExportMap - */ - this.dependencies = new Set(); - /** - * dependencies of this module that are not explicitly re-exported - * @type {Map} from path = () => ExportMap - */ - this.imports = new Map(); - this.errors = []; - /** - * type {'ambiguous' | 'Module' | 'Script'} - */ - this.parseGoal = 'ambiguous'; - } - - get hasDefault() { return this.get('default') != null; } // stronger than this.has - - get size() { - let size = this.namespace.size + this.reexports.size; - this.dependencies.forEach((dep) => { - const d = dep(); - // CJS / ignored dependencies won't exist (#717) - if (d == null) { return; } - size += d.size; - }); - return size; - } - - /** - * Note that this does not check explicitly re-exported names for existence - * in the base namespace, but it will expand all `export * from '...'` exports - * if not found in the explicit namespace. - * @param {string} name - * @return {Boolean} true if `name` is exported by this module. - */ - has(name) { - if (this.namespace.has(name)) { return true; } - if (this.reexports.has(name)) { return true; } - - // default exports must be explicitly re-exported (#328) - if (name !== 'default') { - for (const dep of this.dependencies) { - const innerMap = dep(); - - // todo: report as unresolved? - if (!innerMap) { continue; } - - if (innerMap.has(name)) { return true; } - } - } - - return false; - } - - /** - * ensure that imported name fully resolves. - * @param {string} name - * @return {{ found: boolean, path: ExportMap[] }} - */ - hasDeep(name) { - if (this.namespace.has(name)) { return { found: true, path: [this] }; } - - if (this.reexports.has(name)) { - const reexports = this.reexports.get(name); - const imported = reexports.getImport(); - - // if import is ignored, return explicit 'null' - if (imported == null) { return { found: true, path: [this] }; } - - // safeguard against cycles, only if name matches - if (imported.path === this.path && reexports.local === name) { - return { found: false, path: [this] }; - } - - const deep = imported.hasDeep(reexports.local); - deep.path.unshift(this); - - return deep; - } - - // default exports must be explicitly re-exported (#328) - if (name !== 'default') { - for (const dep of this.dependencies) { - const innerMap = dep(); - if (innerMap == null) { return { found: true, path: [this] }; } - // todo: report as unresolved? - if (!innerMap) { continue; } - - // safeguard against cycles - if (innerMap.path === this.path) { continue; } - - const innerValue = innerMap.hasDeep(name); - if (innerValue.found) { - innerValue.path.unshift(this); - return innerValue; - } - } - } - - return { found: false, path: [this] }; - } - - get(name) { - if (this.namespace.has(name)) { return this.namespace.get(name); } - - if (this.reexports.has(name)) { - const reexports = this.reexports.get(name); - const imported = reexports.getImport(); - - // if import is ignored, return explicit 'null' - if (imported == null) { return null; } - - // safeguard against cycles, only if name matches - if (imported.path === this.path && reexports.local === name) { return undefined; } - - return imported.get(reexports.local); - } - - // default exports must be explicitly re-exported (#328) - if (name !== 'default') { - for (const dep of this.dependencies) { - const innerMap = dep(); - // todo: report as unresolved? - if (!innerMap) { continue; } - - // safeguard against cycles - if (innerMap.path === this.path) { continue; } - - const innerValue = innerMap.get(name); - if (innerValue !== undefined) { return innerValue; } - } - } - - return undefined; - } - - forEach(callback, thisArg) { - this.namespace.forEach((v, n) => { callback.call(thisArg, v, n, this); }); - - this.reexports.forEach((reexports, name) => { - const reexported = reexports.getImport(); - // can't look up meta for ignored re-exports (#348) - callback.call(thisArg, reexported && reexported.get(reexports.local), name, this); - }); - - this.dependencies.forEach((dep) => { - const d = dep(); - // CJS / ignored dependencies won't exist (#717) - if (d == null) { return; } - - d.forEach((v, n) => { - if (n !== 'default') { - callback.call(thisArg, v, n, this); - } - }); - }); - } - - // todo: keys, values, entries? - - reportErrors(context, declaration) { - const msg = this.errors - .map((e) => `${e.message} (${e.lineNumber}:${e.column})`) - .join(', '); - context.report({ - node: declaration.source, - message: `Parse errors in imported module '${declaration.source.value}': ${msg}`, - }); - } - +export default class ExportMapBuilder { static get(source, context) { const path = resolve(source, context); if (path == null) { return null; } - return ExportMap.for(childContext(path, context)); + return ExportMapBuilder.for(childContext(path, context)); } static for(context) { @@ -343,7 +167,7 @@ export default class ExportMap { } log('cache miss', cacheKey, 'for path', path); - exportMap = ExportMap.parse(path, content, context); + exportMap = ExportMapBuilder.parse(path, content, context); // ambiguous modules return null if (exportMap == null) { @@ -447,7 +271,7 @@ export default class ExportMap { function resolveImport(value) { const rp = remotePath(value); if (rp == null) { return null; } - return ExportMap.for(childContext(rp, context)); + return ExportMapBuilder.for(childContext(rp, context)); } function getNamespace(identifier) { @@ -738,7 +562,7 @@ export default class ExportMap { * caused memory leaks. See #1266. */ function thunkFor(p, context) { - return () => ExportMap.for(childContext(p, context)); + return () => ExportMapBuilder.for(childContext(p, context)); } /** diff --git a/src/rules/default.js b/src/rules/default.js index 297a80c463..cbaa49f1fc 100644 --- a/src/rules/default.js +++ b/src/rules/default.js @@ -1,4 +1,4 @@ -import Exports from '../ExportMap'; +import ExportMapBuilder from '../exportMapBuilder'; import docsUrl from '../docsUrl'; module.exports = { @@ -19,7 +19,7 @@ module.exports = { ); if (!defaultSpecifier) { return; } - const imports = Exports.get(node.source.value, context); + const imports = ExportMapBuilder.get(node.source.value, context); if (imports == null) { return; } if (imports.errors.length) { diff --git a/src/rules/export.js b/src/rules/export.js index c540f1e3c9..b1dc5ca9ea 100644 --- a/src/rules/export.js +++ b/src/rules/export.js @@ -1,4 +1,4 @@ -import ExportMap, { recursivePatternCapture } from '../ExportMap'; +import ExportMapBuilder, { recursivePatternCapture } from '../exportMapBuilder'; import docsUrl from '../docsUrl'; import includes from 'array-includes'; import flatMap from 'array.prototype.flatmap'; @@ -197,7 +197,7 @@ module.exports = { // `export * as X from 'path'` does not conflict if (node.exported && node.exported.name) { return; } - const remoteExports = ExportMap.get(node.source.value, context); + const remoteExports = ExportMapBuilder.get(node.source.value, context); if (remoteExports == null) { return; } if (remoteExports.errors.length) { diff --git a/src/rules/named.js b/src/rules/named.js index e7fe4e4dce..043d72eabe 100644 --- a/src/rules/named.js +++ b/src/rules/named.js @@ -1,5 +1,5 @@ import * as path from 'path'; -import Exports from '../ExportMap'; +import ExportMapBuilder from '../exportMapBuilder'; import docsUrl from '../docsUrl'; module.exports = { @@ -41,7 +41,7 @@ module.exports = { return; // no named imports/exports } - const imports = Exports.get(node.source.value, context); + const imports = ExportMapBuilder.get(node.source.value, context); if (imports == null || imports.parseGoal === 'ambiguous') { return; } @@ -93,7 +93,7 @@ module.exports = { const call = node.init; const [source] = call.arguments; const variableImports = node.id.properties; - const variableExports = Exports.get(source.value, context); + const variableExports = ExportMapBuilder.get(source.value, context); if ( // return if it's not a commonjs require statement diff --git a/src/rules/namespace.js b/src/rules/namespace.js index 77a3ea9077..e1ca2870b1 100644 --- a/src/rules/namespace.js +++ b/src/rules/namespace.js @@ -1,5 +1,6 @@ import declaredScope from 'eslint-module-utils/declaredScope'; -import Exports from '../ExportMap'; +import ExportMapBuilder from '../exportMapBuilder'; +import ExportMap from '../exportMap'; import importDeclaration from '../importDeclaration'; import docsUrl from '../docsUrl'; @@ -8,7 +9,7 @@ function processBodyStatement(context, namespaces, declaration) { if (declaration.specifiers.length === 0) { return; } - const imports = Exports.get(declaration.source.value, context); + const imports = ExportMapBuilder.get(declaration.source.value, context); if (imports == null) { return null; } if (imports.errors.length > 0) { @@ -88,7 +89,7 @@ module.exports = { ExportNamespaceSpecifier(namespace) { const declaration = importDeclaration(context); - const imports = Exports.get(declaration.source.value, context); + const imports = ExportMapBuilder.get(declaration.source.value, context); if (imports == null) { return null; } if (imports.errors.length) { @@ -122,7 +123,7 @@ module.exports = { let namespace = namespaces.get(dereference.object.name); const namepath = [dereference.object.name]; // while property is namespace and parent is member expression, keep validating - while (namespace instanceof Exports && dereference.type === 'MemberExpression') { + while (namespace instanceof ExportMap && dereference.type === 'MemberExpression') { if (dereference.computed) { if (!allowComputed) { context.report( @@ -161,7 +162,7 @@ module.exports = { // DFS traverse child namespaces function testKey(pattern, namespace, path = [init.name]) { - if (!(namespace instanceof Exports)) { return; } + if (!(namespace instanceof ExportMap)) { return; } if (pattern.type !== 'ObjectPattern') { return; } diff --git a/src/rules/no-cycle.js b/src/rules/no-cycle.js index 11c2f44fc0..b7b907b062 100644 --- a/src/rules/no-cycle.js +++ b/src/rules/no-cycle.js @@ -4,7 +4,7 @@ */ import resolve from 'eslint-module-utils/resolve'; -import Exports from '../ExportMap'; +import ExportMapBuilder from '../exportMapBuilder'; import { isExternalModule } from '../core/importType'; import moduleVisitor, { makeOptionsSchema } from 'eslint-module-utils/moduleVisitor'; import docsUrl from '../docsUrl'; @@ -88,7 +88,7 @@ module.exports = { return; // ignore type imports } - const imported = Exports.get(sourceNode.value, context); + const imported = ExportMapBuilder.get(sourceNode.value, context); if (imported == null) { return; // no-unresolved territory diff --git a/src/rules/no-deprecated.js b/src/rules/no-deprecated.js index 06eeff8ea7..50072f3f85 100644 --- a/src/rules/no-deprecated.js +++ b/src/rules/no-deprecated.js @@ -1,5 +1,6 @@ import declaredScope from 'eslint-module-utils/declaredScope'; -import Exports from '../ExportMap'; +import ExportMapBuilder from '../exportMapBuilder'; +import ExportMap from '../exportMap'; import docsUrl from '../docsUrl'; function message(deprecation) { @@ -31,7 +32,7 @@ module.exports = { if (node.type !== 'ImportDeclaration') { return; } if (node.source == null) { return; } // local export, ignore - const imports = Exports.get(node.source.value, context); + const imports = ExportMapBuilder.get(node.source.value, context); if (imports == null) { return; } const moduleDeprecation = imports.doc && imports.doc.tags.find((t) => t.title === 'deprecated'); @@ -114,7 +115,7 @@ module.exports = { let namespace = namespaces.get(dereference.object.name); const namepath = [dereference.object.name]; // while property is namespace and parent is member expression, keep validating - while (namespace instanceof Exports && dereference.type === 'MemberExpression') { + while (namespace instanceof ExportMap && dereference.type === 'MemberExpression') { // ignore computed parts for now if (dereference.computed) { return; } diff --git a/src/rules/no-named-as-default-member.js b/src/rules/no-named-as-default-member.js index e00a4cbc87..d594c58433 100644 --- a/src/rules/no-named-as-default-member.js +++ b/src/rules/no-named-as-default-member.js @@ -4,7 +4,7 @@ * @copyright 2016 Desmond Brand. All rights reserved. * See LICENSE in root directory for full license. */ -import Exports from '../ExportMap'; +import ExportMapBuilder from '../exportMapBuilder'; import importDeclaration from '../importDeclaration'; import docsUrl from '../docsUrl'; @@ -36,7 +36,7 @@ module.exports = { return { ImportDefaultSpecifier(node) { const declaration = importDeclaration(context); - const exportMap = Exports.get(declaration.source.value, context); + const exportMap = ExportMapBuilder.get(declaration.source.value, context); if (exportMap == null) { return; } if (exportMap.errors.length) { diff --git a/src/rules/no-named-as-default.js b/src/rules/no-named-as-default.js index 40b1e175b2..3e73ff2f44 100644 --- a/src/rules/no-named-as-default.js +++ b/src/rules/no-named-as-default.js @@ -1,4 +1,4 @@ -import Exports from '../ExportMap'; +import ExportMapBuilder from '../exportMapBuilder'; import importDeclaration from '../importDeclaration'; import docsUrl from '../docsUrl'; @@ -20,7 +20,7 @@ module.exports = { const declaration = importDeclaration(context); - const imports = Exports.get(declaration.source.value, context); + const imports = ExportMapBuilder.get(declaration.source.value, context); if (imports == null) { return; } if (imports.errors.length) { diff --git a/src/rules/no-unused-modules.js b/src/rules/no-unused-modules.js index ec3425dacd..812efffbca 100644 --- a/src/rules/no-unused-modules.js +++ b/src/rules/no-unused-modules.js @@ -13,7 +13,7 @@ import values from 'object.values'; import includes from 'array-includes'; import flatMap from 'array.prototype.flatmap'; -import Exports, { recursivePatternCapture } from '../ExportMap'; +import ExportMapBuilder, { recursivePatternCapture } from '../exportMapBuilder'; import docsUrl from '../docsUrl'; let FileEnumerator; @@ -194,7 +194,7 @@ const prepareImportsAndExports = (srcFiles, context) => { srcFiles.forEach((file) => { const exports = new Map(); const imports = new Map(); - const currentExports = Exports.get(file, context); + const currentExports = ExportMapBuilder.get(file, context); if (currentExports) { const { dependencies, diff --git a/tests/src/core/getExports.js b/tests/src/core/getExports.js index 1dd6e88014..611a13055f 100644 --- a/tests/src/core/getExports.js +++ b/tests/src/core/getExports.js @@ -4,7 +4,7 @@ import sinon from 'sinon'; import eslintPkg from 'eslint/package.json'; import typescriptPkg from 'typescript/package.json'; import * as tsConfigLoader from 'tsconfig-paths/lib/tsconfig-loader'; -import ExportMap from '../../../src/ExportMap'; +import ExportMapBuilder from '../../../src/exportMapBuilder'; import * as fs from 'fs'; @@ -28,7 +28,7 @@ describe('ExportMap', function () { it('handles ExportAllDeclaration', function () { let imports; expect(function () { - imports = ExportMap.get('./export-all', fakeContext); + imports = ExportMapBuilder.get('./export-all', fakeContext); }).not.to.throw(Error); expect(imports).to.exist; @@ -37,25 +37,25 @@ describe('ExportMap', function () { }); it('returns a cached copy on subsequent requests', function () { - expect(ExportMap.get('./named-exports', fakeContext)) - .to.exist.and.equal(ExportMap.get('./named-exports', fakeContext)); + expect(ExportMapBuilder.get('./named-exports', fakeContext)) + .to.exist.and.equal(ExportMapBuilder.get('./named-exports', fakeContext)); }); it('does not return a cached copy after modification', (done) => { - const firstAccess = ExportMap.get('./mutator', fakeContext); + const firstAccess = ExportMapBuilder.get('./mutator', fakeContext); expect(firstAccess).to.exist; // mutate (update modified time) const newDate = new Date(); fs.utimes(getFilename('mutator.js'), newDate, newDate, (error) => { expect(error).not.to.exist; - expect(ExportMap.get('./mutator', fakeContext)).not.to.equal(firstAccess); + expect(ExportMapBuilder.get('./mutator', fakeContext)).not.to.equal(firstAccess); done(); }); }); it('does not return a cached copy with different settings', () => { - const firstAccess = ExportMap.get('./named-exports', fakeContext); + const firstAccess = ExportMapBuilder.get('./named-exports', fakeContext); expect(firstAccess).to.exist; const differentSettings = { @@ -63,7 +63,7 @@ describe('ExportMap', function () { parserPath: 'espree', }; - expect(ExportMap.get('./named-exports', differentSettings)) + expect(ExportMapBuilder.get('./named-exports', differentSettings)) .to.exist.and .not.to.equal(firstAccess); }); @@ -71,7 +71,7 @@ describe('ExportMap', function () { it('does not throw for a missing file', function () { let imports; expect(function () { - imports = ExportMap.get('./does-not-exist', fakeContext); + imports = ExportMapBuilder.get('./does-not-exist', fakeContext); }).not.to.throw(Error); expect(imports).not.to.exist; @@ -81,7 +81,7 @@ describe('ExportMap', function () { it('exports explicit names for a missing file in exports', function () { let imports; expect(function () { - imports = ExportMap.get('./exports-missing', fakeContext); + imports = ExportMapBuilder.get('./exports-missing', fakeContext); }).not.to.throw(Error); expect(imports).to.exist; @@ -92,7 +92,7 @@ describe('ExportMap', function () { it('finds exports for an ES7 module with babel-eslint', function () { const path = getFilename('jsx/FooES7.js'); const contents = fs.readFileSync(path, { encoding: 'utf8' }); - const imports = ExportMap.parse( + const imports = ExportMapBuilder.parse( path, contents, { parserPath: 'babel-eslint', settings: {} }, @@ -112,7 +112,7 @@ describe('ExportMap', function () { before('parse file', function () { const path = getFilename('deprecated.js'); const contents = fs.readFileSync(path, { encoding: 'utf8' }).replace(/[\r]\n/g, lineEnding); - imports = ExportMap.parse(path, contents, parseContext); + imports = ExportMapBuilder.parse(path, contents, parseContext); // sanity checks expect(imports.errors).to.be.empty; @@ -181,7 +181,7 @@ describe('ExportMap', function () { before('parse file', function () { const path = getFilename('deprecated-file.js'); const contents = fs.readFileSync(path, { encoding: 'utf8' }); - imports = ExportMap.parse(path, contents, parseContext); + imports = ExportMapBuilder.parse(path, contents, parseContext); // sanity checks expect(imports.errors).to.be.empty; @@ -243,7 +243,7 @@ describe('ExportMap', function () { it('works with espree & traditional namespace exports', function () { const path = getFilename('deep/a.js'); const contents = fs.readFileSync(path, { encoding: 'utf8' }); - const a = ExportMap.parse(path, contents, espreeContext); + const a = ExportMapBuilder.parse(path, contents, espreeContext); expect(a.errors).to.be.empty; expect(a.get('b').namespace).to.exist; expect(a.get('b').namespace.has('c')).to.be.true; @@ -252,7 +252,7 @@ describe('ExportMap', function () { it('captures namespace exported as default', function () { const path = getFilename('deep/default.js'); const contents = fs.readFileSync(path, { encoding: 'utf8' }); - const def = ExportMap.parse(path, contents, espreeContext); + const def = ExportMapBuilder.parse(path, contents, espreeContext); expect(def.errors).to.be.empty; expect(def.get('default').namespace).to.exist; expect(def.get('default').namespace.has('c')).to.be.true; @@ -261,7 +261,7 @@ describe('ExportMap', function () { it('works with babel-eslint & ES7 namespace exports', function () { const path = getFilename('deep-es7/a.js'); const contents = fs.readFileSync(path, { encoding: 'utf8' }); - const a = ExportMap.parse(path, contents, babelContext); + const a = ExportMapBuilder.parse(path, contents, babelContext); expect(a.errors).to.be.empty; expect(a.get('b').namespace).to.exist; expect(a.get('b').namespace.has('c')).to.be.true; @@ -278,7 +278,7 @@ describe('ExportMap', function () { const path = getFilename('deep/cache-1.js'); const contents = fs.readFileSync(path, { encoding: 'utf8' }); - a = ExportMap.parse(path, contents, espreeContext); + a = ExportMapBuilder.parse(path, contents, espreeContext); expect(a.errors).to.be.empty; expect(a.get('b').namespace).to.exist; @@ -304,10 +304,10 @@ describe('ExportMap', function () { context('Map API', function () { context('#size', function () { - it('counts the names', () => expect(ExportMap.get('./named-exports', fakeContext)) + it('counts the names', () => expect(ExportMapBuilder.get('./named-exports', fakeContext)) .to.have.property('size', 12)); - it('includes exported namespace size', () => expect(ExportMap.get('./export-all', fakeContext)) + it('includes exported namespace size', () => expect(ExportMapBuilder.get('./export-all', fakeContext)) .to.have.property('size', 1)); }); @@ -315,14 +315,14 @@ describe('ExportMap', function () { context('issue #210: self-reference', function () { it(`doesn't crash`, function () { - expect(() => ExportMap.get('./narcissist', fakeContext)).not.to.throw(Error); + expect(() => ExportMapBuilder.get('./narcissist', fakeContext)).not.to.throw(Error); }); it(`'has' circular reference`, function () { - expect(ExportMap.get('./narcissist', fakeContext)) + expect(ExportMapBuilder.get('./narcissist', fakeContext)) .to.exist.and.satisfy((m) => m.has('soGreat')); }); it(`can 'get' circular reference`, function () { - expect(ExportMap.get('./narcissist', fakeContext)) + expect(ExportMapBuilder.get('./narcissist', fakeContext)) .to.exist.and.satisfy((m) => m.get('soGreat') != null); }); }); @@ -335,7 +335,7 @@ describe('ExportMap', function () { let imports; before('load imports', function () { - imports = ExportMap.get('./typescript.ts', context); + imports = ExportMapBuilder.get('./typescript.ts', context); }); it('returns nothing for a TypeScript file', function () { @@ -372,7 +372,7 @@ describe('ExportMap', function () { before('load imports', function () { this.timeout(20e3); // takes a long time :shrug: sinon.spy(tsConfigLoader, 'tsConfigLoader'); - imports = ExportMap.get('./typescript.ts', context); + imports = ExportMapBuilder.get('./typescript.ts', context); }); after('clear spies', function () { tsConfigLoader.tsConfigLoader.restore(); @@ -414,9 +414,9 @@ describe('ExportMap', function () { }, }; expect(tsConfigLoader.tsConfigLoader.callCount).to.equal(0); - ExportMap.parse('./baz.ts', 'export const baz = 5', customContext); + ExportMapBuilder.parse('./baz.ts', 'export const baz = 5', customContext); expect(tsConfigLoader.tsConfigLoader.callCount).to.equal(1); - ExportMap.parse('./baz.ts', 'export const baz = 5', customContext); + ExportMapBuilder.parse('./baz.ts', 'export const baz = 5', customContext); expect(tsConfigLoader.tsConfigLoader.callCount).to.equal(1); const differentContext = { @@ -426,17 +426,17 @@ describe('ExportMap', function () { }, }; - ExportMap.parse('./baz.ts', 'export const baz = 5', differentContext); + ExportMapBuilder.parse('./baz.ts', 'export const baz = 5', differentContext); expect(tsConfigLoader.tsConfigLoader.callCount).to.equal(2); }); it('should cache after parsing for an ambiguous module', function () { const source = './typescript-declare-module.ts'; - const parseSpy = sinon.spy(ExportMap, 'parse'); + const parseSpy = sinon.spy(ExportMapBuilder, 'parse'); - expect(ExportMap.get(source, context)).to.be.null; + expect(ExportMapBuilder.get(source, context)).to.be.null; - ExportMap.get(source, context); + ExportMapBuilder.get(source, context); expect(parseSpy.callCount).to.equal(1); From f3e505bc2bc87f28aa10267aed8172f08c7d9c9a Mon Sep 17 00:00:00 2001 From: minervabot <53988640+minervabot@users.noreply.github.com> Date: Mon, 2 Jan 2023 02:28:07 -0700 Subject: [PATCH 19/50] [Docs] `order`: Add a quick note on how unbound imports and --fix Having unbound imports mixed among the bound ones causes unexpected and incorrect seeming results. I spent several hours trying to fix this problem only to find it was well known! --- CHANGELOG.md | 3 +++ docs/rules/order.md | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ad93fbd30..77c908dc33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange - [`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]) ## [2.29.1] - 2023-12-14 @@ -1130,6 +1131,7 @@ for info on changes for earlier releases. [#2735]: https://github.com/import-js/eslint-plugin-import/pull/2735 [#2699]: https://github.com/import-js/eslint-plugin-import/pull/2699 [#2664]: https://github.com/import-js/eslint-plugin-import/pull/2664 +[#2640]: https://github.com/import-js/eslint-plugin-import/pull/2640 [#2613]: https://github.com/import-js/eslint-plugin-import/pull/2613 [#2608]: https://github.com/import-js/eslint-plugin-import/pull/2608 [#2605]: https://github.com/import-js/eslint-plugin-import/pull/2605 @@ -1846,6 +1848,7 @@ for info on changes for earlier releases. [@mgwalker]: https://github.com/mgwalker [@mhmadhamster]: https://github.com/MhMadHamster [@MikeyBeLike]: https://github.com/MikeyBeLike +[@minervabot]: https://github.com/minervabot [@mpint]: https://github.com/mpint [@mplewis]: https://github.com/mplewis [@mrmckeb]: https://github.com/mrmckeb diff --git a/docs/rules/order.md b/docs/rules/order.md index 2335699e6c..24692f2d21 100644 --- a/docs/rules/order.md +++ b/docs/rules/order.md @@ -77,6 +77,25 @@ import foo from './foo'; var path = require('path'); ``` +## Limitations of `--fix` + +Unbound imports are assumed to have side effects, and will never be moved/reordered. This can cause other imports to get "stuck" around them, and the fix to fail. + +```javascript +import b from 'b' +import 'format.css'; // This will prevent --fix from working. +import a from 'a' +``` + +As a workaround, move unbound imports to be entirely above or below bound ones. + +```javascript +import 'format1.css'; // OK +import b from 'b' +import a from 'a' +import 'format2.css'; // OK +``` + ## Options This rule supports the following options: From fa60e3d738078fa2fccbc36260b9da534795f720 Mon Sep 17 00:00:00 2001 From: Joey Guerra Date: Tue, 19 Mar 2024 17:33:40 -0500 Subject: [PATCH 20/50] [Tests] appveyor -> GHA (run tests on Windows in both pwsh and WSL + Ubuntu) --- .github/workflows/native-wsl.yml | 151 ++++++++++++++++++++++++++++ CHANGELOG.md | 3 + appveyor.yml | 165 ------------------------------- 3 files changed, 154 insertions(+), 165 deletions(-) create mode 100644 .github/workflows/native-wsl.yml delete mode 100644 appveyor.yml diff --git a/.github/workflows/native-wsl.yml b/.github/workflows/native-wsl.yml new file mode 100644 index 0000000000..893d2248d1 --- /dev/null +++ b/.github/workflows/native-wsl.yml @@ -0,0 +1,151 @@ +name: Native and WSL + +on: [push, pull_request] + +jobs: + build: + runs-on: ${{ matrix.os }} + defaults: + run: + shell: ${{ matrix.configuration == 'wsl' && 'wsl-bash {0}' || 'pwsh' }} + strategy: + fail-fast: false + matrix: + os: [windows-2019] + node-version: [18, 16, 14, 12, 10, 8, 6, 4] + configuration: [wsl, native] + + steps: + - uses: actions/checkout@v4 + - uses: Vampire/setup-wsl@v3 + if: matrix.configuration == 'wsl' + with: + distribution: Ubuntu-22.04 + - run: curl --version + - name: 'WSL: do all npm install steps' + if: matrix.configuration == 'wsl' + env: + ESLINT_VERSION: 7 + TRAVIS_NODE_VERSION: ${{ matrix.node-version }} + run: | + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm + nvm install --latest-npm ${{ matrix.node-version }} + + if [ ${{ matrix.node-version }} -ge 4 ] && [ ${{ matrix.node-version }} -lt 6 ]; then + npm install eslint@4 --no-save --ignore-scripts + npm install + npm install eslint-import-resolver-typescript@1.0.2 --no-save + npm uninstall @angular-eslint/template-parser @typescript-eslint/parser --no-save + fi + if [ ${{ matrix.node-version }} -ge 6 ] && [ ${{ matrix.node-version }} -lt 7 ]; then + npm install eslint@5 --no-save --ignore-scripts + npm install + npm uninstall @angular-eslint/template-parser --no-save + npm install eslint-import-resolver-typescript@1.0.2 @typescript-eslint/parser@3 --no-save + fi + if [ ${{ matrix.node-version }} -ge 7 ] && [ ${{ matrix.node-version }} -lt 8 ]; then + npm install eslint@6 --no-save --ignore-scripts + npm install + npm install eslint-import-resolver-typescript@1.0.2 typescript-eslint-parser@20 --no-save + npm uninstall @angular-eslint/template-parser --no-save + fi + if [ ${{ matrix.node-version }} -eq 8 ]; then + npm install eslint@6 --no-save --ignore-scripts + npm install + npm uninstall @angular-eslint/template-parser --no-save + npm install @typescript-eslint/parser@3 --no-save + fi + if [ ${{ matrix.node-version }} -gt 8 ] && [ ${{ matrix.node-version }} -lt 10 ]; then + npm install eslint@7 --no-save --ignore-scripts + npm install + npm install @typescript-eslint/parser@3 --no-save + fi + if [ ${{ matrix.node-version }} -ge 10 ] && [ ${{ matrix.node-version }} -lt 12 ]; then + npm install + npm install @typescript-eslint/parser@4 --no-save + fi + if [ ${{ matrix.node-version }} -ge 12 ]; then + npm install + fi + npm run copy-metafiles + npm run pretest + npm run tests-only + + - name: install dependencies for node <= 10 + if: matrix.node-version <= '10' && matrix.configuration == 'native' + run: | + npm install --legacy-peer-deps + npm install eslint@7 --no-save + + - name: Install dependencies for node > 10 + if: matrix.node-version > '10' && matrix.configuration == 'native' + run: npm install + + - name: install the latest version of nyc + if: matrix.configuration == 'native' + run: npm install nyc@latest --no-save + + - name: copy metafiles for node <= 8 + if: matrix.node-version <= 8 && matrix.configuration == 'native' + env: + ESLINT_VERSION: 6 + TRAVIS_NODE_VERSION: ${{ matrix.node-version }} + run: | + npm run copy-metafiles + bash ./tests/dep-time-travel.sh 2>&1 + - name: copy metafiles for Node > 8 + if: matrix.node-version > 8 && matrix.configuration == 'native' + env: + ESLINT_VERSION: 7 + TRAVIS_NODE_VERSION: ${{ matrix.node-version }} + run: | + npm run copy-metafiles + bash ./tests/dep-time-travel.sh 2>&1 + + - name: install ./resolver dependencies in Native + if: matrix.configuration == 'native' + shell: pwsh + run: | + npm config set package-lock false + $resolverDir = "./resolvers" + Get-ChildItem -Directory $resolverDir | + ForEach { + Write-output $(Resolve-Path $(Join-Path $resolverDir $_.Name)) + Push-Location $(Resolve-Path $(Join-Path $resolverDir $_.Name)) + npm install + npm ls nyc > $null; + if ($?) { + npm install nyc@latest --no-save + } + Pop-Location + } + + - name: run tests in Native + if: matrix.configuration == 'native' + shell: pwsh + run: | + npm run pretest + npm run tests-only + $resolverDir = "./resolvers"; + $resolvers = @(); + Get-ChildItem -Directory $resolverDir | + ForEach { + $resolvers += "$(Resolve-Path $(Join-Path $resolverDir $_.Name))"; + } + $env:RESOLVERS = [string]::Join(";", $resolvers); + foreach ($resolver in $resolvers) { + Set-Location -Path $resolver.Trim('"') + npm run tests-only + Set-Location -Path $PSScriptRoot + } + + - name: codecov + uses: codecov/codecov-action@v3.1.5 + + windows: + runs-on: ubuntu-latest + needs: [build] + steps: + - run: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 77c908dc33..f8a6f80d0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange - [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]) +- [Tests] appveyor -> GHA (run tests on Windows in both pwsh and WSL + Ubuntu) ([#2987], thanks [@joeyguerra]) ## [2.29.1] - 2023-12-14 @@ -1111,6 +1112,7 @@ for info on changes for earlier releases. [`memo-parser`]: ./memo-parser/README.md +[#2987]: https://github.com/import-js/eslint-plugin-import/pull/2987 [#2985]: https://github.com/import-js/eslint-plugin-import/pull/2985 [#2982]: https://github.com/import-js/eslint-plugin-import/pull/2982 [#2944]: https://github.com/import-js/eslint-plugin-import/pull/2944 @@ -1791,6 +1793,7 @@ for info on changes for earlier releases. [@jkimbo]: https://github.com/jkimbo [@joaovieira]: https://github.com/joaovieira [@joe-matsec]: https://github.com/joe-matsec +[@joeyguerra]: https://github.com/joeyguerra [@johndevedu]: https://github.com/johndevedu [@johnthagen]: https://github.com/johnthagen [@jonboiser]: https://github.com/jonboiser diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index e50ab87d2a..0000000000 --- a/appveyor.yml +++ /dev/null @@ -1,165 +0,0 @@ -configuration: - - Native - - WSL - -# Test against this version of Node.js -environment: - matrix: - - nodejs_version: "16" - - nodejs_version: "14" - - nodejs_version: "12" - - nodejs_version: "10" - - nodejs_version: "8" - # - nodejs_version: "6" - # - nodejs_version: "4" - -image: Visual Studio 2019 -matrix: - fast_finish: false - exclude: - - configuration: WSL - nodejs_version: "8" - - configuration: WSL - nodejs_version: "6" - - configuration: WSL - nodejs_version: "4" - - allow_failures: - - nodejs_version: "4" # for eslint 5 - - configuration: WSL - -platform: - - x86 - - x64 - -# Initialization scripts. (runs before repo cloning) -init: - # Declare version-numbers of packages to install - - ps: >- - if ($env:nodejs_version -eq "4") { - $env:NPM_VERSION="3" - } - if ($env:nodejs_version -in @("8")) { - $env:NPM_VERSION="6" - } - if ($env:nodejs_version -in @("10", "12", "14", "16")) { - $env:NPM_VERSION="6" # TODO: use npm 7 - $env:NPM_CONFIG_LEGACY_PEER_DEPS="true" - } - - ps: >- - $env:ESLINT_VERSION="7"; - if ([int]$env:nodejs_version -le 8) { - $env:ESLINT_VERSION="6" - } - if ([int]$env:nodejs_version -le 7) { - $env:ESLINT_VERSION="5" - } - if ([int]$env:nodejs_version -le 6) { - $env:ESLINT_VERSION="4" - } - - ps: $env:WINDOWS_NYC_VERSION = "15.0.1" - - ps: $env:TRAVIS_NODE_VERSION = $env:nodejs_version - - # Add `ci`-command to `PATH` for running commands either using cmd or wsl depending on the configuration - - ps: $env:PATH += ";$(Join-Path $(pwd) "scripts")" - -# Install scripts. (runs after repo cloning) -before_build: - # Install propert `npm`-version - - IF DEFINED NPM_VERSION ci sudo npm install -g npm@%NPM_VERSION% - - # Install dependencies - - ci npm install - - ci npm run copy-metafiles - - bash ./tests/dep-time-travel.sh 2>&1 - - # fix symlinks - - git config core.symlinks true - - git reset --hard - - ci git reset --hard - - # Install dependencies of resolvers - - ps: >- - $resolverDir = "./resolvers"; - $resolvers = @(); - Get-ChildItem -Directory $resolverDir | - ForEach { - $resolvers += "$(Resolve-Path $(Join-Path $resolverDir $_))"; - } - $env:RESOLVERS = [string]::Join(";", $resolvers); - - FOR %%G in ("%RESOLVERS:;=";"%") do ( pushd %%~G & ci npm install & popd ) - - # Install proper `eslint`-version - - IF DEFINED ESLINT_VERSION ci npm install --no-save eslint@%ESLINT_VERSION% - -# Build scripts (project isn't actually built) -build_script: - - ps: "# This Project isn't actually built" - -# Test scripts -test_script: - # Output useful info for debugging. - - ci node --version - - ci npm --version - - # Run core tests - - ci npm run pretest - - ci npm run tests-only - - # Run resolver tests - - ps: >- - $resolverDir = "./resolvers"; - $resolvers = @(); - Get-ChildItem -Directory $resolverDir | - ForEach { - $resolvers += "$(Resolve-Path $(Join-Path $resolverDir $_))"; - } - $env:RESOLVERS = [string]::Join(";", $resolvers); - - FOR %%G in ("%RESOLVERS:;=";"%") do ( pushd %%~G & ci npm test & popd ) - -# Configuration-specific steps -for: - - matrix: - except: - - configuration: WSL - install: - # Get the latest stable version of Node.js or io.js - - ps: Install-Product node $env:nodejs_version - before_test: - # Upgrade nyc - - ci npm i --no-save nyc@%WINDOWS_NYC_VERSION% - - ps: >- - $resolverDir = "./resolvers"; - $resolvers = @(); - Get-ChildItem -Directory $resolverDir | - ForEach { - Push-Location $(Resolve-Path $(Join-Path $resolverDir $_)); - ci npm ls nyc > $null; - if ($?) { - $resolvers += "$(pwd)"; - } - Pop-Location; - } - $env:RESOLVERS = [string]::Join(";", $resolvers); - - IF DEFINED RESOLVERS FOR %%G in ("%RESOLVERS:;=";"%") do ( pushd %%~G & ci npm install --no-save nyc@%WINDOWS_NYC_VERSION% & popd ) - # TODO: enable codecov for native windows builds - #on_success: - #- ci $ProgressPreference = 'SilentlyContinue' - #- ci Invoke-WebRequest -Uri https://uploader.codecov.io/latest/windows/codecov.exe - #- ci -Outfile codecov.exe - #- ci .\codecov.exe - - matrix: - only: - - configuration: WSL - # Install scripts. (runs after repo cloning) - install: - # Get a specific version of Node.js - - ps: $env:WSLENV += ":nodejs_version" - - ps: wsl curl -sL 'https://deb.nodesource.com/setup_${nodejs_version}.x' `| sudo APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=1 -E bash - - - wsl sudo DEBIAN_FRONTEND=noninteractive apt install -y nodejs - on_success: - - ci curl -Os https://uploader.codecov.io/latest/linux/codecov - - ci chmod +x codecov - - ci ./codecov - -build: on From 5508b6c6dfd4c0472ad98b10d3f45788bd6718fc Mon Sep 17 00:00:00 2001 From: Ashok Suthar Date: Thu, 14 Mar 2024 07:45:50 +0100 Subject: [PATCH 21/50] [actions] migrate OSX tests to GHA Co-authored-by: Ashok Suthar Co-authored-by: Jordan Harband --- .github/workflows/node-4+.yml | 8 ++++++- .travis.yml | 40 ----------------------------------- CHANGELOG.md | 4 ++++ 3 files changed, 11 insertions(+), 41 deletions(-) delete mode 100644 .travis.yml diff --git a/.github/workflows/node-4+.yml b/.github/workflows/node-4+.yml index 2925adda8a..62c654decc 100644 --- a/.github/workflows/node-4+.yml +++ b/.github/workflows/node-4+.yml @@ -22,11 +22,14 @@ jobs: latest: needs: [matrix] name: 'majors' - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: + os: + - ubuntu-latest + - macos-latest node-version: ${{ fromJson(needs.matrix.outputs.latest) }} eslint: - 8 @@ -38,16 +41,19 @@ jobs: - 2 include: - node-version: 'lts/*' + os: ubuntu-latest eslint: 7 ts-parser: 4 env: TS_PARSER: 4 - node-version: 'lts/*' + os: ubuntu-latest eslint: 7 ts-parser: 3 env: TS_PARSER: 3 - node-version: 'lts/*' + os: ubuntu-latest eslint: 7 ts-parser: 2 env: diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 21a7070fb7..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,40 +0,0 @@ -language: node_js - -# osx backlog is often deep, so to be polite we can just hit these highlights -matrix: - include: - - os: osx - env: ESLINT_VERSION=5 - node_js: 14 - - os: osx - env: ESLINT_VERSION=5 - node_js: 12 - - os: osx - env: ESLINT_VERSION=5 - node_js: 10 - - os: osx - env: ESLINT_VERSION=4 - node_js: 8 - - os: osx - env: ESLINT_VERSION=3 - node_js: 6 - - os: osx - env: ESLINT_VERSION=2 - node_js: 4 - - fast_finish: true - -before_install: - - 'nvm install-latest-npm' - - 'NPM_CONFIG_LEGACY_PEER_DEPS=true npm install' - - 'npm run copy-metafiles' -install: - - 'NPM_CONFIG_LEGACY_PEER_DEPS=true npm install' - - 'if [ -n "${ESLINT_VERSION}" ]; then ./tests/dep-time-travel.sh; fi' - - 'npm run pretest' - -script: - - npm run tests-only - -after_success: - - bash <(curl -Os https://uploader.codecov.io/latest/linux/codecov) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8a6f80d0e..b1aa3a9785 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange - [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]) - [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-]) ## [2.29.1] - 2023-12-14 @@ -1458,6 +1459,8 @@ for info on changes for earlier releases. [#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 + [#2930]: https://github.com/import-js/eslint-plugin-import/issues/2930 [#2687]: https://github.com/import-js/eslint-plugin-import/issues/2687 [#2684]: https://github.com/import-js/eslint-plugin-import/issues/2684 @@ -1690,6 +1693,7 @@ for info on changes for earlier releases. [@adjerbetian]: https://github.com/adjerbetian [@AdriAt360]: https://github.com/AdriAt360 [@ai]: https://github.com/ai +[@aks-]: https://github.com/aks- [@aladdin-add]: https://github.com/aladdin-add [@alex-page]: https://github.com/alex-page [@alexgorbatchev]: https://github.com/alexgorbatchev From 38f8d25ef710747fe318d99b3f42e70bd1efc0f4 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Sat, 23 Mar 2024 22:47:24 -0700 Subject: [PATCH 22/50] [actions] update actions to node20 --- .github/workflows/node-4+.yml | 4 ++-- .github/workflows/node-pretest.yml | 4 ++-- .github/workflows/packages.yml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/node-4+.yml b/.github/workflows/node-4+.yml index 62c654decc..60fa609dbe 100644 --- a/.github/workflows/node-4+.yml +++ b/.github/workflows/node-4+.yml @@ -105,7 +105,7 @@ jobs: eslint: 5 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ljharb/actions/node/install@main continue-on-error: ${{ matrix.eslint == 4 && matrix.node-version == 4 }} name: 'nvm install ${{ matrix.node-version }} && npm install, with eslint ${{ matrix.eslint }}' @@ -119,7 +119,7 @@ jobs: skip-ls-check: true - run: npm run pretest - run: npm run tests-only - - uses: codecov/codecov-action@v3 + - uses: codecov/codecov-action@v3.1.5 node: name: 'node 4+' diff --git a/.github/workflows/node-pretest.yml b/.github/workflows/node-pretest.yml index e4340018e4..f8db36de57 100644 --- a/.github/workflows/node-pretest.yml +++ b/.github/workflows/node-pretest.yml @@ -10,7 +10,7 @@ jobs: # runs-on: ubuntu-latest # steps: - # - uses: actions/checkout@v3 + # - uses: actions/checkout@v4 # - uses: ljharb/actions/node/install@main # name: 'nvm install lts/* && npm install' # with: @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ljharb/actions/node/install@main name: 'nvm install lts/* && npm install' with: diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index a6fb4e4cb5..213e1a43ce 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -38,7 +38,7 @@ jobs: # - utils steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ljharb/actions/node/install@main name: 'nvm install ${{ matrix.node-version }} && npm install' env: @@ -50,7 +50,7 @@ jobs: after_install: npm run copy-metafiles && ./tests/dep-time-travel.sh && cd ${{ matrix.package }} && npm install skip-ls-check: true - run: cd ${{ matrix.package }} && npm run tests-only - - uses: codecov/codecov-action@v3 + - uses: codecov/codecov-action@v3.1.5 packages: name: 'packages: all tests' From 2de78c1eac5064858cc366e211913c4f9e43919b Mon Sep 17 00:00:00 2001 From: Ivan Rubinson Date: Mon, 25 Mar 2024 19:21:56 +0200 Subject: [PATCH 23/50] [Refactor] `exportMapBuilder`: avoid hoisting --- .eslintrc | 1 - CHANGELOG.md | 2 + src/exportMapBuilder.js | 305 ++++++++++++++++++++-------------------- 3 files changed, 155 insertions(+), 153 deletions(-) diff --git a/.eslintrc b/.eslintrc index 1bbbba0148..80e1014c60 100644 --- a/.eslintrc +++ b/.eslintrc @@ -229,7 +229,6 @@ { "files": [ "utils/**", // TODO - "src/exportMapBuilder.js", // TODO ], "rules": { "no-use-before-define": "off", diff --git a/CHANGELOG.md b/CHANGELOG.md index b1aa3a9785..c05ea32c01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange - [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]) ## [2.29.1] - 2023-12-14 @@ -1113,6 +1114,7 @@ for info on changes for earlier releases. [`memo-parser`]: ./memo-parser/README.md +[#2989]: https://github.com/import-js/eslint-plugin-import/pull/2989 [#2987]: https://github.com/import-js/eslint-plugin-import/pull/2987 [#2985]: https://github.com/import-js/eslint-plugin-import/pull/2985 [#2982]: https://github.com/import-js/eslint-plugin-import/pull/2982 diff --git a/src/exportMapBuilder.js b/src/exportMapBuilder.js index f2b40e7b4c..5aeb306d0b 100644 --- a/src/exportMapBuilder.js +++ b/src/exportMapBuilder.js @@ -118,6 +118,100 @@ const availableDocStyleParsers = { const supportedImportTypes = new Set(['ImportDefaultSpecifier', 'ImportNamespaceSpecifier']); +let parserOptionsHash = ''; +let prevParserOptions = ''; +let settingsHash = ''; +let prevSettings = ''; +/** + * 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 + */ +function childContext(path, context) { + const { settings, parserOptions, parserPath } = 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); + } + + return { + cacheKey: String(parserPath) + parserOptionsHash + settingsHash + String(path), + settings, + parserOptions, + parserPath, + path, + }; +} + +/** + * sometimes legacy support isn't _that_ hard... right? + */ +function makeSourceCode(text, ast) { + if (SourceCode.length > 1) { + // ESLint 3 + return new SourceCode(text, ast); + } else { + // ESLint 4, 5 + return new SourceCode({ text, ast }); + } +} + +/** + * Traverse a pattern/identifier node, calling 'callback' + * for each leaf identifier. + * @param {node} pattern + * @param {Function} callback + * @return {void} + */ +export function recursivePatternCapture(pattern, callback) { + switch (pattern.type) { + case 'Identifier': // base case + callback(pattern); + break; + + case 'ObjectPattern': + pattern.properties.forEach((p) => { + if (p.type === 'ExperimentalRestProperty' || p.type === 'RestElement') { + callback(p.argument); + return; + } + recursivePatternCapture(p.value, callback); + }); + break; + + case 'ArrayPattern': + pattern.elements.forEach((element) => { + if (element == null) { return; } + if (element.type === 'ExperimentalRestProperty' || element.type === 'RestElement') { + callback(element.argument); + return; + } + recursivePatternCapture(element, callback); + }); + break; + + case 'AssignmentPattern': + callback(pattern.left); + break; + default: + } +} + +/** + * The creation of this closure is isolated from other scopes + * to avoid over-retention of unrelated variables, which has + * caused memory leaks. See #1266. + */ +function thunkFor(p, context) { + // eslint-disable-next-line no-use-before-define + return () => ExportMapBuilder.for(childContext(p, context)); +} + export default class ExportMapBuilder { static get(source, context) { const path = resolve(source, context); @@ -183,6 +277,43 @@ export default class ExportMapBuilder { } static parse(path, content, context) { + function readTsConfig(context) { + const tsconfigInfo = tsConfigLoader({ + cwd: context.parserOptions && context.parserOptions.tsconfigRootDir || process.cwd(), + getEnv: (key) => process.env[key], + }); + try { + if (tsconfigInfo.tsConfigPath !== undefined) { + // Projects not using TypeScript won't have `typescript` installed. + if (!ts) { ts = require('typescript'); } // eslint-disable-line import/no-extraneous-dependencies + + const configFile = ts.readConfigFile(tsconfigInfo.tsConfigPath, ts.sys.readFile); + return ts.parseJsonConfigFileContent( + configFile.config, + ts.sys, + dirname(tsconfigInfo.tsConfigPath), + ); + } + } catch (e) { + // Catch any errors + } + + return null; + } + + function isEsModuleInterop() { + const cacheKey = hashObject({ + tsconfigRootDir: context.parserOptions && context.parserOptions.tsconfigRootDir, + }).digest('hex'); + let tsConfig = tsconfigCache.get(cacheKey); + if (typeof tsConfig === 'undefined') { + tsConfig = readTsConfig(context); + tsconfigCache.set(cacheKey, tsConfig); + } + + return tsConfig && tsConfig.options ? tsConfig.options.esModuleInterop : false; + } + const m = new ExportMap(path); const isEsModuleInteropTrue = isEsModuleInterop(); @@ -201,6 +332,10 @@ export default class ExportMapBuilder { let hasDynamicImports = false; + function remotePath(value) { + return resolve.relative(value, path, context.settings); + } + function processDynamicImport(source) { hasDynamicImports = true; if (source.type !== 'Literal') { @@ -264,10 +399,6 @@ export default class ExportMapBuilder { const namespaces = new Map(); - function remotePath(value) { - return resolve.relative(value, path, context.settings); - } - function resolveImport(value) { const rp = remotePath(value); if (rp == null) { return null; } @@ -324,27 +455,6 @@ export default class ExportMapBuilder { m.reexports.set(s.exported.name, { local, getImport: () => resolveImport(nsource) }); } - function captureDependencyWithSpecifiers(n) { - // import type { Foo } (TS and Flow); import typeof { Foo } (Flow) - const declarationIsType = n.importKind === 'type' || n.importKind === 'typeof'; - // import './foo' or import {} from './foo' (both 0 specifiers) is a side effect and - // shouldn't be considered to be just importing types - let specifiersOnlyImportingTypes = n.specifiers.length > 0; - const importedSpecifiers = new Set(); - n.specifiers.forEach((specifier) => { - if (specifier.type === 'ImportSpecifier') { - importedSpecifiers.add(specifier.imported.name || specifier.imported.value); - } else if (supportedImportTypes.has(specifier.type)) { - importedSpecifiers.add(specifier.type); - } - - // import { type Foo } (Flow); import { typeof Foo } (Flow) - specifiersOnlyImportingTypes = specifiersOnlyImportingTypes - && (specifier.importKind === 'type' || specifier.importKind === 'typeof'); - }); - captureDependency(n, declarationIsType || specifiersOnlyImportingTypes, importedSpecifiers); - } - function captureDependency({ source }, isOnlyImportingTypes, importedSpecifiers = new Set()) { if (source == null) { return null; } @@ -369,44 +479,28 @@ export default class ExportMapBuilder { return getter; } - const source = makeSourceCode(content, ast); - - function readTsConfig(context) { - const tsconfigInfo = tsConfigLoader({ - cwd: context.parserOptions && context.parserOptions.tsconfigRootDir || process.cwd(), - getEnv: (key) => process.env[key], - }); - try { - if (tsconfigInfo.tsConfigPath !== undefined) { - // Projects not using TypeScript won't have `typescript` installed. - if (!ts) { ts = require('typescript'); } // eslint-disable-line import/no-extraneous-dependencies - - const configFile = ts.readConfigFile(tsconfigInfo.tsConfigPath, ts.sys.readFile); - return ts.parseJsonConfigFileContent( - configFile.config, - ts.sys, - dirname(tsconfigInfo.tsConfigPath), - ); + function captureDependencyWithSpecifiers(n) { + // import type { Foo } (TS and Flow); import typeof { Foo } (Flow) + const declarationIsType = n.importKind === 'type' || n.importKind === 'typeof'; + // import './foo' or import {} from './foo' (both 0 specifiers) is a side effect and + // shouldn't be considered to be just importing types + let specifiersOnlyImportingTypes = n.specifiers.length > 0; + const importedSpecifiers = new Set(); + n.specifiers.forEach((specifier) => { + if (specifier.type === 'ImportSpecifier') { + importedSpecifiers.add(specifier.imported.name || specifier.imported.value); + } else if (supportedImportTypes.has(specifier.type)) { + importedSpecifiers.add(specifier.type); } - } catch (e) { - // Catch any errors - } - return null; + // import { type Foo } (Flow); import { typeof Foo } (Flow) + specifiersOnlyImportingTypes = specifiersOnlyImportingTypes + && (specifier.importKind === 'type' || specifier.importKind === 'typeof'); + }); + captureDependency(n, declarationIsType || specifiersOnlyImportingTypes, importedSpecifiers); } - function isEsModuleInterop() { - const cacheKey = hashObject({ - tsconfigRootDir: context.parserOptions && context.parserOptions.tsconfigRootDir, - }).digest('hex'); - let tsConfig = tsconfigCache.get(cacheKey); - if (typeof tsConfig === 'undefined') { - tsConfig = readTsConfig(context); - tsconfigCache.set(cacheKey, tsConfig); - } - - return tsConfig && tsConfig.options ? tsConfig.options.esModuleInterop : false; - } + const source = makeSourceCode(content, ast); ast.body.forEach(function (n) { if (n.type === 'ExportDefaultDeclaration') { @@ -555,96 +649,3 @@ export default class ExportMapBuilder { return m; } } - -/** - * The creation of this closure is isolated from other scopes - * to avoid over-retention of unrelated variables, which has - * caused memory leaks. See #1266. - */ -function thunkFor(p, context) { - return () => ExportMapBuilder.for(childContext(p, context)); -} - -/** - * Traverse a pattern/identifier node, calling 'callback' - * for each leaf identifier. - * @param {node} pattern - * @param {Function} callback - * @return {void} - */ -export function recursivePatternCapture(pattern, callback) { - switch (pattern.type) { - case 'Identifier': // base case - callback(pattern); - break; - - case 'ObjectPattern': - pattern.properties.forEach((p) => { - if (p.type === 'ExperimentalRestProperty' || p.type === 'RestElement') { - callback(p.argument); - return; - } - recursivePatternCapture(p.value, callback); - }); - break; - - case 'ArrayPattern': - pattern.elements.forEach((element) => { - if (element == null) { return; } - if (element.type === 'ExperimentalRestProperty' || element.type === 'RestElement') { - callback(element.argument); - return; - } - recursivePatternCapture(element, callback); - }); - break; - - case 'AssignmentPattern': - callback(pattern.left); - break; - default: - } -} - -let parserOptionsHash = ''; -let prevParserOptions = ''; -let settingsHash = ''; -let prevSettings = ''; -/** - * 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 - */ -function childContext(path, context) { - const { settings, parserOptions, parserPath } = 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); - } - - return { - cacheKey: String(parserPath) + parserOptionsHash + settingsHash + String(path), - settings, - parserOptions, - parserPath, - path, - }; -} - -/** - * sometimes legacy support isn't _that_ hard... right? - */ -function makeSourceCode(text, ast) { - if (SourceCode.length > 1) { - // ESLint 3 - return new SourceCode(text, ast); - } else { - // ESLint 4, 5 - return new SourceCode({ text, ast }); - } -} From 8587c85a60ccb3839cb2eabe50cf098a4a2c03c2 Mon Sep 17 00:00:00 2001 From: Ivan Rubinson Date: Wed, 27 Mar 2024 18:39:53 +0200 Subject: [PATCH 24/50] [Refactor] `ExportMap`: extract "builder" logic to separate files --- CHANGELOG.md | 2 + src/exportMap/builder.js | 206 +++++++ src/exportMap/captureDependency.js | 60 +++ src/exportMap/childContext.js | 32 ++ src/exportMap/doc.js | 90 ++++ src/{exportMap.js => exportMap/index.js} | 0 src/exportMap/namespace.js | 39 ++ src/exportMap/patternCapture.js | 40 ++ src/exportMap/remotePath.js | 12 + src/exportMap/specifier.js | 32 ++ src/exportMap/typescript.js | 43 ++ src/exportMap/visitor.js | 171 ++++++ src/exportMapBuilder.js | 651 ----------------------- src/rules/default.js | 2 +- src/rules/export.js | 3 +- src/rules/named.js | 2 +- src/rules/namespace.js | 2 +- src/rules/no-cycle.js | 2 +- src/rules/no-deprecated.js | 2 +- src/rules/no-named-as-default-member.js | 2 +- src/rules/no-named-as-default.js | 2 +- src/rules/no-unused-modules.js | 3 +- tests/src/core/getExports.js | 2 +- 23 files changed, 739 insertions(+), 661 deletions(-) create mode 100644 src/exportMap/builder.js create mode 100644 src/exportMap/captureDependency.js create mode 100644 src/exportMap/childContext.js create mode 100644 src/exportMap/doc.js rename src/{exportMap.js => exportMap/index.js} (100%) create mode 100644 src/exportMap/namespace.js create mode 100644 src/exportMap/patternCapture.js create mode 100644 src/exportMap/remotePath.js create mode 100644 src/exportMap/specifier.js create mode 100644 src/exportMap/typescript.js create mode 100644 src/exportMap/visitor.js delete mode 100644 src/exportMapBuilder.js diff --git a/CHANGELOG.md b/CHANGELOG.md index c05ea32c01..a07647c82d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange - [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]) +- [Refactor] `ExportMap`: extract "builder" logic to separate files ([#2991], thanks [@soryy708]) ## [2.29.1] - 2023-12-14 @@ -1114,6 +1115,7 @@ for info on changes for earlier releases. [`memo-parser`]: ./memo-parser/README.md +[#2991]: https://github.com/import-js/eslint-plugin-import/pull/2991 [#2989]: https://github.com/import-js/eslint-plugin-import/pull/2989 [#2987]: https://github.com/import-js/eslint-plugin-import/pull/2987 [#2985]: https://github.com/import-js/eslint-plugin-import/pull/2985 diff --git a/src/exportMap/builder.js b/src/exportMap/builder.js new file mode 100644 index 0000000000..5348dba375 --- /dev/null +++ b/src/exportMap/builder.js @@ -0,0 +1,206 @@ +import fs from 'fs'; + +import doctrine from 'doctrine'; + +import debug from 'debug'; + +import parse from 'eslint-module-utils/parse'; +import visit from 'eslint-module-utils/visit'; +import resolve from 'eslint-module-utils/resolve'; +import isIgnored, { hasValidExtension } from 'eslint-module-utils/ignore'; + +import { hashObject } from 'eslint-module-utils/hash'; +import * as unambiguous from 'eslint-module-utils/unambiguous'; + +import ExportMap from '.'; +import childContext from './childContext'; +import { isEsModuleInterop } from './typescript'; +import { RemotePath } from './remotePath'; +import ImportExportVisitorBuilder from './visitor'; + +const log = debug('eslint-plugin-import:ExportMap'); + +const exportCache = new Map(); + +/** + * The creation of this closure is isolated from other scopes + * to avoid over-retention of unrelated variables, which has + * caused memory leaks. See #1266. + */ +function thunkFor(p, context) { + // eslint-disable-next-line no-use-before-define + return () => ExportMapBuilder.for(childContext(p, context)); +} + +export default class ExportMapBuilder { + static get(source, context) { + const path = resolve(source, context); + if (path == null) { return null; } + + return ExportMapBuilder.for(childContext(path, context)); + } + + static for(context) { + const { path } = context; + + const cacheKey = context.cacheKey || hashObject(context).digest('hex'); + let exportMap = exportCache.get(cacheKey); + + // return cached ignore + if (exportMap === null) { return null; } + + const stats = fs.statSync(path); + if (exportMap != null) { + // date equality check + if (exportMap.mtime - stats.mtime === 0) { + return exportMap; + } + // future: check content equality? + } + + // check valid extensions first + if (!hasValidExtension(path, context)) { + exportCache.set(cacheKey, null); + return null; + } + + // check for and cache ignore + if (isIgnored(path, context)) { + log('ignored path due to ignore settings:', path); + exportCache.set(cacheKey, null); + return null; + } + + const content = fs.readFileSync(path, { encoding: 'utf8' }); + + // check for and cache unambiguous modules + if (!unambiguous.test(content)) { + log('ignored path due to unambiguous regex:', path); + exportCache.set(cacheKey, null); + return null; + } + + log('cache miss', cacheKey, 'for path', path); + exportMap = ExportMapBuilder.parse(path, content, context); + + // ambiguous modules return null + if (exportMap == null) { + log('ignored path due to ambiguous parse:', path); + exportCache.set(cacheKey, null); + return null; + } + + exportMap.mtime = stats.mtime; + + exportCache.set(cacheKey, exportMap); + return exportMap; + } + + static parse(path, content, context) { + const exportMap = new ExportMap(path); + const isEsModuleInteropTrue = isEsModuleInterop(context); + + let ast; + let visitorKeys; + try { + const result = parse(path, content, context); + ast = result.ast; + visitorKeys = result.visitorKeys; + } catch (err) { + exportMap.errors.push(err); + return exportMap; // can't continue + } + + exportMap.visitorKeys = visitorKeys; + + let hasDynamicImports = false; + + const remotePathResolver = new RemotePath(path, context); + + function processDynamicImport(source) { + hasDynamicImports = true; + if (source.type !== 'Literal') { + return null; + } + const p = remotePathResolver.resolve(source.value); + if (p == null) { + return null; + } + const importedSpecifiers = new Set(); + importedSpecifiers.add('ImportNamespaceSpecifier'); + const getter = thunkFor(p, context); + exportMap.imports.set(p, { + getter, + declarations: new Set([{ + source: { + // capturing actual node reference holds full AST in memory! + value: source.value, + loc: source.loc, + }, + importedSpecifiers, + dynamic: true, + }]), + }); + } + + visit(ast, visitorKeys, { + ImportExpression(node) { + processDynamicImport(node.source); + }, + CallExpression(node) { + if (node.callee.type === 'Import') { + processDynamicImport(node.arguments[0]); + } + }, + }); + + const unambiguouslyESM = unambiguous.isModule(ast); + if (!unambiguouslyESM && !hasDynamicImports) { return null; } + + // attempt to collect module doc + if (ast.comments) { + ast.comments.some((c) => { + if (c.type !== 'Block') { return false; } + try { + const doc = doctrine.parse(c.value, { unwrap: true }); + if (doc.tags.some((t) => t.title === 'module')) { + exportMap.doc = doc; + return true; + } + } catch (err) { /* ignore */ } + return false; + }); + } + + const visitorBuilder = new ImportExportVisitorBuilder( + path, + context, + exportMap, + ExportMapBuilder, + content, + ast, + isEsModuleInteropTrue, + thunkFor, + ); + ast.body.forEach(function (astNode) { + const visitor = visitorBuilder.build(astNode); + + if (visitor[astNode.type]) { + visitor[astNode.type].call(visitorBuilder); + } + }); + + if ( + isEsModuleInteropTrue // esModuleInterop is on in tsconfig + && exportMap.namespace.size > 0 // anything is exported + && !exportMap.namespace.has('default') // and default isn't added already + ) { + exportMap.namespace.set('default', {}); // add default export + } + + if (unambiguouslyESM) { + exportMap.parseGoal = 'Module'; + } + return exportMap; + } +} diff --git a/src/exportMap/captureDependency.js b/src/exportMap/captureDependency.js new file mode 100644 index 0000000000..9ad37d0e20 --- /dev/null +++ b/src/exportMap/captureDependency.js @@ -0,0 +1,60 @@ +export function captureDependency( + { source }, + isOnlyImportingTypes, + remotePathResolver, + exportMap, + context, + thunkFor, + importedSpecifiers = new Set(), +) { + if (source == null) { return null; } + + const p = remotePathResolver.resolve(source.value); + if (p == null) { return null; } + + const declarationMetadata = { + // capturing actual node reference holds full AST in memory! + source: { value: source.value, loc: source.loc }, + isOnlyImportingTypes, + importedSpecifiers, + }; + + const existing = exportMap.imports.get(p); + if (existing != null) { + existing.declarations.add(declarationMetadata); + return existing.getter; + } + + const getter = thunkFor(p, context); + exportMap.imports.set(p, { getter, declarations: new Set([declarationMetadata]) }); + return getter; +} + +const supportedImportTypes = new Set(['ImportDefaultSpecifier', 'ImportNamespaceSpecifier']); + +export function captureDependencyWithSpecifiers( + n, + remotePathResolver, + exportMap, + context, + thunkFor, +) { + // import type { Foo } (TS and Flow); import typeof { Foo } (Flow) + const declarationIsType = n.importKind === 'type' || n.importKind === 'typeof'; + // import './foo' or import {} from './foo' (both 0 specifiers) is a side effect and + // shouldn't be considered to be just importing types + let specifiersOnlyImportingTypes = n.specifiers.length > 0; + const importedSpecifiers = new Set(); + n.specifiers.forEach((specifier) => { + if (specifier.type === 'ImportSpecifier') { + importedSpecifiers.add(specifier.imported.name || specifier.imported.value); + } else if (supportedImportTypes.has(specifier.type)) { + importedSpecifiers.add(specifier.type); + } + + // import { type Foo } (Flow); import { typeof Foo } (Flow) + specifiersOnlyImportingTypes = specifiersOnlyImportingTypes + && (specifier.importKind === 'type' || specifier.importKind === 'typeof'); + }); + captureDependency(n, declarationIsType || specifiersOnlyImportingTypes, remotePathResolver, exportMap, context, thunkFor, importedSpecifiers); +} diff --git a/src/exportMap/childContext.js b/src/exportMap/childContext.js new file mode 100644 index 0000000000..5f82b8e575 --- /dev/null +++ b/src/exportMap/childContext.js @@ -0,0 +1,32 @@ +import { hashObject } from 'eslint-module-utils/hash'; + +let parserOptionsHash = ''; +let prevParserOptions = ''; +let settingsHash = ''; +let prevSettings = ''; + +/** + * 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; + + 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); + } + + return { + cacheKey: String(parserPath) + parserOptionsHash + settingsHash + String(path), + settings, + parserOptions, + parserPath, + path, + }; +} diff --git a/src/exportMap/doc.js b/src/exportMap/doc.js new file mode 100644 index 0000000000..c721ae25fc --- /dev/null +++ b/src/exportMap/doc.js @@ -0,0 +1,90 @@ +import doctrine from 'doctrine'; + +/** + * parse docs from the first node that has leading comments + */ +export function captureDoc(source, docStyleParsers, ...nodes) { + const metadata = {}; + + // 'some' short-circuits on first 'true' + nodes.some((n) => { + try { + + let leadingComments; + + // n.leadingComments is legacy `attachComments` behavior + if ('leadingComments' in n) { + leadingComments = n.leadingComments; + } else if (n.range) { + leadingComments = source.getCommentsBefore(n); + } + + if (!leadingComments || leadingComments.length === 0) { return false; } + + for (const name in docStyleParsers) { + const doc = docStyleParsers[name](leadingComments); + if (doc) { + metadata.doc = doc; + } + } + + return true; + } catch (err) { + return false; + } + }); + + return metadata; +} + +/** + * parse JSDoc from leading comments + * @param {object[]} comments + * @return {{ doc: object }} + */ +function captureJsDoc(comments) { + let doc; + + // capture XSDoc + comments.forEach((comment) => { + // skip non-block comments + if (comment.type !== 'Block') { return; } + try { + doc = doctrine.parse(comment.value, { unwrap: true }); + } catch (err) { + /* don't care, for now? maybe add to `errors?` */ + } + }); + + return doc; +} + +/** + * parse TomDoc section from comments + */ +function captureTomDoc(comments) { + // collect lines up to first paragraph break + const lines = []; + for (let i = 0; i < comments.length; i++) { + const comment = comments[i]; + if (comment.value.match(/^\s*$/)) { break; } + lines.push(comment.value.trim()); + } + + // return doctrine-like object + const statusMatch = lines.join(' ').match(/^(Public|Internal|Deprecated):\s*(.+)/); + if (statusMatch) { + return { + description: statusMatch[2], + tags: [{ + title: statusMatch[1].toLowerCase(), + description: statusMatch[2], + }], + }; + } +} + +export const availableDocStyleParsers = { + jsdoc: captureJsDoc, + tomdoc: captureTomDoc, +}; diff --git a/src/exportMap.js b/src/exportMap/index.js similarity index 100% rename from src/exportMap.js rename to src/exportMap/index.js diff --git a/src/exportMap/namespace.js b/src/exportMap/namespace.js new file mode 100644 index 0000000000..370f47579d --- /dev/null +++ b/src/exportMap/namespace.js @@ -0,0 +1,39 @@ +import childContext from './childContext'; +import { RemotePath } from './remotePath'; + +export default class Namespace { + constructor( + path, + context, + ExportMapBuilder, + ) { + this.remotePathResolver = new RemotePath(path, context); + this.context = context; + this.ExportMapBuilder = ExportMapBuilder; + this.namespaces = new Map(); + } + + resolveImport(value) { + const rp = this.remotePathResolver.resolve(value); + if (rp == null) { return null; } + return this.ExportMapBuilder.for(childContext(rp, this.context)); + } + + getNamespace(identifier) { + if (!this.namespaces.has(identifier.name)) { return; } + return () => this.resolveImport(this.namespaces.get(identifier.name)); + } + + add(object, identifier) { + const nsfn = this.getNamespace(identifier); + if (nsfn) { + Object.defineProperty(object, 'namespace', { get: nsfn }); + } + + return object; + } + + rawSet(name, value) { + this.namespaces.set(name, value); + } +} diff --git a/src/exportMap/patternCapture.js b/src/exportMap/patternCapture.js new file mode 100644 index 0000000000..5bc9806417 --- /dev/null +++ b/src/exportMap/patternCapture.js @@ -0,0 +1,40 @@ +/** + * Traverse a pattern/identifier node, calling 'callback' + * for each leaf identifier. + * @param {node} pattern + * @param {Function} callback + * @return {void} + */ +export default function recursivePatternCapture(pattern, callback) { + switch (pattern.type) { + case 'Identifier': // base case + callback(pattern); + break; + + case 'ObjectPattern': + pattern.properties.forEach((p) => { + if (p.type === 'ExperimentalRestProperty' || p.type === 'RestElement') { + callback(p.argument); + return; + } + recursivePatternCapture(p.value, callback); + }); + break; + + case 'ArrayPattern': + pattern.elements.forEach((element) => { + if (element == null) { return; } + if (element.type === 'ExperimentalRestProperty' || element.type === 'RestElement') { + callback(element.argument); + return; + } + recursivePatternCapture(element, callback); + }); + break; + + case 'AssignmentPattern': + callback(pattern.left); + break; + default: + } +} diff --git a/src/exportMap/remotePath.js b/src/exportMap/remotePath.js new file mode 100644 index 0000000000..0dc5fc0954 --- /dev/null +++ b/src/exportMap/remotePath.js @@ -0,0 +1,12 @@ +import resolve from 'eslint-module-utils/resolve'; + +export class RemotePath { + constructor(path, context) { + this.path = path; + this.context = context; + } + + resolve(value) { + return resolve.relative(value, this.path, this.context.settings); + } +} diff --git a/src/exportMap/specifier.js b/src/exportMap/specifier.js new file mode 100644 index 0000000000..dfaaf618e4 --- /dev/null +++ b/src/exportMap/specifier.js @@ -0,0 +1,32 @@ +export default function processSpecifier(specifier, astNode, exportMap, namespace) { + const nsource = astNode.source && astNode.source.value; + const exportMeta = {}; + let local; + + switch (specifier.type) { + case 'ExportDefaultSpecifier': + if (!nsource) { return; } + local = 'default'; + break; + case 'ExportNamespaceSpecifier': + exportMap.namespace.set(specifier.exported.name, Object.defineProperty(exportMeta, 'namespace', { + get() { return namespace.resolveImport(nsource); }, + })); + return; + case 'ExportAllDeclaration': + exportMap.namespace.set(specifier.exported.name || specifier.exported.value, namespace.add(exportMeta, specifier.source.value)); + return; + case 'ExportSpecifier': + if (!astNode.source) { + exportMap.namespace.set(specifier.exported.name || specifier.exported.value, namespace.add(exportMeta, specifier.local)); + return; + } + // else falls through + default: + local = specifier.local.name; + break; + } + + // todo: JSDoc + exportMap.reexports.set(specifier.exported.name, { local, getImport: () => namespace.resolveImport(nsource) }); +} diff --git a/src/exportMap/typescript.js b/src/exportMap/typescript.js new file mode 100644 index 0000000000..7db4356da8 --- /dev/null +++ b/src/exportMap/typescript.js @@ -0,0 +1,43 @@ +import { dirname } from 'path'; +import { tsConfigLoader } from 'tsconfig-paths/lib/tsconfig-loader'; +import { hashObject } from 'eslint-module-utils/hash'; + +let ts; +const tsconfigCache = new Map(); + +function readTsConfig(context) { + const tsconfigInfo = tsConfigLoader({ + cwd: context.parserOptions && context.parserOptions.tsconfigRootDir || process.cwd(), + getEnv: (key) => process.env[key], + }); + try { + if (tsconfigInfo.tsConfigPath !== undefined) { + // Projects not using TypeScript won't have `typescript` installed. + if (!ts) { ts = require('typescript'); } // eslint-disable-line import/no-extraneous-dependencies + + const configFile = ts.readConfigFile(tsconfigInfo.tsConfigPath, ts.sys.readFile); + return ts.parseJsonConfigFileContent( + configFile.config, + ts.sys, + dirname(tsconfigInfo.tsConfigPath), + ); + } + } catch (e) { + // Catch any errors + } + + return null; +} + +export function isEsModuleInterop(context) { + const cacheKey = hashObject({ + tsconfigRootDir: context.parserOptions && context.parserOptions.tsconfigRootDir, + }).digest('hex'); + let tsConfig = tsconfigCache.get(cacheKey); + if (typeof tsConfig === 'undefined') { + tsConfig = readTsConfig(context); + tsconfigCache.set(cacheKey, tsConfig); + } + + return tsConfig && tsConfig.options ? tsConfig.options.esModuleInterop : false; +} diff --git a/src/exportMap/visitor.js b/src/exportMap/visitor.js new file mode 100644 index 0000000000..21c1a7c644 --- /dev/null +++ b/src/exportMap/visitor.js @@ -0,0 +1,171 @@ +import includes from 'array-includes'; +import { SourceCode } from 'eslint'; +import { availableDocStyleParsers, captureDoc } from './doc'; +import Namespace from './namespace'; +import processSpecifier from './specifier'; +import { captureDependency, captureDependencyWithSpecifiers } from './captureDependency'; +import recursivePatternCapture from './patternCapture'; +import { RemotePath } from './remotePath'; + +/** + * sometimes legacy support isn't _that_ hard... right? + */ +function makeSourceCode(text, ast) { + if (SourceCode.length > 1) { + // ESLint 3 + return new SourceCode(text, ast); + } else { + // ESLint 4, 5 + return new SourceCode({ text, ast }); + } +} + +export default class ImportExportVisitorBuilder { + constructor( + path, + context, + exportMap, + ExportMapBuilder, + content, + ast, + isEsModuleInteropTrue, + thunkFor, + ) { + this.context = context; + this.namespace = new Namespace(path, context, ExportMapBuilder); + this.remotePathResolver = new RemotePath(path, context); + this.source = makeSourceCode(content, ast); + this.exportMap = exportMap; + this.ast = ast; + this.isEsModuleInteropTrue = isEsModuleInteropTrue; + this.thunkFor = thunkFor; + const docstyle = this.context.settings && this.context.settings['import/docstyle'] || ['jsdoc']; + this.docStyleParsers = {}; + docstyle.forEach((style) => { + this.docStyleParsers[style] = availableDocStyleParsers[style]; + }); + } + + build(astNode) { + return { + ExportDefaultDeclaration() { + const exportMeta = captureDoc(this.source, this.docStyleParsers, astNode); + if (astNode.declaration.type === 'Identifier') { + this.namespace.add(exportMeta, astNode.declaration); + } + this.exportMap.namespace.set('default', exportMeta); + }, + ExportAllDeclaration() { + const getter = captureDependency(astNode, astNode.exportKind === 'type', this.remotePathResolver, this.exportMap, this.context, this.thunkFor); + if (getter) { this.exportMap.dependencies.add(getter); } + if (astNode.exported) { + processSpecifier(astNode, astNode.exported, this.exportMap, this.namespace); + } + }, + /** capture namespaces in case of later export */ + ImportDeclaration() { + captureDependencyWithSpecifiers(astNode, this.remotePathResolver, this.exportMap, this.context, this.thunkFor); + const ns = astNode.specifiers.find((s) => s.type === 'ImportNamespaceSpecifier'); + if (ns) { + this.namespace.rawSet(ns.local.name, astNode.source.value); + } + }, + ExportNamedDeclaration() { + captureDependencyWithSpecifiers(astNode, this.remotePathResolver, this.exportMap, this.context, this.thunkFor); + // capture declaration + if (astNode.declaration != null) { + switch (astNode.declaration.type) { + case 'FunctionDeclaration': + case 'ClassDeclaration': + case 'TypeAlias': // flowtype with babel-eslint parser + case 'InterfaceDeclaration': + case 'DeclareFunction': + case 'TSDeclareFunction': + case 'TSEnumDeclaration': + case 'TSTypeAliasDeclaration': + case 'TSInterfaceDeclaration': + case 'TSAbstractClassDeclaration': + case 'TSModuleDeclaration': + this.exportMap.namespace.set(astNode.declaration.id.name, captureDoc(this.source, this.docStyleParsers, astNode)); + break; + case 'VariableDeclaration': + astNode.declaration.declarations.forEach((d) => { + recursivePatternCapture( + d.id, + (id) => this.exportMap.namespace.set(id.name, captureDoc(this.source, this.docStyleParsers, d, astNode)), + ); + }); + break; + default: + } + } + astNode.specifiers.forEach((s) => processSpecifier(s, astNode, this.exportMap, this.namespace)); + }, + TSExportAssignment: () => this.typeScriptExport(astNode), + ...this.isEsModuleInteropTrue && { TSNamespaceExportDeclaration: () => this.typeScriptExport(astNode) }, + }; + } + + // This doesn't declare anything, but changes what's being exported. + typeScriptExport(astNode) { + const exportedName = astNode.type === 'TSNamespaceExportDeclaration' + ? (astNode.id || astNode.name).name + : astNode.expression && astNode.expression.name || astNode.expression.id && astNode.expression.id.name || null; + const declTypes = [ + 'VariableDeclaration', + 'ClassDeclaration', + 'TSDeclareFunction', + 'TSEnumDeclaration', + 'TSTypeAliasDeclaration', + 'TSInterfaceDeclaration', + 'TSAbstractClassDeclaration', + 'TSModuleDeclaration', + ]; + const exportedDecls = this.ast.body.filter(({ type, id, declarations }) => includes(declTypes, type) && ( + id && id.name === exportedName || declarations && declarations.find((d) => d.id.name === exportedName) + )); + if (exportedDecls.length === 0) { + // Export is not referencing any local declaration, must be re-exporting + this.exportMap.namespace.set('default', captureDoc(this.source, this.docStyleParsers, astNode)); + return; + } + if ( + this.isEsModuleInteropTrue // esModuleInterop is on in tsconfig + && !this.exportMap.namespace.has('default') // and default isn't added already + ) { + this.exportMap.namespace.set('default', {}); // add default export + } + exportedDecls.forEach((decl) => { + if (decl.type === 'TSModuleDeclaration') { + if (decl.body && decl.body.type === 'TSModuleDeclaration') { + this.exportMap.namespace.set(decl.body.id.name, captureDoc(this.source, this.docStyleParsers, decl.body)); + } else if (decl.body && decl.body.body) { + decl.body.body.forEach((moduleBlockNode) => { + // Export-assignment exports all members in the namespace, + // explicitly exported or not. + const namespaceDecl = moduleBlockNode.type === 'ExportNamedDeclaration' + ? moduleBlockNode.declaration + : moduleBlockNode; + + if (!namespaceDecl) { + // TypeScript can check this for us; we needn't + } else if (namespaceDecl.type === 'VariableDeclaration') { + namespaceDecl.declarations.forEach((d) => recursivePatternCapture(d.id, (id) => this.exportMap.namespace.set( + id.name, + captureDoc(this.source, this.docStyleParsers, decl, namespaceDecl, moduleBlockNode), + )), + ); + } else { + this.exportMap.namespace.set( + namespaceDecl.id.name, + captureDoc(this.source, this.docStyleParsers, moduleBlockNode)); + } + }); + } + } else { + // Export as default + this.exportMap.namespace.set('default', captureDoc(this.source, this.docStyleParsers, decl)); + } + }); + } +} diff --git a/src/exportMapBuilder.js b/src/exportMapBuilder.js deleted file mode 100644 index 5aeb306d0b..0000000000 --- a/src/exportMapBuilder.js +++ /dev/null @@ -1,651 +0,0 @@ -import fs from 'fs'; -import { dirname } from 'path'; - -import doctrine from 'doctrine'; - -import debug from 'debug'; - -import { SourceCode } from 'eslint'; - -import parse from 'eslint-module-utils/parse'; -import visit from 'eslint-module-utils/visit'; -import resolve from 'eslint-module-utils/resolve'; -import isIgnored, { hasValidExtension } from 'eslint-module-utils/ignore'; - -import { hashObject } from 'eslint-module-utils/hash'; -import * as unambiguous from 'eslint-module-utils/unambiguous'; - -import { tsConfigLoader } from 'tsconfig-paths/lib/tsconfig-loader'; - -import includes from 'array-includes'; -import ExportMap from './exportMap'; - -let ts; - -const log = debug('eslint-plugin-import:ExportMap'); - -const exportCache = new Map(); -const tsconfigCache = new Map(); - -/** - * parse docs from the first node that has leading comments - */ -function captureDoc(source, docStyleParsers, ...nodes) { - const metadata = {}; - - // 'some' short-circuits on first 'true' - nodes.some((n) => { - try { - - let leadingComments; - - // n.leadingComments is legacy `attachComments` behavior - if ('leadingComments' in n) { - leadingComments = n.leadingComments; - } else if (n.range) { - leadingComments = source.getCommentsBefore(n); - } - - if (!leadingComments || leadingComments.length === 0) { return false; } - - for (const name in docStyleParsers) { - const doc = docStyleParsers[name](leadingComments); - if (doc) { - metadata.doc = doc; - } - } - - return true; - } catch (err) { - return false; - } - }); - - return metadata; -} - -/** - * parse JSDoc from leading comments - * @param {object[]} comments - * @return {{ doc: object }} - */ -function captureJsDoc(comments) { - let doc; - - // capture XSDoc - comments.forEach((comment) => { - // skip non-block comments - if (comment.type !== 'Block') { return; } - try { - doc = doctrine.parse(comment.value, { unwrap: true }); - } catch (err) { - /* don't care, for now? maybe add to `errors?` */ - } - }); - - return doc; -} - -/** - * parse TomDoc section from comments - */ -function captureTomDoc(comments) { - // collect lines up to first paragraph break - const lines = []; - for (let i = 0; i < comments.length; i++) { - const comment = comments[i]; - if (comment.value.match(/^\s*$/)) { break; } - lines.push(comment.value.trim()); - } - - // return doctrine-like object - const statusMatch = lines.join(' ').match(/^(Public|Internal|Deprecated):\s*(.+)/); - if (statusMatch) { - return { - description: statusMatch[2], - tags: [{ - title: statusMatch[1].toLowerCase(), - description: statusMatch[2], - }], - }; - } -} - -const availableDocStyleParsers = { - jsdoc: captureJsDoc, - tomdoc: captureTomDoc, -}; - -const supportedImportTypes = new Set(['ImportDefaultSpecifier', 'ImportNamespaceSpecifier']); - -let parserOptionsHash = ''; -let prevParserOptions = ''; -let settingsHash = ''; -let prevSettings = ''; -/** - * 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 - */ -function childContext(path, context) { - const { settings, parserOptions, parserPath } = 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); - } - - return { - cacheKey: String(parserPath) + parserOptionsHash + settingsHash + String(path), - settings, - parserOptions, - parserPath, - path, - }; -} - -/** - * sometimes legacy support isn't _that_ hard... right? - */ -function makeSourceCode(text, ast) { - if (SourceCode.length > 1) { - // ESLint 3 - return new SourceCode(text, ast); - } else { - // ESLint 4, 5 - return new SourceCode({ text, ast }); - } -} - -/** - * Traverse a pattern/identifier node, calling 'callback' - * for each leaf identifier. - * @param {node} pattern - * @param {Function} callback - * @return {void} - */ -export function recursivePatternCapture(pattern, callback) { - switch (pattern.type) { - case 'Identifier': // base case - callback(pattern); - break; - - case 'ObjectPattern': - pattern.properties.forEach((p) => { - if (p.type === 'ExperimentalRestProperty' || p.type === 'RestElement') { - callback(p.argument); - return; - } - recursivePatternCapture(p.value, callback); - }); - break; - - case 'ArrayPattern': - pattern.elements.forEach((element) => { - if (element == null) { return; } - if (element.type === 'ExperimentalRestProperty' || element.type === 'RestElement') { - callback(element.argument); - return; - } - recursivePatternCapture(element, callback); - }); - break; - - case 'AssignmentPattern': - callback(pattern.left); - break; - default: - } -} - -/** - * The creation of this closure is isolated from other scopes - * to avoid over-retention of unrelated variables, which has - * caused memory leaks. See #1266. - */ -function thunkFor(p, context) { - // eslint-disable-next-line no-use-before-define - return () => ExportMapBuilder.for(childContext(p, context)); -} - -export default class ExportMapBuilder { - static get(source, context) { - const path = resolve(source, context); - if (path == null) { return null; } - - return ExportMapBuilder.for(childContext(path, context)); - } - - static for(context) { - const { path } = context; - - const cacheKey = context.cacheKey || hashObject(context).digest('hex'); - let exportMap = exportCache.get(cacheKey); - - // return cached ignore - if (exportMap === null) { return null; } - - const stats = fs.statSync(path); - if (exportMap != null) { - // date equality check - if (exportMap.mtime - stats.mtime === 0) { - return exportMap; - } - // future: check content equality? - } - - // check valid extensions first - if (!hasValidExtension(path, context)) { - exportCache.set(cacheKey, null); - return null; - } - - // check for and cache ignore - if (isIgnored(path, context)) { - log('ignored path due to ignore settings:', path); - exportCache.set(cacheKey, null); - return null; - } - - const content = fs.readFileSync(path, { encoding: 'utf8' }); - - // check for and cache unambiguous modules - if (!unambiguous.test(content)) { - log('ignored path due to unambiguous regex:', path); - exportCache.set(cacheKey, null); - return null; - } - - log('cache miss', cacheKey, 'for path', path); - exportMap = ExportMapBuilder.parse(path, content, context); - - // ambiguous modules return null - if (exportMap == null) { - log('ignored path due to ambiguous parse:', path); - exportCache.set(cacheKey, null); - return null; - } - - exportMap.mtime = stats.mtime; - - exportCache.set(cacheKey, exportMap); - return exportMap; - } - - static parse(path, content, context) { - function readTsConfig(context) { - const tsconfigInfo = tsConfigLoader({ - cwd: context.parserOptions && context.parserOptions.tsconfigRootDir || process.cwd(), - getEnv: (key) => process.env[key], - }); - try { - if (tsconfigInfo.tsConfigPath !== undefined) { - // Projects not using TypeScript won't have `typescript` installed. - if (!ts) { ts = require('typescript'); } // eslint-disable-line import/no-extraneous-dependencies - - const configFile = ts.readConfigFile(tsconfigInfo.tsConfigPath, ts.sys.readFile); - return ts.parseJsonConfigFileContent( - configFile.config, - ts.sys, - dirname(tsconfigInfo.tsConfigPath), - ); - } - } catch (e) { - // Catch any errors - } - - return null; - } - - function isEsModuleInterop() { - const cacheKey = hashObject({ - tsconfigRootDir: context.parserOptions && context.parserOptions.tsconfigRootDir, - }).digest('hex'); - let tsConfig = tsconfigCache.get(cacheKey); - if (typeof tsConfig === 'undefined') { - tsConfig = readTsConfig(context); - tsconfigCache.set(cacheKey, tsConfig); - } - - return tsConfig && tsConfig.options ? tsConfig.options.esModuleInterop : false; - } - - const m = new ExportMap(path); - const isEsModuleInteropTrue = isEsModuleInterop(); - - let ast; - let visitorKeys; - try { - const result = parse(path, content, context); - ast = result.ast; - visitorKeys = result.visitorKeys; - } catch (err) { - m.errors.push(err); - return m; // can't continue - } - - m.visitorKeys = visitorKeys; - - let hasDynamicImports = false; - - function remotePath(value) { - return resolve.relative(value, path, context.settings); - } - - function processDynamicImport(source) { - hasDynamicImports = true; - if (source.type !== 'Literal') { - return null; - } - const p = remotePath(source.value); - if (p == null) { - return null; - } - const importedSpecifiers = new Set(); - importedSpecifiers.add('ImportNamespaceSpecifier'); - const getter = thunkFor(p, context); - m.imports.set(p, { - getter, - declarations: new Set([{ - source: { - // capturing actual node reference holds full AST in memory! - value: source.value, - loc: source.loc, - }, - importedSpecifiers, - dynamic: true, - }]), - }); - } - - visit(ast, visitorKeys, { - ImportExpression(node) { - processDynamicImport(node.source); - }, - CallExpression(node) { - if (node.callee.type === 'Import') { - processDynamicImport(node.arguments[0]); - } - }, - }); - - const unambiguouslyESM = unambiguous.isModule(ast); - if (!unambiguouslyESM && !hasDynamicImports) { return null; } - - const docstyle = context.settings && context.settings['import/docstyle'] || ['jsdoc']; - const docStyleParsers = {}; - docstyle.forEach((style) => { - docStyleParsers[style] = availableDocStyleParsers[style]; - }); - - // attempt to collect module doc - if (ast.comments) { - ast.comments.some((c) => { - if (c.type !== 'Block') { return false; } - try { - const doc = doctrine.parse(c.value, { unwrap: true }); - if (doc.tags.some((t) => t.title === 'module')) { - m.doc = doc; - return true; - } - } catch (err) { /* ignore */ } - return false; - }); - } - - const namespaces = new Map(); - - function resolveImport(value) { - const rp = remotePath(value); - if (rp == null) { return null; } - return ExportMapBuilder.for(childContext(rp, context)); - } - - function getNamespace(identifier) { - if (!namespaces.has(identifier.name)) { return; } - - return function () { - return resolveImport(namespaces.get(identifier.name)); - }; - } - - function addNamespace(object, identifier) { - const nsfn = getNamespace(identifier); - if (nsfn) { - Object.defineProperty(object, 'namespace', { get: nsfn }); - } - - return object; - } - - function processSpecifier(s, n, m) { - const nsource = n.source && n.source.value; - const exportMeta = {}; - let local; - - switch (s.type) { - case 'ExportDefaultSpecifier': - if (!nsource) { return; } - local = 'default'; - break; - case 'ExportNamespaceSpecifier': - m.namespace.set(s.exported.name, Object.defineProperty(exportMeta, 'namespace', { - get() { return resolveImport(nsource); }, - })); - return; - case 'ExportAllDeclaration': - m.namespace.set(s.exported.name || s.exported.value, addNamespace(exportMeta, s.source.value)); - return; - case 'ExportSpecifier': - if (!n.source) { - m.namespace.set(s.exported.name || s.exported.value, addNamespace(exportMeta, s.local)); - return; - } - // else falls through - default: - local = s.local.name; - break; - } - - // todo: JSDoc - m.reexports.set(s.exported.name, { local, getImport: () => resolveImport(nsource) }); - } - - function captureDependency({ source }, isOnlyImportingTypes, importedSpecifiers = new Set()) { - if (source == null) { return null; } - - const p = remotePath(source.value); - if (p == null) { return null; } - - const declarationMetadata = { - // capturing actual node reference holds full AST in memory! - source: { value: source.value, loc: source.loc }, - isOnlyImportingTypes, - importedSpecifiers, - }; - - const existing = m.imports.get(p); - if (existing != null) { - existing.declarations.add(declarationMetadata); - return existing.getter; - } - - const getter = thunkFor(p, context); - m.imports.set(p, { getter, declarations: new Set([declarationMetadata]) }); - return getter; - } - - function captureDependencyWithSpecifiers(n) { - // import type { Foo } (TS and Flow); import typeof { Foo } (Flow) - const declarationIsType = n.importKind === 'type' || n.importKind === 'typeof'; - // import './foo' or import {} from './foo' (both 0 specifiers) is a side effect and - // shouldn't be considered to be just importing types - let specifiersOnlyImportingTypes = n.specifiers.length > 0; - const importedSpecifiers = new Set(); - n.specifiers.forEach((specifier) => { - if (specifier.type === 'ImportSpecifier') { - importedSpecifiers.add(specifier.imported.name || specifier.imported.value); - } else if (supportedImportTypes.has(specifier.type)) { - importedSpecifiers.add(specifier.type); - } - - // import { type Foo } (Flow); import { typeof Foo } (Flow) - specifiersOnlyImportingTypes = specifiersOnlyImportingTypes - && (specifier.importKind === 'type' || specifier.importKind === 'typeof'); - }); - captureDependency(n, declarationIsType || specifiersOnlyImportingTypes, importedSpecifiers); - } - - const source = makeSourceCode(content, ast); - - ast.body.forEach(function (n) { - if (n.type === 'ExportDefaultDeclaration') { - const exportMeta = captureDoc(source, docStyleParsers, n); - if (n.declaration.type === 'Identifier') { - addNamespace(exportMeta, n.declaration); - } - m.namespace.set('default', exportMeta); - return; - } - - if (n.type === 'ExportAllDeclaration') { - const getter = captureDependency(n, n.exportKind === 'type'); - if (getter) { m.dependencies.add(getter); } - if (n.exported) { - processSpecifier(n, n.exported, m); - } - return; - } - - // capture namespaces in case of later export - if (n.type === 'ImportDeclaration') { - captureDependencyWithSpecifiers(n); - - const ns = n.specifiers.find((s) => s.type === 'ImportNamespaceSpecifier'); - if (ns) { - namespaces.set(ns.local.name, n.source.value); - } - return; - } - - if (n.type === 'ExportNamedDeclaration') { - captureDependencyWithSpecifiers(n); - - // capture declaration - if (n.declaration != null) { - switch (n.declaration.type) { - case 'FunctionDeclaration': - case 'ClassDeclaration': - case 'TypeAlias': // flowtype with babel-eslint parser - case 'InterfaceDeclaration': - case 'DeclareFunction': - case 'TSDeclareFunction': - case 'TSEnumDeclaration': - case 'TSTypeAliasDeclaration': - case 'TSInterfaceDeclaration': - case 'TSAbstractClassDeclaration': - case 'TSModuleDeclaration': - m.namespace.set(n.declaration.id.name, captureDoc(source, docStyleParsers, n)); - break; - case 'VariableDeclaration': - n.declaration.declarations.forEach((d) => { - recursivePatternCapture( - d.id, - (id) => m.namespace.set(id.name, captureDoc(source, docStyleParsers, d, n)), - ); - }); - break; - default: - } - } - - n.specifiers.forEach((s) => processSpecifier(s, n, m)); - } - - const exports = ['TSExportAssignment']; - if (isEsModuleInteropTrue) { - exports.push('TSNamespaceExportDeclaration'); - } - - // This doesn't declare anything, but changes what's being exported. - if (includes(exports, n.type)) { - const exportedName = n.type === 'TSNamespaceExportDeclaration' - ? (n.id || n.name).name - : n.expression && n.expression.name || n.expression.id && n.expression.id.name || null; - const declTypes = [ - 'VariableDeclaration', - 'ClassDeclaration', - 'TSDeclareFunction', - 'TSEnumDeclaration', - 'TSTypeAliasDeclaration', - 'TSInterfaceDeclaration', - 'TSAbstractClassDeclaration', - 'TSModuleDeclaration', - ]; - const exportedDecls = ast.body.filter(({ type, id, declarations }) => includes(declTypes, type) && ( - id && id.name === exportedName || declarations && declarations.find((d) => d.id.name === exportedName) - )); - if (exportedDecls.length === 0) { - // Export is not referencing any local declaration, must be re-exporting - m.namespace.set('default', captureDoc(source, docStyleParsers, n)); - return; - } - if ( - isEsModuleInteropTrue // esModuleInterop is on in tsconfig - && !m.namespace.has('default') // and default isn't added already - ) { - m.namespace.set('default', {}); // add default export - } - exportedDecls.forEach((decl) => { - if (decl.type === 'TSModuleDeclaration') { - if (decl.body && decl.body.type === 'TSModuleDeclaration') { - m.namespace.set(decl.body.id.name, captureDoc(source, docStyleParsers, decl.body)); - } else if (decl.body && decl.body.body) { - decl.body.body.forEach((moduleBlockNode) => { - // Export-assignment exports all members in the namespace, - // explicitly exported or not. - const namespaceDecl = moduleBlockNode.type === 'ExportNamedDeclaration' - ? moduleBlockNode.declaration - : moduleBlockNode; - - if (!namespaceDecl) { - // TypeScript can check this for us; we needn't - } else if (namespaceDecl.type === 'VariableDeclaration') { - namespaceDecl.declarations.forEach((d) => recursivePatternCapture(d.id, (id) => m.namespace.set( - id.name, - captureDoc(source, docStyleParsers, decl, namespaceDecl, moduleBlockNode), - )), - ); - } else { - m.namespace.set( - namespaceDecl.id.name, - captureDoc(source, docStyleParsers, moduleBlockNode)); - } - }); - } - } else { - // Export as default - m.namespace.set('default', captureDoc(source, docStyleParsers, decl)); - } - }); - } - }); - - if ( - isEsModuleInteropTrue // esModuleInterop is on in tsconfig - && m.namespace.size > 0 // anything is exported - && !m.namespace.has('default') // and default isn't added already - ) { - m.namespace.set('default', {}); // add default export - } - - if (unambiguouslyESM) { - m.parseGoal = 'Module'; - } - return m; - } -} diff --git a/src/rules/default.js b/src/rules/default.js index cbaa49f1fc..0de787c33c 100644 --- a/src/rules/default.js +++ b/src/rules/default.js @@ -1,4 +1,4 @@ -import ExportMapBuilder from '../exportMapBuilder'; +import ExportMapBuilder from '../exportMap/builder'; import docsUrl from '../docsUrl'; module.exports = { diff --git a/src/rules/export.js b/src/rules/export.js index b1dc5ca9ea..197a0eb51c 100644 --- a/src/rules/export.js +++ b/src/rules/export.js @@ -1,4 +1,5 @@ -import ExportMapBuilder, { recursivePatternCapture } from '../exportMapBuilder'; +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'; diff --git a/src/rules/named.js b/src/rules/named.js index 043d72eabe..ed7e5e018b 100644 --- a/src/rules/named.js +++ b/src/rules/named.js @@ -1,5 +1,5 @@ import * as path from 'path'; -import ExportMapBuilder from '../exportMapBuilder'; +import ExportMapBuilder from '../exportMap/builder'; import docsUrl from '../docsUrl'; module.exports = { diff --git a/src/rules/namespace.js b/src/rules/namespace.js index e1ca2870b1..60a4220de2 100644 --- a/src/rules/namespace.js +++ b/src/rules/namespace.js @@ -1,5 +1,5 @@ import declaredScope from 'eslint-module-utils/declaredScope'; -import ExportMapBuilder from '../exportMapBuilder'; +import ExportMapBuilder from '../exportMap/builder'; import ExportMap from '../exportMap'; import importDeclaration from '../importDeclaration'; import docsUrl from '../docsUrl'; diff --git a/src/rules/no-cycle.js b/src/rules/no-cycle.js index b7b907b062..e65ff11a49 100644 --- a/src/rules/no-cycle.js +++ b/src/rules/no-cycle.js @@ -4,7 +4,7 @@ */ import resolve from 'eslint-module-utils/resolve'; -import ExportMapBuilder from '../exportMapBuilder'; +import ExportMapBuilder from '../exportMap/builder'; import { isExternalModule } from '../core/importType'; import moduleVisitor, { makeOptionsSchema } from 'eslint-module-utils/moduleVisitor'; import docsUrl from '../docsUrl'; diff --git a/src/rules/no-deprecated.js b/src/rules/no-deprecated.js index 50072f3f85..b4299a51d4 100644 --- a/src/rules/no-deprecated.js +++ b/src/rules/no-deprecated.js @@ -1,5 +1,5 @@ import declaredScope from 'eslint-module-utils/declaredScope'; -import ExportMapBuilder from '../exportMapBuilder'; +import ExportMapBuilder from '../exportMap/builder'; import ExportMap from '../exportMap'; import docsUrl from '../docsUrl'; diff --git a/src/rules/no-named-as-default-member.js b/src/rules/no-named-as-default-member.js index d594c58433..54bec64a2a 100644 --- a/src/rules/no-named-as-default-member.js +++ b/src/rules/no-named-as-default-member.js @@ -4,7 +4,7 @@ * @copyright 2016 Desmond Brand. All rights reserved. * See LICENSE in root directory for full license. */ -import ExportMapBuilder from '../exportMapBuilder'; +import ExportMapBuilder from '../exportMap/builder'; import importDeclaration from '../importDeclaration'; import docsUrl from '../docsUrl'; diff --git a/src/rules/no-named-as-default.js b/src/rules/no-named-as-default.js index 3e73ff2f44..5b24f8e883 100644 --- a/src/rules/no-named-as-default.js +++ b/src/rules/no-named-as-default.js @@ -1,4 +1,4 @@ -import ExportMapBuilder from '../exportMapBuilder'; +import ExportMapBuilder from '../exportMap/builder'; import importDeclaration from '../importDeclaration'; import docsUrl from '../docsUrl'; diff --git a/src/rules/no-unused-modules.js b/src/rules/no-unused-modules.js index 812efffbca..0ad330b486 100644 --- a/src/rules/no-unused-modules.js +++ b/src/rules/no-unused-modules.js @@ -13,7 +13,8 @@ import values from 'object.values'; import includes from 'array-includes'; import flatMap from 'array.prototype.flatmap'; -import ExportMapBuilder, { recursivePatternCapture } from '../exportMapBuilder'; +import ExportMapBuilder from '../exportMap/builder'; +import recursivePatternCapture from '../exportMap/patternCapture'; import docsUrl from '../docsUrl'; let FileEnumerator; diff --git a/tests/src/core/getExports.js b/tests/src/core/getExports.js index 611a13055f..76003410d5 100644 --- a/tests/src/core/getExports.js +++ b/tests/src/core/getExports.js @@ -4,7 +4,7 @@ import sinon from 'sinon'; import eslintPkg from 'eslint/package.json'; import typescriptPkg from 'typescript/package.json'; import * as tsConfigLoader from 'tsconfig-paths/lib/tsconfig-loader'; -import ExportMapBuilder from '../../../src/exportMapBuilder'; +import ExportMapBuilder from '../../../src/exportMap/builder'; import * as fs from 'fs'; From f77ceb679d59ced5d9a633123385470a9eea10d9 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Sun, 7 Apr 2024 12:55:28 +1200 Subject: [PATCH 25/50] [actions] cancel in-progress runs on PR updates --- .github/workflows/native-wsl.yml | 4 ++++ .github/workflows/node-4+.yml | 4 ++++ .github/workflows/packages.yml | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/.github/workflows/native-wsl.yml b/.github/workflows/native-wsl.yml index 893d2248d1..5e8318899e 100644 --- a/.github/workflows/native-wsl.yml +++ b/.github/workflows/native-wsl.yml @@ -2,6 +2,10 @@ name: Native and WSL on: [push, pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: build: runs-on: ${{ matrix.os }} diff --git a/.github/workflows/node-4+.yml b/.github/workflows/node-4+.yml index 60fa609dbe..f2dad098ca 100644 --- a/.github/workflows/node-4+.yml +++ b/.github/workflows/node-4+.yml @@ -2,6 +2,10 @@ name: 'Tests: node.js' on: [pull_request, push] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + permissions: contents: read diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index 213e1a43ce..f73f8e18ff 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -2,6 +2,10 @@ name: 'Tests: packages' on: [pull_request, push] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + permissions: contents: read From c0ac54b8a721c2b1c9048838acc4d6282f4fe7a7 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Thu, 25 Apr 2024 10:57:27 -0700 Subject: [PATCH 26/50] [Dev Deps] pin `find-babel-config` to v1.2.0, due to a breaking change in v1.2.1 See https://github.com/tleunen/find-babel-config/issues/70#issuecomment-2077838243 --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 638942f97c..b9fa1eb35f 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "eslint-plugin-eslint-plugin": "^2.3.0", "eslint-plugin-import": "2.x", "eslint-plugin-json": "^2.1.2", + "find-babel-config": "=1.2.0", "fs-copy-file-sync": "^1.1.1", "glob": "^7.2.3", "in-publish": "^2.0.1", From a3a7176f6bc8a5e614eda95df74c43c30e148022 Mon Sep 17 00:00:00 2001 From: Ankit Sardesai Date: Tue, 23 Apr 2024 21:40:29 -0700 Subject: [PATCH 27/50] [New] `dynamic-import-chunkname`: Allow empty chunk name when webpackMode: 'eager' is set; add suggestions to remove name in eager mode' --- CHANGELOG.md | 3 + README.md | 2 +- docs/rules/dynamic-import-chunkname.md | 9 + src/rules/dynamic-import-chunkname.js | 51 ++++- tests/src/rules/dynamic-import-chunkname.js | 221 ++++++++++++++++++-- 5 files changed, 268 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a07647c82d..c7cd6c4431 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ### 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]) ### Changed - [Docs] `no-extraneous-dependencies`: Make glob pattern description more explicit ([#2944], thanks [@mulztob]) @@ -1115,6 +1116,7 @@ for info on changes for earlier releases. [`memo-parser`]: ./memo-parser/README.md +[#3004]: https://github.com/import-js/eslint-plugin-import/pull/3004 [#2991]: https://github.com/import-js/eslint-plugin-import/pull/2991 [#2989]: https://github.com/import-js/eslint-plugin-import/pull/2989 [#2987]: https://github.com/import-js/eslint-plugin-import/pull/2987 @@ -1701,6 +1703,7 @@ for info on changes for earlier releases. [@aladdin-add]: https://github.com/aladdin-add [@alex-page]: https://github.com/alex-page [@alexgorbatchev]: https://github.com/alexgorbatchev +[@amsardesai]: https://github.com/amsardesai [@andreubotella]: https://github.com/andreubotella [@AndrewLeedham]: https://github.com/AndrewLeedham [@andyogo]: https://github.com/andyogo diff --git a/README.md b/README.md index d6f107d1c9..1fd113c7d0 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a | Name                            | Description | 💼 | ⚠️ | 🚫 | 🔧 | 💡 | ❌ | | :------------------------------------------------------------------------------- | :------------------------------------------------------------------------- | :- | :---- | :- | :- | :- | :- | | [consistent-type-specifier-style](docs/rules/consistent-type-specifier-style.md) | Enforce or ban the use of inline type-only markers for named imports. | | | | 🔧 | | | -| [dynamic-import-chunkname](docs/rules/dynamic-import-chunkname.md) | Enforce a leading comment with the webpackChunkName for dynamic imports. | | | | | | | +| [dynamic-import-chunkname](docs/rules/dynamic-import-chunkname.md) | Enforce a leading comment with the webpackChunkName for dynamic imports. | | | | | 💡 | | | [exports-last](docs/rules/exports-last.md) | Ensure all exports appear after other statements. | | | | | | | | [extensions](docs/rules/extensions.md) | Ensure consistent use of file extension within the import path. | | | | | | | | [first](docs/rules/first.md) | Ensure all imports appear before other statements. | | | | 🔧 | | | diff --git a/docs/rules/dynamic-import-chunkname.md b/docs/rules/dynamic-import-chunkname.md index dd526c8913..de554148ee 100644 --- a/docs/rules/dynamic-import-chunkname.md +++ b/docs/rules/dynamic-import-chunkname.md @@ -1,5 +1,7 @@ # import/dynamic-import-chunkname +💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions). + This rule reports any dynamic imports without a webpackChunkName specified in a leading block comment in the proper format. @@ -56,6 +58,13 @@ import( // webpackChunkName: "someModule" 'someModule', ); + +// chunk names are disallowed when eager mode is set +import( + /* webpackMode: "eager" */ + /* webpackChunkName: "someModule" */ + 'someModule', +) ``` ### valid diff --git a/src/rules/dynamic-import-chunkname.js b/src/rules/dynamic-import-chunkname.js index a62e5c6c12..a72b04d123 100644 --- a/src/rules/dynamic-import-chunkname.js +++ b/src/rules/dynamic-import-chunkname.js @@ -27,6 +27,7 @@ module.exports = { }, }, }], + hasSuggestions: true, }, create(context) { @@ -36,8 +37,10 @@ module.exports = { const paddedCommentRegex = /^ (\S[\s\S]+\S) $/; const commentStyleRegex = /^( ((webpackChunkName: .+)|((webpackPrefetch|webpackPreload): (true|false|-?[0-9]+))|(webpackIgnore: (true|false))|((webpackInclude|webpackExclude): \/.*\/)|(webpackMode: ["'](lazy|lazy-once|eager|weak)["'])|(webpackExports: (['"]\w+['"]|\[(['"]\w+['"], *)+(['"]\w+['"]*)\]))),?)+ $/; - const chunkSubstrFormat = ` webpackChunkName: ["']${webpackChunknameFormat}["'],? `; + const chunkSubstrFormat = `webpackChunkName: ["']${webpackChunknameFormat}["'],? `; const chunkSubstrRegex = new RegExp(chunkSubstrFormat); + const eagerModeFormat = `webpackMode: ["']eager["'],? `; + const eagerModeRegex = new RegExp(eagerModeFormat); function run(node, arg) { const sourceCode = context.getSourceCode(); @@ -54,6 +57,7 @@ module.exports = { } let isChunknamePresent = false; + let isEagerModePresent = false; for (const comment of leadingComments) { if (comment.type !== 'Block') { @@ -92,12 +96,55 @@ module.exports = { return; } + if (eagerModeRegex.test(comment.value)) { + isEagerModePresent = true; + } + if (chunkSubstrRegex.test(comment.value)) { isChunknamePresent = true; } } - if (!isChunknamePresent && !allowEmpty) { + if (isChunknamePresent && isEagerModePresent) { + context.report({ + node, + message: 'dynamic imports using eager mode do not need a webpackChunkName', + suggest: [ + { + desc: 'Remove webpackChunkName', + fix(fixer) { + for (const comment of leadingComments) { + if (chunkSubstrRegex.test(comment.value)) { + const replacement = comment.value.replace(chunkSubstrRegex, '').trim().replace(/,$/, ''); + if (replacement === '') { + return fixer.remove(comment); + } else { + return fixer.replaceText(comment, `/* ${replacement} */`); + } + } + } + }, + }, + { + desc: 'Remove webpackMode', + fix(fixer) { + for (const comment of leadingComments) { + if (eagerModeRegex.test(comment.value)) { + const replacement = comment.value.replace(eagerModeRegex, '').trim().replace(/,$/, ''); + if (replacement === '') { + return fixer.remove(comment); + } else { + return fixer.replaceText(comment, `/* ${replacement} */`); + } + } + } + }, + }, + ], + }); + } + + if (!isChunknamePresent && !allowEmpty && !isEagerModePresent) { context.report({ node, message: diff --git a/tests/src/rules/dynamic-import-chunkname.js b/tests/src/rules/dynamic-import-chunkname.js index c710507b26..6afd834ab0 100644 --- a/tests/src/rules/dynamic-import-chunkname.js +++ b/tests/src/rules/dynamic-import-chunkname.js @@ -26,8 +26,9 @@ const nonBlockCommentError = 'dynamic imports require a /* foo */ style comment, const noPaddingCommentError = 'dynamic imports require a block comment padded with spaces - /* foo */'; const invalidSyntaxCommentError = 'dynamic imports require a "webpack" comment with valid syntax'; const commentFormatError = `dynamic imports require a "webpack" comment with valid syntax`; -const chunkNameFormatError = `dynamic imports require a leading comment in the form /* webpackChunkName: ["']${commentFormat}["'],? */`; -const pickyChunkNameFormatError = `dynamic imports require a leading comment in the form /* webpackChunkName: ["']${pickyCommentFormat}["'],? */`; +const chunkNameFormatError = `dynamic imports require a leading comment in the form /*webpackChunkName: ["']${commentFormat}["'],? */`; +const pickyChunkNameFormatError = `dynamic imports require a leading comment in the form /*webpackChunkName: ["']${pickyCommentFormat}["'],? */`; +const eagerModeError = `dynamic imports using eager mode do not need a webpackChunkName`; ruleTester.run('dynamic-import-chunkname', rule, { valid: [ @@ -354,7 +355,6 @@ ruleTester.run('dynamic-import-chunkname', rule, { }, { code: `import( - /* webpackChunkName: "someModule" */ /* webpackMode: "eager" */ 'someModule' )`, @@ -412,7 +412,7 @@ ruleTester.run('dynamic-import-chunkname', rule, { /* webpackPrefetch: true */ /* webpackPreload: true */ /* webpackIgnore: false */ - /* webpackMode: "eager" */ + /* webpackMode: "lazy" */ /* webpackExports: ["default", "named"] */ 'someModule' )`, @@ -981,6 +981,42 @@ ruleTester.run('dynamic-import-chunkname', rule, { type: 'CallExpression', }], }, + { + code: `import( + /* webpackChunkName: "someModule" */ + /* webpackMode: "eager" */ + 'someModule' + )`, + options, + parser, + output: `import( + /* webpackChunkName: "someModule" */ + /* webpackMode: "eager" */ + 'someModule' + )`, + errors: [{ + message: eagerModeError, + type: 'CallExpression', + suggestions: [ + { + desc: 'Remove webpackChunkName', + output: `import( + + /* webpackMode: "eager" */ + 'someModule' + )`, + }, + { + desc: 'Remove webpackMode', + output: `import( + /* webpackChunkName: "someModule" */ + + 'someModule' + )`, + }, + ], + }], + }, ], }); @@ -1213,15 +1249,6 @@ context('TypeScript', () => { options, parser: typescriptParser, }, - { - code: `import( - /* webpackChunkName: "someModule" */ - /* webpackMode: "lazy" */ - 'someModule' - )`, - options, - parser: typescriptParser, - }, { code: `import( /* webpackChunkName: 'someModule', webpackMode: 'lazy' */ @@ -1242,7 +1269,7 @@ context('TypeScript', () => { { code: `import( /* webpackChunkName: "someModule" */ - /* webpackMode: "eager" */ + /* webpackMode: "lazy" */ 'someModule' )`, options, @@ -1299,13 +1326,21 @@ context('TypeScript', () => { /* webpackPrefetch: true */ /* webpackPreload: true */ /* webpackIgnore: false */ - /* webpackMode: "eager" */ + /* webpackMode: "lazy" */ /* webpackExports: ["default", "named"] */ 'someModule' )`, options, parser: typescriptParser, }, + { + code: `import( + /* webpackMode: "eager" */ + 'someModule' + )`, + options, + parser: typescriptParser, + }, ], invalid: [ { @@ -1752,6 +1787,162 @@ context('TypeScript', () => { type: nodeType, }], }, + { + code: `import( + /* webpackChunkName: "someModule", webpackMode: "eager" */ + 'someModule' + )`, + options, + parser: typescriptParser, + output: `import( + /* webpackChunkName: "someModule", webpackMode: "eager" */ + 'someModule' + )`, + errors: [{ + message: eagerModeError, + type: nodeType, + suggestions: [ + { + desc: 'Remove webpackChunkName', + output: `import( + /* webpackMode: "eager" */ + 'someModule' + )`, + }, + { + desc: 'Remove webpackMode', + output: `import( + /* webpackChunkName: "someModule" */ + 'someModule' + )`, + }, + ], + }], + }, + { + code: ` + import( + /* webpackMode: "eager", webpackChunkName: "someModule" */ + 'someModule' + ) + `, + options, + parser: typescriptParser, + output: ` + import( + /* webpackMode: "eager", webpackChunkName: "someModule" */ + 'someModule' + ) + `, + errors: [{ + message: eagerModeError, + type: nodeType, + suggestions: [ + { + desc: 'Remove webpackChunkName', + output: ` + import( + /* webpackMode: "eager" */ + 'someModule' + ) + `, + }, + { + desc: 'Remove webpackMode', + output: ` + import( + /* webpackChunkName: "someModule" */ + 'someModule' + ) + `, + }, + ], + }], + }, + { + code: ` + import( + /* webpackMode: "eager", webpackPrefetch: true, webpackChunkName: "someModule" */ + 'someModule' + ) + `, + options, + parser: typescriptParser, + output: ` + import( + /* webpackMode: "eager", webpackPrefetch: true, webpackChunkName: "someModule" */ + 'someModule' + ) + `, + errors: [{ + message: eagerModeError, + type: nodeType, + suggestions: [ + { + desc: 'Remove webpackChunkName', + output: ` + import( + /* webpackMode: "eager", webpackPrefetch: true */ + 'someModule' + ) + `, + }, + { + desc: 'Remove webpackMode', + output: ` + import( + /* webpackPrefetch: true, webpackChunkName: "someModule" */ + 'someModule' + ) + `, + }, + ], + }], + }, + { + code: ` + import( + /* webpackChunkName: "someModule" */ + /* webpackMode: "eager" */ + 'someModule' + ) + `, + options, + parser: typescriptParser, + output: ` + import( + /* webpackChunkName: "someModule" */ + /* webpackMode: "eager" */ + 'someModule' + ) + `, + errors: [{ + message: eagerModeError, + type: nodeType, + suggestions: [ + { + desc: 'Remove webpackChunkName', + output: ` + import( + ${''} + /* webpackMode: "eager" */ + 'someModule' + ) + `, + }, + { + desc: 'Remove webpackMode', + output: ` + import( + /* webpackChunkName: "someModule" */ + ${''} + 'someModule' + ) + `, + }, + ], + }], + }, ], }); }); From 6554bd5c30976290024cecc44ef1e96746cf3cf7 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Thu, 23 May 2024 12:47:41 -0700 Subject: [PATCH 28/50] [meta] add `repository.directory` field --- memo-parser/package.json | 3 ++- resolvers/node/package.json | 3 ++- resolvers/webpack/package.json | 3 ++- utils/package.json | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/memo-parser/package.json b/memo-parser/package.json index 723005d21b..b89c3c5ada 100644 --- a/memo-parser/package.json +++ b/memo-parser/package.json @@ -12,7 +12,8 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/import-js/eslint-plugin-import.git" + "url": "git+https://github.com/import-js/eslint-plugin-import.git", + "directory": "memo-parser" }, "keywords": [ "eslint", diff --git a/resolvers/node/package.json b/resolvers/node/package.json index bfaab40413..6f6999e6cb 100644 --- a/resolvers/node/package.json +++ b/resolvers/node/package.json @@ -13,7 +13,8 @@ }, "repository": { "type": "git", - "url": "https://github.com/import-js/eslint-plugin-import" + "url": "https://github.com/import-js/eslint-plugin-import", + "directory": "resolvers/node" }, "keywords": [ "eslint", diff --git a/resolvers/webpack/package.json b/resolvers/webpack/package.json index 3fa47d9362..7f8cb718f1 100644 --- a/resolvers/webpack/package.json +++ b/resolvers/webpack/package.json @@ -14,7 +14,8 @@ ], "repository": { "type": "git", - "url": "git+https://github.com/import-js/eslint-plugin-import.git" + "url": "git+https://github.com/import-js/eslint-plugin-import.git", + "directory": "resolvers/webpack" }, "keywords": [ "eslint-plugin-import", diff --git a/utils/package.json b/utils/package.json index df4871790b..4704971505 100644 --- a/utils/package.json +++ b/utils/package.json @@ -12,7 +12,8 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/import-js/eslint-plugin-import.git" + "url": "git+https://github.com/import-js/eslint-plugin-import.git", + "directory": "utils" }, "keywords": [ "eslint-plugin-import", From fc361a9998b14b9528d841d8349078a5af2da436 Mon Sep 17 00:00:00 2001 From: U812320 Date: Mon, 3 Jun 2024 13:45:31 +0200 Subject: [PATCH 29/50] [Fix] `no-extraneous-dependencies`: allow wrong path - If you pass only one path to a package.json file, then this path should be correct - If you pass multiple paths, there are some situations when those paths point to a wrong path, this happens typically in a nx monorepo with husky -- NX will run eslint in the projects folder, so we need to grab the root package.json -- Husky will run in the root folder, so one of the path given will be an incorrect path, but we do not want throw there, otherwise the rull will fail --- CHANGELOG.md | 5 +++++ src/rules/no-extraneous-dependencies.js | 17 +++++++++++------ tests/src/rules/no-extraneous-dependencies.js | 9 +++++++++ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7cd6c4431..941cd6ef87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange - [`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]) +### Fixed +- [`no-extraneous-dependencies`]: allow wrong path ([#3012], thanks [@chabb]) + ### Changed - [Docs] `no-extraneous-dependencies`: Make glob pattern description more explicit ([#2944], thanks [@mulztob]) - [`no-unused-modules`]: add console message to help debug [#2866] @@ -1116,6 +1119,7 @@ for info on changes for earlier releases. [`memo-parser`]: ./memo-parser/README.md +[#3012]: https://github.com/import-js/eslint-plugin-import/pull/3012 [#3004]: https://github.com/import-js/eslint-plugin-import/pull/3004 [#2991]: https://github.com/import-js/eslint-plugin-import/pull/2991 [#2989]: https://github.com/import-js/eslint-plugin-import/pull/2989 @@ -1733,6 +1737,7 @@ for info on changes for earlier releases. [@bradzacher]: https://github.com/bradzacher [@brendo]: https://github.com/brendo [@brettz9]: https://github.com/brettz9 +[@chabb]: https://github.com/chabb [@Chamion]: https://github.com/Chamion [@charlessuh]: https://github.com/charlessuh [@charpeni]: https://github.com/charpeni diff --git a/src/rules/no-extraneous-dependencies.js b/src/rules/no-extraneous-dependencies.js index df97987901..0fe42f56f8 100644 --- a/src/rules/no-extraneous-dependencies.js +++ b/src/rules/no-extraneous-dependencies.js @@ -42,8 +42,11 @@ function extractDepFields(pkg) { function getPackageDepFields(packageJsonPath, throwAtRead) { if (!depFieldCache.has(packageJsonPath)) { - const depFields = extractDepFields(readJSON(packageJsonPath, throwAtRead)); - depFieldCache.set(packageJsonPath, depFields); + const packageJson = readJSON(packageJsonPath, throwAtRead); + if (packageJson) { + const depFields = extractDepFields(packageJson); + depFieldCache.set(packageJsonPath, depFields); + } } return depFieldCache.get(packageJsonPath); @@ -72,10 +75,12 @@ function getDependencies(context, packageDir) { // use rule config to find package.json paths.forEach((dir) => { const packageJsonPath = path.join(dir, 'package.json'); - const _packageContent = getPackageDepFields(packageJsonPath, true); - Object.keys(packageContent).forEach((depsKey) => { - Object.assign(packageContent[depsKey], _packageContent[depsKey]); - }); + const _packageContent = getPackageDepFields(packageJsonPath, paths.length === 1); + if (_packageContent) { + Object.keys(packageContent).forEach((depsKey) => { + Object.assign(packageContent[depsKey], _packageContent[depsKey]); + }); + } }); } else { const packageJsonPath = pkgUp({ diff --git a/tests/src/rules/no-extraneous-dependencies.js b/tests/src/rules/no-extraneous-dependencies.js index cb0398ada2..4b221de353 100644 --- a/tests/src/rules/no-extraneous-dependencies.js +++ b/tests/src/rules/no-extraneous-dependencies.js @@ -26,6 +26,7 @@ const packageDirWithEmpty = path.join(__dirname, '../../files/empty'); const packageDirBundleDeps = path.join(__dirname, '../../files/bundled-dependencies/as-array-bundle-deps'); const packageDirBundledDepsAsObject = path.join(__dirname, '../../files/bundled-dependencies/as-object'); const packageDirBundledDepsRaceCondition = path.join(__dirname, '../../files/bundled-dependencies/race-condition'); +const emptyPackageDir = path.join(__dirname, '../../files/empty-folder'); const { dependencies: deps, @@ -104,6 +105,14 @@ ruleTester.run('no-extraneous-dependencies', rule, { code: 'import leftpad from "left-pad";', options: [{ packageDir: packageDirMonoRepoRoot }], }), + test({ + code: 'import leftpad from "left-pad";', + options: [{ packageDir: [emptyPackageDir, packageDirMonoRepoRoot] }], + }), + test({ + code: 'import leftpad from "left-pad";', + options: [{ packageDir: [packageDirMonoRepoRoot, emptyPackageDir] }], + }), test({ code: 'import react from "react";', options: [{ packageDir: [packageDirMonoRepoRoot, packageDirMonoRepoWithNested] }], From 09476d7dac1ab36668283f9626f85e2223652b37 Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 28 May 2024 22:27:21 +0200 Subject: [PATCH 30/50] [New] `no-unused-modules`: Add `ignoreUnusedTypeExports` option Fixes #2694 --- CHANGELOG.md | 3 ++ docs/rules/no-unused-modules.md | 15 ++++++- src/rules/no-unused-modules.js | 35 ++++++++++----- tests/src/rules/no-unused-modules.js | 67 ++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 941cd6ef87..b1d2a3425b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ### 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]) +- [`no-unused-modules`]: Add `ignoreUnusedTypeExports` option ([#3011], thanks [@silverwind]) ### Fixed - [`no-extraneous-dependencies`]: allow wrong path ([#3012], thanks [@chabb]) @@ -1120,6 +1121,7 @@ for info on changes for earlier releases. [`memo-parser`]: ./memo-parser/README.md [#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 [#2991]: https://github.com/import-js/eslint-plugin-import/pull/2991 [#2989]: https://github.com/import-js/eslint-plugin-import/pull/2989 @@ -1915,6 +1917,7 @@ for info on changes for earlier releases. [@sergei-startsev]: https://github.com/sergei-startsev [@sharmilajesupaul]: https://github.com/sharmilajesupaul [@sheepsteak]: https://github.com/sheepsteak +[@silverwind]: https://github.com/silverwind [@silviogutierrez]: https://github.com/silviogutierrez [@SimenB]: https://github.com/SimenB [@simmo]: https://github.com/simmo diff --git a/docs/rules/no-unused-modules.md b/docs/rules/no-unused-modules.md index 53c2479272..359c341ea0 100644 --- a/docs/rules/no-unused-modules.md +++ b/docs/rules/no-unused-modules.md @@ -29,8 +29,9 @@ This rule takes the following option: - **`missingExports`**: if `true`, files without any exports are reported (defaults to `false`) - **`unusedExports`**: if `true`, exports without any static usage within other modules are reported (defaults to `false`) - - `src`: an array with files/paths to be analyzed. It only applies to unused exports. Defaults to `process.cwd()`, if not provided - - `ignoreExports`: an array with files/paths for which unused exports will not be reported (e.g module entry points in a published package) + - **`ignoreUnusedTypeExports`**: if `true`, TypeScript type exports without any static usage within other modules are reported (defaults to `false` and has no effect unless `unusedExports` is `true`) + - **`src`**: an array with files/paths to be analyzed. It only applies to unused exports. Defaults to `process.cwd()`, if not provided + - **`ignoreExports`**: an array with files/paths for which unused exports will not be reported (e.g module entry points in a published package) ### Example for missing exports @@ -116,6 +117,16 @@ export function doAnything() { export default 5 // will not be reported ``` +### Unused exports with `ignoreUnusedTypeExports` set to `true` + +The following will not be reported: + +```ts +export type Foo = {}; // will not be reported +export interface Foo = {}; // will not be reported +export enum Foo {}; // will not be reported +``` + #### Important Note Exports from files listed as a main file (`main`, `browser`, or `bin` fields in `package.json`) will be ignored by default. This only applies if the `package.json` is not set to `private: true` diff --git a/src/rules/no-unused-modules.js b/src/rules/no-unused-modules.js index 0ad330b486..46fc93bfe0 100644 --- a/src/rules/no-unused-modules.js +++ b/src/rules/no-unused-modules.js @@ -83,28 +83,30 @@ const DEFAULT = 'default'; function forEachDeclarationIdentifier(declaration, cb) { if (declaration) { + const isTypeDeclaration = declaration.type === TS_INTERFACE_DECLARATION + || declaration.type === TS_TYPE_ALIAS_DECLARATION + || declaration.type === TS_ENUM_DECLARATION; + if ( declaration.type === FUNCTION_DECLARATION || declaration.type === CLASS_DECLARATION - || declaration.type === TS_INTERFACE_DECLARATION - || declaration.type === TS_TYPE_ALIAS_DECLARATION - || declaration.type === TS_ENUM_DECLARATION + || isTypeDeclaration ) { - cb(declaration.id.name); + cb(declaration.id.name, isTypeDeclaration); } else if (declaration.type === VARIABLE_DECLARATION) { declaration.declarations.forEach(({ id }) => { if (id.type === OBJECT_PATTERN) { recursivePatternCapture(id, (pattern) => { if (pattern.type === IDENTIFIER) { - cb(pattern.name); + cb(pattern.name, false); } }); } else if (id.type === ARRAY_PATTERN) { id.elements.forEach(({ name }) => { - cb(name); + cb(name, false); }); } else { - cb(id.name); + cb(id.name, false); } }); } @@ -443,6 +445,10 @@ module.exports = { description: 'report exports without any usage', type: 'boolean', }, + ignoreUnusedTypeExports: { + description: 'ignore type exports without any usage', + type: 'boolean', + }, }, anyOf: [ { @@ -470,6 +476,7 @@ module.exports = { ignoreExports = [], missingExports, unusedExports, + ignoreUnusedTypeExports, } = context.options[0] || {}; if (unusedExports) { @@ -502,11 +509,15 @@ module.exports = { exportCount.set(IMPORT_NAMESPACE_SPECIFIER, namespaceImports); }; - const checkUsage = (node, exportedValue) => { + const checkUsage = (node, exportedValue, isTypeExport) => { if (!unusedExports) { return; } + if (isTypeExport && ignoreUnusedTypeExports) { + return; + } + if (ignoredFiles.has(file)) { return; } @@ -935,14 +946,14 @@ module.exports = { checkExportPresence(node); }, ExportDefaultDeclaration(node) { - checkUsage(node, IMPORT_DEFAULT_SPECIFIER); + checkUsage(node, IMPORT_DEFAULT_SPECIFIER, false); }, ExportNamedDeclaration(node) { node.specifiers.forEach((specifier) => { - checkUsage(specifier, specifier.exported.name || specifier.exported.value); + checkUsage(specifier, specifier.exported.name || specifier.exported.value, false); }); - forEachDeclarationIdentifier(node.declaration, (name) => { - checkUsage(node, name); + forEachDeclarationIdentifier(node.declaration, (name, isTypeExport) => { + checkUsage(node, name, isTypeExport); }); }, }; diff --git a/tests/src/rules/no-unused-modules.js b/tests/src/rules/no-unused-modules.js index b09d5d759c..80bd70227e 100644 --- a/tests/src/rules/no-unused-modules.js +++ b/tests/src/rules/no-unused-modules.js @@ -38,6 +38,13 @@ const unusedExportsTypescriptOptions = [{ ignoreExports: undefined, }]; +const unusedExportsTypescriptIgnoreUnusedTypesOptions = [{ + unusedExports: true, + ignoreUnusedTypeExports: true, + src: [testFilePath('./no-unused-modules/typescript')], + ignoreExports: undefined, +}]; + const unusedExportsJsxOptions = [{ unusedExports: true, src: [testFilePath('./no-unused-modules/jsx')], @@ -1209,6 +1216,66 @@ context('TypeScript', function () { }); }); +describe('ignoreUnusedTypeExports', () => { + getTSParsers().forEach((parser) => { + typescriptRuleTester.run('no-unused-modules', rule, { + valid: [ + // unused vars should not report + test({ + options: unusedExportsTypescriptIgnoreUnusedTypesOptions, + code: `export interface c {};`, + parser, + filename: testFilePath( + './no-unused-modules/typescript/file-ts-c-unused.ts', + ), + }), + test({ + options: unusedExportsTypescriptIgnoreUnusedTypesOptions, + code: `export type d = {};`, + parser, + filename: testFilePath( + './no-unused-modules/typescript/file-ts-d-unused.ts', + ), + }), + test({ + options: unusedExportsTypescriptIgnoreUnusedTypesOptions, + code: `export enum e { f };`, + parser, + filename: testFilePath( + './no-unused-modules/typescript/file-ts-e-unused.ts', + ), + }), + // used vars should not report + test({ + options: unusedExportsTypescriptIgnoreUnusedTypesOptions, + code: `export interface c {};`, + parser, + filename: testFilePath( + './no-unused-modules/typescript/file-ts-c-used-as-type.ts', + ), + }), + test({ + options: unusedExportsTypescriptIgnoreUnusedTypesOptions, + code: `export type d = {};`, + parser, + filename: testFilePath( + './no-unused-modules/typescript/file-ts-d-used-as-type.ts', + ), + }), + test({ + options: unusedExportsTypescriptIgnoreUnusedTypesOptions, + code: `export enum e { f };`, + parser, + filename: testFilePath( + './no-unused-modules/typescript/file-ts-e-used-as-type.ts', + ), + }), + ], + invalid: [], + }); + }); +}); + describe('correctly work with JSX only files', () => { jsxRuleTester.run('no-unused-modules', rule, { valid: [ From c387276efac8da06e0d8a4eae06989aa0e6631eb Mon Sep 17 00:00:00 2001 From: Mysak0CZ Date: Fri, 23 Aug 2024 14:01:22 +0200 Subject: [PATCH 31/50] [utils] [fix] `parse`: also delete parserOptions.projectService --- utils/CHANGELOG.md | 5 +++++ utils/parse.js | 1 + 2 files changed, 6 insertions(+) diff --git a/utils/CHANGELOG.md b/utils/CHANGELOG.md index 3e2f5a8997..366a834bdb 100644 --- a/utils/CHANGELOG.md +++ b/utils/CHANGELOG.md @@ -8,6 +8,9 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ### Changed - [types] use shared config (thanks [@ljharb]) +### Fixed +- `parse`: also delete `parserOptions.projectService` ([#3039], thanks [@Mysak0CZ]) + ## v2.8.1 - 2024-02-26 ### Fixed @@ -142,6 +145,7 @@ Yanked due to critical issue with cache key resulting from #839. ### Fixed - `unambiguous.test()` regex is now properly in multiline mode +[#3039]: https://github.com/import-js/eslint-plugin-import/pull/3039 [#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 @@ -188,6 +192,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 +[@Mysak0CZ]: https://github.com/Mysak0CZ [@nicolo-ribaudo]: https://github.com/nicolo-ribaudo [@pmcelhaney]: https://github.com/pmcelhaney [@sergei-startsev]: https://github.com/sergei-startsev diff --git a/utils/parse.js b/utils/parse.js index 94aca1d0f8..75d527b008 100644 --- a/utils/parse.js +++ b/utils/parse.js @@ -134,6 +134,7 @@ exports.default = function parse(path, content, context) { // only parse one file in isolate mode, which is much, much faster. // https://github.com/import-js/eslint-plugin-import/issues/1408#issuecomment-509298962 delete parserOptions.EXPERIMENTAL_useProjectService; + delete parserOptions.projectService; delete parserOptions.project; delete parserOptions.projects; From bab3a109dce63d0b4fdc4b876dc77f01c34aca4c Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Sun, 25 Aug 2024 15:15:43 -0700 Subject: [PATCH 32/50] [utils] [meta] add `exports`, `main` In theory this should only be breaking if someone is requiring tsconfig.json --- utils/CHANGELOG.md | 1 + utils/package.json | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/utils/CHANGELOG.md b/utils/CHANGELOG.md index 366a834bdb..ac6883b1c4 100644 --- a/utils/CHANGELOG.md +++ b/utils/CHANGELOG.md @@ -7,6 +7,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ### Changed - [types] use shared config (thanks [@ljharb]) +- [meta] add `exports`, `main` ### Fixed - `parse`: also delete `parserOptions.projectService` ([#3039], thanks [@Mysak0CZ]) diff --git a/utils/package.json b/utils/package.json index 4704971505..23c8b08e9c 100644 --- a/utils/package.json +++ b/utils/package.json @@ -5,15 +5,46 @@ "engines": { "node": ">=4" }, + "main": false, + "exports": { + "./ModuleCache": "./ModuleCache.js", + "./ModuleCache.js": "./ModuleCache.js", + "./declaredScope": "./declaredScope.js", + "./declaredScope.js": "./declaredScope.js", + "./hash": "./hash.js", + "./hash.js": "./hash.js", + "./ignore": "./ignore.js", + "./ignore.js": "./ignore.js", + "./module-require": "./module-require.js", + "./module-require.js": "./module-require.js", + "./moduleVisitor": "./moduleVisitor.js", + "./moduleVisitor.js": "./moduleVisitor.js", + "./parse": "./parse.js", + "./parse.js": "./parse.js", + "./pkgDir": "./pkgDir.js", + "./pkgDir.js": "./pkgDir.js", + "./pkgUp": "./pkgUp.js", + "./pkgUp.js": "./pkgUp.js", + "./readPkgUp": "./readPkgUp.js", + "./readPkgUp.js": "./readPkgUp.js", + "./resolve": "./resolve.js", + "./resolve.js": "./resolve.js", + "./unambiguous": "./unambiguous.js", + "./unambiguous.js": "./unambiguous.js", + "./visit": "./visit.js", + "./visit.js": "./visit.js", + "./package.json": "./package.json" + }, "scripts": { "prepublishOnly": "cp ../{LICENSE,.npmrc} ./", "tsc": "tsc -p .", + "posttsc": "attw -P .", "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", "url": "git+https://github.com/import-js/eslint-plugin-import.git", - "directory": "utils" + "directory": "utils" }, "keywords": [ "eslint-plugin-import", @@ -31,6 +62,7 @@ "debug": "^3.2.7" }, "devDependencies": { + "@arethetypeswrong/cli": "^0.15.4", "@ljharb/tsconfig": "^0.2.0", "@types/debug": "^4.1.12", "@types/eslint": "^8.56.3", From 9b1a3b96caa656fe94bda709c364c7e230028432 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Sun, 25 Aug 2024 16:22:22 -0700 Subject: [PATCH 33/50] [utils] v2.8.2 --- utils/CHANGELOG.md | 10 +++++++--- utils/package.json | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/utils/CHANGELOG.md b/utils/CHANGELOG.md index ac6883b1c4..43bd0e022b 100644 --- a/utils/CHANGELOG.md +++ b/utils/CHANGELOG.md @@ -5,13 +5,17 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ## Unreleased -### Changed -- [types] use shared config (thanks [@ljharb]) -- [meta] add `exports`, `main` +## v2.8.2 - 2024-08-25 ### Fixed - `parse`: also delete `parserOptions.projectService` ([#3039], thanks [@Mysak0CZ]) +### Changed +- [types] use shared config (thanks [@ljharb]) +- [meta] add `exports`, `main` +- [meta] add `repository.directory` field +- [refactor] avoid hoisting + ## v2.8.1 - 2024-02-26 ### Fixed diff --git a/utils/package.json b/utils/package.json index 23c8b08e9c..6d69e2414a 100644 --- a/utils/package.json +++ b/utils/package.json @@ -1,6 +1,6 @@ { "name": "eslint-module-utils", - "version": "2.8.1", + "version": "2.8.2", "description": "Core utilities to support eslint-plugin-import and other module-related plugins.", "engines": { "node": ">=4" From bdff75d51fc73895f9e1697a02765daf12815714 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Sun, 25 Aug 2024 19:00:48 -0700 Subject: [PATCH 34/50] [Deps] update `array-includes`, `array.prototype.findlastindex`, `eslint-module-utils`, `hasown`, `is-core-module`, `object.fromentries`, `object.groupby`, `object.values` --- package.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index b9fa1eb35f..de0055a09a 100644 --- a/package.json +++ b/package.json @@ -105,21 +105,21 @@ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" }, "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.4", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", "array.prototype.flat": "^1.3.2", "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.1", - "is-core-module": "^2.13.1", + "eslint-module-utils": "^2.8.2", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.2", - "object.values": "^1.1.7", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", "semver": "^6.3.1", "tsconfig-paths": "^3.15.0" } From db8b95d7a5768c0ccd372b6b5228287a182c7679 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Sun, 14 Jul 2024 13:45:26 +0000 Subject: [PATCH 35/50] [resolvers/webpack] [refactor] simplify loop --- resolvers/webpack/CHANGELOG.md | 5 ++++- resolvers/webpack/index.js | 31 ++++++++++++++++++------------- resolvers/webpack/package.json | 1 - 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/resolvers/webpack/CHANGELOG.md b/resolvers/webpack/CHANGELOG.md index 4fed046b46..cd49cc3f4e 100644 --- a/resolvers/webpack/CHANGELOG.md +++ b/resolvers/webpack/CHANGELOG.md @@ -5,11 +5,12 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ## Unreleased +- [refactor] simplify loop ([#3029], thanks [@fregante]) + ## 0.13.8 - 2023-10-22 - [refactor] use `hasown` instead of `has` - [deps] update `array.prototype.find`, `is-core-module`, `resolve` - ## 0.13.7 - 2023-08-19 - [fix] use the `dirname` of the `configPath` as `basedir` ([#2859]) @@ -178,6 +179,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]). +[#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 [#1967]: https://github.com/import-js/eslint-plugin-import/pull/1967 @@ -222,6 +224,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange [@benmvp]: https://github.com/benmvp [@daltones]: https://github.com/daltones [@echenley]: https://github.com/echenley +[@fregante]: https://github.com/fregante [@gausie]: https://github.com/gausie [@grahamb]: https://github.com/grahamb [@graingert]: https://github.com/graingert diff --git a/resolvers/webpack/index.js b/resolvers/webpack/index.js index da16eda593..eabe3cf338 100644 --- a/resolvers/webpack/index.js +++ b/resolvers/webpack/index.js @@ -3,7 +3,6 @@ const findRoot = require('find-root'); const path = require('path'); const isEqual = require('lodash/isEqual'); -const find = require('array.prototype.find'); const interpret = require('interpret'); const fs = require('fs'); const isCore = require('is-core-module'); @@ -293,17 +292,20 @@ const MAX_CACHE = 10; const _cache = []; function getResolveSync(configPath, webpackConfig, cwd) { const cacheKey = { configPath, webpackConfig }; - let cached = find(_cache, function (entry) { return isEqual(entry.key, cacheKey); }); - if (!cached) { - cached = { - key: cacheKey, - value: createResolveSync(configPath, webpackConfig, cwd), - }; - // put in front and pop last item - if (_cache.unshift(cached) > MAX_CACHE) { - _cache.pop(); + for (let i = 0; i < _cache.length; i++) { + if (isEqual(_cache[i].key, cacheKey)) { + return _cache[i].value; } } + + const cached = { + key: cacheKey, + value: createResolveSync(configPath, webpackConfig, cwd), + }; + // put in front and pop last item + if (_cache.unshift(cached) > MAX_CACHE) { + _cache.pop(); + } return cached.value; } @@ -409,9 +411,12 @@ exports.resolve = function (source, file, settings) { if (typeof configIndex !== 'undefined' && webpackConfig.length > configIndex) { webpackConfig = webpackConfig[configIndex]; } else { - webpackConfig = find(webpackConfig, function findFirstWithResolve(config) { - return !!config.resolve; - }); + for (let i = 0; i < webpackConfig.length; i++) { + if (webpackConfig[i].resolve) { + webpackConfig = webpackConfig[i]; + break; + } + } } } diff --git a/resolvers/webpack/package.json b/resolvers/webpack/package.json index 7f8cb718f1..38465bcdee 100644 --- a/resolvers/webpack/package.json +++ b/resolvers/webpack/package.json @@ -31,7 +31,6 @@ }, "homepage": "https://github.com/import-js/eslint-plugin-import/tree/HEAD/resolvers/webpack", "dependencies": { - "array.prototype.find": "^2.2.2", "debug": "^3.2.7", "enhanced-resolve": "^0.9.1", "find-root": "^1.1.0", From 19dbc33ecf09c774db35f362b05caaec027d2e18 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Mon, 26 Aug 2024 15:23:07 -0700 Subject: [PATCH 36/50] [resolvers/webpack] [refactor] misc cleanup --- resolvers/webpack/index.js | 66 ++++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/resolvers/webpack/index.js b/resolvers/webpack/index.js index eabe3cf338..83297cd185 100644 --- a/resolvers/webpack/index.js +++ b/resolvers/webpack/index.js @@ -4,12 +4,15 @@ const findRoot = require('find-root'); const path = require('path'); const isEqual = require('lodash/isEqual'); const interpret = require('interpret'); -const fs = require('fs'); +const existsSync = require('fs').existsSync; const isCore = require('is-core-module'); const resolve = require('resolve/sync'); const semver = require('semver'); const hasOwn = require('hasown'); const isRegex = require('is-regex'); +const isArray = Array.isArray; +const keys = Object.keys; +const assign = Object.assign; const log = require('debug')('eslint-plugin-import:resolver:webpack'); @@ -19,7 +22,7 @@ function registerCompiler(moduleDescriptor) { if (moduleDescriptor) { if (typeof moduleDescriptor === 'string') { require(moduleDescriptor); - } else if (!Array.isArray(moduleDescriptor)) { + } else if (!isArray(moduleDescriptor)) { moduleDescriptor.register(require(moduleDescriptor.module)); } else { for (let i = 0; i < moduleDescriptor.length; i++) { @@ -35,42 +38,34 @@ function registerCompiler(moduleDescriptor) { } function findConfigPath(configPath, packageDir) { - const extensions = Object.keys(interpret.extensions).sort(function (a, b) { + const extensions = keys(interpret.extensions).sort(function (a, b) { return a === '.js' ? -1 : b === '.js' ? 1 : a.length - b.length; }); let extension; if (configPath) { - // extensions is not reused below, so safe to mutate it here. - extensions.reverse(); - extensions.forEach(function (maybeExtension) { - if (extension) { - return; - } - - if (configPath.substr(-maybeExtension.length) === maybeExtension) { + for (let i = extensions.length - 1; i >= 0 && !extension; i--) { + const maybeExtension = extensions[i]; + if (configPath.slice(-maybeExtension.length) === maybeExtension) { extension = maybeExtension; } - }); + } // see if we've got an absolute path if (!path.isAbsolute(configPath)) { configPath = path.join(packageDir, configPath); } } else { - extensions.forEach(function (maybeExtension) { - if (extension) { - return; - } - + for (let i = 0; i < extensions.length && !extension; i++) { + const maybeExtension = extensions[i]; const maybePath = path.resolve( path.join(packageDir, 'webpack.config' + maybeExtension) ); - if (fs.existsSync(maybePath)) { + if (existsSync(maybePath)) { configPath = maybePath; extension = maybeExtension; } - }); + } } registerCompiler(interpret.extensions[extension]); @@ -84,7 +79,7 @@ function findExternal(source, externals, context, resolveSync) { if (typeof externals === 'string') { return source === externals; } // array: recurse - if (Array.isArray(externals)) { + if (isArray(externals)) { return externals.some(function (e) { return findExternal(source, e, context, resolveSync); }); } @@ -138,8 +133,9 @@ function findExternal(source, externals, context, resolveSync) { // else, vanilla object for (const key in externals) { - if (!hasOwn(externals, key)) { continue; } - if (source === key) { return true; } + if (hasOwn(externals, key) && source === key) { + return true; + } } return false; } @@ -160,15 +156,20 @@ const webpack2DefaultResolveConfig = { function createWebpack2ResolveSync(webpackRequire, resolveConfig) { const EnhancedResolve = webpackRequire('enhanced-resolve'); - return EnhancedResolve.create.sync(Object.assign({}, webpack2DefaultResolveConfig, resolveConfig)); + return EnhancedResolve.create.sync(assign({}, webpack2DefaultResolveConfig, resolveConfig)); } /** * webpack 1 defaults: https://webpack.github.io/docs/configuration.html#resolve-packagemains - * @type {Array} + * @type {string[]} */ const webpack1DefaultMains = [ - 'webpack', 'browser', 'web', 'browserify', ['jam', 'main'], 'main', + 'webpack', + 'browser', + 'web', + 'browserify', + ['jam', 'main'], + 'main', ]; /* eslint-disable */ @@ -176,8 +177,9 @@ const webpack1DefaultMains = [ function makeRootPlugin(ModulesInRootPlugin, name, root) { if (typeof root === 'string') { return new ModulesInRootPlugin(name, root); - } else if (Array.isArray(root)) { - return function() { + } + if (isArray(root)) { + return function () { root.forEach(function (root) { this.apply(new ModulesInRootPlugin(name, root)); }, this); @@ -236,7 +238,7 @@ function createWebpack1ResolveSync(webpackRequire, resolveConfig, plugins) { if ( plugin.constructor && plugin.constructor.name === 'ResolverPlugin' - && Array.isArray(plugin.plugins) + && isArray(plugin.plugins) ) { resolvePlugins.push.apply(resolvePlugins, plugin.plugins); } @@ -313,9 +315,9 @@ function getResolveSync(configPath, webpackConfig, cwd) { * Find the full path to 'source', given 'file' as a full reference path. * * resolveImport('./foo', '/Users/ben/bar.js') => '/Users/ben/foo.js' - * @param {string} source - the module to resolve; i.e './some-module' - * @param {string} file - the importing file's full path; i.e. '/usr/local/bin/file.js' - * @param {object} settings - the webpack config file name, as well as cwd + * @param {string} source - the module to resolve; i.e './some-module' + * @param {string} file - the importing file's full path; i.e. '/usr/local/bin/file.js' + * @param {object} settings - the webpack config file name, as well as cwd * @example * options: { * // Path to the webpack config @@ -399,7 +401,7 @@ exports.resolve = function (source, file, settings) { webpackConfig = webpackConfig(env, argv); } - if (Array.isArray(webpackConfig)) { + if (isArray(webpackConfig)) { webpackConfig = webpackConfig.map((cfg) => { if (typeof cfg === 'function') { return cfg(env, argv); From 98a0991aa248216fb904cc88d11aa9070ccb6249 Mon Sep 17 00:00:00 2001 From: Ivan Rubinson Date: Mon, 8 Apr 2024 20:50:56 +0300 Subject: [PATCH 37/50] [New] [Refactor] `no-cycle`: use scc algorithm to optimize; add `skipErrorMessagePath` for faster error messages --- CHANGELOG.md | 2 + package.json | 1 + src/rules/no-cycle.js | 21 +++++ src/scc.js | 86 +++++++++++++++++ tests/src/rules/no-cycle.js | 28 +++++- tests/src/scc.js | 179 ++++++++++++++++++++++++++++++++++++ 6 files changed, 316 insertions(+), 1 deletion(-) create mode 100644 src/scc.js create mode 100644 tests/src/scc.js diff --git a/CHANGELOG.md b/CHANGELOG.md index b1d2a3425b..0ba781c235 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ### Fixed - [`no-extraneous-dependencies`]: allow wrong path ([#3012], thanks [@chabb]) +- [`no-cycle`]: use scc algorithm to optimize ([#2998], thanks [@soryy708]) ### Changed - [Docs] `no-extraneous-dependencies`: Make glob pattern description more explicit ([#2944], thanks [@mulztob]) @@ -1123,6 +1124,7 @@ for info on changes for earlier releases. [#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 [#2991]: https://github.com/import-js/eslint-plugin-import/pull/2991 [#2989]: https://github.com/import-js/eslint-plugin-import/pull/2989 [#2987]: https://github.com/import-js/eslint-plugin-import/pull/2987 diff --git a/package.json b/package.json index de0055a09a..2aaa786538 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,7 @@ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" }, "dependencies": { + "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", "array.prototype.findlastindex": "^1.2.5", "array.prototype.flat": "^1.3.2", diff --git a/src/rules/no-cycle.js b/src/rules/no-cycle.js index e65ff11a49..be8c288dd4 100644 --- a/src/rules/no-cycle.js +++ b/src/rules/no-cycle.js @@ -5,6 +5,7 @@ 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'; @@ -47,6 +48,11 @@ module.exports = { type: 'boolean', default: false, }, + disableScc: { + description: 'When true, don\'t calculate a strongly-connected-components graph. SCC is used to reduce the time-complexity of cycle detection, but adds overhead.', + type: 'boolean', + default: false, + }, })], }, @@ -62,6 +68,8 @@ module.exports = { context, ); + const scc = options.disableScc ? {} : StronglyConnectedComponentsBuilder.get(myPath, context); + function checkSourceValue(sourceNode, importer) { if (ignoreModule(sourceNode.value)) { return; // ignore external modules @@ -98,6 +106,16 @@ module.exports = { return; // no-self-import territory } + /* If we're in the same Strongly Connected Component, + * Then there exists a path from each node in the SCC to every other node in the SCC, + * Then there exists at least one path from them to us and from us to them, + * Then we have a cycle between us. + */ + const hasDependencyCycle = options.disableScc || scc[myPath] === scc[imported.path]; + if (!hasDependencyCycle) { + return; + } + const untraversed = [{ mget: () => imported, route: [] }]; function detectCycle({ mget, route }) { const m = mget(); @@ -106,6 +124,9 @@ module.exports = { traversed.add(m.path); for (const [path, { getter, declarations }] of m.imports) { + // If we're in different SCCs, we can't have a circular dependency + if (!options.disableScc && scc[myPath] !== scc[path]) { continue; } + if (traversed.has(path)) { continue; } const toTraverse = [...declarations].filter(({ source, isOnlyImportingTypes }) => !ignoreModule(source.value) // Ignore only type imports diff --git a/src/scc.js b/src/scc.js new file mode 100644 index 0000000000..44c818bbe1 --- /dev/null +++ b/src/scc.js @@ -0,0 +1,86 @@ +import calculateScc from '@rtsao/scc'; +import { hashObject } from 'eslint-module-utils/hash'; +import resolve from 'eslint-module-utils/resolve'; +import ExportMapBuilder from './exportMap/builder'; +import childContext from './exportMap/childContext'; + +let cache = new Map(); + +export default class StronglyConnectedComponentsBuilder { + static clearCache() { + cache = new Map(); + } + + static get(source, context) { + const path = resolve(source, context); + if (path == null) { return null; } + return StronglyConnectedComponentsBuilder.for(childContext(path, context)); + } + + static for(context) { + const cacheKey = context.cacheKey || hashObject(context).digest('hex'); + if (cache.has(cacheKey)) { + return cache.get(cacheKey); + } + const scc = StronglyConnectedComponentsBuilder.calculate(context); + cache.set(cacheKey, scc); + return scc; + } + + static calculate(context) { + const exportMap = ExportMapBuilder.for(context); + const adjacencyList = this.exportMapToAdjacencyList(exportMap); + const calculatedScc = calculateScc(adjacencyList); + return StronglyConnectedComponentsBuilder.calculatedSccToPlainObject(calculatedScc); + } + + /** @returns {Map>} for each dep, what are its direct deps */ + static exportMapToAdjacencyList(initialExportMap) { + const adjacencyList = new Map(); + // BFS + function visitNode(exportMap) { + if (!exportMap) { + return; + } + exportMap.imports.forEach((v, importedPath) => { + const from = exportMap.path; + const to = importedPath; + + // Ignore type-only imports, because we care only about SCCs of value imports + const toTraverse = [...v.declarations].filter(({ isOnlyImportingTypes }) => !isOnlyImportingTypes); + if (toTraverse.length === 0) { return; } + + if (!adjacencyList.has(from)) { + adjacencyList.set(from, new Set()); + } + + if (adjacencyList.get(from).has(to)) { + return; // prevent endless loop + } + adjacencyList.get(from).add(to); + visitNode(v.getter()); + }); + } + visitNode(initialExportMap); + // Fill gaps + adjacencyList.forEach((values) => { + values.forEach((value) => { + if (!adjacencyList.has(value)) { + adjacencyList.set(value, new Set()); + } + }); + }); + return adjacencyList; + } + + /** @returns {Record} for each key, its SCC's index */ + static calculatedSccToPlainObject(sccs) { + const obj = {}; + sccs.forEach((scc, index) => { + scc.forEach((node) => { + obj[node] = index; + }); + }); + return obj; + } +} diff --git a/tests/src/rules/no-cycle.js b/tests/src/rules/no-cycle.js index d2adbf61f9..efc0fb6eb9 100644 --- a/tests/src/rules/no-cycle.js +++ b/tests/src/rules/no-cycle.js @@ -17,7 +17,7 @@ const testVersion = (specifier, t) => _testVersion(specifier, () => Object.assig const testDialects = ['es6']; -ruleTester.run('no-cycle', rule, { +const cases = { valid: [].concat( // this rule doesn't care if the cycle length is 0 test({ code: 'import foo from "./foo.js"' }), @@ -290,4 +290,30 @@ ruleTester.run('no-cycle', rule, { ], }), ), +}; + +ruleTester.run('no-cycle', rule, { + valid: flatMap(cases.valid, (testCase) => [ + testCase, + { + ...testCase, + code: `${testCase.code} // disableScc=true`, + options: [{ + ...testCase.options && testCase.options[0] || {}, + disableScc: true, + }], + }, + ]), + + invalid: flatMap(cases.invalid, (testCase) => [ + testCase, + { + ...testCase, + code: `${testCase.code} // disableScc=true`, + options: [{ + ...testCase.options && testCase.options[0] || {}, + disableScc: true, + }], + }, + ]), }); diff --git a/tests/src/scc.js b/tests/src/scc.js new file mode 100644 index 0000000000..376b783ce1 --- /dev/null +++ b/tests/src/scc.js @@ -0,0 +1,179 @@ +import sinon from 'sinon'; +import { expect } from 'chai'; +import StronglyConnectedComponentsBuilder from '../../src/scc'; +import ExportMapBuilder from '../../src/exportMap/builder'; + +function exportMapFixtureBuilder(path, imports, isOnlyImportingTypes = false) { + return { + path, + imports: new Map(imports.map((imp) => [imp.path, { getter: () => imp, declarations: [{ isOnlyImportingTypes }] }])), + }; +} + +describe('Strongly Connected Components Builder', () => { + afterEach(() => ExportMapBuilder.for.restore()); + afterEach(() => StronglyConnectedComponentsBuilder.clearCache()); + + describe('When getting an SCC', () => { + const source = ''; + const context = { + settings: {}, + parserOptions: {}, + parserPath: '', + }; + + describe('Given two files', () => { + describe('When they don\'t value-cycle', () => { + it('Should return foreign SCCs', () => { + sinon.stub(ExportMapBuilder, 'for').returns( + exportMapFixtureBuilder('foo.js', [exportMapFixtureBuilder('bar.js', [])]), + ); + const actual = StronglyConnectedComponentsBuilder.for(source, context); + expect(actual).to.deep.equal({ 'foo.js': 1, 'bar.js': 0 }); + }); + }); + + describe('When they do value-cycle', () => { + it('Should return same SCC', () => { + sinon.stub(ExportMapBuilder, 'for').returns( + exportMapFixtureBuilder('foo.js', [ + exportMapFixtureBuilder('bar.js', [ + exportMapFixtureBuilder('foo.js', [exportMapFixtureBuilder('bar.js', [])]), + ]), + ]), + ); + const actual = StronglyConnectedComponentsBuilder.for(source, context); + expect(actual).to.deep.equal({ 'foo.js': 0, 'bar.js': 0 }); + }); + }); + + describe('When they type-cycle', () => { + it('Should return foreign SCCs', () => { + sinon.stub(ExportMapBuilder, 'for').returns( + exportMapFixtureBuilder('foo.js', [ + exportMapFixtureBuilder('bar.js', [ + exportMapFixtureBuilder('foo.js', []), + ], true), + ]), + ); + const actual = StronglyConnectedComponentsBuilder.for(source, context); + expect(actual).to.deep.equal({ 'foo.js': 1, 'bar.js': 0 }); + }); + }); + }); + + describe('Given three files', () => { + describe('When they form a line', () => { + describe('When A -> B -> C', () => { + it('Should return foreign SCCs', () => { + sinon.stub(ExportMapBuilder, 'for').returns( + exportMapFixtureBuilder('foo.js', [ + exportMapFixtureBuilder('bar.js', [ + exportMapFixtureBuilder('buzz.js', []), + ]), + ]), + ); + const actual = StronglyConnectedComponentsBuilder.for(source, context); + expect(actual).to.deep.equal({ 'foo.js': 2, 'bar.js': 1, 'buzz.js': 0 }); + }); + }); + + describe('When A -> B <-> C', () => { + it('Should return 2 SCCs, A on its own', () => { + sinon.stub(ExportMapBuilder, 'for').returns( + exportMapFixtureBuilder('foo.js', [ + exportMapFixtureBuilder('bar.js', [ + exportMapFixtureBuilder('buzz.js', [ + exportMapFixtureBuilder('bar.js', []), + ]), + ]), + ]), + ); + const actual = StronglyConnectedComponentsBuilder.for(source, context); + expect(actual).to.deep.equal({ 'foo.js': 1, 'bar.js': 0, 'buzz.js': 0 }); + }); + }); + + describe('When A <-> B -> C', () => { + it('Should return 2 SCCs, C on its own', () => { + sinon.stub(ExportMapBuilder, 'for').returns( + exportMapFixtureBuilder('foo.js', [ + exportMapFixtureBuilder('bar.js', [ + exportMapFixtureBuilder('buzz.js', []), + exportMapFixtureBuilder('foo.js', []), + ]), + ]), + ); + const actual = StronglyConnectedComponentsBuilder.for(source, context); + expect(actual).to.deep.equal({ 'foo.js': 1, 'bar.js': 1, 'buzz.js': 0 }); + }); + }); + + describe('When A <-> B <-> C', () => { + it('Should return same SCC', () => { + sinon.stub(ExportMapBuilder, 'for').returns( + exportMapFixtureBuilder('foo.js', [ + exportMapFixtureBuilder('bar.js', [ + exportMapFixtureBuilder('foo.js', []), + exportMapFixtureBuilder('buzz.js', [ + exportMapFixtureBuilder('bar.js', []), + ]), + ]), + ]), + ); + const actual = StronglyConnectedComponentsBuilder.for(source, context); + expect(actual).to.deep.equal({ 'foo.js': 0, 'bar.js': 0, 'buzz.js': 0 }); + }); + }); + }); + + describe('When they form a loop', () => { + it('Should return same SCC', () => { + sinon.stub(ExportMapBuilder, 'for').returns( + exportMapFixtureBuilder('foo.js', [ + exportMapFixtureBuilder('bar.js', [ + exportMapFixtureBuilder('buzz.js', [ + exportMapFixtureBuilder('foo.js', []), + ]), + ]), + ]), + ); + const actual = StronglyConnectedComponentsBuilder.for(source, context); + expect(actual).to.deep.equal({ 'foo.js': 0, 'bar.js': 0, 'buzz.js': 0 }); + }); + }); + + describe('When they form a Y', () => { + it('Should return 3 distinct SCCs', () => { + sinon.stub(ExportMapBuilder, 'for').returns( + exportMapFixtureBuilder('foo.js', [ + exportMapFixtureBuilder('bar.js', []), + exportMapFixtureBuilder('buzz.js', []), + ]), + ); + const actual = StronglyConnectedComponentsBuilder.for(source, context); + expect(actual).to.deep.equal({ 'foo.js': 2, 'bar.js': 0, 'buzz.js': 1 }); + }); + }); + + describe('When they form a Mercedes', () => { + it('Should return 1 SCC', () => { + sinon.stub(ExportMapBuilder, 'for').returns( + exportMapFixtureBuilder('foo.js', [ + exportMapFixtureBuilder('bar.js', [ + exportMapFixtureBuilder('foo.js', []), + exportMapFixtureBuilder('buzz.js', []), + ]), + exportMapFixtureBuilder('buzz.js', [ + exportMapFixtureBuilder('foo.js', []), + exportMapFixtureBuilder('bar.js', []), + ]), + ]), + ); + const actual = StronglyConnectedComponentsBuilder.for(source, context); + expect(actual).to.deep.equal({ 'foo.js': 0, 'bar.js': 0, 'buzz.js': 0 }); + }); + }); + }); + }); +}); From 4bdf61af182dc1793e229d6f0da2a0e7472f86e6 Mon Sep 17 00:00:00 2001 From: yesl-kim Date: Sun, 11 Aug 2024 17:34:29 +0900 Subject: [PATCH 38/50] [Fix] `no-duplicates`: Removing duplicates breaks in TypeScript Fixes #3016. Fixes #2792. --- CHANGELOG.md | 3 +++ src/rules/no-duplicates.js | 16 ++++++++++++++-- tests/src/rules/no-duplicates.js | 18 ++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ba781c235..667d9fffb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ### Fixed - [`no-extraneous-dependencies`]: allow wrong path ([#3012], thanks [@chabb]) - [`no-cycle`]: use scc algorithm to optimize ([#2998], thanks [@soryy708]) +- [`no-duplicates`]: Removing duplicates breaks in TypeScript ([#3033], thanks [@yesl-kim]) ### Changed - [Docs] `no-extraneous-dependencies`: Make glob pattern description more explicit ([#2944], thanks [@mulztob]) @@ -1121,6 +1122,7 @@ for info on changes for earlier releases. [`memo-parser`]: ./memo-parser/README.md +[#3033]: https://github.com/import-js/eslint-plugin-import/pull/3033 [#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 @@ -1962,6 +1964,7 @@ for info on changes for earlier releases. [@wtgtybhertgeghgtwtg]: https://github.com/wtgtybhertgeghgtwtg [@xM8WVqaG]: https://github.com/xM8WVqaG [@xpl]: https://github.com/xpl +[@yesl-kim]: https://github.com/yesl-kim [@yndajas]: https://github.com/yndajas [@yordis]: https://github.com/yordis [@Zamiell]: https://github.com/Zamiell diff --git a/src/rules/no-duplicates.js b/src/rules/no-duplicates.js index 033e854e03..d9fb1a1309 100644 --- a/src/rules/no-duplicates.js +++ b/src/rules/no-duplicates.js @@ -132,6 +132,7 @@ function getFix(first, rest, sourceCode, context) { const shouldAddDefault = getDefaultImportName(first) == null && defaultImportNames.size === 1; const shouldAddSpecifiers = specifiers.length > 0; const shouldRemoveUnnecessary = unnecessaryImports.length > 0; + const preferInline = context.options[0] && context.options[0]['prefer-inline']; if (!(shouldAddDefault || shouldAddSpecifiers || shouldRemoveUnnecessary)) { return undefined; @@ -157,8 +158,7 @@ function getFix(first, rest, sourceCode, context) { ([result, needsComma, existingIdentifiers], specifier) => { const isTypeSpecifier = specifier.importNode.importKind === 'type'; - const preferInline = context.options[0] && context.options[0]['prefer-inline']; - // a user might set prefer-inline but not have a supporting TypeScript version. Flow does not support inline types so this should fail in that case as well. + // a user might set prefer-inline but not have a supporting TypeScript version. Flow does not support inline types so this should fail in that case as well. if (preferInline && (!typescriptPkg || !semver.satisfies(typescriptPkg.version, '>= 4.5'))) { throw new Error('Your version of TypeScript does not support inline type imports.'); } @@ -186,6 +186,18 @@ function getFix(first, rest, sourceCode, context) { const fixes = []; + if (shouldAddSpecifiers && preferInline && first.importKind === 'type') { + // `import type {a} from './foo'` → `import {type a} from './foo'` + const typeIdentifierToken = tokens.find((token) => token.type === 'Identifier' && token.value === 'type'); + fixes.push(fixer.removeRange([typeIdentifierToken.range[0], typeIdentifierToken.range[1] + 1])); + + tokens + .filter((token) => firstExistingIdentifiers.has(token.value)) + .forEach((identifier) => { + fixes.push(fixer.replaceTextRange([identifier.range[0], identifier.range[1]], `type ${identifier.value}`)); + }); + } + if (shouldAddDefault && openBrace == null && shouldAddSpecifiers) { // `import './foo'` → `import def, {...} from './foo'` fixes.push( diff --git a/tests/src/rules/no-duplicates.js b/tests/src/rules/no-duplicates.js index f83221105a..e682f22354 100644 --- a/tests/src/rules/no-duplicates.js +++ b/tests/src/rules/no-duplicates.js @@ -704,6 +704,24 @@ context('TypeScript', function () { }, ], }), + test({ + code: "import type {x} from 'foo'; import {type y} from 'foo'", + ...parserConfig, + options: [{ 'prefer-inline': true }], + output: `import {type x,type y} from 'foo'; `, + errors: [ + { + line: 1, + column: 22, + message: "'foo' imported multiple times.", + }, + { + line: 1, + column: 50, + message: "'foo' imported multiple times.", + }, + ], + }), test({ code: "import {type x} from 'foo'; import type {y} from 'foo'", ...parserConfig, From 6407c1ce2ad16f6116bd8927fc4ba1d2fef56880 Mon Sep 17 00:00:00 2001 From: Bryan Lee <38807139+liby@users.noreply.github.com> Date: Tue, 28 Sep 2021 11:26:25 +0800 Subject: [PATCH 39/50] [Docs] `order`: update the description of the `pathGroupsExcludedImportTypes` option --- CHANGELOG.md | 2 ++ docs/rules/order.md | 26 ++------------------------ 2 files changed, 4 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 667d9fffb3..7752473019 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange - [actions] migrate OSX tests to GHA ([ljharb#37], thanks [@aks-]) - [Refactor] `exportMapBuilder`: avoid hoisting ([#2989], thanks [@soryy708]) - [Refactor] `ExportMap`: extract "builder" logic to separate files ([#2991], thanks [@soryy708]) +- [Docs] [`order`]: update the description of the `pathGroupsExcludedImportTypes` option ([#3036], thanks [@liby]) ## [2.29.1] - 2023-12-14 @@ -1122,6 +1123,7 @@ for info on changes for earlier releases. [`memo-parser`]: ./memo-parser/README.md +[#3036]: https://github.com/import-js/eslint-plugin-import/pull/3036 [#3033]: https://github.com/import-js/eslint-plugin-import/pull/3033 [#3012]: https://github.com/import-js/eslint-plugin-import/pull/3012 [#3011]: https://github.com/import-js/eslint-plugin-import/pull/3011 diff --git a/docs/rules/order.md b/docs/rules/order.md index 24692f2d21..67849bb7ed 100644 --- a/docs/rules/order.md +++ b/docs/rules/order.md @@ -193,7 +193,7 @@ Example: ### `pathGroupsExcludedImportTypes: [array]` This defines import types that are not handled by configured pathGroups. -This is mostly needed when you want to handle path groups that look like external imports. +If you have added path groups with patterns that look like `"builtin"` or `"external"` imports, you have to remove this group (`"builtin"` and/or `"external"`) from the default exclusion list (e.g., `["builtin", "external", "object"]`, etc) to sort these path groups correctly. Example: @@ -212,29 +212,7 @@ Example: } ``` -You can also use `patterns`(e.g., `react`, `react-router-dom`, etc). - -Example: - -```json -{ - "import/order": [ - "error", - { - "pathGroups": [ - { - "pattern": "react", - "group": "builtin", - "position": "before" - } - ], - "pathGroupsExcludedImportTypes": ["react"] - } - ] -} -``` - -The default value is `["builtin", "external", "object"]`. +[Import Type](https://github.com/import-js/eslint-plugin-import/blob/HEAD/src/core/importType.js#L90) is resolved as a fixed string in predefined set, it can't be a `patterns`(e.g., `react`, `react-router-dom`, etc). See [#2156] for details. ### `newlines-between: [ignore|always|always-and-inside-groups|never]` From b340f1f321f1804f6db9d024e42d743f96f48126 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Thu, 29 Aug 2024 10:32:30 -0700 Subject: [PATCH 40/50] [meta] no need to ship contrib docs --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 2aaa786538..b0ddaf8168 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "files": [ "*.md", + "!{CONTRIBUTING,RELEASE}.md", "LICENSE", "docs", "lib", From 806e3c2ccc65456a2d8532d575c9f443355bda82 Mon Sep 17 00:00:00 2001 From: michael faith Date: Tue, 18 Jun 2024 07:10:18 -0500 Subject: [PATCH 41/50] [New] add support for Flat Config This change adds support for ESLint's new Flat config system. It maintains backwards compatibility with `eslintrc`-style configs as well. To achieve this, we're now dynamically creating flat configs on a new `flatConfigs` export. Usage ```js import importPlugin from 'eslint-plugin-import'; import js from '@eslint/js'; import tsParser from '@typescript-eslint/parser'; export default [ js.configs.recommended, importPlugin.flatConfigs.recommended, importPlugin.flatConfigs.react, importPlugin.flatConfigs.typescript, { files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'], languageOptions: { parser: tsParser, ecmaVersion: 'latest', sourceType: 'module', }, ignores: ['eslint.config.js'], rules: { 'no-unused-vars': 'off', 'import/no-dynamic-require': 'warn', 'import/no-nodejs-modules': 'warn', }, }, ]; ``` --- .editorconfig | 1 + .eslintignore | 1 + CHANGELOG.md | 3 + README.md | 31 +++- config/flat/errors.js | 14 ++ config/flat/react.js | 19 +++ config/flat/recommended.js | 26 ++++ config/flat/warnings.js | 11 ++ config/react.js | 2 - config/typescript.js | 2 +- examples/flat/eslint.config.mjs | 25 +++ examples/flat/package.json | 17 +++ examples/flat/src/exports-unused.ts | 12 ++ examples/flat/src/exports.ts | 12 ++ examples/flat/src/imports.ts | 7 + examples/flat/src/jsx.tsx | 3 + examples/flat/tsconfig.json | 14 ++ examples/legacy/.eslintrc.cjs | 24 +++ examples/legacy/package.json | 16 ++ examples/legacy/src/exports-unused.ts | 12 ++ examples/legacy/src/exports.ts | 12 ++ examples/legacy/src/imports.ts | 7 + examples/legacy/src/jsx.tsx | 3 + examples/legacy/tsconfig.json | 14 ++ package.json | 3 + src/core/fsWalk.js | 48 ++++++ src/index.js | 31 ++++ src/rules/no-unused-modules.js | 210 ++++++++++++++++++++------ utils/ignore.js | 10 +- 29 files changed, 541 insertions(+), 49 deletions(-) create mode 100644 config/flat/errors.js create mode 100644 config/flat/react.js create mode 100644 config/flat/recommended.js create mode 100644 config/flat/warnings.js create mode 100644 examples/flat/eslint.config.mjs create mode 100644 examples/flat/package.json create mode 100644 examples/flat/src/exports-unused.ts create mode 100644 examples/flat/src/exports.ts create mode 100644 examples/flat/src/imports.ts create mode 100644 examples/flat/src/jsx.tsx create mode 100644 examples/flat/tsconfig.json create mode 100644 examples/legacy/.eslintrc.cjs create mode 100644 examples/legacy/package.json create mode 100644 examples/legacy/src/exports-unused.ts create mode 100644 examples/legacy/src/exports.ts create mode 100644 examples/legacy/src/imports.ts create mode 100644 examples/legacy/src/jsx.tsx create mode 100644 examples/legacy/tsconfig.json create mode 100644 src/core/fsWalk.js diff --git a/.editorconfig b/.editorconfig index e2bfac523f..b7b8d09991 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,3 +7,4 @@ insert_final_newline = true indent_style = space indent_size = 2 end_of_line = lf +quote_type = single diff --git a/.eslintignore b/.eslintignore index 9d22006820..3516f09b9c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,6 +7,7 @@ tests/files/with-syntax-error tests/files/just-json-files/invalid.json tests/files/typescript-d-ts/ resolvers/webpack/test/files +examples # we want to ignore "tests/files" here, but unfortunately doing so would # interfere with unit test and fail it for some reason. # tests/files diff --git a/CHANGELOG.md b/CHANGELOG.md index 7752473019..05d623f410 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange - [`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]) - [`no-unused-modules`]: Add `ignoreUnusedTypeExports` option ([#3011], thanks [@silverwind]) +- add support for Flat Config ([#3018], thanks [@michaelfaith]) ### Fixed - [`no-extraneous-dependencies`]: allow wrong path ([#3012], thanks [@chabb]) @@ -1125,6 +1126,7 @@ for info on changes for earlier releases. [#3036]: https://github.com/import-js/eslint-plugin-import/pull/3036 [#3033]: https://github.com/import-js/eslint-plugin-import/pull/3033 +[#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 @@ -1874,6 +1876,7 @@ for info on changes for earlier releases. [@meowtec]: https://github.com/meowtec [@mgwalker]: https://github.com/mgwalker [@mhmadhamster]: https://github.com/MhMadHamster +[@michaelfaith]: https://github.com/michaelfaith [@MikeyBeLike]: https://github.com/MikeyBeLike [@minervabot]: https://github.com/minervabot [@mpint]: https://github.com/mpint diff --git a/README.md b/README.md index 1fd113c7d0..bf563f4d7b 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,8 @@ The maintainers of `eslint-plugin-import` and thousands of other packages are wo npm install eslint-plugin-import --save-dev ``` +### Config - Legacy (`.eslintrc`) + All rules are off by default. However, you may configure them manually in your `.eslintrc.(yml|json|js)`, or extend one of the canned configs: @@ -123,7 +125,7 @@ plugins: - import rules: - import/no-unresolved: [2, {commonjs: true, amd: true}] + import/no-unresolved: [2, { commonjs: true, amd: true }] import/named: 2 import/namespace: 2 import/default: 2 @@ -131,6 +133,33 @@ rules: # etc... ``` +### Config - Flat (`eslint.config.js`) + +All rules are off by default. However, you may configure them manually +in your `eslint.config.(js|cjs|mjs)`, or extend one of the canned configs: + +```js +import importPlugin from 'eslint-plugin-import'; +import js from '@eslint/js'; + +export default [ + js.configs.recommended, + importPlugin.flatConfigs.recommended, + { + files: ['**/*.{js,mjs,cjs}'], + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + rules: { + 'no-unused-vars': 'off', + 'import/no-dynamic-require': 'warn', + 'import/no-nodejs-modules': 'warn', + }, + }, +]; +``` + ## TypeScript You may use the following snippet or assemble your own config using the granular settings described below it. diff --git a/config/flat/errors.js b/config/flat/errors.js new file mode 100644 index 0000000000..98c19f824d --- /dev/null +++ b/config/flat/errors.js @@ -0,0 +1,14 @@ +/** + * unopinionated config. just the things that are necessarily runtime errors + * waiting to happen. + * @type {Object} + */ +module.exports = { + rules: { + 'import/no-unresolved': 2, + 'import/named': 2, + 'import/namespace': 2, + 'import/default': 2, + 'import/export': 2, + }, +}; diff --git a/config/flat/react.js b/config/flat/react.js new file mode 100644 index 0000000000..0867471422 --- /dev/null +++ b/config/flat/react.js @@ -0,0 +1,19 @@ +/** + * Adds `.jsx` as an extension, and enables JSX parsing. + * + * Even if _you_ aren't using JSX (or .jsx) directly, if your dependencies + * define jsnext:main and have JSX internally, you may run into problems + * if you don't enable these settings at the top level. + */ +module.exports = { + settings: { + 'import/extensions': ['.js', '.jsx', '.mjs', '.cjs'], + }, + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, +}; diff --git a/config/flat/recommended.js b/config/flat/recommended.js new file mode 100644 index 0000000000..11bc1f52a4 --- /dev/null +++ b/config/flat/recommended.js @@ -0,0 +1,26 @@ +/** + * The basics. + * @type {Object} + */ +module.exports = { + rules: { + // analysis/correctness + 'import/no-unresolved': 'error', + 'import/named': 'error', + 'import/namespace': 'error', + 'import/default': 'error', + 'import/export': 'error', + + // red flags (thus, warnings) + 'import/no-named-as-default': 'warn', + 'import/no-named-as-default-member': 'warn', + 'import/no-duplicates': 'warn', + }, + + // need all these for parsing dependencies (even if _your_ code doesn't need + // all of them) + languageOptions: { + ecmaVersion: 2018, + sourceType: 'module', + }, +}; diff --git a/config/flat/warnings.js b/config/flat/warnings.js new file mode 100644 index 0000000000..e788ff9cde --- /dev/null +++ b/config/flat/warnings.js @@ -0,0 +1,11 @@ +/** + * more opinionated config. + * @type {Object} + */ +module.exports = { + rules: { + 'import/no-named-as-default': 1, + 'import/no-named-as-default-member': 1, + 'import/no-duplicates': 1, + }, +}; diff --git a/config/react.js b/config/react.js index 68555512d7..1ae8e1a51a 100644 --- a/config/react.js +++ b/config/react.js @@ -6,7 +6,6 @@ * if you don't enable these settings at the top level. */ module.exports = { - settings: { 'import/extensions': ['.js', '.jsx'], }, @@ -14,5 +13,4 @@ module.exports = { parserOptions: { ecmaFeatures: { jsx: true }, }, - }; diff --git a/config/typescript.js b/config/typescript.js index ff7d0795c8..d5eb57a465 100644 --- a/config/typescript.js +++ b/config/typescript.js @@ -9,7 +9,7 @@ // `.ts`/`.tsx`/`.js`/`.jsx` implementation. const typeScriptExtensions = ['.ts', '.cts', '.mts', '.tsx']; -const allExtensions = [...typeScriptExtensions, '.js', '.jsx']; +const allExtensions = [...typeScriptExtensions, '.js', '.jsx', '.mjs', '.cjs']; module.exports = { settings: { diff --git a/examples/flat/eslint.config.mjs b/examples/flat/eslint.config.mjs new file mode 100644 index 0000000000..370514a65b --- /dev/null +++ b/examples/flat/eslint.config.mjs @@ -0,0 +1,25 @@ +import importPlugin from 'eslint-plugin-import'; +import js from '@eslint/js'; +import tsParser from '@typescript-eslint/parser'; + +export default [ + js.configs.recommended, + importPlugin.flatConfigs.recommended, + importPlugin.flatConfigs.react, + importPlugin.flatConfigs.typescript, + { + files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'], + languageOptions: { + parser: tsParser, + ecmaVersion: 'latest', + sourceType: 'module', + }, + ignores: ['eslint.config.mjs', '**/exports-unused.ts'], + rules: { + 'no-unused-vars': 'off', + 'import/no-dynamic-require': 'warn', + 'import/no-nodejs-modules': 'warn', + 'import/no-unused-modules': ['warn', { unusedExports: true }], + }, + }, +]; diff --git a/examples/flat/package.json b/examples/flat/package.json new file mode 100644 index 0000000000..0894d29f28 --- /dev/null +++ b/examples/flat/package.json @@ -0,0 +1,17 @@ +{ + "name": "flat", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "lint": "cross-env ESLINT_USE_FLAT_CONFIG=true eslint src --report-unused-disable-directives" + }, + "devDependencies": { + "@eslint/js": "^9.5.0", + "@types/node": "^20.14.5", + "@typescript-eslint/parser": "^7.13.1", + "cross-env": "^7.0.3", + "eslint": "^8.57.0", + "eslint-plugin-import": "file:../..", + "typescript": "^5.4.5" + } +} diff --git a/examples/flat/src/exports-unused.ts b/examples/flat/src/exports-unused.ts new file mode 100644 index 0000000000..af8061ec2b --- /dev/null +++ b/examples/flat/src/exports-unused.ts @@ -0,0 +1,12 @@ +export type ScalarType = string | number; +export type ObjType = { + a: ScalarType; + b: ScalarType; +}; + +export const a = 13; +export const b = 18; + +const defaultExport: ObjType = { a, b }; + +export default defaultExport; diff --git a/examples/flat/src/exports.ts b/examples/flat/src/exports.ts new file mode 100644 index 0000000000..af8061ec2b --- /dev/null +++ b/examples/flat/src/exports.ts @@ -0,0 +1,12 @@ +export type ScalarType = string | number; +export type ObjType = { + a: ScalarType; + b: ScalarType; +}; + +export const a = 13; +export const b = 18; + +const defaultExport: ObjType = { a, b }; + +export default defaultExport; diff --git a/examples/flat/src/imports.ts b/examples/flat/src/imports.ts new file mode 100644 index 0000000000..643219ae42 --- /dev/null +++ b/examples/flat/src/imports.ts @@ -0,0 +1,7 @@ +//import c from './exports'; +import { a, b } from './exports'; +import type { ScalarType, ObjType } from './exports'; + +import path from 'path'; +import fs from 'node:fs'; +import console from 'console'; diff --git a/examples/flat/src/jsx.tsx b/examples/flat/src/jsx.tsx new file mode 100644 index 0000000000..970d53cb84 --- /dev/null +++ b/examples/flat/src/jsx.tsx @@ -0,0 +1,3 @@ +const Components = () => { + return <>; +}; diff --git a/examples/flat/tsconfig.json b/examples/flat/tsconfig.json new file mode 100644 index 0000000000..e100bfc980 --- /dev/null +++ b/examples/flat/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "rootDir": "./", + "moduleResolution": "Bundler", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + } +} diff --git a/examples/legacy/.eslintrc.cjs b/examples/legacy/.eslintrc.cjs new file mode 100644 index 0000000000..e3cec097f0 --- /dev/null +++ b/examples/legacy/.eslintrc.cjs @@ -0,0 +1,24 @@ +module.exports = { + root: true, + env: { es2022: true }, + extends: [ + 'eslint:recommended', + 'plugin:import/recommended', + 'plugin:import/react', + 'plugin:import/typescript', + ], + settings: {}, + ignorePatterns: ['.eslintrc.cjs', '**/exports-unused.ts'], + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + plugins: ['import'], + rules: { + 'no-unused-vars': 'off', + 'import/no-dynamic-require': 'warn', + 'import/no-nodejs-modules': 'warn', + 'import/no-unused-modules': ['warn', { unusedExports: true }], + }, +}; diff --git a/examples/legacy/package.json b/examples/legacy/package.json new file mode 100644 index 0000000000..e3ca094887 --- /dev/null +++ b/examples/legacy/package.json @@ -0,0 +1,16 @@ +{ + "name": "legacy", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "lint": "cross-env ESLINT_USE_FLAT_CONFIG=false eslint src --ext js,jsx,ts,tsx --report-unused-disable-directives" + }, + "devDependencies": { + "@types/node": "^20.14.5", + "@typescript-eslint/parser": "^7.13.1", + "cross-env": "^7.0.3", + "eslint": "^8.57.0", + "eslint-plugin-import": "file:../..", + "typescript": "^5.4.5" + } +} diff --git a/examples/legacy/src/exports-unused.ts b/examples/legacy/src/exports-unused.ts new file mode 100644 index 0000000000..af8061ec2b --- /dev/null +++ b/examples/legacy/src/exports-unused.ts @@ -0,0 +1,12 @@ +export type ScalarType = string | number; +export type ObjType = { + a: ScalarType; + b: ScalarType; +}; + +export const a = 13; +export const b = 18; + +const defaultExport: ObjType = { a, b }; + +export default defaultExport; diff --git a/examples/legacy/src/exports.ts b/examples/legacy/src/exports.ts new file mode 100644 index 0000000000..af8061ec2b --- /dev/null +++ b/examples/legacy/src/exports.ts @@ -0,0 +1,12 @@ +export type ScalarType = string | number; +export type ObjType = { + a: ScalarType; + b: ScalarType; +}; + +export const a = 13; +export const b = 18; + +const defaultExport: ObjType = { a, b }; + +export default defaultExport; diff --git a/examples/legacy/src/imports.ts b/examples/legacy/src/imports.ts new file mode 100644 index 0000000000..643219ae42 --- /dev/null +++ b/examples/legacy/src/imports.ts @@ -0,0 +1,7 @@ +//import c from './exports'; +import { a, b } from './exports'; +import type { ScalarType, ObjType } from './exports'; + +import path from 'path'; +import fs from 'node:fs'; +import console from 'console'; diff --git a/examples/legacy/src/jsx.tsx b/examples/legacy/src/jsx.tsx new file mode 100644 index 0000000000..970d53cb84 --- /dev/null +++ b/examples/legacy/src/jsx.tsx @@ -0,0 +1,3 @@ +const Components = () => { + return <>; +}; diff --git a/examples/legacy/tsconfig.json b/examples/legacy/tsconfig.json new file mode 100644 index 0000000000..e100bfc980 --- /dev/null +++ b/examples/legacy/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "rootDir": "./", + "moduleResolution": "Bundler", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + } +} diff --git a/package.json b/package.json index b0ddaf8168..f9a95fabb4 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,9 @@ "test": "npm run tests-only", "test-compiled": "npm run prepublish && BABEL_ENV=testCompiled mocha --compilers js:babel-register tests/src", "test-all": "node --require babel-register ./scripts/testAll", + "test-examples": "npm run build && npm run test-example:legacy && npm run test-example:flat", + "test-example:legacy": "cd examples/legacy && npm install && npm run lint", + "test-example:flat": "cd examples/flat && npm install && npm run lint", "prepublishOnly": "safe-publish-latest && npm run build", "prepublish": "not-in-publish || npm run prepublishOnly", "preupdate:eslint-docs": "npm run build", diff --git a/src/core/fsWalk.js b/src/core/fsWalk.js new file mode 100644 index 0000000000..fa112590f1 --- /dev/null +++ b/src/core/fsWalk.js @@ -0,0 +1,48 @@ +/** + * This is intended to provide similar capability as the sync api from @nodelib/fs.walk, until `eslint-plugin-import` + * is willing to modernize and update their minimum node version to at least v16. I intentionally made the + * shape of the API (for the part we're using) the same as @nodelib/fs.walk so that that can be swapped in + * when the repo is ready for it. + */ + +import path from 'path'; +import { readdirSync } from 'fs'; + +/** @typedef {{ name: string, path: string, dirent: import('fs').Dirent }} Entry */ + +/** + * Do a comprehensive walk of the provided src directory, and collect all entries. Filter out + * any directories or entries using the optional filter functions. + * @param {string} root - path to the root of the folder we're walking + * @param {{ deepFilter?: (entry: Entry) => boolean, entryFilter?: (entry: Entry) => boolean }} options + * @param {Entry} currentEntry - entry for the current directory we're working in + * @param {Entry[]} existingEntries - list of all entries so far + * @returns {Entry[]} an array of directory entries + */ +export function walkSync(root, options, currentEntry, existingEntries) { + // Extract the filter functions. Default to evaluating true, if no filter passed in. + const { deepFilter = () => true, entryFilter = () => true } = options; + + let entryList = existingEntries || []; + const currentRelativePath = currentEntry ? currentEntry.path : '.'; + const fullPath = currentEntry ? path.join(root, currentEntry.path) : root; + + const dirents = readdirSync(fullPath, { withFileTypes: true }); + dirents.forEach((dirent) => { + /** @type {Entry} */ + const entry = { + name: dirent.name, + path: path.join(currentRelativePath, dirent.name), + dirent, + }; + + if (dirent.isDirectory() && deepFilter(entry)) { + entryList.push(entry); + entryList = walkSync(root, options, entry, entryList); + } else if (dirent.isFile() && entryFilter(entry)) { + entryList.push(entry); + } + }); + + return entryList; +} diff --git a/src/index.js b/src/index.js index feafba9003..0ab82ebee8 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,5 @@ +import { name, version } from '../package.json'; + export const rules = { 'no-unresolved': require('./rules/no-unresolved'), named: require('./rules/named'), @@ -69,3 +71,32 @@ export const configs = { electron: require('../config/electron'), typescript: require('../config/typescript'), }; + +// Base Plugin Object +const importPlugin = { + meta: { name, version }, + rules, +}; + +// Create flat configs (Only ones that declare plugins and parser options need to be different from the legacy config) +const createFlatConfig = (baseConfig, configName) => ({ + ...baseConfig, + name: `import/${configName}`, + plugins: { import: importPlugin }, +}); + +export const flatConfigs = { + recommended: createFlatConfig( + require('../config/flat/recommended'), + 'recommended', + ), + + errors: createFlatConfig(require('../config/flat/errors'), 'errors'), + warnings: createFlatConfig(require('../config/flat/warnings'), 'warnings'), + + // useful stuff for folks using various environments + react: require('../config/flat/react'), + 'react-native': configs['react-native'], + electron: configs.electron, + typescript: configs.typescript, +}; diff --git a/src/rules/no-unused-modules.js b/src/rules/no-unused-modules.js index 46fc93bfe0..702f2f8899 100644 --- a/src/rules/no-unused-modules.js +++ b/src/rules/no-unused-modules.js @@ -7,61 +7,177 @@ import { getFileExtensions } from 'eslint-module-utils/ignore'; import resolve from 'eslint-module-utils/resolve'; import visit from 'eslint-module-utils/visit'; -import { dirname, join } from 'path'; +import { dirname, join, resolve as resolvePath } from 'path'; import readPkgUp from 'eslint-module-utils/readPkgUp'; import values from 'object.values'; import includes from 'array-includes'; import flatMap from 'array.prototype.flatmap'; +import { walkSync } from '../core/fsWalk'; import ExportMapBuilder from '../exportMap/builder'; import recursivePatternCapture from '../exportMap/patternCapture'; import docsUrl from '../docsUrl'; -let FileEnumerator; -let listFilesToProcess; +/** + * Attempt to load the internal `FileEnumerator` class, which has existed in a couple + * of different places, depending on the version of `eslint`. Try requiring it from both + * locations. + * @returns Returns the `FileEnumerator` class if its requirable, otherwise `undefined`. + */ +function requireFileEnumerator() { + let FileEnumerator; -try { - ({ FileEnumerator } = require('eslint/use-at-your-own-risk')); -} catch (e) { + // Try getting it from the eslint private / deprecated api try { - // has been moved to eslint/lib/cli-engine/file-enumerator in version 6 - ({ FileEnumerator } = require('eslint/lib/cli-engine/file-enumerator')); + ({ FileEnumerator } = require('eslint/use-at-your-own-risk')); } catch (e) { + // Absorb this if it's MODULE_NOT_FOUND + if (e.code !== 'MODULE_NOT_FOUND') { + throw e; + } + + // If not there, then try getting it from eslint/lib/cli-engine/file-enumerator (moved there in v6) try { - // eslint/lib/util/glob-util has been moved to eslint/lib/util/glob-utils with version 5.3 - const { listFilesToProcess: originalListFilesToProcess } = require('eslint/lib/util/glob-utils'); - - // Prevent passing invalid options (extensions array) to old versions of the function. - // https://github.com/eslint/eslint/blob/v5.16.0/lib/util/glob-utils.js#L178-L280 - // https://github.com/eslint/eslint/blob/v5.2.0/lib/util/glob-util.js#L174-L269 - listFilesToProcess = function (src, extensions) { - return originalListFilesToProcess(src, { - extensions, - }); - }; + ({ FileEnumerator } = require('eslint/lib/cli-engine/file-enumerator')); } catch (e) { - const { listFilesToProcess: originalListFilesToProcess } = require('eslint/lib/util/glob-util'); + // Absorb this if it's MODULE_NOT_FOUND + if (e.code !== 'MODULE_NOT_FOUND') { + throw e; + } + } + } + return FileEnumerator; +} + +/** + * + * @param FileEnumerator the `FileEnumerator` class from `eslint`'s internal api + * @param {string} src path to the src root + * @param {string[]} extensions list of supported extensions + * @returns {{ filename: string, ignored: boolean }[]} list of files to operate on + */ +function listFilesUsingFileEnumerator(FileEnumerator, src, extensions) { + const e = new FileEnumerator({ + extensions, + }); + + return Array.from( + e.iterateFiles(src), + ({ filePath, ignored }) => ({ filename: filePath, ignored }), + ); +} - listFilesToProcess = function (src, extensions) { - const patterns = src.concat(flatMap(src, (pattern) => extensions.map((extension) => (/\*\*|\*\./).test(pattern) ? pattern : `${pattern}/**/*${extension}`))); +/** + * Attempt to require old versions of the file enumeration capability from v6 `eslint` and earlier, and use + * those functions to provide the list of files to operate on + * @param {string} src path to the src root + * @param {string[]} extensions list of supported extensions + * @returns {string[]} list of files to operate on + */ +function listFilesWithLegacyFunctions(src, extensions) { + try { + // eslint/lib/util/glob-util has been moved to eslint/lib/util/glob-utils with version 5.3 + const { listFilesToProcess: originalListFilesToProcess } = require('eslint/lib/util/glob-utils'); + // Prevent passing invalid options (extensions array) to old versions of the function. + // https://github.com/eslint/eslint/blob/v5.16.0/lib/util/glob-utils.js#L178-L280 + // https://github.com/eslint/eslint/blob/v5.2.0/lib/util/glob-util.js#L174-L269 - return originalListFilesToProcess(patterns); - }; + return originalListFilesToProcess(src, { + extensions, + }); + } catch (e) { + // Absorb this if it's MODULE_NOT_FOUND + if (e.code !== 'MODULE_NOT_FOUND') { + throw e; } + + // Last place to try (pre v5.3) + const { + listFilesToProcess: originalListFilesToProcess, + } = require('eslint/lib/util/glob-util'); + const patterns = src.concat( + flatMap( + src, + (pattern) => extensions.map((extension) => (/\*\*|\*\./).test(pattern) ? pattern : `${pattern}/**/*${extension}`), + ), + ); + + return originalListFilesToProcess(patterns); } } -if (FileEnumerator) { - listFilesToProcess = function (src, extensions) { - const e = new FileEnumerator({ - extensions, +/** + * Given a source root and list of supported extensions, use fsWalk and the + * new `eslint` `context.session` api to build the list of files we want to operate on + * @param {string[]} srcPaths array of source paths (for flat config this should just be a singular root (e.g. cwd)) + * @param {string[]} extensions list of supported extensions + * @param {{ isDirectoryIgnored: (path: string) => boolean, isFileIgnored: (path: string) => boolean }} session eslint context session object + * @returns {string[]} list of files to operate on + */ +function listFilesWithModernApi(srcPaths, extensions, session) { + /** @type {string[]} */ + const files = []; + + for (let i = 0; i < srcPaths.length; i++) { + const src = srcPaths[i]; + // Use walkSync along with the new session api to gather the list of files + const entries = walkSync(src, { + deepFilter(entry) { + const fullEntryPath = resolvePath(src, entry.path); + + // Include the directory if it's not marked as ignore by eslint + return !session.isDirectoryIgnored(fullEntryPath); + }, + entryFilter(entry) { + const fullEntryPath = resolvePath(src, entry.path); + + // Include the file if it's not marked as ignore by eslint and its extension is included in our list + return ( + !session.isFileIgnored(fullEntryPath) + && extensions.find((extension) => entry.path.endsWith(extension)) + ); + }, }); - return Array.from(e.iterateFiles(src), ({ filePath, ignored }) => ({ - ignored, - filename: filePath, - })); - }; + // Filter out directories and map entries to their paths + files.push( + ...entries + .filter((entry) => !entry.dirent.isDirectory()) + .map((entry) => entry.path), + ); + } + return files; +} + +/** + * Given a src pattern and list of supported extensions, return a list of files to process + * with this rule. + * @param {string} src - file, directory, or glob pattern of files to act on + * @param {string[]} extensions - list of supported file extensions + * @param {import('eslint').Rule.RuleContext} context - the eslint context object + * @returns {string[] | { filename: string, ignored: boolean }[]} the list of files that this rule will evaluate. + */ +function listFilesToProcess(src, extensions, context) { + // If the context object has the new session functions, then prefer those + // Otherwise, fallback to using the deprecated `FileEnumerator` for legacy support. + // https://github.com/eslint/eslint/issues/18087 + if ( + context.session + && context.session.isFileIgnored + && context.session.isDirectoryIgnored + ) { + return listFilesWithModernApi(src, extensions, context.session); + } + + // Fallback to og FileEnumerator + const FileEnumerator = requireFileEnumerator(); + + // If we got the FileEnumerator, then let's go with that + if (FileEnumerator) { + return listFilesUsingFileEnumerator(FileEnumerator, src, extensions); + } + // If not, then we can try even older versions of this capability (listFilesToProcess) + return listFilesWithLegacyFunctions(src, extensions); } const EXPORT_DEFAULT_DECLARATION = 'ExportDefaultDeclaration'; @@ -163,6 +279,7 @@ const exportList = new Map(); const visitorKeyMap = new Map(); +/** @type {Set} */ const ignoredFiles = new Set(); const filesOutsideSrc = new Set(); @@ -172,22 +289,30 @@ const isNodeModule = (path) => (/\/(node_modules)\//).test(path); * read all files matching the patterns in src and ignoreExports * * return all files matching src pattern, which are not matching the ignoreExports pattern + * @type {(src: string, ignoreExports: string, context: import('eslint').Rule.RuleContext) => Set} */ -const resolveFiles = (src, ignoreExports, context) => { +function resolveFiles(src, ignoreExports, context) { const extensions = Array.from(getFileExtensions(context.settings)); - const srcFileList = listFilesToProcess(src, extensions); + const srcFileList = listFilesToProcess(src, extensions, context); // prepare list of ignored files - const ignoredFilesList = listFilesToProcess(ignoreExports, extensions); - ignoredFilesList.forEach(({ filename }) => ignoredFiles.add(filename)); + const ignoredFilesList = listFilesToProcess(ignoreExports, extensions, context); + + // The modern api will return a list of file paths, rather than an object + if (ignoredFilesList.length && typeof ignoredFilesList[0] === 'string') { + ignoredFilesList.forEach((filename) => ignoredFiles.add(filename)); + } else { + ignoredFilesList.forEach(({ filename }) => ignoredFiles.add(filename)); + } // prepare list of source files, don't consider files from node_modules + const resolvedFiles = srcFileList.length && typeof srcFileList[0] === 'string' + ? srcFileList.filter((filePath) => !isNodeModule(filePath)) + : flatMap(srcFileList, ({ filename }) => isNodeModule(filename) ? [] : filename); - return new Set( - flatMap(srcFileList, ({ filename }) => isNodeModule(filename) ? [] : filename), - ); -}; + return new Set(resolvedFiles); +} /** * parse all source files and build up 2 maps containing the existing imports and exports @@ -226,7 +351,7 @@ const prepareImportsAndExports = (srcFiles, context) => { } else { exports.set(key, { whereUsed: new Set() }); } - const reexport = value.getImport(); + const reexport = value.getImport(); if (!reexport) { return; } @@ -329,6 +454,7 @@ const getSrc = (src) => { * prepare the lists of existing imports and exports - should only be executed once at * the start of a new eslint run */ +/** @type {Set} */ let srcFiles; let lastPrepareKey; const doPreparation = (src, ignoreExports, context) => { diff --git a/utils/ignore.js b/utils/ignore.js index 56f2ef7239..a42d4ceb1f 100644 --- a/utils/ignore.js +++ b/utils/ignore.js @@ -14,7 +14,7 @@ const log = require('debug')('eslint-plugin-import:utils:ignore'); function makeValidExtensionSet(settings) { // start with explicit JS-parsed extensions /** @type {Set} */ - const exts = new Set(settings['import/extensions'] || ['.js']); + const exts = new Set(settings['import/extensions'] || ['.js', '.mjs', '.cjs']); // all alternate parser extensions are also valid if ('import/parsers' in settings) { @@ -52,9 +52,13 @@ exports.hasValidExtension = hasValidExtension; /** @type {import('./ignore').default} */ exports.default = function ignore(path, context) { // check extension whitelist first (cheap) - if (!hasValidExtension(path, context)) { return true; } + if (!hasValidExtension(path, context)) { + return true; + } - if (!('import/ignore' in context.settings)) { return false; } + if (!('import/ignore' in context.settings)) { + return false; + } const ignoreStrings = context.settings['import/ignore']; for (let i = 0; i < ignoreStrings.length; i++) { From ee1ea025a6843fe4380927832a31761f1f4ae339 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Sat, 13 Jan 2024 01:42:54 +0900 Subject: [PATCH 42/50] [Fix] `newline-after-import`: fix considerComments option when require --- CHANGELOG.md | 3 ++ src/rules/newline-after-import.js | 20 +++++++++---- tests/src/rules/newline-after-import.js | 38 +++++++++++++++++++++---- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05d623f410..2d0ff1a912 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange - [`no-extraneous-dependencies`]: allow wrong path ([#3012], thanks [@chabb]) - [`no-cycle`]: use scc algorithm to optimize ([#2998], thanks [@soryy708]) - [`no-duplicates`]: Removing duplicates breaks in TypeScript ([#3033], thanks [@yesl-kim]) +- [`newline-after-import`]: fix considerComments option when require ([#2952], thanks [@developer-bandi]) ### Changed - [Docs] `no-extraneous-dependencies`: Make glob pattern description more explicit ([#2944], thanks [@mulztob]) @@ -1136,6 +1137,7 @@ for info on changes for earlier releases. [#2987]: https://github.com/import-js/eslint-plugin-import/pull/2987 [#2985]: https://github.com/import-js/eslint-plugin-import/pull/2985 [#2982]: https://github.com/import-js/eslint-plugin-import/pull/2982 +[#2952]: https://github.com/import-js/eslint-plugin-import/pull/2952 [#2944]: https://github.com/import-js/eslint-plugin-import/pull/2944 [#2942]: https://github.com/import-js/eslint-plugin-import/pull/2942 [#2919]: https://github.com/import-js/eslint-plugin-import/pull/2919 @@ -1742,6 +1744,7 @@ for info on changes for earlier releases. [@bicstone]: https://github.com/bicstone [@Blasz]: https://github.com/Blasz [@bmish]: https://github.com/bmish +[@developer-bandi]: https://github.com/developer-bandi [@borisyankov]: https://github.com/borisyankov [@bradennapier]: https://github.com/bradennapier [@bradzacher]: https://github.com/bradzacher diff --git a/src/rules/newline-after-import.js b/src/rules/newline-after-import.js index a33bb615b9..d10b87d78c 100644 --- a/src/rules/newline-after-import.js +++ b/src/rules/newline-after-import.js @@ -124,7 +124,7 @@ module.exports = { } } - function commentAfterImport(node, nextComment) { + function commentAfterImport(node, nextComment, type) { const lineDifference = getLineDifference(node, nextComment); const EXPECTED_LINE_DIFFERENCE = options.count + 1; @@ -140,7 +140,7 @@ module.exports = { line: node.loc.end.line, column, }, - message: `Expected ${options.count} empty line${options.count > 1 ? 's' : ''} after import statement not followed by another import.`, + message: `Expected ${options.count} empty line${options.count > 1 ? 's' : ''} after ${type} statement not followed by another ${type}.`, fix: options.exactCount && EXPECTED_LINE_DIFFERENCE < lineDifference ? undefined : (fixer) => fixer.insertTextAfter( node, '\n'.repeat(EXPECTED_LINE_DIFFERENCE - lineDifference), @@ -178,7 +178,7 @@ module.exports = { } if (nextComment && typeof nextComment !== 'undefined') { - commentAfterImport(node, nextComment); + commentAfterImport(node, nextComment, 'import'); } else if (nextNode && nextNode.type !== 'ImportDeclaration' && (nextNode.type !== 'TSImportEqualsDeclaration' || nextNode.isExport)) { checkForNewLine(node, nextNode, 'import'); } @@ -215,8 +215,18 @@ module.exports = { || !containsNodeOrEqual(nextStatement, nextRequireCall) ) ) { - - checkForNewLine(statementWithRequireCall, nextStatement, 'require'); + let nextComment; + if (typeof statementWithRequireCall.parent.comments !== 'undefined' && options.considerComments) { + const endLine = node.loc.end.line; + nextComment = statementWithRequireCall.parent.comments.find((o) => o.loc.start.line >= endLine && o.loc.start.line <= endLine + options.count + 1); + } + + if (nextComment && typeof nextComment !== 'undefined') { + + commentAfterImport(statementWithRequireCall, nextComment, 'require'); + } else { + checkForNewLine(statementWithRequireCall, nextStatement, 'require'); + } } }); }, diff --git a/tests/src/rules/newline-after-import.js b/tests/src/rules/newline-after-import.js index 6a8fb83e40..b78e891a35 100644 --- a/tests/src/rules/newline-after-import.js +++ b/tests/src/rules/newline-after-import.js @@ -8,6 +8,7 @@ import { getTSParsers, parsers, testVersion } from '../utils'; const IMPORT_ERROR_MESSAGE = 'Expected 1 empty line after import statement not followed by another import.'; const IMPORT_ERROR_MESSAGE_MULTIPLE = (count) => `Expected ${count} empty lines after import statement not followed by another import.`; const REQUIRE_ERROR_MESSAGE = 'Expected 1 empty line after require statement not followed by another require.'; +const REQUIRE_ERROR_MESSAGE_MULTIPLE = (count) => `Expected ${count} empty lines after require statement not followed by another require.`; const ruleTester = new RuleTester(); @@ -202,7 +203,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { options: [{ count: 4, exactCount: true }], }, { - code: `var foo = require('foo-module');\n\n\n\n// Some random comment\nvar foo = 'bar';`, + code: `var foo = require('foo-module');\n\n\n\n\n// Some random comment\nvar foo = 'bar';`, parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, options: [{ count: 4, exactCount: true, considerComments: true }], }, @@ -394,6 +395,19 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { `, parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, }, + { + code: `var foo = require('foo-module');\n\n\n// Some random comment\nvar foo = 'bar';`, + options: [{ count: 2, considerComments: true }], + }, + { + code: `var foo = require('foo-module');\n\n\n/**\n * Test comment\n */\nvar foo = 'bar';`, + options: [{ count: 2, considerComments: true }], + }, + { + code: `const foo = require('foo');\n\n\n// some random comment\nconst bar = function() {};`, + options: [{ count: 2, exactCount: true, considerComments: true }], + parserOptions: { ecmaVersion: 2015 }, + }, ), invalid: [].concat( @@ -825,7 +839,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { errors: [{ line: 1, column: 1, - message: 'Expected 2 empty lines after require statement not followed by another require.', + message: REQUIRE_ERROR_MESSAGE_MULTIPLE(2), }], parserOptions: { ecmaVersion: 2015 }, }, @@ -836,7 +850,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { errors: [{ line: 1, column: 1, - message: 'Expected 2 empty lines after require statement not followed by another require.', + message: REQUIRE_ERROR_MESSAGE_MULTIPLE(2), }], parserOptions: { ecmaVersion: 2015 }, }, @@ -852,14 +866,26 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { parserOptions: { ecmaVersion: 2015, considerComments: true, sourceType: 'module' }, }, { - code: `const foo = require('foo');\n\n\n// some random comment\nconst bar = function() {};`, - options: [{ count: 2, exactCount: true, considerComments: true }], + code: `var foo = require('foo-module');\nvar foo = require('foo-module');\n\n// Some random comment\nvar foo = 'bar';`, + output: `var foo = require('foo-module');\nvar foo = require('foo-module');\n\n\n// Some random comment\nvar foo = 'bar';`, + errors: [{ + line: 2, + column: 1, + message: REQUIRE_ERROR_MESSAGE_MULTIPLE(2), + }], + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + options: [{ considerComments: true, count: 2 }], + }, + { + code: `var foo = require('foo-module');\n\n/**\n * Test comment\n */\nvar foo = 'bar';`, + output: `var foo = require('foo-module');\n\n\n/**\n * Test comment\n */\nvar foo = 'bar';`, errors: [{ line: 1, column: 1, - message: 'Expected 2 empty lines after require statement not followed by another require.', + message: REQUIRE_ERROR_MESSAGE_MULTIPLE(2), }], parserOptions: { ecmaVersion: 2015 }, + options: [{ considerComments: true, count: 2 }], }, ), }); From 32a2b8986961639cc9c19ebac1f1f0640fb78ef5 Mon Sep 17 00:00:00 2001 From: Mihkel Eidast Date: Tue, 26 Sep 2023 14:13:04 +0300 Subject: [PATCH 43/50] [Fix] `order`: do not compare first path segment for relative paths (#2682) --- CHANGELOG.md | 4 ++++ src/rules/order.js | 6 ++++++ tests/src/rules/order.js | 28 ++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d0ff1a912..92431a1563 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange - [`no-cycle`]: use scc algorithm to optimize ([#2998], thanks [@soryy708]) - [`no-duplicates`]: Removing duplicates breaks in TypeScript ([#3033], thanks [@yesl-kim]) - [`newline-after-import`]: fix considerComments option when require ([#2952], thanks [@developer-bandi]) +- [`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]) @@ -1141,6 +1142,7 @@ for info on changes for earlier releases. [#2944]: https://github.com/import-js/eslint-plugin-import/pull/2944 [#2942]: https://github.com/import-js/eslint-plugin-import/pull/2942 [#2919]: https://github.com/import-js/eslint-plugin-import/pull/2919 +[#2885]: https://github.com/import-js/eslint-plugin-import/pull/2885 [#2884]: https://github.com/import-js/eslint-plugin-import/pull/2884 [#2866]: https://github.com/import-js/eslint-plugin-import/pull/2866 [#2854]: https://github.com/import-js/eslint-plugin-import/pull/2854 @@ -1486,6 +1488,7 @@ for info on changes for earlier releases. [#2930]: https://github.com/import-js/eslint-plugin-import/issues/2930 [#2687]: https://github.com/import-js/eslint-plugin-import/issues/2687 [#2684]: https://github.com/import-js/eslint-plugin-import/issues/2684 +[#2682]: https://github.com/import-js/eslint-plugin-import/issues/2682 [#2674]: https://github.com/import-js/eslint-plugin-import/issues/2674 [#2668]: https://github.com/import-js/eslint-plugin-import/issues/2668 [#2666]: https://github.com/import-js/eslint-plugin-import/issues/2666 @@ -1880,6 +1883,7 @@ for info on changes for earlier releases. [@mgwalker]: https://github.com/mgwalker [@mhmadhamster]: https://github.com/MhMadHamster [@michaelfaith]: https://github.com/michaelfaith +[@mihkeleidast]: https://github.com/mihkeleidast [@MikeyBeLike]: https://github.com/MikeyBeLike [@minervabot]: https://github.com/minervabot [@mpint]: https://github.com/mpint diff --git a/src/rules/order.js b/src/rules/order.js index 7071513bf3..1b25273c65 100644 --- a/src/rules/order.js +++ b/src/rules/order.js @@ -302,6 +302,12 @@ function getSorter(alphabetizeOptions) { const b = B.length; for (let i = 0; i < Math.min(a, b); i++) { + // Skip comparing the first path segment, if they are relative segments for both imports + if (i === 0 && ((A[i] === '.' || A[i] === '..') && (B[i] === '.' || B[i] === '..'))) { + // If one is sibling and the other parent import, no need to compare at all, since the paths belong in different groups + if (A[i] !== B[i]) { break; } + continue; + } result = compareString(A[i], B[i]); if (result) { break; } } diff --git a/tests/src/rules/order.js b/tests/src/rules/order.js index a6a8735a6f..c2d659f839 100644 --- a/tests/src/rules/order.js +++ b/tests/src/rules/order.js @@ -169,6 +169,34 @@ ruleTester.run('order', rule, { ['sibling', 'parent', 'external'], ] }], }), + // Grouping import types and alphabetize + test({ + code: ` + import async from 'async'; + import fs from 'fs'; + import path from 'path'; + + import index from '.'; + import relParent3 from '../'; + import relParent1 from '../foo'; + import sibling from './foo'; + `, + options: [{ groups: [ + ['builtin', 'external'], + ], alphabetize: { order: 'asc', caseInsensitive: true } }], + }), + test({ + code: ` + import { fooz } from '../baz.js' + import { foo } from './bar.js' + `, + options: [{ + alphabetize: { order: 'asc', caseInsensitive: true }, + groups: ['builtin', 'external', 'internal', ['parent', 'sibling', 'index'], 'object'], + 'newlines-between': 'always', + warnOnUnassignedImports: true, + }], + }), // Omitted types should implicitly be considered as the last type test({ code: ` From 038c26cade3c85c823ba2eafd52bb91ae458f2b2 Mon Sep 17 00:00:00 2001 From: jwbth <33615628+jwbth@users.noreply.github.com> Date: Thu, 28 Mar 2024 22:38:13 +0400 Subject: [PATCH 44/50] [readme] Clarify how to install the plugin The markup was misleading, as it put several alternatives into one block of code while not making it clear where the alternatives begin and end, forcing the reader to think hard about it. Also converted most yaml examples to jsonc. Co-authored-by: jwbth <33615628+jwbth@users.noreply.github.com> Co-authored-by: Jordan Harband --- CHANGELOG.md | 3 + README.md | 176 ++++++++++++++++++++++++++++++++------------------- 2 files changed, 115 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92431a1563..9022dc887b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange - [Refactor] `exportMapBuilder`: avoid hoisting ([#2989], thanks [@soryy708]) - [Refactor] `ExportMap`: extract "builder" logic to separate files ([#2991], thanks [@soryy708]) - [Docs] [`order`]: update the description of the `pathGroupsExcludedImportTypes` option ([#3036], thanks [@liby]) +- [readme] Clarify how to install the plugin ([#2993], thanks [@jwbth]) ## [2.29.1] - 2023-12-14 @@ -1133,6 +1134,7 @@ for info on changes for earlier releases. [#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 +[#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 [#2987]: https://github.com/import-js/eslint-plugin-import/pull/2987 @@ -1835,6 +1837,7 @@ for info on changes for earlier releases. [@jseminck]: https://github.com/jseminck [@julien1619]: https://github.com/julien1619 [@justinanastos]: https://github.com/justinanastos +[@jwbth]: https://github.com/jwbth [@k15a]: https://github.com/k15a [@kentcdodds]: https://github.com/kentcdodds [@kevin940726]: https://github.com/kevin940726 diff --git a/README.md b/README.md index bf563f4d7b..8cc723423f 100644 --- a/README.md +++ b/README.md @@ -108,35 +108,37 @@ npm install eslint-plugin-import --save-dev ### Config - Legacy (`.eslintrc`) -All rules are off by default. However, you may configure them manually -in your `.eslintrc.(yml|json|js)`, or extend one of the canned configs: +All rules are off by default. However, you may extend one of the preset configs, or configure them manually in your `.eslintrc.(yml|json|js)`. -```yaml ---- -extends: - - eslint:recommended - - plugin:import/recommended - # alternatively, 'recommended' is the combination of these two rule sets: - - plugin:import/errors - - plugin:import/warnings - -# or configure manually: -plugins: - - import - -rules: - import/no-unresolved: [2, { commonjs: true, amd: true }] - import/named: 2 - import/namespace: 2 - import/default: 2 - import/export: 2 - # etc... + - Extending a preset config: + +```jsonc +{ + "extends": [ + "eslint:recommended", + "plugin:import/recommended", + ], +} +``` + + - Configuring manually: + +```jsonc +{ + "rules": { + "import/no-unresolved": ["error", { "commonjs": true, "amd": true }] + "import/named": "error", + "import/namespace": "error", + "import/default": "error", + "import/export": "error", + // etc... + }, +}, ``` ### Config - Flat (`eslint.config.js`) -All rules are off by default. However, you may configure them manually -in your `eslint.config.(js|cjs|mjs)`, or extend one of the canned configs: +All rules are off by default. However, you may configure them manually in your `eslint.config.(js|cjs|mjs)`, or extend one of the preset configs: ```js import importPlugin from 'eslint-plugin-import'; @@ -166,18 +168,23 @@ You may use the following snippet or assemble your own config using the granular Make sure you have installed [`@typescript-eslint/parser`] and [`eslint-import-resolver-typescript`] which are used in the following configuration. -```yaml -extends: - - eslint:recommended - - plugin:import/recommended -# the following lines do the trick - - plugin:import/typescript -settings: - import/resolver: - # You will also need to install and configure the TypeScript resolver - # See also https://github.com/import-js/eslint-import-resolver-typescript#configuration - typescript: true - node: true +```jsonc +{ + "extends": [ + "eslint:recommended", + "plugin:import/recommended", +// the following lines do the trick + "plugin:import/typescript", + ], + "settings": { + "import/resolver": { + // You will also need to install and configure the TypeScript resolver + // See also https://github.com/import-js/eslint-import-resolver-typescript#configuration + "typescript": true, + "node": true, + }, + }, +} ``` [`@typescript-eslint/parser`]: https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser @@ -206,6 +213,16 @@ You can reference resolvers in several ways (in order of precedence): - as a conventional `eslint-import-resolver` name, like `eslint-import-resolver-foo`: + ```jsonc +// .eslintrc +{ + "settings": { + // uses 'eslint-import-resolver-foo': + "import/resolver": "foo", + }, +} +``` + ```yaml # .eslintrc.yml settings: @@ -226,6 +243,15 @@ module.exports = { - with a full npm module name, like `my-awesome-npm-module`: +```jsonc +// .eslintrc +{ + "settings": { + "import/resolver": "my-awesome-npm-module", + }, +} +``` + ```yaml # .eslintrc.yml settings: @@ -321,11 +347,15 @@ In practice, this means rules other than [`no-unresolved`](./docs/rules/no-unres `no-unresolved` has its own [`ignore`](./docs/rules/no-unresolved.md#ignore) setting. -```yaml -settings: - import/ignore: - - \.coffee$ # fraught with parse errors - - \.(scss|less|css)$ # can't parse unprocessed CSS modules, either +```jsonc +{ + "settings": { + "import/ignore": [ + "\.coffee$", // fraught with parse errors + "\.(scss|less|css)$", // can't parse unprocessed CSS modules, either + ], + }, +} ``` ### `import/core-modules` @@ -344,10 +374,13 @@ import 'electron' // without extra config, will be flagged as unresolved! that would otherwise be unresolved. To avoid this, you may provide `electron` as a core module: -```yaml -# .eslintrc.yml -settings: - import/core-modules: [ electron ] +```jsonc +// .eslintrc +{ + "settings": { + "import/core-modules": ["electron"], + }, +} ``` In Electron's specific case, there is a shared config named `electron` @@ -380,11 +413,15 @@ dependency parser will require and use the map key as the parser instead of the configured ESLint parser. This is useful if you're inter-op-ing with TypeScript directly using webpack, for example: -```yaml -# .eslintrc.yml -settings: - import/parsers: - "@typescript-eslint/parser": [ .ts, .tsx ] +```jsonc +// .eslintrc +{ + "settings": { + "import/parsers": { + "@typescript-eslint/parser": [".ts", ".tsx"], + }, + }, +} ``` In this case, [`@typescript-eslint/parser`](https://www.npmjs.com/package/@typescript-eslint/parser) @@ -414,20 +451,28 @@ For long-lasting processes, like [`eslint_d`] or [`eslint-loader`], however, it' If you never use [`eslint_d`] or [`eslint-loader`], you may set the cache lifetime to `Infinity` and everything should be fine: -```yaml -# .eslintrc.yml -settings: - import/cache: - lifetime: ∞ # or Infinity +```jsonc +// .eslintrc +{ + "settings": { + "import/cache": { + "lifetime": "∞", // or Infinity, in a JS config + }, + }, +} ``` Otherwise, set some integer, and cache entries will be evicted after that many seconds have elapsed: -```yaml -# .eslintrc.yml -settings: - import/cache: - lifetime: 5 # 30 is the default +```jsonc +// .eslintrc +{ + "settings": { + "import/cache": { + "lifetime": 5, // 30 is the default + }, + }, +} ``` [`eslint_d`]: https://www.npmjs.com/package/eslint_d @@ -441,10 +486,13 @@ By default, any package referenced from [`import/external-module-folders`](#impo For example, if your packages in a monorepo are all in `@scope`, you can configure `import/internal-regex` like this -```yaml -# .eslintrc.yml -settings: - import/internal-regex: ^@scope/ +```jsonc +// .eslintrc +{ + "settings": { + "import/internal-regex": "^@scope/", + }, +} ``` ## SublimeLinter-eslint From 8bdb32bc8be5364f4adeb781b2321ea62c9ab46e Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Tue, 3 Sep 2024 08:27:44 +1200 Subject: [PATCH 45/50] [Test] add explicit marker for trailing whitespace in cases --- tests/src/rules/dynamic-import-chunkname.js | 4 ++-- tests/src/rules/no-duplicates.js | 10 +++++----- tests/src/rules/no-import-module-exports.js | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/src/rules/dynamic-import-chunkname.js b/tests/src/rules/dynamic-import-chunkname.js index 6afd834ab0..81e018af76 100644 --- a/tests/src/rules/dynamic-import-chunkname.js +++ b/tests/src/rules/dynamic-import-chunkname.js @@ -1001,7 +1001,7 @@ ruleTester.run('dynamic-import-chunkname', rule, { { desc: 'Remove webpackChunkName', output: `import( - + ${''} /* webpackMode: "eager" */ 'someModule' )`, @@ -1010,7 +1010,7 @@ ruleTester.run('dynamic-import-chunkname', rule, { desc: 'Remove webpackMode', output: `import( /* webpackChunkName: "someModule" */ - + ${''} 'someModule' )`, }, diff --git a/tests/src/rules/no-duplicates.js b/tests/src/rules/no-duplicates.js index e682f22354..c46f9df85d 100644 --- a/tests/src/rules/no-duplicates.js +++ b/tests/src/rules/no-duplicates.js @@ -455,28 +455,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."], diff --git a/tests/src/rules/no-import-module-exports.js b/tests/src/rules/no-import-module-exports.js index c2bf7ed132..aa927857e0 100644 --- a/tests/src/rules/no-import-module-exports.js +++ b/tests/src/rules/no-import-module-exports.js @@ -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 From a3015ebd1bb6251990aee79e292d7116e9f191ff Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Tue, 3 Sep 2024 13:19:35 +1200 Subject: [PATCH 46/50] [Test] `namespace`: ensure valid case is actually included --- tests/src/rules/namespace.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/src/rules/namespace.js b/tests/src/rules/namespace.js index 1475ae9b7d..3f768a5717 100644 --- a/tests/src/rules/namespace.js +++ b/tests/src/rules/namespace.js @@ -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 }); From 0a58d7572c8267203182043608d56ead1c50f4ce Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Mon, 2 Sep 2024 23:05:07 -0700 Subject: [PATCH 47/50] [resolvers/webpack] v0.13.9 --- resolvers/webpack/CHANGELOG.md | 3 +++ resolvers/webpack/package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/resolvers/webpack/CHANGELOG.md b/resolvers/webpack/CHANGELOG.md index cd49cc3f4e..79b2837e3d 100644 --- a/resolvers/webpack/CHANGELOG.md +++ b/resolvers/webpack/CHANGELOG.md @@ -5,7 +5,10 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ## Unreleased +## 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` diff --git a/resolvers/webpack/package.json b/resolvers/webpack/package.json index 38465bcdee..60e5c900f0 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": { From 9d194a6e4690cc8afed68cb7736a81dd3a919135 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Mon, 2 Sep 2024 23:07:55 -0700 Subject: [PATCH 48/50] [utils] v2.9.0 --- utils/CHANGELOG.md | 7 +++++++ utils/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/utils/CHANGELOG.md b/utils/CHANGELOG.md index 43bd0e022b..27102bc73a 100644 --- a/utils/CHANGELOG.md +++ b/utils/CHANGELOG.md @@ -5,6 +5,11 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ## Unreleased +## v2.9.0 - 2024-09-02 + +### New +- add support for Flat Config ([#3018], thanks [@michaelfaith]) + ## v2.8.2 - 2024-08-25 ### Fixed @@ -151,6 +156,7 @@ Yanked due to critical issue with cache key resulting from #839. - `unambiguous.test()` regex is now properly in multiline mode [#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 @@ -197,6 +203,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/package.json b/utils/package.json index 6d69e2414a..fe3541ada3 100644 --- a/utils/package.json +++ b/utils/package.json @@ -1,6 +1,6 @@ { "name": "eslint-module-utils", - "version": "2.8.2", + "version": "2.9.0", "description": "Core utilities to support eslint-plugin-import and other module-related plugins.", "engines": { "node": ">=4" From 990229879c6790d64f3673ac59b0c9a9736f79fe Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Mon, 2 Sep 2024 23:09:11 -0700 Subject: [PATCH 49/50] [Deps] update `eslint-module-utils` --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f9a95fabb4..72c4a8259a 100644 --- a/package.json +++ b/package.json @@ -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.9.0", "hasown": "^2.0.2", "is-core-module": "^2.15.1", "is-glob": "^4.0.3", From 18787d3e6966028983af81a878d1a505893932d4 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Mon, 2 Sep 2024 23:09:39 -0700 Subject: [PATCH 50/50] Bump to 2.30.0 --- CHANGELOG.md | 5 ++++- package.json | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9022dc887b..cf97fff94d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ## [Unreleased] +## [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]) @@ -1615,7 +1617,8 @@ for info on changes for earlier releases. [#119]: https://github.com/import-js/eslint-plugin-import/issues/119 [#89]: https://github.com/import-js/eslint-plugin-import/issues/89 -[Unreleased]: https://github.com/import-js/eslint-plugin-import/compare/v2.29.1...HEAD +[Unreleased]: https://github.com/import-js/eslint-plugin-import/compare/v2.30.0...HEAD +[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 diff --git a/package.json b/package.json index 72c4a8259a..be150064d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-import", - "version": "2.29.1", + "version": "2.30.0", "description": "Import with sanity.", "engines": { "node": ">=4"