@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.
Files changed (46) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +17 -1
  3. package/dist/base.js +14 -1
  4. package/dist/jest.d.ts +3 -0
  5. package/dist/jest.js +56 -0
  6. package/dist/plugins/props-destructuring.js +12 -12
  7. package/dist/plugins/react-native-a11y/rules/accessibility-actions-need-handler.js +2 -2
  8. package/dist/plugins/react-native-a11y/rules/accessibility-hint.js +2 -2
  9. package/dist/plugins/react-native-a11y/rules/accessibility-label.js +2 -2
  10. package/dist/plugins/react-native-a11y/rules/accessibility-role.js +2 -2
  11. package/dist/plugins/react-native-a11y/rules/accessibility-value-needs-range.js +25 -27
  12. package/dist/plugins/react-native-a11y/rules/adjustable-needs-value.js +16 -18
  13. package/dist/plugins/react-native-a11y/rules/gate-announce-for-accessibility.js +4 -4
  14. package/dist/plugins/react-native-a11y/rules/input-purpose.js +2 -2
  15. package/dist/plugins/react-native-a11y/rules/no-aria-legacy-mix.js +20 -22
  16. package/dist/plugins/react-native-a11y/rules/no-deprecated-accessibility-focus.js +12 -14
  17. package/dist/plugins/react-native-a11y/rules/no-dropped-aria-on-text.js +2 -2
  18. package/dist/plugins/react-native-a11y/rules/no-font-scaling-disabled.js +24 -26
  19. package/dist/plugins/react-native-a11y/rules/no-ineffective-accessibility-props.js +34 -36
  20. package/dist/plugins/react-native-a11y/rules/no-invalid-accessibility-state.js +31 -33
  21. package/dist/plugins/react-native-a11y/rules/no-nested-focus-stops.js +2 -2
  22. package/dist/plugins/react-native-a11y/rules/no-press-in-activation.js +2 -2
  23. package/dist/plugins/react-native-a11y/rules/no-unnamed-interactive.js +2 -2
  24. package/dist/plugins/react-native-a11y/rules/require-invert-colors-opt-out.js +2 -2
  25. package/dist/plugins/react-native-a11y/rules/require-modal-a11y-on-sheets.js +2 -2
  26. package/dist/plugins/react-native-a11y/utils/get-amount-of-text-children.js +15 -13
  27. package/dist/plugins/rules/boolean-guard.js +3 -3
  28. package/dist/plugins/rules/comment-style.js +43 -34
  29. package/dist/plugins/rules/effect-takes-callback.js +1 -1
  30. package/dist/plugins/rules/jsdoc-for-props.js +1 -1
  31. package/dist/plugins/rules/no-callback-payload.js +18 -15
  32. package/dist/plugins/rules/no-markup-variable.js +41 -35
  33. package/dist/plugins/rules/no-unnecessary-use-callback.js +2 -2
  34. package/dist/plugins/rules/regex-pattern-const.js +1 -1
  35. package/dist/plugins/rules/test-id-default.js +23 -27
  36. package/dist/plugins/rules/test-id-format.js +14 -18
  37. package/dist/plugins/rules/test-id-from-props.js +23 -27
  38. package/dist/plugins/rules/undefined-check.js +2 -2
  39. package/dist/plugins/rules/webview-allow-list.js +22 -26
  40. package/dist/plugins/utils/expressions.d.ts +3 -3
  41. package/dist/plugins/utils/expressions.js +94 -78
  42. package/dist/plugins/utils/types.js +14 -12
  43. package/dist/react-native.js +28 -0
  44. package/dist/shared.d.ts +3 -3
  45. package/dist/shared.js +168 -112
  46. 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
- ## Custom rules
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: { '@hypersolid-dev': hypersolid, 'no-secrets': noSecrets },
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
@@ -0,0 +1,3 @@
1
+ import type { Linter } from 'eslint';
2
+ export declare const JEST_FILES: string[];
3
+ export default function jest(): Linter.Config[];
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
- function isJsx(node) {
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
- function returnsJsx(node) {
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(returnsJsx);
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' && returnsJsx(value));
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
- function unwrap(node) {
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' || !returnsJsx(fn.body) && !isJsx(fn.body) || isUnion(props)) {
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
- return {
23
- JSXOpeningElement(node) {
24
- const attribute = getProperty(node, "accessibilityValue");
25
- if (attribute?.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) {
26
- return;
27
- }
28
- const { expression } = attribute.value;
29
- // A value built elsewhere cannot be read here
30
- if (expression.type !== AST_NODE_TYPES.ObjectExpression) {
31
- return;
32
- }
33
- // A spread may carry the range, and cannot be read from here
34
- if (expression.properties.some((property) => property.type === AST_NODE_TYPES.SpreadElement)) {
35
- return;
36
- }
37
- const keys = keysOf(expression);
38
- if (!keys.includes("now") || keys.includes("text")) {
39
- return;
40
- }
41
- if (keys.includes("min") && keys.includes("max")) {
42
- return;
43
- }
44
- context.report({ node: attribute, messageId: "valueWithoutRange" });
45
- },
46
- };
47
- },
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
- return {
23
- JSXOpeningElement(node) {
24
- const role = getLiteralPropertyValue(node, ["accessibilityRole", "role"]);
25
- if (typeof role !== "string" || !ROLES_NEEDING_VALUE.has(role)) {
26
- return;
27
- }
28
- if (hasSpreadProps(node) || hasProperty(node, VALUE_PROPS)) {
29
- return;
30
- }
31
- context.report({
32
- node,
33
- messageId: "missingAccessibilityValue",
34
- data: { role },
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
- return {
35
- JSXOpeningElement(node) {
36
- for (const [alias, legacy] of Object.entries(ALIAS_TO_LEGACY)) {
37
- if (!getProperty(node, alias)) {
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
- return {
18
- // Matched on the property so an aliased import is caught too
19
- MemberExpression(node) {
20
- if (node.property.type === AST_NODE_TYPES.Identifier &&
21
- node.property.name === "setAccessibilityFocus") {
22
- context.report({
23
- node,
24
- messageId: "deprecatedSetAccessibilityFocus",
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
- return {
24
- JSXOpeningElement(node) {
25
- const allowFontScaling = getProperty(node, "allowFontScaling");
26
- if (allowFontScaling &&
27
- getLiteralPropertyValue(node, "allowFontScaling") === false) {
28
- context.report({
29
- node: allowFontScaling,
30
- messageId: "fontScalingDisabled",
31
- });
32
- }
33
- const cap = getProperty(node, "maxFontSizeMultiplier");
34
- const multiplier = getLiteralPropertyValue(node, "maxFontSizeMultiplier");
35
- if (cap &&
36
- typeof multiplier === "number" &&
37
- multiplier !== NO_MAXIMUM &&
38
- multiplier < MINIMUM_MULTIPLIER) {
39
- context.report({
40
- node: cap,
41
- messageId: "fontScalingCapped",
42
- data: { multiplier, minimum: MINIMUM_MULTIPLIER },
43
- });
44
- }
45
- },
46
- };
47
- },
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;