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/frogs-like-green.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": patch
---

Tweaked the diagnostics range for [useAltText](https://biomejs.dev/linter/rules/use-alt-text), [useButtonType](https://biomejs.dev/linter/rules/use-button-type), [useHtmlLang](https://biomejs.dev/linter/rules/use-html-lang), [useIframeTitle](https://biomejs.dev/linter/rules/use-iframe-title), [useValidAriaRole](https://biomejs.dev/linter/rules/use-valid-aria-role) & [useIfameSandbox](https://biomejs.dev/linter/rules/use-iframe-sandbox) to report on the opening tag instead of the full tag.
Original file line number Diff line number Diff line change
Expand Up @@ -167,14 +167,8 @@ file.astro:41:1 lint/a11y/useHtmlLang ━━━━━━━━━━━━━━
40 │
> 41 │ <html>
│ ^^^^^^
> 42 │ <head>
> 43 │ <title>Astro</title>
> 44 │ </head>
> 45 │ <body></body>
> 46 │ </html>
│ ^^^^^^^
47 │
48 │ <style>
42 │ <head>
43 │ <title>Astro</title>

i Setting a lang attribute on HTML document elements configures the language used by screen readers when no user default is specified.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,8 @@ file.svelte:10:1 lint/a11y/useHtmlLang ━━━━━━━━━━━━━
9 │
> 10 │ <html>
│ ^^^^^^
> 11 │ <head>
> 12 │ <title>Svelte</title>
> 13 │ </head>
> 14 │ <body></body>
> 15 │ </html>
│ ^^^^^^^
16 │
17 │ <style>
11 │ <head>
12 │ <title>Svelte</title>

i Setting a lang attribute on HTML document elements configures the language used by screen readers when no user default is specified.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,8 @@ file.svelte:15:1 lint/a11y/useHtmlLang ━━━━━━━━━━━━━
14 │
> 15 │ <html>
│ ^^^^^^
> 16 │ <head>
> 17 │ <title>Svelte</title>
> 18 │ </head>
> 19 │ <body></body>
> 20 │ </html>
│ ^^^^^^^
21 │
22 │ <style>
16 │ <head>
17 │ <title>Svelte</title>

i Setting a lang attribute on HTML document elements configures the language used by screen readers when no user default is specified.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,8 @@ file.vue:10:1 lint/a11y/useHtmlLang ━━━━━━━━━━━━━━
9 │
> 10 │ <html>
│ ^^^^^^
> 11 │ <head>
> 12 │ <title>Svelte</title>
> 13 │ </head>
> 14 │ <body></body>
> 15 │ </html>
│ ^^^^^^^
16 │
17 │ <style>
11 │ <head>
12 │ <title>Svelte</title>

i Setting a lang attribute on HTML document elements configures the language used by screen readers when no user default is specified.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,8 @@ file.vue:15:1 lint/a11y/useHtmlLang ━━━━━━━━━━━━━━
14 │
> 15 │ <html>
│ ^^^^^^
> 16 │ <head>
> 17 │ <title>Svelte</title>
> 18 │ </head>
> 19 │ <body></body>
> 20 │ </html>
│ ^^^^^^^
21 │
22 │ <style>
16 │ <head>
17 │ <title>Svelte</title>

i Setting a lang attribute on HTML document elements configures the language used by screen readers when no user default is specified.

Expand Down
12 changes: 7 additions & 5 deletions crates/biome_html_analyze/src/a11y.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
//! 2. **Element helpers** (public): Higher-level checks on HTML elements
//! 3. **Type-specific variants** (public): Optimized versions that avoid cloning

use biome_html_syntax::HtmlAttribute;
use biome_html_syntax::element_ext::AnyHtmlTagElement;
use biome_html_syntax::{AnyHtmlElement, HtmlAttribute};

// ============================================================================
// Core attribute value helpers (private)
Expand Down Expand Up @@ -90,7 +90,7 @@ pub(crate) fn is_hidden_from_screen_reader(element: &AnyHtmlTagElement) -> bool
/// Unlike [`is_aria_hidden_value_truthy`], this only matches the exact string `"true"`.
///
/// Ref: <https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-hidden>
pub(crate) fn is_aria_hidden_true(element: &AnyHtmlElement) -> bool {
pub(crate) fn is_aria_hidden_true(element: &AnyHtmlTagElement) -> bool {
element
.find_attribute_by_name("aria-hidden")
.is_some_and(|attr| is_strict_true_value(&attr))
Expand All @@ -102,7 +102,9 @@ pub(crate) fn is_aria_hidden_true(element: &AnyHtmlElement) -> bool {
/// Useful for code fixes that need to reference the attribute node.
///
/// Ref: <https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-hidden>
pub(crate) fn get_truthy_aria_hidden_attribute(element: &AnyHtmlElement) -> Option<HtmlAttribute> {
pub(crate) fn get_truthy_aria_hidden_attribute(
element: &AnyHtmlTagElement,
) -> Option<HtmlAttribute> {
let attribute = element.find_attribute_by_name("aria-hidden")?;
if is_aria_hidden_value_truthy(&attribute) {
Some(attribute)
Expand All @@ -114,15 +116,15 @@ pub(crate) fn get_truthy_aria_hidden_attribute(element: &AnyHtmlElement) -> Opti
/// Returns `true` if the element has the named attribute with a non-empty value.
///
/// Whitespace-only values are considered empty.
pub(crate) fn has_non_empty_attribute(element: &AnyHtmlElement, name: &str) -> bool {
pub(crate) fn has_non_empty_attribute(element: &AnyHtmlTagElement, name: &str) -> bool {
element
.find_attribute_by_name(name)
.is_some_and(|attr| has_non_empty_value(&attr))
}

/// Returns `true` if the element has an accessible name via `aria-label`,
/// `aria-labelledby`, or `title` attributes.
pub(crate) fn has_accessible_name(element: &AnyHtmlElement) -> bool {
pub(crate) fn has_accessible_name(element: &AnyHtmlTagElement) -> bool {
has_non_empty_attribute(element, "aria-label")
|| has_non_empty_attribute(element, "aria-labelledby")
|| has_non_empty_attribute(element, "title")
Expand Down
6 changes: 4 additions & 2 deletions crates/biome_html_analyze/src/a11y_tests.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
use super::*;
use biome_html_parser::parse_html;
use biome_html_syntax::HtmlRoot;
use biome_html_syntax::{AnyHtmlElement, HtmlRoot};
use biome_rowan::AstNode;

/// Helper to parse HTML and extract the first element
fn parse_first_element(html: &str) -> AnyHtmlElement {
fn parse_first_element(html: &str) -> AnyHtmlTagElement {
let parsed = parse_html(html, Default::default());
let root = HtmlRoot::cast(parsed.syntax()).unwrap();
root.syntax()
.descendants()
.find_map(AnyHtmlElement::cast)
.expect("No element found in parsed HTML")
.as_any_html_tag_element()
.expect("No opening or self-closing element found")
}

// ============================================================================
Expand Down
1 change: 1 addition & 0 deletions crates/biome_html_analyze/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod lint;
pub mod options;
mod registry;
mod suppression_action;
mod utils;

pub use crate::registry::visit_registry;
use crate::suppression_action::HtmlSuppressionAction;
Expand Down
21 changes: 9 additions & 12 deletions crates/biome_html_analyze/src/lint/a11y/no_autofocus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ use biome_analyze::{
use biome_console::markup;
use biome_diagnostics::Severity;
use biome_html_syntax::element_ext::AnyHtmlTagElement;
use biome_html_syntax::{HtmlAttribute, HtmlElement, HtmlSelfClosingElement};
use biome_html_syntax::{HtmlAttribute, HtmlElement, HtmlFileSource, HtmlSelfClosingElement};
use biome_rowan::{AstNode, BatchMutationExt};
use biome_rule_options::no_autofocus::NoAutofocusOptions;

use crate::HtmlRuleAction;
use crate::utils::is_html_tag;

declare_lint_rule! {
/// Enforce that the `autofocus` attribute is not used on elements.
Expand Down Expand Up @@ -72,14 +73,15 @@ impl Rule for NoAutofocus {

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

// Check if this is an autofocus attribute
if !is_autofocus_attribute(node) {
return None;
}

// Check if element is inside a dialog or has popover attribute in ancestors
if is_inside_allowed_context(node).unwrap_or(false) {
if is_inside_allowed_context(node, source_type).unwrap_or(false) {
return None;
}

Expand Down Expand Up @@ -128,7 +130,7 @@ fn is_autofocus_attribute(node: &HtmlAttribute) -> bool {
/// Note: We skip the first [HtmlElement] (the one containing the autofocus attribute)
/// because we only want to check if it's *inside* a dialog/popover, not if
/// it *is* the dialog/popover itself.
fn is_inside_allowed_context(attr: &HtmlAttribute) -> Option<bool> {
fn is_inside_allowed_context(attr: &HtmlAttribute, source_type: &HtmlFileSource) -> Option<bool> {
let mut skip_first_element = true;

// Walk up the ancestors to find if we're inside a dialog or popover
Expand All @@ -142,7 +144,7 @@ fn is_inside_allowed_context(attr: &HtmlAttribute) -> Option<bool> {
continue;
}

if is_dialog_or_popover(&tag_element) {
if is_dialog_or_popover(&tag_element, source_type) {
return Some(true);
}
}
Expand All @@ -161,12 +163,7 @@ fn get_tag_element(node: &biome_html_syntax::HtmlSyntaxNode) -> Option<AnyHtmlTa
}

/// Check if the tag element is a dialog or has popover attribute
fn is_dialog_or_popover(tag_element: &AnyHtmlTagElement) -> bool {
let is_dialog = tag_element
.name()
.ok()
.and_then(|n| n.token_text_trimmed())
.is_some_and(|text| text.eq_ignore_ascii_case("dialog"));

is_dialog || tag_element.find_attribute_by_name("popover").is_some()
fn is_dialog_or_popover(element: &AnyHtmlTagElement, source_type: &HtmlFileSource) -> bool {
is_html_tag(element, source_type, "dialog")
|| element.find_attribute_by_name("popover").is_some()
}
17 changes: 10 additions & 7 deletions crates/biome_html_analyze/src/lint/a11y/no_distracting_elements.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ use biome_analyze::context::RuleContext;
use biome_analyze::{Ast, FixKind, Rule, RuleDiagnostic, RuleSource, declare_lint_rule};
use biome_console::markup;
use biome_diagnostics::Severity;
use biome_html_syntax::AnyHtmlElement;
use biome_html_syntax::{AnyHtmlElement, HtmlFileSource};
use biome_rowan::BatchMutationExt;
use biome_rowan::{AstNode, TokenText};
use biome_rule_options::no_distracting_elements::NoDistractingElementsOptions;

use crate::HtmlRuleAction;
use crate::utils::is_html_tag;

declare_lint_rule! {
/// Enforces that no distracting elements are used.
Expand Down Expand Up @@ -57,10 +58,16 @@ impl Rule for NoDistractingElements {

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let element = ctx.query();
let element_name = element.name()?;
if is_marquee_or_blink_element(element_name.text()) {
let source_type = ctx.source_type::<HtmlFileSource>();

let tag_element = element.clone().as_any_html_tag_element()?;
let element_name = tag_element.tag_name()?;
if is_html_tag(&tag_element, source_type, "marquee")
|| is_html_tag(&tag_element, source_type, "blink")
{
return Some(element_name);
}

None
}

Expand Down Expand Up @@ -91,7 +98,3 @@ impl Rule for NoDistractingElements {
))
}
}

fn is_marquee_or_blink_element(element_name: &str) -> bool {
element_name.eq_ignore_ascii_case("marquee") || element_name.eq_ignore_ascii_case("blink")
}
14 changes: 6 additions & 8 deletions crates/biome_html_analyze/src/lint/a11y/no_header_scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ use biome_analyze::context::RuleContext;
use biome_analyze::{Ast, FixKind, Rule, RuleDiagnostic, RuleSource, declare_lint_rule};
use biome_console::markup;
use biome_diagnostics::Severity;
use biome_html_syntax::{AnyHtmlElement, HtmlAttribute};
use biome_html_syntax::element_ext::AnyHtmlTagElement;
use biome_html_syntax::{HtmlAttribute, HtmlFileSource};
use biome_rowan::{AstNode, BatchMutationExt};
use biome_rule_options::no_header_scope::NoHeaderScopeOptions;

use crate::HtmlRuleAction;
use crate::utils::is_html_tag;

declare_lint_rule! {
/// The scope prop should be used only on `<th>` elements.
Expand Down Expand Up @@ -50,16 +52,17 @@ declare_lint_rule! {
}

impl Rule for NoHeaderScope {
type Query = Ast<AnyHtmlElement>;
type Query = Ast<AnyHtmlTagElement>;
type State = HtmlAttribute;
type Signals = Option<Self::State>;
type Options = NoHeaderScopeOptions;

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

// Check if element is NOT a th element and has a scope attribute
if is_th_element(element)? {
if is_html_tag(element, source_type, "th") {
return None;
}

Expand Down Expand Up @@ -97,8 +100,3 @@ impl Rule for NoHeaderScope {
))
}
}

// Helper function to check if element is a th element
fn is_th_element(element: &AnyHtmlElement) -> Option<bool> {
Some(element.name()?.text().eq_ignore_ascii_case("th"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ use biome_analyze::context::RuleContext;
use biome_analyze::{Ast, FixKind, Rule, RuleDiagnostic, RuleSource, declare_lint_rule};
use biome_console::markup;
use biome_diagnostics::Severity;
use biome_html_syntax::{AnyHtmlElement, HtmlAttribute};
use biome_html_syntax::HtmlAttribute;
use biome_html_syntax::element_ext::AnyHtmlTagElement;
use biome_rowan::{AstNode, BatchMutationExt, TextRange};
use biome_rule_options::no_positive_tabindex::NoPositiveTabindexOptions;

Expand Down Expand Up @@ -56,7 +57,7 @@ pub struct NoPositiveTabindexState {
}

impl Rule for NoPositiveTabindex {
type Query = Ast<AnyHtmlElement>;
type Query = Ast<AnyHtmlTagElement>;
type State = NoPositiveTabindexState;
type Signals = Option<Self::State>;
type Options = NoPositiveTabindexOptions;
Expand Down
9 changes: 4 additions & 5 deletions crates/biome_html_analyze/src/lint/a11y/no_redundant_alt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use biome_rowan::AstNode;
use biome_rule_options::is_redundant_alt;
use biome_rule_options::no_redundant_alt::NoRedundantAltOptions;

use crate::utils::is_html_tag;

declare_lint_rule! {
/// Enforce `img` alt prop does not contain the word "image", "picture", or "photo".
///
Expand Down Expand Up @@ -54,12 +56,9 @@ impl Rule for NoRedundantAlt {

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

let name = node.name().ok()?.token_text_trimmed()?;
if (file_source.is_html() && !name.eq_ignore_ascii_case("img"))
|| (!file_source.is_html() && name != "img")
{
if !is_html_tag(node, source_type, "img") {
return None;
}

Expand Down
10 changes: 6 additions & 4 deletions crates/biome_html_analyze/src/lint/a11y/no_svg_without_title.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use biome_analyze::{Ast, Rule, RuleDiagnostic, context::RuleContext, declare_lint_rule};
use biome_console::markup;
use biome_diagnostics::Severity;
use biome_html_syntax::{AnyHtmlElement, HtmlAttribute, HtmlElementList};
use biome_html_syntax::{AnyHtmlElement, HtmlAttribute, HtmlElementList, HtmlFileSource};
use biome_rowan::AstNode;
use biome_rule_options::no_svg_without_title::NoSvgWithoutTitleOptions;
use biome_string_case::StrLikeExtension;

use crate::a11y::is_aria_hidden_true;
use crate::{a11y::is_aria_hidden_true, utils::is_html_tag};

const NAME_REQUIRED_ROLES: &[&str] = &["img", "image", "graphics-document", "graphics-symbol"];

Expand Down Expand Up @@ -128,12 +128,14 @@ impl Rule for NoSvgWithoutTitle {

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

if node.name()? != "svg" {
let tag_element = node.clone().as_any_html_tag_element()?;
if !is_html_tag(&tag_element, source_type, "svg") {
return None;
}

if is_aria_hidden_true(node) {
if is_aria_hidden_true(&tag_element) {
return None;
}

Expand Down
Loading
Loading