diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e148eb2e4..961a47b73c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange - [`no-deprecated`]: detect `@deprecated` on default-exported identifier declarations ([#3247], thanks [@mixelburg] [@etyrrell22]) ### Fixed +- [`export`]: Fixed false positives for duplicate exports when a value export and `export type *` expose the same name. ([#3260], [#3136], thanks [@mykola-mokhnach] [@morgan-coded]) +- [`export`]: Fixed false positives for duplicate exports when a value export and a type-only named re-export (`export type { x }` or `export { type x }`) expose the same name. ([#3136], thanks [@mykola-mokhnach] [@ljharb]) - [`no-duplicates`]: fix `prefer-inline` autofix producing invalid syntax when an identifier named `from` is imported ([#3236], thanks [@DukeDeSouth] [@Quantaly]) - [`no-duplicates`]: avoid false positives for TypeScript namespace and default type imports that cannot be merged ([#3195], thanks [@sjh9714] [@robyoder]) - ExportMap: resolve export * as ns re-exports under modern parsers ([#3250], thanks [@rasmi] [@JounQin] [@butterybread]) @@ -1195,6 +1197,7 @@ for info on changes for earlier releases. [`memo-parser`]: ./memo-parser/README.md +[#3260]: https://github.com/import-js/eslint-plugin-import/pull/3260 [#3250]: https://github.com/import-js/eslint-plugin-import/pull/3250 [#3247]: https://github.com/import-js/eslint-plugin-import/pull/3247 [#3246]: https://github.com/import-js/eslint-plugin-import/pull/3246 @@ -1212,6 +1215,7 @@ for info on changes for earlier releases. [#3152]: https://github.com/import-js/eslint-plugin-import/pull/3152 [#3151]: https://github.com/import-js/eslint-plugin-import/pull/3151 [#3138]: https://github.com/import-js/eslint-plugin-import/pull/3138 +[#3136]: https://github.com/import-js/eslint-plugin-import/issues/3136 [#3129]: https://github.com/import-js/eslint-plugin-import/pull/3129 [#3128]: https://github.com/import-js/eslint-plugin-import/pull/3128 [#3127]: https://github.com/import-js/eslint-plugin-import/pull/3127 @@ -2029,6 +2033,7 @@ for info on changes for earlier releases. [@msvab]: https://github.com/msvab [@mulztob]: https://github.com/mulztob [@mx-bernhard]: https://github.com/mx-bernhard +[@mykola-mokhnach]: https://github.com/mykola-mokhnach [@Nfinished]: https://github.com/Nfinished [@nickofthyme]: https://github.com/nickofthyme [@nicolashenry]: https://github.com/nicolashenry diff --git a/src/rules/export.js b/src/rules/export.js index fbbc39d75f..4a59272de7 100644 --- a/src/rules/export.js +++ b/src/rules/export.js @@ -142,10 +142,14 @@ module.exports = { }, ExportSpecifier(node) { + // `export type { x }` sets exportKind on the declaration; `export { type x }` sets it on the specifier. + // Either way the name belongs to the type namespace and must not clash with a value re-export. + const isType = node.exportKind === 'type' || node.parent.exportKind === 'type'; addNamed( node.exported.name || node.exported.value, node.exported, getParent(node.parent), + isType, ); }, @@ -194,7 +198,7 @@ module.exports = { remoteExports.forEach((v, name) => { if (name !== 'default') { any = true; // poor man's filter - addNamed(name, node, parent); + addNamed(name, node, parent, node.exportKind === 'type'); } }); diff --git a/tests/files/export-type-star/m.ts b/tests/files/export-type-star/m.ts new file mode 100644 index 0000000000..4ef784995d --- /dev/null +++ b/tests/files/export-type-star/m.ts @@ -0,0 +1,3 @@ +export interface Foo {} + +export function f(foo: Foo): void {} diff --git a/tests/src/rules/export.js b/tests/src/rules/export.js index 943bd72037..fa22538f12 100644 --- a/tests/src/rules/export.js +++ b/tests/src/rules/export.js @@ -1,4 +1,4 @@ -import { test, testFilePath, SYNTAX_CASES, getTSParsers, testVersion } from '../utils'; +import { test, testFilePath, SYNTAX_CASES, getTSParsers, testVersion, typescriptEslintParserSatisfies } from '../utils'; import { RuleTester } from '../rule-tester'; import eslintPkg from 'eslint/package.json'; @@ -337,6 +337,41 @@ context('TypeScript', function () { ...parserConfig, }), + // https://github.com/import-js/eslint-plugin-import/issues/3136 + // `export type *` re-exports names into the type namespace, so they must + // not be reported as duplicates of a same-named value re-export. + // `export type *` is TypeScript 5.0 syntax, first parsed by `@typescript-eslint/parser` 5.54, + // so this is gated on the parser version (below), not on the TypeScript version. + typescriptEslintParserSatisfies('>= 5.54') ? test({ + code: ` + export { f } from './m'; + export type * from './m'; + `, + filename: testFilePath('export-type-star/index.ts'), + ...parserConfig, + }) : [], + + // https://github.com/import-js/eslint-plugin-import/issues/3136 + // Named type-only re-exports (`export type { x }`) and inline type specifiers + // (`export { type x }`) also land in the type namespace, so they must not be + // reported as duplicates of a same-named value re-export. + typescriptEslintParserSatisfies('>= 5.54') ? test({ + code: ` + export { f } from './m'; + export type { f } from './m'; + `, + filename: testFilePath('export-type-star/index.ts'), + ...parserConfig, + }) : [], + typescriptEslintParserSatisfies('>= 5.54') ? test({ + code: ` + export { f } from './m'; + export { type f } from './m'; + `, + filename: testFilePath('export-type-star/index.ts'), + ...parserConfig, + }) : [], + semver.satisfies(eslintPkg.version, '>= 6') ? [ test({ code: `