Skip to content

Commit a064082

Browse files
feat(linter): add import/consistent-type-specifier-style rule (#10858)
Relates to #1117 Rule detail:https://github.com/import-js/eslint-plugin-import/blob/v2.31.0/docs/rules/consistent-type-specifier-style.md
1 parent faf0a95 commit a064082

File tree

3 files changed

+319
-0
lines changed

3 files changed

+319
-0
lines changed

crates/oxc_linter/src/rules.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
88
/// <https://github.com/import-js/eslint-plugin-import>
99
mod import {
10+
pub mod consistent_type_specifier_style;
1011
pub mod exports_last;
1112
pub mod group_exports;
1213
pub mod no_absolute_path;
@@ -707,6 +708,7 @@ oxc_macros::declare_all_lint_rules! {
707708
eslint::valid_typeof,
708709
eslint::vars_on_top,
709710
eslint::yoda,
711+
import::consistent_type_specifier_style,
710712
import::default,
711713
import::export,
712714
import::exports_last,
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
use oxc_ast::{AstKind, ast::ImportDeclarationSpecifier};
2+
use oxc_diagnostics::OxcDiagnostic;
3+
use oxc_macros::declare_oxc_lint;
4+
use oxc_span::{GetSpan, Span};
5+
use serde_json::Value;
6+
7+
use crate::{AstNode, context::LintContext, rule::Rule};
8+
9+
fn consistent_type_specifier_style_diagnostic(span: Span, mode: &Mode) -> OxcDiagnostic {
10+
let (warn_msg, help_msg) = if *mode == Mode::PreferInline {
11+
(
12+
"Prefer using inline type specifiers instead of a top-level type-only import.",
13+
"Replace top‐level import type with an inline type specifier.",
14+
)
15+
} else {
16+
(
17+
"Prefer using a top-level type-only import instead of inline type specifiers.",
18+
"Replace inline type specifiers with a top‐level import type statement.",
19+
)
20+
};
21+
OxcDiagnostic::warn(warn_msg).with_help(help_msg).with_label(span)
22+
}
23+
24+
#[derive(Debug, Default, PartialEq, Clone)]
25+
enum Mode {
26+
#[default]
27+
PreferTopLevel,
28+
PreferInline,
29+
}
30+
31+
impl Mode {
32+
pub fn from(raw: &str) -> Self {
33+
if raw == "prefer-inline" { Self::PreferInline } else { Self::PreferTopLevel }
34+
}
35+
}
36+
37+
#[derive(Debug, Default, Clone)]
38+
pub struct ConsistentTypeSpecifierStyle {
39+
mode: Mode,
40+
}
41+
42+
declare_oxc_lint!(
43+
/// ### What it does
44+
///
45+
/// This rule either enforces or bans the use of inline type-only markers for named imports.
46+
///
47+
/// ### Why is this bad?
48+
///
49+
/// Mixing top-level `import type { Foo } from 'foo'` with inline `{ type Bar }`
50+
/// forces readers to mentally switch contexts when scanning your imports.
51+
/// Enforcing one style makes it immediately obvious which imports are types and which are value imports.
52+
///
53+
/// ### Examples
54+
///
55+
/// Examples of incorrect code for the default `prefer-top-level` option:
56+
/// ```typescript
57+
/// import {type Foo} from 'Foo';
58+
/// import Foo, {type Bar} from 'Foo';
59+
/// ```
60+
///
61+
/// Examples of correct code for the default option:
62+
/// ```typescript
63+
/// import type {Foo} from 'Foo';
64+
/// import type Foo, {Bar} from 'Foo';
65+
/// ```
66+
///
67+
/// Examples of incorrect code for the `prefer-inline` option:
68+
/// ```typescript
69+
/// import type {Foo} from 'Foo';
70+
/// import type Foo, {Bar} from 'Foo';
71+
/// ```
72+
///
73+
/// Examples of correct code for the `prefer-inline` option:
74+
/// ```typescript
75+
/// import {type Foo} from 'Foo';
76+
/// import Foo, {type Bar} from 'Foo';
77+
/// ```
78+
ConsistentTypeSpecifierStyle,
79+
import,
80+
style,
81+
conditional_fix
82+
);
83+
84+
impl Rule for ConsistentTypeSpecifierStyle {
85+
fn from_configuration(value: Value) -> Self {
86+
Self { mode: value.get(0).and_then(Value::as_str).map(Mode::from).unwrap_or_default() }
87+
}
88+
#[expect(clippy::cast_possible_truncation)]
89+
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
90+
let AstKind::ImportDeclaration(import_decl) = node.kind() else {
91+
return;
92+
};
93+
let Some(specifiers) = &import_decl.specifiers else {
94+
return;
95+
};
96+
let len = specifiers.len();
97+
if len == 0
98+
|| (len == 1
99+
&& !matches!(specifiers[0], ImportDeclarationSpecifier::ImportSpecifier(_)))
100+
{
101+
return;
102+
}
103+
if self.mode == Mode::PreferTopLevel && import_decl.import_kind.is_value() {
104+
for item in specifiers {
105+
if matches!(item, ImportDeclarationSpecifier::ImportSpecifier(specifier) if specifier.import_kind.is_type())
106+
{
107+
ctx.diagnostic(consistent_type_specifier_style_diagnostic(
108+
item.span(),
109+
&self.mode,
110+
));
111+
}
112+
}
113+
}
114+
if self.mode == Mode::PreferInline && import_decl.import_kind.is_type() {
115+
ctx.diagnostic_with_fix(
116+
consistent_type_specifier_style_diagnostic(import_decl.span, &self.mode),
117+
|fixer| {
118+
let fixer = fixer.for_multifix();
119+
let mut rule_fixes = fixer.new_fix_with_capacity(len);
120+
for item in specifiers {
121+
rule_fixes.push(fixer.insert_text_before(item, "type "));
122+
}
123+
// find the 'type' keyword and remove it
124+
if let Some(type_token_span) = ctx
125+
.source_range(Span::new(import_decl.span.start, specifiers[0].span().start))
126+
.find("type")
127+
.map(|pos| {
128+
let start = import_decl.span.start + pos as u32;
129+
Span::new(start, start + 4)
130+
})
131+
{
132+
let remove_fix = fixer.delete_range(type_token_span);
133+
rule_fixes.push(remove_fix);
134+
}
135+
rule_fixes.with_message("Convert to an `inline` type import")
136+
},
137+
);
138+
}
139+
}
140+
}
141+
142+
#[test]
143+
fn test() {
144+
use crate::tester::Tester;
145+
use serde_json::json;
146+
147+
let pass = vec![
148+
("import Foo from 'Foo'", None),
149+
("import type Foo from 'Foo'", None),
150+
("import { Foo } from 'Foo';", None),
151+
("import { Foo as Bar } from 'Foo';", None),
152+
("import * as Foo from 'Foo';", None),
153+
("import 'Foo';", None),
154+
("import {} from 'Foo';", None),
155+
("import type {} from 'Foo';", None),
156+
("import type { Foo as Bar } from 'Foo';", Some(json!(["prefer-top-level"]))),
157+
("import type { Foo, Bar, Baz, Bam } from 'Foo';", Some(json!(["prefer-top-level"]))),
158+
("import type {Foo} from 'Foo'", Some(json!(["prefer-top-level"]))),
159+
("import {type Foo} from 'Foo'", Some(json!(["prefer-inline"]))),
160+
("import Foo from 'Foo';", Some(json!(["prefer-inline"]))),
161+
("import type Foo from 'Foo';", Some(json!(["prefer-inline"]))),
162+
("import { Foo } from 'Foo';", Some(json!(["prefer-inline"]))),
163+
("import { Foo as Bar } from 'Foo';", Some(json!(["prefer-inline"]))),
164+
("import * as Foo from 'Foo';", Some(json!(["prefer-inline"]))),
165+
("import 'Foo';", Some(json!(["prefer-inline"]))),
166+
("import {} from 'Foo';", Some(json!(["prefer-inline"]))),
167+
("import type {} from 'Foo';", Some(json!(["prefer-inline"]))),
168+
("import { type Foo } from 'Foo';", Some(json!(["prefer-inline"]))),
169+
("import { type Foo as Bar } from 'Foo';", Some(json!(["prefer-inline"]))),
170+
("import { type Foo, type Bar, Baz, Bam } from 'Foo';", Some(json!(["prefer-inline"]))),
171+
("import type * as Foo from 'Foo';", None),
172+
];
173+
174+
let fail = vec![
175+
("import { type Foo, type Bar } from 'Foo'", None),
176+
("import type { Foo } from 'Foo'", Some(json!(["prefer-inline"]))),
177+
("import { type Foo as Bar } from 'Foo';", None),
178+
("import { type Foo, type Bar } from 'Foo';", None),
179+
("import { Foo, type Bar } from 'Foo';", None),
180+
("import { type Foo, Bar } from 'Foo';", None),
181+
("import Foo, { type Bar } from 'Foo';", None),
182+
("import Foo, { type Bar, Baz } from 'Foo';", None),
183+
("import { Component, type ComponentProps } from 'package-1';", None),
184+
("import type { Foo, Bar, Baz } from 'Foo';", Some(json!(["prefer-inline"]))),
185+
];
186+
187+
let fix = vec![
188+
(
189+
"import type { foo, bar } from 'foo'",
190+
"import { type foo, type bar } from 'foo'",
191+
Some(json!(["prefer-inline"])),
192+
),
193+
(
194+
"import type{ foo } from 'foo'",
195+
"import { type foo } from 'foo'",
196+
Some(json!(["prefer-inline"])),
197+
),
198+
(
199+
"import type /** comment */{ foo } from 'foo'",
200+
"import /** comment */{ type foo } from 'foo'",
201+
Some(json!(["prefer-inline"])),
202+
),
203+
(
204+
"import type { foo, /** comments */ bar } from 'foo'",
205+
"import { type foo, /** comments */ type bar } from 'foo'",
206+
Some(json!(["prefer-inline"])),
207+
),
208+
(
209+
r"
210+
import type {
211+
bar,
212+
} from 'foo'
213+
",
214+
r"
215+
import {
216+
type bar,
217+
} from 'foo'
218+
",
219+
Some(json!(["prefer-inline"])),
220+
),
221+
];
222+
223+
Tester::new(
224+
ConsistentTypeSpecifierStyle::NAME,
225+
ConsistentTypeSpecifierStyle::PLUGIN,
226+
pass,
227+
fail,
228+
)
229+
.expect_fix(fix)
230+
.test_and_snapshot();
231+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
---
2+
source: crates/oxc_linter/src/tester.rs
3+
---
4+
eslint-plugin-import(consistent-type-specifier-style): Prefer using a top-level type-only import instead of inline type specifiers.
5+
╭─[consistent_type_specifier_style.tsx:1:10]
6+
1import { type Foo, type Bar } from 'Foo'
7+
· ────────
8+
╰────
9+
help: Replace inline type specifiers with a toplevel import type statement.
10+
11+
eslint-plugin-import(consistent-type-specifier-style): Prefer using a top-level type-only import instead of inline type specifiers.
12+
╭─[consistent_type_specifier_style.tsx:1:20]
13+
1import { type Foo, type Bar } from 'Foo'
14+
· ────────
15+
╰────
16+
help: Replace inline type specifiers with a toplevel import type statement.
17+
18+
eslint-plugin-import(consistent-type-specifier-style): Prefer using inline type specifiers instead of a top-level type-only import.
19+
╭─[consistent_type_specifier_style.tsx:1:1]
20+
1import type { Foo } from 'Foo'
21+
· ──────────────────────────────
22+
╰────
23+
help: Replace toplevel import type with an inline type specifier.
24+
25+
eslint-plugin-import(consistent-type-specifier-style): Prefer using a top-level type-only import instead of inline type specifiers.
26+
╭─[consistent_type_specifier_style.tsx:1:10]
27+
1import { type Foo as Bar } from 'Foo';
28+
· ───────────────
29+
╰────
30+
help: Replace inline type specifiers with a toplevel import type statement.
31+
32+
eslint-plugin-import(consistent-type-specifier-style): Prefer using a top-level type-only import instead of inline type specifiers.
33+
╭─[consistent_type_specifier_style.tsx:1:10]
34+
1import { type Foo, type Bar } from 'Foo';
35+
· ────────
36+
╰────
37+
help: Replace inline type specifiers with a toplevel import type statement.
38+
39+
eslint-plugin-import(consistent-type-specifier-style): Prefer using a top-level type-only import instead of inline type specifiers.
40+
╭─[consistent_type_specifier_style.tsx:1:20]
41+
1import { type Foo, type Bar } from 'Foo';
42+
· ────────
43+
╰────
44+
help: Replace inline type specifiers with a toplevel import type statement.
45+
46+
eslint-plugin-import(consistent-type-specifier-style): Prefer using a top-level type-only import instead of inline type specifiers.
47+
╭─[consistent_type_specifier_style.tsx:1:15]
48+
1import { Foo, type Bar } from 'Foo';
49+
· ────────
50+
╰────
51+
help: Replace inline type specifiers with a toplevel import type statement.
52+
53+
eslint-plugin-import(consistent-type-specifier-style): Prefer using a top-level type-only import instead of inline type specifiers.
54+
╭─[consistent_type_specifier_style.tsx:1:10]
55+
1import { type Foo, Bar } from 'Foo';
56+
· ────────
57+
╰────
58+
help: Replace inline type specifiers with a toplevel import type statement.
59+
60+
eslint-plugin-import(consistent-type-specifier-style): Prefer using a top-level type-only import instead of inline type specifiers.
61+
╭─[consistent_type_specifier_style.tsx:1:15]
62+
1import Foo, { type Bar } from 'Foo';
63+
· ────────
64+
╰────
65+
help: Replace inline type specifiers with a toplevel import type statement.
66+
67+
eslint-plugin-import(consistent-type-specifier-style): Prefer using a top-level type-only import instead of inline type specifiers.
68+
╭─[consistent_type_specifier_style.tsx:1:15]
69+
1import Foo, { type Bar, Baz } from 'Foo';
70+
· ────────
71+
╰────
72+
help: Replace inline type specifiers with a toplevel import type statement.
73+
74+
eslint-plugin-import(consistent-type-specifier-style): Prefer using a top-level type-only import instead of inline type specifiers.
75+
╭─[consistent_type_specifier_style.tsx:1:21]
76+
1import { Component, type ComponentProps } from 'package-1';
77+
· ───────────────────
78+
╰────
79+
help: Replace inline type specifiers with a toplevel import type statement.
80+
81+
eslint-plugin-import(consistent-type-specifier-style): Prefer using inline type specifiers instead of a top-level type-only import.
82+
╭─[consistent_type_specifier_style.tsx:1:1]
83+
1import type { Foo, Bar, Baz } from 'Foo';
84+
· ─────────────────────────────────────────
85+
╰────
86+
help: Replace toplevel import type with an inline type specifier.

0 commit comments

Comments
 (0)