mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
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:
1 parent
dab4780527
commit
55868235b3
2 files changed
+111
-43
No files matched your search
@@ -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};
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user