diff --git a/crates/oxc_linter/src/rules/import/consistent_type_specifier_style.rs b/crates/oxc_linter/src/rules/import/consistent_type_specifier_style.rs index 4153c71e25f06..bff12016653c2 100644 --- a/crates/oxc_linter/src/rules/import/consistent_type_specifier_style.rs +++ b/crates/oxc_linter/src/rules/import/consistent_type_specifier_style.rs @@ -1,3 +1,5 @@ +use std::{path::Path, str::FromStr}; + use oxc_allocator::{Allocator, CloneIn}; use oxc_ast::{ AstBuilder, AstKind, @@ -6,7 +8,7 @@ use oxc_ast::{ use oxc_codegen::{Context, Gen}; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; -use oxc_span::{GetSpan, SPAN, Span}; +use oxc_span::{FileExtension, GetSpan, SPAN, Span}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -18,6 +20,14 @@ use crate::{ rule::{DefaultRuleConfig, Rule}, }; +fn use_top_level_for_declaration_file_import_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn( + "Type imports from declaration files must use top-level `import type` syntax.", + ) + .with_help("Replace inline type specifiers with a top-level import type statement.") + .with_label(span) +} + fn consistent_type_specifier_style_diagnostic(span: Span, mode: &Mode) -> OxcDiagnostic { let (warn_msg, help_msg) = if *mode == Mode::PreferInline { ( @@ -109,36 +119,44 @@ impl Rule for ConsistentTypeSpecifierStyle { { return; } - if self.0 == Mode::PreferTopLevel && import_decl.import_kind.is_value() { + + // Declaration file imports should always be top-level type imports + if (self.0 == Mode::PreferTopLevel || is_declaration_file_import(import_decl)) + && import_decl.import_kind.is_value() + { let (value_specifiers, type_specifiers) = split_import_specifiers_by_kind(specifiers); if type_specifiers.is_empty() { return; } for item in &type_specifiers { - ctx.diagnostic_with_fix( - consistent_type_specifier_style_diagnostic(item.span(), &self.0), - |fixer| { - let mut import_source = String::new(); + let diagnostic = if is_declaration_file_import(import_decl) { + use_top_level_for_declaration_file_import_diagnostic(item.span()) + } else { + consistent_type_specifier_style_diagnostic(item.span(), &self.0) + }; + ctx.diagnostic_with_fix(diagnostic, |fixer| { + let mut import_source = String::new(); - if !value_specifiers.is_empty() { - let value_import_declaration = - gen_value_import_declaration(fixer, import_decl, &value_specifiers); - import_source.push_str(&value_import_declaration); - } + if !value_specifiers.is_empty() { + let value_import_declaration = + gen_value_import_declaration(fixer, import_decl, &value_specifiers); + import_source.push_str(&value_import_declaration); + } - let type_import_declaration = - gen_type_import_declaration(fixer, import_decl, &type_specifiers); - import_source.push_str(&type_import_declaration); + let type_import_declaration = + gen_type_import_declaration(fixer, import_decl, &type_specifiers); + import_source.push_str(&type_import_declaration); - fixer - .replace(import_decl.span, import_source.trim_end().to_string()) - .with_message("Convert to a `top-level` type import") - }, - ); + fixer + .replace(import_decl.span, import_source.trim_end().to_string()) + .with_message("Convert to a `top-level` type import") + }); + } + } else if self.0 == Mode::PreferInline && import_decl.import_kind.is_type() { + if is_declaration_file_import(import_decl) { + return; } - } - if self.0 == Mode::PreferInline && import_decl.import_kind.is_type() { ctx.diagnostic_with_fix( consistent_type_specifier_style_diagnostic(import_decl.span, &self.0), |fixer| { @@ -247,6 +265,24 @@ fn gen_type_import_declaration<'c, 'a: 'c>( codegen.into_source_text() } +fn is_declaration_file_import(import_decl: &ImportDeclaration) -> bool { + let source = &import_decl.source.value; + // Relatively fast check to avoid unnecessary Path and extension parsing + // if it doesn't even look like a declaration file import + if !source.contains(".d") { + return false; + } + // Slower check that parses the file name to check if it's a declaration file + let path = Path::new(source.as_str()); + let Some(extension) = path.extension().and_then(std::ffi::os_str::OsStr::to_str) else { + return false; + }; + match FileExtension::from_str(extension) { + Ok(file_ext) => file_ext.is_ts_declaration(source), + Err(_) => false, + } +} + #[test] fn test() { use crate::tester::Tester; @@ -277,6 +313,15 @@ fn test() { ("import { type Foo as Bar } from 'Foo';", Some(json!(["prefer-inline"]))), ("import { type Foo, type Bar, Baz, Bam } from 'Foo';", Some(json!(["prefer-inline"]))), ("import type * as Foo from 'Foo';", None), + // declaration files always require `import type` syntax + ("import type { Foo } from './index.d.ts';", Some(json!(["prefer-top-level"]))), + ("import type { Foo } from './index.d.ts';", Some(json!(["prefer-inline"]))), + ("import type { Foo } from './index.d.mts';", Some(json!(["prefer-top-level"]))), + ("import type { Foo } from './index.d.mts';", Some(json!(["prefer-inline"]))), + ("import type { Foo } from './index.d.cts';", Some(json!(["prefer-top-level"]))), + ("import type { Foo } from './index.d.cts';", Some(json!(["prefer-inline"]))), + ("import type { Foo } from './app.d.css.ts';", Some(json!(["prefer-top-level"]))), + ("import type { Foo } from './app.d.css.ts';", Some(json!(["prefer-inline"]))), ]; let fail = vec![ @@ -290,6 +335,15 @@ fn test() { ("import Foo, { type Bar, Baz } from 'Foo';", None), ("import { Component, type ComponentProps } from 'package-1';", None), ("import type { Foo, Bar, Baz } from 'Foo';", Some(json!(["prefer-inline"]))), + // declaration files always require `import type` syntax + ("import { type Foo } from './index.d.ts';", Some(json!(["prefer-top-level"]))), + ("import { type Foo } from './index.d.ts';", Some(json!(["prefer-inline"]))), + ("import { type Foo } from './index.d.mts';", Some(json!(["prefer-top-level"]))), + ("import { type Foo } from './index.d.mts';", Some(json!(["prefer-inline"]))), + ("import { type Foo } from './index.d.cts';", Some(json!(["prefer-top-level"]))), + ("import { type Foo } from './index.d.cts';", Some(json!(["prefer-inline"]))), + ("import { type Foo } from './app.d.css.ts';", Some(json!(["prefer-top-level"]))), + ("import { type Foo } from './app.d.css.ts';", Some(json!(["prefer-inline"]))), ]; let fix = vec![ @@ -371,6 +425,16 @@ fn test() { "import Foo, { Baz } from 'Foo';\nimport type { Bar } from 'Foo';", Some(json!(["prefer-top-level"])), ), + ( + "import { type Foo } from 'index.d.ts';", + "import type { Foo } from 'index.d.ts';", + Some(json!(["prefer-top-level"])), + ), + ( + "import { type Foo } from 'index.d.ts';", + "import type { Foo } from 'index.d.ts';", + Some(json!(["prefer-inline"])), + ), ]; Tester::new( diff --git a/crates/oxc_linter/src/snapshots/import_consistent_type_specifier_style.snap b/crates/oxc_linter/src/snapshots/import_consistent_type_specifier_style.snap index 79a13e175b4bb..7c4bda2780a62 100644 --- a/crates/oxc_linter/src/snapshots/import_consistent_type_specifier_style.snap +++ b/crates/oxc_linter/src/snapshots/import_consistent_type_specifier_style.snap @@ -84,3 +84,59 @@ source: crates/oxc_linter/src/tester.rs · ───────────────────────────────────────── ╰──── help: Replace top‐level import type with an inline type specifier. + + ⚠ eslint-plugin-import(consistent-type-specifier-style): Type imports from declaration files must use top-level `import type` syntax. + ╭─[consistent_type_specifier_style.tsx:1:10] + 1 │ import { type Foo } from './index.d.ts'; + · ──────── + ╰──── + help: Replace inline type specifiers with a top-level import type statement. + + ⚠ eslint-plugin-import(consistent-type-specifier-style): Type imports from declaration files must use top-level `import type` syntax. + ╭─[consistent_type_specifier_style.tsx:1:10] + 1 │ import { type Foo } from './index.d.ts'; + · ──────── + ╰──── + help: Replace inline type specifiers with a top-level import type statement. + + ⚠ eslint-plugin-import(consistent-type-specifier-style): Type imports from declaration files must use top-level `import type` syntax. + ╭─[consistent_type_specifier_style.tsx:1:10] + 1 │ import { type Foo } from './index.d.mts'; + · ──────── + ╰──── + help: Replace inline type specifiers with a top-level import type statement. + + ⚠ eslint-plugin-import(consistent-type-specifier-style): Type imports from declaration files must use top-level `import type` syntax. + ╭─[consistent_type_specifier_style.tsx:1:10] + 1 │ import { type Foo } from './index.d.mts'; + · ──────── + ╰──── + help: Replace inline type specifiers with a top-level import type statement. + + ⚠ eslint-plugin-import(consistent-type-specifier-style): Type imports from declaration files must use top-level `import type` syntax. + ╭─[consistent_type_specifier_style.tsx:1:10] + 1 │ import { type Foo } from './index.d.cts'; + · ──────── + ╰──── + help: Replace inline type specifiers with a top-level import type statement. + + ⚠ eslint-plugin-import(consistent-type-specifier-style): Type imports from declaration files must use top-level `import type` syntax. + ╭─[consistent_type_specifier_style.tsx:1:10] + 1 │ import { type Foo } from './index.d.cts'; + · ──────── + ╰──── + help: Replace inline type specifiers with a top-level import type statement. + + ⚠ eslint-plugin-import(consistent-type-specifier-style): Type imports from declaration files must use top-level `import type` syntax. + ╭─[consistent_type_specifier_style.tsx:1:10] + 1 │ import { type Foo } from './app.d.css.ts'; + · ──────── + ╰──── + help: Replace inline type specifiers with a top-level import type statement. + + ⚠ eslint-plugin-import(consistent-type-specifier-style): Type imports from declaration files must use top-level `import type` syntax. + ╭─[consistent_type_specifier_style.tsx:1:10] + 1 │ import { type Foo } from './app.d.css.ts'; + · ──────── + ╰──── + help: Replace inline type specifiers with a top-level import type statement.