Skip to content

refactor: extract landmark-roles util (preserving deliberate per-rule region exclusion)#38

Closed
johanrd wants to merge 1 commit into
masterfrom
fix/landmark-roles-util
Closed

refactor: extract landmark-roles util (preserving deliberate per-rule region exclusion)#38
johanrd wants to merge 1 commit into
masterfrom
fix/landmark-roles-util

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

Three a11y rules share a concept of "landmark ARIA role." Today they each maintain their own hand-list. The counts look like drift:

Rule Count Missing
template-no-redundant-role 7 region
template-no-nested-landmark 7 region
template-no-duplicate-landmark-elements 8

But the two 7-role lists aren't drift — the region exclusion is deliberate, documented, and was landed last week in #2694 after @NullVoxPopuli's "what's MDN say?" review. The reasoning:

<section> only gets the region landmark role when it has an accessible name (aria-label / aria-labelledby / title). Without one, <section> has role generic. A static linter cannot verify at lint time whether aria-label resolves to a non-empty name at runtime.

Source: WAI-ARIA APG HTML5 landmark examples.

The third rule (template-no-duplicate-landmark-elements) DOES include region because that rule inspects aria-label / aria-labelledby before classifying a node as a landmark — it can safely handle the full 8-role set.

So the three rules have different landmark sets for different, justifiable reasons. Not drift.

What this PR does

Adds lib/utils/landmark-roles.js that makes the distinction explicit by exporting two sets:

  • ALL_LANDMARK_ROLES — the 8 WAI-ARIA 1.2 §5.3.4 landmark roles, derived from aria-query (non-abstract roles whose superClass chain includes 'landmark', minus DPub-ARIA doc-*).
  • LANDMARK_ROLES — the 7-role subset excluding region. The safe default for static-linting rules that can't verify accessible names.

Both sets are derived from aria-query's role metadata so future WAI-ARIA additions flow through on aria-query upgrade.

Rule migrations:

Also removes the region: ['section'] entry from template-no-redundant-role's ROLE_TO_ELEMENTS mapping, since ember-cli#2694's reasoning applies to that rule too: <section role=\"region\"> shouldn't be flagged as redundant when we can't verify the section has an accessible name.

Behavior vs. master

Earlier (rejected) direction

An earlier revision of this PR treated the region exclusion as "drift" and re-added region to both rules via a single shared set. That would have silently reverted ember-cli#2694's fix last week. This revision is the correction — preserving the intent, codifying the reasoning in the util.

Spec references

Prior art — how peers handle the landmark set

All four peer plugins draw landmark role definitions from aria-query (the same source as this util). None expose a shared landmark-roles helper; each rule that needs landmarks includes the roles inline, typically the full 8-role spec list.

Plugin Rule Landmark-role source
jsx-a11y no direct equivalent rule N/A — no no-nested-landmark or no-duplicate-landmark-elements rule
vuejs-accessibility no direct equivalent rule N/A
lit-a11y no direct equivalent rule N/A
@angular-eslint/template no direct equivalent rule N/A

So the nested-landmark / duplicate-landmark / redundant-role checks are Ember-template-lint conventions; this util consolidates the landmark set across them. The region exclusion for static-linting is this plugin's specific convention (grounded in the WAI-ARIA APG's HTML5 landmark example via ember-cli#2694) — it is not a peer-plugin convention because peers don't ship these rules.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 21, 2026

🏎️ Benchmark Comparison

Benchmark Control (p50) Experiment (p50) Δ
🟢 js small 15.62 ms 14.83 ms -5.0%
🟢 js medium 7.62 ms 7.14 ms -6.4%
🟢 js large 2.85 ms 2.67 ms -6.4%
gjs small 1.24 ms 1.23 ms -1.4%
gjs medium 622.27 µs 634.12 µs +1.9%
gjs large 247.15 µs 248.23 µs +0.4%
gts small 1.24 ms 1.23 ms -0.2%
gts medium 624.68 µs 617.85 µs -1.1%
gts large 246.67 µs 247.27 µs +0.2%

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

Full mitata output
clk: ~3.06 GHz
cpu: AMD EPYC 7763 64-Core Processor
runtime: node 24.15.0 (x64-linux)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
js small (control)            17.60 ms/iter  18.71 ms █                    
                      (12.58 ms … 33.00 ms)  30.38 ms █▅▇   ▂              
                    (  5.87 mb …  10.53 mb)   7.29 mb ███▄▇▇█▄▄▁▄▄▁▁▄▁▇▁▁▁▇

js small (experiment)         15.61 ms/iter  16.12 ms  █ ▅                 
                      (13.50 ms … 22.79 ms)  22.41 ms ██▆█▃▃ ▆             
                    (  6.19 mb …   8.31 mb)   6.84 mb ████████▁▄▁▁▁▁▁▁▄▁▁▄▄

                             ┌                                            ┐
                             ╷┌───────────┬─┐                             ╷
          js small (control) ├┤           │ ├─────────────────────────────┤
                             ╵└───────────┴─┘                             ╵
                               ╷ ┌───┬┐               ╷
       js small (experiment)   ├─┤   │├───────────────┤
                               ╵ └───┴┘               ╵
                             └                                            ┘
                             12.58 ms           21.48 ms           30.38 ms

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

------------------------------------------- -------------------------------
js medium (control)            8.43 ms/iter   8.82 ms ▄█                   
                       (7.04 ms … 14.10 ms)  12.89 ms ██▅                  
                    (  2.60 mb …   4.44 mb)   3.53 mb ████▅▆█▆▂▁▄▄▁▁▂▂▂▂▂▁▅

js medium (experiment)         7.99 ms/iter   8.08 ms █▃                   
                       (6.66 ms … 15.72 ms)  15.45 ms ██▄                  
                    (  3.28 mb …   3.95 mb)   3.53 mb ███▅▄▃▄▂▂▂▁▅▂▁▁▁▁▁▁▁▃

                             ┌                                            ┐
                               ╷┌─────┬─┐                    ╷
         js medium (control)   ├┤     │ ├────────────────────┤
                               ╵└─────┴─┘                    ╵
                             ╷┌─────┬                                     ╷
      js medium (experiment) ├┤     │─────────────────────────────────────┤
                             ╵└─────┴                                     ╵
                             └                                            ┘
                             6.66 ms           11.05 ms            15.45 ms

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

------------------------------------------- -------------------------------
js large (control)             3.21 ms/iter   3.01 ms  █                   
                        (2.66 ms … 7.35 ms)   6.71 ms ▄█                   
                    (388.84 kb …   2.60 mb)   1.44 mb ██▄▃▃▁▂▁▂▂▁▁▁▂▁▁▁▁▁▁▁

js large (experiment)          3.05 ms/iter   2.85 ms  █                   
                        (2.45 ms … 8.82 ms)   7.35 ms ▄█                   
                    (135.53 kb …   3.04 mb)   1.43 mb ██▅▃▁▂▂▂▁▁▂▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                               ╷┌───┬                               ╷
          js large (control)   ├┤   │───────────────────────────────┤
                               ╵└───┴                               ╵
                             ╷┌───┬                                       ╷
       js large (experiment) ├┤   │───────────────────────────────────────┤
                             ╵└───┴                                       ╵
                             └                                            ┘
                             2.45 ms            4.90 ms             7.35 ms

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

------------------------------------------- -------------------------------
gjs small (control)            1.39 ms/iter   1.31 ms █                    
                        (1.19 ms … 7.17 ms)   6.18 ms █                    
                    (281.15 kb …   1.86 mb)   1.06 mb █▇▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs small (experiment)         1.35 ms/iter   1.25 ms █                    
                        (1.18 ms … 6.32 ms)   4.84 ms █                    
                    (508.58 kb …   1.63 mb)   1.06 mb █▅▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌─┬                                          ╷
         gjs small (control) │ │──────────────────────────────────────────┤
                             └─┴                                          ╵
                             ┌─┬                              ╷
      gjs small (experiment) │ │──────────────────────────────┤
                             └─┴                              ╵
                             └                                            ┘
                             1.18 ms            3.68 ms             6.18 ms

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

------------------------------------------- -------------------------------
gjs medium (control)         670.35 µs/iter 637.07 µs  █                   
                      (585.48 µs … 5.85 ms)   1.44 ms  █                   
                    (  3.01 kb …   1.06 mb) 540.29 kb ██▄▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs medium (experiment)      730.12 µs/iter 670.62 µs  █                   
                      (586.20 µs … 7.15 ms)   1.30 ms ▃█▅                  
                    (245.52 kb … 985.26 kb) 541.27 kb ███▄▃▂▂▂▂▂▁▁▁▂▂▂▂▁▁▁▁

                             ┌                                            ┐
                             ╷┌──┬                                        ╷
        gjs medium (control) ├┤  │────────────────────────────────────────┤
                             ╵└──┴                                        ╵
                             ╷ ┌─────┬                             ╷
     gjs medium (experiment) ├─┤     │─────────────────────────────┤
                             ╵ └─────┴                             ╵
                             └                                            ┘
                             585.48 µs           1.01 ms            1.44 ms

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

------------------------------------------- -------------------------------
gjs large (control)          275.63 µs/iter 263.35 µs  █                   
                      (234.37 µs … 5.60 ms) 414.63 µs ▄█▂                  
                    (125.91 kb … 802.19 kb) 217.25 kb █████▄▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs large (experiment)       282.48 µs/iter 265.63 µs █▂                   
                      (236.36 µs … 6.30 ms) 672.81 µs ██                   
                    (184.20 kb … 965.80 kb) 216.95 kb ███▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷┌──┬              ╷
         gjs large (control) ├┤  │──────────────┤
                             ╵└──┴              ╵
                             ╷┌───┬                                       ╷
      gjs large (experiment) ├┤   │───────────────────────────────────────┤
                             ╵└───┴                                       ╵
                             └                                            ┘
                             234.37 µs         453.59 µs          672.81 µs

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

------------------------------------------- -------------------------------
gts small (control)            1.34 ms/iter   1.27 ms █                    
                        (1.20 ms … 7.17 ms)   4.38 ms █                    
                    (227.80 kb …   1.63 mb)   1.06 mb █▅▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts small (experiment)         1.34 ms/iter   1.26 ms █                    
                        (1.20 ms … 6.97 ms)   6.12 ms █                    
                    (212.16 kb …   1.88 mb)   1.05 mb █▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌┬                           ╷
         gts small (control) ││───────────────────────────┤
                             └┴                           ╵
                             ┌┬                                           ╷
      gts small (experiment) ││───────────────────────────────────────────┤
                             └┴                                           ╵
                             └                                            ┘
                             1.20 ms            3.66 ms             6.12 ms

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

------------------------------------------- -------------------------------
gts medium (control)         672.02 µs/iter 644.40 µs  █                   
                      (586.68 µs … 5.96 ms)   1.15 ms ▂██                  
                    (135.08 kb …   1.68 mb) 542.45 kb ███▅▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts medium (experiment)      669.20 µs/iter 635.08 µs  █                   
                      (585.60 µs … 5.93 ms)   1.64 ms ▅█                   
                    ( 74.48 kb …   1.16 mb) 540.22 kb ██▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷┌──┬                   ╷
        gts medium (control) ├┤  │───────────────────┤
                             ╵└──┴                   ╵
                             ╷┌──┬                                        ╷
     gts medium (experiment) ├┤  │────────────────────────────────────────┤
                             ╵└──┴                                        ╵
                             └                                            ┘
                             585.60 µs           1.11 ms            1.64 ms

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

------------------------------------------- -------------------------------
gts large (control)          277.18 µs/iter 263.77 µs ▄█                   
                      (235.49 µs … 5.57 ms) 491.04 µs ██▂                  
                    (216.09 kb … 966.18 kb) 217.17 kb ████▃▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts large (experiment)       275.86 µs/iter 264.19 µs  █▄                  
                      (236.07 µs … 5.90 ms) 357.08 µs  ██                  
                    (133.60 kb … 769.77 kb) 216.52 kb ▇██▅▄██▃▂▂▂▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷┌─────┬                                     ╷
         gts large (control) ├┤     │─────────────────────────────────────┤
                             ╵└─────┴                                     ╵
                             ╷┌─────┬             ╷
      gts large (experiment) ├┤     │─────────────┤
                             ╵└─────┴             ╵
                             └                                            ┘
                             235.49 µs         363.27 µs          491.04 µs

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

@johanrd johanrd force-pushed the fix/landmark-roles-util branch from a22e465 to 3084cc8 Compare April 21, 2026 19:33
@johanrd johanrd changed the title refactor: extract landmark-roles util, fixing region drift refactor: extract landmark-roles util (preserving deliberate per-rule region exclusion) Apr 21, 2026
@johanrd johanrd requested a review from Copilot April 22, 2026 10:28
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

Refactors landmark ARIA role handling across multiple template a11y rules by extracting a shared landmark-roles utility that preserves the intentional region exclusion for rules that can’t statically verify accessible names.

Changes:

  • Add lib/utils/landmark-roles.js exporting ALL_LANDMARK_ROLES (full landmark set from aria-query, excluding doc-*) and LANDMARK_ROLES (subset excluding region).
  • Update template-no-{nested,duplicate}-landmark* and template-no-redundant-role to consume the shared role sets (keeping per-rule intent).
  • Adjust tests/comments to reflect the intended region handling and add coverage for the new util.

Reviewed changes

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

Show a summary per file
File Description
tests/lib/utils/landmark-roles-test.js Adds tests for the new landmark role sets.
tests/lib/rules/template-no-redundant-role.js Updates valid cases to reflect region handling.
tests/lib/rules/template-no-nested-landmark.js Updates spec link/comment around region exclusion rationale.
lib/utils/landmark-roles.js Introduces shared landmark role set derivation from aria-query.
lib/rules/template-no-redundant-role.js Switches to shared LANDMARK_ROLES and removes regionsection mapping.
lib/rules/template-no-nested-landmark.js Switches to shared LANDMARK_ROLES.
lib/rules/template-no-duplicate-landmark-elements.js Switches to shared full landmark set (including region) with clarifying comment.

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

Comment thread tests/lib/utils/landmark-roles-test.js Outdated
Comment thread lib/utils/landmark-roles.js Outdated
Comment thread tests/lib/rules/template-no-redundant-role.js
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 no new comments.


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

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-duplicate-landmark-elements.js
Comment thread lib/rules/template-no-duplicate-landmark-elements.js
johanrd added a commit that referenced this pull request Apr 24, 2026
@johanrd johanrd requested a review from Copilot April 24, 2026 13:38
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 8 out of 8 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/utils/landmark-roles.js
Comment thread lib/rules/template-no-duplicate-landmark-elements.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.

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.

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

@johanrd johanrd requested a review from Copilot April 27, 2026 11:31
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 1 comment.


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

Comment thread lib/utils/landmark-roles.js Outdated
@johanrd johanrd force-pushed the fix/landmark-roles-util branch from 35094e6 to c893ddb Compare April 27, 2026 14:07
@johanrd johanrd force-pushed the fix/landmark-roles-util branch from c893ddb to e888a65 Compare April 27, 2026 19:29
@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 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 on lines +11 to +27
const ALL_LANDMARK_ROLES = buildLandmarkRoleSet();

// The subset that's safe for static-linting rules to treat as landmarks
// without further verification. `region` is EXCLUDED because a static linter
// cannot determine at lint time whether the element has an accessible name
// (via aria-label / aria-labelledby / title), which is required for the
// `region` role to actually apply to a `<section>` per HTML-AAM.
//
// See PR #2694 for the full rationale and spec citation
// (https://www.w3.org/TR/html-aam-1.0/#el-section):
// `<section>` without an accessible name has role `generic`, not `region`.
//
// Most a11y rules that enumerate landmarks should use this subset.
// Rules that DO verify accessible names (e.g. template-no-duplicate-
// landmark-elements, which inspects aria-label / aria-labelledby before
// classifying a node as a landmark) should import ALL_LANDMARK_ROLES.
const LANDMARK_ROLES = new Set([...ALL_LANDMARK_ROLES].filter((role) => role !== 'region'));
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.

Both exports are mutable Set instances shared by all importers; any accidental .add()/.delete() in one consumer would affect all rules/tests in the same process. To make this safer, consider exporting accessor functions that return a fresh Set copy (or exporting arrays and letting each rule construct its own Set), or at minimum documenting that these sets must be treated as immutable.

Copilot uses AI. Check for mistakes.
return result;
}

module.exports = { LANDMARK_ROLES, ALL_LANDMARK_ROLES };
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.

Both exports are mutable Set instances shared by all importers; any accidental .add()/.delete() in one consumer would affect all rules/tests in the same process. To make this safer, consider exporting accessor functions that return a fresh Set copy (or exporting arrays and letting each rule construct its own Set), or at minimum documenting that these sets must be treated as immutable.

Suggested change
module.exports = { LANDMARK_ROLES, ALL_LANDMARK_ROLES };
module.exports = {
get LANDMARK_ROLES() {
return new Set(LANDMARK_ROLES);
},
get ALL_LANDMARK_ROLES() {
return new Set(ALL_LANDMARK_ROLES);
},
};

Copilot uses AI. Check for mistakes.
@johanrd johanrd closed this Apr 28, 2026
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