diff --git a/crates/oxc_linter/src/loader/partial_loader/vue.rs b/crates/oxc_linter/src/loader/partial_loader/vue.rs
index f86f9a5b70e6e..70caba8c0702e 100644
--- a/crates/oxc_linter/src/loader/partial_loader/vue.rs
+++ b/crates/oxc_linter/src/loader/partial_loader/vue.rs
@@ -51,15 +51,8 @@ impl<'a> VuePartialLoader<'a> {
let content = &self.source_text[*pointer..*pointer + offset];
// 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 lang = Self::extract_lang_attribute(content);
+
let Ok(mut source_type) = SourceType::from_extension(lang) else { return None };
if !lang.contains('x') {
source_type = source_type.with_standard(true);
@@ -79,6 +72,43 @@ impl<'a> VuePartialLoader<'a> {
#[expect(clippy::cast_possible_truncation)]
Some(JavaScriptSource::partial(source_text, source_type, js_start as u32))
}
+
+ fn extract_lang_attribute(content: &str) -> &str {
+ let content = content.trim();
+
+ let Some(lang_index) = content.find("lang") else { return "mjs" };
+
+ // Move past "lang"
+ let mut rest = content[lang_index + 4..].trim_start();
+
+ if !rest.starts_with('=') {
+ return "mjs";
+ }
+
+ // Move past "="
+ rest = rest[1..].trim_start();
+
+ let first_char = rest.chars().next();
+
+ match first_char {
+ Some('"' | '\'') => {
+ let quote = first_char.unwrap();
+ rest = &rest[1..];
+ match rest.find(quote) {
+ Some(end) => &rest[..end],
+ None => "mjs", // Unterminated quote
+ }
+ }
+ Some(_) => {
+ // Unquoted value: take until first whitespace or attribute separator
+ match rest.find(|c: char| c.is_whitespace() || c == '>') {
+ Some(end) => &rest[..end],
+ None => rest, // whole rest is the lang value
+ }
+ }
+ None => "mjs", // nothing after =
+ }
+ }
}
#[cfg(test)]
@@ -257,11 +287,11 @@ mod test {
("", Some(SourceType::mjs())),
("", Some(SourceType::tsx())),
(r#""#, Some(SourceType::cjs())),
+ ("", Some(SourceType::tsx())),
("", None),
(r#""#, None),
("", None),
(r#""#, None),
- ("", None), // this is valid but too compliated to parse
];
for (source_text, source_type) in cases {