From d66933cb1df96812fc36a08c27b062e271f7581a Mon Sep 17 00:00:00 2001 From: Joshua O'Brien Date: Fri, 20 Sep 2024 18:37:04 +1000 Subject: [PATCH 1/6] [Docs] `no-relative-packages`: fix typo --- CHANGELOG.md | 5 +++++ docs/rules/no-relative-packages.md | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e7ef808e..32899988a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange - `ExportMap` / flat config: include `languageOptions` in context ([#3052], thanks [@michaelfaith]) - [`no-named-as-default`]: Allow using an identifier if the export is both a named and a default export ([#3032], thanks [@akwodkiewicz]) +### Changed +- [Docs] [`no-relative-packages`]: fix typo ([#3066], thanks [@joshuaobrien]) + ## [2.30.0] - 2024-09-02 ### Added @@ -1136,6 +1139,7 @@ for info on changes for earlier releases. [`memo-parser`]: ./memo-parser/README.md +[#3066]: https://github.com/import-js/eslint-plugin-import/pull/3066 [#3052]: https://github.com/import-js/eslint-plugin-import/pull/3052 [#3043]: https://github.com/import-js/eslint-plugin-import/pull/3043 [#3036]: https://github.com/import-js/eslint-plugin-import/pull/3036 @@ -1840,6 +1844,7 @@ for info on changes for earlier releases. [@johnthagen]: https://github.com/johnthagen [@jonboiser]: https://github.com/jonboiser [@josh]: https://github.com/josh +[@joshuaobrien]: https://github.com/joshuaobrien [@JounQin]: https://github.com/JounQin [@jquense]: https://github.com/jquense [@jseminck]: https://github.com/jseminck diff --git a/docs/rules/no-relative-packages.md b/docs/rules/no-relative-packages.md index 4014ed985..ed724a9eb 100644 --- a/docs/rules/no-relative-packages.md +++ b/docs/rules/no-relative-packages.md @@ -6,8 +6,7 @@ Use this rule to prevent importing packages through relative paths. -It's useful in Yarn/Lerna workspaces, were it's possible to import a sibling -package using `../package` relative path, while direct `package` is the correct one. +It's useful in Yarn/Lerna workspaces, where it's possible to import a sibling package using `../package` relative path, while direct `package` is the correct one. ## Examples From 6075b9cb579022f45c2d132e1fbaa22976ca87c4 Mon Sep 17 00:00:00 2001 From: michael faith Date: Sun, 15 Sep 2024 10:10:47 -0500 Subject: [PATCH 2/6] [utils] [fix] `parse`: espree parser isn't working with flat config This change addresses an issue with using the espree parser with flat config. `keysFromParser` is responsible for finding the `visitorKeys` from a combination of the parser instance, parserPath, and the results of a call to `parseForESLint`. When using flat config, the `parserPath` will not be on the context object., and there are no results from `parseForESLint`. The espree parser _does_ provide visitor keys as a prop on the parser itself. However, this logic was only returning it when it found that "espree" was somewhere in the `parserPath` (which doesn't exist on flat config). I changed it so that it first does the check for the old-school babel parser using parser path, and then just checks the parser instance for the presence of `VisitorKeys`, rather than using parserPath at all. The reason for the re-order is that if the old babel-eslint parser, for some reason has `VisitorKeys`, having the new condition first, would change the behavior. --- .nycrc | 3 ++- examples/flat/eslint.config.mjs | 1 + examples/flat/src/depth-zero.js | 3 +++ examples/flat/src/es6/depth-one-dynamic.js | 3 +++ examples/legacy/.eslintrc.cjs | 1 + examples/legacy/src/depth-zero.js | 3 +++ examples/legacy/src/es6/depth-one-dynamic.js | 3 +++ utils/CHANGELOG.md | 2 ++ utils/parse.js | 12 +++++++++--- 9 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 examples/flat/src/depth-zero.js create mode 100644 examples/flat/src/es6/depth-one-dynamic.js create mode 100644 examples/legacy/src/depth-zero.js create mode 100644 examples/legacy/src/es6/depth-one-dynamic.js diff --git a/.nycrc b/.nycrc index 5d75e2157..c5396cb18 100644 --- a/.nycrc +++ b/.nycrc @@ -14,6 +14,7 @@ "resolvers/*/test", "scripts", "memo-parser", - "lib" + "lib", + "examples" ] } diff --git a/examples/flat/eslint.config.mjs b/examples/flat/eslint.config.mjs index 370514a65..143265265 100644 --- a/examples/flat/eslint.config.mjs +++ b/examples/flat/eslint.config.mjs @@ -20,6 +20,7 @@ export default [ 'import/no-dynamic-require': 'warn', 'import/no-nodejs-modules': 'warn', 'import/no-unused-modules': ['warn', { unusedExports: true }], + 'import/no-cycle': 'warn', }, }, ]; diff --git a/examples/flat/src/depth-zero.js b/examples/flat/src/depth-zero.js new file mode 100644 index 000000000..8cfde9979 --- /dev/null +++ b/examples/flat/src/depth-zero.js @@ -0,0 +1,3 @@ +import { foo } from "./es6/depth-one-dynamic"; + +foo(); diff --git a/examples/flat/src/es6/depth-one-dynamic.js b/examples/flat/src/es6/depth-one-dynamic.js new file mode 100644 index 000000000..ca129fd62 --- /dev/null +++ b/examples/flat/src/es6/depth-one-dynamic.js @@ -0,0 +1,3 @@ +export function foo() {} + +export const bar = () => import("../depth-zero").then(({foo}) => foo); diff --git a/examples/legacy/.eslintrc.cjs b/examples/legacy/.eslintrc.cjs index e3cec097f..90e065c9d 100644 --- a/examples/legacy/.eslintrc.cjs +++ b/examples/legacy/.eslintrc.cjs @@ -20,5 +20,6 @@ module.exports = { 'import/no-dynamic-require': 'warn', 'import/no-nodejs-modules': 'warn', 'import/no-unused-modules': ['warn', { unusedExports: true }], + 'import/no-cycle': 'warn', }, }; diff --git a/examples/legacy/src/depth-zero.js b/examples/legacy/src/depth-zero.js new file mode 100644 index 000000000..8cfde9979 --- /dev/null +++ b/examples/legacy/src/depth-zero.js @@ -0,0 +1,3 @@ +import { foo } from "./es6/depth-one-dynamic"; + +foo(); diff --git a/examples/legacy/src/es6/depth-one-dynamic.js b/examples/legacy/src/es6/depth-one-dynamic.js new file mode 100644 index 000000000..cda7091cd --- /dev/null +++ b/examples/legacy/src/es6/depth-one-dynamic.js @@ -0,0 +1,3 @@ +export function foo() {} + +export const bar = () => import("../depth-zero").then(({ foo }) => foo); diff --git a/utils/CHANGELOG.md b/utils/CHANGELOG.md index ca733eee0..94fc20f7d 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 ### Fixed - `parse`: remove unneeded extra backticks ([#3057], thanks [@G-Rath]) +- `parse`: espree parser isn't working with flat config ([#3061], thanks [@michaelfaith]) ## v2.11.0 - 2024-09-05 @@ -168,6 +169,7 @@ Yanked due to critical issue with cache key resulting from #839. ### Fixed - `unambiguous.test()` regex is now properly in multiline mode +[#3061]: https://github.com/import-js/eslint-plugin-import/pull/3061 [#3057]: https://github.com/import-js/eslint-plugin-import/pull/3057 [#3049]: https://github.com/import-js/eslint-plugin-import/pull/3049 [#3039]: https://github.com/import-js/eslint-plugin-import/pull/3039 diff --git a/utils/parse.js b/utils/parse.js index 21a443eca..a41937166 100644 --- a/utils/parse.js +++ b/utils/parse.js @@ -29,12 +29,18 @@ function keysFromParser(parserPath, parserInstance, parsedResult) { if (parsedResult && parsedResult.visitorKeys) { return parsedResult.visitorKeys; } - if (typeof parserPath === 'string' && (/.*espree.*/).test(parserPath)) { - return parserInstance.VisitorKeys; - } + // The old babel parser doesn't have a `parseForESLint` eslint function, so we don't end + // up with a `parsedResult` here. It also doesn't expose the visitor keys on the parser itself, + // so we have to try and infer the visitor-keys module from the parserPath. + // This is NOT supported in flat config! if (typeof parserPath === 'string' && (/.*babel-eslint.*/).test(parserPath)) { return getBabelEslintVisitorKeys(parserPath); } + // The espree parser doesn't have the `parseForESLint` function, so we don't ended up with a + // `parsedResult` here, but it does expose the visitor keys on the parser instance that we can use. + if (parserInstance && parserInstance.VisitorKeys) { + return parserInstance.VisitorKeys; + } return null; } From 0642419aeee4de20f006b1aee1f04510eeb1e6fb Mon Sep 17 00:00:00 2001 From: michael faith Date: Wed, 18 Sep 2024 17:01:51 -0500 Subject: [PATCH 3/6] [utils] [fix] `parse`: add `ecmaVersion` and `sourceType` to `parserOptions` This change adds `ecmaVersion` and `sourceType` to the options we're passing to the parsers, if they're present on `languageOptions` (which would only be the case for flat config). --- tests/src/core/parse.js | 14 ++++++++++++++ utils/CHANGELOG.md | 1 + utils/parse.js | 16 ++++++++++++++-- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/tests/src/core/parse.js b/tests/src/core/parse.js index 275b93982..b21326890 100644 --- a/tests/src/core/parse.js +++ b/tests/src/core/parse.js @@ -138,4 +138,18 @@ describe('parse(content, { settings, ecmaFeatures })', function () { parseStubParser.parse = parseSpy; expect(parse.bind(null, path, content, { settings: {}, parserPath: 'espree', languageOptions: { parserOptions: { sourceType: 'module', ecmaVersion: 2015, ecmaFeatures: { jsx: true } } }, parserOptions: { sourceType: 'script' } })).not.to.throw(Error); }); + + it('passes ecmaVersion and sourceType from languageOptions to parser', () => { + const parseSpy = sinon.spy(); + const languageOptions = { ecmaVersion: 'latest', sourceType: 'module', parserOptions: { ecmaFeatures: { jsx: true } } }; + parseStubParser.parse = parseSpy; + parse(path, content, { settings: {}, parserPath: parseStubParserPath, languageOptions }); + expect(parseSpy.args[0][1], 'custom parser to clone the parserOptions object').to.not.equal(languageOptions); + expect(parseSpy.args[0][1], 'custom parser to get ecmaFeatures in parserOptions which is a clone of ecmaFeatures passed in') + .to.have.property('ecmaFeatures') + .that.is.eql(languageOptions.parserOptions.ecmaFeatures) + .and.is.not.equal(languageOptions.parserOptions.ecmaFeatures); + expect(parseSpy.args[0][1], 'custom parser to get ecmaVersion in parserOptions from languageOptions').to.have.property('ecmaVersion', languageOptions.ecmaVersion); + expect(parseSpy.args[0][1], 'custom parser to get sourceType in parserOptions from languageOptions').to.have.property('sourceType', languageOptions.sourceType); + }); }); diff --git a/utils/CHANGELOG.md b/utils/CHANGELOG.md index 94fc20f7d..704a282da 100644 --- a/utils/CHANGELOG.md +++ b/utils/CHANGELOG.md @@ -8,6 +8,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ### Fixed - `parse`: remove unneeded extra backticks ([#3057], thanks [@G-Rath]) - `parse`: espree parser isn't working with flat config ([#3061], thanks [@michaelfaith]) +- `parse`: add `ecmaVersion` and `sourceType` to `parserOptions` ([#3061], thanks [@michaelfaith]) ## v2.11.0 - 2024-09-05 diff --git a/utils/parse.js b/utils/parse.js index a41937166..03022ac40 100644 --- a/utils/parse.js +++ b/utils/parse.js @@ -36,7 +36,7 @@ function keysFromParser(parserPath, parserInstance, parsedResult) { if (typeof parserPath === 'string' && (/.*babel-eslint.*/).test(parserPath)) { return getBabelEslintVisitorKeys(parserPath); } - // The espree parser doesn't have the `parseForESLint` function, so we don't ended up with a + // The espree parser doesn't have the `parseForESLint` function, so we don't end up with a // `parsedResult` here, but it does expose the visitor keys on the parser instance that we can use. if (parserInstance && parserInstance.VisitorKeys) { return parserInstance.VisitorKeys; @@ -113,7 +113,8 @@ exports.default = function parse(path, content, context) { if (context == null) { throw new Error('need context to parse properly'); } // ESLint in "flat" mode only sets context.languageOptions.parserOptions - let parserOptions = context.languageOptions && context.languageOptions.parserOptions || context.parserOptions; + const languageOptions = context.languageOptions; + let parserOptions = languageOptions && languageOptions.parserOptions || context.parserOptions; const parserOrPath = getParser(path, context); if (!parserOrPath) { throw new Error('parserPath or languageOptions.parser is required!'); } @@ -144,6 +145,17 @@ exports.default = function parse(path, content, context) { delete parserOptions.project; delete parserOptions.projects; + // If this is a flat config, we need to add ecmaVersion and sourceType (if present) from languageOptions + if (languageOptions && languageOptions.ecmaVersion) { + parserOptions.ecmaVersion = languageOptions.ecmaVersion; + } + if (languageOptions && languageOptions.sourceType) { + // @ts-expect-error languageOptions is from the flatConfig Linter type in 8.57 while parserOptions is not. + // Non-flat config parserOptions.sourceType doesn't have "commonjs" in the type. Once upgraded to v9 types, + // they'll be the same and this expect-error should be removed. + parserOptions.sourceType = languageOptions.sourceType; + } + // require the parser relative to the main module (i.e., ESLint) const parser = typeof parserOrPath === 'string' ? moduleRequire(parserOrPath) : parserOrPath; From 91f944d8e4b1591aa45f7d1b734dc2a2fe9acc57 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Mon, 23 Sep 2024 15:34:39 -0700 Subject: [PATCH 4/6] [utils] v2.11.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 704a282da..6e71a26f7 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.11.1 - 2024-09-23 + ### Fixed - `parse`: remove unneeded extra backticks ([#3057], thanks [@G-Rath]) - `parse`: espree parser isn't working with flat config ([#3061], thanks [@michaelfaith]) diff --git a/utils/package.json b/utils/package.json index df63ac168..709142faf 100644 --- a/utils/package.json +++ b/utils/package.json @@ -1,6 +1,6 @@ { "name": "eslint-module-utils", - "version": "2.11.0", + "version": "2.11.1", "description": "Core utilities to support eslint-plugin-import and other module-related plugins.", "engines": { "node": ">=4" From a9815dac72e667f1cb9d898022183c428111b2fa Mon Sep 17 00:00:00 2001 From: liuxingbaoyu <30521560+liuxingbaoyu@users.noreply.github.com> Date: Thu, 19 Sep 2024 21:01:07 +0800 Subject: [PATCH 5/6] [Fix] `export`: False positive for exported overloaded functions in TS --- CHANGELOG.md | 3 +++ src/rules/export.js | 48 +++++++++++++-------------------------- tests/src/rules/export.js | 26 +++++++++++++++++++-- 3 files changed, 43 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32899988a..1dbd46fc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ### Fixed - `ExportMap` / flat config: include `languageOptions` in context ([#3052], thanks [@michaelfaith]) - [`no-named-as-default`]: Allow using an identifier if the export is both a named and a default export ([#3032], thanks [@akwodkiewicz]) +- [`export`]: False positive for exported overloaded functions in TS ([#3065], thanks [@liuxingbaoyu]) ### Changed - [Docs] [`no-relative-packages`]: fix typo ([#3066], thanks [@joshuaobrien]) @@ -1140,6 +1141,7 @@ for info on changes for earlier releases. [`memo-parser`]: ./memo-parser/README.md [#3066]: https://github.com/import-js/eslint-plugin-import/pull/3066 +[#3065]: https://github.com/import-js/eslint-plugin-import/pull/3065 [#3052]: https://github.com/import-js/eslint-plugin-import/pull/3052 [#3043]: https://github.com/import-js/eslint-plugin-import/pull/3043 [#3036]: https://github.com/import-js/eslint-plugin-import/pull/3036 @@ -1875,6 +1877,7 @@ for info on changes for earlier releases. [@lilling]: https://github.com/lilling [@ljharb]: https://github.com/ljharb [@ljqx]: https://github.com/ljqx +[@liuxingbaoyu]: https://github.com/liuxingbaoyu [@lo1tuma]: https://github.com/lo1tuma [@loganfsmyth]: https://github.com/loganfsmyth [@luczsoma]: https://github.com/luczsoma diff --git a/src/rules/export.js b/src/rules/export.js index 197a0eb51..fbbc39d75 100644 --- a/src/rules/export.js +++ b/src/rules/export.js @@ -2,7 +2,6 @@ import ExportMapBuilder from '../exportMap/builder'; import recursivePatternCapture from '../exportMap/patternCapture'; import docsUrl from '../docsUrl'; import includes from 'array-includes'; -import flatMap from 'array.prototype.flatmap'; /* Notes on TypeScript namespaces aka TSModuleDeclaration: @@ -27,42 +26,25 @@ const rootProgram = 'root'; const tsTypePrefix = 'type:'; /** - * Detect function overloads like: + * remove function overloads like: * ```ts * export function foo(a: number); * export function foo(a: string); - * export function foo(a: number|string) { return a; } * ``` * @param {Set} nodes - * @returns {boolean} */ -function isTypescriptFunctionOverloads(nodes) { - const nodesArr = Array.from(nodes); - - const idents = flatMap( - nodesArr, - (node) => node.declaration && ( - node.declaration.type === 'TSDeclareFunction' // eslint 6+ - || node.declaration.type === 'TSEmptyBodyFunctionDeclaration' // eslint 4-5 - ) - ? node.declaration.id.name - : [], - ); - if (new Set(idents).size !== idents.length) { - return true; - } - - const types = new Set(nodesArr.map((node) => node.parent.type)); - if (!types.has('TSDeclareFunction')) { - return false; - } - if (types.size === 1) { - return true; - } - if (types.size === 2 && types.has('FunctionDeclaration')) { - return true; - } - return false; +function removeTypescriptFunctionOverloads(nodes) { + nodes.forEach((node) => { + const declType = node.type === 'ExportDefaultDeclaration' ? node.declaration.type : node.parent.type; + if ( + // eslint 6+ + declType === 'TSDeclareFunction' + // eslint 4-5 + || declType === 'TSEmptyBodyFunctionDeclaration' + ) { + nodes.delete(node); + } + }); } /** @@ -227,9 +209,11 @@ module.exports = { 'Program:exit'() { for (const [, named] of namespace) { for (const [name, nodes] of named) { + removeTypescriptFunctionOverloads(nodes); + if (nodes.size <= 1) { continue; } - if (isTypescriptFunctionOverloads(nodes) || isTypescriptNamespaceMerging(nodes)) { continue; } + if (isTypescriptNamespaceMerging(nodes)) { continue; } for (const node of nodes) { if (shouldSkipTypescriptNamespace(node, nodes)) { continue; } diff --git a/tests/src/rules/export.js b/tests/src/rules/export.js index a7f2bec12..f16a25ecf 100644 --- a/tests/src/rules/export.js +++ b/tests/src/rules/export.js @@ -56,6 +56,15 @@ ruleTester.run('export', rule, { `, parser, })), + getTSParsers().map((parser) => ({ + code: ` + export default function foo(param: string): boolean; + export default function foo(param: string, param1?: number): boolean { + return param && param1; + } + `, + parser, + })), ), invalid: [].concat( @@ -154,6 +163,19 @@ ruleTester.run('export', rule, { ecmaVersion: 2022, }, })), + + getTSParsers().map((parser) => ({ + code: ` + export default function a(): void; + export default function a() {} + export { x as default }; + `, + errors: [ + 'Multiple default exports.', + 'Multiple default exports.', + ], + parser, + })), ), }); @@ -510,7 +532,7 @@ context('TypeScript', function () { }), test({ code: ` - export function Foo(); + export function Foo() { }; export class Foo { } export namespace Foo { } `, @@ -529,7 +551,7 @@ context('TypeScript', function () { test({ code: ` export const Foo = 'bar'; - export function Foo(); + export function Foo() { }; export namespace Foo { } `, errors: [ From 5c9757c5ac1a0e32392333f60f9e1f11dc53aebf Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Mon, 23 Sep 2024 22:30:42 -0700 Subject: [PATCH 6/6] [Refactor] add some more type info; switch for-ofs to forEaches --- src/rules/consistent-type-specifier-style.js | 19 ++++++- src/rules/no-cycle.js | 4 +- src/rules/no-duplicates.js | 34 +++++++---- src/rules/no-mutable-exports.js | 59 ++++++++++---------- 4 files changed, 72 insertions(+), 44 deletions(-) diff --git a/src/rules/consistent-type-specifier-style.js b/src/rules/consistent-type-specifier-style.js index ee5ff9fbc..84c33ecd8 100644 --- a/src/rules/consistent-type-specifier-style.js +++ b/src/rules/consistent-type-specifier-style.js @@ -6,6 +6,12 @@ function isComma(token) { return token.type === 'Punctuator' && token.value === ','; } +/** + * @param {import('eslint').Rule.Fix[]} fixes + * @param {import('eslint').Rule.RuleFixer} fixer + * @param {import('eslint').SourceCode.SourceCode} sourceCode + * @param {(ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier)[]} specifiers + * */ function removeSpecifiers(fixes, fixer, sourceCode, specifiers) { for (const specifier of specifiers) { // remove the trailing comma @@ -17,6 +23,7 @@ function removeSpecifiers(fixes, fixer, sourceCode, specifiers) { } } +/** @type {(node: import('estree').Node, sourceCode: import('eslint').SourceCode.SourceCode, specifiers: (ImportSpecifier | ImportNamespaceSpecifier)[], kind: 'type' | 'typeof') => string} */ function getImportText( node, sourceCode, @@ -38,6 +45,7 @@ function getImportText( return `import ${kind} {${names.join(', ')}} from ${sourceString};`; } +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { type: 'suggestion', @@ -102,6 +110,7 @@ module.exports = { // prefer-top-level return { + /** @param {import('estree').ImportDeclaration} node */ ImportDeclaration(node) { if ( // already top-level is valid @@ -120,9 +129,13 @@ module.exports = { return; } + /** @type {typeof node.specifiers} */ const typeSpecifiers = []; + /** @type {typeof node.specifiers} */ const typeofSpecifiers = []; + /** @type {typeof node.specifiers} */ const valueSpecifiers = []; + /** @type {typeof node.specifiers[number]} */ let defaultSpecifier = null; for (const specifier of node.specifiers) { if (specifier.type === 'ImportDefaultSpecifier') { @@ -144,6 +157,7 @@ module.exports = { const newImports = `${typeImport}\n${typeofImport}`.trim(); if (typeSpecifiers.length + typeofSpecifiers.length === node.specifiers.length) { + /** @type {('type' | 'typeof')[]} */ // all specifiers have inline specifiers - so we replace the entire import const kind = [].concat( typeSpecifiers.length > 0 ? 'type' : [], @@ -162,7 +176,7 @@ module.exports = { }); } else { // remove specific specifiers and insert new imports for them - for (const specifier of typeSpecifiers.concat(typeofSpecifiers)) { + typeSpecifiers.concat(typeofSpecifiers).forEach((specifier) => { context.report({ node: specifier, message: 'Prefer using a top-level {{kind}}-only import instead of inline {{kind}} specifiers.', @@ -170,6 +184,7 @@ module.exports = { kind: specifier.importKind, }, fix(fixer) { + /** @type {import('eslint').Rule.Fix[]} */ const fixes = []; // if there are no value specifiers, then the other report fixer will be called, not this one @@ -215,7 +230,7 @@ module.exports = { ); }, }); - } + }); } }, }; diff --git a/src/rules/no-cycle.js b/src/rules/no-cycle.js index d7c748d80..713503d9f 100644 --- a/src/rules/no-cycle.js +++ b/src/rules/no-cycle.js @@ -152,9 +152,9 @@ module.exports = { */ if (path === myPath && toTraverse.length > 0) { return true; } if (route.length + 1 < maxDepth) { - for (const { source } of toTraverse) { + toTraverse.forEach(({ source }) => { untraversed.push({ mget: getter, route: route.concat(source) }); - } + }); } } } diff --git a/src/rules/no-duplicates.js b/src/rules/no-duplicates.js index 32557802f..b8c8d848c 100644 --- a/src/rules/no-duplicates.js +++ b/src/rules/no-duplicates.js @@ -74,6 +74,7 @@ function hasProblematicComments(node, sourceCode) { ); } +/** @type {(first: import('estree').ImportDeclaration, rest: import('estree').ImportDeclaration[], sourceCode: import('eslint').SourceCode.SourceCode, context: import('eslint').Rule.RuleContext) => import('eslint').Rule.ReportFixer | undefined} */ function getFix(first, rest, sourceCode, context) { // Sorry ESLint <= 3 users, no autofix for you. Autofixing duplicate imports // requires multiple `fixer.whatever()` calls in the `fix`: We both need to @@ -123,7 +124,7 @@ function getFix(first, rest, sourceCode, context) { isEmpty: !hasSpecifiers(node), }; }) - .filter(Boolean); + .filter((x) => !!x); const unnecessaryImports = restWithoutComments.filter((node) => !hasSpecifiers(node) && !hasNamespace(node) @@ -139,6 +140,7 @@ function getFix(first, rest, sourceCode, context) { return undefined; } + /** @type {import('eslint').Rule.ReportFixer} */ return (fixer) => { const tokens = sourceCode.getTokens(first); const openBrace = tokens.find((token) => isPunctuator(token, '{')); @@ -185,6 +187,7 @@ function getFix(first, rest, sourceCode, context) { ['', !firstHasTrailingComma && !firstIsEmpty, firstExistingIdentifiers], ); + /** @type {import('eslint').Rule.Fix[]} */ const fixes = []; if (shouldAddSpecifiers && preferInline && first.importKind === 'type') { @@ -228,7 +231,7 @@ function getFix(first, rest, sourceCode, context) { } // Remove imports whose specifiers have been moved into the first import. - for (const specifier of specifiers) { + specifiers.forEach((specifier) => { const importNode = specifier.importNode; fixes.push(fixer.remove(importNode)); @@ -237,12 +240,12 @@ function getFix(first, rest, sourceCode, context) { if (charAfterImport === '\n') { fixes.push(fixer.removeRange(charAfterImportRange)); } - } + }); // Remove imports whose default import has been moved to the first import, // and side-effect-only imports that are unnecessary due to the first // import. - for (const node of unnecessaryImports) { + unnecessaryImports.forEach((node) => { fixes.push(fixer.remove(node)); const charAfterImportRange = [node.range[1], node.range[1] + 1]; @@ -250,12 +253,13 @@ function getFix(first, rest, sourceCode, context) { if (charAfterImport === '\n') { fixes.push(fixer.removeRange(charAfterImportRange)); } - } + }); return fixes; }; } +/** @type {(imported: Map, context: import('eslint').Rule.RuleContext) => void} */ function checkImports(imported, context) { for (const [module, nodes] of imported.entries()) { if (nodes.length > 1) { @@ -270,16 +274,17 @@ function checkImports(imported, context) { fix, // Attach the autofix (if any) to the first import. }); - for (const node of rest) { + rest.forEach((node) => { context.report({ node: node.source, message, }); - } + }); } } } +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { type: 'problem', @@ -305,10 +310,13 @@ module.exports = { ], }, + /** @param {import('eslint').Rule.RuleContext} context */ create(context) { + /** @type {boolean} */ // Prepare the resolver from options. - const considerQueryStringOption = context.options[0] - && context.options[0].considerQueryString; + const considerQueryStringOption = context.options[0] && context.options[0].considerQueryString; + /** @type {boolean} */ + const preferInline = context.options[0] && context.options[0]['prefer-inline']; const defaultResolver = (sourcePath) => resolve(sourcePath, context) || sourcePath; const resolver = considerQueryStringOption ? (sourcePath) => { const parts = sourcePath.match(/^([^?]*)\?(.*)$/); @@ -318,11 +326,14 @@ module.exports = { return `${defaultResolver(parts[1])}?${parts[2]}`; } : defaultResolver; + /** @type {Map, nsImported: Map, defaultTypesImported: Map, namedTypesImported: Map}>} */ const moduleMaps = new Map(); + /** @param {import('estree').ImportDeclaration} n */ + /** @returns {typeof moduleMaps[keyof typeof moduleMaps]} */ function getImportMap(n) { if (!moduleMaps.has(n.parent)) { - moduleMaps.set(n.parent, { + moduleMaps.set(n.parent, /** @type {typeof moduleMaps} */ { imported: new Map(), nsImported: new Map(), defaultTypesImported: new Map(), @@ -330,7 +341,6 @@ module.exports = { }); } const map = moduleMaps.get(n.parent); - const preferInline = context.options[0] && context.options[0]['prefer-inline']; if (!preferInline && n.importKind === 'type') { return n.specifiers.length > 0 && n.specifiers[0].type === 'ImportDefaultSpecifier' ? map.defaultTypesImported : map.namedTypesImported; } @@ -342,7 +352,9 @@ module.exports = { } return { + /** @param {import('estree').ImportDeclaration} n */ ImportDeclaration(n) { + /** @type {string} */ // resolved path will cover aliased duplicates const resolvedPath = resolver(n.source.value); const importMap = getImportMap(n); diff --git a/src/rules/no-mutable-exports.js b/src/rules/no-mutable-exports.js index c3d18b2c9..0a0e128dc 100644 --- a/src/rules/no-mutable-exports.js +++ b/src/rules/no-mutable-exports.js @@ -2,6 +2,7 @@ import { getScope } from 'eslint-module-utils/contextCompat'; import docsUrl from '../docsUrl'; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { type: 'suggestion', @@ -21,41 +22,41 @@ module.exports = { } } + /** @type {(scope: import('eslint').Scope.Scope, name: string) => void} */ function checkDeclarationsInScope({ variables }, name) { - for (const variable of variables) { - if (variable.name === name) { - for (const def of variable.defs) { - if (def.type === 'Variable' && def.parent) { + variables + .filter((variable) => variable.name === name) + .forEach((variable) => { + variable.defs + .filter((def) => def.type === 'Variable' && def.parent) + .forEach((def) => { checkDeclaration(def.parent); - } - } - } - } - } - - function handleExportDefault(node) { - const scope = getScope(context, node); - - if (node.declaration.name) { - checkDeclarationsInScope(scope, node.declaration.name); - } + }); + }); } - function handleExportNamed(node) { - const scope = getScope(context, node); + return { + /** @param {import('estree').ExportDefaultDeclaration} node */ + ExportDefaultDeclaration(node) { + const scope = getScope(context, node); - if (node.declaration) { - checkDeclaration(node.declaration); - } else if (!node.source) { - for (const specifier of node.specifiers) { - checkDeclarationsInScope(scope, specifier.local.name); + if ('name' in node.declaration && node.declaration.name) { + checkDeclarationsInScope(scope, node.declaration.name); } - } - } - - return { - ExportDefaultDeclaration: handleExportDefault, - ExportNamedDeclaration: handleExportNamed, + }, + + /** @param {import('estree').ExportNamedDeclaration} node */ + ExportNamedDeclaration(node) { + const scope = getScope(context, node); + + if ('declaration' in node && node.declaration) { + checkDeclaration(node.declaration); + } else if (!('source' in node) || !node.source) { + node.specifiers.forEach((specifier) => { + checkDeclarationsInScope(scope, specifier.local.name); + }); + } + }, }; }, };