Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod import {
pub mod exports_last;
pub mod no_absolute_path;
pub mod no_anonymous_default_export;
pub mod no_empty_named_blocks;
pub mod no_mutable_exports;
// pub mod no_deprecated;
// pub mod no_unused_modules;
Expand Down Expand Up @@ -701,6 +702,7 @@ oxc_macros::declare_all_lint_rules! {
import::export,
import::exports_last,
import::first,
import::no_empty_named_blocks,
import::no_anonymous_default_export,
import::no_absolute_path,
import::no_mutable_exports,
Expand Down
130 changes: 130 additions & 0 deletions crates/oxc_linter/src/rules/import/no_empty_named_blocks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
use oxc_ast::AstKind;
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;

use crate::{AstNode, context::LintContext, rule::Rule};

fn no_empty_named_blocks_diagnostic(span: Span) -> OxcDiagnostic {
// See <https://oxc.rs/docs/contribute/linter/adding-rules.html#diagnostics> for details
OxcDiagnostic::warn("Unexpected empty named import block.").with_label(span)
}

#[derive(Debug, Default, Clone)]
pub struct NoEmptyNamedBlocks;

declare_oxc_lint!(
/// ### What it does
///
/// Enforces that named import blocks are not empty
///
/// ### Why is this bad?
///
/// Empty named imports serve no practical purpose and
/// often result from accidental deletions or tool-generated code.
///
/// ### Examples
///
/// Examples of **incorrect** code for this rule:
/// ```js
/// import {} from 'mod'
/// import Default, {} from 'mod'
/// ```
///
/// Examples of **correct** code for this rule:
/// ```js
/// import { mod } from 'mod'
/// import Default, { mod } from 'mod'
/// ```
NoEmptyNamedBlocks,
import,
suspicious,
fix
);

impl Rule for NoEmptyNamedBlocks {
#[expect(clippy::cast_possible_truncation)]
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
let AstKind::ImportDeclaration(import_decl) = node.kind() else {
return;
};
// if "import 'foo'"
let Some(specifiers) = import_decl.specifiers.as_ref() else {
return;
};
if specifiers.is_empty() {
// for "import {} from 'mod'"
ctx.diagnostic_with_fix(no_empty_named_blocks_diagnostic(import_decl.span), |fixer| {
fixer.delete_range(import_decl.span)
});
}

let source_token_str = Span::new(import_decl.span.start, import_decl.source.span.start - 1)
.source_text(ctx.source_text());
// find is there anything between '{' and '}'
if let Some(start) = source_token_str.find('{') {
if let Some(end) = source_token_str[start..].find('}') {
let between_braces = &source_token_str[start + 1..start + end];
if between_braces.trim().is_empty() {
ctx.diagnostic_with_fix(
no_empty_named_blocks_diagnostic(import_decl.span),
|fixer| {
// "import a, {} from 'mod" => "import a from 'mod'"
// we just remove the space between ',' and '}'
if let Some(comma_idx) = source_token_str[..start].rfind(',') {
let remove_start = comma_idx as u32;
let remove_end = (start + end + 1) as u32;
fixer.delete_range(Span::new(remove_start, remove_end))
} else {
fixer.noop()
}
},
);
}
}
}
}
}

#[test]
fn test() {
use crate::tester::Tester;

let pass = vec![
"import 'mod'",
"import { mod } from 'mod'",
"import Default, { mod } from 'mod'",
"import { Named } from 'mod'",
"import type { Named } from 'mod'",
"import type Default, { Named } from 'mod'",
"import type * as Namespace from 'mod'",
"import * as Namespace from 'mod'",
];

let fail = vec![
"import {} from 'mod'",
"import Default, {} from 'mod'",
"import{}from'mod'",
"import type {}from'mod'",
"import type {} from 'mod'",
"import type{}from 'mod'",
];

let fix = vec![
("import Default, {} from 'mod'", "import Default from 'mod'", None),
("import { } from 'mod'", "", None),
("import a, {} from 'mod'", "import a from 'mod'", None),
("import a, { } from 'mod'", "import a from 'mod'", None),
("import a, { } from 'mod'", "import a from 'mod'", None),
("import a, { } from 'mod'", "import a from 'mod'", None),
("import a, { } from'mod'", "import a from'mod'", None),
("import type a, { } from'mod'", "import type a from'mod'", None),
("import a,{} from 'mod'", "import a from 'mod'", None),
("import type a,{} from 'foo'", "import type a from 'foo'", None),
("import type {} from 'foo'", "", None),
];

Tester::new(NoEmptyNamedBlocks::NAME, NoEmptyNamedBlocks::PLUGIN, pass, fail)
.expect_fix(fix)
.test_and_snapshot();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
source: crates/oxc_linter/src/tester.rs
---
⚠ eslint-plugin-import(no-empty-named-blocks): Unexpected empty named import block.
╭─[no_empty_named_blocks.tsx:1:1]
1 │ import {} from 'mod'
· ────────────────────
╰────
help: Delete this code.

⚠ eslint-plugin-import(no-empty-named-blocks): Unexpected empty named import block.
╭─[no_empty_named_blocks.tsx:1:1]
1 │ import {} from 'mod'
· ────────────────────
╰────

⚠ eslint-plugin-import(no-empty-named-blocks): Unexpected empty named import block.
╭─[no_empty_named_blocks.tsx:1:1]
1 │ import Default, {} from 'mod'
· ─────────────────────────────
╰────
help: Delete this code.

⚠ eslint-plugin-import(no-empty-named-blocks): Unexpected empty named import block.
╭─[no_empty_named_blocks.tsx:1:1]
1 │ import{}from'mod'
· ─────────────────
╰────
help: Delete this code.

⚠ eslint-plugin-import(no-empty-named-blocks): Unexpected empty named import block.
╭─[no_empty_named_blocks.tsx:1:1]
1 │ import{}from'mod'
· ─────────────────
╰────

⚠ eslint-plugin-import(no-empty-named-blocks): Unexpected empty named import block.
╭─[no_empty_named_blocks.tsx:1:1]
1 │ import type {}from'mod'
· ───────────────────────
╰────
help: Delete this code.

⚠ eslint-plugin-import(no-empty-named-blocks): Unexpected empty named import block.
╭─[no_empty_named_blocks.tsx:1:1]
1 │ import type {}from'mod'
· ───────────────────────
╰────

⚠ eslint-plugin-import(no-empty-named-blocks): Unexpected empty named import block.
╭─[no_empty_named_blocks.tsx:1:1]
1 │ import type {} from 'mod'
· ─────────────────────────
╰────
help: Delete this code.

⚠ eslint-plugin-import(no-empty-named-blocks): Unexpected empty named import block.
╭─[no_empty_named_blocks.tsx:1:1]
1 │ import type {} from 'mod'
· ─────────────────────────
╰────

⚠ eslint-plugin-import(no-empty-named-blocks): Unexpected empty named import block.
╭─[no_empty_named_blocks.tsx:1:1]
1 │ import type{}from 'mod'
· ───────────────────────
╰────
help: Delete this code.

⚠ eslint-plugin-import(no-empty-named-blocks): Unexpected empty named import block.
╭─[no_empty_named_blocks.tsx:1:1]
1 │ import type{}from 'mod'
· ───────────────────────
╰────