fix(parse/html/vue): parse dynamic slot directives that contain quotes#8856
fix(parse/html/vue): parse dynamic slot directives that contain quotes#8856
Conversation
🦋 Changeset detectedLatest commit: 368c9ee The changes in this PR will be included in the next version bump. This PR includes changesets to release 13 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
WalkthroughAdds a new lexing context Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In @.changeset/fair-ligers-smell.md:
- Line 5: Update the issue reference in the changeset content: replace the
incorrect "[`#8803`]" with the correct "[`#8710`]" in the sentence "Fixed
[`#8803`](https://github.com/biomejs/biome/issues/8803): Biome now parses Vue
dynamic slot shorthand arguments..." so the changeset points to issue `#8710`
instead of `#8803`.
35c39b3 to
48b8dc0
Compare
ematipico
left a comment
There was a problem hiding this comment.
Damn, these are very complex cases... 🤣
48b8dc0 to
368c9ee
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@crates/biome_html_parser/src/lexer/mod.rs`:
- Around line 158-192: The quote state in consume_token_vue_directive_argument
is toggled blindly by QuotesSeen::check_byte, so escaped quotes like \" or \'
break tracking and let a ] inside a quoted string close the argument; update
QuotesSeen::check_byte (or add an escape-aware layer in
consume_token_vue_directive_argument using current_byte/peek_previous logic) to
ignore quote bytes that are escaped (handle backslash escaping, including
escaped backslashes) while preserving normal toggling otherwise, and add a
regression test for HtmlLexContext::VueDirectiveArgument that includes escaped
quotes inside brackets to ensure the parser does not prematurely close on ].
| /// Consume a token in the [HtmlLexContext::VueDirectiveArgument] context. | ||
| fn consume_token_vue_directive_argument(&mut self) -> HtmlSyntaxKind { | ||
| let start = self.text_position(); | ||
| let mut brackets_stack = 0; | ||
| let mut quotes_seen = QuotesSeen::new(); | ||
|
|
||
| while let Some(byte) = self.current_byte() { | ||
| quotes_seen.check_byte(byte); | ||
| let char = biome_unicode_table::lookup_byte(byte); | ||
| use biome_unicode_table::Dispatch::*; | ||
|
|
||
| if quotes_seen.is_empty() { | ||
| match char { | ||
| BTO => { | ||
| brackets_stack += 1; | ||
| } | ||
| BTC => { | ||
| if brackets_stack == 0 { | ||
| break; | ||
| } | ||
| brackets_stack -= 1; | ||
| } | ||
| _ => {} | ||
| } | ||
| } | ||
|
|
||
| self.advance_byte_or_char(byte); | ||
| } | ||
|
|
||
| if self.text_position() != start { | ||
| HTML_LITERAL | ||
| } else { | ||
| ERROR_TOKEN | ||
| } | ||
| } |
There was a problem hiding this comment.
❓ Verification inconclusive
Script executed:
# Find and inspect the QuotesSeen implementation
rg -A 10 "struct QuotesSeen" crates/biome_html_parser/src/Repository: biomejs/biome
Repository: biomejs/biome
Exit code: 0
stdout:
crates/biome_html_parser/src/lexer/mod.rs:struct QuotesSeen {
crates/biome_html_parser/src/lexer/mod.rs- single: u16,
crates/biome_html_parser/src/lexer/mod.rs- double: u16,
crates/biome_html_parser/src/lexer/mod.rs- template: u16,
crates/biome_html_parser/src/lexer/mod.rs- inside_comment: bool,
crates/biome_html_parser/src/lexer/mod.rs- prev_byte: Option<u8>,
crates/biome_html_parser/src/lexer/mod.rs-}
crates/biome_html_parser/src/lexer/mod.rs-
crates/biome_html_parser/src/lexer/mod.rs-impl QuotesSeen {
crates/biome_html_parser/src/lexer/mod.rs- fn new() -> Self {
crates/biome_html_parser/src/lexer/mod.rs- Self {
Script executed:
# Check the full consume_token_vue_directive_argument function and surrounding context
cat -n crates/biome_html_parser/src/lexer/mod.rs | sed -n '150,200p'Repository: biomejs/biome
Repository: biomejs/biome
Exit code: 0
stdout:
150 }
151 _ if (self.current_kind != T![<] && is_attribute_name_byte_vue(current)) => {
152 self.consume_identifier(current, IdentifierContext::Vue)
153 }
154 _ => self.consume_unexpected_character(),
155 }
156 }
157
158 /// Consume a token in the [HtmlLexContext::VueDirectiveArgument] context.
159 fn consume_token_vue_directive_argument(&mut self) -> HtmlSyntaxKind {
160 let start = self.text_position();
161 let mut brackets_stack = 0;
162 let mut quotes_seen = QuotesSeen::new();
163
164 while let Some(byte) = self.current_byte() {
165 quotes_seen.check_byte(byte);
166 let char = biome_unicode_table::lookup_byte(byte);
167 use biome_unicode_table::Dispatch::*;
168
169 if quotes_seen.is_empty() {
170 match char {
171 BTO => {
172 brackets_stack += 1;
173 }
174 BTC => {
175 if brackets_stack == 0 {
176 break;
177 }
178 brackets_stack -= 1;
179 }
180 _ => {}
181 }
182 }
183
184 self.advance_byte_or_char(byte);
185 }
186
187 if self.text_position() != start {
188 HTML_LITERAL
189 } else {
190 ERROR_TOKEN
191 }
192 }
193
194 /// Consume a token in the [HtmlLexContext::Regular] context.
195 fn consume_token(&mut self, current: u8) -> HtmlSyntaxKind {
196 match current {
197 b'\n' | b'\r' | b'\t' | b' ' => self.consume_newline_or_whitespaces(),
198 b'!' if self.current() == T![<] => self.consume_byte(T![!]),
199 b'/' if self.current() == T![<] => self.consume_byte(T![/]),
200 b',' if self.current() == T![<] => self.consume_byte(T![,]),
Script executed:
# Search for tests related to Vue directives
find crates/biome_html_parser -name "*.rs" -type f | xargs grep -l "vue_directive\|VueDirective" | head -10Repository: biomejs/biome
Repository: biomejs/biome
Exit code: 0
stdout:
crates/biome_html_parser/src/lexer/mod.rs
crates/biome_html_parser/src/syntax/mod.rs
crates/biome_html_parser/src/syntax/parse_error.rs
crates/biome_html_parser/src/syntax/vue.rs
crates/biome_html_parser/src/token_source.rs
Script executed:
# Look for escape handling in the lexer
rg -i "escape\|backslash\|\\\\" crates/biome_html_parser/src/lexer/ -A 2 -B 2Repository: biomejs/biome
Repository: biomejs/biome
Exit code: 1
Script executed:
# Get the full QuotesSeen impl block, especially check_byte method
rg -A 50 "impl QuotesSeen" crates/biome_html_parser/src/lexer/mod.rs | head -80Repository: biomejs/biome
Repository: biomejs/biome
Exit code: 0
stdout:
impl QuotesSeen {
fn new() -> Self {
Self {
single: 0,
double: 0,
template: 0,
inside_comment: false,
prev_byte: None,
}
}
/// It checks the given byte. If it's a quote, it's tracked
fn check_byte(&mut self, byte: u8) {
// Check for comment exit first
if self.inside_comment {
if byte == b'\n' {
// Exit single-line comment
self.inside_comment = false;
} else if self.prev_byte == Some(b'*') && byte == b'/' {
// Exit multi-line comment
self.inside_comment = false;
}
self.prev_byte = Some(byte);
return; // Don't track quotes inside comments
}
// Check for comment entry - but only if we're not inside quotes
if self.prev_byte == Some(b'/')
&& (byte == b'/' || byte == b'*')
&& self.single == 0
&& self.double == 0
&& self.template == 0
{
self.inside_comment = true;
self.prev_byte = Some(byte);
return;
}
// Normal quote tracking
match byte {
b'"' => self.track_double(),
b'\'' => self.track_single(),
b'`' => self.track_template(),
_ => {}
}
self.prev_byte = Some(byte);
}
/// It adds a single quote if single quotes are zero and the others are greater than zero. It removes it otherwise
fn track_single(&mut self) {
Script executed:
# Search for Vue directive tests
find crates/biome_html_parser/tests -type f -name "*.rs" 2>/dev/null | xargs grep -l "vue\|directive" 2>/dev/null || find crates -name "*vue*.rs" -path "*/tests/*"Repository: biomejs/biome
Repository: biomejs/biome
Exit code: 0
stdout:
crates/biome_html_parser/tests/spec_test.rs
crates/biome_html_parser/tests/spec_tests.rs
Script executed:
# Also check if there are snapshot/fixture tests for Vue
find crates/biome_html_parser -name "*.html" -o -name "*vue*" | grep -i testRepository: biomejs/biome
Repository: biomejs/biome
Exit code: 0
stdout:
crates/biome_html_parser/tests/html_specs/error/attributes/invalid-unquoted-value1.html
crates/biome_html_parser/tests/html_specs/error/attributes/invalid-unquoted-value2.html
crates/biome_html_parser/tests/html_specs/error/attributes/missing-initializer.html
crates/biome_html_parser/tests/html_specs/error/element/br-with-end.html
crates/biome_html_parser/tests/html_specs/error/element/child-no-tag-name.html
crates/biome_html_parser/tests/html_specs/error/element/missing-close-tag-2.html
crates/biome_html_parser/tests/html_specs/error/element/missing-close-tag.html
crates/biome_html_parser/tests/html_specs/error/element/missing-element-name.html
crates/biome_html_parser/tests/html_specs/error/element/solo-no-tag-name.html
crates/biome_html_parser/tests/html_specs/error/frontmatter.html
crates/biome_html_parser/tests/html_specs/error/frontmatter_bogus.html
crates/biome_html_parser/tests/html_specs/error/interpolation-attributes.html
crates/biome_html_parser/tests/html_specs/error/interpolation.html
crates/biome_html_parser/tests/html_specs/error/template-langs/django/issue-5450.html
crates/biome_html_parser/tests/html_specs/error/vue
crates/biome_html_parser/tests/html_specs/error/vue/invalid-v-bind-shorthand.vue
crates/biome_html_parser/tests/html_specs/error/vue/invalid-v-bind-shorthand.vue.snap
crates/biome_html_parser/tests/html_specs/error/vue/invalid-v-on-shorthand.vue
crates/biome_html_parser/tests/html_specs/error/vue/invalid-v-on-shorthand.vue.snap
crates/biome_html_parser/tests/html_specs/error/vue/invalid-v-slot-shorthand.vue
crates/biome_html_parser/tests/html_specs/error/vue/invalid-v-slot-shorthand.vue.snap
crates/biome_html_parser/tests/html_specs/error/vue/vue_unclosed_expression.vue
crates/biome_html_parser/tests/html_specs/error/vue/vue_unclosed_expression.vue.snap
crates/biome_html_parser/tests/html_specs/ok/attributes/attributes-unquoted.html
crates/biome_html_parser/tests/html_specs/ok/attributes/attributes.html
crates/biome_html_parser/tests/html_specs/ok/attributes/multiline-attribute-value.html
crates/biome_html_parser/tests/html_specs/ok/attributes/multiline-attributes.html
crates/biome_html_parser/tests/html_specs/ok/bom.html
crates/biome_html_parser/tests/html_specs/ok/cdata.html
crates/biome_html_parser/tests/html_specs/ok/comment-inline.html
crates/biome_html_parser/tests/html_specs/ok/comment.html
crates/biome_html_parser/tests/html_specs/ok/comment2.html
crates/biome_html_parser/tests/html_specs/ok/compat/non-spec-compliant-tag-name.html
crates/biome_html_parser/tests/html_specs/ok/content-blocks.html
crates/biome_html_parser/tests/html_specs/ok/doctype/basic.html
crates/biome_html_parser/tests/html_specs/ok/doctype/long-legacy1.html
crates/biome_html_parser/tests/html_specs/ok/doctype/long-legacy2.html
crates/biome_html_parser/tests/html_specs/ok/doctype/long-legacy3.html
crates/biome_html_parser/tests/html_specs/ok/doctype/minimal.html
crates/biome_html_parser/tests/html_specs/ok/element.html
crates/biome_html_parser/tests/html_specs/ok/element_list.html
crates/biome_html_parser/tests/html_specs/ok/element_list2.html
crates/biome_html_parser/tests/html_specs/ok/embedded-languages/script-basic.html
crates/biome_html_parser/tests/html_specs/ok/embedded-languages/script-empty.html
crates/biome_html_parser/tests/html_specs/ok/embedded-languages/script-in-string.html
crates/biome_html_parser/tests/html_specs/ok/embedded-languages/script-less-than.html
crates/biome_html_parser/tests/html_specs/ok/hello-world.html
crates/biome_html_parser/tests/html_specs/ok/no-end-tags/br-in-span.html
crates/biome_html_parser/tests/html_specs/ok/no-end-tags/br.html
crates/biome_html_parser/tests/html_specs/ok/no-end-tags/casing.html
crates/biome_html_parser/tests/html_specs/ok/no-end-tags/meta.html
crates/biome_html_parser/tests/html_specs/ok/no-end-tags/param.html
crates/biome_html_parser/tests/html_specs/ok/no-end-tags/wbr.html
crates/biome_html_parser/tests/html_specs/ok/ok.html
crates/biome_html_parser/tests/html_specs/ok/ok2.html
crates/biome_html_parser/tests/html_specs/ok/quote-in-child.html
crates/biome_html_parser/tests/html_specs/ok/special-chars.html
crates/biome_html_parser/tests/html_specs/ok/text_keywords.html
crates/biome_html_parser/tests/html_specs/ok/vue
crates/biome_html_parser/tests/html_specs/ok/vue/component.vue
crates/biome_html_parser/tests/html_specs/ok/vue/component.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/event-with-colon.vue
crates/biome_html_parser/tests/html_specs/ok/vue/event-with-colon.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/generic-directives/arg-modifiers-no-value.vue
crates/biome_html_parser/tests/html_specs/ok/vue/generic-directives/arg-modifiers-no-value.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/generic-directives/arg-no-modifiers-no-value.vue
crates/biome_html_parser/tests/html_specs/ok/vue/generic-directives/arg-no-modifiers-no-value.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/generic-directives/directive-only.vue
crates/biome_html_parser/tests/html_specs/ok/vue/generic-directives/directive-only.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/generic-directives/modifiers-no-arg-no-value.vue
crates/biome_html_parser/tests/html_specs/ok/vue/generic-directives/modifiers-no-arg-no-value.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/interpolation.vue
crates/biome_html_parser/tests/html_specs/ok/vue/interpolation.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/issue-8174.vue
crates/biome_html_parser/tests/html_specs/ok/vue/issue-8174.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/issue-8765.vue
crates/biome_html_parser/tests/html_specs/ok/vue/issue-8765.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/modifier.vue
crates/biome_html_parser/tests/html_specs/ok/vue/modifier.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/modifiers-all-variants.vue
crates/biome_html_parser/tests/html_specs/ok/vue/modifiers-all-variants.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/multiple_expressions.vue
crates/biome_html_parser/tests/html_specs/ok/vue/multiple_expressions.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-bind-dynamic.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-bind-dynamic.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-bind-mixed.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-bind-mixed.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-bind-shorthand.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-bind-shorthand.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-bind.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-bind.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-dynamic-chains.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-dynamic-chains.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-else-if.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-else-if.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-else.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-else.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-for.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-for.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-html-text.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-html-text.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-if.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-if.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-mixed-complex.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-mixed-complex.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-model-mixed.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-model-mixed.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-on-mixed.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-on-mixed.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-on-shorthand.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-on-shorthand.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-slot-shorthand.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-slot-shorthand.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/vue_expressions.vue
crates/biome_html_parser/tests/html_specs/ok/vue/vue_expressions.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/vue_expressions_escaped.vue
crates/biome_html_parser/tests/html_specs/ok/vue/vue_expressions_escaped.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/dynamic-slot-arg.vue
crates/biome_html_parser/tests/html_specs/ok/vue/dynamic-slot-arg.vue.snap
Script executed:
# Check the vue_expressions_escaped.vue test
cat crates/biome_html_parser/tests/html_specs/ok/vue/vue_expressions_escaped.vueRepository: biomejs/biome
Repository: biomejs/biome
Exit code: 0
stdout:
<template>
\{\{ xyz \}\}
</template>
Script executed:
# Check the full QuotesSeen implementation, specifically the tracking methods
rg -A 100 "fn track_single" crates/biome_html_parser/src/lexer/mod.rs | head -60Repository: biomejs/biome
Repository: biomejs/biome
Exit code: 0
stdout:
fn track_single(&mut self) {
if (self.single == 0 && (self.double > 0 || self.template > 0))
|| (self.single == 0 && self.double == 0 && self.template == 0)
{
self.single.add_assign(1);
} else {
self.single = self.single.saturating_sub(1);
}
}
/// It adds a double quote if double quotes are zero and the others are greater than zero. It removes it otherwise
fn track_double(&mut self) {
if (self.double == 0 && (self.single > 0 || self.template > 0))
|| (self.double == 0 && self.single == 0 && self.template == 0)
{
self.double.add_assign(1);
} else {
self.double = self.double.saturating_sub(1);
}
}
/// It adds a template quote if template quotes are zero and the others are greater than zero. It removes it otherwise
fn track_template(&mut self) {
if (self.template == 0 && (self.single > 0 || self.double > 0))
|| (self.template == 0 && self.single == 0 && self.double == 0)
{
self.template.add_assign(1);
} else {
self.template = self.template.saturating_sub(1);
}
}
fn is_empty(&self) -> bool {
self.single == 0 && self.double == 0 && self.template == 0
}
}
#[cfg(test)]
mod quotes_seen {
use crate::lexer::QuotesSeen;
fn track(source: &str, quotes_seen: &mut QuotesSeen) {
for char in source.as_bytes() {
quotes_seen.check_byte(*char);
}
}
#[test]
fn is_not_empty() {
let source = r#"'"`"#;
let mut quotes_seen = QuotesSeen::new();
track(source, &mut quotes_seen);
assert!(!quotes_seen.is_empty());
let source = r#"`'""'"#;
let mut quotes_seen = QuotesSeen::new();
track(source, &mut quotes_seen);
assert!(!quotes_seen.is_empty());
}
#[test]
fn is_empty() {
Script executed:
# Look for any escape handling elsewhere in consume_token_vue_directive_argument or related code
rg -B 5 -A 15 "consume_token_vue_directive_argument" crates/biome_html_parser/src/lexer/mod.rs | tail -40Repository: biomejs/biome
Repository: biomejs/biome
Exit code: 0
stdout:
/// Consume a token in the [HtmlLexContext::VueDirectiveArgument] context.
fn consume_token_vue_directive_argument(&mut self) -> HtmlSyntaxKind {
let start = self.text_position();
let mut brackets_stack = 0;
let mut quotes_seen = QuotesSeen::new();
while let Some(byte) = self.current_byte() {
quotes_seen.check_byte(byte);
let char = biome_unicode_table::lookup_byte(byte);
use biome_unicode_table::Dispatch::*;
if quotes_seen.is_empty() {
match char {
BTO => {
brackets_stack += 1;
}
BTC => {
--
Some(current) => match context {
HtmlLexContext::Regular => self.consume_token(current),
HtmlLexContext::InsideTag => self.consume_token_inside_tag(current),
HtmlLexContext::InsideTagVue => self.consume_token_inside_tag_vue(current),
HtmlLexContext::VueDirectiveArgument => {
self.consume_token_vue_directive_argument()
}
HtmlLexContext::AttributeValue => self.consume_token_attribute_value(current),
HtmlLexContext::Doctype => self.consume_token_doctype(current),
HtmlLexContext::EmbeddedLanguage(lang) => {
self.consume_token_embedded_language(current, lang, context)
}
HtmlLexContext::TextExpression(kind) => match kind {
TextExpressionKind::Double => self.consume_double_text_expression(current),
TextExpressionKind::Single => self.consume_single_text_expression(),
},
HtmlLexContext::RestrictedSingleExpression(kind) => {
self.consume_restricted_single_text_expression(kind)
}
HtmlLexContext::CdataSection => self.consume_inside_cdata(current),
HtmlLexContext::AstroFencedCodeBlock => {
Script executed:
# Look for Vue directive argument tests with special characters
find crates/biome_html_parser/tests/html_specs -name "*.vue" -type f -exec grep -l "\[" {} \; | head -5Repository: biomejs/biome
Repository: biomejs/biome
Exit code: 0
stdout:
crates/biome_html_parser/tests/html_specs/ok/vue/v-bind-dynamic.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-bind-mixed.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-dynamic-chains.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-else-if.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-html-text.vue
stderr:
find: 'grep' terminated by signal 13
find: 'grep' terminated by signal 13
find: 'grep' terminated by signal 13
find: 'grep' terminated by signal 13
find: 'grep' terminated by signal 13
Script executed:
# Check a Vue test file that likely has dynamic arguments
cat crates/biome_html_parser/tests/html_specs/ok/vue/v-bind-dynamic.vueRepository: biomejs/biome
Repository: biomejs/biome
Exit code: 0
stdout:
<template>
<Foo :[key]="foo" />
</template>
Script executed:
# Search for any backslash handling in Vue context
rg "backslash|escape.*quote|quote.*escape" crates/biome_html_parser/src/ -iRepository: biomejs/biome
Repository: biomejs/biome
Exit code: 0
stdout:
crates/biome_html_parser/src/lexer/mod.rs: /// Consumes a quoted string literal token, handling escaped characters and unicode sequences.
crates/biome_html_parser/src/lexer/mod.rs: // Handle escaped `'` but only if this is a end quote string.
crates/biome_html_parser/src/lexer/mod.rs: // Handle escaped `'` but only if this is a end quote string.
Script executed:
# Check if there are any existing issues or TODOs about escape handling
rg "TODO|FIXME|BUG" crates/biome_html_parser/src/lexer/mod.rs -A 2Repository: biomejs/biome
Repository: biomejs/biome
Exit code: 0
stdout:
AS_KW, ATTACH_KW, AWAIT_KW, CATCH_KW, COMMENT, CONST_KW, DEBUG_KW, DOCTYPE_KW, EACH_KW,
ELSE_KW, EOF, ERROR_TOKEN, HTML_KW, HTML_LITERAL, HTML_STRING_LITERAL, IDENT, IF_KW, KEY_KW,
NEWLINE, RENDER_KW, SNIPPET_KW, THEN_KW, TOMBSTONE, UNICODE_BOM, WHITESPACE,
--
b"debug" => DEBUG_KW,
b"attach" => ATTACH_KW,
b"const" => CONST_KW,
Script executed:
# Check for escaped character handling mentioned in the comments
rg -B 5 -A 10 "Handle escaped" crates/biome_html_parser/src/lexer/mod.rsRepository: biomejs/biome
Repository: biomejs/biome
Exit code: 0
stdout:
self.advance(1);
match self.current_byte() {
Some(b'\n' | b'\r') => self.advance(1),
// Handle escaped `'` but only if this is a end quote string.
Some(b'\'') if quote == b'\'' => {
self.advance(1);
}
// Handle escaped `'` but only if this is a end quote string.
Some(b'"') if quote == b'"' => {
self.advance(1);
}
Some(b'u') => match (self.consume_unicode_escape(), state) {
(Ok(_), _) => {}
(Err(err), LexStringState::InString) => {
self.diagnostics.push(err);
state = LexStringState::InvalidEscapeSequence;
}
Script executed:
# Look for the consume_double_text_expression or similar to see how escapes are handled elsewhere
rg -A 30 "fn consume_double_text_expression" crates/biome_html_parser/src/lexer/mod.rs | head -50Repository: biomejs/biome
Repository: biomejs/biome
Exit code: 0
stdout:
fn consume_double_text_expression(&mut self, current: u8) -> HtmlSyntaxKind {
match current {
b'}' if self.at_closing_double_text_expression() => {
self.consume_r_double_text_expression()
}
b'<' => self.consume_byte(T![<]),
_ => {
while let Some(current) = self.current_byte() {
match current {
b'}' if self.at_closing_double_text_expression() => break,
_ => {
self.advance(1);
}
}
}
HTML_LITERAL
}
}
}
/// Consumes tokens within a single text expression ('{...}') while tracking nested
/// brackets until the matching closing bracket is found.
fn consume_single_text_expression(&mut self) -> HtmlSyntaxKind {
let mut brackets_stack = 0;
while let Some(current) = self.current_byte() {
match current {
b'}' => {
if brackets_stack == 0 {
break;
} else {
brackets_stack -= 1;
Script executed:
# Check Vue spec or documentation on escaped quotes in directive arguments
find crates/biome_html_parser -name "*.rs" -type f -exec grep -l "directive.*argument\|VueDirectiveArgument" {} \; | xargs grep -B 5 -A 5 "escape" 2>/dev/null | head -40Repository: biomejs/biome
Repository: biomejs/biome
Exit code: 0
stdout:
crates/biome_html_parser/src/lexer/mod.rs- {
crates/biome_html_parser/src/lexer/mod.rs- self.consume_l_angle()
crates/biome_html_parser/src/lexer/mod.rs- } else {
crates/biome_html_parser/src/lexer/mod.rs- self.push_diagnostic(
crates/biome_html_parser/src/lexer/mod.rs- ParseDiagnostic::new(
crates/biome_html_parser/src/lexer/mod.rs: "Unescaped `<` bracket character. Expected a tag or escaped character.",
crates/biome_html_parser/src/lexer/mod.rs- self.text_position()..self.text_position() + TextSize::from(1),
crates/biome_html_parser/src/lexer/mod.rs- )
crates/biome_html_parser/src/lexer/mod.rs: .with_hint("Replace this character with `<` to escape it."),
crates/biome_html_parser/src/lexer/mod.rs- );
crates/biome_html_parser/src/lexer/mod.rs- self.consume_byte(HTML_LITERAL)
crates/biome_html_parser/src/lexer/mod.rs- }
crates/biome_html_parser/src/lexer/mod.rs- }
crates/biome_html_parser/src/lexer/mod.rs- _ if is_at_start_identifier(current) => self
--
crates/biome_html_parser/src/lexer/mod.rs- }
crates/biome_html_parser/src/lexer/mod.rs-
crates/biome_html_parser/src/lexer/mod.rs- HTML_LITERAL
crates/biome_html_parser/src/lexer/mod.rs- }
crates/biome_html_parser/src/lexer/mod.rs-
crates/biome_html_parser/src/lexer/mod.rs: /// Consumes a quoted string literal token, handling escaped characters and unicode sequences.
crates/biome_html_parser/src/lexer/mod.rs: /// Returns ERROR_TOKEN if the string is not properly terminated or contains invalid escapes.
crates/biome_html_parser/src/lexer/mod.rs- fn consume_string_literal(&mut self, quote: u8) -> HtmlSyntaxKind {
crates/biome_html_parser/src/lexer/mod.rs- self.assert_current_char_boundary();
crates/biome_html_parser/src/lexer/mod.rs- let start = self.text_position();
crates/biome_html_parser/src/lexer/mod.rs-
crates/biome_html_parser/src/lexer/mod.rs- self.advance(1); // Skip over the quote
--
crates/biome_html_parser/src/lexer/mod.rs- self.advance(1);
crates/biome_html_parser/src/lexer/mod.rs-
crates/biome_html_parser/src/lexer/mod.rs- match self.current_byte() {
crates/biome_html_parser/src/lexer/mod.rs- Some(b'\n' | b'\r') => self.advance(1),
crates/biome_html_parser/src/lexer/mod.rs-
crates/biome_html_parser/src/lexer/mod.rs: // Handle escaped `'` but only if this is a end quote string.
crates/biome_html_parser/src/lexer/mod.rs- Some(b'\'') if quote == b'\'' => {
crates/biome_html_parser/src/lexer/mod.rs- self.advance(1);
crates/biome_html_parser/src/lexer/mod.rs- }
crates/biome_html_parser/src/lexer/mod.rs-
crates/biome_html_parser/src/lexer/mod.rs: // Handle escaped `'` but only if this is a end quote string.
crates/biome_html_parser/src/lexer/mod.rs- Some(b'"') if quote == b'"' => {
Script executed:
# Check if there are any tests for Vue directive arguments with escaped characters
find crates/biome_html_parser/tests -name "*.vue" -type f -exec grep -H "\\\[" {} \;Repository: biomejs/biome
Repository: biomejs/biome
Exit code: 0
stderr:
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
Script executed:
# Look at one of the dynamic argument tests more carefully
cat crates/biome_html_parser/tests/html_specs/ok/vue/v-bind-dynamic.vue.snapRepository: biomejs/biome
Repository: biomejs/biome
Exit code: 0
stdout:
---
source: crates/biome_html_parser/tests/spec_test.rs
expression: snapshot
---
## Input
```vue
<template>
<Foo :[key]="foo" />
</template>
```
## AST
```
HtmlRoot {
bom_token: missing (optional),
frontmatter: missing (optional),
directive: missing (optional),
html: HtmlElementList [
HtmlElement {
opening_element: HtmlOpeningElement {
l_angle_token: L_ANGLE@0..1 "<" [] [],
name: HtmlTagName {
value_token: HTML_LITERAL@1..9 "template" [] [],
},
attributes: HtmlAttributeList [],
r_angle_token: R_ANGLE@9..10 ">" [] [],
},
children: HtmlElementList [
HtmlSelfClosingElement {
l_angle_token: L_ANGLE@10..13 "<" [Newline("\n"), Whitespace("\t")] [],
name: HtmlTagName {
value_token: HTML_LITERAL@13..17 "Foo" [] [Whitespace(" ")],
},
attributes: HtmlAttributeList [
VueVBindShorthandDirective {
arg: VueDirectiveArgument {
colon_token: COLON@17..18 ":" [] [],
arg: VueDynamicArgument {
l_brack_token: L_BRACKET@18..19 "[" [] [],
name_token: HTML_LITERAL@19..22 "key" [] [],
r_brack_token: R_BRACKET@22..23 "]" [] [],
},
},
modifiers: VueModifierList [],
initializer: HtmlAttributeInitializerClause {
eq_token: EQ@23..24 "=" [] [],
value: HtmlString {
value_token: HTML_STRING_LITERAL@24..30 "\"foo\"" [] [Whitespace(" ")],
},
},
},
],
slash_token: SLASH@30..31 "/" [] [],
r_angle_token: R_ANGLE@31..32 ">" [] [],
},
],
closing_element: HtmlClosingElement {
l_angle_token: L_ANGLE@32..34 "<" [Newline("\n")] [],
slash_token: SLASH@34..35 "/" [] [],
name: HtmlTagName {
value_token: HTML_LITERAL@35..43 "template" [] [],
},
r_angle_token: R_ANGLE@43..44 ">" [] [],
},
},
],
eof_token: EOF@44..45 "" [Newline("\n")] [],
}
```
## CST
```
0: HTML_ROOT@0..45
0: (empty)
1: (empty)
2: (empty)
3: HTML_ELEMENT_LIST@0..44
0: HTML_ELEMENT@0..44
0: HTML_OPENING_ELEMENT@0..10
0: L_ANGLE@0..1 "<" [] []
1: HTML_TAG_NAME@1..9
0: HTML_LITERAL@1..9 "template" [] []
2: HTML_ATTRIBUTE_LIST@9..9
3: R_ANGLE@9..10 ">" [] []
1: HTML_ELEMENT_LIST@10..32
0: HTML_SELF_CLOSING_ELEMENT@10..32
0: L_ANGLE@10..13 "<" [Newline("\n"), Whitespace("\t")] []
1: HTML_TAG_NAME@13..17
0: HTML_LITERAL@13..17 "Foo" [] [Whitespace(" ")]
2: HTML_ATTRIBUTE_LIST@17..30
0: VUE_V_BIND_SHORTHAND_DIRECTIVE@17..30
0: VUE_DIRECTIVE_ARGUMENT@17..23
0: COLON@17..18 ":" [] []
1: VUE_DYNAMIC_ARGUMENT@18..23
0: L_BRACKET@18..19 "[" [] []
1: HTML_LITERAL@19..22 "key" [] []
2: R_BRACKET@22..23 "]" [] []
1: VUE_MODIFIER_LIST@23..23
2: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@23..30
0: EQ@23..24 "=" [] []
1: HTML_STRING@24..30
0: HTML_STRING_LITERAL@24..30 "\"foo\"" [] [Whitespace(" ")]
3: SLASH@30..31 "/" [] []
4: R_ANGLE@31..32 ">" [] []
2: HTML_CLOSING_ELEMENT@32..44
0: L_ANGLE@32..34 "<" [Newline("\n")] []
1: SLASH@34..35 "/" [] []
2: HTML_TAG_NAME@35..43
0: HTML_LITERAL@35..43 "template" [] []
3: R_ANGLE@43..44 ">" [] []
4: EOF@44..45 "" [Newline("\n")] []
```
Script executed:
# Check the full QuotesSeen test to see if escapes are tested
rg -A 50 "#\[cfg\(test\)\]" crates/biome_html_parser/src/lexer/mod.rs | grep -A 50 "mod quotes_seen"Repository: biomejs/biome
Repository: biomejs/biome
Exit code: 0
stdout:
mod quotes_seen {
use crate::lexer::QuotesSeen;
fn track(source: &str, quotes_seen: &mut QuotesSeen) {
for char in source.as_bytes() {
quotes_seen.check_byte(*char);
}
}
#[test]
fn is_not_empty() {
let source = r#"'"`"#;
let mut quotes_seen = QuotesSeen::new();
track(source, &mut quotes_seen);
assert!(!quotes_seen.is_empty());
let source = r#"`'""'"#;
let mut quotes_seen = QuotesSeen::new();
track(source, &mut quotes_seen);
assert!(!quotes_seen.is_empty());
}
#[test]
fn is_empty() {
let source = r#" '"``"' "#;
let mut quotes_seen = QuotesSeen::new();
track(source, &mut quotes_seen);
assert!(quotes_seen.is_empty());
let source = r#"// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh."#;
let mut quotes_seen = QuotesSeen::new();
track(source, &mut quotes_seen);
assert!(quotes_seen.is_empty());
}
#[test]
fn empty_inside_comments() {
let source = r#"// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh."#;
let mut quotes_seen = QuotesSeen::new();
track(source, &mut quotes_seen);
assert!(quotes_seen.is_empty());
}
#[test]
fn empty_with_comments_outside_comments_1() {
let source = r#"// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh.
const f = "something" "#;
let mut quotes_seen = QuotesSeen::new();
track(source, &mut quotes_seen);
assert!(quotes_seen.is_empty());
}
Handle escaped quotes in directive arguments.
QuotesSeen toggles quote state on every quote byte without escape awareness, so \" or \' inside a directive argument would prematurely end quote tracking and allow ] inside a string literal to close the argument early. Add escape-aware quote tracking to check_byte() or add a regression test documenting the intended behaviour.
🤖 Prompt for AI Agents
In `@crates/biome_html_parser/src/lexer/mod.rs` around lines 158 - 192, The quote
state in consume_token_vue_directive_argument is toggled blindly by
QuotesSeen::check_byte, so escaped quotes like \" or \' break tracking and let a
] inside a quoted string close the argument; update QuotesSeen::check_byte (or
add an escape-aware layer in consume_token_vue_directive_argument using
current_byte/peek_previous logic) to ignore quote bytes that are escaped (handle
backslash escaping, including escaped backslashes) while preserving normal
toggling otherwise, and add a regression test for
HtmlLexContext::VueDirectiveArgument that includes escaped quotes inside
brackets to ensure the parser does not prematurely close on ].
Summary
This PR lets the HTML parser parse v-slot directives that have more complex expressions inside them:
#[`${episode.id}-cell`]="{ row }"Primarily generated by gpt-5.2-codex, but added a couple more test cases myself.
fixes #8710
Test Plan
added snapshots
Docs