Skip to content

Commit 99978a8

Browse files
committed
feat(linter/import/consistent-type-specifier-style): support prefer-top-level-if-only-type-imports option (#24502)
## Summary - Re-port upstream support for prefer-top-level-if-only-type-imports. - Allow mixed value/type imports while requiring top-level syntax for imports containing only types. - Preserve Oxlint's existing default and declaration-file behavior. - Regenerate the JSON schema and TypeScript configuration types. ## Reference #1117 (comment)
1 parent 4de1624 commit 99978a8

4 files changed

Lines changed: 169 additions & 21 deletions

File tree

‎apps/oxlint/src-js/package/config.generated.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export type FuncNamesConfigType = "always" | "as-needed" | "never";
8686
export type Style = "expression" | "declaration";
8787
export type NamedExports = "ignore" | "expression" | "declaration";
8888
export type PairOrder = "anyOrder" | "getBeforeSet" | "setBeforeGet";
89-
export type Mode = "prefer-top-level" | "prefer-inline";
89+
export type Mode = "prefer-top-level" | "prefer-inline" | "prefer-top-level-if-only-type-imports";
9090
/**
9191
* Extension rule configuration; Copy to avoid extra indirection.
9292
*/

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

Lines changed: 132 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,28 +30,36 @@ fn use_top_level_for_declaration_file_import_diagnostic(span: Span) -> OxcDiagno
3030
}
3131

3232
fn 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")]
4951
enum 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';\nimport type { Bar } from './index.d.ts';",
553+
Some(json!(["prefer-top-level-if-only-type-imports"])),
554+
),
443555
];
444556

445557
Tester::new(

‎crates/oxc_linter/src/snapshots/import_consistent_type_specifier_style.snap‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,27 @@ source: crates/oxc_linter/src/tester.rs
8686
╰────
8787
help: Replace top‐level import type with an inline type specifier.
8888

89+
⚠ import(consistent-type-specifier-style): Prefer using a top-level type-only import instead of inline type specifiers when there are only type imports.
90+
╭─[consistent_type_specifier_style.tsx:1:1]
91+
1 │ import { type Foo } from 'Foo';
92+
· ───────────────────────────────
93+
╰────
94+
help: Replace inline type specifiers with a top‐level import type statement.
95+
96+
⚠ import(consistent-type-specifier-style): Prefer using a top-level type-only import instead of inline type specifiers when there are only type imports.
97+
╭─[consistent_type_specifier_style.tsx:1:1]
98+
1 │ import { type Foo as Bar } from 'Foo';
99+
· ──────────────────────────────────────
100+
╰────
101+
help: Replace inline type specifiers with a top‐level import type statement.
102+
103+
⚠ import(consistent-type-specifier-style): Prefer using a top-level type-only import instead of inline type specifiers when there are only type imports.
104+
╭─[consistent_type_specifier_style.tsx:1:1]
105+
1 │ import { type Foo, type Bar } from 'Foo';
106+
· ─────────────────────────────────────────
107+
╰────
108+
help: Replace inline type specifiers with a top‐level import type statement.
109+
89110
⚠ import(consistent-type-specifier-style): Type imports from declaration files must use top-level `import type` syntax.
90111
╭─[consistent_type_specifier_style.tsx:1:10]
91112
1 │ import { type Foo } from './index.d.ts';
@@ -141,3 +162,10 @@ source: crates/oxc_linter/src/tester.rs
141162
· ────────
142163
╰────
143164
help: Replace inline type specifiers with a top-level import type statement.
165+
166+
⚠ import(consistent-type-specifier-style): Type imports from declaration files must use top-level `import type` syntax.
167+
╭─[consistent_type_specifier_style.tsx:1:15]
168+
1 │ import { Foo, type Bar } from './index.d.ts';
169+
· ────────
170+
╰────
171+
help: Replace inline type specifiers with a top-level import type statement.

‎npm/oxlint/configuration_schema.json‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12903,6 +12903,14 @@
1290312903
"prefer-inline"
1290412904
],
1290512905
"markdownDescription": "Prefer `import { type Foo } from 'foo'` for type imports."
12906+
},
12907+
{
12908+
"description": "Prefer `import type { Foo } from 'foo'` when all named imports are types, but allow\n`import { type Foo, bar } from 'foo'` when value imports are present.",
12909+
"type": "string",
12910+
"enum": [
12911+
"prefer-top-level-if-only-type-imports"
12912+
],
12913+
"markdownDescription": "Prefer `import type { Foo } from 'foo'` when all named imports are types, but allow\n`import { type Foo, bar } from 'foo'` when value imports are present."
1290612914
}
1290712915
]
1290812916
},

0 commit comments

Comments
 (0)