@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flint.fyi/jsx",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "[Experimental] A Flint plugin for JSX code.",
5
5
  "homepage": "https://flint.fyi/rules/jsx",
6
6
  "repository": {
@@ -16,23 +16,24 @@
16
16
  "sideEffects": false,
17
17
  "type": "module",
18
18
  "exports": {
19
- ".": "./lib/index.js"
19
+ ".": "./dist/index.mjs"
20
20
  },
21
21
  "files": [
22
- "lib/"
22
+ "dist/"
23
23
  ],
24
24
  "dependencies": {
25
25
  "language-tags": "^2.1.0",
26
26
  "typescript": "^6.0.0",
27
- "@flint.fyi/typescript-language": "^0.18.4",
28
- "@flint.fyi/core": "^0.23.3",
29
- "@flint.fyi/utils": "^0.14.1"
27
+ "@flint.fyi/core": "^0.24.0",
28
+ "@flint.fyi/typescript-language": "^0.19.0",
29
+ "@flint.fyi/utils": "^0.15.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/language-tags": "1.0.4",
33
33
  "tsdown": "0.22.3",
34
34
  "vitest": "4.1.0",
35
- "@flint.fyi/rule-tester": "^0.16.4"
35
+ "@flint.fyi/rule-tester": "^0.17.0",
36
+ "@flint.fyi/build": "^0.1.0"
36
37
  },
37
38
  "engines": {
38
39
  "node": ">=24.0.0"
package/lib/index.d.ts DELETED
@@ -1,2 +0,0 @@
1
- import { jsx } from "./plugin.js";
2
- export { jsx };
package/lib/index.js DELETED
@@ -1,2 +0,0 @@
1
- import { jsx } from "./plugin.js";
2
- export { jsx };
package/lib/plugin.js DELETED
@@ -1,104 +0,0 @@
1
- import accessKeys_default from "./rules/accessKeys.js";
2
- import altTexts_default from "./rules/altTexts.js";
3
- import anchorAmbiguousText_default from "./rules/anchorAmbiguousText.js";
4
- import anchorContent_default from "./rules/anchorContent.js";
5
- import anchorValidity_default from "./rules/anchorValidity.js";
6
- import ariaActiveDescendantTabIndex_default from "./rules/ariaActiveDescendantTabIndex.js";
7
- import ariaHiddenFocusables_default from "./rules/ariaHiddenFocusables.js";
8
- import ariaProps_default from "./rules/ariaProps.js";
9
- import ariaPropTypes_default from "./rules/ariaPropTypes.js";
10
- import ariaRoleValidity_default from "./rules/ariaRoleValidity.js";
11
- import ariaUnsupportedElements_default from "./rules/ariaUnsupportedElements.js";
12
- import autocomplete_default from "./rules/autocomplete.js";
13
- import autoFocusProps_default from "./rules/autoFocusProps.js";
14
- import booleanValues_default from "./rules/booleanValues.js";
15
- import bracedStatements_default from "./rules/bracedStatements.js";
16
- import buttonTypes_default from "./rules/buttonTypes.js";
17
- import childrenProps_default from "./rules/childrenProps.js";
18
- import clickEventKeyEvents_default from "./rules/clickEventKeyEvents.js";
19
- import commentTextNodes_default from "./rules/commentTextNodes.js";
20
- import distractingElements_default from "./rules/distractingElements.js";
21
- import elementChildrenValidity_default from "./rules/elementChildrenValidity.js";
22
- import headingContents_default from "./rules/headingContents.js";
23
- import htmlLangs_default from "./rules/htmlLangs.js";
24
- import iframeTitles_default from "./rules/iframeTitles.js";
25
- import interactiveElementRoles_default from "./rules/interactiveElementRoles.js";
26
- import interactiveElementsFocusable_default from "./rules/interactiveElementsFocusable.js";
27
- import labelAssociatedControls_default from "./rules/labelAssociatedControls.js";
28
- import langValidity_default from "./rules/langValidity.js";
29
- import mediaCaptions_default from "./rules/mediaCaptions.js";
30
- import mouseEventKeyEvents_default from "./rules/mouseEventKeyEvents.js";
31
- import nonInteractiveElementInteractions_default from "./rules/nonInteractiveElementInteractions.js";
32
- import nonInteractiveElementRoles_default from "./rules/nonInteractiveElementRoles.js";
33
- import nonInteractiveElementTabIndexes_default from "./rules/nonInteractiveElementTabIndexes.js";
34
- import propDuplicates_default from "./rules/propDuplicates.js";
35
- import roleRedundancies_default from "./rules/roleRedundancies.js";
36
- import roleRequiredAriaProps_default from "./rules/roleRequiredAriaProps.js";
37
- import roleSupportedAriaProps_default from "./rules/roleSupportedAriaProps.js";
38
- import roleTags_default from "./rules/roleTags.js";
39
- import scopeProps_default from "./rules/scopeProps.js";
40
- import staticElementInteractions_default from "./rules/staticElementInteractions.js";
41
- import svgTitles_default from "./rules/svgTitles.js";
42
- import tabIndexPositiveValues_default from "./rules/tabIndexPositiveValues.js";
43
- import unescapedEntities_default from "./rules/unescapedEntities.js";
44
- import unnecessaryFragments_default from "./rules/unnecessaryFragments.js";
45
- import { createPlugin } from "@flint.fyi/core";
46
- //#region src/plugin.ts
47
- const jsFiles = ["**/*.jsx"];
48
- const tsFiles = ["**/*.tsx"];
49
- const jsx = createPlugin({
50
- files: {
51
- all: [...jsFiles, ...tsFiles],
52
- javascript: jsFiles,
53
- typescript: tsFiles
54
- },
55
- name: "JSX",
56
- rules: [
57
- accessKeys_default,
58
- altTexts_default,
59
- anchorAmbiguousText_default,
60
- anchorContent_default,
61
- anchorValidity_default,
62
- ariaActiveDescendantTabIndex_default,
63
- ariaHiddenFocusables_default,
64
- ariaProps_default,
65
- ariaPropTypes_default,
66
- ariaRoleValidity_default,
67
- ariaUnsupportedElements_default,
68
- autocomplete_default,
69
- autoFocusProps_default,
70
- booleanValues_default,
71
- bracedStatements_default,
72
- buttonTypes_default,
73
- childrenProps_default,
74
- clickEventKeyEvents_default,
75
- commentTextNodes_default,
76
- distractingElements_default,
77
- elementChildrenValidity_default,
78
- headingContents_default,
79
- htmlLangs_default,
80
- iframeTitles_default,
81
- interactiveElementRoles_default,
82
- interactiveElementsFocusable_default,
83
- labelAssociatedControls_default,
84
- langValidity_default,
85
- mediaCaptions_default,
86
- mouseEventKeyEvents_default,
87
- nonInteractiveElementInteractions_default,
88
- nonInteractiveElementRoles_default,
89
- nonInteractiveElementTabIndexes_default,
90
- propDuplicates_default,
91
- roleRedundancies_default,
92
- roleRequiredAriaProps_default,
93
- roleSupportedAriaProps_default,
94
- roleTags_default,
95
- scopeProps_default,
96
- staticElementInteractions_default,
97
- svgTitles_default,
98
- tabIndexPositiveValues_default,
99
- unescapedEntities_default,
100
- unnecessaryFragments_default
101
- ]
102
- });
103
- //#endregion
104
- export { jsx };
@@ -1,9 +0,0 @@
1
- declare const _default: import("@flint.fyi/core").Rule<{
2
- readonly description: "Disallow the use of the `accessKey` / `accesskey` attribute on JSX elements.";
3
- readonly id: "accessKeys";
4
- readonly presets: readonly ["logical", "logicalStrict"];
5
- } & {
6
- readonly pluginId: string;
7
- readonly url: string;
8
- }, "avoidAccessKey", undefined>;
9
- export default _default;
@@ -1,30 +0,0 @@
1
- import { ruleCreator } from "./ruleCreator.js";
2
- import { SyntaxKind } from "typescript";
3
- import { getTSNodeRange, typescriptLanguage } from "@flint.fyi/typescript-language";
4
- //#region src/rules/accessKeys.ts
5
- var accessKeys_default = ruleCreator.createRule(typescriptLanguage, {
6
- about: {
7
- description: "Disallow the use of the `accessKey` / `accesskey` attribute on JSX elements.",
8
- id: "accessKeys",
9
- presets: ["logical", "logicalStrict"]
10
- },
11
- messages: { avoidAccessKey: {
12
- primary: "The native DOM `{{ attribute }}` prop causes accessibility issues with keyboard-only users and screen readers.",
13
- 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."],
14
- suggestions: ["Remove the attribute and provide a documented, configurable keyboard shortcut instead."]
15
- } },
16
- setup(context) {
17
- return { visitors: { JsxAttribute(node, { sourceFile }) {
18
- if (node.name.kind !== SyntaxKind.Identifier) return;
19
- const attribute = node.name.text;
20
- if (attribute.toLowerCase() !== "accesskey") return;
21
- context.report({
22
- data: { attribute },
23
- message: "avoidAccessKey",
24
- range: getTSNodeRange(node.name, sourceFile)
25
- });
26
- } } };
27
- }
28
- });
29
- //#endregion
30
- export { accessKeys_default as default };
@@ -1,9 +0,0 @@
1
- declare const _default: import("@flint.fyi/core").Rule<{
2
- readonly description: "Reports elements that require alt text but are missing it.";
3
- readonly id: "altTexts";
4
- readonly presets: readonly ["logical", "logicalStrict"];
5
- } & {
6
- readonly pluginId: string;
7
- readonly url: string;
8
- }, "missingAlt", undefined>;
9
- export default _default;
@@ -1,76 +0,0 @@
1
- import { ruleCreator } from "./ruleCreator.js";
2
- import { SyntaxKind } from "typescript";
3
- import { getTSNodeRange, typescriptLanguage } from "@flint.fyi/typescript-language";
4
- //#region src/rules/altTexts.ts
5
- const alternateProperties = /* @__PURE__ */ new Set([
6
- "aria-label",
7
- "aria-labelledby",
8
- "title"
9
- ]);
10
- var altTexts_default = ruleCreator.createRule(typescriptLanguage, {
11
- about: {
12
- description: "Reports elements that require alt text but are missing it.",
13
- id: "altTexts",
14
- presets: ["logical", "logicalStrict"]
15
- },
16
- messages: { missingAlt: {
17
- primary: "{{ element }} element is missing alt text for non-visual users.",
18
- secondary: [
19
- "Alternative text provides a textual description for images and other media.",
20
- "Screen readers use this text to describe the element to users who cannot see it.",
21
- "This is required for WCAG 1.1.1 compliance."
22
- ],
23
- suggestions: [
24
- "Add an alt attribute with descriptive text",
25
- "Use alt=\"\" for decorative images",
26
- "Use aria-label or aria-labelledby for alternative labeling"
27
- ]
28
- } },
29
- setup(context) {
30
- function checkNode(node, { sourceFile }) {
31
- const { attributes, tagName } = node;
32
- if (tagName.kind !== SyntaxKind.Identifier) return;
33
- const elementName = tagName.text.toLowerCase();
34
- if (elementName === "img" || elementName === "area") checkAltAttribute(attributes, tagName, elementName, sourceFile);
35
- else if (elementName === "input") checkInputElement(attributes, tagName, sourceFile);
36
- else if (elementName === "object") checkObjectAccessibility(attributes, tagName, sourceFile);
37
- }
38
- function checkAltAttribute(attributes, tagName, elementName, sourceFile) {
39
- const properties = attributes.properties.find((attr) => attr.kind === SyntaxKind.JsxAttribute && attr.name.kind === SyntaxKind.Identifier && attr.name.text === "alt");
40
- 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;
41
- if (!properties) {
42
- context.report({
43
- data: { element: elementName },
44
- message: "missingAlt",
45
- range: getTSNodeRange(tagName, sourceFile)
46
- });
47
- return;
48
- }
49
- if (properties.kind === SyntaxKind.JsxAttribute && properties.initializer?.kind === SyntaxKind.JsxExpression) {
50
- const { expression } = properties.initializer;
51
- if (expression?.kind === SyntaxKind.Identifier && expression.text === "undefined") context.report({
52
- data: { element: elementName },
53
- message: "missingAlt",
54
- range: getTSNodeRange(tagName, sourceFile)
55
- });
56
- }
57
- }
58
- function checkInputElement(attributes, tagName, sourceFile) {
59
- const typeAttribute = attributes.properties.find((properties) => properties.kind === SyntaxKind.JsxAttribute && properties.name.kind === SyntaxKind.Identifier && properties.name.text === "type");
60
- if (typeAttribute?.kind === SyntaxKind.JsxAttribute && typeAttribute.initializer?.kind === SyntaxKind.StringLiteral && typeAttribute.initializer.text === "image") checkAltAttribute(attributes, tagName, "input[type='image']", sourceFile);
61
- }
62
- function checkObjectAccessibility(attributes, tagName, sourceFile) {
63
- if (!attributes.properties.some((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && alternateProperties.has(property.name.text) && property.initializer)) context.report({
64
- data: { element: "object" },
65
- message: "missingAlt",
66
- range: getTSNodeRange(tagName, sourceFile)
67
- });
68
- }
69
- return { visitors: {
70
- JsxOpeningElement: checkNode,
71
- JsxSelfClosingElement: checkNode
72
- } };
73
- }
74
- });
75
- //#endregion
76
- export { altTexts_default as default };
@@ -1,9 +0,0 @@
1
- declare const _default: import("@flint.fyi/core").Rule<{
2
- readonly description: "Reports anchor elements with ambiguous text that doesn't describe the link destination.";
3
- readonly id: "anchorAmbiguousText";
4
- readonly presets: readonly ["logical", "logicalStrict"];
5
- } & {
6
- readonly pluginId: string;
7
- readonly url: string;
8
- }, "ambiguousText", undefined>;
9
- export default _default;
@@ -1,55 +0,0 @@
1
- import { ruleCreator } from "./ruleCreator.js";
2
- import { SyntaxKind } from "typescript";
3
- import { getTSNodeRange, typescriptLanguage } from "@flint.fyi/typescript-language";
4
- //#region src/rules/anchorAmbiguousText.ts
5
- const ambiguousWords = /* @__PURE__ */ new Set([
6
- "a link",
7
- "click here",
8
- "here",
9
- "learn more",
10
- "link",
11
- "more",
12
- "read more"
13
- ]);
14
- var anchorAmbiguousText_default = ruleCreator.createRule(typescriptLanguage, {
15
- about: {
16
- description: "Reports anchor elements with ambiguous text that doesn't describe the link destination.",
17
- id: "anchorAmbiguousText",
18
- presets: ["logical", "logicalStrict"]
19
- },
20
- messages: { ambiguousText: {
21
- primary: "This anchor element has ambiguous text that doesn't describe the link destination.",
22
- secondary: [
23
- "Ambiguous text like '{{ text }}' doesn't provide context about where the link goes.",
24
- "Screen reader users often navigate by links and need descriptive text to understand the purpose.",
25
- "Provide descriptive text that explains what the link does or where it leads."
26
- ],
27
- suggestions: [
28
- "Replace vague text with descriptive text that explains the link destination",
29
- "Include the page or section name the link leads to",
30
- "Describe the action that will occur when clicking the link"
31
- ]
32
- } },
33
- setup(context) {
34
- function getTextContent(node) {
35
- let text = "";
36
- for (const child of node.children) if (child.kind === SyntaxKind.JsxText) text += child.text;
37
- else if (child.kind === SyntaxKind.JsxElement) text += getTextContent(child);
38
- else if (child.kind === SyntaxKind.JsxExpression && child.expression?.kind === SyntaxKind.StringLiteral) text += child.expression.text;
39
- return text;
40
- }
41
- return { visitors: { JsxElement(node, { sourceFile }) {
42
- if (node.openingElement.tagName.kind !== SyntaxKind.Identifier || node.openingElement.tagName.text !== "a") return;
43
- const textContent = getTextContent(node);
44
- if (!ambiguousWords.has(textContent.toLowerCase().trim())) return;
45
- const textNodes = node.children.filter((child) => child.kind === SyntaxKind.JsxText && child.text.trim());
46
- context.report({
47
- data: { text: textContent.trim() },
48
- message: "ambiguousText",
49
- range: getTSNodeRange(textNodes[0] ?? node.openingElement, sourceFile)
50
- });
51
- } } };
52
- }
53
- });
54
- //#endregion
55
- export { anchorAmbiguousText_default as default };
@@ -1,9 +0,0 @@
1
- declare const _default: import("@flint.fyi/core").Rule<{
2
- readonly description: "Reports anchor elements without accessible content.";
3
- readonly id: "anchorContent";
4
- readonly presets: readonly ["logical", "logicalStrict"];
5
- } & {
6
- readonly pluginId: string;
7
- readonly url: string;
8
- }, "missingContent", undefined>;
9
- export default _default;
@@ -1,55 +0,0 @@
1
- import { ruleCreator } from "./ruleCreator.js";
2
- import { SyntaxKind } from "typescript";
3
- import { getTSNodeRange, typescriptLanguage } from "@flint.fyi/typescript-language";
4
- //#region src/rules/anchorContent.ts
5
- var anchorContent_default = ruleCreator.createRule(typescriptLanguage, {
6
- about: {
7
- description: "Reports anchor elements without accessible content.",
8
- id: "anchorContent",
9
- presets: ["logical", "logicalStrict"]
10
- },
11
- messages: { missingContent: {
12
- primary: "This anchor element is missing accessible content.",
13
- secondary: [
14
- "Non-visual tools such as screen readers and search engine crawlers need content to describe links.",
15
- "Provide text content, aria-label, aria-labelledby, or title attribute.",
16
- "This is required for WCAG 2.4.4 and 4.1.2 compliance."
17
- ],
18
- suggestions: [
19
- "Add text content inside the anchor",
20
- "Add an aria-label attribute",
21
- "Add a title attribute"
22
- ]
23
- } },
24
- setup(context) {
25
- function hasAccessibleContent(element) {
26
- 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);
27
- }
28
- function hasTextContent(element) {
29
- return element.children.some((child) => {
30
- if (child.kind === SyntaxKind.JsxText && child.text.trim()) return true;
31
- if (child.kind === SyntaxKind.JsxElement || child.kind === SyntaxKind.JsxSelfClosingElement) {
32
- 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;
33
- }
34
- if (child.kind === SyntaxKind.JsxExpression && child.expression) return true;
35
- });
36
- }
37
- return { visitors: {
38
- JsxElement(node, { sourceFile }) {
39
- const openingElement = node.openingElement;
40
- if (openingElement.tagName.kind === SyntaxKind.Identifier && openingElement.tagName.text === "a" && !hasAccessibleContent(openingElement) && !hasTextContent(node)) context.report({
41
- message: "missingContent",
42
- range: getTSNodeRange(openingElement, sourceFile)
43
- });
44
- },
45
- JsxSelfClosingElement(node, { sourceFile }) {
46
- if (node.tagName.kind === SyntaxKind.Identifier && node.tagName.text === "a" && !hasAccessibleContent(node)) context.report({
47
- message: "missingContent",
48
- range: getTSNodeRange(node, sourceFile)
49
- });
50
- }
51
- } };
52
- }
53
- });
54
- //#endregion
55
- export { anchorContent_default as default };
@@ -1,9 +0,0 @@
1
- declare const _default: import("@flint.fyi/core").Rule<{
2
- readonly description: "Reports invalid usage of anchor elements.";
3
- readonly id: "anchorValidity";
4
- readonly presets: readonly ["logical", "logicalStrict"];
5
- } & {
6
- readonly pluginId: string;
7
- readonly url: string;
8
- }, "invalidHref" | "missingHref" | "shouldBeButton", undefined>;
9
- export default _default;
@@ -1,77 +0,0 @@
1
- import { ruleCreator } from "./ruleCreator.js";
2
- import { SyntaxKind } from "typescript";
3
- import { getTSNodeRange, typescriptLanguage } from "@flint.fyi/typescript-language";
4
- //#region src/rules/anchorValidity.ts
5
- var anchorValidity_default = ruleCreator.createRule(typescriptLanguage, {
6
- about: {
7
- description: "Reports invalid usage of anchor elements.",
8
- id: "anchorValidity",
9
- presets: ["logical", "logicalStrict"]
10
- },
11
- messages: {
12
- invalidHref: {
13
- primary: "Anchor has an invalid href value '{{ href }}'.",
14
- secondary: [
15
- "Anchors should navigate to valid URLs or page sections.",
16
- "Use a button element for click handlers without navigation.",
17
- "This is required for WCAG 2.4.4 compliance."
18
- ],
19
- suggestions: ["Use a valid URL or fragment identifier", "Use a <button> element instead"]
20
- },
21
- missingHref: {
22
- primary: "Anchor element is missing an href attribute.",
23
- secondary: [
24
- "Anchors without href are not keyboard accessible.",
25
- "Use a button element for actions without navigation.",
26
- "This is required for WCAG 4.1.2 compliance."
27
- ],
28
- suggestions: ["Add a valid href attribute", "Use a <button> element instead"]
29
- },
30
- shouldBeButton: {
31
- primary: "Anchor with onClick handler should be a button.",
32
- secondary: [
33
- "Interactive elements without valid href should use button elements.",
34
- "Buttons provide better semantics and keyboard accessibility.",
35
- "This is required for WCAG 4.1.2 compliance."
36
- ],
37
- suggestions: ["Replace <a> with <button>"]
38
- }
39
- },
40
- setup(context) {
41
- function getHrefValue(attributes) {
42
- const hrefProperty = attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "href");
43
- if (hrefProperty?.kind !== SyntaxKind.JsxAttribute) return;
44
- if (hrefProperty.initializer?.kind === SyntaxKind.StringLiteral) return hrefProperty.initializer.text;
45
- return "";
46
- }
47
- function hasOnClick(attributes) {
48
- return attributes.properties.some((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "onClick");
49
- }
50
- function isInvalidHref(href) {
51
- return href === "#" || href.startsWith("javascript:");
52
- }
53
- function checkAnchor(node, { sourceFile }) {
54
- if (node.tagName.kind !== SyntaxKind.Identifier || node.tagName.text !== "a") return;
55
- const href = getHrefValue(node.attributes);
56
- const hasClick = hasOnClick(node.attributes);
57
- if (href === void 0) {
58
- context.report({
59
- message: hasClick ? "shouldBeButton" : "missingHref",
60
- range: getTSNodeRange(node, sourceFile)
61
- });
62
- return;
63
- }
64
- if (typeof href === "string" && isInvalidHref(href)) context.report({
65
- data: { href },
66
- message: hasClick ? "shouldBeButton" : "invalidHref",
67
- range: getTSNodeRange(node, sourceFile)
68
- });
69
- }
70
- return { visitors: {
71
- JsxOpeningElement: checkAnchor,
72
- JsxSelfClosingElement: checkAnchor
73
- } };
74
- }
75
- });
76
- //#endregion
77
- export { anchorValidity_default as default };
@@ -1,9 +0,0 @@
1
- declare const _default: import("@flint.fyi/core").Rule<{
2
- readonly description: "Reports elements with aria-activedescendant without tabIndex.";
3
- readonly id: "ariaActiveDescendantTabIndex";
4
- readonly presets: readonly ["logical", "logicalStrict"];
5
- } & {
6
- readonly pluginId: string;
7
- readonly url: string;
8
- }, "missingTabIndex", undefined>;
9
- export default _default;
@@ -1,51 +0,0 @@
1
- import { ruleCreator } from "./ruleCreator.js";
2
- import { SyntaxKind } from "typescript";
3
- import { getTSNodeRange, typescriptLanguage } from "@flint.fyi/typescript-language";
4
- //#region src/rules/ariaActiveDescendantTabIndex.ts
5
- const inherentlyTabbableElements = /* @__PURE__ */ new Set([
6
- "a",
7
- "area",
8
- "button",
9
- "input",
10
- "select",
11
- "textarea"
12
- ]);
13
- var ariaActiveDescendantTabIndex_default = ruleCreator.createRule(typescriptLanguage, {
14
- about: {
15
- description: "Reports elements with aria-activedescendant without tabIndex.",
16
- id: "ariaActiveDescendantTabIndex",
17
- presets: ["logical", "logicalStrict"]
18
- },
19
- messages: { missingTabIndex: {
20
- primary: "This element with `aria-activedescendant` is missing a `tabIndex` attribute to manage focus state.",
21
- secondary: [
22
- "aria-activedescendant is used to manage focus within a composite widget.",
23
- "The element must be tabbable, either with an inherent tabIndex or explicit tabIndex attribute.",
24
- "Without it, keyboard users cannot reach the element."
25
- ],
26
- suggestions: ["Add tabIndex=\"0\" to make the element tabbable", "Add tabIndex=\"-1\" to make it programmatically focusable"]
27
- } },
28
- setup(context) {
29
- function checkElement({ attributes, tagName }, { sourceFile }) {
30
- if (tagName.kind === SyntaxKind.Identifier) {
31
- const firstCharacter = tagName.text.charAt(0);
32
- if (firstCharacter === firstCharacter.toUpperCase() && firstCharacter !== firstCharacter.toLowerCase()) return;
33
- }
34
- if (!attributes.properties.some((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "aria-activedescendant" && property.initializer)) return;
35
- if (tagName.kind === SyntaxKind.Identifier && inherentlyTabbableElements.has(tagName.text.toLowerCase())) return;
36
- if (!attributes.properties.some((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text.toLowerCase() === "tabindex")) {
37
- const ariaProperty = attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "aria-activedescendant");
38
- if (ariaProperty?.kind === SyntaxKind.JsxAttribute) context.report({
39
- message: "missingTabIndex",
40
- range: getTSNodeRange(ariaProperty, sourceFile)
41
- });
42
- }
43
- }
44
- return { visitors: {
45
- JsxOpeningElement: checkElement,
46
- JsxSelfClosingElement: checkElement
47
- } };
48
- }
49
- });
50
- //#endregion
51
- export { ariaActiveDescendantTabIndex_default as default };
@@ -1,9 +0,0 @@
1
- declare const _default: import("@flint.fyi/core").Rule<{
2
- readonly description: "Reports elements with aria-hidden='true' that are focusable.";
3
- readonly id: "ariaHiddenFocusables";
4
- readonly presets: readonly ["logical", "logicalStrict"];
5
- } & {
6
- readonly pluginId: string;
7
- readonly url: string;
8
- }, "ariaHiddenFocusable", undefined>;
9
- export default _default;
@@ -1,66 +0,0 @@
1
- import { ruleCreator } from "./ruleCreator.js";
2
- import { SyntaxKind } from "typescript";
3
- import { getTSNodeRange, typescriptLanguage } from "@flint.fyi/typescript-language";
4
- //#region src/rules/ariaHiddenFocusables.ts
5
- const focusableElements = /* @__PURE__ */ new Set([
6
- "a",
7
- "audio",
8
- "button",
9
- "input",
10
- "select",
11
- "textarea",
12
- "video"
13
- ]);
14
- var ariaHiddenFocusables_default = ruleCreator.createRule(typescriptLanguage, {
15
- about: {
16
- description: "Reports elements with aria-hidden='true' that are focusable.",
17
- id: "ariaHiddenFocusables",
18
- presets: ["logical", "logicalStrict"]
19
- },
20
- messages: { ariaHiddenFocusable: {
21
- primary: "This element has `aria-hidden=\"true\"` but is focusable, which is misleading to users navigating with keyboards.",
22
- 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."],
23
- suggestions: [
24
- "Remove aria-hidden=\"true\"",
25
- "Add tabIndex=\"-1\" to remove from focus order",
26
- "Use a non-focusable element"
27
- ]
28
- } },
29
- setup(context) {
30
- function checkElement(node, { sourceFile }) {
31
- const { attributes, tagName } = node;
32
- if (tagName.kind !== SyntaxKind.Identifier) return;
33
- const ariaHiddenProperty = attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text.toLowerCase() === "aria-hidden");
34
- if (ariaHiddenProperty?.kind !== SyntaxKind.JsxAttribute || !isAriaHiddenTrue(ariaHiddenProperty)) return;
35
- const tabIndexValue = findTabIndexValue(node);
36
- if (tabIndexValue === -1) return;
37
- if (focusableElements.has(tagName.text.toLowerCase()) || tabIndexValue !== void 0 && tabIndexValue >= 0) context.report({
38
- message: "ariaHiddenFocusable",
39
- range: getTSNodeRange(ariaHiddenProperty, sourceFile)
40
- });
41
- }
42
- return { visitors: {
43
- JsxOpeningElement: checkElement,
44
- JsxSelfClosingElement: checkElement
45
- } };
46
- }
47
- });
48
- function findTabIndexValue(node) {
49
- const tabIndexProperty = node.attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text.toLowerCase() === "tabindex");
50
- if (!tabIndexProperty?.initializer) return;
51
- if (tabIndexProperty.initializer.kind === SyntaxKind.JsxExpression) {
52
- const expression = tabIndexProperty.initializer.expression;
53
- if (expression?.kind === SyntaxKind.NumericLiteral) return Number(expression.text);
54
- }
55
- if (tabIndexProperty.initializer.kind === SyntaxKind.StringLiteral) return Number(tabIndexProperty.initializer.text);
56
- }
57
- function isAriaHiddenTrue(ariaHiddenProperty) {
58
- if (!ariaHiddenProperty.initializer) return false;
59
- if (ariaHiddenProperty.initializer.kind === SyntaxKind.StringLiteral) return ariaHiddenProperty.initializer.text === "true";
60
- if (ariaHiddenProperty.initializer.kind === SyntaxKind.JsxExpression) {
61
- if (ariaHiddenProperty.initializer.expression?.kind === SyntaxKind.TrueKeyword) return true;
62
- }
63
- return false;
64
- }
65
- //#endregion
66
- export { ariaHiddenFocusables_default as default };
@@ -1,9 +0,0 @@
1
- declare const _default: import("@flint.fyi/core").Rule<{
2
- readonly description: "Reports ARIA properties with invalid value types.";
3
- readonly id: "ariaPropTypes";
4
- readonly presets: readonly ["logical", "logicalStrict"];
5
- } & {
6
- readonly pluginId: string;
7
- readonly url: string;
8
- }, "invalidPropType", undefined>;
9
- export default _default;