From a67eb47ae8a4b6ca22ab8a47f65560ded35055b7 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Fri, 31 Dec 2021 17:06:25 -0500 Subject: [PATCH 1/6] feat(eslint-plugin): add `no-useless-empty-export` rule --- .../docs/rules/no-useless-empty-export.md | 43 +++++++ packages/eslint-plugin/src/configs/all.ts | 1 + packages/eslint-plugin/src/rules/index.ts | 2 + .../src/rules/no-useless-empty-export.ts | 73 +++++++++++ .../rules/no-useless-empty-export.test.ts | 121 ++++++++++++++++++ 5 files changed, 240 insertions(+) create mode 100644 packages/eslint-plugin/docs/rules/no-useless-empty-export.md create mode 100644 packages/eslint-plugin/src/rules/no-useless-empty-export.ts create mode 100644 packages/eslint-plugin/tests/rules/no-useless-empty-export.test.ts diff --git a/packages/eslint-plugin/docs/rules/no-useless-empty-export.md b/packages/eslint-plugin/docs/rules/no-useless-empty-export.md new file mode 100644 index 00000000000..8ab2d8a613d --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-useless-empty-export.md @@ -0,0 +1,43 @@ +# Disallow empty exports that don't change anything in a module file (`no-useless-empty-export`) + +## 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: + + + +### ❌ 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 diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index 538be53ce58..8fc92146df8 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -114,6 +114,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', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 83e76f0a23d..80f66601352 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -81,6 +81,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'; @@ -204,6 +205,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, diff --git a/packages/eslint-plugin/src/rules/no-useless-empty-export.ts b/packages/eslint-plugin/src/rules/no-useless-empty-export.ts new file mode 100644 index 00000000000..9dbfc49de5f --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-useless-empty-export.ts @@ -0,0 +1,73 @@ +import { + AST_NODE_TYPES, + TSESTree, +} from '@typescript-eslint/experimental-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) { + return { + Program(node): void { + 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, + }); + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/no-useless-empty-export.test.ts b/packages/eslint-plugin/tests/rules/no-useless-empty-export.test.ts new file mode 100644 index 00000000000..7135e465fb9 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-useless-empty-export.test.ts @@ -0,0 +1,121 @@ +/* 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: [ + "import {} from '_';", + "import * as _ from '_';", + 'export = {};', + 'export = 3;', + 'export const _ = {};', + ` + const _ = {}; + export default _; + `, + ` + export * from '_'; + 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('_'); + + `, + }, + ], +}); From 7896f4f9dd69261c2db6afd57a5cce14fdb6deb5 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sat, 1 Jan 2022 12:30:58 -0500 Subject: [PATCH 2/6] chore: handle module declarations and fix table list --- packages/eslint-plugin/README.md | 1 + .../src/rules/no-useless-empty-export.ts | 49 +++++++++++-------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 47012b81b8f..7c98d5bd845 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -150,6 +150,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: | | diff --git a/packages/eslint-plugin/src/rules/no-useless-empty-export.ts b/packages/eslint-plugin/src/rules/no-useless-empty-export.ts index 9dbfc49de5f..97a5ef9e2c1 100644 --- a/packages/eslint-plugin/src/rules/no-useless-empty-export.ts +++ b/packages/eslint-plugin/src/rules/no-useless-empty-export.ts @@ -43,31 +43,40 @@ export default util.createRule({ }, defaultOptions: [], create(context) { - return { - Program(node): void { - let emptyExport: TSESTree.ExportNamedDeclaration | undefined; - let foundOtherExport = false; + 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; + for (const statement of node.body) { + if (isEmptyExport(statement)) { + emptyExport = statement; - if (foundOtherExport) { - break; - } - } else if (exportOrImportNodeTypes.has(statement.type)) { - foundOtherExport = true; + 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, - }); - } - }, + if (emptyExport && foundOtherExport) { + context.report({ + fix: fixer => fixer.remove(emptyExport!), + messageId: 'uselessExport', + node: emptyExport, + }); + } + } + + return { + Program: checkNode, + TSModuleDeclaration: checkNode, }; }, }); From 4181694d34e7a3d6bc053c823549f8b6eac097d3 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sun, 2 Jan 2022 15:06:56 -0500 Subject: [PATCH 3/6] chore: add empty body case --- .../eslint-plugin/tests/rules/no-useless-empty-export.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/eslint-plugin/tests/rules/no-useless-empty-export.test.ts b/packages/eslint-plugin/tests/rules/no-useless-empty-export.test.ts index 7135e465fb9..e07a361540e 100644 --- a/packages/eslint-plugin/tests/rules/no-useless-empty-export.test.ts +++ b/packages/eslint-plugin/tests/rules/no-useless-empty-export.test.ts @@ -19,6 +19,7 @@ const error = { ruleTester.run('no-useless-empty-export', rule, { valid: [ + "declare module '_'", "import {} from '_';", "import * as _ from '_';", 'export = {};', From a6c635d0ea061fbfdb16385d9ea8a7b765e15973 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sun, 23 Jan 2022 18:01:52 -0500 Subject: [PATCH 4/6] chore: update utils package name in no-useless-empty-export.ts --- packages/eslint-plugin/src/rules/no-useless-empty-export.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-useless-empty-export.ts b/packages/eslint-plugin/src/rules/no-useless-empty-export.ts index 97a5ef9e2c1..c06c47b8f8e 100644 --- a/packages/eslint-plugin/src/rules/no-useless-empty-export.ts +++ b/packages/eslint-plugin/src/rules/no-useless-empty-export.ts @@ -1,7 +1,4 @@ -import { - AST_NODE_TYPES, - TSESTree, -} from '@typescript-eslint/experimental-utils'; +import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; import * as util from '../util'; function isEmptyExport( From b4e7ddfddfd0f125b2a259c4c81a82b5758cef3a Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 2 Feb 2022 15:53:02 -0500 Subject: [PATCH 5/6] Update packages/eslint-plugin/tests/rules/no-useless-empty-export.test.ts Co-authored-by: Brad Zacher --- .../eslint-plugin/tests/rules/no-useless-empty-export.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/no-useless-empty-export.test.ts b/packages/eslint-plugin/tests/rules/no-useless-empty-export.test.ts index e07a361540e..ea13395ec9e 100644 --- a/packages/eslint-plugin/tests/rules/no-useless-empty-export.test.ts +++ b/packages/eslint-plugin/tests/rules/no-useless-empty-export.test.ts @@ -33,6 +33,9 @@ ruleTester.run('no-useless-empty-export', rule, { export * from '_'; export = {}; `, + ` + export {}; + `, ], invalid: [ { From e909d2ae1e338a9b636b5e17128837889cdb79a8 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 23 Feb 2022 15:32:14 -0500 Subject: [PATCH 6/6] docs: corrected docs file --- packages/eslint-plugin/docs/rules/no-useless-empty-export.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/no-useless-empty-export.md b/packages/eslint-plugin/docs/rules/no-useless-empty-export.md index 8ab2d8a613d..0cb24763f12 100644 --- a/packages/eslint-plugin/docs/rules/no-useless-empty-export.md +++ b/packages/eslint-plugin/docs/rules/no-useless-empty-export.md @@ -1,4 +1,6 @@ -# Disallow empty exports that don't change anything in a module file (`no-useless-empty-export`) +# `no-useless-empty-export` + +Disallow empty exports that don't change anything in a module file. ## Rule Details