Skip to content

Commit 209d006

Browse files
committed
fix(linter): parse vue lang attribute without quotes (#12517)
1 parent 8912134 commit 209d006

File tree

1 file changed

+40
-10
lines changed
  • crates/oxc_linter/src/loader/partial_loader

1 file changed

+40
-10
lines changed

crates/oxc_linter/src/loader/partial_loader/vue.rs

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,8 @@ impl<'a> VuePartialLoader<'a> {
5151
let content = &self.source_text[*pointer..*pointer + offset];
5252

5353
// parse `lang`
54-
let lang = content.split_once("lang").map_or(Some("mjs"), |(_, s)| {
55-
const QUOTES: [char; 2] = ['"', '\''];
56-
s.trim_start()
57-
.trim_start_matches('=')
58-
.trim_start()
59-
.trim_start_matches(QUOTES)
60-
.split_once(QUOTES)
61-
.map(|(s, _)| s)
62-
})?;
54+
let lang = Self::extract_lang_attribute(content);
55+
6356
let Ok(mut source_type) = SourceType::from_extension(lang) else { return None };
6457
if !lang.contains('x') {
6558
source_type = source_type.with_standard(true);
@@ -79,6 +72,43 @@ impl<'a> VuePartialLoader<'a> {
7972
#[expect(clippy::cast_possible_truncation)]
8073
Some(JavaScriptSource::partial(source_text, source_type, js_start as u32))
8174
}
75+
76+
fn extract_lang_attribute(content: &str) -> &str {
77+
let content = content.trim();
78+
79+
let Some(lang_index) = content.find("lang") else { return "mjs" };
80+
81+
// Move past "lang"
82+
let mut rest = content[lang_index + 4..].trim_start();
83+
84+
if !rest.starts_with('=') {
85+
return "mjs";
86+
}
87+
88+
// Move past "="
89+
rest = rest[1..].trim_start();
90+
91+
let first_char = rest.chars().next();
92+
93+
match first_char {
94+
Some('"' | '\'') => {
95+
let quote = first_char.unwrap();
96+
rest = &rest[1..];
97+
match rest.find(quote) {
98+
Some(end) => &rest[..end],
99+
None => "mjs", // Unterminated quote
100+
}
101+
}
102+
Some(_) => {
103+
// Unquoted value: take until first whitespace or attribute separator
104+
match rest.find(|c: char| c.is_whitespace() || c == '>') {
105+
Some(end) => &rest[..end],
106+
None => rest, // whole rest is the lang value
107+
}
108+
}
109+
None => "mjs", // nothing after =
110+
}
111+
}
82112
}
83113

84114
#[cfg(test)]
@@ -257,11 +287,11 @@ mod test {
257287
("<script>debugger</script>", Some(SourceType::mjs())),
258288
("<script lang = 'tsx' >debugger</script>", Some(SourceType::tsx())),
259289
(r#"<script lang = "cjs" >debugger</script>"#, Some(SourceType::cjs())),
290+
("<script lang=tsx>debugger</script>", Some(SourceType::tsx())),
260291
("<script lang = 'xxx'>debugger</script>", None),
261292
(r#"<script lang = "xxx">debugger</script>"#, None),
262293
("<script lang='xxx'>debugger</script>", None),
263294
(r#"<script lang="xxx">debugger</script>"#, None),
264-
("<script lang=tsx>debugger</script>", None), // this is valid but too compliated to parse
265295
];
266296

267297
for (source_text, source_type) in cases {

0 commit comments

Comments
 (0)