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/small-seas-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": minor
---

Added the rule [`noRedundantAlt`](https://biomejs.dev/linter/rules/no-redundant-alt/) to HTML. The rule enforces that the `img` element `alt` attribute does not contain the words “image”, “picture”, or “photo”.
3 changes: 2 additions & 1 deletion crates/biome_html_analyze/src/lint/a11y.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ pub mod no_autofocus;
pub mod no_distracting_elements;
pub mod no_header_scope;
pub mod no_positive_tabindex;
pub mod no_redundant_alt;
pub mod no_svg_without_title;
pub mod use_alt_text;
pub mod use_aria_props_for_role;
pub mod use_button_type;
pub mod use_html_lang;
pub mod use_iframe_title;
pub mod use_valid_aria_role;
declare_lint_group! { pub A11y { name : "a11y" , rules : [self :: no_access_key :: NoAccessKey , self :: no_autofocus :: NoAutofocus , self :: no_distracting_elements :: NoDistractingElements , self :: no_header_scope :: NoHeaderScope , self :: no_positive_tabindex :: NoPositiveTabindex , self :: no_svg_without_title :: NoSvgWithoutTitle , self :: use_alt_text :: UseAltText , self :: use_aria_props_for_role :: UseAriaPropsForRole , self :: use_button_type :: UseButtonType , self :: use_html_lang :: UseHtmlLang , self :: use_iframe_title :: UseIframeTitle , self :: use_valid_aria_role :: UseValidAriaRole ,] } }
declare_lint_group! { pub A11y { name : "a11y" , rules : [self :: no_access_key :: NoAccessKey , self :: no_autofocus :: NoAutofocus , self :: no_distracting_elements :: NoDistractingElements , self :: no_header_scope :: NoHeaderScope , self :: no_positive_tabindex :: NoPositiveTabindex , self :: no_redundant_alt :: NoRedundantAlt , self :: no_svg_without_title :: NoSvgWithoutTitle , self :: use_alt_text :: UseAltText , self :: use_aria_props_for_role :: UseAriaPropsForRole , self :: use_button_type :: UseButtonType , self :: use_html_lang :: UseHtmlLang , self :: use_iframe_title :: UseIframeTitle , self :: use_valid_aria_role :: UseValidAriaRole ,] } }
121 changes: 121 additions & 0 deletions crates/biome_html_analyze/src/lint/a11y/no_redundant_alt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use biome_analyze::{
Ast, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule,
};
use biome_console::markup;
use biome_diagnostics::Severity;
use biome_html_syntax::element_ext::AnyHtmlTagElement;
use biome_html_syntax::{AnyHtmlAttributeInitializer, HtmlFileSource};
use biome_rowan::AstNode;
use biome_rule_options::is_redundant_alt;
use biome_rule_options::no_redundant_alt::NoRedundantAltOptions;

declare_lint_rule! {
/// Enforce `img` alt prop does not contain the word "image", "picture", or "photo".
///
/// The rule will first check if `aria-hidden` is truthy to determine whether to enforce the rule. If the image is
/// hidden, then the rule will always succeed.
///
/// ## Examples
///
/// ### Invalid
///
/// ```html,expect_diagnostic
/// <img src="src" alt="photo content" />;
/// ```
///
/// ```html,expect_diagnostic
/// <img alt="picture of cool person" aria-hidden="false" />;
/// ```
///
/// ### Valid
///
/// ```html
/// <>
/// <img src="src" alt="alt" />
/// <img src="bar" aria-hidden alt="Picture of me taking a photo of an image" />
/// </>
/// ```
///
pub NoRedundantAlt {
version: "next",
name: "noRedundantAlt",
language: "html",
sources: &[RuleSource::EslintJsxA11y("img-redundant-alt").same()],
recommended: true,
severity: Severity::Error,
}
}

impl Rule for NoRedundantAlt {
type Query = Ast<AnyHtmlTagElement>;
type State = AnyHtmlAttributeInitializer;
type Signals = Option<Self::State>;
type Options = NoRedundantAltOptions;

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let node = ctx.query();
let file_source = ctx.source_type::<HtmlFileSource>();

let name = node.name().ok()?.value_token().ok()?;
if (file_source.is_html() && !name.text_trimmed().eq_ignore_ascii_case("img"))
|| (!file_source.is_html() && name.text_trimmed() != "img")
{
return None;
}

let aria_hidden_attribute = node.find_attribute_by_name("aria-hidden");
if let Some(aria_hidden) = aria_hidden_attribute {
let is_false = match aria_hidden.initializer()?.value().ok()? {
AnyHtmlAttributeInitializer::HtmlSingleTextExpression(aria_hidden) => {
aria_hidden
.expression()
.ok()?
.html_literal_token()
.ok()?
.text_trimmed()
== "false"
}
AnyHtmlAttributeInitializer::HtmlString(aria_hidden) => {
aria_hidden.inner_string_text().ok()?.text() == "false"
}
};

if !is_false {
return None;
}
}

let alt = node
.find_attribute_by_name("alt")?
.initializer()?
.value()
.ok()?;

match alt {
AnyHtmlAttributeInitializer::HtmlSingleTextExpression(ref expression) => {
let value = expression.expression().ok()?.html_literal_token().ok()?;

is_redundant_alt(value.text_trimmed()).then_some(alt)
}
AnyHtmlAttributeInitializer::HtmlString(ref value) => {
let inner_string_text = value.inner_string_text().ok()?;
is_redundant_alt(inner_string_text.text()).then_some(alt)
}
}
}

fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
Some(
RuleDiagnostic::new(
rule_category!(),
state.range(),
markup! {
"Avoid the words \"image\", \"picture\", or \"photo\" in " <Emphasis>"img"</Emphasis>" element alt text."
},
)
.note(markup! {
"Screen readers announce img elements as \"images\", so it is not necessary to redeclare this in alternative text."
}),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!-- should generate diagnostics -->
<img alt="Photo of friend." />
<img alt="Picture of friend." />
<img alt="Image of friend." />
<img alt="PhOtO of friend." />
<img alt={"photo"} />
<img alt="piCTUre of friend." />
<img alt="imAGE of friend." />
<img alt="image of cool person" aria-hidden="false" />
<IMG alt="imAGE of friend." />
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
---
source: crates/biome_html_analyze/tests/spec_tests.rs
expression: invalid.html
---
# Input
```html
<!-- should generate diagnostics -->
<img alt="Photo of friend." />
<img alt="Picture of friend." />
<img alt="Image of friend." />
<img alt="PhOtO of friend." />
<img alt={"photo"} />
<img alt="piCTUre of friend." />
<img alt="imAGE of friend." />
<img alt="image of cool person" aria-hidden="false" />
<IMG alt="imAGE of friend." />

```

_Note: The parser emitted 2 diagnostics which are not shown here._

# Diagnostics
```
invalid.html:2:10 lint/a11y/noRedundantAlt ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Avoid the words "image", "picture", or "photo" in img element alt text.

1 │ <!-- should generate diagnostics -->
> 2 │ <img alt="Photo of friend." />
│ ^^^^^^^^^^^^^^^^^^
3 │ <img alt="Picture of friend." />
4 │ <img alt="Image of friend." />

i Screen readers announce img elements as "images", so it is not necessary to redeclare this in alternative text.


```

```
invalid.html:3:10 lint/a11y/noRedundantAlt ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Avoid the words "image", "picture", or "photo" in img element alt text.

1 │ <!-- should generate diagnostics -->
2 │ <img alt="Photo of friend." />
> 3 │ <img alt="Picture of friend." />
│ ^^^^^^^^^^^^^^^^^^^^
4 │ <img alt="Image of friend." />
5 │ <img alt="PhOtO of friend." />

i Screen readers announce img elements as "images", so it is not necessary to redeclare this in alternative text.


```

```
invalid.html:4:10 lint/a11y/noRedundantAlt ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Avoid the words "image", "picture", or "photo" in img element alt text.

2 │ <img alt="Photo of friend." />
3 │ <img alt="Picture of friend." />
> 4 │ <img alt="Image of friend." />
│ ^^^^^^^^^^^^^^^^^^
5 │ <img alt="PhOtO of friend." />
6 │ <img alt={"photo"} />

i Screen readers announce img elements as "images", so it is not necessary to redeclare this in alternative text.


```

```
invalid.html:5:10 lint/a11y/noRedundantAlt ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Avoid the words "image", "picture", or "photo" in img element alt text.

3 │ <img alt="Picture of friend." />
4 │ <img alt="Image of friend." />
> 5 │ <img alt="PhOtO of friend." />
│ ^^^^^^^^^^^^^^^^^^
6 │ <img alt={"photo"} />
7 │ <img alt="piCTUre of friend." />

i Screen readers announce img elements as "images", so it is not necessary to redeclare this in alternative text.


```

```
invalid.html:7:10 lint/a11y/noRedundantAlt ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Avoid the words "image", "picture", or "photo" in img element alt text.

5 │ <img alt="PhOtO of friend." />
6 │ <img alt={"photo"} />
> 7 │ <img alt="piCTUre of friend." />
│ ^^^^^^^^^^^^^^^^^^^^
8 │ <img alt="imAGE of friend." />
9 │ <img alt="image of cool person" aria-hidden="false" />

i Screen readers announce img elements as "images", so it is not necessary to redeclare this in alternative text.


```

```
invalid.html:8:10 lint/a11y/noRedundantAlt ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Avoid the words "image", "picture", or "photo" in img element alt text.

6 │ <img alt={"photo"} />
7 │ <img alt="piCTUre of friend." />
> 8 │ <img alt="imAGE of friend." />
│ ^^^^^^^^^^^^^^^^^^
9 │ <img alt="image of cool person" aria-hidden="false" />
10 │ <IMG alt="imAGE of friend." />

i Screen readers announce img elements as "images", so it is not necessary to redeclare this in alternative text.


```

```
invalid.html:9:10 lint/a11y/noRedundantAlt ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Avoid the words "image", "picture", or "photo" in img element alt text.

7 │ <img alt="piCTUre of friend." />
8 │ <img alt="imAGE of friend." />
> 9 │ <img alt="image of cool person" aria-hidden="false" />
│ ^^^^^^^^^^^^^^^^^^^^^^
10 │ <IMG alt="imAGE of friend." />
11 │

i Screen readers announce img elements as "images", so it is not necessary to redeclare this in alternative text.


```

```
invalid.html:10:10 lint/a11y/noRedundantAlt ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Avoid the words "image", "picture", or "photo" in img element alt text.

8 │ <img alt="imAGE of friend." />
9 │ <img alt="image of cool person" aria-hidden="false" />
> 10 │ <IMG alt="imAGE of friend." />
│ ^^^^^^^^^^^^^^^^^^
11 │

i Screen readers announce img elements as "images", so it is not necessary to redeclare this in alternative text.


```
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!-- should generate diagnostics -->
<img alt="Photo of friend." />
<img alt="Picture of friend." />
<img alt="Image of friend." />
<img alt="PhOtO of friend." />
<img alt={"photo"} />
<img alt="piCTUre of friend." />
<img alt="imAGE of friend." />
<img alt="image of cool person" aria-hidden="false" />
Loading