Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-use-alt-text.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": patch
---

Fixed [#9349](https://github.com/biomejs/biome/issues/9349): Biome now correctly handles Vue dynamic `:alt` and `v-bind:alt` bindings in `useAltText`, preventing false positives in `.vue` files.
5 changes: 1 addition & 4 deletions crates/biome_html_analyze/src/lint/a11y/use_alt_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,5 @@ fn has_type_image_attribute(element: &AnyHtmlElement) -> bool {

/// Check if the element has a valid alt attribute
fn has_valid_alt_text(element: &AnyHtmlElement) -> bool {
// The alt attribute exists - even an empty alt="" is valid for decorative images
// If there's no initializer, it's treated as an empty string (valid)
// If there's an initializer with a value, any value is valid
element.find_attribute_by_name("alt").is_some()
element.find_attribute_or_vue_binding("alt").is_some()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!-- should not generate diagnostics -->

<!-- img with dynamic :alt binding (Vue shorthand) -->
<img src="image.png" :alt="description" />

<!-- img with dynamic v-bind:alt binding (Vue explicit) -->
<img src="image.png" v-bind:alt="description" />

<!-- img with dynamic :alt and :src -->
<img :src="image.src" :alt="image.description" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
source: crates/biome_html_analyze/tests/spec_tests.rs
expression: valid.vue
---
# Input
```html
<!-- should not generate diagnostics -->

<!-- img with dynamic :alt binding (Vue shorthand) -->
<img src="image.png" :alt="description" />

<!-- img with dynamic v-bind:alt binding (Vue explicit) -->
<img src="image.png" v-bind:alt="description" />

<!-- img with dynamic :alt and :src -->
<img :src="image.src" :alt="image.description" />

```
61 changes: 57 additions & 4 deletions crates/biome_html_syntax/src/element_ext.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use crate::{
AnyHtmlContent, AnyHtmlElement, AnyHtmlTagName, AnyHtmlTextExpression, AnySvelteBlock,
AstroEmbeddedContent, HtmlAttribute, HtmlAttributeList, HtmlElement, HtmlEmbeddedContent,
HtmlOpeningElement, HtmlSelfClosingElement, HtmlSyntaxToken, HtmlTagName, ScriptType,
inner_string_text,
AnyHtmlAttribute, AnyHtmlContent, AnyHtmlElement, AnyHtmlTagName, AnyHtmlTextExpression,
AnySvelteBlock, AnyVueDirective, AstroEmbeddedContent, HtmlAttribute, HtmlAttributeList,
HtmlElement, HtmlEmbeddedContent, HtmlOpeningElement, HtmlSelfClosingElement, HtmlSyntaxToken,
HtmlTagName, ScriptType, inner_string_text,
};

use biome_rowan::{AstNodeList, SyntaxResult, TokenText, declare_node_union};

/// https://html.spec.whatwg.org/#void-elements
Expand Down Expand Up @@ -123,6 +124,58 @@ impl AnyHtmlElement {
_ => None,
}
}

/// Check if the element has a given HTML attribute or a Vue v-bind binding
/// targeting the same attribute name.
///
/// Handles:
/// - `name="..."` — standard HTML attribute
/// - `:name="..."` — Vue v-bind shorthand (`VueVBindShorthandDirective`)
/// - `v-bind:name="..."` — explicit Vue v-bind (`VueDirective`)
pub fn find_attribute_or_vue_binding(&self, name_to_lookup: &str) -> Option<AnyHtmlAttribute> {
let attrs = self.attributes()?;

attrs.iter().find_map(|attr| {
let matches = match &attr {
AnyHtmlAttribute::HtmlAttribute(a) => a
.name()
.ok()
.and_then(|n| n.value_token().ok())
.is_some_and(|t| t.text_trimmed().eq_ignore_ascii_case(name_to_lookup)),

AnyHtmlAttribute::AnyVueDirective(vue) => match vue {
// :name="..."
AnyVueDirective::VueVBindShorthandDirective(d) => d
.arg()
.ok()
.and_then(|arg| arg.arg().ok())
.and_then(|arg| arg.as_vue_static_argument().cloned())
.and_then(|s| s.name_token().ok())
.is_some_and(|t| t.text_trimmed().eq_ignore_ascii_case(name_to_lookup)),

// v-bind:name="..."
AnyVueDirective::VueDirective(d) => {
let is_bind = d
.name_token()
.is_ok_and(|t| t.text_trimmed().eq_ignore_ascii_case("v-bind"));
is_bind
&& d.arg()
.and_then(|arg| arg.arg().ok())
.and_then(|arg| arg.as_vue_static_argument().cloned())
.and_then(|s| s.name_token().ok())
.is_some_and(|t| {
t.text_trimmed().eq_ignore_ascii_case(name_to_lookup)
})
}

_ => false,
},

_ => false,
};
if matches { Some(attr) } else { None }
})
}
}

impl HtmlSelfClosingElement {
Expand Down