Skip to content

Commit

Permalink
feat: add ignoreUnusedTypeExports option to no-unused-modules (#116)
Browse files Browse the repository at this point in the history
  • Loading branch information
silverwind committed Jul 19, 2024
1 parent f2f6a48 commit 38aa4cb
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 13 deletions.
5 changes: 5 additions & 0 deletions .changeset/hot-fireants-provide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eslint-plugin-import-x": minor
---

Add `ignoreUnusedTypeExports` option to `no-unused-modules`
11 changes: 11 additions & 0 deletions docs/rules/no-unused-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ This rule takes the following option:

- **`missingExports`**: if `true`, files without any exports are reported (defaults to `false`)
- **`unusedExports`**: if `true`, exports without any static usage within other modules are reported (defaults to `false`)
- **`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`)
- `src`: an array with files/paths to be analyzed. It only applies to unused exports. Defaults to `process.cwd()`, if not provided
- `ignoreExports`: an array with files/paths for which unused exports will not be reported (e.g module entry points in a published package)

Expand Down Expand Up @@ -120,6 +121,16 @@ export function doAnything() {
export default 5 // will not be reported
```

### Unused exports with `ignoreUnusedTypeExports` set to `true`

The following will not be reported:

```ts
export type Foo = {}; // will not be reported
export interface Foo = {}; // will not be reported
export enum Foo {}; // will not be reported
```

#### Important Note

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`
Expand Down
47 changes: 34 additions & 13 deletions src/rules/no-unused-modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,33 +37,36 @@ const { AST_NODE_TYPES } = TSESTree

function forEachDeclarationIdentifier(
declaration: TSESTree.Node | null,
cb: (name: string) => void,
cb: (name: string, isTypeExport: boolean) => void,
) {
if (declaration) {
if (
declaration.type === AST_NODE_TYPES.FunctionDeclaration ||
declaration.type === AST_NODE_TYPES.ClassDeclaration ||
const isTypeDeclaration =
declaration.type === AST_NODE_TYPES.TSInterfaceDeclaration ||
declaration.type === AST_NODE_TYPES.TSTypeAliasDeclaration ||
declaration.type === AST_NODE_TYPES.TSEnumDeclaration

if (
declaration.type === AST_NODE_TYPES.FunctionDeclaration ||
declaration.type === AST_NODE_TYPES.ClassDeclaration ||
isTypeDeclaration
) {
cb(declaration.id!.name)
cb(declaration.id!.name, isTypeDeclaration)
} else if (declaration.type === AST_NODE_TYPES.VariableDeclaration) {
for (const { id } of declaration.declarations) {
if (id.type === AST_NODE_TYPES.ObjectPattern) {
recursivePatternCapture(id, pattern => {
if (pattern.type === AST_NODE_TYPES.Identifier) {
cb(pattern.name)
cb(pattern.name, false)
}
})
} else if (id.type === AST_NODE_TYPES.ArrayPattern) {
for (const el of id.elements) {
if (el?.type === AST_NODE_TYPES.Identifier) {
cb(el.name)
cb(el.name, false)
}
}
} else {
cb(id.name)
cb(id.name, false)
}
}
}
Expand Down Expand Up @@ -397,6 +400,7 @@ type Options = {
ignoreExports?: string[]
missingExports?: string[]
unusedExports?: boolean
ignoreUnusedTypeExports?: boolean
}

type MessageId = 'notFound' | 'unused'
Expand Down Expand Up @@ -441,6 +445,10 @@ export = createRule<Options[], MessageId>({
description: 'report exports without any usage',
type: 'boolean',
},
ignoreUnusedTypeExports: {
description: 'ignore type exports without any usage',
type: 'boolean',
},
},
anyOf: [
{
Expand Down Expand Up @@ -482,6 +490,7 @@ export = createRule<Options[], MessageId>({
ignoreExports = [],
missingExports,
unusedExports,
ignoreUnusedTypeExports,
} = context.options[0] || {}

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

if (ignoreUnusedTypeExports) {
return
}

if (ignoredFiles.has(filename)) {
return
}
Expand All @@ -519,11 +532,19 @@ export = createRule<Options[], MessageId>({
exportCount.set(AST_NODE_TYPES.ImportNamespaceSpecifier, namespaceImports)
}

const checkUsage = (node: TSESTree.Node, exportedValue: string) => {
const checkUsage = (
node: TSESTree.Node,
exportedValue: string,
isTypeExport: boolean,
) => {
if (!unusedExports) {
return
}

if (isTypeExport && ignoreUnusedTypeExports) {
return
}

if (ignoredFiles.has(filename)) {
return
}
Expand Down Expand Up @@ -991,14 +1012,14 @@ export = createRule<Options[], MessageId>({
checkExportPresence(node)
},
ExportDefaultDeclaration(node) {
checkUsage(node, AST_NODE_TYPES.ImportDefaultSpecifier)
checkUsage(node, AST_NODE_TYPES.ImportDefaultSpecifier, false)
},
ExportNamedDeclaration(node) {
for (const specifier of node.specifiers) {
checkUsage(specifier, getValue(specifier.exported))
checkUsage(specifier, getValue(specifier.exported), false)
}
forEachDeclarationIdentifier(node.declaration, name => {
checkUsage(node, name)
forEachDeclarationIdentifier(node.declaration, (name, isTypeExport) => {
checkUsage(node, name, isTypeExport)
})
},
}
Expand Down
69 changes: 69 additions & 0 deletions test/rules/no-unused-modules.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ const unusedExportsTypescriptOptions = [
},
]

const unusedExportsTypescriptIgnoreUnusedTypesOptions = [
{
unusedExports: true,
ignoreUnusedTypeExports: true,
src: [testFilePath('./no-unused-modules/typescript')],
ignoreExports: undefined,
},
]

const unusedExportsJsxOptions = [
{
unusedExports: true,
Expand Down Expand Up @@ -1332,6 +1341,66 @@ describe('TypeScript', () => {
})
})

describe('ignoreUnusedTypeExports', () => {
const parser = parsers.TS

typescriptRuleTester.run('no-unused-modules', rule, {
valid: [
// unused vars should not report
test({
options: unusedExportsTypescriptIgnoreUnusedTypesOptions,
code: `export interface c {};`,
parser,
filename: testFilePath(
'./no-unused-modules/typescript/file-ts-c-unused.ts',
),
}),
test({
options: unusedExportsTypescriptIgnoreUnusedTypesOptions,
code: `export type d = {};`,
parser,
filename: testFilePath(
'./no-unused-modules/typescript/file-ts-d-unused.ts',
),
}),
test({
options: unusedExportsTypescriptIgnoreUnusedTypesOptions,
code: `export enum e { f };`,
parser,
filename: testFilePath(
'./no-unused-modules/typescript/file-ts-e-unused.ts',
),
}),
// used vars should not report
test({
options: unusedExportsTypescriptIgnoreUnusedTypesOptions,
code: `export interface c {};`,
parser,
filename: testFilePath(
'./no-unused-modules/typescript/file-ts-c-used-as-type.ts',
),
}),
test({
options: unusedExportsTypescriptIgnoreUnusedTypesOptions,
code: `export type d = {};`,
parser,
filename: testFilePath(
'./no-unused-modules/typescript/file-ts-d-used-as-type.ts',
),
}),
test({
options: unusedExportsTypescriptIgnoreUnusedTypesOptions,
code: `export enum e { f };`,
parser,
filename: testFilePath(
'./no-unused-modules/typescript/file-ts-e-used-as-type.ts',
),
}),
],
invalid: [],
})
})

describe('correctly work with JSX only files', () => {
jsxRuleTester.run('no-unused-modules', rule, {
valid: [
Expand Down

0 comments on commit 38aa4cb

Please sign in to comment.