@hypersolid-dev/eslint-config 1.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +28 -1
  3. package/dist/base.d.ts +1 -0
  4. package/dist/base.js +63 -8
  5. package/dist/plugins/react-native-a11y/constants/accessibility-props.d.ts +32 -0
  6. package/dist/plugins/react-native-a11y/constants/accessibility-props.js +50 -0
  7. package/dist/plugins/react-native-a11y/constants/accessibility-state.d.ts +2 -0
  8. package/dist/plugins/react-native-a11y/constants/accessibility-state.js +8 -0
  9. package/dist/plugins/react-native-a11y/constants/interactive.d.ts +6 -0
  10. package/dist/plugins/react-native-a11y/constants/interactive.js +33 -0
  11. package/dist/plugins/react-native-a11y/constants/react-native-components.d.ts +7 -0
  12. package/dist/plugins/react-native-a11y/constants/react-native-components.js +13 -0
  13. package/dist/plugins/react-native-a11y/constants/schema-properties.d.ts +4 -0
  14. package/dist/plugins/react-native-a11y/constants/schema-properties.js +10 -0
  15. package/dist/plugins/react-native-a11y/index.d.ts +3 -0
  16. package/dist/plugins/react-native-a11y/index.js +48 -0
  17. package/dist/plugins/react-native-a11y/models/optional-options.d.ts +3 -0
  18. package/dist/plugins/react-native-a11y/models/optional-options.js +1 -0
  19. package/dist/plugins/react-native-a11y/rules/accessibility-actions-need-handler.d.ts +5 -0
  20. package/dist/plugins/react-native-a11y/rules/accessibility-actions-need-handler.js +82 -0
  21. package/dist/plugins/react-native-a11y/rules/accessibility-hint.d.ts +5 -0
  22. package/dist/plugins/react-native-a11y/rules/accessibility-hint.js +74 -0
  23. package/dist/plugins/react-native-a11y/rules/accessibility-label.d.ts +6 -0
  24. package/dist/plugins/react-native-a11y/rules/accessibility-label.js +103 -0
  25. package/dist/plugins/react-native-a11y/rules/accessibility-role.d.ts +5 -0
  26. package/dist/plugins/react-native-a11y/rules/accessibility-role.js +72 -0
  27. package/dist/plugins/react-native-a11y/rules/accessibility-value-needs-range.d.ts +4 -0
  28. package/dist/plugins/react-native-a11y/rules/accessibility-value-needs-range.js +49 -0
  29. package/dist/plugins/react-native-a11y/rules/adjustable-needs-value.d.ts +4 -0
  30. package/dist/plugins/react-native-a11y/rules/adjustable-needs-value.js +40 -0
  31. package/dist/plugins/react-native-a11y/rules/gate-announce-for-accessibility.d.ts +4 -0
  32. package/dist/plugins/react-native-a11y/rules/gate-announce-for-accessibility.js +77 -0
  33. package/dist/plugins/react-native-a11y/rules/input-purpose.d.ts +6 -0
  34. package/dist/plugins/react-native-a11y/rules/input-purpose.js +84 -0
  35. package/dist/plugins/react-native-a11y/rules/no-aria-legacy-mix.d.ts +4 -0
  36. package/dist/plugins/react-native-a11y/rules/no-aria-legacy-mix.js +57 -0
  37. package/dist/plugins/react-native-a11y/rules/no-deprecated-accessibility-focus.d.ts +4 -0
  38. package/dist/plugins/react-native-a11y/rules/no-deprecated-accessibility-focus.js +31 -0
  39. package/dist/plugins/react-native-a11y/rules/no-dropped-aria-on-text.d.ts +5 -0
  40. package/dist/plugins/react-native-a11y/rules/no-dropped-aria-on-text.js +66 -0
  41. package/dist/plugins/react-native-a11y/rules/no-font-scaling-disabled.d.ts +5 -0
  42. package/dist/plugins/react-native-a11y/rules/no-font-scaling-disabled.js +49 -0
  43. package/dist/plugins/react-native-a11y/rules/no-ineffective-accessibility-props.d.ts +9 -0
  44. package/dist/plugins/react-native-a11y/rules/no-ineffective-accessibility-props.js +76 -0
  45. package/dist/plugins/react-native-a11y/rules/no-invalid-accessibility-state.d.ts +5 -0
  46. package/dist/plugins/react-native-a11y/rules/no-invalid-accessibility-state.js +82 -0
  47. package/dist/plugins/react-native-a11y/rules/no-nested-focus-stops.d.ts +5 -0
  48. package/dist/plugins/react-native-a11y/rules/no-nested-focus-stops.js +70 -0
  49. package/dist/plugins/react-native-a11y/rules/no-press-in-activation.d.ts +5 -0
  50. package/dist/plugins/react-native-a11y/rules/no-press-in-activation.js +59 -0
  51. package/dist/plugins/react-native-a11y/rules/no-unnamed-interactive.d.ts +5 -0
  52. package/dist/plugins/react-native-a11y/rules/no-unnamed-interactive.js +75 -0
  53. package/dist/plugins/react-native-a11y/rules/require-invert-colors-opt-out.d.ts +5 -0
  54. package/dist/plugins/react-native-a11y/rules/require-invert-colors-opt-out.js +62 -0
  55. package/dist/plugins/react-native-a11y/rules/require-modal-a11y-on-sheets.d.ts +5 -0
  56. package/dist/plugins/react-native-a11y/rules/require-modal-a11y-on-sheets.js +55 -0
  57. package/dist/plugins/react-native-a11y/utils/create-rule.d.ts +5 -0
  58. package/dist/plugins/react-native-a11y/utils/create-rule.js +3 -0
  59. package/dist/plugins/react-native-a11y/utils/get-amount-of-text-children.d.ts +8 -0
  60. package/dist/plugins/react-native-a11y/utils/get-amount-of-text-children.js +67 -0
  61. package/dist/plugins/react-native-a11y/utils/get-components.d.ts +7 -0
  62. package/dist/plugins/react-native-a11y/utils/get-components.js +11 -0
  63. package/dist/plugins/react-native-a11y/utils/get-element-name.d.ts +10 -0
  64. package/dist/plugins/react-native-a11y/utils/get-element-name.js +66 -0
  65. package/dist/plugins/react-native-a11y/utils/get-jsx-descendants.d.ts +4 -0
  66. package/dist/plugins/react-native-a11y/utils/get-jsx-descendants.js +49 -0
  67. package/dist/plugins/react-native-a11y/utils/get-literal-property-value.d.ts +7 -0
  68. package/dist/plugins/react-native-a11y/utils/get-literal-property-value.js +21 -0
  69. package/dist/plugins/react-native-a11y/utils/get-literal-text-child.d.ts +8 -0
  70. package/dist/plugins/react-native-a11y/utils/get-literal-text-child.js +40 -0
  71. package/dist/plugins/react-native-a11y/utils/get-property.d.ts +4 -0
  72. package/dist/plugins/react-native-a11y/utils/get-property.js +9 -0
  73. package/dist/plugins/react-native-a11y/utils/has-accessible-name.d.ts +6 -0
  74. package/dist/plugins/react-native-a11y/utils/has-accessible-name.js +27 -0
  75. package/dist/plugins/react-native-a11y/utils/has-property.d.ts +3 -0
  76. package/dist/plugins/react-native-a11y/utils/has-property.js +3 -0
  77. package/dist/plugins/react-native-a11y/utils/has-spread-props.d.ts +8 -0
  78. package/dist/plugins/react-native-a11y/utils/has-spread-props.js +8 -0
  79. package/dist/plugins/react-native-a11y/utils/is-definitely-grouped.d.ts +7 -0
  80. package/dist/plugins/react-native-a11y/utils/is-definitely-grouped.js +20 -0
  81. package/dist/plugins/react-native-a11y/utils/is-hidden-from-accessibility.d.ts +19 -0
  82. package/dist/plugins/react-native-a11y/utils/is-hidden-from-accessibility.js +51 -0
  83. package/dist/plugins/react-native-a11y/utils/is-inside-focus-stop.d.ts +9 -0
  84. package/dist/plugins/react-native-a11y/utils/is-inside-focus-stop.js +36 -0
  85. package/dist/plugins/react-native-a11y/utils/is-interactive.d.ts +16 -0
  86. package/dist/plugins/react-native-a11y/utils/is-interactive.js +38 -0
  87. package/dist/plugins/react-native-a11y/utils/is-not-a-focus-stop.d.ts +7 -0
  88. package/dist/plugins/react-native-a11y/utils/is-not-a-focus-stop.js +19 -0
  89. package/dist/react-native.js +85 -1
  90. package/dist/shared.d.ts +8 -0
  91. package/dist/shared.js +22 -1
  92. package/package.json +4 -2
@@ -0,0 +1,67 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/types";
2
+ import { TEXTS } from "../constants/react-native-components.js";
3
+ // ponytail: `Text` by name only. A renamed import or a differently named text
4
+ // atom reads as no text; pass it through `additionalComponents` if that bites.
5
+ const isTextElement = (node) => node.openingElement.name.type === AST_NODE_TYPES.JSXIdentifier &&
6
+ TEXTS.includes(node.openingElement.name.name);
7
+ /** `<Text>{null}</Text>` and `<Text> </Text>` render nothing a user can read. */
8
+ const hasRenderedContent = (child) => {
9
+ if (child.type === AST_NODE_TYPES.JSXText) {
10
+ return child.value.trim().length > 0;
11
+ }
12
+ if (child.type === AST_NODE_TYPES.JSXExpressionContainer) {
13
+ const { expression } = child;
14
+ return (expression.type !== AST_NODE_TYPES.JSXEmptyExpression &&
15
+ !(expression.type === AST_NODE_TYPES.Literal && expression.value === null) &&
16
+ !(expression.type === AST_NODE_TYPES.Identifier &&
17
+ expression.name === "undefined"));
18
+ }
19
+ return true;
20
+ };
21
+ /** The JSX a conditional can render, so a branch carrying text still counts. */
22
+ const branchesOf = (expression) => {
23
+ if (expression.type === AST_NODE_TYPES.ConditionalExpression) {
24
+ return [expression.consequent, expression.alternate];
25
+ }
26
+ if (expression.type === AST_NODE_TYPES.LogicalExpression) {
27
+ return [expression.left, expression.right];
28
+ }
29
+ return [];
30
+ };
31
+ const isJsxChild = (expression) => expression.type === AST_NODE_TYPES.JSXElement ||
32
+ expression.type === AST_NODE_TYPES.JSXFragment;
33
+ /**
34
+ * Counts children that render visible text, which is what makes an
35
+ * `accessibilityLabel` redundant. Whitespace between JSX tags is not text, and
36
+ * `<Text>{t("Save")}</Text>` is, so neither can be judged on node type alone.
37
+ */
38
+ const getAmountOfTextChildren = (children, oldAmount = 0) => children.reduce((amount, child) => {
39
+ if (child.type === AST_NODE_TYPES.JSXText) {
40
+ return child.value.trim().length > 0 ? amount + 1 : amount;
41
+ }
42
+ if (child.type === AST_NODE_TYPES.JSXExpressionContainer) {
43
+ const { expression } = child;
44
+ if (expression.type === AST_NODE_TYPES.Literal ||
45
+ expression.type === AST_NODE_TYPES.TemplateLiteral) {
46
+ return amount + 1;
47
+ }
48
+ // JSX inside an expression renders like any other child, and a branch
49
+ // that renders text still names the control
50
+ const rendered = (isJsxChild(expression) ? [expression] : branchesOf(expression)).filter(isJsxChild);
51
+ return rendered.length > 0
52
+ ? getAmountOfTextChildren(rendered, amount)
53
+ : amount;
54
+ }
55
+ if (child.type === AST_NODE_TYPES.JSXFragment) {
56
+ return getAmountOfTextChildren(child.children, amount);
57
+ }
58
+ if (child.type === AST_NODE_TYPES.JSXElement) {
59
+ // A `Text` renders whatever it is given, including an i18n call
60
+ if (isTextElement(child)) {
61
+ return child.children.some(hasRenderedContent) ? amount + 1 : amount;
62
+ }
63
+ return getAmountOfTextChildren(child.children, amount);
64
+ }
65
+ return amount;
66
+ }, oldAmount);
67
+ export default getAmountOfTextChildren;
@@ -0,0 +1,7 @@
1
+ import type { RuleContext } from "@typescript-eslint/utils/ts-eslint";
2
+ import type { Options } from "../models/optional-options.js";
3
+ declare const getComponents: <T extends string, P extends Options>({ context, defaultComponents, }: {
4
+ context: RuleContext<T, P>;
5
+ defaultComponents?: string[];
6
+ }) => string[];
7
+ export default getComponents;
@@ -0,0 +1,11 @@
1
+ const getComponents = ({ context, defaultComponents, }) => {
2
+ const options = context.options.length > 0 ? context.options[0] : {};
3
+ const { additionalComponents } = options;
4
+ // Combine default React Native components with user-defined custom components
5
+ const componentsToCheck = [
6
+ ...(defaultComponents ?? []),
7
+ ...(additionalComponents ?? []),
8
+ ];
9
+ return componentsToCheck;
10
+ };
11
+ export default getComponents;
@@ -0,0 +1,10 @@
1
+ import { type TSESTree } from "@typescript-eslint/types";
2
+ /**
3
+ * The name to match a component by. A qualified name resolves to its last part,
4
+ * since `Animated.Image` is an `Image` wrapped for animation and owes the same
5
+ * accessibility props. A renamed import resolves to the name it was imported
6
+ * under. Returns undefined for a namespaced name (`svg:rect`), which is not a
7
+ * React Native component.
8
+ */
9
+ declare const getElementName: (node: TSESTree.JSXOpeningElement) => string | undefined;
10
+ export default getElementName;
@@ -0,0 +1,66 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/types";
2
+ /** Local name → imported name, per file, so the walk below runs once. */
3
+ const aliasesByProgram = new WeakMap();
4
+ /** The file's root. Only a Program has no parent, so the walk ends there. */
5
+ const getProgram = (node) => {
6
+ let current = node;
7
+ while (current.parent) {
8
+ current = current.parent;
9
+ }
10
+ return current;
11
+ };
12
+ /**
13
+ * Every renamed import in the file. A default import carries no name to match
14
+ * on — `import Img from "expo-image"` says nothing about what `Img` is — so
15
+ * only named specifiers are collected.
16
+ */
17
+ const collectAliases = (program) => {
18
+ const aliases = new Map();
19
+ for (const statement of program.body) {
20
+ if (statement.type !== AST_NODE_TYPES.ImportDeclaration) {
21
+ continue;
22
+ }
23
+ for (const specifier of statement.specifiers) {
24
+ if (specifier.type !== AST_NODE_TYPES.ImportSpecifier) {
25
+ continue;
26
+ }
27
+ if (specifier.imported.type !== AST_NODE_TYPES.Identifier) {
28
+ continue;
29
+ }
30
+ aliases.set(specifier.local.name, specifier.imported.name);
31
+ }
32
+ }
33
+ return aliases;
34
+ };
35
+ /**
36
+ * What a local name was imported under, or the name itself when nothing
37
+ * renamed it. `Image as ExpoImage` is still an `Image` and owes the same
38
+ * accessibility props; matching the local name would let a rename opt a
39
+ * component out of every rule in this plugin.
40
+ */
41
+ const resolveAlias = (node, name) => {
42
+ const program = getProgram(node);
43
+ let aliases = aliasesByProgram.get(program);
44
+ if (aliases === undefined) {
45
+ aliases = collectAliases(program);
46
+ aliasesByProgram.set(program, aliases);
47
+ }
48
+ return aliases.get(name) ?? name;
49
+ };
50
+ /**
51
+ * The name to match a component by. A qualified name resolves to its last part,
52
+ * since `Animated.Image` is an `Image` wrapped for animation and owes the same
53
+ * accessibility props. A renamed import resolves to the name it was imported
54
+ * under. Returns undefined for a namespaced name (`svg:rect`), which is not a
55
+ * React Native component.
56
+ */
57
+ const getElementName = (node) => {
58
+ if (node.name.type === AST_NODE_TYPES.JSXIdentifier) {
59
+ return resolveAlias(node, node.name.name);
60
+ }
61
+ if (node.name.type === AST_NODE_TYPES.JSXMemberExpression) {
62
+ return node.name.property.name;
63
+ }
64
+ return;
65
+ };
66
+ export default getElementName;
@@ -0,0 +1,4 @@
1
+ import { type TSESTree } from "@typescript-eslint/types";
2
+ /** Every JSX element rendered below this one, at any depth. */
3
+ declare const getJsxDescendants: (node: TSESTree.JSXOpeningElement) => TSESTree.JSXOpeningElement[];
4
+ export default getJsxDescendants;
@@ -0,0 +1,49 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/types";
2
+ const isJsx = (node) => node.type === AST_NODE_TYPES.JSXElement ||
3
+ node.type === AST_NODE_TYPES.JSXFragment;
4
+ /** The JSX an expression can render, including through a conditional. */
5
+ const renderedBy = (expression) => {
6
+ if (isJsx(expression)) {
7
+ return [expression];
8
+ }
9
+ if (expression.type === AST_NODE_TYPES.ConditionalExpression) {
10
+ return [
11
+ ...renderedBy(expression.consequent),
12
+ ...renderedBy(expression.alternate),
13
+ ];
14
+ }
15
+ if (expression.type === AST_NODE_TYPES.LogicalExpression) {
16
+ return [...renderedBy(expression.left), ...renderedBy(expression.right)];
17
+ }
18
+ // `items.map(() => <Row />)` renders its callback's JSX
19
+ if (expression.type === AST_NODE_TYPES.CallExpression) {
20
+ return expression.arguments.flatMap((argument) => argument.type === AST_NODE_TYPES.ArrowFunctionExpression &&
21
+ argument.body.type !== AST_NODE_TYPES.BlockStatement
22
+ ? renderedBy(argument.body)
23
+ : []);
24
+ }
25
+ return [];
26
+ };
27
+ const collect = (children, found) => {
28
+ for (const child of children) {
29
+ if (child.type === AST_NODE_TYPES.JSXElement) {
30
+ found.push(child.openingElement);
31
+ collect(child.children, found);
32
+ continue;
33
+ }
34
+ if (child.type === AST_NODE_TYPES.JSXFragment) {
35
+ collect(child.children, found);
36
+ continue;
37
+ }
38
+ if (child.type === AST_NODE_TYPES.JSXExpressionContainer) {
39
+ collect(renderedBy(child.expression), found);
40
+ }
41
+ }
42
+ };
43
+ /** Every JSX element rendered below this one, at any depth. */
44
+ const getJsxDescendants = (node) => {
45
+ const found = [];
46
+ collect(node.parent.children, found);
47
+ return found;
48
+ };
49
+ export default getJsxDescendants;
@@ -0,0 +1,7 @@
1
+ import { type TSESTree } from "@typescript-eslint/types";
2
+ /**
3
+ * The literal value of a prop, or `undefined` when it is absent or computed.
4
+ * A computed value cannot be judged statically, so callers skip it.
5
+ */
6
+ declare const getLiteralPropertyValue: (node: TSESTree.JSXOpeningElement, propertyNames: string | string[]) => TSESTree.Literal["value"] | undefined;
7
+ export default getLiteralPropertyValue;
@@ -0,0 +1,21 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/types";
2
+ import getProperty from "./get-property.js";
3
+ /**
4
+ * The literal value of a prop, or `undefined` when it is absent or computed.
5
+ * A computed value cannot be judged statically, so callers skip it.
6
+ */
7
+ const getLiteralPropertyValue = (node, propertyNames) => {
8
+ const attribute = getProperty(node, propertyNames);
9
+ if (!attribute?.value) {
10
+ return undefined;
11
+ }
12
+ if (attribute.value.type === AST_NODE_TYPES.Literal) {
13
+ return attribute.value.value;
14
+ }
15
+ if (attribute.value.type === AST_NODE_TYPES.JSXExpressionContainer &&
16
+ attribute.value.expression.type === AST_NODE_TYPES.Literal) {
17
+ return attribute.value.expression.value;
18
+ }
19
+ return undefined;
20
+ };
21
+ export default getLiteralPropertyValue;
@@ -0,0 +1,8 @@
1
+ import { type TSESTree } from "@typescript-eslint/types";
2
+ /**
3
+ * The visible text a node renders, where every part of it is a plain literal.
4
+ * Anything resolved at runtime — an i18n call, an interpolation — reads as
5
+ * undefined, since a substring check against it would be guesswork.
6
+ */
7
+ declare const getLiteralTextChild: (children: TSESTree.JSXChild[]) => string | undefined;
8
+ export default getLiteralTextChild;
@@ -0,0 +1,40 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/types";
2
+ import { TEXTS } from "../constants/react-native-components.js";
3
+ /** A childless non-text element renders no words of its own. */
4
+ const hasNoWords = (node) => node.openingElement.name.type === AST_NODE_TYPES.JSXIdentifier &&
5
+ !TEXTS.includes(node.openingElement.name.name) &&
6
+ node.children.length === 0;
7
+ /** The literal words in these children, or undefined if any part is computed. */
8
+ const wordsIn = (children) => children.reduce((words, child) => {
9
+ if (!words) {
10
+ return;
11
+ }
12
+ if (child.type === AST_NODE_TYPES.JSXText) {
13
+ const text = child.value.trim();
14
+ return text.length > 0 ? [...words, text] : words;
15
+ }
16
+ if (child.type === AST_NODE_TYPES.JSXExpressionContainer) {
17
+ // A value only known at runtime cannot be compared against
18
+ return child.expression.type === AST_NODE_TYPES.Literal
19
+ ? [...words, String(child.expression.value)]
20
+ : undefined;
21
+ }
22
+ if (child.type === AST_NODE_TYPES.JSXElement) {
23
+ if (hasNoWords(child)) {
24
+ return words;
25
+ }
26
+ const nested = wordsIn(child.children);
27
+ return nested ? [...words, ...nested] : undefined;
28
+ }
29
+ return words;
30
+ }, []);
31
+ /**
32
+ * The visible text a node renders, where every part of it is a plain literal.
33
+ * Anything resolved at runtime — an i18n call, an interpolation — reads as
34
+ * undefined, since a substring check against it would be guesswork.
35
+ */
36
+ const getLiteralTextChild = (children) => {
37
+ const words = wordsIn(children);
38
+ return words && words.length > 0 ? words.join(" ") : undefined;
39
+ };
40
+ export default getLiteralTextChild;
@@ -0,0 +1,4 @@
1
+ import { type TSESTree } from "@typescript-eslint/types";
2
+ /** The first attribute on `node` matching one of `propNames`, if any. */
3
+ declare const getProperty: (node: TSESTree.JSXOpeningElement, propertyNames: string | string[]) => TSESTree.JSXAttribute | undefined;
4
+ export default getProperty;
@@ -0,0 +1,9 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/types";
2
+ /** The first attribute on `node` matching one of `propNames`, if any. */
3
+ const getProperty = (node, propertyNames) => {
4
+ const names = Array.isArray(propertyNames) ? propertyNames : [propertyNames];
5
+ return node.attributes.find((attribute) => attribute.type === AST_NODE_TYPES.JSXAttribute &&
6
+ attribute.name.type === AST_NODE_TYPES.JSXIdentifier &&
7
+ names.includes(attribute.name.name));
8
+ };
9
+ export default getProperty;
@@ -0,0 +1,6 @@
1
+ import { type TSESTree } from "@typescript-eslint/types";
2
+ /** An `accessibilityLabel=""` clears the name rather than setting one. */
3
+ export declare const hasEmptyLabel: (node: TSESTree.JSXOpeningElement) => boolean;
4
+ /** The label's text where it is a plain literal, otherwise undefined. */
5
+ export declare const getLiteralLabel: (node: TSESTree.JSXOpeningElement) => string | undefined;
6
+ export declare const hasLabelProperty: (node: TSESTree.JSXOpeningElement) => boolean;
@@ -0,0 +1,27 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/types";
2
+ import getLiteralPropertyValue from "./get-literal-property-value.js";
3
+ import getProperty from "./get-property.js";
4
+ const LABEL_PROPS = ["accessibilityLabel", "aria-label"];
5
+ /** An `accessibilityLabel=""` clears the name rather than setting one. */
6
+ export const hasEmptyLabel = (node) => {
7
+ const attribute = getProperty(node, LABEL_PROPS);
8
+ if (!attribute?.value) {
9
+ return false;
10
+ }
11
+ if (attribute.value.type === AST_NODE_TYPES.Literal) {
12
+ return attribute.value.value === "";
13
+ }
14
+ if (attribute.value.type !== AST_NODE_TYPES.JSXExpressionContainer) {
15
+ return false;
16
+ }
17
+ const { expression } = attribute.value;
18
+ return expression.type === AST_NODE_TYPES.Literal ? expression.value === "" : (expression.type === AST_NODE_TYPES.TemplateLiteral &&
19
+ expression.expressions.length === 0 &&
20
+ expression.quasis.every((quasi) => quasi.value.raw === ""));
21
+ };
22
+ /** The label's text where it is a plain literal, otherwise undefined. */
23
+ export const getLiteralLabel = (node) => {
24
+ const value = getLiteralPropertyValue(node, LABEL_PROPS);
25
+ return typeof value === "string" && value.length > 0 ? value : undefined;
26
+ };
27
+ export const hasLabelProperty = (node) => getProperty(node, LABEL_PROPS) !== undefined;
@@ -0,0 +1,3 @@
1
+ import type { TSESTree } from "@typescript-eslint/types";
2
+ declare const hasProperty: (node: TSESTree.JSXOpeningElement, propertyNames: string | string[]) => boolean;
3
+ export default hasProperty;
@@ -0,0 +1,3 @@
1
+ import getProperty from "./get-property.js";
2
+ const hasProperty = (node, propertyNames) => getProperty(node, propertyNames) !== undefined;
3
+ export default hasProperty;
@@ -0,0 +1,8 @@
1
+ import { type TSESTree } from "@typescript-eslint/types";
2
+ /**
3
+ * A node spreading unknown props may already carry the accessibility prop a
4
+ * rule is looking for. Wrapper components pass them down this way constantly,
5
+ * so reporting one is a false positive rather than a finding.
6
+ */
7
+ declare const hasSpreadProps: (node: TSESTree.JSXOpeningElement) => boolean;
8
+ export default hasSpreadProps;
@@ -0,0 +1,8 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/types";
2
+ /**
3
+ * A node spreading unknown props may already carry the accessibility prop a
4
+ * rule is looking for. Wrapper components pass them down this way constantly,
5
+ * so reporting one is a false positive rather than a finding.
6
+ */
7
+ const hasSpreadProps = (node) => node.attributes.some((attribute) => attribute.type === AST_NODE_TYPES.JSXSpreadAttribute);
8
+ export default hasSpreadProps;
@@ -0,0 +1,7 @@
1
+ import { type TSESTree } from "@typescript-eslint/types";
2
+ /**
3
+ * Whether the node is collapsed into one focus stop for certain. Reporting
4
+ * needs that certainty: `accessible={isGrouped}` may well be false at runtime.
5
+ */
6
+ declare const isDefinitelyGrouped: (node: TSESTree.JSXOpeningElement) => boolean;
7
+ export default isDefinitelyGrouped;
@@ -0,0 +1,20 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/types";
2
+ import getProperty from "./get-property.js";
3
+ /**
4
+ * Whether the node is collapsed into one focus stop for certain. Reporting
5
+ * needs that certainty: `accessible={isGrouped}` may well be false at runtime.
6
+ */
7
+ const isDefinitelyGrouped = (node) => {
8
+ const attribute = getProperty(node, "accessible");
9
+ if (!attribute) {
10
+ return false;
11
+ }
12
+ // A bare `accessible` is `true`
13
+ if (!attribute.value) {
14
+ return true;
15
+ }
16
+ return (attribute.value.type === AST_NODE_TYPES.JSXExpressionContainer &&
17
+ attribute.value.expression.type === AST_NODE_TYPES.Literal &&
18
+ attribute.value.expression.value === true);
19
+ };
20
+ export default isDefinitelyGrouped;
@@ -0,0 +1,19 @@
1
+ import { type TSESTree } from "@typescript-eslint/types";
2
+ /**
3
+ * Whether this node hides everything below it. `accessible={false}` does not:
4
+ * it drops the node as a focus stop and leaves its children reachable.
5
+ */
6
+ export declare const isHidingSubtree: (node: TSESTree.JSXOpeningElement) => boolean;
7
+ /**
8
+ * Whether the node is hidden for certain. Reporting a node needs this
9
+ * certainty, since a value computed at runtime may well be `false`.
10
+ */
11
+ export declare const isDefinitelyHidden: (node: TSESTree.JSXOpeningElement) => boolean;
12
+ /**
13
+ * Decorative content is hidden from assistive tech rather than labelled.
14
+ *
15
+ * A value computed at runtime counts as hidden here: suppressing a report on a
16
+ * node that may be hidden beats reporting one that is.
17
+ */
18
+ declare const isHiddenFromAccessibility: (node: TSESTree.JSXOpeningElement) => boolean;
19
+ export default isHiddenFromAccessibility;
@@ -0,0 +1,51 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/types";
2
+ import { HIDDEN_PROPS } from "../constants/accessibility-props.js";
3
+ import getProperty from "./get-property.js";
4
+ const literalValue = (attribute) => {
5
+ // A bare `aria-hidden` is `true`
6
+ if (!attribute.value) {
7
+ return true;
8
+ }
9
+ if (attribute.value.type === AST_NODE_TYPES.Literal) {
10
+ return attribute.value.value;
11
+ }
12
+ if (attribute.value.type === AST_NODE_TYPES.JSXExpressionContainer &&
13
+ attribute.value.expression.type === AST_NODE_TYPES.Literal) {
14
+ return attribute.value.expression.value;
15
+ }
16
+ return;
17
+ };
18
+ const isHidingDescendants = (node) => {
19
+ const attribute = getProperty(node, "importantForAccessibility");
20
+ return attribute !== undefined && literalValue(attribute) === "no-hide-descendants";
21
+ };
22
+ /**
23
+ * Whether this node hides everything below it. `accessible={false}` does not:
24
+ * it drops the node as a focus stop and leaves its children reachable.
25
+ */
26
+ export const isHidingSubtree = (node) => {
27
+ const hidden = getProperty(node, HIDDEN_PROPS);
28
+ return ((hidden !== undefined && literalValue(hidden) !== false) ||
29
+ isHidingDescendants(node));
30
+ };
31
+ /**
32
+ * Whether the node is hidden for certain. Reporting a node needs this
33
+ * certainty, since a value computed at runtime may well be `false`.
34
+ */
35
+ export const isDefinitelyHidden = (node) => {
36
+ const hidden = getProperty(node, HIDDEN_PROPS);
37
+ return ((hidden !== undefined && literalValue(hidden) === true) ||
38
+ isHidingDescendants(node));
39
+ };
40
+ /**
41
+ * Decorative content is hidden from assistive tech rather than labelled.
42
+ *
43
+ * A value computed at runtime counts as hidden here: suppressing a report on a
44
+ * node that may be hidden beats reporting one that is.
45
+ */
46
+ const isHiddenFromAccessibility = (node) => {
47
+ const hidden = getProperty(node, HIDDEN_PROPS);
48
+ return ((hidden !== undefined && literalValue(hidden) !== false) ||
49
+ isHidingDescendants(node));
50
+ };
51
+ export default isHiddenFromAccessibility;
@@ -0,0 +1,9 @@
1
+ import { type TSESTree } from "@typescript-eslint/types";
2
+ /**
3
+ * Whether an ancestor already answers for this node: a touchable is one focus
4
+ * stop, so its children are not stepped through separately and the role and
5
+ * label belong on the surface rather than on every child. The same holds for a
6
+ * composite control grouped with `accessible`, and for a hidden subtree.
7
+ */
8
+ declare const isInsideFocusStop: (node: TSESTree.JSXOpeningElement) => boolean;
9
+ export default isInsideFocusStop;
@@ -0,0 +1,36 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/types";
2
+ import { TOUCHABLES } from "../constants/react-native-components.js";
3
+ import getElementName from "./get-element-name.js";
4
+ import getProperty from "./get-property.js";
5
+ import { isHidingSubtree } from "./is-hidden-from-accessibility.js";
6
+ const jsxAncestors = (node) => {
7
+ const ancestors = [];
8
+ // The node's own JSXElement is `node.parent`, so ancestry starts above it
9
+ let current = node.parent.parent;
10
+ while (current) {
11
+ if (current.type === AST_NODE_TYPES.JSXElement) {
12
+ ancestors.push(current.openingElement);
13
+ }
14
+ current = current.parent;
15
+ }
16
+ return ancestors;
17
+ };
18
+ /** A composite control marked as one focus stop carries the name for all of it. */
19
+ const isGrouped = (node) => {
20
+ const accessible = getProperty(node, "accessible");
21
+ return accessible !== undefined && !(accessible.value?.type === AST_NODE_TYPES.JSXExpressionContainer &&
22
+ accessible.value.expression.type === AST_NODE_TYPES.Literal &&
23
+ accessible.value.expression.value === false);
24
+ };
25
+ const isTouchable = (node) => {
26
+ const name = getElementName(node);
27
+ return name !== undefined && TOUCHABLES.includes(name);
28
+ };
29
+ /**
30
+ * Whether an ancestor already answers for this node: a touchable is one focus
31
+ * stop, so its children are not stepped through separately and the role and
32
+ * label belong on the surface rather than on every child. The same holds for a
33
+ * composite control grouped with `accessible`, and for a hidden subtree.
34
+ */
35
+ const isInsideFocusStop = (node) => jsxAncestors(node).some((ancestor) => isTouchable(ancestor) || isGrouped(ancestor) || isHidingSubtree(ancestor));
36
+ export default isInsideFocusStop;
@@ -0,0 +1,16 @@
1
+ import { type TSESTree } from "@typescript-eslint/types";
2
+ /**
3
+ * `accessible` makes the node a single focus stop. It defaults to true on the
4
+ * touchables and false elsewhere, so only an explicit prop is read here.
5
+ */
6
+ export declare const isMarkedAccessible: (node: TSESTree.JSXOpeningElement) => boolean;
7
+ export declare const hasInteractiveRole: (node: TSESTree.JSXOpeningElement) => boolean;
8
+ export declare const hasTouchHandler: (node: TSESTree.JSXOpeningElement) => boolean;
9
+ /**
10
+ * A node a user can operate: an RN host (or a wrapper listed as one) that
11
+ * announces itself as a control, handles a touch, or has been collapsed into
12
+ * one focus stop on purpose. An unlisted wrapper's `onPress` / `role` /
13
+ * `accessible` are its own API, not a sign that this JSX node is the control.
14
+ */
15
+ declare const isInteractive: (node: TSESTree.JSXOpeningElement, extraHosts?: string[]) => boolean;
16
+ export default isInteractive;
@@ -0,0 +1,38 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/types";
2
+ import { INTERACTIVE_ROLES, TOUCH_HANDLER_PROPS } from "../constants/interactive.js";
3
+ import { IMAGES, INPUTS, TEXTS, TOUCHABLES, } from "../constants/react-native-components.js";
4
+ import getElementName from "./get-element-name.js";
5
+ import getLiteralPropertyValue from "./get-literal-property-value.js";
6
+ import getProperty from "./get-property.js";
7
+ import hasProperty from "./has-property.js";
8
+ const RN_HOSTS = new Set(["View", ...TOUCHABLES, ...TEXTS, ...IMAGES, ...INPUTS]);
9
+ /**
10
+ * `accessible` makes the node a single focus stop. It defaults to true on the
11
+ * touchables and false elsewhere, so only an explicit prop is read here.
12
+ */
13
+ export const isMarkedAccessible = (node) => {
14
+ const attribute = getProperty(node, "accessible");
15
+ return attribute !== undefined && !(attribute.value?.type === AST_NODE_TYPES.JSXExpressionContainer &&
16
+ attribute.value.expression.type === AST_NODE_TYPES.Literal &&
17
+ attribute.value.expression.value === false);
18
+ };
19
+ export const hasInteractiveRole = (node) => {
20
+ const role = getLiteralPropertyValue(node, ["accessibilityRole", "role"]);
21
+ return typeof role === "string" && INTERACTIVE_ROLES.includes(role);
22
+ };
23
+ export const hasTouchHandler = (node) => hasProperty(node, TOUCH_HANDLER_PROPS);
24
+ const isHost = (node, extraHosts) => {
25
+ const name = getElementName(node);
26
+ return (name !== undefined && (RN_HOSTS.has(name) || extraHosts.includes(name)));
27
+ };
28
+ /**
29
+ * A node a user can operate: an RN host (or a wrapper listed as one) that
30
+ * announces itself as a control, handles a touch, or has been collapsed into
31
+ * one focus stop on purpose. An unlisted wrapper's `onPress` / `role` /
32
+ * `accessible` are its own API, not a sign that this JSX node is the control.
33
+ */
34
+ const isInteractive = (node, extraHosts = []) => isHost(node, extraHosts) &&
35
+ (isMarkedAccessible(node) ||
36
+ hasInteractiveRole(node) ||
37
+ hasTouchHandler(node));
38
+ export default isInteractive;
@@ -0,0 +1,7 @@
1
+ import { type TSESTree } from "@typescript-eslint/types";
2
+ /**
3
+ * `accessible={false}` only opts a touchable out of being a focus stop. On an
4
+ * `Image` or `View` it is not a hide, and cannot excuse a missing name or role.
5
+ */
6
+ declare const isNotAFocusStop: (node: TSESTree.JSXOpeningElement) => boolean;
7
+ export default isNotAFocusStop;
@@ -0,0 +1,19 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/types";
2
+ import { TOUCHABLES } from "../constants/react-native-components.js";
3
+ import getElementName from "./get-element-name.js";
4
+ import getProperty from "./get-property.js";
5
+ /**
6
+ * `accessible={false}` only opts a touchable out of being a focus stop. On an
7
+ * `Image` or `View` it is not a hide, and cannot excuse a missing name or role.
8
+ */
9
+ const isNotAFocusStop = (node) => {
10
+ const name = getElementName(node);
11
+ if (name === undefined || !TOUCHABLES.includes(name)) {
12
+ return false;
13
+ }
14
+ const attribute = getProperty(node, "accessible");
15
+ return (attribute?.value?.type === AST_NODE_TYPES.JSXExpressionContainer &&
16
+ attribute.value.expression.type === AST_NODE_TYPES.Literal &&
17
+ attribute.value.expression.value === false);
18
+ };
19
+ export default isNotAFocusStop;