@hypersolid-dev/eslint-config 5.0.0 → 7.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 (140) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +37 -21
  3. package/dist/base.js +19 -6
  4. package/dist/jest.d.ts +3 -0
  5. package/dist/jest.js +56 -0
  6. package/dist/plugins/{hypersolid.d.ts → base/index.d.ts} +1 -0
  7. package/dist/plugins/base/index.js +15 -0
  8. package/dist/plugins/{rules → base/rules}/comment-style.js +45 -36
  9. package/dist/plugins/{rules → base/rules}/regex-pattern-const.js +2 -2
  10. package/dist/plugins/{rules → base/rules}/undefined-check.js +4 -4
  11. package/dist/plugins/react/index.d.ts +4 -0
  12. package/dist/plugins/react/index.js +23 -0
  13. package/dist/plugins/{rules → react/rules}/boolean-guard.js +5 -5
  14. package/dist/plugins/{rules → react/rules}/effect-takes-callback.js +2 -2
  15. package/dist/plugins/{rules → react/rules}/jsdoc-for-props.js +3 -3
  16. package/dist/plugins/{rules → react/rules}/no-callback-payload.js +20 -17
  17. package/dist/plugins/{rules → react/rules}/no-markup-variable.js +43 -37
  18. package/dist/plugins/{rules → react/rules}/no-unnecessary-use-callback.js +3 -3
  19. package/dist/plugins/react/rules/props-destructuring.d.ts +3 -0
  20. package/dist/plugins/{props-destructuring.js → react/rules/props-destructuring.js} +13 -17
  21. package/dist/plugins/{react-native-a11y → react-native/a11y}/index.js +1 -1
  22. package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/accessibility-actions-need-handler.js +3 -3
  23. package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/accessibility-hint.js +3 -3
  24. package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/accessibility-label.js +3 -3
  25. package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/accessibility-role.js +3 -3
  26. package/dist/plugins/react-native/a11y/rules/accessibility-value-needs-range.js +47 -0
  27. package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/adjustable-needs-value.js +17 -19
  28. package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/gate-announce-for-accessibility.js +5 -5
  29. package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/input-purpose.js +3 -3
  30. package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-aria-legacy-mix.js +21 -23
  31. package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-deprecated-accessibility-focus.js +13 -15
  32. package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-dropped-aria-on-text.js +3 -3
  33. package/dist/plugins/react-native/a11y/rules/no-font-scaling-disabled.js +47 -0
  34. package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-ineffective-accessibility-props.js +35 -37
  35. package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-invalid-accessibility-state.js +32 -34
  36. package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-nested-focus-stops.js +3 -3
  37. package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-press-in-activation.js +3 -3
  38. package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-unnamed-interactive.js +3 -3
  39. package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/require-invert-colors-opt-out.js +3 -3
  40. package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/require-modal-a11y-on-sheets.js +3 -3
  41. package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-amount-of-text-children.js +15 -13
  42. package/dist/plugins/{props-destructuring.d.ts → react-native/index.d.ts} +1 -0
  43. package/dist/plugins/react-native/index.js +17 -0
  44. package/dist/plugins/{rules → react-native/rules}/test-id-default.js +24 -28
  45. package/dist/plugins/{rules → react-native/rules}/test-id-format.js +16 -20
  46. package/dist/plugins/{rules → react-native/rules}/test-id-from-props.js +25 -29
  47. package/dist/plugins/{rules → react-native/rules}/webview-allow-list.js +24 -28
  48. package/dist/plugins/{utils → shared}/expressions.d.ts +3 -3
  49. package/dist/plugins/{utils → shared}/expressions.js +94 -78
  50. package/dist/plugins/{utils → shared}/types.js +14 -12
  51. package/dist/react-native.js +37 -8
  52. package/dist/shared.d.ts +3 -3
  53. package/dist/shared.js +169 -113
  54. package/package.json +12 -2
  55. package/dist/plugins/hypersolid.js +0 -38
  56. package/dist/plugins/react-native-a11y/rules/accessibility-value-needs-range.js +0 -49
  57. package/dist/plugins/react-native-a11y/rules/no-font-scaling-disabled.js +0 -49
  58. package/dist/plugins/react-native-a11y/utils/create-rule.js +0 -3
  59. package/dist/plugins/utils/create-rule.d.ts +0 -5
  60. /package/dist/plugins/{rules → base/rules}/comment-style.d.ts +0 -0
  61. /package/dist/plugins/{rules → base/rules}/regex-pattern-const.d.ts +0 -0
  62. /package/dist/plugins/{rules → base/rules}/undefined-check.d.ts +0 -0
  63. /package/dist/plugins/{rules → react/rules}/boolean-guard.d.ts +0 -0
  64. /package/dist/plugins/{rules → react/rules}/effect-takes-callback.d.ts +0 -0
  65. /package/dist/plugins/{rules → react/rules}/jsdoc-for-props.d.ts +0 -0
  66. /package/dist/plugins/{rules → react/rules}/no-callback-payload.d.ts +0 -0
  67. /package/dist/plugins/{rules → react/rules}/no-markup-variable.d.ts +0 -0
  68. /package/dist/plugins/{rules → react/rules}/no-unnecessary-use-callback.d.ts +0 -0
  69. /package/dist/plugins/{react-native-a11y → react-native/a11y}/constants/accessibility-props.d.ts +0 -0
  70. /package/dist/plugins/{react-native-a11y → react-native/a11y}/constants/accessibility-props.js +0 -0
  71. /package/dist/plugins/{react-native-a11y → react-native/a11y}/constants/accessibility-state.d.ts +0 -0
  72. /package/dist/plugins/{react-native-a11y → react-native/a11y}/constants/accessibility-state.js +0 -0
  73. /package/dist/plugins/{react-native-a11y → react-native/a11y}/constants/interactive.d.ts +0 -0
  74. /package/dist/plugins/{react-native-a11y → react-native/a11y}/constants/interactive.js +0 -0
  75. /package/dist/plugins/{react-native-a11y → react-native/a11y}/constants/react-native-components.d.ts +0 -0
  76. /package/dist/plugins/{react-native-a11y → react-native/a11y}/constants/react-native-components.js +0 -0
  77. /package/dist/plugins/{react-native-a11y → react-native/a11y}/constants/schema-properties.d.ts +0 -0
  78. /package/dist/plugins/{react-native-a11y → react-native/a11y}/constants/schema-properties.js +0 -0
  79. /package/dist/plugins/{react-native-a11y → react-native/a11y}/index.d.ts +0 -0
  80. /package/dist/plugins/{react-native-a11y → react-native/a11y}/models/optional-options.d.ts +0 -0
  81. /package/dist/plugins/{react-native-a11y → react-native/a11y}/models/optional-options.js +0 -0
  82. /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/accessibility-actions-need-handler.d.ts +0 -0
  83. /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/accessibility-hint.d.ts +0 -0
  84. /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/accessibility-label.d.ts +0 -0
  85. /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/accessibility-role.d.ts +0 -0
  86. /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/accessibility-value-needs-range.d.ts +0 -0
  87. /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/adjustable-needs-value.d.ts +0 -0
  88. /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/gate-announce-for-accessibility.d.ts +0 -0
  89. /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/input-purpose.d.ts +0 -0
  90. /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-aria-legacy-mix.d.ts +0 -0
  91. /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-deprecated-accessibility-focus.d.ts +0 -0
  92. /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-dropped-aria-on-text.d.ts +0 -0
  93. /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-font-scaling-disabled.d.ts +0 -0
  94. /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-ineffective-accessibility-props.d.ts +0 -0
  95. /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-invalid-accessibility-state.d.ts +0 -0
  96. /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-nested-focus-stops.d.ts +0 -0
  97. /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-press-in-activation.d.ts +0 -0
  98. /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-unnamed-interactive.d.ts +0 -0
  99. /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/require-invert-colors-opt-out.d.ts +0 -0
  100. /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/require-modal-a11y-on-sheets.d.ts +0 -0
  101. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-amount-of-text-children.d.ts +0 -0
  102. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-components.d.ts +0 -0
  103. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-components.js +0 -0
  104. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-element-name.d.ts +0 -0
  105. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-element-name.js +0 -0
  106. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-jsx-descendants.d.ts +0 -0
  107. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-jsx-descendants.js +0 -0
  108. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-literal-property-value.d.ts +0 -0
  109. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-literal-property-value.js +0 -0
  110. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-literal-text-child.d.ts +0 -0
  111. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-literal-text-child.js +0 -0
  112. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-property.d.ts +0 -0
  113. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-property.js +0 -0
  114. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/has-accessible-name.d.ts +0 -0
  115. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/has-accessible-name.js +0 -0
  116. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/has-property.d.ts +0 -0
  117. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/has-property.js +0 -0
  118. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/has-spread-props.d.ts +0 -0
  119. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/has-spread-props.js +0 -0
  120. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/is-definitely-grouped.d.ts +0 -0
  121. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/is-definitely-grouped.js +0 -0
  122. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/is-hidden-from-accessibility.d.ts +0 -0
  123. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/is-hidden-from-accessibility.js +0 -0
  124. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/is-inside-focus-stop.d.ts +0 -0
  125. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/is-inside-focus-stop.js +0 -0
  126. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/is-interactive.d.ts +0 -0
  127. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/is-interactive.js +0 -0
  128. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/is-not-a-focus-stop.d.ts +0 -0
  129. /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/is-not-a-focus-stop.js +0 -0
  130. /package/dist/plugins/{rules → react-native/rules}/test-id-default.d.ts +0 -0
  131. /package/dist/plugins/{rules → react-native/rules}/test-id-format.d.ts +0 -0
  132. /package/dist/plugins/{rules → react-native/rules}/test-id-from-props.d.ts +0 -0
  133. /package/dist/plugins/{rules → react-native/rules}/webview-allow-list.d.ts +0 -0
  134. /package/dist/plugins/{utils → shared}/comments.d.ts +0 -0
  135. /package/dist/plugins/{utils → shared}/comments.js +0 -0
  136. /package/dist/plugins/{utils → shared}/component-name.d.ts +0 -0
  137. /package/dist/plugins/{utils → shared}/component-name.js +0 -0
  138. /package/dist/plugins/{react-native-a11y/utils → shared}/create-rule.d.ts +0 -0
  139. /package/dist/plugins/{utils → shared}/create-rule.js +0 -0
  140. /package/dist/plugins/{utils → shared}/types.d.ts +0 -0
@@ -1,7 +1,7 @@
1
1
  import { ARIA_DROPPED_ON_TEXT } from "../constants/accessibility-props.js";
2
2
  import { TEXTS } from "../constants/react-native-components.js";
3
3
  import schemaProperties from "../constants/schema-properties.js";
4
- import createRule from "../utils/create-rule.js";
4
+ import createRule from "../../../shared/create-rule.js";
5
5
  import getComponents from "../utils/get-components.js";
6
6
  import getElementName from "../utils/get-element-name.js";
7
7
  import getProperty from "../utils/get-property.js";
@@ -36,13 +36,13 @@ const noDroppedAriaOnText = createRule({
36
36
  },
37
37
  ],
38
38
  },
39
- create(context) {
39
+ create: (context) => {
40
40
  const components = getComponents({
41
41
  context,
42
42
  defaultComponents: TEXTS,
43
43
  });
44
44
  return {
45
- JSXOpeningElement(node) {
45
+ JSXOpeningElement: (node) => {
46
46
  const componentName = getElementName(node);
47
47
  if (componentName === undefined) {
48
48
  return;
@@ -0,0 +1,47 @@
1
+ import createRule from "../../../shared/create-rule.js";
2
+ import getLiteralPropertyValue from "../utils/get-literal-property-value.js";
3
+ import getProperty from "../utils/get-property.js";
4
+ /** WCAG 1.4.4 asks for text to resize up to 200%. */
5
+ const MINIMUM_MULTIPLIER = 2;
6
+ /** React Native reads `0` as "no maximum", which is the opposite of a cap. */
7
+ const NO_MAXIMUM = 0;
8
+ const noFontScalingDisabled = createRule({
9
+ name: "no-font-scaling-disabled",
10
+ meta: {
11
+ defaultOptions: [],
12
+ type: "problem",
13
+ docs: {
14
+ description: "Disallow opting text out of the system font scale below the level WCAG 1.4.4 requires.",
15
+ },
16
+ messages: {
17
+ fontScalingDisabled: "'allowFontScaling={false}' stops this text respecting the system font size. Cap it with 'maxFontSizeMultiplier' instead if one element cannot grow.",
18
+ fontScalingCapped: "'maxFontSizeMultiplier={{{ multiplier }}}' caps text below the 200% WCAG 1.4.4 requires. Reserve a cap under {{ minimum }} for chrome whose information is available elsewhere.",
19
+ },
20
+ schema: [],
21
+ },
22
+ create: (context) => ({
23
+ JSXOpeningElement: (node) => {
24
+ const allowFontScaling = getProperty(node, "allowFontScaling");
25
+ if (allowFontScaling &&
26
+ getLiteralPropertyValue(node, "allowFontScaling") === false) {
27
+ context.report({
28
+ node: allowFontScaling,
29
+ messageId: "fontScalingDisabled",
30
+ });
31
+ }
32
+ const cap = getProperty(node, "maxFontSizeMultiplier");
33
+ const multiplier = getLiteralPropertyValue(node, "maxFontSizeMultiplier");
34
+ if (cap &&
35
+ typeof multiplier === "number" &&
36
+ multiplier !== NO_MAXIMUM &&
37
+ multiplier < MINIMUM_MULTIPLIER) {
38
+ context.report({
39
+ node: cap,
40
+ messageId: "fontScalingCapped",
41
+ data: { multiplier, minimum: MINIMUM_MULTIPLIER },
42
+ });
43
+ }
44
+ },
45
+ }),
46
+ });
47
+ export default noFontScalingDisabled;
@@ -1,5 +1,5 @@
1
1
  import { IMAGES, VIEWS } from "../constants/react-native-components.js";
2
- import createRule from "../utils/create-rule.js";
2
+ import createRule from "../../../shared/create-rule.js";
3
3
  import getElementName from "../utils/get-element-name.js";
4
4
  import getLiteralPropertyValue from "../utils/get-literal-property-value.js";
5
5
  import getProperty from "../utils/get-property.js";
@@ -31,46 +31,44 @@ const noIneffectiveAccessibilityProps = createRule({
31
31
  },
32
32
  schema: [],
33
33
  },
34
- create(context) {
35
- return {
36
- JSXOpeningElement(node) {
37
- const role = getLiteralPropertyValue(node, ["accessibilityRole", "role"]);
38
- if (role === "listitem") {
39
- context.report({ node, messageId: "listItemRole" });
34
+ create: (context) => ({
35
+ JSXOpeningElement: (node) => {
36
+ const role = getLiteralPropertyValue(node, ["accessibilityRole", "role"]);
37
+ if (role === "listitem") {
38
+ context.report({ node, messageId: "listItemRole" });
39
+ }
40
+ if (role === "alert") {
41
+ context.report({ node, messageId: "alertRole" });
42
+ }
43
+ const isHidden = isDefinitelyHidden(node);
44
+ if (isHidden && hasLabelProperty(node)) {
45
+ context.report({ node, messageId: "hiddenWithLabel" });
46
+ }
47
+ if (getLiteralPropertyValue(node, "accessible") === false) {
48
+ const name = getElementName(node);
49
+ if (isHidden) {
50
+ context.report({ node, messageId: "hiddenWithAccessibleFalse" });
40
51
  }
41
- if (role === "alert") {
42
- context.report({ node, messageId: "alertRole" });
43
- }
44
- const isHidden = isDefinitelyHidden(node);
45
- if (isHidden && hasLabelProperty(node)) {
46
- context.report({ node, messageId: "hiddenWithLabel" });
47
- }
48
- if (getLiteralPropertyValue(node, "accessible") === false) {
49
- const name = getElementName(node);
50
- if (isHidden) {
51
- context.report({ node, messageId: "hiddenWithAccessibleFalse" });
52
- }
53
- else if (name !== undefined &&
54
- DEFAULTS_TO_INACCESSIBLE.has(name) &&
55
- // A spread may carry the `accessible` this one is overriding.
56
- !hasSpreadProps(node)) {
57
- context.report({
58
- node,
59
- messageId: "accessibleFalseIsDefault",
60
- data: { name },
61
- });
62
- }
63
- }
64
- const labelledBy = getProperty(node, LABELLED_BY_PROPS);
65
- if (labelledBy && !hasLabelProperty(node)) {
52
+ else if (name !== undefined &&
53
+ DEFAULTS_TO_INACCESSIBLE.has(name) &&
54
+ // A spread may carry the `accessible` this one is overriding.
55
+ !hasSpreadProps(node)) {
66
56
  context.report({
67
57
  node,
68
- messageId: "labelledByWithoutLabel",
69
- data: { prop: labelledBy.name.name },
58
+ messageId: "accessibleFalseIsDefault",
59
+ data: { name },
70
60
  });
71
61
  }
72
- },
73
- };
74
- },
62
+ }
63
+ const labelledBy = getProperty(node, LABELLED_BY_PROPS);
64
+ if (labelledBy && !hasLabelProperty(node)) {
65
+ context.report({
66
+ node,
67
+ messageId: "labelledByWithoutLabel",
68
+ data: { prop: labelledBy.name.name },
69
+ });
70
+ }
71
+ },
72
+ }),
75
73
  });
76
74
  export default noIneffectiveAccessibilityProps;
@@ -1,6 +1,6 @@
1
1
  import { ASTUtils, AST_NODE_TYPES, } from "@typescript-eslint/utils";
2
2
  import { ACCESSIBILITY_STATE_KEYS } from "../constants/accessibility-state.js";
3
- import createRule from "../utils/create-rule.js";
3
+ import createRule from "../../../shared/create-rule.js";
4
4
  import getProperty from "../utils/get-property.js";
5
5
  /** `{ … } as const` and `{ … } satisfies T` are still that object. */
6
6
  const unwrap = (node) => {
@@ -44,39 +44,37 @@ const noInvalidAccessibilityState = createRule({
44
44
  },
45
45
  schema: [],
46
46
  },
47
- create(context) {
48
- return {
49
- JSXOpeningElement(node) {
50
- const attribute = getProperty(node, "accessibilityState");
51
- if (attribute?.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) {
52
- return;
47
+ create: (context) => ({
48
+ JSXOpeningElement: (node) => {
49
+ const attribute = getProperty(node, "accessibilityState");
50
+ if (attribute?.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) {
51
+ return;
52
+ }
53
+ const { expression } = attribute.value;
54
+ // An object literal here is already rejected by the React Native types
55
+ if (expression.type !== AST_NODE_TYPES.Identifier) {
56
+ return;
57
+ }
58
+ const resolved = resolveObjectBinding(context.sourceCode.getScope(expression), expression);
59
+ if (!resolved) {
60
+ return;
61
+ }
62
+ for (const property of resolved.properties) {
63
+ if (property.type !== AST_NODE_TYPES.Property ||
64
+ property.key.type !== AST_NODE_TYPES.Identifier ||
65
+ ACCESSIBILITY_STATE_KEYS.includes(property.key.name)) {
66
+ continue;
53
67
  }
54
- const { expression } = attribute.value;
55
- // An object literal here is already rejected by the React Native types
56
- if (expression.type !== AST_NODE_TYPES.Identifier) {
57
- return;
58
- }
59
- const resolved = resolveObjectBinding(context.sourceCode.getScope(expression), expression);
60
- if (!resolved) {
61
- return;
62
- }
63
- for (const property of resolved.properties) {
64
- if (property.type !== AST_NODE_TYPES.Property ||
65
- property.key.type !== AST_NODE_TYPES.Identifier ||
66
- ACCESSIBILITY_STATE_KEYS.includes(property.key.name)) {
67
- continue;
68
- }
69
- context.report({
70
- node: property,
71
- messageId: "invalidStateKey",
72
- data: {
73
- key: property.key.name,
74
- supported: ACCESSIBILITY_STATE_KEYS.join(", "),
75
- },
76
- });
77
- }
78
- },
79
- };
80
- },
68
+ context.report({
69
+ node: property,
70
+ messageId: "invalidStateKey",
71
+ data: {
72
+ key: property.key.name,
73
+ supported: ACCESSIBILITY_STATE_KEYS.join(", "),
74
+ },
75
+ });
76
+ }
77
+ },
78
+ }),
81
79
  });
82
80
  export default noInvalidAccessibilityState;
@@ -1,6 +1,6 @@
1
1
  import { TOUCHABLES } from "../constants/react-native-components.js";
2
2
  import schemaProperties from "../constants/schema-properties.js";
3
- import createRule from "../utils/create-rule.js";
3
+ import createRule from "../../../shared/create-rule.js";
4
4
  import getComponents from "../utils/get-components.js";
5
5
  import getElementName from "../utils/get-element-name.js";
6
6
  import getJsxDescendants from "../utils/get-jsx-descendants.js";
@@ -30,7 +30,7 @@ const noNestedFocusStops = createRule({
30
30
  },
31
31
  ],
32
32
  },
33
- create(context) {
33
+ create: (context) => {
34
34
  // The touchables plus whatever this project wraps them in
35
35
  const components = getComponents({
36
36
  context,
@@ -44,7 +44,7 @@ const noNestedFocusStops = createRule({
44
44
  hasTouchHandler(node));
45
45
  };
46
46
  return {
47
- JSXOpeningElement(node) {
47
+ JSXOpeningElement: (node) => {
48
48
  const componentName = getElementName(node);
49
49
  if (componentName === undefined ||
50
50
  !isDefinitelyGrouped(node) ||
@@ -1,6 +1,6 @@
1
1
  import { TOUCHABLES } from "../constants/react-native-components.js";
2
2
  import schemaProperties from "../constants/schema-properties.js";
3
- import createRule from "../utils/create-rule.js";
3
+ import createRule from "../../../shared/create-rule.js";
4
4
  import getComponents from "../utils/get-components.js";
5
5
  import getElementName from "../utils/get-element-name.js";
6
6
  import getProperty from "../utils/get-property.js";
@@ -29,13 +29,13 @@ const noPressInActivation = createRule({
29
29
  },
30
30
  ],
31
31
  },
32
- create(context) {
32
+ create: (context) => {
33
33
  const components = getComponents({
34
34
  context,
35
35
  defaultComponents: TOUCHABLES,
36
36
  });
37
37
  return {
38
- JSXOpeningElement(node) {
38
+ JSXOpeningElement: (node) => {
39
39
  const componentName = getElementName(node);
40
40
  if (componentName === undefined) {
41
41
  return;
@@ -1,7 +1,7 @@
1
1
  import { NAME_PROPS } from "../constants/interactive.js";
2
2
  import { IMAGES, INPUTS, TEXTS, TOUCHABLES, } from "../constants/react-native-components.js";
3
3
  import schemaProperties from "../constants/schema-properties.js";
4
- import createRule from "../utils/create-rule.js";
4
+ import createRule from "../../../shared/create-rule.js";
5
5
  import getAmountOfTextChildren from "../utils/get-amount-of-text-children.js";
6
6
  import getComponents from "../utils/get-components.js";
7
7
  import getElementName from "../utils/get-element-name.js";
@@ -38,10 +38,10 @@ const noUnnamedInteractive = createRule({
38
38
  },
39
39
  ],
40
40
  },
41
- create(context) {
41
+ create: (context) => {
42
42
  const components = getComponents({ context });
43
43
  return {
44
- JSXOpeningElement(node) {
44
+ JSXOpeningElement: (node) => {
45
45
  const componentName = getElementName(node);
46
46
  if (componentName === undefined) {
47
47
  return;
@@ -1,6 +1,6 @@
1
1
  import { IMAGES } from "../constants/react-native-components.js";
2
2
  import schemaProperties from "../constants/schema-properties.js";
3
- import createRule from "../utils/create-rule.js";
3
+ import createRule from "../../../shared/create-rule.js";
4
4
  import getComponents from "../utils/get-components.js";
5
5
  import getElementName from "../utils/get-element-name.js";
6
6
  import hasProperty from "../utils/has-property.js";
@@ -29,7 +29,7 @@ const requireInvertColorsOptOut = createRule({
29
29
  },
30
30
  ],
31
31
  },
32
- create(context) {
32
+ create: (context) => {
33
33
  /**
34
34
  * Matched on the element name, so an image from expo-image or a wrapper
35
35
  * named through additionalComponents is checked the same way. Deliberately
@@ -41,7 +41,7 @@ const requireInvertColorsOptOut = createRule({
41
41
  defaultComponents: IMAGES,
42
42
  });
43
43
  return {
44
- JSXOpeningElement(node) {
44
+ JSXOpeningElement: (node) => {
45
45
  const componentName = getElementName(node);
46
46
  if (componentName === undefined) {
47
47
  return;
@@ -1,5 +1,5 @@
1
1
  import schemaProperties from "../constants/schema-properties.js";
2
- import createRule from "../utils/create-rule.js";
2
+ import createRule from "../../../shared/create-rule.js";
3
3
  import getComponents from "../utils/get-components.js";
4
4
  import getElementName from "../utils/get-element-name.js";
5
5
  import hasProperty from "../utils/has-property.js";
@@ -27,7 +27,7 @@ const requireModalA11yOnSheets = createRule({
27
27
  },
28
28
  ],
29
29
  },
30
- create(context) {
30
+ create: (context) => {
31
31
  /**
32
32
  * No default list: a sheet is whatever this project calls one, and
33
33
  * hardcoding one library's component names would fire on a codebase that
@@ -35,7 +35,7 @@ const requireModalA11yOnSheets = createRule({
35
35
  */
36
36
  const components = getComponents({ context });
37
37
  return {
38
- JSXOpeningElement(node) {
38
+ JSXOpeningElement: (node) => {
39
39
  const componentName = getElementName(node);
40
40
  if (componentName === undefined) {
41
41
  return;
@@ -32,6 +32,20 @@ const branchesOf = (expression) => {
32
32
  };
33
33
  const isJsxChild = (expression) => expression.type === AST_NODE_TYPES.JSXElement ||
34
34
  expression.type === AST_NODE_TYPES.JSXFragment;
35
+ const countExpressionText = (expression, amount) => {
36
+ if (expression.type === AST_NODE_TYPES.Literal ||
37
+ expression.type === AST_NODE_TYPES.TemplateLiteral) {
38
+ return amount + 1;
39
+ }
40
+ /**
41
+ * JSX inside an expression renders like any other child, and a branch
42
+ * that renders text still names the control
43
+ */
44
+ const rendered = (isJsxChild(expression) ? [expression] : branchesOf(expression)).filter(isJsxChild);
45
+ return rendered.length > 0
46
+ ? getAmountOfTextChildren(rendered, amount)
47
+ : amount;
48
+ };
35
49
  /**
36
50
  * Counts children that render visible text, which is what makes an
37
51
  * `accessibilityLabel` redundant. Whitespace between JSX tags is not text, and
@@ -42,19 +56,7 @@ const getAmountOfTextChildren = (children, oldAmount = 0) => children.reduce((am
42
56
  return child.value.trim().length > 0 ? amount + 1 : amount;
43
57
  }
44
58
  if (child.type === AST_NODE_TYPES.JSXExpressionContainer) {
45
- const { expression } = child;
46
- if (expression.type === AST_NODE_TYPES.Literal ||
47
- expression.type === AST_NODE_TYPES.TemplateLiteral) {
48
- return amount + 1;
49
- }
50
- /**
51
- * JSX inside an expression renders like any other child, and a branch
52
- * that renders text still names the control
53
- */
54
- const rendered = (isJsxChild(expression) ? [expression] : branchesOf(expression)).filter(isJsxChild);
55
- return rendered.length > 0
56
- ? getAmountOfTextChildren(rendered, amount)
57
- : amount;
59
+ return countExpressionText(child.expression, amount);
58
60
  }
59
61
  if (child.type === AST_NODE_TYPES.JSXFragment) {
60
62
  return getAmountOfTextChildren(child.children, amount);
@@ -1,3 +1,4 @@
1
1
  import type { ESLint } from 'eslint';
2
+ /** ESLint reads a scoped rule ID up to its last slash as the plugin name. */
2
3
  declare const plugin: ESLint.Plugin;
3
4
  export default plugin;
@@ -0,0 +1,17 @@
1
+ import testIdDefault from './rules/test-id-default.js';
2
+ import testIdFormat from './rules/test-id-format.js';
3
+ import testIdFromProps from './rules/test-id-from-props.js';
4
+ import webviewAllowList from './rules/webview-allow-list.js';
5
+ /** ESLint reads a scoped rule ID up to its last slash as the plugin name. */
6
+ // One object per plugin name: flat config rejects two different objects under the same name.
7
+ const plugin = {
8
+ meta: { name: '@hypersolid-dev/react-native' },
9
+ // typescript-eslint's `RuleModule` and ESLint's `Rule.RuleModule` type the same runtime object differently.
10
+ rules: {
11
+ 'test-id-default': testIdDefault,
12
+ 'test-id-format': testIdFormat,
13
+ 'test-id-from-props': testIdFromProps,
14
+ 'webview-allow-list': webviewAllowList,
15
+ },
16
+ };
17
+ export default plugin;
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { AST_NODE_TYPES } from '@typescript-eslint/utils';
3
- import createRule from '../utils/create-rule.js';
3
+ import createRule from '../../shared/create-rule.js';
4
4
  /**
5
5
  * A component's `testID` prop defaults to the component's own name, kebab-cased.
6
6
  *
@@ -31,24 +31,20 @@ const PASCAL_CASE_PATTERN = /^[A-Z]/;
31
31
  const LOWER_TO_UPPER_PATTERN = /(?<=[a-z0-9])(?=[A-Z])/g;
32
32
  const ACRONYM_END_PATTERN = /(?<=[A-Z])(?=[A-Z][a-z])/g;
33
33
  /** `CompetitionLogo` -> `competition-logo`, `AdSlotURLBar` -> `ad-slot-url-bar`. */
34
- function kebab(name) {
35
- return name.replaceAll(LOWER_TO_UPPER_PATTERN, '-').replaceAll(ACRONYM_END_PATTERN, '-').toLowerCase();
36
- }
34
+ const kebab = (name) => name.replaceAll(LOWER_TO_UPPER_PATTERN, '-').replaceAll(ACRONYM_END_PATTERN, '-').toLowerCase();
37
35
  /** The component a file is named for: its basename, or its folder's for an `index` file. */
38
- function componentName(filename) {
36
+ const componentName = (filename) => {
39
37
  const name = path.basename(filename).replace(EXTENSION_PATTERN, '').replace(PLATFORM_PATTERN, '');
40
38
  return name === 'index' ? path.basename(path.dirname(filename)) : name;
41
- }
42
- function isHoc(callee) {
39
+ };
40
+ const isHoc = (callee) => {
43
41
  if (callee.type === AST_NODE_TYPES.Identifier)
44
42
  return HOC_NAMES.has(callee.name);
45
43
  return callee.type === AST_NODE_TYPES.MemberExpression && !callee.computed && HOC_NAMES.has(callee.property.name);
46
- }
47
- function isFunction(node) {
48
- return Boolean(node && FUNCTION_TYPES.has(node.type));
49
- }
44
+ };
45
+ const isFunction = (node) => Boolean(node && FUNCTION_TYPES.has(node.type));
50
46
  /** Through `memo(…)`, `forwardRef(…)` and type assertions, to the component inside. */
51
- function unwrapComponent(node) {
47
+ const unwrapComponent = (node) => {
52
48
  let current = node ?? undefined;
53
49
  while (current) {
54
50
  if (TS_WRAPPERS.has(current.type))
@@ -59,31 +55,31 @@ function unwrapComponent(node) {
59
55
  return current;
60
56
  }
61
57
  return current;
62
- }
58
+ };
63
59
  /** What a function is held by, out through the calls and assertions it is declared behind. */
64
- function heldBy(fn) {
60
+ const heldBy = (fn) => {
65
61
  let held = fn.parent;
66
62
  while (held && (held.type === AST_NODE_TYPES.CallExpression || TS_WRAPPERS.has(held.type)))
67
63
  held = held.parent;
68
64
  return held;
69
- }
65
+ };
70
66
  /** Whether a function is the file's own component: named `component`, or an anonymous default export. */
71
- function isComponent(fn, component) {
67
+ const isComponent = (fn, component) => {
72
68
  if (fn.type === AST_NODE_TYPES.FunctionDeclaration)
73
69
  return !fn.id || fn.id.name === component;
74
70
  const held = heldBy(fn);
75
71
  if (held?.type === AST_NODE_TYPES.ExportDefaultDeclaration)
76
72
  return true;
77
73
  return held?.type === AST_NODE_TYPES.VariableDeclarator && held.id.type === AST_NODE_TYPES.Identifier && held.id.name === component;
78
- }
74
+ };
79
75
  /** The `testID` property of a destructured parameter or declarator. */
80
- function testIdProperty(pattern) {
76
+ const testIdProperty = (pattern) => {
81
77
  if (pattern?.type !== AST_NODE_TYPES.ObjectPattern)
82
78
  return undefined;
83
79
  return pattern.properties.find((property) => property.type === AST_NODE_TYPES.Property && !property.computed && property.key.type === AST_NODE_TYPES.Identifier && property.key.name === 'testID');
84
- }
80
+ };
85
81
  /** The names a component's props object goes by: the first parameter, or the rest of a destructured one without `testID`. */
86
- function propsNames(fn) {
82
+ const propsNames = (fn) => {
87
83
  const first = fn.params.at(0);
88
84
  if (first?.type === AST_NODE_TYPES.Identifier)
89
85
  return [first.name];
@@ -91,15 +87,15 @@ function propsNames(fn) {
91
87
  return [];
92
88
  const rest = first.properties.find((property) => property.type === AST_NODE_TYPES.RestElement);
93
89
  return rest?.argument.type === AST_NODE_TYPES.Identifier ? [rest.argument.name] : [];
94
- }
90
+ };
95
91
  /** The file's own component that `node` sits in, however deep in callbacks. */
96
- function ownComponentAround(node, component) {
92
+ const ownComponentAround = (node, component) => {
97
93
  for (let current = node.parent; current; current = current.parent) {
98
94
  if (isFunction(current) && isComponent(current, component))
99
95
  return current;
100
96
  }
101
97
  return undefined;
102
- }
98
+ };
103
99
  const testIdDefault = createRule({
104
100
  name: 'test-id-default',
105
101
  meta: {
@@ -113,7 +109,7 @@ const testIdDefault = createRule({
113
109
  wrong: 'Default this to `"{{ expected }}"`, not `{{ actual }}`: the default is the component\'s own name in kebab-case, so a reader can predict the id without opening the file.',
114
110
  },
115
111
  },
116
- create(context) {
112
+ create: (context) => {
117
113
  const component = componentName(context.filename);
118
114
  // The file's own component only; anything else in here is private to it.
119
115
  if (!PASCAL_CASE_PATTERN.test(component))
@@ -146,7 +142,7 @@ const testIdDefault = createRule({
146
142
  check(testIdProperty(node.params[0]));
147
143
  };
148
144
  return {
149
- VariableDeclarator(node) {
145
+ VariableDeclarator: (node) => {
150
146
  // `const { testID } = props` inside the file's own component reads the same prop.
151
147
  if (node.id.type === AST_NODE_TYPES.ObjectPattern && node.init?.type === AST_NODE_TYPES.Identifier) {
152
148
  const fn = ownComponentAround(node, component);
@@ -157,20 +153,20 @@ const testIdDefault = createRule({
157
153
  if (node.id.type === AST_NODE_TYPES.Identifier)
158
154
  checkFunction(unwrapComponent(node.init), node.id.name);
159
155
  },
160
- FunctionDeclaration(node) {
156
+ FunctionDeclaration: (node) => {
161
157
  if (node.id)
162
158
  checkFunction(node, node.id.name);
163
159
  else if (node.parent.type === AST_NODE_TYPES.ExportDefaultDeclaration)
164
160
  checkFunction(node, component);
165
161
  },
166
162
  // `export default memo(({ testID }) => …)`: the anonymous component.
167
- ExportDefaultDeclaration(node) {
163
+ ExportDefaultDeclaration: (node) => {
168
164
  const init = unwrapComponent(node.declaration);
169
165
  if (init?.type !== AST_NODE_TYPES.FunctionDeclaration)
170
166
  checkFunction(init, component);
171
167
  },
172
168
  // `props.testID` read inside the component, with no default anywhere.
173
- MemberExpression(node) {
169
+ MemberExpression: (node) => {
174
170
  if (node.computed || node.property.type !== AST_NODE_TYPES.Identifier || node.property.name !== 'testID')
175
171
  return;
176
172
  if (node.object.type !== AST_NODE_TYPES.Identifier)
@@ -1,6 +1,6 @@
1
1
  import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
- import createRule from '../utils/create-rule.js';
3
- import { leavesOf } from '../utils/expressions.js';
2
+ import createRule from '../../shared/create-rule.js';
3
+ import { leavesOf } from '../../shared/expressions.js';
4
4
  /**
5
5
  * A `testID` written out in full is lowercase kebab-case: `crest`,
6
6
  * `vote-input`, `top-scorers-card-home-crest`.
@@ -29,34 +29,30 @@ const LEADING_SEPARATOR_PATTERN = /^[^A-Za-z0-9]/;
29
29
  const TRAILING_SEPARATOR_PATTERN = /[^A-Za-z0-9]$/;
30
30
  const TEST_ID = 'testID';
31
31
  /** `Match.playerItem_photo` -> `match-player-item-photo`. */
32
- function kebab(value) {
33
- return value
34
- .replaceAll(LOWER_TO_UPPER_PATTERN, '-')
35
- .replaceAll(ACRONYM_END_PATTERN, '-')
36
- .replaceAll(NON_ALPHANUMERIC_RUN_PATTERN, '-')
37
- .replaceAll(EDGE_DASHES_PATTERN, '')
38
- .toLowerCase();
39
- }
32
+ const kebab = (value) => value
33
+ .replaceAll(LOWER_TO_UPPER_PATTERN, '-')
34
+ .replaceAll(ACRONYM_END_PATTERN, '-')
35
+ .replaceAll(NON_ALPHANUMERIC_RUN_PATTERN, '-')
36
+ .replaceAll(EDGE_DASHES_PATTERN, '')
37
+ .toLowerCase();
40
38
  /** A piece between substitutions keeps a separator where it meets one: `_photo` becomes `-photo`. */
41
- function kebabFragment(value) {
39
+ const kebabFragment = (value) => {
42
40
  const core = kebab(value);
43
41
  const lead = LEADING_SEPARATOR_PATTERN.test(value) ? '-' : '';
44
42
  const trail = TRAILING_SEPARATOR_PATTERN.test(value) ? '-' : '';
45
43
  return core ? `${lead}${core}${trail}` : '-';
46
- }
44
+ };
47
45
  /** Whether `inner` is written inside `outer`, rather than elsewhere in the file. */
48
- function isInside(outer, inner) {
49
- return inner.range[0] >= outer.range[0] && inner.range[1] <= outer.range[1];
50
- }
46
+ const isInside = (outer, inner) => inner.range[0] >= outer.range[0] && inner.range[1] <= outer.range[1];
51
47
  /** The written string an expression is, or `undefined` when it is not one. */
52
- function writtenString(node) {
48
+ const writtenString = (node) => {
53
49
  if (node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string')
54
50
  return node.value;
55
51
  // A template with nothing substituted is a literal written the long way.
56
52
  if (node.type === AST_NODE_TYPES.TemplateLiteral && node.expressions.length === 0)
57
53
  return node.quasis[0]?.value.cooked ?? undefined;
58
54
  return undefined;
59
- }
55
+ };
60
56
  const testIdFormat = createRule({
61
57
  name: 'test-id-format',
62
58
  meta: {
@@ -68,7 +64,7 @@ const testIdFormat = createRule({
68
64
  format: 'Write this `testID` as `{{ suggestion }}`, not "{{ value }}": an id is lowercase kebab-case throughout, depth included. A dot is a wildcard in Maestro\'s `id:` matcher, and a capital is one shift key away from a flake — so a child derives with a dash, `` `${testID}-remote-image` ``, at every level.',
69
65
  },
70
66
  },
71
- create(context) {
67
+ create: (context) => {
72
68
  // Reported on the string when it sits inside the expression, else on the expression a name led from.
73
69
  const check = (expression, leaf) => {
74
70
  const value = writtenString(leaf);
@@ -101,7 +97,7 @@ const testIdFormat = createRule({
101
97
  }
102
98
  };
103
99
  return {
104
- JSXAttribute(node) {
100
+ JSXAttribute: (node) => {
105
101
  if (node.name.type !== AST_NODE_TYPES.JSXIdentifier || node.name.name !== TEST_ID)
106
102
  return;
107
103
  const { value } = node;
@@ -111,7 +107,7 @@ const testIdFormat = createRule({
111
107
  checkExpression(value.expression);
112
108
  },
113
109
  // A `testID` default, and a `testID:` in an options or fixture object.
114
- Property(node) {
110
+ Property: (node) => {
115
111
  if (node.computed || node.key.type !== AST_NODE_TYPES.Identifier || node.key.name !== TEST_ID)
116
112
  return;
117
113
  checkExpression(node.value.type === AST_NODE_TYPES.AssignmentPattern ? node.value.right : node.value);