From 757ffa9d4850d4decac467bf3b5e8c950a8a720e Mon Sep 17 00:00:00 2001 From: Sukka Date: Thu, 29 Aug 2024 16:20:51 +0800 Subject: [PATCH] fix(#123): make `no-named-as-default` ignores TS `namespace` (#133) --- .changeset/hungry-paws-whisper.md | 5 ++ package.json | 5 +- src/rules/no-named-as-default.ts | 67 ++++++++++++++------------ src/utils/export-map.ts | 4 +- test/package.spec.ts | 1 - test/rules/no-named-as-default.spec.ts | 55 +++++++++++++++++++-- test/utils.ts | 24 ++++----- yarn.lock | 12 +++++ 8 files changed, 123 insertions(+), 50 deletions(-) create mode 100644 .changeset/hungry-paws-whisper.md diff --git a/.changeset/hungry-paws-whisper.md b/.changeset/hungry-paws-whisper.md new file mode 100644 index 000000000..dd31a6734 --- /dev/null +++ b/.changeset/hungry-paws-whisper.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-import-x": patch +--- + +Fix #123 where the rule `no-named-as-default` will confuse TypeScript namespace exports with actual exports. diff --git a/package.json b/package.json index 5c0333bf0..0ad9a0475 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "@types/is-glob": "^4.0.4", "@types/jest": "^29.5.12", "@types/json-schema": "^7.0.15", + "@types/klaw-sync": "^6.0.5", "@types/node": "^20.11.30", "@typescript-eslint/eslint-plugin": "^8.1.0", "@typescript-eslint/parser": "^8.1.0", @@ -114,6 +115,7 @@ "eslint9": "npm:eslint@^9.8.0", "hermes-eslint": "^0.23.1", "jest": "^29.7.0", + "klaw-sync": "^6.0.0", "npm-run-all2": "^6.1.2", "prettier": "^3.2.5", "redux": "^5.0.1", @@ -121,6 +123,7 @@ "svelte": "^4.2.12", "ts-node": "^10.9.2", "type-fest": "^4.14.0", - "typescript": "^5.5.4" + "typescript": "^5.5.4", + "zod": "^3.23.8" } } diff --git a/src/rules/no-named-as-default.ts b/src/rules/no-named-as-default.ts index 7d6697a4e..673f44e86 100644 --- a/src/rules/no-named-as-default.ts +++ b/src/rules/no-named-as-default.ts @@ -21,44 +21,51 @@ export = createRule<[], MessageId>({ }, defaultOptions: [], create(context) { - function checkDefault( - nameKey: 'local' | 'exported', - defaultSpecifier: TSESTree.ImportDefaultSpecifier, - // | TSESTree.ExportDefaultSpecifier, - ) { - // #566: default is a valid specifier - // @ts-expect-error - ExportDefaultSpecifier is unavailable yet - const nameValue = defaultSpecifier[nameKey].name as string + function createCheckDefault(nameKey: 'local' | 'exported') { + return function checkDefault( + defaultSpecifier: TSESTree.ImportDefaultSpecifier, + // | TSESTree.ExportDefaultSpecifier, + ) { + // #566: default is a valid specifier + // @ts-expect-error - ExportDefaultSpecifier is unavailable yet + const nameValue = defaultSpecifier[nameKey].name as string - if (nameValue === 'default') { - return - } + if (nameValue === 'default') { + return + } - const declaration = importDeclaration(context, defaultSpecifier) + const declaration = importDeclaration(context, defaultSpecifier) - const imports = ExportMap.get(declaration.source.value, context) - if (imports == null) { - return - } + const exportMapOfImported = ExportMap.get( + declaration.source.value, + context, + ) + if (exportMapOfImported == null) { + return + } - if (imports.errors.length > 0) { - imports.reportErrors(context, declaration) - return - } + if (exportMapOfImported.errors.length > 0) { + exportMapOfImported.reportErrors(context, declaration) + return + } - if (imports.has('default') && imports.has(nameValue)) { - context.report({ - node: defaultSpecifier, - messageId: 'default', - data: { - name: nameValue, - }, - }) + if ( + exportMapOfImported.exports.has('default') && + exportMapOfImported.exports.has(nameValue) + ) { + context.report({ + node: defaultSpecifier, + messageId: 'default', + data: { + name: nameValue, + }, + }) + } } } return { - ImportDefaultSpecifier: checkDefault.bind(null, 'local'), - ExportDefaultSpecifier: checkDefault.bind(null, 'exported'), + ImportDefaultSpecifier: createCheckDefault('local'), + ExportDefaultSpecifier: createCheckDefault('exported'), } }, }) diff --git a/src/utils/export-map.ts b/src/utils/export-map.ts index 84e548f5e..1b3efb2e1 100644 --- a/src/utils/export-map.ts +++ b/src/utils/export-map.ts @@ -283,7 +283,7 @@ export class ExportMap { return } case 'ExportAllDeclaration': { - m.exports.set(s.exported!.name, n) + m.exports.set(getValue(s.exported!), n) m.namespace.set( getValue(s.exported!), addNamespace(exportMeta, s.exported!), @@ -293,7 +293,7 @@ export class ExportMap { } case 'ExportSpecifier': { if (!('source' in n && n.source)) { - m.exports.set(s.exported!.name, n) + m.exports.set(getValue(s.exported!), n) m.namespace.set( getValue(s.exported), addNamespace(exportMeta, s.local), diff --git a/test/package.spec.ts b/test/package.spec.ts index 33c63dc27..9488e4797 100644 --- a/test/package.spec.ts +++ b/test/package.spec.ts @@ -52,7 +52,6 @@ describe('package', () => { continue } for (const rule of Object.keys(config.rules)) { - console.log({ rule, preamble }) expect(() => require(getRulePath(rule.slice(preamble.length))), ).not.toThrow() diff --git a/test/rules/no-named-as-default.spec.ts b/test/rules/no-named-as-default.spec.ts index 7e70b266c..ec84fa1c1 100644 --- a/test/rules/no-named-as-default.spec.ts +++ b/test/rules/no-named-as-default.spec.ts @@ -6,17 +6,54 @@ import rule from 'eslint-plugin-import-x/rules/no-named-as-default' const ruleTester = new TSESLintRuleTester() -console.log({ babel: require(parsers.BABEL) }) - ruleTester.run('no-named-as-default', rule, { valid: [ + // https://github.com/un-ts/eslint-plugin-import-x/issues/123 + test({ + code: `/** TypeScript */ import klawSync from "klaw-sync";`, + settings: { + 'import-x/extensions': [ + '.ts', + '.cts', + '.mts', + '.tsx', + '.js', + '.cjs', + '.mjs', + '.jsx', + ], + 'import-x/external-module-folders': [ + 'node_modules', + 'node_modules/@types', + ], + 'import-x/parsers': { + '@typescript-eslint/parser': ['.ts', '.cts', '.mts', '.tsx'], + }, + 'import-x/resolver': { + typescript: true, + node: { + extensions: [ + '.ts', + '.cts', + '.mts', + '.tsx', + '.js', + '.cjs', + '.mjs', + '.jsx', + ], + }, + }, + }, + }), + test({ code: 'import "./malformed.js"', languageOptions: { parser: require(parsers.ESPREE) }, }), - test({ code: 'import bar, { foo } from "./bar";' }), - test({ code: 'import bar, { foo } from "./empty-folder";' }), + 'import bar, { foo } from "./bar";', + 'import bar, { foo } from "./empty-folder";', // es7 test({ @@ -133,5 +170,15 @@ ruleTester.run('no-named-as-default', rule, { parserOptions: { ecmaVersion: 2022 }, }, }), + + test({ + code: `import z from 'zod';`, + errors: [ + { + message: "Using exported name 'z' as identifier for default export.", + type: 'ImportDefaultSpecifier', + }, + ], + }), ], }) diff --git a/test/utils.ts b/test/utils.ts index 07c74a853..86e1ba514 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -109,29 +109,29 @@ export function testContext(settings?: PluginSettings) { * to crash at runtime */ export const SYNTAX_CASES = [ - test({ code: 'for (let { foo, bar } of baz) {}' }), - test({ code: 'for (let [ foo, bar ] of baz) {}' }), + 'for (let { foo, bar } of baz) {}', + 'for (let [ foo, bar ] of baz) {}', - test({ code: 'const { x, y } = bar' }), + 'const { x, y } = bar', test({ code: 'const { x, y, ...z } = bar', languageOptions: { parser: require(parsers.BABEL) }, }), // all the exports - test({ code: 'let x; export { x }' }), - test({ code: 'let x; export { x as y }' }), + 'let x; export { x }', + 'let x; export { x as y }', // not sure about these since they reference a file - // test({ code: 'export { x } from "./y.js"'}), - // test({ code: 'export * as y from "./y.js"', languageOptions: { parser: require(parsers.BABEL) } }), + // 'export { x } from "./y.js"'}), + // 'export * as y from "./y.js"', languageOptions: { parser: require(parsers.BABEL) } }), - test({ code: 'export const x = null' }), - test({ code: 'export var x = null' }), - test({ code: 'export let x = null' }), + 'export const x = null', + 'export var x = null', + 'export let x = null', - test({ code: 'export default x' }), - test({ code: 'export default class x {}' }), + 'export default x', + 'export default class x {}', // issue #267: parser opt-in extension list test({ diff --git a/yarn.lock b/yarn.lock index f9f51603b..3eb55d0ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2285,6 +2285,13 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== +"@types/klaw-sync@^6.0.5": + version "6.0.5" + resolved "https://registry.yarnpkg.com/@types/klaw-sync/-/klaw-sync-6.0.5.tgz#0a87fa0762673a1a1d2e7b08a51d5d65f0c842cd" + integrity sha512-xlavCRyu5ibDjsOc7PSgeUbwOBZdnJsND8gFUVfBilplbBIWhLZVjwtqbZq0327ny3jNt7oviEh5NS9a4LudeQ== + dependencies: + "@types/node" "*" + "@types/minimist@^1.2.0": version "1.2.5" resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.5.tgz#ec10755e871497bcd83efe927e43ec46e8c0747e" @@ -7121,3 +7128,8 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zod@^3.23.8: + version "3.23.8" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" + integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==