@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
@@ -8,14 +8,14 @@ const DEFAULT_SELECT_OBJECTS = new Set(['Platform']);
8
8
  const isWrapper = (node) => TS_WRAPPERS.has(node.type);
9
9
  const isFunction = (node) => FUNCTIONS.has(node.type);
10
10
  /** Through `as`, `satisfies`, `!` and `?.`. */
11
- export function unwrap(node) {
11
+ export const unwrap = (node) => {
12
12
  let current = node ?? undefined;
13
13
  while (current && isWrapper(current))
14
14
  current = current.expression;
15
15
  return current;
16
- }
16
+ };
17
17
  /** The name of the memo hook a callee calls (`useMemo`, `React.useMemo`), if it is one. */
18
- function memoHookName(callee) {
18
+ const memoHookName = (callee) => {
19
19
  if (callee.type === AST_NODE_TYPES.Identifier)
20
20
  return MEMO_HOOKS.has(callee.name) ? callee.name : undefined;
21
21
  if (callee.type === AST_NODE_TYPES.MemberExpression &&
@@ -26,29 +26,27 @@ function memoHookName(callee) {
26
26
  return callee.property.name;
27
27
  }
28
28
  return undefined;
29
- }
30
- function findVariable(scope, name) {
29
+ };
30
+ const findVariable = (scope, name) => {
31
31
  for (let current = scope; current; current = current.upper) {
32
32
  const variable = current.set.get(name);
33
33
  if (variable)
34
34
  return variable;
35
35
  }
36
36
  return undefined;
37
- }
38
- function bindingOf(context, identifier) {
39
- return findVariable(context.sourceCode.getScope(identifier), identifier.name);
40
- }
37
+ };
38
+ const bindingOf = (context, identifier) => findVariable(context.sourceCode.getScope(identifier), identifier.name);
41
39
  /** The declarator a name resolves to, when it is a local with an initialiser; `.init` need not be the name's own value. */
42
- function localDeclarator(context, identifier) {
40
+ const localDeclarator = (context, identifier) => {
43
41
  const definition = bindingOf(context, identifier)?.defs[0];
44
42
  return definition?.type === DefinitionType.Variable && definition.node.init ? definition.node : undefined;
45
- }
43
+ };
46
44
  /**
47
45
  * A property's key when it is written literally (`a`, `"a"`, `1`, `["a"]`,
48
46
  * `[\`a\`]`), else null: a computed key that is a variable, a call or a
49
47
  * template with expressions, a spread and a pattern have no key to read.
50
48
  */
51
- export function keyOf(property) {
49
+ export const keyOf = (property) => {
52
50
  if (property.type !== AST_NODE_TYPES.Property)
53
51
  return null;
54
52
  const { key } = property;
@@ -57,13 +55,13 @@ export function keyOf(property) {
57
55
  if (key.type === AST_NODE_TYPES.Literal)
58
56
  return String(key.value);
59
57
  return key.type === AST_NODE_TYPES.TemplateLiteral && key.expressions.length === 0 ? (key.quasis[0]?.value.cooked ?? null) : null;
60
- }
58
+ };
61
59
  /**
62
60
  * The expression a destructuring pattern hands to `name`, read off a literal
63
61
  * initialiser. A non-literal value, a rest element, a computed key and a spread
64
62
  * before the wanted element all give up, since the value is not in this file.
65
63
  */
66
- function picked(pattern, init, name) {
64
+ const picked = (pattern, init, name) => {
67
65
  if (pattern.type === AST_NODE_TYPES.Identifier)
68
66
  return pattern.name === name ? (init ?? undefined) : undefined;
69
67
  // The default only applies when the source has nothing there.
@@ -71,37 +69,44 @@ function picked(pattern, init, name) {
71
69
  return picked(pattern.left, init ?? pattern.right, name);
72
70
  const source = unwrap(init);
73
71
  if (pattern.type === AST_NODE_TYPES.ObjectPattern && source?.type === AST_NODE_TYPES.ObjectExpression) {
74
- for (const property of pattern.properties) {
75
- const wanted = keyOf(property);
76
- if (wanted === null || property.type !== AST_NODE_TYPES.Property)
77
- continue;
78
- const entry = source.properties.find((candidate) => keyOf(candidate) === wanted);
79
- const found = picked(property.value, entry?.type === AST_NODE_TYPES.Property ? entry.value : undefined, name);
80
- if (found)
81
- return found;
82
- }
83
- return undefined;
72
+ return pickedFromObject(pattern, source, name);
84
73
  }
85
74
  if (pattern.type === AST_NODE_TYPES.ArrayPattern && source?.type === AST_NODE_TYPES.ArrayExpression) {
86
- for (const [index, element] of pattern.elements.entries()) {
87
- if (!element)
88
- continue;
89
- const before = source.elements.slice(0, index + 1);
90
- if (before.some((entry) => entry?.type === AST_NODE_TYPES.SpreadElement))
91
- continue;
92
- const found = picked(element, source.elements[index], name);
93
- if (found)
94
- return found;
95
- }
75
+ return pickedFromArray(pattern, source, name);
76
+ }
77
+ return undefined;
78
+ };
79
+ const pickedFromObject = (pattern, source, name) => {
80
+ for (const property of pattern.properties) {
81
+ const wanted = keyOf(property);
82
+ if (wanted === null || property.type !== AST_NODE_TYPES.Property)
83
+ continue;
84
+ const entry = source.properties.find((candidate) => keyOf(candidate) === wanted);
85
+ const found = picked(property.value, entry?.type === AST_NODE_TYPES.Property ? entry.value : undefined, name);
86
+ if (found)
87
+ return found;
88
+ }
89
+ return undefined;
90
+ };
91
+ const pickedFromArray = (pattern, source, name) => {
92
+ for (const [index, element] of pattern.elements.entries()) {
93
+ if (!element)
94
+ continue;
95
+ const before = source.elements.slice(0, index + 1);
96
+ if (before.some((entry) => entry?.type === AST_NODE_TYPES.SpreadElement))
97
+ continue;
98
+ const found = picked(element, source.elements[index], name);
99
+ if (found)
100
+ return found;
96
101
  }
97
102
  return undefined;
98
- }
103
+ };
99
104
  /**
100
105
  * What a name stands for: a `const`'s own value, or a declared function itself.
101
106
  * A `let` or `var` is not followed, and a destructured `const` resolves only
102
107
  * when its initialiser is a literal holding the matching entry.
103
108
  */
104
- function boundTo(context, identifier) {
109
+ const boundTo = (context, identifier) => {
105
110
  const definition = bindingOf(context, identifier)?.defs[0];
106
111
  if (definition?.type === DefinitionType.FunctionName)
107
112
  return definition.node;
@@ -111,17 +116,17 @@ function boundTo(context, identifier) {
111
116
  return undefined;
112
117
  const { id, init } = definition.node;
113
118
  return id.type === AST_NODE_TYPES.Identifier ? init : picked(id, init, identifier.name);
114
- }
115
- function selectBranches(node, selectObjects) {
119
+ };
120
+ const selectBranches = (node, selectObjects) => {
116
121
  const { callee } = node;
117
122
  if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed)
118
123
  return undefined;
119
124
  if (callee.property.name !== 'select' || callee.object.type !== AST_NODE_TYPES.Identifier)
120
125
  return undefined;
121
126
  return selectObjects.has(callee.object.name) ? node.arguments[0] : undefined;
122
- }
127
+ };
123
128
  /** Every `return` with an argument in a body, including nested blocks but not nested functions. */
124
- function returnsIn(body) {
129
+ const returnsIn = (body) => {
125
130
  const found = [];
126
131
  const visit = (value) => {
127
132
  if (Array.isArray(value)) {
@@ -146,7 +151,7 @@ function returnsIn(body) {
146
151
  };
147
152
  visit(body);
148
153
  return found;
149
- }
154
+ };
150
155
  /**
151
156
  * Every leaf an expression can produce.
152
157
  *
@@ -160,7 +165,7 @@ function returnsIn(body) {
160
165
  * itself: a name from another file, a `let`, a method call, a computed member,
161
166
  * a spread. A cycle yields nothing rather than looping.
162
167
  */
163
- export function leavesOf(context, node, options = {}) {
168
+ export const leavesOf = (context, node, options = {}) => {
164
169
  const { names = true, members = true, returns = false, selectObjects = DEFAULT_SELECT_OBJECTS, calls, seen = new Set(), } = options;
165
170
  const out = [];
166
171
  const walkFunction = (fn) => {
@@ -171,31 +176,38 @@ export function leavesOf(context, node, options = {}) {
171
176
  for (const argument of returnsIn(fn.body))
172
177
  walk(argument);
173
178
  };
179
+ /** Each branch is a value for whatever slot the call sits in. */
180
+ const walkSelect = (branches) => {
181
+ for (const branch of leavesOf(context, branches, { ...options, seen })) {
182
+ if (branch.type !== AST_NODE_TYPES.ObjectExpression)
183
+ continue;
184
+ for (const property of branch.properties) {
185
+ if (property.type === AST_NODE_TYPES.Property)
186
+ walk(property.value);
187
+ }
188
+ }
189
+ };
190
+ /** A call to a local function is what it returns (a factory); a method call is never followed. */
191
+ const factoryOf = (call) => {
192
+ if (!returns || call.callee.type !== AST_NODE_TYPES.Identifier)
193
+ return undefined;
194
+ const bound = boundTo(context, call.callee);
195
+ return bound && isFunction(bound) ? bound : undefined;
196
+ };
174
197
  const didWalkCall = (call) => {
175
198
  const branches = selectBranches(call, selectObjects);
176
199
  if (branches) {
177
- // Each branch is a value for whatever slot the call sits in.
178
- for (const branch of leavesOf(context, branches, { ...options, seen })) {
179
- if (branch.type !== AST_NODE_TYPES.ObjectExpression)
180
- continue;
181
- for (const property of branch.properties) {
182
- if (property.type === AST_NODE_TYPES.Property)
183
- walk(property.value);
184
- }
185
- }
200
+ walkSelect(branches);
186
201
  return true;
187
202
  }
188
203
  if (calls?.(call)) {
189
204
  walk(call.arguments[0]);
190
205
  return true;
191
206
  }
192
- // A call to a local function is what it returns (a factory); a method call is never followed.
193
- if (returns && call.callee.type === AST_NODE_TYPES.Identifier) {
194
- const bound = boundTo(context, call.callee);
195
- if (bound && isFunction(bound)) {
196
- walk(bound);
197
- return true;
198
- }
207
+ const factory = factoryOf(call);
208
+ if (factory) {
209
+ walk(factory);
210
+ return true;
199
211
  }
200
212
  const hook = memoHookName(call.callee);
201
213
  const callback = call.arguments.at(0);
@@ -208,6 +220,25 @@ export function leavesOf(context, node, options = {}) {
208
220
  walkFunction(callback);
209
221
  return true;
210
222
  };
223
+ /** `OBJ.key`'s value when `OBJ` is a local `const` object literal. */
224
+ const memberEntry = (member) => {
225
+ if (member.computed || member.object.type !== AST_NODE_TYPES.Identifier)
226
+ return undefined;
227
+ const declarator = localDeclarator(context, member.object);
228
+ // A destructured name is not what the whole initialiser holds.
229
+ const held = declarator?.id.type === AST_NODE_TYPES.Identifier ? unwrap(declarator.init) : undefined;
230
+ if (held?.type !== AST_NODE_TYPES.ObjectExpression)
231
+ return undefined;
232
+ const key = member.property.name;
233
+ const entry = held.properties.find((property) => keyOf(property) === key);
234
+ return entry?.type === AST_NODE_TYPES.Property ? entry.value : undefined;
235
+ };
236
+ /** What a name or `OBJ.key` stands for, when the options let it be followed. */
237
+ const referenced = (node) => {
238
+ if (node.type === AST_NODE_TYPES.Identifier)
239
+ return names ? boundTo(context, node) : undefined;
240
+ return members ? memberEntry(node) : undefined;
241
+ };
211
242
  const walk = (current) => {
212
243
  if (!current || seen.has(current))
213
244
  return;
@@ -229,26 +260,11 @@ export function leavesOf(context, node, options = {}) {
229
260
  walk(current.left);
230
261
  walk(current.right);
231
262
  return;
232
- case AST_NODE_TYPES.Identifier: {
233
- const bound = names ? boundTo(context, current) : undefined;
234
- if (bound) {
235
- walk(bound);
236
- return;
237
- }
238
- break;
239
- }
263
+ case AST_NODE_TYPES.Identifier:
240
264
  case AST_NODE_TYPES.MemberExpression: {
241
- if (!members || current.computed || current.object.type !== AST_NODE_TYPES.Identifier)
242
- break;
243
- const declarator = localDeclarator(context, current.object);
244
- // A destructured name is not what the whole initialiser holds.
245
- const held = declarator?.id.type === AST_NODE_TYPES.Identifier ? unwrap(declarator.init) : undefined;
246
- if (held?.type !== AST_NODE_TYPES.ObjectExpression)
247
- break;
248
- const key = current.property.name;
249
- const entry = held.properties.find((property) => keyOf(property) === key);
250
- if (entry?.type === AST_NODE_TYPES.Property) {
251
- walk(entry.value);
265
+ const target = referenced(current);
266
+ if (target) {
267
+ walk(target);
252
268
  return;
253
269
  }
254
270
  break;
@@ -271,4 +287,4 @@ export function leavesOf(context, node, options = {}) {
271
287
  };
272
288
  walk(node);
273
289
  return out;
274
- }
290
+ };
@@ -21,6 +21,18 @@ const UNRESOLVED = TypeFlags.TypeParameter |
21
21
  TypeFlags.Conditional |
22
22
  TypeFlags.Index |
23
23
  TypeFlags.Substitution;
24
+ /**
25
+ * A callable or constructable type is truthy. Any other object type is only
26
+ * as good as what the falsy primitives do against it: `{}`, `Object` and
27
+ * `{ length: number }` all accept `""`, `0` or `false`.
28
+ */
29
+ const isTruthyObject = (type, checker) => {
30
+ if (type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0)
31
+ return true;
32
+ if (!checker)
33
+ return false;
34
+ return [checker.getStringLiteralType(""), checker.getNumberLiteralType(0), checker.getFalseType()].every((falsy) => !checker.isTypeAssignableTo(falsy, type));
35
+ };
24
36
  /**
25
37
  * Whether every value of this type is truthy, `undefined` aside.
26
38
  *
@@ -50,18 +62,8 @@ export const isSameAsBangBang = (type, checker) => {
50
62
  if (type.flags & (TypeFlags.ESSymbolLike | TypeFlags.NonPrimitive)) {
51
63
  return true;
52
64
  }
53
- if (type.flags & TypeFlags.Object) {
54
- /**
55
- * A callable or constructable type is truthy. Any other object type is only
56
- * as good as what the falsy primitives do against it: `{}`, `Object` and
57
- * `{ length: number }` all accept `""`, `0` or `false`.
58
- */
59
- if (type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0)
60
- return true;
61
- if (!checker)
62
- return false;
63
- return [checker.getStringLiteralType(""), checker.getNumberLiteralType(0), checker.getFalseType()].every((falsy) => !checker.isTypeAssignableTo(falsy, type));
64
- }
65
+ if (type.flags & TypeFlags.Object)
66
+ return isTruthyObject(type, checker);
65
67
  // A literal is as truthy as its value: only `""`, `0` and `0n` are falsy.
66
68
  if (type.isStringLiteral())
67
69
  return type.value !== "";
@@ -1,4 +1,5 @@
1
1
  import expo from 'eslint-plugin-expo';
2
+ import reactDoctor from 'eslint-plugin-react-doctor';
2
3
  import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript';
3
4
  import { createNodeResolver } from 'eslint-plugin-import-x';
4
5
  import globals from 'globals';
@@ -152,6 +153,33 @@ export default function reactNative() {
152
153
  '@hypersolid-dev/test-id-default': 'error',
153
154
  },
154
155
  },
156
+ {
157
+ // React Doctor's React Native rules, as used in the ESPN Goal pilot.
158
+ name: '@hypersolid-dev/react-native/react-doctor',
159
+ files: CODE_FILES,
160
+ ignores: TEST_FILES,
161
+ plugins: { 'react-doctor': reactDoctor },
162
+ rules: {
163
+ // A state update per scroll event re-renders mid-gesture; track position in a shared value. Bad: `onScroll={(event) => setOffset(event.nativeEvent.contentOffset.y)}`.
164
+ 'react-doctor/rn-no-scroll-state': 'error',
165
+ // A reaction that only copies one value into another is a `useDerivedValue`. Bad: `useAnimatedReaction(() => x.value, (value) => { y.value = value; })`.
166
+ 'react-doctor/rn-animation-reaction-as-derived': 'error',
167
+ // A string outside `<Text>` crashes on native. Bad: `<View>Hello</View>`.
168
+ 'react-doctor/rn-no-raw-text': 'error',
169
+ // A list keys rows from `keyExtractor`; a `key` on the row is ignored. Bad: `renderItem={({ item }) => <Row key={item.id} />}`.
170
+ 'react-doctor/rn-no-renderitem-key': 'error',
171
+ // A press animation set from `onPressIn` runs on the JS thread. Bad: `onPressIn={() => { scale.value = 0.9; }}`.
172
+ 'react-doctor/rn-pressable-shared-value-mutation': 'error',
173
+ // `PanResponder` runs on the JS thread; `Gesture.Pan()` runs on the UI thread. Bad: `PanResponder.create({ … })`.
174
+ 'react-doctor/rn-no-panresponder': 'error',
175
+ // `Dimensions.get()` reads the size once and misses rotation and split screen. Bad: `const { width } = Dimensions.get('window')` (use `useWindowDimensions`).
176
+ 'react-doctor/rn-no-dimensions-get': 'error',
177
+ // `react-native/Libraries/…` paths are private and move on upgrade. Bad: `import … from 'react-native/Libraries/Image/resolveAssetSource'`.
178
+ 'react-doctor/rn-no-deep-imports': 'error',
179
+ // Unmaintained Expo-era packages each have a successor. Bad: `import { Audio } from 'expo-av'` (use `expo-audio`).
180
+ 'react-doctor/rn-no-legacy-expo-packages': 'error',
181
+ },
182
+ },
155
183
  {
156
184
  name: '@hypersolid-dev/react-native/json',
157
185
  files: ['**/*.json'],
package/dist/shared.d.ts CHANGED
@@ -31,6 +31,6 @@ export declare const NAMING_CONVENTION: ({
31
31
  format: string[];
32
32
  modifiers?: undefined;
33
33
  })[];
34
- export declare function raiseWarnings(configs: Linter.Config[]): Linter.Config[];
35
- export declare function scopeToCode(configs: readonly Linter.Config[]): Linter.Config[];
36
- export declare function reactConfigs(): Linter.Config[];
34
+ export declare const raiseWarnings: (configs: Linter.Config[]) => Linter.Config[];
35
+ export declare const scopeToCode: (configs: readonly Linter.Config[]) => Linter.Config[];
36
+ export declare const reactConfigs: () => Linter.Config[];