@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/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,42 @@
|
|
|
1
|
+
## [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)
|
|
2
|
+
|
|
3
|
+
### ⚠ BREAKING CHANGES
|
|
4
|
+
|
|
5
|
+
* **react:** react and react-native report new React Doctor errors.
|
|
6
|
+
* **base:** function declarations and expressions now fail lint.
|
|
7
|
+
* **base:** functions above a cognitive complexity of 15 now fail lint.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **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)
|
|
12
|
+
* **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)
|
|
13
|
+
* **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)
|
|
14
|
+
* **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)
|
|
15
|
+
|
|
16
|
+
## [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)
|
|
17
|
+
|
|
18
|
+
### ⚠ BREAKING CHANGES
|
|
19
|
+
|
|
20
|
+
* **react:** effect dependency findings now come from
|
|
21
|
+
react-hooks/exhaustive-effect-dependencies, so existing
|
|
22
|
+
eslint-disable comments for react-hooks/exhaustive-deps no longer apply.
|
|
23
|
+
* **react-native:** importing `Animated` from `react-native` is now an error in the react-native preset.
|
|
24
|
+
* **react-native:** react-native reports code it accepted before.
|
|
25
|
+
* **react:** react and react-native report code they accepted before.
|
|
26
|
+
* **base:** base reports code it accepted before.
|
|
27
|
+
|
|
28
|
+
### Features
|
|
29
|
+
|
|
30
|
+
* **base:** add tier 1 rules from the ESPN Goal pilot ([68d6391](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/commit/68d6391ce228e6b205fd3a7abc77b6bd179569d0)), closes [AB#5179](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_workitems/edit/5179)
|
|
31
|
+
* port boolean-guard and undefined-check rules ([79f50cc](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/commit/79f50ccf5bfb34ba1683b7d46947a423fbe56ee5))
|
|
32
|
+
* port comment-style, jsdoc-for-props and regex-pattern-const rules ([d039153](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/commit/d03915380754f723bf875cd9874519d291f7532b))
|
|
33
|
+
* port no-markup-variable, no-callback-payload, effect-takes-callback and no-unnecessary-use-callback rules ([dde97ee](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/commit/dde97ee3e6bf97c6337d0b660083acdaf32cc548))
|
|
34
|
+
* port test-id-format, test-id-from-props, test-id-default and webview-allow-list rules ([33d2660](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/commit/33d266031431fa1f37961974d91645c1df553acc))
|
|
35
|
+
* **react-native:** add tier 1 React Native rules from the ESPN Goal pilot ([e61ef0c](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/commit/e61ef0c550638e77b30128e9b776712714706bc8)), closes [AB#5179](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_workitems/edit/5179)
|
|
36
|
+
* **react-native:** ban Animated from react-native in favour of Reanimated ([19b6952](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/commit/19b6952dde8c462be851e6dd90744167374d80ea)), closes [AB#5179](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_workitems/edit/5179)
|
|
37
|
+
* **react:** add tier 1 React rules from the ESPN Goal pilot ([b9f9e58](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/commit/b9f9e58ebd6dc6d80454580ae3969daeafe5da2c)), closes [AB#5179](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_workitems/edit/5179)
|
|
38
|
+
* **react:** check dependency arrays once, with the compiler rules ([5502049](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/commit/5502049745c8e732b30426697325940d11ea44b4)), closes [AB#5179](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_workitems/edit/5179)
|
|
39
|
+
|
|
1
40
|
## [4.0.0](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/branchCompare?baseVersion=GTv3.0.0&targetVersion=GTv4.0.0) (2026-11-09)
|
|
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
|
|
@@ -90,13 +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
|
-
|
|
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
|
-
This package ships its own plugin, namespaced `@hypersolid-dev
|
|
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
|
-
| Rule | Enforces |
|
|
98
|
-
| --- | --- |
|
|
99
|
-
| `@hypersolid-dev/props-destructuring` | 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`). |
|
|
112
|
+
| Rule | Preset | Enforces |
|
|
113
|
+
| --- | --- | --- |
|
|
114
|
+
| `@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`). |
|
|
115
|
+
| `@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. |
|
|
116
|
+
| `@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. |
|
|
117
|
+
| `@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). |
|
|
118
|
+
| `@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`. |
|
|
119
|
+
| `@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. |
|
|
120
|
+
| `@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. |
|
|
121
|
+
| `@hypersolid-dev/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/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/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/test-id-format` | react-native | A `testID` is lowercase kebab-case. Bad: `testID="Header.Title"`. |
|
|
125
|
+
| `@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. |
|
|
126
|
+
| `@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)`. |
|
|
127
|
+
| `@hypersolid-dev/webview-allow-list` | react-native | A `react-native-webview` WebView sets `originWhitelist={['*']}` and filters every navigation in an always-present `onShouldStartLoadWithRequest`. |
|
|
100
128
|
|
|
101
129
|
## Overriding rules
|
|
102
130
|
|
|
@@ -137,7 +165,7 @@ export default [...base({ tsconfigRootDir: import.meta.dirname }), { ignores: ['
|
|
|
137
165
|
|
|
138
166
|
### Test files
|
|
139
167
|
|
|
140
|
-
Every preset relaxes a few rules for tests, stories, mocks and test setup (`TEST_FILES`, exported from `@hypersolid-dev/eslint-config/base`): `@typescript-eslint/no-unsafe-assignment`, `no-unsafe-member-access`, `no-invalid-void-type`, `unicorn/no-error-property-assignment` and `no-global-object-property-assignment` in `base`; `@eslint-react/no-unnecessary-use-prefix` in `react` and `react-native`; and the browser-globals list (`no-restricted-globals`) in `react-native`. Source code keeps them all.
|
|
168
|
+
Every preset relaxes a few rules for tests, stories, mocks and test setup (`TEST_FILES`, exported from `@hypersolid-dev/eslint-config/base`): `@typescript-eslint/no-unsafe-assignment`, `no-unsafe-member-access`, `no-invalid-void-type`, `unicorn/no-error-property-assignment` and `no-global-object-property-assignment` in `base`; `@eslint-react/no-unnecessary-use-prefix` in `react` and `react-native`; and the browser-globals list (`no-restricted-globals`) in `react-native`. `unicorn/consistent-function-scoping` and the source-only custom rules marked "Off in tests" above are also off there. Source code keeps them all.
|
|
141
169
|
|
|
142
170
|
```js
|
|
143
171
|
import base, { TEST_FILES } from '@hypersolid-dev/eslint-config/base';
|
|
@@ -149,7 +177,25 @@ export default [...base({ tsconfigRootDir: import.meta.dirname }), { files: TEST
|
|
|
149
177
|
|
|
150
178
|
- `unicorn/consistent-boolean-name` skips function names in every preset; `react` and `react-native` also skip hook (`^use[A-Z]`) and jest mock (`^mock`) names, and `react-native` reads Reanimated's `SharedValue` through `.value`.
|
|
151
179
|
- `no-secrets/no-secrets` ignores `data:` URIs in JSON; in `react-native` it also ignores Expo and native identifiers (`EXPO_*`, `android.settings.*`, `@xml…`, `NSPrivacy*`, i18n `Namespace/Key` paths) in code and JSON.
|
|
152
|
-
-
|
|
180
|
+
- `no-restricted-syntax` bans an empty `.catch(() => {})` and `{ default as x }` specifiers in `base`; `react` adds `"use no memo"`, hooks called inside JSX and `React.useX`; `react-native` adds `pointerEvents` as a prop. Each preset restates the earlier entries, because flat config replaces options; spread them the same way when you add your own.
|
|
181
|
+
- `@typescript-eslint/naming-convention`: values camelCase, PascalCase or UPPER_CASE, functions camelCase or PascalCase, types PascalCase; destructured names are exempt. `react` adds that a boolean prop doesn't start with `show`, `display` or `hide`.
|
|
182
|
+
- `@typescript-eslint/no-restricted-imports` bans `InteractionManager` (deprecated in the New Architecture) and `Animated` (runs on the JS thread; use react-native-reanimated) from `react-native`. Type imports are allowed.
|
|
183
|
+
- `@typescript-eslint/switch-exhaustiveness-check` requires a `default` on a switch over a non-union value (`requireDefaultForNonUnion`), and accepts a `default` branch as exhaustive (`considerDefaultExhaustiveForUnions`). Set it to `false` to make every new union member fail its switches.
|
|
184
|
+
|
|
185
|
+
- `react-hooks/exhaustive-deps` is off: the compiler rules `react-hooks/memo-dependencies` and `react-hooks/exhaustive-effect-dependencies` check the same arrays, and both on would report one missing dependency twice. `eslint-plugin-react-hooks` is pinned to an exact version, so a plugin release can't change what the compiler rules report without a release of this package.
|
|
186
|
+
|
|
187
|
+
### Component folders
|
|
188
|
+
|
|
189
|
+
`@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:
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
export default [
|
|
193
|
+
...base({ tsconfigRootDir: import.meta.dirname }),
|
|
194
|
+
...reactNative(),
|
|
195
|
+
{ files: ['**/components/**/*.tsx'], rules: { '@hypersolid-dev/test-id-default': 'off' } },
|
|
196
|
+
{ files: ['src/ui/**/*.tsx'], rules: { '@hypersolid-dev/test-id-default': 'error' } },
|
|
197
|
+
];
|
|
198
|
+
```
|
|
153
199
|
|
|
154
200
|
## Formatting and import order
|
|
155
201
|
|
|
@@ -162,6 +208,7 @@ Root files such as `eslint.config.js` or `vite.config.ts` are often outside the
|
|
|
162
208
|
## Known issues
|
|
163
209
|
|
|
164
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.
|
|
165
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:
|
|
166
213
|
|
|
167
214
|
```js
|
package/dist/base.js
CHANGED
|
@@ -3,9 +3,12 @@ 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
|
|
10
|
+
import hypersolid from './plugins/hypersolid.js';
|
|
11
|
+
import { BASE_RESTRICTED_SYNTAX, BOOLEAN_NAME_OPTIONS, CODE_FILES, NAMING_CONVENTION, NUMBER_GLOBAL_RESTRICTIONS, raiseWarnings, scopeToCode, TEST_FILES, } from './shared.js';
|
|
9
12
|
// The glob the presets relax test-only rules for; reuse it for your own test overrides.
|
|
10
13
|
export { TEST_FILES } from './shared.js';
|
|
11
14
|
export default function base(options) {
|
|
@@ -22,9 +25,11 @@ export default function base(options) {
|
|
|
22
25
|
js.configs.recommended,
|
|
23
26
|
// Type-aware TypeScript rules, the strict set. Bad: `const n: any = data; n.foo()`, `if (alwaysTrue)`.
|
|
24
27
|
...tsConfigs.strictTypeChecked,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Unicorn's recommended set minus its taste rules (file naming, `no-null`, `no-array-reduce`,
|
|
30
|
+
* abbreviations, formatting); what remains catches bugs and outdated APIs.
|
|
31
|
+
* Bad: `array.indexOf(x) !== -1` (use `includes`), `new Error()` without a message.
|
|
32
|
+
*/
|
|
28
33
|
unicorn.configs.unopinionated,
|
|
29
34
|
// Import mistakes. Bad: `import { missing } from './module.js'`, importing the same module twice.
|
|
30
35
|
importX.recommended,
|
|
@@ -41,7 +46,12 @@ export default function base(options) {
|
|
|
41
46
|
tsconfigRootDir: options.tsconfigRootDir,
|
|
42
47
|
},
|
|
43
48
|
},
|
|
44
|
-
plugins: {
|
|
49
|
+
plugins: {
|
|
50
|
+
'@hypersolid-dev': hypersolid,
|
|
51
|
+
'no-secrets': noSecrets,
|
|
52
|
+
'prefer-arrow-functions': preferArrowFunctions,
|
|
53
|
+
sonarjs,
|
|
54
|
+
},
|
|
45
55
|
settings: {
|
|
46
56
|
'import-x/resolver-next': [createTypeScriptImportResolver(), createNodeResolver()],
|
|
47
57
|
},
|
|
@@ -56,10 +66,22 @@ export default function base(options) {
|
|
|
56
66
|
'no-nested-ternary': 'error',
|
|
57
67
|
// Arrow functions as callbacks. Bad: `list.map(function (x) { return x; })`.
|
|
58
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 }],
|
|
59
71
|
// A rethrow keeps the original error as `cause`. Bad: `catch (error) { throw new Error('failed'); }`.
|
|
60
72
|
'preserve-caught-error': 'error',
|
|
61
73
|
// The coercing globals. Bad: `isNaN('x')` (use `Number.isNaN`).
|
|
62
74
|
'no-restricted-globals': ['error', ...NUMBER_GLOBAL_RESTRICTIONS],
|
|
75
|
+
// The same globals reached through an object, and `Math.pow`. Bad: `globalThis.isNaN(x)`, `Math.pow(2, 8)` (use `2 ** 8`).
|
|
76
|
+
'no-restricted-properties': [
|
|
77
|
+
'error',
|
|
78
|
+
...['global', 'globalThis'].flatMap((object) => NUMBER_GLOBAL_RESTRICTIONS.map(({ name, message }) => ({ object, property: name, message }))),
|
|
79
|
+
{ object: 'Math', property: 'pow', message: 'Use the `**` operator.' },
|
|
80
|
+
],
|
|
81
|
+
// A silent catch and renamed default specifiers. Bad: `.catch(() => {})`, `import { default as user } from './user.js'`.
|
|
82
|
+
'no-restricted-syntax': ['error', ...BASE_RESTRICTED_SYNTAX],
|
|
83
|
+
// `let` only for a reassigned binding, ignoring one read in a closure before its assignment. Bad: `let total = 1` never reassigned.
|
|
84
|
+
'prefer-const': ['error', { destructuring: 'any', ignoreReadBeforeAssign: true }],
|
|
63
85
|
// `type` over `interface`. Bad: `interface User { name: string }`.
|
|
64
86
|
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
|
|
65
87
|
// Every promise is awaited or handled; `void` does not count. Bad: `save();`, `void save();`.
|
|
@@ -79,8 +101,22 @@ export default function base(options) {
|
|
|
79
101
|
],
|
|
80
102
|
// No value returned where `void` is expected. Bad: `run(() => 1)` with `run(callback: () => void)`.
|
|
81
103
|
'@typescript-eslint/strict-void-return': ['error', { allowReturnAny: true }],
|
|
82
|
-
|
|
83
|
-
|
|
104
|
+
/**
|
|
105
|
+
* A `switch` on a union handles every member or has a `default`. Bad: `switch (shape) { case 'circle': … }` missing `'square'`.
|
|
106
|
+
* and a `switch` on a non-union value has a `default`. Bad: `switch (count) { case 0: … }` without `default`.
|
|
107
|
+
*/
|
|
108
|
+
'@typescript-eslint/switch-exhaustiveness-check': [
|
|
109
|
+
'error',
|
|
110
|
+
{ considerDefaultExhaustiveForUnions: true, requireDefaultForNonUnion: true },
|
|
111
|
+
],
|
|
112
|
+
// Rest siblings omit a key, and `_` skips an argument. Bad: `const unused = 1`.
|
|
113
|
+
'@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true, argsIgnorePattern: '^_+$' }],
|
|
114
|
+
// A type the literal already says. Bad: `const count: number = 5`.
|
|
115
|
+
'@typescript-eslint/no-inferrable-types': 'error',
|
|
116
|
+
// `startsWith`/`endsWith` state the intent. Bad: `name.indexOf('use') === 0`.
|
|
117
|
+
'@typescript-eslint/prefer-string-starts-ends-with': 'error',
|
|
118
|
+
// Values camelCase, PascalCase or UPPER_CASE; functions camelCase or PascalCase; types PascalCase. Bad: `const user_name = ''`, `type props = {}`.
|
|
119
|
+
'@typescript-eslint/naming-convention': ['error', ...NAMING_CONVENTION],
|
|
84
120
|
// The caught value is named `error`. Bad: `catch (e)`.
|
|
85
121
|
'unicorn/catch-error-name': 'error',
|
|
86
122
|
// A boolean reads as a claim. Bad: `const visible = true` (use `isVisible`).
|
|
@@ -108,6 +144,20 @@ export default function base(options) {
|
|
|
108
144
|
'unicorn/no-unsafe-promise-all-settled-values': 'error',
|
|
109
145
|
// An async function already wraps its return value. Bad: `async () => Promise.resolve(1)`.
|
|
110
146
|
'unicorn/no-useless-promise-resolve-reject': 'error',
|
|
147
|
+
// No cleartext URLs. Bad: a cleartext `http` URL in a `fetch`.
|
|
148
|
+
'unicorn/prefer-https': 'error',
|
|
149
|
+
// `.catch()` over a second `.then()` argument. Bad: `promise.then(render, report)`.
|
|
150
|
+
'unicorn/prefer-then-catch': 'error',
|
|
151
|
+
// A helper that closes over nothing belongs at module level. Bad: `function outer() { const double = (x) => x * 2; }`.
|
|
152
|
+
'unicorn/consistent-function-scoping': 'error',
|
|
153
|
+
// One escape style for `${` in template literals. Bad: `` `\${a} $\{b}` ``.
|
|
154
|
+
'unicorn/consistent-template-literal-escape': 'error',
|
|
155
|
+
// `x !== undefined` where truthiness already decides. Bad: `if (user !== undefined)` with `user: User | undefined`.
|
|
156
|
+
'@hypersolid-dev/undefined-check': 'error',
|
|
157
|
+
// A regex is a module-level `*_PATTERN` constant, named for what it matches. Bad: `if (/^\d+$/.test(id))` in a function.
|
|
158
|
+
'@hypersolid-dev/regex-pattern-const': 'error',
|
|
159
|
+
// `//` and `/* */` comments stay one row; longer prose is a `/** */` block. Bad: three `//` lines in a row.
|
|
160
|
+
'@hypersolid-dev/comment-style': 'error',
|
|
111
161
|
// `undefined` as an argument is often required by the callee's signature. Bad: `let value = undefined`.
|
|
112
162
|
'unicorn/no-useless-undefined': ['error', { checkArguments: false }],
|
|
113
163
|
// Off: `.then()` chains are fine. Bad: `fetchUser().then(render)` reported.
|
|
@@ -142,12 +192,16 @@ export default function base(options) {
|
|
|
142
192
|
'import-x/namespace': 'off',
|
|
143
193
|
// High-entropy strings that look like credentials. Bad: `const token = 'ghp_Zx8kQ2vLm9…'`.
|
|
144
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',
|
|
145
197
|
},
|
|
146
198
|
},
|
|
147
199
|
{
|
|
148
200
|
name: '@hypersolid-dev/base/tests',
|
|
149
201
|
files: TEST_FILES,
|
|
150
202
|
rules: {
|
|
203
|
+
// A test's setup and table of cases are read once, not maintained as logic.
|
|
204
|
+
'sonarjs/cognitive-complexity': 'off',
|
|
151
205
|
// Off: mocks and fixtures are loosely typed by design. Bad: `const user = jest.fn()()` reported.
|
|
152
206
|
'@typescript-eslint/no-unsafe-assignment': 'off',
|
|
153
207
|
// Off: mocks and fixtures are loosely typed by design. Bad: `mock.calls[0].user` reported.
|
|
@@ -158,6 +212,10 @@ export default function base(options) {
|
|
|
158
212
|
'unicorn/no-error-property-assignment': 'off',
|
|
159
213
|
// Off: test setup stubs globals. Bad: `globalThis.fetch = mockFetch` reported.
|
|
160
214
|
'unicorn/no-global-object-property-assignment': 'off',
|
|
215
|
+
// Off: helpers inside a `describe` keep each suite readable on its own. Bad: `const render = () => …` in a `describe` reported.
|
|
216
|
+
'unicorn/consistent-function-scoping': 'off',
|
|
217
|
+
// Off: a test keeps its inline `toMatch(/…/)` literal. Bad: `expect(id).toMatch(/^\d+$/)` reported.
|
|
218
|
+
'@hypersolid-dev/regex-pattern-const': 'off',
|
|
161
219
|
},
|
|
162
220
|
},
|
|
163
221
|
{
|
package/dist/jest.d.ts
ADDED
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
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
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,8 +1,9 @@
|
|
|
1
1
|
const FUNCTION_TYPES = new Set(['ArrowFunctionExpression', 'FunctionExpression', 'FunctionDeclaration']);
|
|
2
2
|
const WRAPPERS = new Set(['memo', 'forwardRef']);
|
|
3
3
|
const isFunction = (node) => typeof node === 'object' && node !== null && FUNCTION_TYPES.has(node.type);
|
|
4
|
-
const
|
|
5
|
-
|
|
4
|
+
const PASCAL_CASE_PATTERN = /^[A-Z]/;
|
|
5
|
+
const isPascalCase = (name) => typeof name === 'string' && PASCAL_CASE_PATTERN.test(name);
|
|
6
|
+
const isJsx = (node) => {
|
|
6
7
|
if (typeof node !== 'object' || node === null) {
|
|
7
8
|
return false;
|
|
8
9
|
}
|
|
@@ -14,22 +15,22 @@ function isJsx(node) {
|
|
|
14
15
|
return isJsx(consequent) || isJsx(alternate);
|
|
15
16
|
}
|
|
16
17
|
return type === 'LogicalExpression' && (isJsx(left) || isJsx(right));
|
|
17
|
-
}
|
|
18
|
+
};
|
|
18
19
|
// Searches the function's own return statements; nested functions return for themselves.
|
|
19
|
-
|
|
20
|
+
const isReturningJsx = (node) => {
|
|
20
21
|
if (typeof node !== 'object' || node === null) {
|
|
21
22
|
return false;
|
|
22
23
|
}
|
|
23
24
|
if (Array.isArray(node)) {
|
|
24
|
-
return node.some(
|
|
25
|
+
return node.some(isReturningJsx);
|
|
25
26
|
}
|
|
26
27
|
const current = node;
|
|
27
28
|
return current.type === 'ReturnStatement'
|
|
28
29
|
? isJsx(current.argument)
|
|
29
|
-
: Object.entries(current).some(([key, value]) => key !== 'parent' && !isFunction(value) && typeof value === 'object' &&
|
|
30
|
-
}
|
|
30
|
+
: Object.entries(current).some(([key, value]) => key !== 'parent' && !isFunction(value) && typeof value === 'object' && isReturningJsx(value));
|
|
31
|
+
};
|
|
31
32
|
// `memo(fn)`, `React.forwardRef(fn)` and `memo(forwardRef(fn))`.
|
|
32
|
-
|
|
33
|
+
const unwrap = (node) => {
|
|
33
34
|
if (isFunction(node)) {
|
|
34
35
|
return node;
|
|
35
36
|
}
|
|
@@ -40,7 +41,7 @@ function unwrap(node) {
|
|
|
40
41
|
const { type: calleeType, name, property } = callee;
|
|
41
42
|
const calleeName = calleeType === 'MemberExpression' ? property.name : name;
|
|
42
43
|
return typeof calleeName === 'string' && WRAPPERS.has(calleeName) ? unwrap(args[0]) : undefined;
|
|
43
|
-
}
|
|
44
|
+
};
|
|
44
45
|
const propsDestructuring = {
|
|
45
46
|
meta: {
|
|
46
47
|
type: 'suggestion',
|
|
@@ -50,7 +51,7 @@ const propsDestructuring = {
|
|
|
50
51
|
destructure: 'Destructure the props in the signature: `({ title }: Props)`. A discriminated-union props type may keep `props`.',
|
|
51
52
|
},
|
|
52
53
|
},
|
|
53
|
-
create(context) {
|
|
54
|
+
create: (context) => {
|
|
54
55
|
const services = context.sourceCode.parserServices;
|
|
55
56
|
const checker = services.program?.getTypeChecker();
|
|
56
57
|
// Destructuring a union in the signature breaks narrowing (`if (props.kind === 'a') props.aOnly`).
|
|
@@ -60,18 +61,18 @@ const propsDestructuring = {
|
|
|
60
61
|
};
|
|
61
62
|
const check = (fn) => {
|
|
62
63
|
const props = fn?.params[0];
|
|
63
|
-
if (!fn || props?.type !== 'Identifier' || !
|
|
64
|
+
if (!fn || props?.type !== 'Identifier' || !isReturningJsx(fn.body) && !isJsx(fn.body) || isUnion(props)) {
|
|
64
65
|
return;
|
|
65
66
|
}
|
|
66
67
|
context.report({ node: props, messageId: 'destructure' });
|
|
67
68
|
};
|
|
68
69
|
return {
|
|
69
|
-
FunctionDeclaration(node) {
|
|
70
|
+
FunctionDeclaration: (node) => {
|
|
70
71
|
if (isPascalCase(node.id.name)) {
|
|
71
72
|
check(node);
|
|
72
73
|
}
|
|
73
74
|
},
|
|
74
|
-
VariableDeclarator(node) {
|
|
75
|
+
VariableDeclarator: (node) => {
|
|
75
76
|
if (node.id.type === 'Identifier' && isPascalCase(node.id.name)) {
|
|
76
77
|
check(unwrap(node.init));
|
|
77
78
|
}
|
|
@@ -38,8 +38,10 @@ const rules = {
|
|
|
38
38
|
"require-invert-colors-opt-out": requireInvertColorsOptOut,
|
|
39
39
|
"require-modal-a11y-on-sheets": requireModalA11yOnSheets,
|
|
40
40
|
};
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
/**
|
|
42
|
+
* React Native accessibility rules, maintained in-house. Originally @hypersolid-dev/eslint-config (react-native preset) v2.2.0 (MIT), from
|
|
43
|
+
* before Triple became Hypersolid. Rule docs live in ./docs.
|
|
44
|
+
*/
|
|
43
45
|
const plugin = {
|
|
44
46
|
meta: { name: "@hypersolid-dev/react-native-a11y" },
|
|
45
47
|
rules,
|
|
@@ -26,7 +26,7 @@ const accessibilityActionsNeedHandler = createRule({
|
|
|
26
26
|
},
|
|
27
27
|
schema: [],
|
|
28
28
|
},
|
|
29
|
-
create(context) {
|
|
29
|
+
create: (context) => {
|
|
30
30
|
const reportUnlabelledActions = (expression) => {
|
|
31
31
|
if (expression.type !== AST_NODE_TYPES.ArrayExpression) {
|
|
32
32
|
return;
|
|
@@ -54,7 +54,7 @@ const accessibilityActionsNeedHandler = createRule({
|
|
|
54
54
|
}
|
|
55
55
|
};
|
|
56
56
|
return {
|
|
57
|
-
JSXOpeningElement(node) {
|
|
57
|
+
JSXOpeningElement: (node) => {
|
|
58
58
|
if (hasSpreadProps(node)) {
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
@@ -33,13 +33,13 @@ const accessibilityHint = createRule({
|
|
|
33
33
|
},
|
|
34
34
|
],
|
|
35
35
|
},
|
|
36
|
-
create(context) {
|
|
36
|
+
create: (context) => {
|
|
37
37
|
const components = getComponents({
|
|
38
38
|
context,
|
|
39
39
|
defaultComponents: TOUCHABLES,
|
|
40
40
|
});
|
|
41
41
|
return {
|
|
42
|
-
JSXOpeningElement(node) {
|
|
42
|
+
JSXOpeningElement: (node) => {
|
|
43
43
|
const componentName = getElementName(node);
|
|
44
44
|
if (componentName === undefined) {
|
|
45
45
|
return;
|
|
@@ -54,8 +54,10 @@ const accessibilityHint = createRule({
|
|
|
54
54
|
isInsideFocusStop(node)) {
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Visible text already names the control, so the hint has a name to
|
|
59
|
+
* hang off even without a label
|
|
60
|
+
*/
|
|
59
61
|
const { children } = node.parent;
|
|
60
62
|
if (hasLabelProperty(node) || getAmountOfTextChildren(children) > 0) {
|
|
61
63
|
return;
|
|
@@ -35,13 +35,13 @@ const accessibilityLabel = createRule({
|
|
|
35
35
|
},
|
|
36
36
|
],
|
|
37
37
|
},
|
|
38
|
-
create(context) {
|
|
38
|
+
create: (context) => {
|
|
39
39
|
const components = getComponents({
|
|
40
40
|
context,
|
|
41
41
|
defaultComponents: [...IMAGES, ...TOUCHABLES, ...INPUTS],
|
|
42
42
|
});
|
|
43
43
|
return {
|
|
44
|
-
JSXOpeningElement(node) {
|
|
44
|
+
JSXOpeningElement: (node) => {
|
|
45
45
|
const componentName = getElementName(node);
|
|
46
46
|
if (componentName === undefined) {
|
|
47
47
|
return;
|
|
@@ -49,8 +49,10 @@ const accessibilityLabel = createRule({
|
|
|
49
49
|
if (!components.includes(componentName)) {
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Decorative content is hidden rather than labelled
|
|
54
|
+
* The surface above answers for its children
|
|
55
|
+
*/
|
|
54
56
|
if (isHiddenFromAccessibility(node) ||
|
|
55
57
|
isNotAFocusStop(node) ||
|
|
56
58
|
isInsideFocusStop(node)) {
|
|
@@ -73,8 +75,10 @@ const accessibilityLabel = createRule({
|
|
|
73
75
|
// Visible text already names the control
|
|
74
76
|
const { children } = node.parent;
|
|
75
77
|
const amountOfTextChildren = getAmountOfTextChildren(children);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
/**
|
|
79
|
+
* 2.5.3: a spoken command matches the accessible name, so the name
|
|
80
|
+
* has to contain what the user can read
|
|
81
|
+
*/
|
|
78
82
|
const label = getLiteralLabel(node);
|
|
79
83
|
const visible = getLiteralTextChild(children);
|
|
80
84
|
if (label !== undefined &&
|
|
@@ -31,13 +31,13 @@ const accessibilityRole = createRule({
|
|
|
31
31
|
},
|
|
32
32
|
],
|
|
33
33
|
},
|
|
34
|
-
create(context) {
|
|
34
|
+
create: (context) => {
|
|
35
35
|
const components = getComponents({
|
|
36
36
|
context,
|
|
37
37
|
defaultComponents: [...TOUCHABLES, ...IMAGES],
|
|
38
38
|
});
|
|
39
39
|
return {
|
|
40
|
-
JSXOpeningElement(node) {
|
|
40
|
+
JSXOpeningElement: (node) => {
|
|
41
41
|
const componentName = getElementName(node);
|
|
42
42
|
if (componentName === undefined) {
|
|
43
43
|
return;
|
|
@@ -45,8 +45,10 @@ const accessibilityRole = createRule({
|
|
|
45
45
|
if (!components.includes(componentName)) {
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Decorative content is hidden rather than given a role
|
|
50
|
+
* The surface above answers for its children
|
|
51
|
+
*/
|
|
50
52
|
if (isHiddenFromAccessibility(node) ||
|
|
51
53
|
isNotAFocusStop(node) ||
|
|
52
54
|
isInsideFocusStop(node)) {
|