@blumintinc/eslint-plugin-blumint 1.20.138 → 1.20.139

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.138',
226
+ version: '1.20.139',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -304,6 +304,15 @@ function templateCanSpell(quasis, target) {
304
304
  }
305
305
  return true;
306
306
  }
307
+ /**
308
+ * A `: number` type annotation. Every declaration site spells the proof with
309
+ * the same `TSTypeAnnotation` wrapper — a binding name, a class property, a
310
+ * function's return type — so one predicate reads them all, and which site the
311
+ * author chose stops deciding the verdict.
312
+ */
313
+ function isNumberTypeAnnotation(annotation) {
314
+ return annotation?.typeAnnotation.type === utils_1.AST_NODE_TYPES.TSNumberKeyword;
315
+ }
307
316
  /**
308
317
  * A `: number` annotation on a binding name. Parameters and variable
309
318
  * declarators are the bindings that carry one, and TypeScript checks every
@@ -311,7 +320,171 @@ function templateCanSpell(quasis, target) {
311
320
  */
312
321
  function isNumberAnnotated(node) {
313
322
  return (node.type === utils_1.AST_NODE_TYPES.Identifier &&
314
- node.typeAnnotation?.typeAnnotation.type === utils_1.AST_NODE_TYPES.TSNumberKeyword);
323
+ isNumberTypeAnnotation(node.typeAnnotation));
324
+ }
325
+ /** The node read as a function, or null when it is not one. */
326
+ function asFunctionNode(node) {
327
+ switch (node.type) {
328
+ case utils_1.AST_NODE_TYPES.FunctionDeclaration:
329
+ case utils_1.AST_NODE_TYPES.FunctionExpression:
330
+ case utils_1.AST_NODE_TYPES.ArrowFunctionExpression:
331
+ case utils_1.AST_NODE_TYPES.TSDeclareFunction:
332
+ case utils_1.AST_NODE_TYPES.TSEmptyBodyFunctionExpression:
333
+ return node;
334
+ default:
335
+ return null;
336
+ }
337
+ }
338
+ /**
339
+ * Whether a function declares that it returns a number. The `: number` on a
340
+ * return type is the author's own claim at a declaration site, and TypeScript
341
+ * rejects every `return` that contradicts it — the same trust a `: number`
342
+ * binding annotation already earns, spelled one node over.
343
+ *
344
+ * A laundering assertion inside the body (`return raw as unknown as number`)
345
+ * switches that check off for the one statement carrying it, and is still
346
+ * credited here. `const k: number = raw as unknown as number` is credited for
347
+ * exactly the same reason: the annotation, not the initializer, is what the
348
+ * proof rests on. Refusing the return annotation alone would reinstate the very
349
+ * asymmetry between declaration sites this predicate exists to remove — and a
350
+ * body scan is not a proof anyway, since the laundering can happen one call or
351
+ * one local alias further away and read as clean.
352
+ */
353
+ function returnsNumberType(node) {
354
+ const fn = asFunctionNode(node);
355
+ return !!fn && isNumberTypeAnnotation(fn.returnType);
356
+ }
357
+ /** The binding a constructor parameter property declares, default and all. */
358
+ function parameterPropertyBinding(node) {
359
+ return node.parameter.type === utils_1.AST_NODE_TYPES.AssignmentPattern
360
+ ? node.parameter.left
361
+ : node.parameter;
362
+ }
363
+ /** Class members that declare a value under a name of their own. */
364
+ const NAMED_CLASS_MEMBER_TYPES = new Set([
365
+ utils_1.AST_NODE_TYPES.MethodDefinition,
366
+ utils_1.AST_NODE_TYPES.TSAbstractMethodDefinition,
367
+ utils_1.AST_NODE_TYPES.PropertyDefinition,
368
+ utils_1.AST_NODE_TYPES.TSAbstractPropertyDefinition,
369
+ utils_1.AST_NODE_TYPES.AccessorProperty,
370
+ utils_1.AST_NODE_TYPES.TSAbstractAccessorProperty,
371
+ ]);
372
+ /**
373
+ * The name a class member declares, or null for an index signature, a static
374
+ * block, or a computed key whose text names no member the syntax can match.
375
+ */
376
+ function classMemberName(member) {
377
+ if (!NAMED_CLASS_MEMBER_TYPES.has(member.type)) {
378
+ return null;
379
+ }
380
+ const { key, computed } = member;
381
+ if (computed) {
382
+ return null;
383
+ }
384
+ if (key.type === utils_1.AST_NODE_TYPES.Identifier) {
385
+ return key.name;
386
+ }
387
+ return key.type === utils_1.AST_NODE_TYPES.Literal && typeof key.value === 'string'
388
+ ? key.value
389
+ : null;
390
+ }
391
+ /**
392
+ * A setter declares what a write to the member accepts, never what a read of it
393
+ * yields — a `get depth(): number` paired with a `set depth(v: string)` reads
394
+ * as a number. So the setter is left out of the judgement rather than failing
395
+ * it, which would let adding a setter re-report the getter's own proof.
396
+ */
397
+ function isSetterDeclaration(node) {
398
+ return ((node.type === utils_1.AST_NODE_TYPES.MethodDefinition ||
399
+ node.type === utils_1.AST_NODE_TYPES.TSAbstractMethodDefinition) &&
400
+ node.kind === 'set');
401
+ }
402
+ /**
403
+ * Whether reading the member yields a number by its own declaration: a property
404
+ * annotated `: number`, a getter returning `: number`, or a constructor
405
+ * parameter property annotated `: number`.
406
+ */
407
+ function memberReadsNumber(declaration) {
408
+ switch (declaration.type) {
409
+ case utils_1.AST_NODE_TYPES.PropertyDefinition:
410
+ case utils_1.AST_NODE_TYPES.TSAbstractPropertyDefinition:
411
+ case utils_1.AST_NODE_TYPES.AccessorProperty:
412
+ case utils_1.AST_NODE_TYPES.TSAbstractAccessorProperty:
413
+ return isNumberTypeAnnotation(declaration.typeAnnotation);
414
+ case utils_1.AST_NODE_TYPES.MethodDefinition:
415
+ case utils_1.AST_NODE_TYPES.TSAbstractMethodDefinition:
416
+ return declaration.kind === 'get' && returnsNumberType(declaration.value);
417
+ case utils_1.AST_NODE_TYPES.TSParameterProperty:
418
+ return isNumberAnnotated(parameterPropertyBinding(declaration));
419
+ default:
420
+ return false;
421
+ }
422
+ }
423
+ /** Whether calling the member returns a number by its own declaration. */
424
+ function memberCallReturnsNumber(declaration) {
425
+ switch (declaration.type) {
426
+ case utils_1.AST_NODE_TYPES.MethodDefinition:
427
+ case utils_1.AST_NODE_TYPES.TSAbstractMethodDefinition:
428
+ return (declaration.kind === 'method' && returnsNumberType(declaration.value));
429
+ case utils_1.AST_NODE_TYPES.PropertyDefinition:
430
+ case utils_1.AST_NODE_TYPES.TSAbstractPropertyDefinition:
431
+ case utils_1.AST_NODE_TYPES.AccessorProperty:
432
+ case utils_1.AST_NODE_TYPES.TSAbstractAccessorProperty:
433
+ return !!declaration.value && returnsNumberType(declaration.value);
434
+ default:
435
+ return false;
436
+ }
437
+ }
438
+ /**
439
+ * Whether the function is the body of a class member rather than a function of
440
+ * its own. A method's `FunctionExpression` receives the class instance as
441
+ * `this`; every other non-arrow function receives its own call-time receiver.
442
+ */
443
+ function isClassMemberBody(node) {
444
+ const owner = node.parent;
445
+ switch (owner?.type) {
446
+ case utils_1.AST_NODE_TYPES.MethodDefinition:
447
+ case utils_1.AST_NODE_TYPES.TSAbstractMethodDefinition:
448
+ case utils_1.AST_NODE_TYPES.PropertyDefinition:
449
+ case utils_1.AST_NODE_TYPES.TSAbstractPropertyDefinition:
450
+ case utils_1.AST_NODE_TYPES.AccessorProperty:
451
+ case utils_1.AST_NODE_TYPES.TSAbstractAccessorProperty:
452
+ return owner.value === node;
453
+ default:
454
+ return false;
455
+ }
456
+ }
457
+ /**
458
+ * The half of a class — static or instance — that `this` reaches at a node,
459
+ * together with the body whose members it names. A non-arrow function rebinds
460
+ * `this` to its own call-time receiver, so the walk stops there unless that
461
+ * function IS a member's body; an arrow keeps the enclosing `this`, which is
462
+ * what makes `read = () => this.rank` resolve against the class it is written
463
+ * in. The walk stops at the innermost class body, so a nested class shadows the
464
+ * outer one exactly as `this` does at run time.
465
+ */
466
+ function enclosingClassContext(node) {
467
+ let child = node;
468
+ let parent = node.parent;
469
+ while (parent) {
470
+ if (parent.type === utils_1.AST_NODE_TYPES.ClassBody) {
471
+ // A static block's `this` is the class object, the same half of the class
472
+ // a `static` member lives on.
473
+ return {
474
+ body: parent,
475
+ isStatic: child.type === utils_1.AST_NODE_TYPES.StaticBlock ||
476
+ child.static === true,
477
+ };
478
+ }
479
+ if ((parent.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
480
+ parent.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) &&
481
+ !isClassMemberBody(parent)) {
482
+ return null;
483
+ }
484
+ child = parent;
485
+ parent = parent.parent;
486
+ }
487
+ return null;
315
488
  }
316
489
  /** The types an assertion can launder any value through without complaint. */
317
490
  const LAUNDERING_ASSERTION_TYPES = new Set([
@@ -993,6 +1166,140 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
993
1166
  }
994
1167
  return keyAnnotations.every((keyAnnotation) => recordKeyTypes.every((recordKeyType) => recordKeyCovers(keyAnnotation, recordKeyType, node)));
995
1168
  };
1169
+ /**
1170
+ * The class body an identifier names. A class reached by its own name
1171
+ * exposes only the static half of itself, which is why the caller is told
1172
+ * so rather than left to guess.
1173
+ */
1174
+ const classBodyOfName = (identifier) => {
1175
+ const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, identifier), identifier.name);
1176
+ if (!variable || variable.defs.length !== 1) {
1177
+ return null;
1178
+ }
1179
+ const { node: definition } = variable.defs[0];
1180
+ if (definition.type === utils_1.AST_NODE_TYPES.ClassDeclaration) {
1181
+ return definition.body;
1182
+ }
1183
+ return definition.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
1184
+ definition.init?.type === utils_1.AST_NODE_TYPES.ClassExpression
1185
+ ? definition.init.body
1186
+ : null;
1187
+ };
1188
+ /**
1189
+ * The class body a receiver's members are declared in, and which half of it
1190
+ * the receiver reaches. `this` resolves to the class it is written in;
1191
+ * a bare name resolves to the class that name binds to, whose members it
1192
+ * reaches statically. Anything else — a parameter, an import, a `super`
1193
+ * whose class may live in another file — resolves to no body, so the
1194
+ * annotation on a same-named member of some other class is never read.
1195
+ */
1196
+ const receiverClassContext = (receiver) => {
1197
+ const target = unwrapWrittenKey(receiver);
1198
+ if (target.type === utils_1.AST_NODE_TYPES.ThisExpression) {
1199
+ return enclosingClassContext(target);
1200
+ }
1201
+ if (target.type !== utils_1.AST_NODE_TYPES.Identifier) {
1202
+ return null;
1203
+ }
1204
+ const body = classBodyOfName(target);
1205
+ return body ? { body, isStatic: true } : null;
1206
+ };
1207
+ /**
1208
+ * The declarations of a member name on one half of a class. A `static`
1209
+ * member and an instance member of the same name are separate declarations
1210
+ * that TypeScript keeps apart, so crediting the wrong half would credit an
1211
+ * annotation the reference does not resolve to. Constructor parameter
1212
+ * properties declare a member too, and are read alongside the body's own
1213
+ * elements.
1214
+ */
1215
+ const classMemberDeclarations = (body, name, isStatic) => {
1216
+ const declarations = [];
1217
+ for (const member of body.body) {
1218
+ if (!isStatic &&
1219
+ member.type === utils_1.AST_NODE_TYPES.MethodDefinition &&
1220
+ member.kind === 'constructor') {
1221
+ for (const parameter of member.value.params) {
1222
+ if (parameter.type === utils_1.AST_NODE_TYPES.TSParameterProperty &&
1223
+ parameterPropertyBinding(parameter)
1224
+ .name === name) {
1225
+ declarations.push(parameter);
1226
+ }
1227
+ }
1228
+ }
1229
+ if (classMemberName(member) === name &&
1230
+ member.static === isStatic) {
1231
+ declarations.push(member);
1232
+ }
1233
+ }
1234
+ return declarations.filter((declaration) => !isSetterDeclaration(declaration));
1235
+ };
1236
+ /**
1237
+ * Whether a member read resolves to a member its own class declares
1238
+ * `: number`. Every declaration of the name has to carry the proof: an
1239
+ * overload or a second declaration that does not is a value the read can
1240
+ * also yield.
1241
+ */
1242
+ const isNumberMemberRead = (node) => {
1243
+ if (node.computed || node.property.type !== utils_1.AST_NODE_TYPES.Identifier) {
1244
+ return false;
1245
+ }
1246
+ const receiver = receiverClassContext(node.object);
1247
+ if (!receiver) {
1248
+ return false;
1249
+ }
1250
+ const declarations = classMemberDeclarations(receiver.body, node.property.name, receiver.isStatic);
1251
+ return declarations.length > 0 && declarations.every(memberReadsNumber);
1252
+ };
1253
+ /**
1254
+ * Whether a call resolves to a function the author declared `: number`
1255
+ * returning — a free function, a method, or a function-valued class member.
1256
+ * The callee is resolved through the scope chain, so a local that shadows a
1257
+ * numeric helper is judged by the shadowing declaration alone; a binding
1258
+ * reassigned to another function has to prove every write, because the
1259
+ * declaration no longer says what the call reaches.
1260
+ */
1261
+ const isNumberReturningCall = (node) => {
1262
+ const callee = unwrapWrittenKey(node.callee);
1263
+ if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
1264
+ const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, callee), callee.name);
1265
+ if (!variable || variable.defs.length === 0) {
1266
+ return false;
1267
+ }
1268
+ // The definition KIND decides which node carries the annotation: a
1269
+ // function name is annotated on the function it names, while a
1270
+ // parameter's definition node is the enclosing function — whose own
1271
+ // return type says nothing about what the parameter holds.
1272
+ const declaresNumeric = variable.defs.every((def) => {
1273
+ switch (def.type) {
1274
+ case utils_1.TSESLint.Scope.DefinitionType.FunctionName:
1275
+ return returnsNumberType(def.node);
1276
+ case utils_1.TSESLint.Scope.DefinitionType.Variable:
1277
+ // The declarator's initializer is a write, so the write pass is
1278
+ // what reads the annotation off the function it holds.
1279
+ return (def.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
1280
+ !!def.node.init);
1281
+ default:
1282
+ return false;
1283
+ }
1284
+ });
1285
+ return (declaresNumeric &&
1286
+ variable.references
1287
+ .filter((reference) => reference.isWrite())
1288
+ .every((reference) => !!reference.writeExpr &&
1289
+ returnsNumberType(unwrapWrittenKey(reference.writeExpr))));
1290
+ }
1291
+ if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
1292
+ callee.computed ||
1293
+ callee.property.type !== utils_1.AST_NODE_TYPES.Identifier) {
1294
+ return false;
1295
+ }
1296
+ const receiver = receiverClassContext(callee.object);
1297
+ if (!receiver) {
1298
+ return false;
1299
+ }
1300
+ const declarations = classMemberDeclarations(receiver.body, callee.property.name, receiver.isStatic);
1301
+ return (declarations.length > 0 && declarations.every(memberCallReturnsNumber));
1302
+ };
996
1303
  /**
997
1304
  * Whether the syntax alone proves the key is a number. `__proto__`,
998
1305
  * `constructor` and `prototype` are never the string form of a number, so a
@@ -1027,12 +1334,15 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
1027
1334
  isStaticallyNumeric(target.left, seen) &&
1028
1335
  isStaticallyNumeric(target.right, seen));
1029
1336
  case utils_1.AST_NODE_TYPES.CallExpression:
1030
- return isNumericCall(target);
1337
+ return isNumericCall(target) || isNumberReturningCall(target);
1031
1338
  case utils_1.AST_NODE_TYPES.MemberExpression:
1032
1339
  // `.length` is a number on arrays, typed arrays and strings alike.
1033
- return (!target.computed &&
1340
+ if (!target.computed &&
1034
1341
  target.property.type === utils_1.AST_NODE_TYPES.Identifier &&
1035
- target.property.name === 'length');
1342
+ target.property.name === 'length') {
1343
+ return true;
1344
+ }
1345
+ return isNumberMemberRead(target);
1036
1346
  case utils_1.AST_NODE_TYPES.Identifier:
1037
1347
  return isNumericIdentifier(target, seen);
1038
1348
  default:
@@ -55,6 +55,30 @@ const referenceTypeNameOf = (typeName) => {
55
55
  function unwrapOptionalChain(node) {
56
56
  return node.type === utils_1.AST_NODE_TYPES.ChainExpression ? node.expression : node;
57
57
  }
58
+ function isFunctionNode(node) {
59
+ return (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
60
+ node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
61
+ node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression);
62
+ }
63
+ /**
64
+ * The nearest function a node sits inside.
65
+ *
66
+ * A return statement is reached from its function through an arbitrary depth of
67
+ * blocks, conditionals and loops, so stepping a fixed number of parents up
68
+ * answers a question about indentation rather than about ownership: it finds the
69
+ * function only when the `return` is written directly in the body, and only for
70
+ * the spelling whose body is a block.
71
+ */
72
+ function enclosingFunction(node) {
73
+ let current = node.parent;
74
+ while (current) {
75
+ if (isFunctionNode(current)) {
76
+ return current;
77
+ }
78
+ current = current.parent;
79
+ }
80
+ return undefined;
81
+ }
58
82
  /**
59
83
  * The type declaration a statement makes, looking through `export`.
60
84
  *
@@ -276,6 +300,10 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
276
300
  if (nodeCache.has(node)) {
277
301
  return nodeCache.get(node);
278
302
  }
303
+ // The child the walk arrived from, which is what distinguishes a function
304
+ // whose returned expression is under examination from one that merely
305
+ // contains the expression somewhere in its body.
306
+ let previous;
279
307
  let current = node;
280
308
  while (current) {
281
309
  // Type assertions using 'as' keyword
@@ -295,15 +323,27 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
295
323
  nodeCache.set(node, true);
296
324
  return true;
297
325
  }
298
- // Return statements in functions with return type annotations
326
+ // Return statements in functions with return type annotations. The
327
+ // annotation states the schema whichever way its function is written,
328
+ // so a declaration, a function expression and an arrow are all read.
299
329
  if (current.type === utils_1.AST_NODE_TYPES.ReturnStatement) {
300
- const func = current.parent?.parent;
301
- if (func?.type === utils_1.AST_NODE_TYPES.FunctionDeclaration &&
302
- func.returnType) {
330
+ if (enclosingFunction(current)?.returnType) {
303
331
  nodeCache.set(node, true);
304
332
  return true;
305
333
  }
306
334
  }
335
+ // A concise arrow body is the returned expression itself and produces no
336
+ // ReturnStatement, so the branch above cannot see it. Requiring the walk
337
+ // to have arrived from the body keeps the annotation describing only
338
+ // what the function hands back: a reference built and stored inside a
339
+ // block body is described by nothing and still reports.
340
+ if (isFunctionNode(current) &&
341
+ current.returnType &&
342
+ current.body === previous &&
343
+ current.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
344
+ nodeCache.set(node, true);
345
+ return true;
346
+ }
307
347
  // Assignment expressions to class properties
308
348
  if (current.type === utils_1.AST_NODE_TYPES.AssignmentExpression) {
309
349
  const left = current.left;
@@ -324,6 +364,7 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
324
364
  }
325
365
  }
326
366
  }
367
+ previous = current;
327
368
  current = current.parent;
328
369
  }
329
370
  nodeCache.set(node, false);
@@ -652,9 +693,6 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
652
693
  return false;
653
694
  }
654
695
  function findFunctionParameter(node) {
655
- const isFunctionNode = (n) => n.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
656
- n.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
657
- n.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression;
658
696
  const findParamInFunction = (func) => {
659
697
  const param = func.params.find((p) => p.type === utils_1.AST_NODE_TYPES.Identifier &&
660
698
  p.name === node.name &&
@@ -16,9 +16,122 @@ function isInsideFunction(node) {
16
16
  }
17
17
  return false;
18
18
  }
19
- function isFunctionDefinition(node) {
20
- return (node?.type === 'FunctionExpression' ||
21
- node?.type === 'ArrowFunctionExpression');
19
+ /**
20
+ * Walks every child node except `parent` (which would loop back out of the
21
+ * subtree) and reports whether any of them satisfies the predicate.
22
+ */
23
+ function someChildNode(node, predicate) {
24
+ for (const key of Object.keys(node)) {
25
+ if (key === 'parent') {
26
+ continue;
27
+ }
28
+ const value = node[key];
29
+ if (Array.isArray(value)) {
30
+ for (const item of value) {
31
+ if (ASTHelpers_1.ASTHelpers.isNode(item) && predicate(item)) {
32
+ return true;
33
+ }
34
+ }
35
+ }
36
+ else if (ASTHelpers_1.ASTHelpers.isNode(value) && predicate(value)) {
37
+ return true;
38
+ }
39
+ }
40
+ return false;
41
+ }
42
+ /**
43
+ * Whether the subtree reads `this`, `super`, or `new.target` from the scope
44
+ * that lexically encloses it. `declarationIncludesIdentifier` answers the
45
+ * identifier half of "does this helper read from its scope?", but a bare
46
+ * `this` (or `super`, or `new.target`) is not an Identifier node, so an arrow
47
+ * whose only capture is its lexical `this` would otherwise read as
48
+ * free-standing — an inverted answer, not a missed one. The walk stops at
49
+ * nodes that rebind `this` (`function` bodies and class bodies): a `this`
50
+ * inside those belongs to them, so it does not pin the helper to its
51
+ * surroundings.
52
+ */
53
+ function capturesLexicalContext(node) {
54
+ if (node.type === utils_1.AST_NODE_TYPES.ThisExpression ||
55
+ node.type === utils_1.AST_NODE_TYPES.Super) {
56
+ return true;
57
+ }
58
+ if (node.type === utils_1.AST_NODE_TYPES.MetaProperty) {
59
+ return node.meta.name === 'new';
60
+ }
61
+ if (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
62
+ node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
63
+ node.type === utils_1.AST_NODE_TYPES.ClassBody) {
64
+ return false;
65
+ }
66
+ return someChildNode(node, capturesLexicalContext);
67
+ }
68
+ /**
69
+ * Type parameters declared by the functions and classes that enclose `node`.
70
+ * A helper whose signature references one of these names cannot be hoisted:
71
+ * the name is scope-bound even when the runtime body closes over nothing.
72
+ * The `TSTypeParameterDeclaration` check keeps instantiation sites
73
+ * (`TSTypeParameterInstantiation`, e.g. on a call or a type reference) from
74
+ * contributing names.
75
+ */
76
+ function enclosingTypeParameterNames(node) {
77
+ const names = new Set();
78
+ let current = node.parent;
79
+ while (current) {
80
+ const { typeParameters } = current;
81
+ if (typeParameters?.type === utils_1.AST_NODE_TYPES.TSTypeParameterDeclaration) {
82
+ for (const param of typeParameters.params) {
83
+ names.add(param.name.name);
84
+ }
85
+ }
86
+ current = current.parent;
87
+ }
88
+ return names;
89
+ }
90
+ function referencesTypeName(node, names) {
91
+ if (node.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
92
+ node.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
93
+ names.has(node.typeName.name)) {
94
+ return true;
95
+ }
96
+ return someChildNode(node, (child) => referencesTypeName(child, names));
97
+ }
98
+ /**
99
+ * Whether the helper's types are pinned to an enclosing scope. Runtime
100
+ * dependencies are not the only hoisting blocker: `function outer<T>() {
101
+ * const makeList = (): T[] => []; }` reads no value from `outer`, yet moving
102
+ * it to module scope leaves `T` unresolvable. Names the helper redeclares as
103
+ * its own type parameters shadow the enclosing ones, so they are subtracted
104
+ * before scanning; the declarator's own annotation (`const makeList: () =>
105
+ * T[] = ...`) sits outside the helper and can never see the helper's type
106
+ * parameters, so it is scanned against the unsubtracted set.
107
+ */
108
+ function referencesEnclosingTypeParameter(fn, declaration) {
109
+ const enclosingNames = enclosingTypeParameterNames(fn);
110
+ if (enclosingNames.size === 0) {
111
+ return false;
112
+ }
113
+ const idAnnotation = declaration?.id?.type === utils_1.AST_NODE_TYPES.Identifier
114
+ ? declaration.id.typeAnnotation
115
+ : undefined;
116
+ if (idAnnotation && referencesTypeName(idAnnotation, enclosingNames)) {
117
+ return true;
118
+ }
119
+ const visibleNames = new Set(enclosingNames);
120
+ for (const param of fn.typeParameters?.params ?? []) {
121
+ visibleNames.delete(param.name.name);
122
+ }
123
+ return visibleNames.size > 0 && referencesTypeName(fn, visibleNames);
124
+ }
125
+ /**
126
+ * Whether hoisting the helper to module scope could change behaviour or break
127
+ * compilation. Shared by both spellings of a nested helper — `function
128
+ * inner()` and `const inner = () =>` — so the two answer the hoisting
129
+ * question identically (#1755).
130
+ */
131
+ function helperCannotBeHoisted(fn, declaration) {
132
+ return (ASTHelpers_1.ASTHelpers.declarationIncludesIdentifier(fn) ||
133
+ capturesLexicalContext(fn) ||
134
+ referencesEnclosingTypeParameter(fn, declaration));
22
135
  }
23
136
  const unwrapOnce = (node) => {
24
137
  if (!node)
@@ -124,9 +237,33 @@ function isAsConstExpression(node) {
124
237
  }
125
238
  return false;
126
239
  }
240
+ /**
241
+ * The function definition an initializer resolves to, stepping over TS
242
+ * assertion wrappers so `(() => 1) as Handler` classifies the same way as the
243
+ * bare arrow.
244
+ */
245
+ function functionDefinitionOf(node) {
246
+ const unwrapped = unwrapExpression(node);
247
+ if (unwrapped?.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
248
+ unwrapped?.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
249
+ return unwrapped;
250
+ }
251
+ return null;
252
+ }
127
253
  function analyzeDeclarator(declaration) {
128
254
  const init = declaration.init ?? null;
129
- const isFunctionOrMutable = isFunctionDefinition(init) || isMutableValue(init);
255
+ const functionInit = functionDefinitionOf(init);
256
+ /**
257
+ * A function-valued initializer is a nested helper: the same hoisting
258
+ * question as a `function` declaration, asked in a different spelling
259
+ * (#1755). It is exempt only when hoisting could change behaviour — it
260
+ * closes over an enclosing binding, captures lexical `this`/`super`, or
261
+ * names an enclosing type parameter. A helper pinned by none of these flows
262
+ * through the same report path as a plain constant.
263
+ */
264
+ const isFunctionOrMutable = functionInit
265
+ ? helperCannotBeHoisted(functionInit, declaration)
266
+ : isMutableValue(init);
130
267
  return {
131
268
  isFunctionOrMutable,
132
269
  hasDependencies: !isFunctionOrMutable && init
@@ -196,7 +333,7 @@ exports.extractGlobalConstants = (0, createRule_1.createRule)({
196
333
  */
197
334
  if (node.parent && isInsideFunction(node.parent)) {
198
335
  const scope = context.getScope();
199
- const hasDependencies = ASTHelpers_1.ASTHelpers.blockIncludesIdentifier(node.body);
336
+ const hasDependencies = helperCannotBeHoisted(node);
200
337
  if (!hasDependencies && scope.type === 'function') {
201
338
  const funcName = node.id?.name;
202
339
  context.report({