Skip to content

Commit f8c8a9d

Browse files
authored
fix(no-wildcard-imports): mix-and-match import types (#249)
* fix(no-wildcard-imports): mix-and-match import types * chore: add changeset * refactor: update logic to reuse imports * fix: add support for default * fix: reuse existing type import declarations if they exist * chore: fix eslint warnings * fix: lookup for specifiers * fix: update overlap when removing and replacing node
1 parent 2c5ea48 commit f8c8a9d

File tree

3 files changed

+152
-58
lines changed

3 files changed

+152
-58
lines changed

.changeset/grumpy-masks-lie.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'eslint-plugin-primer-react': patch
3+
---
4+
5+
Update no-wildcard-imports rule to not create separate imports for type only imports. This prevents an issue downstream with autofixers

src/rules/__tests__/no-wildcard-imports.test.js

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ ruleTester.run('no-wildcard-imports', rule, {
3030
// Test type import
3131
{
3232
code: `import type {SxProp} from '@primer/react/lib-esm/sx'`,
33-
output: `import type {SxProp} from '@primer/react'`,
33+
output: `import {type SxProp} from '@primer/react'`,
3434
errors: [
3535
{
3636
messageId: 'wildcardMigration',
@@ -44,7 +44,7 @@ ruleTester.run('no-wildcard-imports', rule, {
4444
// Test multiple type imports
4545
{
4646
code: `import type {BetterSystemStyleObject, SxProp, BetterCssProperties} from '@primer/react/lib-esm/sx'`,
47-
output: `import type {BetterSystemStyleObject, SxProp, BetterCssProperties} from '@primer/react'`,
47+
output: `import {type BetterSystemStyleObject, type SxProp, type BetterCssProperties} from '@primer/react'`,
4848
errors: [
4949
{
5050
messageId: 'wildcardMigration',
@@ -58,7 +58,7 @@ ruleTester.run('no-wildcard-imports', rule, {
5858
// Test import alias
5959
{
6060
code: `import type {SxProp as RenamedSxProp} from '@primer/react/lib-esm/sx'`,
61-
output: `import type {SxProp as RenamedSxProp} from '@primer/react'`,
61+
output: `import {type SxProp as RenamedSxProp} from '@primer/react'`,
6262
errors: [
6363
{
6464
messageId: 'wildcardMigration',
@@ -108,7 +108,7 @@ ruleTester.run('no-wildcard-imports', rule, {
108108
// Test renamed wildcard imports
109109
{
110110
code: `import type {ItemProps} from '@primer/react/lib-esm/deprecated/ActionList/Item'`,
111-
output: `import type {ActionListItemProps as ItemProps} from '@primer/react/deprecated'`,
111+
output: `import {type ActionListItemProps as ItemProps} from '@primer/react/deprecated'`,
112112
errors: [
113113
{
114114
messageId: 'wildcardMigration',
@@ -122,8 +122,7 @@ ruleTester.run('no-wildcard-imports', rule, {
122122
// Test mixed imports
123123
{
124124
code: `import {Dialog, type DialogProps} from '@primer/react/lib-esm/Dialog/Dialog'`,
125-
output: `import {Dialog} from '@primer/react/experimental'
126-
import type {DialogProps} from '@primer/react/experimental'`,
125+
output: `import {Dialog, type DialogProps} from '@primer/react/experimental'`,
127126
errors: [
128127
{
129128
messageId: 'wildcardMigration',
@@ -134,6 +133,22 @@ import type {DialogProps} from '@primer/react/experimental'`,
134133
],
135134
},
136135

136+
// Use existing imports
137+
{
138+
code: `import {Box, type BoxProps} from '@primer/react'
139+
import type {BetterSystemStyleObject} from '@primer/react/lib-esm/sx'`,
140+
output: `import {Box, type BoxProps, type BetterSystemStyleObject} from '@primer/react'
141+
`,
142+
errors: [
143+
{
144+
messageId: 'wildcardMigration',
145+
data: {
146+
wildcardEntrypoint: '@primer/react/lib-esm/sx',
147+
},
148+
},
149+
],
150+
},
151+
137152
// Test migrations
138153

139154
// Test helpers ------------------------------------------------------------
@@ -155,7 +170,7 @@ import type {DialogProps} from '@primer/react/experimental'`,
155170
code: `import {ButtonBase} from '@primer/react/lib-esm/Button/ButtonBase';
156171
import type {ButtonBaseProps} from '@primer/react/lib-esm/Button/ButtonBase'`,
157172
output: `import {ButtonBase} from '@primer/react'
158-
import type {ButtonBaseProps} from '@primer/react'`,
173+
import {type ButtonBaseProps} from '@primer/react'`,
159174
errors: [
160175
{
161176
messageId: 'wildcardMigration',
@@ -173,7 +188,7 @@ import type {ButtonBaseProps} from '@primer/react'`,
173188
},
174189
{
175190
code: `import type {ButtonBaseProps} from '@primer/react/lib-esm/Button/types'`,
176-
output: `import type {ButtonBaseProps} from '@primer/react'`,
191+
output: `import {type ButtonBaseProps} from '@primer/react'`,
177192
errors: [
178193
{
179194
messageId: 'wildcardMigration',
@@ -209,7 +224,7 @@ import type {ButtonBaseProps} from '@primer/react'`,
209224
},
210225
{
211226
code: `import type {SelectPanelProps} from '@primer/react/lib-esm/SelectPanel/SelectPanel'`,
212-
output: `import type {SelectPanelProps} from '@primer/react'`,
227+
output: `import {type SelectPanelProps} from '@primer/react'`,
213228
errors: [
214229
{
215230
messageId: 'wildcardMigration',
@@ -221,7 +236,7 @@ import type {ButtonBaseProps} from '@primer/react'`,
221236
},
222237
{
223238
code: `import type {LabelColorOptions} from '@primer/react/lib-esm/Label/Label'`,
224-
output: `import type {LabelColorOptions} from '@primer/react'`,
239+
output: `import {type LabelColorOptions} from '@primer/react'`,
225240
errors: [
226241
{
227242
messageId: 'wildcardMigration',
@@ -245,7 +260,7 @@ import type {ButtonBaseProps} from '@primer/react'`,
245260
},
246261
{
247262
code: `import type {IssueLabelTokenProps} from '@primer/react/lib-esm/Token/IssueLabelToken'`,
248-
output: `import type {IssueLabelTokenProps} from '@primer/react'`,
263+
output: `import {type IssueLabelTokenProps} from '@primer/react'`,
249264
errors: [
250265
{
251266
messageId: 'wildcardMigration',
@@ -257,7 +272,7 @@ import type {ButtonBaseProps} from '@primer/react'`,
257272
},
258273
{
259274
code: `import type {TokenSizeKeys} from '@primer/react/lib-esm/Token/TokenBase'`,
260-
output: `import type {TokenSizeKeys} from '@primer/react'`,
275+
output: `import {type TokenSizeKeys} from '@primer/react'`,
261276
errors: [
262277
{
263278
messageId: 'wildcardMigration',
@@ -269,7 +284,7 @@ import type {ButtonBaseProps} from '@primer/react'`,
269284
},
270285
{
271286
code: `import type {ItemProps} from '@primer/react/lib-esm/deprecated/ActionList'`,
272-
output: `import type {ActionListItemProps as ItemProps} from '@primer/react/deprecated'`,
287+
output: `import {type ActionListItemProps as ItemProps} from '@primer/react/deprecated'`,
273288
errors: [
274289
{
275290
messageId: 'wildcardMigration',
@@ -281,7 +296,7 @@ import type {ButtonBaseProps} from '@primer/react'`,
281296
},
282297
{
283298
code: `import type {GroupedListProps} from '@primer/react/lib-esm/deprecated/ActionList/List'`,
284-
output: `import type {ActionListGroupedListProps as GroupedListProps} from '@primer/react/deprecated'`,
299+
output: `import {type ActionListGroupedListProps as GroupedListProps} from '@primer/react/deprecated'`,
285300
errors: [
286301
{
287302
messageId: 'wildcardMigration',
@@ -305,7 +320,7 @@ import type {ButtonBaseProps} from '@primer/react'`,
305320
},
306321
{
307322
code: `import type {ItemProps} from '@primer/react/lib-esm/deprecated/ActionList/Item'`,
308-
output: `import type {ActionListItemProps as ItemProps} from '@primer/react/deprecated'`,
323+
output: `import {type ActionListItemProps as ItemProps} from '@primer/react/deprecated'`,
309324
errors: [
310325
{
311326
messageId: 'wildcardMigration',
@@ -375,7 +390,7 @@ import type {ButtonBaseProps} from '@primer/react'`,
375390
},
376391
{
377392
code: `import type {ResponsiveValue} from '@primer/react/lib-esm/hooks/useResponsiveValue'`,
378-
output: `import type {ResponsiveValue} from '@primer/react'`,
393+
output: `import {type ResponsiveValue} from '@primer/react'`,
379394
errors: [
380395
{
381396
messageId: 'wildcardMigration',
@@ -391,7 +406,7 @@ import type {ButtonBaseProps} from '@primer/react'`,
391406
// @primer/react/lib-esm/sx
392407
{
393408
code: `import type {BetterSystemStyleObject, SxProp, BetterCssProperties} from '@primer/react/lib-esm/sx'`,
394-
output: `import type {BetterSystemStyleObject, SxProp, BetterCssProperties} from '@primer/react'`,
409+
output: `import {type BetterSystemStyleObject, type SxProp, type BetterCssProperties} from '@primer/react'`,
395410
errors: [
396411
{
397412
messageId: 'wildcardMigration',

src/rules/no-wildcard-imports.js

Lines changed: 115 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,20 @@ module.exports = {
265265
create(context) {
266266
return {
267267
ImportDeclaration(node) {
268+
if (node.source.value === '@primer/react/lib-esm/utils/test-helpers') {
269+
context.report({
270+
node,
271+
messageId: 'wildcardMigration',
272+
data: {
273+
wildcardEntrypoint: node.source.value,
274+
},
275+
fix(fixer) {
276+
return fixer.replaceText(node.source, `'@primer/react/test-helpers'`)
277+
},
278+
})
279+
return
280+
}
281+
268282
if (!node.source.value.startsWith('@primer/react/lib-esm')) {
269283
return
270284
}
@@ -340,64 +354,124 @@ module.exports = {
340354
},
341355
*fix(fixer) {
342356
for (const [entrypoint, importSpecifiers] of changes) {
343-
const typeSpecifiers = importSpecifiers.filter(([, , type]) => {
344-
return type === 'type'
357+
const importDeclaration = node.parent.body.find(node => {
358+
return (
359+
node.type === 'ImportDeclaration' && node.source.value === entrypoint && node.importKind !== 'type'
360+
)
361+
})
362+
const typeImportDeclaration = node.parent.body.find(node => {
363+
return (
364+
node.type === 'ImportDeclaration' && node.source.value === entrypoint && node.importKind === 'type'
365+
)
366+
})
367+
let originalImportReplaced = false
368+
const namedSpecifiers = importSpecifiers.filter(([imported, , type]) => {
369+
return imported !== 'default' && type !== 'type'
370+
})
371+
const namedTypeSpecifiers = importSpecifiers.filter(([imported, , type]) => {
372+
return imported !== 'default' && type === 'type'
373+
})
374+
let defaultSpecifier = importSpecifiers.find(([imported, , type]) => {
375+
return imported === 'default' && type !== 'type'
376+
})
377+
if (defaultSpecifier) {
378+
defaultSpecifier = defaultSpecifier[1]
379+
}
380+
let defaultTypeSpecifier = importSpecifiers.find(([imported, , type]) => {
381+
return imported === 'default' && type === 'type'
345382
})
383+
if (defaultTypeSpecifier) {
384+
defaultTypeSpecifier = `type ${defaultTypeSpecifier[1]}`
385+
}
346386

347-
// If all imports are type imports, emit emit as `import type {specifier} from '...'`
348-
if (typeSpecifiers.length === importSpecifiers.length) {
349-
const namedSpecifiers = typeSpecifiers.filter(([imported]) => {
350-
return imported !== 'default'
351-
})
352-
const defaultSpecifier = typeSpecifiers.find(([imported]) => {
353-
return imported === 'default'
354-
})
387+
// Reuse a type import if it exists
388+
if (typeImportDeclaration) {
389+
const firstSpecifier = typeImportDeclaration.specifiers[0]
390+
const lastSpecifier = typeImportDeclaration.specifiers[typeImportDeclaration.specifiers.length - 1]
355391

356-
if (namedSpecifiers.length > 0 && !defaultSpecifier) {
357-
const specifiers = namedSpecifiers.map(([imported, local]) => {
392+
if (defaultTypeSpecifier) {
393+
const postfix =
394+
namedTypeSpecifiers.length > 0 || typeImportDeclaration.specifiers.length > 0 ? ', ' : ' '
395+
yield fixer.insertTextBeforeRange(
396+
[firstSpecifier.range[0] - 2, firstSpecifier.range[1]],
397+
`${defaultTypeSpecifier}${postfix}`,
398+
)
399+
}
400+
401+
if (namedTypeSpecifiers.length > 0) {
402+
const specifiers = namedTypeSpecifiers.map(([imported, local]) => {
358403
if (imported !== local) {
359404
return `${imported} as ${local}`
360405
}
361406
return imported
362407
})
363-
yield fixer.replaceText(node, `import type {${specifiers.join(', ')}} from '${entrypoint}'`)
364-
} else if (namedSpecifiers.length > 0 && defaultSpecifier) {
365-
yield fixer.replaceText(
366-
node,
367-
`import type ${defaultSpecifier[1]}, ${specifiers.join(', ')} from '${entrypoint}'`,
368-
)
369-
} else if (defaultSpecifier && namedSpecifiers.length === 0) {
370-
yield fixer.replaceText(node, `import type ${defaultSpecifier[1]} from '${entrypoint}'`)
408+
yield fixer.insertTextAfter(lastSpecifier, `, ${specifiers.join(', ')}`)
371409
}
372-
373-
return
374410
}
375411

376-
// Otherwise, we have a mix of type and value imports to emit
377-
const valueSpecifiers = importSpecifiers.filter(([, , type]) => {
378-
return type !== 'type'
379-
})
380-
381-
if (valueSpecifiers.length === 0) {
382-
return
383-
}
412+
// Reuse an import declaration if one exists
413+
if (importDeclaration) {
414+
const firstSpecifier = importDeclaration.specifiers[0]
415+
const lastSpecifier = importDeclaration.specifiers[importDeclaration.specifiers.length - 1]
384416

385-
const specifiers = valueSpecifiers.map(([imported, local]) => {
386-
if (imported !== local) {
387-
return `${imported} as ${local}`
417+
if (defaultSpecifier) {
418+
const postfix = namedSpecifiers.length > 0 || importDeclaration.specifiers.length > 0 ? ', ' : ' '
419+
yield fixer.insertTextBeforeRange(
420+
[firstSpecifier.range[0] - 2, firstSpecifier.range[1]],
421+
`${defaultSpecifier}${postfix}`,
422+
)
388423
}
389-
return imported
390-
})
391-
yield fixer.replaceText(node, `import {${specifiers.join(', ')}} from '${entrypoint}'`)
392424

393-
if (typeSpecifiers.length > 0) {
394-
const specifiers = typeSpecifiers.map(([imported, local]) => {
425+
if (namedSpecifiers.length > 0 || (!typeImportDeclaration && namedTypeSpecifiers.length > 0)) {
426+
let specifiers = [...namedSpecifiers]
427+
if (!typeImportDeclaration) {
428+
specifiers.push(...namedTypeSpecifiers)
429+
}
430+
specifiers = specifiers.map(([imported, local, type]) => {
431+
const prefix = type === 'type' ? 'type ' : ''
432+
if (imported !== local) {
433+
return `${prefix}${imported} as ${local}`
434+
}
435+
return `${prefix}${imported}`
436+
})
437+
yield fixer.insertTextAfter(lastSpecifier, `, ${specifiers.join(', ')}`)
438+
}
439+
} else {
440+
let specifiers = [...namedSpecifiers]
441+
if (!typeImportDeclaration) {
442+
specifiers.push(...namedTypeSpecifiers)
443+
}
444+
specifiers = specifiers.map(([imported, local, type]) => {
445+
const prefix = type === 'type' ? 'type ' : ''
395446
if (imported !== local) {
396-
return `${imported} as ${local}`
447+
return `${prefix}${imported} as ${local}`
397448
}
398-
return imported
449+
return `${prefix}${imported}`
399450
})
400-
yield fixer.insertTextAfter(node, `\nimport type {${specifiers.join(', ')}} from '${entrypoint}'`)
451+
let declaration = 'import '
452+
453+
if (defaultSpecifier) {
454+
declaration += defaultSpecifier
455+
}
456+
457+
if (defaultTypeSpecifier && !typeImportDeclaration) {
458+
declaration += defaultTypeSpecifier
459+
}
460+
461+
if (specifiers.length > 0) {
462+
if (defaultSpecifier || defaultTypeSpecifier) {
463+
declaration += ', '
464+
}
465+
declaration += `{${specifiers.join(', ')}}`
466+
}
467+
468+
declaration += ` from '${entrypoint}'`
469+
yield fixer.replaceText(node, declaration)
470+
originalImportReplaced = true
471+
}
472+
473+
if (!originalImportReplaced) {
474+
yield fixer.remove(node)
401475
}
402476
}
403477
},

0 commit comments

Comments
 (0)