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 {