Skip to content

Commit

Permalink
[New] no-unused-modules: Add unusedTypeExports option
Browse files Browse the repository at this point in the history
  • Loading branch information
silverwind committed May 28, 2024
1 parent 6554bd5 commit b09fd56
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 12 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `unusedTypeExports` option ([#3011], thanks [@silverwind])

### Changed
- [Docs] `no-extraneous-dependencies`: Make glob pattern description more explicit ([#2944], thanks [@mulztob])
Expand Down
9 changes: 9 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`)
- **`unusedTypeExports`**: if `true`, type exports without any static usage within other modules are reported (defaults to `true` when `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 @@ -116,6 +117,14 @@ export function doAnything() {
export default 5 // will not be reported
```

### Example for unused exports with `unusedTypeExports` set to `false`

```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
36 changes: 24 additions & 12 deletions src/rules/no-unused-modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
}
Expand Down Expand Up @@ -443,6 +445,11 @@ module.exports = {
description: 'report exports without any usage',
type: 'boolean',
},
unusedTypeExports: {
description: 'report type exports without any usage',
type: 'boolean',
default: true,
},
},
anyOf: [
{
Expand Down Expand Up @@ -470,6 +477,7 @@ module.exports = {
ignoreExports = [],
missingExports,
unusedExports,
unusedTypeExports,
} = context.options[0] || {};

if (unusedExports) {
Expand Down Expand Up @@ -502,11 +510,15 @@ module.exports = {
exportCount.set(IMPORT_NAMESPACE_SPECIFIER, namespaceImports);
};

const checkUsage = (node, exportedValue) => {
const checkUsage = (node, exportedValue, isTypeExport) => {
if (!unusedExports) {
return;
}

if (isTypeExport && unusedExports && !unusedTypeExports) {
return;
}

if (ignoredFiles.has(file)) {
return;
}
Expand Down Expand Up @@ -935,14 +947,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);
});
},
};
Expand Down
52 changes: 52 additions & 0 deletions tests/src/rules/no-unused-modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ const unusedExportsTypescriptOptions = [{
ignoreExports: undefined,
}];

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

const unusedExportsJsxOptions = [{
unusedExports: true,
src: [testFilePath('./no-unused-modules/jsx')],
Expand Down Expand Up @@ -1141,6 +1148,24 @@ context('TypeScript', function () {
parser,
filename: testFilePath('./no-unused-modules/typescript/file-ts-e-used-as-type.ts'),
}),
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'),
}),
// Should also be valid when the exporting files are linted before the importing ones
isESLint4TODO ? [] : test({
options: unusedExportsTypescriptOptions,
Expand All @@ -1166,6 +1191,33 @@ context('TypeScript', function () {
parser,
filename: testFilePath('./no-unused-modules/typescript/file-ts-f-import-type.ts'),
}),
test({
options: unusedExportsTypescriptIgnoreUnusedTypesOptions,
code: `export interface c {};`,
parser,
filename: testFilePath('./no-unused-modules/typescript/file-ts-c-unused.ts'),
errors: [
error(`exported declaration 'c' not used within other modules`),
],
}),
test({
options: unusedExportsTypescriptIgnoreUnusedTypesOptions,
code: `export type d = {};`,
parser,
filename: testFilePath('./no-unused-modules/typescript/file-ts-d-unused.ts'),
errors: [
error(`exported declaration 'd' not used within other modules`),
],
}),
test({
options: unusedExportsTypescriptIgnoreUnusedTypesOptions,
code: `export enum e { f };`,
parser,
filename: testFilePath('./no-unused-modules/typescript/file-ts-e-unused.ts'),
errors: [
error(`exported declaration 'e' not used within other modules`),
],
}),
),
invalid: [].concat(
test({
Expand Down

0 comments on commit b09fd56

Please sign in to comment.