feat(span): store file extension in SourceType (#18893)

## Summary

Store the original file extension in `SourceType` when parsed from a path. This allows downstream code to distinguish between e.g. `.mts` and `.ts` files that resolved to `ModuleKind::Module`.

- Adds `extension: Option<FileExtension>` field to `SourceType`
- Adds `FileExtension` to AST schema with `#[ast]` attribute
- Adds `SourceType::extension()` getter method

related https://github.com/oxc-project/oxc/issues/15184

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Boshen committed 2026-02-03 15:20:18 +00:00
1 parent df2b7faad2
commit 18320c6fff
4 files changed
+62 -16

No files matched your search

@@ -9,7 +9,7 @@ use crate::ast::*;
#[cfg(target_pointer_width = "64")]
const _: () = {
// Padding: 1 bytes
// Padding: 0 bytes
assert!(size_of::<Program>() == 128);
assert!(align_of::<Program>() == 8);
assert!(offset_of!(Program, span) == 0);
@@ -1631,7 +1631,7 @@ const _: () = {
#[cfg(target_pointer_width = "32")]
const _: () = if cfg!(target_family = "wasm") || align_of::<u64>() == 8 {
// Padding: 1 bytes
// Padding: 0 bytes
assert!(size_of::<Program>() == 88);
assert!(align_of::<Program>() == 4);
assert!(offset_of!(Program, span) == 0);
@@ -16,11 +16,12 @@ const _: () = {
assert!(offset_of!(Span, end) == 4);
// Padding: 0 bytes
assert!(size_of::<SourceType>() == 3);
assert!(size_of::<SourceType>() == 4);
assert!(align_of::<SourceType>() == 1);
assert!(offset_of!(SourceType, language) == 0);
assert!(offset_of!(SourceType, module_kind) == 1);
assert!(offset_of!(SourceType, variant) == 2);
assert!(offset_of!(SourceType, extension) == 3);
assert!(size_of::<Language>() == 1);
assert!(align_of::<Language>() == 1);
@@ -30,6 +31,9 @@ const _: () = {
assert!(size_of::<LanguageVariant>() == 1);
assert!(align_of::<LanguageVariant>() == 1);
assert!(size_of::<FileExtension>() == 1);
assert!(align_of::<FileExtension>() == 1);
};
#[cfg(target_pointer_width = "32")]
@@ -41,11 +45,12 @@ const _: () = if cfg!(target_family = "wasm") || align_of::<u64>() == 8 {
assert!(offset_of!(Span, end) == 4);
// Padding: 0 bytes
assert!(size_of::<SourceType>() == 3);
assert!(size_of::<SourceType>() == 4);
assert!(align_of::<SourceType>() == 1);
assert!(offset_of!(SourceType, language) == 0);
assert!(offset_of!(SourceType, module_kind) == 1);
assert!(offset_of!(SourceType, variant) == 2);
assert!(offset_of!(SourceType, extension) == 3);
assert!(size_of::<Language>() == 1);
assert!(align_of::<Language>() == 1);
@@ -55,6 +60,9 @@ const _: () = if cfg!(target_family = "wasm") || align_of::<u64>() == 8 {
assert!(size_of::<LanguageVariant>() == 1);
assert!(align_of::<LanguageVariant>() == 1);
assert!(size_of::<FileExtension>() == 1);
assert!(align_of::<FileExtension>() == 1);
};
#[cfg(not(any(target_pointer_width = "64", target_pointer_width = "32")))]
@@ -16,6 +16,7 @@ impl<'a> Dummy<'a> for SourceType {
language: Dummy::dummy(allocator),
module_kind: Dummy::dummy(allocator),
variant: Dummy::dummy(allocator),
extension: Dummy::dummy(allocator),
}
}
}
@@ -49,3 +50,13 @@ impl<'a> Dummy<'a> for LanguageVariant {
Self::Standard
}
}
impl<'a> Dummy<'a> for FileExtension {
/// Create a dummy [`FileExtension`].
///
/// Does not allocate any data into arena.
#[inline(always)]
fn dummy(allocator: &'a Allocator) -> Self {
Self::Js
}
}
+39 -12
View File
@@ -30,6 +30,10 @@ pub struct SourceType {
/// Support JSX for JavaScript and TypeScript? default without JSX
#[estree(skip)]
pub(super) variant: LanguageVariant,
/// The original file extension, if parsed from a path.
#[estree(skip)]
pub(super) extension: Option<FileExtension>,
}
/// JavaScript or TypeScript
@@ -115,24 +119,26 @@ impl ContentEq for SourceType {
pub const VALID_EXTENSIONS: &[&str] = &["js", "mjs", "cjs", "jsx", "ts", "mts", "cts", "tsx"];
/// Valid file extension.
#[derive(Clone, Copy, PartialEq, Eq)]
#[ast]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[generate_derive(Dummy)]
pub enum FileExtension {
/// `.js` file extension
Js,
Js = 0,
/// `.mjs` file extension
Mjs,
Mjs = 1,
/// `.cjs` file extension
Cjs,
Cjs = 2,
/// `.jsx` file extension
Jsx,
Jsx = 3,
/// `.ts` file extension
Ts,
Ts = 4,
/// `.mts` file extension
Mts,
Mts = 5,
/// `.cts` file extension
Cts,
Cts = 6,
/// `.tsx` file extension
Tsx,
Tsx = 7,
}
impl FileExtension {
@@ -206,7 +212,7 @@ impl From<FileExtension> for SourceType {
Js | Mjs | Cjs | Ts | Mts | Cts => LanguageVariant::Standard,
};
SourceType { language, module_kind, variant }
SourceType { language, module_kind, variant, extension: Some(file_ext) }
}
}
@@ -234,6 +240,7 @@ impl SourceType {
language: Language::JavaScript,
module_kind: ModuleKind::CommonJS,
variant: LanguageVariant::Standard,
extension: None,
}
}
@@ -252,6 +259,7 @@ impl SourceType {
language: Language::JavaScript,
module_kind: ModuleKind::Module,
variant: LanguageVariant::Standard,
extension: None,
}
}
@@ -271,6 +279,7 @@ impl SourceType {
language: Language::JavaScript,
module_kind: ModuleKind::Script,
variant: LanguageVariant::Standard,
extension: None,
}
}
@@ -286,6 +295,7 @@ impl SourceType {
language: Language::JavaScript,
module_kind: ModuleKind::Unambiguous,
variant: LanguageVariant::Standard,
extension: None,
}
}
@@ -330,6 +340,7 @@ impl SourceType {
language: Language::TypeScript,
module_kind: ModuleKind::Unambiguous,
variant: LanguageVariant::Standard,
extension: None,
}
}
@@ -369,6 +380,7 @@ impl SourceType {
language: Language::TypeScriptDefinition,
module_kind: ModuleKind::Module,
variant: LanguageVariant::Standard,
extension: None,
}
}
@@ -437,6 +449,11 @@ impl SourceType {
self.is_module()
}
/// Returns the original file extension if this source type was created from a path.
pub fn extension(self) -> Option<FileExtension> {
self.extension
}
/// Mark this [`SourceType`] as a [script] if `yes` is `true`. No change
/// will occur if `yes` is `false`.
///
@@ -655,7 +672,7 @@ impl Error for UnknownExtension {}
#[cfg(test)]
mod tests {
use super::SourceType;
use super::{FileExtension, SourceType};
#[test]
fn test_ts_from_path() {
@@ -674,6 +691,12 @@ mod tests {
assert!(!ty.is_javascript());
}
// Verify extension is captured
assert_eq!(ts.extension(), Some(FileExtension::Ts));
assert_eq!(mts.extension(), Some(FileExtension::Mts));
assert_eq!(cts.extension(), Some(FileExtension::Cts));
assert_eq!(tsx.extension(), Some(FileExtension::Tsx));
// .ts and .tsx use Unambiguous (content-based detection)
assert!(ts.is_unambiguous());
assert!(tsx.is_unambiguous());
@@ -741,7 +764,11 @@ mod tests {
assert!(!ty.is_typescript(), "{ty:?}");
}
assert_eq!(SourceType::mjs(), mjs);
// Verify extension is captured
assert_eq!(mjs.extension(), Some(FileExtension::Mjs));
assert_eq!(cjs.extension(), Some(FileExtension::Cjs));
assert_eq!(js.extension(), Some(FileExtension::Js));
assert_eq!(jsx.extension(), Some(FileExtension::Jsx));
// .js and .jsx use Unambiguous (content-based detection)
assert!(js.is_unambiguous());