Skip to content

Commit 823b945

Browse files
feat(eslint-plugin): add no-useless-empty-export rule (#4380)
1 parent 63d051e commit 823b945

6 files changed

Lines changed: 253 additions & 0 deletions

File tree

‎packages/eslint-plugin/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int
151151
| [`@typescript-eslint/no-unsafe-call`](./docs/rules/no-unsafe-call.md) | Disallows calling an any type value | :white_check_mark: | | :thought_balloon: |
152152
| [`@typescript-eslint/no-unsafe-member-access`](./docs/rules/no-unsafe-member-access.md) | Disallows member access on any typed variables | :white_check_mark: | | :thought_balloon: |
153153
| [`@typescript-eslint/no-unsafe-return`](./docs/rules/no-unsafe-return.md) | Disallows returning any from a function | :white_check_mark: | | :thought_balloon: |
154+
| [`@typescript-eslint/no-useless-empty-export`](./docs/rules/no-useless-empty-export.md) | Disallow empty exports that don't change anything in a module file | | :wrench: | |
154155
| [`@typescript-eslint/no-var-requires`](./docs/rules/no-var-requires.md) | Disallows the use of require statements except in import statements | :white_check_mark: | | |
155156
| [`@typescript-eslint/non-nullable-type-assertion-style`](./docs/rules/non-nullable-type-assertion-style.md) | Prefers a non-null assertion over explicit type cast when possible | | :wrench: | :thought_balloon: |
156157
| [`@typescript-eslint/prefer-as-const`](./docs/rules/prefer-as-const.md) | Prefer usage of `as const` over literal type | :white_check_mark: | :wrench: | |
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# `no-useless-empty-export`
2+
3+
Disallow empty exports that don't change anything in a module file.
4+
5+
## Rule Details
6+
7+
An empty `export {}` statement is sometimes useful in TypeScript code to turn a file that would otherwise be a script file into a module file.
8+
Per the TypeScript Handbook [Modules](https://www.typescriptlang.org/docs/handbook/modules.html) page:
9+
10+
> In TypeScript, just as in ECMAScript 2015, any file containing a top-level import or export is considered a module.
11+
> Conversely, a file without any top-level import or export declarations is treated as a script whose contents are available in the global scope (and therefore to modules as well).
12+
13+
However, an `export {}` statement does nothing if there are any other top-level import or export statements in a file.
14+
15+
Examples of code for this rule:
16+
17+
<!--tabs-->
18+
19+
### ❌ Incorrect
20+
21+
```ts
22+
export const value = 'Hello, world!';
23+
export {};
24+
```
25+
26+
```ts
27+
import 'some-other-module';
28+
export {};
29+
```
30+
31+
### ✅ Correct
32+
33+
```ts
34+
export const value = 'Hello, world!';
35+
```
36+
37+
```ts
38+
import 'some-other-module';
39+
```
40+
41+
## Attributes
42+
43+
- [ ] ✅ Recommended
44+
- [x] 🔧 Fixable
45+
- [ ] 💭 Requires type information

‎packages/eslint-plugin/src/configs/all.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ export = {
115115
'@typescript-eslint/no-unused-vars': 'error',
116116
'no-use-before-define': 'off',
117117
'@typescript-eslint/no-use-before-define': 'error',
118+
'@typescript-eslint/no-useless-empty-export': 'error',
118119
'no-useless-constructor': 'off',
119120
'@typescript-eslint/no-useless-constructor': 'error',
120121
'@typescript-eslint/no-var-requires': 'error',

‎packages/eslint-plugin/src/rules/index.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ import noUnusedExpressions from './no-unused-expressions';
8282
import noUnusedVars from './no-unused-vars';
8383
import noUseBeforeDefine from './no-use-before-define';
8484
import noUselessConstructor from './no-useless-constructor';
85+
import noUselessEmptyExport from './no-useless-empty-export';
8586
import noVarRequires from './no-var-requires';
8687
import nonNullableTypeAssertionStyle from './non-nullable-type-assertion-style';
8788
import objectCurlySpacing from './object-curly-spacing';
@@ -206,6 +207,7 @@ export default {
206207
'no-unused-vars': noUnusedVars,
207208
'no-use-before-define': noUseBeforeDefine,
208209
'no-useless-constructor': noUselessConstructor,
210+
'no-useless-empty-export': noUselessEmptyExport,
209211
'no-var-requires': noVarRequires,
210212
'non-nullable-type-assertion-style': nonNullableTypeAssertionStyle,
211213
'object-curly-spacing': objectCurlySpacing,
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
2+
import * as util from '../util';
3+
4+
function isEmptyExport(
5+
node: TSESTree.Node,
6+
): node is TSESTree.ExportNamedDeclaration {
7+
return (
8+
node.type === AST_NODE_TYPES.ExportNamedDeclaration &&
9+
node.specifiers.length === 0 &&
10+
!node.declaration
11+
);
12+
}
13+
14+
const exportOrImportNodeTypes = new Set([
15+
AST_NODE_TYPES.ExportAllDeclaration,
16+
AST_NODE_TYPES.ExportDefaultDeclaration,
17+
AST_NODE_TYPES.ExportNamedDeclaration,
18+
AST_NODE_TYPES.ExportSpecifier,
19+
AST_NODE_TYPES.ImportDeclaration,
20+
AST_NODE_TYPES.TSExportAssignment,
21+
AST_NODE_TYPES.TSImportEqualsDeclaration,
22+
]);
23+
24+
export default util.createRule({
25+
name: 'no-useless-empty-export',
26+
meta: {
27+
docs: {
28+
description:
29+
"Disallow empty exports that don't change anything in a module file",
30+
recommended: false,
31+
suggestion: true,
32+
},
33+
fixable: 'code',
34+
hasSuggestions: true,
35+
messages: {
36+
uselessExport: 'Empty export does nothing and can be removed.',
37+
},
38+
schema: [],
39+
type: 'suggestion',
40+
},
41+
defaultOptions: [],
42+
create(context) {
43+
function checkNode(
44+
node: TSESTree.Program | TSESTree.TSModuleDeclaration,
45+
): void {
46+
if (!Array.isArray(node.body)) {
47+
return;
48+
}
49+
50+
let emptyExport: TSESTree.ExportNamedDeclaration | undefined;
51+
let foundOtherExport = false;
52+
53+
for (const statement of node.body) {
54+
if (isEmptyExport(statement)) {
55+
emptyExport = statement;
56+
57+
if (foundOtherExport) {
58+
break;
59+
}
60+
} else if (exportOrImportNodeTypes.has(statement.type)) {
61+
foundOtherExport = true;
62+
}
63+
}
64+
65+
if (emptyExport && foundOtherExport) {
66+
context.report({
67+
fix: fixer => fixer.remove(emptyExport!),
68+
messageId: 'uselessExport',
69+
node: emptyExport,
70+
});
71+
}
72+
}
73+
74+
return {
75+
Program: checkNode,
76+
TSModuleDeclaration: checkNode,
77+
};
78+
},
79+
});
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/* eslint-disable eslint-comments/no-use */
2+
// this rule tests the spacing, which prettier will want to fix and break the tests
3+
/* eslint "@typescript-eslint/internal/plugin-test-formatting": ["error", { formatWithPrettier: false }] */
4+
/* eslint-enable eslint-comments/no-use */
5+
import rule from '../../src/rules/no-useless-empty-export';
6+
import { RuleTester } from '../RuleTester';
7+
8+
const ruleTester = new RuleTester({
9+
parserOptions: {
10+
ecmaVersion: 2020,
11+
sourceType: 'module',
12+
},
13+
parser: '@typescript-eslint/parser',
14+
});
15+
16+
const error = {
17+
messageId: 'uselessExport',
18+
} as const;
19+
20+
ruleTester.run('no-useless-empty-export', rule, {
21+
valid: [
22+
"declare module '_'",
23+
"import {} from '_';",
24+
"import * as _ from '_';",
25+
'export = {};',
26+
'export = 3;',
27+
'export const _ = {};',
28+
`
29+
const _ = {};
30+
export default _;
31+
`,
32+
`
33+
export * from '_';
34+
export = {};
35+
`,
36+
`
37+
export {};
38+
`,
39+
],
40+
invalid: [
41+
{
42+
code: `
43+
export const _ = {};
44+
export {};
45+
`,
46+
errors: [error],
47+
output: `
48+
export const _ = {};
49+
50+
`,
51+
},
52+
{
53+
code: `
54+
export * from '_';
55+
export {};
56+
`,
57+
errors: [error],
58+
output: `
59+
export * from '_';
60+
61+
`,
62+
},
63+
{
64+
code: `
65+
export {};
66+
export * from '_';
67+
`,
68+
errors: [error],
69+
output: `
70+
71+
export * from '_';
72+
`,
73+
},
74+
{
75+
code: `
76+
const _ = {};
77+
export default _;
78+
export {};
79+
`,
80+
errors: [error],
81+
output: `
82+
const _ = {};
83+
export default _;
84+
85+
`,
86+
},
87+
{
88+
code: `
89+
export {};
90+
const _ = {};
91+
export default _;
92+
`,
93+
errors: [error],
94+
output: `
95+
96+
const _ = {};
97+
export default _;
98+
`,
99+
},
100+
{
101+
code: `
102+
const _ = {};
103+
export { _ };
104+
export {};
105+
`,
106+
errors: [error],
107+
output: `
108+
const _ = {};
109+
export { _ };
110+
111+
`,
112+
},
113+
{
114+
code: `
115+
import _ = require('_');
116+
export {};
117+
`,
118+
errors: [error],
119+
output: `
120+
import _ = require('_');
121+
122+
`,
123+
},
124+
],
125+
});

0 commit comments

Comments
 (0)