@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,19 @@
1
1
  import { AST_NODE_TYPES } from "@typescript-eslint/utils";
2
- import createRule from "../utils/create-rule.js";
3
- import { componentName, isWrapperCall } from "../utils/component-name.js";
2
+ import createRule from "../../shared/create-rule.js";
3
+ import { componentName, isWrapperCall } from "../../shared/component-name.js";
4
4
  const CALLBACK_PATTERN = /^on[A-Z]/;
5
+ /** Adds each identifier an object pattern binds to `props`, and its rest binding to `objects` too. */
6
+ const addPatternBindings = (pattern, props, objects) => {
7
+ for (const property of pattern.properties) {
8
+ const value = property.type === AST_NODE_TYPES.RestElement ? property.argument : property.value;
9
+ const binding = value.type === AST_NODE_TYPES.AssignmentPattern ? value.left : value;
10
+ if (binding.type !== AST_NODE_TYPES.Identifier)
11
+ continue;
12
+ props.add(binding);
13
+ if (property.type === AST_NODE_TYPES.RestElement)
14
+ objects.add(binding);
15
+ }
16
+ };
5
17
  /** The first parameter's bindings: each prop it destructures, and the props object itself or a rest of it. */
6
18
  const propertyBindings = (parameter) => {
7
19
  const props = new Set();
@@ -11,17 +23,8 @@ const propertyBindings = (parameter) => {
11
23
  props.add(target);
12
24
  objects.add(target);
13
25
  }
14
- if (target?.type === AST_NODE_TYPES.ObjectPattern) {
15
- for (const property of target.properties) {
16
- const value = property.type === AST_NODE_TYPES.RestElement ? property.argument : property.value;
17
- const binding = value.type === AST_NODE_TYPES.AssignmentPattern ? value.left : value;
18
- if (binding.type !== AST_NODE_TYPES.Identifier)
19
- continue;
20
- props.add(binding);
21
- if (property.type === AST_NODE_TYPES.RestElement)
22
- objects.add(binding);
23
- }
24
- }
26
+ if (target?.type === AST_NODE_TYPES.ObjectPattern)
27
+ addPatternBindings(target, props, objects);
25
28
  return { props, objects };
26
29
  };
27
30
  /**
@@ -53,7 +56,7 @@ const noCallbackPayload = createRule({
53
56
  payload: "`{{ callback }}` is called with values `{{ component }}` only received from its parent. Let the parent bind them where it passes `{{ callback }}`, and call it with what this component itself knows (or nothing), so the component stays free of the payload's domain shape.",
54
57
  },
55
58
  },
56
- create(context) {
59
+ create: (context) => {
57
60
  const { sourceCode } = context;
58
61
  const exported = new Set();
59
62
  const pending = [];
@@ -173,7 +176,7 @@ const noCallbackPayload = createRule({
173
176
  ArrowFunctionExpression: check,
174
177
  FunctionDeclaration: check,
175
178
  FunctionExpression: check,
176
- ExportDefaultDeclaration(node) {
179
+ ExportDefaultDeclaration: (node) => {
177
180
  // `export default memo(Row)` exports `Row`, however many wrappers it is under.
178
181
  let declaration = node.declaration;
179
182
  while (isWrapperCall(declaration))
@@ -187,7 +190,7 @@ const noCallbackPayload = createRule({
187
190
  exported.add(declaration.id.name);
188
191
  }
189
192
  },
190
- ExportNamedDeclaration(node) {
193
+ ExportNamedDeclaration: (node) => {
191
194
  for (const specifier of node.specifiers) {
192
195
  if (specifier.local.type === AST_NODE_TYPES.Identifier)
193
196
  exported.add(specifier.local.name);
@@ -202,7 +205,7 @@ const noCallbackPayload = createRule({
202
205
  }
203
206
  }
204
207
  },
205
- "Program:exit"() {
208
+ "Program:exit": () => {
206
209
  for (const { node, data } of pending) {
207
210
  if (exported.has(data.component))
208
211
  context.report({ node, messageId: "payload", data });
@@ -1,6 +1,6 @@
1
1
  import { AST_NODE_TYPES } from "@typescript-eslint/utils";
2
- import createRule from "../utils/create-rule.js";
3
- import { componentName } from "../utils/component-name.js";
2
+ import createRule from "../../shared/create-rule.js";
3
+ import { componentName } from "../../shared/component-name.js";
4
4
  /** The JSX a declarator's value draws: itself, or a branch of a ternary or `&&`. */
5
5
  const drawnJsx = (init) => {
6
6
  if (!init)
@@ -63,7 +63,45 @@ const noMarkupVariable = createRule({
63
63
  inline: "`{{ name }}` holds JSX that `{{ component }}` reads only once. Write it where it is rendered.",
64
64
  },
65
65
  },
66
- create(context) {
66
+ create: (context) => {
67
+ /** Read once, never reassigned, and that read is a JSX child rather than an attribute value. */
68
+ const isRenderedOnceInline = (declarator) => {
69
+ const variable = context.sourceCode.getDeclaredVariables(declarator).at(0);
70
+ const references = variable?.references ?? [];
71
+ const reads = references.filter((ref) => ref.isRead());
72
+ const isReassigned = references.some((ref) => ref.isWrite() && ref.identifier !== declarator.id);
73
+ const container = reads.at(0)?.identifier.parent;
74
+ return (reads.length === 1 &&
75
+ !isReassigned &&
76
+ container?.type === AST_NODE_TYPES.JSXExpressionContainer &&
77
+ container.parent.type !== AST_NODE_TYPES.JSXAttribute);
78
+ };
79
+ const checkDeclarator = (declarator, component) => {
80
+ if (declarator.id.type !== AST_NODE_TYPES.Identifier)
81
+ return;
82
+ const jsx = drawnJsx(declarator.init);
83
+ if (jsx.length === 0)
84
+ return;
85
+ if (jsx.some(hasStyledElement)) {
86
+ context.report({
87
+ node: declarator.id,
88
+ messageId: "extract",
89
+ data: {
90
+ name: declarator.id.name,
91
+ component,
92
+ suggestion: `${component}${toComponentName(declarator.id.name)}`,
93
+ },
94
+ });
95
+ return;
96
+ }
97
+ if (isRenderedOnceInline(declarator)) {
98
+ context.report({
99
+ node: declarator.id,
100
+ messageId: "inline",
101
+ data: { name: declarator.id.name, component },
102
+ });
103
+ }
104
+ };
67
105
  const check = (fn) => {
68
106
  const component = componentName(fn);
69
107
  if (!component || fn.body.type !== AST_NODE_TYPES.BlockStatement)
@@ -71,40 +109,8 @@ const noMarkupVariable = createRule({
71
109
  for (const statement of fn.body.body) {
72
110
  if (statement.type !== AST_NODE_TYPES.VariableDeclaration)
73
111
  continue;
74
- for (const declarator of statement.declarations) {
75
- if (declarator.id.type !== AST_NODE_TYPES.Identifier)
76
- continue;
77
- const jsx = drawnJsx(declarator.init);
78
- if (jsx.length === 0)
79
- continue;
80
- if (!jsx.some(hasStyledElement)) {
81
- const variable = context.sourceCode.getDeclaredVariables(declarator).at(0);
82
- const references = variable?.references ?? [];
83
- const reads = references.filter((ref) => ref.isRead());
84
- const isReassigned = references.some((ref) => ref.isWrite() && ref.identifier !== declarator.id);
85
- const container = reads.at(0)?.identifier.parent;
86
- if (reads.length === 1 &&
87
- !isReassigned &&
88
- container?.type === AST_NODE_TYPES.JSXExpressionContainer &&
89
- container.parent.type !== AST_NODE_TYPES.JSXAttribute) {
90
- context.report({
91
- node: declarator.id,
92
- messageId: "inline",
93
- data: { name: declarator.id.name, component },
94
- });
95
- }
96
- continue;
97
- }
98
- context.report({
99
- node: declarator.id,
100
- messageId: "extract",
101
- data: {
102
- name: declarator.id.name,
103
- component,
104
- suggestion: `${component}${toComponentName(declarator.id.name)}`,
105
- },
106
- });
107
- }
112
+ for (const declarator of statement.declarations)
113
+ checkDeclarator(declarator, component);
108
114
  }
109
115
  };
110
116
  return {
@@ -1,5 +1,5 @@
1
1
  import { AST_NODE_TYPES, TSESLint } from "@typescript-eslint/utils";
2
- import createRule from "../utils/create-rule.js";
2
+ import createRule from "../../shared/create-rule.js";
3
3
  const HAZARD_NAMES = new Set(["arguments", "eval"]);
4
4
  /** The import specifier a name resolves to, when it is a value import from `react`. */
5
5
  const reactImport = (sourceCode, identifier) => {
@@ -84,10 +84,10 @@ const noUnnecessaryUseCallback = createRule({
84
84
  unnecessary: "This callback captures no render-local values. Use an existing function or define it at module scope, unless a proven per-instance identity contract requires memoization; explain that contract in a scoped suppression.",
85
85
  },
86
86
  },
87
- create(context) {
87
+ create: (context) => {
88
88
  const { sourceCode } = context;
89
89
  return {
90
- CallExpression(node) {
90
+ CallExpression: (node) => {
91
91
  if (!isUseCallback(sourceCode, node.callee))
92
92
  return;
93
93
  const [callback, dependencies] = [node.arguments.at(0), node.arguments.at(1)];
@@ -0,0 +1,3 @@
1
+ import type { Rule } from 'eslint';
2
+ declare const propsDestructuring: Rule.RuleModule;
3
+ export default propsDestructuring;
@@ -3,7 +3,7 @@ const WRAPPERS = new Set(['memo', 'forwardRef']);
3
3
  const isFunction = (node) => typeof node === 'object' && node !== null && FUNCTION_TYPES.has(node.type);
4
4
  const PASCAL_CASE_PATTERN = /^[A-Z]/;
5
5
  const isPascalCase = (name) => typeof name === 'string' && PASCAL_CASE_PATTERN.test(name);
6
- function isJsx(node) {
6
+ const isJsx = (node) => {
7
7
  if (typeof node !== 'object' || node === null) {
8
8
  return false;
9
9
  }
@@ -15,22 +15,22 @@ function isJsx(node) {
15
15
  return isJsx(consequent) || isJsx(alternate);
16
16
  }
17
17
  return type === 'LogicalExpression' && (isJsx(left) || isJsx(right));
18
- }
18
+ };
19
19
  // Searches the function's own return statements; nested functions return for themselves.
20
- function returnsJsx(node) {
20
+ const isReturningJsx = (node) => {
21
21
  if (typeof node !== 'object' || node === null) {
22
22
  return false;
23
23
  }
24
24
  if (Array.isArray(node)) {
25
- return node.some(returnsJsx);
25
+ return node.some(isReturningJsx);
26
26
  }
27
27
  const current = node;
28
28
  return current.type === 'ReturnStatement'
29
29
  ? isJsx(current.argument)
30
- : Object.entries(current).some(([key, value]) => key !== 'parent' && !isFunction(value) && typeof value === 'object' && returnsJsx(value));
31
- }
30
+ : Object.entries(current).some(([key, value]) => key !== 'parent' && !isFunction(value) && typeof value === 'object' && isReturningJsx(value));
31
+ };
32
32
  // `memo(fn)`, `React.forwardRef(fn)` and `memo(forwardRef(fn))`.
33
- function unwrap(node) {
33
+ const unwrap = (node) => {
34
34
  if (isFunction(node)) {
35
35
  return node;
36
36
  }
@@ -41,7 +41,7 @@ function unwrap(node) {
41
41
  const { type: calleeType, name, property } = callee;
42
42
  const calleeName = calleeType === 'MemberExpression' ? property.name : name;
43
43
  return typeof calleeName === 'string' && WRAPPERS.has(calleeName) ? unwrap(args[0]) : undefined;
44
- }
44
+ };
45
45
  const propsDestructuring = {
46
46
  meta: {
47
47
  type: 'suggestion',
@@ -51,7 +51,7 @@ const propsDestructuring = {
51
51
  destructure: 'Destructure the props in the signature: `({ title }: Props)`. A discriminated-union props type may keep `props`.',
52
52
  },
53
53
  },
54
- create(context) {
54
+ create: (context) => {
55
55
  const services = context.sourceCode.parserServices;
56
56
  const checker = services.program?.getTypeChecker();
57
57
  // Destructuring a union in the signature breaks narrowing (`if (props.kind === 'a') props.aOnly`).
@@ -61,18 +61,18 @@ const propsDestructuring = {
61
61
  };
62
62
  const check = (fn) => {
63
63
  const props = fn?.params[0];
64
- if (!fn || props?.type !== 'Identifier' || !returnsJsx(fn.body) && !isJsx(fn.body) || isUnion(props)) {
64
+ if (!fn || props?.type !== 'Identifier' || !isReturningJsx(fn.body) && !isJsx(fn.body) || isUnion(props)) {
65
65
  return;
66
66
  }
67
67
  context.report({ node: props, messageId: 'destructure' });
68
68
  };
69
69
  return {
70
- FunctionDeclaration(node) {
70
+ FunctionDeclaration: (node) => {
71
71
  if (isPascalCase(node.id.name)) {
72
72
  check(node);
73
73
  }
74
74
  },
75
- VariableDeclarator(node) {
75
+ VariableDeclarator: (node) => {
76
76
  if (node.id.type === 'Identifier' && isPascalCase(node.id.name)) {
77
77
  check(unwrap(node.init));
78
78
  }
@@ -80,8 +80,4 @@ const propsDestructuring = {
80
80
  };
81
81
  },
82
82
  };
83
- const plugin = {
84
- meta: { name: '@hypersolid-dev/eslint-config' },
85
- rules: { 'props-destructuring': propsDestructuring },
86
- };
87
- export default plugin;
83
+ export default propsDestructuring;
@@ -43,7 +43,7 @@ const rules = {
43
43
  * before Triple became Hypersolid. Rule docs live in ./docs.
44
44
  */
45
45
  const plugin = {
46
- meta: { name: "@hypersolid-dev/react-native-a11y" },
46
+ meta: { name: "@hypersolid-dev/react-native/a11y" },
47
47
  rules,
48
48
  };
49
49
  // typescript-eslint rule types are stricter than ESLint's `Plugin` type and not assignable to it; the runtime shape is the same.
@@ -1,5 +1,5 @@
1
1
  import { AST_NODE_TYPES } from "@typescript-eslint/utils";
2
- import createRule from "../utils/create-rule.js";
2
+ import createRule from "../../../shared/create-rule.js";
3
3
  import getProperty from "../utils/get-property.js";
4
4
  import hasSpreadProps from "../utils/has-spread-props.js";
5
5
  /** Names the platforms already describe to the user. */
@@ -26,7 +26,7 @@ const accessibilityActionsNeedHandler = createRule({
26
26
  },
27
27
  schema: [],
28
28
  },
29
- create(context) {
29
+ create: (context) => {
30
30
  const reportUnlabelledActions = (expression) => {
31
31
  if (expression.type !== AST_NODE_TYPES.ArrayExpression) {
32
32
  return;
@@ -54,7 +54,7 @@ const accessibilityActionsNeedHandler = createRule({
54
54
  }
55
55
  };
56
56
  return {
57
- JSXOpeningElement(node) {
57
+ JSXOpeningElement: (node) => {
58
58
  if (hasSpreadProps(node)) {
59
59
  return;
60
60
  }
@@ -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 getAmountOfTextChildren from "../utils/get-amount-of-text-children.js";
5
5
  import getComponents from "../utils/get-components.js";
6
6
  import getElementName from "../utils/get-element-name.js";
@@ -33,13 +33,13 @@ const accessibilityHint = createRule({
33
33
  },
34
34
  ],
35
35
  },
36
- create(context) {
36
+ create: (context) => {
37
37
  const components = getComponents({
38
38
  context,
39
39
  defaultComponents: TOUCHABLES,
40
40
  });
41
41
  return {
42
- JSXOpeningElement(node) {
42
+ JSXOpeningElement: (node) => {
43
43
  const componentName = getElementName(node);
44
44
  if (componentName === undefined) {
45
45
  return;
@@ -1,6 +1,6 @@
1
1
  import { IMAGES, INPUTS, 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 getAmountOfTextChildren from "../utils/get-amount-of-text-children.js";
5
5
  import getComponents from "../utils/get-components.js";
6
6
  import getElementName from "../utils/get-element-name.js";
@@ -35,13 +35,13 @@ const accessibilityLabel = createRule({
35
35
  },
36
36
  ],
37
37
  },
38
- create(context) {
38
+ create: (context) => {
39
39
  const components = getComponents({
40
40
  context,
41
41
  defaultComponents: [...IMAGES, ...TOUCHABLES, ...INPUTS],
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,6 +1,6 @@
1
1
  import { IMAGES, 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 hasProperty from "../utils/has-property.js";
@@ -31,13 +31,13 @@ const accessibilityRole = createRule({
31
31
  },
32
32
  ],
33
33
  },
34
- create(context) {
34
+ create: (context) => {
35
35
  const components = getComponents({
36
36
  context,
37
37
  defaultComponents: [...TOUCHABLES, ...IMAGES],
38
38
  });
39
39
  return {
40
- JSXOpeningElement(node) {
40
+ JSXOpeningElement: (node) => {
41
41
  const componentName = getElementName(node);
42
42
  if (componentName === undefined) {
43
43
  return;
@@ -0,0 +1,47 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/utils";
2
+ import createRule from "../../../shared/create-rule.js";
3
+ import getProperty from "../utils/get-property.js";
4
+ const keysOf = (expression) => expression.properties.flatMap((property) => property.type === AST_NODE_TYPES.Property &&
5
+ property.key.type === AST_NODE_TYPES.Identifier
6
+ ? [property.key.name]
7
+ : []);
8
+ const accessibilityValueNeedsRange = createRule({
9
+ name: "accessibility-value-needs-range",
10
+ meta: {
11
+ defaultOptions: [],
12
+ type: "problem",
13
+ docs: {
14
+ description: "Enforce accessibilityValue carries a range, so a current value means something.",
15
+ },
16
+ messages: {
17
+ valueWithoutRange: "'accessibilityValue' sets a current value with no scale to read it against, so assistive tech announces a bare number. Add 'min' and 'max', or a 'text' that says the value in words.",
18
+ },
19
+ schema: [],
20
+ },
21
+ create: (context) => ({
22
+ JSXOpeningElement: (node) => {
23
+ const attribute = getProperty(node, "accessibilityValue");
24
+ if (attribute?.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) {
25
+ return;
26
+ }
27
+ const { expression } = attribute.value;
28
+ // A value built elsewhere cannot be read here
29
+ if (expression.type !== AST_NODE_TYPES.ObjectExpression) {
30
+ return;
31
+ }
32
+ // A spread may carry the range, and cannot be read from here
33
+ if (expression.properties.some((property) => property.type === AST_NODE_TYPES.SpreadElement)) {
34
+ return;
35
+ }
36
+ const keys = keysOf(expression);
37
+ if (!keys.includes("now") || keys.includes("text")) {
38
+ return;
39
+ }
40
+ if (keys.includes("min") && keys.includes("max")) {
41
+ return;
42
+ }
43
+ context.report({ node: attribute, messageId: "valueWithoutRange" });
44
+ },
45
+ }),
46
+ });
47
+ export default accessibilityValueNeedsRange;
@@ -1,5 +1,5 @@
1
1
  import { VALUE_ALIAS_KEYS } from "../constants/accessibility-props.js";
2
- import createRule from "../utils/create-rule.js";
2
+ import createRule from "../../../shared/create-rule.js";
3
3
  import getLiteralPropertyValue from "../utils/get-literal-property-value.js";
4
4
  import hasProperty from "../utils/has-property.js";
5
5
  import hasSpreadProps from "../utils/has-spread-props.js";
@@ -18,23 +18,21 @@ const adjustableNeedsValue = createRule({
18
18
  },
19
19
  schema: [],
20
20
  },
21
- create(context) {
22
- return {
23
- JSXOpeningElement(node) {
24
- const role = getLiteralPropertyValue(node, ["accessibilityRole", "role"]);
25
- if (typeof role !== "string" || !ROLES_NEEDING_VALUE.has(role)) {
26
- return;
27
- }
28
- if (hasSpreadProps(node) || hasProperty(node, VALUE_PROPS)) {
29
- return;
30
- }
31
- context.report({
32
- node,
33
- messageId: "missingAccessibilityValue",
34
- data: { role },
35
- });
36
- },
37
- };
38
- },
21
+ create: (context) => ({
22
+ JSXOpeningElement: (node) => {
23
+ const role = getLiteralPropertyValue(node, ["accessibilityRole", "role"]);
24
+ if (typeof role !== "string" || !ROLES_NEEDING_VALUE.has(role)) {
25
+ return;
26
+ }
27
+ if (hasSpreadProps(node) || hasProperty(node, VALUE_PROPS)) {
28
+ return;
29
+ }
30
+ context.report({
31
+ node,
32
+ messageId: "missingAccessibilityValue",
33
+ data: { role },
34
+ });
35
+ },
36
+ }),
39
37
  });
40
38
  export default adjustableNeedsValue;
@@ -1,5 +1,5 @@
1
1
  import { AST_NODE_TYPES } from "@typescript-eslint/utils";
2
- import createRule from "../utils/create-rule.js";
2
+ import createRule from "../../../shared/create-rule.js";
3
3
  import getProperty from "../utils/get-property.js";
4
4
  const LIVE_REGION_PROPS = ["accessibilityLiveRegion", "aria-live"];
5
5
  const FUNCTION_TYPES = new Set([
@@ -28,7 +28,7 @@ const gateAnnounceForAccessibility = createRule({
28
28
  },
29
29
  schema: [],
30
30
  },
31
- create(context) {
31
+ create: (context) => {
32
32
  /**
33
33
  * A bare announce is the legitimate cross-platform pattern. The double
34
34
  * announcement only happens where a live region covers Android already, so
@@ -38,12 +38,12 @@ const gateAnnounceForAccessibility = createRule({
38
38
  const announces = [];
39
39
  const platformChecks = [];
40
40
  return {
41
- JSXOpeningElement(node) {
41
+ JSXOpeningElement: (node) => {
42
42
  if (getProperty(node, LIVE_REGION_PROPS)) {
43
43
  hasLiveRegion = true;
44
44
  }
45
45
  },
46
- MemberExpression(node) {
46
+ MemberExpression: (node) => {
47
47
  if (node.property.type !== AST_NODE_TYPES.Identifier) {
48
48
  return;
49
49
  }
@@ -58,7 +58,7 @@ const gateAnnounceForAccessibility = createRule({
58
58
  platformChecks.push(node);
59
59
  }
60
60
  },
61
- "Program:exit"() {
61
+ "Program:exit": () => {
62
62
  if (!hasLiveRegion) {
63
63
  return;
64
64
  }
@@ -1,6 +1,6 @@
1
1
  import { TEXT_INPUTS } 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 getLiteralPropertyValue from "../utils/get-literal-property-value.js";
@@ -42,13 +42,13 @@ const inputPurpose = createRule({
42
42
  },
43
43
  ],
44
44
  },
45
- create(context) {
45
+ create: (context) => {
46
46
  const components = getComponents({
47
47
  context,
48
48
  defaultComponents: TEXT_INPUTS,
49
49
  });
50
50
  return {
51
- JSXOpeningElement(node) {
51
+ JSXOpeningElement: (node) => {
52
52
  const componentName = getElementName(node);
53
53
  if (componentName === undefined) {
54
54
  return;
@@ -1,6 +1,6 @@
1
1
  import { AST_NODE_TYPES } from "@typescript-eslint/utils";
2
2
  import { ALIAS_TO_LEGACY, STATE_ALIAS_KEYS, VALUE_ALIAS_KEYS, } from "../constants/accessibility-props.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
  /**
6
6
  * Both spellings map to the same native behaviour and only one of them reaches
@@ -30,28 +30,26 @@ const noAriaLegacyMix = createRule({
30
30
  },
31
31
  schema: [],
32
32
  },
33
- create(context) {
34
- return {
35
- JSXOpeningElement(node) {
36
- for (const [alias, legacy] of Object.entries(ALIAS_TO_LEGACY)) {
37
- if (!getProperty(node, alias)) {
38
- continue;
39
- }
40
- const stateKey = STATE_ALIAS_KEYS[alias] ?? VALUE_ALIAS_KEYS[alias];
41
- // A grouped prop only clashes on the key the alias maps onto
42
- const isClashes = stateKey
43
- ? hasStateKey(node, legacy, stateKey)
44
- : getProperty(node, legacy) !== undefined;
45
- if (isClashes) {
46
- context.report({
47
- node,
48
- messageId: "ariaLegacyMix",
49
- data: { alias, legacy },
50
- });
51
- }
33
+ create: (context) => ({
34
+ JSXOpeningElement: (node) => {
35
+ for (const [alias, legacy] of Object.entries(ALIAS_TO_LEGACY)) {
36
+ if (!getProperty(node, alias)) {
37
+ continue;
52
38
  }
53
- },
54
- };
55
- },
39
+ const stateKey = STATE_ALIAS_KEYS[alias] ?? VALUE_ALIAS_KEYS[alias];
40
+ // A grouped prop only clashes on the key the alias maps onto
41
+ const isClashes = stateKey
42
+ ? hasStateKey(node, legacy, stateKey)
43
+ : getProperty(node, legacy) !== undefined;
44
+ if (isClashes) {
45
+ context.report({
46
+ node,
47
+ messageId: "ariaLegacyMix",
48
+ data: { alias, legacy },
49
+ });
50
+ }
51
+ }
52
+ },
53
+ }),
56
54
  });
57
55
  export default noAriaLegacyMix;
@@ -1,5 +1,5 @@
1
1
  import { AST_NODE_TYPES } from "@typescript-eslint/utils";
2
- import createRule from "../utils/create-rule.js";
2
+ import createRule from "../../../shared/create-rule.js";
3
3
  const noDeprecatedAccessibilityFocus = createRule({
4
4
  name: "no-deprecated-accessibility-focus",
5
5
  meta: {
@@ -13,19 +13,17 @@ const noDeprecatedAccessibilityFocus = createRule({
13
13
  },
14
14
  schema: [],
15
15
  },
16
- create(context) {
17
- return {
18
- // Matched on the property so an aliased import is caught too
19
- MemberExpression(node) {
20
- if (node.property.type === AST_NODE_TYPES.Identifier &&
21
- node.property.name === "setAccessibilityFocus") {
22
- context.report({
23
- node,
24
- messageId: "deprecatedSetAccessibilityFocus",
25
- });
26
- }
27
- },
28
- };
29
- },
16
+ create: (context) => ({
17
+ // Matched on the property so an aliased import is caught too
18
+ MemberExpression: (node) => {
19
+ if (node.property.type === AST_NODE_TYPES.Identifier &&
20
+ node.property.name === "setAccessibilityFocus") {
21
+ context.report({
22
+ node,
23
+ messageId: "deprecatedSetAccessibilityFocus",
24
+ });
25
+ }
26
+ },
27
+ }),
30
28
  });
31
29
  export default noDeprecatedAccessibilityFocus;