Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
feat(eslint-plugin): add
no-useless-empty-export
rule (#4380)
- Loading branch information
1 parent
63d051e
commit 823b945
Showing
6 changed files
with
253 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
45 changes: 45 additions & 0 deletions
45
packages/eslint-plugin/docs/rules/no-useless-empty-export.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
79 changes: 79 additions & 0 deletions
79
packages/eslint-plugin/src/rules/no-useless-empty-export.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
125
packages/eslint-plugin/tests/rules/no-useless-empty-export.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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('_'); | ||
`, | ||
}, | ||
], | ||
}); |