@hypersolid-dev/eslint-config 4.0.0 → 6.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.
- package/CHANGELOG.md +39 -0
- package/README.md +54 -7
- package/dist/base.js +65 -7
- package/dist/jest.d.ts +3 -0
- package/dist/jest.js +56 -0
- package/dist/plugins/hypersolid.d.ts +3 -0
- package/dist/plugins/hypersolid.js +38 -0
- package/dist/plugins/props-destructuring.js +14 -13
- package/dist/plugins/react-native-a11y/index.js +4 -2
- package/dist/plugins/react-native-a11y/rules/accessibility-actions-need-handler.js +2 -2
- package/dist/plugins/react-native-a11y/rules/accessibility-hint.js +6 -4
- package/dist/plugins/react-native-a11y/rules/accessibility-label.js +10 -6
- package/dist/plugins/react-native-a11y/rules/accessibility-role.js +6 -4
- package/dist/plugins/react-native-a11y/rules/accessibility-value-needs-range.js +25 -27
- package/dist/plugins/react-native-a11y/rules/adjustable-needs-value.js +16 -18
- package/dist/plugins/react-native-a11y/rules/gate-announce-for-accessibility.js +14 -10
- package/dist/plugins/react-native-a11y/rules/input-purpose.js +10 -6
- package/dist/plugins/react-native-a11y/rules/no-aria-legacy-mix.js +20 -22
- package/dist/plugins/react-native-a11y/rules/no-deprecated-accessibility-focus.js +12 -14
- package/dist/plugins/react-native-a11y/rules/no-dropped-aria-on-text.js +2 -2
- package/dist/plugins/react-native-a11y/rules/no-font-scaling-disabled.js +24 -26
- package/dist/plugins/react-native-a11y/rules/no-ineffective-accessibility-props.js +34 -36
- package/dist/plugins/react-native-a11y/rules/no-invalid-accessibility-state.js +31 -33
- package/dist/plugins/react-native-a11y/rules/no-nested-focus-stops.js +2 -2
- package/dist/plugins/react-native-a11y/rules/no-press-in-activation.js +2 -2
- package/dist/plugins/react-native-a11y/rules/no-unnamed-interactive.js +6 -4
- package/dist/plugins/react-native-a11y/rules/require-invert-colors-opt-out.js +8 -6
- package/dist/plugins/react-native-a11y/rules/require-modal-a11y-on-sheets.js +7 -5
- package/dist/plugins/react-native-a11y/utils/get-amount-of-text-children.js +19 -13
- package/dist/plugins/react-native-a11y/utils/get-element-name.js +1 -1
- package/dist/plugins/rules/boolean-guard.d.ts +30 -0
- package/dist/plugins/rules/boolean-guard.js +143 -0
- package/dist/plugins/rules/comment-style.d.ts +26 -0
- package/dist/plugins/rules/comment-style.js +167 -0
- package/dist/plugins/rules/effect-takes-callback.d.ts +20 -0
- package/dist/plugins/rules/effect-takes-callback.js +106 -0
- package/dist/plugins/rules/jsdoc-for-props.d.ts +18 -0
- package/dist/plugins/rules/jsdoc-for-props.js +57 -0
- package/dist/plugins/rules/no-callback-payload.d.ts +21 -0
- package/dist/plugins/rules/no-callback-payload.js +217 -0
- package/dist/plugins/rules/no-markup-variable.d.ts +22 -0
- package/dist/plugins/rules/no-markup-variable.js +123 -0
- package/dist/plugins/rules/no-unnecessary-use-callback.d.ts +24 -0
- package/dist/plugins/rules/no-unnecessary-use-callback.js +117 -0
- package/dist/plugins/rules/regex-pattern-const.d.ts +19 -0
- package/dist/plugins/rules/regex-pattern-const.js +82 -0
- package/dist/plugins/rules/test-id-default.d.ts +4 -0
- package/dist/plugins/rules/test-id-default.js +182 -0
- package/dist/plugins/rules/test-id-format.d.ts +4 -0
- package/dist/plugins/rules/test-id-format.js +118 -0
- package/dist/plugins/rules/test-id-from-props.d.ts +4 -0
- package/dist/plugins/rules/test-id-from-props.js +90 -0
- package/dist/plugins/rules/undefined-check.d.ts +5 -0
- package/dist/plugins/rules/undefined-check.js +64 -0
- package/dist/plugins/rules/webview-allow-list.d.ts +5 -0
- package/dist/plugins/rules/webview-allow-list.js +105 -0
- package/dist/plugins/utils/comments.d.ts +15 -0
- package/dist/plugins/utils/comments.js +15 -0
- package/dist/plugins/utils/component-name.d.ts +11 -0
- package/dist/plugins/utils/component-name.js +32 -0
- package/dist/plugins/utils/create-rule.d.ts +5 -0
- package/dist/plugins/utils/create-rule.js +4 -0
- package/dist/plugins/utils/expressions.d.ts +52 -0
- package/dist/plugins/utils/expressions.js +290 -0
- package/dist/plugins/utils/types.d.ts +20 -0
- package/dist/plugins/utils/types.js +91 -0
- package/dist/react-native.js +93 -12
- package/dist/shared.d.ts +22 -3
- package/dist/shared.js +202 -75
- package/package.json +13 -3
package/dist/shared.d.ts
CHANGED
|
@@ -12,6 +12,25 @@ export declare const NUMBER_GLOBAL_RESTRICTIONS: {
|
|
|
12
12
|
name: string;
|
|
13
13
|
message: string;
|
|
14
14
|
}[];
|
|
15
|
-
|
|
16
|
-
export declare
|
|
17
|
-
|
|
15
|
+
/** `no-restricted-syntax` entries per preset; a later config replaces the options, so each preset spreads the ones before it. */
|
|
16
|
+
export declare const BASE_RESTRICTED_SYNTAX: {
|
|
17
|
+
selector: string;
|
|
18
|
+
message: string;
|
|
19
|
+
}[];
|
|
20
|
+
export declare const REACT_RESTRICTED_SYNTAX: {
|
|
21
|
+
selector: string;
|
|
22
|
+
message: string;
|
|
23
|
+
}[];
|
|
24
|
+
/** `@typescript-eslint/naming-convention` entries per preset, for the same reason. Destructured names come from elsewhere (`const { user_id } = payload`). */
|
|
25
|
+
export declare const NAMING_CONVENTION: ({
|
|
26
|
+
selector: string;
|
|
27
|
+
modifiers: string[];
|
|
28
|
+
format: null;
|
|
29
|
+
} | {
|
|
30
|
+
selector: string;
|
|
31
|
+
format: string[];
|
|
32
|
+
modifiers?: undefined;
|
|
33
|
+
})[];
|
|
34
|
+
export declare const raiseWarnings: (configs: Linter.Config[]) => Linter.Config[];
|
|
35
|
+
export declare const scopeToCode: (configs: readonly Linter.Config[]) => Linter.Config[];
|
|
36
|
+
export declare const reactConfigs: () => Linter.Config[];
|
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/
|
|
5
|
+
import hypersolid from './plugins/hypersolid.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 = [
|
|
@@ -18,82 +19,208 @@ export const NUMBER_GLOBAL_RESTRICTIONS = ['isFinite', 'isNaN'].map((name) => ({
|
|
|
18
19
|
name,
|
|
19
20
|
message: `Use Number.${name}; the global coerces its argument.`,
|
|
20
21
|
}));
|
|
22
|
+
/** `no-restricted-syntax` entries per preset; a later config replaces the options, so each preset spreads the ones before it. */
|
|
23
|
+
export const BASE_RESTRICTED_SYNTAX = [
|
|
24
|
+
{
|
|
25
|
+
selector: "CallExpression[callee.property.name='catch'] > :matches(ArrowFunctionExpression, FunctionExpression).arguments:matches([body.type='Identifier'][body.name='undefined'], [body.type='Literal'][body.value=null], [body.type='BlockStatement'][body.body.length=0], [body.type='UnaryExpression'][body.operator='void'])",
|
|
26
|
+
message: 'An empty catch hides the failure. Log it, recover from it, or drop the catch when the promise cannot reject.',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
selector: ":matches(ExportSpecifier[local.name='default'], ImportSpecifier[imported.name='default'])",
|
|
30
|
+
message: 'Import a default as a default (`import name from "…"`), then `export { name };` if it has to be re-exported.',
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
export const REACT_RESTRICTED_SYNTAX = [
|
|
34
|
+
...BASE_RESTRICTED_SYNTAX,
|
|
35
|
+
{
|
|
36
|
+
selector: 'ExpressionStatement > Literal[value=/^use no (memo|forget)$/]',
|
|
37
|
+
message: 'Leave this component in the React Compiler; opting a subtree out is a performance decision that needs a reviewed reason.',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
selector: ":matches(JSXExpressionContainer, JSXSpreadAttribute) CallExpression[callee.type='Identifier'][callee.name=/^use([A-Z0-9]|$)/]:not(Property[key.name=/^[A-Z]/] > ArrowFunctionExpression CallExpression)",
|
|
41
|
+
message: 'Call this hook at the top of the component and pass its result: `const fade = useAnimatedStyle(…)`, then `style={fade}`.',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
selector: "MemberExpression[object.name='React'][property.name=/^use/]",
|
|
45
|
+
message: "Import React hooks from 'react' (`import { useEffect } from 'react'`) instead of `React.use*`.",
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
/** `@typescript-eslint/naming-convention` entries per preset, for the same reason. Destructured names come from elsewhere (`const { user_id } = payload`). */
|
|
49
|
+
export const NAMING_CONVENTION = [
|
|
50
|
+
{ selector: 'variable', modifiers: ['destructured'], format: null },
|
|
51
|
+
{ selector: 'variable', format: ['camelCase', 'PascalCase', 'UPPER_CASE'] },
|
|
52
|
+
{ selector: 'function', format: ['camelCase', 'PascalCase'] },
|
|
53
|
+
{ selector: 'typeLike', format: ['PascalCase'] },
|
|
54
|
+
];
|
|
21
55
|
// Every enabled rule is an error: a warning is either worth fixing or not worth reporting.
|
|
22
|
-
export
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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;
|
|
26
64
|
}
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
rules[name] = severity === 'warn' || severity === 1 ? ['error', ...ruleOptions] : entry;
|
|
34
|
-
}
|
|
35
|
-
return { ...config, rules };
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
export function scopeToCode(configs) {
|
|
39
|
-
return configs.map((config) => ({ files: CODE_FILES, ...config }));
|
|
40
|
-
}
|
|
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 }));
|
|
41
71
|
// The React block shared by `react` and `react-native`.
|
|
42
|
-
export
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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': hypersolid, 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/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/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/no-markup-variable': 'error',
|
|
141
|
+
// A `use*Effect` hook takes its callback first, like `useEffect`. Bad: `useFocusEffect(deps, () => …)`.
|
|
142
|
+
'@hypersolid-dev/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/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/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/jsdoc-for-props': 'error',
|
|
149
|
+
},
|
|
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',
|
|
89
210
|
},
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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/boolean-guard': 'off',
|
|
218
|
+
'@hypersolid-dev/no-markup-variable': 'off',
|
|
219
|
+
'@hypersolid-dev/effect-takes-callback': 'off',
|
|
220
|
+
'@hypersolid-dev/no-unnecessary-use-callback': 'off',
|
|
221
|
+
'@hypersolid-dev/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',
|
|
97
224
|
},
|
|
98
|
-
|
|
99
|
-
|
|
225
|
+
},
|
|
226
|
+
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypersolid-dev/eslint-config",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.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",
|
|
80
|
-
"eslint-plugin-
|
|
86
|
+
"eslint-plugin-prefer-arrow-functions": "^3.10.2",
|
|
87
|
+
"eslint-plugin-react-doctor": "0.9.22",
|
|
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"
|