diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e66a4563c5..2c5ffe5d39d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -139,6 +139,10 @@ if no error diagnostics are emitted. This rule disallows `\8` and `\9` escape sequences in string literals. +- Add [`noUselessEmptyExport`](https://docs.rome.tools/lint/rules/noUselessEmptyExport/) + + This rule disallows useless `export {}`. + #### Other changes - Add new TypeScript globals (`AsyncDisposable`, `Awaited`, `DecoratorContext`, and others) [4643](https://github.com/rome/tools/issues/4643). diff --git a/crates/rome_diagnostics_categories/src/categories.rs b/crates/rome_diagnostics_categories/src/categories.rs index 305feebf152..d338dd6b1c3 100644 --- a/crates/rome_diagnostics_categories/src/categories.rs +++ b/crates/rome_diagnostics_categories/src/categories.rs @@ -93,6 +93,7 @@ define_categories! { "lint/nursery/noRedundantRoles": "https://docs.rome.tools/lint/rules/noRedundantRoles", "lint/nursery/noSelfAssign": "https://docs.rome.tools/lint/rules/noSelfAssign", "lint/nursery/noStaticOnlyClass": "https://docs.rome.tools/lint/rules/noStaticOnlyClass", + "lint/nursery/noUselessEmptyExport": "https://docs.rome.tools/lint/rules/noUselessEmptyExport", "lint/nursery/noVoid": "https://docs.rome.tools/lint/rules/noVoid", "lint/nursery/useAriaPropTypes": "https://docs.rome.tools/lint/rules/useAriaPropTypes", "lint/nursery/useArrowFunction": "https://docs.rome.tools/lint/rules/useArrowFunction", diff --git a/crates/rome_js_analyze/src/analyzers/nursery.rs b/crates/rome_js_analyze/src/analyzers/nursery.rs index cbf2d104332..942f41becbc 100644 --- a/crates/rome_js_analyze/src/analyzers/nursery.rs +++ b/crates/rome_js_analyze/src/analyzers/nursery.rs @@ -10,6 +10,7 @@ pub(crate) mod no_for_each; pub(crate) mod no_nonoctal_decimal_escape; pub(crate) mod no_self_assign; pub(crate) mod no_static_only_class; +pub(crate) mod no_useless_empty_export; pub(crate) mod no_void; pub(crate) mod use_arrow_function; pub(crate) mod use_grouped_type_import; @@ -30,6 +31,7 @@ declare_group! { self :: no_nonoctal_decimal_escape :: NoNonoctalDecimalEscape , self :: no_self_assign :: NoSelfAssign , self :: no_static_only_class :: NoStaticOnlyClass , + self :: no_useless_empty_export :: NoUselessEmptyExport , self :: no_void :: NoVoid , self :: use_arrow_function :: UseArrowFunction , self :: use_grouped_type_import :: UseGroupedTypeImport , diff --git a/crates/rome_js_analyze/src/analyzers/nursery/no_useless_empty_export.rs b/crates/rome_js_analyze/src/analyzers/nursery/no_useless_empty_export.rs new file mode 100644 index 00000000000..eeb3789a088 --- /dev/null +++ b/crates/rome_js_analyze/src/analyzers/nursery/no_useless_empty_export.rs @@ -0,0 +1,125 @@ +use rome_analyze::{context::RuleContext, declare_rule, ActionCategory, Ast, Rule, RuleDiagnostic}; +use rome_console::markup; +use rome_diagnostics::Applicability; +use rome_js_syntax::{AnyJsModuleItem, JsExport, JsModuleItemList, JsSyntaxToken}; +use rome_rowan::{AstNode, AstSeparatedList, BatchMutationExt}; + +use crate::JsRuleAction; + +declare_rule! { + /// Disallow empty exports that don't change anything in a module file. + /// + /// An empty `export {}` is sometimes useful to turn a file that would otherwise be a script into a module. + /// Per the [TypeScript Handbook Modules page](https://www.typescriptlang.org/docs/handbook/modules.html): + /// + /// > 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. + /// + /// However, an `export {}` statement does nothing if there are any other top-level import or export in the file. + /// + /// Source: https://typescript-eslint.io/rules/no-useless-empty-export/ + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```js,expect_diagnostic + /// import { A } from "module"; + /// export {}; + /// ``` + /// + /// ```js,expect_diagnostic + /// export const A = 0; + /// export {}; + /// ``` + /// + /// ## Valid + /// + /// ```js + /// export {}; + /// ``` + /// + pub(crate) NoUselessEmptyExport { + version: "next", + name: "noUselessEmptyExport", + recommended: true, + } +} + +impl Rule for NoUselessEmptyExport { + type Query = Ast; + /// The first import or export that makes useless the empty export. + type State = JsSyntaxToken; + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let node = ctx.query(); + if is_empty_export(node) { + let module_item_list = JsModuleItemList::cast(node.syntax().parent()?)?; + // allow reporting an empty export that precedes another empty export. + let mut ignore_empty_export = true; + for module_item in module_item_list { + match module_item { + AnyJsModuleItem::AnyJsStatement(_) => {} + AnyJsModuleItem::JsImport(import) => return import.import_token().ok(), + AnyJsModuleItem::JsExport(export) => { + if !is_empty_export(&export) { + return export.export_token().ok(); + } + if !ignore_empty_export { + return export.export_token().ok(); + } + if node == &export { + ignore_empty_export = false + } + } + } + } + } + None + } + + fn diagnostic(ctx: &RuleContext, token: &Self::State) -> Option { + Some( + RuleDiagnostic::new( + rule_category!(), + ctx.query().range(), + markup! { + "This empty ""export"" is useless because there's another ""export"" or ""import""." + }, + ).detail(token.text_trimmed_range(), markup! { + "This "{token.text_trimmed()}" makes useless the empty export." + }), + ) + } + + fn action(ctx: &RuleContext, _: &Self::State) -> Option { + let mut mutation = ctx.root().begin(); + mutation.remove_node(ctx.query().clone()); + Some(JsRuleAction { + category: ActionCategory::QuickFix, + applicability: Applicability::Always, + message: markup! { "Remove this useless empty export." }.to_owned(), + mutation, + }) + } +} + +fn is_empty_export(export: &JsExport) -> bool { + (|| -> Option { + Some( + export + .export_clause() + .ok()? + .as_js_export_named_clause()? + .specifiers() + .iter() + .count() + == 0, + ) + })() + .unwrap_or(false) +} diff --git a/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_default_export.js b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_default_export.js new file mode 100644 index 00000000000..36b4a642a5e --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_default_export.js @@ -0,0 +1,2 @@ +export default {}; +export {} \ No newline at end of file diff --git a/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_default_export.js.snap b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_default_export.js.snap new file mode 100644 index 00000000000..02434c6e262 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_default_export.js.snap @@ -0,0 +1,35 @@ +--- +source: crates/rome_js_analyze/tests/spec_tests.rs +expression: invalid_with_default_export.js +--- +# Input +```js +export default {}; +export {} +``` + +# Diagnostics +``` +invalid_with_default_export.js:2:1 lint/nursery/noUselessEmptyExport FIXABLE ━━━━━━━━━━━━━━━━━━━━━ + + ! This empty export is useless because there's another export or import. + + 1 │ export default {}; + > 2 │ export {} + │ ^^^^^^^^^ + + i This export makes useless the empty export. + + > 1 │ export default {}; + │ ^^^^^^ + 2 │ export {} + + i Safe fix: Remove this useless empty export. + + 1 1 │ export default {}; + 2 │ - export·{} + + +``` + + diff --git a/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_empty_export.js b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_empty_export.js new file mode 100644 index 00000000000..c12bd3bc4d3 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_empty_export.js @@ -0,0 +1,2 @@ +export {} +export {} \ No newline at end of file diff --git a/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_empty_export.js.snap b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_empty_export.js.snap new file mode 100644 index 00000000000..4345edd0068 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_empty_export.js.snap @@ -0,0 +1,34 @@ +--- +source: crates/rome_js_analyze/tests/spec_tests.rs +expression: invalid_with_empty_export.js +--- +# Input +```js +export {} +export {} +``` + +# Diagnostics +``` +invalid_with_empty_export.js:1:1 lint/nursery/noUselessEmptyExport FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━ + + ! This empty export is useless because there's another export or import. + + > 1 │ export {} + │ ^^^^^^^^^ + 2 │ export {} + + i This export makes useless the empty export. + + 1 │ export {} + > 2 │ export {} + │ ^^^^^^ + + i Safe fix: Remove this useless empty export. + + 1 │ export·{} + │ --------- + +``` + + diff --git a/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_export.js b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_export.js new file mode 100644 index 00000000000..e474951e06c --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_export.js @@ -0,0 +1,3 @@ +function f() { return 0 } +export const A = f(); +export {} \ No newline at end of file diff --git a/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_export.js.snap b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_export.js.snap new file mode 100644 index 00000000000..5ee66a00acb --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_export.js.snap @@ -0,0 +1,39 @@ +--- +source: crates/rome_js_analyze/tests/spec_tests.rs +expression: invalid_with_export.js +--- +# Input +```js +function f() { return 0 } +export const A = f(); +export {} +``` + +# Diagnostics +``` +invalid_with_export.js:3:1 lint/nursery/noUselessEmptyExport FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This empty export is useless because there's another export or import. + + 1 │ function f() { return 0 } + 2 │ export const A = f(); + > 3 │ export {} + │ ^^^^^^^^^ + + i This export makes useless the empty export. + + 1 │ function f() { return 0 } + > 2 │ export const A = f(); + │ ^^^^^^ + 3 │ export {} + + i Safe fix: Remove this useless empty export. + + 1 1 │ function f() { return 0 } + 2 2 │ export const A = f(); + 3 │ - export·{} + + +``` + + diff --git a/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_export_from.js b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_export_from.js new file mode 100644 index 00000000000..ed333b772d6 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_export_from.js @@ -0,0 +1,2 @@ +export * from "mod"; +export {} \ No newline at end of file diff --git a/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_export_from.js.snap b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_export_from.js.snap new file mode 100644 index 00000000000..b7b58706498 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_export_from.js.snap @@ -0,0 +1,35 @@ +--- +source: crates/rome_js_analyze/tests/spec_tests.rs +expression: invalid_with_export_from.js +--- +# Input +```js +export * from "mod"; +export {} +``` + +# Diagnostics +``` +invalid_with_export_from.js:2:1 lint/nursery/noUselessEmptyExport FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This empty export is useless because there's another export or import. + + 1 │ export * from "mod"; + > 2 │ export {} + │ ^^^^^^^^^ + + i This export makes useless the empty export. + + > 1 │ export * from "mod"; + │ ^^^^^^ + 2 │ export {} + + i Safe fix: Remove this useless empty export. + + 1 1 │ export * from "mod"; + 2 │ - export·{} + + +``` + + diff --git a/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_import.js b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_import.js new file mode 100644 index 00000000000..97aff851bb5 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_import.js @@ -0,0 +1,3 @@ +import { A } from "mod" +function f() { return 0 } +export {} \ No newline at end of file diff --git a/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_import.js.snap b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_import.js.snap new file mode 100644 index 00000000000..6165cad2d47 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_import.js.snap @@ -0,0 +1,39 @@ +--- +source: crates/rome_js_analyze/tests/spec_tests.rs +expression: invalid_with_import.js +--- +# Input +```js +import { A } from "mod" +function f() { return 0 } +export {} +``` + +# Diagnostics +``` +invalid_with_import.js:3:1 lint/nursery/noUselessEmptyExport FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This empty export is useless because there's another export or import. + + 1 │ import { A } from "mod" + 2 │ function f() { return 0 } + > 3 │ export {} + │ ^^^^^^^^^ + + i This import makes useless the empty export. + + > 1 │ import { A } from "mod" + │ ^^^^^^ + 2 │ function f() { return 0 } + 3 │ export {} + + i Safe fix: Remove this useless empty export. + + 1 1 │ import { A } from "mod" + 2 2 │ function f() { return 0 } + 3 │ - export·{} + + +``` + + diff --git a/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_sideeffect_import.js b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_sideeffect_import.js new file mode 100644 index 00000000000..b888278bf5f --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_sideeffect_import.js @@ -0,0 +1,3 @@ +import "mod" +function f() { return 0 } +export {} \ No newline at end of file diff --git a/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_sideeffect_import.js.snap b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_sideeffect_import.js.snap new file mode 100644 index 00000000000..6391ece26a8 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/invalid_with_sideeffect_import.js.snap @@ -0,0 +1,39 @@ +--- +source: crates/rome_js_analyze/tests/spec_tests.rs +expression: invalid_with_sideeffect_import.js +--- +# Input +```js +import "mod" +function f() { return 0 } +export {} +``` + +# Diagnostics +``` +invalid_with_sideeffect_import.js:3:1 lint/nursery/noUselessEmptyExport FIXABLE ━━━━━━━━━━━━━━━━━━ + + ! This empty export is useless because there's another export or import. + + 1 │ import "mod" + 2 │ function f() { return 0 } + > 3 │ export {} + │ ^^^^^^^^^ + + i This import makes useless the empty export. + + > 1 │ import "mod" + │ ^^^^^^ + 2 │ function f() { return 0 } + 3 │ export {} + + i Safe fix: Remove this useless empty export. + + 1 1 │ import "mod" + 2 2 │ function f() { return 0 } + 3 │ - export·{} + + +``` + + diff --git a/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/valid_empty_export.js b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/valid_empty_export.js new file mode 100644 index 00000000000..cd30285c1ba --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/valid_empty_export.js @@ -0,0 +1,3 @@ +function f() {} +f(); +export {} \ No newline at end of file diff --git a/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/valid_empty_export.js.snap b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/valid_empty_export.js.snap new file mode 100644 index 00000000000..4e0a0720f0a --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/valid_empty_export.js.snap @@ -0,0 +1,12 @@ +--- +source: crates/rome_js_analyze/tests/spec_tests.rs +expression: valid_empty_export.js +--- +# Input +```js +function f() {} +f(); +export {} +``` + + diff --git a/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/valid_export.js b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/valid_export.js new file mode 100644 index 00000000000..1a06073b134 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/valid_export.js @@ -0,0 +1 @@ +export const A = 0; \ No newline at end of file diff --git a/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/valid_export.js.snap b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/valid_export.js.snap new file mode 100644 index 00000000000..eb47f880d0a --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noUselessEmptyExport/valid_export.js.snap @@ -0,0 +1,10 @@ +--- +source: crates/rome_js_analyze/tests/spec_tests.rs +expression: valid_export.js +--- +# Input +```js +export const A = 0; +``` + + diff --git a/crates/rome_service/src/configuration/linter/rules.rs b/crates/rome_service/src/configuration/linter/rules.rs index 5459402e617..89c7922359c 100644 --- a/crates/rome_service/src/configuration/linter/rules.rs +++ b/crates/rome_service/src/configuration/linter/rules.rs @@ -1899,6 +1899,15 @@ pub struct Nursery { #[bpaf(long("no-static-only-class"), argument("on|off|warn"), optional, hide)] #[serde(skip_serializing_if = "Option::is_none")] pub no_static_only_class: Option, + #[doc = "Disallow empty exports that don't change anything in a module file."] + #[bpaf( + long("no-useless-empty-export"), + argument("on|off|warn"), + optional, + hide + )] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_useless_empty_export: Option, #[doc = "Disallow the use of void operators, which is not a familiar operator."] #[bpaf(long("no-void"), argument("on|off|warn"), optional, hide)] #[serde(skip_serializing_if = "Option::is_none")] @@ -1974,7 +1983,7 @@ pub struct Nursery { } impl Nursery { const GROUP_NAME: &'static str = "nursery"; - pub(crate) const GROUP_RULES: [&'static str; 31] = [ + pub(crate) const GROUP_RULES: [&'static str; 32] = [ "noAccumulatingSpread", "noAriaUnsupportedElements", "noBannedTypes", @@ -1993,6 +2002,7 @@ impl Nursery { "noRedundantRoles", "noSelfAssign", "noStaticOnlyClass", + "noUselessEmptyExport", "noVoid", "useAriaPropTypes", "useArrowFunction", @@ -2007,7 +2017,7 @@ impl Nursery { "useNamingConvention", "useSimpleNumberKeys", ]; - const RECOMMENDED_RULES: [&'static str; 17] = [ + const RECOMMENDED_RULES: [&'static str; 18] = [ "noAriaUnsupportedElements", "noBannedTypes", "noConstantCondition", @@ -2019,6 +2029,7 @@ impl Nursery { "noRedundantRoles", "noSelfAssign", "noStaticOnlyClass", + "noUselessEmptyExport", "useArrowFunction", "useExhaustiveDependencies", "useGroupedTypeImport", @@ -2026,7 +2037,7 @@ impl Nursery { "useLiteralEnumMembers", "useLiteralKeys", ]; - const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 17] = [ + const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 18] = [ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5]), @@ -2038,14 +2049,15 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29]), ]; - const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 31] = [ + const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 32] = [ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), @@ -2077,6 +2089,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended(&self) -> bool { matches!(self.recommended, Some(true)) } @@ -2177,71 +2190,76 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } - if let Some(rule) = self.no_void.as_ref() { + if let Some(rule) = self.no_useless_empty_export.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } } - if let Some(rule) = self.use_aria_prop_types.as_ref() { + if let Some(rule) = self.no_void.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } } - if let Some(rule) = self.use_arrow_function.as_ref() { + if let Some(rule) = self.use_aria_prop_types.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } } - if let Some(rule) = self.use_camel_case.as_ref() { + if let Some(rule) = self.use_arrow_function.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } } - if let Some(rule) = self.use_exhaustive_dependencies.as_ref() { + if let Some(rule) = self.use_camel_case.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } } - if let Some(rule) = self.use_grouped_type_import.as_ref() { + if let Some(rule) = self.use_exhaustive_dependencies.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } } - if let Some(rule) = self.use_heading_content.as_ref() { + if let Some(rule) = self.use_grouped_type_import.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); } } - if let Some(rule) = self.use_hook_at_top_level.as_ref() { + if let Some(rule) = self.use_heading_content.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25])); } } - if let Some(rule) = self.use_is_nan.as_ref() { + if let Some(rule) = self.use_hook_at_top_level.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26])); } } - if let Some(rule) = self.use_literal_enum_members.as_ref() { + if let Some(rule) = self.use_is_nan.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27])); } } - if let Some(rule) = self.use_literal_keys.as_ref() { + if let Some(rule) = self.use_literal_enum_members.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } } - if let Some(rule) = self.use_naming_convention.as_ref() { + if let Some(rule) = self.use_literal_keys.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } - if let Some(rule) = self.use_simple_number_keys.as_ref() { + if let Some(rule) = self.use_naming_convention.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } } + if let Some(rule) = self.use_simple_number_keys.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> IndexSet { @@ -2336,71 +2354,76 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } - if let Some(rule) = self.no_void.as_ref() { + if let Some(rule) = self.no_useless_empty_export.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } } - if let Some(rule) = self.use_aria_prop_types.as_ref() { + if let Some(rule) = self.no_void.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } } - if let Some(rule) = self.use_arrow_function.as_ref() { + if let Some(rule) = self.use_aria_prop_types.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } } - if let Some(rule) = self.use_camel_case.as_ref() { + if let Some(rule) = self.use_arrow_function.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } } - if let Some(rule) = self.use_exhaustive_dependencies.as_ref() { + if let Some(rule) = self.use_camel_case.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } } - if let Some(rule) = self.use_grouped_type_import.as_ref() { + if let Some(rule) = self.use_exhaustive_dependencies.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } } - if let Some(rule) = self.use_heading_content.as_ref() { + if let Some(rule) = self.use_grouped_type_import.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); } } - if let Some(rule) = self.use_hook_at_top_level.as_ref() { + if let Some(rule) = self.use_heading_content.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25])); } } - if let Some(rule) = self.use_is_nan.as_ref() { + if let Some(rule) = self.use_hook_at_top_level.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26])); } } - if let Some(rule) = self.use_literal_enum_members.as_ref() { + if let Some(rule) = self.use_is_nan.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27])); } } - if let Some(rule) = self.use_literal_keys.as_ref() { + if let Some(rule) = self.use_literal_enum_members.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } } - if let Some(rule) = self.use_naming_convention.as_ref() { + if let Some(rule) = self.use_literal_keys.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } - if let Some(rule) = self.use_simple_number_keys.as_ref() { + if let Some(rule) = self.use_naming_convention.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } } + if let Some(rule) = self.use_simple_number_keys.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -2409,10 +2432,10 @@ impl Nursery { pub(crate) fn is_recommended_rule(rule_name: &str) -> bool { Self::RECOMMENDED_RULES.contains(&rule_name) } - pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 17] { + pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 18] { Self::RECOMMENDED_RULES_AS_FILTERS } - pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 31] { Self::ALL_RULES_AS_FILTERS } + pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 32] { Self::ALL_RULES_AS_FILTERS } #[doc = r" Select preset rules"] pub(crate) fn collect_preset_rules( &self, @@ -2451,6 +2474,7 @@ impl Nursery { "noRedundantRoles" => self.no_redundant_roles.as_ref(), "noSelfAssign" => self.no_self_assign.as_ref(), "noStaticOnlyClass" => self.no_static_only_class.as_ref(), + "noUselessEmptyExport" => self.no_useless_empty_export.as_ref(), "noVoid" => self.no_void.as_ref(), "useAriaPropTypes" => self.use_aria_prop_types.as_ref(), "useArrowFunction" => self.use_arrow_function.as_ref(), diff --git a/crates/rome_service/src/configuration/parse/json/rules.rs b/crates/rome_service/src/configuration/parse/json/rules.rs index 2ae8527e5e6..f00bbc93979 100644 --- a/crates/rome_service/src/configuration/parse/json/rules.rs +++ b/crates/rome_service/src/configuration/parse/json/rules.rs @@ -1662,6 +1662,7 @@ impl VisitNode for Nursery { "noRedundantRoles", "noSelfAssign", "noStaticOnlyClass", + "noUselessEmptyExport", "noVoid", "useAriaPropTypes", "useArrowFunction", @@ -2108,6 +2109,29 @@ impl VisitNode for Nursery { )); } }, + "noUselessEmptyExport" => match value { + AnyJsonValue::JsonStringValue(_) => { + let mut configuration = RuleConfiguration::default(); + self.map_to_known_string(&value, name_text, &mut configuration, diagnostics)?; + self.no_useless_empty_export = Some(configuration); + } + AnyJsonValue::JsonObjectValue(_) => { + let mut rule_configuration = RuleConfiguration::default(); + rule_configuration.map_rule_configuration( + &value, + name_text, + "noUselessEmptyExport", + diagnostics, + )?; + self.no_useless_empty_export = Some(rule_configuration); + } + _ => { + diagnostics.push(DeserializationDiagnostic::new_incorrect_type( + "object or string", + value.range(), + )); + } + }, "noVoid" => match value { AnyJsonValue::JsonStringValue(_) => { let mut configuration = RuleConfiguration::default(); diff --git a/editors/vscode/configuration_schema.json b/editors/vscode/configuration_schema.json index 471f2f0899a..e224d3017d1 100644 --- a/editors/vscode/configuration_schema.json +++ b/editors/vscode/configuration_schema.json @@ -880,6 +880,13 @@ { "type": "null" } ] }, + "noUselessEmptyExport": { + "description": "Disallow empty exports that don't change anything in a module file.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noVoid": { "description": "Disallow the use of void operators, which is not a familiar operator.", "anyOf": [ diff --git a/npm/backend-jsonrpc/src/workspace.ts b/npm/backend-jsonrpc/src/workspace.ts index 07da99cac66..8a654fb3ef3 100644 --- a/npm/backend-jsonrpc/src/workspace.ts +++ b/npm/backend-jsonrpc/src/workspace.ts @@ -577,6 +577,10 @@ export interface Nursery { * This rule reports when a class has no non-static members, such as for a class used exclusively as a static namespace. */ noStaticOnlyClass?: RuleConfiguration; + /** + * Disallow empty exports that don't change anything in a module file. + */ + noUselessEmptyExport?: RuleConfiguration; /** * Disallow the use of void operators, which is not a familiar operator. */ @@ -1146,6 +1150,7 @@ export type Category = | "lint/nursery/noRedundantRoles" | "lint/nursery/noSelfAssign" | "lint/nursery/noStaticOnlyClass" + | "lint/nursery/noUselessEmptyExport" | "lint/nursery/noVoid" | "lint/nursery/useAriaPropTypes" | "lint/nursery/useArrowFunction" diff --git a/npm/rome/configuration_schema.json b/npm/rome/configuration_schema.json index 471f2f0899a..e224d3017d1 100644 --- a/npm/rome/configuration_schema.json +++ b/npm/rome/configuration_schema.json @@ -880,6 +880,13 @@ { "type": "null" } ] }, + "noUselessEmptyExport": { + "description": "Disallow empty exports that don't change anything in a module file.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noVoid": { "description": "Disallow the use of void operators, which is not a familiar operator.", "anyOf": [ diff --git a/website/src/components/generated/NumberOfRules.astro b/website/src/components/generated/NumberOfRules.astro index 86103f15a71..8859b1cc2ef 100644 --- a/website/src/components/generated/NumberOfRules.astro +++ b/website/src/components/generated/NumberOfRules.astro @@ -1,2 +1,2 @@ -

Rome's linter has a total of 150 rules

\ No newline at end of file +

Rome's linter has a total of 151 rules

\ No newline at end of file diff --git a/website/src/frontend-scripts/mobile.ts b/website/src/frontend-scripts/mobile.ts index a3a1cfbadd3..21f1bb877d1 100644 --- a/website/src/frontend-scripts/mobile.ts +++ b/website/src/frontend-scripts/mobile.ts @@ -1,6 +1,3 @@ -// Yes TS I am a module. -export {}; - export let isMobile = false; window.addEventListener("DOMContentLoaded", () => { const mobileMatchMedia = matchMedia("(max-width: 768px)"); diff --git a/website/src/pages/lint/rules/index.mdx b/website/src/pages/lint/rules/index.mdx index 74b9ae99ee7..86d4b045130 100644 --- a/website/src/pages/lint/rules/index.mdx +++ b/website/src/pages/lint/rules/index.mdx @@ -1005,6 +1005,12 @@ Disallow assignments where both sides are exactly the same. This rule reports when a class has no non-static members, such as for a class used exclusively as a static namespace.

+

+ noUselessEmptyExport +

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

noVoid

diff --git a/website/src/pages/lint/rules/noUselessEmptyExport.md b/website/src/pages/lint/rules/noUselessEmptyExport.md new file mode 100644 index 00000000000..7a6df1b68e0 --- /dev/null +++ b/website/src/pages/lint/rules/noUselessEmptyExport.md @@ -0,0 +1,94 @@ +--- +title: Lint Rule noUselessEmptyExport +parent: lint/rules/index +--- + +# noUselessEmptyExport (since vnext) + +Disallow empty exports that don't change anything in a module file. + +An empty `export {}` is sometimes useful to turn a file that would otherwise be a script into a module. +Per the [TypeScript Handbook Modules page](https://www.typescriptlang.org/docs/handbook/modules.html): + +>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. + + +However, an `export {}` statement does nothing if there are any other top-level import or export in the file. + +Source: https://typescript-eslint.io/rules/no-useless-empty-export/ + +## Examples + +### Invalid + +```jsx +import { A } from "module"; +export {}; +``` + +
nursery/noUselessEmptyExport.js:2:1 lint/nursery/noUselessEmptyExport  FIXABLE  ━━━━━━━━━━━━━━━━━━━━
+
+   This empty export is useless because there's another export or import.
+  
+    1 │ import { A } from "module";
+  > 2 │ export {};
+   ^^^^^^^^^^
+    3 │ 
+  
+   This import makes useless the empty export.
+  
+  > 1 │ import { A } from "module";
+   ^^^^^^
+    2 │ export {};
+    3 │ 
+  
+   Safe fix: Remove this useless empty export.
+  
+    1 1  import { A } from "module";
+    2  - export·{};
+    3 2  
+  
+
+ +```jsx +export const A = 0; +export {}; +``` + +
nursery/noUselessEmptyExport.js:2:1 lint/nursery/noUselessEmptyExport  FIXABLE  ━━━━━━━━━━━━━━━━━━━━
+
+   This empty export is useless because there's another export or import.
+  
+    1 │ export const A = 0;
+  > 2 │ export {};
+   ^^^^^^^^^^
+    3 │ 
+  
+   This export makes useless the empty export.
+  
+  > 1 │ export const A = 0;
+   ^^^^^^
+    2 │ export {};
+    3 │ 
+  
+   Safe fix: Remove this useless empty export.
+  
+    1 1  export const A = 0;
+    2  - export·{};
+    3 2  
+  
+
+ +## Valid + +```jsx +export {}; +``` + +## Related links + +- [Disable a rule](/linter/#disable-a-lint-rule) +- [Rule options](/linter/#rule-options)