Thanks to visit codestin.com
Credit goes to Github.com

Skip to content

feat(lint): port useValidAnchor to html#8987

Open
DerTimonius wants to merge 4 commits intobiomejs:nextfrom
DerTimonius:feat/html-use-valid-anchor
Open

feat(lint): port useValidAnchor to html#8987
DerTimonius wants to merge 4 commits intobiomejs:nextfrom
DerTimonius:feat/html-use-valid-anchor

Conversation

@DerTimonius
Copy link
Contributor

Summary

This PR ports the useValidAnchor a11y rule to HTML as part of #8155. It was mostly a 1:1 port of the original rule with small tweaks.

As noted in the original issue, we are unable to detect whether or not the given JS variables are null or undefined, so I have not added checks for those cases.

I have not used any AI assistance for this PR.

Test Plan

  • Added .html, .astro and .svelte files
  • both valid and invalid

Docs

Doc comments have been added, with a note regarding the aforementioned issue with null and undefined

@changeset-bot
Copy link

changeset-bot bot commented Feb 6, 2026

🦋 Changeset detected

Latest commit: 2af6116

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-Linter Area: linter L-HTML Language: HTML and super languages labels Feb 6, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 6, 2026

Walkthrough

This pull request ports the useValidAnchor accessibility rule from JavaScript to HTML, introducing a new lint rule that validates anchor element usage. The rule detects three error conditions: missing href attributes, incorrect hrefs (empty strings, hash-only, or javascript: pseudo-protocols), and onclick handlers without href attributes. Test specifications are provided across three template formats—HTML, Astro, and Svelte—covering both valid and invalid anchor usage patterns.

Possibly related PRs

  • feat: implement noScriptUrl rule #8232: Adds noScriptUrl HTML linting rule that similarly detects and disallows javascript: URLs in anchor elements, providing complementary validation for safe anchor configurations.

Suggested labels

A-Linter, A-Diagnostic, L-HTML

Suggested reviewers

  • ematipico
  • dyc3
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: porting the useValidAnchor rule to HTML.
Description check ✅ Passed The description is directly related to the changeset, providing context about the port, limitations, and testing approach.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

Copy link
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: 5

🤖 Fix all issues with AI agents
In `@crates/biome_html_analyze/src/lint/a11y/use_valid_anchor.rs`:
- Around line 84-91: The doc comment for the UseValidAnchorState::CantBeAnchor
enum variant is incorrect — it currently says "The element has `href` and
`onClick`" but the variant is emitted when the element has no `href` and only an
`onclick` handler; update the comment on CantBeAnchor to accurately state that
it represents an element with no href and an onclick (or interactive-only)
handler so the semantics match its use in the validation logic referencing
UseValidAnchorState::CantBeAnchor.
- Around line 120-123: In the match arm for Self::IncorrectHref(_) in
use_valid_anchor.rs, fix the typo in the message string by removing the extra
article so it reads "The href attribute should be a valid URL" instead of "The
href attribute should be a valid a URL"; update the string literal produced by
the markup! invocation (the Self::IncorrectHref(_) arm) accordingly so the
returned ownership (.to_owned()) contains the corrected text.
- Around line 10-78: Fix two small typos in the UseValidAnchor rule docs: in the
doc comment for the declare_lint_rule! block (the UseValidAnchor rule) replace
the double space in the phrase "it's now  easier" with a single space and change
"it can source of invalid links" to "it can be a source of invalid links" so the
documentation reads correctly; update the comment text inside the
declare_lint_rule! macro where the rule documentation is written.
- Around line 160-167: Replace the current static-value check that uses
anchor_attribute.value() and is_none_or(...) with a call to as_static_value()?
so dynamic hrefs are not treated as errors (i.e., if let Some(const_str) =
anchor_attribute.as_static_value()? { ... } ), and when returning the error
produce UseValidAnchorState::IncorrectHref(anchor_attribute.range()) only for
truly invalid static values; also change the javascript protocol check from
contains("javascript:") to a prefix check after trimming (e.g.,
trimmed.starts_with("javascript:")) so queries containing "javascript:" do not
falsely match.

In `@crates/biome_html_analyze/tests/specs/a11y/useValidAnchor/invalid.svelte`:
- Around line 1-4: Add a fixture that includes a bare anchor element to trigger
the MissingHrefAttribute variant: insert a plain "<a>no attributes</a>" into one
of the invalid test files (e.g.,
crates/biome_html_analyze/tests/specs/a11y/useValidAnchor/invalid.svelte) so the
rule's (None, None) branch (MissingHrefAttribute at line 174) is exercised; also
update the test expectations/diagnostics for that fixture to assert the
MissingHrefAttribute diagnostic is produced.
🧹 Nitpick comments (1)
.changeset/cold-pianos-try.md (1)

1-5: Changeset could be more informative.

Per project conventions, changesets for new lint rules should include:

  1. A link to the rule's documentation page.
  2. An example of an invalid case (inline code or code block).

Something along these lines would do:

Ported the [useValidAnchor](https://biomejs.dev/linter/rules/use-valid-anchor/) rule to HTML.

Example of invalid code:

`<a href="javascript:void(0)">navigate here</a>`

It is not currently possible to detect whether a given variable is `null` or `undefined`, so those cases are not covered.

Based on learnings: "For new lint rules in changesets, show an example of invalid case in inline code or code block" and "In changesets, reference rules and assists with links to the website documentation".

Comment on lines +10 to +78
declare_lint_rule! {
/// Enforce that all anchors are valid, and they are navigable elements.
///
/// The anchor element (`<a></a>`) - also called **hyperlink** - is an important element
/// that allows users to navigate pages, in the same page, same website or on another website.
///
/// While before it was possible to attach logic to an anchor element, with the advent of JSX libraries,
/// it's now easier to attach logic to any HTML element, anchors included.
///
/// This rule is designed to prevent users from attaching logic at the click of anchors when the `href`
/// provided to the anchor element is not valid. Avoid using `#` symbol inside the `href` when you are
/// attaching the logic to the anchor element. If the anchor has logic attached to it with an incorrect `href`
/// the rules suggests to turn it to a `button`, because that's likely what the user wants.
///
/// Anchor `<a></a>` elements should be used for navigation, while `<button></button>` should be
/// used for user interaction.
///
/// There are **many reasons** why an anchor should not have a logic with an incorrect `href` attribute:
/// - it can disrupt the correct flow of the user navigation e.g. a user that wants to open the link
/// in another tab, but the default "click" behavior is prevented
/// - it can source of invalid links, and crawlers can't navigate the website, risking to penalize
/// SEO ranking
///
///
/// :::note
/// At the moment, it is not possible to check whether or not a specified variable in the `href` attribute resolves
/// to `null` or `undefined`.
/// :::
///
/// For a detailed explanation, check out https://marcysutton.com/links-vs-buttons-in-modern-web-applications
///
/// ## Examples
///
/// ### Invalid
///
/// ```html,expect_diagnostic
/// <a href>navigate here</a>
/// ```
/// ```html,expect_diagnostic
/// <a href="javascript:void(0)">navigate here</a>
/// ```
/// ```html,expect_diagnostic
/// <a onclick={something}>navigate here</a>
/// ```
///
/// ### Valid
///
/// ```html
/// <a href="https://example.com" onclick={something}>navigate here</a>
/// ```
/// ```html
/// <a href={`https://www.javascript.com`}>navigate here</a>
/// ```
/// ```astro
/// <a href={somewhere}>navigate here</a>
/// ```
///
/// ## Accessibility guidelines
///
/// - [WCAG 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard)
///
pub UseValidAnchor {
version: "next",
name: "useValidAnchor",
language: "html",
sources: &[RuleSource::EslintJsxA11y("anchor-is-valid").same(), RuleSource::EslintQwik("jsx-a").same()],
recommended: true,
severity: Severity::Error,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Rule declaration and documentation look solid overall.

Metadata is correct: version: "next", severity: Error for a11y, recommended: true, sources properly declared with .same(), and no fix_kind (since there's no auto-fix). Documentation follows the expected format with ## Examples, ### Invalid, and ### Valid subsections.

A couple of small doc nits:

  • Line 17: double space in "it's now easier".
  • Line 30: "it can source of invalid links" → should be "it can be a source of invalid links".
🤖 Prompt for AI Agents
In `@crates/biome_html_analyze/src/lint/a11y/use_valid_anchor.rs` around lines 10
- 78, Fix two small typos in the UseValidAnchor rule docs: in the doc comment
for the declare_lint_rule! block (the UseValidAnchor rule) replace the double
space in the phrase "it's now  easier" with a single space and change "it can
source of invalid links" to "it can be a source of invalid links" so the
documentation reads correctly; update the comment text inside the
declare_lint_rule! macro where the rule documentation is written.

Comment on lines +84 to +91
pub enum UseValidAnchorState {
/// The anchor element has not `href` attribute
MissingHrefAttribute(TextRange),
/// The value assigned to attribute `href` is not valid
IncorrectHref(TextRange),
/// The element has `href` and `onClick`
CantBeAnchor(TextRange),
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Misleading doc comment on CantBeAnchor variant.

Line 89 says "The element has href and onClick", but this variant is used (Line 169-172) when there is no href and only onclick. The comment should reflect the actual semantics.

Proposed fix
-    /// The element has `href` and `onClick`
+    /// The element has `onClick` without `href`
     CantBeAnchor(TextRange),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub enum UseValidAnchorState {
/// The anchor element has not `href` attribute
MissingHrefAttribute(TextRange),
/// The value assigned to attribute `href` is not valid
IncorrectHref(TextRange),
/// The element has `href` and `onClick`
CantBeAnchor(TextRange),
}
pub enum UseValidAnchorState {
/// The anchor element has not `href` attribute
MissingHrefAttribute(TextRange),
/// The value assigned to attribute `href` is not valid
IncorrectHref(TextRange),
/// The element has `onClick` without `href`
CantBeAnchor(TextRange),
}
🤖 Prompt for AI Agents
In `@crates/biome_html_analyze/src/lint/a11y/use_valid_anchor.rs` around lines 84
- 91, The doc comment for the UseValidAnchorState::CantBeAnchor enum variant is
incorrect — it currently says "The element has `href` and `onClick`" but the
variant is emitted when the element has no `href` and only an `onclick` handler;
update the comment on CantBeAnchor to accurately state that it represents an
element with no href and an onclick (or interactive-only) handler so the
semantics match its use in the validation logic referencing
UseValidAnchorState::CantBeAnchor.

Comment on lines +120 to +123
Self::IncorrectHref(_) => (markup! {
"The href attribute should be a valid a URL"
})
.to_owned(),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Typo: extra article in note text.

"The href attribute should be a valid a URL" — one a too many.

Proposed fix
-            Self::IncorrectHref(_) => (markup! {
-                "The href attribute should be a valid a URL"
-            })
+            Self::IncorrectHref(_) => (markup! {
+                "The href attribute should be a valid URL"
+            })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Self::IncorrectHref(_) => (markup! {
"The href attribute should be a valid a URL"
})
.to_owned(),
Self::IncorrectHref(_) => (markup! {
"The href attribute should be a valid URL"
})
.to_owned(),
🤖 Prompt for AI Agents
In `@crates/biome_html_analyze/src/lint/a11y/use_valid_anchor.rs` around lines 120
- 123, In the match arm for Self::IncorrectHref(_) in use_valid_anchor.rs, fix
the typo in the message string by removing the extra article so it reads "The
href attribute should be a valid URL" instead of "The href attribute should be a
valid a URL"; update the string literal produced by the markup! invocation (the
Self::IncorrectHref(_) arm) accordingly so the returned ownership (.to_owned())
contains the corrected text.

Comment on lines +160 to +167
let static_value = anchor_attribute.value();
if static_value.is_none_or(|const_str| {
const_str.is_empty()
|| const_str == "#"
|| const_str.contains("javascript:")
}) {
return Some(UseValidAnchorState::IncorrectHref(anchor_attribute.range()));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Search for the value() method definition on HTML attributes
echo "=== Searching for value() method on HTML attribute type ==="
rg -n "fn value" crates/biome_html_analyze --type rust -A 5

echo ""
echo "=== Looking for the attribute type definition ==="
rg -n "trait.*Attribute\|struct.*Attribute" crates/biome_html_analyze --type rust | head -20

echo ""
echo "=== Searching for test fixtures with dynamic href patterns ==="
find . -name "*.astro" -o -name "*.svelte" | xargs grep -l "href.*{" 2>/dev/null | head -5

echo ""
echo "=== Checking what is_none_or does ==="
rg -n "is_none_or" --type rust -B 2 -A 2

Repository: biomejs/biome

Length of output: 34130


🏁 Script executed:

#!/bin/bash

# Find the value() method definition in HTML syntax
echo "=== Searching for value() method definition in HTML syntax ==="
rg -n "fn value" crates/biome_html_syntax --type rust -B 2 -A 8

echo ""
echo "=== Checking test fixture: valid.astro ==="
head -50 crates/biome_html_analyze/tests/specs/a11y/useValidAnchor/valid.astro 2>/dev/null || echo "File not found"

echo ""
echo "=== Checking test fixture: valid.svelte ==="
head -50 crates/biome_html_analyze/tests/specs/a11y/useValidAnchor/valid.svelte 2>/dev/null || echo "File not found"

echo ""
echo "=== Comparing JS rule with contains check ==="
sed -n '160,190p' crates/biome_js_analyze/src/lint/a11y/use_valid_anchor.rs

echo ""
echo "=== Finding how HTML attributes work ==="
rg -n "impl.*HtmlAttribute" crates/biome_html_syntax --type rust -A 20 | head -60

Repository: biomejs/biome

Length of output: 24022


🏁 Script executed:

#!/bin/bash

echo "=== Searching for invalid test fixtures (should contain dynamic hrefs flagged as errors) ==="
if [ -f crates/biome_html_analyze/tests/specs/a11y/useValidAnchor/invalid.astro ]; then
  head -30 crates/biome_html_analyze/tests/specs/a11y/useValidAnchor/invalid.astro
else
  echo "File not found"
fi

echo ""
echo "=== Checking entire use_valid_anchor.rs to understand the logic ==="
sed -n '140,200p' crates/biome_html_analyze/src/lint/a11y/use_valid_anchor.rs

echo ""
echo "=== Comparing as_static_value method ==="
cat -n crates/biome_html_syntax/src/attribute_ext.rs | head -15

Repository: biomejs/biome

Length of output: 3017


Remove is_none_or and use as_static_value()? instead to avoid false positives on dynamic href values.

For dynamic attributes like <a href={somewhere}>, value() returns None because it only extracts static string values. Using is_none_or(...) treats None as an error, which incorrectly flags valid dynamic hrefs. The existing as_static_value() method handles this correctly (returning None for HtmlSingleTextExpression), so use that with the ? operator like the JS rule does.

Additionally, contains("javascript:") is overly broad—it would match strings like "https://example.com?q=javascript:void". Use a prefix check (or starts-with after trimming) instead.

🤖 Prompt for AI Agents
In `@crates/biome_html_analyze/src/lint/a11y/use_valid_anchor.rs` around lines 160
- 167, Replace the current static-value check that uses anchor_attribute.value()
and is_none_or(...) with a call to as_static_value()? so dynamic hrefs are not
treated as errors (i.e., if let Some(const_str) =
anchor_attribute.as_static_value()? { ... } ), and when returning the error
produce UseValidAnchorState::IncorrectHref(anchor_attribute.range()) only for
truly invalid static values; also change the javascript protocol check from
contains("javascript:") to a prefix check after trimming (e.g.,
trimmed.starts_with("javascript:")) so queries containing "javascript:" do not
falsely match.

Comment on lines +1 to +4
<!-- should generate diagnostics -->
<a href="javascript:void(0)">invalid</a>
<a href>invalid</a>
<a onclick={something}>invalid</a>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing test case for the MissingHrefAttribute variant.

All three invalid fixtures (.svelte, .astro, .html) cover IncorrectHref and CantBeAnchor, but none include a bare <a>no attributes</a> — the only way to trigger MissingHrefAttribute (the (None, None) branch at Line 174 of the rule). Worth adding to at least one fixture.

🤖 Prompt for AI Agents
In `@crates/biome_html_analyze/tests/specs/a11y/useValidAnchor/invalid.svelte`
around lines 1 - 4, Add a fixture that includes a bare anchor element to trigger
the MissingHrefAttribute variant: insert a plain "<a>no attributes</a>" into one
of the invalid test files (e.g.,
crates/biome_html_analyze/tests/specs/a11y/useValidAnchor/invalid.svelte) so the
rule's (None, None) branch (MissingHrefAttribute at line 174) is exercised; also
update the test expectations/diagnostics for that fixture to assert the
MissingHrefAttribute diagnostic is produced.

@codspeed-hq
Copy link

codspeed-hq bot commented Feb 6, 2026

Merging this PR will degrade performance by 8.16%

❌ 2 regressed benchmarks
✅ 1 untouched benchmark
🆕 1 new benchmark
⏩ 152 skipped benchmarks1

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
index_1033418810622582172.html[cached] 102 µs 110.8 µs -7.96%
index_1033418810622582172.html[uncached] 118.2 µs 128.7 µs -8.16%
🆕 html_analyzer[index_1033418810622582172.html] N/A 418.9 µs N/A

Comparing DerTimonius:feat/html-use-valid-anchor (2af6116) with next (38c3fd7)

Open in CodSpeed

Footnotes

  1. 152 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.

"@biomejs/biome": minor
---

This PR ports the `useValidAnchor` rule to HTML. As it's not possible at the moment to detect, whether or not a given variable is `null` or `undefined`, those cases are not covered.
Copy link
Contributor

Choose a reason for hiding this comment

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

As it's not possible at the moment to detect...

This is not worth calling out in the changeset, imo.

"@biomejs/biome": minor
---

This PR ports the `useValidAnchor` rule to HTML. As it's not possible at the moment to detect, whether or not a given variable is `null` or `undefined`, those cases are not covered.
Copy link
Contributor

Choose a reason for hiding this comment

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

  • needs to link to the rule docs.
  • needs to describe the change as it should appear in the changelog

take a look at .changeset/html-no-positive-tabindex.md to see a good example

Comment on lines +94 to +97
fn message(&self) -> MarkupBuf {
match self {
Self::MissingHrefAttribute(_) => {
(markup! {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: would prefer for these to be in the fn diagnostic()

}
}

impl Rule for UseValidAnchor {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: move any helpers below this impl Rule block. the impl Rule should be the first thing after the declare_lint_rule!

Copy link
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.

The rule has some bugs, which I highlighted in some comments. We should add a .vue file too in the tests

Comment on lines +34 to +37
/// :::note
/// At the moment, it is not possible to check whether or not a specified variable in the `href` attribute resolves
/// to `null` or `undefined`.
/// :::
Copy link
Member

Choose a reason for hiding this comment

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

I don't think Biome will ever be able to do, so maybe it's not worth mentioning it, and just assume that rule works without that

let node = ctx.query();
let name = node.name().ok()?.token_text_trimmed();

if name.is_some_and(|n| n == "a") {
Copy link
Member

Choose a reason for hiding this comment

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

Bug: the name of a tag is case-insensitive in .html but case-sensitive in files like .vue, .svelte and .astro.

Here's an example of implementation

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")
{
return None;
}

if static_value.is_none_or(|const_str| {
const_str.is_empty()
|| const_str == "#"
|| const_str.contains("javascript:")
Copy link
Member

Choose a reason for hiding this comment

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

This is incorrect. It should use starts_with()

@@ -0,0 +1,4 @@
<!-- should not generate diagnostics -->
<a href="#id">ok</a>
Copy link
Member

Choose a reason for hiding this comment

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

This test should contain components too

@@ -0,0 +1,4 @@
<!-- should generate diagnostics -->
<a href="javascript:void(0)">invalid</a>
Copy link
Member

Choose a reason for hiding this comment

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

This test should contain components too

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants