feat(lint): port useValidAnchor to html#8987
feat(lint): port useValidAnchor to html#8987DerTimonius wants to merge 4 commits intobiomejs:nextfrom
Conversation
🦋 Changeset detectedLatest commit: 2af6116 The changes in this PR will be included in the next version bump. This PR includes changesets to release 14 packages
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 |
WalkthroughThis 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
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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:
- A link to the rule's documentation page.
- 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".
| 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, | ||
| } |
There was a problem hiding this comment.
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.
| 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), | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| Self::IncorrectHref(_) => (markup! { | ||
| "The href attribute should be a valid a URL" | ||
| }) | ||
| .to_owned(), |
There was a problem hiding this comment.
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.
| 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.
| 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())); | ||
| } |
There was a problem hiding this comment.
🧩 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 2Repository: 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 -60Repository: 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 -15Repository: 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.
| <!-- should generate diagnostics --> | ||
| <a href="javascript:void(0)">invalid</a> | ||
| <a href>invalid</a> | ||
| <a onclick={something}>invalid</a> |
There was a problem hiding this comment.
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.
Merging this PR will degrade performance by 8.16%
Performance Changes
Comparing Footnotes
|
| "@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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
- 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
| fn message(&self) -> MarkupBuf { | ||
| match self { | ||
| Self::MissingHrefAttribute(_) => { | ||
| (markup! { |
There was a problem hiding this comment.
nit: would prefer for these to be in the fn diagnostic()
| } | ||
| } | ||
|
|
||
| impl Rule for UseValidAnchor { |
There was a problem hiding this comment.
nit: move any helpers below this impl Rule block. the impl Rule should be the first thing after the declare_lint_rule!
ematipico
left a comment
There was a problem hiding this comment.
The rule has some bugs, which I highlighted in some comments. We should add a .vue file too in the tests
| /// :::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`. | ||
| /// ::: |
There was a problem hiding this comment.
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") { |
There was a problem hiding this comment.
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
biome/crates/biome_html_analyze/src/lint/a11y/no_redundant_alt.rs
Lines 59 to 64 in 0a147f1
| if static_value.is_none_or(|const_str| { | ||
| const_str.is_empty() | ||
| || const_str == "#" | ||
| || const_str.contains("javascript:") |
There was a problem hiding this comment.
This is incorrect. It should use starts_with()
| @@ -0,0 +1,4 @@ | |||
| <!-- should not generate diagnostics --> | |||
| <a href="#id">ok</a> | |||
There was a problem hiding this comment.
This test should contain components too
| @@ -0,0 +1,4 @@ | |||
| <!-- should generate diagnostics --> | |||
| <a href="javascript:void(0)">invalid</a> | |||
There was a problem hiding this comment.
This test should contain components too
Summary
This PR ports the
useValidAnchora11y 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
nullorundefined, so I have not added checks for those cases.I have not used any AI assistance for this PR.
Test Plan
.html,.astroand.sveltefilesDocs
Doc comments have been added, with a note regarding the aforementioned issue with
nullandundefined