@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 +1 -1
- package/lib/rules/enforce-assert-safe-object-key.js +314 -4
- package/lib/rules/enforce-firestore-doc-ref-generic.js +45 -7
- package/lib/rules/extract-global-constants.js +142 -5
- package/lib/rules/memo-nested-react-components.js +105 -40
- package/lib/rules/no-unused-props.d.ts +1 -1
- package/lib/rules/no-unused-props.js +42 -28
- package/lib/rules/parallelize-async-operations.js +285 -46
- package/lib/rules/prefer-map-over-conditional-dispatch.js +164 -14
- package/lib/rules/prefer-spread-over-reassembly.js +6 -0
- package/lib/rules/prefer-utility-function-over-private-static.d.ts +4 -1
- package/lib/rules/prefer-utility-function-over-private-static.js +430 -54
- package/lib/rules/require-memo.js +110 -32
- package/lib/utils/ASTHelpers.d.ts +28 -0
- package/lib/utils/ASTHelpers.js +111 -170
- package/lib/utils/graph/ClassGraphSorterReadability.d.ts +6 -0
- package/lib/utils/graph/ClassGraphSorterReadability.js +18 -5
- package/package.json +1 -1
- package/release-manifest.json +108 -0
package/lib/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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 =
|
|
336
|
+
const hasDependencies = helperCannotBeHoisted(node);
|
|
200
337
|
if (!hasDependencies && scope.type === 'function') {
|
|
201
338
|
const funcName = node.id?.name;
|
|
202
339
|
context.report({
|