@@ -30,28 +30,36 @@ fn use_top_level_for_declaration_file_import_diagnostic(span: Span) -> OxcDiagno
3030}
3131
3232fn consistent_type_specifier_style_diagnostic ( span : Span , mode : & Mode ) -> OxcDiagnostic {
33- let ( warn_msg, help_msg) = if * mode == Mode :: PreferInline {
34- (
33+ let ( warn_msg, help_msg) = match mode {
34+ Mode :: Inline => (
3535 "Prefer using inline type specifiers instead of a top-level type-only import." ,
3636 "Replace top‐level import type with an inline type specifier." ,
37- )
38- } else {
39- (
37+ ) ,
38+ Mode :: TopLevel => (
4039 "Prefer using a top-level type-only import instead of inline type specifiers." ,
4140 "Replace inline type specifiers with a top‐level import type statement." ,
42- )
41+ ) ,
42+ Mode :: TopLevelIfOnlyTypeImports => (
43+ "Prefer using a top-level type-only import instead of inline type specifiers when there are only type imports." ,
44+ "Replace inline type specifiers with a top‐level import type statement." ,
45+ ) ,
4346 } ;
4447 OxcDiagnostic :: warn ( warn_msg) . with_help ( help_msg) . with_label ( span)
4548}
4649
4750#[ derive( Debug , Default , PartialEq , Clone , Serialize , Deserialize , JsonSchema ) ]
48- #[ serde( rename_all = "kebab-case" ) ]
4951enum Mode {
5052 /// Prefer `import type { Foo } from 'foo'` for type imports.
5153 #[ default]
52- PreferTopLevel ,
54+ #[ serde( rename = "prefer-top-level" ) ]
55+ TopLevel ,
5356 /// Prefer `import { type Foo } from 'foo'` for type imports.
54- PreferInline ,
57+ #[ serde( rename = "prefer-inline" ) ]
58+ Inline ,
59+ /// Prefer `import type { Foo } from 'foo'` when all named imports are types, but allow
60+ /// `import { type Foo, bar } from 'foo'` when value imports are present.
61+ #[ serde( rename = "prefer-top-level-if-only-type-imports" ) ]
62+ TopLevelIfOnlyTypeImports ,
5563}
5664
5765#[ derive( Debug , Default , Clone , Serialize , Deserialize ) ]
@@ -82,6 +90,19 @@ declare_oxc_lint!(
8290 /// import type Foo, { Bar } from 'Foo';
8391 /// ```
8492 ///
93+ /// Examples of incorrect code for the `prefer-top-level-if-only-type-imports` option:
94+ /// ```typescript
95+ /// import { type Foo } from 'Foo';
96+ /// import { type Foo, type Bar } from 'Foo';
97+ /// ```
98+ ///
99+ /// Examples of correct code for the `prefer-top-level-if-only-type-imports` option:
100+ /// ```typescript
101+ /// import type { Foo } from 'Foo';
102+ /// import { type Foo, someValue } from 'Foo';
103+ /// import type Foo, { Bar } from 'Foo';
104+ /// ```
105+ ///
85106 /// Examples of incorrect code for the `prefer-inline` option:
86107 /// ```typescript
87108 /// import type { Foo } from 'Foo';
@@ -122,21 +143,26 @@ impl Rule for ConsistentTypeSpecifierStyle {
122143 return ;
123144 }
124145
125- // Declaration file imports should always be top-level type imports
126- if ( self . 0 == Mode :: PreferTopLevel || is_declaration_file_import ( import_decl) )
127- && import_decl. import_kind . is_value ( )
128- {
146+ // Declaration file imports should always be top-level type imports.
147+ if import_decl. import_kind . is_value ( ) {
148+ let is_declaration_file_import = is_declaration_file_import ( import_decl) ;
149+ if self . 0 == Mode :: Inline && !is_declaration_file_import {
150+ return ;
151+ }
152+
129153 let ( value_specifiers, type_specifiers) = split_import_specifiers_by_kind ( specifiers) ;
130154 if type_specifiers. is_empty ( ) {
131155 return ;
132156 }
133157
134- for item in & type_specifiers {
135- let diagnostic = if is_declaration_file_import ( import_decl) {
136- use_top_level_for_declaration_file_import_diagnostic ( item. span ( ) )
137- } else {
138- consistent_type_specifier_style_diagnostic ( item. span ( ) , & self . 0 )
139- } ;
158+ let should_use_top_level = is_declaration_file_import
159+ || self . 0 == Mode :: TopLevel
160+ || ( self . 0 == Mode :: TopLevelIfOnlyTypeImports && value_specifiers. is_empty ( ) ) ;
161+ if !should_use_top_level {
162+ return ;
163+ }
164+
165+ let report = |diagnostic| {
140166 ctx. diagnostic_with_fix ( diagnostic, |fixer| {
141167 let mut import_source = String :: new ( ) ;
142168
@@ -154,8 +180,21 @@ impl Rule for ConsistentTypeSpecifierStyle {
154180 . replace ( import_decl. span , import_source. trim_end ( ) . to_string ( ) )
155181 . with_message ( "Convert to a `top-level` type import" )
156182 } ) ;
183+ } ;
184+
185+ if self . 0 == Mode :: TopLevelIfOnlyTypeImports && !is_declaration_file_import {
186+ report ( consistent_type_specifier_style_diagnostic ( import_decl. span , & self . 0 ) ) ;
187+ } else {
188+ for item in & type_specifiers {
189+ let diagnostic = if is_declaration_file_import {
190+ use_top_level_for_declaration_file_import_diagnostic ( item. span ( ) )
191+ } else {
192+ consistent_type_specifier_style_diagnostic ( item. span ( ) , & self . 0 )
193+ } ;
194+ report ( diagnostic) ;
195+ }
157196 }
158- } else if self . 0 == Mode :: PreferInline && import_decl. import_kind . is_type ( ) {
197+ } else if self . 0 == Mode :: Inline && import_decl. import_kind . is_type ( ) {
159198 if is_declaration_file_import ( import_decl) {
160199 return ;
161200 }
@@ -305,6 +344,42 @@ fn test() {
305344 ( "import type { Foo as Bar } from 'Foo';" , Some ( json!( [ "prefer-top-level" ] ) ) ) ,
306345 ( "import type { Foo, Bar, Baz, Bam } from 'Foo';" , Some ( json!( [ "prefer-top-level" ] ) ) ) ,
307346 ( "import type {Foo} from 'Foo'" , Some ( json!( [ "prefer-top-level" ] ) ) ) ,
347+ ( "import Foo from 'Foo';" , Some ( json!( [ "prefer-top-level-if-only-type-imports" ] ) ) ) ,
348+ ( "import type Foo from 'Foo';" , Some ( json!( [ "prefer-top-level-if-only-type-imports" ] ) ) ) ,
349+ ( "import { Foo } from 'Foo';" , Some ( json!( [ "prefer-top-level-if-only-type-imports" ] ) ) ) ,
350+ (
351+ "import { Foo as Bar } from 'Foo';" ,
352+ Some ( json!( [ "prefer-top-level-if-only-type-imports" ] ) ) ,
353+ ) ,
354+ ( "import * as Foo from 'Foo';" , Some ( json!( [ "prefer-top-level-if-only-type-imports" ] ) ) ) ,
355+ ( "import 'Foo';" , Some ( json!( [ "prefer-top-level-if-only-type-imports" ] ) ) ) ,
356+ ( "import {} from 'Foo';" , Some ( json!( [ "prefer-top-level-if-only-type-imports" ] ) ) ) ,
357+ ( "import type {} from 'Foo';" , Some ( json!( [ "prefer-top-level-if-only-type-imports" ] ) ) ) ,
358+ ( "import type { Foo } from 'Foo';" , Some ( json!( [ "prefer-top-level-if-only-type-imports" ] ) ) ) ,
359+ (
360+ "import type { Foo as Bar } from 'Foo';" ,
361+ Some ( json!( [ "prefer-top-level-if-only-type-imports" ] ) ) ,
362+ ) ,
363+ (
364+ "import type { Foo, Bar, Baz, Bam } from 'Foo';" ,
365+ Some ( json!( [ "prefer-top-level-if-only-type-imports" ] ) ) ,
366+ ) ,
367+ (
368+ "import { Foo, type Bar } from 'Foo';" ,
369+ Some ( json!( [ "prefer-top-level-if-only-type-imports" ] ) ) ,
370+ ) ,
371+ (
372+ "import { type Foo, Bar } from 'Foo';" ,
373+ Some ( json!( [ "prefer-top-level-if-only-type-imports" ] ) ) ,
374+ ) ,
375+ (
376+ "import Foo, { type Bar } from 'Foo';" ,
377+ Some ( json!( [ "prefer-top-level-if-only-type-imports" ] ) ) ,
378+ ) ,
379+ (
380+ "import Foo, { type Bar, Baz } from 'Foo';" ,
381+ Some ( json!( [ "prefer-top-level-if-only-type-imports" ] ) ) ,
382+ ) ,
308383 ( "import {type Foo} from 'Foo'" , Some ( json!( [ "prefer-inline" ] ) ) ) ,
309384 ( "import Foo from 'Foo';" , Some ( json!( [ "prefer-inline" ] ) ) ) ,
310385 ( "import type Foo from 'Foo';" , Some ( json!( [ "prefer-inline" ] ) ) ) ,
@@ -327,6 +402,10 @@ fn test() {
327402 ( "import type { Foo } from './index.d.cts';" , Some ( json!( [ "prefer-inline" ] ) ) ) ,
328403 ( "import type { Foo } from './app.d.css.ts';" , Some ( json!( [ "prefer-top-level" ] ) ) ) ,
329404 ( "import type { Foo } from './app.d.css.ts';" , Some ( json!( [ "prefer-inline" ] ) ) ) ,
405+ (
406+ "import type { Foo } from './index.d.ts';" ,
407+ Some ( json!( [ "prefer-top-level-if-only-type-imports" ] ) ) ,
408+ ) ,
330409 ] ;
331410
332411 let fail = vec ! [
@@ -340,6 +419,15 @@ fn test() {
340419 ( "import Foo, { type Bar, Baz } from 'Foo';" , None ) ,
341420 ( "import { Component, type ComponentProps } from 'package-1';" , None ) ,
342421 ( "import type { Foo, Bar, Baz } from 'Foo';" , Some ( json!( [ "prefer-inline" ] ) ) ) ,
422+ ( "import { type Foo } from 'Foo';" , Some ( json!( [ "prefer-top-level-if-only-type-imports" ] ) ) ) ,
423+ (
424+ "import { type Foo as Bar } from 'Foo';" ,
425+ Some ( json!( [ "prefer-top-level-if-only-type-imports" ] ) ) ,
426+ ) ,
427+ (
428+ "import { type Foo, type Bar } from 'Foo';" ,
429+ Some ( json!( [ "prefer-top-level-if-only-type-imports" ] ) ) ,
430+ ) ,
343431 // declaration files always require `import type` syntax
344432 ( "import { type Foo } from './index.d.ts';" , Some ( json!( [ "prefer-top-level" ] ) ) ) ,
345433 ( "import { type Foo } from './index.d.ts';" , Some ( json!( [ "prefer-inline" ] ) ) ) ,
@@ -349,6 +437,10 @@ fn test() {
349437 ( "import { type Foo } from './index.d.cts';" , Some ( json!( [ "prefer-inline" ] ) ) ) ,
350438 ( "import { type Foo } from './app.d.css.ts';" , Some ( json!( [ "prefer-top-level" ] ) ) ) ,
351439 ( "import { type Foo } from './app.d.css.ts';" , Some ( json!( [ "prefer-inline" ] ) ) ) ,
440+ (
441+ "import { Foo, type Bar } from './index.d.ts';" ,
442+ Some ( json!( [ "prefer-top-level-if-only-type-imports" ] ) ) ,
443+ ) ,
352444 ] ;
353445
354446 let fix = vec ! [
@@ -440,6 +532,26 @@ fn test() {
440532 "import type { Foo } from 'index.d.ts';" ,
441533 Some ( json!( [ "prefer-inline" ] ) ) ,
442534 ) ,
535+ (
536+ "import { type Foo } from 'Foo';" ,
537+ "import type { Foo } from 'Foo';" ,
538+ Some ( json!( [ "prefer-top-level-if-only-type-imports" ] ) ) ,
539+ ) ,
540+ (
541+ "import { type Foo as Bar } from 'Foo';" ,
542+ "import type { Foo as Bar } from 'Foo';" ,
543+ Some ( json!( [ "prefer-top-level-if-only-type-imports" ] ) ) ,
544+ ) ,
545+ (
546+ "import { type Foo, type Bar } from 'Foo';" ,
547+ "import type { Foo, Bar } from 'Foo';" ,
548+ Some ( json!( [ "prefer-top-level-if-only-type-imports" ] ) ) ,
549+ ) ,
550+ (
551+ "import { Foo, type Bar } from './index.d.ts';" ,
552+ "import { Foo } from './index.d.ts';\n import type { Bar } from './index.d.ts';" ,
553+ Some ( json!( [ "prefer-top-level-if-only-type-imports" ] ) ) ,
554+ ) ,
443555 ] ;
444556
445557 Tester :: new (
0 commit comments