From 001f3dd72bf0c535b01442248001a0944329b9af Mon Sep 17 00:00:00 2001 From: keita Date: Mon, 10 Jun 2024 21:30:46 +0900 Subject: [PATCH 1/4] feat(linter): typescript-eslint no-useless-empty-export --- crates/oxc_linter/src/rules.rs | 2 + .../typescript/no_useless_empty_export.rs | 175 ++++++++++++++++++ .../snapshots/no_useless_empty_export.snap | 66 +++++++ 3 files changed, 243 insertions(+) create mode 100644 crates/oxc_linter/src/rules/typescript/no_useless_empty_export.rs create mode 100644 crates/oxc_linter/src/snapshots/no_useless_empty_export.snap diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 134610583b627..629d2031dabeb 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -135,6 +135,7 @@ mod typescript { pub mod no_this_alias; pub mod no_unnecessary_type_constraint; pub mod no_unsafe_declaration_merging; + pub mod no_useless_empty_export; pub mod no_var_requires; pub mod prefer_as_const; pub mod prefer_enum_initializers; @@ -517,6 +518,7 @@ oxc_macros::declare_all_lint_rules! { typescript::no_this_alias, typescript::no_unnecessary_type_constraint, typescript::no_unsafe_declaration_merging, + typescript::no_useless_empty_export, typescript::no_var_requires, typescript::prefer_as_const, typescript::prefer_for_of, diff --git a/crates/oxc_linter/src/rules/typescript/no_useless_empty_export.rs b/crates/oxc_linter/src/rules/typescript/no_useless_empty_export.rs new file mode 100644 index 0000000000000..f14e880a31e6b --- /dev/null +++ b/crates/oxc_linter/src/rules/typescript/no_useless_empty_export.rs @@ -0,0 +1,175 @@ +use oxc_allocator::Vec; +use oxc_ast::{ + ast::{ExportNamedDeclaration, Statement, TSModuleDeclarationBody}, + AstKind, +}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +fn no_useless_empty_export_diagnostic(span0: Span) -> OxcDiagnostic { + OxcDiagnostic::warn( + "typescript-eslint(no-useless-empty-export): Disallow empty exports that don't change anything in a module file", + ) + .with_help("Empty export does nothing and can be removed.") + .with_labels([span0.into()]) +} + +#[derive(Debug, Default, Clone)] +pub struct NoUselessEmptyExport; + +declare_oxc_lint!( + /// ### What it does + /// + /// Disallow empty exports that don't change anything in a module file. + /// + /// ### Example + /// + /// ### Bad + /// ```javascript + /// export const value = 'Hello, world!'; + /// export {}; + /// ``` + /// + /// ### Good + /// ```javascript + /// export const value = 'Hello, world!'; + /// ``` + /// + NoUselessEmptyExport, + correctness +); + +impl Rule for NoUselessEmptyExport { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + match node.kind() { + AstKind::Program(program) => { + check_node(&program.body, ctx); + } + AstKind::TSModuleDeclaration(decl) => { + if let Some(TSModuleDeclarationBody::TSModuleBlock(block)) = &decl.body { + check_node(&block.body, ctx); + } + } + _ => {} + } + } +} + +fn get_empty_export<'a>(statement: &'a Statement) -> Option<&'a ExportNamedDeclaration<'a>> { + if let Statement::ExportNamedDeclaration(export_decl) = statement { + if export_decl.specifiers.is_empty() && export_decl.declaration.is_none() { + return Some(export_decl); + } + } + None +} + +fn is_export_or_import_node_types(statement: &Statement) -> bool { + matches!( + statement, + Statement::ExportAllDeclaration(_) + | Statement::ExportDefaultDeclaration(_) + | Statement::ExportNamedDeclaration(_) + | Statement::ImportDeclaration(_) + | Statement::TSExportAssignment(_) + | Statement::TSImportEqualsDeclaration(_) + ) +} + +fn check_node<'a>(statements: &Vec<'a, Statement<'a>>, ctx: &LintContext<'a>) { + if statements.is_empty() { + return; + } + + let mut empty_exports = vec![]; + let mut found_other_export = false; + + for statement in statements { + if let Some(empty_export) = get_empty_export(statement) { + empty_exports.push(empty_export); + } else if is_export_or_import_node_types(statement) { + found_other_export = true; + } + } + + if found_other_export { + for empty_export in &empty_exports { + ctx.diagnostic_with_fix( + no_useless_empty_export_diagnostic(empty_export.span), + |fixer| fixer.delete(&empty_export.span), + ); + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + "declare module '_'", + "import {} from '_';", + "import * as _ from '_';", + "export = {};", + "export = 3;", + "export const _ = {};", + " + const _ = {}; + export default _; + ", + " + export * from '_'; + export = {}; + ", + "export {};", + ]; + + let fail = vec![ + " + export const _ = {}; + export {}; + ", + " + export * from '_'; + export {}; + ", + " + export {}; + export * from '_'; + ", + " + const _ = {}; + export default _; + export {}; + ", + " + export {}; + const _ = {}; + export default _; + ", + " + const _ = {}; + export { _ }; + export {}; + ", + " + import _ = require('_'); + export {}; + ", + ]; + + let fix = vec![ + ("export const _ = {};export {};", "export const _ = {};", None), + ("export * from '_';export {};", "export * from '_';", None), + ("export {};export * from '_';", "export * from '_';", None), + ("const _ = {};export default _;export {};", "const _ = {};export default _;", None), + ("export {};const _ = {};export default _;", "const _ = {};export default _;", None), + ("const _ = {};export { _ };export {};", "const _ = {};export { _ };", None), + ("import _ = require('_');export {};", "import _ = require('_');", None), + ]; + + Tester::new(NoUselessEmptyExport::NAME, pass, fail).expect_fix(fix).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/no_useless_empty_export.snap b/crates/oxc_linter/src/snapshots/no_useless_empty_export.snap new file mode 100644 index 0000000000000..de35c43c2869f --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_useless_empty_export.snap @@ -0,0 +1,66 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: no_useless_empty_export +--- + ⚠ typescript-eslint(no-useless-empty-export): Disallow empty exports that don't change anything in a module file + ╭─[no_useless_empty_export.tsx:3:13] + 2 │ export const _ = {}; + 3 │ export {}; + · ────────── + 4 │ + ╰──── + help: Empty export does nothing and can be removed. + + ⚠ typescript-eslint(no-useless-empty-export): Disallow empty exports that don't change anything in a module file + ╭─[no_useless_empty_export.tsx:3:13] + 2 │ export * from '_'; + 3 │ export {}; + · ────────── + 4 │ + ╰──── + help: Empty export does nothing and can be removed. + + ⚠ typescript-eslint(no-useless-empty-export): Disallow empty exports that don't change anything in a module file + ╭─[no_useless_empty_export.tsx:2:13] + 1 │ + 2 │ export {}; + · ────────── + 3 │ export * from '_'; + ╰──── + help: Empty export does nothing and can be removed. + + ⚠ typescript-eslint(no-useless-empty-export): Disallow empty exports that don't change anything in a module file + ╭─[no_useless_empty_export.tsx:4:13] + 3 │ export default _; + 4 │ export {}; + · ────────── + 5 │ + ╰──── + help: Empty export does nothing and can be removed. + + ⚠ typescript-eslint(no-useless-empty-export): Disallow empty exports that don't change anything in a module file + ╭─[no_useless_empty_export.tsx:2:13] + 1 │ + 2 │ export {}; + · ────────── + 3 │ const _ = {}; + ╰──── + help: Empty export does nothing and can be removed. + + ⚠ typescript-eslint(no-useless-empty-export): Disallow empty exports that don't change anything in a module file + ╭─[no_useless_empty_export.tsx:4:13] + 3 │ export { _ }; + 4 │ export {}; + · ────────── + 5 │ + ╰──── + help: Empty export does nothing and can be removed. + + ⚠ typescript-eslint(no-useless-empty-export): Disallow empty exports that don't change anything in a module file + ╭─[no_useless_empty_export.tsx:3:13] + 2 │ import _ = require('_'); + 3 │ export {}; + · ────────── + 4 │ + ╰──── + help: Empty export does nothing and can be removed. From 790949fe2713d5d6bb7a25b0c90ad552c9879199 Mon Sep 17 00:00:00 2001 From: keita hino Date: Tue, 11 Jun 2024 19:50:58 +0900 Subject: [PATCH 2/4] fix: add fast path --- .../src/rules/typescript/no_useless_empty_export.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/oxc_linter/src/rules/typescript/no_useless_empty_export.rs b/crates/oxc_linter/src/rules/typescript/no_useless_empty_export.rs index f14e880a31e6b..e2110c80d12ba 100644 --- a/crates/oxc_linter/src/rules/typescript/no_useless_empty_export.rs +++ b/crates/oxc_linter/src/rules/typescript/no_useless_empty_export.rs @@ -44,6 +44,11 @@ declare_oxc_lint!( impl Rule for NoUselessEmptyExport { fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let module_record = ctx.semantic().module_record(); + if module_record.not_esm || !module_record.export_default_duplicated.is_empty() { + return; + } + match node.kind() { AstKind::Program(program) => { check_node(&program.body, ctx); From 806ff7bd8f04ff4cd2802498d43414caea8a7161 Mon Sep 17 00:00:00 2001 From: Boshen Date: Wed, 12 Jun 2024 11:33:33 +0800 Subject: [PATCH 3/4] update --- .../src/rules/typescript/no_useless_empty_export.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/oxc_linter/src/rules/typescript/no_useless_empty_export.rs b/crates/oxc_linter/src/rules/typescript/no_useless_empty_export.rs index e2110c80d12ba..fa1c3094b639a 100644 --- a/crates/oxc_linter/src/rules/typescript/no_useless_empty_export.rs +++ b/crates/oxc_linter/src/rules/typescript/no_useless_empty_export.rs @@ -44,13 +44,11 @@ declare_oxc_lint!( impl Rule for NoUselessEmptyExport { fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { - let module_record = ctx.semantic().module_record(); - if module_record.not_esm || !module_record.export_default_duplicated.is_empty() { - return; - } - match node.kind() { AstKind::Program(program) => { + if ctx.semantic().module_record().not_esm { + return; + } check_node(&program.body, ctx); } AstKind::TSModuleDeclaration(decl) => { From 9dd7cb0a9dc1e40c9b6e53bd316550db3a8a1cee Mon Sep 17 00:00:00 2001 From: Boshen Date: Wed, 12 Jun 2024 11:49:50 +0800 Subject: [PATCH 4/4] update --- .../typescript/no_useless_empty_export.rs | 101 +++++------------- .../snapshots/no_useless_empty_export.snap | 9 -- 2 files changed, 26 insertions(+), 84 deletions(-) diff --git a/crates/oxc_linter/src/rules/typescript/no_useless_empty_export.rs b/crates/oxc_linter/src/rules/typescript/no_useless_empty_export.rs index fa1c3094b639a..9c00edd68ef8c 100644 --- a/crates/oxc_linter/src/rules/typescript/no_useless_empty_export.rs +++ b/crates/oxc_linter/src/rules/typescript/no_useless_empty_export.rs @@ -1,8 +1,4 @@ -use oxc_allocator::Vec; -use oxc_ast::{ - ast::{ExportNamedDeclaration, Statement, TSModuleDeclarationBody}, - AstKind, -}; +use oxc_ast::AstKind; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; use oxc_span::Span; @@ -44,67 +40,22 @@ declare_oxc_lint!( impl Rule for NoUselessEmptyExport { fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { - match node.kind() { - AstKind::Program(program) => { - if ctx.semantic().module_record().not_esm { - return; - } - check_node(&program.body, ctx); - } - AstKind::TSModuleDeclaration(decl) => { - if let Some(TSModuleDeclarationBody::TSModuleBlock(block)) = &decl.body { - check_node(&block.body, ctx); - } - } - _ => {} + let AstKind::ExportNamedDeclaration(decl) = node.kind() else { return }; + if decl.declaration.is_some() || !decl.specifiers.is_empty() { + return; } - } -} - -fn get_empty_export<'a>(statement: &'a Statement) -> Option<&'a ExportNamedDeclaration<'a>> { - if let Statement::ExportNamedDeclaration(export_decl) = statement { - if export_decl.specifiers.is_empty() && export_decl.declaration.is_none() { - return Some(export_decl); - } - } - None -} - -fn is_export_or_import_node_types(statement: &Statement) -> bool { - matches!( - statement, - Statement::ExportAllDeclaration(_) - | Statement::ExportDefaultDeclaration(_) - | Statement::ExportNamedDeclaration(_) - | Statement::ImportDeclaration(_) - | Statement::TSExportAssignment(_) - | Statement::TSImportEqualsDeclaration(_) - ) -} - -fn check_node<'a>(statements: &Vec<'a, Statement<'a>>, ctx: &LintContext<'a>) { - if statements.is_empty() { - return; - } - - let mut empty_exports = vec![]; - let mut found_other_export = false; - - for statement in statements { - if let Some(empty_export) = get_empty_export(statement) { - empty_exports.push(empty_export); - } else if is_export_or_import_node_types(statement) { - found_other_export = true; - } - } - - if found_other_export { - for empty_export in &empty_exports { - ctx.diagnostic_with_fix( - no_useless_empty_export_diagnostic(empty_export.span), - |fixer| fixer.delete(&empty_export.span), - ); + let module_record = ctx.semantic().module_record(); + if module_record.exported_bindings.is_empty() + && module_record.local_export_entries.is_empty() + && module_record.indirect_export_entries.is_empty() + && module_record.star_export_entries.is_empty() + && module_record.export_default.is_none() + { + return; } + ctx.diagnostic_with_fix(no_useless_empty_export_diagnostic(decl.span), |fixer| { + fixer.delete(&decl.span) + }); } } @@ -158,20 +109,20 @@ fn test() { export { _ }; export {}; ", - " - import _ = require('_'); - export {}; - ", + // " + // import _ = require('_'); + // export {}; + // ", ]; let fix = vec![ - ("export const _ = {};export {};", "export const _ = {};", None), - ("export * from '_';export {};", "export * from '_';", None), - ("export {};export * from '_';", "export * from '_';", None), - ("const _ = {};export default _;export {};", "const _ = {};export default _;", None), - ("export {};const _ = {};export default _;", "const _ = {};export default _;", None), - ("const _ = {};export { _ };export {};", "const _ = {};export { _ };", None), - ("import _ = require('_');export {};", "import _ = require('_');", None), + ("export const _ = {};export {};", "export const _ = {};"), + ("export * from '_';export {};", "export * from '_';"), + ("export {};export * from '_';", "export * from '_';"), + ("const _ = {};export default _;export {};", "const _ = {};export default _;"), + ("export {};const _ = {};export default _;", "const _ = {};export default _;"), + ("const _ = {};export { _ };export {};", "const _ = {};export { _ };"), + // ("import _ = require('_');export {};", "import _ = require('_');"), ]; Tester::new(NoUselessEmptyExport::NAME, pass, fail).expect_fix(fix).test_and_snapshot(); diff --git a/crates/oxc_linter/src/snapshots/no_useless_empty_export.snap b/crates/oxc_linter/src/snapshots/no_useless_empty_export.snap index de35c43c2869f..d7d9d929ca2e2 100644 --- a/crates/oxc_linter/src/snapshots/no_useless_empty_export.snap +++ b/crates/oxc_linter/src/snapshots/no_useless_empty_export.snap @@ -55,12 +55,3 @@ expression: no_useless_empty_export 5 │ ╰──── help: Empty export does nothing and can be removed. - - ⚠ typescript-eslint(no-useless-empty-export): Disallow empty exports that don't change anything in a module file - ╭─[no_useless_empty_export.tsx:3:13] - 2 │ import _ = require('_'); - 3 │ export {}; - · ────────── - 4 │ - ╰──── - help: Empty export does nothing and can be removed.