Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat(eslint-plugin): add no-useless-empty-export rule (#4380)
  • Loading branch information
JoshuaKGoldberg committed Feb 23, 2022
1 parent 63d051e commit 823b945
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Expand Up @@ -151,6 +151,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int
| [`@typescript-eslint/no-unsafe-call`](./docs/rules/no-unsafe-call.md) | Disallows calling an any type value | :white_check_mark: | | :thought_balloon: |
| [`@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: |
| [`@typescript-eslint/no-unsafe-return`](./docs/rules/no-unsafe-return.md) | Disallows returning any from a function | :white_check_mark: | | :thought_balloon: |
| [`@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: | |
| [`@typescript-eslint/no-var-requires`](./docs/rules/no-var-requires.md) | Disallows the use of require statements except in import statements | :white_check_mark: | | |
| [`@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: |
| [`@typescript-eslint/prefer-as-const`](./docs/rules/prefer-as-const.md) | Prefer usage of `as const` over literal type | :white_check_mark: | :wrench: | |
Expand Down
45 changes: 45 additions & 0 deletions packages/eslint-plugin/docs/rules/no-useless-empty-export.md
@@ -0,0 +1,45 @@
# `no-useless-empty-export`

Disallow empty exports that don't change anything in a module file.

## Rule Details

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.
Per the TypeScript Handbook [Modules](https://www.typescriptlang.org/docs/handbook/modules.html) page:

> In TypeScript, just as in ECMAScript 2015, any file containing a top-level import or export is considered a module.
> 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).
However, an `export {}` statement does nothing if there are any other top-level import or export statements in a file.

Examples of code for this rule:

<!--tabs-->

### ❌ Incorrect

```ts
export const value = 'Hello, world!';
export {};
```

```ts
import 'some-other-module';
export {};
```

### ✅ Correct

```ts
export const value = 'Hello, world!';
```

```ts
import 'some-other-module';
```

## Attributes

- [ ] ✅ Recommended
- [x] 🔧 Fixable
- [ ] 💭 Requires type information
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/all.ts
Expand Up @@ -115,6 +115,7 @@ export = {
'@typescript-eslint/no-unused-vars': 'error',
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': 'error',
'@typescript-eslint/no-useless-empty-export': 'error',
'no-useless-constructor': 'off',
'@typescript-eslint/no-useless-constructor': 'error',
'@typescript-eslint/no-var-requires': 'error',
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Expand Up @@ -82,6 +82,7 @@ import noUnusedExpressions from './no-unused-expressions';
import noUnusedVars from './no-unused-vars';
import noUseBeforeDefine from './no-use-before-define';
import noUselessConstructor from './no-useless-constructor';
import noUselessEmptyExport from './no-useless-empty-export';
import noVarRequires from './no-var-requires';
import nonNullableTypeAssertionStyle from './non-nullable-type-assertion-style';
import objectCurlySpacing from './object-curly-spacing';
Expand Down Expand Up @@ -206,6 +207,7 @@ export default {
'no-unused-vars': noUnusedVars,
'no-use-before-define': noUseBeforeDefine,
'no-useless-constructor': noUselessConstructor,
'no-useless-empty-export': noUselessEmptyExport,
'no-var-requires': noVarRequires,
'non-nullable-type-assertion-style': nonNullableTypeAssertionStyle,
'object-curly-spacing': objectCurlySpacing,
Expand Down
79 changes: 79 additions & 0 deletions packages/eslint-plugin/src/rules/no-useless-empty-export.ts
@@ -0,0 +1,79 @@
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
import * as util from '../util';

function isEmptyExport(
node: TSESTree.Node,
): node is TSESTree.ExportNamedDeclaration {
return (
node.type === AST_NODE_TYPES.ExportNamedDeclaration &&
node.specifiers.length === 0 &&
!node.declaration
);
}

const exportOrImportNodeTypes = new Set([
AST_NODE_TYPES.ExportAllDeclaration,
AST_NODE_TYPES.ExportDefaultDeclaration,
AST_NODE_TYPES.ExportNamedDeclaration,
AST_NODE_TYPES.ExportSpecifier,
AST_NODE_TYPES.ImportDeclaration,
AST_NODE_TYPES.TSExportAssignment,
AST_NODE_TYPES.TSImportEqualsDeclaration,
]);

export default util.createRule({
name: 'no-useless-empty-export',
meta: {
docs: {
description:
"Disallow empty exports that don't change anything in a module file",
recommended: false,
suggestion: true,
},
fixable: 'code',
hasSuggestions: true,
messages: {
uselessExport: 'Empty export does nothing and can be removed.',
},
schema: [],
type: 'suggestion',
},
defaultOptions: [],
create(context) {
function checkNode(
node: TSESTree.Program | TSESTree.TSModuleDeclaration,
): void {
if (!Array.isArray(node.body)) {
return;
}

let emptyExport: TSESTree.ExportNamedDeclaration | undefined;
let foundOtherExport = false;

for (const statement of node.body) {
if (isEmptyExport(statement)) {
emptyExport = statement;

if (foundOtherExport) {
break;
}
} else if (exportOrImportNodeTypes.has(statement.type)) {
foundOtherExport = true;
}
}

if (emptyExport && foundOtherExport) {
context.report({
fix: fixer => fixer.remove(emptyExport!),
messageId: 'uselessExport',
node: emptyExport,
});
}
}

return {
Program: checkNode,
TSModuleDeclaration: checkNode,
};
},
});
125 changes: 125 additions & 0 deletions packages/eslint-plugin/tests/rules/no-useless-empty-export.test.ts
@@ -0,0 +1,125 @@
/* eslint-disable eslint-comments/no-use */
// this rule tests the spacing, which prettier will want to fix and break the tests
/* eslint "@typescript-eslint/internal/plugin-test-formatting": ["error", { formatWithPrettier: false }] */
/* eslint-enable eslint-comments/no-use */
import rule from '../../src/rules/no-useless-empty-export';
import { RuleTester } from '../RuleTester';

const ruleTester = new RuleTester({
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
parser: '@typescript-eslint/parser',
});

const error = {
messageId: 'uselessExport',
} as const;

ruleTester.run('no-useless-empty-export', rule, {
valid: [
"declare module '_'",
"import {} from '_';",
"import * as _ from '_';",
'export = {};',
'export = 3;',
'export const _ = {};',
`
const _ = {};
export default _;
`,
`
export * from '_';
export = {};
`,
`
export {};
`,
],
invalid: [
{
code: `
export const _ = {};
export {};
`,
errors: [error],
output: `
export const _ = {};
`,
},
{
code: `
export * from '_';
export {};
`,
errors: [error],
output: `
export * from '_';
`,
},
{
code: `
export {};
export * from '_';
`,
errors: [error],
output: `
export * from '_';
`,
},
{
code: `
const _ = {};
export default _;
export {};
`,
errors: [error],
output: `
const _ = {};
export default _;
`,
},
{
code: `
export {};
const _ = {};
export default _;
`,
errors: [error],
output: `
const _ = {};
export default _;
`,
},
{
code: `
const _ = {};
export { _ };
export {};
`,
errors: [error],
output: `
const _ = {};
export { _ };
`,
},
{
code: `
import _ = require('_');
export {};
`,
errors: [error],
output: `
import _ = require('_');
`,
},
],
});

0 comments on commit 823b945

Please sign in to comment.