@surea11y/core 1.1.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -4
- package/LICENSE +373 -21
- package/README.md +70 -1
- package/bin/core.js +240 -11
- package/docs/API_STABILITY.md +61 -0
- package/docs/BASELINE.md +66 -0
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/CLI.md +80 -1
- package/docs/ENGINE_OPTIONS.md +4 -0
- package/docs/INTEGRATION.md +19 -1
- package/docs/OUTPUT_SCHEMA.md +2 -2
- package/docs/REPORT.md +33 -0
- package/docs/RULE_AUTHORING.md +31 -0
- package/docs/RULE_CATALOG.md +1 -1
- package/docs/SARIF.md +59 -0
- package/package.json +15 -4
- package/src/baseline.js +0 -0
- package/src/catalogs/composites.wcag.js +414 -450
- package/src/checks/automatic/area-alt-present.js +59 -25
- package/src/checks/automatic/aria-allowed-attr.js +193 -33
- package/src/checks/automatic/aria-allowed-role.js +21 -7
- package/src/checks/automatic/aria-braille-equivalent.js +32 -10
- package/src/checks/automatic/aria-conditional-attr.js +24 -7
- package/src/checks/automatic/aria-deprecated-role.js +22 -8
- package/src/checks/automatic/aria-hidden-body.js +50 -19
- package/src/checks/automatic/aria-hidden-focus.js +408 -53
- package/src/checks/automatic/aria-prohibited-attr.js +296 -22
- package/src/checks/automatic/aria-prohibited-children.js +125 -29
- package/src/checks/automatic/aria-required-attr.js +23 -8
- package/src/checks/automatic/aria-required-children.js +37 -14
- package/src/checks/automatic/aria-required-parent.js +48 -14
- package/src/checks/automatic/aria-role-name-present.js +47 -21
- package/src/checks/automatic/aria-roles-valid.js +22 -12
- package/src/checks/automatic/aria-valid-attr-value.js +28 -7
- package/src/checks/automatic/aria-valid-attr.js +17 -5
- package/src/checks/automatic/autocomplete-valid.js +74 -16
- package/src/checks/automatic/avoid-inline-spacing.js +20 -6
- package/src/checks/automatic/binary-control-name-present.js +60 -50
- package/src/checks/automatic/button-name-present.js +48 -18
- package/src/checks/automatic/bypass-blocks-present.js +50 -25
- package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
- package/src/checks/automatic/combobox-name-present.js +38 -45
- package/src/checks/automatic/contrast-computable.js +361 -341
- package/src/checks/automatic/contrast-enhanced.js +487 -466
- package/src/checks/automatic/contrast-minimum.js +486 -465
- package/src/checks/automatic/css-orientation-lock.js +39 -9
- package/src/checks/automatic/definition-list-children-valid.js +40 -19
- package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
- package/src/checks/automatic/dialog-name-present.js +37 -75
- package/src/checks/automatic/dlitem-parent-valid.js +23 -8
- package/src/checks/automatic/duplicate-id-aria.js +24 -6
- package/src/checks/automatic/embed-text-alternative-present.js +86 -35
- package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
- package/src/checks/automatic/form-control-single-label.js +47 -10
- package/src/checks/automatic/html-xml-lang-mismatch.js +43 -19
- package/src/checks/automatic/iframe-focusable-content.js +26 -11
- package/src/checks/automatic/iframe-name-present.js +31 -9
- package/src/checks/automatic/iframe-title-unique.js +29 -8
- package/src/checks/automatic/img-alt-present.js +47 -43
- package/src/checks/automatic/input-image-alt-present.js +143 -112
- package/src/checks/automatic/label-in-name.js +50 -22
- package/src/checks/automatic/language-page-present.js +117 -109
- package/src/checks/automatic/link-in-text-block.js +59 -19
- package/src/checks/automatic/link-name-present.js +45 -14
- package/src/checks/automatic/list-children-valid.js +26 -9
- package/src/checks/automatic/listbox-name-present.js +39 -19
- package/src/checks/automatic/listitem-parent-valid.js +18 -6
- package/src/checks/automatic/menuitem-name-present.js +39 -61
- package/src/checks/automatic/meta-refresh-no-exceptions.js +37 -8
- package/src/checks/automatic/meta-refresh-timing-absent.js +28 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +32 -7
- package/src/checks/automatic/meter-name-present.js +36 -33
- package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
- package/src/checks/automatic/object-text-alternative-present.js +91 -39
- package/src/checks/automatic/option-name-present.js +38 -21
- package/src/checks/automatic/page-title-present.js +26 -7
- package/src/checks/automatic/progressbar-name-present.js +41 -34
- package/src/checks/automatic/role-img-alt-present.js +209 -157
- package/src/checks/automatic/searchbox-name-present.js +39 -19
- package/src/checks/automatic/server-side-image-map-absent.js +23 -8
- package/src/checks/automatic/slider-name-present.js +40 -47
- package/src/checks/automatic/spinbutton-name-present.js +39 -19
- package/src/checks/automatic/summary-name-present.js +37 -17
- package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
- package/src/checks/automatic/svg-text-alternative-present.js +246 -226
- package/src/checks/automatic/tab-name-present.js +37 -60
- package/src/checks/automatic/table-headers-attr-valid.js +24 -8
- package/src/checks/automatic/table-th-has-data-cells.js +22 -8
- package/src/checks/automatic/target-size-minimum.js +118 -48
- package/src/checks/automatic/td-has-header.js +29 -11
- package/src/checks/automatic/textbox-name-present.js +39 -19
- package/src/checks/automatic/tooltip-name-present.js +37 -18
- package/src/checks/automatic/treeitem-name-present.js +38 -21
- package/src/checks/automatic/valid-lang.js +20 -6
- package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
- package/src/checks/manual/accesskeys-manual.js +14 -5
- package/src/checks/manual/area-alt-decorative-manual.js +192 -193
- package/src/checks/manual/area-alt-quality-manual.js +182 -141
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
- package/src/checks/manual/aria-text-manual.js +14 -6
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
- package/src/checks/manual/css-hidden-focus.js +196 -165
- package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
- package/src/checks/manual/empty-heading-manual.js +24 -12
- package/src/checks/manual/empty-table-header-manual.js +21 -14
- package/src/checks/manual/focus-order-semantics-manual.js +45 -10
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
- package/src/checks/manual/heading-order-manual.js +22 -12
- package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
- package/src/checks/manual/image-redundant-alt-manual.js +17 -7
- package/src/checks/manual/img-alt-decorative-manual.js +131 -96
- package/src/checks/manual/img-alt-quality-manual.js +176 -127
- package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
- package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
- package/src/checks/manual/label-title-only-manual.js +14 -5
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -29
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +82 -29
- package/src/checks/manual/landmark-main-is-top-level-manual.js +64 -23
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +54 -23
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +54 -23
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
- package/src/checks/manual/landmark-one-main-manual.js +35 -21
- package/src/checks/manual/landmark-unique-manual.js +73 -45
- package/src/checks/manual/link-name-quality-manual.js +43 -12
- package/src/checks/manual/media-transcript-present-manual.js +35 -22
- package/src/checks/manual/meta-viewport-large-manual.js +25 -6
- package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
- package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
- package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
- package/src/checks/manual/p-as-heading-manual.js +22 -7
- package/src/checks/manual/page-has-heading-one-manual.js +40 -23
- package/src/checks/manual/page-title-patterns-manual.js +87 -51
- package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
- package/src/checks/manual/region-manual.js +275 -62
- package/src/checks/manual/scope-attr-valid-manual.js +11 -9
- package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
- package/src/checks/manual/skip-link-manual.js +35 -12
- package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
- package/src/checks/manual/tabindex-manual.js +11 -9
- package/src/checks/manual/table-duplicate-name-manual.js +17 -7
- package/src/checks/manual/table-fake-caption-manual.js +24 -7
- package/src/checks/manual/video-caption-manual.js +15 -4
- package/src/checks/manual-review.js +56 -12
- package/src/core/aria-helpers.js +1128 -823
- package/src/core/contrast-helpers.js +1217 -1062
- package/src/core/dom-helpers.js +4176 -3886
- package/src/core/dom-runner.js +720 -596
- package/src/core/frame-messaging.js +189 -138
- package/src/core/frame-scan.js +94 -82
- package/src/core/rollup-composites.js +94 -102
- package/src/core/rule-meta.js +71 -35
- package/src/core.js +37939 -29121
- package/src/i18n/en.js +1194 -887
- package/src/i18n/fr.js +1136 -793
- package/src/policy/contracts.js +13 -13
- package/src/policy/resolvePolicy.js +48 -44
- package/src/report.js +502 -0
- package/src/sarif.js +175 -0
- package/surea11y.browser.js +36042 -0
|
@@ -7,16 +7,21 @@
|
|
|
7
7
|
* @standard WCAG 2.2
|
|
8
8
|
* @sc 4.1.2
|
|
9
9
|
* @applicability
|
|
10
|
-
* Applies to elements whose explicit, valid role is one of the small
|
|
11
|
-
* of WAI-ARIA 1.2 roles with a documented "Prohibited ARIA States and
|
|
10
|
+
* Applies to (a) elements whose explicit, valid role is one of the small
|
|
11
|
+
* set of WAI-ARIA 1.2 roles with a documented "Prohibited ARIA States and
|
|
12
12
|
* Properties" list (pure text-semantics / non-naming structural roles:
|
|
13
13
|
* caption, code, deletion, emphasis, generic, insertion, mark, none,
|
|
14
14
|
* paragraph, presentation, strong, subscript, suggestion, superscript,
|
|
15
|
-
* time), and
|
|
15
|
+
* time), and (b) a small, curated set of native HTML tags verified to
|
|
16
|
+
* carry no explicit or implicit ARIA role at all (see ROLELESS_NATIVE_TAGS
|
|
17
|
+
* below) — in both cases, only elements that also carry aria-label or
|
|
18
|
+
* aria-labelledby.
|
|
16
19
|
* @expectation
|
|
17
|
-
* Prohibited attributes must not be present
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
+
* Prohibited attributes must not be present on (a); for (b), the naming
|
|
21
|
+
* attribute is at best unreliable (nothing accessible-name-aware to hang
|
|
22
|
+
* it off) and at worst silently ignored by assistive technology — see the
|
|
23
|
+
* roleless-branch implementation note below for the confidence split
|
|
24
|
+
* this produces.
|
|
20
25
|
* @implementation-notes
|
|
21
26
|
* - Deliberately scoped to the single, well-established prohibition class
|
|
22
27
|
* (naming attributes on pure text-semantics roles) rather than
|
|
@@ -58,13 +63,66 @@
|
|
|
58
63
|
* - Not rule-gated on isAccTreeEligible: this remains a static-markup
|
|
59
64
|
* property, while engine-level hidden-subtree filtering still applies
|
|
60
65
|
* unless engineOptions.includeHiddenElements is true.
|
|
66
|
+
* - Widened 2026-07-31 to add a second, independent branch covering
|
|
67
|
+
* naming attributes on ROLELESS elements (no explicit role="", no
|
|
68
|
+
* implicit/native role either) — found on the emoji-mart demo page
|
|
69
|
+
* (missive.github.io/emoji-mart): hundreds of
|
|
70
|
+
* `<span aria-label="party_parrot" class="emoji-mart-emoji...">` tiles,
|
|
71
|
+
* plain roleless spans with no other accessible-name source, which this
|
|
72
|
+
* rule previously ignored entirely, since its own Tier-1 branch only ever
|
|
73
|
+
* looked at the EXPLICIT role="" attribute, never at "no role at all."
|
|
74
|
+
* Empirically determined (not guessed) which native tags genuinely carry
|
|
75
|
+
* no role at all, by resolving each candidate tag's role against a live
|
|
76
|
+
* Chromium page — several surprises: common text-level tags like `<p>`,
|
|
77
|
+
* `<strong>`, `<em>`, `<code>`, `<mark>`, `<time>` have no implicit role
|
|
78
|
+
* at all (their prohibited-attrs entries only ever matter for an
|
|
79
|
+
* EXPLICIT `role="paragraph"`/`role="strong"`/etc. restatement, a rare
|
|
80
|
+
* case — the native tag itself resolves to role `null`, same as a bare
|
|
81
|
+
* `<div>`/`<span>`, and falls into this same roleless branch). See
|
|
82
|
+
* ROLELESS_NATIVE_TAGS below for the resulting curated list —
|
|
83
|
+
* deliberately conservative: `<section>`/`<form>`/`<a>` are excluded
|
|
84
|
+
* even though they can also resolve to no role, because their native
|
|
85
|
+
* role is conditional (name-dependent/href-dependent) and already has
|
|
86
|
+
* dedicated, more nuanced handling elsewhere in this engine
|
|
87
|
+
* (`getElementRoleKey`'s `section`/`section[named]`/`header`/
|
|
88
|
+
* `header[toplevel]` branches) that this rule doesn't attempt to
|
|
89
|
+
* duplicate.
|
|
90
|
+
* Two confidence tiers instead of a flat fail: if the element's subtree
|
|
91
|
+
* ALREADY produces a non-empty accessible name from its content
|
|
92
|
+
* (computed the same way link-name-present/button-name-present do, via
|
|
93
|
+
* `helpers.getContentNameInfo`), the naming attribute might just be a
|
|
94
|
+
* redundant/intentional override — reported as `cantTell`, not a hard
|
|
95
|
+
* fail. Only a roleless element with NO other accessible-name source at
|
|
96
|
+
* all (the emoji-mart case: an icon-only span, background-image styled,
|
|
97
|
+
* no text anywhere in its subtree) is a confident, deterministic `fail`
|
|
98
|
+
* — nothing else could ever expose this element's name, and no role
|
|
99
|
+
* exists to make it a Name/Role/Value candidate in the first place.
|
|
100
|
+
* The widget-ancestor exemption (skip when the closest real ancestor role
|
|
101
|
+
* is a "widget"-type role) avoids over-flagging roleless helper
|
|
102
|
+
* spans/divs used as internal decoration inside a custom composite
|
|
103
|
+
* widget.
|
|
104
|
+
* - Fixed 2026-07-31 (same day as introduced): the Tier-2 "already has a
|
|
105
|
+
* role, not this branch's concern" guard checked only whether `role=""`
|
|
106
|
+
* was present (`getExplicitRole`), not whether the value was a real,
|
|
107
|
+
* recognized ARIA role. An invalid/typo'd role token (e.g.
|
|
108
|
+
* `role="totally-bogus"`) therefore silently suppressed detection of an
|
|
109
|
+
* otherwise-flaggable roleless naming attribute — identical markup with
|
|
110
|
+
* the bogus role attribute removed entirely correctly failed, but with
|
|
111
|
+
* it present the element was skipped as if it had a real role. Per spec
|
|
112
|
+
* (and per this same file's own `getNearestAncestorRole` helper a few
|
|
113
|
+
* lines below, which already gets this right), an unrecognized role
|
|
114
|
+
* token is ignored by the accessibility tree, not honored — the element
|
|
115
|
+
* is still effectively roleless. Now validates via the existing
|
|
116
|
+
* `isValidConcreteRole` before treating an explicit role as real,
|
|
117
|
+
* matching `getNearestAncestorRole`'s own pattern.
|
|
61
118
|
*/
|
|
62
119
|
|
|
63
120
|
const id = 'aria-prohibited-attr';
|
|
64
121
|
|
|
65
122
|
const meta = {
|
|
66
123
|
title: 'ARIA naming attributes must not be used on roles that prohibit them',
|
|
67
|
-
description:
|
|
124
|
+
description:
|
|
125
|
+
'Checks that aria-label/aria-labelledby are not present on WAI-ARIA roles whose specification explicitly prohibits ARIA naming (e.g. generic, emphasis, strong, paragraph).',
|
|
68
126
|
i18n: {
|
|
69
127
|
titleKey: 'ariaProhibitedAttr_title',
|
|
70
128
|
descriptionKey: 'ariaProhibitedAttr_description'
|
|
@@ -73,7 +131,13 @@ const meta = {
|
|
|
73
131
|
tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
|
|
74
132
|
wcagSc: ['4.1.2'],
|
|
75
133
|
normativeMappings: [
|
|
76
|
-
{
|
|
134
|
+
{
|
|
135
|
+
standard: 'WCAG',
|
|
136
|
+
version: '2.2',
|
|
137
|
+
requirement: '4.1.2',
|
|
138
|
+
title: 'Name, Role, Value',
|
|
139
|
+
conformanceLevel: 'A'
|
|
140
|
+
}
|
|
77
141
|
],
|
|
78
142
|
defaultSeverity: 'moderate',
|
|
79
143
|
category: 'robust',
|
|
@@ -83,7 +147,7 @@ const meta = {
|
|
|
83
147
|
};
|
|
84
148
|
|
|
85
149
|
function runInPage(ctx) {
|
|
86
|
-
const {
|
|
150
|
+
const { helpers, rule } = ctx;
|
|
87
151
|
|
|
88
152
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
89
153
|
if (!ariaHelpers) {
|
|
@@ -96,19 +160,36 @@ function runInPage(ctx) {
|
|
|
96
160
|
// scope) because the build inlines only this function's own source text
|
|
97
161
|
// — see scripts/build-core.js header ("runInPage MUST be self-contained").
|
|
98
162
|
const ROLES_PROHIBITING_NAME = new Set([
|
|
99
|
-
'caption',
|
|
100
|
-
'
|
|
101
|
-
'
|
|
163
|
+
'caption',
|
|
164
|
+
'code',
|
|
165
|
+
'deletion',
|
|
166
|
+
'emphasis',
|
|
167
|
+
'generic',
|
|
168
|
+
'insertion',
|
|
169
|
+
'mark',
|
|
170
|
+
'none',
|
|
171
|
+
'paragraph',
|
|
172
|
+
'presentation',
|
|
173
|
+
'strong',
|
|
174
|
+
'subscript',
|
|
175
|
+
'suggestion',
|
|
176
|
+
'superscript',
|
|
177
|
+
'time'
|
|
102
178
|
]);
|
|
103
179
|
|
|
104
180
|
const PROHIBITED_NAMING_ATTRS = ['aria-label', 'aria-labelledby'];
|
|
105
181
|
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
const occurrences = [];
|
|
182
|
+
const failOccurrences = [];
|
|
183
|
+
const cantTellOccurrences = [];
|
|
109
184
|
let applicableCount = 0;
|
|
110
185
|
|
|
111
|
-
|
|
186
|
+
// --- Tier 1: explicit, valid role from the naming-prohibited set ---
|
|
187
|
+
|
|
188
|
+
const roleNodes = helpers.queryAllSmart
|
|
189
|
+
? helpers.queryAllSmart('[role]')
|
|
190
|
+
: helpers.queryAll('[role]');
|
|
191
|
+
|
|
192
|
+
for (const el of roleNodes) {
|
|
112
193
|
if (!el || !el.getAttribute) continue;
|
|
113
194
|
|
|
114
195
|
const role = ariaHelpers.getExplicitRole(el);
|
|
@@ -125,10 +206,10 @@ function runInPage(ctx) {
|
|
|
125
206
|
if (!present.length) continue;
|
|
126
207
|
|
|
127
208
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
128
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
209
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
129
210
|
|
|
130
211
|
for (const attr of present) {
|
|
131
|
-
|
|
212
|
+
failOccurrences.push({
|
|
132
213
|
selector: stableSelector,
|
|
133
214
|
html,
|
|
134
215
|
summary: 'This attribute is prohibited on this element’s role.',
|
|
@@ -145,13 +226,206 @@ function runInPage(ctx) {
|
|
|
145
226
|
}
|
|
146
227
|
}
|
|
147
228
|
|
|
229
|
+
// --- Tier 2: no role at all (see header comment for the full rationale
|
|
230
|
+
// and how ROLELESS_NATIVE_TAGS/WIDGET_TYPE_ROLES were derived) ---
|
|
231
|
+
|
|
232
|
+
// Small, curated set of native tags empirically verified (against a
|
|
233
|
+
// widely-used reference engine's own getRole() at runtime, not guessed)
|
|
234
|
+
// to carry no explicit or implicit ARIA role. Deliberately excludes
|
|
235
|
+
// <section>/<form>/<a> — all conditionally roleless too, but already
|
|
236
|
+
// handled with more nuance elsewhere in this engine (see header comment).
|
|
237
|
+
const ROLELESS_NATIVE_TAGS = new Set([
|
|
238
|
+
'p',
|
|
239
|
+
'b',
|
|
240
|
+
'i',
|
|
241
|
+
'em',
|
|
242
|
+
'strong',
|
|
243
|
+
'span',
|
|
244
|
+
'div',
|
|
245
|
+
'code',
|
|
246
|
+
'mark',
|
|
247
|
+
'time',
|
|
248
|
+
'ins',
|
|
249
|
+
'del',
|
|
250
|
+
'small',
|
|
251
|
+
'sub',
|
|
252
|
+
'sup',
|
|
253
|
+
'abbr',
|
|
254
|
+
'cite',
|
|
255
|
+
'q',
|
|
256
|
+
'kbd',
|
|
257
|
+
'samp',
|
|
258
|
+
'var',
|
|
259
|
+
'address',
|
|
260
|
+
'blockquote',
|
|
261
|
+
'pre',
|
|
262
|
+
'figcaption',
|
|
263
|
+
'picture',
|
|
264
|
+
'template',
|
|
265
|
+
'hgroup',
|
|
266
|
+
'wbr',
|
|
267
|
+
'br',
|
|
268
|
+
'legend'
|
|
269
|
+
]);
|
|
270
|
+
|
|
271
|
+
// WAI-ARIA roles a widely-used reference engine's own role table types as
|
|
272
|
+
// "widget" (verified directly against its source, not the six-category
|
|
273
|
+
// WAI-ARIA taxonomy — this engine's algorithm branches on its own `type`
|
|
274
|
+
// field, so parity means matching that field exactly).
|
|
275
|
+
const WIDGET_TYPE_ROLES = new Set([
|
|
276
|
+
'alert',
|
|
277
|
+
'alertdialog',
|
|
278
|
+
'button',
|
|
279
|
+
'checkbox',
|
|
280
|
+
'combobox',
|
|
281
|
+
'dialog',
|
|
282
|
+
'gridcell',
|
|
283
|
+
'link',
|
|
284
|
+
'listbox',
|
|
285
|
+
'log',
|
|
286
|
+
'marquee',
|
|
287
|
+
'menuitem',
|
|
288
|
+
'menuitemcheckbox',
|
|
289
|
+
'menuitemradio',
|
|
290
|
+
'option',
|
|
291
|
+
'progressbar',
|
|
292
|
+
'radio',
|
|
293
|
+
'scrollbar',
|
|
294
|
+
'searchbox',
|
|
295
|
+
'slider',
|
|
296
|
+
'spinbutton',
|
|
297
|
+
'status',
|
|
298
|
+
'switch',
|
|
299
|
+
'tab',
|
|
300
|
+
'tabpanel',
|
|
301
|
+
'textbox',
|
|
302
|
+
'timer',
|
|
303
|
+
'treeitem'
|
|
304
|
+
]);
|
|
305
|
+
|
|
306
|
+
const getComposedParent =
|
|
307
|
+
helpers && typeof helpers.composedParent === 'function'
|
|
308
|
+
? helpers.composedParent
|
|
309
|
+
: function (n) {
|
|
310
|
+
return n && n.parentElement ? n.parentElement : null;
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// Nearest ancestor's real role (explicit-if-valid, else native/implicit),
|
|
314
|
+
// skipping roleless/presentation/none ancestors — used only to check
|
|
315
|
+
// whether that role is a "widget"-type one (the roleless-branch
|
|
316
|
+
// exemption). Not the same helper as aria-required-parent's containment
|
|
317
|
+
// walk: this one also accepts non-required-context roles.
|
|
318
|
+
function getNearestAncestorRole(el) {
|
|
319
|
+
let cur = getComposedParent(el);
|
|
320
|
+
let guard = 0;
|
|
321
|
+
while (cur && guard++ < 200) {
|
|
322
|
+
if (cur.nodeType !== 1) {
|
|
323
|
+
cur = getComposedParent(cur);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const explicit = ariaHelpers.getExplicitRole(cur);
|
|
327
|
+
const role =
|
|
328
|
+
explicit && ariaHelpers.isValidConcreteRole(explicit)
|
|
329
|
+
? explicit
|
|
330
|
+
: ariaHelpers.getNativeRoleForElement(cur);
|
|
331
|
+
if (!role || role === 'presentation' || role === 'none') {
|
|
332
|
+
cur = getComposedParent(cur);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
return role;
|
|
336
|
+
}
|
|
337
|
+
return '';
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const namingSelector = '[aria-label],[aria-labelledby]';
|
|
341
|
+
const namingNodes = helpers.queryAllSmart
|
|
342
|
+
? helpers.queryAllSmart(namingSelector)
|
|
343
|
+
: helpers.queryAll(namingSelector);
|
|
344
|
+
|
|
345
|
+
for (const el of namingNodes) {
|
|
346
|
+
if (!el || !el.getAttribute) continue;
|
|
347
|
+
|
|
348
|
+
const tag = String(el.tagName || '').toLowerCase();
|
|
349
|
+
if (!ROLELESS_NATIVE_TAGS.has(tag)) continue;
|
|
350
|
+
const explicitRole = ariaHelpers.getExplicitRole(el);
|
|
351
|
+
if (explicitRole && ariaHelpers.isValidConcreteRole(explicitRole)) continue; // has a real, recognized role — Tier 1's concern (if in ROLES_PROHIBITING_NAME) or a role this rule has no opinion on. An INVALID role token (e.g. a typo) is ignored per spec, same as no role attribute at all, and must still fall through to this branch.
|
|
352
|
+
if (ariaHelpers.getNativeRoleForElement(el)) continue; // has a real implicit role after all — not this branch's concern
|
|
353
|
+
|
|
354
|
+
const present = [];
|
|
355
|
+
for (const attr of PROHIBITED_NAMING_ATTRS) {
|
|
356
|
+
const v = el.getAttribute(attr);
|
|
357
|
+
if (v != null && String(v).trim() !== '') present.push(attr);
|
|
358
|
+
}
|
|
359
|
+
if (!present.length) continue;
|
|
360
|
+
|
|
361
|
+
applicableCount += 1;
|
|
362
|
+
|
|
363
|
+
const ancestorRole = getNearestAncestorRole(el);
|
|
364
|
+
if (ancestorRole && WIDGET_TYPE_ROLES.has(ancestorRole)) continue; // roleless helper node inside a real widget — not flagged
|
|
365
|
+
|
|
366
|
+
const nameInfo = helpers.getContentNameInfo ? helpers.getContentNameInfo(el, ctx) : null;
|
|
367
|
+
const hasContentFallback = !!(
|
|
368
|
+
nameInfo &&
|
|
369
|
+
nameInfo.present &&
|
|
370
|
+
String(nameInfo.value || '').trim() !== ''
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
374
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
375
|
+
|
|
376
|
+
for (const attr of present) {
|
|
377
|
+
if (hasContentFallback) {
|
|
378
|
+
cantTellOccurrences.push({
|
|
379
|
+
selector: stableSelector,
|
|
380
|
+
html,
|
|
381
|
+
summary: `This ${tag} has no role, so ${attr} may not be exposed as its accessible name by assistive technology — but the element's own content already provides one.`,
|
|
382
|
+
hint: 'Verify whether the existing text content already serves as this element’s label; if so the naming attribute is redundant, otherwise give the element a role that supports naming (e.g. role="img").',
|
|
383
|
+
i18n: {
|
|
384
|
+
summaryKey: 'ariaProhibitedAttr_summary_cantTell_roleless',
|
|
385
|
+
hintKey: 'ariaProhibitedAttr_hint_cantTell_roleless',
|
|
386
|
+
params: { attr, element: tag }
|
|
387
|
+
},
|
|
388
|
+
data: {
|
|
389
|
+
details: {
|
|
390
|
+
reasonCode: 'ARIA_ATTR_PROHIBITED_ROLELESS_NEEDS_REVIEW',
|
|
391
|
+
attr,
|
|
392
|
+
role: null,
|
|
393
|
+
element: tag
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
} else {
|
|
398
|
+
failOccurrences.push({
|
|
399
|
+
selector: stableSelector,
|
|
400
|
+
html,
|
|
401
|
+
summary: `This ${tag} has no role and no other accessible-name source, so ${attr} is not reliably exposed to assistive technology.`,
|
|
402
|
+
hint: 'Give this element a role that supports an accessible name (e.g. role="img"/"button"), or remove this attribute if it serves no purpose without one.',
|
|
403
|
+
i18n: {
|
|
404
|
+
summaryKey: 'ariaProhibitedAttr_summary_fail_roleless',
|
|
405
|
+
hintKey: 'ariaProhibitedAttr_hint_fail_roleless',
|
|
406
|
+
params: { attr, element: tag }
|
|
407
|
+
},
|
|
408
|
+
data: {
|
|
409
|
+
details: { reasonCode: 'ARIA_ATTR_PROHIBITED_ROLELESS', attr, role: null, element: tag }
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
148
416
|
if (applicableCount === 0) {
|
|
149
417
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
150
418
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
419
|
+
|
|
420
|
+
// See helpers.resolveTieredOutcome's own header comment (src/core/dom-helpers.js):
|
|
421
|
+
// a fail-tier finding never silently discards cantTell-tier findings from
|
|
422
|
+
// the same run — both are returned together when the outcome is 'fail'.
|
|
423
|
+
const resolved = helpers.resolveTieredOutcome(
|
|
424
|
+
failOccurrences,
|
|
425
|
+
cantTellOccurrences,
|
|
426
|
+
rule.defaultSeverity || 'moderate'
|
|
427
|
+
);
|
|
428
|
+
return { ruleId: rule.ruleId, ...resolved };
|
|
155
429
|
}
|
|
156
430
|
|
|
157
431
|
module.exports = { id, meta, runInPage };
|
|
@@ -52,12 +52,42 @@
|
|
|
52
52
|
* focusability) are static, declarative markup facts with no live-DOM/
|
|
53
53
|
* hydration risk, unlike e.g. `aria-checked-state-mismatch`'s DOM-
|
|
54
54
|
* property comparison.
|
|
55
|
+
* - Fixed 2026-07-30: a roleless-but-focusable descendant's message and
|
|
56
|
+
* `data.details.attr` used to always claim "carries tabindex" even when
|
|
57
|
+
* the element had no tabindex attribute at all and was only focusable
|
|
58
|
+
* natively (e.g. an <a href> link). `helpers.getFocusableInfo`'s
|
|
59
|
+
* `mechanism` field ('tabindex' | 'native' | ...) is now used to tell
|
|
60
|
+
* the two apart, with a distinct `nativeFocusable` attr/message for the
|
|
61
|
+
* native case. Found via a real Angular app: a routerLink <a> inside a
|
|
62
|
+
* role="list" was reported as "carries tabindex" though the rendered
|
|
63
|
+
* markup had no such attribute.
|
|
55
64
|
* - Recursion stops at the first non-transparent role boundary, same as
|
|
56
65
|
* that reference engine: a nested container with its own real role (e.g. a
|
|
57
66
|
* <div role="listbox"> inside a menubar) is evaluated as ITS OWN
|
|
58
67
|
* owned-role entry against the outer container (and, separately, gets
|
|
59
68
|
* its own applicability pass as a container in the same rule run) —
|
|
60
69
|
* its descendants are never misattributed to the outer container.
|
|
70
|
+
* - Fixed 2026-07-31: child-role resolution used `ariaHelpers.getExplicitRole`
|
|
71
|
+
* (explicit role="" attribute only), unlike aria-required-children's
|
|
72
|
+
* descendant matching which uses `ariaHelpers.getContainmentRole` (explicit
|
|
73
|
+
* role, falling back to the native-tag map — li/tr/td/th/tbody/ul/ol/
|
|
74
|
+
* table/select/input[type=radio] — see that helper's own header comment).
|
|
75
|
+
* A bare `<li>` with no role="" attribute — the common CSS-reset
|
|
76
|
+
* workaround `<ul role="list"><li>...</li></ul>` that getContainmentRole
|
|
77
|
+
* exists specifically to handle — was therefore read as roleless here,
|
|
78
|
+
* making it structurally transparent: the walk recursed straight through
|
|
79
|
+
* the listitem boundary into its subtree and could report a focusable
|
|
80
|
+
* descendant several levels down as a disallowed owned child of the list,
|
|
81
|
+
* instead of stopping at the (implicit) listitem the way
|
|
82
|
+
* aria-required-children already does. Switched to getContainmentRole so
|
|
83
|
+
* both rules resolve an owned child's role identically. This is a general
|
|
84
|
+
* fix, not list/listitem-specific: it applies to every container role in
|
|
85
|
+
* REQUIRED_OWNED_ROLES whose native-tag counterpart the child map covers
|
|
86
|
+
* (e.g. a bare `<tr>`/`<td>` under a role="table"/"grid"/"row" container
|
|
87
|
+
* with no explicit role="" was subject to the same flattening bug). Found
|
|
88
|
+
* via a real Angular Material-style component library: an `<a routerlink>`
|
|
89
|
+
* several DOM levels inside a bare `<li>` under `<ul role="list">` was
|
|
90
|
+
* reported as an unallowed owned child of the list.
|
|
61
91
|
* - Gated on isAccTreeEligible for the container itself, matching the fix
|
|
62
92
|
* applied to aria-required-children (see that rule's header): the
|
|
63
93
|
* original "not gated" note here just cited that rule's reasoning
|
|
@@ -81,7 +111,8 @@ const id = 'aria-prohibited-children';
|
|
|
81
111
|
|
|
82
112
|
const meta = {
|
|
83
113
|
title: 'Container roles must not own a child with a disallowed role',
|
|
84
|
-
description:
|
|
114
|
+
description:
|
|
115
|
+
"Checks that every accessible-tree-owned child of a container role (list, listbox, menu, menubar, radiogroup, rowgroup, table, grid, treegrid, tablist, tree, row) has one of that role's allowed owned roles — the same set as its required owned roles.",
|
|
85
116
|
i18n: {
|
|
86
117
|
titleKey: 'ariaProhibitedChildren_title',
|
|
87
118
|
descriptionKey: 'ariaProhibitedChildren_description'
|
|
@@ -90,7 +121,13 @@ const meta = {
|
|
|
90
121
|
tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
|
|
91
122
|
wcagSc: ['4.1.2'],
|
|
92
123
|
normativeMappings: [
|
|
93
|
-
{
|
|
124
|
+
{
|
|
125
|
+
standard: 'WCAG',
|
|
126
|
+
version: '2.2',
|
|
127
|
+
requirement: '4.1.2',
|
|
128
|
+
title: 'Name, Role, Value',
|
|
129
|
+
conformanceLevel: 'A'
|
|
130
|
+
}
|
|
94
131
|
],
|
|
95
132
|
defaultSeverity: 'moderate',
|
|
96
133
|
category: 'robust',
|
|
@@ -100,7 +137,7 @@ const meta = {
|
|
|
100
137
|
};
|
|
101
138
|
|
|
102
139
|
function runInPage(ctx) {
|
|
103
|
-
const {
|
|
140
|
+
const { helpers, rule } = ctx;
|
|
104
141
|
|
|
105
142
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
106
143
|
if (!ariaHelpers) {
|
|
@@ -108,7 +145,8 @@ function runInPage(ctx) {
|
|
|
108
145
|
}
|
|
109
146
|
|
|
110
147
|
function isEligibleAcc(el) {
|
|
111
|
-
const fn =
|
|
148
|
+
const fn =
|
|
149
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
112
150
|
if (!fn) return true;
|
|
113
151
|
try {
|
|
114
152
|
const r = fn(el, ctx);
|
|
@@ -125,12 +163,30 @@ function runInPage(ctx) {
|
|
|
125
163
|
// roleless descendant carrying any of these is a real accessible-tree
|
|
126
164
|
// node a widely-used reference engine's getOwnedRoles also flags, not a transparent wrapper.
|
|
127
165
|
const GLOBAL_ARIA_ATTRS = [
|
|
128
|
-
'aria-atomic',
|
|
129
|
-
'aria-
|
|
130
|
-
'aria-
|
|
131
|
-
'aria-
|
|
132
|
-
'aria-
|
|
133
|
-
'aria-
|
|
166
|
+
'aria-atomic',
|
|
167
|
+
'aria-braillelabel',
|
|
168
|
+
'aria-brailleroledescription',
|
|
169
|
+
'aria-busy',
|
|
170
|
+
'aria-controls',
|
|
171
|
+
'aria-current',
|
|
172
|
+
'aria-describedby',
|
|
173
|
+
'aria-description',
|
|
174
|
+
'aria-details',
|
|
175
|
+
'aria-disabled',
|
|
176
|
+
'aria-dropeffect',
|
|
177
|
+
'aria-errormessage',
|
|
178
|
+
'aria-flowto',
|
|
179
|
+
'aria-grabbed',
|
|
180
|
+
'aria-haspopup',
|
|
181
|
+
'aria-hidden',
|
|
182
|
+
'aria-invalid',
|
|
183
|
+
'aria-keyshortcuts',
|
|
184
|
+
'aria-label',
|
|
185
|
+
'aria-labelledby',
|
|
186
|
+
'aria-live',
|
|
187
|
+
'aria-owns',
|
|
188
|
+
'aria-relevant',
|
|
189
|
+
'aria-roledescription'
|
|
134
190
|
];
|
|
135
191
|
|
|
136
192
|
function getGlobalAriaAttr(el) {
|
|
@@ -151,6 +207,11 @@ function runInPage(ctx) {
|
|
|
151
207
|
// entry with role: null, which can never satisfy a required-role set)
|
|
152
208
|
// when it carries a global aria-* attribute or is focusable — matches
|
|
153
209
|
// a widely-used reference engine's own getOwnedRoles exactly (see header comment).
|
|
210
|
+
// kidRole comes from getContainmentRole, not getExplicitRole (see header
|
|
211
|
+
// comment's 2026-07-31 fix): "roleless" here means neither an explicit
|
|
212
|
+
// role="" NOR one of the native containment tags (li, tr, td, ...), so a
|
|
213
|
+
// bare <li>/<tr>/... is a real listitem/row boundary, not a transparent
|
|
214
|
+
// wrapper the walk should pass through.
|
|
154
215
|
function collectOwnedRoles(el, requiredSet, out, depth) {
|
|
155
216
|
if (depth > MAX_DEPTH) return;
|
|
156
217
|
const kids = el.children ? Array.prototype.slice.call(el.children) : [];
|
|
@@ -158,21 +219,31 @@ function runInPage(ctx) {
|
|
|
158
219
|
if (!kid || kid.nodeType !== 1) continue;
|
|
159
220
|
if (!isEligibleAcc(kid)) continue;
|
|
160
221
|
|
|
161
|
-
const kidRole = ariaHelpers.
|
|
222
|
+
const kidRole = ariaHelpers.getContainmentRole(kid);
|
|
162
223
|
const isPresentational = kidRole === 'presentation' || kidRole === 'none';
|
|
163
|
-
const isTransparentGroup =
|
|
224
|
+
const isTransparentGroup =
|
|
225
|
+
(kidRole === 'group' || kidRole === 'rowgroup') && requiredSet.has(kidRole);
|
|
164
226
|
|
|
165
227
|
if (!kidRole && !isPresentational) {
|
|
166
228
|
const globalAttr = getGlobalAriaAttr(kid);
|
|
167
|
-
let
|
|
229
|
+
let mechanism;
|
|
168
230
|
try {
|
|
169
|
-
const fi =
|
|
170
|
-
|
|
231
|
+
const fi =
|
|
232
|
+
helpers && typeof helpers.getFocusableInfo === 'function'
|
|
233
|
+
? helpers.getFocusableInfo(kid, ctx)
|
|
234
|
+
: null;
|
|
235
|
+
mechanism = (fi && fi.focusable && fi.mechanism) || 'none';
|
|
171
236
|
} catch {
|
|
172
|
-
|
|
237
|
+
mechanism = 'none';
|
|
173
238
|
}
|
|
174
|
-
if (globalAttr ||
|
|
175
|
-
|
|
239
|
+
if (globalAttr || mechanism !== 'none') {
|
|
240
|
+
// `mechanism` distinguishes an actual tabindex="" attribute from
|
|
241
|
+
// native focusability (e.g. <a href>, <button>, <input>) — these
|
|
242
|
+
// are different facts and must not be reported as the same
|
|
243
|
+
// "carries tabindex" claim (a native anchor with no tabindex
|
|
244
|
+
// attribute at all is not "carrying tabindex").
|
|
245
|
+
const attr = globalAttr || (mechanism === 'tabindex' ? 'tabindex' : 'nativeFocusable');
|
|
246
|
+
out.push({ el: kid, role: null, attr });
|
|
176
247
|
continue; // real accessible-tree node: stop here, do not recurse further
|
|
177
248
|
}
|
|
178
249
|
}
|
|
@@ -189,7 +260,9 @@ function runInPage(ctx) {
|
|
|
189
260
|
}
|
|
190
261
|
}
|
|
191
262
|
|
|
192
|
-
const nodes = helpers.queryAllSmart
|
|
263
|
+
const nodes = helpers.queryAllSmart
|
|
264
|
+
? helpers.queryAllSmart('[role]')
|
|
265
|
+
: helpers.queryAll('[role]');
|
|
193
266
|
|
|
194
267
|
const occurrences = [];
|
|
195
268
|
let applicableCount = 0;
|
|
@@ -215,16 +288,34 @@ function runInPage(ctx) {
|
|
|
215
288
|
if (entry.role && requiredSet.has(entry.role)) continue;
|
|
216
289
|
|
|
217
290
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(entry.el) : 'html';
|
|
218
|
-
const html = helpers.getOuterHtmlSnippet
|
|
291
|
+
const html = helpers.getOuterHtmlSnippet
|
|
292
|
+
? helpers.getOuterHtmlSnippet(entry.el)
|
|
293
|
+
: entry.el.outerHTML || '';
|
|
219
294
|
const containerSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
220
295
|
|
|
221
296
|
const isRoleless = !entry.role;
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
297
|
+
const isNativeFocusable = entry.attr === 'nativeFocusable';
|
|
298
|
+
|
|
299
|
+
let summary;
|
|
300
|
+
let hint;
|
|
301
|
+
let summaryKey;
|
|
302
|
+
let hintKey;
|
|
303
|
+
if (isNativeFocusable) {
|
|
304
|
+
summary = `This element has no explicit role but is natively focusable, making it a real accessible-tree node that is not an allowed owned child of the enclosing role="${role}" container.`;
|
|
305
|
+
hint = `Give this element role="presentation"/"none", remove its native focusability (e.g. drop the href/tabindex-granting attribute), or move it outside the ${role} container.`;
|
|
306
|
+
summaryKey = 'ariaProhibitedChildren_summary_fail_native_focusable';
|
|
307
|
+
hintKey = 'ariaProhibitedChildren_hint_fail_native_focusable';
|
|
308
|
+
} else if (isRoleless) {
|
|
309
|
+
summary = `This element has no explicit role but carries ${entry.attr}, making it a real accessible-tree node that is not an allowed owned child of the enclosing role="${role}" container.`;
|
|
310
|
+
hint = `Remove ${entry.attr} (or the role="${role}" container ownership), or give this element role="presentation"/"none" if it isn't meant to be its own accessible-tree node.`;
|
|
311
|
+
summaryKey = 'ariaProhibitedChildren_summary_fail_roleless';
|
|
312
|
+
hintKey = 'ariaProhibitedChildren_hint_fail_roleless';
|
|
313
|
+
} else {
|
|
314
|
+
summary = `This element has role="${entry.role}", which is not an allowed owned child of the enclosing role="${role}" container.`;
|
|
315
|
+
hint = `Remove or change this role so it matches one of the container's allowed owned roles (${requiredOwned.join(', ')}), or move this element outside the ${role} container.`;
|
|
316
|
+
summaryKey = 'ariaProhibitedChildren_summary_fail';
|
|
317
|
+
hintKey = 'ariaProhibitedChildren_hint_fail';
|
|
318
|
+
}
|
|
228
319
|
|
|
229
320
|
occurrences.push({
|
|
230
321
|
selector: stableSelector,
|
|
@@ -232,8 +323,8 @@ function runInPage(ctx) {
|
|
|
232
323
|
summary,
|
|
233
324
|
hint,
|
|
234
325
|
i18n: {
|
|
235
|
-
summaryKey
|
|
236
|
-
hintKey
|
|
326
|
+
summaryKey,
|
|
327
|
+
hintKey,
|
|
237
328
|
params: isRoleless
|
|
238
329
|
? { attr: entry.attr, containerRole: role }
|
|
239
330
|
: { childRole: entry.role, containerRole: role, allowedRoles: requiredOwned.join(', ') }
|
|
@@ -256,7 +347,12 @@ function runInPage(ctx) {
|
|
|
256
347
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
257
348
|
}
|
|
258
349
|
if (occurrences.length) {
|
|
259
|
-
return {
|
|
350
|
+
return {
|
|
351
|
+
ruleId: rule.ruleId,
|
|
352
|
+
outcome: 'fail',
|
|
353
|
+
severity: rule.defaultSeverity || 'moderate',
|
|
354
|
+
occurrences
|
|
355
|
+
};
|
|
260
356
|
}
|
|
261
357
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
262
358
|
}
|