@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
package/CHANGELOG.md CHANGED
@@ -1,3 +1,42 @@
1
+ ## [7.0.0](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/branchCompare?baseVersion=GTv6.0.0&targetVersion=GTv7.0.0) (2026-11-10)
2
+
3
+ ### ⚠ BREAKING CHANGES
4
+
5
+ * **plugins:** the base-preset rules are @hypersolid-dev/base/<rule>
6
+ (comment-style, regex-pattern-const, undefined-check), not
7
+ @hypersolid-dev/core/<rule> as the previous commit stated.
8
+ * **plugins:** every in-house rule ID gains its domain. Update
9
+ eslint-disable comments and rule overrides:
10
+ - @hypersolid-dev/{comment-style,regex-pattern-const,undefined-check}
11
+ -> @hypersolid-dev/core/<rule>
12
+ - @hypersolid-dev/{boolean-guard,effect-takes-callback,jsdoc-for-props,
13
+ no-callback-payload,no-markup-variable,no-unnecessary-use-callback,
14
+ props-destructuring} -> @hypersolid-dev/react/<rule>
15
+ - @hypersolid-dev/{test-id-default,test-id-format,test-id-from-props,
16
+ webview-allow-list} -> @hypersolid-dev/react-native/<rule>
17
+ - @hypersolid-dev/react-native-a11y/<rule>
18
+ -> @hypersolid-dev/react-native/a11y/<rule>
19
+
20
+ ### Code Refactoring
21
+
22
+ * **plugins:** group in-house rules by domain with prefixed rule IDs ([5c4917c](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/commit/5c4917c3714a51aeea907ad4e453300ee689d722)), closes [AB#5181](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_workitems/edit/5181)
23
+ * **plugins:** rename the core rule domain to base ([f13bfc4](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/commit/f13bfc492fa035c7034b4bcfc0a2839b41fcd1f4)), closes [AB#5181](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_workitems/edit/5181)
24
+
25
+ ## [6.0.0](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/branchCompare?baseVersion=GTv5.0.0&targetVersion=GTv6.0.0) (2026-11-10)
26
+
27
+ ### ⚠ BREAKING CHANGES
28
+
29
+ * **react:** react and react-native report new React Doctor errors.
30
+ * **base:** function declarations and expressions now fail lint.
31
+ * **base:** functions above a cognitive complexity of 15 now fail lint.
32
+
33
+ ### Features
34
+
35
+ * **base:** enable prefer-arrow-functions ([a1fcf80](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/commit/a1fcf801956aeb42ecd0d123e454bc17f376cbd3)), closes [AB#5180](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_workitems/edit/5180)
36
+ * **base:** enable sonarjs/cognitive-complexity ([8ee0e2a](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/commit/8ee0e2aa70053e513c6df6fbff63cd726d38bfde)), closes [AB#5180](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_workitems/edit/5180)
37
+ * **jest:** add opt-in jest preset ([6f26b68](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/commit/6f26b68552cd9dc0f0651e654d621b539b5d6836)), closes [AB#5180](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_workitems/edit/5180)
38
+ * **react:** add React Doctor rules ([026a2f3](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/commit/026a2f3f8cce13ff74c032cfaa78d9081ddc3fe8)), closes [AB#5180](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_workitems/edit/5180)
39
+
1
40
  ## [5.0.0](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/branchCompare?baseVersion=GTv4.0.0&targetVersion=GTv5.0.0) (2026-11-10)
2
41
 
3
42
  ### ⚠ BREAKING CHANGES
package/README.md CHANGED
@@ -26,6 +26,7 @@ The plugins are regular dependencies of this package, so you don't install them
26
26
  | `react-native` | `@hypersolid-dev/eslint-config/react-native` | React Native and Expo. Use instead of `react`. |
27
27
  | `node` | `@hypersolid-dev/eslint-config/node` | Node.js services, scripts and CLIs. |
28
28
  | `next` | `@hypersolid-dev/eslint-config/next` | Next.js. Stack on top of `react`. |
29
+ | `jest` | `@hypersolid-dev/eslint-config/jest` | Jest and Testing Library suites. Opt-in, on top of the others. |
29
30
 
30
31
  Each preset is a function that returns an array of config objects. Presets don't include `base`: spread `base` first, then the presets you need, then your own overrides.
31
32
 
@@ -58,6 +59,8 @@ import react from '@hypersolid-dev/eslint-config/react';
58
59
  export default [...base({ tsconfigRootDir: import.meta.dirname }), ...react()];
59
60
  ```
60
61
 
62
+ `react` and `react-native` include rules from [React Doctor](https://react.doctor) (`react-doctor/*`); `react-native` adds its `rn-*` rules. They are off in test files.
63
+
61
64
  ### React Native / Expo
62
65
 
63
66
  ```js
@@ -69,13 +72,13 @@ export default [...base({ tsconfigRootDir: import.meta.dirname }), ...reactNativ
69
72
 
70
73
  `react-native` bans browser globals that don't exist on React Native (`document`, `localStorage`, …), and ignores `.expo`, `android`, `ios` and `expo-env.d.ts`.
71
74
 
72
- It also enables the bundled `@hypersolid-dev/react-native-a11y` rules, each citing the WCAG criterion it enforces ([rule docs](src/plugins/react-native-a11y/docs)). Custom components are matched by name, so tell a rule about your wrappers with `additionalComponents`. `require-modal-a11y-on-sheets` has no defaults and stays off until you list your sheet components:
75
+ It also enables the bundled `@hypersolid-dev/react-native/a11y` rules, each citing the WCAG criterion it enforces ([rule docs](src/plugins/react-native/a11y/docs)). Custom components are matched by name, so tell a rule about your wrappers with `additionalComponents`. `require-modal-a11y-on-sheets` has no defaults and stays off until you list your sheet components:
73
76
 
74
77
  ```js
75
78
  {
76
79
  rules: {
77
- '@hypersolid-dev/react-native-a11y/accessibility-label': ['error', { additionalComponents: ['IconButton'] }],
78
- '@hypersolid-dev/react-native-a11y/require-modal-a11y-on-sheets': ['error', { additionalComponents: ['BottomSheet'] }],
80
+ '@hypersolid-dev/react-native/a11y/accessibility-label': ['error', { additionalComponents: ['IconButton'] }],
81
+ '@hypersolid-dev/react-native/a11y/require-modal-a11y-on-sheets': ['error', { additionalComponents: ['BottomSheet'] }],
79
82
  },
80
83
  }
81
84
  ```
@@ -90,26 +93,38 @@ import react from '@hypersolid-dev/eslint-config/react';
90
93
  export default [...base({ tsconfigRootDir: import.meta.dirname }), ...react(), ...next()];
91
94
  ```
92
95
 
93
- ## Custom rules
96
+ ### Jest and Testing Library
97
+
98
+ ```js
99
+ import base from '@hypersolid-dev/eslint-config/base';
100
+ import jest from '@hypersolid-dev/eslint-config/jest';
101
+ import reactNative from '@hypersolid-dev/eslint-config/react-native';
102
+
103
+ export default [...base({ tsconfigRootDir: import.meta.dirname }), ...reactNative(), ...jest()];
104
+ ```
105
+
106
+ `jest` applies `eslint-plugin-jest`'s recommended set plus a few stricter rules (`it` over `test`, no conditionals in tests, hooks on top) to `*.test.*`, `*.spec.*` and `jest.setup.*` (`JEST_FILES`, exported from `@hypersolid-dev/eslint-config/jest`), and Testing Library's React rules to the suites. The React set also covers React Native Testing Library; `no-await-sync-events` is off because its `fireEvent` returns a promise. `eslint-plugin-jest` reads your installed Jest version; set `settings.jest.version` if Jest isn't resolvable from the config.
107
+
108
+
94
109
 
95
110
  This package ships its own plugin, namespaced `@hypersolid-dev`. The preset column says where each rule is on; `react` rules are on in `react-native` too.
96
111
 
97
112
  | Rule | Preset | Enforces |
98
113
  | --- | --- | --- |
99
- | `@hypersolid-dev/props-destructuring` | react | A component destructures its props in the signature, so its props are readable there. Replaces `react/destructuring-assignment` (`always`, `destructureInSignature: always`), as `eslint-plugin-react` doesn't support ESLint 10. Checks PascalCase functions that return JSX, including those wrapped in `memo` and `forwardRef`. Bad: `const Card = (props: Props) => <h1>{props.title}</h1>`, or `const { title } = props` in the body. Good: `const Card = ({ title }: Props) => …`. When the props type is a union, such as a discriminated union, `props` may stay whole, because destructuring breaks narrowing (`if (props.kind === 'a') props.a`). |
100
- | `@hypersolid-dev/undefined-check` | base | No `x !== undefined` (or `=== undefined`) where `undefined` is the type's only falsy value, so truthiness already decides. Bad: `if (user !== undefined)` with `user: User \| undefined`. Good: `if (user)`. Needs type information. |
101
- | `@hypersolid-dev/regex-pattern-const` | base | A regex is a module-level `const <NAME>_PATTERN`, so it's read by its name. Bad: `/^\d+$/.test(id)` in a function. Off in tests. |
102
- | `@hypersolid-dev/comment-style` | base | `//` and `/* */` comments are one row; longer prose is a `/** */` JSDoc block. Directive-only runs (`eslint-disable…`, `@ts-…`) are fine. Option `maxLength` caps each row (unset by default). |
103
- | `@hypersolid-dev/jsdoc-for-props` | react | A comment on a type member is JSDoc, the form editors show at the call site. Bad: `// The title` above `title: string`. |
104
- | `@hypersolid-dev/boolean-guard` | react | A JSX `&&` guard can't leak `0` or `''`, which React renders (and which crashes React Native outside `<Text>`). Bad: `{count && <Badge />}`. Good: `{count > 0 && <Badge />}`. Needs type information. Off in tests. |
105
- | `@hypersolid-dev/no-markup-variable` | react | Styled JSX isn't stored in a variable in a component body; it's its own component, or written where it renders. Off in tests. |
106
- | `@hypersolid-dev/effect-takes-callback` | react | A `use*Effect` hook takes the callback it runs as its first parameter, like `useEffect`. Off in tests. |
107
- | `@hypersolid-dev/no-unnecessary-use-callback` | react | A `useCallback` with empty deps that captures nothing from render is a module-level function. Off in tests. |
108
- | `@hypersolid-dev/no-callback-payload` | react | A component passes a callback prop straight through instead of calling it with a payload built from its own props; the caller binds the payload. Bad: `onPress={() => onSelect(id)}` where `id` is a prop. Off in tests. |
109
- | `@hypersolid-dev/test-id-format` | react-native | A `testID` is lowercase kebab-case. Bad: `testID="Header.Title"`. |
110
- | `@hypersolid-dev/test-id-from-props` | react-native | In `**/components/**/*.tsx`, a `testID` comes from the `testID` prop (suffixed for inner nodes), never a fixed string. Off in screens and tests. |
111
- | `@hypersolid-dev/test-id-default` | react-native | In `**/components/**/*.tsx`, a `testID` prop defaults to the component's kebab-case name. Bad: `({ testID }: Props)` in `Card`. Good: `({ testID = 'card' }: Props)`. |
112
- | `@hypersolid-dev/webview-allow-list` | react-native | A `react-native-webview` WebView sets `originWhitelist={['*']}` and filters every navigation in an always-present `onShouldStartLoadWithRequest`. |
114
+ | `@hypersolid-dev/react/props-destructuring` | react | A component destructures its props in the signature, so its props are readable there. Replaces `react/destructuring-assignment` (`always`, `destructureInSignature: always`), as `eslint-plugin-react` doesn't support ESLint 10. Checks PascalCase functions that return JSX, including those wrapped in `memo` and `forwardRef`. Bad: `const Card = (props: Props) => <h1>{props.title}</h1>`, or `const { title } = props` in the body. Good: `const Card = ({ title }: Props) => …`. When the props type is a union, such as a discriminated union, `props` may stay whole, because destructuring breaks narrowing (`if (props.kind === 'a') props.a`). |
115
+ | `@hypersolid-dev/base/undefined-check` | base | No `x !== undefined` (or `=== undefined`) where `undefined` is the type's only falsy value, so truthiness already decides. Bad: `if (user !== undefined)` with `user: User \| undefined`. Good: `if (user)`. Needs type information. |
116
+ | `@hypersolid-dev/base/regex-pattern-const` | base | A regex is a module-level `const <NAME>_PATTERN`, so it's read by its name. Bad: `/^\d+$/.test(id)` in a function. Off in tests. |
117
+ | `@hypersolid-dev/base/comment-style` | base | `//` and `/* */` comments are one row; longer prose is a `/** */` JSDoc block. Directive-only runs (`eslint-disable…`, `@ts-…`) are fine. Option `maxLength` caps each row (unset by default). |
118
+ | `@hypersolid-dev/react/jsdoc-for-props` | react | A comment on a type member is JSDoc, the form editors show at the call site. Bad: `// The title` above `title: string`. |
119
+ | `@hypersolid-dev/react/boolean-guard` | react | A JSX `&&` guard can't leak `0` or `''`, which React renders (and which crashes React Native outside `<Text>`). Bad: `{count && <Badge />}`. Good: `{count > 0 && <Badge />}`. Needs type information. Off in tests. |
120
+ | `@hypersolid-dev/react/no-markup-variable` | react | Styled JSX isn't stored in a variable in a component body; it's its own component, or written where it renders. Off in tests. |
121
+ | `@hypersolid-dev/react/effect-takes-callback` | react | A `use*Effect` hook takes the callback it runs as its first parameter, like `useEffect`. Off in tests. |
122
+ | `@hypersolid-dev/react/no-unnecessary-use-callback` | react | A `useCallback` with empty deps that captures nothing from render is a module-level function. Off in tests. |
123
+ | `@hypersolid-dev/react/no-callback-payload` | react | A component passes a callback prop straight through instead of calling it with a payload built from its own props; the caller binds the payload. Bad: `onPress={() => onSelect(id)}` where `id` is a prop. Off in tests. |
124
+ | `@hypersolid-dev/react-native/test-id-format` | react-native | A `testID` is lowercase kebab-case. Bad: `testID="Header.Title"`. |
125
+ | `@hypersolid-dev/react-native/test-id-from-props` | react-native | In `**/components/**/*.tsx`, a `testID` comes from the `testID` prop (suffixed for inner nodes), never a fixed string. Off in screens and tests. |
126
+ | `@hypersolid-dev/react-native/test-id-default` | react-native | In `**/components/**/*.tsx`, a `testID` prop defaults to the component's kebab-case name. Bad: `({ testID }: Props)` in `Card`. Good: `({ testID = 'card' }: Props)`. |
127
+ | `@hypersolid-dev/react-native/webview-allow-list` | react-native | A `react-native-webview` WebView sets `originWhitelist={['*']}` and filters every navigation in an always-present `onShouldStartLoadWithRequest`. |
113
128
 
114
129
  ## Overriding rules
115
130
 
@@ -171,14 +186,14 @@ export default [...base({ tsconfigRootDir: import.meta.dirname }), { files: TEST
171
186
 
172
187
  ### Component folders
173
188
 
174
- `@hypersolid-dev/no-callback-payload` applies to every component, and the `test-id-*` rules to `**/components/**/*.tsx`. If your components live elsewhere, scope them to your own folders:
189
+ `@hypersolid-dev/react/no-callback-payload` applies to every component, and the `test-id-*` rules to `**/components/**/*.tsx`. If your components live elsewhere, scope them to your own folders:
175
190
 
176
191
  ```js
177
192
  export default [
178
193
  ...base({ tsconfigRootDir: import.meta.dirname }),
179
194
  ...reactNative(),
180
- { files: ['**/components/**/*.tsx'], rules: { '@hypersolid-dev/test-id-default': 'off' } },
181
- { files: ['src/ui/**/*.tsx'], rules: { '@hypersolid-dev/test-id-default': 'error' } },
195
+ { files: ['**/components/**/*.tsx'], rules: { '@hypersolid-dev/react-native/test-id-default': 'off' } },
196
+ { files: ['src/ui/**/*.tsx'], rules: { '@hypersolid-dev/react-native/test-id-default': 'error' } },
182
197
  ];
183
198
  ```
184
199
 
@@ -193,6 +208,7 @@ Root files such as `eslint.config.js` or `vite.config.ts` are often outside the
193
208
  ## Known issues
194
209
 
195
210
  - `eslint-plugin-jsx-a11y` (used by `react`) declares support for ESLint 9 only. It works on ESLint 10, but installing prints a peer-dependency warning, and installs with `strict-peer-dependencies` fail.
211
+ - `eslint-plugin-react-doctor` declares no `eslint` peer dependency. It reads the nearest `package.json` to decide which rules apply (the `rn-*` rules need `react-native` there), and skips some rules for files under `test/`, `__tests__/` and similar paths.
196
212
  - In `next`, `@next/next/no-html-link-for-pages` prints "Pages directory cannot be found" on App Router projects. Turn it off if you don't use the `pages` directory:
197
213
 
198
214
  ```js
package/dist/base.js CHANGED
@@ -3,9 +3,11 @@ import json from '@eslint/json';
3
3
  import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript';
4
4
  import { createNodeResolver, flatConfigs as importX } from 'eslint-plugin-import-x';
5
5
  import noSecrets from 'eslint-plugin-no-secrets';
6
+ import preferArrowFunctions from 'eslint-plugin-prefer-arrow-functions';
7
+ import sonarjs from 'eslint-plugin-sonarjs';
6
8
  import unicorn from 'eslint-plugin-unicorn';
7
9
  import { configs as tsConfigs } from 'typescript-eslint';
8
- import hypersolid from './plugins/hypersolid.js';
10
+ import basePlugin from './plugins/base/index.js';
9
11
  import { BASE_RESTRICTED_SYNTAX, BOOLEAN_NAME_OPTIONS, CODE_FILES, NAMING_CONVENTION, NUMBER_GLOBAL_RESTRICTIONS, raiseWarnings, scopeToCode, TEST_FILES, } from './shared.js';
10
12
  // The glob the presets relax test-only rules for; reuse it for your own test overrides.
11
13
  export { TEST_FILES } from './shared.js';
@@ -44,7 +46,12 @@ export default function base(options) {
44
46
  tsconfigRootDir: options.tsconfigRootDir,
45
47
  },
46
48
  },
47
- plugins: { '@hypersolid-dev': hypersolid, 'no-secrets': noSecrets },
49
+ plugins: {
50
+ '@hypersolid-dev/base': basePlugin,
51
+ 'no-secrets': noSecrets,
52
+ 'prefer-arrow-functions': preferArrowFunctions,
53
+ sonarjs,
54
+ },
48
55
  settings: {
49
56
  'import-x/resolver-next': [createTypeScriptImportResolver(), createNodeResolver()],
50
57
  },
@@ -59,6 +66,8 @@ export default function base(options) {
59
66
  'no-nested-ternary': 'error',
60
67
  // Arrow functions as callbacks. Bad: `list.map(function (x) { return x; })`.
61
68
  'prefer-arrow-callback': 'error',
69
+ // One function form throughout, and no `this` to reason about. Bad: `function load() {}`, `export default function base() {}`.
70
+ 'prefer-arrow-functions/prefer-arrow-functions': ['error', { disallowPrototype: true }],
62
71
  // A rethrow keeps the original error as `cause`. Bad: `catch (error) { throw new Error('failed'); }`.
63
72
  'preserve-caught-error': 'error',
64
73
  // The coercing globals. Bad: `isNaN('x')` (use `Number.isNaN`).
@@ -144,11 +153,11 @@ export default function base(options) {
144
153
  // One escape style for `${` in template literals. Bad: `` `\${a} $\{b}` ``.
145
154
  'unicorn/consistent-template-literal-escape': 'error',
146
155
  // `x !== undefined` where truthiness already decides. Bad: `if (user !== undefined)` with `user: User | undefined`.
147
- '@hypersolid-dev/undefined-check': 'error',
156
+ '@hypersolid-dev/base/undefined-check': 'error',
148
157
  // A regex is a module-level `*_PATTERN` constant, named for what it matches. Bad: `if (/^\d+$/.test(id))` in a function.
149
- '@hypersolid-dev/regex-pattern-const': 'error',
158
+ '@hypersolid-dev/base/regex-pattern-const': 'error',
150
159
  // `//` and `/* */` comments stay one row; longer prose is a `/** */` block. Bad: three `//` lines in a row.
151
- '@hypersolid-dev/comment-style': 'error',
160
+ '@hypersolid-dev/base/comment-style': 'error',
152
161
  // `undefined` as an argument is often required by the callee's signature. Bad: `let value = undefined`.
153
162
  'unicorn/no-useless-undefined': ['error', { checkArguments: false }],
154
163
  // Off: `.then()` chains are fine. Bad: `fetchUser().then(render)` reported.
@@ -183,12 +192,16 @@ export default function base(options) {
183
192
  'import-x/namespace': 'off',
184
193
  // High-entropy strings that look like credentials. Bad: `const token = 'ghp_Zx8kQ2vLm9…'`.
185
194
  'no-secrets/no-secrets': 'error',
195
+ // A function too tangled to follow, nesting weighted over length (limit 15). Bad: a loop holding an `if` holding a `try`.
196
+ 'sonarjs/cognitive-complexity': 'error',
186
197
  },
187
198
  },
188
199
  {
189
200
  name: '@hypersolid-dev/base/tests',
190
201
  files: TEST_FILES,
191
202
  rules: {
203
+ // A test's setup and table of cases are read once, not maintained as logic.
204
+ 'sonarjs/cognitive-complexity': 'off',
192
205
  // Off: mocks and fixtures are loosely typed by design. Bad: `const user = jest.fn()()` reported.
193
206
  '@typescript-eslint/no-unsafe-assignment': 'off',
194
207
  // Off: mocks and fixtures are loosely typed by design. Bad: `mock.calls[0].user` reported.
@@ -202,7 +215,7 @@ export default function base(options) {
202
215
  // Off: helpers inside a `describe` keep each suite readable on its own. Bad: `const render = () => …` in a `describe` reported.
203
216
  'unicorn/consistent-function-scoping': 'off',
204
217
  // Off: a test keeps its inline `toMatch(/…/)` literal. Bad: `expect(id).toMatch(/^\d+$/)` reported.
205
- '@hypersolid-dev/regex-pattern-const': 'off',
218
+ '@hypersolid-dev/base/regex-pattern-const': 'off',
206
219
  },
207
220
  },
208
221
  {
package/dist/jest.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { Linter } from 'eslint';
2
+ export declare const JEST_FILES: string[];
3
+ export default function jest(): Linter.Config[];
package/dist/jest.js ADDED
@@ -0,0 +1,56 @@
1
+ import jestPlugin from 'eslint-plugin-jest';
2
+ import testingLibrary from 'eslint-plugin-testing-library';
3
+ import { raiseWarnings } from './shared.js';
4
+ // Test suites and Jest setup. Mocks and stories are left out: `jest/no-export` would reject what they exist to export.
5
+ export const JEST_FILES = ['**/*.{test,spec}.{js,mjs,cjs,jsx,ts,mts,cts,tsx}', '**/jest.setup.*'];
6
+ const SUITE_FILES = ['**/*.{test,spec}.{js,mjs,cjs,jsx,ts,mts,cts,tsx}'];
7
+ export default function jest() {
8
+ return raiseWarnings([
9
+ {
10
+ // Jest's recommended set and its globals; the plugin ships it unscoped. Bad: `it.only(…)`, `expect` inside a `catch`.
11
+ ...jestPlugin.configs['flat/recommended'],
12
+ name: '@hypersolid-dev/jest',
13
+ files: JEST_FILES,
14
+ rules: {
15
+ ...jestPlugin.configs['flat/recommended'].rules,
16
+ // Off: jest/unbound-method replaces it and knows `expect(service.method)` does not call it. Bad: `expect(Alert.alert)` reported.
17
+ '@typescript-eslint/unbound-method': 'off',
18
+ // A method passed on loses its `this`, except into `expect`. Bad: `const { save } = service; save()`.
19
+ 'jest/unbound-method': 'error',
20
+ // A test must assert; `expectX()` helpers count as assertions. Bad: `it('renders', () => { render(<A />); })`.
21
+ 'jest/expect-expect': ['error', { assertFunctionNames: ['expect', 'expect*'] }],
22
+ // One spelling for a test case. Bad: `test('…')` next to `it('…')`.
23
+ 'jest/consistent-test-it': ['error', { fn: 'it' }],
24
+ // A conditional in a test means it does not know what it asserts. Bad: `if (items.length) expect(…)`.
25
+ 'jest/no-conditional-in-test': 'error',
26
+ // Two hooks of a kind in one block run in an order nobody chose. Bad: two `beforeEach` in one `describe`.
27
+ 'jest/no-duplicate-hooks': 'error',
28
+ // Returning from a test hides an unawaited promise. Bad: `it('loads', () => { return load(); })`.
29
+ 'jest/no-test-return-statement': 'error',
30
+ // Setup is read before what it sets up. Bad: `beforeEach` below the first `it`.
31
+ 'jest/prefer-hooks-on-top': 'error',
32
+ // A blank line around each block, hook and assertion group. Bad: `it(…)` directly under `beforeEach(…)`.
33
+ 'jest/padding-around-all': 'error',
34
+ // `jest.setTimeout` below the top level applies to nothing. Bad: `jest.setTimeout(10_000)` inside a `describe`.
35
+ 'jest/no-confusing-set-timeout': 'error',
36
+ },
37
+ },
38
+ {
39
+ // Testing Library's React set; it covers React Native Testing Library, which has no flavour of its own. Bad: `container.querySelector(…)`, an unawaited `findBy…`.
40
+ ...testingLibrary.configs['flat/react'],
41
+ name: '@hypersolid-dev/jest/testing-library',
42
+ files: SUITE_FILES,
43
+ rules: {
44
+ ...testingLibrary.configs['flat/react'].rules,
45
+ // Events are awaited. Bad: `userEvent.press(button)` without `await`.
46
+ 'testing-library/await-async-events': 'error',
47
+ // Testing Library already wraps its utilities in `act`. Bad: `act(() => fireEvent.press(button))`.
48
+ 'testing-library/no-unnecessary-act': 'error',
49
+ // Off: React Native Testing Library 14's `fireEvent` returns a promise to await. Bad: `await fireEvent.press(button)` reported.
50
+ 'testing-library/no-await-sync-events': 'off',
51
+ // Off: suites query through `screen`, so it only reaches helpers that return a mock. Bad: `const view = render(<A />)` reported.
52
+ 'testing-library/render-result-naming-convention': 'off',
53
+ },
54
+ },
55
+ ]);
56
+ }
@@ -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,15 @@
1
+ import commentStyle from './rules/comment-style.js';
2
+ import regexPatternConst from './rules/regex-pattern-const.js';
3
+ import undefinedCheck from './rules/undefined-check.js';
4
+ /** ESLint reads a scoped rule ID up to its last slash as the plugin name. */
5
+ // One object per plugin name: flat config rejects two different objects under the same name.
6
+ const plugin = {
7
+ meta: { name: '@hypersolid-dev/base' },
8
+ // typescript-eslint's `RuleModule` and ESLint's `Rule.RuleModule` type the same runtime object differently.
9
+ rules: {
10
+ 'comment-style': commentStyle,
11
+ 'regex-pattern-const': regexPatternConst,
12
+ 'undefined-check': undefinedCheck,
13
+ },
14
+ };
15
+ export default plugin;
@@ -1,6 +1,6 @@
1
1
  import { AST_NODE_TYPES, AST_TOKEN_TYPES } from "@typescript-eslint/utils";
2
- import createRule from "../utils/create-rule.js";
3
- import { DIRECTIVE_PATTERN, isJsdoc, isRowStart } from "../utils/comments.js";
2
+ import createRule from "../../shared/create-rule.js";
3
+ import { DIRECTIVE_PATTERN, isJsdoc, isRowStart } from "../../shared/comments.js";
4
4
  const REASON_PATTERN = /\s--\s([\s\S]*)$/;
5
5
  const LINK_PATTERN = /\S*https?:\/\/\S*/g;
6
6
  const JSDOC_ROW_PATTERN = /^\s*\*?\s?/;
@@ -74,7 +74,7 @@ export default createRule({
74
74
  tooLong: "This comment line is {{length}} characters. Keep it to {{max}}.",
75
75
  },
76
76
  },
77
- create(context) {
77
+ create: (context) => {
78
78
  const { sourceCode } = context;
79
79
  const maxLength = context.options[0]?.maxLength;
80
80
  const isTooLong = (comment) => Boolean(maxLength) && lineLength(comment) > (maxLength ?? 0);
@@ -86,7 +86,7 @@ export default createRule({
86
86
  });
87
87
  };
88
88
  return {
89
- "Program:exit"() {
89
+ "Program:exit": () => {
90
90
  let run = [];
91
91
  const doesContinue = (comment) => {
92
92
  const last = run.at(-1);
@@ -108,48 +108,57 @@ export default createRule({
108
108
  }
109
109
  run = [];
110
110
  };
111
- for (const comment of sourceCode.getAllComments()) {
112
- if (comment.type === AST_TOKEN_TYPES.Block) {
113
- const rows = comment.loc.end.line - comment.loc.start.line + 1;
114
- // A JSX comment's `{` is syntax, so a row holding only `{/* … */}` starts its row.
115
- const isOwnRow = isRowStart(sourceCode, comment) ||
116
- (isJsxComment(sourceCode, comment) &&
117
- !sourceCode.lines[comment.loc.start.line - 1]
118
- ?.slice(0, Math.max(0, comment.loc.start.column - 1))
119
- .trim());
120
- if (!isJsdoc(comment) && rows === 1 && isOwnRow) {
121
- if (!doesContinue(comment))
122
- flush();
123
- run.push(comment);
124
- continue;
125
- }
126
- flush();
127
- if (isJsdoc(comment)) {
128
- if (isTooLong(comment))
129
- reportTooLong(comment);
130
- continue;
131
- }
132
- if (rows > 1) {
133
- context.report({
134
- loc: comment.loc,
135
- messageId: isJsxComment(sourceCode, comment) ? "jsxBlock" : "blockRun",
136
- data: { rows: String(rows) },
137
- });
138
- }
139
- else if (isTooLong(comment)) {
111
+ /** A JSX comment's `{` is syntax, so a row holding only a braced JSX comment starts its row. */
112
+ const isOwnRow = (comment) => isRowStart(sourceCode, comment) ||
113
+ (isJsxComment(sourceCode, comment) &&
114
+ !sourceCode.lines[comment.loc.start.line - 1]
115
+ ?.slice(0, Math.max(0, comment.loc.start.column - 1))
116
+ .trim());
117
+ /** A block comment that is not part of a run: JSDoc, multi-row, or sharing its row with code. */
118
+ const checkLoneBlock = (comment, rows) => {
119
+ if (isJsdoc(comment)) {
120
+ if (isTooLong(comment))
140
121
  reportTooLong(comment);
141
- }
142
- continue;
122
+ return;
123
+ }
124
+ if (rows > 1) {
125
+ context.report({
126
+ loc: comment.loc,
127
+ messageId: isJsxComment(sourceCode, comment) ? "jsxBlock" : "blockRun",
128
+ data: { rows: String(rows) },
129
+ });
143
130
  }
131
+ else if (isTooLong(comment)) {
132
+ reportTooLong(comment);
133
+ }
134
+ };
135
+ const visitBlock = (comment) => {
136
+ const rows = comment.loc.end.line - comment.loc.start.line + 1;
137
+ if (!isJsdoc(comment) && rows === 1 && isOwnRow(comment)) {
138
+ if (!doesContinue(comment))
139
+ flush();
140
+ run.push(comment);
141
+ return;
142
+ }
143
+ flush();
144
+ checkLoneBlock(comment, rows);
145
+ };
146
+ const visitLine = (comment) => {
144
147
  if (!doesContinue(comment))
145
148
  flush();
146
149
  if (!isRowStart(sourceCode, comment)) {
147
150
  flush();
148
151
  if (isTooLong(comment))
149
152
  reportTooLong(comment);
150
- continue;
153
+ return;
151
154
  }
152
155
  run.push(comment);
156
+ };
157
+ for (const comment of sourceCode.getAllComments()) {
158
+ if (comment.type === AST_TOKEN_TYPES.Block)
159
+ visitBlock(comment);
160
+ else
161
+ visitLine(comment);
153
162
  }
154
163
  flush();
155
164
  },
@@ -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 NAME_PATTERN = /^[A-Z][A-Z0-9_]*_PATTERN$/;
4
4
  /** A string literal, or a template (plain or `String.raw`) with nothing interpolated. */
5
5
  const isStatic = (node) => (node.type === AST_NODE_TYPES.Literal && typeof node.value === "string") ||
@@ -64,7 +64,7 @@ export default createRule({
64
64
  rename: "Rename `{{name}}` to end in `_PATTERN` (SCREAMING_SNAKE_CASE), so the name says it is a regular expression.",
65
65
  },
66
66
  },
67
- create(context) {
67
+ create: (context) => {
68
68
  const check = (node) => {
69
69
  if (!isRegex(node))
70
70
  return;
@@ -1,6 +1,6 @@
1
1
  import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils";
2
- import createRule from "../utils/create-rule.js";
3
- import { isSameAsBangBang, undefinedComparison } from "../utils/types.js";
2
+ import createRule from "../../shared/create-rule.js";
3
+ import { isSameAsBangBang, undefinedComparison } from "../../shared/types.js";
4
4
  /** The slots where a value is being asked whether it is there. */
5
5
  const isCondition = (node) => {
6
6
  const { parent } = node;
@@ -35,13 +35,13 @@ const undefinedCheck = createRule({
35
35
  redundant: "`{{ text }}` is `{{ suggestion }}` for this type: `undefined` is the only falsy value it admits, so the comparison draws no line. Write the value.",
36
36
  },
37
37
  },
38
- create(context) {
38
+ create: (context) => {
39
39
  const services = ESLintUtils.getParserServices(context, true);
40
40
  if (!services.program)
41
41
  return {};
42
42
  const checker = services.program.getTypeChecker();
43
43
  return {
44
- BinaryExpression(node) {
44
+ BinaryExpression: (node) => {
45
45
  if (!isCondition(node))
46
46
  return;
47
47
  const compared = undefinedComparison(node);
@@ -0,0 +1,4 @@
1
+ import type { ESLint } from 'eslint';
2
+ /** ESLint reads a scoped rule ID up to its last slash as the plugin name. */
3
+ declare const plugin: ESLint.Plugin;
4
+ export default plugin;
@@ -0,0 +1,23 @@
1
+ import booleanGuard from './rules/boolean-guard.js';
2
+ import effectTakesCallback from './rules/effect-takes-callback.js';
3
+ import jsdocForProps from './rules/jsdoc-for-props.js';
4
+ import noCallbackPayload from './rules/no-callback-payload.js';
5
+ import noMarkupVariable from './rules/no-markup-variable.js';
6
+ import noUnnecessaryUseCallback from './rules/no-unnecessary-use-callback.js';
7
+ import propsDestructuring from './rules/props-destructuring.js';
8
+ /** ESLint reads a scoped rule ID up to its last slash as the plugin name. */
9
+ // One object per plugin name: flat config rejects two different objects under the same name.
10
+ const plugin = {
11
+ meta: { name: '@hypersolid-dev/react' },
12
+ // typescript-eslint's `RuleModule` and ESLint's `Rule.RuleModule` type the same runtime object differently.
13
+ rules: {
14
+ 'boolean-guard': booleanGuard,
15
+ 'effect-takes-callback': effectTakesCallback,
16
+ 'jsdoc-for-props': jsdocForProps,
17
+ 'no-callback-payload': noCallbackPayload,
18
+ 'no-markup-variable': noMarkupVariable,
19
+ 'no-unnecessary-use-callback': noUnnecessaryUseCallback,
20
+ 'props-destructuring': propsDestructuring,
21
+ },
22
+ };
23
+ export default plugin;
@@ -1,7 +1,7 @@
1
1
  import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils";
2
2
  import ts, { TypeFlags } from "typescript";
3
- import createRule from "../utils/create-rule.js";
4
- import { isSameAsBangBang, undefinedComparison } from "../utils/types.js";
3
+ import createRule from "../../shared/create-rule.js";
4
+ import { isSameAsBangBang, undefinedComparison } from "../../shared/types.js";
5
5
  /** React draws these; every other falsy value renders as nothing. */
6
6
  const RENDERS_WHEN_FALSY = TypeFlags.NumberLike | TypeFlags.StringLike | TypeFlags.BigIntLike;
7
7
  /** `any` and `unknown` could be anything, so they are treated as leaking. */
@@ -69,7 +69,7 @@ const booleanGuard = createRule({
69
69
  preferBangBang: "`!== undefined` is `!!` for this type. Write `!!{{condition}} && …`.",
70
70
  },
71
71
  },
72
- create(context) {
72
+ create: (context) => {
73
73
  const services = ESLintUtils.getParserServices(context, true);
74
74
  if (!services.program)
75
75
  return {};
@@ -99,7 +99,7 @@ const booleanGuard = createRule({
99
99
  return reads.every((reference) => isInRenderPosition(reference.identifier));
100
100
  };
101
101
  return {
102
- ConditionalExpression(node) {
102
+ ConditionalExpression: (node) => {
103
103
  // The shape is what proves a `const` holding this is a guard.
104
104
  const isConsequentGuarded = isRenderable(node.consequent) && isNothing(node.alternate);
105
105
  const isAlternateGuarded = isRenderable(node.alternate) && isNothing(node.consequent);
@@ -122,7 +122,7 @@ const booleanGuard = createRule({
122
122
  data: { condition: isAlternateGuarded ? `!${nameOf(node.test)}` : nameOf(node.test) },
123
123
  });
124
124
  },
125
- LogicalExpression(node) {
125
+ LogicalExpression: (node) => {
126
126
  if (node.operator !== "&&")
127
127
  return;
128
128
  if (!isInRenderPosition(node))
@@ -1,6 +1,6 @@
1
1
  import { AST_NODE_TYPES } from "@typescript-eslint/utils";
2
2
  import ts, { TypeFlags } from "typescript";
3
- import createRule from "../utils/create-rule.js";
3
+ import createRule from "../../shared/create-rule.js";
4
4
  const EFFECT_HOOK_PATTERN = /^use[A-Z]\w*Effect$/;
5
5
  const NOTHING = TypeFlags.Undefined | TypeFlags.Null | TypeFlags.Void;
6
6
  const ABSENT = new Set(["TSUndefinedKeyword", "TSNullKeyword", "TSVoidKeyword"]);
@@ -48,7 +48,7 @@ const effectTakesCallback = createRule({
48
48
  callbackFirst: "`{{name}}` ends in Effect but its first parameter is not a callback. Take the callback first, or drop the Effect suffix.",
49
49
  },
50
50
  },
51
- create(context) {
51
+ create: (context) => {
52
52
  const services = context.sourceCode.parserServices;
53
53
  const checker = services?.program ? services.program.getTypeChecker() : undefined;
54
54
  /** Whether a value of this type can be called, once `undefined` is set aside. */
@@ -1,6 +1,6 @@
1
1
  import { AST_TOKEN_TYPES } from "@typescript-eslint/utils";
2
- import createRule from "../utils/create-rule.js";
3
- import { DIRECTIVE_PATTERN, isJsdoc, isRowStart } from "../utils/comments.js";
2
+ import createRule from "../../shared/create-rule.js";
3
+ import { DIRECTIVE_PATTERN, isJsdoc, isRowStart } from "../../shared/comments.js";
4
4
  /**
5
5
  * A comment on a type member is a JSDoc block, the only form an editor shows when the member is hovered elsewhere.
6
6
  *
@@ -28,7 +28,7 @@ export default createRule({
28
28
  form: "Write this as a JSDoc block, not `{{ form }}`. A member's comment is what an editor shows when someone hovers it elsewhere, and only JSDoc reaches there.",
29
29
  },
30
30
  },
31
- create(context) {
31
+ create: (context) => {
32
32
  const { sourceCode } = context;
33
33
  const check = (node) => {
34
34
  // A directive neither documents the member nor separates it from the comment above.