@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/dist/shared.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import eslintReact from '@eslint-react/eslint-plugin';
2
+ import reactDoctor from 'eslint-plugin-react-doctor';
2
3
  import reactHooks from 'eslint-plugin-react-hooks';
3
4
  import unicorn from 'eslint-plugin-unicorn';
4
- import hypersolid from './plugins/hypersolid.js';
5
+ import reactPlugin from './plugins/react/index.js';
5
6
  export const CODE_FILES = ['**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx}'];
6
7
  // Tests, stories, mocks and test setup: each preset relaxes a few rules here that test doubles break by design.
7
8
  export const TEST_FILES = [
@@ -52,119 +53,174 @@ export const NAMING_CONVENTION = [
52
53
  { selector: 'typeLike', format: ['PascalCase'] },
53
54
  ];
54
55
  // Every enabled rule is an error: a warning is either worth fixing or not worth reporting.
55
- export function raiseWarnings(configs) {
56
- return configs.map((config) => {
57
- if (!config.rules) {
58
- return config;
56
+ export const raiseWarnings = (configs) => configs.map((config) => {
57
+ if (!config.rules) {
58
+ return config;
59
+ }
60
+ const rules = {};
61
+ for (const [name, entry] of Object.entries(config.rules)) {
62
+ if (entry === undefined) {
63
+ continue;
59
64
  }
60
- const rules = {};
61
- for (const [name, entry] of Object.entries(config.rules)) {
62
- if (entry === undefined) {
63
- continue;
64
- }
65
- const [severity, ...ruleOptions] = Array.isArray(entry) ? entry : [entry];
66
- rules[name] = severity === 'warn' || severity === 1 ? ['error', ...ruleOptions] : entry;
67
- }
68
- return { ...config, rules };
69
- });
70
- }
71
- export function scopeToCode(configs) {
72
- return configs.map((config) => ({ files: CODE_FILES, ...config }));
73
- }
65
+ const [severity, ...ruleOptions] = Array.isArray(entry) ? entry : [entry];
66
+ rules[name] = severity === 'warn' || severity === 1 ? ['error', ...ruleOptions] : entry;
67
+ }
68
+ return { ...config, rules };
69
+ });
70
+ export const scopeToCode = (configs) => configs.map((config) => ({ files: CODE_FILES, ...config }));
74
71
  // The React block shared by `react` and `react-native`.
75
- export function reactConfigs() {
76
- return [
77
- ...scopeToCode([
78
- // React correctness for TypeScript projects. Bad: `this.state.count = 1`, `<li>` in a `.map()` without `key`.
79
- eslintReact.configs['recommended-typescript'],
80
- // The official hooks rules plus the React Compiler checks. Bad: `if (isOpen) useEffect(…)`, a missing effect dependency.
81
- reactHooks.configs.flat['recommended-latest'],
82
- ]),
83
- {
84
- name: '@hypersolid-dev/react',
85
- files: CODE_FILES,
86
- plugins: { '@hypersolid-dev': hypersolid, unicorn },
87
- languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } },
88
- rules: {
89
- // Off: react-hooks/exhaustive-deps reports it. Bad: `useEffect(() => log(id), [])` reported twice.
90
- '@eslint-react/exhaustive-deps': 'off',
91
- // Off: react-hooks/rules-of-hooks reports it. Bad: `if (isOpen) useEffect(…)` reported twice.
92
- '@eslint-react/rules-of-hooks': 'off',
93
- // Off: react-hooks/error-boundaries reports it. Bad: `try { return <Child /> } catch {…}` reported twice.
94
- '@eslint-react/error-boundaries': 'off',
95
- // Off: react-hooks/purity reports it. Bad: `const id = Math.random()` in render reported twice.
96
- '@eslint-react/purity': 'off',
97
- // Off: react-hooks/set-state-in-effect reports it. Bad: `useEffect(() => setCount(1))` reported twice.
98
- '@eslint-react/set-state-in-effect': 'off',
99
- // Off: react-hooks/set-state-in-render reports it. Bad: `setCount(1)` in the component body reported twice.
100
- '@eslint-react/set-state-in-render': 'off',
101
- // Off: react-hooks/static-components reports it. Bad: a component created during render reported twice.
102
- '@eslint-react/static-components': 'off',
103
- // Off: react-hooks/unsupported-syntax reports it. Bad: `eval(code)` in a component reported twice.
104
- '@eslint-react/unsupported-syntax': 'off',
105
- // Off: react-hooks/use-memo reports it. Bad: `useMemo(async () => …)` reported twice.
106
- '@eslint-react/use-memo': 'off',
107
- // A component defined inside another remounts on every render. Bad: `function List() { function Row() {…} return <Row />; }`.
108
- '@eslint-react/no-nested-component-definitions': 'error',
109
- // List items need a `key`. Bad: `items.map((item) => <Row item={item} />)`.
110
- '@eslint-react/no-missing-key': 'error',
111
- // Sibling keys are unique. Bad: `<Row key="a" /><Row key="a" />`.
112
- '@eslint-react/no-duplicate-key': 'error',
113
- // An index key breaks state when the list reorders. Bad: `items.map((item, index) => <Row key={index} />)`.
114
- '@eslint-react/no-array-index-key': 'error',
115
- // A new context value each render re-renders every consumer. Bad: `<Context value={{ user }}>`.
116
- '@eslint-react/no-unstable-context-value': 'error',
117
- // Props are readable from the component's signature; a union props type may keep `props`. Bad: `const Card = (props: Props) => <h1>{props.title}</h1>`.
118
- '@hypersolid-dev/props-destructuring': 'error',
119
- // A boolean reads as a claim; hook and mock names are exempt. Bad: `const visible = true` (use `isVisible`).
120
- 'unicorn/consistent-boolean-name': ['error', REACT_BOOLEAN_NAME_OPTIONS],
121
- // Base's empty catch and default specifiers, plus compiler opt-outs, hooks inside JSX and `React.useX`. Bad: `style={useAnimatedStyle(…)}`, `React.useState(0)`.
122
- 'no-restricted-syntax': ['error', ...REACT_RESTRICTED_SYNTAX],
123
- // Base's naming, plus: a boolean prop is a state, not a command. Bad: `type Props = { showBadge: boolean }` (use `hasBadge`).
124
- '@typescript-eslint/naming-convention': [
125
- 'error',
126
- ...NAMING_CONVENTION,
127
- { selector: 'typeProperty', types: ['boolean'], format: null, custom: { regex: '^(show|display|hide)[A-Z]', match: false } },
128
- ],
129
- // A fragment around one child renders nothing extra. Bad: `<><Row /></>`.
130
- '@eslint-react/jsx-no-useless-fragment': 'error',
131
- // A literal default prop is a new object each render and breaks memoization. Bad: `({ items = [] }: Props)`.
132
- '@eslint-react/no-unstable-default-props': 'error',
133
- // State synced from props in an effect renders one frame stale. Bad: `useEffect(() => setFullName(first + last), [first, last])`.
134
- 'react-hooks/no-deriving-state-in-effects': 'error',
135
- // Off: memo-dependencies and exhaustive-effect-dependencies check the same arrays with the compiler's analysis, so one missing dependency would be reported twice.
136
- 'react-hooks/exhaustive-deps': 'off',
137
- // The compiler's dependency check for memos: missing and unused deps. Bad: `useMemo(() => a + b, [a])`.
138
- 'react-hooks/memo-dependencies': 'error',
139
- // The same check for effects; plugin 7.1.1 ships it inert, so `environment` turns it on. Bad: `useEffect(() => log(id), [])`.
140
- 'react-hooks/exhaustive-effect-dependencies': ['error', { environment: { validateExhaustiveEffectDependencies: 'all' } }],
141
- // `{count && <Badge />}` renders `0`, which crashes React Native outside `<Text>`. Bad: `{items.length && <List />}`.
142
- '@hypersolid-dev/boolean-guard': 'error',
143
- // Styled JSX built in a component body is a component without a name. Bad: `const header = <View style={s.header}>…</View>`.
144
- '@hypersolid-dev/no-markup-variable': 'error',
145
- // A `use*Effect` hook takes its callback first, like `useEffect`. Bad: `useFocusEffect(deps, () => …)`.
146
- '@hypersolid-dev/effect-takes-callback': 'error',
147
- // A `useCallback` with empty deps that captures nothing is a module-level function. Bad: `useCallback(() => track('open'), [])`.
148
- '@hypersolid-dev/no-unnecessary-use-callback': 'error',
149
- // A component forwards the event; the caller builds the payload. Bad: `onPress={() => onSelect(item.id)}` inside `Row`.
150
- '@hypersolid-dev/no-callback-payload': 'error',
151
- // A comment on a type member is JSDoc, the form editors show. Bad: `// The title\ntitle: string` in a props type.
152
- '@hypersolid-dev/jsdoc-for-props': 'error',
153
- },
72
+ export const reactConfigs = () => [
73
+ ...scopeToCode([
74
+ // React correctness for TypeScript projects. Bad: `this.state.count = 1`, `<li>` in a `.map()` without `key`.
75
+ eslintReact.configs['recommended-typescript'],
76
+ // The official hooks rules plus the React Compiler checks. Bad: `if (isOpen) useEffect(…)`, a missing effect dependency.
77
+ reactHooks.configs.flat['recommended-latest'],
78
+ ]),
79
+ {
80
+ name: '@hypersolid-dev/react',
81
+ files: CODE_FILES,
82
+ plugins: { '@hypersolid-dev/react': reactPlugin, unicorn },
83
+ languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } },
84
+ rules: {
85
+ // Off: react-hooks/exhaustive-deps reports it. Bad: `useEffect(() => log(id), [])` reported twice.
86
+ '@eslint-react/exhaustive-deps': 'off',
87
+ // Off: react-hooks/rules-of-hooks reports it. Bad: `if (isOpen) useEffect(…)` reported twice.
88
+ '@eslint-react/rules-of-hooks': 'off',
89
+ // Off: react-hooks/error-boundaries reports it. Bad: `try { return <Child /> } catch {…}` reported twice.
90
+ '@eslint-react/error-boundaries': 'off',
91
+ // Off: react-hooks/purity reports it. Bad: `const id = Math.random()` in render reported twice.
92
+ '@eslint-react/purity': 'off',
93
+ // Off: react-hooks/set-state-in-effect reports it. Bad: `useEffect(() => setCount(1))` reported twice.
94
+ '@eslint-react/set-state-in-effect': 'off',
95
+ // Off: react-hooks/set-state-in-render reports it. Bad: `setCount(1)` in the component body reported twice.
96
+ '@eslint-react/set-state-in-render': 'off',
97
+ // Off: react-hooks/static-components reports it. Bad: a component created during render reported twice.
98
+ '@eslint-react/static-components': 'off',
99
+ // Off: react-hooks/unsupported-syntax reports it. Bad: `eval(code)` in a component reported twice.
100
+ '@eslint-react/unsupported-syntax': 'off',
101
+ // Off: react-hooks/use-memo reports it. Bad: `useMemo(async () => …)` reported twice.
102
+ '@eslint-react/use-memo': 'off',
103
+ // A component defined inside another remounts on every render. Bad: `function List() { function Row() {…} return <Row />; }`.
104
+ '@eslint-react/no-nested-component-definitions': 'error',
105
+ // List items need a `key`. Bad: `items.map((item) => <Row item={item} />)`.
106
+ '@eslint-react/no-missing-key': 'error',
107
+ // Sibling keys are unique. Bad: `<Row key="a" /><Row key="a" />`.
108
+ '@eslint-react/no-duplicate-key': 'error',
109
+ // An index key breaks state when the list reorders. Bad: `items.map((item, index) => <Row key={index} />)`.
110
+ '@eslint-react/no-array-index-key': 'error',
111
+ // A new context value each render re-renders every consumer. Bad: `<Context value={{ user }}>`.
112
+ '@eslint-react/no-unstable-context-value': 'error',
113
+ // Props are readable from the component's signature; a union props type may keep `props`. Bad: `const Card = (props: Props) => <h1>{props.title}</h1>`.
114
+ '@hypersolid-dev/react/props-destructuring': 'error',
115
+ // A boolean reads as a claim; hook and mock names are exempt. Bad: `const visible = true` (use `isVisible`).
116
+ 'unicorn/consistent-boolean-name': ['error', REACT_BOOLEAN_NAME_OPTIONS],
117
+ // Base's empty catch and default specifiers, plus compiler opt-outs, hooks inside JSX and `React.useX`. Bad: `style={useAnimatedStyle(…)}`, `React.useState(0)`.
118
+ 'no-restricted-syntax': ['error', ...REACT_RESTRICTED_SYNTAX],
119
+ // Base's naming, plus: a boolean prop is a state, not a command. Bad: `type Props = { showBadge: boolean }` (use `hasBadge`).
120
+ '@typescript-eslint/naming-convention': [
121
+ 'error',
122
+ ...NAMING_CONVENTION,
123
+ { selector: 'typeProperty', types: ['boolean'], format: null, custom: { regex: '^(show|display|hide)[A-Z]', match: false } },
124
+ ],
125
+ // A fragment around one child renders nothing extra. Bad: `<><Row /></>`.
126
+ '@eslint-react/jsx-no-useless-fragment': 'error',
127
+ // A literal default prop is a new object each render and breaks memoization. Bad: `({ items = [] }: Props)`.
128
+ '@eslint-react/no-unstable-default-props': 'error',
129
+ // State synced from props in an effect renders one frame stale. Bad: `useEffect(() => setFullName(first + last), [first, last])`.
130
+ 'react-hooks/no-deriving-state-in-effects': 'error',
131
+ // Off: memo-dependencies and exhaustive-effect-dependencies check the same arrays with the compiler's analysis, so one missing dependency would be reported twice.
132
+ 'react-hooks/exhaustive-deps': 'off',
133
+ // The compiler's dependency check for memos: missing and unused deps. Bad: `useMemo(() => a + b, [a])`.
134
+ 'react-hooks/memo-dependencies': 'error',
135
+ // The same check for effects; plugin 7.1.1 ships it inert, so `environment` turns it on. Bad: `useEffect(() => log(id), [])`.
136
+ 'react-hooks/exhaustive-effect-dependencies': ['error', { environment: { validateExhaustiveEffectDependencies: 'all' } }],
137
+ // `{count && <Badge />}` renders `0`, which crashes React Native outside `<Text>`. Bad: `{items.length && <List />}`.
138
+ '@hypersolid-dev/react/boolean-guard': 'error',
139
+ // Styled JSX built in a component body is a component without a name. Bad: `const header = <View style={s.header}>…</View>`.
140
+ '@hypersolid-dev/react/no-markup-variable': 'error',
141
+ // A `use*Effect` hook takes its callback first, like `useEffect`. Bad: `useFocusEffect(deps, () => …)`.
142
+ '@hypersolid-dev/react/effect-takes-callback': 'error',
143
+ // A `useCallback` with empty deps that captures nothing is a module-level function. Bad: `useCallback(() => track('open'), [])`.
144
+ '@hypersolid-dev/react/no-unnecessary-use-callback': 'error',
145
+ // A component forwards the event; the caller builds the payload. Bad: `onPress={() => onSelect(item.id)}` inside `Row`.
146
+ '@hypersolid-dev/react/no-callback-payload': 'error',
147
+ // A comment on a type member is JSDoc, the form editors show. Bad: `// The title\ntitle: string` in a props type.
148
+ '@hypersolid-dev/react/jsdoc-for-props': 'error',
154
149
  },
155
- {
156
- name: '@hypersolid-dev/react/tests',
157
- files: TEST_FILES,
158
- rules: {
159
- // Off: tests build markup and callbacks inline by design. Bad: `const tree = <Row />` in a test reported.
160
- '@hypersolid-dev/boolean-guard': 'off',
161
- '@hypersolid-dev/no-markup-variable': 'off',
162
- '@hypersolid-dev/effect-takes-callback': 'off',
163
- '@hypersolid-dev/no-unnecessary-use-callback': 'off',
164
- '@hypersolid-dev/no-callback-payload': 'off',
165
- // Off: a mocked hook keeps the `use` name of the hook it replaces. Bad: `const useUser = () => fakeUser` in a mock.
166
- '@eslint-react/no-unnecessary-use-prefix': 'off',
167
- },
150
+ },
151
+ {
152
+ /**
153
+ * React Doctor's bug catchers, as used in the ESPN Goal pilot; test doubles break them by design.
154
+ * Left out as duplicates: `no-create-ref-in-function-component` (@eslint-react/no-create-ref), and
155
+ * `effect-listener-cleanup-mismatch` and `effect-remove-listener-inline-handler` (@eslint-react/web-api-no-leaked-event-listener).
156
+ */
157
+ name: '@hypersolid-dev/react/react-doctor',
158
+ files: CODE_FILES,
159
+ ignores: TEST_FILES,
160
+ plugins: { 'react-doctor': reactDoctor },
161
+ rules: {
162
+ // A value computed from props or state is read where used, not copied into state. Bad: `const [full, setFull] = useState(first + last)` kept in sync by an effect.
163
+ 'react-doctor/no-derived-state': 'error',
164
+ // A refetch from an effect double-fetches and races the cache; key the query instead. Bad: `useEffect(() => { refetch(); }, [id])`.
165
+ 'react-doctor/query-no-query-in-effect': 'error',
166
+ // A write behind `useQuery` posts again on focus and mount; writes are mutations. Bad: `useQuery({ queryFn: () => api.post(…) })`.
167
+ 'react-doctor/query-no-usequery-for-mutation': 'error',
168
+ // A `requestAnimationFrame` loop without a cancel keeps running after unmount. Bad: an effect that schedules `tick` with no `cancelAnimationFrame` cleanup.
169
+ 'react-doctor/effect-raf-loop-needs-cancel': 'error',
170
+ // A timer ref holding a cleared id still reads as pending. Bad: `clearTimeout(timer.current)` without `timer.current = undefined`.
171
+ 'react-doctor/no-stale-timer-ref': 'error',
172
+ // An effect wrapper must return the callback's cleanup. Bad: `useEffect(() => { callback(); }, deps)` in a `useMountEffect(callback)`.
173
+ 'react-doctor/no-effect-wrapper-discards-callback-cleanup-return': 'error',
174
+ // A fetch in an effect races on re-run and is neither cached nor deduplicated. Bad: `useEffect(() => { fetch(url).then(setData); }, [url])`.
175
+ 'react-doctor/no-fetch-in-effect': 'error',
176
+ // A toggle from a deferred callback reads a stale value; use the updater. Bad: `setTimeout(() => setOpen(!open))`.
177
+ 'react-doctor/no-boolean-toggle-without-functional-update': 'error',
178
+ // Mutating state and setting it hands React the same reference, so nothing re-renders. Bad: `items.push(item); setItems(items)`.
179
+ 'react-doctor/no-direct-state-mutation': 'error',
180
+ // The same mutation through an alias. Bad: `const next = state; next.done = true; setState(next)`.
181
+ 'react-doctor/no-mutate-then-set-or-return-same-reference': 'error',
182
+ // `sort`, `reverse` and `splice` reorder a prop in place; use `toSorted` and friends. Bad: `props.items.sort(byName)`.
183
+ 'react-doctor/no-mutating-array-method-on-prop-or-hook-result': 'error',
184
+ // React may call an updater twice, so a side effect in one runs twice. Bad: `setCount((count) => { track(count); return count + 1; })`.
185
+ 'react-doctor/no-side-effect-in-state-updater-function': 'error',
186
+ // After `.fill(x)` every element is x, so every key is the same. Bad: `Array(3).fill(0).map((value) => <Dot key={value} />)`.
187
+ 'react-doctor/no-fill-map-element-as-key': 'error',
188
+ // Calling a component runs its hooks as the caller's. Bad: `{Row(props)}` instead of `<Row {...props} />`.
189
+ 'react-doctor/no-call-component-as-function': 'error',
190
+ // A context created in render is new each time, so consumers lose their provider. Bad: `const Theme = createContext(light)` inside a component.
191
+ 'react-doctor/no-create-context-in-render': 'error',
192
+ // Render can be discarded, so calling a prop callback from it reports UI that never committed. Bad: `onReady()` in the component body.
193
+ 'react-doctor/no-prop-callback-in-render': 'error',
194
+ // State plus a subscribing effect tears under concurrent rendering. Bad: `useEffect(() => store.subscribe(() => setValue(store.get())))`.
195
+ 'react-doctor/prefer-use-sync-external-store': 'error',
196
+ // A hook renamed on import hides from the hooks rules, which match on the name. Bad: `import { useQuery as query } from '…'`.
197
+ 'react-doctor/hook-import-rename-loses-use-prefix': 'error',
198
+ // An index read is typed `T` but is `undefined` on a short array. Bad: `items[0].title` without a length check.
199
+ 'react-doctor/no-array-index-deref-without-bounds-or-empty-guard': 'error',
200
+ // Arithmetic binds tighter than `??`. Bad: `count ?? 0 + 1` (means `count ?? (0 + 1)`).
201
+ 'react-doctor/no-nullish-coalescing-arithmetic-precedence': 'error',
202
+ // A whole-object parameter default is dropped as soon as any object is passed. Bad: `({ size, color } = { size: 1, color: 'red' })`.
203
+ 'react-doctor/no-whole-object-default-losing-per-key-defaults': 'error',
204
+ // An explicit `undefined` prop erases the default it is spread over. Bad: `{ ...DEFAULTS, ...props }`.
205
+ 'react-doctor/no-spread-props-over-defaults-clobbers-with-undefined': 'error',
206
+ // fetch resolves on a 4xx or 5xx, so parsing without checking `ok` reads the error body as data. Bad: `(await fetch(url)).json()`.
207
+ 'react-doctor/no-fetch-response-used-without-status-check': 'error',
208
+ // An unescaped search term turns `.` and `(` into pattern syntax. Bad: `new RegExp(query)`.
209
+ 'react-doctor/no-unescaped-dynamic-string-in-regexp': 'error',
168
210
  },
169
- ];
170
- }
211
+ },
212
+ {
213
+ name: '@hypersolid-dev/react/tests',
214
+ files: TEST_FILES,
215
+ rules: {
216
+ // Off: tests build markup and callbacks inline by design. Bad: `const tree = <Row />` in a test reported.
217
+ '@hypersolid-dev/react/boolean-guard': 'off',
218
+ '@hypersolid-dev/react/no-markup-variable': 'off',
219
+ '@hypersolid-dev/react/effect-takes-callback': 'off',
220
+ '@hypersolid-dev/react/no-unnecessary-use-callback': 'off',
221
+ '@hypersolid-dev/react/no-callback-payload': 'off',
222
+ // Off: a mocked hook keeps the `use` name of the hook it replaces. Bad: `const useUser = () => fakeUser` in a mock.
223
+ '@eslint-react/no-unnecessary-use-prefix': 'off',
224
+ },
225
+ },
226
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypersolid-dev/eslint-config",
3
- "version": "5.0.0",
3
+ "version": "7.0.0",
4
4
  "description": "Shared ESLint flat config for Hypersolid TypeScript projects",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -12,7 +12,8 @@
12
12
  "react",
13
13
  "react-native",
14
14
  "next",
15
- "node"
15
+ "node",
16
+ "jest"
16
17
  ],
17
18
  "type": "module",
18
19
  "exports": {
@@ -35,6 +36,10 @@
35
36
  "./next": {
36
37
  "types": "./dist/next.d.ts",
37
38
  "default": "./dist/next.js"
39
+ },
40
+ "./jest": {
41
+ "types": "./dist/jest.d.ts",
42
+ "default": "./dist/jest.js"
38
43
  }
39
44
  },
40
45
  "files": [
@@ -75,9 +80,14 @@
75
80
  "eslint-import-resolver-typescript": "^4.4.5",
76
81
  "eslint-plugin-expo": "^1.2.1",
77
82
  "eslint-plugin-import-x": "^4.17.1",
83
+ "eslint-plugin-jest": "^29.17.0",
78
84
  "eslint-plugin-jsx-a11y": "^6.10.2",
79
85
  "eslint-plugin-no-secrets": "^2.3.3",
86
+ "eslint-plugin-prefer-arrow-functions": "^3.10.2",
87
+ "eslint-plugin-react-doctor": "0.9.22",
80
88
  "eslint-plugin-react-hooks": "7.1.1",
89
+ "eslint-plugin-sonarjs": "^4.2.2",
90
+ "eslint-plugin-testing-library": "^7.16.2",
81
91
  "eslint-plugin-unicorn": "^79.0.0",
82
92
  "globals": "^17.13.0",
83
93
  "typescript-eslint": "^8.72.1"
@@ -1,38 +0,0 @@
1
- import propsDestructuring from './props-destructuring.js';
2
- import booleanGuard from './rules/boolean-guard.js';
3
- import commentStyle from './rules/comment-style.js';
4
- import effectTakesCallback from './rules/effect-takes-callback.js';
5
- import jsdocForProps from './rules/jsdoc-for-props.js';
6
- import noCallbackPayload from './rules/no-callback-payload.js';
7
- import noMarkupVariable from './rules/no-markup-variable.js';
8
- import noUnnecessaryUseCallback from './rules/no-unnecessary-use-callback.js';
9
- import regexPatternConst from './rules/regex-pattern-const.js';
10
- import testIdDefault from './rules/test-id-default.js';
11
- import testIdFormat from './rules/test-id-format.js';
12
- import testIdFromProps from './rules/test-id-from-props.js';
13
- import undefinedCheck from './rules/undefined-check.js';
14
- import webviewAllowList from './rules/webview-allow-list.js';
15
- // One plugin object for every preset: flat config rejects two different objects under the same plugin name.
16
- const plugin = {
17
- meta: { name: '@hypersolid-dev/eslint-config' },
18
- rules: {
19
- ...propsDestructuring.rules,
20
- // typescript-eslint's `RuleModule` and ESLint's `Rule.RuleModule` type the same runtime object differently.
21
- ...Object.fromEntries(Object.entries({
22
- 'boolean-guard': booleanGuard,
23
- 'comment-style': commentStyle,
24
- 'effect-takes-callback': effectTakesCallback,
25
- 'jsdoc-for-props': jsdocForProps,
26
- 'no-callback-payload': noCallbackPayload,
27
- 'no-markup-variable': noMarkupVariable,
28
- 'no-unnecessary-use-callback': noUnnecessaryUseCallback,
29
- 'regex-pattern-const': regexPatternConst,
30
- 'test-id-default': testIdDefault,
31
- 'test-id-format': testIdFormat,
32
- 'test-id-from-props': testIdFromProps,
33
- 'undefined-check': undefinedCheck,
34
- 'webview-allow-list': webviewAllowList,
35
- })),
36
- },
37
- };
38
- export default plugin;
@@ -1,49 +0,0 @@
1
- import { AST_NODE_TYPES } from "@typescript-eslint/utils";
2
- import createRule from "../utils/create-rule.js";
3
- import getProperty from "../utils/get-property.js";
4
- const keysOf = (expression) => expression.properties.flatMap((property) => property.type === AST_NODE_TYPES.Property &&
5
- property.key.type === AST_NODE_TYPES.Identifier
6
- ? [property.key.name]
7
- : []);
8
- const accessibilityValueNeedsRange = createRule({
9
- name: "accessibility-value-needs-range",
10
- meta: {
11
- defaultOptions: [],
12
- type: "problem",
13
- docs: {
14
- description: "Enforce accessibilityValue carries a range, so a current value means something.",
15
- },
16
- messages: {
17
- valueWithoutRange: "'accessibilityValue' sets a current value with no scale to read it against, so assistive tech announces a bare number. Add 'min' and 'max', or a 'text' that says the value in words.",
18
- },
19
- schema: [],
20
- },
21
- create(context) {
22
- return {
23
- JSXOpeningElement(node) {
24
- const attribute = getProperty(node, "accessibilityValue");
25
- if (attribute?.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) {
26
- return;
27
- }
28
- const { expression } = attribute.value;
29
- // A value built elsewhere cannot be read here
30
- if (expression.type !== AST_NODE_TYPES.ObjectExpression) {
31
- return;
32
- }
33
- // A spread may carry the range, and cannot be read from here
34
- if (expression.properties.some((property) => property.type === AST_NODE_TYPES.SpreadElement)) {
35
- return;
36
- }
37
- const keys = keysOf(expression);
38
- if (!keys.includes("now") || keys.includes("text")) {
39
- return;
40
- }
41
- if (keys.includes("min") && keys.includes("max")) {
42
- return;
43
- }
44
- context.report({ node: attribute, messageId: "valueWithoutRange" });
45
- },
46
- };
47
- },
48
- });
49
- export default accessibilityValueNeedsRange;
@@ -1,49 +0,0 @@
1
- import createRule from "../utils/create-rule.js";
2
- import getLiteralPropertyValue from "../utils/get-literal-property-value.js";
3
- import getProperty from "../utils/get-property.js";
4
- /** WCAG 1.4.4 asks for text to resize up to 200%. */
5
- const MINIMUM_MULTIPLIER = 2;
6
- /** React Native reads `0` as "no maximum", which is the opposite of a cap. */
7
- const NO_MAXIMUM = 0;
8
- const noFontScalingDisabled = createRule({
9
- name: "no-font-scaling-disabled",
10
- meta: {
11
- defaultOptions: [],
12
- type: "problem",
13
- docs: {
14
- description: "Disallow opting text out of the system font scale below the level WCAG 1.4.4 requires.",
15
- },
16
- messages: {
17
- fontScalingDisabled: "'allowFontScaling={false}' stops this text respecting the system font size. Cap it with 'maxFontSizeMultiplier' instead if one element cannot grow.",
18
- fontScalingCapped: "'maxFontSizeMultiplier={{{ multiplier }}}' caps text below the 200% WCAG 1.4.4 requires. Reserve a cap under {{ minimum }} for chrome whose information is available elsewhere.",
19
- },
20
- schema: [],
21
- },
22
- create(context) {
23
- return {
24
- JSXOpeningElement(node) {
25
- const allowFontScaling = getProperty(node, "allowFontScaling");
26
- if (allowFontScaling &&
27
- getLiteralPropertyValue(node, "allowFontScaling") === false) {
28
- context.report({
29
- node: allowFontScaling,
30
- messageId: "fontScalingDisabled",
31
- });
32
- }
33
- const cap = getProperty(node, "maxFontSizeMultiplier");
34
- const multiplier = getLiteralPropertyValue(node, "maxFontSizeMultiplier");
35
- if (cap &&
36
- typeof multiplier === "number" &&
37
- multiplier !== NO_MAXIMUM &&
38
- multiplier < MINIMUM_MULTIPLIER) {
39
- context.report({
40
- node: cap,
41
- messageId: "fontScalingCapped",
42
- data: { multiplier, minimum: MINIMUM_MULTIPLIER },
43
- });
44
- }
45
- },
46
- };
47
- },
48
- });
49
- export default noFontScalingDisabled;
@@ -1,3 +0,0 @@
1
- import { ESLintUtils } from "@typescript-eslint/utils";
2
- const createRule = ESLintUtils.RuleCreator((name) => name);
3
- export default createRule;
@@ -1,5 +0,0 @@
1
- import { ESLintUtils } from "@typescript-eslint/utils";
2
- declare const createRule: <Options extends readonly unknown[], MessageIds extends string>({ meta, name, ...rule }: Readonly<ESLintUtils.RuleWithMetaAndName<Options, MessageIds, unknown>>) => ESLintUtils.RuleModule<MessageIds, Options, unknown, ESLintUtils.RuleListener> & {
3
- name: string;
4
- };
5
- export default createRule;