@hypersolid-dev/eslint-config 5.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 +15 -0
- package/README.md +17 -1
- package/dist/base.js +14 -1
- package/dist/jest.d.ts +3 -0
- package/dist/jest.js +56 -0
- package/dist/plugins/props-destructuring.js +12 -12
- package/dist/plugins/react-native-a11y/rules/accessibility-actions-need-handler.js +2 -2
- package/dist/plugins/react-native-a11y/rules/accessibility-hint.js +2 -2
- package/dist/plugins/react-native-a11y/rules/accessibility-label.js +2 -2
- package/dist/plugins/react-native-a11y/rules/accessibility-role.js +2 -2
- 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 +4 -4
- package/dist/plugins/react-native-a11y/rules/input-purpose.js +2 -2
- 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 +2 -2
- package/dist/plugins/react-native-a11y/rules/require-invert-colors-opt-out.js +2 -2
- package/dist/plugins/react-native-a11y/rules/require-modal-a11y-on-sheets.js +2 -2
- package/dist/plugins/react-native-a11y/utils/get-amount-of-text-children.js +15 -13
- package/dist/plugins/rules/boolean-guard.js +3 -3
- package/dist/plugins/rules/comment-style.js +43 -34
- package/dist/plugins/rules/effect-takes-callback.js +1 -1
- package/dist/plugins/rules/jsdoc-for-props.js +1 -1
- package/dist/plugins/rules/no-callback-payload.js +18 -15
- package/dist/plugins/rules/no-markup-variable.js +41 -35
- package/dist/plugins/rules/no-unnecessary-use-callback.js +2 -2
- package/dist/plugins/rules/regex-pattern-const.js +1 -1
- package/dist/plugins/rules/test-id-default.js +23 -27
- package/dist/plugins/rules/test-id-format.js +14 -18
- package/dist/plugins/rules/test-id-from-props.js +23 -27
- package/dist/plugins/rules/undefined-check.js +2 -2
- package/dist/plugins/rules/webview-allow-list.js +22 -26
- package/dist/plugins/utils/expressions.d.ts +3 -3
- package/dist/plugins/utils/expressions.js +94 -78
- package/dist/plugins/utils/types.js +14 -12
- package/dist/react-native.js +28 -0
- package/dist/shared.d.ts +3 -3
- package/dist/shared.js +168 -112
- package/package.json +12 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
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
|
+
|
|
1
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)
|
|
2
17
|
|
|
3
18
|
### ⚠ 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,7 +93,19 @@ 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
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
|
|
|
@@ -193,6 +208,7 @@ Root files such as `eslint.config.js` or `vite.config.ts` are often outside the
|
|
|
193
208
|
## Known issues
|
|
194
209
|
|
|
195
210
|
- `eslint-plugin-jsx-a11y` (used by `react`) declares support for ESLint 9 only. It works on ESLint 10, but installing prints a peer-dependency warning, and installs with `strict-peer-dependencies` fail.
|
|
211
|
+
- `eslint-plugin-react-doctor` declares no `eslint` peer dependency. It reads the nearest `package.json` to decide which rules apply (the `rn-*` rules need `react-native` there), and skips some rules for files under `test/`, `__tests__/` and similar paths.
|
|
196
212
|
- In `next`, `@next/next/no-html-link-for-pages` prints "Pages directory cannot be found" on App Router projects. Turn it off if you don't use the `pages` directory:
|
|
197
213
|
|
|
198
214
|
```js
|
package/dist/base.js
CHANGED
|
@@ -3,6 +3,8 @@ 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
10
|
import hypersolid from './plugins/hypersolid.js';
|
|
@@ -44,7 +46,12 @@ export default function base(options) {
|
|
|
44
46
|
tsconfigRootDir: options.tsconfigRootDir,
|
|
45
47
|
},
|
|
46
48
|
},
|
|
47
|
-
plugins: {
|
|
49
|
+
plugins: {
|
|
50
|
+
'@hypersolid-dev': hypersolid,
|
|
51
|
+
'no-secrets': noSecrets,
|
|
52
|
+
'prefer-arrow-functions': preferArrowFunctions,
|
|
53
|
+
sonarjs,
|
|
54
|
+
},
|
|
48
55
|
settings: {
|
|
49
56
|
'import-x/resolver-next': [createTypeScriptImportResolver(), createNodeResolver()],
|
|
50
57
|
},
|
|
@@ -59,6 +66,8 @@ export default function base(options) {
|
|
|
59
66
|
'no-nested-ternary': 'error',
|
|
60
67
|
// Arrow functions as callbacks. Bad: `list.map(function (x) { return x; })`.
|
|
61
68
|
'prefer-arrow-callback': 'error',
|
|
69
|
+
// One function form throughout, and no `this` to reason about. Bad: `function load() {}`, `export default function base() {}`.
|
|
70
|
+
'prefer-arrow-functions/prefer-arrow-functions': ['error', { disallowPrototype: true }],
|
|
62
71
|
// A rethrow keeps the original error as `cause`. Bad: `catch (error) { throw new Error('failed'); }`.
|
|
63
72
|
'preserve-caught-error': 'error',
|
|
64
73
|
// The coercing globals. Bad: `isNaN('x')` (use `Number.isNaN`).
|
|
@@ -183,12 +192,16 @@ export default function base(options) {
|
|
|
183
192
|
'import-x/namespace': 'off',
|
|
184
193
|
// High-entropy strings that look like credentials. Bad: `const token = 'ghp_Zx8kQ2vLm9…'`.
|
|
185
194
|
'no-secrets/no-secrets': 'error',
|
|
195
|
+
// A function too tangled to follow, nesting weighted over length (limit 15). Bad: a loop holding an `if` holding a `try`.
|
|
196
|
+
'sonarjs/cognitive-complexity': 'error',
|
|
186
197
|
},
|
|
187
198
|
},
|
|
188
199
|
{
|
|
189
200
|
name: '@hypersolid-dev/base/tests',
|
|
190
201
|
files: TEST_FILES,
|
|
191
202
|
rules: {
|
|
203
|
+
// A test's setup and table of cases are read once, not maintained as logic.
|
|
204
|
+
'sonarjs/cognitive-complexity': 'off',
|
|
192
205
|
// Off: mocks and fixtures are loosely typed by design. Bad: `const user = jest.fn()()` reported.
|
|
193
206
|
'@typescript-eslint/no-unsafe-assignment': 'off',
|
|
194
207
|
// Off: mocks and fixtures are loosely typed by design. Bad: `mock.calls[0].user` reported.
|
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
|
+
}
|
|
@@ -3,7 +3,7 @@ const WRAPPERS = new Set(['memo', 'forwardRef']);
|
|
|
3
3
|
const isFunction = (node) => typeof node === 'object' && node !== null && FUNCTION_TYPES.has(node.type);
|
|
4
4
|
const PASCAL_CASE_PATTERN = /^[A-Z]/;
|
|
5
5
|
const isPascalCase = (name) => typeof name === 'string' && PASCAL_CASE_PATTERN.test(name);
|
|
6
|
-
|
|
6
|
+
const isJsx = (node) => {
|
|
7
7
|
if (typeof node !== 'object' || node === null) {
|
|
8
8
|
return false;
|
|
9
9
|
}
|
|
@@ -15,22 +15,22 @@ function isJsx(node) {
|
|
|
15
15
|
return isJsx(consequent) || isJsx(alternate);
|
|
16
16
|
}
|
|
17
17
|
return type === 'LogicalExpression' && (isJsx(left) || isJsx(right));
|
|
18
|
-
}
|
|
18
|
+
};
|
|
19
19
|
// Searches the function's own return statements; nested functions return for themselves.
|
|
20
|
-
|
|
20
|
+
const isReturningJsx = (node) => {
|
|
21
21
|
if (typeof node !== 'object' || node === null) {
|
|
22
22
|
return false;
|
|
23
23
|
}
|
|
24
24
|
if (Array.isArray(node)) {
|
|
25
|
-
return node.some(
|
|
25
|
+
return node.some(isReturningJsx);
|
|
26
26
|
}
|
|
27
27
|
const current = node;
|
|
28
28
|
return current.type === 'ReturnStatement'
|
|
29
29
|
? isJsx(current.argument)
|
|
30
|
-
: Object.entries(current).some(([key, value]) => key !== 'parent' && !isFunction(value) && typeof value === 'object' &&
|
|
31
|
-
}
|
|
30
|
+
: Object.entries(current).some(([key, value]) => key !== 'parent' && !isFunction(value) && typeof value === 'object' && isReturningJsx(value));
|
|
31
|
+
};
|
|
32
32
|
// `memo(fn)`, `React.forwardRef(fn)` and `memo(forwardRef(fn))`.
|
|
33
|
-
|
|
33
|
+
const unwrap = (node) => {
|
|
34
34
|
if (isFunction(node)) {
|
|
35
35
|
return node;
|
|
36
36
|
}
|
|
@@ -41,7 +41,7 @@ function unwrap(node) {
|
|
|
41
41
|
const { type: calleeType, name, property } = callee;
|
|
42
42
|
const calleeName = calleeType === 'MemberExpression' ? property.name : name;
|
|
43
43
|
return typeof calleeName === 'string' && WRAPPERS.has(calleeName) ? unwrap(args[0]) : undefined;
|
|
44
|
-
}
|
|
44
|
+
};
|
|
45
45
|
const propsDestructuring = {
|
|
46
46
|
meta: {
|
|
47
47
|
type: 'suggestion',
|
|
@@ -51,7 +51,7 @@ const propsDestructuring = {
|
|
|
51
51
|
destructure: 'Destructure the props in the signature: `({ title }: Props)`. A discriminated-union props type may keep `props`.',
|
|
52
52
|
},
|
|
53
53
|
},
|
|
54
|
-
create(context) {
|
|
54
|
+
create: (context) => {
|
|
55
55
|
const services = context.sourceCode.parserServices;
|
|
56
56
|
const checker = services.program?.getTypeChecker();
|
|
57
57
|
// Destructuring a union in the signature breaks narrowing (`if (props.kind === 'a') props.aOnly`).
|
|
@@ -61,18 +61,18 @@ const propsDestructuring = {
|
|
|
61
61
|
};
|
|
62
62
|
const check = (fn) => {
|
|
63
63
|
const props = fn?.params[0];
|
|
64
|
-
if (!fn || props?.type !== 'Identifier' || !
|
|
64
|
+
if (!fn || props?.type !== 'Identifier' || !isReturningJsx(fn.body) && !isJsx(fn.body) || isUnion(props)) {
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
67
|
context.report({ node: props, messageId: 'destructure' });
|
|
68
68
|
};
|
|
69
69
|
return {
|
|
70
|
-
FunctionDeclaration(node) {
|
|
70
|
+
FunctionDeclaration: (node) => {
|
|
71
71
|
if (isPascalCase(node.id.name)) {
|
|
72
72
|
check(node);
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
|
-
VariableDeclarator(node) {
|
|
75
|
+
VariableDeclarator: (node) => {
|
|
76
76
|
if (node.id.type === 'Identifier' && isPascalCase(node.id.name)) {
|
|
77
77
|
check(unwrap(node.init));
|
|
78
78
|
}
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -18,32 +18,30 @@ const accessibilityValueNeedsRange = createRule({
|
|
|
18
18
|
},
|
|
19
19
|
schema: [],
|
|
20
20
|
},
|
|
21
|
-
create(context) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
};
|
|
47
|
-
},
|
|
21
|
+
create: (context) => ({
|
|
22
|
+
JSXOpeningElement: (node) => {
|
|
23
|
+
const attribute = getProperty(node, "accessibilityValue");
|
|
24
|
+
if (attribute?.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const { expression } = attribute.value;
|
|
28
|
+
// A value built elsewhere cannot be read here
|
|
29
|
+
if (expression.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// A spread may carry the range, and cannot be read from here
|
|
33
|
+
if (expression.properties.some((property) => property.type === AST_NODE_TYPES.SpreadElement)) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const keys = keysOf(expression);
|
|
37
|
+
if (!keys.includes("now") || keys.includes("text")) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (keys.includes("min") && keys.includes("max")) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
context.report({ node: attribute, messageId: "valueWithoutRange" });
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
48
46
|
});
|
|
49
47
|
export default accessibilityValueNeedsRange;
|
|
@@ -18,23 +18,21 @@ const adjustableNeedsValue = createRule({
|
|
|
18
18
|
},
|
|
19
19
|
schema: [],
|
|
20
20
|
},
|
|
21
|
-
create(context) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
};
|
|
38
|
-
},
|
|
21
|
+
create: (context) => ({
|
|
22
|
+
JSXOpeningElement: (node) => {
|
|
23
|
+
const role = getLiteralPropertyValue(node, ["accessibilityRole", "role"]);
|
|
24
|
+
if (typeof role !== "string" || !ROLES_NEEDING_VALUE.has(role)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (hasSpreadProps(node) || hasProperty(node, VALUE_PROPS)) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
context.report({
|
|
31
|
+
node,
|
|
32
|
+
messageId: "missingAccessibilityValue",
|
|
33
|
+
data: { role },
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
39
37
|
});
|
|
40
38
|
export default adjustableNeedsValue;
|
|
@@ -28,7 +28,7 @@ const gateAnnounceForAccessibility = createRule({
|
|
|
28
28
|
},
|
|
29
29
|
schema: [],
|
|
30
30
|
},
|
|
31
|
-
create(context) {
|
|
31
|
+
create: (context) => {
|
|
32
32
|
/**
|
|
33
33
|
* A bare announce is the legitimate cross-platform pattern. The double
|
|
34
34
|
* announcement only happens where a live region covers Android already, so
|
|
@@ -38,12 +38,12 @@ const gateAnnounceForAccessibility = createRule({
|
|
|
38
38
|
const announces = [];
|
|
39
39
|
const platformChecks = [];
|
|
40
40
|
return {
|
|
41
|
-
JSXOpeningElement(node) {
|
|
41
|
+
JSXOpeningElement: (node) => {
|
|
42
42
|
if (getProperty(node, LIVE_REGION_PROPS)) {
|
|
43
43
|
hasLiveRegion = true;
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
|
-
MemberExpression(node) {
|
|
46
|
+
MemberExpression: (node) => {
|
|
47
47
|
if (node.property.type !== AST_NODE_TYPES.Identifier) {
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
@@ -58,7 +58,7 @@ const gateAnnounceForAccessibility = createRule({
|
|
|
58
58
|
platformChecks.push(node);
|
|
59
59
|
}
|
|
60
60
|
},
|
|
61
|
-
"Program:exit"() {
|
|
61
|
+
"Program:exit": () => {
|
|
62
62
|
if (!hasLiveRegion) {
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
@@ -42,13 +42,13 @@ const inputPurpose = createRule({
|
|
|
42
42
|
},
|
|
43
43
|
],
|
|
44
44
|
},
|
|
45
|
-
create(context) {
|
|
45
|
+
create: (context) => {
|
|
46
46
|
const components = getComponents({
|
|
47
47
|
context,
|
|
48
48
|
defaultComponents: TEXT_INPUTS,
|
|
49
49
|
});
|
|
50
50
|
return {
|
|
51
|
-
JSXOpeningElement(node) {
|
|
51
|
+
JSXOpeningElement: (node) => {
|
|
52
52
|
const componentName = getElementName(node);
|
|
53
53
|
if (componentName === undefined) {
|
|
54
54
|
return;
|
|
@@ -30,28 +30,26 @@ const noAriaLegacyMix = createRule({
|
|
|
30
30
|
},
|
|
31
31
|
schema: [],
|
|
32
32
|
},
|
|
33
|
-
create(context) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
const stateKey = STATE_ALIAS_KEYS[alias] ?? VALUE_ALIAS_KEYS[alias];
|
|
41
|
-
// A grouped prop only clashes on the key the alias maps onto
|
|
42
|
-
const isClashes = stateKey
|
|
43
|
-
? hasStateKey(node, legacy, stateKey)
|
|
44
|
-
: getProperty(node, legacy) !== undefined;
|
|
45
|
-
if (isClashes) {
|
|
46
|
-
context.report({
|
|
47
|
-
node,
|
|
48
|
-
messageId: "ariaLegacyMix",
|
|
49
|
-
data: { alias, legacy },
|
|
50
|
-
});
|
|
51
|
-
}
|
|
33
|
+
create: (context) => ({
|
|
34
|
+
JSXOpeningElement: (node) => {
|
|
35
|
+
for (const [alias, legacy] of Object.entries(ALIAS_TO_LEGACY)) {
|
|
36
|
+
if (!getProperty(node, alias)) {
|
|
37
|
+
continue;
|
|
52
38
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
39
|
+
const stateKey = STATE_ALIAS_KEYS[alias] ?? VALUE_ALIAS_KEYS[alias];
|
|
40
|
+
// A grouped prop only clashes on the key the alias maps onto
|
|
41
|
+
const isClashes = stateKey
|
|
42
|
+
? hasStateKey(node, legacy, stateKey)
|
|
43
|
+
: getProperty(node, legacy) !== undefined;
|
|
44
|
+
if (isClashes) {
|
|
45
|
+
context.report({
|
|
46
|
+
node,
|
|
47
|
+
messageId: "ariaLegacyMix",
|
|
48
|
+
data: { alias, legacy },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
56
54
|
});
|
|
57
55
|
export default noAriaLegacyMix;
|
|
@@ -13,19 +13,17 @@ const noDeprecatedAccessibilityFocus = createRule({
|
|
|
13
13
|
},
|
|
14
14
|
schema: [],
|
|
15
15
|
},
|
|
16
|
-
create(context) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
};
|
|
29
|
-
},
|
|
16
|
+
create: (context) => ({
|
|
17
|
+
// Matched on the property so an aliased import is caught too
|
|
18
|
+
MemberExpression: (node) => {
|
|
19
|
+
if (node.property.type === AST_NODE_TYPES.Identifier &&
|
|
20
|
+
node.property.name === "setAccessibilityFocus") {
|
|
21
|
+
context.report({
|
|
22
|
+
node,
|
|
23
|
+
messageId: "deprecatedSetAccessibilityFocus",
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
30
28
|
});
|
|
31
29
|
export default noDeprecatedAccessibilityFocus;
|
|
@@ -36,13 +36,13 @@ const noDroppedAriaOnText = createRule({
|
|
|
36
36
|
},
|
|
37
37
|
],
|
|
38
38
|
},
|
|
39
|
-
create(context) {
|
|
39
|
+
create: (context) => {
|
|
40
40
|
const components = getComponents({
|
|
41
41
|
context,
|
|
42
42
|
defaultComponents: TEXTS,
|
|
43
43
|
});
|
|
44
44
|
return {
|
|
45
|
-
JSXOpeningElement(node) {
|
|
45
|
+
JSXOpeningElement: (node) => {
|
|
46
46
|
const componentName = getElementName(node);
|
|
47
47
|
if (componentName === undefined) {
|
|
48
48
|
return;
|
|
@@ -19,31 +19,29 @@ const noFontScalingDisabled = createRule({
|
|
|
19
19
|
},
|
|
20
20
|
schema: [],
|
|
21
21
|
},
|
|
22
|
-
create(context) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
};
|
|
47
|
-
},
|
|
22
|
+
create: (context) => ({
|
|
23
|
+
JSXOpeningElement: (node) => {
|
|
24
|
+
const allowFontScaling = getProperty(node, "allowFontScaling");
|
|
25
|
+
if (allowFontScaling &&
|
|
26
|
+
getLiteralPropertyValue(node, "allowFontScaling") === false) {
|
|
27
|
+
context.report({
|
|
28
|
+
node: allowFontScaling,
|
|
29
|
+
messageId: "fontScalingDisabled",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const cap = getProperty(node, "maxFontSizeMultiplier");
|
|
33
|
+
const multiplier = getLiteralPropertyValue(node, "maxFontSizeMultiplier");
|
|
34
|
+
if (cap &&
|
|
35
|
+
typeof multiplier === "number" &&
|
|
36
|
+
multiplier !== NO_MAXIMUM &&
|
|
37
|
+
multiplier < MINIMUM_MULTIPLIER) {
|
|
38
|
+
context.report({
|
|
39
|
+
node: cap,
|
|
40
|
+
messageId: "fontScalingCapped",
|
|
41
|
+
data: { multiplier, minimum: MINIMUM_MULTIPLIER },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
48
46
|
});
|
|
49
47
|
export default noFontScalingDisabled;
|