Skip to content

feat(linter): add noAutofocus rule for HTML#8641

Merged
dyc3 merged 4 commits intobiomejs:nextfrom
tt-a1i:feat/html-no-autofocus
Jan 2, 2026
Merged

feat(linter): add noAutofocus rule for HTML#8641
dyc3 merged 4 commits intobiomejs:nextfrom
tt-a1i:feat/html-no-autofocus

Conversation

@tt-a1i
Copy link
Copy Markdown
Contributor

@tt-a1i tt-a1i commented Jan 1, 2026

Summary

Port the noAutofocus a11y rule to HTML. This rule enforces that the autofocus attribute is not used on elements, as it can cause usability issues for sighted and non-sighted users.

The rule allows autofocus in the following contexts:

  • Inside <dialog> elements
  • Inside elements with the popover attribute

Part of #8155

Test plan

  • Added invalid.html test with 5 cases that should trigger the rule
  • Added valid.html test with dialog and popover exception cases
  • All tests pass with cargo test -p biome_html_analyze no_autofocus

AI Assistance Disclosure

I used Codex to review the changes, sanity-check the implementation against existing patterns, and help spot potential edge cases.

Port the noAutofocus a11y rule to HTML. The rule enforces that the
autofocus attribute is not used on elements, with exceptions for
dialog elements and elements with the popover attribute.

Part of biomejs#8155
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jan 1, 2026

🦋 Changeset detected

Latest commit: d83126c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 14 packages
Name Type
@biomejs/biome Minor
@biomejs/cli-win32-x64 Minor
@biomejs/cli-win32-arm64 Minor
@biomejs/cli-darwin-x64 Minor
@biomejs/cli-darwin-arm64 Minor
@biomejs/cli-linux-x64 Minor
@biomejs/cli-linux-arm64 Minor
@biomejs/cli-linux-x64-musl Minor
@biomejs/cli-linux-arm64-musl Minor
@biomejs/wasm-web Minor
@biomejs/wasm-bundler Minor
@biomejs/wasm-nodejs Minor
@biomejs/backend-jsonrpc Patch
@biomejs/js-api Major

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

@github-actions github-actions Bot added A-Project Area: project A-Linter Area: linter L-HTML Language: HTML and super languages labels Jan 1, 2026
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Jan 1, 2026

CodSpeed Performance Report

Merging #8641 will not alter performance

Comparing tt-a1i:feat/html-no-autofocus (d83126c) with next (857d450)

Summary

✅ 1 untouched
⏩ 154 skipped1

Footnotes

  1. 154 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@tt-a1i tt-a1i marked this pull request as ready for review January 1, 2026 13:37
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 1, 2026

Walkthrough

This change adds a new HTML accessibility lint rule noAutofocus to the @biomejs/biome package. The rule flags use of the autofocus attribute except when the element is inside a dialog or an element with a popover attribute. It introduces the rule implementation, integrates it into the A11y lint group, and adds valid/invalid HTML test cases exercising the rule and its automatic fix to remove the attribute.

Possibly related PRs

Suggested reviewers

  • dyc3
  • ematipico

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding a new noAutofocus lint rule for HTML accessibility checks.
Description check ✅ Passed The description is well-detailed and directly related to the changeset, explaining the rule's purpose, allowed contexts, test coverage, and including proper disclosures.
✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@dyc3 dyc3 mentioned this pull request Jan 2, 2026
32 tasks
Copy link
Copy Markdown
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. The code can be improved


/// Check if the element is inside an allowed context (dialog or popover)
///
/// Note: We skip the first element (the one containing the autofocus attribute)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Note: We skip the first element (the one containing the autofocus attribute)
/// Note: We skip the first [HtmlElement] (the one containing the autofocus attribute)

/// Note: We skip the first element (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) -> bool {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return Option<bool>, so that you can convert all let Ok() in .ok()?

Comment on lines +139 to +151
if let Ok(opening) = element.opening_element() {
if let Ok(name) = opening.name()
&& let Ok(token) = name.value_token()
&& token.text_trimmed().eq_ignore_ascii_case("dialog")
{
return true;
}
// Check if element has popover attribute
if opening.find_attribute_by_name("popover").is_some() {
return true;
}
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're repeating these checks, which means this code isn't well-maintainable. Instead, let's do something more generic.

We already have AnyHtmlTagElement from biome_html_syntax, let's use that so we can extract the name of the element and then do one single check

Address code review feedback:
- Add [HtmlElement] link in doc comment for better rustdoc
- Change is_inside_allowed_context to return Option<bool>
- Use AnyHtmlTagElement to eliminate duplicate HtmlElement/HtmlSelfClosingElement checks
- Extract helper functions for better maintainability
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
crates/biome_html_analyze/src/lint/a11y/no_autofocus.rs (2)

71-71: Options type is unused.

NoAutofocusOptions is declared but never accessed in the implementation. If options aren't currently needed, consider removing lines 9 and 71 to simplify the code.

🔎 Proposed change to remove unused Options

Remove the import on line 9:

-use biome_rule_options::no_autofocus::NoAutofocusOptions;

And change line 71 to:

-    type Options = NoAutofocusOptions;
+    type Options = ();

126-146: Consider simplifying return type to bool.

The function returns Option<bool> but always returns Some(...) (lines 141, 145), never None. This means the .unwrap_or(false) on line 82 never uses its default value.

Whilst a past review suggested Option<bool>, the current implementation doesn't have any operations that would benefit from early-returning None. Consider simplifying to fn is_inside_allowed_context(attr: &HtmlAttribute) -> bool and removing the Some(...) wrappers.

🔎 Proposed simplification
-fn is_inside_allowed_context(attr: &HtmlAttribute) -> Option<bool> {
+fn is_inside_allowed_context(attr: &HtmlAttribute) -> bool {
     let mut skip_first_element = true;
 
     // Walk up the ancestors to find if we're inside a dialog or popover
     for ancestor in attr.syntax().ancestors() {
         let Some(tag_element) = get_tag_element(&ancestor) else {
             continue;
         };
 
         if skip_first_element {
             skip_first_element = false;
             continue;
         }
 
         if is_dialog_or_popover(&tag_element) {
-            return Some(true);
+            return true;
         }
     }
 
-    Some(false)
+    false
 }

And update line 82:

-    if is_inside_allowed_context(node).unwrap_or(false) {
+    if is_inside_allowed_context(node) {
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3278074 and d83126c.

📒 Files selected for processing (1)
  • crates/biome_html_analyze/src/lint/a11y/no_autofocus.rs
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.rs: Use inline rustdoc documentation for rules, assists, and their options
Use the dbg!() macro for debugging output in Rust tests and code
Use doc tests (doctest) format with code blocks in rustdoc comments; ensure assertions pass in tests

Files:

  • crates/biome_html_analyze/src/lint/a11y/no_autofocus.rs
🧠 Learnings (14)
📓 Common learnings
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Set rule severity to `error` for rules in correctness, security, and a11y groups
📚 Learning: 2025-12-22T09:26:56.943Z
Learnt from: ematipico
Repo: biomejs/biome PR: 8537
File: crates/biome_js_analyze/src/lint/nursery/no_leaked_render.rs:167-210
Timestamp: 2025-12-22T09:26:56.943Z
Learning: When defining lint rules (declare_lint_rule!), only specify fix_kind if the rule implements an action(...) function. Rules that only emit diagnostics without a code fix should omit fix_kind. This applies to all Rust lint rule definitions under crates/.../src/lint (e.g., crates/biome_js_analyze/src/lint/...).

Applied to files:

  • crates/biome_html_analyze/src/lint/a11y/no_autofocus.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/lint/**/*.rs : Lint rules should perform static analysis of source code to detect invalid or error-prone patterns and emit diagnostics with proposed fixes

Applied to files:

  • crates/biome_html_analyze/src/lint/a11y/no_autofocus.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Specify `fix_kind: FixKind::Unsafe` in `declare_lint_rule!` for unsafe code actions

Applied to files:

  • crates/biome_html_analyze/src/lint/a11y/no_autofocus.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Specify `fix_kind: FixKind::Safe` in `declare_lint_rule!` for safe code actions

Applied to files:

  • crates/biome_html_analyze/src/lint/a11y/no_autofocus.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Use `declare_lint_rule!` macro to declare analyzer rule types and implement the RuleMeta trait

Applied to files:

  • crates/biome_html_analyze/src/lint/a11y/no_autofocus.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Add `sources` field with `RuleSource` to cite ESLint or other rules that inspired the implementation

Applied to files:

  • crates/biome_html_analyze/src/lint/a11y/no_autofocus.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Use `domains` field in `declare_lint_rule!` to tag rules that belong to specific concepts like testing or frameworks

Applied to files:

  • crates/biome_html_analyze/src/lint/a11y/no_autofocus.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Assist rules should detect refactoring opportunities and emit code action signals

Applied to files:

  • crates/biome_html_analyze/src/lint/a11y/no_autofocus.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Set rule severity to `error` for rules in correctness, security, and a11y groups

Applied to files:

  • crates/biome_html_analyze/src/lint/a11y/no_autofocus.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Add `deprecated` field to `declare_lint_rule!` macro when deprecating a rule

Applied to files:

  • crates/biome_html_analyze/src/lint/a11y/no_autofocus.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Use try operator `?` when `run` function returns `Option` to transform `Result` into `Option`

Applied to files:

  • crates/biome_html_analyze/src/lint/a11y/no_autofocus.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Use `RuleSource::Eslint(...).same()` when implementing a rule that matches the behavior of an ESLint rule

Applied to files:

  • crates/biome_html_analyze/src/lint/a11y/no_autofocus.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Use `RuleSource::Eslint(...).inspired()` when implementing a rule inspired by but with different behavior than an ESLint rule

Applied to files:

  • crates/biome_html_analyze/src/lint/a11y/no_autofocus.rs
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Lint project (depot-ubuntu-24.04-arm-16)
  • GitHub Check: Lint project (depot-windows-2022)
  • GitHub Check: Test (depot-windows-2022-16)
  • GitHub Check: Check Dependencies
  • GitHub Check: End-to-end tests
  • GitHub Check: Documentation
  • GitHub Check: Test (depot-ubuntu-24.04-arm-16)
  • GitHub Check: autofix
  • GitHub Check: Bench (biome_configuration)
🔇 Additional comments (3)
crates/biome_html_analyze/src/lint/a11y/no_autofocus.rs (3)

13-65: Well-structured rule declaration.

The metadata is correct: Error severity for a11y group, fix_kind specified with an action implementation, and proper source attribution. Documentation is comprehensive with clear examples.


113-119: Correct attribute check.

Case-insensitive comparison is appropriate for HTML attribute names.


148-167: Clean helper functions.

Good use of AnyHtmlTagElement to handle both HtmlElement and HtmlSelfClosingElement uniformly. The is_dialog_or_popover logic correctly checks both the tag name and the presence of a popover attribute.

@tt-a1i
Copy link
Copy Markdown
Contributor Author

tt-a1i commented Jan 2, 2026

Looks good. The code can be improved

hi, thank you for your review, i fix these issue already

Comment on lines +149 to +156
fn get_tag_element(node: &biome_html_syntax::HtmlSyntaxNode) -> Option<AnyHtmlTagElement> {
HtmlElement::cast_ref(node)
.and_then(|e| e.opening_element().ok())
.map(AnyHtmlTagElement::from)
.or_else(|| {
HtmlSelfClosingElement::cast_ref(node).map(|e| AnyHtmlTagElement::from(e.clone()))
})
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this function. You can do AnyHtmlTagElement::cast_ref(node) and it will work. Technically, this function isn't needed, and you can use the cast_ref inside the loop.

@tt-a1i
Copy link
Copy Markdown
Contributor Author

tt-a1i commented Jan 2, 2026

Thanks! I did try AnyHtmlTagElement::cast_ref(node) first, but it won’t match what we get from ancestors().

For normal tags like <div popover>…</div>, the ancestor node is HtmlElement, and the attributes live on its opening_element (HtmlOpeningElement). AnyHtmlTagElement only covers HtmlOpeningElement / HtmlSelfClosingElement, so casting a HtmlElement directly always returns None.

That’s why I keep get_tag_element(): it’s just the small adapter that turns HtmlElement -> opening_element (and also handles self-closing tags) so the rest of the check can be written once.

@ematipico
Copy link
Copy Markdown
Member

I understand now, thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Linter Area: linter A-Project Area: project L-HTML Language: HTML and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants