@blumintinc/eslint-plugin-blumint 1.15.0 → 1.16.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 (37) hide show
  1. package/README.md +28 -5
  2. package/lib/index.js +7 -1
  3. package/lib/rules/consistent-callback-naming.js +26 -0
  4. package/lib/rules/dynamic-https-errors.js +5 -26
  5. package/lib/rules/enforce-boolean-naming-prefixes.d.ts +1 -0
  6. package/lib/rules/enforce-boolean-naming-prefixes.js +86 -119
  7. package/lib/rules/enforce-dynamic-imports.d.ts +2 -1
  8. package/lib/rules/enforce-dynamic-imports.js +42 -21
  9. package/lib/rules/enforce-memoize-async.js +1 -4
  10. package/lib/rules/enforce-mui-rounded-icons.js +42 -1
  11. package/lib/rules/enforce-verb-noun-naming.js +3 -0
  12. package/lib/rules/global-const-style.js +9 -0
  13. package/lib/rules/logical-top-to-bottom-grouping.js +80 -2
  14. package/lib/rules/memo-compare-deeply-complex-props.js +167 -3
  15. package/lib/rules/memo-nested-react-components.js +143 -8
  16. package/lib/rules/no-array-length-in-deps.js +74 -3
  17. package/lib/rules/no-circular-references.js +145 -482
  18. package/lib/rules/no-compositing-layer-props.js +31 -0
  19. package/lib/rules/no-entire-object-hook-deps.js +132 -97
  20. package/lib/rules/no-explicit-return-type.js +6 -0
  21. package/lib/rules/no-hungarian.js +119 -24
  22. package/lib/rules/no-margin-properties.js +7 -38
  23. package/lib/rules/no-unnecessary-verb-suffix.js +79 -0
  24. package/lib/rules/no-unused-props.js +215 -37
  25. package/lib/rules/no-useless-fragment.js +10 -2
  26. package/lib/rules/parallelize-async-operations.js +1 -3
  27. package/lib/rules/prefer-type-alias-over-typeof-constant.js +73 -11
  28. package/lib/rules/prefer-use-deep-compare-memo.js +8 -14
  29. package/lib/rules/react-memoize-literals.js +87 -1
  30. package/lib/rules/require-migration-script-metadata.d.ts +9 -0
  31. package/lib/rules/require-migration-script-metadata.js +206 -0
  32. package/lib/rules/warn-https-error-message-user-friendly.d.ts +1 -0
  33. package/lib/rules/warn-https-error-message-user-friendly.js +239 -0
  34. package/lib/utils/ASTHelpers.d.ts +15 -0
  35. package/lib/utils/ASTHelpers.js +48 -0
  36. package/package.json +7 -6
  37. package/release-manifest.json +166 -0
@@ -67,7 +67,13 @@ exports.noMarginProperties = (0, createRule_1.createRule)({
67
67
  const normalizedName = normalizePropertyName(propertyName);
68
68
  return MARGIN_PROPERTIES.has(normalizedName);
69
69
  }
70
- // Check if a node is within an sx prop context or theme override context
70
+ /**
71
+ * True if node is in a flagged MUI styling context (sx attribute, sx
72
+ * object property, or css() call). Excludes createTheme styleOverrides:
73
+ * theme margins ARE the container-controlled styling (viewport insets,
74
+ * resets of MUI's built-in margins, internal layout), not the
75
+ * sibling-spacing this rule targets.
76
+ */
71
77
  function isMuiStylingContext(node) {
72
78
  let current = node;
73
79
  while (current?.parent) {
@@ -83,13 +89,6 @@ exports.noMarginProperties = (0, createRule_1.createRule)({
83
89
  current.parent.key.name === 'sx') {
84
90
  return true;
85
91
  }
86
- // Check for theme overrides (MUI's createTheme)
87
- if (current.parent.type === utils_1.AST_NODE_TYPES.Property &&
88
- current.parent.key.type === utils_1.AST_NODE_TYPES.Identifier &&
89
- (current.parent.key.name === 'styleOverrides' ||
90
- current.parent.key.name === 'components')) {
91
- return true;
92
- }
93
92
  // Check for MUI's css function
94
93
  if (current.parent.type === utils_1.AST_NODE_TYPES.CallExpression &&
95
94
  current.parent.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
@@ -290,36 +289,6 @@ exports.noMarginProperties = (0, createRule_1.createRule)({
290
289
  checkObjectExpression(arg);
291
290
  }
292
291
  }
293
- // Handle createTheme for MUI theme overrides
294
- if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
295
- node.callee.name === 'createTheme' &&
296
- node.arguments.length > 0 &&
297
- node.arguments[0].type === utils_1.AST_NODE_TYPES.ObjectExpression) {
298
- const themeObj = node.arguments[0];
299
- // Find components property in theme object
300
- const componentsProperty = themeObj.properties.find((prop) => prop.type === utils_1.AST_NODE_TYPES.Property &&
301
- prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
302
- prop.key.name === 'components' &&
303
- prop.value.type === utils_1.AST_NODE_TYPES.ObjectExpression);
304
- if (componentsProperty &&
305
- componentsProperty.value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
306
- // Check each component override
307
- componentsProperty.value.properties.forEach((componentProp) => {
308
- if (componentProp.type === utils_1.AST_NODE_TYPES.Property &&
309
- componentProp.value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
310
- // Find styleOverrides property
311
- const styleOverrides = componentProp.value.properties.find((prop) => prop.type === utils_1.AST_NODE_TYPES.Property &&
312
- prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
313
- prop.key.name === 'styleOverrides' &&
314
- prop.value.type === utils_1.AST_NODE_TYPES.ObjectExpression);
315
- if (styleOverrides &&
316
- styleOverrides.value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
317
- checkObjectExpression(styleOverrides.value);
318
- }
319
- }
320
- });
321
- }
322
- }
323
292
  },
324
293
  };
325
294
  },
@@ -63,6 +63,77 @@ const COMMON_PREPOSITION_SUFFIXES = new Set([
63
63
  'Async',
64
64
  'Sync',
65
65
  ]);
66
+ /**
67
+ * Phrasal-verb particles that fuse with a preceding past participle to form an
68
+ * inseparable state adjective (e.g. "signed in", "logged in", "opted in",
69
+ * "logged out", "zoomed in"). When such a particle is the trailing suffix AND
70
+ * the token before it is a past participle, the ending is NOT a redundant
71
+ * verb-preposition action suffix — it is a single adjective describing state.
72
+ */
73
+ const PHRASAL_PARTICLES = new Set(['In', 'On', 'Out', 'Up', 'Off', 'Down']);
74
+ /**
75
+ * Verb stems that fuse with a phrasal particle into an established phrasal verb
76
+ * where the particle is inseparable (e.g. "signIn", "logOut", "optIn",
77
+ * "checkIn"). Matched in base form ("signIn", "useGuardSignIn") and
78
+ * past-participle form ("signedIn", "loggedOut", "droppedIn"). A particle
79
+ * preceded by a NOUN object instead — "searchItemsIn", "processEventOn",
80
+ * "loadEmbedIn", "isWidgetIn" — is a genuine redundant verb-preposition suffix
81
+ * and stays flagged. Extend this set when a new phrasal verb appears.
82
+ */
83
+ const PHRASAL_VERB_STEMS = new Set([
84
+ 'sign',
85
+ 'log',
86
+ 'opt',
87
+ 'check',
88
+ 'zoom',
89
+ 'drop',
90
+ 'shut',
91
+ 'turn',
92
+ 'switch',
93
+ 'scroll',
94
+ ]);
95
+ /**
96
+ * Resolves the lowercased phrasal-verb stem of a final camelCase word, or null
97
+ * when it is not a known phrasal verb. Handles the base form ("sign"), the
98
+ * regular past participle ("signed" → "sign"), and the doubled-consonant
99
+ * participle ("dropped" → "drop"). A noun that merely ends in "ed" (e.g.
100
+ * "embed" → "emb", "shed" → "sh") resolves to null, so it stays flagged — this
101
+ * is what keeps a leaky "ends in ed" heuristic from exempting genuine targets.
102
+ */
103
+ function phrasalVerbStem(lastWord) {
104
+ const word = lastWord.toLowerCase();
105
+ if (PHRASAL_VERB_STEMS.has(word)) {
106
+ return word;
107
+ }
108
+ if (word.endsWith('ed')) {
109
+ let stem = word.slice(0, -2);
110
+ if (stem.length >= 2 && stem[stem.length - 1] === stem[stem.length - 2]) {
111
+ stem = stem.slice(0, -1);
112
+ }
113
+ if (PHRASAL_VERB_STEMS.has(stem)) {
114
+ return stem;
115
+ }
116
+ }
117
+ return null;
118
+ }
119
+ /**
120
+ * Returns true when the trailing `suffix` of `name` is a phrasal-verb particle
121
+ * fused to its verb — a past-participle adjective ("signedIn", "loggedOut") or a
122
+ * base-form phrasal verb ("signIn", "logOut"). The word immediately before the
123
+ * particle must be a KNOWN phrasal verb (via phrasalVerbStem), not merely any
124
+ * word ending in "ed"; that keeps redundant suffixes where a noun object
125
+ * precedes the particle ("loadEmbedIn", "isWidgetIn", "searchItemsIn") flagged.
126
+ * This single check also covers boolean predicates: "isSignedIn" resolves the
127
+ * pre-particle "signed" → "sign", while "isWidgetIn" resolves "widget" → null.
128
+ */
129
+ function isPhrasalVerbEnding(name, suffix) {
130
+ if (!PHRASAL_PARTICLES.has(suffix)) {
131
+ return false;
132
+ }
133
+ const beforeSuffix = name.substring(0, name.length - suffix.length);
134
+ const lastWord = beforeSuffix.match(/[A-Z]?[a-z]+$/)?.[0] ?? '';
135
+ return phrasalVerbStem(lastWord) !== null;
136
+ }
66
137
  exports.noUnnecessaryVerbSuffix = (0, createRule_1.createRule)({
67
138
  name: 'no-unnecessary-verb-suffix',
68
139
  meta: {
@@ -94,6 +165,14 @@ exports.noUnnecessaryVerbSuffix = (0, createRule_1.createRule)({
94
165
  // Skip if the suggestion would be empty or just a single character
95
166
  if (suggestion.length <= 1)
96
167
  continue;
168
+ // Skip phrasal-verb endings (e.g. past-participle adjectives
169
+ // "signedIn"/"loggedOut" or compound phrasal verbs "signIn"/
170
+ // "logOut"): the trailing particle fuses with its verb, so stripping
171
+ // it ("signed", "sign") destroys the meaning. The pre-particle word
172
+ // must be a known phrasal verb, so noun-object endings like
173
+ // "loadEmbedIn"/"isWidgetIn" remain flagged.
174
+ if (isPhrasalVerbEnding(name, suffix))
175
+ continue;
97
176
  context.report({
98
177
  node,
99
178
  messageId: 'unnecessaryVerbSuffix',
@@ -118,6 +118,45 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
118
118
  }
119
119
  return null;
120
120
  };
121
+ /**
122
+ * Resolves the `*Props` type name from a parameter's type-annotation node.
123
+ * Unwraps parenthesized types and descends through generic wrapper type
124
+ * arguments (e.g. `Readonly<Foo>`, `Partial<...>`, `Readonly<Partial<Foo>>`)
125
+ * to find the underlying `*Props` reference. Returns the first `*Props`
126
+ * identifier found, or `null`. Uses the same `endsWith('Props')` heuristic
127
+ * as the direct path so forward-declared Props types still resolve.
128
+ */
129
+ const resolvePropsTypeName = (typeNode, visited = new Set()) => {
130
+ if (!typeNode || visited.has(typeNode)) {
131
+ return null;
132
+ }
133
+ visited.add(typeNode);
134
+ if (isTSParenthesizedType(typeNode)) {
135
+ return resolvePropsTypeName(typeNode.typeAnnotation, visited);
136
+ }
137
+ if (typeNode.type !== utils_1.AST_NODE_TYPES.TSTypeReference) {
138
+ return null;
139
+ }
140
+ if (typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
141
+ typeNode.typeName.name.endsWith('Props')) {
142
+ return typeNode.typeName.name;
143
+ }
144
+ // Generic wrapper (Readonly/Partial/etc.): descend into its type arguments
145
+ // to find the nested *Props reference. typeParameters is the v5 AST shape;
146
+ // typeArguments is the v6+ shape — handle both for forward compatibility.
147
+ const typeArgs = typeNode.typeParameters ??
148
+ typeNode
149
+ .typeArguments;
150
+ if (typeArgs) {
151
+ for (const arg of typeArgs.params) {
152
+ const resolved = resolvePropsTypeName(arg, visited);
153
+ if (resolved) {
154
+ return resolved;
155
+ }
156
+ }
157
+ }
158
+ return null;
159
+ };
121
160
  const isAnyPropFromSpreadTypeUsed = (spreadTypeName, used, knownProps) => {
122
161
  const spreadTypeProps = spreadTypeToPropNames.get(spreadTypeName);
123
162
  if (!spreadTypeProps || spreadTypeProps.size === 0) {
@@ -170,6 +209,131 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
170
209
  }
171
210
  return false;
172
211
  };
212
+ /**
213
+ * Records which props an ObjectPattern consumes into `used`. A rest element
214
+ * (`...rest`) consumes all remaining props, so it flips `restUsed` and also
215
+ * marks spread-type keys used. Renamed bindings (`{ a: localA }`) still mark
216
+ * the original prop name `a`. Returns whether a rest element was present.
217
+ */
218
+ const collectUsedFromObjectPattern = (pattern, typeName, used) => {
219
+ let restUsed = false;
220
+ pattern.properties.forEach((prop) => {
221
+ if (prop.type === utils_1.AST_NODE_TYPES.Property) {
222
+ const propName = getStaticPropName(prop.key);
223
+ if (propName) {
224
+ used.add(propName);
225
+ }
226
+ }
227
+ else if (prop.type === utils_1.AST_NODE_TYPES.RestElement &&
228
+ prop.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
229
+ // Rest spread (`{...rest}`): all remaining props are forwarded ⇒ used.
230
+ // Spread-type markers are also flagged so the spread-forwarding skip
231
+ // applies; remaining plain props are handled at report time via
232
+ // `restUsed` (the props map may be incomplete here on forward refs).
233
+ restUsed = true;
234
+ const propsType = propsTypes.get(typeName);
235
+ if (propsType) {
236
+ Object.keys(propsType).forEach((key) => {
237
+ if (key.startsWith('...')) {
238
+ used.add(key);
239
+ }
240
+ });
241
+ }
242
+ }
243
+ });
244
+ return restUsed;
245
+ };
246
+ /**
247
+ * Whether an Identifier node is a value-position *reference* to a variable
248
+ * (vs. a declaration binding, an object-property key, or the property of a
249
+ * member expression like `foo.paramName`). Used to detect opaque param use.
250
+ */
251
+ const isParamReference = (parent, parentKey) => {
252
+ if (!parent) {
253
+ return false;
254
+ }
255
+ // `foo.paramName` — the identifier is the member's property, not the param.
256
+ if (parent.type === utils_1.AST_NODE_TYPES.MemberExpression &&
257
+ parentKey === 'property' &&
258
+ !parent.computed) {
259
+ return false;
260
+ }
261
+ // Non-shorthand object property key: `{ paramName: x }`.
262
+ if (parent.type === utils_1.AST_NODE_TYPES.Property &&
263
+ parentKey === 'key' &&
264
+ !parent.computed) {
265
+ return false;
266
+ }
267
+ // A binding position (declaration id, function param, etc.).
268
+ if ((parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
269
+ parentKey === 'id') ||
270
+ parentKey === 'params') {
271
+ return false;
272
+ }
273
+ return true;
274
+ };
275
+ const collectBodyDestructuring = (body, paramName, typeName, used) => {
276
+ const result = {
277
+ destructureCount: 0,
278
+ restUsed: false,
279
+ hasOpaqueUsage: false,
280
+ };
281
+ const declaresParamName = (declarators) => declarators.some((d) => d.id.type === utils_1.AST_NODE_TYPES.Identifier && d.id.name === paramName);
282
+ /** Identifier nodes that are the `init` of a `const {} = props` declarator. */
283
+ const destructureInits = new Set();
284
+ const visit = (node, parent, parentKey) => {
285
+ if (!node || typeof node.type !== 'string') {
286
+ return;
287
+ }
288
+ if (node.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
289
+ if (node.id.type === utils_1.AST_NODE_TYPES.ObjectPattern &&
290
+ node.init?.type === utils_1.AST_NODE_TYPES.Identifier &&
291
+ node.init.name === paramName) {
292
+ destructureInits.add(node.init);
293
+ result.destructureCount += 1;
294
+ if (collectUsedFromObjectPattern(node.id, typeName, used)) {
295
+ result.restUsed = true;
296
+ }
297
+ }
298
+ }
299
+ // A bare reference to the param that is NOT a destructuring init is an
300
+ // opaque consumption (member access, spread, argument, return, etc.).
301
+ if (node.type === utils_1.AST_NODE_TYPES.Identifier &&
302
+ node.name === paramName &&
303
+ !destructureInits.has(node) &&
304
+ isParamReference(parent, parentKey)) {
305
+ result.hasOpaqueUsage = true;
306
+ }
307
+ // Stop descending into nested functions that re-bind `paramName`, since
308
+ // that shadowing binding refers to a different variable.
309
+ if ((node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
310
+ node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
311
+ node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) &&
312
+ node.params.some((p) => p.type === utils_1.AST_NODE_TYPES.Identifier && p.name === paramName)) {
313
+ return;
314
+ }
315
+ if (node.type === utils_1.AST_NODE_TYPES.VariableDeclaration &&
316
+ declaresParamName(node.declarations)) {
317
+ // A `const <paramName> = ...` rebind shadows the param from here on.
318
+ return;
319
+ }
320
+ for (const key of Object.keys(node)) {
321
+ if (key === 'parent') {
322
+ continue;
323
+ }
324
+ const value = node[key];
325
+ if (Array.isArray(value)) {
326
+ value.forEach((child) => visit(child, node, key));
327
+ }
328
+ else if (value &&
329
+ typeof value.type === 'string') {
330
+ visit(value, node, key);
331
+ }
332
+ }
333
+ };
334
+ visit(body, null, null);
335
+ return result;
336
+ };
173
337
  const reportUnusedProps = (typeName, used, restUsed) => {
174
338
  const propsType = propsTypes.get(typeName);
175
339
  if (!propsType) {
@@ -190,6 +354,11 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
190
354
  if (isGenericTypeSpread(prop)) {
191
355
  shouldReport = false;
192
356
  }
357
+ else if (restUsed) {
358
+ // An explicit rest element (`{ a, ...rest }`) captures every prop not
359
+ // named before it, so the remaining props are forwarded ⇒ used.
360
+ shouldReport = false;
361
+ }
193
362
  else if (prop.startsWith('...')) {
194
363
  shouldReport = !shouldSkipSpreadType(prop, hasRestSpread, used, knownProps);
195
364
  }
@@ -590,45 +759,54 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
590
759
  }
591
760
  },
592
761
  VariableDeclaration(node) {
593
- if (node.declarations.length === 1) {
594
- const declaration = node.declarations[0];
595
- if (declaration.init?.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
596
- const param = declaration.init.params[0];
597
- if (param?.type === utils_1.AST_NODE_TYPES.ObjectPattern &&
598
- param.typeAnnotation?.typeAnnotation.type ===
599
- utils_1.AST_NODE_TYPES.TSTypeReference &&
600
- param.typeAnnotation.typeAnnotation.typeName.type ===
601
- utils_1.AST_NODE_TYPES.Identifier) {
602
- const typeName = param.typeAnnotation.typeAnnotation.typeName.name;
603
- if (typeName.endsWith('Props')) {
604
- const used = new Set();
605
- let restUsed = false;
606
- param.properties.forEach((prop) => {
607
- if (prop.type === utils_1.AST_NODE_TYPES.Property) {
608
- const propName = getStaticPropName(prop.key);
609
- if (propName) {
610
- used.add(propName);
611
- }
612
- }
613
- else if (prop.type === utils_1.AST_NODE_TYPES.RestElement &&
614
- prop.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
615
- // Handle rest spread operator {...rest}
616
- // When a rest operator is used, all remaining props are considered used
617
- restUsed = true;
618
- const propsType = propsTypes.get(typeName);
619
- if (propsType) {
620
- Object.keys(propsType).forEach((key) => {
621
- if (key.startsWith('...')) {
622
- used.add(key);
623
- }
624
- });
625
- }
626
- }
627
- });
628
- currentComponent = { node, typeName, used, restUsed };
629
- }
762
+ if (node.declarations.length !== 1) {
763
+ return;
764
+ }
765
+ const declaration = node.declarations[0];
766
+ if (declaration.init?.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
767
+ return;
768
+ }
769
+ const fn = declaration.init;
770
+ const param = fn.params[0];
771
+ if (param?.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
772
+ // Resolve through generic wrappers (e.g. `Readonly<FooProps>`) as well
773
+ // as the plain `FooProps` annotation.
774
+ const typeName = resolvePropsTypeName(param.typeAnnotation?.typeAnnotation);
775
+ if (typeName) {
776
+ const used = new Set();
777
+ const restUsed = collectUsedFromObjectPattern(param, typeName, used);
778
+ currentComponent = { node, typeName, used, restUsed };
779
+ }
780
+ return;
781
+ }
782
+ if (param?.type === utils_1.AST_NODE_TYPES.Identifier) {
783
+ // Identifier param (`props: FooProps`): the destructuring happens in
784
+ // the body. Resolve the Props type (unwrapping generic wrappers) and
785
+ // scan the body for `const { ... } = props`.
786
+ const typeName = resolvePropsTypeName(param.typeAnnotation?.typeAnnotation);
787
+ if (!typeName) {
788
+ return;
789
+ }
790
+ const used = new Set();
791
+ const { destructureCount, restUsed, hasOpaqueUsage } = collectBodyDestructuring(fn.body, param.name, typeName, used);
792
+ // Only check when the body destructures the param at least once.
793
+ // Member access (`props.x`), spread (`{...props}`), passing `props`
794
+ // whole, or never referencing it (e.g. `_props`) is left to prior
795
+ // behavior (no report) — those usages can't be enumerated reliably.
796
+ if (destructureCount === 0) {
797
+ return;
798
+ }
799
+ // Mixed usage (some destructure + opaque consumption) forwards the
800
+ // remaining props, so treat every prop as used to avoid false reports.
801
+ if (hasOpaqueUsage) {
802
+ const propsType = propsTypes.get(typeName);
803
+ if (propsType) {
804
+ Object.keys(propsType).forEach((key) => used.add(key));
630
805
  }
806
+ currentComponent = { node, typeName, used, restUsed: true };
807
+ return;
631
808
  }
809
+ currentComponent = { node, typeName, used, restUsed };
632
810
  }
633
811
  },
634
812
  'VariableDeclaration:exit'(node) {
@@ -13,8 +13,6 @@ const describeChild = (child) => {
13
13
  return 'JSX element';
14
14
  case 'JSXFragment':
15
15
  return 'fragment';
16
- case 'JSXExpressionContainer':
17
- return 'expression result';
18
16
  case 'JSXText':
19
17
  return 'text node';
20
18
  case 'JSXSpreadChild':
@@ -29,6 +27,16 @@ exports.noUselessFragment = {
29
27
  JSXFragment(node) {
30
28
  if (node.children.length === 1) {
31
29
  const [child] = node.children;
30
+ /**
31
+ * A fragment whose only child is an expression container — e.g.
32
+ * `<>{portal}</>` — is NOT useless. Unwrapping it to a bare
33
+ * `{portal}` is invalid in statement/return position, and wrapping a
34
+ * single ReactNode expression in a fragment is the idiomatic way to
35
+ * render it. (Mirrors the upstream rule's `allowExpressions`.)
36
+ */
37
+ if (child.type === 'JSXExpressionContainer') {
38
+ return;
39
+ }
32
40
  context.report({
33
41
  node,
34
42
  messageId: 'noUselessFragment',
@@ -113,9 +113,7 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
113
113
  }
114
114
  // Skip non-shorthand keys in object literals as they are not value uses
115
115
  if (parent && parent.type === utils_1.AST_NODE_TYPES.Property) {
116
- if (parent.key === node &&
117
- !parent.computed &&
118
- !parent.shorthand) {
116
+ if (parent.key === node && !parent.computed && !parent.shorthand) {
119
117
  return false;
120
118
  }
121
119
  }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.preferTypeAliasOverTypeofConstant = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
+ const ASTHelpers_1 = require("../utils/ASTHelpers");
6
7
  const PAREN_TYPE = utils_1.AST_NODE_TYPES.TSParenthesizedType ??
7
8
  'TSParenthesizedType';
8
9
  const isParenthesizedType = (node) => {
@@ -87,6 +88,71 @@ function collectReferencedTypeNames(node, acc = new Set()) {
87
88
  collectReferencedTypeNames(e, acc);
88
89
  break;
89
90
  }
91
+ case utils_1.AST_NODE_TYPES.TSIndexedAccessType: {
92
+ const idx = node;
93
+ collectReferencedTypeNames(idx.objectType, acc);
94
+ collectReferencedTypeNames(idx.indexType, acc);
95
+ break;
96
+ }
97
+ case utils_1.AST_NODE_TYPES.TSMappedType: {
98
+ const mapped = node;
99
+ if (mapped.typeAnnotation)
100
+ collectReferencedTypeNames(mapped.typeAnnotation, acc);
101
+ if (mapped.nameType)
102
+ collectReferencedTypeNames(mapped.nameType, acc);
103
+ if (mapped.typeParameter && mapped.typeParameter.constraint) {
104
+ collectReferencedTypeNames(mapped.typeParameter.constraint, acc);
105
+ }
106
+ break;
107
+ }
108
+ case utils_1.AST_NODE_TYPES.TSConditionalType: {
109
+ const cond = node;
110
+ collectReferencedTypeNames(cond.checkType, acc);
111
+ collectReferencedTypeNames(cond.extendsType, acc);
112
+ collectReferencedTypeNames(cond.trueType, acc);
113
+ collectReferencedTypeNames(cond.falseType, acc);
114
+ break;
115
+ }
116
+ case utils_1.AST_NODE_TYPES.TSTypeLiteral: {
117
+ const lit = node;
118
+ for (const m of lit.members) {
119
+ if (m.type === utils_1.AST_NODE_TYPES.TSPropertySignature && m.typeAnnotation) {
120
+ collectReferencedTypeNames(m.typeAnnotation.typeAnnotation, acc);
121
+ }
122
+ else if (m.type === utils_1.AST_NODE_TYPES.TSMethodSignature) {
123
+ if (m.returnType) {
124
+ collectReferencedTypeNames(m.returnType.typeAnnotation, acc);
125
+ }
126
+ for (const p of m.params) {
127
+ if (p.typeAnnotation) {
128
+ collectReferencedTypeNames(p.typeAnnotation.typeAnnotation, acc);
129
+ }
130
+ }
131
+ }
132
+ else if (m.type === utils_1.AST_NODE_TYPES.TSIndexSignature) {
133
+ if (m.typeAnnotation) {
134
+ collectReferencedTypeNames(m.typeAnnotation.typeAnnotation, acc);
135
+ }
136
+ for (const p of m.parameters) {
137
+ if (p.typeAnnotation) {
138
+ collectReferencedTypeNames(p.typeAnnotation.typeAnnotation, acc);
139
+ }
140
+ }
141
+ }
142
+ else if (m.type === utils_1.AST_NODE_TYPES.TSCallSignatureDeclaration ||
143
+ m.type === utils_1.AST_NODE_TYPES.TSConstructSignatureDeclaration) {
144
+ if (m.returnType) {
145
+ collectReferencedTypeNames(m.returnType.typeAnnotation, acc);
146
+ }
147
+ for (const p of m.params) {
148
+ if (p.typeAnnotation) {
149
+ collectReferencedTypeNames(p.typeAnnotation.typeAnnotation, acc);
150
+ }
151
+ }
152
+ }
153
+ }
154
+ break;
155
+ }
90
156
  default: {
91
157
  if (isParenthesizedType(node)) {
92
158
  collectReferencedTypeNames(node.typeAnnotation, acc);
@@ -225,19 +291,15 @@ exports.preferTypeAliasOverTypeofConstant = (0, createRule_1.createRule)({
225
291
  TSTypeQuery(node) {
226
292
  if (!collected)
227
293
  return;
228
- // Skip if inside a type alias declaration (Issue #1117)
294
+ const ancestors = ASTHelpers_1.ASTHelpers.getAncestors(context, node);
295
+ // Skip if inside a type alias declaration (Issue #1117, #1175)
229
296
  // This allows 'type T = typeof CONST' as the canonical way to define the alias.
230
- let current = node.parent;
231
- while (current) {
232
- if (current.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
233
- return;
234
- }
235
- current = current.parent;
297
+ if (ancestors.some((a) => a.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration)) {
298
+ return;
236
299
  }
237
- // Skip `keyof typeof X`
238
- if (node.parent &&
239
- node.parent.type === utils_1.AST_NODE_TYPES.TSTypeOperator &&
240
- node.parent.operator === 'keyof') {
300
+ // Skip `keyof typeof X` as it's a canonical way to derive a union of keys from a constant object.
301
+ if (ancestors.some((a) => a.type === utils_1.AST_NODE_TYPES.TSTypeOperator &&
302
+ a.operator === 'keyof')) {
241
303
  return;
242
304
  }
243
305
  if (node.exprName.type !== utils_1.AST_NODE_TYPES.Identifier) {