@hypersolid-dev/eslint-config 3.0.0 → 5.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 +34 -0
- package/README.md +37 -6
- package/dist/base.js +52 -7
- package/dist/plugins/hypersolid.d.ts +3 -0
- package/dist/plugins/hypersolid.js +38 -0
- package/dist/plugins/props-destructuring.js +2 -1
- package/dist/plugins/react-native-a11y/index.js +4 -2
- package/dist/plugins/react-native-a11y/rules/accessibility-hint.js +4 -2
- package/dist/plugins/react-native-a11y/rules/accessibility-label.js +8 -4
- package/dist/plugins/react-native-a11y/rules/accessibility-role.js +4 -2
- package/dist/plugins/react-native-a11y/rules/gate-announce-for-accessibility.js +10 -6
- package/dist/plugins/react-native-a11y/rules/input-purpose.js +8 -4
- package/dist/plugins/react-native-a11y/rules/no-unnamed-interactive.js +4 -2
- package/dist/plugins/react-native-a11y/rules/require-invert-colors-opt-out.js +6 -4
- package/dist/plugins/react-native-a11y/rules/require-modal-a11y-on-sheets.js +5 -3
- package/dist/plugins/react-native-a11y/utils/get-amount-of-text-children.js +8 -4
- 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 +158 -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 +214 -0
- package/dist/plugins/rules/no-markup-variable.d.ts +22 -0
- package/dist/plugins/rules/no-markup-variable.js +117 -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 +186 -0
- package/dist/plugins/rules/test-id-format.d.ts +4 -0
- package/dist/plugins/rules/test-id-format.js +122 -0
- package/dist/plugins/rules/test-id-from-props.d.ts +4 -0
- package/dist/plugins/rules/test-id-from-props.js +94 -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 +109 -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 +274 -0
- package/dist/plugins/utils/types.d.ts +20 -0
- package/dist/plugins/utils/types.js +89 -0
- package/dist/react-native.js +95 -11
- package/dist/shared.d.ts +19 -0
- package/dist/shared.js +72 -1
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
## [5.0.0](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/branchCompare?baseVersion=GTv4.0.0&targetVersion=GTv5.0.0) (2026-11-10)
|
|
2
|
+
|
|
3
|
+
### ⚠ BREAKING CHANGES
|
|
4
|
+
|
|
5
|
+
* **react:** effect dependency findings now come from
|
|
6
|
+
react-hooks/exhaustive-effect-dependencies, so existing
|
|
7
|
+
eslint-disable comments for react-hooks/exhaustive-deps no longer apply.
|
|
8
|
+
* **react-native:** importing `Animated` from `react-native` is now an error in the react-native preset.
|
|
9
|
+
* **react-native:** react-native reports code it accepted before.
|
|
10
|
+
* **react:** react and react-native report code they accepted before.
|
|
11
|
+
* **base:** base reports code it accepted before.
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **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)
|
|
16
|
+
* port boolean-guard and undefined-check rules ([79f50cc](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/commit/79f50ccf5bfb34ba1683b7d46947a423fbe56ee5))
|
|
17
|
+
* 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))
|
|
18
|
+
* 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))
|
|
19
|
+
* 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))
|
|
20
|
+
* **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)
|
|
21
|
+
* **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)
|
|
22
|
+
* **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)
|
|
23
|
+
* **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)
|
|
24
|
+
|
|
25
|
+
## [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)
|
|
26
|
+
|
|
27
|
+
### ⚠ BREAKING CHANGES
|
|
28
|
+
|
|
29
|
+
* **react-native:** the react-native preset enables three new error-level rules.
|
|
30
|
+
|
|
31
|
+
### Features
|
|
32
|
+
|
|
33
|
+
* **react-native:** add Expo rules and Metro asset requires ([059b8c1](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/commit/059b8c1cd8d9bf3772da3b24fc3ed4a91078857a)), closes [AB#5178](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_workitems/edit/5178)
|
|
34
|
+
|
|
1
35
|
## [3.0.0](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/branchCompare?baseVersion=GTv2.0.0&targetVersion=GTv3.0.0) (2026-11-09)
|
|
2
36
|
|
|
3
37
|
### ⚠ BREAKING CHANGES
|
package/README.md
CHANGED
|
@@ -92,11 +92,24 @@ export default [...base({ tsconfigRootDir: import.meta.dirname }), ...react(), .
|
|
|
92
92
|
|
|
93
93
|
## Custom rules
|
|
94
94
|
|
|
95
|
-
This package ships its own plugin, namespaced `@hypersolid-dev
|
|
95
|
+
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
96
|
|
|
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`). |
|
|
97
|
+
| Rule | Preset | Enforces |
|
|
98
|
+
| --- | --- | --- |
|
|
99
|
+
| `@hypersolid-dev/props-destructuring` | react | A component destructures its props in the signature, so its props are readable there. Replaces `react/destructuring-assignment` (`always`, `destructureInSignature: always`), as `eslint-plugin-react` doesn't support ESLint 10. Checks PascalCase functions that return JSX, including those wrapped in `memo` and `forwardRef`. Bad: `const Card = (props: Props) => <h1>{props.title}</h1>`, or `const { title } = props` in the body. Good: `const Card = ({ title }: Props) => …`. When the props type is a union, such as a discriminated union, `props` may stay whole, because destructuring breaks narrowing (`if (props.kind === 'a') props.a`). |
|
|
100
|
+
| `@hypersolid-dev/undefined-check` | base | No `x !== undefined` (or `=== undefined`) where `undefined` is the type's only falsy value, so truthiness already decides. Bad: `if (user !== undefined)` with `user: User \| undefined`. Good: `if (user)`. Needs type information. |
|
|
101
|
+
| `@hypersolid-dev/regex-pattern-const` | base | A regex is a module-level `const <NAME>_PATTERN`, so it's read by its name. Bad: `/^\d+$/.test(id)` in a function. Off in tests. |
|
|
102
|
+
| `@hypersolid-dev/comment-style` | base | `//` and `/* */` comments are one row; longer prose is a `/** */` JSDoc block. Directive-only runs (`eslint-disable…`, `@ts-…`) are fine. Option `maxLength` caps each row (unset by default). |
|
|
103
|
+
| `@hypersolid-dev/jsdoc-for-props` | react | A comment on a type member is JSDoc, the form editors show at the call site. Bad: `// The title` above `title: string`. |
|
|
104
|
+
| `@hypersolid-dev/boolean-guard` | react | A JSX `&&` guard can't leak `0` or `''`, which React renders (and which crashes React Native outside `<Text>`). Bad: `{count && <Badge />}`. Good: `{count > 0 && <Badge />}`. Needs type information. Off in tests. |
|
|
105
|
+
| `@hypersolid-dev/no-markup-variable` | react | Styled JSX isn't stored in a variable in a component body; it's its own component, or written where it renders. Off in tests. |
|
|
106
|
+
| `@hypersolid-dev/effect-takes-callback` | react | A `use*Effect` hook takes the callback it runs as its first parameter, like `useEffect`. Off in tests. |
|
|
107
|
+
| `@hypersolid-dev/no-unnecessary-use-callback` | react | A `useCallback` with empty deps that captures nothing from render is a module-level function. Off in tests. |
|
|
108
|
+
| `@hypersolid-dev/no-callback-payload` | react | A component passes a callback prop straight through instead of calling it with a payload built from its own props; the caller binds the payload. Bad: `onPress={() => onSelect(id)}` where `id` is a prop. Off in tests. |
|
|
109
|
+
| `@hypersolid-dev/test-id-format` | react-native | A `testID` is lowercase kebab-case. Bad: `testID="Header.Title"`. |
|
|
110
|
+
| `@hypersolid-dev/test-id-from-props` | react-native | In `**/components/**/*.tsx`, a `testID` comes from the `testID` prop (suffixed for inner nodes), never a fixed string. Off in screens and tests. |
|
|
111
|
+
| `@hypersolid-dev/test-id-default` | react-native | In `**/components/**/*.tsx`, a `testID` prop defaults to the component's kebab-case name. Bad: `({ testID }: Props)` in `Card`. Good: `({ testID = 'card' }: Props)`. |
|
|
112
|
+
| `@hypersolid-dev/webview-allow-list` | react-native | A `react-native-webview` WebView sets `originWhitelist={['*']}` and filters every navigation in an always-present `onShouldStartLoadWithRequest`. |
|
|
100
113
|
|
|
101
114
|
## Overriding rules
|
|
102
115
|
|
|
@@ -137,7 +150,7 @@ export default [...base({ tsconfigRootDir: import.meta.dirname }), { ignores: ['
|
|
|
137
150
|
|
|
138
151
|
### Test files
|
|
139
152
|
|
|
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.
|
|
153
|
+
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
154
|
|
|
142
155
|
```js
|
|
143
156
|
import base, { TEST_FILES } from '@hypersolid-dev/eslint-config/base';
|
|
@@ -149,7 +162,25 @@ export default [...base({ tsconfigRootDir: import.meta.dirname }), { files: TEST
|
|
|
149
162
|
|
|
150
163
|
- `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
164
|
- `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
|
-
-
|
|
165
|
+
- `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.
|
|
166
|
+
- `@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`.
|
|
167
|
+
- `@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.
|
|
168
|
+
- `@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.
|
|
169
|
+
|
|
170
|
+
- `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.
|
|
171
|
+
|
|
172
|
+
### Component folders
|
|
173
|
+
|
|
174
|
+
`@hypersolid-dev/no-callback-payload` applies to every component, and the `test-id-*` rules to `**/components/**/*.tsx`. If your components live elsewhere, scope them to your own folders:
|
|
175
|
+
|
|
176
|
+
```js
|
|
177
|
+
export default [
|
|
178
|
+
...base({ tsconfigRootDir: import.meta.dirname }),
|
|
179
|
+
...reactNative(),
|
|
180
|
+
{ files: ['**/components/**/*.tsx'], rules: { '@hypersolid-dev/test-id-default': 'off' } },
|
|
181
|
+
{ files: ['src/ui/**/*.tsx'], rules: { '@hypersolid-dev/test-id-default': 'error' } },
|
|
182
|
+
];
|
|
183
|
+
```
|
|
153
184
|
|
|
154
185
|
## Formatting and import order
|
|
155
186
|
|
package/dist/base.js
CHANGED
|
@@ -5,7 +5,8 @@ import { createNodeResolver, flatConfigs as importX } from 'eslint-plugin-import
|
|
|
5
5
|
import noSecrets from 'eslint-plugin-no-secrets';
|
|
6
6
|
import unicorn from 'eslint-plugin-unicorn';
|
|
7
7
|
import { configs as tsConfigs } from 'typescript-eslint';
|
|
8
|
-
import
|
|
8
|
+
import hypersolid from './plugins/hypersolid.js';
|
|
9
|
+
import { BASE_RESTRICTED_SYNTAX, BOOLEAN_NAME_OPTIONS, CODE_FILES, NAMING_CONVENTION, NUMBER_GLOBAL_RESTRICTIONS, raiseWarnings, scopeToCode, TEST_FILES, } from './shared.js';
|
|
9
10
|
// The glob the presets relax test-only rules for; reuse it for your own test overrides.
|
|
10
11
|
export { TEST_FILES } from './shared.js';
|
|
11
12
|
export default function base(options) {
|
|
@@ -22,9 +23,11 @@ export default function base(options) {
|
|
|
22
23
|
js.configs.recommended,
|
|
23
24
|
// Type-aware TypeScript rules, the strict set. Bad: `const n: any = data; n.foo()`, `if (alwaysTrue)`.
|
|
24
25
|
...tsConfigs.strictTypeChecked,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Unicorn's recommended set minus its taste rules (file naming, `no-null`, `no-array-reduce`,
|
|
28
|
+
* abbreviations, formatting); what remains catches bugs and outdated APIs.
|
|
29
|
+
* Bad: `array.indexOf(x) !== -1` (use `includes`), `new Error()` without a message.
|
|
30
|
+
*/
|
|
28
31
|
unicorn.configs.unopinionated,
|
|
29
32
|
// Import mistakes. Bad: `import { missing } from './module.js'`, importing the same module twice.
|
|
30
33
|
importX.recommended,
|
|
@@ -41,7 +44,7 @@ export default function base(options) {
|
|
|
41
44
|
tsconfigRootDir: options.tsconfigRootDir,
|
|
42
45
|
},
|
|
43
46
|
},
|
|
44
|
-
plugins: { 'no-secrets': noSecrets },
|
|
47
|
+
plugins: { '@hypersolid-dev': hypersolid, 'no-secrets': noSecrets },
|
|
45
48
|
settings: {
|
|
46
49
|
'import-x/resolver-next': [createTypeScriptImportResolver(), createNodeResolver()],
|
|
47
50
|
},
|
|
@@ -60,6 +63,16 @@ export default function base(options) {
|
|
|
60
63
|
'preserve-caught-error': 'error',
|
|
61
64
|
// The coercing globals. Bad: `isNaN('x')` (use `Number.isNaN`).
|
|
62
65
|
'no-restricted-globals': ['error', ...NUMBER_GLOBAL_RESTRICTIONS],
|
|
66
|
+
// The same globals reached through an object, and `Math.pow`. Bad: `globalThis.isNaN(x)`, `Math.pow(2, 8)` (use `2 ** 8`).
|
|
67
|
+
'no-restricted-properties': [
|
|
68
|
+
'error',
|
|
69
|
+
...['global', 'globalThis'].flatMap((object) => NUMBER_GLOBAL_RESTRICTIONS.map(({ name, message }) => ({ object, property: name, message }))),
|
|
70
|
+
{ object: 'Math', property: 'pow', message: 'Use the `**` operator.' },
|
|
71
|
+
],
|
|
72
|
+
// A silent catch and renamed default specifiers. Bad: `.catch(() => {})`, `import { default as user } from './user.js'`.
|
|
73
|
+
'no-restricted-syntax': ['error', ...BASE_RESTRICTED_SYNTAX],
|
|
74
|
+
// `let` only for a reassigned binding, ignoring one read in a closure before its assignment. Bad: `let total = 1` never reassigned.
|
|
75
|
+
'prefer-const': ['error', { destructuring: 'any', ignoreReadBeforeAssign: true }],
|
|
63
76
|
// `type` over `interface`. Bad: `interface User { name: string }`.
|
|
64
77
|
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
|
|
65
78
|
// Every promise is awaited or handled; `void` does not count. Bad: `save();`, `void save();`.
|
|
@@ -79,8 +92,22 @@ export default function base(options) {
|
|
|
79
92
|
],
|
|
80
93
|
// No value returned where `void` is expected. Bad: `run(() => 1)` with `run(callback: () => void)`.
|
|
81
94
|
'@typescript-eslint/strict-void-return': ['error', { allowReturnAny: true }],
|
|
82
|
-
|
|
83
|
-
|
|
95
|
+
/**
|
|
96
|
+
* A `switch` on a union handles every member or has a `default`. Bad: `switch (shape) { case 'circle': … }` missing `'square'`.
|
|
97
|
+
* and a `switch` on a non-union value has a `default`. Bad: `switch (count) { case 0: … }` without `default`.
|
|
98
|
+
*/
|
|
99
|
+
'@typescript-eslint/switch-exhaustiveness-check': [
|
|
100
|
+
'error',
|
|
101
|
+
{ considerDefaultExhaustiveForUnions: true, requireDefaultForNonUnion: true },
|
|
102
|
+
],
|
|
103
|
+
// Rest siblings omit a key, and `_` skips an argument. Bad: `const unused = 1`.
|
|
104
|
+
'@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true, argsIgnorePattern: '^_+$' }],
|
|
105
|
+
// A type the literal already says. Bad: `const count: number = 5`.
|
|
106
|
+
'@typescript-eslint/no-inferrable-types': 'error',
|
|
107
|
+
// `startsWith`/`endsWith` state the intent. Bad: `name.indexOf('use') === 0`.
|
|
108
|
+
'@typescript-eslint/prefer-string-starts-ends-with': 'error',
|
|
109
|
+
// Values camelCase, PascalCase or UPPER_CASE; functions camelCase or PascalCase; types PascalCase. Bad: `const user_name = ''`, `type props = {}`.
|
|
110
|
+
'@typescript-eslint/naming-convention': ['error', ...NAMING_CONVENTION],
|
|
84
111
|
// The caught value is named `error`. Bad: `catch (e)`.
|
|
85
112
|
'unicorn/catch-error-name': 'error',
|
|
86
113
|
// A boolean reads as a claim. Bad: `const visible = true` (use `isVisible`).
|
|
@@ -108,6 +135,20 @@ export default function base(options) {
|
|
|
108
135
|
'unicorn/no-unsafe-promise-all-settled-values': 'error',
|
|
109
136
|
// An async function already wraps its return value. Bad: `async () => Promise.resolve(1)`.
|
|
110
137
|
'unicorn/no-useless-promise-resolve-reject': 'error',
|
|
138
|
+
// No cleartext URLs. Bad: a cleartext `http` URL in a `fetch`.
|
|
139
|
+
'unicorn/prefer-https': 'error',
|
|
140
|
+
// `.catch()` over a second `.then()` argument. Bad: `promise.then(render, report)`.
|
|
141
|
+
'unicorn/prefer-then-catch': 'error',
|
|
142
|
+
// A helper that closes over nothing belongs at module level. Bad: `function outer() { const double = (x) => x * 2; }`.
|
|
143
|
+
'unicorn/consistent-function-scoping': 'error',
|
|
144
|
+
// One escape style for `${` in template literals. Bad: `` `\${a} $\{b}` ``.
|
|
145
|
+
'unicorn/consistent-template-literal-escape': 'error',
|
|
146
|
+
// `x !== undefined` where truthiness already decides. Bad: `if (user !== undefined)` with `user: User | undefined`.
|
|
147
|
+
'@hypersolid-dev/undefined-check': 'error',
|
|
148
|
+
// A regex is a module-level `*_PATTERN` constant, named for what it matches. Bad: `if (/^\d+$/.test(id))` in a function.
|
|
149
|
+
'@hypersolid-dev/regex-pattern-const': 'error',
|
|
150
|
+
// `//` and `/* */` comments stay one row; longer prose is a `/** */` block. Bad: three `//` lines in a row.
|
|
151
|
+
'@hypersolid-dev/comment-style': 'error',
|
|
111
152
|
// `undefined` as an argument is often required by the callee's signature. Bad: `let value = undefined`.
|
|
112
153
|
'unicorn/no-useless-undefined': ['error', { checkArguments: false }],
|
|
113
154
|
// Off: `.then()` chains are fine. Bad: `fetchUser().then(render)` reported.
|
|
@@ -158,6 +199,10 @@ export default function base(options) {
|
|
|
158
199
|
'unicorn/no-error-property-assignment': 'off',
|
|
159
200
|
// Off: test setup stubs globals. Bad: `globalThis.fetch = mockFetch` reported.
|
|
160
201
|
'unicorn/no-global-object-property-assignment': 'off',
|
|
202
|
+
// Off: helpers inside a `describe` keep each suite readable on its own. Bad: `const render = () => …` in a `describe` reported.
|
|
203
|
+
'unicorn/consistent-function-scoping': 'off',
|
|
204
|
+
// Off: a test keeps its inline `toMatch(/…/)` literal. Bad: `expect(id).toMatch(/^\d+$/)` reported.
|
|
205
|
+
'@hypersolid-dev/regex-pattern-const': 'off',
|
|
161
206
|
},
|
|
162
207
|
},
|
|
163
208
|
{
|
|
@@ -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,7 +1,8 @@
|
|
|
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
|
|
4
|
+
const PASCAL_CASE_PATTERN = /^[A-Z]/;
|
|
5
|
+
const isPascalCase = (name) => typeof name === 'string' && PASCAL_CASE_PATTERN.test(name);
|
|
5
6
|
function isJsx(node) {
|
|
6
7
|
if (typeof node !== 'object' || node === null) {
|
|
7
8
|
return false;
|
|
@@ -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,
|
|
@@ -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;
|
|
@@ -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 &&
|
|
@@ -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)) {
|
|
@@ -29,9 +29,11 @@ const gateAnnounceForAccessibility = createRule({
|
|
|
29
29
|
schema: [],
|
|
30
30
|
},
|
|
31
31
|
create(context) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
/**
|
|
33
|
+
* A bare announce is the legitimate cross-platform pattern. The double
|
|
34
|
+
* announcement only happens where a live region covers Android already, so
|
|
35
|
+
* that pairing is the trigger rather than the call on its own.
|
|
36
|
+
*/
|
|
35
37
|
let hasLiveRegion = false;
|
|
36
38
|
const announces = [];
|
|
37
39
|
const platformChecks = [];
|
|
@@ -60,9 +62,11 @@ const gateAnnounceForAccessibility = createRule({
|
|
|
60
62
|
if (!hasLiveRegion) {
|
|
61
63
|
return;
|
|
62
64
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
/**
|
|
66
|
+
* ponytail: any Platform.OS in the same function counts as the gate.
|
|
67
|
+
* Lenient on purpose — a guard clause, a switch and a named boolean are
|
|
68
|
+
* all real gates, and none of them is an ancestor of the call.
|
|
69
|
+
*/
|
|
66
70
|
const gatedScopes = new Set(platformChecks.map((check) => enclosingScope(check)));
|
|
67
71
|
for (const announce of announces) {
|
|
68
72
|
if (gatedScopes.has(enclosingScope(announce))) {
|
|
@@ -56,12 +56,16 @@ const inputPurpose = createRule({
|
|
|
56
56
|
if (!components.includes(componentName)) {
|
|
57
57
|
return;
|
|
58
58
|
}
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
/**
|
|
60
|
+
* The missing token may arrive through the spread; an explicit one is
|
|
61
|
+
* already wrong whatever the spread carries
|
|
62
|
+
*/
|
|
61
63
|
const autoComplete = getProperty(node, "autoComplete");
|
|
62
64
|
const textContentType = getProperty(node, "textContentType");
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
/**
|
|
66
|
+
* One fix either way, so one finding: the sharper message where only
|
|
67
|
+
* the iOS half is already set
|
|
68
|
+
*/
|
|
65
69
|
if (!autoComplete && !hasSpreadProps(node)) {
|
|
66
70
|
context.report(textContentType
|
|
67
71
|
? { node: textContentType, messageId: "missingAutoComplete" }
|
|
@@ -46,8 +46,10 @@ const noUnnamedInteractive = createRule({
|
|
|
46
46
|
if (componentName === undefined) {
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
/**
|
|
50
|
+
* accessibility-label owns the components it knows by name, so a
|
|
51
|
+
* control does not collect a finding from both rules
|
|
52
|
+
*/
|
|
51
53
|
if (ALREADY_COVERED.has(componentName) &&
|
|
52
54
|
!components.includes(componentName)) {
|
|
53
55
|
return;
|
|
@@ -30,10 +30,12 @@ const requireInvertColorsOptOut = createRule({
|
|
|
30
30
|
],
|
|
31
31
|
},
|
|
32
32
|
create(context) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Matched on the element name, so an image from expo-image or a wrapper
|
|
35
|
+
* named through additionalComponents is checked the same way. Deliberately
|
|
36
|
+
* not gated on the import statement: that reads the source text and misses
|
|
37
|
+
* a codebase whose quote style differs.
|
|
38
|
+
*/
|
|
37
39
|
const components = getComponents({
|
|
38
40
|
context,
|
|
39
41
|
defaultComponents: IMAGES,
|
|
@@ -28,9 +28,11 @@ const requireModalA11yOnSheets = createRule({
|
|
|
28
28
|
],
|
|
29
29
|
},
|
|
30
30
|
create(context) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
/**
|
|
32
|
+
* No default list: a sheet is whatever this project calls one, and
|
|
33
|
+
* hardcoding one library's component names would fire on a codebase that
|
|
34
|
+
* happens to reuse them. Configure through additionalComponents.
|
|
35
|
+
*/
|
|
34
36
|
const components = getComponents({ context });
|
|
35
37
|
return {
|
|
36
38
|
JSXOpeningElement(node) {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
2
2
|
import { TEXTS } from "../constants/react-native-components.js";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* ponytail: `Text` by name only. A renamed import or a differently named text
|
|
5
|
+
* atom reads as no text; pass it through `additionalComponents` if that bites.
|
|
6
|
+
*/
|
|
5
7
|
const isTextElement = (node) => node.openingElement.name.type === AST_NODE_TYPES.JSXIdentifier &&
|
|
6
8
|
TEXTS.includes(node.openingElement.name.name);
|
|
7
9
|
/** `<Text>{null}</Text>` and `<Text> </Text>` render nothing a user can read. */
|
|
@@ -45,8 +47,10 @@ const getAmountOfTextChildren = (children, oldAmount = 0) => children.reduce((am
|
|
|
45
47
|
expression.type === AST_NODE_TYPES.TemplateLiteral) {
|
|
46
48
|
return amount + 1;
|
|
47
49
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
/**
|
|
51
|
+
* JSX inside an expression renders like any other child, and a branch
|
|
52
|
+
* that renders text still names the control
|
|
53
|
+
*/
|
|
50
54
|
const rendered = (isJsxChild(expression) ? [expression] : branchesOf(expression)).filter(isJsxChild);
|
|
51
55
|
return rendered.length > 0
|
|
52
56
|
? getAmountOfTextChildren(rendered, amount)
|
|
@@ -41,7 +41,7 @@ const collectAliases = (program) => {
|
|
|
41
41
|
const resolveAlias = (node, name) => {
|
|
42
42
|
const program = getProgram(node);
|
|
43
43
|
let aliases = aliasesByProgram.get(program);
|
|
44
|
-
if (aliases
|
|
44
|
+
if (!aliases) {
|
|
45
45
|
aliases = collectAliases(program);
|
|
46
46
|
aliasesByProgram.set(program, aliases);
|
|
47
47
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
2
|
+
/**
|
|
3
|
+
* Render a conditional child with `&&`, and coerce the condition when it can be
|
|
4
|
+
* a falsy value React draws.
|
|
5
|
+
*
|
|
6
|
+
* React skips `null`, `undefined`, `true` and `false`, but renders `0` and `""`.
|
|
7
|
+
* On React Native a rendered `0` outside a `<Text>` throws, so
|
|
8
|
+
* `{items.length && <List />}` is a crash on an empty list.
|
|
9
|
+
*
|
|
10
|
+
* - `{cond ? <X /> : null}` is a plain `&&` (`preferLogical`), or `!!cond && …`
|
|
11
|
+
* when the condition can leak (`preferLogicalCoerced`). `{cond ? null : <X />}`
|
|
12
|
+
* is advised as `!cond && …`.
|
|
13
|
+
* - `{count && <X />}` leaks when the type admits `0`, `""` or `0n` (`coerce`).
|
|
14
|
+
* For `a && b && <X />` only `b` is judged.
|
|
15
|
+
* - `{x !== undefined && <X />}` is `!!x` when `undefined` is the type's only
|
|
16
|
+
* falsy value (`preferBangBang`).
|
|
17
|
+
*
|
|
18
|
+
* Leaking is read from the type: string, number, bigint, branded primitives,
|
|
19
|
+
* `any` and `unknown` leak; literal unions leak only if they hold `""`/`0`/`0n`.
|
|
20
|
+
* Attributes and element-vs-element ternaries are left alone. A guard held in a
|
|
21
|
+
* `const` is checked when every read of that name is drawn as a child.
|
|
22
|
+
*
|
|
23
|
+
* Limits: guards returned from functions or render props are not followed, and
|
|
24
|
+
* `||`/`??` guards are not examined. Silent without type information.
|
|
25
|
+
*/
|
|
26
|
+
type MessageIds = "preferLogical" | "preferLogicalCoerced" | "coerce" | "preferBangBang";
|
|
27
|
+
declare const booleanGuard: ESLintUtils.RuleModule<MessageIds, [], unknown, ESLintUtils.RuleListener> & {
|
|
28
|
+
name: string;
|
|
29
|
+
};
|
|
30
|
+
export default booleanGuard;
|