@flint.fyi/jsx 0.4.0 → 0.5.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.
Files changed (96) hide show
  1. package/dist/index.mjs +3235 -0
  2. package/package.json +8 -7
  3. package/lib/index.d.ts +0 -2
  4. package/lib/index.js +0 -2
  5. package/lib/plugin.js +0 -104
  6. package/lib/rules/accessKeys.d.ts +0 -9
  7. package/lib/rules/accessKeys.js +0 -30
  8. package/lib/rules/altTexts.d.ts +0 -9
  9. package/lib/rules/altTexts.js +0 -76
  10. package/lib/rules/anchorAmbiguousText.d.ts +0 -9
  11. package/lib/rules/anchorAmbiguousText.js +0 -55
  12. package/lib/rules/anchorContent.d.ts +0 -9
  13. package/lib/rules/anchorContent.js +0 -55
  14. package/lib/rules/anchorValidity.d.ts +0 -9
  15. package/lib/rules/anchorValidity.js +0 -77
  16. package/lib/rules/ariaActiveDescendantTabIndex.d.ts +0 -9
  17. package/lib/rules/ariaActiveDescendantTabIndex.js +0 -51
  18. package/lib/rules/ariaHiddenFocusables.d.ts +0 -9
  19. package/lib/rules/ariaHiddenFocusables.js +0 -66
  20. package/lib/rules/ariaPropTypes.d.ts +0 -9
  21. package/lib/rules/ariaPropTypes.js +0 -222
  22. package/lib/rules/ariaProps.d.ts +0 -9
  23. package/lib/rules/ariaProps.js +0 -90
  24. package/lib/rules/ariaRoleValidity.d.ts +0 -9
  25. package/lib/rules/ariaRoleValidity.js +0 -111
  26. package/lib/rules/ariaUnsupportedElements.d.ts +0 -9
  27. package/lib/rules/ariaUnsupportedElements.js +0 -86
  28. package/lib/rules/autoFocusProps.d.ts +0 -9
  29. package/lib/rules/autoFocusProps.js +0 -42
  30. package/lib/rules/autocomplete.d.ts +0 -9
  31. package/lib/rules/autocomplete.js +0 -120
  32. package/lib/rules/booleanValues.d.ts +0 -9
  33. package/lib/rules/booleanValues.js +0 -31
  34. package/lib/rules/bracedStatements.d.ts +0 -9
  35. package/lib/rules/bracedStatements.js +0 -31
  36. package/lib/rules/buttonTypes.d.ts +0 -9
  37. package/lib/rules/buttonTypes.js +0 -68
  38. package/lib/rules/childrenProps.d.ts +0 -9
  39. package/lib/rules/childrenProps.js +0 -37
  40. package/lib/rules/clickEventKeyEvents.d.ts +0 -9
  41. package/lib/rules/clickEventKeyEvents.js +0 -54
  42. package/lib/rules/commentTextNodes.d.ts +0 -9
  43. package/lib/rules/commentTextNodes.js +0 -30
  44. package/lib/rules/distractingElements.d.ts +0 -9
  45. package/lib/rules/distractingElements.js +0 -38
  46. package/lib/rules/elementChildrenValidity.d.ts +0 -9
  47. package/lib/rules/elementChildrenValidity.js +0 -52
  48. package/lib/rules/headingContents.d.ts +0 -9
  49. package/lib/rules/headingContents.js +0 -52
  50. package/lib/rules/htmlLangs.d.ts +0 -9
  51. package/lib/rules/htmlLangs.js +0 -30
  52. package/lib/rules/iframeTitles.d.ts +0 -9
  53. package/lib/rules/iframeTitles.js +0 -61
  54. package/lib/rules/interactiveElementRoles.d.ts +0 -9
  55. package/lib/rules/interactiveElementRoles.js +0 -91
  56. package/lib/rules/interactiveElementsFocusable.d.ts +0 -9
  57. package/lib/rules/interactiveElementsFocusable.js +0 -170
  58. package/lib/rules/labelAssociatedControls.d.ts +0 -9
  59. package/lib/rules/labelAssociatedControls.js +0 -68
  60. package/lib/rules/langValidity.d.ts +0 -9
  61. package/lib/rules/langValidity.js +0 -41
  62. package/lib/rules/mediaCaptions.d.ts +0 -9
  63. package/lib/rules/mediaCaptions.js +0 -50
  64. package/lib/rules/mouseEventKeyEvents.d.ts +0 -9
  65. package/lib/rules/mouseEventKeyEvents.js +0 -47
  66. package/lib/rules/nonInteractiveElementInteractions.d.ts +0 -9
  67. package/lib/rules/nonInteractiveElementInteractions.js +0 -93
  68. package/lib/rules/nonInteractiveElementRoles.d.ts +0 -9
  69. package/lib/rules/nonInteractiveElementRoles.js +0 -116
  70. package/lib/rules/nonInteractiveElementTabIndexes.d.ts +0 -9
  71. package/lib/rules/nonInteractiveElementTabIndexes.js +0 -107
  72. package/lib/rules/propDuplicates.d.ts +0 -9
  73. package/lib/rules/propDuplicates.js +0 -37
  74. package/lib/rules/roleRedundancies.d.ts +0 -9
  75. package/lib/rules/roleRedundancies.js +0 -74
  76. package/lib/rules/roleRequiredAriaProps.d.ts +0 -9
  77. package/lib/rules/roleRequiredAriaProps.js +0 -76
  78. package/lib/rules/roleSupportedAriaProps.d.ts +0 -9
  79. package/lib/rules/roleSupportedAriaProps.js +0 -417
  80. package/lib/rules/roleTags.d.ts +0 -9
  81. package/lib/rules/roleTags.js +0 -70
  82. package/lib/rules/ruleCreator.d.ts +0 -2
  83. package/lib/rules/ruleCreator.js +0 -13
  84. package/lib/rules/scopeProps.d.ts +0 -9
  85. package/lib/rules/scopeProps.js +0 -39
  86. package/lib/rules/staticElementInteractions.d.ts +0 -9
  87. package/lib/rules/staticElementInteractions.js +0 -58
  88. package/lib/rules/svgTitles.d.ts +0 -9
  89. package/lib/rules/svgTitles.js +0 -57
  90. package/lib/rules/tabIndexPositiveValues.d.ts +0 -9
  91. package/lib/rules/tabIndexPositiveValues.js +0 -43
  92. package/lib/rules/unescapedEntities.d.ts +0 -9
  93. package/lib/rules/unescapedEntities.js +0 -68
  94. package/lib/rules/unnecessaryFragments.d.ts +0 -9
  95. package/lib/rules/unnecessaryFragments.js +0 -49
  96. /package/{lib/plugin.d.ts → dist/index.d.mts} +0 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,3235 @@
1
+ import { RuleCreator, createPlugin } from "@flint.fyi/core";
2
+ import { SyntaxKind } from "typescript";
3
+ import { getStaticStringValue, getTSNodeRange, typescriptLanguage } from "@flint.fyi/typescript-language";
4
+ import { nullThrows } from "@flint.fyi/utils";
5
+ import languageTags from "language-tags";
6
+ //#region src/rules/ruleCreator.ts
7
+ const ruleCreator = new RuleCreator({
8
+ docs: (ruleId) => `https://flint.fyi/rules/jsx/${ruleId.toLowerCase()}`,
9
+ pluginId: "jsx",
10
+ presets: [
11
+ "logical",
12
+ "logicalStrict",
13
+ "stylistic"
14
+ ]
15
+ });
16
+ //#endregion
17
+ //#region src/rules/accessKeys.ts
18
+ var accessKeys_default = ruleCreator.createRule(typescriptLanguage, {
19
+ about: {
20
+ description: "Disallow the use of the `accessKey` / `accesskey` attribute on JSX elements.",
21
+ id: "accessKeys",
22
+ presets: ["logical", "logicalStrict"]
23
+ },
24
+ messages: { avoidAccessKey: {
25
+ primary: "The native DOM `{{ attribute }}` prop causes accessibility issues with keyboard-only users and screen readers.",
26
+ secondary: ["Access keys are inconsistent across browsers and can interfere with assistive technologies.", "Although they may work in limited use cases, it's best to avoid them."],
27
+ suggestions: ["Remove the attribute and provide a documented, configurable keyboard shortcut instead."]
28
+ } },
29
+ setup(context) {
30
+ return { visitors: { JsxAttribute(node, { sourceFile }) {
31
+ if (node.name.kind !== SyntaxKind.Identifier) return;
32
+ const attribute = node.name.text;
33
+ if (attribute.toLowerCase() !== "accesskey") return;
34
+ context.report({
35
+ data: { attribute },
36
+ message: "avoidAccessKey",
37
+ range: getTSNodeRange(node.name, sourceFile)
38
+ });
39
+ } } };
40
+ }
41
+ });
42
+ //#endregion
43
+ //#region src/rules/altTexts.ts
44
+ const alternateProperties = /* @__PURE__ */ new Set([
45
+ "aria-label",
46
+ "aria-labelledby",
47
+ "title"
48
+ ]);
49
+ var altTexts_default = ruleCreator.createRule(typescriptLanguage, {
50
+ about: {
51
+ description: "Reports elements that require alt text but are missing it.",
52
+ id: "altTexts",
53
+ presets: ["logical", "logicalStrict"]
54
+ },
55
+ messages: { missingAlt: {
56
+ primary: "{{ element }} element is missing alt text for non-visual users.",
57
+ secondary: [
58
+ "Alternative text provides a textual description for images and other media.",
59
+ "Screen readers use this text to describe the element to users who cannot see it.",
60
+ "This is required for WCAG 1.1.1 compliance."
61
+ ],
62
+ suggestions: [
63
+ "Add an alt attribute with descriptive text",
64
+ "Use alt=\"\" for decorative images",
65
+ "Use aria-label or aria-labelledby for alternative labeling"
66
+ ]
67
+ } },
68
+ setup(context) {
69
+ function checkNode(node, { sourceFile }) {
70
+ const { attributes, tagName } = node;
71
+ if (tagName.kind !== SyntaxKind.Identifier) return;
72
+ const elementName = tagName.text.toLowerCase();
73
+ if (elementName === "img" || elementName === "area") checkAltAttribute(attributes, tagName, elementName, sourceFile);
74
+ else if (elementName === "input") checkInputElement(attributes, tagName, sourceFile);
75
+ else if (elementName === "object") checkObjectAccessibility(attributes, tagName, sourceFile);
76
+ }
77
+ function checkAltAttribute(attributes, tagName, elementName, sourceFile) {
78
+ const properties = attributes.properties.find((attr) => attr.kind === SyntaxKind.JsxAttribute && attr.name.kind === SyntaxKind.Identifier && attr.name.text === "alt");
79
+ if (attributes.properties.some((attr) => attr.kind === SyntaxKind.JsxAttribute && attr.name.kind === SyntaxKind.Identifier && (attr.name.text === "aria-label" || attr.name.text === "aria-labelledby") && attr.initializer)) return;
80
+ if (!properties) {
81
+ context.report({
82
+ data: { element: elementName },
83
+ message: "missingAlt",
84
+ range: getTSNodeRange(tagName, sourceFile)
85
+ });
86
+ return;
87
+ }
88
+ if (properties.kind === SyntaxKind.JsxAttribute && properties.initializer?.kind === SyntaxKind.JsxExpression) {
89
+ const { expression } = properties.initializer;
90
+ if (expression?.kind === SyntaxKind.Identifier && expression.text === "undefined") context.report({
91
+ data: { element: elementName },
92
+ message: "missingAlt",
93
+ range: getTSNodeRange(tagName, sourceFile)
94
+ });
95
+ }
96
+ }
97
+ function checkInputElement(attributes, tagName, sourceFile) {
98
+ const typeAttribute = attributes.properties.find((properties) => properties.kind === SyntaxKind.JsxAttribute && properties.name.kind === SyntaxKind.Identifier && properties.name.text === "type");
99
+ if (typeAttribute?.kind === SyntaxKind.JsxAttribute && typeAttribute.initializer?.kind === SyntaxKind.StringLiteral && typeAttribute.initializer.text === "image") checkAltAttribute(attributes, tagName, "input[type='image']", sourceFile);
100
+ }
101
+ function checkObjectAccessibility(attributes, tagName, sourceFile) {
102
+ if (!attributes.properties.some((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && alternateProperties.has(property.name.text) && property.initializer)) context.report({
103
+ data: { element: "object" },
104
+ message: "missingAlt",
105
+ range: getTSNodeRange(tagName, sourceFile)
106
+ });
107
+ }
108
+ return { visitors: {
109
+ JsxOpeningElement: checkNode,
110
+ JsxSelfClosingElement: checkNode
111
+ } };
112
+ }
113
+ });
114
+ //#endregion
115
+ //#region src/rules/anchorAmbiguousText.ts
116
+ const ambiguousWords = /* @__PURE__ */ new Set([
117
+ "a link",
118
+ "click here",
119
+ "here",
120
+ "learn more",
121
+ "link",
122
+ "more",
123
+ "read more"
124
+ ]);
125
+ var anchorAmbiguousText_default = ruleCreator.createRule(typescriptLanguage, {
126
+ about: {
127
+ description: "Reports anchor elements with ambiguous text that doesn't describe the link destination.",
128
+ id: "anchorAmbiguousText",
129
+ presets: ["logical", "logicalStrict"]
130
+ },
131
+ messages: { ambiguousText: {
132
+ primary: "This anchor element has ambiguous text that doesn't describe the link destination.",
133
+ secondary: [
134
+ "Ambiguous text like '{{ text }}' doesn't provide context about where the link goes.",
135
+ "Screen reader users often navigate by links and need descriptive text to understand the purpose.",
136
+ "Provide descriptive text that explains what the link does or where it leads."
137
+ ],
138
+ suggestions: [
139
+ "Replace vague text with descriptive text that explains the link destination",
140
+ "Include the page or section name the link leads to",
141
+ "Describe the action that will occur when clicking the link"
142
+ ]
143
+ } },
144
+ setup(context) {
145
+ function getTextContent(node) {
146
+ let text = "";
147
+ for (const child of node.children) if (child.kind === SyntaxKind.JsxText) text += child.text;
148
+ else if (child.kind === SyntaxKind.JsxElement) text += getTextContent(child);
149
+ else if (child.kind === SyntaxKind.JsxExpression && child.expression?.kind === SyntaxKind.StringLiteral) text += child.expression.text;
150
+ return text;
151
+ }
152
+ return { visitors: { JsxElement(node, { sourceFile }) {
153
+ if (node.openingElement.tagName.kind !== SyntaxKind.Identifier || node.openingElement.tagName.text !== "a") return;
154
+ const textContent = getTextContent(node);
155
+ if (!ambiguousWords.has(textContent.toLowerCase().trim())) return;
156
+ const textNodes = node.children.filter((child) => child.kind === SyntaxKind.JsxText && child.text.trim());
157
+ context.report({
158
+ data: { text: textContent.trim() },
159
+ message: "ambiguousText",
160
+ range: getTSNodeRange(textNodes[0] ?? node.openingElement, sourceFile)
161
+ });
162
+ } } };
163
+ }
164
+ });
165
+ //#endregion
166
+ //#region src/rules/anchorContent.ts
167
+ var anchorContent_default = ruleCreator.createRule(typescriptLanguage, {
168
+ about: {
169
+ description: "Reports anchor elements without accessible content.",
170
+ id: "anchorContent",
171
+ presets: ["logical", "logicalStrict"]
172
+ },
173
+ messages: { missingContent: {
174
+ primary: "This anchor element is missing accessible content.",
175
+ secondary: [
176
+ "Non-visual tools such as screen readers and search engine crawlers need content to describe links.",
177
+ "Provide text content, aria-label, aria-labelledby, or title attribute.",
178
+ "This is required for WCAG 2.4.4 and 4.1.2 compliance."
179
+ ],
180
+ suggestions: [
181
+ "Add text content inside the anchor",
182
+ "Add an aria-label attribute",
183
+ "Add a title attribute"
184
+ ]
185
+ } },
186
+ setup(context) {
187
+ function hasAccessibleContent(element) {
188
+ return element.attributes.properties.some((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && (property.name.text === "aria-label" || property.name.text === "aria-labelledby" || property.name.text === "title") && property.initializer);
189
+ }
190
+ function hasTextContent(element) {
191
+ return element.children.some((child) => {
192
+ if (child.kind === SyntaxKind.JsxText && child.text.trim()) return true;
193
+ if (child.kind === SyntaxKind.JsxElement || child.kind === SyntaxKind.JsxSelfClosingElement) {
194
+ if (!(child.kind === SyntaxKind.JsxElement ? child.openingElement : child).attributes.properties.some((attr) => attr.kind === SyntaxKind.JsxAttribute && attr.name.kind === SyntaxKind.Identifier && attr.name.text === "aria-hidden")) return true;
195
+ }
196
+ if (child.kind === SyntaxKind.JsxExpression && child.expression) return true;
197
+ });
198
+ }
199
+ return { visitors: {
200
+ JsxElement(node, { sourceFile }) {
201
+ const openingElement = node.openingElement;
202
+ if (openingElement.tagName.kind === SyntaxKind.Identifier && openingElement.tagName.text === "a" && !hasAccessibleContent(openingElement) && !hasTextContent(node)) context.report({
203
+ message: "missingContent",
204
+ range: getTSNodeRange(openingElement, sourceFile)
205
+ });
206
+ },
207
+ JsxSelfClosingElement(node, { sourceFile }) {
208
+ if (node.tagName.kind === SyntaxKind.Identifier && node.tagName.text === "a" && !hasAccessibleContent(node)) context.report({
209
+ message: "missingContent",
210
+ range: getTSNodeRange(node, sourceFile)
211
+ });
212
+ }
213
+ } };
214
+ }
215
+ });
216
+ //#endregion
217
+ //#region src/rules/anchorValidity.ts
218
+ var anchorValidity_default = ruleCreator.createRule(typescriptLanguage, {
219
+ about: {
220
+ description: "Reports invalid usage of anchor elements.",
221
+ id: "anchorValidity",
222
+ presets: ["logical", "logicalStrict"]
223
+ },
224
+ messages: {
225
+ invalidHref: {
226
+ primary: "Anchor has an invalid href value '{{ href }}'.",
227
+ secondary: [
228
+ "Anchors should navigate to valid URLs or page sections.",
229
+ "Use a button element for click handlers without navigation.",
230
+ "This is required for WCAG 2.4.4 compliance."
231
+ ],
232
+ suggestions: ["Use a valid URL or fragment identifier", "Use a <button> element instead"]
233
+ },
234
+ missingHref: {
235
+ primary: "Anchor element is missing an href attribute.",
236
+ secondary: [
237
+ "Anchors without href are not keyboard accessible.",
238
+ "Use a button element for actions without navigation.",
239
+ "This is required for WCAG 4.1.2 compliance."
240
+ ],
241
+ suggestions: ["Add a valid href attribute", "Use a <button> element instead"]
242
+ },
243
+ shouldBeButton: {
244
+ primary: "Anchor with onClick handler should be a button.",
245
+ secondary: [
246
+ "Interactive elements without valid href should use button elements.",
247
+ "Buttons provide better semantics and keyboard accessibility.",
248
+ "This is required for WCAG 4.1.2 compliance."
249
+ ],
250
+ suggestions: ["Replace <a> with <button>"]
251
+ }
252
+ },
253
+ setup(context) {
254
+ function getHrefValue(attributes) {
255
+ const hrefProperty = attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "href");
256
+ if (hrefProperty?.kind !== SyntaxKind.JsxAttribute) return;
257
+ if (hrefProperty.initializer?.kind === SyntaxKind.StringLiteral) return hrefProperty.initializer.text;
258
+ return "";
259
+ }
260
+ function hasOnClick(attributes) {
261
+ return attributes.properties.some((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "onClick");
262
+ }
263
+ function isInvalidHref(href) {
264
+ return href === "#" || href.startsWith("javascript:");
265
+ }
266
+ function checkAnchor(node, { sourceFile }) {
267
+ if (node.tagName.kind !== SyntaxKind.Identifier || node.tagName.text !== "a") return;
268
+ const href = getHrefValue(node.attributes);
269
+ const hasClick = hasOnClick(node.attributes);
270
+ if (href === void 0) {
271
+ context.report({
272
+ message: hasClick ? "shouldBeButton" : "missingHref",
273
+ range: getTSNodeRange(node, sourceFile)
274
+ });
275
+ return;
276
+ }
277
+ if (typeof href === "string" && isInvalidHref(href)) context.report({
278
+ data: { href },
279
+ message: hasClick ? "shouldBeButton" : "invalidHref",
280
+ range: getTSNodeRange(node, sourceFile)
281
+ });
282
+ }
283
+ return { visitors: {
284
+ JsxOpeningElement: checkAnchor,
285
+ JsxSelfClosingElement: checkAnchor
286
+ } };
287
+ }
288
+ });
289
+ //#endregion
290
+ //#region src/rules/ariaActiveDescendantTabIndex.ts
291
+ const inherentlyTabbableElements = /* @__PURE__ */ new Set([
292
+ "a",
293
+ "area",
294
+ "button",
295
+ "input",
296
+ "select",
297
+ "textarea"
298
+ ]);
299
+ var ariaActiveDescendantTabIndex_default = ruleCreator.createRule(typescriptLanguage, {
300
+ about: {
301
+ description: "Reports elements with aria-activedescendant without tabIndex.",
302
+ id: "ariaActiveDescendantTabIndex",
303
+ presets: ["logical", "logicalStrict"]
304
+ },
305
+ messages: { missingTabIndex: {
306
+ primary: "This element with `aria-activedescendant` is missing a `tabIndex` attribute to manage focus state.",
307
+ secondary: [
308
+ "aria-activedescendant is used to manage focus within a composite widget.",
309
+ "The element must be tabbable, either with an inherent tabIndex or explicit tabIndex attribute.",
310
+ "Without it, keyboard users cannot reach the element."
311
+ ],
312
+ suggestions: ["Add tabIndex=\"0\" to make the element tabbable", "Add tabIndex=\"-1\" to make it programmatically focusable"]
313
+ } },
314
+ setup(context) {
315
+ function checkElement({ attributes, tagName }, { sourceFile }) {
316
+ if (tagName.kind === SyntaxKind.Identifier) {
317
+ const firstCharacter = tagName.text.charAt(0);
318
+ if (firstCharacter === firstCharacter.toUpperCase() && firstCharacter !== firstCharacter.toLowerCase()) return;
319
+ }
320
+ if (!attributes.properties.some((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "aria-activedescendant" && property.initializer)) return;
321
+ if (tagName.kind === SyntaxKind.Identifier && inherentlyTabbableElements.has(tagName.text.toLowerCase())) return;
322
+ if (!attributes.properties.some((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text.toLowerCase() === "tabindex")) {
323
+ const ariaProperty = attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "aria-activedescendant");
324
+ if (ariaProperty?.kind === SyntaxKind.JsxAttribute) context.report({
325
+ message: "missingTabIndex",
326
+ range: getTSNodeRange(ariaProperty, sourceFile)
327
+ });
328
+ }
329
+ }
330
+ return { visitors: {
331
+ JsxOpeningElement: checkElement,
332
+ JsxSelfClosingElement: checkElement
333
+ } };
334
+ }
335
+ });
336
+ //#endregion
337
+ //#region src/rules/ariaHiddenFocusables.ts
338
+ const focusableElements = /* @__PURE__ */ new Set([
339
+ "a",
340
+ "audio",
341
+ "button",
342
+ "input",
343
+ "select",
344
+ "textarea",
345
+ "video"
346
+ ]);
347
+ var ariaHiddenFocusables_default = ruleCreator.createRule(typescriptLanguage, {
348
+ about: {
349
+ description: "Reports elements with aria-hidden='true' that are focusable.",
350
+ id: "ariaHiddenFocusables",
351
+ presets: ["logical", "logicalStrict"]
352
+ },
353
+ messages: { ariaHiddenFocusable: {
354
+ primary: "This element has `aria-hidden=\"true\"` but is focusable, which is misleading to users navigating with keyboards.",
355
+ secondary: ["Elements with aria-hidden='true' should not be reachable via keyboard navigation.", "This creates confusion when users can focus elements they cannot perceive with a screen reader."],
356
+ suggestions: [
357
+ "Remove aria-hidden=\"true\"",
358
+ "Add tabIndex=\"-1\" to remove from focus order",
359
+ "Use a non-focusable element"
360
+ ]
361
+ } },
362
+ setup(context) {
363
+ function checkElement(node, { sourceFile }) {
364
+ const { attributes, tagName } = node;
365
+ if (tagName.kind !== SyntaxKind.Identifier) return;
366
+ const ariaHiddenProperty = attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text.toLowerCase() === "aria-hidden");
367
+ if (ariaHiddenProperty?.kind !== SyntaxKind.JsxAttribute || !isAriaHiddenTrue(ariaHiddenProperty)) return;
368
+ const tabIndexValue = findTabIndexValue(node);
369
+ if (tabIndexValue === -1) return;
370
+ if (focusableElements.has(tagName.text.toLowerCase()) || tabIndexValue !== void 0 && tabIndexValue >= 0) context.report({
371
+ message: "ariaHiddenFocusable",
372
+ range: getTSNodeRange(ariaHiddenProperty, sourceFile)
373
+ });
374
+ }
375
+ return { visitors: {
376
+ JsxOpeningElement: checkElement,
377
+ JsxSelfClosingElement: checkElement
378
+ } };
379
+ }
380
+ });
381
+ function findTabIndexValue(node) {
382
+ const tabIndexProperty = node.attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text.toLowerCase() === "tabindex");
383
+ if (!tabIndexProperty?.initializer) return;
384
+ if (tabIndexProperty.initializer.kind === SyntaxKind.JsxExpression) {
385
+ const expression = tabIndexProperty.initializer.expression;
386
+ if (expression?.kind === SyntaxKind.NumericLiteral) return Number(expression.text);
387
+ }
388
+ if (tabIndexProperty.initializer.kind === SyntaxKind.StringLiteral) return Number(tabIndexProperty.initializer.text);
389
+ }
390
+ function isAriaHiddenTrue(ariaHiddenProperty) {
391
+ if (!ariaHiddenProperty.initializer) return false;
392
+ if (ariaHiddenProperty.initializer.kind === SyntaxKind.StringLiteral) return ariaHiddenProperty.initializer.text === "true";
393
+ if (ariaHiddenProperty.initializer.kind === SyntaxKind.JsxExpression) {
394
+ if (ariaHiddenProperty.initializer.expression?.kind === SyntaxKind.TrueKeyword) return true;
395
+ }
396
+ return false;
397
+ }
398
+ //#endregion
399
+ //#region src/rules/ariaProps.ts
400
+ const validAriaProps = /* @__PURE__ */ new Set([
401
+ "aria-activedescendant",
402
+ "aria-atomic",
403
+ "aria-autocomplete",
404
+ "aria-busy",
405
+ "aria-checked",
406
+ "aria-colcount",
407
+ "aria-colindex",
408
+ "aria-colspan",
409
+ "aria-controls",
410
+ "aria-current",
411
+ "aria-describedby",
412
+ "aria-details",
413
+ "aria-disabled",
414
+ "aria-dropeffect",
415
+ "aria-errormessage",
416
+ "aria-expanded",
417
+ "aria-flowto",
418
+ "aria-grabbed",
419
+ "aria-haspopup",
420
+ "aria-hidden",
421
+ "aria-invalid",
422
+ "aria-keyshortcuts",
423
+ "aria-label",
424
+ "aria-labelledby",
425
+ "aria-level",
426
+ "aria-live",
427
+ "aria-modal",
428
+ "aria-multiline",
429
+ "aria-multiselectable",
430
+ "aria-orientation",
431
+ "aria-owns",
432
+ "aria-placeholder",
433
+ "aria-posinset",
434
+ "aria-pressed",
435
+ "aria-readonly",
436
+ "aria-relevant",
437
+ "aria-required",
438
+ "aria-roledescription",
439
+ "aria-rowcount",
440
+ "aria-rowindex",
441
+ "aria-rowspan",
442
+ "aria-selected",
443
+ "aria-setsize",
444
+ "aria-sort",
445
+ "aria-valuemax",
446
+ "aria-valuemin",
447
+ "aria-valuenow",
448
+ "aria-valuetext"
449
+ ]);
450
+ var ariaProps_default = ruleCreator.createRule(typescriptLanguage, {
451
+ about: {
452
+ description: "Reports invalid ARIA properties.",
453
+ id: "ariaProps",
454
+ presets: ["logical", "logicalStrict"]
455
+ },
456
+ messages: { invalidAriaProp: {
457
+ primary: "`{{ prop }}` is not a valid ARIA property and so has no effect on the browser.",
458
+ secondary: [
459
+ "This aria-* attribute is not a valid ARIA property according to the WAI-ARIA spec.",
460
+ "Check the spelling of the property name.",
461
+ "This is required for WCAG 4.1.2 compliance."
462
+ ],
463
+ suggestions: ["Use a valid property from the WAI-ARIA specification."]
464
+ } },
465
+ setup(context) {
466
+ function checkElement(node, { sourceFile }) {
467
+ for (const property of node.attributes.properties) {
468
+ if (property.kind !== SyntaxKind.JsxAttribute || property.name.kind !== SyntaxKind.Identifier) continue;
469
+ const propertyName = property.name.text;
470
+ if (!propertyName.startsWith("aria-")) continue;
471
+ if (!validAriaProps.has(propertyName.toLowerCase())) context.report({
472
+ data: { prop: propertyName },
473
+ message: "invalidAriaProp",
474
+ range: getTSNodeRange(property.name, sourceFile)
475
+ });
476
+ }
477
+ }
478
+ return { visitors: {
479
+ JsxOpeningElement: checkElement,
480
+ JsxSelfClosingElement: checkElement
481
+ } };
482
+ }
483
+ });
484
+ //#endregion
485
+ //#region src/rules/ariaPropTypes.ts
486
+ const ariaPropertyTypes = {
487
+ "aria-activedescendant": "id",
488
+ "aria-atomic": "boolean",
489
+ "aria-autocomplete": "token",
490
+ "aria-busy": "boolean",
491
+ "aria-checked": "tristate",
492
+ "aria-colcount": "integer",
493
+ "aria-colindex": "integer",
494
+ "aria-colspan": "integer",
495
+ "aria-controls": "idlist",
496
+ "aria-current": "token",
497
+ "aria-describedby": "idlist",
498
+ "aria-details": "id",
499
+ "aria-disabled": "boolean",
500
+ "aria-dropeffect": "token",
501
+ "aria-errormessage": "id",
502
+ "aria-expanded": "tristate",
503
+ "aria-flowto": "idlist",
504
+ "aria-grabbed": "tristate",
505
+ "aria-haspopup": "token",
506
+ "aria-hidden": "tristate",
507
+ "aria-invalid": "token",
508
+ "aria-keyshortcuts": "string",
509
+ "aria-label": "string",
510
+ "aria-labelledby": "idlist",
511
+ "aria-level": "integer",
512
+ "aria-live": "token",
513
+ "aria-modal": "boolean",
514
+ "aria-multiline": "boolean",
515
+ "aria-multiselectable": "boolean",
516
+ "aria-orientation": "token",
517
+ "aria-owns": "idlist",
518
+ "aria-placeholder": "string",
519
+ "aria-posinset": "integer",
520
+ "aria-pressed": "tristate",
521
+ "aria-readonly": "boolean",
522
+ "aria-relevant": "token",
523
+ "aria-required": "boolean",
524
+ "aria-roledescription": "string",
525
+ "aria-rowcount": "integer",
526
+ "aria-rowindex": "integer",
527
+ "aria-rowspan": "integer",
528
+ "aria-selected": "tristate",
529
+ "aria-setsize": "integer",
530
+ "aria-sort": "token",
531
+ "aria-valuemax": "number",
532
+ "aria-valuemin": "number",
533
+ "aria-valuenow": "number",
534
+ "aria-valuetext": "string"
535
+ };
536
+ const tokenValues = {
537
+ "aria-autocomplete": /* @__PURE__ */ new Set([
538
+ "both",
539
+ "inline",
540
+ "list",
541
+ "none"
542
+ ]),
543
+ "aria-current": /* @__PURE__ */ new Set([
544
+ "date",
545
+ "false",
546
+ "location",
547
+ "page",
548
+ "step",
549
+ "time",
550
+ "true"
551
+ ]),
552
+ "aria-dropeffect": /* @__PURE__ */ new Set([
553
+ "copy",
554
+ "execute",
555
+ "link",
556
+ "move",
557
+ "none",
558
+ "popup"
559
+ ]),
560
+ "aria-haspopup": /* @__PURE__ */ new Set([
561
+ "dialog",
562
+ "false",
563
+ "grid",
564
+ "listbox",
565
+ "menu",
566
+ "tree",
567
+ "true"
568
+ ]),
569
+ "aria-invalid": /* @__PURE__ */ new Set([
570
+ "false",
571
+ "grammar",
572
+ "spelling",
573
+ "true"
574
+ ]),
575
+ "aria-live": /* @__PURE__ */ new Set([
576
+ "assertive",
577
+ "off",
578
+ "polite"
579
+ ]),
580
+ "aria-orientation": /* @__PURE__ */ new Set([
581
+ "horizontal",
582
+ "undefined",
583
+ "vertical"
584
+ ]),
585
+ "aria-relevant": /* @__PURE__ */ new Set([
586
+ "additions",
587
+ "additions text",
588
+ "all",
589
+ "removals",
590
+ "text"
591
+ ]),
592
+ "aria-sort": /* @__PURE__ */ new Set([
593
+ "ascending",
594
+ "descending",
595
+ "none",
596
+ "other"
597
+ ])
598
+ };
599
+ function getAttributeValue(property) {
600
+ if (!property.initializer) return true;
601
+ if (property.initializer.kind === SyntaxKind.StringLiteral) return property.initializer.text;
602
+ if (property.initializer.kind === SyntaxKind.JsxExpression) {
603
+ const expression = property.initializer.expression;
604
+ if (!expression) return;
605
+ if (expression.kind === SyntaxKind.TrueKeyword) return true;
606
+ if (expression.kind === SyntaxKind.FalseKeyword) return false;
607
+ if (expression.kind === SyntaxKind.NumericLiteral) return Number(expression.text);
608
+ if (expression.kind === SyntaxKind.StringLiteral) return expression.text;
609
+ }
610
+ }
611
+ function getExpectedValueDescription(propertyName, expectedType) {
612
+ switch (expectedType) {
613
+ case "boolean": return "true or false";
614
+ case "id": return "an ID reference";
615
+ case "idlist": return "a space-separated list of IDs";
616
+ case "integer": return "an integer";
617
+ case "number": return "a number";
618
+ case "string": return "a string";
619
+ case "token": {
620
+ const validTokens = tokenValues[propertyName];
621
+ if (validTokens !== void 0) {
622
+ const tokens = Array.from(validTokens).sort();
623
+ return tokens.length > 4 ? `one of: ${tokens.slice(0, 4).join(", ")}, ...` : `one of: ${tokens.join(", ")}`;
624
+ }
625
+ return "a valid token value";
626
+ }
627
+ case "tristate": return "true, false, or mixed";
628
+ default: return "a valid value";
629
+ }
630
+ }
631
+ function isValidPropertyValue(propertyName, value, expectedType) {
632
+ if (value === void 0) return true;
633
+ switch (expectedType) {
634
+ case "boolean": return value === "true" || value === "false" || typeof value === "boolean";
635
+ case "id": return typeof value === "string" && !!value.length;
636
+ case "idlist": return typeof value === "string" && !!value.trim().length;
637
+ case "integer":
638
+ if (typeof value === "number") return Number.isInteger(value);
639
+ if (typeof value === "string") {
640
+ const num = Number(value);
641
+ return !Number.isNaN(num) && Number.isInteger(num);
642
+ }
643
+ return false;
644
+ case "number":
645
+ if (typeof value === "number") return !Number.isNaN(value);
646
+ if (typeof value === "string") {
647
+ const num = Number(value);
648
+ return !Number.isNaN(num);
649
+ }
650
+ return false;
651
+ case "string": return typeof value === "string";
652
+ case "token":
653
+ if (typeof value !== "string") return false;
654
+ return tokenValues[propertyName]?.has(value.toLowerCase()) ?? true;
655
+ case "tristate": return value === "true" || value === "false" || value === "mixed" || typeof value === "boolean";
656
+ default: return true;
657
+ }
658
+ }
659
+ var ariaPropTypes_default = ruleCreator.createRule(typescriptLanguage, {
660
+ about: {
661
+ description: "Reports ARIA properties with invalid value types.",
662
+ id: "ariaPropTypes",
663
+ presets: ["logical", "logicalStrict"]
664
+ },
665
+ messages: { invalidPropType: {
666
+ primary: "`{{ prop }}` should have a value of {{ expected }}, but received `{{ actual }}`.",
667
+ secondary: [
668
+ "ARIA properties must have values that match their expected type according to the WAI-ARIA specification.",
669
+ "Invalid property values can confuse assistive technologies and lead to accessibility issues.",
670
+ "This is required for WCAG 4.1.2 compliance."
671
+ ],
672
+ suggestions: ["Provide a value that matches the expected type for this ARIA property."]
673
+ } },
674
+ setup(context) {
675
+ function checkElement(node, { sourceFile }) {
676
+ for (const property of node.attributes.properties) {
677
+ if (property.kind !== SyntaxKind.JsxAttribute || property.name.kind !== SyntaxKind.Identifier) continue;
678
+ const propertyName = property.name.text.toLowerCase();
679
+ if (!propertyName.startsWith("aria-")) continue;
680
+ const expectedType = ariaPropertyTypes[propertyName];
681
+ if (expectedType === void 0) continue;
682
+ const value = getAttributeValue(property);
683
+ if (isValidPropertyValue(propertyName, value, expectedType)) return;
684
+ const expected = getExpectedValueDescription(propertyName, expectedType);
685
+ context.report({
686
+ data: {
687
+ actual: String(value),
688
+ expected,
689
+ prop: propertyName
690
+ },
691
+ message: "invalidPropType",
692
+ range: property.initializer ? getTSNodeRange(property.initializer, sourceFile) : getTSNodeRange(property.name, sourceFile)
693
+ });
694
+ }
695
+ }
696
+ return { visitors: {
697
+ JsxOpeningElement: checkElement,
698
+ JsxSelfClosingElement: checkElement
699
+ } };
700
+ }
701
+ });
702
+ //#endregion
703
+ //#region src/rules/ariaRoleValidity.ts
704
+ const validAriaRoles = /* @__PURE__ */ new Set([
705
+ "alert",
706
+ "alertdialog",
707
+ "application",
708
+ "article",
709
+ "banner",
710
+ "button",
711
+ "cell",
712
+ "checkbox",
713
+ "columnheader",
714
+ "combobox",
715
+ "complementary",
716
+ "contentinfo",
717
+ "definition",
718
+ "dialog",
719
+ "directory",
720
+ "document",
721
+ "feed",
722
+ "figure",
723
+ "form",
724
+ "grid",
725
+ "gridcell",
726
+ "group",
727
+ "heading",
728
+ "img",
729
+ "link",
730
+ "list",
731
+ "listbox",
732
+ "listitem",
733
+ "log",
734
+ "main",
735
+ "marquee",
736
+ "math",
737
+ "menu",
738
+ "menubar",
739
+ "menuitem",
740
+ "menuitemcheckbox",
741
+ "menuitemradio",
742
+ "meter",
743
+ "navigation",
744
+ "none",
745
+ "note",
746
+ "option",
747
+ "presentation",
748
+ "progressbar",
749
+ "radio",
750
+ "radiogroup",
751
+ "region",
752
+ "row",
753
+ "rowgroup",
754
+ "rowheader",
755
+ "scrollbar",
756
+ "search",
757
+ "searchbox",
758
+ "separator",
759
+ "slider",
760
+ "spinbutton",
761
+ "status",
762
+ "switch",
763
+ "tab",
764
+ "table",
765
+ "tablist",
766
+ "tabpanel",
767
+ "term",
768
+ "textbox",
769
+ "timer",
770
+ "toolbar",
771
+ "tooltip",
772
+ "tree",
773
+ "treegrid",
774
+ "treeitem"
775
+ ]);
776
+ var ariaRoleValidity_default = ruleCreator.createRule(typescriptLanguage, {
777
+ about: {
778
+ description: "Reports invalid or abstract ARIA roles.",
779
+ id: "ariaRoleValidity",
780
+ presets: ["logical", "logicalStrict"]
781
+ },
782
+ messages: { invalidRole: {
783
+ primary: "Invalid ARIA role '{{ role }}'. Use a valid, non-abstract role.",
784
+ secondary: [
785
+ "ARIA roles must be valid according to the WAI-ARIA specification.",
786
+ "Abstract roles cannot be used directly in HTML.",
787
+ "Required for WCAG 4.1.2 compliance."
788
+ ],
789
+ suggestions: ["Use a valid ARIA role from the WAI-ARIA specification", "Remove the role attribute if not needed"]
790
+ } },
791
+ setup(context) {
792
+ function checkRole(node, { sourceFile }) {
793
+ if (node.tagName.kind !== SyntaxKind.Identifier || node.tagName.text.toLowerCase() !== node.tagName.text) return;
794
+ const roleProperty = node.attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier);
795
+ if (roleProperty?.initializer?.kind !== SyntaxKind.StringLiteral) return;
796
+ const role = roleProperty.initializer.text.trim();
797
+ if (!role || !validAriaRoles.has(role)) context.report({
798
+ data: { role: role || "(empty)" },
799
+ message: "invalidRole",
800
+ range: getTSNodeRange(roleProperty, sourceFile)
801
+ });
802
+ }
803
+ return { visitors: {
804
+ JsxOpeningElement: checkRole,
805
+ JsxSelfClosingElement: checkRole
806
+ } };
807
+ }
808
+ });
809
+ //#endregion
810
+ //#region src/rules/ariaUnsupportedElements.ts
811
+ const unsupportedElements = /* @__PURE__ */ new Set([
812
+ "base",
813
+ "body",
814
+ "caption",
815
+ "col",
816
+ "colgroup",
817
+ "datalist",
818
+ "dd",
819
+ "details",
820
+ "dialog",
821
+ "dt",
822
+ "fieldset",
823
+ "figcaption",
824
+ "figure",
825
+ "footer",
826
+ "form",
827
+ "frame",
828
+ "frameset",
829
+ "head",
830
+ "header",
831
+ "hgroup",
832
+ "html",
833
+ "iframe",
834
+ "legend",
835
+ "link",
836
+ "main",
837
+ "meta",
838
+ "meter",
839
+ "nav",
840
+ "noscript",
841
+ "optgroup",
842
+ "option",
843
+ "output",
844
+ "param",
845
+ "picture",
846
+ "progress",
847
+ "script",
848
+ "slot",
849
+ "style",
850
+ "template",
851
+ "textarea",
852
+ "title",
853
+ "track"
854
+ ]);
855
+ var ariaUnsupportedElements_default = ruleCreator.createRule(typescriptLanguage, {
856
+ about: {
857
+ description: "Reports ARIA attributes on elements that don't support them.",
858
+ id: "ariaUnsupportedElements",
859
+ presets: ["logical", "logicalStrict"]
860
+ },
861
+ messages: { unsupportedElement: {
862
+ primary: "The `{{ element }}` element does not support ARIA roles, states, or properties.",
863
+ secondary: [
864
+ "Certain reserved DOM elements do not support ARIA attributes.",
865
+ "These elements are often not visible or have specific semantic meaning.",
866
+ "This is required for WCAG 4.1.2 compliance."
867
+ ],
868
+ suggestions: ["Remove ARIA attributes from this element"]
869
+ } },
870
+ setup(context) {
871
+ function checkElement(node, { sourceFile }) {
872
+ if (node.tagName.kind !== SyntaxKind.Identifier) return;
873
+ const elementName = node.tagName.text.toLowerCase();
874
+ if (!unsupportedElements.has(elementName)) return;
875
+ if (node.attributes.properties.some((property) => {
876
+ if (property.kind !== SyntaxKind.JsxAttribute || property.name.kind !== SyntaxKind.Identifier) return false;
877
+ const attributeName = property.name.text.toLowerCase();
878
+ return attributeName === "role" || attributeName.startsWith("aria-");
879
+ })) context.report({
880
+ data: { element: elementName },
881
+ message: "unsupportedElement",
882
+ range: getTSNodeRange(node.tagName, sourceFile)
883
+ });
884
+ }
885
+ return { visitors: {
886
+ JsxOpeningElement: checkElement,
887
+ JsxSelfClosingElement: checkElement
888
+ } };
889
+ }
890
+ });
891
+ //#endregion
892
+ //#region src/rules/autocomplete.ts
893
+ const validAutocompleteValues = /* @__PURE__ */ new Set([
894
+ "address-level1",
895
+ "address-level2",
896
+ "address-level3",
897
+ "address-level4",
898
+ "address-line1",
899
+ "address-line2",
900
+ "address-line3",
901
+ "bday",
902
+ "bday-day",
903
+ "bday-month",
904
+ "bday-year",
905
+ "cc-additional-name",
906
+ "cc-csc",
907
+ "cc-exp",
908
+ "cc-exp-month",
909
+ "cc-exp-year",
910
+ "cc-family-name",
911
+ "cc-given-name",
912
+ "cc-name",
913
+ "cc-number",
914
+ "cc-type",
915
+ "country",
916
+ "country-name",
917
+ "current-password",
918
+ "email",
919
+ "impp",
920
+ "language",
921
+ "name",
922
+ "new-password",
923
+ "off",
924
+ "on",
925
+ "one-time-code",
926
+ "organization",
927
+ "organization-title",
928
+ "photo",
929
+ "postal-code",
930
+ "sex",
931
+ "street-address",
932
+ "tel",
933
+ "tel-area-code",
934
+ "tel-country-code",
935
+ "tel-extension",
936
+ "tel-local",
937
+ "tel-national",
938
+ "transaction-amount",
939
+ "transaction-currency",
940
+ "url",
941
+ "username",
942
+ "webauthn"
943
+ ]);
944
+ const billingAndShippingValues = /* @__PURE__ */ new Set([
945
+ "address-level1",
946
+ "address-level2",
947
+ "address-level3",
948
+ "address-level4",
949
+ "address-line1",
950
+ "address-line2",
951
+ "address-line3",
952
+ "country",
953
+ "country-name",
954
+ "postal-code",
955
+ "street-address"
956
+ ]);
957
+ function isValidAutocompleteValue(value) {
958
+ const parts = value.trim().split(/\s+/);
959
+ if (parts.length === 1) return validAutocompleteValues.has(nullThrows(parts[0], "First part is expected to be present by prior length check"));
960
+ if (parts.length === 2) {
961
+ const [prefix, token] = parts;
962
+ if (prefix === "billing" || prefix === "shipping") return billingAndShippingValues.has(nullThrows(token, "Second part is expected to be present by prior length check"));
963
+ }
964
+ return false;
965
+ }
966
+ var autocomplete_default = ruleCreator.createRule(typescriptLanguage, {
967
+ about: {
968
+ description: "Ensure the autocomplete attribute is correct and suitable for the form field.",
969
+ id: "autocomplete",
970
+ presets: ["logical", "logicalStrict"]
971
+ },
972
+ messages: { invalid: {
973
+ primary: "`{{ value }}` is not a valid value for autocomplete.",
974
+ secondary: [
975
+ "The autocomplete attribute must use valid tokens from the HTML specification.",
976
+ "Valid values help browsers and assistive technologies provide better user experiences.",
977
+ "This is required for WCAG 1.3.5 compliance."
978
+ ],
979
+ suggestions: [
980
+ "Use standard autocomplete tokens like 'email', 'name', 'off', or 'tel'",
981
+ "For address fields, use 'billing' or 'shipping' prefix with appropriate field tokens",
982
+ "Check the HTML specification for the complete list of valid autocomplete tokens"
983
+ ]
984
+ } },
985
+ setup(context) {
986
+ function checkNode(node, { sourceFile }) {
987
+ const { attributes, tagName } = node;
988
+ if (tagName.kind !== SyntaxKind.Identifier) return;
989
+ if (tagName.text.toLowerCase() !== "input") return;
990
+ const autocomplete = attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text.toLowerCase() === "autocomplete");
991
+ if (autocomplete?.kind !== SyntaxKind.JsxAttribute || !autocomplete.initializer) return;
992
+ const value = getStaticStringValue(autocomplete.initializer);
993
+ if (value === void 0 || isValidAutocompleteValue(value)) return;
994
+ context.report({
995
+ data: { value },
996
+ message: "invalid",
997
+ range: getTSNodeRange(autocomplete.name, sourceFile)
998
+ });
999
+ }
1000
+ return { visitors: {
1001
+ JsxOpeningElement: checkNode,
1002
+ JsxSelfClosingElement: checkNode
1003
+ } };
1004
+ }
1005
+ });
1006
+ //#endregion
1007
+ //#region src/rules/autoFocusProps.ts
1008
+ var autoFocusProps_default = ruleCreator.createRule(typescriptLanguage, {
1009
+ about: {
1010
+ description: "Reports autoFocus props that are not set to false.",
1011
+ id: "autoFocusProps",
1012
+ presets: ["logical", "logicalStrict"]
1013
+ },
1014
+ messages: { noAutoFocus: {
1015
+ primary: "The `autoFocus` prop disruptively forces unintuitive focus behavior.",
1016
+ secondary: [
1017
+ "Auto-focusing elements can cause usability issues for sighted and non-sighted users.",
1018
+ "It can be disruptive to users who rely on screen readers or keyboard navigation.",
1019
+ "Consider letting users focus elements manually instead."
1020
+ ],
1021
+ suggestions: ["Remove the autoFocus prop", "Set autoFocus to false: autoFocus={false}"]
1022
+ } },
1023
+ setup(context) {
1024
+ function isSetToFalse(property) {
1025
+ if (!property.initializer) return false;
1026
+ if (property.initializer.kind === SyntaxKind.StringLiteral) return true;
1027
+ if (property.initializer.kind === SyntaxKind.JsxExpression) {
1028
+ if (property.initializer.expression?.kind === SyntaxKind.FalseKeyword) return true;
1029
+ }
1030
+ return false;
1031
+ }
1032
+ function checkElement(node, { sourceFile }) {
1033
+ for (const property of node.attributes.properties) if (property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text.toLowerCase() === "autofocus" && !isSetToFalse(property)) context.report({
1034
+ message: "noAutoFocus",
1035
+ range: getTSNodeRange(property, sourceFile)
1036
+ });
1037
+ }
1038
+ return { visitors: {
1039
+ JsxOpeningElement: checkElement,
1040
+ JsxSelfClosingElement: checkElement
1041
+ } };
1042
+ }
1043
+ });
1044
+ //#endregion
1045
+ //#region src/rules/booleanValues.ts
1046
+ var booleanValues_default = ruleCreator.createRule(typescriptLanguage, {
1047
+ about: {
1048
+ description: "Prefer shorthand boolean attributes over explicit {true} values in JSX.",
1049
+ id: "booleanValues",
1050
+ presets: ["stylistic"]
1051
+ },
1052
+ messages: { preferShorthand: {
1053
+ primary: "Prefer shorthand boolean attribute `{{ name }}` over explicit `{{ name }}={true}`.",
1054
+ secondary: ["Boolean attributes with explicit `{true}` values are redundant and verbose.", "The shorthand syntax is more concise and idiomatic in JSX."],
1055
+ suggestions: ["Use the shorthand syntax: `{{ name }}`"]
1056
+ } },
1057
+ setup(context) {
1058
+ function checkElement(node, { sourceFile }) {
1059
+ for (const property of node.attributes.properties) if (property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.initializer?.kind === SyntaxKind.JsxExpression && property.initializer.expression?.kind === SyntaxKind.TrueKeyword) context.report({
1060
+ data: { name: property.name.text },
1061
+ message: "preferShorthand",
1062
+ range: getTSNodeRange(property, sourceFile)
1063
+ });
1064
+ }
1065
+ return { visitors: {
1066
+ JsxOpeningElement: checkElement,
1067
+ JsxSelfClosingElement: checkElement
1068
+ } };
1069
+ }
1070
+ });
1071
+ //#endregion
1072
+ //#region src/rules/bracedStatements.ts
1073
+ var bracedStatements_default = ruleCreator.createRule(typescriptLanguage, {
1074
+ about: {
1075
+ description: "Disallow unnecessary JSX curly braces around literals and JSX elements.",
1076
+ id: "bracedStatements",
1077
+ presets: ["stylistic"]
1078
+ },
1079
+ messages: { unnecessaryBraces: {
1080
+ primary: "Curly braces are unnecessary around {{ type }}.",
1081
+ secondary: ["Curly braces are unnecessary when they wrap simple literals or JSX elements.", "Removing them improves readability and reduces visual clutter."],
1082
+ suggestions: ["Remove the curly braces and use the content directly."]
1083
+ } },
1084
+ setup(context) {
1085
+ return { visitors: { JsxExpression(node, { sourceFile }) {
1086
+ if (!node.expression || node.parent.kind !== SyntaxKind.JsxElement && node.parent.kind !== SyntaxKind.JsxFragment) return;
1087
+ let unnecessaryType;
1088
+ if (node.expression.kind === SyntaxKind.StringLiteral) unnecessaryType = "string literals";
1089
+ else if (node.expression.kind === SyntaxKind.JsxElement || node.expression.kind === SyntaxKind.JsxSelfClosingElement || node.expression.kind === SyntaxKind.JsxFragment) unnecessaryType = "JSX elements";
1090
+ if (unnecessaryType) context.report({
1091
+ data: { type: unnecessaryType },
1092
+ message: "unnecessaryBraces",
1093
+ range: getTSNodeRange(node, sourceFile)
1094
+ });
1095
+ } } };
1096
+ }
1097
+ });
1098
+ //#endregion
1099
+ //#region src/rules/buttonTypes.ts
1100
+ const validButtonTypes = /* @__PURE__ */ new Set([
1101
+ "button",
1102
+ "reset",
1103
+ "submit"
1104
+ ]);
1105
+ var buttonTypes_default = ruleCreator.createRule(typescriptLanguage, {
1106
+ about: {
1107
+ description: "Reports button elements without an explicit type attribute.",
1108
+ id: "buttonTypes",
1109
+ presets: ["logical", "logicalStrict"]
1110
+ },
1111
+ messages: {
1112
+ invalidType: {
1113
+ primary: "Button type '{{ type }}' is invalid. Use 'button', 'submit', or 'reset'.",
1114
+ secondary: ["The type attribute must be one of: 'button', 'submit', or 'reset'.", "Invalid types will not behave as expected in all browsers."],
1115
+ suggestions: [
1116
+ "Change to type='button' for general action buttons",
1117
+ "Change to type='submit' for form submission buttons",
1118
+ "Change to type='reset' for form reset buttons"
1119
+ ]
1120
+ },
1121
+ missingType: {
1122
+ primary: "It is generally preferable to add an explicit `type` attribute to buttons.",
1123
+ secondary: [
1124
+ "Buttons without an explicit type default to 'submit', which can cause unintended form submissions.",
1125
+ "Valid button types are 'button', 'submit', and 'reset'.",
1126
+ "Specify an explicit type to make the button's behavior clear."
1127
+ ],
1128
+ suggestions: [
1129
+ "Add type='button' for general action buttons",
1130
+ "Add type='submit' for form submission buttons",
1131
+ "Add type='reset' for form reset buttons"
1132
+ ]
1133
+ }
1134
+ },
1135
+ setup(context) {
1136
+ function checkElement(node, { sourceFile }) {
1137
+ if (node.tagName.kind !== SyntaxKind.Identifier) return;
1138
+ if (node.tagName.text.toLowerCase() !== "button") return;
1139
+ const typeAttribute = node.attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text.toLowerCase() === "type");
1140
+ if (!typeAttribute) {
1141
+ context.report({
1142
+ message: "missingType",
1143
+ range: getTSNodeRange(node.tagName, sourceFile)
1144
+ });
1145
+ return;
1146
+ }
1147
+ const typeInitializer = typeAttribute.initializer;
1148
+ const typeExpression = typeInitializer?.kind === SyntaxKind.JsxExpression ? typeInitializer.expression : typeInitializer;
1149
+ const typeValue = typeExpression && getStaticStringValue(typeExpression);
1150
+ if (typeValue && !validButtonTypes.has(typeValue)) context.report({
1151
+ data: { type: typeValue },
1152
+ message: "invalidType",
1153
+ range: getTSNodeRange(typeAttribute, sourceFile)
1154
+ });
1155
+ }
1156
+ return { visitors: {
1157
+ JsxOpeningElement: checkElement,
1158
+ JsxSelfClosingElement: checkElement
1159
+ } };
1160
+ }
1161
+ });
1162
+ //#endregion
1163
+ //#region src/rules/childrenProps.ts
1164
+ var childrenProps_default = ruleCreator.createRule(typescriptLanguage, {
1165
+ about: {
1166
+ description: "Reports usage of the children prop.",
1167
+ id: "childrenProps",
1168
+ presets: ["stylistic"]
1169
+ },
1170
+ messages: { noChildrenProp: {
1171
+ primary: "Prefer providing children as content between opening and closing tags, not as a `children` prop.",
1172
+ secondary: [
1173
+ "Using the `children` prop is not idiomatic in JSX.",
1174
+ "Pass children as content between opening and closing tags instead.",
1175
+ "This makes the JSX more readable and maintainable."
1176
+ ],
1177
+ suggestions: ["Remove the children prop and pass children as content between tags."]
1178
+ } },
1179
+ setup(context) {
1180
+ function checkElement(node, { sourceFile }) {
1181
+ for (const property of node.attributes.properties) if (property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "children") {
1182
+ context.report({
1183
+ message: "noChildrenProp",
1184
+ range: getTSNodeRange(property, sourceFile)
1185
+ });
1186
+ return;
1187
+ }
1188
+ }
1189
+ return { visitors: {
1190
+ JsxOpeningElement: checkElement,
1191
+ JsxSelfClosingElement: checkElement
1192
+ } };
1193
+ }
1194
+ });
1195
+ //#endregion
1196
+ //#region src/rules/clickEventKeyEvents.ts
1197
+ const interactiveElements$2 = /* @__PURE__ */ new Set([
1198
+ "a",
1199
+ "button",
1200
+ "input",
1201
+ "select",
1202
+ "textarea"
1203
+ ]);
1204
+ var clickEventKeyEvents_default = ruleCreator.createRule(typescriptLanguage, {
1205
+ about: {
1206
+ description: "Reports onClick without keyboard event handlers.",
1207
+ id: "clickEventKeyEvents",
1208
+ presets: ["logical", "logicalStrict"]
1209
+ },
1210
+ messages: { missingKeyEvent: {
1211
+ primary: "This `onClick` is missing accompanying `onKeyUp`, `onKeyDown`, and/or `onKeyPress` keyboard events.",
1212
+ secondary: [
1213
+ "Click events should have keyboard equivalents for users who cannot use a mouse.",
1214
+ "This is important for users with physical disabilities and screen reader users.",
1215
+ "Required for WCAG 2.1.1 compliance."
1216
+ ],
1217
+ suggestions: ["Add onKeyDown, onKeyUp, and/or onKeyPress handlers", "Use a button element which is inherently keyboard accessible"]
1218
+ } },
1219
+ setup(context) {
1220
+ function checkClickEvent(node, { sourceFile }) {
1221
+ if (node.tagName.kind !== SyntaxKind.Identifier || node.tagName.text.toLowerCase() !== node.tagName.text) return;
1222
+ const elementName = node.tagName.text.toLowerCase();
1223
+ if (interactiveElements$2.has(elementName)) return;
1224
+ let onClickName;
1225
+ for (const property of node.attributes.properties) if (property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier) switch (property.name.text) {
1226
+ case "aria-hidden":
1227
+ case "onKeyDown":
1228
+ case "onKeyPress":
1229
+ case "onKeyUp": return;
1230
+ case "onClick":
1231
+ onClickName = property.name;
1232
+ break;
1233
+ }
1234
+ if (onClickName) context.report({
1235
+ message: "missingKeyEvent",
1236
+ range: getTSNodeRange(onClickName, sourceFile)
1237
+ });
1238
+ }
1239
+ return { visitors: {
1240
+ JsxOpeningElement: checkClickEvent,
1241
+ JsxSelfClosingElement: checkClickEvent
1242
+ } };
1243
+ }
1244
+ });
1245
+ //#endregion
1246
+ //#region src/rules/commentTextNodes.ts
1247
+ var commentTextNodes_default = ruleCreator.createRule(typescriptLanguage, {
1248
+ about: {
1249
+ description: "Reports JSX text nodes that contain comment syntax but are rendered as text.",
1250
+ id: "commentTextNodes",
1251
+ presets: ["logical", "logicalStrict"]
1252
+ },
1253
+ messages: { commentAsText: {
1254
+ primary: "This text looks like a comment but will be rendered as text in the JSX output.",
1255
+ secondary: [
1256
+ "In JSX, text that looks like comments (// or /* */) is rendered as literal text.",
1257
+ "To add actual comments in JSX, wrap them in braces: {/* comment */}.",
1258
+ "If this is intentional text, consider making it clearer that it's not a comment."
1259
+ ],
1260
+ suggestions: ["Use {/* comment */} for actual comments", "Remove the comment-like syntax if it's unintended"]
1261
+ } },
1262
+ setup(context) {
1263
+ return { visitors: { JsxText(node, { sourceFile }) {
1264
+ const text = node.text;
1265
+ if (/^\s*(?:\/\/|\/\*)/.test(text)) context.report({
1266
+ message: "commentAsText",
1267
+ range: getTSNodeRange(node, sourceFile)
1268
+ });
1269
+ } } };
1270
+ }
1271
+ });
1272
+ //#endregion
1273
+ //#region src/rules/distractingElements.ts
1274
+ const distractingElements = /* @__PURE__ */ new Set(["blink", "marquee"]);
1275
+ var distractingElements_default = ruleCreator.createRule(typescriptLanguage, {
1276
+ about: {
1277
+ description: "Reports distracting elements like <marquee> and <blink>.",
1278
+ id: "distractingElements",
1279
+ presets: ["logical", "logicalStrict"]
1280
+ },
1281
+ messages: { noDistractingElement: {
1282
+ primary: "The <{{ element }}> element is distracting and deprecated.",
1283
+ secondary: [
1284
+ "Visually distracting elements cause accessibility issues for users with visual impairments.",
1285
+ "The <marquee> and <blink> elements are deprecated and should not be used.",
1286
+ "Use modern CSS animations if movement is necessary for your design."
1287
+ ],
1288
+ suggestions: ["Remove the distracting element", "Use CSS animations for visual effects"]
1289
+ } },
1290
+ setup(context) {
1291
+ function checkElement({ tagName }, { sourceFile }) {
1292
+ if (tagName.kind !== SyntaxKind.Identifier) return;
1293
+ const elementName = tagName.text.toLowerCase();
1294
+ if (distractingElements.has(elementName)) context.report({
1295
+ data: { element: elementName },
1296
+ message: "noDistractingElement",
1297
+ range: getTSNodeRange(tagName, sourceFile)
1298
+ });
1299
+ }
1300
+ return { visitors: {
1301
+ JsxOpeningElement: checkElement,
1302
+ JsxSelfClosingElement: checkElement
1303
+ } };
1304
+ }
1305
+ });
1306
+ //#endregion
1307
+ //#region src/rules/elementChildrenValidity.ts
1308
+ const voidElements = /* @__PURE__ */ new Set([
1309
+ "area",
1310
+ "base",
1311
+ "br",
1312
+ "col",
1313
+ "embed",
1314
+ "hr",
1315
+ "img",
1316
+ "input",
1317
+ "link",
1318
+ "meta",
1319
+ "param",
1320
+ "source",
1321
+ "track",
1322
+ "wbr"
1323
+ ]);
1324
+ var elementChildrenValidity_default = ruleCreator.createRule(typescriptLanguage, {
1325
+ about: {
1326
+ description: "Reports void DOM elements that have children, which is invalid HTML.",
1327
+ id: "elementChildrenValidity",
1328
+ presets: ["logical", "logicalStrict"]
1329
+ },
1330
+ messages: { voidElementWithChildren: {
1331
+ primary: "The `<{{ element }}>` element is a void element and cannot have children.",
1332
+ secondary: [
1333
+ "Void elements are self-closing and cannot contain any content or children.",
1334
+ "Remove the children or use a different element type.",
1335
+ "This violates HTML specification and may cause rendering issues."
1336
+ ],
1337
+ suggestions: ["Remove the children from the element", "Use a self-closing syntax: `<{{ element }} />`"]
1338
+ } },
1339
+ setup(context) {
1340
+ return { visitors: { JsxElement(node, { sourceFile }) {
1341
+ if (!node.children.length) return;
1342
+ const openingElement = node.openingElement;
1343
+ if (openingElement.tagName.kind !== SyntaxKind.Identifier) return;
1344
+ const elementName = openingElement.tagName.text.toLowerCase();
1345
+ if (!voidElements.has(elementName)) return;
1346
+ context.report({
1347
+ data: { element: elementName },
1348
+ message: "voidElementWithChildren",
1349
+ range: getTSNodeRange(openingElement.tagName, sourceFile)
1350
+ });
1351
+ } } };
1352
+ }
1353
+ });
1354
+ //#endregion
1355
+ //#region src/rules/headingContents.ts
1356
+ const headingElements = /* @__PURE__ */ new Set([
1357
+ "h1",
1358
+ "h2",
1359
+ "h3",
1360
+ "h4",
1361
+ "h5",
1362
+ "h6"
1363
+ ]);
1364
+ var headingContents_default = ruleCreator.createRule(typescriptLanguage, {
1365
+ about: {
1366
+ description: "Reports heading elements without accessible content.",
1367
+ id: "headingContents",
1368
+ presets: ["logical", "logicalStrict"]
1369
+ },
1370
+ messages: { emptyHeading: {
1371
+ primary: "This heading element is missing accessible content.",
1372
+ secondary: [
1373
+ "Headings without content are not accessible to screen readers.",
1374
+ "Ensure the heading has text content or uses aria-label/aria-labelledby.",
1375
+ "This is required for WCAG 2.4.6 compliance."
1376
+ ],
1377
+ suggestions: ["Add text content to the heading", "Use aria-label or aria-labelledby to provide accessible text"]
1378
+ } },
1379
+ setup(context) {
1380
+ function checkHeading(node, { sourceFile }) {
1381
+ const tagName = node.kind === SyntaxKind.JsxElement ? node.openingElement.tagName : node.tagName;
1382
+ if (tagName.kind !== SyntaxKind.Identifier || !headingElements.has(tagName.text.toLowerCase())) return;
1383
+ if ((node.kind === SyntaxKind.JsxElement ? node.openingElement.attributes : node.attributes).properties.some((property) => {
1384
+ if (property.kind !== SyntaxKind.JsxAttribute || property.name.kind !== SyntaxKind.Identifier) return false;
1385
+ return (property.name.text === "aria-label" || property.name.text === "aria-labelledby") && !!property.initializer;
1386
+ })) return;
1387
+ if (node.kind === SyntaxKind.JsxElement && node.children.some((child) => {
1388
+ if (child.kind === SyntaxKind.JsxText) return !!child.text.trim().length;
1389
+ return child.kind === SyntaxKind.JsxElement || child.kind === SyntaxKind.JsxSelfClosingElement || child.kind === SyntaxKind.JsxExpression;
1390
+ })) return;
1391
+ context.report({
1392
+ message: "emptyHeading",
1393
+ range: getTSNodeRange(tagName, sourceFile)
1394
+ });
1395
+ }
1396
+ return { visitors: {
1397
+ JsxElement: checkHeading,
1398
+ JsxSelfClosingElement: checkHeading
1399
+ } };
1400
+ }
1401
+ });
1402
+ //#endregion
1403
+ //#region src/rules/htmlLangs.ts
1404
+ var htmlLangs_default = ruleCreator.createRule(typescriptLanguage, {
1405
+ about: {
1406
+ description: "Reports <html> elements without a lang prop.",
1407
+ id: "htmlLangs",
1408
+ presets: ["logical", "logicalStrict"]
1409
+ },
1410
+ messages: { missingLang: {
1411
+ primary: "This <html> element is missing a `lang` prop.",
1412
+ secondary: [
1413
+ "The lang attribute identifies the language of the document for screen readers and assistive technologies.",
1414
+ "Without it, screen readers may default to the user's system language, causing confusion.",
1415
+ "This is required for WCAG 3.1.1 compliance."
1416
+ ],
1417
+ suggestions: ["Add a lang prop with a valid language code (e.g., lang=\"en\")", "Use lang=\"en-US\" for more specific localization"]
1418
+ } },
1419
+ setup(context) {
1420
+ return { visitors: { JsxOpeningElement(node, { sourceFile }) {
1421
+ if (node.tagName.kind === SyntaxKind.Identifier && node.tagName.text === "html" && !node.attributes.properties.some((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text.toLowerCase() === "lang")) context.report({
1422
+ message: "missingLang",
1423
+ range: getTSNodeRange(node.tagName, sourceFile)
1424
+ });
1425
+ } } };
1426
+ }
1427
+ });
1428
+ //#endregion
1429
+ //#region src/rules/iframeTitles.ts
1430
+ var iframeTitles_default = ruleCreator.createRule(typescriptLanguage, {
1431
+ about: {
1432
+ description: "Reports <iframe> elements without a title prop.",
1433
+ id: "iframeTitles",
1434
+ presets: ["logical", "logicalStrict"]
1435
+ },
1436
+ messages: { missingTitle: {
1437
+ primary: "This <iframe> element is missing a `title` prop.",
1438
+ secondary: [
1439
+ "The title attribute provides a label for the iframe that describes its content to screen reader users.",
1440
+ "Without it, users may have difficulty understanding the purpose of the iframe.",
1441
+ "This is required for WCAG 2.4.1 and 4.1.2 compliance."
1442
+ ],
1443
+ suggestions: ["Add a descriptive title prop (e.g., title=\"Embedded content\")", "Ensure the title clearly describes the iframe's content"]
1444
+ } },
1445
+ setup(context) {
1446
+ function checkIframe({ attributes, tagName }, { sourceFile }) {
1447
+ if (tagName.kind !== SyntaxKind.Identifier || tagName.text.toLowerCase() !== "iframe") return;
1448
+ const titleAttribute = attributes.properties.find((property) => {
1449
+ return property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text.toLowerCase() === "title";
1450
+ });
1451
+ if (titleAttribute?.kind !== SyntaxKind.JsxAttribute) {
1452
+ context.report({
1453
+ message: "missingTitle",
1454
+ range: getTSNodeRange(tagName, sourceFile)
1455
+ });
1456
+ return;
1457
+ }
1458
+ if (!titleAttribute.initializer) {
1459
+ context.report({
1460
+ message: "missingTitle",
1461
+ range: getTSNodeRange(tagName, sourceFile)
1462
+ });
1463
+ return;
1464
+ }
1465
+ if (titleAttribute.initializer.kind === SyntaxKind.StringLiteral) {
1466
+ if (titleAttribute.initializer.text === "") context.report({
1467
+ message: "missingTitle",
1468
+ range: getTSNodeRange(tagName, sourceFile)
1469
+ });
1470
+ } else if (titleAttribute.initializer.kind === SyntaxKind.JsxExpression) {
1471
+ const { expression } = titleAttribute.initializer;
1472
+ if (!expression) return;
1473
+ if (expression.kind === SyntaxKind.StringLiteral && expression.text === "" || expression.kind === SyntaxKind.NoSubstitutionTemplateLiteral && expression.text === "" || expression.kind === SyntaxKind.Identifier && expression.text === "undefined") context.report({
1474
+ message: "missingTitle",
1475
+ range: getTSNodeRange(tagName, sourceFile)
1476
+ });
1477
+ }
1478
+ }
1479
+ return { visitors: {
1480
+ JsxOpeningElement: checkIframe,
1481
+ JsxSelfClosingElement: checkIframe
1482
+ } };
1483
+ }
1484
+ });
1485
+ //#endregion
1486
+ //#region src/rules/interactiveElementRoles.ts
1487
+ const interactiveElements$1 = /* @__PURE__ */ new Set([
1488
+ "a",
1489
+ "audio",
1490
+ "button",
1491
+ "details",
1492
+ "input",
1493
+ "select",
1494
+ "summary",
1495
+ "textarea",
1496
+ "video"
1497
+ ]);
1498
+ const nonInteractiveRoles$2 = /* @__PURE__ */ new Set([
1499
+ "article",
1500
+ "banner",
1501
+ "complementary",
1502
+ "contentinfo",
1503
+ "definition",
1504
+ "directory",
1505
+ "document",
1506
+ "feed",
1507
+ "figure",
1508
+ "form",
1509
+ "group",
1510
+ "heading",
1511
+ "img",
1512
+ "list",
1513
+ "listitem",
1514
+ "log",
1515
+ "main",
1516
+ "marquee",
1517
+ "math",
1518
+ "meter",
1519
+ "navigation",
1520
+ "none",
1521
+ "note",
1522
+ "presentation",
1523
+ "region",
1524
+ "rowgroup",
1525
+ "search",
1526
+ "separator",
1527
+ "status",
1528
+ "table",
1529
+ "tabpanel",
1530
+ "term",
1531
+ "timer",
1532
+ "tooltip"
1533
+ ]);
1534
+ var interactiveElementRoles_default = ruleCreator.createRule(typescriptLanguage, {
1535
+ about: {
1536
+ description: "Reports interactive elements with non-interactive ARIA roles.",
1537
+ id: "interactiveElementRoles",
1538
+ presets: ["logical", "logicalStrict"]
1539
+ },
1540
+ messages: { invalidRole: {
1541
+ primary: "Interactive element <{{ element }}> should not have the non-interactive role `'{{ role }}'`.",
1542
+ secondary: [
1543
+ "Interactive elements should not be converted to non-interactive elements using ARIA roles.",
1544
+ "This removes expected browser interactions from the element.",
1545
+ "This is required for WCAG 4.1.2 compliance."
1546
+ ],
1547
+ suggestions: ["Remove the role attribute", "Use a native non-interactive element instead"]
1548
+ } },
1549
+ setup(context) {
1550
+ function checkElement(element, { sourceFile }) {
1551
+ if (element.tagName.kind !== SyntaxKind.Identifier) return;
1552
+ const elementName = element.tagName.text.toLowerCase();
1553
+ if (!interactiveElements$1.has(elementName)) return;
1554
+ const roleProperty = element.attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "role");
1555
+ if (roleProperty?.kind !== SyntaxKind.JsxAttribute || roleProperty.initializer?.kind !== SyntaxKind.StringLiteral) return;
1556
+ const role = roleProperty.initializer.text;
1557
+ if (nonInteractiveRoles$2.has(role)) context.report({
1558
+ data: {
1559
+ element: elementName,
1560
+ role
1561
+ },
1562
+ message: "invalidRole",
1563
+ range: getTSNodeRange(roleProperty, sourceFile)
1564
+ });
1565
+ }
1566
+ return { visitors: {
1567
+ JsxOpeningElement: checkElement,
1568
+ JsxSelfClosingElement: checkElement
1569
+ } };
1570
+ }
1571
+ });
1572
+ //#endregion
1573
+ //#region src/rules/interactiveElementsFocusable.ts
1574
+ const interactiveHandlers$2 = [
1575
+ "onClick",
1576
+ "onKeyDown",
1577
+ "onKeyPress",
1578
+ "onKeyUp",
1579
+ "onMouseDown",
1580
+ "onMouseUp"
1581
+ ];
1582
+ const interactiveRoles$2 = /* @__PURE__ */ new Set([
1583
+ "button",
1584
+ "checkbox",
1585
+ "columnheader",
1586
+ "combobox",
1587
+ "grid",
1588
+ "gridcell",
1589
+ "link",
1590
+ "listbox",
1591
+ "menu",
1592
+ "menubar",
1593
+ "menuitem",
1594
+ "menuitemcheckbox",
1595
+ "menuitemradio",
1596
+ "option",
1597
+ "progressbar",
1598
+ "radio",
1599
+ "radiogroup",
1600
+ "row",
1601
+ "rowheader",
1602
+ "searchbox",
1603
+ "slider",
1604
+ "spinbutton",
1605
+ "switch",
1606
+ "tab",
1607
+ "tablist",
1608
+ "textbox",
1609
+ "toolbar",
1610
+ "tree",
1611
+ "treegrid",
1612
+ "treeitem"
1613
+ ]);
1614
+ const inherentlyFocusableElements = /* @__PURE__ */ new Set([
1615
+ "a",
1616
+ "audio",
1617
+ "button",
1618
+ "input",
1619
+ "select",
1620
+ "textarea",
1621
+ "video"
1622
+ ]);
1623
+ const nonInteractiveElements$3 = /* @__PURE__ */ new Set([
1624
+ "article",
1625
+ "aside",
1626
+ "body",
1627
+ "br",
1628
+ "details",
1629
+ "div",
1630
+ "footer",
1631
+ "h1",
1632
+ "h2",
1633
+ "h3",
1634
+ "h4",
1635
+ "h5",
1636
+ "h6",
1637
+ "header",
1638
+ "hr",
1639
+ "img",
1640
+ "li",
1641
+ "main",
1642
+ "nav",
1643
+ "ol",
1644
+ "p",
1645
+ "section",
1646
+ "span",
1647
+ "table",
1648
+ "td",
1649
+ "ul"
1650
+ ]);
1651
+ const nonInteractiveRoles$1 = /* @__PURE__ */ new Set([
1652
+ "article",
1653
+ "banner",
1654
+ "complementary",
1655
+ "img",
1656
+ "listitem",
1657
+ "main",
1658
+ "navigation",
1659
+ "region",
1660
+ "tooltip"
1661
+ ]);
1662
+ var interactiveElementsFocusable_default = ruleCreator.createRule(typescriptLanguage, {
1663
+ about: {
1664
+ description: "Reports interactive elements that are not focusable via keyboard.",
1665
+ id: "interactiveElementsFocusable",
1666
+ presets: ["logical", "logicalStrict"]
1667
+ },
1668
+ messages: { notFocusable: {
1669
+ primary: "The '{{ role }}' role makes this element interactive, so it should also be focusable.",
1670
+ secondary: [
1671
+ "Interactive elements must be reachable via keyboard navigation.",
1672
+ "Add a tabIndex attribute or use an inherently focusable element.",
1673
+ "This is required for WCAG 2.1.1 compliance."
1674
+ ],
1675
+ suggestions: [
1676
+ "Add tabIndex={0} to make the element tabbable",
1677
+ "Add tabIndex={-1} to make the element programmatically focusable",
1678
+ "Use a native interactive element like <button> or <a>"
1679
+ ]
1680
+ } },
1681
+ setup(context) {
1682
+ function getTabIndexValue(node) {
1683
+ const tabIndex = node.attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "tabIndex");
1684
+ if (!tabIndex?.initializer) return;
1685
+ if (tabIndex.initializer.kind === SyntaxKind.JsxExpression) {
1686
+ const expression = tabIndex.initializer.expression;
1687
+ if (expression?.kind === SyntaxKind.NumericLiteral) return Number(expression.text);
1688
+ }
1689
+ if (tabIndex.initializer.kind === SyntaxKind.StringLiteral) return Number(tabIndex.initializer.text);
1690
+ }
1691
+ function getRoleValue(node) {
1692
+ const role = node.attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "role");
1693
+ if (role?.kind === SyntaxKind.JsxAttribute && role.initializer?.kind === SyntaxKind.StringLiteral) return role.initializer.text;
1694
+ }
1695
+ function isAriaHidden(node) {
1696
+ const ariaHidden = node.attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "aria-hidden");
1697
+ if (ariaHidden?.kind !== SyntaxKind.JsxAttribute || !ariaHidden.initializer) return false;
1698
+ if (ariaHidden.initializer.kind === SyntaxKind.JsxExpression) return ariaHidden.initializer.expression?.kind === SyntaxKind.TrueKeyword;
1699
+ if (ariaHidden.initializer.kind === SyntaxKind.StringLiteral) return ariaHidden.initializer.text === "true";
1700
+ return false;
1701
+ }
1702
+ function hasInteractiveHandler(node) {
1703
+ return node.attributes.properties.some((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && interactiveHandlers$2.includes(property.name.text));
1704
+ }
1705
+ function isDisabled(node) {
1706
+ const disabledProperty = node.attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "disabled");
1707
+ if (disabledProperty?.kind !== SyntaxKind.JsxAttribute) return false;
1708
+ if (!disabledProperty.initializer) return true;
1709
+ if (disabledProperty.initializer.kind === SyntaxKind.JsxExpression) return disabledProperty.initializer.expression?.kind === SyntaxKind.TrueKeyword;
1710
+ return false;
1711
+ }
1712
+ function checkElement(node, { sourceFile }) {
1713
+ if (node.tagName.kind !== SyntaxKind.Identifier) return;
1714
+ const elementName = node.tagName.text.toLowerCase();
1715
+ if (elementName !== node.tagName.text || !hasInteractiveHandler(node) || isAriaHidden(node) || isDisabled(node)) return;
1716
+ const role = getRoleValue(node);
1717
+ if (role === "presentation" || role === "none") return;
1718
+ if (!(role !== void 0 && interactiveRoles$2.has(role) || role === void 0 && !nonInteractiveElements$3.has(elementName))) return;
1719
+ if (role && nonInteractiveRoles$1.has(role)) return;
1720
+ const hasInherentFocus = inherentlyFocusableElements.has(elementName);
1721
+ const hasFocusableTabIndex = getTabIndexValue(node) !== void 0;
1722
+ const roleProperty = node.attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "role");
1723
+ if (!hasInherentFocus && !hasFocusableTabIndex) {
1724
+ const displayRole = role ?? elementName;
1725
+ context.report({
1726
+ data: { role: displayRole },
1727
+ message: "notFocusable",
1728
+ range: getTSNodeRange(roleProperty?.kind === SyntaxKind.JsxAttribute ? roleProperty : node.tagName, sourceFile)
1729
+ });
1730
+ }
1731
+ }
1732
+ return { visitors: {
1733
+ JsxOpeningElement: checkElement,
1734
+ JsxSelfClosingElement: checkElement
1735
+ } };
1736
+ }
1737
+ });
1738
+ //#endregion
1739
+ //#region src/rules/labelAssociatedControls.ts
1740
+ const controlElements = /* @__PURE__ */ new Set([
1741
+ "input",
1742
+ "meter",
1743
+ "output",
1744
+ "progress",
1745
+ "select",
1746
+ "textarea"
1747
+ ]);
1748
+ var labelAssociatedControls_default = ruleCreator.createRule(typescriptLanguage, {
1749
+ about: {
1750
+ description: "Reports <label> elements without an associated control element.",
1751
+ id: "labelAssociatedControls",
1752
+ presets: ["logical", "logicalStrict"]
1753
+ },
1754
+ messages: { missingAssociatedControl: {
1755
+ primary: "This <label> element is missing an associated control element.",
1756
+ secondary: [
1757
+ "Labels must be associated with a control element to be accessible to screen readers.",
1758
+ "Use the htmlFor prop to reference a control by id, or nest the control inside the label.",
1759
+ "This is required for WCAG 1.3.1, 2.4.6, and 4.1.2 compliance."
1760
+ ],
1761
+ suggestions: ["Add an htmlFor prop that references a control element by id", "Nest a control element (input, select, textarea) inside the label"]
1762
+ } },
1763
+ setup(context) {
1764
+ function hasHtmlForAttribute(attributes) {
1765
+ return attributes.properties.some((property) => {
1766
+ if (property.kind !== SyntaxKind.JsxAttribute || property.name.kind !== SyntaxKind.Identifier || property.name.text !== "htmlFor" || !property.initializer) return false;
1767
+ if (property.initializer.kind === SyntaxKind.StringLiteral) return property.initializer.text !== "";
1768
+ if (property.initializer.kind === SyntaxKind.JsxExpression) {
1769
+ const { expression } = property.initializer;
1770
+ if (!expression) return false;
1771
+ if (expression.kind === SyntaxKind.StringLiteral && expression.text === "" || expression.kind === SyntaxKind.NoSubstitutionTemplateLiteral && expression.text === "" || expression.kind === SyntaxKind.Identifier && expression.text === "undefined" || expression.kind === SyntaxKind.NullKeyword) return false;
1772
+ }
1773
+ return true;
1774
+ });
1775
+ }
1776
+ function hasNestedControl(children) {
1777
+ return children.some((child) => {
1778
+ if (child.kind === SyntaxKind.JsxElement) {
1779
+ const { tagName } = child.openingElement;
1780
+ return tagName.kind === SyntaxKind.Identifier && controlElements.has(tagName.text.toLowerCase()) || hasNestedControl(child.children);
1781
+ }
1782
+ if (child.kind === SyntaxKind.JsxSelfClosingElement) return child.tagName.kind === SyntaxKind.Identifier && controlElements.has(child.tagName.text.toLowerCase());
1783
+ return false;
1784
+ });
1785
+ }
1786
+ function checkLabel(node, { sourceFile }) {
1787
+ if (node.kind === SyntaxKind.JsxElement && hasNestedControl(node.children)) return;
1788
+ const tagName = node.kind === SyntaxKind.JsxElement ? node.openingElement.tagName : node.tagName;
1789
+ if (tagName.kind !== SyntaxKind.Identifier || tagName.text.toLowerCase() !== "label") return;
1790
+ if (hasHtmlForAttribute(node.kind === SyntaxKind.JsxElement ? node.openingElement.attributes : node.attributes)) return;
1791
+ context.report({
1792
+ message: "missingAssociatedControl",
1793
+ range: getTSNodeRange(tagName, sourceFile)
1794
+ });
1795
+ }
1796
+ return { visitors: {
1797
+ JsxElement: checkLabel,
1798
+ JsxSelfClosingElement: checkLabel
1799
+ } };
1800
+ }
1801
+ });
1802
+ //#endregion
1803
+ //#region src/rules/langValidity.ts
1804
+ var langValidity_default = ruleCreator.createRule(typescriptLanguage, {
1805
+ about: {
1806
+ description: "Reports invalid lang attribute values.",
1807
+ id: "langValidity",
1808
+ presets: ["logical", "logicalStrict"]
1809
+ },
1810
+ messages: { invalidLang: {
1811
+ primary: "The lang attribute value '{{ value }}' is not a valid BCP 47 language tag.",
1812
+ secondary: [
1813
+ "The lang attribute must contain a valid BCP 47 language tag.",
1814
+ "Valid examples include 'en', 'en-US', 'zh-Hans', or 'fr-CA'.",
1815
+ "This is required for WCAG 3.1.2 compliance."
1816
+ ],
1817
+ suggestions: ["Use a valid BCP 47 language tag like 'en' or 'en-US'", "Check the language code spelling and format"]
1818
+ } },
1819
+ setup(context) {
1820
+ function checkElement(node, { sourceFile }) {
1821
+ const langAttribute = node.attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "lang");
1822
+ if (!langAttribute?.initializer) return;
1823
+ if (langAttribute.initializer.kind === SyntaxKind.StringLiteral) {
1824
+ const langValue = langAttribute.initializer.text;
1825
+ if (!languageTags.check(langValue)) context.report({
1826
+ data: { value: langValue || "(empty)" },
1827
+ message: "invalidLang",
1828
+ range: getTSNodeRange(langAttribute.initializer, sourceFile)
1829
+ });
1830
+ }
1831
+ }
1832
+ return { visitors: {
1833
+ JsxOpeningElement: checkElement,
1834
+ JsxSelfClosingElement: checkElement
1835
+ } };
1836
+ }
1837
+ });
1838
+ //#endregion
1839
+ //#region src/rules/mediaCaptions.ts
1840
+ var mediaCaptions_default = ruleCreator.createRule(typescriptLanguage, {
1841
+ about: {
1842
+ description: "Reports media elements without captions.",
1843
+ id: "mediaCaptions",
1844
+ presets: ["logical", "logicalStrict"]
1845
+ },
1846
+ messages: { missingCaptions: {
1847
+ primary: "This media element is missing <track> element captions.",
1848
+ secondary: [
1849
+ "Captions are essential for deaf users to follow along with media content.",
1850
+ "The <track> element with kind='captions' provides this accessibility feature.",
1851
+ "This is required for WCAG 1.2.2 and 1.2.3 compliance."
1852
+ ],
1853
+ suggestions: ["Add a <track kind=\"captions\"> element as a child", "Add the muted attribute if the media has no audio"]
1854
+ } },
1855
+ setup(context) {
1856
+ function checkMediaElement(node, { sourceFile }) {
1857
+ const tagName = node.kind === SyntaxKind.JsxElement ? node.openingElement.tagName : node.tagName;
1858
+ if (tagName.kind !== SyntaxKind.Identifier) return;
1859
+ const elementName = tagName.text.toLowerCase();
1860
+ if (elementName !== "audio" && elementName !== "video") return;
1861
+ if ((node.kind === SyntaxKind.JsxElement ? node.openingElement.attributes : node.attributes).properties.some((properties) => properties.kind === SyntaxKind.JsxAttribute && properties.name.kind === SyntaxKind.Identifier && properties.name.text === "muted")) return;
1862
+ if (node.kind === SyntaxKind.JsxElement && node.children.some(isCaptionsTrack)) return;
1863
+ context.report({
1864
+ message: "missingCaptions",
1865
+ range: getTSNodeRange(tagName, sourceFile)
1866
+ });
1867
+ }
1868
+ return { visitors: {
1869
+ JsxElement: checkMediaElement,
1870
+ JsxSelfClosingElement: checkMediaElement
1871
+ } };
1872
+ }
1873
+ });
1874
+ function isCaptionsTrack(node) {
1875
+ if (node.kind !== SyntaxKind.JsxElement && node.kind !== SyntaxKind.JsxSelfClosingElement) return false;
1876
+ const childTagName = node.kind === SyntaxKind.JsxElement ? node.openingElement.tagName : node.tagName;
1877
+ if (childTagName.kind !== SyntaxKind.Identifier || childTagName.text !== "track") return false;
1878
+ return (node.kind === SyntaxKind.JsxElement ? node.openingElement.attributes : node.attributes).properties.some((property) => {
1879
+ if (property.kind !== SyntaxKind.JsxAttribute || property.name.kind !== SyntaxKind.Identifier || property.name.text !== "kind") return false;
1880
+ if (property.initializer?.kind === SyntaxKind.StringLiteral) return property.initializer.text === "captions";
1881
+ return false;
1882
+ });
1883
+ }
1884
+ //#endregion
1885
+ //#region src/rules/mouseEventKeyEvents.ts
1886
+ const mouseNamesToKeyboardNames = /* @__PURE__ */ new Map([["onMouseOut", "onBlur"], ["onMouseOver", "onFocus"]]);
1887
+ var mouseEventKeyEvents_default = ruleCreator.createRule(typescriptLanguage, {
1888
+ about: {
1889
+ description: "Reports mouse events without corresponding keyboard events.",
1890
+ id: "mouseEventKeyEvents",
1891
+ presets: ["logical", "logicalStrict"]
1892
+ },
1893
+ messages: { missingKeyEvent: {
1894
+ primary: "`{{ mouseEvent }}` is missing an accompanying `{{ keyEvent }}` for keyboard accessibility.",
1895
+ secondary: [
1896
+ "Mouse events should have keyboard equivalents for users who cannot use a mouse.",
1897
+ "This ensures that functionality is accessible to all users, such as those with limited hardware or personal disabilities.",
1898
+ "Required for WCAG 2.1.1 compliance."
1899
+ ],
1900
+ suggestions: ["Add the corresponding keyboard event handler", "Ensure both mouse and keyboard users can access the functionality"]
1901
+ } },
1902
+ setup(context) {
1903
+ function checkMouseEvents(node, { sourceFile }) {
1904
+ const { attributes } = node;
1905
+ const presentAttributes = /* @__PURE__ */ new Set();
1906
+ for (const property of attributes.properties) if (property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier) presentAttributes.add(property.name.text);
1907
+ for (const property of attributes.properties) {
1908
+ if (property.kind !== SyntaxKind.JsxAttribute || property.name.kind !== SyntaxKind.Identifier) continue;
1909
+ const mouseEvent = property.name.text;
1910
+ const keyEvent = mouseNamesToKeyboardNames.get(mouseEvent);
1911
+ if (keyEvent && !presentAttributes.has(keyEvent)) context.report({
1912
+ data: {
1913
+ keyEvent,
1914
+ mouseEvent
1915
+ },
1916
+ message: "missingKeyEvent",
1917
+ range: getTSNodeRange(property.name, sourceFile)
1918
+ });
1919
+ }
1920
+ }
1921
+ return { visitors: {
1922
+ JsxOpeningElement: checkMouseEvents,
1923
+ JsxSelfClosingElement: checkMouseEvents
1924
+ } };
1925
+ }
1926
+ });
1927
+ //#endregion
1928
+ //#region src/rules/nonInteractiveElementInteractions.ts
1929
+ const interactiveHandlers$1 = [
1930
+ "onClick",
1931
+ "onKeyDown",
1932
+ "onKeyPress",
1933
+ "onKeyUp",
1934
+ "onMouseDown",
1935
+ "onMouseUp"
1936
+ ];
1937
+ const nonInteractiveElements$2 = /* @__PURE__ */ new Set([
1938
+ "area",
1939
+ "article",
1940
+ "aside",
1941
+ "body",
1942
+ "br",
1943
+ "details",
1944
+ "footer",
1945
+ "h1",
1946
+ "h2",
1947
+ "h3",
1948
+ "h4",
1949
+ "h5",
1950
+ "h6",
1951
+ "header",
1952
+ "hr",
1953
+ "img",
1954
+ "li",
1955
+ "main",
1956
+ "nav",
1957
+ "ol",
1958
+ "p",
1959
+ "section",
1960
+ "table",
1961
+ "td",
1962
+ "ul"
1963
+ ]);
1964
+ const nonInteractiveRoles = /* @__PURE__ */ new Set([
1965
+ "article",
1966
+ "banner",
1967
+ "complementary",
1968
+ "img",
1969
+ "listitem",
1970
+ "main",
1971
+ "navigation",
1972
+ "region",
1973
+ "tooltip"
1974
+ ]);
1975
+ var nonInteractiveElementInteractions_default = ruleCreator.createRule(typescriptLanguage, {
1976
+ about: {
1977
+ description: "Reports non-interactive elements with interactive event handlers.",
1978
+ id: "nonInteractiveElementInteractions",
1979
+ presets: ["logical", "logicalStrict"]
1980
+ },
1981
+ messages: { invalidHandler: {
1982
+ primary: "`<{{ element }}>` elements are non-interactive and so should not have interactive event handlers.",
1983
+ secondary: [
1984
+ "Non-interactive elements indicate content and containers in the user interface.",
1985
+ "Use native interactive elements like <button> or <a> instead, or add an interactive role.",
1986
+ "This is required for WCAG 4.1.2 compliance."
1987
+ ],
1988
+ suggestions: [
1989
+ "Use a native interactive element instead (e.g. <button>)",
1990
+ "Add an interactive role attribute (e.g. role='button')",
1991
+ "Move the handler to an inner element with an interactive role"
1992
+ ]
1993
+ } },
1994
+ setup(context) {
1995
+ function checkElement(element, { sourceFile }) {
1996
+ if (element.tagName.kind !== SyntaxKind.Identifier) return;
1997
+ const elementName = element.tagName.text.toLowerCase();
1998
+ if (elementName !== element.tagName.text || !nonInteractiveElements$2.has(elementName)) return;
1999
+ let hasInteractiveHandler = false;
2000
+ for (const property of element.attributes.properties) if (property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier) {
2001
+ if (property.name.text === "role" && property.initializer?.kind === SyntaxKind.StringLiteral && !nonInteractiveRoles.has(property.initializer.text)) return;
2002
+ if (interactiveHandlers$1.includes(property.name.text)) hasInteractiveHandler = true;
2003
+ }
2004
+ if (hasInteractiveHandler) context.report({
2005
+ data: { element: elementName },
2006
+ message: "invalidHandler",
2007
+ range: getTSNodeRange(element.tagName, sourceFile)
2008
+ });
2009
+ }
2010
+ return { visitors: {
2011
+ JsxOpeningElement: checkElement,
2012
+ JsxSelfClosingElement: checkElement
2013
+ } };
2014
+ }
2015
+ });
2016
+ //#endregion
2017
+ //#region src/rules/nonInteractiveElementRoles.ts
2018
+ const nonInteractiveElements$1 = /* @__PURE__ */ new Set([
2019
+ "article",
2020
+ "aside",
2021
+ "body",
2022
+ "br",
2023
+ "details",
2024
+ "div",
2025
+ "footer",
2026
+ "h1",
2027
+ "h2",
2028
+ "h3",
2029
+ "h4",
2030
+ "h5",
2031
+ "h6",
2032
+ "header",
2033
+ "hr",
2034
+ "img",
2035
+ "li",
2036
+ "main",
2037
+ "nav",
2038
+ "ol",
2039
+ "p",
2040
+ "section",
2041
+ "table",
2042
+ "td",
2043
+ "ul"
2044
+ ]);
2045
+ const interactiveRoles$1 = /* @__PURE__ */ new Set([
2046
+ "button",
2047
+ "checkbox",
2048
+ "link",
2049
+ "menuitem",
2050
+ "menuitemcheckbox",
2051
+ "menuitemradio",
2052
+ "option",
2053
+ "radio",
2054
+ "searchbox",
2055
+ "slider",
2056
+ "spinbutton",
2057
+ "switch",
2058
+ "tab",
2059
+ "textbox"
2060
+ ]);
2061
+ const allowedExceptions = {
2062
+ li: /* @__PURE__ */ new Set([
2063
+ "menuitem",
2064
+ "option",
2065
+ "row",
2066
+ "tab",
2067
+ "treeitem"
2068
+ ]),
2069
+ ol: /* @__PURE__ */ new Set([
2070
+ "listbox",
2071
+ "menu",
2072
+ "menubar",
2073
+ "radiogroup",
2074
+ "tablist",
2075
+ "tree",
2076
+ "treegrid"
2077
+ ]),
2078
+ table: /* @__PURE__ */ new Set(["grid"]),
2079
+ td: /* @__PURE__ */ new Set(["gridcell"]),
2080
+ ul: /* @__PURE__ */ new Set([
2081
+ "listbox",
2082
+ "menu",
2083
+ "menubar",
2084
+ "radiogroup",
2085
+ "tablist",
2086
+ "tree",
2087
+ "treegrid"
2088
+ ])
2089
+ };
2090
+ var nonInteractiveElementRoles_default = ruleCreator.createRule(typescriptLanguage, {
2091
+ about: {
2092
+ description: "Reports non-interactive elements with interactive ARIA roles.",
2093
+ id: "nonInteractiveElementRoles",
2094
+ presets: ["logical", "logicalStrict"]
2095
+ },
2096
+ messages: { invalidRole: {
2097
+ primary: "Non-interactive element <{{ element }}> should not have the interactive role `'{{ role }}'`.",
2098
+ secondary: [
2099
+ "Non-interactive elements should not be converted to interactive controls using ARIA roles.",
2100
+ "Use native interactive elements like <button> or <a> instead.",
2101
+ "This is required for WCAG 4.1.2 compliance."
2102
+ ],
2103
+ suggestions: ["Use a native interactive element instead", "Remove the role attribute"]
2104
+ } },
2105
+ setup(context) {
2106
+ function checkElement(element, { sourceFile }) {
2107
+ if (element.tagName.kind !== SyntaxKind.Identifier) return;
2108
+ const elementName = element.tagName.text.toLowerCase();
2109
+ if (!nonInteractiveElements$1.has(elementName)) return;
2110
+ const roleProperty = element.attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "role");
2111
+ if (roleProperty?.kind !== SyntaxKind.JsxAttribute || roleProperty.initializer?.kind !== SyntaxKind.StringLiteral) return;
2112
+ const role = roleProperty.initializer.text;
2113
+ if (interactiveRoles$1.has(role) && !allowedExceptions[elementName]?.has(role)) context.report({
2114
+ data: {
2115
+ element: elementName,
2116
+ role
2117
+ },
2118
+ message: "invalidRole",
2119
+ range: getTSNodeRange(roleProperty, sourceFile)
2120
+ });
2121
+ }
2122
+ return { visitors: {
2123
+ JsxOpeningElement: checkElement,
2124
+ JsxSelfClosingElement: checkElement
2125
+ } };
2126
+ }
2127
+ });
2128
+ //#endregion
2129
+ //#region src/rules/nonInteractiveElementTabIndexes.ts
2130
+ const nonInteractiveElements = /* @__PURE__ */ new Set([
2131
+ "article",
2132
+ "aside",
2133
+ "body",
2134
+ "br",
2135
+ "details",
2136
+ "div",
2137
+ "footer",
2138
+ "h1",
2139
+ "h2",
2140
+ "h3",
2141
+ "h4",
2142
+ "h5",
2143
+ "h6",
2144
+ "header",
2145
+ "hr",
2146
+ "img",
2147
+ "li",
2148
+ "main",
2149
+ "nav",
2150
+ "ol",
2151
+ "p",
2152
+ "section",
2153
+ "span",
2154
+ "ul"
2155
+ ]);
2156
+ const interactiveRoles = /* @__PURE__ */ new Set([
2157
+ "button",
2158
+ "checkbox",
2159
+ "link",
2160
+ "menuitem",
2161
+ "menuitemcheckbox",
2162
+ "menuitemradio",
2163
+ "option",
2164
+ "radio",
2165
+ "searchbox",
2166
+ "slider",
2167
+ "spinbutton",
2168
+ "switch",
2169
+ "tab",
2170
+ "textbox"
2171
+ ]);
2172
+ var nonInteractiveElementTabIndexes_default = ruleCreator.createRule(typescriptLanguage, {
2173
+ about: {
2174
+ description: "Reports non-interactive elements with positive or zero tabIndex values.",
2175
+ id: "nonInteractiveElementTabIndexes",
2176
+ presets: ["logical", "logicalStrict"]
2177
+ },
2178
+ messages: { nonInteractiveTabIndex: {
2179
+ primary: "Non-interactive element `<{{ element }}>` should not have an explicit, non-negative `tabIndex`.",
2180
+ secondary: [
2181
+ "Tab navigation should be limited to interactive elements.",
2182
+ "Non-interactive elements with tabIndex add unnecessary stops in the tab order.",
2183
+ "Use tabIndex='-1' if you need to programmatically focus the element."
2184
+ ],
2185
+ suggestions: [
2186
+ "Remove the tabIndex attribute",
2187
+ "Use tabIndex='-1' instead",
2188
+ "Add an interactive role to the element"
2189
+ ]
2190
+ } },
2191
+ setup(context) {
2192
+ function getTabIndexValue(attr) {
2193
+ if (!attr.initializer) return;
2194
+ if (attr.initializer.kind === SyntaxKind.StringLiteral) {
2195
+ const value = parseInt(attr.initializer.text, 10);
2196
+ return Number.isNaN(value) ? void 0 : value;
2197
+ }
2198
+ if (attr.initializer.kind === SyntaxKind.JsxExpression) {
2199
+ const expr = attr.initializer.expression;
2200
+ if (expr?.kind === SyntaxKind.NumericLiteral) {
2201
+ const value = parseInt(expr.text, 10);
2202
+ return Number.isNaN(value) ? void 0 : value;
2203
+ }
2204
+ }
2205
+ }
2206
+ function getRoleValue(attributes) {
2207
+ const roleProperty = attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "role");
2208
+ if (roleProperty?.kind === SyntaxKind.JsxAttribute && roleProperty.initializer?.kind === SyntaxKind.StringLiteral) return roleProperty.initializer.text;
2209
+ }
2210
+ function checkTabIndex(node, { sourceFile }) {
2211
+ if (node.tagName.kind !== SyntaxKind.Identifier) return;
2212
+ const elementName = node.tagName.text.toLowerCase();
2213
+ if (!nonInteractiveElements.has(elementName)) return;
2214
+ const role = getRoleValue(node.attributes);
2215
+ if (role && interactiveRoles.has(role)) return;
2216
+ const tabIndexProperty = node.attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "tabIndex");
2217
+ if (tabIndexProperty?.kind !== SyntaxKind.JsxAttribute) return;
2218
+ const tabIndexValue = getTabIndexValue(tabIndexProperty);
2219
+ if (tabIndexValue !== void 0 && tabIndexValue >= 0) context.report({
2220
+ data: { element: elementName },
2221
+ message: "nonInteractiveTabIndex",
2222
+ range: getTSNodeRange(tabIndexProperty, sourceFile)
2223
+ });
2224
+ }
2225
+ return { visitors: {
2226
+ JsxOpeningElement: checkTabIndex,
2227
+ JsxSelfClosingElement: checkTabIndex
2228
+ } };
2229
+ }
2230
+ });
2231
+ //#endregion
2232
+ //#region src/rules/propDuplicates.ts
2233
+ var propDuplicates_default = ruleCreator.createRule(typescriptLanguage, {
2234
+ about: {
2235
+ description: "Disallow duplicate props in JSX elements.",
2236
+ id: "propDuplicates",
2237
+ presets: ["logical", "logicalStrict"]
2238
+ },
2239
+ messages: { duplicateProp: {
2240
+ primary: "Duplicate prop `{{ propName }}` found in JSX element. The last occurrence will override earlier ones.",
2241
+ secondary: ["Having duplicate props can lead to unexpected behavior since only the last value is used.", "This makes the code harder to understand and maintain."],
2242
+ suggestions: ["Remove the duplicate prop and keep only one definition."]
2243
+ } },
2244
+ setup(context) {
2245
+ function checkElement(node, { sourceFile }) {
2246
+ const seenProps = /* @__PURE__ */ new Set();
2247
+ for (const property of node.attributes.properties) {
2248
+ if (property.kind !== SyntaxKind.JsxAttribute || property.name.kind !== SyntaxKind.Identifier) continue;
2249
+ const propName = property.name.text;
2250
+ if (seenProps.has(propName)) context.report({
2251
+ data: { propName },
2252
+ message: "duplicateProp",
2253
+ range: getTSNodeRange(property.name, sourceFile)
2254
+ });
2255
+ else seenProps.add(propName);
2256
+ }
2257
+ }
2258
+ return { visitors: {
2259
+ JsxOpeningElement: checkElement,
2260
+ JsxSelfClosingElement: checkElement
2261
+ } };
2262
+ }
2263
+ });
2264
+ //#endregion
2265
+ //#region src/rules/roleRedundancies.ts
2266
+ const implicitRoles$1 = {
2267
+ a: "link",
2268
+ article: "article",
2269
+ aside: "complementary",
2270
+ button: "button",
2271
+ dialog: "dialog",
2272
+ footer: "contentinfo",
2273
+ form: "form",
2274
+ h1: "heading",
2275
+ h2: "heading",
2276
+ h3: "heading",
2277
+ h4: "heading",
2278
+ h5: "heading",
2279
+ h6: "heading",
2280
+ header: "banner",
2281
+ hr: "separator",
2282
+ img: "img",
2283
+ input: "textbox",
2284
+ li: "listitem",
2285
+ main: "main",
2286
+ nav: "navigation",
2287
+ ol: "list",
2288
+ section: "region",
2289
+ select: "listbox",
2290
+ textarea: "textbox",
2291
+ ul: "list"
2292
+ };
2293
+ var roleRedundancies_default = ruleCreator.createRule(typescriptLanguage, {
2294
+ about: {
2295
+ description: "Reports redundant ARIA roles on elements with implicit roles.",
2296
+ id: "roleRedundancies",
2297
+ presets: ["logical", "logicalStrict"]
2298
+ },
2299
+ messages: { redundantRole: {
2300
+ primary: "`<{{ element }}>` elements already implicitly have a role of `{{ role }}`. This explicit role is unnecessary.",
2301
+ secondary: ["HTML elements have default semantics implemented by the browser.", "Setting an explicit role that matches the implicit role is redundant and doesn't provide any benefit."],
2302
+ suggestions: ["Remove the redundant role attribute"]
2303
+ } },
2304
+ setup(context) {
2305
+ function checkRedundantRole(node, { sourceFile }) {
2306
+ if (node.tagName.kind !== SyntaxKind.Identifier) return;
2307
+ const element = node.tagName.text.toLowerCase();
2308
+ const implicitRole = implicitRoles$1[element];
2309
+ if (!implicitRole) return;
2310
+ const roleProperty = node.attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "role");
2311
+ if (roleProperty?.kind === SyntaxKind.JsxAttribute && roleProperty.initializer?.kind === SyntaxKind.StringLiteral && roleProperty.initializer.text === implicitRole) {
2312
+ const range = getTSNodeRange(roleProperty, sourceFile);
2313
+ context.report({
2314
+ data: {
2315
+ element,
2316
+ role: roleProperty.initializer.text
2317
+ },
2318
+ message: "redundantRole",
2319
+ range,
2320
+ suggestions: [{
2321
+ id: "removeRole",
2322
+ range,
2323
+ text: ""
2324
+ }]
2325
+ });
2326
+ }
2327
+ }
2328
+ return { visitors: {
2329
+ JsxOpeningElement: checkRedundantRole,
2330
+ JsxSelfClosingElement: checkRedundantRole
2331
+ } };
2332
+ }
2333
+ });
2334
+ //#endregion
2335
+ //#region src/rules/roleRequiredAriaProps.ts
2336
+ const requiredAriaPropsForRole = {
2337
+ checkbox: ["aria-checked"],
2338
+ combobox: ["aria-controls", "aria-expanded"],
2339
+ heading: ["aria-level"],
2340
+ menuitemcheckbox: ["aria-checked"],
2341
+ menuitemradio: ["aria-checked"],
2342
+ option: ["aria-selected"],
2343
+ radio: ["aria-checked"],
2344
+ scrollbar: [
2345
+ "aria-controls",
2346
+ "aria-valuenow",
2347
+ "aria-valuemax",
2348
+ "aria-valuemin"
2349
+ ],
2350
+ slider: [
2351
+ "aria-valuenow",
2352
+ "aria-valuemax",
2353
+ "aria-valuemin"
2354
+ ],
2355
+ spinbutton: [
2356
+ "aria-valuenow",
2357
+ "aria-valuemax",
2358
+ "aria-valuemin"
2359
+ ],
2360
+ switch: ["aria-checked"]
2361
+ };
2362
+ var roleRequiredAriaProps_default = ruleCreator.createRule(typescriptLanguage, {
2363
+ about: {
2364
+ description: "Reports ARIA roles missing their required ARIA properties.",
2365
+ id: "roleRequiredAriaProps",
2366
+ presets: ["logical", "logicalStrict"]
2367
+ },
2368
+ messages: { missingRequiredProps: {
2369
+ primary: "Elements with ARIA role `{{ role }}` must have the following ARIA property(s) defined: {{ props }}.",
2370
+ secondary: [
2371
+ "ARIA roles define how elements are exposed to assistive technologies.",
2372
+ "Some roles require specific ARIA properties to function correctly.",
2373
+ "This is required for WCAG 4.1.2 compliance."
2374
+ ],
2375
+ suggestions: ["Add the required ARIA properties to the element."]
2376
+ } },
2377
+ setup(context) {
2378
+ function checkElement(node, { sourceFile }) {
2379
+ const roleAttribute = node.attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "role");
2380
+ if (roleAttribute?.kind !== SyntaxKind.JsxAttribute || roleAttribute.initializer?.kind !== SyntaxKind.StringLiteral) return;
2381
+ const role = roleAttribute.initializer.text.toLowerCase();
2382
+ const requiredProps = requiredAriaPropsForRole[role];
2383
+ if (!requiredProps) return;
2384
+ const existingProps = /* @__PURE__ */ new Set();
2385
+ for (const property of node.attributes.properties) {
2386
+ if (property.kind !== SyntaxKind.JsxAttribute || property.name.kind !== SyntaxKind.Identifier) continue;
2387
+ const propertyName = property.name.text.toLowerCase();
2388
+ if (propertyName.startsWith("aria-")) existingProps.add(propertyName);
2389
+ }
2390
+ const missingProps = requiredProps.filter((prop) => !existingProps.has(prop));
2391
+ if (missingProps.length) context.report({
2392
+ data: {
2393
+ props: missingProps.join(", "),
2394
+ role
2395
+ },
2396
+ message: "missingRequiredProps",
2397
+ range: getTSNodeRange(roleAttribute, sourceFile)
2398
+ });
2399
+ }
2400
+ return { visitors: {
2401
+ JsxOpeningElement: checkElement,
2402
+ JsxSelfClosingElement: checkElement
2403
+ } };
2404
+ }
2405
+ });
2406
+ //#endregion
2407
+ //#region src/rules/roleSupportedAriaProps.ts
2408
+ const globalAriaProperties = /* @__PURE__ */ new Set([
2409
+ "aria-atomic",
2410
+ "aria-busy",
2411
+ "aria-controls",
2412
+ "aria-current",
2413
+ "aria-describedby",
2414
+ "aria-details",
2415
+ "aria-disabled",
2416
+ "aria-dropeffect",
2417
+ "aria-errormessage",
2418
+ "aria-flowto",
2419
+ "aria-grabbed",
2420
+ "aria-haspopup",
2421
+ "aria-hidden",
2422
+ "aria-invalid",
2423
+ "aria-keyshortcuts",
2424
+ "aria-label",
2425
+ "aria-labelledby",
2426
+ "aria-live",
2427
+ "aria-owns",
2428
+ "aria-relevant",
2429
+ "aria-roledescription"
2430
+ ]);
2431
+ const roleToSupportedProps = {
2432
+ alert: /* @__PURE__ */ new Set(["aria-expanded"]),
2433
+ alertdialog: /* @__PURE__ */ new Set(["aria-expanded", "aria-modal"]),
2434
+ application: /* @__PURE__ */ new Set(["aria-activedescendant", "aria-expanded"]),
2435
+ article: /* @__PURE__ */ new Set([
2436
+ "aria-expanded",
2437
+ "aria-posinset",
2438
+ "aria-setsize"
2439
+ ]),
2440
+ banner: /* @__PURE__ */ new Set(["aria-expanded"]),
2441
+ button: /* @__PURE__ */ new Set(["aria-expanded", "aria-pressed"]),
2442
+ cell: /* @__PURE__ */ new Set([
2443
+ "aria-colindex",
2444
+ "aria-colspan",
2445
+ "aria-expanded",
2446
+ "aria-rowindex",
2447
+ "aria-rowspan"
2448
+ ]),
2449
+ checkbox: /* @__PURE__ */ new Set([
2450
+ "aria-checked",
2451
+ "aria-expanded",
2452
+ "aria-readonly",
2453
+ "aria-required"
2454
+ ]),
2455
+ columnheader: /* @__PURE__ */ new Set([
2456
+ "aria-colindex",
2457
+ "aria-colspan",
2458
+ "aria-expanded",
2459
+ "aria-readonly",
2460
+ "aria-required",
2461
+ "aria-rowindex",
2462
+ "aria-rowspan",
2463
+ "aria-selected",
2464
+ "aria-sort"
2465
+ ]),
2466
+ combobox: /* @__PURE__ */ new Set([
2467
+ "aria-activedescendant",
2468
+ "aria-autocomplete",
2469
+ "aria-expanded",
2470
+ "aria-readonly",
2471
+ "aria-required"
2472
+ ]),
2473
+ command: /* @__PURE__ */ new Set(["aria-expanded"]),
2474
+ complementary: /* @__PURE__ */ new Set(["aria-expanded"]),
2475
+ composite: /* @__PURE__ */ new Set(["aria-activedescendant", "aria-expanded"]),
2476
+ contentinfo: /* @__PURE__ */ new Set(["aria-expanded"]),
2477
+ definition: /* @__PURE__ */ new Set(["aria-expanded"]),
2478
+ dialog: /* @__PURE__ */ new Set(["aria-expanded", "aria-modal"]),
2479
+ directory: /* @__PURE__ */ new Set(["aria-expanded"]),
2480
+ document: /* @__PURE__ */ new Set(["aria-expanded"]),
2481
+ feed: /* @__PURE__ */ new Set(["aria-expanded"]),
2482
+ figure: /* @__PURE__ */ new Set(["aria-expanded"]),
2483
+ form: /* @__PURE__ */ new Set(["aria-expanded"]),
2484
+ grid: /* @__PURE__ */ new Set([
2485
+ "aria-activedescendant",
2486
+ "aria-colcount",
2487
+ "aria-expanded",
2488
+ "aria-level",
2489
+ "aria-multiselectable",
2490
+ "aria-readonly",
2491
+ "aria-rowcount"
2492
+ ]),
2493
+ gridcell: /* @__PURE__ */ new Set([
2494
+ "aria-colindex",
2495
+ "aria-colspan",
2496
+ "aria-expanded",
2497
+ "aria-readonly",
2498
+ "aria-required",
2499
+ "aria-rowindex",
2500
+ "aria-rowspan",
2501
+ "aria-selected"
2502
+ ]),
2503
+ group: /* @__PURE__ */ new Set(["aria-activedescendant", "aria-expanded"]),
2504
+ heading: /* @__PURE__ */ new Set(["aria-expanded", "aria-level"]),
2505
+ img: /* @__PURE__ */ new Set(["aria-expanded"]),
2506
+ input: /* @__PURE__ */ new Set(["aria-expanded"]),
2507
+ landmark: /* @__PURE__ */ new Set(["aria-expanded"]),
2508
+ link: /* @__PURE__ */ new Set(["aria-expanded"]),
2509
+ list: /* @__PURE__ */ new Set(["aria-expanded"]),
2510
+ listbox: /* @__PURE__ */ new Set([
2511
+ "aria-activedescendant",
2512
+ "aria-expanded",
2513
+ "aria-multiselectable",
2514
+ "aria-orientation",
2515
+ "aria-readonly",
2516
+ "aria-required"
2517
+ ]),
2518
+ listitem: /* @__PURE__ */ new Set([
2519
+ "aria-expanded",
2520
+ "aria-level",
2521
+ "aria-posinset",
2522
+ "aria-setsize"
2523
+ ]),
2524
+ log: /* @__PURE__ */ new Set(["aria-expanded"]),
2525
+ main: /* @__PURE__ */ new Set(["aria-expanded"]),
2526
+ marquee: /* @__PURE__ */ new Set(["aria-expanded"]),
2527
+ math: /* @__PURE__ */ new Set(["aria-expanded"]),
2528
+ menu: /* @__PURE__ */ new Set([
2529
+ "aria-activedescendant",
2530
+ "aria-expanded",
2531
+ "aria-orientation"
2532
+ ]),
2533
+ menubar: /* @__PURE__ */ new Set([
2534
+ "aria-activedescendant",
2535
+ "aria-expanded",
2536
+ "aria-orientation"
2537
+ ]),
2538
+ menuitem: /* @__PURE__ */ new Set([
2539
+ "aria-expanded",
2540
+ "aria-posinset",
2541
+ "aria-setsize"
2542
+ ]),
2543
+ menuitemcheckbox: /* @__PURE__ */ new Set([
2544
+ "aria-checked",
2545
+ "aria-expanded",
2546
+ "aria-posinset",
2547
+ "aria-readonly",
2548
+ "aria-setsize"
2549
+ ]),
2550
+ menuitemradio: /* @__PURE__ */ new Set([
2551
+ "aria-checked",
2552
+ "aria-expanded",
2553
+ "aria-posinset",
2554
+ "aria-readonly",
2555
+ "aria-setsize"
2556
+ ]),
2557
+ navigation: /* @__PURE__ */ new Set(["aria-expanded"]),
2558
+ none: /* @__PURE__ */ new Set([]),
2559
+ note: /* @__PURE__ */ new Set(["aria-expanded"]),
2560
+ option: /* @__PURE__ */ new Set([
2561
+ "aria-checked",
2562
+ "aria-expanded",
2563
+ "aria-posinset",
2564
+ "aria-selected",
2565
+ "aria-setsize"
2566
+ ]),
2567
+ presentation: /* @__PURE__ */ new Set([]),
2568
+ progressbar: /* @__PURE__ */ new Set([
2569
+ "aria-expanded",
2570
+ "aria-valuemax",
2571
+ "aria-valuemin",
2572
+ "aria-valuenow",
2573
+ "aria-valuetext"
2574
+ ]),
2575
+ radio: /* @__PURE__ */ new Set([
2576
+ "aria-checked",
2577
+ "aria-expanded",
2578
+ "aria-posinset",
2579
+ "aria-readonly",
2580
+ "aria-setsize"
2581
+ ]),
2582
+ radiogroup: /* @__PURE__ */ new Set([
2583
+ "aria-activedescendant",
2584
+ "aria-expanded",
2585
+ "aria-orientation",
2586
+ "aria-readonly",
2587
+ "aria-required"
2588
+ ]),
2589
+ range: /* @__PURE__ */ new Set([
2590
+ "aria-expanded",
2591
+ "aria-valuemax",
2592
+ "aria-valuemin",
2593
+ "aria-valuenow",
2594
+ "aria-valuetext"
2595
+ ]),
2596
+ region: /* @__PURE__ */ new Set(["aria-expanded"]),
2597
+ roletype: /* @__PURE__ */ new Set(["aria-expanded"]),
2598
+ row: /* @__PURE__ */ new Set([
2599
+ "aria-activedescendant",
2600
+ "aria-colindex",
2601
+ "aria-expanded",
2602
+ "aria-level",
2603
+ "aria-rowindex",
2604
+ "aria-selected"
2605
+ ]),
2606
+ rowgroup: /* @__PURE__ */ new Set(["aria-activedescendant", "aria-expanded"]),
2607
+ rowheader: /* @__PURE__ */ new Set([
2608
+ "aria-colindex",
2609
+ "aria-colspan",
2610
+ "aria-expanded",
2611
+ "aria-readonly",
2612
+ "aria-required",
2613
+ "aria-rowindex",
2614
+ "aria-rowspan",
2615
+ "aria-selected",
2616
+ "aria-sort"
2617
+ ]),
2618
+ scrollbar: /* @__PURE__ */ new Set([
2619
+ "aria-controls",
2620
+ "aria-expanded",
2621
+ "aria-orientation",
2622
+ "aria-valuemax",
2623
+ "aria-valuemin",
2624
+ "aria-valuenow",
2625
+ "aria-valuetext"
2626
+ ]),
2627
+ search: /* @__PURE__ */ new Set(["aria-expanded"]),
2628
+ searchbox: /* @__PURE__ */ new Set([
2629
+ "aria-activedescendant",
2630
+ "aria-autocomplete",
2631
+ "aria-expanded",
2632
+ "aria-multiline",
2633
+ "aria-placeholder",
2634
+ "aria-readonly",
2635
+ "aria-required"
2636
+ ]),
2637
+ section: /* @__PURE__ */ new Set(["aria-expanded"]),
2638
+ sectionhead: /* @__PURE__ */ new Set(["aria-expanded"]),
2639
+ select: /* @__PURE__ */ new Set([
2640
+ "aria-activedescendant",
2641
+ "aria-expanded",
2642
+ "aria-orientation"
2643
+ ]),
2644
+ separator: /* @__PURE__ */ new Set([
2645
+ "aria-expanded",
2646
+ "aria-orientation",
2647
+ "aria-valuemax",
2648
+ "aria-valuemin",
2649
+ "aria-valuenow",
2650
+ "aria-valuetext"
2651
+ ]),
2652
+ slider: /* @__PURE__ */ new Set([
2653
+ "aria-expanded",
2654
+ "aria-orientation",
2655
+ "aria-readonly",
2656
+ "aria-valuemax",
2657
+ "aria-valuemin",
2658
+ "aria-valuenow",
2659
+ "aria-valuetext"
2660
+ ]),
2661
+ spinbutton: /* @__PURE__ */ new Set([
2662
+ "aria-activedescendant",
2663
+ "aria-expanded",
2664
+ "aria-readonly",
2665
+ "aria-required",
2666
+ "aria-valuemax",
2667
+ "aria-valuemin",
2668
+ "aria-valuenow",
2669
+ "aria-valuetext"
2670
+ ]),
2671
+ status: /* @__PURE__ */ new Set(["aria-expanded"]),
2672
+ structure: /* @__PURE__ */ new Set(["aria-expanded"]),
2673
+ switch: /* @__PURE__ */ new Set([
2674
+ "aria-checked",
2675
+ "aria-expanded",
2676
+ "aria-readonly",
2677
+ "aria-required"
2678
+ ]),
2679
+ tab: /* @__PURE__ */ new Set([
2680
+ "aria-expanded",
2681
+ "aria-posinset",
2682
+ "aria-selected",
2683
+ "aria-setsize"
2684
+ ]),
2685
+ table: /* @__PURE__ */ new Set([
2686
+ "aria-colcount",
2687
+ "aria-expanded",
2688
+ "aria-level",
2689
+ "aria-multiselectable",
2690
+ "aria-rowcount"
2691
+ ]),
2692
+ tablist: /* @__PURE__ */ new Set([
2693
+ "aria-activedescendant",
2694
+ "aria-expanded",
2695
+ "aria-level",
2696
+ "aria-multiselectable",
2697
+ "aria-orientation"
2698
+ ]),
2699
+ tabpanel: /* @__PURE__ */ new Set(["aria-expanded"]),
2700
+ term: /* @__PURE__ */ new Set(["aria-expanded"]),
2701
+ textbox: /* @__PURE__ */ new Set([
2702
+ "aria-activedescendant",
2703
+ "aria-autocomplete",
2704
+ "aria-expanded",
2705
+ "aria-multiline",
2706
+ "aria-placeholder",
2707
+ "aria-readonly",
2708
+ "aria-required"
2709
+ ]),
2710
+ timer: /* @__PURE__ */ new Set(["aria-expanded"]),
2711
+ toolbar: /* @__PURE__ */ new Set([
2712
+ "aria-activedescendant",
2713
+ "aria-expanded",
2714
+ "aria-orientation"
2715
+ ]),
2716
+ tooltip: /* @__PURE__ */ new Set(["aria-expanded"]),
2717
+ tree: /* @__PURE__ */ new Set([
2718
+ "aria-activedescendant",
2719
+ "aria-expanded",
2720
+ "aria-multiselectable",
2721
+ "aria-orientation",
2722
+ "aria-required"
2723
+ ]),
2724
+ treegrid: /* @__PURE__ */ new Set([
2725
+ "aria-activedescendant",
2726
+ "aria-colcount",
2727
+ "aria-expanded",
2728
+ "aria-level",
2729
+ "aria-multiselectable",
2730
+ "aria-orientation",
2731
+ "aria-readonly",
2732
+ "aria-required",
2733
+ "aria-rowcount"
2734
+ ]),
2735
+ treeitem: /* @__PURE__ */ new Set([
2736
+ "aria-checked",
2737
+ "aria-expanded",
2738
+ "aria-level",
2739
+ "aria-posinset",
2740
+ "aria-selected",
2741
+ "aria-setsize"
2742
+ ]),
2743
+ widget: /* @__PURE__ */ new Set(["aria-expanded"]),
2744
+ window: /* @__PURE__ */ new Set(["aria-expanded", "aria-modal"])
2745
+ };
2746
+ const implicitRoles = {
2747
+ a: "link",
2748
+ article: "article",
2749
+ aside: "complementary",
2750
+ button: "button",
2751
+ dialog: "dialog",
2752
+ footer: "contentinfo",
2753
+ form: "form",
2754
+ h1: "heading",
2755
+ h2: "heading",
2756
+ h3: "heading",
2757
+ h4: "heading",
2758
+ h5: "heading",
2759
+ h6: "heading",
2760
+ header: "banner",
2761
+ hr: "separator",
2762
+ img: "img",
2763
+ input: "textbox",
2764
+ li: "listitem",
2765
+ main: "main",
2766
+ nav: "navigation",
2767
+ ol: "list",
2768
+ section: "region",
2769
+ select: "listbox",
2770
+ textarea: "textbox",
2771
+ ul: "list"
2772
+ };
2773
+ function getSupportedPropsForRole(role) {
2774
+ return /* @__PURE__ */ new Set([...roleToSupportedProps[role] ?? [], ...globalAriaProperties]);
2775
+ }
2776
+ var roleSupportedAriaProps_default = ruleCreator.createRule(typescriptLanguage, {
2777
+ about: {
2778
+ description: "Reports ARIA properties that are not supported by an element's role.",
2779
+ id: "roleSupportedAriaProps",
2780
+ presets: ["logical", "logicalStrict"]
2781
+ },
2782
+ messages: { unsupportedProp: {
2783
+ primary: "The `{{ prop }}` ARIA property is not supported by the `{{ role }}` role.",
2784
+ secondary: [
2785
+ "Elements with ARIA roles should only use ARIA properties that are supported by that role.",
2786
+ "Using unsupported ARIA properties can confuse assistive technologies.",
2787
+ "This is required for WCAG 4.1.2 compliance."
2788
+ ],
2789
+ suggestions: ["Remove the unsupported ARIA property or change the element's role."]
2790
+ } },
2791
+ setup(context) {
2792
+ function checkElement(node, { sourceFile }) {
2793
+ if (node.tagName.kind !== SyntaxKind.Identifier) return;
2794
+ const elementName = node.tagName.text.toLowerCase();
2795
+ const roleProperty = node.attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "role");
2796
+ const role = roleProperty?.kind === SyntaxKind.JsxAttribute && roleProperty.initializer?.kind === SyntaxKind.StringLiteral ? roleProperty.initializer.text.toLowerCase() : implicitRoles[elementName];
2797
+ if (!role) return;
2798
+ const supportedProps = getSupportedPropsForRole(role);
2799
+ for (const property of node.attributes.properties) {
2800
+ if (property.kind !== SyntaxKind.JsxAttribute || property.name.kind !== SyntaxKind.Identifier) continue;
2801
+ const propertyName = property.name.text.toLowerCase();
2802
+ if (!propertyName.startsWith("aria-") || supportedProps.has(propertyName)) continue;
2803
+ context.report({
2804
+ data: {
2805
+ prop: propertyName,
2806
+ role
2807
+ },
2808
+ message: "unsupportedProp",
2809
+ range: getTSNodeRange(property.name, sourceFile)
2810
+ });
2811
+ }
2812
+ }
2813
+ return { visitors: {
2814
+ JsxOpeningElement: checkElement,
2815
+ JsxSelfClosingElement: checkElement
2816
+ } };
2817
+ }
2818
+ });
2819
+ //#endregion
2820
+ //#region src/rules/roleTags.ts
2821
+ const roleToElement = {
2822
+ article: "article",
2823
+ banner: "header",
2824
+ button: "button",
2825
+ checkbox: "input[type='checkbox']",
2826
+ complementary: "aside",
2827
+ contentinfo: "footer",
2828
+ definition: "dd",
2829
+ dialog: "dialog",
2830
+ figure: "figure",
2831
+ form: "form",
2832
+ heading: "h1-h6",
2833
+ img: "img",
2834
+ link: "a",
2835
+ list: "ul/ol",
2836
+ listitem: "li",
2837
+ main: "main",
2838
+ navigation: "nav",
2839
+ radio: "input[type='radio']",
2840
+ region: "section",
2841
+ row: "tr",
2842
+ rowgroup: "tbody/tfoot/thead",
2843
+ table: "table",
2844
+ term: "dt",
2845
+ textbox: "input[type='text']/textarea"
2846
+ };
2847
+ var roleTags_default = ruleCreator.createRule(typescriptLanguage, {
2848
+ about: {
2849
+ description: "Reports ARIA roles that have semantic HTML element equivalents.",
2850
+ id: "roleTags",
2851
+ presets: ["logical", "logicalStrict"]
2852
+ },
2853
+ messages: { preferSemanticElement: {
2854
+ primary: "<{{ currentElement }}> with role='{{ role }}' is a less-accessible equivalent to <{{ semanticElement }}>.",
2855
+ secondary: [
2856
+ "Semantic HTML elements have built-in accessibility features.",
2857
+ "Using native elements is more maintainable than ARIA roles.",
2858
+ "Browsers provide better default behavior for semantic elements."
2859
+ ],
2860
+ suggestions: ["Replace with the semantic HTML element"]
2861
+ } },
2862
+ setup(context) {
2863
+ function checkRole(node, { sourceFile }) {
2864
+ if (node.tagName.kind !== SyntaxKind.Identifier || node.tagName.text.toLowerCase() !== node.tagName.text) return;
2865
+ const roleProperty = node.attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "role");
2866
+ if (roleProperty?.kind !== SyntaxKind.JsxAttribute || roleProperty.initializer?.kind !== SyntaxKind.StringLiteral) return;
2867
+ const role = roleProperty.initializer.text;
2868
+ const semanticElement = roleToElement[role];
2869
+ if (semanticElement) context.report({
2870
+ data: {
2871
+ currentElement: node.tagName.text,
2872
+ role,
2873
+ semanticElement
2874
+ },
2875
+ message: "preferSemanticElement",
2876
+ range: getTSNodeRange(roleProperty, sourceFile)
2877
+ });
2878
+ }
2879
+ return { visitors: {
2880
+ JsxOpeningElement: checkRole,
2881
+ JsxSelfClosingElement: checkRole
2882
+ } };
2883
+ }
2884
+ });
2885
+ //#endregion
2886
+ //#region src/rules/scopeProps.ts
2887
+ var scopeProps_default = ruleCreator.createRule(typescriptLanguage, {
2888
+ about: {
2889
+ description: "Reports scope props on non-th elements.",
2890
+ id: "scopeProps",
2891
+ presets: ["logical", "logicalStrict"]
2892
+ },
2893
+ messages: { invalidScope: {
2894
+ primary: "The `scope` prop only has an effect on <th> elements.",
2895
+ secondary: [
2896
+ "The scope attribute defines whether a table header is a column header or row header.",
2897
+ "Using it on non-<th> elements has no semantic meaning and may confuse assistive technologies.",
2898
+ "This is required for WCAG 1.3.1 and 4.1.1 compliance."
2899
+ ],
2900
+ suggestions: ["Remove the scope prop from this element", "Change this element to a <th> element if it's a table header"]
2901
+ } },
2902
+ setup(context) {
2903
+ function checkElement(node, { sourceFile }) {
2904
+ if (node.tagName.kind !== SyntaxKind.Identifier || node.tagName.text.toLowerCase() === "th") return;
2905
+ const scopeProperty = node.attributes.properties.find((property) => {
2906
+ return property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text.toLowerCase() === "scope";
2907
+ });
2908
+ if (!scopeProperty) return;
2909
+ context.report({
2910
+ message: "invalidScope",
2911
+ range: getTSNodeRange(scopeProperty, sourceFile)
2912
+ });
2913
+ }
2914
+ return { visitors: {
2915
+ JsxOpeningElement: checkElement,
2916
+ JsxSelfClosingElement: checkElement
2917
+ } };
2918
+ }
2919
+ });
2920
+ //#endregion
2921
+ //#region src/rules/staticElementInteractions.ts
2922
+ const interactiveHandlers = [
2923
+ "onClick",
2924
+ "onKeyDown",
2925
+ "onKeyPress",
2926
+ "onKeyUp",
2927
+ "onMouseDown",
2928
+ "onMouseUp"
2929
+ ];
2930
+ const interactiveElements = /* @__PURE__ */ new Set([
2931
+ "a",
2932
+ "button",
2933
+ "input",
2934
+ "select",
2935
+ "textarea"
2936
+ ]);
2937
+ var staticElementInteractions_default = ruleCreator.createRule(typescriptLanguage, {
2938
+ about: {
2939
+ description: "Reports static elements with event handlers that lack ARIA roles.",
2940
+ id: "staticElementInteractions",
2941
+ presets: ["logical", "logicalStrict"]
2942
+ },
2943
+ messages: { missingRole: {
2944
+ primary: "This static element that handles events is missing a role attribute.",
2945
+ secondary: [
2946
+ "Static HTML elements like <div> and <span> have no semantic meaning.",
2947
+ "Interactive handlers require a role to convey purpose to assistive technology and search engine crawlers.",
2948
+ "Required for WCAG 4.1.2 compliance."
2949
+ ],
2950
+ suggestions: ["Add a role attribute (e.g. `role='button'`)", "Use a semantic HTML element instead (e.g. `<button>`)"]
2951
+ } },
2952
+ setup(context) {
2953
+ function checkElement(node, { sourceFile }) {
2954
+ if (node.tagName.kind !== SyntaxKind.Identifier || node.tagName.text.toLowerCase() !== node.tagName.text) return;
2955
+ const elementName = node.tagName.text.toLowerCase();
2956
+ if (interactiveElements.has(elementName)) return;
2957
+ let hadInteractiveHandler = false;
2958
+ for (const property of node.attributes.properties) if (property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier) {
2959
+ if (property.name.text === "role") return;
2960
+ if (interactiveHandlers.includes(property.name.text)) hadInteractiveHandler = true;
2961
+ }
2962
+ if (!hadInteractiveHandler) return;
2963
+ context.report({
2964
+ message: "missingRole",
2965
+ range: getTSNodeRange(node.tagName, sourceFile)
2966
+ });
2967
+ }
2968
+ return { visitors: {
2969
+ JsxOpeningElement: checkElement,
2970
+ JsxSelfClosingElement: checkElement
2971
+ } };
2972
+ }
2973
+ });
2974
+ //#endregion
2975
+ //#region src/rules/svgTitles.ts
2976
+ var svgTitles_default = ruleCreator.createRule(typescriptLanguage, {
2977
+ about: {
2978
+ description: "Reports <svg> elements without a <title> child element.",
2979
+ id: "svgTitles",
2980
+ presets: ["logical", "logicalStrict"]
2981
+ },
2982
+ messages: { missingTitle: {
2983
+ primary: "This <svg> element is missing a <title> child element.",
2984
+ secondary: [
2985
+ "SVG elements without a title are not accessible to screen readers.",
2986
+ "The <title> element provides a text description of the SVG's content.",
2987
+ "This is required for WCAG 1.1.1 compliance."
2988
+ ],
2989
+ suggestions: ["Add a <title> child element with descriptive text", "Use aria-label or aria-labelledby as an alternative"]
2990
+ } },
2991
+ setup(context) {
2992
+ function hasValidAriaLabel(attributes) {
2993
+ return attributes.properties.some((property) => {
2994
+ if (property.kind !== SyntaxKind.JsxAttribute || property.name.kind !== SyntaxKind.Identifier || property.name.text !== "aria-label" && property.name.text !== "aria-labelledby") return false;
2995
+ if (!property.initializer) return false;
2996
+ if (property.initializer.kind === SyntaxKind.JsxExpression) {
2997
+ const { expression } = property.initializer;
2998
+ if (!expression) return false;
2999
+ if (expression.kind === SyntaxKind.StringLiteral || expression.kind === SyntaxKind.NoSubstitutionTemplateLiteral) return expression.text !== "";
3000
+ if (expression.kind === SyntaxKind.Identifier) return expression.text !== "undefined";
3001
+ }
3002
+ if (property.initializer.kind === SyntaxKind.StringLiteral) return property.initializer.text !== "";
3003
+ return false;
3004
+ });
3005
+ }
3006
+ function checkElement(node, { sourceFile }) {
3007
+ const tagName = node.kind === SyntaxKind.JsxElement ? node.openingElement.tagName : node.tagName;
3008
+ if (tagName.kind !== SyntaxKind.Identifier || tagName.text.toLowerCase() !== "svg") return;
3009
+ if (hasValidAriaLabel(node.kind === SyntaxKind.JsxElement ? node.openingElement.attributes : node.attributes)) return;
3010
+ if (node.kind === SyntaxKind.JsxElement && node.children.some(isTitleElement)) return;
3011
+ context.report({
3012
+ message: "missingTitle",
3013
+ range: getTSNodeRange(tagName, sourceFile)
3014
+ });
3015
+ }
3016
+ return { visitors: {
3017
+ JsxElement: checkElement,
3018
+ JsxSelfClosingElement: checkElement
3019
+ } };
3020
+ }
3021
+ });
3022
+ function isTitleElement(node) {
3023
+ if (node.kind !== SyntaxKind.JsxElement && node.kind !== SyntaxKind.JsxSelfClosingElement) return false;
3024
+ const childTagName = node.kind === SyntaxKind.JsxElement ? node.openingElement.tagName : node.tagName;
3025
+ return childTagName.kind === SyntaxKind.Identifier && childTagName.text === "title";
3026
+ }
3027
+ //#endregion
3028
+ //#region src/rules/tabIndexPositiveValues.ts
3029
+ var tabIndexPositiveValues_default = ruleCreator.createRule(typescriptLanguage, {
3030
+ about: {
3031
+ description: "Reports positive tabIndex values.",
3032
+ id: "tabIndexPositiveValues",
3033
+ presets: ["logical", "logicalStrict"]
3034
+ },
3035
+ messages: { noPositiveTabIndex: {
3036
+ primary: "Positive `tabIndex` values disrupt tab order and make keyboard navigation unpredictable.",
3037
+ secondary: [
3038
+ "Injecting elements on top of the default tab order with positive `tabIndex` values changes from the intuitive order for users.",
3039
+ "Use tabIndex=\"0\" to include elements in the natural tab order, or tabIndex=\"-1\" to make them programmatically focusable.",
3040
+ "This is required for WCAG 2.4.3 compliance."
3041
+ ],
3042
+ suggestions: [
3043
+ "Use tabIndex=\"0\" for elements that should be in the tab order",
3044
+ "Use tabIndex=\"-1\" for elements that should be programmatically focusable",
3045
+ "Remove tabIndex to use the default tab order"
3046
+ ]
3047
+ } },
3048
+ setup(context) {
3049
+ return { visitors: { JsxAttribute(node, { sourceFile }) {
3050
+ if (node.name.kind !== SyntaxKind.Identifier || node.name.text.toLowerCase() !== "tabindex" || !node.initializer) return;
3051
+ const value = getInitializerValue(node.initializer);
3052
+ if (value !== void 0 && value > 0) context.report({
3053
+ message: "noPositiveTabIndex",
3054
+ range: getTSNodeRange(node, sourceFile)
3055
+ });
3056
+ } } };
3057
+ }
3058
+ });
3059
+ function getInitializerValue(initializer) {
3060
+ if (initializer.kind === SyntaxKind.StringLiteral) {
3061
+ const parsed = Number(initializer.text);
3062
+ return Number.isNaN(parsed) ? void 0 : parsed;
3063
+ }
3064
+ if (initializer.kind === SyntaxKind.JsxExpression) return initializer.expression?.kind === SyntaxKind.NumericLiteral ? Number(initializer.expression.text) : void 0;
3065
+ }
3066
+ //#endregion
3067
+ //#region src/rules/unescapedEntities.ts
3068
+ const problematicEntities = [
3069
+ {
3070
+ entity: "\"",
3071
+ toBrace: "{\"\\\"\"}",
3072
+ toHTML: "&quot;"
3073
+ },
3074
+ {
3075
+ entity: "'",
3076
+ toBrace: "{\"'\"}",
3077
+ toHTML: "&#39;"
3078
+ },
3079
+ {
3080
+ entity: ">",
3081
+ toBrace: "{'>'}",
3082
+ toHTML: "&gt;"
3083
+ },
3084
+ {
3085
+ entity: "}",
3086
+ toBrace: "{'}'}",
3087
+ toHTML: "&#125;"
3088
+ }
3089
+ ];
3090
+ var unescapedEntities_default = ruleCreator.createRule(typescriptLanguage, {
3091
+ about: {
3092
+ description: "Disallow unescaped HTML entities in JSX text that may cause rendering issues.",
3093
+ id: "unescapedEntities",
3094
+ presets: ["stylistic"]
3095
+ },
3096
+ messages: { unescapedEntity: {
3097
+ primary: "This unescaped entity `{{ entity }}` may not render properly.",
3098
+ secondary: ["Certain characters like >, <, \", ', {, and } can cause issues in JSX when not properly escaped.", "React may not render these characters correctly or they might break JSX syntax."],
3099
+ suggestions: ["Use HTML entities: {{ html }}", "Wrap in JSX expression: {{ brace }}"]
3100
+ } },
3101
+ setup(context) {
3102
+ return { visitors: { JsxText(node, { sourceFile }) {
3103
+ const nodeStart = node.getStart(sourceFile);
3104
+ const reports = [];
3105
+ for (const { entity, toBrace, toHTML } of problematicEntities) {
3106
+ let index = 0;
3107
+ while ((index = node.text.indexOf(entity, index)) !== -1) {
3108
+ reports.push({
3109
+ begin: nodeStart + index,
3110
+ data: {
3111
+ brace: toBrace,
3112
+ entity,
3113
+ html: toHTML
3114
+ },
3115
+ end: nodeStart + index + entity.length
3116
+ });
3117
+ index += entity.length;
3118
+ }
3119
+ }
3120
+ for (const report of reports.toSorted((a, b) => a.begin - b.begin)) context.report({
3121
+ data: report.data,
3122
+ message: "unescapedEntity",
3123
+ range: {
3124
+ begin: report.begin,
3125
+ end: report.end
3126
+ }
3127
+ });
3128
+ } } };
3129
+ }
3130
+ });
3131
+ //#endregion
3132
+ //#region src/rules/unnecessaryFragments.ts
3133
+ var unnecessaryFragments_default = ruleCreator.createRule(typescriptLanguage, {
3134
+ about: {
3135
+ description: "Disallow unnecessary JSX fragments that wrap a single child or have no children.",
3136
+ id: "unnecessaryFragments",
3137
+ presets: ["stylistic"]
3138
+ },
3139
+ messages: { unnecessaryFragment: {
3140
+ primary: "Unnecessary fragment wrapping {{ childType }}.",
3141
+ secondary: [
3142
+ "Fragments are only needed when rendering multiple adjacent elements.",
3143
+ "A fragment wrapping a single child or no children adds unnecessary complexity.",
3144
+ "Remove the fragment and use the child directly, or remove empty fragments entirely."
3145
+ ],
3146
+ suggestions: ["Remove the fragment wrapper and use the child element directly."]
3147
+ } },
3148
+ setup(context) {
3149
+ function checkNodeChildren(node, range) {
3150
+ const children = node.children.filter((child) => child.kind !== SyntaxKind.JsxText || !!child.text.trim().length);
3151
+ let childType;
3152
+ if (!children.length) childType = "no children";
3153
+ else if (children.length === 1) childType = "a single child";
3154
+ if (childType) context.report({
3155
+ data: { childType },
3156
+ message: "unnecessaryFragment",
3157
+ range
3158
+ });
3159
+ }
3160
+ return { visitors: {
3161
+ JsxElement(node, { sourceFile }) {
3162
+ if (node.openingElement.tagName.kind === SyntaxKind.Identifier && !node.openingElement.attributes.properties.length && node.openingElement.tagName.text === "Fragment") checkNodeChildren(node, {
3163
+ begin: node.openingElement.getStart(sourceFile),
3164
+ end: node.closingElement.getEnd()
3165
+ });
3166
+ },
3167
+ JsxFragment(node, { sourceFile }) {
3168
+ checkNodeChildren(node, {
3169
+ begin: node.openingFragment.getStart(sourceFile),
3170
+ end: node.closingFragment.getEnd()
3171
+ });
3172
+ }
3173
+ } };
3174
+ }
3175
+ });
3176
+ //#endregion
3177
+ //#region src/plugin.ts
3178
+ const jsFiles = ["**/*.jsx"];
3179
+ const tsFiles = ["**/*.tsx"];
3180
+ const jsx = createPlugin({
3181
+ files: {
3182
+ all: [...jsFiles, ...tsFiles],
3183
+ javascript: jsFiles,
3184
+ typescript: tsFiles
3185
+ },
3186
+ name: "JSX",
3187
+ rules: [
3188
+ accessKeys_default,
3189
+ altTexts_default,
3190
+ anchorAmbiguousText_default,
3191
+ anchorContent_default,
3192
+ anchorValidity_default,
3193
+ ariaActiveDescendantTabIndex_default,
3194
+ ariaHiddenFocusables_default,
3195
+ ariaProps_default,
3196
+ ariaPropTypes_default,
3197
+ ariaRoleValidity_default,
3198
+ ariaUnsupportedElements_default,
3199
+ autocomplete_default,
3200
+ autoFocusProps_default,
3201
+ booleanValues_default,
3202
+ bracedStatements_default,
3203
+ buttonTypes_default,
3204
+ childrenProps_default,
3205
+ clickEventKeyEvents_default,
3206
+ commentTextNodes_default,
3207
+ distractingElements_default,
3208
+ elementChildrenValidity_default,
3209
+ headingContents_default,
3210
+ htmlLangs_default,
3211
+ iframeTitles_default,
3212
+ interactiveElementRoles_default,
3213
+ interactiveElementsFocusable_default,
3214
+ labelAssociatedControls_default,
3215
+ langValidity_default,
3216
+ mediaCaptions_default,
3217
+ mouseEventKeyEvents_default,
3218
+ nonInteractiveElementInteractions_default,
3219
+ nonInteractiveElementRoles_default,
3220
+ nonInteractiveElementTabIndexes_default,
3221
+ propDuplicates_default,
3222
+ roleRedundancies_default,
3223
+ roleRequiredAriaProps_default,
3224
+ roleSupportedAriaProps_default,
3225
+ roleTags_default,
3226
+ scopeProps_default,
3227
+ staticElementInteractions_default,
3228
+ svgTitles_default,
3229
+ tabIndexPositiveValues_default,
3230
+ unescapedEntities_default,
3231
+ unnecessaryFragments_default
3232
+ ]
3233
+ });
3234
+ //#endregion
3235
+ export { jsx };