@blumintinc/eslint-plugin-blumint 1.20.145 → 1.20.147

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.
@@ -103,33 +103,205 @@ function aliasDeclarationNamed(statement, name) {
103
103
  : undefined;
104
104
  }
105
105
  /**
106
- * Lexical alias lookup over pre-collected statement lists, innermost first, so
107
- * an inner declaration shadows a same-named outer one.
106
+ * The string members of `const NAME = ['a', 'b'] as const`, or `undefined` when
107
+ * the statement declares something else or an element is not a string literal.
108
+ *
109
+ * `prefer-union-from-const-array` rewrites a string-literal union alias into
110
+ * exactly this pair of declarations plus `(typeof NAME)[number]`, so a keep-list
111
+ * that reads decidable before that transform has to stay decidable after it.
112
+ */
113
+ function constArrayMembersNamed(statement, name) {
114
+ const declared = (0, lexicalScope_1.declarationOf)(statement);
115
+ if (declared.type !== utils_1.AST_NODE_TYPES.VariableDeclaration ||
116
+ declared.kind !== 'const') {
117
+ return undefined;
118
+ }
119
+ for (const declarator of declared.declarations) {
120
+ if (declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier ||
121
+ declarator.id.name !== name ||
122
+ declarator.init?.type !== utils_1.AST_NODE_TYPES.TSAsExpression) {
123
+ continue;
124
+ }
125
+ const { expression, typeAnnotation } = declarator.init;
126
+ // Without `as const` the elements widen to `string`, and the indexed access
127
+ // yields `string` rather than a union of the literals.
128
+ const isConstAssertion = typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
129
+ typeAnnotation.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
130
+ typeAnnotation.typeName.name === 'const';
131
+ if (!isConstAssertion || expression.type !== utils_1.AST_NODE_TYPES.ArrayExpression)
132
+ return undefined;
133
+ const members = [];
134
+ for (const element of expression.elements) {
135
+ if (element?.type !== utils_1.AST_NODE_TYPES.Literal ||
136
+ typeof element.value !== 'string') {
137
+ return undefined;
138
+ }
139
+ members.push(element.value);
140
+ }
141
+ return members;
142
+ }
143
+ return undefined;
144
+ }
145
+ /**
146
+ * Lexical lookup over pre-collected statement lists, innermost first, so an
147
+ * inner declaration shadows a same-named outer one.
108
148
  *
109
149
  * Resolution is by scope rather than by a map built during traversal because
110
150
  * type aliases hoist: a component declared above its own props alias must still
111
151
  * resolve it.
112
152
  */
113
153
  function aliasResolverFrom(lists, startIndex = 0) {
114
- return (name) => {
115
- for (let index = startIndex; index < lists.length; index += 1) {
116
- for (const statement of lists[index]) {
117
- const declaration = aliasDeclarationNamed(statement, name);
118
- if (declaration) {
119
- return {
120
- typeNode: declaration.typeAnnotation,
121
- resolve: aliasResolverFrom(lists, index),
122
- };
154
+ return {
155
+ typeAlias(name) {
156
+ for (let index = startIndex; index < lists.length; index += 1) {
157
+ for (const statement of lists[index]) {
158
+ const declaration = aliasDeclarationNamed(statement, name);
159
+ if (declaration) {
160
+ return {
161
+ typeNode: declaration.typeAnnotation,
162
+ resolve: aliasResolverFrom(lists, index),
163
+ };
164
+ }
123
165
  }
124
166
  }
125
- }
126
- return undefined;
167
+ return undefined;
168
+ },
169
+ constArrayKeys(name) {
170
+ for (let index = startIndex; index < lists.length; index += 1) {
171
+ for (const statement of lists[index]) {
172
+ const members = constArrayMembersNamed(statement, name);
173
+ if (members)
174
+ return members;
175
+ }
176
+ }
177
+ return undefined;
178
+ },
127
179
  };
128
180
  }
129
181
  function aliasResolverAt(from) {
130
182
  return aliasResolverFrom((0, lexicalScope_1.enclosingStatementLists)(from));
131
183
  }
132
- function typeNodeExcludesProperty(node, propertyName, resolveAlias, seen = new Set()) {
184
+ /**
185
+ * The property names a `Pick<T, K>` keep-list names, or `null` when the list is
186
+ * not decidable from syntax alone.
187
+ *
188
+ * `null` means "prove nothing", never "keeps nothing": a keep-list spelled as a
189
+ * type parameter, `keyof T`, `string`, or a template-literal type can include
190
+ * `children`, and treating an undecidable list as an empty one would exempt
191
+ * exactly those.
192
+ */
193
+ function stringLiteralKeySet(node, resolveAlias, seen = new Set()) {
194
+ if (node.type === utils_1.AST_NODE_TYPES.TSLiteralType) {
195
+ return node.literal.type === utils_1.AST_NODE_TYPES.Literal &&
196
+ typeof node.literal.value === 'string'
197
+ ? new Set([node.literal.value])
198
+ : null;
199
+ }
200
+ if (node.type === utils_1.AST_NODE_TYPES.TSUnionType) {
201
+ const keys = new Set();
202
+ for (const member of node.types) {
203
+ const memberKeys = stringLiteralKeySet(member, resolveAlias, seen);
204
+ if (!memberKeys)
205
+ return null;
206
+ for (const key of memberKeys) {
207
+ keys.add(key);
208
+ }
209
+ }
210
+ return keys;
211
+ }
212
+ // `(typeof KEYS)[number]` over a `const KEYS = [...] as const` names exactly
213
+ // the array's members — the spelling `prefer-union-from-const-array` rewrites
214
+ // a literal union alias into.
215
+ if (node.type === utils_1.AST_NODE_TYPES.TSIndexedAccessType &&
216
+ node.indexType.type === utils_1.AST_NODE_TYPES.TSNumberKeyword &&
217
+ node.objectType.type === utils_1.AST_NODE_TYPES.TSTypeQuery &&
218
+ node.objectType.exprName.type === utils_1.AST_NODE_TYPES.Identifier) {
219
+ const members = resolveAlias?.constArrayKeys(node.objectType.exprName.name);
220
+ return members ? new Set(members) : null;
221
+ }
222
+ // A bare alias standing for a literal union (`type Keys = 'sx' | 'size'`) is
223
+ // as decidable as the union written inline. A parameterized reference is not:
224
+ // it is a computed key list whose members this rule cannot enumerate.
225
+ if (node.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
226
+ node.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
227
+ !node.typeParameters) {
228
+ const name = node.typeName.name;
229
+ if (!resolveAlias || seen.has(name))
230
+ return null;
231
+ const alias = resolveAlias.typeAlias(name);
232
+ if (!alias)
233
+ return null;
234
+ // Guarding before the recursion terminates a self-referential alias.
235
+ seen.add(name);
236
+ return stringLiteralKeySet(alias.typeNode, alias.resolve, seen);
237
+ }
238
+ return null;
239
+ }
240
+ /**
241
+ * Whether a closed object type provably declares no `propertyName` member.
242
+ *
243
+ * An index signature reopens the type — `{ [key: string]: unknown }` admits
244
+ * `children` — and so does a computed key the rule cannot evaluate, since the
245
+ * constant behind `[KEY]` may well be `'children'`. Both surrender the proof.
246
+ */
247
+ function typeLiteralExcludesProperty(node, propertyName) {
248
+ return node.members.every((member) => {
249
+ if (member.type === utils_1.AST_NODE_TYPES.TSIndexSignature)
250
+ return false;
251
+ if (member.type !== utils_1.AST_NODE_TYPES.TSPropertySignature &&
252
+ member.type !== utils_1.AST_NODE_TYPES.TSMethodSignature) {
253
+ // Call and construct signatures declare no named member.
254
+ return true;
255
+ }
256
+ const key = member.key;
257
+ if (member.computed) {
258
+ return (key.type === utils_1.AST_NODE_TYPES.Literal &&
259
+ String(key.value) !== propertyName);
260
+ }
261
+ if (key.type === utils_1.AST_NODE_TYPES.Identifier) {
262
+ return key.name !== propertyName;
263
+ }
264
+ if (key.type === utils_1.AST_NODE_TYPES.Literal) {
265
+ return String(key.value) !== propertyName;
266
+ }
267
+ return false;
268
+ });
269
+ }
270
+ /**
271
+ * Generic wrappers that re-map a type's own members without contributing any of
272
+ * their own, so their argument still stands for the whole props type.
273
+ *
274
+ * `PropsWithChildren` is the counter-example the list exists for: it ADDS
275
+ * `children`, so a proof about its argument says nothing about the wrapper.
276
+ */
277
+ const PROPERTY_PRESERVING_WRAPPERS = new Set([
278
+ 'Readonly',
279
+ 'Required',
280
+ 'Partial',
281
+ 'NonNullable',
282
+ ]);
283
+ function wrapperNameOf(typeName) {
284
+ if (typeName.type === utils_1.AST_NODE_TYPES.Identifier)
285
+ return typeName.name;
286
+ if (typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
287
+ return typeName.right.type === utils_1.AST_NODE_TYPES.Identifier
288
+ ? typeName.right.name
289
+ : null;
290
+ }
291
+ return null;
292
+ }
293
+ /**
294
+ * Whether a type node provably lacks `propertyName`.
295
+ *
296
+ * `closedLiteralCounts` records whether this position IS the props type rather
297
+ * than merely a part of it. Two of the proofs below — a `Pick<>` keep-list and
298
+ * a closed object literal — describe the type they sit on, so they only carry
299
+ * to the binding when nothing can add members on the way back out. The blanket
300
+ * recursion through a type reference's arguments is where that distinction
301
+ * bites: `PropsWithChildren<{ sx?: string }>` and `Record<string, { a: number }>`
302
+ * both wrap a children-free argument in something that admits `children`.
303
+ */
304
+ function typeNodeExcludesProperty(node, propertyName, resolveAlias, seen = new Set(), closedLiteralCounts = false) {
133
305
  if (node.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
134
306
  const typeName = node.typeName.type === utils_1.AST_NODE_TYPES.Identifier
135
307
  ? node.typeName.name
@@ -141,34 +313,62 @@ function typeNodeExcludesProperty(node, propertyName, resolveAlias, seen = new S
141
313
  return true;
142
314
  }
143
315
  }
316
+ // A keep-list is a stronger guarantee than an omit-list: `Pick` drops every
317
+ // member it does not name, so a fully decidable list without `propertyName`
318
+ // excludes it outright.
319
+ if (closedLiteralCounts &&
320
+ typeName === 'Pick' &&
321
+ node.typeParameters?.params?.[1]) {
322
+ const kept = stringLiteralKeySet(node.typeParameters.params[1], resolveAlias);
323
+ if (kept && !kept.has(propertyName)) {
324
+ return true;
325
+ }
326
+ }
144
327
  if (node.typeParameters?.params) {
145
- return node.typeParameters.params.some((param) => typeNodeExcludesProperty(param, propertyName, resolveAlias, seen));
328
+ const argumentsAreTheType = closedLiteralCounts &&
329
+ PROPERTY_PRESERVING_WRAPPERS.has(wrapperNameOf(node.typeName) ?? '');
330
+ return node.typeParameters.params.some((param) => typeNodeExcludesProperty(param, propertyName, resolveAlias, seen, argumentsAreTheType));
146
331
  }
147
- if (typeName && resolveAlias && !seen.has(typeName)) {
148
- const alias = resolveAlias(typeName);
332
+ // Keyed by position as well as by name: the same alias proves different
333
+ // things in a props position than inside an arbitrary generic, so a visit
334
+ // in one position must not suppress the visit in the other.
335
+ const seenKey = `${closedLiteralCounts ? 'props' : 'part'}:${typeName}`;
336
+ if (typeName && resolveAlias && !seen.has(seenKey)) {
337
+ const alias = resolveAlias.typeAlias(typeName);
149
338
  if (alias) {
150
339
  // Guarding before the recursion terminates a self-referential alias.
151
- seen.add(typeName);
152
- if (typeNodeExcludesProperty(alias.typeNode, propertyName, alias.resolve, seen)) {
340
+ seen.add(seenKey);
341
+ if (typeNodeExcludesProperty(alias.typeNode, propertyName, alias.resolve, seen, closedLiteralCounts)) {
153
342
  return true;
154
343
  }
155
344
  }
156
345
  }
157
346
  }
347
+ if (node.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
348
+ return (closedLiteralCounts && typeLiteralExcludesProperty(node, propertyName));
349
+ }
158
350
  if (node.type === utils_1.AST_NODE_TYPES.TSUnionType) {
159
- return node.types.every((typeNode) => typeNodeExcludesProperty(typeNode, propertyName, resolveAlias, seen));
351
+ return node.types.every((typeNode) => typeNodeExcludesProperty(typeNode, propertyName, resolveAlias, seen, closedLiteralCounts));
160
352
  }
161
353
  if (node.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
162
- return node.types.every((typeNode) => typeNodeExcludesProperty(typeNode, propertyName, resolveAlias, seen));
354
+ return node.types.every((typeNode) => typeNodeExcludesProperty(typeNode, propertyName, resolveAlias, seen, closedLiteralCounts));
163
355
  }
164
356
  return false;
165
357
  }
166
- function typeAnnotationExcludesProperty(annotation, propertyName, resolveAlias) {
167
- if (!annotation)
358
+ /**
359
+ * Entry point for a type node that stands for a whole props type, whether it
360
+ * was written as a parameter annotation or supplied as a `forwardRef` type
361
+ * argument.
362
+ */
363
+ function propsTypeNodeExcludesProperty(typeNode, propertyName, resolveAlias) {
364
+ if (!typeNode)
168
365
  return false;
169
- return typeNodeExcludesProperty(annotation.typeAnnotation, propertyName, resolveAlias);
366
+ return typeNodeExcludesProperty(typeNode, propertyName, resolveAlias, new Set(), true);
367
+ }
368
+ function typeAnnotationExcludesProperty(annotation, propertyName, resolveAlias) {
369
+ return propsTypeNodeExcludesProperty(annotation?.typeAnnotation, propertyName, resolveAlias);
170
370
  }
171
- function collectRestBindingsFromPattern(pattern, ctx, annotation, resolveAlias, sourceChildrenSourceId) {
371
+ function collectRestBindingsFromPattern(pattern, ctx, typeNode, resolveAlias, sourceChildrenSourceId) {
172
372
  const childrenPresent = patternHasChildrenProperty(pattern);
173
373
  for (const prop of pattern.properties) {
174
374
  if (prop.type === utils_1.AST_NODE_TYPES.RestElement &&
@@ -177,7 +377,7 @@ function collectRestBindingsFromPattern(pattern, ctx, annotation, resolveAlias,
177
377
  ctx.bindings.set(prop.argument.name, {
178
378
  identifier: prop.argument,
179
379
  childrenExcluded: childrenPresent,
180
- typeAnnotationExcludesProperty: typeAnnotationExcludesProperty(annotation, 'children', resolveAlias),
380
+ typeAnnotationExcludesProperty: propsTypeNodeExcludesProperty(typeNode, 'children', resolveAlias),
181
381
  childrenSourceId: sourceChildrenSourceId ?? prop.argument.name,
182
382
  });
183
383
  }
@@ -212,13 +412,31 @@ function recordChildrenValueBindingsFromPattern(pattern, ctx, sourceChildrenSour
212
412
  }
213
413
  }
214
414
  }
215
- function recordParamBindings(param, ctx, resolveAlias) {
415
+ /**
416
+ * The props type a parameter carries: its own annotation when it has one, and
417
+ * otherwise the type the call site supplies contextually.
418
+ *
419
+ * An explicit annotation always wins. It is the type the body is checked
420
+ * against, so a `forwardRef<E, Clean>((props: MenuProps, ref) => …)` still
421
+ * carries whatever `MenuProps` declares.
422
+ */
423
+ function propsTypeNodeOf(param, contextualTypeNode) {
424
+ // A `TSParameterProperty` (a constructor's `private x: T`) carries its
425
+ // annotation on the parameter it wraps and never takes part in a component's
426
+ // props, so it simply has no props type of its own.
427
+ const own = param.type === utils_1.AST_NODE_TYPES.TSParameterProperty
428
+ ? undefined
429
+ : param.typeAnnotation?.typeAnnotation;
430
+ return own ?? contextualTypeNode;
431
+ }
432
+ function recordParamBindings(param, ctx, resolveAlias, contextualTypeNode) {
433
+ const typeNode = propsTypeNodeOf(param, contextualTypeNode);
216
434
  if (param.type === utils_1.AST_NODE_TYPES.Identifier) {
217
435
  ctx.propsLikeIdentifiers.add(param.name);
218
436
  ctx.bindings.set(param.name, {
219
437
  identifier: param,
220
438
  childrenExcluded: false,
221
- typeAnnotationExcludesProperty: typeAnnotationExcludesProperty(param.typeAnnotation, 'children', resolveAlias),
439
+ typeAnnotationExcludesProperty: propsTypeNodeExcludesProperty(typeNode, 'children', resolveAlias),
222
440
  childrenSourceId: param.name,
223
441
  });
224
442
  return;
@@ -229,20 +447,61 @@ function recordParamBindings(param, ctx, resolveAlias) {
229
447
  ctx.bindings.set(param.left.name, {
230
448
  identifier: param.left,
231
449
  childrenExcluded: false,
232
- typeAnnotationExcludesProperty: typeAnnotationExcludesProperty(param.typeAnnotation, 'children', resolveAlias),
450
+ typeAnnotationExcludesProperty: propsTypeNodeExcludesProperty(typeNode, 'children', resolveAlias),
233
451
  childrenSourceId: param.left.name,
234
452
  });
235
453
  return;
236
454
  }
237
455
  if (param.type === utils_1.AST_NODE_TYPES.AssignmentPattern &&
238
456
  param.left.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
239
- collectRestBindingsFromPattern(param.left, ctx, param.typeAnnotation, resolveAlias);
457
+ collectRestBindingsFromPattern(param.left, ctx, typeNode, resolveAlias);
240
458
  return;
241
459
  }
242
460
  if (param.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
243
- collectRestBindingsFromPattern(param, ctx, param.typeAnnotation, resolveAlias);
461
+ collectRestBindingsFromPattern(param, ctx, typeNode, resolveAlias);
244
462
  }
245
463
  }
464
+ /**
465
+ * The props type a `forwardRef<Element, Props>(…)` call supplies to its render
466
+ * callback, whose parameters carry no annotation of their own.
467
+ *
468
+ * Without this the props type is invisible in the `forwardRef` spelling, so
469
+ * even the documented `Omit<…, 'children'>` remedy could not be seen and the
470
+ * rule reported on props it could prove children-free in every other spelling.
471
+ */
472
+ function forwardRefPropsTypeNode(node) {
473
+ const call = node.parent;
474
+ if (call?.type !== utils_1.AST_NODE_TYPES.CallExpression)
475
+ return undefined;
476
+ if (call.arguments[0] !== node)
477
+ return undefined;
478
+ // An optional call detaches the type arguments onto a
479
+ // `TSInstantiationExpression` — `forwardRef<E, P>?.(…)` parses as
480
+ // instantiate-then-call — so both the callee and the type arguments are read
481
+ // through it. The node predates the `LeftHandSideExpression` union this
482
+ // version declares, hence the hand-written narrowing.
483
+ const rawCallee = call.callee;
484
+ const instantiation = rawCallee.type === utils_1.AST_NODE_TYPES.TSInstantiationExpression
485
+ ? rawCallee
486
+ : undefined;
487
+ const callee = instantiation?.expression ?? call.callee;
488
+ const calleeName = callee.type === utils_1.AST_NODE_TYPES.Identifier
489
+ ? callee.name
490
+ : callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
491
+ !callee.computed &&
492
+ callee.property.type === utils_1.AST_NODE_TYPES.Identifier
493
+ ? callee.property.name
494
+ : null;
495
+ if (calleeName !== 'forwardRef')
496
+ return undefined;
497
+ // `typeParameters` is where @typescript-eslint/utils 5 puts a call's type
498
+ // arguments; `typeArguments` is the name later majors use. Reading both keeps
499
+ // this working across the rename.
500
+ const instantiated = (instantiation ?? call);
501
+ const typeArguments = instantiated.typeParameters ?? instantiated.typeArguments;
502
+ // The first type argument is the element type; the props type is the second.
503
+ return typeArguments?.params?.[1];
504
+ }
246
505
  function findNearestComponentContext(stack) {
247
506
  for (let i = stack.length - 1; i >= 0; i -= 1) {
248
507
  if (stack[i].isComponent)
@@ -460,9 +719,12 @@ exports.preventChildrenClobber = (0, createRule_1.createRule)({
460
719
  // ESLint has assigned `parent` up the chain by the time this visitor
461
720
  // runs, but not yet onto the parameter's own type annotation.
462
721
  const resolveAlias = aliasResolverAt(node);
463
- for (const param of node.params) {
464
- recordParamBindings(param, ctx, resolveAlias);
465
- }
722
+ // Only the first parameter receives the contextual props type; the
723
+ // second is the forwarded ref.
724
+ const contextualPropsType = forwardRefPropsTypeNode(node);
725
+ node.params.forEach((param, index) => {
726
+ recordParamBindings(param, ctx, resolveAlias, index === 0 ? contextualPropsType : undefined);
727
+ });
466
728
  }
467
729
  functionStack.push(ctx);
468
730
  },
@@ -528,7 +790,7 @@ exports.preventChildrenClobber = (0, createRule_1.createRule)({
528
790
  const initBinding = findBinding(init.name, functionStack);
529
791
  const sourceChildrenSourceId = initBinding?.childrenSourceId ?? init.name;
530
792
  recordChildrenValueBindingsFromPattern(id, componentCtx, sourceChildrenSourceId);
531
- collectRestBindingsFromPattern(id, componentCtx, id.typeAnnotation ?? null, aliasResolverAt(node), sourceChildrenSourceId);
793
+ collectRestBindingsFromPattern(id, componentCtx, id.typeAnnotation?.typeAnnotation ?? null, aliasResolverAt(node), sourceChildrenSourceId);
532
794
  }
533
795
  },
534
796
  JSXElement(node) {
@@ -422,6 +422,109 @@ function collectReferencedNames(node) {
422
422
  visit(node, null);
423
423
  return names;
424
424
  }
425
+ // Type-space shells whose contents are erased at runtime. A reference inside
426
+ // one (`const x: ReturnType<typeof buildHit> = …`) never reads the binding at
427
+ // module evaluation, so it must not count as an eager reference — counting it
428
+ // would withhold autofixes that are perfectly safe to apply.
429
+ const TYPE_SPACE_NODE_TYPES = new Set([
430
+ 'TSTypeAnnotation',
431
+ 'TSTypeParameterInstantiation',
432
+ 'TSTypeParameterDeclaration',
433
+ 'TSTypeAliasDeclaration',
434
+ 'TSInterfaceDeclaration',
435
+ 'TSTypeQuery',
436
+ 'TSDeclareFunction',
437
+ ]);
438
+ // Names a statement reads at MODULE EVALUATION time. A read inside a function
439
+ // body is deferred to call time and constrains nothing — except when that
440
+ // function is an immediately invoked callee, which runs during evaluation. The
441
+ // distinction is the whole point of this rule: callers referencing helpers
442
+ // inside their bodies is the safe pattern being enforced, while an initializer
443
+ // calling a helper runs the moment the module loads.
444
+ function collectEagerlyReferencedNames(node) {
445
+ const names = new Set();
446
+ const visit = (current, parent) => {
447
+ if (!current || !ASTHelpers_1.ASTHelpers.isNode(current)) {
448
+ return;
449
+ }
450
+ if (TYPE_SPACE_NODE_TYPES.has(current.type)) {
451
+ return;
452
+ }
453
+ // An `as`/`satisfies` wrapper evaluates only its expression; the type side
454
+ // is erased.
455
+ if (current.type === 'TSAsExpression' ||
456
+ current.type === 'TSSatisfiesExpression') {
457
+ visit(current.expression, current);
458
+ return;
459
+ }
460
+ if (current.type === 'FunctionDeclaration' ||
461
+ current.type === 'FunctionExpression' ||
462
+ current.type === 'ArrowFunctionExpression') {
463
+ // Deferred to call time — unless this function is the callee of the call
464
+ // being visited, which the CallExpression case enters directly.
465
+ return;
466
+ }
467
+ if (current.type === 'CallExpression' &&
468
+ (current.callee.type === 'ArrowFunctionExpression' ||
469
+ current.callee.type === 'FunctionExpression')) {
470
+ // An IIFE's body runs during module evaluation, so its reads are eager.
471
+ visit(current.callee.body, current.callee);
472
+ current.callee.params.forEach((param) => visit(param, current.callee));
473
+ current.arguments.forEach((argument) => visit(argument, current));
474
+ return;
475
+ }
476
+ if (current.type === 'ClassDeclaration' ||
477
+ current.type === 'ClassExpression') {
478
+ // A class's heritage clause, computed member keys, static initializers,
479
+ // and static blocks all run at evaluation; method bodies and instance
480
+ // property values wait for construction.
481
+ visit(current.superClass, current);
482
+ current.body.body.forEach((member) => {
483
+ if (member.type === 'StaticBlock') {
484
+ member.body.forEach((statement) => visit(statement, member));
485
+ return;
486
+ }
487
+ if ('computed' in member && member.computed && 'key' in member) {
488
+ visit(member.key, member);
489
+ }
490
+ if (member.type === 'PropertyDefinition' &&
491
+ member.static &&
492
+ member.value) {
493
+ visit(member.value, member);
494
+ }
495
+ });
496
+ return;
497
+ }
498
+ if (current.type === 'Identifier') {
499
+ const isMemberProperty = parent?.type === 'MemberExpression' &&
500
+ parent.property === current &&
501
+ !parent.computed;
502
+ const isObjectKey = parent?.type === 'Property' &&
503
+ parent.key === current &&
504
+ !parent.computed;
505
+ if (!isMemberProperty && !isObjectKey) {
506
+ names.add(current.name);
507
+ }
508
+ }
509
+ Object.values(current).forEach((value) => {
510
+ if (!value || value === current || current.parent === value) {
511
+ return;
512
+ }
513
+ if (Array.isArray(value)) {
514
+ value.forEach((child) => {
515
+ if (ASTHelpers_1.ASTHelpers.isNode(child)) {
516
+ visit(child, current);
517
+ }
518
+ });
519
+ }
520
+ else if (ASTHelpers_1.ASTHelpers.isNode(value)) {
521
+ visit(value, current);
522
+ }
523
+ });
524
+ };
525
+ visit(node, null);
526
+ return names;
527
+ }
425
528
  // Names bound by an interleaved VALUE declaration (const/let/var) — but not by a
426
529
  // reorderable function. Hoisting a function above a value binding it references
427
530
  // is a genuine runtime declare-before-use. Type aliases are tracked separately
@@ -548,6 +651,50 @@ function reorderHoistsFunctionAboveDependency(regionStatements, functionStatemen
548
651
  }
549
652
  return false;
550
653
  }
654
+ // Decline (return true) any reorder that would place a const/let/var-declared
655
+ // function BELOW an interleaved statement that reads it at module evaluation
656
+ // time. The fixer pins interleaved statements and swaps functions among their
657
+ // own slots, so a helper `const` can be carried past its own module-scope
658
+ // caller (`const CHAMPION = buildHit(…)`) — the emitted file parses and
659
+ // type-checks but throws `ReferenceError: Cannot access '…' before
660
+ // initialization` the moment anything imports it, and `--fix` reports nothing
661
+ // about the file it just broke. Function declarations hoist, so demoting one
662
+ // past an eager reference stays loadable and is not declined. This is the
663
+ // mirror of reorderHoistsFunctionAboveDependency: that guard protects a moved
664
+ // function from losing a dependency ABOVE it; this one protects a pinned
665
+ // statement from losing its dependency BELOW it.
666
+ function reorderDemotesDeclarationBelowEagerReference(regionStatements, functionStatements, expectedOrderInfos) {
667
+ const variableDeclaredNames = new Set();
668
+ expectedOrderInfos.forEach((info) => {
669
+ const declaration = info.statementNode.type === 'ExportNamedDeclaration'
670
+ ? info.statementNode.declaration
671
+ : info.statementNode;
672
+ if (declaration?.type === 'VariableDeclaration') {
673
+ variableDeclaredNames.add(info.name);
674
+ }
675
+ });
676
+ if (variableDeclaredNames.size === 0) {
677
+ return false;
678
+ }
679
+ const placedNames = new Set();
680
+ let slotCursor = 0;
681
+ for (const statement of regionStatements) {
682
+ if (functionStatements.has(statement)) {
683
+ const occupant = expectedOrderInfos[slotCursor];
684
+ slotCursor += 1;
685
+ if (occupant) {
686
+ placedNames.add(occupant.name);
687
+ }
688
+ continue;
689
+ }
690
+ for (const name of collectEagerlyReferencedNames(statement)) {
691
+ if (variableDeclaredNames.has(name) && !placedNames.has(name)) {
692
+ return true;
693
+ }
694
+ }
695
+ }
696
+ return false;
697
+ }
551
698
  exports.verticallyGroupRelatedFunctions = (0, createRule_1.createRule)({
552
699
  name: 'vertically-group-related-functions',
553
700
  meta: {
@@ -729,6 +876,13 @@ exports.verticallyGroupRelatedFunctions = (0, createRule_1.createRule)({
729
876
  if (reorderHoistsFunctionAboveDependency(slice, functionStatements, expectedOrderInfos)) {
730
877
  return null;
731
878
  }
879
+ // The mirror hazard: a const-declared helper carried below a pinned
880
+ // statement that calls it at module evaluation. The emitted file
881
+ // would throw at import time, so the rewrite is withheld; the
882
+ // misorderedFunction report still fires.
883
+ if (reorderDemotesDeclarationBelowEagerReference(slice, functionStatements, expectedOrderInfos)) {
884
+ return null;
885
+ }
732
886
  if (!blockContainsOnlyFunctions) {
733
887
  // Real modules interleave type aliases, consts, and top-level
734
888
  // calls (e.g. `void autoRunIfMain();`) between functions. Rather
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.145",
3
+ "version": "1.20.147",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,56 @@
1
1
  [
2
+ {
3
+ "version": "1.20.147",
4
+ "date": "2026-08-13T06:24:35.769Z",
5
+ "rules": [
6
+ {
7
+ "name": "no-entire-object-hook-deps",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1985
11
+ ],
12
+ "summary": "stop hoisting a guarded dereference into the dependency array (closes #1985)"
13
+ }
14
+ ]
15
+ },
16
+ {
17
+ "version": "1.20.146",
18
+ "date": "2026-08-13T00:14:52.842Z",
19
+ "rules": [
20
+ {
21
+ "name": "enforce-memoize-async",
22
+ "changeType": "fix",
23
+ "issues": [
24
+ 1975
25
+ ],
26
+ "summary": "exempt methods taking part in a transaction (closes #1975)"
27
+ },
28
+ {
29
+ "name": "prefer-use-deep-compare-memo",
30
+ "changeType": "fix",
31
+ "issues": [
32
+ 1979
33
+ ],
34
+ "summary": "stop promoting primitive deps via member access (closes #1979)"
35
+ },
36
+ {
37
+ "name": "prevent-children-clobber",
38
+ "changeType": "fix",
39
+ "issues": [
40
+ 1980
41
+ ],
42
+ "summary": "widen the syntactic exemption beyond Omit<> (closes #1980)"
43
+ },
44
+ {
45
+ "name": "vertically-group-related-functions",
46
+ "changeType": "fix",
47
+ "issues": [
48
+ 1983
49
+ ],
50
+ "summary": "decline reorders that demote a helper below its module-scope caller (closes #1983)"
51
+ }
52
+ ]
53
+ },
2
54
  {
3
55
  "version": "1.20.145",
4
56
  "date": "2026-08-12T16:05:12.174Z",