diff --git a/crates/oxc_linter/src/loader/partial_loader/vue.rs b/crates/oxc_linter/src/loader/partial_loader/vue.rs
index 2d145479eb52b..3a6a1a41a6c95 100644
--- a/crates/oxc_linter/src/loader/partial_loader/vue.rs
+++ b/crates/oxc_linter/src/loader/partial_loader/vue.rs
@@ -1,4 +1,5 @@
use memchr::memmem::Finder;
+
use oxc_span::SourceType;
use super::{JavaScriptSource, SCRIPT_END, SCRIPT_START, find_script_closing_angle};
@@ -48,8 +49,21 @@ impl<'a> VuePartialLoader<'a> {
// get ts and jsx attribute
let content = &self.source_text[*pointer..*pointer + offset];
- let is_ts = content.contains("ts");
- let is_jsx = content.contains("tsx") || content.contains("jsx");
+
+ // parse `lang`
+ let lang = content.split_once("lang").map_or(Some("mjs"), |(_, s)| {
+ const QUOTES: [char; 2] = ['"', '\''];
+ s.trim_start()
+ .trim_start_matches('=')
+ .trim_start()
+ .trim_start_matches(QUOTES)
+ .split_once(QUOTES)
+ .map(|(s, _)| s)
+ })?;
+ let Ok(mut source_type) = SourceType::from_extension(lang) else { return None };
+ if !lang.contains('x') {
+ source_type = source_type.with_standard(true);
+ }
*pointer += offset + 1;
let js_start = *pointer;
@@ -61,7 +75,6 @@ impl<'a> VuePartialLoader<'a> {
*pointer += offset + SCRIPT_END.len();
let source_text = &self.source_text[js_start..js_end];
- let source_type = SourceType::mjs().with_typescript(is_ts).with_jsx(is_jsx);
// NOTE: loader checked that source_text.len() is less than u32::MAX
#[expect(clippy::cast_possible_truncation)]
Some(JavaScriptSource::partial(source_text, source_type, js_start as u32))
@@ -70,6 +83,8 @@ impl<'a> VuePartialLoader<'a> {
#[cfg(test)]
mod test {
+ use oxc_span::SourceType;
+
use super::{JavaScriptSource, VuePartialLoader};
fn parse_vue(source_text: &str) -> JavaScriptSource<'_> {
@@ -99,20 +114,7 @@ mod test {
"#;
let result = parse_vue(source_text);
- assert!(result.source_type.is_typescript());
- assert_eq!(result.source_text.trim(), "1/1");
- }
-
- #[test]
- fn test_build_vue_with_ts_flag_2() {
- let source_text = r"
-
- ";
-
- let result = parse_vue(source_text);
- assert!(result.source_type.is_typescript());
+ assert_eq!(result.source_type, SourceType::ts());
assert_eq!(result.source_text.trim(), "1/1");
}
@@ -125,21 +127,7 @@ mod test {
";
let result = parse_vue(source_text);
- assert!(result.source_type.is_typescript());
- assert_eq!(result.source_text.trim(), "1/1");
- }
-
- #[test]
- fn test_build_vue_with_tsx_flag() {
- let source_text = r"
-
- ";
-
- let result = parse_vue(source_text);
- assert!(result.source_type.is_jsx());
- assert!(result.source_type.is_typescript());
+ assert_eq!(result.source_type, SourceType::ts());
assert_eq!(result.source_text.trim(), "1/1");
}
@@ -252,4 +240,28 @@ mod test {
assert_eq!(sources.len(), 1);
assert_eq!(sources[0].source_text, "a");
}
+
+ #[test]
+ fn lang() {
+ let cases = [
+ ("", Some(SourceType::mjs())),
+ ("", Some(SourceType::tsx())),
+ (r#""#, Some(SourceType::cjs())),
+ ("", None),
+ (r#""#, None),
+ ("", None),
+ (r#""#, None),
+ ("", None), // this is valid but too compliated to parse
+ ];
+
+ for (source_text, source_type) in cases {
+ let sources = VuePartialLoader::new(source_text).parse();
+ if let Some(expected) = source_type {
+ assert_eq!(sources.len(), 1);
+ assert_eq!(sources[0].source_type, expected);
+ } else {
+ assert_eq!(sources.len(), 0);
+ }
+ }
+ }
}
diff --git a/crates/oxc_span/src/source_type/mod.rs b/crates/oxc_span/src/source_type/mod.rs
index e672488e5c36a..4a081783921ad 100644
--- a/crates/oxc_span/src/source_type/mod.rs
+++ b/crates/oxc_span/src/source_type/mod.rs
@@ -462,36 +462,44 @@ impl SourceType {
)
})?;
+ let mut source_type = Self::from_extension(extension)?;
+
+ #[expect(clippy::case_sensitive_file_extension_comparisons)]
+ if match extension {
+ "ts" if file_name[..file_name.len() - 3].split('.').rev().take(2).any(|c| c == "d") => {
+ true
+ }
+ "mts" | "cts" if file_name[..file_name.len() - 4].ends_with(".d") => true,
+ _ => false,
+ } {
+ source_type.language = Language::TypeScriptDefinition;
+ }
+
+ Ok(source_type)
+ }
+
+ /// Converts a file extension to [`SourceType`].
+ ///
+ /// # Errors
+ ///
+ /// Returns [`UnknownExtension`] if:
+ /// * the file extension is not one of "js", "mjs", "cjs", "jsx", "ts",
+ /// "mts", "cts", "tsx". See [`VALID_EXTENSIONS`] for the list of valid
+ /// extensions.
+ pub fn from_extension(extension: &str) -> Result {
let module_kind = match extension {
"js" | "tsx" | "ts" | "jsx" | "mts" | "mjs" => ModuleKind::Module,
"cjs" | "cts" => ModuleKind::Script,
- _ => unreachable!(),
+ _ => return Err(UnknownExtension::new("Unknown extension.")),
};
let language = match extension {
// https://www.typescriptlang.org/tsconfig/#allowArbitraryExtensions
// `{file basename}.d.{extension}.ts`
// https://github.com/microsoft/TypeScript/issues/50133
- "ts" => {
- if file_name[..file_name.len() - 3].split('.').rev().take(2).any(|c| c == "d") {
- Language::TypeScriptDefinition
- } else {
- Language::TypeScript
- }
- }
"js" | "cjs" | "mjs" | "jsx" => Language::JavaScript,
- "tsx" => Language::TypeScript,
- #[expect(clippy::case_sensitive_file_extension_comparisons)]
- "mts" | "cts" => {
- if file_name[..file_name.len() - 4].ends_with(".d") {
- Language::TypeScriptDefinition
- } else {
- Language::TypeScript
- }
- }
- _ => {
- unreachable!();
- }
+ "ts" | "tsx" | "mts" | "cts" => Language::TypeScript,
+ _ => return Err(UnknownExtension::new("Unknown extension.")),
};
let variant = match extension {