Skip to content

feat: add template-no-invalid-link-href#23

Open
johanrd wants to merge 3 commits intomasterfrom
feat/template-no-invalid-link-href
Open

feat: add template-no-invalid-link-href#23
johanrd wants to merge 3 commits intomasterfrom
feat/template-no-invalid-link-href

Conversation

@johanrd
Copy link
Copy Markdown
Owner

@johanrd johanrd commented Apr 21, 2026

Note

This is part of a series where Claude has audited eslint-plugin-ember against jsx-a11y, vuejs-accessibility, angular-eslint, lit-a11y and html-validate, ember-template-lint, and the HTML and WCAG specs.

  • Premise: The HTML spec requires href on <a>/<area> to have "a value that is a valid URL potentially surrounded by spaces." Placeholders like href="#", href="", and href="javascript:..." parse as valid URLs (so they don't violate the HTML spec per se) but fake a clickable link at the expense of keyboard/screen-reader semantics — the element announces as a link but doesn't navigate. This is an accessibility and UX anti-pattern documented by MDN ("Anchor elements are often abused as fake buttons by setting their href to # or javascript:void(0)... These bogus href values cause unexpected behavior... Use a <button> instead.").
  • Problem: Our existing template-link-href-attributes only checks for the presence of href. It accepts href="#" and href="javascript:void(0)" as valid.

The rule's premise is a pragmatic a11y/UX check backed by MDN guidance and peer-plugin prior art (jsx-a11y, lit-a11y), not a literal HTML-spec violation.

Fix: add template-no-invalid-link-href. Validates the href VALUE, complementing the existing presence check.

Flags

<a href="#">Click</a>
<a href="#!">Click</a>
<a href="">Click</a>
<a href>Click</a>                     {{! valueless = empty string per HTML syntax rules }}
<a href="javascript:void(0)">Click</a>

Allows

<a href="/about">About</a>
<a href="https://example.com/">External</a>
<a href="#section-id">Fragment link</a>
<a href="mailto:[email protected]">Email</a>
<a href="tel:+47123">Phone</a>
<a href={{this.url}}>Dynamic</a>                   {{! dynamic — skipped, we can't know the resolved value }}
<a href="{{this.prefix}}/path">Concat with mustache</a>  {{! same — dynamic-skipped }}

Prior art

Verified each peer in source:

Plugin Rule Behavior
jsx-a11y anchor-is-valid Flags href="", href="#", href="javascript:void(0)". Tests at anchor-is-valid-test.js:286-289.
lit-a11y anchor-is-valid Similar checks, but allowHash defaults to true — does NOT flag href="#" by default.
vuejs-accessibility No equivalent rule.
@angular-eslint/template No equivalent rule.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 21, 2026

🏎️ Benchmark Comparison

Benchmark Control (p50) Experiment (p50) Δ
🟢 js small 13.50 ms 12.73 ms -5.7%
🟢 js medium 6.43 ms 6.26 ms -2.6%
js large 2.47 ms 2.43 ms -1.8%
gjs small 1.16 ms 1.16 ms -0.0%
gjs medium 584.39 µs 582.20 µs -0.4%
gjs large 228.78 µs 229.36 µs +0.3%
gts small 1.17 ms 1.16 ms -0.7%
gts medium 585.02 µs 580.54 µs -0.8%
gts large 229.33 µs 228.75 µs -0.3%

🟢 faster · 🔴 slower · 🟠 slightly slower · ⚪ within 2%

Full mitata output
clk: ~3.36 GHz
cpu: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz
runtime: node 24.14.1 (x64-linux)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
js small (control)            15.19 ms/iter  16.75 ms ▅▅█                  
                      (10.77 ms … 29.62 ms)  25.66 ms ███ ▆▃               
                    (  5.69 mb …  10.38 mb)   7.21 mb ███▄██▄█▄▄▄▁▁█▄▁▁▁█▄▄

js small (experiment)         13.56 ms/iter  14.42 ms ▅ █                  
                      (11.48 ms … 21.39 ms)  17.43 ms █▆█▃▆ ▃         ▃   ▃
                    (  6.56 mb …   7.85 mb)   6.87 mb ███████▄▄█▄█▁▄▄▄█▁▁▁█

                             ┌                                            ┐
                             ╷ ┌──────────┬────┐                          ╷
          js small (control) ├─┤          │    ├──────────────────────────┤
                             ╵ └──────────┴────┘                          ╵
                               ╷ ┌───┬──┐        ╷
       js small (experiment)   ├─┤   │  ├────────┤
                               ╵ └───┴──┘        ╵
                             └                                            ┘
                             10.77 ms           18.21 ms           25.66 ms

summary
  js small (experiment)
   1.12x faster than js small (control)

------------------------------------------- -------------------------------
js medium (control)            7.17 ms/iter   7.78 ms ▂█                   
                       (5.94 ms … 13.33 ms)  12.58 ms ██▂                  
                    (  2.35 mb …   4.75 mb)   3.54 mb ███▅▄▄▇▄▂▃▅▁▁▃▁▁▂▁▂▂▂

js medium (experiment)         6.89 ms/iter   6.89 ms  █                   
                       (5.96 ms … 12.23 ms)  12.15 ms ▆█                   
                    (  2.91 mb …   4.07 mb)   3.53 mb ██▃▅▂▂▃▃▂▂▁▁▂▁▁▂▁▂▁▂▂

                             ┌                                            ┐
                             ╷┌──────┬───┐                                ╷
         js medium (control) ├┤      │   ├────────────────────────────────┤
                             ╵└──────┴───┘                                ╵
                             ╷┌────┬                                   ╷
      js medium (experiment) ├┤    │───────────────────────────────────┤
                             ╵└────┴                                   ╵
                             └                                            ┘
                             5.94 ms            9.26 ms            12.58 ms

summary
  js medium (experiment)
   1.04x faster than js medium (control)

------------------------------------------- -------------------------------
js large (control)             2.87 ms/iter   2.70 ms █▂                   
                        (2.20 ms … 8.96 ms)   6.42 ms ██▄                  
                    (101.82 kb …   3.09 mb)   1.43 mb ███▃▂▃▂▃▂▂▁▂▂▁▂▂▂▂▂▁▁

js large (experiment)          2.69 ms/iter   2.56 ms ▅█                   
                        (2.28 ms … 7.22 ms)   5.76 ms ██                   
                    (318.83 kb …   2.55 mb)   1.42 mb ███▃▂▂▂▂▂▂▂▂▁▁▁▂▂▂▁▁▁

                             ┌                                            ┐
                             ╷┌─────┬                                     ╷
          js large (control) ├┤     │─────────────────────────────────────┤
                             ╵└─────┴                                     ╵
                              ╷┌──┬                                ╷
       js large (experiment)  ├┤  │────────────────────────────────┤
                              ╵└──┴                                ╵
                             └                                            ┘
                             2.20 ms            4.31 ms             6.42 ms

summary
  js large (experiment)
   1.07x faster than js large (control)

------------------------------------------- -------------------------------
gjs small (control)            1.28 ms/iter   1.22 ms █                    
                        (1.14 ms … 5.64 ms)   5.03 ms █                    
                    (356.62 kb …   1.78 mb)   1.06 mb █▅▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs small (experiment)         1.30 ms/iter   1.22 ms █                    
                        (1.13 ms … 6.05 ms)   5.34 ms █                    
                    (508.50 kb …   1.63 mb)   1.06 mb █▄▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌─┬                                       ╷
         gjs small (control) │ │───────────────────────────────────────┤
                             └─┴                                       ╵
                             ┌─┬                                          ╷
      gjs small (experiment) │ │──────────────────────────────────────────┤
                             └─┴                                          ╵
                             └                                            ┘
                             1.13 ms            3.24 ms             5.34 ms

summary
  gjs small (control)
   1.01x faster than gjs small (experiment)

------------------------------------------- -------------------------------
gjs medium (control)         637.79 µs/iter 596.15 µs █                    
                      (559.54 µs … 5.51 ms)   2.88 ms █                    
                    ( 84.66 kb …   1.41 mb) 541.68 kb █▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs medium (experiment)      623.27 µs/iter 592.05 µs  █                   
                      (557.62 µs … 5.17 ms)   1.34 ms ▅█                   
                    (124.77 kb …   1.20 mb) 540.69 kb ██▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌─┬                                          ╷
        gjs medium (control) │ │──────────────────────────────────────────┤
                             └─┴                                          ╵
                             ┌┬             ╷
     gjs medium (experiment) ││─────────────┤
                             └┴             ╵
                             └                                            ┘
                             557.62 µs           1.72 ms            2.88 ms

summary
  gjs medium (experiment)
   1.02x faster than gjs medium (control)

------------------------------------------- -------------------------------
gjs large (control)          250.95 µs/iter 239.91 µs  █                   
                      (221.73 µs … 5.07 ms) 288.32 µs  ██                  
                    (106.34 kb …   1.27 mb) 217.18 kb ▅███▄▄▅▆▄▃▂▂▁▁▁▁▁▁▁▁▁

gjs large (experiment)       258.30 µs/iter 242.79 µs █▂                   
                      (221.51 µs … 5.09 ms) 484.80 µs ██▄                  
                    ( 42.67 kb … 696.01 kb) 216.71 kb ███▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷┌───┬     ╷
         gjs large (control) ├┤   │─────┤
                             ╵└───┴     ╵
                             ╷┌────┬                                      ╷
      gjs large (experiment) ├┤    │──────────────────────────────────────┤
                             ╵└────┴                                      ╵
                             └                                            ┘
                             221.51 µs         353.15 µs          484.80 µs

summary
  gjs large (control)
   1.03x faster than gjs large (experiment)

------------------------------------------- -------------------------------
gts small (control)            1.31 ms/iter   1.20 ms █                    
                        (1.14 ms … 6.18 ms)   5.39 ms █                    
                    (507.34 kb …   1.64 mb)   1.06 mb █▃▄▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts small (experiment)         1.25 ms/iter   1.18 ms █                    
                        (1.14 ms … 5.74 ms)   4.83 ms █                    
                    (614.77 kb …   1.57 mb)   1.05 mb █▄▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌─┬                                          ╷
         gts small (control) │ │──────────────────────────────────────────┤
                             └─┴                                          ╵
                             ┌┬                                     ╷
      gts small (experiment) ││─────────────────────────────────────┤
                             └┴                                     ╵
                             └                                            ┘
                             1.14 ms            3.27 ms             5.39 ms

summary
  gts small (experiment)
   1.05x faster than gts small (control)

------------------------------------------- -------------------------------
gts medium (control)         627.66 µs/iter 593.90 µs  █                   
                      (562.04 µs … 5.15 ms)   1.18 ms ▄█                   
                    (368.28 kb …   1.32 mb) 542.13 kb ██▄▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts medium (experiment)      623.02 µs/iter 590.73 µs  █                   
                      (556.47 µs … 5.06 ms)   1.21 ms ▃█                   
                    (176.23 kb …   1.59 mb) 540.91 kb ██▃▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷┌───┬                                     ╷
        gts medium (control) ├┤   │─────────────────────────────────────┤
                             ╵└───┴                                     ╵
                             ╷┌───┬                                       ╷
     gts medium (experiment) ├┤   │───────────────────────────────────────┤
                             ╵└───┴                                       ╵
                             └                                            ┘
                             556.47 µs          882.15 µs           1.21 ms

summary
  gts medium (experiment)
   1.01x faster than gts medium (control)

------------------------------------------- -------------------------------
gts large (control)          251.12 µs/iter 240.12 µs  █                   
                      (222.12 µs … 5.33 ms) 282.18 µs  ██▄                 
                    (170.63 kb … 712.84 kb) 216.78 kb ▅███▅▃▄▆▆▄▃▂▁▂▁▁▁▁▁▁▁

gts large (experiment)       251.85 µs/iter 240.72 µs  █                   
                      (221.50 µs … 5.62 ms) 320.27 µs  █▂                  
                    ( 73.59 kb …   1.24 mb) 216.75 kb ███▄▇▅▃▂▂▂▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷ ┌──────────┬              ╷
         gts large (control) ├─┤          │──────────────┤
                             ╵ └──────────┴              ╵
                             ╷ ┌───────────┬                              ╷
      gts large (experiment) ├─┤           │──────────────────────────────┤
                             ╵ └───────────┴                              ╵
                             └                                            ┘
                             221.50 µs         270.88 µs          320.27 µs

summary
  gts large (control)
   1x faster than gts large (experiment)

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new template accessibility rule to disallow placeholder / non-navigational href values on <a> elements, complementing the existing template-link-href-attributes (presence-only) rule.

Changes:

  • Implement template-no-invalid-link-href to flag empty/whitespace href, bare # / #!, and javascript: URLs (static-only; skips dynamic values).
  • Add RuleTester coverage for both GJS/GTS (<template>) and HBS parsing modes.
  • Add rule documentation and list the rule in the README rule table; add a peer-parity audit fixture.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
lib/rules/template-no-invalid-link-href.js New rule implementation for detecting placeholder/unsafe static href values on <a>.
tests/lib/rules/template-no-invalid-link-href.js Unit tests covering valid/invalid static href cases plus dynamic-skip behavior.
tests/audit/anchor-is-valid-href-only/peer-parity.js Audit fixture translating peer-plugin cases for behavioral comparison.
docs/rules/template-no-invalid-link-href.md Rule documentation with examples and references.
README.md Adds the rule to the documented rule list/table.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/audit/anchor-is-valid-href-only/peer-parity.js Outdated
Comment thread docs/rules/template-no-invalid-link-href.md Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docs/rules/template-no-invalid-link-href.md Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/lib/rules/template-no-invalid-link-href.js
Comment thread lib/rules/template-no-invalid-link-href.js Outdated
Comment thread lib/rules/template-no-invalid-link-href.js Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/rules/template-no-invalid-link-href.js Outdated
Comment thread lib/rules/template-no-invalid-link-href.js
Comment thread docs/rules/template-no-invalid-link-href.md Outdated
Comment thread tests/lib/rules/template-no-invalid-link-href.js
@johanrd johanrd requested a review from Copilot April 24, 2026 17:48
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/rules/template-no-invalid-link-href.js
Comment thread tests/audit/anchor-is-valid-href-only/peer-parity.js Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/lib/rules/template-no-invalid-link-href.js Outdated
Comment thread docs/rules/template-no-invalid-link-href.md Outdated
johanrd added a commit that referenced this pull request Apr 27, 2026
@johanrd johanrd force-pushed the feat/template-no-invalid-link-href branch 2 times, most recently from 5ed004c to e5aa482 Compare April 27, 2026 14:59
@johanrd johanrd force-pushed the feat/template-no-invalid-link-href branch from cd248b9 to d2c4dad Compare April 27, 2026 19:26
@johanrd johanrd requested a review from Copilot April 27, 2026 19:34
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1 to +8
'use strict';

const { getStaticAttrValue } = require('../../../lib/utils/static-attr-value');

describe('getStaticAttrValue', () => {
it('returns empty string for null/undefined (valueless attribute)', () => {
expect(getStaticAttrValue(null)).toBe('');
expect(getStaticAttrValue(undefined)).toBe('');
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

This test file relies on a global expect without importing it. If the repo’s test runner doesn’t provide Jest/Vitest-style globals, this will fail at runtime. Prefer using Node’s built-in assert (or the repo’s standard assertion library) so the test is self-contained and consistent with the rest of the suite.

Copilot uses AI. Check for mistakes.
Comment thread lib/utils/static-attr-value.js Outdated
return extractLiteral(value.path);
}
if (value.type === 'GlimmerConcatStatement') {
const parts = value.parts || [];
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

Defaulting value.parts to an empty array means a GlimmerConcatStatement with missing/invalid parts will resolve to the empty string ('') instead of undefined. That can cause callers to treat an otherwise-unresolvable value as statically known and (for href validation) potentially flag it as an empty href. Consider returning undefined when parts is not an array (e.g. !Array.isArray(value.parts)), and only joining when parts is a valid array.

Suggested change
const parts = value.parts || [];
if (!Array.isArray(value.parts)) {
return undefined;
}
const parts = value.parts;

Copilot uses AI. Check for mistakes.

<!-- end auto-generated rule header -->

Disallow link elements — `<a>` and `<area>` — whose `href` value is a commonly-misused placeholder (e.g. `href="#"`, `href=""`, `href="javascript:..."`). Both carry URL semantics per HTML §4.5.1 / §4.8.14, so the same validity rules apply on each.
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

The doc references specific HTML section numbers ("HTML §4.5.1 / §4.8.14"), which don’t align well with the Living Standard’s current structure and can become stale/misleading. Prefer linking directly to the relevant Living Standard anchors for both elements (e.g. the <a> element and the <area> element sections) and drop the numeric section references.

Copilot uses AI. Check for mistakes.
Comment thread README.md Outdated
| [template-no-heading-inside-button](docs/rules/template-no-heading-inside-button.md) | disallow heading elements inside button elements | 📋 | | |
| [template-no-invalid-aria-attributes](docs/rules/template-no-invalid-aria-attributes.md) | disallow invalid aria-* attributes | 📋 | | |
| [template-no-invalid-interactive](docs/rules/template-no-invalid-interactive.md) | disallow non-interactive elements with interactive handlers | 📋 | | |
| [template-no-invalid-link-href](docs/rules/template-no-invalid-link-href.md) | disallow invalid href values on anchor elements | | | |
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

The README description says "anchor elements", but the rule (and its docs/tests) also applies to <area>. Update the wording to reflect both, e.g. "disallow invalid href values on <a>/<area> elements" (or "link elements").

Suggested change
| [template-no-invalid-link-href](docs/rules/template-no-invalid-link-href.md) | disallow invalid href values on anchor elements | | | |
| [template-no-invalid-link-href](docs/rules/template-no-invalid-link-href.md) | disallow invalid href values on `<a>`/`<area>` elements | | | |

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants