Skip to content

Commit 7f450fc

Browse files

File tree

4 files changed

+355
-0
lines changed

4 files changed

+355
-0
lines changed

crates/oxc_linter/src/generated/rule_runner_impls.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2834,6 +2834,13 @@ impl RuleRunner for crate::rules::unicorn::require_array_join_separator::Require
28342834
Some(&AstTypesBitset::from_types(&[AstType::CallExpression]));
28352835
}
28362836

2837+
impl RuleRunner for crate::rules::unicorn::require_module_specifiers::RequireModuleSpecifiers {
2838+
const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[
2839+
AstType::ExportNamedDeclaration,
2840+
AstType::ImportDeclaration,
2841+
]));
2842+
}
2843+
28372844
impl RuleRunner for crate::rules::unicorn::require_number_to_fixed_digits_argument::RequireNumberToFixedDigitsArgument {
28382845
const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[AstType::CallExpression]));
28392846
}

crates/oxc_linter/src/rules.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,7 @@ pub(crate) mod unicorn {
493493
pub mod prefer_top_level_await;
494494
pub mod prefer_type_error;
495495
pub mod require_array_join_separator;
496+
pub mod require_module_specifiers;
496497
pub mod require_number_to_fixed_digits_argument;
497498
pub mod require_post_message_target_origin;
498499
pub mod switch_case_braces;
@@ -1227,6 +1228,7 @@ oxc_macros::declare_all_lint_rules! {
12271228
unicorn::prefer_string_trim_start_end,
12281229
unicorn::prefer_structured_clone,
12291230
unicorn::prefer_type_error,
1231+
unicorn::require_module_specifiers,
12301232
unicorn::require_post_message_target_origin,
12311233
unicorn::require_array_join_separator,
12321234
unicorn::require_number_to_fixed_digits_argument,
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
use oxc_ast::{
2+
AstKind,
3+
ast::{ExportNamedDeclaration, ImportDeclaration, ImportDeclarationSpecifier},
4+
};
5+
use oxc_diagnostics::OxcDiagnostic;
6+
use oxc_macros::declare_oxc_lint;
7+
use oxc_span::Span;
8+
9+
use crate::{
10+
AstNode,
11+
context::LintContext,
12+
fixer::{RuleFix, RuleFixer},
13+
rule::Rule,
14+
};
15+
16+
fn require_module_specifiers_diagnostic(span: Span, statement_type: &str) -> OxcDiagnostic {
17+
OxcDiagnostic::warn(format!("Empty {statement_type} specifier is not allowed"))
18+
.with_help("Remove empty braces")
19+
.with_label(span)
20+
}
21+
22+
#[derive(Debug, Default, Clone)]
23+
pub struct RequireModuleSpecifiers;
24+
25+
declare_oxc_lint!(
26+
/// ### What it does
27+
///
28+
/// Enforce non-empty specifier list in `import` and `export` statements.
29+
///
30+
/// ### Why is this bad?
31+
///
32+
/// Empty import/export specifiers add no value and can be confusing.
33+
/// If you want to import a module for side effects, use `import 'module'` instead.
34+
///
35+
/// ### Examples
36+
///
37+
/// Examples of **incorrect** code for this rule:
38+
/// ```js
39+
/// import {} from 'foo';
40+
/// import foo, {} from 'foo';
41+
/// export {} from 'foo';
42+
/// export {};
43+
/// ```
44+
///
45+
/// Examples of **correct** code for this rule:
46+
/// ```js
47+
/// import 'foo';
48+
/// import foo from 'foo';
49+
/// ```
50+
RequireModuleSpecifiers,
51+
unicorn,
52+
suspicious,
53+
fix
54+
);
55+
56+
impl Rule for RequireModuleSpecifiers {
57+
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
58+
match node.kind() {
59+
AstKind::ImportDeclaration(import_decl) => {
60+
let Some(span) = find_empty_braces_in_import(ctx, import_decl) else {
61+
return;
62+
};
63+
ctx.diagnostic_with_fix(
64+
require_module_specifiers_diagnostic(span, "import"),
65+
|fixer| fix_import(fixer, import_decl),
66+
);
67+
}
68+
AstKind::ExportNamedDeclaration(export_decl) => {
69+
if export_decl.declaration.is_none() && export_decl.specifiers.is_empty() {
70+
let span =
71+
find_empty_braces_in_export(ctx, export_decl).unwrap_or(export_decl.span);
72+
ctx.diagnostic_with_fix(
73+
require_module_specifiers_diagnostic(span, "export"),
74+
|fixer| fix_export(fixer, export_decl),
75+
);
76+
}
77+
}
78+
_ => {}
79+
}
80+
}
81+
}
82+
83+
/// Finds empty braces `{}` in the given text and returns their span
84+
fn find_empty_braces_in_text(text: &str, base_span: Span) -> Option<Span> {
85+
let open_brace = text.find('{')?;
86+
let close_brace = text[open_brace + 1..].find('}')?;
87+
88+
// Check if braces contain only whitespace
89+
if !text[open_brace + 1..open_brace + 1 + close_brace].trim().is_empty() {
90+
return None;
91+
}
92+
93+
// Calculate absolute positions
94+
let start = base_span.start + u32::try_from(open_brace).ok()?;
95+
let end = start + u32::try_from(close_brace + 2).ok()?; // +2 to span from '{' to position after '}'
96+
Some(Span::new(start, end))
97+
}
98+
99+
fn find_empty_braces_in_import(
100+
ctx: &LintContext<'_>,
101+
import_decl: &ImportDeclaration<'_>,
102+
) -> Option<Span> {
103+
// Side-effect imports don't have specifiers
104+
let specifiers = import_decl.specifiers.as_ref()?;
105+
106+
// Check for patterns that could have empty braces
107+
let could_have_empty_braces = matches!(
108+
specifiers.as_slice(),
109+
[] | [ImportDeclarationSpecifier::ImportDefaultSpecifier(_)]
110+
);
111+
112+
if !could_have_empty_braces {
113+
return None;
114+
}
115+
116+
let import_text = ctx.source_range(import_decl.span);
117+
find_empty_braces_in_text(import_text, import_decl.span)
118+
}
119+
120+
fn find_empty_braces_in_export(
121+
ctx: &LintContext<'_>,
122+
export_decl: &ExportNamedDeclaration<'_>,
123+
) -> Option<Span> {
124+
let export_text = ctx.source_range(export_decl.span);
125+
find_empty_braces_in_text(export_text, export_decl.span)
126+
}
127+
128+
fn fix_import<'a>(fixer: RuleFixer<'_, 'a>, import_decl: &ImportDeclaration<'a>) -> RuleFix<'a> {
129+
let import_text = fixer.source_range(import_decl.span);
130+
131+
let Some(comma_pos) = import_text.find(',') else {
132+
return fixer.noop();
133+
};
134+
let Some(from_pos) = import_text[comma_pos..].find("from") else {
135+
return fixer.noop();
136+
};
137+
138+
// Remove empty braces: "import foo, {} from 'bar'" -> "import foo from 'bar'"
139+
let default_part = &import_text[..comma_pos];
140+
let from_part = &import_text[comma_pos + from_pos..];
141+
fixer.replace(import_decl.span, format!("{default_part} {from_part}"))
142+
}
143+
144+
fn fix_export<'a>(
145+
fixer: RuleFixer<'_, 'a>,
146+
export_decl: &ExportNamedDeclaration<'a>,
147+
) -> RuleFix<'a> {
148+
if export_decl.source.is_some() {
149+
return fixer.noop();
150+
}
151+
152+
// Remove the entire `export {}` statement
153+
fixer.delete(&export_decl.span)
154+
}
155+
156+
#[test]
157+
fn test() {
158+
use crate::tester::Tester;
159+
160+
let pass = vec![
161+
r#"import "foo""#,
162+
r#"import foo from "foo""#,
163+
r#"import * as foo from "foo""#,
164+
r#"import {foo} from "foo""#,
165+
r#"import foo,{bar} from "foo""#,
166+
r#"import type foo from "foo""#,
167+
r#"import type foo,{bar} from "foo""#,
168+
r#"import foo,{type bar} from "foo""#,
169+
"const foo = 1;
170+
export {foo};",
171+
r#"export {foo} from "foo""#,
172+
r#"export * as foo from "foo""#,
173+
r"export type {Foo}",
174+
r"export type foo = Foo",
175+
r#"export type {foo} from "foo""#,
176+
r#"export type * as foo from "foo""#,
177+
"export const foo = 1",
178+
"export function foo() {}",
179+
"export class foo {}",
180+
"export const {} = foo",
181+
"export const [] = foo",
182+
];
183+
184+
let fail = vec![
185+
r#"import {} from "foo";"#,
186+
r#"import{}from"foo";"#,
187+
r#"import {
188+
} from "foo";"#,
189+
r#"import foo, {} from "foo";"#,
190+
r#"import foo,{}from "foo";"#,
191+
r#"import foo, {
192+
} from "foo";"#,
193+
r#"import foo,{}/* comment */from "foo";"#,
194+
r#"import type {} from "foo""#,
195+
r#"import type{}from"foo""#,
196+
r#"import type foo, {} from "foo""#,
197+
r#"import type foo,{}from "foo""#,
198+
"export {}",
199+
r#"export {} from "foo";"#,
200+
r#"export{}from"foo";"#,
201+
r#"export {
202+
} from "foo";"#,
203+
r#"export {} from "foo" with {type: "json"};"#,
204+
r"export type{}",
205+
r#"export type {} from "foo""#,
206+
];
207+
208+
let fix = vec![
209+
(r#"import foo, {} from "foo";"#, r#"import foo from "foo";"#),
210+
(r#"import foo,{} from "foo";"#, r#"import foo from "foo";"#),
211+
("export {}", ""),
212+
("export {};", ""),
213+
];
214+
215+
Tester::new(RequireModuleSpecifiers::NAME, RequireModuleSpecifiers::PLUGIN, pass, fail)
216+
.expect_fix(fix)
217+
.test_and_snapshot();
218+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
---
2+
source: crates/oxc_linter/src/tester.rs
3+
---
4+
eslint-plugin-unicorn(require-module-specifiers): Empty import specifier is not allowed
5+
╭─[require_module_specifiers.tsx:1:8]
6+
1import {} from "foo";
7+
· ──
8+
╰────
9+
help: Remove empty braces
10+
11+
eslint-plugin-unicorn(require-module-specifiers): Empty import specifier is not allowed
12+
╭─[require_module_specifiers.tsx:1:7]
13+
1import{}from"foo";
14+
· ──
15+
╰────
16+
help: Remove empty braces
17+
18+
eslint-plugin-unicorn(require-module-specifiers): Empty import specifier is not allowed
19+
╭─[require_module_specifiers.tsx:1:8]
20+
1 │ ╭─▶ import {
21+
2 │ ╰─▶ } from "foo";
22+
╰────
23+
help: Remove empty braces
24+
25+
eslint-plugin-unicorn(require-module-specifiers): Empty import specifier is not allowed
26+
╭─[require_module_specifiers.tsx:1:13]
27+
1import foo, {} from "foo";
28+
· ──
29+
╰────
30+
help: Remove empty braces
31+
32+
eslint-plugin-unicorn(require-module-specifiers): Empty import specifier is not allowed
33+
╭─[require_module_specifiers.tsx:1:12]
34+
1import foo,{}from "foo";
35+
· ──
36+
╰────
37+
help: Remove empty braces
38+
39+
eslint-plugin-unicorn(require-module-specifiers): Empty import specifier is not allowed
40+
╭─[require_module_specifiers.tsx:1:13]
41+
1 │ ╭─▶ import foo, {
42+
2 │ ╰─▶ } from "foo";
43+
╰────
44+
help: Remove empty braces
45+
46+
eslint-plugin-unicorn(require-module-specifiers): Empty import specifier is not allowed
47+
╭─[require_module_specifiers.tsx:1:12]
48+
1import foo,{}/* comment */from "foo";
49+
· ──
50+
╰────
51+
help: Remove empty braces
52+
53+
eslint-plugin-unicorn(require-module-specifiers): Empty import specifier is not allowed
54+
╭─[require_module_specifiers.tsx:1:13]
55+
1import type {} from "foo"
56+
· ──
57+
╰────
58+
help: Remove empty braces
59+
60+
eslint-plugin-unicorn(require-module-specifiers): Empty import specifier is not allowed
61+
╭─[require_module_specifiers.tsx:1:12]
62+
1import type{}from"foo"
63+
· ──
64+
╰────
65+
help: Remove empty braces
66+
67+
eslint-plugin-unicorn(require-module-specifiers): Empty import specifier is not allowed
68+
╭─[require_module_specifiers.tsx:1:18]
69+
1import type foo, {} from "foo"
70+
· ──
71+
╰────
72+
help: Remove empty braces
73+
74+
eslint-plugin-unicorn(require-module-specifiers): Empty import specifier is not allowed
75+
╭─[require_module_specifiers.tsx:1:17]
76+
1import type foo,{}from "foo"
77+
· ──
78+
╰────
79+
help: Remove empty braces
80+
81+
eslint-plugin-unicorn(require-module-specifiers): Empty export specifier is not allowed
82+
╭─[require_module_specifiers.tsx:1:8]
83+
1export {}
84+
· ──
85+
╰────
86+
help: Remove empty braces
87+
88+
eslint-plugin-unicorn(require-module-specifiers): Empty export specifier is not allowed
89+
╭─[require_module_specifiers.tsx:1:8]
90+
1export {} from "foo";
91+
· ──
92+
╰────
93+
help: Remove empty braces
94+
95+
eslint-plugin-unicorn(require-module-specifiers): Empty export specifier is not allowed
96+
╭─[require_module_specifiers.tsx:1:7]
97+
1export{}from"foo";
98+
· ──
99+
╰────
100+
help: Remove empty braces
101+
102+
eslint-plugin-unicorn(require-module-specifiers): Empty export specifier is not allowed
103+
╭─[require_module_specifiers.tsx:1:8]
104+
1 │ ╭─▶ export {
105+
2 │ ╰─▶ } from "foo";
106+
╰────
107+
help: Remove empty braces
108+
109+
eslint-plugin-unicorn(require-module-specifiers): Empty export specifier is not allowed
110+
╭─[require_module_specifiers.tsx:1:8]
111+
1export {} from "foo" with {type: "json"};
112+
· ──
113+
╰────
114+
help: Remove empty braces
115+
116+
eslint-plugin-unicorn(require-module-specifiers): Empty export specifier is not allowed
117+
╭─[require_module_specifiers.tsx:1:12]
118+
1export type{}
119+
· ──
120+
╰────
121+
help: Remove empty braces
122+
123+
eslint-plugin-unicorn(require-module-specifiers): Empty export specifier is not allowed
124+
╭─[require_module_specifiers.tsx:1:13]
125+
1export type {} from "foo"
126+
· ──
127+
╰────
128+
help: Remove empty braces

0 commit comments

Comments
 (0)