diff --git a/crates/oxc_linter/src/fixer.rs b/crates/oxc_linter/src/fixer.rs index 40e242f004593..9c6fdf8439efd 100644 --- a/crates/oxc_linter/src/fixer.rs +++ b/crates/oxc_linter/src/fixer.rs @@ -148,6 +148,40 @@ impl<'c, 'a: 'c> RuleFixer<'c, 'a> { Fix::new(replacement, target) } + /// Creates a fix command that inserts text before the given node. + pub fn insert_text_before>>( + self, + target: &T, + text: S, + ) -> Fix<'a> { + self.insert_text_before_range(target.span(), text) + } + + /// Creates a fix command that inserts text before the specified range in the source text. + pub fn insert_text_before_range>>(self, span: Span, text: S) -> Fix<'a> { + self.insert_text_at(span.start, text) + } + + /// Creates a fix command that inserts text after the given node. + pub fn insert_text_after>>( + self, + target: &T, + text: S, + ) -> Fix<'a> { + self.insert_text_after_range(target.span(), text) + } + + /// Creates a fix command that inserts text after the specified range in the source text. + pub fn insert_text_after_range>>(self, span: Span, text: S) -> Fix<'a> { + self.insert_text_at(span.end, text) + } + + /// Creates a fix command that inserts text at the specified index in the source text. + #[allow(clippy::unused_self)] + fn insert_text_at>>(self, index: u32, text: S) -> Fix<'a> { + Fix::new(text, Span::new(index, index)) + } + #[allow(clippy::unused_self)] pub fn codegen(self) -> Codegen<'a, false> { Codegen::::new() diff --git a/crates/oxc_linter/src/rules/typescript/consistent_type_imports.rs b/crates/oxc_linter/src/rules/typescript/consistent_type_imports.rs index b0454c55616de..86c25344e0626 100644 --- a/crates/oxc_linter/src/rules/typescript/consistent_type_imports.rs +++ b/crates/oxc_linter/src/rules/typescript/consistent_type_imports.rs @@ -1,12 +1,24 @@ -use std::ops::Deref; +use std::{error::Error, ops::Deref}; -use oxc_ast::{ast::ImportDeclarationSpecifier, AstKind}; +use itertools::Itertools; +use oxc_ast::{ + ast::{ + ImportDeclaration, ImportDeclarationSpecifier, ImportDefaultSpecifier, + ImportNamespaceSpecifier, ImportSpecifier, Statement, + }, + AstKind, +}; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; use oxc_semantic::SymbolId; -use oxc_span::{CompactStr, Span}; +use oxc_span::{CompactStr, GetSpan, Span}; -use crate::{context::LintContext, rule::Rule, AstNode}; +use crate::{ + context::LintContext, + fixer::{Fix, RuleFixer}, + rule::Rule, + AstNode, +}; fn no_import_type_annotations_diagnostic(span: Span) -> OxcDiagnostic { OxcDiagnostic::warn( @@ -49,8 +61,6 @@ impl Deref for ConsistentTypeImports { #[derive(Default, Debug, Clone)] pub struct ConsistentTypeImportsConfig { disallow_type_annotations: DisallowTypeAnnotations, - // TODO: Remove - #[allow(unused)] fix_style: FixStyle, prefer: Prefer, } @@ -71,7 +81,7 @@ impl Default for DisallowTypeAnnotations { } } -#[derive(Default, Debug, Clone)] +#[derive(Default, Debug, Clone, Copy)] enum FixStyle { #[default] SeparateTypeImports, @@ -151,13 +161,31 @@ impl Rule for ConsistentTypeImports { // `import type { Foo } from 'foo'` AstKind::ImportDeclaration(import_decl) => { if import_decl.import_kind.is_type() { - ctx.diagnostic(avoid_import_type_diagnostic(import_decl.span)); + ctx.diagnostic_with_fix( + avoid_import_type_diagnostic(import_decl.span), + |fixer| { + fix_remove_type_specifier_from_import_declaration( + fixer, + import_decl.span, + ctx, + ) + }, + ); } } // import { type Foo } from 'foo' AstKind::ImportSpecifier(import_specifier) => { if import_specifier.import_kind.is_type() { - ctx.diagnostic(avoid_import_type_diagnostic(import_specifier.span)); + ctx.diagnostic_with_fix( + avoid_import_type_diagnostic(import_specifier.span), + |fixer| { + fix_remove_type_specifier_from_import_specifier( + fixer, + import_specifier.span, + ctx, + ) + }, + ); } } _ => {} @@ -206,22 +234,46 @@ impl Rule for ConsistentTypeImports { if import_decl.import_kind.is_value() && !type_references_without_type_qualifier.is_empty() { - // `import type {} from 'foo' assert { type: 'json' }` is invalid - // Import assertions cannot be used with type-only imports or exports. - if is_only_type_references && import_decl.with_clause.is_none() { - ctx.diagnostic(type_over_value_diagnostic(import_decl.span)); - return; - } - - let names = type_references_without_type_qualifier + let type_names = type_references_without_type_qualifier .iter() .map(|specifier| specifier.name()) .collect::>(); // ['foo', 'bar', 'baz' ] => "foo, bar, and baz". - let type_imports = format_word_list(&names); + let type_imports = format_word_list(&type_names); + let type_names = type_names.iter().map(CompactStr::as_str).collect::>(); + + let fixer_fn = |fixer: RuleFixer<'_, 'a>| { + let fix_options = FixOptions { + fixer, + import_decl, + type_names: &type_names, + fix_style: self.fix_style, + ctx, + }; + + match fix_to_type_import_declaration(&fix_options) { + Ok(fixes) => fixes, + Err(err) => { + debug_assert!(false, "Failed to fix: {err}"); + vec![] + } + } + }; + + if is_only_type_references && import_decl.import_kind.is_value() { + // `import type {} from 'foo' assert { type: 'json' }` is invalid + // Import assertions cannot be used with type-only imports or exports. + if import_decl.with_clause.is_none() { + ctx.diagnostic_with_fix(type_over_value_diagnostic(import_decl.span), fixer_fn); + } + return; + } - ctx.diagnostic(some_imports_are_only_types_diagnostic(import_decl.span, &type_imports)); + ctx.diagnostic_with_fix( + some_imports_are_only_types_diagnostic(import_decl.span, &type_imports), + fixer_fn, + ); } } } @@ -260,10 +312,605 @@ fn is_only_has_type_references(symbol_id: SymbolId, ctx: &LintContext) -> bool { peekable_iter.all(oxc_semantic::Reference::is_type) } +struct FixOptions<'a, 'b> { + fixer: RuleFixer<'b, 'a>, + import_decl: &'b ImportDeclaration<'a>, + type_names: &'b [&'b str], + fix_style: FixStyle, + ctx: &'b LintContext<'a>, +} + +type FixerResult = Result>; + +fn fixer_error, T>(message: S) -> FixerResult { + let s: String = message.into(); + Err(s.into()) +} + +// import { Foo, Bar } from 'foo' => import type { Foo, Bar } from 'foo' +#[allow(clippy::unnecessary_cast, clippy::cast_possible_truncation)] +fn fix_to_type_import_declaration<'a>(options: &FixOptions<'a, '_>) -> FixerResult>> { + let FixOptions { fixer, import_decl, type_names, fix_style, ctx } = options; + + let GroupedSpecifiers { namespace_specifier, named_specifiers, default_specifier } = + classify_specifier(import_decl); + + // import * as type from 'foo' + if namespace_specifier.is_some() + && default_specifier.is_none() + // checks for presence of import assertions + && import_decl.with_clause.is_none() + { + return fix_insert_type_specifier_for_import_declaration( + options, /* is_default_import */ false, + ); + } else if let Some(default_specifier) = default_specifier { + let default_specifier_is_type = + type_names.iter().any(|name| *name == default_specifier.local.name.as_str()); + // import Type from 'foo' + if default_specifier_is_type && named_specifiers.is_empty() && namespace_specifier.is_none() + { + return fix_insert_type_specifier_for_import_declaration( + options, /* is_default_import */ true, + ); + } else if matches!(fix_style, FixStyle::InlineTypeImports) + && !default_specifier_is_type + && !named_specifiers.is_empty() + && namespace_specifier.is_none() + { + // if there is a default specifier but it isn't a type specifier, then just add the inline type modifier to the named specifiers + // import AValue, {BValue, Type1, Type2} from 'foo' + return fix_inline_type_import_declaration(options); + } + } else if namespace_specifier.is_none() { + if matches!(fix_style, FixStyle::InlineTypeImports) + && named_specifiers + .iter() + .any(|specifier| type_names.iter().contains(&specifier.local.name.as_str())) + { + // import {AValue, Type1, Type2} from 'foo' + return fix_inline_type_import_declaration(options); + } else if named_specifiers + .iter() + .all(|specifier| type_names.iter().contains(&specifier.local.name.as_str())) + { + // import { Type1, Type2 } from 'foo' => import type { Type1, Type2 } from 'foo' + return fix_insert_type_specifier_for_import_declaration( + options, /* is_default_import */ false, + ); + } + } + let type_names_specifiers = named_specifiers + .iter() + .filter(|specifier| type_names.iter().contains(&specifier.local.name.as_str())) + .copied() + .collect::>(); + + let fixes_named_specifiers = + get_fixes_named_specifiers(options, &type_names_specifiers, &named_specifiers)?; + + let mut fixes = vec![]; + let mut after_fixes = vec![]; + + if !type_names_specifiers.is_empty() { + // definitely all type references: `import type { A, B } from 'foo'` + if let Some(type_only_named_import) = + get_type_only_named_import(ctx, import_decl.source.value.as_str()) + { + let new_options = FixOptions { + import_decl: type_only_named_import, + ctx, + fixer: options.fixer, + type_names, + fix_style: options.fix_style, + }; + let fix = fix_insert_named_specifiers_in_named_specifier_list( + &new_options, + &fixes_named_specifiers.type_named_specifiers_text, + )?; + if type_only_named_import.span.end <= import_decl.span.start { + fixes.push(fix); + } else { + after_fixes.push(fix); + } + } else { + // The import is both default and named. Insert named on new line because can't mix default type import and named type imports + if matches!(fix_style, FixStyle::InlineTypeImports) { + let text = format!( + "import {{{}}} from {};\n", + type_names_specifiers + .iter() + .map(|spec| { + let insert_text = ctx.source_range(spec.span()); + format!("type {insert_text}") + }) + .join(", "), + ctx.source_range(import_decl.source.span), + ); + fixes.push(fixer.insert_text_before(*import_decl, text)); + } else { + fixes.push(fixer.insert_text_before( + *import_decl, + format!( + "import type {{{}}} from {};\n", + fixes_named_specifiers.type_named_specifiers_text, + ctx.source_range(import_decl.source.span), + ), + )); + } + } + } + + let mut fixes_remove_type_namespace_specifier = vec![]; + + if let Some(namespace_specifier) = namespace_specifier { + if type_names.iter().contains(&namespace_specifier.local.name.as_str()) { + // import Foo, * as Type from 'foo' + // import DefType, * as Type from 'foo' + // import DefType, * as Type from 'foo' + let comma = try_find_char(ctx.source_range(import_decl.span), ',')?; + + // import Def, * as Ns from 'foo' + // ^^^^^^^^^ remove + fixes_remove_type_namespace_specifier.push(fixer.delete(&Span::new( + import_decl.span.start + comma, + namespace_specifier.span().end, + ))); + + // import type * as Ns from 'foo' + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ insert + fixes.push(fixer.insert_text_before( + *import_decl, + format!( + "import type {} from {};\n", + ctx.source_range(namespace_specifier.span()), + ctx.source_range(import_decl.source.span) + ), + )); + } + } + + if let Some(default_specifier) = default_specifier { + if type_names.iter().contains(&default_specifier.local.name.as_str()) { + if type_names.len() == import_decl.specifiers.as_ref().map_or(0, |s| s.len()) { + // import type Type from 'foo' + // ^^^^^ insert + fixes.push(fixer.insert_text_after( + &Span::new(import_decl.span().start, import_decl.span().start + 6), + " type", + )); + } else { + let import_text = ctx.source_range(import_decl.span); + // import Type, { Foo } from 'foo' + //let import_text = ctx.source_range(import_decl.span); + let comma = try_find_char(import_text, ',')?; + // import Type , { ... } from 'foo' + // ^^^^^ pick + let default_text = ctx.source_range(Span::new( + default_specifier.span.start, + import_decl.span().start + comma, + )); + fixes.push(fixer.insert_text_before( + *import_decl, + format!( + "import type {default_text} from {};\n", + ctx.source_range(import_decl.source.span) + ), + )); + // + 1 to skip the comma + if let Some(after_token) = + find_first_non_white_space(&import_text[(comma + 1) as usize..]) + { + let after_token = comma as usize + 1 + after_token.0; + fixes.push(fixer.delete_range(Span::new( + default_specifier.span.start, + import_decl.span().start + after_token as u32, + ))); + } + } + } + } + + fixes.extend(fixes_named_specifiers.remove_type_name_specifiers); + fixes.extend(fixes_remove_type_namespace_specifier); + fixes.extend(after_fixes); + Ok(fixes) +} + +fn fix_insert_named_specifiers_in_named_specifier_list<'a>( + options: &FixOptions<'a, '_>, + insert_text: &str, +) -> FixerResult> { + let FixOptions { fixer, import_decl, ctx, .. } = options; + let import_text = ctx.source_range(import_decl.span); + let close_brace = try_find_char(import_text, '}')?; + + let first_non_whitespace_before_close_brace = + import_text[..close_brace as usize].chars().rev().find(|c| c.is_whitespace()); + + let span = + Span::new(import_decl.span().start + close_brace, import_decl.span().start + close_brace); + if first_non_whitespace_before_close_brace.is_some_and(|ch| !matches!(ch, ',' | '{')) { + Ok(fixer.insert_text_before(&span, format!(",{insert_text}"))) + } else { + Ok(fixer.insert_text_before(&span, insert_text.to_string())) + } +} + +// Returns information for fixing named specifiers, type or value +#[derive(Default, Debug)] +struct FixNamedSpecifiers<'a> { + type_named_specifiers_text: String, + remove_type_name_specifiers: Vec>, +} + +// get the type-only named import declaration with same source +// import type { A } from 'foo' +fn get_type_only_named_import<'a>( + ctx: &LintContext<'a>, + source: &str, +) -> Option<&'a ImportDeclaration<'a>> { + let root = ctx.nodes().root_node()?; + let AstKind::Program(program) = root.kind() else { + return None; + }; + + for stmt in &program.body { + let Statement::ImportDeclaration(import_decl) = stmt else { + return None; + }; + if import_decl.import_kind.is_type() + && import_decl.source.value == source + && import_decl.specifiers.as_ref().is_some_and(|specs| { + specs + .iter() + .all(|spec| matches!(spec, ImportDeclarationSpecifier::ImportSpecifier(_))) + }) + { + return Some(import_decl); + } + } + + None +} + +fn get_fixes_named_specifiers<'a>( + options: &FixOptions<'a, '_>, + subset_named_specifiers: &[&ImportSpecifier<'a>], + all_named_specifiers: &[&ImportSpecifier<'a>], +) -> Result, Box> { + let FixOptions { fixer, import_decl, ctx, .. } = options; + + if all_named_specifiers.is_empty() { + return Ok(FixNamedSpecifiers::default()); + } + + let mut type_named_specifiers_text: Vec<&str> = vec![]; + let mut remove_type_named_specifiers: Vec = vec![]; + + if subset_named_specifiers.len() == all_named_specifiers.len() { + // import Foo, {Type1, Type2} from 'foo' + // import DefType, {Type1, Type2} from 'foo' + let import_text = ctx.source_range(import_decl.span); + let open_brace_token = try_find_char(import_text, '{')? as usize; + let Some(comma_token) = import_text[..open_brace_token].rfind(',') else { + return Err(format!("Missing comma token in import declaration: {import_text}").into()); + }; + let close_brace_token = try_find_char(import_text, '}')? as usize; + let open_brace_token_end = open_brace_token + 1; + let close_brace_token_end = close_brace_token + 1; + + // import DefType, {...} from 'foo' + // ^^^^^^ remove + remove_type_named_specifiers.push(fixer.delete(&Span::new( + import_decl.span.start + u32::try_from(comma_token).unwrap_or_default(), + import_decl.span.start + u32::try_from(close_brace_token_end).unwrap_or_default(), + ))); + + type_named_specifiers_text.push(&import_text[open_brace_token_end..close_brace_token]); + } else { + let mut named_specifier_groups = vec![]; + let mut group = vec![]; + + for specifier in all_named_specifiers { + let name = specifier.local.name.as_str(); + if subset_named_specifiers.iter().any(|s| s.local.name == name) { + group.push(*specifier); + } else if !group.is_empty() { + named_specifier_groups.push(group); + group = vec![]; + } + } + + if !group.is_empty() { + named_specifier_groups.push(group); + } + + for named_specifiers in named_specifier_groups { + let (remove_range, text_range) = + get_named_specifier_ranges(&named_specifiers, all_named_specifiers, options)?; + + remove_type_named_specifiers.push(fixer.delete(&remove_range)); + type_named_specifiers_text.push(ctx.source_range(text_range)); + } + } + + Ok(FixNamedSpecifiers { + type_named_specifiers_text: type_named_specifiers_text.join(","), + remove_type_name_specifiers: remove_type_named_specifiers, + }) +} + +fn get_named_specifier_ranges( + named_specifier_group: &[&ImportSpecifier], + all_named_specifiers: &[&ImportSpecifier], + options: &FixOptions<'_, '_>, +) -> FixerResult<(/* remove_range */ Span, /* text_range*/ Span)> { + let FixOptions { ctx, import_decl, .. } = options; + + // It will never empty, in `get_fixes_named_specifiers`, we have already checked every group is + // not empty. + if named_specifier_group.is_empty() { + return fixer_error("Named specifier group is empty"); + } + let first = named_specifier_group[0]; + let last = named_specifier_group[named_specifier_group.len() - 1]; + + let mut remove_range = Span::new(first.span().start, last.span().end); + let mut text_range = Span::new(first.span().start, last.span().end); + + let import_text = ctx.source_range(import_decl.span); + let open_brace_token_start = try_find_char(import_text, '{')?; + let open_brace_token_start = open_brace_token_start as usize; + if let Some(comma) = import_text + [open_brace_token_start..(first.span().start - import_decl.span().start) as usize] + .rfind(',') + { + // It's not the first specifier. + // import { Foo, Bar, Baz } from 'foo' + // ^ start + remove_range.start = import_decl.span().start + + u32::try_from(comma + open_brace_token_start).unwrap_or_default(); + + // Skip the comma + text_range.start = remove_range.start + 1; + } else { + // It's the first specifier. + // import { Foo, Bar, Baz } from 'foo' + // ^ start + remove_range.start = import_decl.span().start + + u32::try_from(open_brace_token_start + 1).unwrap_or_default(); + // skip `{` + text_range.start = remove_range.start; + } + + let is_first = all_named_specifiers + .first() + .is_some_and(|first_specifier| first_specifier.span() == first.span()); + + let is_last = all_named_specifiers + .last() + .is_some_and(|last_specifier| last_specifier.span() == last.span()); + + let after = find_first_non_white_space( + &import_text[(last.span().end - import_decl.span().start) as usize..], + ); + + if let Some((index, ch)) = after { + text_range.end = last.span().end + u32::try_from(index).unwrap_or_default(); + + if (is_first || is_last) && matches!(ch, ',') { + remove_range.end = text_range.end + 1; + } + } + + Ok((remove_range, text_range)) +} + +// Find the index of the first non-whitespace character in a string. +// return (utf8_index, char) +fn find_first_non_white_space(text: &str) -> Option<(usize, char)> { + let mut utf8_idx = 0; + for ch in text.chars() { + if !ch.is_whitespace() { + return Some((utf8_idx, ch)); + } + utf8_idx += ch.len_utf8(); + } + + None +} + +// Find the index of the first occurrence of a character in a string. +// When call this method, **make sure the `c` is in the text.** +// e.g. I know "{" is in the text when call this in ImportDeclaration which contains named +// specifiers. +fn try_find_char(text: &str, c: char) -> Result> { + let index = text.find(c); + if let Some(index) = index { + Ok(u32::try_from(index).unwrap_or_default()) + } else { + Err(format!("Missing character '{c}' in text: {text}").into()) + } +} + +fn fix_inline_type_import_declaration<'a>( + options: &FixOptions<'a, '_>, +) -> FixerResult>> { + let FixOptions { fixer, import_decl, type_names, ctx, .. } = options; + + let mut fixes = vec![]; + + let Some(specifiers) = &import_decl.specifiers else { + return fixer_error("Missing specifiers in import declaration"); + }; + + for specifier in specifiers { + if let ImportDeclarationSpecifier::ImportSpecifier(specifier) = specifier { + if type_names.iter().contains(&specifier.local.name.as_str()) { + fixes.push( + fixer.replace( + specifier.span, + format!("type {}", ctx.source_range(specifier.span)), + ), + ); + } + } + } + + Ok(fixes) +} + +fn fix_insert_type_specifier_for_import_declaration<'a>( + options: &FixOptions<'a, '_>, + is_default_import: bool, +) -> FixerResult>> { + let FixOptions { fixer, import_decl, ctx, .. } = options; + let import_source = ctx.source_range(import_decl.span); + let mut fixes = vec![]; + + // "import { Foo, Bar } from 'foo'" => "import type { Foo, Bar } from 'foo'" + // ^^^^ add + fixes.push( + fixer.replace(Span::new(import_decl.span.start, import_decl.span.start + 6), "import type"), + ); + + if is_default_import { + if let Ok(_opening_brace_token) = try_find_char(import_source, '{') { + // `import foo, {} from 'foo'` + // `import foo, { bar } from 'foo'` + let comma_token = try_find_char(import_source, ',')?; + let closing_brace_token = try_find_char(import_source, '}')?; + let base = import_decl.span.start; + // import foo, {} from 'foo' + // ^^^^ delete + fixes.push( + fixer.delete(&Span::new(base + comma_token, base + (closing_brace_token + 1))), + ); + if import_decl.specifiers.as_ref().is_some_and(|specifiers| specifiers.len() > 1) { + // import type {} from 'asdf' + let Some(specifiers_text) = + import_source.get(((comma_token + 1) as usize)..closing_brace_token as usize) + else { + return fixer_error(format!( + "Invalid slice for {}[{}..{}]", + import_source, + comma_token + 1, + closing_brace_token + )); + }; + + fixes.push(fixer.insert_text_after( + *import_decl, + format!( + "\nimport type {} from {}", + specifiers_text, + ctx.source_range(import_decl.source.span) + ), + )); + } + } + } + + if let Some(specifiers) = &import_decl.specifiers { + for specifier in specifiers { + if let ImportDeclarationSpecifier::ImportSpecifier(specifier) = specifier { + if specifier.import_kind.is_type() { + // import { type A } from 'foo.js' + // ^^^^^^^^ delete + fixes.push( + fixer.delete(&Span::new( + specifier.span.start, + specifier.imported.span().start, + )), + ); + } + } + } + } + + Ok(fixes) +} + +struct GroupedSpecifiers<'a, 'b> { + namespace_specifier: Option<&'b ImportNamespaceSpecifier<'a>>, + named_specifiers: Vec<&'b ImportSpecifier<'a>>, + default_specifier: Option<&'b ImportDefaultSpecifier<'a>>, +} + +fn classify_specifier<'a, 'b>(import_decl: &'b ImportDeclaration<'a>) -> GroupedSpecifiers<'a, 'b> { + let mut namespace_specifier = None; + let mut named_specifiers = vec![]; + let mut default_specifier = None; + + let Some(specifiers) = &import_decl.specifiers else { + return GroupedSpecifiers { namespace_specifier, named_specifiers, default_specifier }; + }; + + for specifier in specifiers { + match specifier { + ImportDeclarationSpecifier::ImportNamespaceSpecifier(namespace) => { + namespace_specifier = Some(namespace); + } + ImportDeclarationSpecifier::ImportSpecifier(named) => { + named_specifiers.push(named); + } + ImportDeclarationSpecifier::ImportDefaultSpecifier(default) => { + default_specifier = Some(default); + } + } + } + + GroupedSpecifiers { namespace_specifier, named_specifiers, default_specifier } +} + +// import type Foo from 'foo' +// ^^^^ remove +fn fix_remove_type_specifier_from_import_declaration<'a>( + fixer: RuleFixer<'_, 'a>, + import_decl_span: Span, + ctx: &LintContext<'a>, +) -> Fix<'a> { + let import_source = ctx.source_range(import_decl_span); + let new_import_source = import_source + // ` type Foo from 'foo'` + .strip_prefix("import") + // `type Foo from 'foo'` + .map(str::trim_start) + // `type Foo from 'foo'` + .and_then(|import_text| import_text.strip_prefix("type")) + // `import Foo from 'foo'` + .map(|import_text| format!("import{import_text}")); + + if let Some(new_import_source) = new_import_source { + fixer.replace(import_decl_span, new_import_source) + } else { + // when encountering an unexpected import declaration, do nothing. + fixer.replace(import_decl_span, import_source) + } +} + +// import { type Foo } from 'foo' +// ^^^^ remove +fn fix_remove_type_specifier_from_import_specifier<'a>( + fixer: RuleFixer<'_, 'a>, + specifier_span: Span, + ctx: &LintContext<'a>, +) -> Fix<'a> { + let specifier_source = ctx.source_range(specifier_span); + let new_specifier_source = specifier_source.strip_prefix("type "); + + fixer.replace(specifier_span, new_specifier_source.unwrap_or(specifier_source)) +} + #[test] fn test() { use crate::tester::Tester; + fn remove_prefix_space(str: &str) -> String { + str.lines().map(str::trim).collect::>().join("\n") + } + let pass = vec![ ( " @@ -473,7 +1120,7 @@ fn test() { const b = B; ", Some( - serde_json::json!([ { "prefer": "type-imports", "fixStyle": "inline-type-imports" }, ]), + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), ), ), ( @@ -484,7 +1131,7 @@ fn test() { const b = B; ", Some( - serde_json::json!([ { "prefer": "type-imports", "fixStyle": "inline-type-imports" }, ]), + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), ), ), ( @@ -495,7 +1142,7 @@ fn test() { const b = B; ", Some( - serde_json::json!([ { "prefer": "no-type-imports", "fixStyle": "inline-type-imports" }, ]), + serde_json::json!([{ "prefer": "no-type-imports", "fixStyle": "inline-type-imports" },]), ), ), ( @@ -895,7 +1542,7 @@ fn test() { let foo: Foo; ", Some( - serde_json::json!([ { "prefer": "type-imports", "fixStyle": "inline-type-imports" }, ]), + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), ), ), ( @@ -1273,7 +1920,7 @@ fn test() { let bar: B; ", Some( - serde_json::json!([ { "prefer": "type-imports", "fixStyle": "inline-type-imports" }, ]), + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), ), ), ( @@ -1284,7 +1931,7 @@ fn test() { B(); ", Some( - serde_json::json!([ { "prefer": "type-imports", "fixStyle": "inline-type-imports" }, ]), + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), ), ), ( @@ -1294,7 +1941,7 @@ fn test() { B(); ", Some( - serde_json::json!([ { "prefer": "type-imports", "fixStyle": "inline-type-imports" }, ]), + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), ), ), ( @@ -1305,7 +1952,7 @@ fn test() { type U = B; ", Some( - serde_json::json!([ { "prefer": "type-imports", "fixStyle": "inline-type-imports" }, ]), + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), ), ), ( @@ -1316,7 +1963,7 @@ fn test() { type U = B; ", Some( - serde_json::json!([ { "prefer": "type-imports", "fixStyle": "inline-type-imports" }, ]), + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), ), ), ( @@ -1327,7 +1974,7 @@ fn test() { A(); ", Some( - serde_json::json!([ { "prefer": "type-imports", "fixStyle": "inline-type-imports" }, ]), + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), ), ), ( @@ -1338,7 +1985,7 @@ fn test() { type V = A; ", Some( - serde_json::json!([ { "prefer": "type-imports", "fixStyle": "inline-type-imports" }, ]), + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), ), ), ( @@ -1349,7 +1996,7 @@ fn test() { type V = A; ", Some( - serde_json::json!([ { "prefer": "type-imports", "fixStyle": "inline-type-imports" }, ]), + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), ), ), ( @@ -1358,7 +2005,7 @@ fn test() { type T = A; ", Some( - serde_json::json!([ { "prefer": "type-imports", "fixStyle": "inline-type-imports" }, ]), + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), ), ), ( @@ -1367,7 +2014,7 @@ fn test() { type T = A; ", Some( - serde_json::json!([ { "prefer": "type-imports", "fixStyle": "inline-type-imports" }, ]), + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), ), ), ( @@ -1380,7 +2027,7 @@ fn test() { let baz: D; ", Some( - serde_json::json!([ { "prefer": "type-imports", "fixStyle": "inline-type-imports" }, ]), + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), ), ), ( @@ -1392,7 +2039,7 @@ fn test() { let baz: D; ", Some( - serde_json::json!([ { "prefer": "type-imports", "fixStyle": "inline-type-imports" }, ]), + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), ), ), ( @@ -1401,7 +2048,7 @@ fn test() { export = {} as A; ", Some( - serde_json::json!([ { "prefer": "type-imports", "fixStyle": "inline-type-imports" }, ]), + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), ), ), ( @@ -1410,7 +2057,7 @@ fn test() { export = {} as A; ", Some( - serde_json::json!([ { "prefer": "type-imports", "fixStyle": "inline-type-imports" }, ]), + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), ), ), // ( @@ -1532,1053 +2179,1096 @@ fn test() { // ), ]; - // let fix = vec![ - // ( - // " - // import Foo from 'foo'; - // let foo: Foo; - // type Bar = Foo; - // interface Baz { - // foo: Foo; - // } - // function fn(a: Foo): Foo {} - // ", - // " - // import type Foo from 'foo'; - // let foo: Foo; - // type Bar = Foo; - // interface Baz { - // foo: Foo; - // } - // function fn(a: Foo): Foo {} - // ", - // None, - // ), - // ( - // " - // import Foo from 'foo'; - // let foo: Foo; - // ", - // " - // import type Foo from 'foo'; - // let foo: Foo; - // ", - // None, - // ), - // ( - // " - // import Foo from 'foo'; - // let foo: Foo; - // ", - // " - // import type Foo from 'foo'; - // let foo: Foo; - // ", - // None, - // ), - // ( - // " - // import { A, B } from 'foo'; - // let foo: A; - // let bar: B; - // ", - // " - // import type { A, B } from 'foo'; - // let foo: A; - // let bar: B; - // ", - // None, - // ), - // ( - // " - // import { A as a, B as b } from 'foo'; - // let foo: a; - // let bar: b; - // ", - // " - // import type { A as a, B as b } from 'foo'; - // let foo: a; - // let bar: b; - // ", - // None, - // ), - // ( - // " - // import Foo from 'foo'; - // type Bar = typeof Foo; // TSTypeQuery - // ", - // " - // import type Foo from 'foo'; - // type Bar = typeof Foo; // TSTypeQuery - // ", - // None, - // ), - // ( - // " - // import foo from 'foo'; - // type Bar = foo.Bar; // TSQualifiedName - // ", - // " - // import type foo from 'foo'; - // type Bar = foo.Bar; // TSQualifiedName - // ", - // None, - // ), - // ( - // " - // import foo from 'foo'; - // type Baz = (typeof foo.bar)['Baz']; // TSQualifiedName & TSTypeQuery - // ", - // " - // import type foo from 'foo'; - // type Baz = (typeof foo.bar)['Baz']; // TSQualifiedName & TSTypeQuery - // ", - // None, - // ), - // ( - // " - // import * as A from 'foo'; - // let foo: A.Foo; - // ", - // " - // import type * as A from 'foo'; - // let foo: A.Foo; - // ", - // None, - // ), - // ( - // " - // import A, { B } from 'foo'; - // let foo: A; - // let bar: B; - // ", - // " - // import type { B } from 'foo'; - // import type A from 'foo'; - // let foo: A; - // let bar: B; - // ", - // None, - // ), - // ( - // " - // import A, {} from 'foo'; - // let foo: A; - // ", - // " - // import type A from 'foo'; - // let foo: A; - // ", - // None, - // ), - // ( - // " - // import { A, B } from 'foo'; - // const foo: A = B(); - // ", - // " - // import type { A} from 'foo'; - // import { B } from 'foo'; - // const foo: A = B(); - // ", - // None, - // ), - // ( - // " - // import { A, B, C } from 'foo'; - // const foo: A = B(); - // let bar: C; - // ", - // " - // import type { A, C } from 'foo'; - // import { B } from 'foo'; - // const foo: A = B(); - // let bar: C; - // ", - // None, - // ), - // ( - // " - // import { A, B, C, D } from 'foo'; - // const foo: A = B(); - // type T = { bar: C; baz: D }; - // ", - // " - // import type { A, C, D } from 'foo'; - // import { B } from 'foo'; - // const foo: A = B(); - // type T = { bar: C; baz: D }; - // ", - // None, - // ), - // ( - // " - // import A, { B, C, D } from 'foo'; - // B(); - // type T = { foo: A; bar: C; baz: D }; - // ", - // " - // import type { C, D } from 'foo'; - // import type A from 'foo'; - // import { B } from 'foo'; - // B(); - // type T = { foo: A; bar: C; baz: D }; - // ", - // None, - // ), - // ( - // " - // import A, { B } from 'foo'; - // B(); - // type T = A; - // ", - // " - // import type A from 'foo'; - // import { B } from 'foo'; - // B(); - // type T = A; - // ", - // None, - // ), - // ( - // " - // import type Already1Def from 'foo'; - // import type { Already1 } from 'foo'; - // import A, { B } from 'foo'; - // import { C, D, E } from 'bar'; - // import type { Already2 } from 'bar'; - // type T = { b: B; c: C; d: D }; - // ", - // " - // import type Already1Def from 'foo'; - // import type { Already1 , B } from 'foo'; - // import A from 'foo'; - // import { E } from 'bar'; - // import type { Already2 , C, D} from 'bar'; - // type T = { b: B; c: C; d: D }; - // ", - // None, - // ), - // ( - // " - // import A, { /* comment */ B } from 'foo'; - // type T = B; - // ", - // " - // import type { /* comment */ B } from 'foo'; - // import A from 'foo'; - // type T = B; - // ", - // None, - // ), - // ( - // " - // import { A, B, C } from 'foo'; - // import { D, E, F, } from 'bar'; - // type T = A | D; - // ", - // " - // import type { A} from 'foo'; - // import { B, C } from 'foo'; - // import type { D} from 'bar'; - // import { E, F, } from 'bar'; - // type T = A | D; - // ", - // None, - // ), - // ( - // " - // import { A, B, C } from 'foo'; - // import { D, E, F, } from 'bar'; - // type T = B | E; - // ", - // " - // import type { B} from 'foo'; - // import { A, C } from 'foo'; - // import type { E} from 'bar'; - // import { D, F, } from 'bar'; - // type T = B | E; - // ", - // None, - // ), - // ( - // " - // import { A, B, C } from 'foo'; - // import { D, E, F, } from 'bar'; - // type T = C | F; - // ", - // " - // import type { C } from 'foo'; - // import { A, B } from 'foo'; - // import type { F} from 'bar'; - // import { D, E } from 'bar'; - // type T = C | F; - // ", - // None, - // ), - // ( - // " - // import { Type1, Type2 } from 'named_types'; - // import Type from 'default_type'; - // import * as Types from 'namespace_type'; - // import Default, { Named } from 'default_and_named_type'; - // type T = Type1 | Type2 | Type | Types.A | Default | Named; - // ", - // " - // import type { Type1, Type2 } from 'named_types'; - // import type Type from 'default_type'; - // import type * as Types from 'namespace_type'; - // import type { Named } from 'default_and_named_type'; - // import type Default from 'default_and_named_type'; - // type T = Type1 | Type2 | Type | Types.A | Default | Named; - // ", - // None, - // ), - // ( - // " - // import { Value1, Type1 } from 'named_import'; - // import Type2, { Value2 } from 'default_import'; - // import Value3, { Type3 } from 'default_import2'; - // import Type4, { Type5, Value4 } from 'default_and_named_import'; - // type T = Type1 | Type2 | Type3 | Type4 | Type5; - // ", - // " - // import type { Type1 } from 'named_import'; - // import { Value1 } from 'named_import'; - // import type Type2 from 'default_import'; - // import { Value2 } from 'default_import'; - // import type { Type3 } from 'default_import2'; - // import Value3 from 'default_import2'; - // import type { Type5} from 'default_and_named_import'; - // import type Type4 from 'default_and_named_import'; - // import { Value4 } from 'default_and_named_import'; - // type T = Type1 | Type2 | Type3 | Type4 | Type5; - // ", - // None, - // ), - // ( - // " - // import type Foo from 'foo'; - // let foo: Foo; - // ", - // " - // import Foo from 'foo'; - // let foo: Foo; - // ", - // None, - // ), - // ( - // " - // import type { Foo } from 'foo'; - // let foo: Foo; - // ", - // " - // import { Foo } from 'foo'; - // let foo: Foo; - // ", - // None, - // ), - // ( - // " - // import Type from 'foo'; - // - // type T = typeof Type; - // type T = typeof Type.foo; - // ", - // " - // import type Type from 'foo'; - // - // type T = typeof Type; - // type T = typeof Type.foo; - // ", - // None, - // ), - // ( - // " - // import { Type } from 'foo'; - // - // type T = typeof Type; - // type T = typeof Type.foo; - // ", - // " - // import type { Type } from 'foo'; - // - // type T = typeof Type; - // type T = typeof Type.foo; - // ", - // None, - // ), - // ( - // " - // import * as Type from 'foo'; - // - // type T = typeof Type; - // type T = typeof Type.foo; - // ", - // " - // import type * as Type from 'foo'; - // - // type T = typeof Type; - // type T = typeof Type.foo; - // ", - // None, - // ), - // ( - // " - // import type Type from 'foo'; - // - // type T = typeof Type; - // type T = typeof Type.foo; - // ", - // " - // import Type from 'foo'; - // - // type T = typeof Type; - // type T = typeof Type.foo; - // ", - // None, - // ), - // ( - // " - // import type { Type } from 'foo'; - // - // type T = typeof Type; - // type T = typeof Type.foo; - // ", - // " - // import { Type } from 'foo'; - // - // type T = typeof Type; - // type T = typeof Type.foo; - // ", - // None, - // ), - // ( - // " - // import type * as Type from 'foo'; - // - // type T = typeof Type; - // type T = typeof Type.foo; - // ", - // " - // import * as Type from 'foo'; - // - // type T = typeof Type; - // type T = typeof Type.foo; - // ", - // None, - // ), - // ( - // " - // import Type from 'foo'; - // - // export type { Type }; // is a type-only export - // ", - // " - // import type Type from 'foo'; - // - // export type { Type }; // is a type-only export - // ", - // None, - // ), - // ( - // " - // import { Type } from 'foo'; - // - // export type { Type }; // is a type-only export - // ", - // " - // import type { Type } from 'foo'; - // - // export type { Type }; // is a type-only export - // ", - // None, - // ), - // ( - // " - // import * as Type from 'foo'; - // - // export type { Type }; // is a type-only export - // ", - // " - // import type * as Type from 'foo'; - // - // export type { Type }; // is a type-only export - // ", - // None, - // ), - // ( - // " - // import type Type from 'foo'; - // - // export { Type }; // is a type-only export - // export default Type; // is a type-only export - // export type { Type }; // is a type-only export - // ", - // " - // import Type from 'foo'; - // - // export { Type }; // is a type-only export - // export default Type; // is a type-only export - // export type { Type }; // is a type-only export - // ", - // None, - // ), - // ( - // " - // import type { Type } from 'foo'; - // - // export { Type }; // is a type-only export - // export default Type; // is a type-only export - // export type { Type }; // is a type-only export - // ", - // " - // import { Type } from 'foo'; - // - // export { Type }; // is a type-only export - // export default Type; // is a type-only export - // export type { Type }; // is a type-only export - // ", - // None, - // ), - // ( - // " - // import type * as Type from 'foo'; - // - // export { Type }; // is a type-only export - // export default Type; // is a type-only export - // export type { Type }; // is a type-only export - // ", - // " - // import * as Type from 'foo'; - // - // export { Type }; // is a type-only export - // export default Type; // is a type-only export - // export type { Type }; // is a type-only export - // ", - // None, - // ), - // ( - // " - // import type /*comment*/ * as AllType from 'foo'; - // import type // comment - // DefType from 'foo'; - // import type /*comment*/ { Type } from 'foo'; - // - // type T = { a: AllType; b: DefType; c: Type }; - // ", - // " - // import /*comment*/ * as AllType from 'foo'; - // import // comment - // DefType from 'foo'; - // import /*comment*/ { Type } from 'foo'; - // - // type T = { a: AllType; b: DefType; c: Type }; - // ", - // None, - // ), - // ( - // " - // import Default, * as Rest from 'module'; - // const a: Rest.A = ''; - // ", - // " - // import type * as Rest from 'module'; - // import Default from 'module'; - // const a: Rest.A = ''; - // ", - // None, - // ), - // ( - // " - // import Default, * as Rest from 'module'; - // const a: Default = ''; - // ", - // " - // import type Default from 'module'; - // import * as Rest from 'module'; - // const a: Default = ''; - // ", - // None, - // ), - // ( - // " - // import Default, * as Rest from 'module'; - // const a: Default = ''; - // const b: Rest.A = ''; - // ", - // " - // import type * as Rest from 'module'; - // import type Default from 'module'; - // const a: Default = ''; - // const b: Rest.A = ''; - // ", - // None, - // ), - // ( - // " - // import Default, /*comment*/ * as Rest from 'module'; - // const a: Default = ''; - // ", - // " - // import type Default from 'module'; - // import /*comment*/ * as Rest from 'module'; - // const a: Default = ''; - // ", - // None, - // ), - // ( - // " - // import Default /*comment1*/, /*comment2*/ { Data } from 'module'; - // const a: Default = ''; - // ", - // " - // import type Default /*comment1*/ from 'module'; - // import /*comment2*/ { Data } from 'module'; - // const a: Default = ''; - // ", - // None, - // ), - // ( - // " - // import Foo from 'foo'; - // @deco - // class A { - // constructor(foo: Foo) {} - // } - // ", - // " - // import type Foo from 'foo'; - // @deco - // class A { - // constructor(foo: Foo) {} - // } - // ", - // None, - // ), - // ( - // " - // import { type A, B } from 'foo'; - // type T = A; - // const b = B; - // ", - // " - // import { A, B } from 'foo'; - // type T = A; - // const b = B; - // ", - // None, - // ), - // ( - // " - // import { A, B, type C } from 'foo'; - // type T = A | C; - // const b = B; - // ", - // " - // import type { A} from 'foo'; - // import { B, type C } from 'foo'; - // type T = A | C; - // const b = B; - // ", - // None, - // ), - // ( - // " - // import { A, B } from 'foo'; - // let foo: A; - // let bar: B; - // ", - // " - // import { type A, type B } from 'foo'; - // let foo: A; - // let bar: B; - // ", - // None, - // ), - // ( - // " - // import { A, B } from 'foo'; - // - // let foo: A; - // B(); - // ", - // " - // import { type A, B } from 'foo'; - // - // let foo: A; - // B(); - // ", - // None, - // ), - // ( - // " - // import { A, B } from 'foo'; - // type T = A; - // B(); - // ", - // " - // import { type A, B } from 'foo'; - // type T = A; - // B(); - // ", - // None, - // ), - // ( - // " - // import { A } from 'foo'; - // import { B } from 'foo'; - // type T = A; - // type U = B; - // ", - // " - // import { type A } from 'foo'; - // import { type B } from 'foo'; - // type T = A; - // type U = B; - // ", - // None, - // ), - // ( - // " - // import { A } from 'foo'; - // import B from 'foo'; - // type T = A; - // type U = B; - // ", - // " - // import { type A } from 'foo'; - // import type B from 'foo'; - // type T = A; - // type U = B; - // ", - // None, - // ), - // ( - // " - // import A, { B, C } from 'foo'; - // type T = B; - // type U = C; - // A(); - // ", - // " - // import A, { type B, type C } from 'foo'; - // type T = B; - // type U = C; - // A(); - // ", - // None, - // ), - // ( - // " - // import A, { B, C } from 'foo'; - // type T = B; - // type U = C; - // type V = A; - // ", - // " - // import {type B, type C} from 'foo'; - // import type A from 'foo'; - // type T = B; - // type U = C; - // type V = A; - // ", - // None, - // ), - // ( - // " - // import A, { B, C as D } from 'foo'; - // type T = B; - // type U = D; - // type V = A; - // ", - // " - // import {type B, type C as D} from 'foo'; - // import type A from 'foo'; - // type T = B; - // type U = D; - // type V = A; - // ", - // None, - // ), - // ( - // " - // import { /* comment */ A, B } from 'foo'; - // type T = A; - // ", - // " - // import { /* comment */ type A, B } from 'foo'; - // type T = A; - // ", - // None, - // ), - // ( - // " - // import { B, /* comment */ A } from 'foo'; - // type T = A; - // ", - // " - // import { B, /* comment */ type A } from 'foo'; - // type T = A; - // ", - // None, - // ), - // ( - // " - // import { A, B, C } from 'foo'; - // import type { D } from 'deez'; - // - // const foo: A = B(); - // let bar: C; - // let baz: D; - // ", - // " - // import { type A, B, type C } from 'foo'; - // import type { D } from 'deez'; - // - // const foo: A = B(); - // let bar: C; - // let baz: D; - // ", - // None, - // ), - // ( - // " - // import { A, B, type C } from 'foo'; - // import type { D } from 'deez'; - // const foo: A = B(); - // let bar: C; - // let baz: D; - // ", - // " - // import { type A, B, type C } from 'foo'; - // import type { D } from 'deez'; - // const foo: A = B(); - // let bar: C; - // let baz: D; - // ", - // None, - // ), - // ( - // " - // import A from 'foo'; - // export = {} as A; - // ", - // " - // import type A from 'foo'; - // export = {} as A; - // ", - // None, - // ), - // ( - // " - // import { A } from 'foo'; - // export = {} as A; - // ", - // " - // import { type A } from 'foo'; - // export = {} as A; - // ", - // None, - // ), - // ( - // " - // import Foo from 'foo'; - // /@deco - // class A { - // constructor(foo: Foo) {} - // } - // ", - // " - // import type Foo from 'foo'; - // @deco - // class A { - // constructor(foo: Foo) {} - // } - // ", - // None, - // ), - // ( - // " - // import Foo from 'foo'; - // class A { - // @deco - // foo: Foo; - // } - // ", - // " - // import type Foo from 'foo'; - // class A { - // @deco - // foo: Foo; - // } - // ", - // None, - // ), - // ( - // " - // import Foo from 'foo'; - // class A { - // @deco - // foo(foo: Foo) {} - // } - // ", - // " - // import type Foo from 'foo'; - // class A { - // @deco - // foo(foo: Foo) {} - // } - // ", - // None, - // ), - // ( - // " - // import Foo from 'foo'; - // class A { - // @deco - // foo(): Foo {} - // } - // ", - // " - // import type Foo from 'foo'; - // class A { - // @deco - // foo(): Foo {} - // } - // ", - // None, - // ), - // ( - // " - // import Foo from 'foo'; - // class A { - // foo(@deco foo: Foo) {} - // } - // ", - // " - // import type Foo from 'foo'; - // class A { - // foo(@deco foo: Foo) {} - // } - // ", - // None, - // ), - // ( - // " - // import Foo from 'foo'; - // class A { - // @deco - // set foo(value: Foo) {} - // } - // ", - // " - // import type Foo from 'foo'; - // class A { - // @deco - // set foo(value: Foo) {} - // } - // ", - // None, - // ), - // ( - // " - // import Foo from 'foo'; - // class A { - // @deco - // get foo() {} - // - // set foo(value: Foo) {} - // } - // ", - // " - // import type Foo from 'foo'; - // class A { - // @deco - // get foo() {} - // - // set foo(value: Foo) {} - // } - // ", - // None, - // ), - // ( - // " - // import Foo from 'foo'; - // class A { - // @deco - // get foo() {} - // - // set ['foo'](value: Foo) {} - // } - // ", - // " - // import type Foo from 'foo'; - // class A { - // @deco - // get foo() {} - // - // set ['foo'](value: Foo) {} - // } - // ", - // None, - // ), - // ( - // " - // import * as foo from 'foo'; - // @deco - // class A { - // constructor(foo: foo.Foo) {} - // } - // ", - // " - // import type * as foo from 'foo'; - // @deco - // class A { - // constructor(foo: foo.Foo) {} - // } - // ", - // None, - // ), - // ( - // " - // import 'foo'; - // import { Foo, Bar } from 'foo'; - // function test(foo: Foo) {} - // ", - // " - // import 'foo'; - // import type { Foo} from 'foo'; - // import { Bar } from 'foo'; - // function test(foo: Foo) {} - // ", - // None, - // ), - // ( - // " - // import {} from 'foo'; - // import { Foo, Bar } from 'foo'; - // function test(foo: Foo) {} - // ", - // " - // import {} from 'foo'; - // import type { Foo} from 'foo'; - // import { Bar } from 'foo'; - // function test(foo: Foo) {} - // ", - // None, - // ), - // ( - // " - // import Foo from 'foo'; - // export type T = Foo; - // ", - // " - // import type Foo from 'foo'; - // export type T = Foo; - // ", - // None, - // ), - // ]; - Tester::new(ConsistentTypeImports::NAME, pass, fail).test_and_snapshot(); + let fix = vec![ + ( + " + import Foo from 'foo'; + let foo: Foo; + type Bar = Foo; + interface Baz { + foo: Foo; + } + function fn(a: Foo): Foo {} + ", + " + import type Foo from 'foo'; + let foo: Foo; + type Bar = Foo; + interface Baz { + foo: Foo; + } + function fn(a: Foo): Foo {} + ", + None, + ), + ( + " + import Foo from 'foo'; + let foo: Foo; + ", + " + import type Foo from 'foo'; + let foo: Foo; + ", + Some(serde_json::json!([{ "prefer": "type-imports" }])), + ), + ( + " + import Foo from 'foo'; + let foo: Foo; + ", + " + import type Foo from 'foo'; + let foo: Foo; + ", + Some( + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), + ), + ), + ( + " + import { A, B } from 'foo'; + let foo: A; + let bar: B; + ", + " + import type { A, B } from 'foo'; + let foo: A; + let bar: B; + ", + None, + ), + ( + " + import { A as a, B as b } from 'foo'; + let foo: a; + let bar: b; + ", + " + import type { A as a, B as b } from 'foo'; + let foo: a; + let bar: b; + ", + None, + ), + ( + " + import Foo from 'foo'; + type Bar = typeof Foo; // TSTypeQuery + ", + " + import type Foo from 'foo'; + type Bar = typeof Foo; // TSTypeQuery + ", + None, + ), + ( + " + import foo from 'foo'; + type Bar = foo.Bar; // TSQualifiedName + ", + " + import type foo from 'foo'; + type Bar = foo.Bar; // TSQualifiedName + ", + None, + ), + ( + " + import foo from 'foo'; + type Baz = (typeof foo.bar)['Baz']; // TSQualifiedName & TSTypeQuery + ", + " + import type foo from 'foo'; + type Baz = (typeof foo.bar)['Baz']; // TSQualifiedName & TSTypeQuery + ", + None, + ), + ( + " + import * as A from 'foo'; + let foo: A.Foo; + ", + " + import type * as A from 'foo'; + let foo: A.Foo; + ", + None, + ), + ( + " + import A, { B } from 'foo'; + let foo: A; + let bar: B; + ", + " + import type { B } from 'foo'; + import type A from 'foo'; + let foo: A; + let bar: B; + ", + None, + ), + ( + " + import A, {} from 'foo'; + let foo: A; + ", + " + import type A from 'foo'; + let foo: A; + ", + None, + ), + ( + " + import { A, B } from 'foo'; + const foo: A = B(); + ", + " + import type { A} from 'foo'; + import { B } from 'foo'; + const foo: A = B(); + ", + None, + ), + ( + " + import { A, B, C } from 'foo'; + const foo: A = B(); + let bar: C; + ", + " + import type { A, C } from 'foo'; + import { B } from 'foo'; + const foo: A = B(); + let bar: C; + ", + None, + ), + ( + " + import { A, B, C, D } from 'foo'; + const foo: A = B(); + type T = { bar: C; baz: D }; + ", + " + import type { A, C, D } from 'foo'; + import { B } from 'foo'; + const foo: A = B(); + type T = { bar: C; baz: D }; + ", + None, + ), + ( + " + import A, { B, C, D } from 'foo'; + B(); + type T = { foo: A; bar: C; baz: D }; + ", + " + import type { C, D } from 'foo'; + import type A from 'foo'; + import { B } from 'foo'; + B(); + type T = { foo: A; bar: C; baz: D }; + ", + None, + ), + ( + " + import A, { B } from 'foo'; + B(); + type T = A; + ", + " + import type A from 'foo'; + import { B } from 'foo'; + B(); + type T = A; + ", + None, + ), + ( + " + import type Already1Def from 'foo'; + import type { Already1 } from 'foo'; + import A, { B } from 'foo'; + import { C, D, E } from 'bar'; + import type { Already2 } from 'bar'; + type T = { b: B; c: C; d: D }; + ", + " + import type Already1Def from 'foo'; + import type { Already1 , B } from 'foo'; + import A from 'foo'; + import { E } from 'bar'; + import type { Already2 , C, D} from 'bar'; + type T = { b: B; c: C; d: D }; + ", + None, + ), + ( + " + import A, { /* comment */ B } from 'foo'; + type T = B; + ", + " + import type { /* comment */ B } from 'foo'; + import A from 'foo'; + type T = B; + ", + None, + ), + ( + " + import { A, B, C } from 'foo'; + import { D, E, F, } from 'bar'; + type T = A | D; + ", + " + import type { A} from 'foo'; + import { B, C } from 'foo'; + import type { D} from 'bar'; + import { E, F, } from 'bar'; + type T = A | D; + ", + None, + ), + ( + " + import { A, B, C } from 'foo'; + import { D, E, F, } from 'bar'; + type T = B | E; + ", + " + import type { B} from 'foo'; + import { A, C } from 'foo'; + import type { E} from 'bar'; + import { D, F, } from 'bar'; + type T = B | E; + ", + None, + ), + ( + " + import { A, B, C } from 'foo'; + import { D, E, F, } from 'bar'; + type T = C | F; + ", + " + import type { C } from 'foo'; + import { A, B } from 'foo'; + import type { F} from 'bar'; + import { D, E } from 'bar'; + type T = C | F; + ", + None, + ), + ( + " + import { Type1, Type2 } from 'named_types'; + import Type from 'default_type'; + import * as Types from 'namespace_type'; + import Default, { Named } from 'default_and_named_type'; + type T = Type1 | Type2 | Type | Types.A | Default | Named; + ", + " + import type { Type1, Type2 } from 'named_types'; + import type Type from 'default_type'; + import type * as Types from 'namespace_type'; + import type { Named } from 'default_and_named_type'; + import type Default from 'default_and_named_type'; + type T = Type1 | Type2 | Type | Types.A | Default | Named; + ", + None, + ), + ( + " + import { Value1, Type1 } from 'named_import'; + import Type2, { Value2 } from 'default_import'; + import Value3, { Type3 } from 'default_import2'; + import Type4, { Type5, Value4 } from 'default_and_named_import'; + type T = Type1 | Type2 | Type3 | Type4 | Type5; + ", + " + import type { Type1 } from 'named_import'; + import { Value1 } from 'named_import'; + import type Type2 from 'default_import'; + import { Value2 } from 'default_import'; + import type { Type3 } from 'default_import2'; + import Value3 from 'default_import2'; + import type { Type5} from 'default_and_named_import'; + import type Type4 from 'default_and_named_import'; + import { Value4 } from 'default_and_named_import'; + type T = Type1 | Type2 | Type3 | Type4 | Type5; + ", + None, + ), + ( + " + import type Foo from 'foo'; + let foo: Foo; + ", + " + import Foo from 'foo'; + let foo: Foo; + ", + Some(serde_json::json!([{ "prefer": "no-type-imports" }])), + ), + ( + " + import type { Foo } from 'foo'; + let foo: Foo; + ", + " + import { Foo } from 'foo'; + let foo: Foo; + ", + Some(serde_json::json!([{ "prefer": "no-type-imports" }])), + ), + // TODO: Identifier `T` has already been declared + // ( + // " + // import Type from 'foo'; + // + // type T = typeof Type; + // type T = typeof Type.foo; + // ", + // " + // import type Type from 'foo'; + // + // type T = typeof Type; + // type T = typeof Type.foo; + // ", + // None, + // ), + // ( + // " + // import { Type } from 'foo'; + // + // type T = typeof Type; + // type T = typeof Type.foo; + // ", + // " + // import type { Type } from 'foo'; + // + // type T = typeof Type; + // type T = typeof Type.foo; + // ", + // None, + // ), + // ( + // " + // import * as Type from 'foo'; + // + // type T = typeof Type; + // type T = typeof Type.foo; + // ", + // " + // import type * as Type from 'foo'; + // + // type T = typeof Type; + // type T = typeof Type.foo; + // ", + // None, + // ), + // ( + // " + // import type Type from 'foo'; + // + // type T = typeof Type; + // type T = typeof Type.foo; + // ", + // " + // import Type from 'foo'; + // + // type T = typeof Type; + // type T = typeof Type.foo; + // ", + // Some(serde_json::json!([{ "prefer": "no-type-imports" }])), + // ), + // ( + // " + // import type { Type } from 'foo'; + // + // type T = typeof Type; + // type T = typeof Type.foo; + // ", + // " + // import { Type } from 'foo'; + // + // type T = typeof Type; + // type T = typeof Type.foo; + // ", + // Some(serde_json::json!([{ "prefer": "no-type-imports" }])), + // ), + // ( + // " + // import type * as Type from 'foo'; + // + // type T = typeof Type; + // type T = typeof Type.foo; + // ", + // " + // import * as Type from 'foo'; + // + // type T = typeof Type; + // type T = typeof Type.foo; + // ", + // Some(serde_json::json!([{ "prefer": "no-type-imports" }])), + // ), + ( + " + import Type from 'foo'; + + export type { Type }; // is a type-only export + ", + " + import type Type from 'foo'; + + export type { Type }; // is a type-only export + ", + None, + ), + ( + " + import { Type } from 'foo'; + + export type { Type }; // is a type-only export + ", + " + import type { Type } from 'foo'; + + export type { Type }; // is a type-only export + ", + None, + ), + ( + " + import * as Type from 'foo'; + + export type { Type }; // is a type-only export + ", + " + import type * as Type from 'foo'; + + export type { Type }; // is a type-only export + ", + None, + ), + ( + " + import type Type from 'foo'; + + export { Type }; // is a type-only export + export default Type; // is a type-only export + export type { Type }; // is a type-only export + ", + " + import Type from 'foo'; + + export { Type }; // is a type-only export + export default Type; // is a type-only export + export type { Type }; // is a type-only export + ", + Some(serde_json::json!([{ "prefer": "no-type-imports" }])), + ), + ( + " + import type { Type } from 'foo'; + + export { Type }; // is a type-only export + export default Type; // is a type-only export + export type { Type }; // is a type-only export + ", + " + import { Type } from 'foo'; + + export { Type }; // is a type-only export + export default Type; // is a type-only export + export type { Type }; // is a type-only export + ", + Some(serde_json::json!([{ "prefer": "no-type-imports" }])), + ), + ( + " + import type * as Type from 'foo'; + + export { Type }; // is a type-only export + export default Type; // is a type-only export + export type { Type }; // is a type-only export + ", + " + import * as Type from 'foo'; + + export { Type }; // is a type-only export + export default Type; // is a type-only export + export type { Type }; // is a type-only export + ", + Some(serde_json::json!([{ "prefer": "no-type-imports" }])), + ), + ( + " + import type /*comment*/ * as AllType from 'foo'; + import type // comment + DefType from 'foo'; + import type /*comment*/ { Type } from 'foo'; + + type T = { a: AllType; b: DefType; c: Type }; + ", + " + import /*comment*/ * as AllType from 'foo'; + import // comment + DefType from 'foo'; + import /*comment*/ { Type } from 'foo'; + + type T = { a: AllType; b: DefType; c: Type }; + ", + Some(serde_json::json!([{ "prefer": "no-type-imports" }])), + ), + ( + " + import Default, * as Rest from 'module'; + const a: Rest.A = ''; + ", + " + import type * as Rest from 'module'; + import Default from 'module'; + const a: Rest.A = ''; + ", + Some(serde_json::json!([{ "prefer": "type-imports" }])), + ), + ( + " + import Default, * as Rest from 'module'; + const a: Default = ''; + ", + " + import type Default from 'module'; + import * as Rest from 'module'; + const a: Default = ''; + ", + Some(serde_json::json!([{ "prefer": "type-imports" }])), + ), + ( + " + import Default, * as Rest from 'module'; + const a: Default = ''; + const b: Rest.A = ''; + ", + " + import type * as Rest from 'module'; + import type Default from 'module'; + const a: Default = ''; + const b: Rest.A = ''; + ", + Some(serde_json::json!([{ "prefer": "type-imports" }])), + ), + ( + " + import Default, /*comment*/ * as Rest from 'module'; + const a: Default = ''; + ", + " + import type Default from 'module'; + import /*comment*/ * as Rest from 'module'; + const a: Default = ''; + ", + Some(serde_json::json!([{ "prefer": "type-imports" }])), + ), + ( + " + import Default /*comment1*/, /*comment2*/ { Data } from 'module'; + const a: Default = ''; + ", + " + import type Default /*comment1*/ from 'module'; + import /*comment2*/ { Data } from 'module'; + const a: Default = ''; + ", + Some(serde_json::json!([{ "prefer": "type-imports" }])), + ), + ( + " + import Foo from 'foo'; + @deco + class A { + constructor(foo: Foo) {} + } + ", + " + import type Foo from 'foo'; + @deco + class A { + constructor(foo: Foo) {} + } + ", + None, + ), + ( + " + import { type A, B } from 'foo'; + type T = A; + const b = B; + ", + " + import { A, B } from 'foo'; + type T = A; + const b = B; + ", + Some(serde_json::json!([{ "prefer": "no-type-imports" }])), + ), + ( + " + import { A, B, type C } from 'foo'; + type T = A | C; + const b = B; + ", + " + import type { A} from 'foo'; + import { B, type C } from 'foo'; + type T = A | C; + const b = B; + ", + Some(serde_json::json!([{ "prefer": "type-imports" }])), + ), + ( + " + import { A, B } from 'foo'; + let foo: A; + let bar: B; + ", + " + import { type A, type B } from 'foo'; + let foo: A; + let bar: B; + ", + Some( + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), + ), + ), + ( + " + import { A, B } from 'foo'; + + let foo: A; + B(); + ", + " + import { type A, B } from 'foo'; + + let foo: A; + B(); + ", + Some( + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), + ), + ), + ( + " + import { A, B } from 'foo'; + type T = A; + B(); + ", + " + import { type A, B } from 'foo'; + type T = A; + B(); + ", + Some( + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), + ), + ), + ( + " + import { A } from 'foo'; + import { B } from 'foo'; + type T = A; + type U = B; + ", + " + import { type A } from 'foo'; + import { type B } from 'foo'; + type T = A; + type U = B; + ", + Some( + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), + ), + ), + ( + " + import { A } from 'foo'; + import B from 'foo'; + type T = A; + type U = B; + ", + " + import { type A } from 'foo'; + import type B from 'foo'; + type T = A; + type U = B; + ", + Some( + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), + ), + ), + ( + " + import A, { B, C } from 'foo'; + type T = B; + type U = C; + A(); + ", + " + import A, { type B, type C } from 'foo'; + type T = B; + type U = C; + A(); + ", + Some( + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), + ), + ), + ( + " + import A, { B, C } from 'foo'; + type T = B; + type U = C; + type V = A; + ", + " + import {type B, type C} from 'foo'; + import type A from 'foo'; + type T = B; + type U = C; + type V = A; + ", + Some( + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), + ), + ), + ( + " + import A, { B, C as D } from 'foo'; + type T = B; + type U = D; + type V = A; + ", + " + import {type B, type C as D} from 'foo'; + import type A from 'foo'; + type T = B; + type U = D; + type V = A; + ", + Some( + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), + ), + ), + ( + " + import { /* comment */ A, B } from 'foo'; + type T = A; + ", + " + import { /* comment */ type A, B } from 'foo'; + type T = A; + ", + Some( + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), + ), + ), + ( + " + import { B, /* comment */ A } from 'foo'; + type T = A; + ", + " + import { B, /* comment */ type A } from 'foo'; + type T = A; + ", + Some( + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), + ), + ), + ( + " + import { A, B, C } from 'foo'; + import type { D } from 'deez'; + + const foo: A = B(); + let bar: C; + let baz: D; + ", + " + import { type A, B, type C } from 'foo'; + import type { D } from 'deez'; + + const foo: A = B(); + let bar: C; + let baz: D; + ", + Some( + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), + ), + ), + ( + " + import { A, B, type C } from 'foo'; + import type { D } from 'deez'; + const foo: A = B(); + let bar: C; + let baz: D; + ", + " + import { type A, B, type C } from 'foo'; + import type { D } from 'deez'; + const foo: A = B(); + let bar: C; + let baz: D; + ", + Some( + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), + ), + ), + ( + " + import A from 'foo'; + export = {} as A; + ", + " + import type A from 'foo'; + export = {} as A; + ", + Some( + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), + ), + ), + ( + " + import { A } from 'foo'; + export = {} as A; + ", + " + import { type A } from 'foo'; + export = {} as A; + ", + Some( + serde_json::json!([{ "prefer": "type-imports", "fixStyle": "inline-type-imports" },]), + ), + ), + ( + " + import Foo from 'foo'; + @deco + class A { + constructor(foo: Foo) {} + } + ", + " + import type Foo from 'foo'; + @deco + class A { + constructor(foo: Foo) {} + } + ", + None, + ), + ( + " + import Foo from 'foo'; + class A { + @deco + foo: Foo; + } + ", + " + import type Foo from 'foo'; + class A { + @deco + foo: Foo; + } + ", + None, + ), + ( + " + import Foo from 'foo'; + class A { + @deco + foo(foo: Foo) {} + } + ", + " + import type Foo from 'foo'; + class A { + @deco + foo(foo: Foo) {} + } + ", + None, + ), + ( + " + import Foo from 'foo'; + class A { + @deco + foo(): Foo {} + } + ", + " + import type Foo from 'foo'; + class A { + @deco + foo(): Foo {} + } + ", + None, + ), + ( + " + import Foo from 'foo'; + class A { + foo(@deco foo: Foo) {} + } + ", + " + import type Foo from 'foo'; + class A { + foo(@deco foo: Foo) {} + } + ", + None, + ), + ( + " + import Foo from 'foo'; + class A { + @deco + set foo(value: Foo) {} + } + ", + " + import type Foo from 'foo'; + class A { + @deco + set foo(value: Foo) {} + } + ", + None, + ), + ( + " + import Foo from 'foo'; + class A { + @deco + get foo() {} + + set foo(value: Foo) {} + } + ", + " + import type Foo from 'foo'; + class A { + @deco + get foo() {} + + set foo(value: Foo) {} + } + ", + None, + ), + ( + " + import Foo from 'foo'; + class A { + @deco + get foo() {} + + set ['foo'](value: Foo) {} + } + ", + " + import type Foo from 'foo'; + class A { + @deco + get foo() {} + + set ['foo'](value: Foo) {} + } + ", + None, + ), + // TODO: need fix foo.Foo shallow + // ( + // " + // import * as foo from 'foo'; + // @deco + // class A { + // constructor(foo: foo.Foo) {} + // } + // ", + // " + // import type * as foo from 'foo'; + // @deco + // class A { + // constructor(foo: foo.Foo) {} + // } + // ", + // None, + // ), + ( + " + import 'foo'; + import { Foo, Bar } from 'foo'; + function test(foo: Foo) {} + ", + " + import 'foo'; + import type { Foo} from 'foo'; + import { Bar } from 'foo'; + function test(foo: Foo) {} + ", + None, + ), + ( + " + import {} from 'foo'; + import { Foo, Bar } from 'foo'; + function test(foo: Foo) {} + ", + " + import {} from 'foo'; + import type { Foo} from 'foo'; + import { Bar } from 'foo'; + function test(foo: Foo) {} + ", + None, + ), + ( + " + import Foo from 'foo'; + export type T = Foo; + ", + " + import type Foo from 'foo'; + export type T = Foo; + ", + None, + ), + ]; + + // To format fix code. + // In fixer may insert import statement: + // `import A, B from 'foo';` => `import A from 'foo';\nimport B from 'foo';`` + // And the insert text didn't follow the indent of last statement, it just insert before `text + + // '\n'`, so let's remove the prefix space. + let fix = fix + .into_iter() + .map(|(a, b, c)| (remove_prefix_space(a), remove_prefix_space(b), c)) + .collect::>(); + + Tester::new(ConsistentTypeImports::NAME, pass, fail).expect_fix(fix).test_and_snapshot(); }