eslint-plugin-aria-state-validator 1.0.1 → 1.1.1

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.ko.md CHANGED
@@ -41,18 +41,16 @@ npm install --save-dev eslint-plugin-aria-state-validator
41
41
  import ariaStateValidator from "eslint-plugin-aria-state-validator";
42
42
 
43
43
  export default [
44
- {
45
- plugins: {
46
- "aria-state-validator": ariaStateValidator,
47
- },
48
- rules: {
49
- "aria-state-validator/state-dependent-aria-validator": "warn",
50
- "aria-state-validator/static-aria-validator": "warn",
51
- },
52
- },
44
+ ...ariaStateValidator.configs["flat/recommended"],
53
45
  ];
54
46
  ```
55
47
 
48
+ 사용 가능한 Flat preset:
49
+ - `flat/recommended`
50
+ - `flat/strict`
51
+ - `flat/dynamic-only`
52
+ - `flat/static-only`
53
+
56
54
  ### Legacy ESLintRC Config
57
55
 
58
56
  ```json
@@ -68,17 +66,17 @@ export default [
68
66
  ### 설정 프리셋
69
67
 
70
68
  ```javascript
71
- // 권장 설정 (경고)
72
- ...ariaStateValidator.configs.recommended
73
-
74
- // 엄격한 설정 (에러)
75
- ...ariaStateValidator.configs.strict
76
-
77
- // 동적 검증만
78
- ...ariaStateValidator.configs['dynamic-only']
79
-
80
- // 정적 검증만
81
- ...ariaStateValidator.configs['static-only']
69
+ // Flat Config용 preset
70
+ ...ariaStateValidator.configs["flat/recommended"]
71
+ ...ariaStateValidator.configs["flat/strict"]
72
+ ...ariaStateValidator.configs["flat/dynamic-only"]
73
+ ...ariaStateValidator.configs["flat/static-only"]
74
+
75
+ // Legacy eslintrc용 preset
76
+ extends: ["plugin:aria-state-validator/recommended"]
77
+ extends: ["plugin:aria-state-validator/strict"]
78
+ extends: ["plugin:aria-state-validator/dynamic-only"]
79
+ extends: ["plugin:aria-state-validator/static-only"]
82
80
  ```
83
81
 
84
82
  ## 두 가지 규칙으로 포괄적인 ARIA 검증
@@ -89,14 +87,38 @@ export default [
89
87
 
90
88
  ARIA **상태** 속성이 컴포넌트 상태와 올바르게 연결되어 동적으로 업데이트되는지 검증합니다.
91
89
 
90
+ 규칙 옵션:
91
+ - `interactiveHandlers` (`string[]`): 인터랙티브 핸들러로 간주할 이벤트 prop 목록 재정의 (기본값: `onClick`, `onKeyDown`, `onKeyPress`, `onChange`, `onToggle`)
92
+ - `strictBooleanInference` (`boolean`): `true`(기본값)면 불리언으로 추론되는 식별자만 경고, `false`면 식별자 바인딩 전체를 불리언 위험으로 간주
93
+ - `ignorePatterns` (`string[]`): 속성 이름 또는 값 소스 텍스트와 매칭되는 항목을 검사에서 제외할 정규식 패턴
94
+
95
+ 예시:
96
+
97
+ ```javascript
98
+ {
99
+ rules: {
100
+ "aria-state-validator/state-dependent-aria-validator": [
101
+ "warn",
102
+ {
103
+ interactiveHandlers: ["onClick", "onPointerDown"],
104
+ strictBooleanInference: true,
105
+ ignorePatterns: ["^aria-current$", "^\\{isOpen\\}$"],
106
+ },
107
+ ],
108
+ },
109
+ }
110
+ ```
111
+
92
112
  ### 규칙 2: `static-aria-validator` (정적 ARIA 검증)
93
113
 
94
114
  사용된 ARIA 속성의 **정확성**을 검증합니다:
95
115
 
96
- - 유효한 ARIA 속성 이름 (오타 감지)
97
- - Boolean 및 Enum ARIA 속성의 올바른 값
98
- - 충돌하는 ARIA 속성 감지
99
- - 네이티브 HTML 요소의 중복 role 감지
116
+ - 유효한 ARIA 속성 이름 (오타 감지 및 **자동 수정**)
117
+ - Boolean 및 Enum ARIA 속성의 올바른 값 (**자동 수정** 지원)
118
+ - 충돌하는 ARIA 속성 감지 (**자동 수정** 지원)
119
+ - 네이티브 HTML 요소의 중복 role 감지 (**자동 수정** 지원)
120
+ - **역할별 필수 ARIA 속성** 검증 (예: `switch` 역할의 `aria-checked` 필수 사용)
121
+ - **역할-속성 호환성** 검증 (예: `img` 역할에 `aria-expanded` 사용 방지)
100
122
 
101
123
  **참고:** 이 플러그인은 ARIA 속성을 **어떻게** 사용하는지를 검증하며, **사용 여부**를 강제하지는 않습니다. 접근성 기능의 존재를 강제하려면 [eslint-plugin-jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y)를 사용하세요.
102
124
 
@@ -116,7 +138,7 @@ ARIA **상태** 속성이 컴포넌트 상태와 올바르게 연결되어 동
116
138
  <button aria-expanded={isOpen ? 'true' : 'false'}>토글</button>
117
139
  ```
118
140
 
119
- **자동 수정 가능:** `eslint --fix`가 자동으로 올바른 문자열 형식으로 변환합니다
141
+ **제안 제공:** 규칙이 문자열 형식(`'true'/'false'`)으로 바꾸는 안전한 제안을 제공합니다.
120
142
 
121
143
  ### 2. 정적 값 + 인터랙티브 핸들러 감지
122
144
 
@@ -134,30 +156,39 @@ ARIA **상태** 속성이 컴포넌트 상태와 올바르게 연결되어 동
134
156
  </button>
135
157
  ```
136
158
 
137
- **스마트 자동 수정:** 핸들러에 상태 변수 참조가 있으면 (예: `() => setExpanded(!expanded)`), 플러그인이 자동으로 변수 이름을 추출하여 ARIA 속성에 바인딩합니다:
159
+ **스마트 제안:** 핸들러에 상태 변수 참조가 있으면 (예: `() => setExpanded(!expanded)`), 플러그인이 변수 이름을 추출해 ARIA 속성 바인딩 제안을 제공합니다:
138
160
 
139
161
  ```jsx
140
- // ❌ 자동 수정
162
+ // ❌ 제안
141
163
  <div onClick={() => setExpanded(!expanded)} aria-expanded="false">
142
164
  클릭하여 확장
143
165
  </div>
144
166
 
145
- // ✅ 자동 수정 후 (자동으로 'expanded' 변수 감지)
167
+ // ✅ 제안 적용 후 (자동으로 'expanded' 변수 감지)
146
168
  <div onClick={() => setExpanded(!expanded)} aria-expanded={expanded ? 'true' : 'false'}>
147
169
  클릭하여 확장
148
170
  </div>
149
171
  ```
150
172
 
173
+ **제안 지원 패턴:**
174
+ - `() => setX(!x)`
175
+ - `() => setX(true)`
176
+ - `() => setX(prev => !prev)`
177
+
151
178
  ### 3. 역할 기반 상태 연관성 검증
152
179
 
153
- **감지 내용:** 정적 ARIA 상태를 가진 인터랙티브 역할
180
+ **감지 내용:** 인터랙티브 역할(Role)에 정적인 ARIA 상태를 사용하거나 필수 속성이 누락된 경우.
154
181
 
155
182
  ```jsx
156
183
  // ❌ 오류: role="button"인데 aria-pressed가 정적
157
184
  <div role="button" aria-pressed="false">버튼</div>
158
185
 
186
+ // ❌ 오류: 인터랙티브 switch role에서 aria-checked 누락
187
+ <div role="switch" onClick={toggle}>토글</div>
188
+
159
189
  // ✅ 올바름: 동적 상태 바인딩
160
190
  <div role="button" aria-pressed={isPressed ? 'true' : 'false'}>버튼</div>
191
+ <div role="switch" aria-checked={isChecked ? 'true' : 'false'}>토글</div>
161
192
  ```
162
193
 
163
194
  ---
@@ -193,7 +224,7 @@ ARIA **상태** 속성이 컴포넌트 상태와 올바르게 연결되어 동
193
224
  **감지 내용:** 잘못된 boolean 형식 값
194
225
 
195
226
  ```jsx
196
- // ❌ 오류: 유효하지 않은 boolean 값
227
+ // ❌ 오류: 유효하지 않은 boolean 값 ("yes"/"no"는 자동 수정 지원)
197
228
  <div aria-hidden="yes">내용</div>
198
229
  <div aria-disabled="1">내용</div>
199
230
 
@@ -252,12 +283,15 @@ ARIA **상태** 속성이 컴포넌트 상태와 올바르게 연결되어 동
252
283
 
253
284
  - `aria-expanded` - 토글 버튼, 확장 가능한 요소
254
285
  - `aria-selected` - 탭, 선택 가능한 옵션
255
- - `aria-checked` - 체크박스, 라디오 버튼
286
+ - `aria-checked` - 체크박스, 라디오 버튼, 스위치
256
287
  - `aria-pressed` - 토글 버튼
257
288
  - `aria-hidden` - 동적으로 표시/숨김되는 요소
258
289
  - `aria-disabled` - 동적으로 활성화/비활성화되는 요소
259
290
  - `aria-modal` - 다이얼로그, 모달
260
291
  - `aria-current` - 현재 활성 항목 표시
292
+ - `aria-valuenow`, `aria-valuemin`, `aria-valuemax` - 수치 입력 위젯 (슬라이더, 프로그레스 바 등)
293
+ - `aria-level` - 계층 구조 레벨
294
+ - `aria-posinset`, `aria-setsize` - 세트 내 위치 및 전체 크기
261
295
 
262
296
  ## 지원하는 인터랙티브 역할
263
297
 
@@ -265,7 +299,7 @@ ARIA **상태** 속성이 컴포넌트 상태와 올바르게 연결되어 동
265
299
 
266
300
  - `button`, `tab`, `checkbox`, `radio`, `switch`
267
301
  - `menuitem`, `menuitemcheckbox`, `menuitemradio`
268
- - `option`, `treeitem`
302
+ - `option`, `treeitem`, `slider`, `progressbar`
269
303
  - `dialog`, `alertdialog`
270
304
 
271
305
  ## 예제
@@ -315,13 +349,14 @@ ARIA **상태** 속성이 컴포넌트 상태와 올바르게 연결되어 동
315
349
 
316
350
  - **기반:** ESLint 플러그인 (Node.js 환경)
317
351
  - **파서:** JSX/TSX AST 파싱을 위한 `@babel/eslint-parser` 또는 TypeScript 파서
352
+ - **ARIA 데이터 소스:** 역할-속성 호환성 검증을 위한 `aria-query` 정의 데이터
318
353
  - **분석:** JSX 요소에 대한 Visitor 패턴
319
354
 
320
355
  ### 분석 로직
321
356
 
322
357
  1. **JSXElement 순회:** `role` 및 `aria-*` 속성 식별
323
- 2. **스코프 추적:** `context.getScope()`를 사용하여 ARIA 속성에 바인딩된 변수 추적
324
- 3. **상태 추론:** 값이 `useState` 훅이나 props에서 파생되었는지 판단
358
+ 2. **패턴 분석:** JSX 표현식에서 불리언 유사 바인딩과 정적 값 패턴 감지
359
+ 3. **상태 추론 휴리스틱:** 핸들러/세터 패턴(예: `setX(!x)`)에서 상태 변수 추론
325
360
  4. **패턴 검증:** 올바른 문자열 변환 패턴 확인
326
361
 
327
362
  ## 기대 효과
@@ -332,7 +367,7 @@ ARIA **상태** 속성이 컴포넌트 상태와 올바르게 연결되어 동
332
367
 
333
368
  ### 2. 개발자 생산성
334
369
 
335
- `eslint --fix`로 반복적인 ARIA 상태 바인딩 오류를 자동 수정
370
+ 반복적인 ARIA 상태 바인딩 오류에 대해 타겟 제안 제공
336
371
 
337
372
  ### 3. 표준화
338
373
 
@@ -346,12 +381,6 @@ ARIA **상태** 속성이 컴포넌트 상태와 올바르게 연결되어 동
346
381
  npm test
347
382
  ```
348
383
 
349
- ### 빌드
350
-
351
- ```bash
352
- npm run build # 해당되는 경우
353
- ```
354
-
355
384
  ## 기여
356
385
 
357
386
  기여는 언제나 환영합니다! 이슈나 풀 리퀘스트를 자유롭게 제출해주세요.
package/README.md CHANGED
@@ -40,17 +40,16 @@ npm install --save-dev eslint-plugin-aria-state-validator
40
40
  import ariaStateValidator from 'eslint-plugin-aria-state-validator';
41
41
 
42
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
- }
43
+ ...ariaStateValidator.configs['flat/recommended']
51
44
  ];
52
45
  ```
53
46
 
47
+ Available flat presets:
48
+ - `flat/recommended`
49
+ - `flat/strict`
50
+ - `flat/dynamic-only`
51
+ - `flat/static-only`
52
+
54
53
  ### Legacy ESLintRC Config
55
54
 
56
55
  ```json
@@ -70,13 +69,34 @@ This plugin provides two complementary rules:
70
69
 
71
70
  Validates that ARIA **state** attributes are properly bound to component state and update dynamically.
72
71
 
72
+ Rule options:
73
+ - `interactiveHandlers` (`string[]`): Override which event props are treated as interactive (default: `onClick`, `onKeyDown`, `onKeyPress`, `onChange`, `onToggle`)
74
+ - `strictBooleanInference` (`boolean`): When `true` (default), only flags identifiers inferred as boolean-like; when `false`, treats any identifier binding as boolean-risk
75
+ - `ignorePatterns` (`string[]`): Regex patterns to skip checks for matching attribute names or value source text
76
+
77
+ Example:
78
+
79
+ ```javascript
80
+ {
81
+ rules: {
82
+ 'aria-state-validator/state-dependent-aria-validator': ['warn', {
83
+ interactiveHandlers: ['onClick', 'onPointerDown'],
84
+ strictBooleanInference: true,
85
+ ignorePatterns: ['^aria-current$', '^\\{isOpen\\}$']
86
+ }]
87
+ }
88
+ }
89
+ ```
90
+
73
91
  ### Rule 2: `static-aria-validator` (Static ARIA Validation)
74
92
 
75
93
  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
94
+ - Valid ARIA property names (catches typos with **auto-fix**)
95
+ - Correct values for boolean and enum ARIA attributes (**auto-fix** for "yes"/"no")
96
+ - No conflicting ARIA attributes (**auto-fix** available)
97
+ - No redundant roles on native HTML elements (**auto-fix** available)
98
+ - **Required ARIA attributes** based on roles (e.g., `aria-checked` for `switch`)
99
+ - **Role-attribute compatibility** (e.g., prevents `aria-expanded` on `img`)
80
100
 
81
101
  **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
102
 
@@ -94,7 +114,7 @@ Validates the **correctness** of ARIA attributes when they are used:
94
114
  <button aria-expanded={isOpen ? 'true' : 'false'}>Toggle</button>
95
115
  ```
96
116
 
97
- **Auto-fix Available:** `eslint --fix` will automatically convert to proper string format
117
+ **Suggestion Available:** The rule provides a safe suggestion to convert bindings to string format (`'true'/'false'`).
98
118
 
99
119
  ### 2. Static Value + Interactive Handler Detection
100
120
 
@@ -112,30 +132,39 @@ Validates the **correctness** of ARIA attributes when they are used:
112
132
  </button>
113
133
  ```
114
134
 
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:
135
+ **Smart Suggestion:** When the handler contains state variable references (e.g., `() => setExpanded(!expanded)`), the plugin can suggest binding the inferred variable to the ARIA attribute:
116
136
 
117
137
  ```jsx
118
- // ❌ Before auto-fix
138
+ // ❌ Before suggestion
119
139
  <div onClick={() => setExpanded(!expanded)} aria-expanded="false">
120
140
  Click to expand
121
141
  </div>
122
142
 
123
- // ✅ After auto-fix (automatically detects 'expanded' variable)
143
+ // ✅ Suggested fix (automatically detects 'expanded' variable)
124
144
  <div onClick={() => setExpanded(!expanded)} aria-expanded={expanded ? 'true' : 'false'}>
125
145
  Click to expand
126
146
  </div>
127
147
  ```
128
148
 
149
+ **Supported Patterns for Suggestions:**
150
+ - `() => setX(!x)`
151
+ - `() => setX(true)`
152
+ - `() => setX(prev => !prev)`
153
+
129
154
  ### 3. Role-Based State Association Validation
130
155
 
131
- **Detects:** Interactive roles with static ARIA states
156
+ **Detects:** Interactive roles with static ARIA states or missing required states.
132
157
 
133
158
  ```jsx
134
159
  // ❌ Error: role="button" with static aria-pressed
135
160
  <div role="button" aria-pressed="false">Button</div>
136
161
 
162
+ // ❌ Error: interactive switch role missing aria-checked
163
+ <div role="switch" onClick={toggle}>Toggle</div>
164
+
137
165
  // ✅ Correct: Dynamic state binding
138
166
  <div role="button" aria-pressed={isPressed ? 'true' : 'false'}>Button</div>
167
+ <div role="switch" aria-checked={isChecked ? 'true' : 'false'}>Toggle</div>
139
168
  ```
140
169
 
141
170
  ---
@@ -150,7 +179,7 @@ Validates the **correctness** of ARIA attributes when they are used:
150
179
  // ❌ Error: Typo in aria-label
151
180
  <button aria-labell="Close">X</button>
152
181
 
153
- // ✅ Correct: Proper spelling
182
+ // ✅ Correct (Auto-fixed): Proper spelling
154
183
  <button aria-label="Close">X</button>
155
184
  ```
156
185
 
@@ -171,7 +200,7 @@ Validates the **correctness** of ARIA attributes when they are used:
171
200
  **Detects:** Incorrect boolean-like values
172
201
 
173
202
  ```jsx
174
- // ❌ Error: Invalid boolean value
203
+ // ❌ Error: Invalid boolean value (Auto-fix available for "yes"/"no")
175
204
  <div aria-hidden="yes">Content</div>
176
205
  <div aria-disabled="1">Content</div>
177
206
 
@@ -230,12 +259,15 @@ The plugin validates these dynamic ARIA state attributes:
230
259
 
231
260
  - `aria-expanded` - Toggle buttons, expandable elements
232
261
  - `aria-selected` - Tabs, selectable options
233
- - `aria-checked` - Checkboxes, radio buttons
262
+ - `aria-checked` - Checkboxes, radio buttons, switches
234
263
  - `aria-pressed` - Toggle buttons
235
264
  - `aria-hidden` - Dynamically shown/hidden elements
236
265
  - `aria-disabled` - Dynamically enabled/disabled elements
237
266
  - `aria-modal` - Dialogs, modals
238
267
  - `aria-current` - Current active item indicators
268
+ - `aria-valuenow`, `aria-valuemin`, `aria-valuemax` - Range widgets (sliders, progress bars)
269
+ - `aria-level` - Hierarchical level
270
+ - `aria-posinset`, `aria-setsize` - Set position and size
239
271
 
240
272
  ## Supported Interactive Roles
241
273
 
@@ -243,7 +275,7 @@ Elements with these roles are expected to have dynamic ARIA states:
243
275
 
244
276
  - `button`, `tab`, `checkbox`, `radio`, `switch`
245
277
  - `menuitem`, `menuitemcheckbox`, `menuitemradio`
246
- - `option`, `treeitem`
278
+ - `option`, `treeitem`, `slider`, `progressbar`
247
279
  - `dialog`, `alertdialog`
248
280
 
249
281
  ## Examples
@@ -293,13 +325,14 @@ Elements with these roles are expected to have dynamic ARIA states:
293
325
 
294
326
  - **Base:** ESLint plugin (Node.js environment)
295
327
  - **Parser:** `@babel/eslint-parser` or TypeScript parser for JSX/TSX AST parsing
328
+ - **ARIA Data Source:** `aria-query` role/property definitions for role-attribute compatibility
296
329
  - **Analysis:** Visitor pattern on JSX elements
297
330
 
298
331
  ### Analysis Logic
299
332
 
300
333
  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
334
+ 2. **Pattern Analysis:** Detects boolean-like bindings and static-value patterns in JSX expressions
335
+ 3. **State Inference Heuristics:** Infers likely state variables from handler/setter patterns (e.g., `setX(!x)`)
303
336
  4. **Pattern Validation:** Checks for proper string conversion patterns
304
337
 
305
338
  ## Expected Benefits
@@ -310,7 +343,7 @@ Catch dynamic ARIA errors at build/CI phase instead of runtime
310
343
 
311
344
  ### 2. Developer Productivity
312
345
 
313
- Auto-fix repetitive ARIA state binding errors with `eslint --fix`
346
+ Apply targeted suggestions for repetitive ARIA state binding errors
314
347
 
315
348
  ### 3. Standardization
316
349
 
@@ -324,12 +357,6 @@ Enforce consistent dynamic ARIA usage patterns across projects
324
357
  npm test
325
358
  ```
326
359
 
327
- ### Building
328
-
329
- ```bash
330
- npm run build # If applicable
331
- ```
332
-
333
360
  ## Contributing
334
361
 
335
362
  Contributions welcome! Please feel free to submit issues or pull requests.
package/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const stateDependentAriaValidator = require('./lib/rules/state-dependent-aria-validator');
2
2
  const staticAriaValidator = require('./lib/rules/static-aria-validator');
3
3
 
4
- module.exports = {
4
+ const plugin = {
5
5
  rules: {
6
6
  'state-dependent-aria-validator': stateDependentAriaValidator,
7
7
  'static-aria-validator': staticAriaValidator
@@ -35,3 +35,51 @@ module.exports = {
35
35
  }
36
36
  }
37
37
  };
38
+
39
+ plugin.configs['flat/recommended'] = [
40
+ {
41
+ plugins: {
42
+ 'aria-state-validator': plugin
43
+ },
44
+ rules: {
45
+ 'aria-state-validator/state-dependent-aria-validator': 'warn',
46
+ 'aria-state-validator/static-aria-validator': 'warn'
47
+ }
48
+ }
49
+ ];
50
+
51
+ plugin.configs['flat/strict'] = [
52
+ {
53
+ plugins: {
54
+ 'aria-state-validator': plugin
55
+ },
56
+ rules: {
57
+ 'aria-state-validator/state-dependent-aria-validator': 'error',
58
+ 'aria-state-validator/static-aria-validator': 'error'
59
+ }
60
+ }
61
+ ];
62
+
63
+ plugin.configs['flat/dynamic-only'] = [
64
+ {
65
+ plugins: {
66
+ 'aria-state-validator': plugin
67
+ },
68
+ rules: {
69
+ 'aria-state-validator/state-dependent-aria-validator': 'warn'
70
+ }
71
+ }
72
+ ];
73
+
74
+ plugin.configs['flat/static-only'] = [
75
+ {
76
+ plugins: {
77
+ 'aria-state-validator': plugin
78
+ },
79
+ rules: {
80
+ 'aria-state-validator/static-aria-validator': 'warn'
81
+ }
82
+ }
83
+ ];
84
+
85
+ module.exports = plugin;
@@ -46,6 +46,30 @@ const DYNAMIC_ARIA_ATTRIBUTES = {
46
46
  validValues: ["page", "step", "location", "date", "time", "true", "false"],
47
47
  description: "Current active item indicator",
48
48
  },
49
+ "aria-valuenow": {
50
+ requiresState: true,
51
+ description: "Current value for range widgets",
52
+ },
53
+ "aria-valuemin": {
54
+ requiresState: true,
55
+ description: "Minimum value for range widgets",
56
+ },
57
+ "aria-valuemax": {
58
+ requiresState: true,
59
+ description: "Maximum value for range widgets",
60
+ },
61
+ "aria-level": {
62
+ requiresState: true,
63
+ description: "Hierarchical level of an element",
64
+ },
65
+ "aria-posinset": {
66
+ requiresState: true,
67
+ description: "Position of an element in a set",
68
+ },
69
+ "aria-setsize": {
70
+ requiresState: true,
71
+ description: "Total size of a set",
72
+ },
49
73
  };
50
74
 
51
75
  const INTERACTIVE_ROLES = [
@@ -61,6 +85,36 @@ const INTERACTIVE_ROLES = [
61
85
  "treeitem",
62
86
  "dialog",
63
87
  "alertdialog",
88
+ "slider",
89
+ "progressbar",
90
+ "scrollbar",
91
+ "spinbutton",
92
+ "tree",
93
+ "grid",
94
+ "listbox",
95
+ ];
96
+
97
+ const ROLE_REQUIRED_DYNAMIC_ARIA = {
98
+ switch: "aria-checked",
99
+ checkbox: "aria-checked",
100
+ radio: "aria-checked",
101
+ menuitemcheckbox: "aria-checked",
102
+ menuitemradio: "aria-checked",
103
+ tab: "aria-selected",
104
+ option: "aria-selected",
105
+ treeitem: "aria-selected",
106
+ slider: "aria-valuenow",
107
+ progressbar: "aria-valuenow",
108
+ scrollbar: "aria-valuenow",
109
+ spinbutton: "aria-valuenow",
110
+ };
111
+
112
+ const DEFAULT_INTERACTIVE_HANDLERS = [
113
+ "onClick",
114
+ "onKeyDown",
115
+ "onKeyPress",
116
+ "onChange",
117
+ "onToggle",
64
118
  ];
65
119
 
66
120
  module.exports = {
@@ -71,10 +125,28 @@ module.exports = {
71
125
  "Validate that ARIA state attributes are properly bound to component state",
72
126
  category: "Accessibility",
73
127
  recommended: true,
74
- url: "https://github.com/SangWoo9734/eslint-plugin-aria-state-validator#readme",
128
+ url: "https://github.com/comento/eslint-plugin-aria-state-validator",
75
129
  },
76
- fixable: "code",
77
- schema: [],
130
+ hasSuggestions: true,
131
+ schema: [
132
+ {
133
+ type: "object",
134
+ properties: {
135
+ interactiveHandlers: {
136
+ type: "array",
137
+ items: { type: "string" },
138
+ },
139
+ strictBooleanInference: {
140
+ type: "boolean",
141
+ },
142
+ ignorePatterns: {
143
+ type: "array",
144
+ items: { type: "string" },
145
+ },
146
+ },
147
+ additionalProperties: false,
148
+ },
149
+ ],
78
150
  messages: {
79
151
  booleanBinding:
80
152
  'ARIA attribute "{{ariaAttr}}" should not directly bind Boolean values. Convert to string.',
@@ -84,10 +156,152 @@ module.exports = {
84
156
  'Element with role="{{role}}" should have "{{ariaAttr}}" attribute bound to dynamic state.',
85
157
  invalidStaticValue:
86
158
  'Static value "{{value}}" for "{{ariaAttr}}" should be managed with dynamic state.',
159
+ suggestStringConversion:
160
+ 'Convert "{{ariaAttr}}" binding to explicit string ("true"/"false") conversion.',
161
+ suggestStateBinding:
162
+ 'Replace static "{{ariaAttr}}" value with inferred state binding.',
87
163
  },
88
164
  },
89
165
 
90
166
  create(context) {
167
+ const sourceCode = context.getSourceCode();
168
+ const options = context.options[0] || {};
169
+ const strictBooleanInference = options.strictBooleanInference !== false;
170
+ const interactiveHandlers =
171
+ Array.isArray(options.interactiveHandlers) &&
172
+ options.interactiveHandlers.length > 0
173
+ ? options.interactiveHandlers
174
+ : DEFAULT_INTERACTIVE_HANDLERS;
175
+ const ignoreRegexes = Array.isArray(options.ignorePatterns)
176
+ ? options.ignorePatterns
177
+ .map((pattern) => {
178
+ try {
179
+ return new RegExp(pattern);
180
+ } catch {
181
+ return null;
182
+ }
183
+ })
184
+ .filter(Boolean)
185
+ : [];
186
+
187
+ function matchesIgnorePattern(text) {
188
+ if (!text || ignoreRegexes.length === 0) return false;
189
+ return ignoreRegexes.some((regex) => regex.test(text));
190
+ }
191
+
192
+ function getVariableFromScope(identifierNode) {
193
+ if (!identifierNode || identifierNode.type !== "Identifier") return null;
194
+ if (!sourceCode.getScope) return null;
195
+
196
+ const scope = sourceCode.getScope(identifierNode);
197
+ let currentScope = scope;
198
+
199
+ while (currentScope) {
200
+ if (currentScope.set && currentScope.set.has(identifierNode.name)) {
201
+ return currentScope.set.get(identifierNode.name);
202
+ }
203
+ currentScope = currentScope.upper;
204
+ }
205
+
206
+ return null;
207
+ }
208
+
209
+ function isBooleanishExpression(expr) {
210
+ if (!expr) return false;
211
+
212
+ if (expr.type === "Literal" && typeof expr.value === "boolean") {
213
+ return true;
214
+ }
215
+
216
+ if (
217
+ expr.type === "UnaryExpression" &&
218
+ (expr.operator === "!" || expr.operator === "!!")
219
+ ) {
220
+ return true;
221
+ }
222
+
223
+ if (
224
+ expr.type === "BinaryExpression" &&
225
+ ["==", "===", "!=", "!==", "<", "<=", ">", ">="].includes(expr.operator)
226
+ ) {
227
+ return true;
228
+ }
229
+
230
+ if (expr.type === "LogicalExpression") {
231
+ return true;
232
+ }
233
+
234
+ if (
235
+ expr.type === "CallExpression" &&
236
+ expr.callee.type === "Identifier" &&
237
+ expr.callee.name === "Boolean"
238
+ ) {
239
+ return true;
240
+ }
241
+
242
+ if (expr.type === "ConditionalExpression") {
243
+ return (
244
+ isBooleanishExpression(expr.consequent) &&
245
+ isBooleanishExpression(expr.alternate)
246
+ );
247
+ }
248
+
249
+ return false;
250
+ }
251
+
252
+ function isIdentifierFromBooleanState(variable, identifierName) {
253
+ if (!variable || !Array.isArray(variable.defs)) return false;
254
+
255
+ return variable.defs.some((def) => {
256
+ const declarator = def.node;
257
+ if (!declarator || declarator.type !== "VariableDeclarator") return false;
258
+
259
+ if (
260
+ declarator.id &&
261
+ declarator.id.type === "ArrayPattern" &&
262
+ declarator.init &&
263
+ declarator.init.type === "CallExpression" &&
264
+ declarator.init.callee &&
265
+ declarator.init.callee.type === "Identifier" &&
266
+ declarator.init.callee.name === "useState"
267
+ ) {
268
+ const hasIdentifier = declarator.id.elements.some(
269
+ (element) =>
270
+ element &&
271
+ element.type === "Identifier" &&
272
+ element.name === identifierName
273
+ );
274
+ if (!hasIdentifier) return false;
275
+
276
+ const firstArg = declarator.init.arguments[0];
277
+ return Boolean(
278
+ firstArg &&
279
+ ((firstArg.type === "Literal" &&
280
+ typeof firstArg.value === "boolean") ||
281
+ isBooleanishExpression(firstArg))
282
+ );
283
+ }
284
+
285
+ return Boolean(
286
+ declarator.id &&
287
+ declarator.id.type === "Identifier" &&
288
+ declarator.id.name === identifierName &&
289
+ isBooleanishExpression(declarator.init)
290
+ );
291
+ });
292
+ }
293
+
294
+ function hasBooleanLikeName(name) {
295
+ if (!name) return false;
296
+
297
+ const strictBooleanPatterns = [
298
+ /^(is|has|can|should|did|will|was|were)[A-Z_]/,
299
+ /^(open|opened|expanded|selected|checked|pressed|hidden|disabled|active|visible|loading|mounted)$/i,
300
+ ];
301
+
302
+ return strictBooleanPatterns.some((pattern) => pattern.test(name));
303
+ }
304
+
91
305
  function isStaticValue(node) {
92
306
  if (!node) return false;
93
307
 
@@ -123,7 +337,17 @@ module.exports = {
123
337
  const expression = node.expression;
124
338
 
125
339
  if (expression.type === "Identifier") {
126
- return true;
340
+ if (matchesIgnorePattern(expression.name)) {
341
+ return false;
342
+ }
343
+ if (!strictBooleanInference) {
344
+ return true;
345
+ }
346
+ const variable = getVariableFromScope(expression);
347
+ return (
348
+ hasBooleanLikeName(expression.name) ||
349
+ isIdentifierFromBooleanState(variable, expression.name)
350
+ );
127
351
  }
128
352
 
129
353
  if (expression.type === "LogicalExpression") {
@@ -181,14 +405,6 @@ module.exports = {
181
405
  }
182
406
 
183
407
  function hasInteractiveHandler(attributes) {
184
- const interactiveHandlers = [
185
- "onClick",
186
- "onKeyDown",
187
- "onKeyPress",
188
- "onChange",
189
- "onToggle",
190
- ];
191
-
192
408
  return attributes.some((attr) => {
193
409
  if (attr.type !== "JSXAttribute") return false;
194
410
  const name = attr.name.name;
@@ -224,55 +440,76 @@ module.exports = {
224
440
 
225
441
  const expression = handlerAttr.value.expression;
226
442
 
227
- // Arrow function or function expression
443
+ // Direct call or arrow function/function expression
444
+ let functionBody = null;
228
445
  if (
229
446
  expression.type === "ArrowFunctionExpression" ||
230
447
  expression.type === "FunctionExpression"
231
448
  ) {
232
- const body = expression.body;
233
-
234
- // Find setState calls in the body
235
- let stateVariable = null;
236
-
237
- // Simple case: () => setX(!x) or () => setX(prev => !prev)
238
- if (body.type === "CallExpression") {
239
- // Look for argument that might be state variable
240
- if (body.arguments.length > 0) {
241
- const arg = body.arguments[0];
242
- if (arg.type === "UnaryExpression" && arg.operator === "!") {
243
- if (arg.argument.type === "Identifier") {
244
- stateVariable = arg.argument.name;
245
- }
449
+ functionBody = expression.body;
450
+ }
451
+
452
+ if (!functionBody) return null;
453
+
454
+ // Find setState calls in the body
455
+ let stateVariable = null;
456
+
457
+ const checkCallExpression = (callExpr) => {
458
+ if (callExpr.type !== "CallExpression") return null;
459
+
460
+ // Simple case: setX(!x), setX(true), setX(false)
461
+ if (callExpr.arguments.length > 0) {
462
+ const arg = callExpr.arguments[0];
463
+
464
+ // Pattern: !x
465
+ if (arg.type === "UnaryExpression" && arg.operator === "!") {
466
+ if (arg.argument.type === "Identifier") {
467
+ return arg.argument.name;
468
+ }
469
+ }
470
+
471
+ // Pattern: boolean literal (true/false)
472
+ if (arg.type === "Literal" && typeof arg.value === "boolean") {
473
+ // We can't easily know which state it's setting from just 'true',
474
+ // but we can look at the setter name if it follows 'setX' pattern
475
+ const callee = callExpr.callee;
476
+ if (callee.type === "Identifier" && callee.name.startsWith("set")) {
477
+ const varName = callee.name.slice(3);
478
+ return varName.charAt(0).toLowerCase() + varName.slice(1);
246
479
  }
247
480
  }
248
- }
249
481
 
250
- // Block statement case: () => { setX(!x); }
251
- if (body.type === "BlockStatement" && body.body.length > 0) {
252
- for (const stmt of body.body) {
253
- if (stmt.type === "ExpressionStatement") {
254
- const expr = stmt.expression;
255
- if (expr.type === "CallExpression" && expr.arguments.length > 0) {
256
- const arg = expr.arguments[0];
257
- if (arg.type === "UnaryExpression" && arg.operator === "!") {
258
- if (arg.argument.type === "Identifier") {
259
- stateVariable = arg.argument.name;
260
- break;
261
- }
262
- }
263
- }
482
+ // Pattern: prev => !prev (functional update)
483
+ if (arg.type === "ArrowFunctionExpression" && arg.body.type === "UnaryExpression" && arg.body.operator === "!") {
484
+ const callee = callExpr.callee;
485
+ if (callee.type === "Identifier" && callee.name.startsWith("set")) {
486
+ const varName = callee.name.slice(3);
487
+ return varName.charAt(0).toLowerCase() + varName.slice(1);
264
488
  }
265
489
  }
266
490
  }
491
+ return null;
492
+ };
267
493
 
268
- return stateVariable;
494
+ // Simple case: () => setX(!x)
495
+ if (functionBody.type === "CallExpression") {
496
+ stateVariable = checkCallExpression(functionBody);
269
497
  }
270
498
 
271
- return null;
499
+ // Block statement case: () => { setX(!x); }
500
+ if (functionBody.type === "BlockStatement" && functionBody.body.length > 0) {
501
+ for (const stmt of functionBody.body) {
502
+ if (stmt.type === "ExpressionStatement") {
503
+ stateVariable = checkCallExpression(stmt.expression);
504
+ if (stateVariable) break;
505
+ }
506
+ }
507
+ }
508
+
509
+ return stateVariable;
272
510
  }
273
511
 
274
512
  function createStringConversionFix(fixer, node) {
275
- const sourceCode = context.getSourceCode();
276
513
  const expression = node.expression;
277
514
 
278
515
  let identifierName;
@@ -301,6 +538,31 @@ module.exports = {
301
538
  const attributes = node.attributes || [];
302
539
  const hasHandler = hasInteractiveHandler(attributes);
303
540
  const role = getRoleValue(attributes);
541
+ const presentAriaAttrs = new Set(
542
+ attributes
543
+ .filter(
544
+ (attr) =>
545
+ attr.type === "JSXAttribute" &&
546
+ attr.name &&
547
+ typeof attr.name.name === "string" &&
548
+ attr.name.name.startsWith("aria-")
549
+ )
550
+ .map((attr) => attr.name.name)
551
+ );
552
+
553
+ if (role && hasHandler && ROLE_REQUIRED_DYNAMIC_ARIA[role]) {
554
+ const requiredAria = ROLE_REQUIRED_DYNAMIC_ARIA[role];
555
+ if (!presentAriaAttrs.has(requiredAria)) {
556
+ context.report({
557
+ node: node.name,
558
+ messageId: "missingStateBinding",
559
+ data: {
560
+ role,
561
+ ariaAttr: requiredAria,
562
+ },
563
+ });
564
+ }
565
+ }
304
566
 
305
567
  attributes.forEach((attr) => {
306
568
  if (attr.type !== "JSXAttribute") return;
@@ -308,12 +570,17 @@ module.exports = {
308
570
  const attrName = attr.name.name;
309
571
 
310
572
  if (!DYNAMIC_ARIA_ATTRIBUTES[attrName]) return;
573
+ if (matchesIgnorePattern(attrName)) return;
311
574
 
312
575
  const attrValue = attr.value;
576
+ if (!attrValue) return;
577
+ if (matchesIgnorePattern(sourceCode.getText(attrValue))) return;
313
578
 
314
579
  if (
315
580
  isBooleanIdentifier(attrValue) &&
316
- !hasProperStringConversion(attrValue)
581
+ !hasProperStringConversion(attrValue) &&
582
+ DYNAMIC_ARIA_ATTRIBUTES[attrName].validValues &&
583
+ (DYNAMIC_ARIA_ATTRIBUTES[attrName].validValues.includes("true") || DYNAMIC_ARIA_ATTRIBUTES[attrName].validValues.includes("false"))
317
584
  ) {
318
585
  context.report({
319
586
  node: attr,
@@ -321,9 +588,15 @@ module.exports = {
321
588
  data: {
322
589
  ariaAttr: attrName,
323
590
  },
324
- fix(fixer) {
325
- return createStringConversionFix(fixer, attrValue);
326
- },
591
+ suggest: [
592
+ {
593
+ messageId: "suggestStringConversion",
594
+ data: { ariaAttr: attrName },
595
+ fix(fixer) {
596
+ return createStringConversionFix(fixer, attrValue);
597
+ },
598
+ },
599
+ ],
327
600
  });
328
601
  return;
329
602
  }
@@ -332,7 +605,7 @@ module.exports = {
332
605
  const handlers = attributes.filter(
333
606
  (a) =>
334
607
  a.type === "JSXAttribute" &&
335
- ["onClick", "onKeyDown", "onChange"].includes(a.name.name)
608
+ interactiveHandlers.includes(a.name.name)
336
609
  );
337
610
 
338
611
  const handlerNames = handlers.map((a) => a.name.name);
@@ -343,6 +616,9 @@ module.exports = {
343
616
  stateVariable = extractStateVariableFromHandler(handler);
344
617
  if (stateVariable) break;
345
618
  }
619
+ if (matchesIgnorePattern(stateVariable)) {
620
+ stateVariable = null;
621
+ }
346
622
 
347
623
  context.report({
348
624
  node: attr,
@@ -351,13 +627,19 @@ module.exports = {
351
627
  ariaAttr: attrName,
352
628
  handler: handlerNames.join(", "),
353
629
  },
354
- fix: stateVariable
355
- ? (fixer) => {
356
- return fixer.replaceText(
357
- attrValue,
358
- `{${stateVariable} ? 'true' : 'false'}`
359
- );
360
- }
630
+ suggest: stateVariable
631
+ ? [
632
+ {
633
+ messageId: "suggestStateBinding",
634
+ data: { ariaAttr: attrName },
635
+ fix(fixer) {
636
+ return fixer.replaceText(
637
+ attrValue,
638
+ `{${stateVariable} ? 'true' : 'false'}`
639
+ );
640
+ },
641
+ },
642
+ ]
361
643
  : undefined,
362
644
  });
363
645
  return;
@@ -5,6 +5,8 @@
5
5
 
6
6
  "use strict";
7
7
 
8
+ const { roles: ARIA_ROLES } = require("aria-query");
9
+
8
10
  // Valid ARIA properties according to ARIA 1.2 spec
9
11
  const VALID_ARIA_PROPS = new Set([
10
12
  // Widget attributes
@@ -76,6 +78,56 @@ const REQUIRES_VALUE = new Set([
76
78
  "aria-flowto",
77
79
  ]);
78
80
 
81
+ // Common ARIA typos and their correct versions
82
+ const ARIA_TYPOS = {
83
+ "aria-labell": "aria-label",
84
+ "aria-hidde": "aria-hidden",
85
+ "aria-expect": "aria-expanded",
86
+ "aria-describeb": "aria-describedby",
87
+ "aria-control": "aria-controls",
88
+ "aria-check": "aria-checked",
89
+ };
90
+
91
+ // Fallback role definitions when aria-query data is unavailable for a role.
92
+ const ROLE_DEFINITIONS = {
93
+ checkbox: {
94
+ required: ["aria-checked"],
95
+ allowed: ["aria-readonly", "aria-disabled"],
96
+ },
97
+ radio: {
98
+ required: ["aria-checked"],
99
+ allowed: ["aria-disabled"],
100
+ },
101
+ combobox: {
102
+ required: ["aria-controls", "aria-expanded"],
103
+ allowed: ["aria-haspopup", "aria-autocomplete", "aria-required"],
104
+ },
105
+ tab: {
106
+ required: ["aria-selected"],
107
+ allowed: ["aria-disabled"],
108
+ },
109
+ slider: {
110
+ required: ["aria-valuenow"],
111
+ allowed: ["aria-valuemin", "aria-valuemax", "aria-orientation", "aria-readonly"],
112
+ },
113
+ switch: {
114
+ required: ["aria-checked"],
115
+ allowed: ["aria-readonly", "aria-disabled"],
116
+ },
117
+ scrollbar: {
118
+ required: ["aria-valuenow", "aria-valuemin", "aria-valuemax", "aria-orientation"],
119
+ allowed: ["aria-controls"],
120
+ },
121
+ heading: {
122
+ required: ["aria-level"],
123
+ allowed: [],
124
+ },
125
+ img: {
126
+ required: [],
127
+ allowed: ["aria-label", "aria-labelledby"],
128
+ },
129
+ };
130
+
79
131
  // Valid values for boolean-like ARIA attributes
80
132
  const BOOLEAN_ARIA_VALUES = new Set(["true", "false"]);
81
133
 
@@ -128,9 +180,9 @@ module.exports = {
128
180
  description: "Validate static ARIA attributes for correctness",
129
181
  category: "Accessibility",
130
182
  recommended: true,
131
- url: "https://github.com/SangWoo9734/eslint-plugin-aria-state-validator#readme",
183
+ url: "https://github.com/comento/eslint-plugin-aria-state-validator",
132
184
  },
133
- fixable: null,
185
+ fixable: "code",
134
186
  schema: [],
135
187
  messages: {
136
188
  invalidAriaProp:
@@ -145,10 +197,17 @@ module.exports = {
145
197
  "Do not use both aria-label and aria-labelledby. aria-labelledby takes precedence.",
146
198
  redundantRole:
147
199
  'Redundant role "{{role}}" on <{{element}}>. Native HTML semantics already convey this role.',
200
+ missingRequiredAria:
201
+ 'Role "{{role}}" requires the following ARIA attributes: {{missing}}.',
202
+ disallowedAriaForRole:
203
+ 'ARIA attribute "{{prop}}" is not recommended for role "{{role}}".',
204
+ ariaTypo:
205
+ 'Invalid ARIA attribute "{{prop}}". Did you mean "{{correct}}"?',
148
206
  },
149
207
  },
150
208
 
151
209
  create(context) {
210
+ const sourceCode = context.getSourceCode();
152
211
  // Elements with implicit roles
153
212
  const IMPLICIT_ROLES = {
154
213
  a: "link", // when href present
@@ -170,7 +229,7 @@ module.exports = {
170
229
  };
171
230
 
172
231
  function getAttributeValue(attr) {
173
- if (!attr.value) return null;
232
+ if (!attr || !attr.value) return null;
174
233
 
175
234
  if (attr.value.type === "Literal") {
176
235
  return attr.value.value;
@@ -235,17 +294,72 @@ module.exports = {
235
294
  role: roleValue,
236
295
  element: elementName,
237
296
  },
297
+ fix(fixer) {
298
+ const [start, end] = roleAttr.range;
299
+ const removeStart =
300
+ start > 0 && sourceCode.text[start - 1] === " " ? start - 1 : start;
301
+ return fixer.removeRange([removeStart, end]);
302
+ },
238
303
  });
239
304
  }
240
305
  }
241
306
 
307
+ function getRoleSpec(roleValue) {
308
+ if (!roleValue) return null;
309
+
310
+ const ariaRole = ARIA_ROLES.get(roleValue);
311
+ if (ariaRole) {
312
+ return {
313
+ required: Object.keys(ariaRole.requiredProps || {}),
314
+ allowedSet: new Set(Object.keys(ariaRole.props || {})),
315
+ };
316
+ }
317
+
318
+ if (ROLE_DEFINITIONS[roleValue]) {
319
+ const fallback = ROLE_DEFINITIONS[roleValue];
320
+ return {
321
+ required: fallback.required,
322
+ allowedSet: new Set([...fallback.required, ...fallback.allowed]),
323
+ };
324
+ }
325
+
326
+ return null;
327
+ }
328
+
242
329
  return {
243
330
  JSXOpeningElement(node) {
244
331
  const attributes = node.attributes || [];
332
+ const elementName = node.name.name;
245
333
 
246
334
  // Check for redundant roles
247
335
  checkRedundantRole(node);
248
336
 
337
+ const roleAttr = attributes.find(
338
+ (attr) => attr.type === "JSXAttribute" && attr.name.name === "role"
339
+ );
340
+ const roleValue = getAttributeValue(roleAttr);
341
+ const roleSpec = getRoleSpec(roleValue);
342
+
343
+ // Required ARIA attributes for role
344
+ if (roleValue && roleSpec) {
345
+ const required = roleSpec.required;
346
+ const present = attributes
347
+ .filter((attr) => attr.type === "JSXAttribute" && attr.name.name.startsWith("aria-"))
348
+ .map((attr) => attr.name.name);
349
+
350
+ const missing = required.filter((req) => !present.includes(req));
351
+ if (missing.length > 0) {
352
+ context.report({
353
+ node: roleAttr || node.name,
354
+ messageId: "missingRequiredAria",
355
+ data: {
356
+ role: roleValue,
357
+ missing: missing.join(", "),
358
+ },
359
+ });
360
+ }
361
+ }
362
+
249
363
  // Check for conflicting aria-label and aria-labelledby
250
364
  const hasAriaLabel = attributes.some(
251
365
  (attr) =>
@@ -264,6 +378,9 @@ module.exports = {
264
378
  context.report({
265
379
  node: ariaLabelAttr,
266
380
  messageId: "ariaLabelWithLabelledBy",
381
+ fix(fixer) {
382
+ return fixer.remove(ariaLabelAttr);
383
+ }
267
384
  });
268
385
  }
269
386
 
@@ -273,6 +390,22 @@ module.exports = {
273
390
 
274
391
  const attrName = attr.name.name;
275
392
 
393
+ // Check for typos in aria- attributes
394
+ if (attrName.startsWith("aria-") && !VALID_ARIA_PROPS.has(attrName)) {
395
+ const correct = ARIA_TYPOS[attrName];
396
+ if (correct) {
397
+ context.report({
398
+ node: attr.name,
399
+ messageId: "ariaTypo",
400
+ data: { prop: attrName, correct: correct },
401
+ fix(fixer) {
402
+ return fixer.replaceText(attr.name, correct);
403
+ }
404
+ });
405
+ return;
406
+ }
407
+ }
408
+
276
409
  // Only check aria-* attributes
277
410
  if (!attrName.startsWith("aria-")) return;
278
411
 
@@ -286,6 +419,16 @@ module.exports = {
286
419
  return;
287
420
  }
288
421
 
422
+ // Check role-attribute compatibility from aria-query role props.
423
+ if (roleValue && roleSpec && roleSpec.allowedSet.size > 0 && !roleSpec.allowedSet.has(attrName)) {
424
+ context.report({
425
+ node: attr,
426
+ messageId: "disallowedAriaForRole",
427
+ data: { prop: attrName, role: roleValue },
428
+ });
429
+ return;
430
+ }
431
+
289
432
  const value = getAttributeValue(attr);
290
433
 
291
434
  // Can't validate dynamic values
@@ -331,6 +474,11 @@ module.exports = {
331
474
  node: attr,
332
475
  messageId: "invalidBooleanValue",
333
476
  data: { prop: attrName, value: String(value) },
477
+ fix(fixer) {
478
+ if (value === "yes" || value === "1") return fixer.replaceText(attr.value, '"true"');
479
+ if (value === "no" || value === "0") return fixer.replaceText(attr.value, '"false"');
480
+ return null;
481
+ }
334
482
  });
335
483
  return;
336
484
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-aria-state-validator",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "description": "Comprehensive ESLint plugin for ARIA accessibility validation: dynamic state binding and static ARIA attributes correctness",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -35,12 +35,12 @@
35
35
  "license": "MIT",
36
36
  "repository": {
37
37
  "type": "git",
38
- "url": "https://github.com/SangWoo9734/eslint-plugin-aria-state-validator.git"
38
+ "url": "https://github.com/comento/eslint-plugin-aria-state-validator.git"
39
39
  },
40
40
  "bugs": {
41
- "url": "https://github.com/SangWoo9734/eslint-plugin-aria-state-validator/issues"
41
+ "url": "https://github.com/comento/eslint-plugin-aria-state-validator/issues"
42
42
  },
43
- "homepage": "https://github.com/SangWoo9734/eslint-plugin-aria-state-validator#readme",
43
+ "homepage": "https://github.com/comento/eslint-plugin-aria-state-validator#readme",
44
44
  "engines": {
45
45
  "node": ">=14.0.0"
46
46
  },
@@ -50,8 +50,12 @@
50
50
  "devDependencies": {
51
51
  "@babel/eslint-parser": "^7.28.5",
52
52
  "@babel/preset-react": "^7.28.5",
53
+ "@typescript-eslint/parser": "^8.57.2",
53
54
  "eslint": "^9.39.1",
54
55
  "eslint-plugin-react": "^7.37.5",
55
56
  "jest": "^30.2.0"
57
+ },
58
+ "dependencies": {
59
+ "aria-query": "^5.3.2"
56
60
  }
57
61
  }