eslint-plugin-aria-state-validator 1.0.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/README.md ADDED
@@ -0,0 +1,360 @@
1
+ # eslint-plugin-aria-state-validator
2
+
3
+ Comprehensive ESLint plugin for ARIA accessibility validation:
4
+ - **Dynamic State Validation**: Validates that ARIA state attributes are properly bound to component state
5
+ - **Static ARIA Validation**: Validates static ARIA attributes for correctness (spelling, values, conflicts)
6
+
7
+ Catches both runtime and static accessibility errors at build time.
8
+
9
+ ## Problem Statement: The "Runtime Gap" in Static Analysis
10
+
11
+ Existing accessibility tools like `eslint-plugin-jsx-a11y` validate the syntactic correctness of ARIA attributes (e.g., ensuring `aria-expanded` uses `'true'` or `'false'`), but they **cannot verify** whether these attributes are correctly connected to the component's JavaScript state and dynamically updated at runtime.
12
+
13
+ ### Issues This Causes:
14
+
15
+ - Toggle buttons with `aria-expanded="false"` hardcoded, never changing when opened
16
+ - Boolean values directly bound to ARIA attributes: `aria-expanded={isOpen}` (incorrect - should be string)
17
+ - Static ARIA states on interactive elements that should be dynamic
18
+ - Screen readers receiving inaccurate information about component states
19
+
20
+ ## Core Concept: State-Dependent ARIA Validation
21
+
22
+ This plugin uses AST (Abstract Syntax Tree) analysis to **statically verify** that ARIA state attributes are logically connected to component state (State/Props) and will update dynamically at runtime.
23
+
24
+ ### Key Differentiator
25
+
26
+ Goes beyond syntax checking to infer component **dynamic behavior**, proactively catching runtime accessibility errors during build/CI phase.
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ npm install --save-dev eslint-plugin-aria-state-validator
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ### ESLint Flat Config (ESLint 9+)
37
+
38
+ ```javascript
39
+ // eslint.config.js
40
+ import ariaStateValidator from 'eslint-plugin-aria-state-validator';
41
+
42
+ export default [
43
+ {
44
+ plugins: {
45
+ 'aria-state-validator': ariaStateValidator
46
+ },
47
+ rules: {
48
+ 'aria-state-validator/state-dependent-aria-validator': 'warn'
49
+ }
50
+ }
51
+ ];
52
+ ```
53
+
54
+ ### Legacy ESLintRC Config
55
+
56
+ ```json
57
+ {
58
+ "plugins": ["aria-state-validator"],
59
+ "rules": {
60
+ "aria-state-validator/state-dependent-aria-validator": "warn"
61
+ }
62
+ }
63
+ ```
64
+
65
+ ## Two Rules for Comprehensive ARIA Validation
66
+
67
+ This plugin provides two complementary rules:
68
+
69
+ ### Rule 1: `state-dependent-aria-validator` (Dynamic State Validation)
70
+
71
+ Validates that ARIA **state** attributes are properly bound to component state and update dynamically.
72
+
73
+ ### Rule 2: `static-aria-validator` (Static ARIA Validation)
74
+
75
+ Validates the **correctness** of ARIA attributes when they are used:
76
+ - Valid ARIA property names (catches typos)
77
+ - Correct values for boolean and enum ARIA attributes
78
+ - No conflicting ARIA attributes
79
+ - No redundant roles on native HTML elements
80
+
81
+ **Note:** This plugin validates **how** ARIA attributes are used, not **whether** they should be used. For enforcing the presence of accessibility features, use [eslint-plugin-jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y).
82
+
83
+ ## Features & Validation Patterns
84
+
85
+ ### 1. Boolean Value Direct Binding Detection
86
+
87
+ **Detects:** Direct binding of Boolean values to ARIA attributes
88
+
89
+ ```jsx
90
+ // ❌ Error: Boolean value directly bound
91
+ <button aria-expanded={isOpen}>Toggle</button>
92
+
93
+ // ✅ Correct: Explicit string conversion
94
+ <button aria-expanded={isOpen ? 'true' : 'false'}>Toggle</button>
95
+ ```
96
+
97
+ **Auto-fix Available:** `eslint --fix` will automatically convert to proper string format
98
+
99
+ ### 2. Static Value + Interactive Handler Detection
100
+
101
+ **Detects:** Static ARIA state values on elements with event handlers
102
+
103
+ ```jsx
104
+ // ❌ Error: Has onClick but aria-expanded is static
105
+ <button onClick={handleClick} aria-expanded="false">
106
+ Toggle
107
+ </button>
108
+
109
+ // ✅ Correct: Dynamic state binding
110
+ <button onClick={handleClick} aria-expanded={isOpen ? 'true' : 'false'}>
111
+ Toggle
112
+ </button>
113
+ ```
114
+
115
+ **Smart Auto-fix:** When the handler contains state variable references (e.g., `() => setExpanded(!expanded)`), the plugin automatically extracts the variable name and binds it to the ARIA attribute:
116
+
117
+ ```jsx
118
+ // ❌ Before auto-fix
119
+ <div onClick={() => setExpanded(!expanded)} aria-expanded="false">
120
+ Click to expand
121
+ </div>
122
+
123
+ // ✅ After auto-fix (automatically detects 'expanded' variable)
124
+ <div onClick={() => setExpanded(!expanded)} aria-expanded={expanded ? 'true' : 'false'}>
125
+ Click to expand
126
+ </div>
127
+ ```
128
+
129
+ ### 3. Role-Based State Association Validation
130
+
131
+ **Detects:** Interactive roles with static ARIA states
132
+
133
+ ```jsx
134
+ // ❌ Error: role="button" with static aria-pressed
135
+ <div role="button" aria-pressed="false">Button</div>
136
+
137
+ // ✅ Correct: Dynamic state binding
138
+ <div role="button" aria-pressed={isPressed ? 'true' : 'false'}>Button</div>
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Static ARIA Validation Patterns
144
+
145
+ ### 4. Invalid ARIA Property Detection
146
+
147
+ **Detects:** Typos in ARIA attribute names
148
+
149
+ ```jsx
150
+ // ❌ Error: Typo in aria-label
151
+ <button aria-labell="Close">X</button>
152
+
153
+ // ✅ Correct: Proper spelling
154
+ <button aria-label="Close">X</button>
155
+ ```
156
+
157
+ ### 5. Empty Required Values
158
+
159
+ **Detects:** ARIA attributes that require non-empty values
160
+
161
+ ```jsx
162
+ // ❌ Error: Empty aria-label
163
+ <div aria-label="">Content</div>
164
+
165
+ // ✅ Correct: Non-empty value
166
+ <div aria-label="Description">Content</div>
167
+ ```
168
+
169
+ ### 6. Invalid Boolean Values
170
+
171
+ **Detects:** Incorrect boolean-like values
172
+
173
+ ```jsx
174
+ // ❌ Error: Invalid boolean value
175
+ <div aria-hidden="yes">Content</div>
176
+ <div aria-disabled="1">Content</div>
177
+
178
+ // ✅ Correct: Use "true" or "false"
179
+ <div aria-hidden="true">Content</div>
180
+ <div aria-disabled="false">Content</div>
181
+ ```
182
+
183
+ ### 7. Invalid Enum Values
184
+
185
+ **Detects:** Invalid values for enum-type ARIA attributes
186
+
187
+ ```jsx
188
+ // ❌ Error: Invalid aria-live value
189
+ <div aria-live="loud">Live region</div>
190
+
191
+ // ✅ Correct: Use valid enum value
192
+ <div aria-live="polite">Live region</div>
193
+
194
+ // Valid values: "off", "polite", "assertive"
195
+ ```
196
+
197
+ ### 8. Conflicting ARIA Attributes
198
+
199
+ **Detects:** aria-label and aria-labelledby used together
200
+
201
+ ```jsx
202
+ // ❌ Error: Both attributes present (aria-labelledby takes precedence)
203
+ <div aria-label="Label" aria-labelledby="other-id">Content</div>
204
+
205
+ // ✅ Correct: Use only one
206
+ <div aria-labelledby="other-id">Content</div>
207
+ ```
208
+
209
+ ### 9. Redundant Roles
210
+
211
+ **Detects:** Explicit roles that match native HTML semantics
212
+
213
+ ```jsx
214
+ // ❌ Error: Redundant role
215
+ <button role="button">Click me</button>
216
+ <nav role="navigation">Nav</nav>
217
+ <a href="#" role="link">Link</a>
218
+
219
+ // ✅ Correct: Use native semantics
220
+ <button>Click me</button>
221
+ <nav>Nav</nav>
222
+ <a href="#">Link</a>
223
+ ```
224
+
225
+ ---
226
+
227
+ ## Supported ARIA Attributes (Dynamic Validation)
228
+
229
+ The plugin validates these dynamic ARIA state attributes:
230
+
231
+ - `aria-expanded` - Toggle buttons, expandable elements
232
+ - `aria-selected` - Tabs, selectable options
233
+ - `aria-checked` - Checkboxes, radio buttons
234
+ - `aria-pressed` - Toggle buttons
235
+ - `aria-hidden` - Dynamically shown/hidden elements
236
+ - `aria-disabled` - Dynamically enabled/disabled elements
237
+ - `aria-modal` - Dialogs, modals
238
+ - `aria-current` - Current active item indicators
239
+
240
+ ## Supported Interactive Roles
241
+
242
+ Elements with these roles are expected to have dynamic ARIA states:
243
+
244
+ - `button`, `tab`, `checkbox`, `radio`, `switch`
245
+ - `menuitem`, `menuitemcheckbox`, `menuitemradio`
246
+ - `option`, `treeitem`
247
+ - `dialog`, `alertdialog`
248
+
249
+ ## Examples
250
+
251
+ ### Valid Patterns ✅
252
+
253
+ ```jsx
254
+ // Ternary operator for string conversion
255
+ <button aria-expanded={isOpen ? 'true' : 'false'}>Toggle</button>
256
+
257
+ // String() function
258
+ <button aria-pressed={String(isPressed)}>Toggle</button>
259
+
260
+ // .toString() method
261
+ <div aria-hidden={isHidden.toString()}>Content</div>
262
+
263
+ // Inverted boolean with ternary
264
+ <button aria-expanded={!isOpen ? 'true' : 'false'}>Toggle</button>
265
+
266
+ // Static ARIA on non-interactive element (no handler)
267
+ <div aria-label="static label">Content</div>
268
+ ```
269
+
270
+ ### Invalid Patterns ❌
271
+
272
+ ```jsx
273
+ // Direct boolean binding
274
+ <button aria-expanded={isOpen}>Toggle</button>
275
+ // Fix: aria-expanded={isOpen ? 'true' : 'false'}
276
+
277
+ // Logical expression without string conversion
278
+ <button aria-expanded={foo && bar}>Toggle</button>
279
+ // Fix: aria-expanded={foo && bar ? 'true' : 'false'}
280
+
281
+ // Static value with event handler
282
+ <button onClick={handleClick} aria-expanded="false">Toggle</button>
283
+ // Fix: Use dynamic state binding
284
+
285
+ // Interactive role with static ARIA
286
+ <div role="tab" aria-selected="true">Tab</div>
287
+ // Fix: Use dynamic state binding
288
+ ```
289
+
290
+ ## Technical Implementation
291
+
292
+ ### Technology Stack
293
+
294
+ - **Base:** ESLint plugin (Node.js environment)
295
+ - **Parser:** `@babel/eslint-parser` or TypeScript parser for JSX/TSX AST parsing
296
+ - **Analysis:** Visitor pattern on JSX elements
297
+
298
+ ### Analysis Logic
299
+
300
+ 1. **JSXElement Traversal:** Identifies `role` and `aria-*` attributes
301
+ 2. **Scope Tracking:** Uses `context.getScope()` to trace variables bound to ARIA attributes
302
+ 3. **State Inference:** Determines if values derive from `useState` hooks or props
303
+ 4. **Pattern Validation:** Checks for proper string conversion patterns
304
+
305
+ ## Expected Benefits
306
+
307
+ ### 1. Accessibility Quality Improvement
308
+
309
+ Catch dynamic ARIA errors at build/CI phase instead of runtime
310
+
311
+ ### 2. Developer Productivity
312
+
313
+ Auto-fix repetitive ARIA state binding errors with `eslint --fix`
314
+
315
+ ### 3. Standardization
316
+
317
+ Enforce consistent dynamic ARIA usage patterns across projects
318
+
319
+ ## Development
320
+
321
+ ### Running Tests
322
+
323
+ ```bash
324
+ npm test
325
+ ```
326
+
327
+ ### Building
328
+
329
+ ```bash
330
+ npm run build # If applicable
331
+ ```
332
+
333
+ ## Contributing
334
+
335
+ Contributions welcome! Please feel free to submit issues or pull requests.
336
+
337
+ ## License
338
+
339
+ ISC
340
+
341
+ ## Keywords
342
+
343
+ - eslint
344
+ - eslint-plugin
345
+ - accessibility
346
+ - a11y
347
+ - aria
348
+ - jsx
349
+ - react
350
+ - state-validation
351
+ - dynamic-attributes
352
+
353
+ ## Related Projects
354
+
355
+ - [eslint-plugin-jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y) - Comprehensive JSX accessibility checking
356
+ - [axe-core](https://github.com/dequelabs/axe-core) - Runtime accessibility testing engine
357
+
358
+ ---
359
+
360
+ **Note:** This plugin focuses on static analysis of ARIA state binding patterns. For comprehensive accessibility testing, combine with runtime testing tools like axe-core.
package/index.js ADDED
@@ -0,0 +1,37 @@
1
+ const stateDependentAriaValidator = require('./lib/rules/state-dependent-aria-validator');
2
+ const staticAriaValidator = require('./lib/rules/static-aria-validator');
3
+
4
+ module.exports = {
5
+ rules: {
6
+ 'state-dependent-aria-validator': stateDependentAriaValidator,
7
+ 'static-aria-validator': staticAriaValidator
8
+ },
9
+ configs: {
10
+ recommended: {
11
+ plugins: ['aria-state-validator'],
12
+ rules: {
13
+ 'aria-state-validator/state-dependent-aria-validator': 'warn',
14
+ 'aria-state-validator/static-aria-validator': 'warn'
15
+ }
16
+ },
17
+ strict: {
18
+ plugins: ['aria-state-validator'],
19
+ rules: {
20
+ 'aria-state-validator/state-dependent-aria-validator': 'error',
21
+ 'aria-state-validator/static-aria-validator': 'error'
22
+ }
23
+ },
24
+ 'dynamic-only': {
25
+ plugins: ['aria-state-validator'],
26
+ rules: {
27
+ 'aria-state-validator/state-dependent-aria-validator': 'warn'
28
+ }
29
+ },
30
+ 'static-only': {
31
+ plugins: ['aria-state-validator'],
32
+ rules: {
33
+ 'aria-state-validator/static-aria-validator': 'warn'
34
+ }
35
+ }
36
+ }
37
+ };