|
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}; |
2 | 7 | use oxc_diagnostics::OxcDiagnostic; |
3 | 8 | use oxc_macros::declare_oxc_lint; |
4 | | -use oxc_span::{GetSpan, Span}; |
| 9 | +use oxc_span::{GetSpan, SPAN, Span}; |
5 | 10 | use serde_json::Value; |
6 | 11 |
|
7 | | -use crate::{AstNode, context::LintContext, rule::Rule}; |
| 12 | +use crate::{AstNode, context::LintContext, fixer::RuleFixer, rule::Rule}; |
8 | 13 |
|
9 | 14 | fn consistent_type_specifier_style_diagnostic(span: Span, mode: &Mode) -> OxcDiagnostic { |
10 | 15 | let (warn_msg, help_msg) = if *mode == Mode::PreferInline { |
@@ -101,14 +106,32 @@ impl Rule for ConsistentTypeSpecifierStyle { |
101 | 106 | return; |
102 | 107 | } |
103 | 108 | 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 | + ); |
112 | 135 | } |
113 | 136 | } |
114 | 137 | if self.mode == Mode::PreferInline && import_decl.import_kind.is_type() { |
@@ -139,6 +162,93 @@ impl Rule for ConsistentTypeSpecifierStyle { |
139 | 162 | } |
140 | 163 | } |
141 | 164 |
|
| 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 | + |
142 | 252 | #[test] |
143 | 253 | fn test() { |
144 | 254 | use crate::tester::Tester; |
@@ -218,6 +328,46 @@ fn test() { |
218 | 328 | ", |
219 | 329 | Some(json!(["prefer-inline"])), |
220 | 330 | ), |
| 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 | + ), |
221 | 371 | ]; |
222 | 372 |
|
223 | 373 | Tester::new( |
|
0 commit comments