feat(span): extract TS declaration file check to its own function (#17037)

Extracted the `is_dts` check that was internal to `SourceType::from_path` to be its own function at `FileExtension::is_ts_declaration`. This allows us to reuse the declaration file extension checking code outside of just `oxc_span` in lint rules, for example.

Since I exported `FileExtension` from `oxc_span`, it required adding documentation for some variants and also changing `from_str` to be an implementation of the `FromStr` trait instead. This entailed some refactoring because `from_str` now returns `Result` instead of `Option`.
This commit is contained in:
camchenry committed 2025-12-18 09:18:22 +00:00
1 parent dab4780527
commit 55868235b3
2 files changed
+111 -43

No files matched your search

+2 -1
View File
@@ -14,7 +14,8 @@ pub use atom::Atom;
pub use cmp::ContentEq;
pub use compact_str::{CompactStr, MAX_INLINE_LEN as ATOM_MAX_INLINE_LEN};
pub use source_type::{
Language, LanguageVariant, ModuleKind, SourceType, UnknownExtension, VALID_EXTENSIONS,
FileExtension, Language, LanguageVariant, ModuleKind, SourceType, UnknownExtension,
VALID_EXTENSIONS,
};
pub use span::{GetSpan, GetSpanMut, SPAN, Span};
+109 -42
View File
@@ -4,6 +4,7 @@ use std::{
fmt::{self, Display},
ops::Deref,
path::Path,
str::FromStr,
};
use oxc_allocator::{Allocator, CloneIn, Dummy};
@@ -105,31 +106,69 @@ pub const VALID_EXTENSIONS: &[&str] = &["js", "mjs", "cjs", "jsx", "ts", "mts",
/// Valid file extension.
#[derive(Clone, Copy, PartialEq, Eq)]
enum FileExtension {
pub enum FileExtension {
/// `.js` file extension
Js,
/// `.mjs` file extension
Mjs,
/// `.cjs` file extension
Cjs,
/// `.jsx` file extension
Jsx,
/// `.ts` file extension
Ts,
/// `.mts` file extension
Mts,
/// `.cts` file extension
Cts,
/// `.tsx` file extension
Tsx,
}
impl FileExtension {
fn from_str(extension: &str) -> Option<Self> {
let file_ext = match extension {
"js" => Self::Js,
"mjs" => Self::Mjs,
"cjs" => Self::Cjs,
"jsx" => Self::Jsx,
"ts" => Self::Ts,
"mts" => Self::Mts,
"cts" => Self::Cts,
"tsx" => Self::Tsx,
_ => return None,
};
Some(file_ext)
/// Checks if this file extension represents a TypeScript declaration file
/// given its full file name.
///
/// # Examples
/// - `index.d.ts` -> true
/// - `index.ts` -> false
/// - `index.d.mts` -> true
/// - `index.mts` -> false
/// - `index.d.css.ts` -> true
/// - `index.d.css.mts` -> false
pub fn is_ts_declaration(self, file_name: &str) -> bool {
match self {
// https://www.typescriptlang.org/tsconfig/#allowArbitraryExtensions
// `{file basename}.d.{extension}.ts`
// https://github.com/microsoft/TypeScript/issues/50133
FileExtension::Ts => {
file_name[..file_name.len() - 3].split('.').rev().take(2).any(|c| c == "d")
}
FileExtension::Mts | FileExtension::Cts =>
{
#[expect(clippy::case_sensitive_file_extension_comparisons)]
file_name[..file_name.len() - 4].ends_with(".d")
}
_ => false,
}
}
}
impl FromStr for FileExtension {
type Err = UnknownExtension;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"js" => Ok(FileExtension::Js),
"mjs" => Ok(FileExtension::Mjs),
"cjs" => Ok(FileExtension::Cjs),
"jsx" => Ok(FileExtension::Jsx),
"ts" => Ok(FileExtension::Ts),
"mts" => Ok(FileExtension::Mts),
"cts" => Ok(FileExtension::Cts),
"tsx" => Ok(FileExtension::Tsx),
_ => Err(UnknownExtension::new("Unknown extension.")),
}
}
}
@@ -507,33 +546,19 @@ impl SourceType {
.and_then(std::ffi::OsStr::to_str)
.ok_or_else(|| UnknownExtension::new("Please provide a valid file name."))?;
let file_ext =
path.as_ref().extension().and_then(std::ffi::OsStr::to_str).and_then(FileExtension::from_str).ok_or_else(|| {
let path = path.as_ref().to_string_lossy();
UnknownExtension::new(
format!("Please provide a valid file extension for {path}: .js, .mjs, .jsx or .cjs for JavaScript, or .ts, .d.ts, .mts, .cts or .tsx for TypeScript"),
)
})?;
let mut source_type = SourceType::from(file_ext);
let is_dts = match file_ext {
// https://www.typescriptlang.org/tsconfig/#allowArbitraryExtensions
// `{file basename}.d.{extension}.ts`
// https://github.com/microsoft/TypeScript/issues/50133
FileExtension::Ts => {
file_name[..file_name.len() - 3].split('.').rev().take(2).any(|c| c == "d")
}
FileExtension::Mts | FileExtension::Cts =>
{
#[expect(clippy::case_sensitive_file_extension_comparisons)]
file_name[..file_name.len() - 4].ends_with(".d")
}
_ => false,
let Some(Ok(file_ext)) = path
.as_ref()
.extension()
.and_then(std::ffi::OsStr::to_str)
.map(FileExtension::from_str)
else {
return Err(UnknownExtension::new(
"Please provide a valid file extension: .js, .mjs, .jsx or .cjs for JavaScript, or .ts, .d.ts, .mts, .cts or .tsx for TypeScript",
));
};
if is_dts {
source_type.language = Language::TypeScriptDefinition;
}
let source_type = SourceType::from(file_ext)
.with_typescript_definition(file_ext.is_ts_declaration(file_name));
Ok(source_type)
}
@@ -548,8 +573,8 @@ impl SourceType {
/// extensions.
pub fn from_extension(extension: &str) -> Result<Self, UnknownExtension> {
match FileExtension::from_str(extension) {
Some(file_ext) => Ok(SourceType::from(file_ext)),
None => Err(UnknownExtension::new("Unknown extension.")),
Ok(file_ext) => Ok(SourceType::from(file_ext)),
Err(_) => Err(UnknownExtension::new("Unknown extension.")),
}
}
}
@@ -692,3 +717,45 @@ mod tests {
assert!(jsx.is_jsx());
}
}
#[cfg(test)]
mod file_extension_tests {
use std::{path::Path, str::FromStr};
use super::FileExtension;
#[test]
fn test_is_ts_declaration() {
let cases = vec![
("index.d.ts", true),
("index.ts", false),
("index.d.mts", true),
("index.mts", false),
("index.d.cts", true),
("index.cts", false),
("index.d.js", false),
("index.js", false),
("index.d.jsx", false),
("index.jsx", false),
("index.d.css.ts", true),
("index.d.css.mts", false),
];
for (file_name, expected) in cases {
let ext = Path::new(file_name)
.extension()
.and_then(std::ffi::OsStr::to_str)
.expect("File name should have an extension.");
let ext = FileExtension::from_str(ext).expect("Extension should be valid.");
assert_eq!(
ext.is_ts_declaration(file_name),
expected,
"Expected is_ts_declaration to be {} for file name {} but got {}",
expected,
file_name,
!expected
);
}
}
}