From 38aa4cb8433f24e5772fe1d224265acaedf67343 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 19 Jul 2024 05:05:00 +0200 Subject: [PATCH] feat: add `ignoreUnusedTypeExports` option to `no-unused-modules` (#116) --- .changeset/hot-fireants-provide.md | 5 ++ docs/rules/no-unused-modules.md | 11 +++++ src/rules/no-unused-modules.ts | 47 +++++++++++++------ test/rules/no-unused-modules.spec.ts | 69 ++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 13 deletions(-) create mode 100644 .changeset/hot-fireants-provide.md diff --git a/.changeset/hot-fireants-provide.md b/.changeset/hot-fireants-provide.md new file mode 100644 index 000000000..db6070e0f --- /dev/null +++ b/.changeset/hot-fireants-provide.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-import-x": minor +--- + +Add `ignoreUnusedTypeExports` option to `no-unused-modules` diff --git a/docs/rules/no-unused-modules.md b/docs/rules/no-unused-modules.md index b11becdbf..1f0f34a91 100644 --- a/docs/rules/no-unused-modules.md +++ b/docs/rules/no-unused-modules.md @@ -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) @@ -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` diff --git a/src/rules/no-unused-modules.ts b/src/rules/no-unused-modules.ts index 20fa269b1..7c7597274 100644 --- a/src/rules/no-unused-modules.ts +++ b/src/rules/no-unused-modules.ts @@ -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) } } } @@ -397,6 +400,7 @@ type Options = { ignoreExports?: string[] missingExports?: string[] unusedExports?: boolean + ignoreUnusedTypeExports?: boolean } type MessageId = 'notFound' | 'unused' @@ -441,6 +445,10 @@ export = createRule({ description: 'report exports without any usage', type: 'boolean', }, + ignoreUnusedTypeExports: { + description: 'ignore type exports without any usage', + type: 'boolean', + }, }, anyOf: [ { @@ -482,6 +490,7 @@ export = createRule({ ignoreExports = [], missingExports, unusedExports, + ignoreUnusedTypeExports, } = context.options[0] || {} if (unusedExports) { @@ -495,6 +504,10 @@ export = createRule({ return } + if (ignoreUnusedTypeExports) { + return + } + if (ignoredFiles.has(filename)) { return } @@ -519,11 +532,19 @@ export = createRule({ 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 } @@ -991,14 +1012,14 @@ export = createRule({ 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) }) }, } diff --git a/test/rules/no-unused-modules.spec.ts b/test/rules/no-unused-modules.spec.ts index 6435dfdcc..ea4de8994 100644 --- a/test/rules/no-unused-modules.spec.ts +++ b/test/rules/no-unused-modules.spec.ts @@ -38,6 +38,15 @@ const unusedExportsTypescriptOptions = [ }, ] +const unusedExportsTypescriptIgnoreUnusedTypesOptions = [ + { + unusedExports: true, + ignoreUnusedTypeExports: true, + src: [testFilePath('./no-unused-modules/typescript')], + ignoreExports: undefined, + }, +] + const unusedExportsJsxOptions = [ { unusedExports: true, @@ -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: [