Skip to content

Commit 757ffa9

Browse files
authored
fix(#123): make no-named-as-default ignores TS namespace (#133)
1 parent ceb8e65 commit 757ffa9

8 files changed

+123
-50
lines changed

.changeset/hungry-paws-whisper.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-import-x": patch
3+
---
4+
5+
Fix #123 where the rule `no-named-as-default` will confuse TypeScript namespace exports with actual exports.

package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
"@types/is-glob": "^4.0.4",
9090
"@types/jest": "^29.5.12",
9191
"@types/json-schema": "^7.0.15",
92+
"@types/klaw-sync": "^6.0.5",
9293
"@types/node": "^20.11.30",
9394
"@typescript-eslint/eslint-plugin": "^8.1.0",
9495
"@typescript-eslint/parser": "^8.1.0",
@@ -114,13 +115,15 @@
114115
"eslint9": "npm:eslint@^9.8.0",
115116
"hermes-eslint": "^0.23.1",
116117
"jest": "^29.7.0",
118+
"klaw-sync": "^6.0.0",
117119
"npm-run-all2": "^6.1.2",
118120
"prettier": "^3.2.5",
119121
"redux": "^5.0.1",
120122
"rimraf": "^5.0.10",
121123
"svelte": "^4.2.12",
122124
"ts-node": "^10.9.2",
123125
"type-fest": "^4.14.0",
124-
"typescript": "^5.5.4"
126+
"typescript": "^5.5.4",
127+
"zod": "^3.23.8"
125128
}
126129
}

src/rules/no-named-as-default.ts

+37-30
Original file line numberDiff line numberDiff line change
@@ -21,44 +21,51 @@ export = createRule<[], MessageId>({
2121
},
2222
defaultOptions: [],
2323
create(context) {
24-
function checkDefault(
25-
nameKey: 'local' | 'exported',
26-
defaultSpecifier: TSESTree.ImportDefaultSpecifier,
27-
// | TSESTree.ExportDefaultSpecifier,
28-
) {
29-
// #566: default is a valid specifier
30-
// @ts-expect-error - ExportDefaultSpecifier is unavailable yet
31-
const nameValue = defaultSpecifier[nameKey].name as string
24+
function createCheckDefault(nameKey: 'local' | 'exported') {
25+
return function checkDefault(
26+
defaultSpecifier: TSESTree.ImportDefaultSpecifier,
27+
// | TSESTree.ExportDefaultSpecifier,
28+
) {
29+
// #566: default is a valid specifier
30+
// @ts-expect-error - ExportDefaultSpecifier is unavailable yet
31+
const nameValue = defaultSpecifier[nameKey].name as string
3232

33-
if (nameValue === 'default') {
34-
return
35-
}
33+
if (nameValue === 'default') {
34+
return
35+
}
3636

37-
const declaration = importDeclaration(context, defaultSpecifier)
37+
const declaration = importDeclaration(context, defaultSpecifier)
3838

39-
const imports = ExportMap.get(declaration.source.value, context)
40-
if (imports == null) {
41-
return
42-
}
39+
const exportMapOfImported = ExportMap.get(
40+
declaration.source.value,
41+
context,
42+
)
43+
if (exportMapOfImported == null) {
44+
return
45+
}
4346

44-
if (imports.errors.length > 0) {
45-
imports.reportErrors(context, declaration)
46-
return
47-
}
47+
if (exportMapOfImported.errors.length > 0) {
48+
exportMapOfImported.reportErrors(context, declaration)
49+
return
50+
}
4851

49-
if (imports.has('default') && imports.has(nameValue)) {
50-
context.report({
51-
node: defaultSpecifier,
52-
messageId: 'default',
53-
data: {
54-
name: nameValue,
55-
},
56-
})
52+
if (
53+
exportMapOfImported.exports.has('default') &&
54+
exportMapOfImported.exports.has(nameValue)
55+
) {
56+
context.report({
57+
node: defaultSpecifier,
58+
messageId: 'default',
59+
data: {
60+
name: nameValue,
61+
},
62+
})
63+
}
5764
}
5865
}
5966
return {
60-
ImportDefaultSpecifier: checkDefault.bind(null, 'local'),
61-
ExportDefaultSpecifier: checkDefault.bind(null, 'exported'),
67+
ImportDefaultSpecifier: createCheckDefault('local'),
68+
ExportDefaultSpecifier: createCheckDefault('exported'),
6269
}
6370
},
6471
})

src/utils/export-map.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ export class ExportMap {
283283
return
284284
}
285285
case 'ExportAllDeclaration': {
286-
m.exports.set(s.exported!.name, n)
286+
m.exports.set(getValue(s.exported!), n)
287287
m.namespace.set(
288288
getValue(s.exported!),
289289
addNamespace(exportMeta, s.exported!),
@@ -293,7 +293,7 @@ export class ExportMap {
293293
}
294294
case 'ExportSpecifier': {
295295
if (!('source' in n && n.source)) {
296-
m.exports.set(s.exported!.name, n)
296+
m.exports.set(getValue(s.exported!), n)
297297
m.namespace.set(
298298
getValue(s.exported),
299299
addNamespace(exportMeta, s.local),

test/package.spec.ts

-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ describe('package', () => {
5252
continue
5353
}
5454
for (const rule of Object.keys(config.rules)) {
55-
console.log({ rule, preamble })
5655
expect(() =>
5756
require(getRulePath(rule.slice(preamble.length))),
5857
).not.toThrow()

test/rules/no-named-as-default.spec.ts

+51-4
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,54 @@ import rule from 'eslint-plugin-import-x/rules/no-named-as-default'
66

77
const ruleTester = new TSESLintRuleTester()
88

9-
console.log({ babel: require(parsers.BABEL) })
10-
119
ruleTester.run('no-named-as-default', rule, {
1210
valid: [
11+
// https://github.com/un-ts/eslint-plugin-import-x/issues/123
12+
test({
13+
code: `/** TypeScript */ import klawSync from "klaw-sync";`,
14+
settings: {
15+
'import-x/extensions': [
16+
'.ts',
17+
'.cts',
18+
'.mts',
19+
'.tsx',
20+
'.js',
21+
'.cjs',
22+
'.mjs',
23+
'.jsx',
24+
],
25+
'import-x/external-module-folders': [
26+
'node_modules',
27+
'node_modules/@types',
28+
],
29+
'import-x/parsers': {
30+
'@typescript-eslint/parser': ['.ts', '.cts', '.mts', '.tsx'],
31+
},
32+
'import-x/resolver': {
33+
typescript: true,
34+
node: {
35+
extensions: [
36+
'.ts',
37+
'.cts',
38+
'.mts',
39+
'.tsx',
40+
'.js',
41+
'.cjs',
42+
'.mjs',
43+
'.jsx',
44+
],
45+
},
46+
},
47+
},
48+
}),
49+
1350
test({
1451
code: 'import "./malformed.js"',
1552
languageOptions: { parser: require(parsers.ESPREE) },
1653
}),
1754

18-
test({ code: 'import bar, { foo } from "./bar";' }),
19-
test({ code: 'import bar, { foo } from "./empty-folder";' }),
55+
'import bar, { foo } from "./bar";',
56+
'import bar, { foo } from "./empty-folder";',
2057

2158
// es7
2259
test({
@@ -133,5 +170,15 @@ ruleTester.run('no-named-as-default', rule, {
133170
parserOptions: { ecmaVersion: 2022 },
134171
},
135172
}),
173+
174+
test({
175+
code: `import z from 'zod';`,
176+
errors: [
177+
{
178+
message: "Using exported name 'z' as identifier for default export.",
179+
type: 'ImportDefaultSpecifier',
180+
},
181+
],
182+
}),
136183
],
137184
})

test/utils.ts

+12-12
Original file line numberDiff line numberDiff line change
@@ -109,29 +109,29 @@ export function testContext(settings?: PluginSettings) {
109109
* to crash at runtime
110110
*/
111111
export const SYNTAX_CASES = [
112-
test({ code: 'for (let { foo, bar } of baz) {}' }),
113-
test({ code: 'for (let [ foo, bar ] of baz) {}' }),
112+
'for (let { foo, bar } of baz) {}',
113+
'for (let [ foo, bar ] of baz) {}',
114114

115-
test({ code: 'const { x, y } = bar' }),
115+
'const { x, y } = bar',
116116
test({
117117
code: 'const { x, y, ...z } = bar',
118118
languageOptions: { parser: require(parsers.BABEL) },
119119
}),
120120

121121
// all the exports
122-
test({ code: 'let x; export { x }' }),
123-
test({ code: 'let x; export { x as y }' }),
122+
'let x; export { x }',
123+
'let x; export { x as y }',
124124

125125
// not sure about these since they reference a file
126-
// test({ code: 'export { x } from "./y.js"'}),
127-
// test({ code: 'export * as y from "./y.js"', languageOptions: { parser: require(parsers.BABEL) } }),
126+
// 'export { x } from "./y.js"'}),
127+
// 'export * as y from "./y.js"', languageOptions: { parser: require(parsers.BABEL) } }),
128128

129-
test({ code: 'export const x = null' }),
130-
test({ code: 'export var x = null' }),
131-
test({ code: 'export let x = null' }),
129+
'export const x = null',
130+
'export var x = null',
131+
'export let x = null',
132132

133-
test({ code: 'export default x' }),
134-
test({ code: 'export default class x {}' }),
133+
'export default x',
134+
'export default class x {}',
135135

136136
// issue #267: parser opt-in extension list
137137
test({

yarn.lock

+12
Original file line numberDiff line numberDiff line change
@@ -2285,6 +2285,13 @@
22852285
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
22862286
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
22872287

2288+
"@types/klaw-sync@^6.0.5":
2289+
version "6.0.5"
2290+
resolved "https://registry.yarnpkg.com/@types/klaw-sync/-/klaw-sync-6.0.5.tgz#0a87fa0762673a1a1d2e7b08a51d5d65f0c842cd"
2291+
integrity sha512-xlavCRyu5ibDjsOc7PSgeUbwOBZdnJsND8gFUVfBilplbBIWhLZVjwtqbZq0327ny3jNt7oviEh5NS9a4LudeQ==
2292+
dependencies:
2293+
"@types/node" "*"
2294+
22882295
"@types/minimist@^1.2.0":
22892296
version "1.2.5"
22902297
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.5.tgz#ec10755e871497bcd83efe927e43ec46e8c0747e"
@@ -7121,3 +7128,8 @@ yocto-queue@^0.1.0:
71217128
version "0.1.0"
71227129
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
71237130
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
7131+
7132+
zod@^3.23.8:
7133+
version "3.23.8"
7134+
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
7135+
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==

0 commit comments

Comments
 (0)