Skip to content

Commit a799982

Browse files
authored
feat(linter/consistent-type-specifier-style): add fixer for top-level style config (#13023)
fixes: #12964
1 parent 2d287d0 commit a799982

File tree

1 file changed

+161
-11
lines changed

1 file changed

+161
-11
lines changed

crates/oxc_linter/src/rules/import/consistent_type_specifier_style.rs

Lines changed: 161 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
use oxc_ast::{AstKind, ast::ImportDeclarationSpecifier};
1+
use oxc_allocator::{Allocator, CloneIn};
2+
use oxc_ast::{
3+
AstBuilder, AstKind,
4+
ast::{ImportDeclaration, ImportDeclarationSpecifier, ImportOrExportKind},
5+
};
6+
use oxc_codegen::{Context, Gen};
27
use oxc_diagnostics::OxcDiagnostic;
38
use oxc_macros::declare_oxc_lint;
4-
use oxc_span::{GetSpan, Span};
9+
use oxc_span::{GetSpan, SPAN, Span};
510
use serde_json::Value;
611

7-
use crate::{AstNode, context::LintContext, rule::Rule};
12+
use crate::{AstNode, context::LintContext, fixer::RuleFixer, rule::Rule};
813

914
fn consistent_type_specifier_style_diagnostic(span: Span, mode: &Mode) -> OxcDiagnostic {
1015
let (warn_msg, help_msg) = if *mode == Mode::PreferInline {
@@ -101,14 +106,32 @@ impl Rule for ConsistentTypeSpecifierStyle {
101106
return;
102107
}
103108
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-
}
109+
let (value_specifiers, type_specifiers) = split_import_specifiers_by_kind(specifiers);
110+
if type_specifiers.is_empty() {
111+
return;
112+
}
113+
114+
for item in &type_specifiers {
115+
ctx.diagnostic_with_fix(
116+
consistent_type_specifier_style_diagnostic(item.span(), &self.mode),
117+
|fixer| {
118+
let mut import_source = String::new();
119+
120+
if !value_specifiers.is_empty() {
121+
let value_import_declaration =
122+
gen_value_import_declaration(fixer, import_decl, &value_specifiers);
123+
import_source.push_str(&value_import_declaration);
124+
}
125+
126+
let type_import_declaration =
127+
gen_type_import_declaration(fixer, import_decl, &type_specifiers);
128+
import_source.push_str(&type_import_declaration);
129+
130+
fixer
131+
.replace(import_decl.span, import_source.trim_end().to_string())
132+
.with_message("Convert to a `top-level` type import")
133+
},
134+
);
112135
}
113136
}
114137
if self.mode == Mode::PreferInline && import_decl.import_kind.is_type() {
@@ -139,6 +162,93 @@ impl Rule for ConsistentTypeSpecifierStyle {
139162
}
140163
}
141164

165+
fn split_import_specifiers_by_kind<'a, I>(specifiers: I) -> (Vec<I::Item>, Vec<I::Item>)
166+
where
167+
I: IntoIterator<Item = &'a ImportDeclarationSpecifier<'a>>,
168+
{
169+
let (value_kind_specifiers, type_kind_specifiers) = specifiers.into_iter().fold(
170+
(vec![], vec![]),
171+
|(mut value_kind_specifiers, mut type_kind_specifiers), it| {
172+
match it {
173+
ImportDeclarationSpecifier::ImportDefaultSpecifier(_) => {
174+
value_kind_specifiers.push(it);
175+
}
176+
ImportDeclarationSpecifier::ImportSpecifier(specifier) => {
177+
if specifier.import_kind.is_value() {
178+
value_kind_specifiers.push(it);
179+
} else {
180+
type_kind_specifiers.push(it);
181+
}
182+
}
183+
ImportDeclarationSpecifier::ImportNamespaceSpecifier(_) => {}
184+
}
185+
(value_kind_specifiers, type_kind_specifiers)
186+
},
187+
);
188+
(value_kind_specifiers, type_kind_specifiers)
189+
}
190+
191+
fn gen_value_import_declaration<'c, 'a: 'c>(
192+
fixer: RuleFixer<'c, 'a>,
193+
import_decl: &'a ImportDeclaration<'a>,
194+
specifiers: &Vec<&'a ImportDeclarationSpecifier<'a>>,
195+
) -> String {
196+
let mut codegen = fixer.codegen();
197+
198+
let alloc = Allocator::default();
199+
let ast_builder = AstBuilder::new(&alloc);
200+
201+
let specifiers: Vec<_> = specifiers.iter().map(|it| it.clone_in(&alloc)).collect();
202+
let import_declaration = ast_builder.alloc_import_declaration(
203+
SPAN,
204+
Some(oxc_allocator::Vec::from_iter_in(specifiers, &alloc)),
205+
import_decl.source.clone_in(&alloc),
206+
None,
207+
import_decl.with_clause.clone_in(&alloc),
208+
ImportOrExportKind::Value,
209+
);
210+
211+
import_declaration.print(&mut codegen, Context::empty());
212+
codegen.into_source_text()
213+
}
214+
215+
fn gen_type_import_declaration<'c, 'a: 'c>(
216+
fixer: RuleFixer<'c, 'a>,
217+
import_decl: &'a ImportDeclaration<'a>,
218+
specifiers: &Vec<&'a ImportDeclarationSpecifier<'a>>,
219+
) -> String {
220+
let mut codegen = fixer.codegen();
221+
222+
let alloc = Allocator::default();
223+
let ast_builder = AstBuilder::new(&alloc);
224+
225+
let specifiers: Vec<_> = specifiers
226+
.iter()
227+
.filter_map(|it| match it {
228+
ImportDeclarationSpecifier::ImportSpecifier(specifier) => {
229+
Some(ast_builder.import_declaration_specifier_import_specifier(
230+
SPAN,
231+
specifier.imported.clone_in(&alloc),
232+
specifier.local.clone_in(&alloc),
233+
ImportOrExportKind::Value,
234+
))
235+
}
236+
_ => None,
237+
})
238+
.collect();
239+
let import_declaration = ast_builder.alloc_import_declaration(
240+
SPAN,
241+
Some(oxc_allocator::Vec::from_iter_in(specifiers, &alloc)),
242+
import_decl.source.clone_in(&alloc),
243+
None,
244+
import_decl.with_clause.clone_in(&alloc),
245+
ImportOrExportKind::Type,
246+
);
247+
248+
import_declaration.print(&mut codegen, Context::empty());
249+
codegen.into_source_text()
250+
}
251+
142252
#[test]
143253
fn test() {
144254
use crate::tester::Tester;
@@ -218,6 +328,46 @@ fn test() {
218328
",
219329
Some(json!(["prefer-inline"])),
220330
),
331+
(
332+
"import { type Foo } from 'Foo';",
333+
"import type { Foo } from 'Foo';",
334+
Some(json!(["prefer-top-level"])),
335+
),
336+
(
337+
"import { type Foo as Bar } from 'Foo';",
338+
"import type { Foo as Bar } from 'Foo';",
339+
Some(json!(["prefer-top-level"])),
340+
),
341+
(
342+
"import { type Foo, type Bar } from 'Foo';",
343+
"import type { Foo, Bar } from 'Foo';",
344+
Some(json!(["prefer-top-level"])),
345+
),
346+
(
347+
"import { type Foo, type Bar } from 'Foo';",
348+
"import type { Foo, Bar } from 'Foo';",
349+
Some(json!(["prefer-top-level"])),
350+
),
351+
(
352+
"import { Foo, type Bar } from 'Foo';",
353+
"import { Foo } from 'Foo';\nimport type { Bar } from 'Foo';",
354+
Some(json!(["prefer-top-level"])),
355+
),
356+
(
357+
"import { type Foo, Bar } from 'Foo';",
358+
"import { Bar } from 'Foo';\nimport type { Foo } from 'Foo';",
359+
Some(json!(["prefer-top-level"])),
360+
),
361+
(
362+
"import Foo, { type Bar } from 'Foo';",
363+
"import Foo from 'Foo';\nimport type { Bar } from 'Foo';",
364+
Some(json!(["prefer-top-level"])),
365+
),
366+
(
367+
"import Foo, { type Bar, Baz } from 'Foo';",
368+
"import Foo, { Baz } from 'Foo';\nimport type { Bar } from 'Foo';",
369+
Some(json!(["prefer-top-level"])),
370+
),
221371
];
222372

223373
Tester::new(

0 commit comments

Comments
 (0)