diff --git a/CHANGELOG.md b/CHANGELOG.md index 941cd6ef8..b1d2a3425 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ### Added - [`dynamic-import-chunkname`]: add `allowEmpty` option to allow empty leading comments ([#2942], thanks [@JiangWeixian]) - [`dynamic-import-chunkname`]: Allow empty chunk name when webpackMode: 'eager' is set; add suggestions to remove name in eager mode ([#3004], thanks [@amsardesai]) +- [`no-unused-modules`]: Add `ignoreUnusedTypeExports` option ([#3011], thanks [@silverwind]) ### Fixed - [`no-extraneous-dependencies`]: allow wrong path ([#3012], thanks [@chabb]) @@ -1120,6 +1121,7 @@ for info on changes for earlier releases. [`memo-parser`]: ./memo-parser/README.md [#3012]: https://github.com/import-js/eslint-plugin-import/pull/3012 +[#3011]: https://github.com/import-js/eslint-plugin-import/pull/3011 [#3004]: https://github.com/import-js/eslint-plugin-import/pull/3004 [#2991]: https://github.com/import-js/eslint-plugin-import/pull/2991 [#2989]: https://github.com/import-js/eslint-plugin-import/pull/2989 @@ -1915,6 +1917,7 @@ for info on changes for earlier releases. [@sergei-startsev]: https://github.com/sergei-startsev [@sharmilajesupaul]: https://github.com/sharmilajesupaul [@sheepsteak]: https://github.com/sheepsteak +[@silverwind]: https://github.com/silverwind [@silviogutierrez]: https://github.com/silviogutierrez [@SimenB]: https://github.com/SimenB [@simmo]: https://github.com/simmo diff --git a/docs/rules/no-unused-modules.md b/docs/rules/no-unused-modules.md index 53c247927..359c341ea 100644 --- a/docs/rules/no-unused-modules.md +++ b/docs/rules/no-unused-modules.md @@ -29,8 +29,9 @@ 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`) - - `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) + - **`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) ### Example for missing exports @@ -116,6 +117,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.js b/src/rules/no-unused-modules.js index 0ad330b48..46fc93bfe 100644 --- a/src/rules/no-unused-modules.js +++ b/src/rules/no-unused-modules.js @@ -83,28 +83,30 @@ const DEFAULT = 'default'; function forEachDeclarationIdentifier(declaration, cb) { if (declaration) { + const isTypeDeclaration = declaration.type === TS_INTERFACE_DECLARATION + || declaration.type === TS_TYPE_ALIAS_DECLARATION + || declaration.type === TS_ENUM_DECLARATION; + if ( declaration.type === FUNCTION_DECLARATION || declaration.type === CLASS_DECLARATION - || declaration.type === TS_INTERFACE_DECLARATION - || declaration.type === TS_TYPE_ALIAS_DECLARATION - || declaration.type === TS_ENUM_DECLARATION + || isTypeDeclaration ) { - cb(declaration.id.name); + cb(declaration.id.name, isTypeDeclaration); } else if (declaration.type === VARIABLE_DECLARATION) { declaration.declarations.forEach(({ id }) => { if (id.type === OBJECT_PATTERN) { recursivePatternCapture(id, (pattern) => { if (pattern.type === IDENTIFIER) { - cb(pattern.name); + cb(pattern.name, false); } }); } else if (id.type === ARRAY_PATTERN) { id.elements.forEach(({ name }) => { - cb(name); + cb(name, false); }); } else { - cb(id.name); + cb(id.name, false); } }); } @@ -443,6 +445,10 @@ module.exports = { description: 'report exports without any usage', type: 'boolean', }, + ignoreUnusedTypeExports: { + description: 'ignore type exports without any usage', + type: 'boolean', + }, }, anyOf: [ { @@ -470,6 +476,7 @@ module.exports = { ignoreExports = [], missingExports, unusedExports, + ignoreUnusedTypeExports, } = context.options[0] || {}; if (unusedExports) { @@ -502,11 +509,15 @@ module.exports = { exportCount.set(IMPORT_NAMESPACE_SPECIFIER, namespaceImports); }; - const checkUsage = (node, exportedValue) => { + const checkUsage = (node, exportedValue, isTypeExport) => { if (!unusedExports) { return; } + if (isTypeExport && ignoreUnusedTypeExports) { + return; + } + if (ignoredFiles.has(file)) { return; } @@ -935,14 +946,14 @@ module.exports = { checkExportPresence(node); }, ExportDefaultDeclaration(node) { - checkUsage(node, IMPORT_DEFAULT_SPECIFIER); + checkUsage(node, IMPORT_DEFAULT_SPECIFIER, false); }, ExportNamedDeclaration(node) { node.specifiers.forEach((specifier) => { - checkUsage(specifier, specifier.exported.name || specifier.exported.value); + checkUsage(specifier, specifier.exported.name || specifier.exported.value, false); }); - forEachDeclarationIdentifier(node.declaration, (name) => { - checkUsage(node, name); + forEachDeclarationIdentifier(node.declaration, (name, isTypeExport) => { + checkUsage(node, name, isTypeExport); }); }, }; diff --git a/tests/src/rules/no-unused-modules.js b/tests/src/rules/no-unused-modules.js index b09d5d759..80bd70227 100644 --- a/tests/src/rules/no-unused-modules.js +++ b/tests/src/rules/no-unused-modules.js @@ -38,6 +38,13 @@ const unusedExportsTypescriptOptions = [{ ignoreExports: undefined, }]; +const unusedExportsTypescriptIgnoreUnusedTypesOptions = [{ + unusedExports: true, + ignoreUnusedTypeExports: true, + src: [testFilePath('./no-unused-modules/typescript')], + ignoreExports: undefined, +}]; + const unusedExportsJsxOptions = [{ unusedExports: true, src: [testFilePath('./no-unused-modules/jsx')], @@ -1209,6 +1216,66 @@ context('TypeScript', function () { }); }); +describe('ignoreUnusedTypeExports', () => { + getTSParsers().forEach((parser) => { + 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: [