Skip to content

Commit 38aa4cb

Browse files
authored
feat: add ignoreUnusedTypeExports option to no-unused-modules (#116)
1 parent f2f6a48 commit 38aa4cb

File tree

4 files changed

+119
-13
lines changed

4 files changed

+119
-13
lines changed

.changeset/hot-fireants-provide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-import-x": minor
3+
---
4+
5+
Add `ignoreUnusedTypeExports` option to `no-unused-modules`

docs/rules/no-unused-modules.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ This rule takes the following option:
2929

3030
- **`missingExports`**: if `true`, files without any exports are reported (defaults to `false`)
3131
- **`unusedExports`**: if `true`, exports without any static usage within other modules are reported (defaults to `false`)
32+
- **`ignoreUnusedTypeExports`**: if `true`, TypeScript type exports without any static usage within other modules are reported (defaults to `false` and has no effect unless `unusedExports` is `true`)
3233
- `src`: an array with files/paths to be analyzed. It only applies to unused exports. Defaults to `process.cwd()`, if not provided
3334
- `ignoreExports`: an array with files/paths for which unused exports will not be reported (e.g module entry points in a published package)
3435

@@ -120,6 +121,16 @@ export function doAnything() {
120121
export default 5 // will not be reported
121122
```
122123

124+
### Unused exports with `ignoreUnusedTypeExports` set to `true`
125+
126+
The following will not be reported:
127+
128+
```ts
129+
export type Foo = {}; // will not be reported
130+
export interface Foo = {}; // will not be reported
131+
export enum Foo {}; // will not be reported
132+
```
133+
123134
#### Important Note
124135

125136
Exports from files listed as a main file (`main`, `browser`, or `bin` fields in `package.json`) will be ignored by default. This only applies if the `package.json` is not set to `private: true`

src/rules/no-unused-modules.ts

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,33 +37,36 @@ const { AST_NODE_TYPES } = TSESTree
3737

3838
function forEachDeclarationIdentifier(
3939
declaration: TSESTree.Node | null,
40-
cb: (name: string) => void,
40+
cb: (name: string, isTypeExport: boolean) => void,
4141
) {
4242
if (declaration) {
43-
if (
44-
declaration.type === AST_NODE_TYPES.FunctionDeclaration ||
45-
declaration.type === AST_NODE_TYPES.ClassDeclaration ||
43+
const isTypeDeclaration =
4644
declaration.type === AST_NODE_TYPES.TSInterfaceDeclaration ||
4745
declaration.type === AST_NODE_TYPES.TSTypeAliasDeclaration ||
4846
declaration.type === AST_NODE_TYPES.TSEnumDeclaration
47+
48+
if (
49+
declaration.type === AST_NODE_TYPES.FunctionDeclaration ||
50+
declaration.type === AST_NODE_TYPES.ClassDeclaration ||
51+
isTypeDeclaration
4952
) {
50-
cb(declaration.id!.name)
53+
cb(declaration.id!.name, isTypeDeclaration)
5154
} else if (declaration.type === AST_NODE_TYPES.VariableDeclaration) {
5255
for (const { id } of declaration.declarations) {
5356
if (id.type === AST_NODE_TYPES.ObjectPattern) {
5457
recursivePatternCapture(id, pattern => {
5558
if (pattern.type === AST_NODE_TYPES.Identifier) {
56-
cb(pattern.name)
59+
cb(pattern.name, false)
5760
}
5861
})
5962
} else if (id.type === AST_NODE_TYPES.ArrayPattern) {
6063
for (const el of id.elements) {
6164
if (el?.type === AST_NODE_TYPES.Identifier) {
62-
cb(el.name)
65+
cb(el.name, false)
6366
}
6467
}
6568
} else {
66-
cb(id.name)
69+
cb(id.name, false)
6770
}
6871
}
6972
}
@@ -397,6 +400,7 @@ type Options = {
397400
ignoreExports?: string[]
398401
missingExports?: string[]
399402
unusedExports?: boolean
403+
ignoreUnusedTypeExports?: boolean
400404
}
401405

402406
type MessageId = 'notFound' | 'unused'
@@ -441,6 +445,10 @@ export = createRule<Options[], MessageId>({
441445
description: 'report exports without any usage',
442446
type: 'boolean',
443447
},
448+
ignoreUnusedTypeExports: {
449+
description: 'ignore type exports without any usage',
450+
type: 'boolean',
451+
},
444452
},
445453
anyOf: [
446454
{
@@ -482,6 +490,7 @@ export = createRule<Options[], MessageId>({
482490
ignoreExports = [],
483491
missingExports,
484492
unusedExports,
493+
ignoreUnusedTypeExports,
485494
} = context.options[0] || {}
486495

487496
if (unusedExports) {
@@ -495,6 +504,10 @@ export = createRule<Options[], MessageId>({
495504
return
496505
}
497506

507+
if (ignoreUnusedTypeExports) {
508+
return
509+
}
510+
498511
if (ignoredFiles.has(filename)) {
499512
return
500513
}
@@ -519,11 +532,19 @@ export = createRule<Options[], MessageId>({
519532
exportCount.set(AST_NODE_TYPES.ImportNamespaceSpecifier, namespaceImports)
520533
}
521534

522-
const checkUsage = (node: TSESTree.Node, exportedValue: string) => {
535+
const checkUsage = (
536+
node: TSESTree.Node,
537+
exportedValue: string,
538+
isTypeExport: boolean,
539+
) => {
523540
if (!unusedExports) {
524541
return
525542
}
526543

544+
if (isTypeExport && ignoreUnusedTypeExports) {
545+
return
546+
}
547+
527548
if (ignoredFiles.has(filename)) {
528549
return
529550
}
@@ -991,14 +1012,14 @@ export = createRule<Options[], MessageId>({
9911012
checkExportPresence(node)
9921013
},
9931014
ExportDefaultDeclaration(node) {
994-
checkUsage(node, AST_NODE_TYPES.ImportDefaultSpecifier)
1015+
checkUsage(node, AST_NODE_TYPES.ImportDefaultSpecifier, false)
9951016
},
9961017
ExportNamedDeclaration(node) {
9971018
for (const specifier of node.specifiers) {
998-
checkUsage(specifier, getValue(specifier.exported))
1019+
checkUsage(specifier, getValue(specifier.exported), false)
9991020
}
1000-
forEachDeclarationIdentifier(node.declaration, name => {
1001-
checkUsage(node, name)
1021+
forEachDeclarationIdentifier(node.declaration, (name, isTypeExport) => {
1022+
checkUsage(node, name, isTypeExport)
10021023
})
10031024
},
10041025
}

test/rules/no-unused-modules.spec.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@ const unusedExportsTypescriptOptions = [
3838
},
3939
]
4040

41+
const unusedExportsTypescriptIgnoreUnusedTypesOptions = [
42+
{
43+
unusedExports: true,
44+
ignoreUnusedTypeExports: true,
45+
src: [testFilePath('./no-unused-modules/typescript')],
46+
ignoreExports: undefined,
47+
},
48+
]
49+
4150
const unusedExportsJsxOptions = [
4251
{
4352
unusedExports: true,
@@ -1332,6 +1341,66 @@ describe('TypeScript', () => {
13321341
})
13331342
})
13341343

1344+
describe('ignoreUnusedTypeExports', () => {
1345+
const parser = parsers.TS
1346+
1347+
typescriptRuleTester.run('no-unused-modules', rule, {
1348+
valid: [
1349+
// unused vars should not report
1350+
test({
1351+
options: unusedExportsTypescriptIgnoreUnusedTypesOptions,
1352+
code: `export interface c {};`,
1353+
parser,
1354+
filename: testFilePath(
1355+
'./no-unused-modules/typescript/file-ts-c-unused.ts',
1356+
),
1357+
}),
1358+
test({
1359+
options: unusedExportsTypescriptIgnoreUnusedTypesOptions,
1360+
code: `export type d = {};`,
1361+
parser,
1362+
filename: testFilePath(
1363+
'./no-unused-modules/typescript/file-ts-d-unused.ts',
1364+
),
1365+
}),
1366+
test({
1367+
options: unusedExportsTypescriptIgnoreUnusedTypesOptions,
1368+
code: `export enum e { f };`,
1369+
parser,
1370+
filename: testFilePath(
1371+
'./no-unused-modules/typescript/file-ts-e-unused.ts',
1372+
),
1373+
}),
1374+
// used vars should not report
1375+
test({
1376+
options: unusedExportsTypescriptIgnoreUnusedTypesOptions,
1377+
code: `export interface c {};`,
1378+
parser,
1379+
filename: testFilePath(
1380+
'./no-unused-modules/typescript/file-ts-c-used-as-type.ts',
1381+
),
1382+
}),
1383+
test({
1384+
options: unusedExportsTypescriptIgnoreUnusedTypesOptions,
1385+
code: `export type d = {};`,
1386+
parser,
1387+
filename: testFilePath(
1388+
'./no-unused-modules/typescript/file-ts-d-used-as-type.ts',
1389+
),
1390+
}),
1391+
test({
1392+
options: unusedExportsTypescriptIgnoreUnusedTypesOptions,
1393+
code: `export enum e { f };`,
1394+
parser,
1395+
filename: testFilePath(
1396+
'./no-unused-modules/typescript/file-ts-e-used-as-type.ts',
1397+
),
1398+
}),
1399+
],
1400+
invalid: [],
1401+
})
1402+
})
1403+
13351404
describe('correctly work with JSX only files', () => {
13361405
jsxRuleTester.run('no-unused-modules', rule, {
13371406
valid: [

0 commit comments

Comments
 (0)