@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,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 component's `testID` comes from its props, never a string it writes itself.
6
6
  *
@@ -23,11 +23,9 @@ import { leavesOf } from '../utils/expressions.js';
23
23
  */
24
24
  const TEST_ID = 'testID';
25
25
  /** A value spelled out where it stands, rather than reached for. */
26
- function isWritten(node) {
27
- return (node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string') || (node.type === AST_NODE_TYPES.TemplateLiteral && node.expressions.length === 0);
28
- }
26
+ const isWritten = (node) => (node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string') || (node.type === AST_NODE_TYPES.TemplateLiteral && node.expressions.length === 0);
29
27
  /** Whether an expression reaches outside itself for its value. */
30
- function readsAName(context, node) {
28
+ const isReadingAName = (context, node) => {
31
29
  switch (node.type) {
32
30
  // A local name bound to a written string is that string; anything unresolved is trusted.
33
31
  case AST_NODE_TYPES.Identifier:
@@ -41,26 +39,26 @@ function readsAName(context, node) {
41
39
  case AST_NODE_TYPES.TemplateLiteral:
42
40
  return node.expressions.length > 0 && node.quasis[0]?.value.cooked === '';
43
41
  case AST_NODE_TYPES.ConditionalExpression:
44
- return readsAName(context, node.consequent) || readsAName(context, node.alternate);
42
+ return isReadingAName(context, node.consequent) || isReadingAName(context, node.alternate);
45
43
  case AST_NODE_TYPES.LogicalExpression:
46
- return readsAName(context, node.left) || readsAName(context, node.right);
44
+ return isReadingAName(context, node.left) || isReadingAName(context, node.right);
47
45
  case AST_NODE_TYPES.TSAsExpression:
48
46
  case AST_NODE_TYPES.TSSatisfiesExpression:
49
47
  case AST_NODE_TYPES.TSNonNullExpression:
50
- return readsAName(context, node.expression);
48
+ return isReadingAName(context, node.expression);
51
49
  default:
52
50
  return false;
53
51
  }
54
- }
52
+ };
55
53
  /** How to name the offending id: a name is followed to the string behind it. */
56
- function writtenValue(context, node) {
54
+ const writtenValue = (context, node) => {
57
55
  if (node?.type === AST_NODE_TYPES.Literal && typeof node.value === 'string')
58
56
  return `"${node.value}"`;
59
57
  const strings = (node ? leavesOf(context, node) : [])
60
58
  .filter(isWritten)
61
59
  .map((leaf) => (leaf.type === AST_NODE_TYPES.Literal ? leaf.value : leaf.quasis[0]?.value.cooked));
62
60
  return strings.length === 1 ? `"${String(strings[0])}"` : 'a fixed value';
63
- }
61
+ };
64
62
  const testIdFromProps = createRule({
65
63
  name: 'test-id-from-props',
66
64
  meta: {
@@ -73,22 +71,20 @@ const testIdFromProps = createRule({
73
71
  prefixed: 'Start this `testID` from the `testID` prop rather than the written "{{ prefix }}": what follows it may differ per instance, but the prefix spells out a name the prop already carries, so a caller cannot scope or rename it. Write `` `${testID}-…` ``.',
74
72
  },
75
73
  },
76
- create(context) {
77
- return {
78
- JSXAttribute(node) {
79
- if (node.name.type !== AST_NODE_TYPES.JSXIdentifier || node.name.name !== TEST_ID)
80
- return;
81
- const { value } = node;
82
- const expression = value?.type === AST_NODE_TYPES.JSXExpressionContainer ? value.expression : (value ?? undefined);
83
- if (value?.type === AST_NODE_TYPES.JSXExpressionContainer && readsAName(context, value.expression))
84
- return;
85
- if (expression?.type === AST_NODE_TYPES.TemplateLiteral && expression.expressions.length > 0) {
86
- context.report({ node, messageId: 'prefixed', data: { prefix: expression.quasis[0]?.value.cooked } });
87
- return;
88
- }
89
- context.report({ node, messageId: 'static', data: { value: writtenValue(context, expression) } });
90
- },
91
- };
92
- },
74
+ create: (context) => ({
75
+ JSXAttribute: (node) => {
76
+ if (node.name.type !== AST_NODE_TYPES.JSXIdentifier || node.name.name !== TEST_ID)
77
+ return;
78
+ const { value } = node;
79
+ const expression = value?.type === AST_NODE_TYPES.JSXExpressionContainer ? value.expression : (value ?? undefined);
80
+ if (value?.type === AST_NODE_TYPES.JSXExpressionContainer && isReadingAName(context, value.expression))
81
+ return;
82
+ if (expression?.type === AST_NODE_TYPES.TemplateLiteral && expression.expressions.length > 0) {
83
+ context.report({ node, messageId: 'prefixed', data: { prefix: expression.quasis[0]?.value.cooked } });
84
+ return;
85
+ }
86
+ context.report({ node, messageId: 'static', data: { value: writtenValue(context, expression) } });
87
+ },
88
+ }),
93
89
  });
94
90
  export default testIdFromProps;
@@ -1,6 +1,6 @@
1
1
  import { AST_NODE_TYPES, TSESLint } from '@typescript-eslint/utils';
2
- import createRule from '../utils/create-rule.js';
3
- import { unwrap } from '../utils/expressions.js';
2
+ import createRule from '../../shared/create-rule.js';
3
+ import { unwrap } from '../../shared/expressions.js';
4
4
  /**
5
5
  * A `react-native-webview` `WebView` sends every navigation through its
6
6
  * `onShouldStartLoadWithRequest`, so that handler is the allow-list.
@@ -23,7 +23,7 @@ const SOURCE = 'react-native-webview';
23
23
  const WEBVIEW = 'WebView';
24
24
  const HANDLER = 'onShouldStartLoadWithRequest';
25
25
  /** Whether a JSX name resolves to the library's `WebView` import. */
26
- function isWebView(context, name) {
26
+ const isWebView = (context, name) => {
27
27
  const identifier = name.type === AST_NODE_TYPES.JSXMemberExpression ? name.object : name;
28
28
  if (identifier.type !== AST_NODE_TYPES.JSXIdentifier)
29
29
  return false;
@@ -44,9 +44,9 @@ function isWebView(context, name) {
44
44
  return false;
45
45
  const imported = node.imported.type === AST_NODE_TYPES.Identifier ? node.imported.name : node.imported.value;
46
46
  return imported === WEBVIEW;
47
- }
47
+ };
48
48
  /** `undefined`, `null`, or a branch that can be one: the prop is set and still absent. */
49
- function canBeAbsent(expression) {
49
+ const canBeAbsent = (expression) => {
50
50
  switch (expression.type) {
51
51
  case AST_NODE_TYPES.JSXEmptyExpression:
52
52
  return true;
@@ -61,18 +61,16 @@ function canBeAbsent(expression) {
61
61
  default:
62
62
  return false;
63
63
  }
64
- }
65
- function attribute(opening, name) {
66
- return opening.attributes.find((item) => item.type === AST_NODE_TYPES.JSXAttribute && item.name.name === name);
67
- }
64
+ };
65
+ const attribute = (opening, name) => opening.attributes.find((item) => item.type === AST_NODE_TYPES.JSXAttribute && item.name.name === name);
68
66
  /** Whether the prop's value is an inline array literal holding the string `"*"`. */
69
- function holdsWildcard(property) {
67
+ const isHoldingWildcard = (property) => {
70
68
  const expression = unwrap(property.value?.type === AST_NODE_TYPES.JSXExpressionContainer ? property.value.expression : undefined);
71
69
  if (expression?.type !== AST_NODE_TYPES.ArrayExpression)
72
70
  return false;
73
71
  return expression.elements.some((element) => (element?.type === AST_NODE_TYPES.Literal && element.value === '*') ||
74
72
  (element?.type === AST_NODE_TYPES.TemplateLiteral && element.expressions.length === 0 && element.quasis[0]?.value.cooked === '*'));
75
- }
73
+ };
76
74
  const webviewAllowList = createRule({
77
75
  name: 'webview-allow-list',
78
76
  meta: {
@@ -88,22 +86,20 @@ const webviewAllowList = createRule({
88
86
  narrowWhitelist: 'Make `originWhitelist` the inline `["*"]`: a URL outside the list skips `{{ handler }}` and the library opens it in another app, so the handler is the only allow-list that holds.',
89
87
  },
90
88
  },
91
- create(context) {
92
- return {
93
- JSXOpeningElement(node) {
94
- if (!isWebView(context, node.name))
95
- return;
96
- const handler = attribute(node, HANDLER);
97
- if (handler?.value?.type !== AST_NODE_TYPES.JSXExpressionContainer || canBeAbsent(handler.value.expression)) {
98
- context.report({ node, messageId: 'missingHandler', data: { handler: HANDLER } });
99
- }
100
- const whitelist = attribute(node, 'originWhitelist');
101
- if (!whitelist)
102
- context.report({ node, messageId: 'missingWhitelist', data: { handler: HANDLER } });
103
- else if (!holdsWildcard(whitelist))
104
- context.report({ node: whitelist, messageId: 'narrowWhitelist', data: { handler: HANDLER } });
105
- },
106
- };
107
- },
89
+ create: (context) => ({
90
+ JSXOpeningElement: (node) => {
91
+ if (!isWebView(context, node.name))
92
+ return;
93
+ const handler = attribute(node, HANDLER);
94
+ if (handler?.value?.type !== AST_NODE_TYPES.JSXExpressionContainer || canBeAbsent(handler.value.expression)) {
95
+ context.report({ node, messageId: 'missingHandler', data: { handler: HANDLER } });
96
+ }
97
+ const whitelist = attribute(node, 'originWhitelist');
98
+ if (!whitelist)
99
+ context.report({ node, messageId: 'missingWhitelist', data: { handler: HANDLER } });
100
+ else if (!isHoldingWildcard(whitelist))
101
+ context.report({ node: whitelist, messageId: 'narrowWhitelist', data: { handler: HANDLER } });
102
+ },
103
+ }),
108
104
  });
109
105
  export default webviewAllowList;
@@ -28,13 +28,13 @@ export type LeavesOptions = {
28
28
  seen?: Set<Node>;
29
29
  };
30
30
  /** Through `as`, `satisfies`, `!` and `?.`. */
31
- export declare function unwrap(node: Node | null | undefined): Node | undefined;
31
+ export declare const unwrap: (node: Node | null | undefined) => Node | undefined;
32
32
  /**
33
33
  * A property's key when it is written literally (`a`, `"a"`, `1`, `["a"]`,
34
34
  * `[\`a\`]`), else null: a computed key that is a variable, a call or a
35
35
  * template with expressions, a spread and a pattern have no key to read.
36
36
  */
37
- export declare function keyOf(property: Node): string | null;
37
+ export declare const keyOf: (property: Node) => string | null;
38
38
  /**
39
39
  * Every leaf an expression can produce.
40
40
  *
@@ -48,5 +48,5 @@ export declare function keyOf(property: Node): string | null;
48
48
  * itself: a name from another file, a `let`, a method call, a computed member,
49
49
  * a spread. A cycle yields nothing rather than looping.
50
50
  */
51
- export declare function leavesOf(context: ScopeContext, node: Node | null | undefined, options?: LeavesOptions): Node[];
51
+ export declare const leavesOf: (context: ScopeContext, node: Node | null | undefined, options?: LeavesOptions) => Node[];
52
52
  export {};
@@ -8,14 +8,14 @@ const DEFAULT_SELECT_OBJECTS = new Set(['Platform']);
8
8
  const isWrapper = (node) => TS_WRAPPERS.has(node.type);
9
9
  const isFunction = (node) => FUNCTIONS.has(node.type);
10
10
  /** Through `as`, `satisfies`, `!` and `?.`. */
11
- export function unwrap(node) {
11
+ export const unwrap = (node) => {
12
12
  let current = node ?? undefined;
13
13
  while (current && isWrapper(current))
14
14
  current = current.expression;
15
15
  return current;
16
- }
16
+ };
17
17
  /** The name of the memo hook a callee calls (`useMemo`, `React.useMemo`), if it is one. */
18
- function memoHookName(callee) {
18
+ const memoHookName = (callee) => {
19
19
  if (callee.type === AST_NODE_TYPES.Identifier)
20
20
  return MEMO_HOOKS.has(callee.name) ? callee.name : undefined;
21
21
  if (callee.type === AST_NODE_TYPES.MemberExpression &&
@@ -26,29 +26,27 @@ function memoHookName(callee) {
26
26
  return callee.property.name;
27
27
  }
28
28
  return undefined;
29
- }
30
- function findVariable(scope, name) {
29
+ };
30
+ const findVariable = (scope, name) => {
31
31
  for (let current = scope; current; current = current.upper) {
32
32
  const variable = current.set.get(name);
33
33
  if (variable)
34
34
  return variable;
35
35
  }
36
36
  return undefined;
37
- }
38
- function bindingOf(context, identifier) {
39
- return findVariable(context.sourceCode.getScope(identifier), identifier.name);
40
- }
37
+ };
38
+ const bindingOf = (context, identifier) => findVariable(context.sourceCode.getScope(identifier), identifier.name);
41
39
  /** The declarator a name resolves to, when it is a local with an initialiser; `.init` need not be the name's own value. */
42
- function localDeclarator(context, identifier) {
40
+ const localDeclarator = (context, identifier) => {
43
41
  const definition = bindingOf(context, identifier)?.defs[0];
44
42
  return definition?.type === DefinitionType.Variable && definition.node.init ? definition.node : undefined;
45
- }
43
+ };
46
44
  /**
47
45
  * A property's key when it is written literally (`a`, `"a"`, `1`, `["a"]`,
48
46
  * `[\`a\`]`), else null: a computed key that is a variable, a call or a
49
47
  * template with expressions, a spread and a pattern have no key to read.
50
48
  */
51
- export function keyOf(property) {
49
+ export const keyOf = (property) => {
52
50
  if (property.type !== AST_NODE_TYPES.Property)
53
51
  return null;
54
52
  const { key } = property;
@@ -57,13 +55,13 @@ export function keyOf(property) {
57
55
  if (key.type === AST_NODE_TYPES.Literal)
58
56
  return String(key.value);
59
57
  return key.type === AST_NODE_TYPES.TemplateLiteral && key.expressions.length === 0 ? (key.quasis[0]?.value.cooked ?? null) : null;
60
- }
58
+ };
61
59
  /**
62
60
  * The expression a destructuring pattern hands to `name`, read off a literal
63
61
  * initialiser. A non-literal value, a rest element, a computed key and a spread
64
62
  * before the wanted element all give up, since the value is not in this file.
65
63
  */
66
- function picked(pattern, init, name) {
64
+ const picked = (pattern, init, name) => {
67
65
  if (pattern.type === AST_NODE_TYPES.Identifier)
68
66
  return pattern.name === name ? (init ?? undefined) : undefined;
69
67
  // The default only applies when the source has nothing there.
@@ -71,37 +69,44 @@ function picked(pattern, init, name) {
71
69
  return picked(pattern.left, init ?? pattern.right, name);
72
70
  const source = unwrap(init);
73
71
  if (pattern.type === AST_NODE_TYPES.ObjectPattern && source?.type === AST_NODE_TYPES.ObjectExpression) {
74
- for (const property of pattern.properties) {
75
- const wanted = keyOf(property);
76
- if (wanted === null || property.type !== AST_NODE_TYPES.Property)
77
- continue;
78
- const entry = source.properties.find((candidate) => keyOf(candidate) === wanted);
79
- const found = picked(property.value, entry?.type === AST_NODE_TYPES.Property ? entry.value : undefined, name);
80
- if (found)
81
- return found;
82
- }
83
- return undefined;
72
+ return pickedFromObject(pattern, source, name);
84
73
  }
85
74
  if (pattern.type === AST_NODE_TYPES.ArrayPattern && source?.type === AST_NODE_TYPES.ArrayExpression) {
86
- for (const [index, element] of pattern.elements.entries()) {
87
- if (!element)
88
- continue;
89
- const before = source.elements.slice(0, index + 1);
90
- if (before.some((entry) => entry?.type === AST_NODE_TYPES.SpreadElement))
91
- continue;
92
- const found = picked(element, source.elements[index], name);
93
- if (found)
94
- return found;
95
- }
75
+ return pickedFromArray(pattern, source, name);
76
+ }
77
+ return undefined;
78
+ };
79
+ const pickedFromObject = (pattern, source, name) => {
80
+ for (const property of pattern.properties) {
81
+ const wanted = keyOf(property);
82
+ if (wanted === null || property.type !== AST_NODE_TYPES.Property)
83
+ continue;
84
+ const entry = source.properties.find((candidate) => keyOf(candidate) === wanted);
85
+ const found = picked(property.value, entry?.type === AST_NODE_TYPES.Property ? entry.value : undefined, name);
86
+ if (found)
87
+ return found;
88
+ }
89
+ return undefined;
90
+ };
91
+ const pickedFromArray = (pattern, source, name) => {
92
+ for (const [index, element] of pattern.elements.entries()) {
93
+ if (!element)
94
+ continue;
95
+ const before = source.elements.slice(0, index + 1);
96
+ if (before.some((entry) => entry?.type === AST_NODE_TYPES.SpreadElement))
97
+ continue;
98
+ const found = picked(element, source.elements[index], name);
99
+ if (found)
100
+ return found;
96
101
  }
97
102
  return undefined;
98
- }
103
+ };
99
104
  /**
100
105
  * What a name stands for: a `const`'s own value, or a declared function itself.
101
106
  * A `let` or `var` is not followed, and a destructured `const` resolves only
102
107
  * when its initialiser is a literal holding the matching entry.
103
108
  */
104
- function boundTo(context, identifier) {
109
+ const boundTo = (context, identifier) => {
105
110
  const definition = bindingOf(context, identifier)?.defs[0];
106
111
  if (definition?.type === DefinitionType.FunctionName)
107
112
  return definition.node;
@@ -111,17 +116,17 @@ function boundTo(context, identifier) {
111
116
  return undefined;
112
117
  const { id, init } = definition.node;
113
118
  return id.type === AST_NODE_TYPES.Identifier ? init : picked(id, init, identifier.name);
114
- }
115
- function selectBranches(node, selectObjects) {
119
+ };
120
+ const selectBranches = (node, selectObjects) => {
116
121
  const { callee } = node;
117
122
  if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed)
118
123
  return undefined;
119
124
  if (callee.property.name !== 'select' || callee.object.type !== AST_NODE_TYPES.Identifier)
120
125
  return undefined;
121
126
  return selectObjects.has(callee.object.name) ? node.arguments[0] : undefined;
122
- }
127
+ };
123
128
  /** Every `return` with an argument in a body, including nested blocks but not nested functions. */
124
- function returnsIn(body) {
129
+ const returnsIn = (body) => {
125
130
  const found = [];
126
131
  const visit = (value) => {
127
132
  if (Array.isArray(value)) {
@@ -146,7 +151,7 @@ function returnsIn(body) {
146
151
  };
147
152
  visit(body);
148
153
  return found;
149
- }
154
+ };
150
155
  /**
151
156
  * Every leaf an expression can produce.
152
157
  *
@@ -160,7 +165,7 @@ function returnsIn(body) {
160
165
  * itself: a name from another file, a `let`, a method call, a computed member,
161
166
  * a spread. A cycle yields nothing rather than looping.
162
167
  */
163
- export function leavesOf(context, node, options = {}) {
168
+ export const leavesOf = (context, node, options = {}) => {
164
169
  const { names = true, members = true, returns = false, selectObjects = DEFAULT_SELECT_OBJECTS, calls, seen = new Set(), } = options;
165
170
  const out = [];
166
171
  const walkFunction = (fn) => {
@@ -171,31 +176,38 @@ export function leavesOf(context, node, options = {}) {
171
176
  for (const argument of returnsIn(fn.body))
172
177
  walk(argument);
173
178
  };
179
+ /** Each branch is a value for whatever slot the call sits in. */
180
+ const walkSelect = (branches) => {
181
+ for (const branch of leavesOf(context, branches, { ...options, seen })) {
182
+ if (branch.type !== AST_NODE_TYPES.ObjectExpression)
183
+ continue;
184
+ for (const property of branch.properties) {
185
+ if (property.type === AST_NODE_TYPES.Property)
186
+ walk(property.value);
187
+ }
188
+ }
189
+ };
190
+ /** A call to a local function is what it returns (a factory); a method call is never followed. */
191
+ const factoryOf = (call) => {
192
+ if (!returns || call.callee.type !== AST_NODE_TYPES.Identifier)
193
+ return undefined;
194
+ const bound = boundTo(context, call.callee);
195
+ return bound && isFunction(bound) ? bound : undefined;
196
+ };
174
197
  const didWalkCall = (call) => {
175
198
  const branches = selectBranches(call, selectObjects);
176
199
  if (branches) {
177
- // Each branch is a value for whatever slot the call sits in.
178
- for (const branch of leavesOf(context, branches, { ...options, seen })) {
179
- if (branch.type !== AST_NODE_TYPES.ObjectExpression)
180
- continue;
181
- for (const property of branch.properties) {
182
- if (property.type === AST_NODE_TYPES.Property)
183
- walk(property.value);
184
- }
185
- }
200
+ walkSelect(branches);
186
201
  return true;
187
202
  }
188
203
  if (calls?.(call)) {
189
204
  walk(call.arguments[0]);
190
205
  return true;
191
206
  }
192
- // A call to a local function is what it returns (a factory); a method call is never followed.
193
- if (returns && call.callee.type === AST_NODE_TYPES.Identifier) {
194
- const bound = boundTo(context, call.callee);
195
- if (bound && isFunction(bound)) {
196
- walk(bound);
197
- return true;
198
- }
207
+ const factory = factoryOf(call);
208
+ if (factory) {
209
+ walk(factory);
210
+ return true;
199
211
  }
200
212
  const hook = memoHookName(call.callee);
201
213
  const callback = call.arguments.at(0);
@@ -208,6 +220,25 @@ export function leavesOf(context, node, options = {}) {
208
220
  walkFunction(callback);
209
221
  return true;
210
222
  };
223
+ /** `OBJ.key`'s value when `OBJ` is a local `const` object literal. */
224
+ const memberEntry = (member) => {
225
+ if (member.computed || member.object.type !== AST_NODE_TYPES.Identifier)
226
+ return undefined;
227
+ const declarator = localDeclarator(context, member.object);
228
+ // A destructured name is not what the whole initialiser holds.
229
+ const held = declarator?.id.type === AST_NODE_TYPES.Identifier ? unwrap(declarator.init) : undefined;
230
+ if (held?.type !== AST_NODE_TYPES.ObjectExpression)
231
+ return undefined;
232
+ const key = member.property.name;
233
+ const entry = held.properties.find((property) => keyOf(property) === key);
234
+ return entry?.type === AST_NODE_TYPES.Property ? entry.value : undefined;
235
+ };
236
+ /** What a name or `OBJ.key` stands for, when the options let it be followed. */
237
+ const referenced = (node) => {
238
+ if (node.type === AST_NODE_TYPES.Identifier)
239
+ return names ? boundTo(context, node) : undefined;
240
+ return members ? memberEntry(node) : undefined;
241
+ };
211
242
  const walk = (current) => {
212
243
  if (!current || seen.has(current))
213
244
  return;
@@ -229,26 +260,11 @@ export function leavesOf(context, node, options = {}) {
229
260
  walk(current.left);
230
261
  walk(current.right);
231
262
  return;
232
- case AST_NODE_TYPES.Identifier: {
233
- const bound = names ? boundTo(context, current) : undefined;
234
- if (bound) {
235
- walk(bound);
236
- return;
237
- }
238
- break;
239
- }
263
+ case AST_NODE_TYPES.Identifier:
240
264
  case AST_NODE_TYPES.MemberExpression: {
241
- if (!members || current.computed || current.object.type !== AST_NODE_TYPES.Identifier)
242
- break;
243
- const declarator = localDeclarator(context, current.object);
244
- // A destructured name is not what the whole initialiser holds.
245
- const held = declarator?.id.type === AST_NODE_TYPES.Identifier ? unwrap(declarator.init) : undefined;
246
- if (held?.type !== AST_NODE_TYPES.ObjectExpression)
247
- break;
248
- const key = current.property.name;
249
- const entry = held.properties.find((property) => keyOf(property) === key);
250
- if (entry?.type === AST_NODE_TYPES.Property) {
251
- walk(entry.value);
265
+ const target = referenced(current);
266
+ if (target) {
267
+ walk(target);
252
268
  return;
253
269
  }
254
270
  break;
@@ -271,4 +287,4 @@ export function leavesOf(context, node, options = {}) {
271
287
  };
272
288
  walk(node);
273
289
  return out;
274
- }
290
+ };
@@ -21,6 +21,18 @@ const UNRESOLVED = TypeFlags.TypeParameter |
21
21
  TypeFlags.Conditional |
22
22
  TypeFlags.Index |
23
23
  TypeFlags.Substitution;
24
+ /**
25
+ * A callable or constructable type is truthy. Any other object type is only
26
+ * as good as what the falsy primitives do against it: `{}`, `Object` and
27
+ * `{ length: number }` all accept `""`, `0` or `false`.
28
+ */
29
+ const isTruthyObject = (type, checker) => {
30
+ if (type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0)
31
+ return true;
32
+ if (!checker)
33
+ return false;
34
+ return [checker.getStringLiteralType(""), checker.getNumberLiteralType(0), checker.getFalseType()].every((falsy) => !checker.isTypeAssignableTo(falsy, type));
35
+ };
24
36
  /**
25
37
  * Whether every value of this type is truthy, `undefined` aside.
26
38
  *
@@ -50,18 +62,8 @@ export const isSameAsBangBang = (type, checker) => {
50
62
  if (type.flags & (TypeFlags.ESSymbolLike | TypeFlags.NonPrimitive)) {
51
63
  return true;
52
64
  }
53
- if (type.flags & TypeFlags.Object) {
54
- /**
55
- * A callable or constructable type is truthy. Any other object type is only
56
- * as good as what the falsy primitives do against it: `{}`, `Object` and
57
- * `{ length: number }` all accept `""`, `0` or `false`.
58
- */
59
- if (type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0)
60
- return true;
61
- if (!checker)
62
- return false;
63
- return [checker.getStringLiteralType(""), checker.getNumberLiteralType(0), checker.getFalseType()].every((falsy) => !checker.isTypeAssignableTo(falsy, type));
64
- }
65
+ if (type.flags & TypeFlags.Object)
66
+ return isTruthyObject(type, checker);
65
67
  // A literal is as truthy as its value: only `""`, `0` and `0n` are falsy.
66
68
  if (type.isStringLiteral())
67
69
  return type.value !== "";
@@ -1,11 +1,13 @@
1
1
  import expo from 'eslint-plugin-expo';
2
+ import reactDoctor from 'eslint-plugin-react-doctor';
2
3
  import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript';
3
4
  import { createNodeResolver } from 'eslint-plugin-import-x';
4
5
  import globals from 'globals';
5
6
  import noSecrets from 'eslint-plugin-no-secrets';
6
- import reactNativeA11y from './plugins/react-native-a11y/index.js';
7
+ import reactNativeA11y from './plugins/react-native/a11y/index.js';
8
+ import reactNativePlugin from './plugins/react-native/index.js';
7
9
  import { CODE_FILES, NUMBER_GLOBAL_RESTRICTIONS, raiseWarnings, REACT_BOOLEAN_NAME_OPTIONS, REACT_RESTRICTED_SYNTAX, reactConfigs, TEST_FILES, } from './shared.js';
8
- const A11Y = '@hypersolid-dev/react-native-a11y';
10
+ const A11Y = '@hypersolid-dev/react-native/a11y';
9
11
  /**
10
12
  * Derived instead of hardcoded: the frozen `confusing-browser-globals` list (last released 2021) misses `document`,
11
13
  * `localStorage` and `getComputedStyle`, and bans `self`, which React Native defines. `globals` is maintained,
@@ -44,7 +46,7 @@ export default function reactNative() {
44
46
  name: '@hypersolid-dev/react-native',
45
47
  files: CODE_FILES,
46
48
  // eslint-plugin-expo is typed with typescript-eslint's RuleModule, which ESLint's Plugin type does not accept.
47
- plugins: { expo: expo, 'no-secrets': noSecrets },
49
+ plugins: { '@hypersolid-dev/react-native': reactNativePlugin, expo: expo, 'no-secrets': noSecrets },
48
50
  settings: {
49
51
  'import-x/resolver-next': [
50
52
  createTypeScriptImportResolver({ extensions: RESOLVE_EXTENSIONS }),
@@ -135,9 +137,9 @@ export default function reactNative() {
135
137
  },
136
138
  ],
137
139
  // `testID` is lowercase kebab-case, so test selectors match exactly. Bad: `testID="Header.Title"`.
138
- '@hypersolid-dev/test-id-format': 'error',
140
+ '@hypersolid-dev/react-native/test-id-format': 'error',
139
141
  // A WebView allows every origin and filters each navigation in `onShouldStartLoadWithRequest`. Bad: `<WebView source={{ uri }} />`.
140
- '@hypersolid-dev/webview-allow-list': 'error',
142
+ '@hypersolid-dev/react-native/webview-allow-list': 'error',
141
143
  },
142
144
  },
143
145
  {
@@ -147,9 +149,36 @@ export default function reactNative() {
147
149
  ignores: TEST_FILES,
148
150
  rules: {
149
151
  // A component's `testID` comes from its props, so two instances stay apart. Bad: `<View testID="card" />` inside `Card`.
150
- '@hypersolid-dev/test-id-from-props': 'error',
152
+ '@hypersolid-dev/react-native/test-id-from-props': 'error',
151
153
  // The `testID` prop defaults to the component's kebab-case name. Bad: `({ testID }: Props)` in `Card` (use `testID = 'card'`).
152
- '@hypersolid-dev/test-id-default': 'error',
154
+ '@hypersolid-dev/react-native/test-id-default': 'error',
155
+ },
156
+ },
157
+ {
158
+ // React Doctor's React Native rules, as used in the ESPN Goal pilot.
159
+ name: '@hypersolid-dev/react-native/react-doctor',
160
+ files: CODE_FILES,
161
+ ignores: TEST_FILES,
162
+ plugins: { 'react-doctor': reactDoctor },
163
+ rules: {
164
+ // A state update per scroll event re-renders mid-gesture; track position in a shared value. Bad: `onScroll={(event) => setOffset(event.nativeEvent.contentOffset.y)}`.
165
+ 'react-doctor/rn-no-scroll-state': 'error',
166
+ // A reaction that only copies one value into another is a `useDerivedValue`. Bad: `useAnimatedReaction(() => x.value, (value) => { y.value = value; })`.
167
+ 'react-doctor/rn-animation-reaction-as-derived': 'error',
168
+ // A string outside `<Text>` crashes on native. Bad: `<View>Hello</View>`.
169
+ 'react-doctor/rn-no-raw-text': 'error',
170
+ // A list keys rows from `keyExtractor`; a `key` on the row is ignored. Bad: `renderItem={({ item }) => <Row key={item.id} />}`.
171
+ 'react-doctor/rn-no-renderitem-key': 'error',
172
+ // A press animation set from `onPressIn` runs on the JS thread. Bad: `onPressIn={() => { scale.value = 0.9; }}`.
173
+ 'react-doctor/rn-pressable-shared-value-mutation': 'error',
174
+ // `PanResponder` runs on the JS thread; `Gesture.Pan()` runs on the UI thread. Bad: `PanResponder.create({ … })`.
175
+ 'react-doctor/rn-no-panresponder': 'error',
176
+ // `Dimensions.get()` reads the size once and misses rotation and split screen. Bad: `const { width } = Dimensions.get('window')` (use `useWindowDimensions`).
177
+ 'react-doctor/rn-no-dimensions-get': 'error',
178
+ // `react-native/Libraries/…` paths are private and move on upgrade. Bad: `import … from 'react-native/Libraries/Image/resolveAssetSource'`.
179
+ 'react-doctor/rn-no-deep-imports': 'error',
180
+ // Unmaintained Expo-era packages each have a successor. Bad: `import { Audio } from 'expo-av'` (use `expo-audio`).
181
+ 'react-doctor/rn-no-legacy-expo-packages': 'error',
153
182
  },
154
183
  },
155
184
  {
@@ -170,7 +199,7 @@ export default function reactNative() {
170
199
  },
171
200
  },
172
201
  {
173
- // React Native accessibility, each rule citing its WCAG criterion. Docs: src/plugins/react-native-a11y/docs.
202
+ // React Native accessibility, each rule citing its WCAG criterion. Docs: src/plugins/react-native/a11y/docs.
174
203
  name: '@hypersolid-dev/react-native/a11y',
175
204
  files: CODE_FILES,
176
205
  plugins: { [A11Y]: reactNativeA11y },
package/dist/shared.d.ts CHANGED
@@ -31,6 +31,6 @@ export declare const NAMING_CONVENTION: ({
31
31
  format: string[];
32
32
  modifiers?: undefined;
33
33
  })[];
34
- export declare function raiseWarnings(configs: Linter.Config[]): Linter.Config[];
35
- export declare function scopeToCode(configs: readonly Linter.Config[]): Linter.Config[];
36
- export declare function reactConfigs(): Linter.Config[];
34
+ export declare const raiseWarnings: (configs: Linter.Config[]) => Linter.Config[];
35
+ export declare const scopeToCode: (configs: readonly Linter.Config[]) => Linter.Config[];
36
+ export declare const reactConfigs: () => Linter.Config[];