@blumintinc/eslint-plugin-blumint 1.20.137 → 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.
Files changed (41) hide show
  1. package/lib/index.js +1 -1
  2. package/lib/rules/enforce-assert-safe-object-key.js +314 -4
  3. package/lib/rules/enforce-centralized-mock-firestore.js +46 -3
  4. package/lib/rules/enforce-firestore-doc-ref-generic.js +45 -7
  5. package/lib/rules/enforce-firestore-set-merge.js +278 -66
  6. package/lib/rules/enforce-microdiff.js +87 -20
  7. package/lib/rules/extract-global-constants.js +142 -5
  8. package/lib/rules/fast-deep-equal-over-microdiff.js +220 -71
  9. package/lib/rules/key-only-outermost-element.js +159 -21
  10. package/lib/rules/memo-nested-react-components.js +105 -40
  11. package/lib/rules/no-empty-dependency-use-callbacks.js +116 -43
  12. package/lib/rules/no-explicit-return-type.js +164 -21
  13. package/lib/rules/no-mock-firebase-admin.js +52 -8
  14. package/lib/rules/no-redundant-usecallback-wrapper.d.ts +2 -1
  15. package/lib/rules/no-redundant-usecallback-wrapper.js +126 -70
  16. package/lib/rules/no-unused-props.d.ts +1 -1
  17. package/lib/rules/no-unused-props.js +42 -28
  18. package/lib/rules/no-useless-usememo-primitives.d.ts +2 -1
  19. package/lib/rules/no-useless-usememo-primitives.js +156 -47
  20. package/lib/rules/no-usememo-for-pass-by-value.js +165 -220
  21. package/lib/rules/parallelize-async-operations.js +285 -46
  22. package/lib/rules/prefer-map-over-conditional-dispatch.js +164 -14
  23. package/lib/rules/prefer-params-over-parent-id.d.ts +2 -1
  24. package/lib/rules/prefer-params-over-parent-id.js +122 -39
  25. package/lib/rules/prefer-spread-over-reassembly.js +6 -0
  26. package/lib/rules/prefer-utility-function-over-private-static.d.ts +4 -1
  27. package/lib/rules/prefer-utility-function-over-private-static.js +430 -54
  28. package/lib/rules/require-memo.js +110 -32
  29. package/lib/rules/use-latest-callback.js +109 -11
  30. package/lib/utils/ASTHelpers.d.ts +28 -0
  31. package/lib/utils/ASTHelpers.js +111 -170
  32. package/lib/utils/graph/ClassGraphSorterReadability.d.ts +6 -0
  33. package/lib/utils/graph/ClassGraphSorterReadability.js +18 -5
  34. package/lib/utils/importRemoval.d.ts +88 -0
  35. package/lib/utils/importRemoval.js +127 -46
  36. package/lib/utils/patternBindingRemoval.d.ts +28 -0
  37. package/lib/utils/patternBindingRemoval.js +199 -0
  38. package/lib/utils/typeDeclarationRemoval.d.ts +32 -0
  39. package/lib/utils/typeDeclarationRemoval.js +105 -0
  40. package/package.json +1 -1
  41. package/release-manifest.json +218 -0
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.137',
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:
@@ -4,6 +4,7 @@ exports.enforceCentralizedMockFirestore = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
6
  const importInsertion_1 = require("../utils/importInsertion");
7
+ const importRemoval_1 = require("../utils/importRemoval");
7
8
  /**
8
9
  * The centralized module's identity: the path segments that name it, without
9
10
  * the relative prefix a specifier needs to reach it. Keeping the identity
@@ -166,6 +167,32 @@ function retiredSpan(node) {
166
167
  }
167
168
  return undefined;
168
169
  }
170
+ function isWithinAny(range, ranges) {
171
+ return ranges.some(([start, end]) => range[0] >= start && range[1] <= end);
172
+ }
173
+ /**
174
+ * The orphans the retirement carries away with it, as opposed to the ones it
175
+ * strands.
176
+ *
177
+ * A binding whose own declaration sits inside a retired span disappears with
178
+ * that span — retiring the local mock is the whole point of the fix, so the
179
+ * binding losing its last reference is not a defect. Every other orphan has a
180
+ * declaration that SURVIVES: `const myMockFirestore = jest.fn();` under
181
+ * `const mockFirestore = myMockFirestore;` is left bound to nothing the moment
182
+ * the alias goes, turning a file that lints clean into one that fails
183
+ * `no-unused-vars` and `noUnusedLocals` (#1900).
184
+ *
185
+ * Deleting the survivor instead is not on offer: its initializer is an
186
+ * arbitrary expression — `jest.fn()`, a factory call, a `require` — whose effect
187
+ * this fixer cannot prove absent, and dropping it would delete working code to
188
+ * settle a lint warning. The fix is therefore withheld and the report stands, so
189
+ * the local mock is still surfaced for a human to retire. An orphaned IMPORT
190
+ * never reaches here; the shared planner retires it in the same fix.
191
+ */
192
+ const retirementCarriesOrphan = (variables, removed) => variables.every((variable) => variable.identifiers.length > 0 &&
193
+ variable.identifiers.every((identifier) => isWithinAny(identifier.range, removed)))
194
+ ? []
195
+ : null;
169
196
  /**
170
197
  * Collapses edits that touch, so an overlap can never rewrite a range twice.
171
198
  */
@@ -392,6 +419,18 @@ exports.enforceCentralizedMockFirestore = (0, createRule_1.createRule)({
392
419
  ? retirementEdit(originalText, span.start, span.end)
393
420
  : { start: span.start, end: span.end, text: '' });
394
421
  }
422
+ // Whatever the retired declarations were the last readers of has
423
+ // to go with them, or the fix trades this report for an unused
424
+ // binding the consumer's build fails on (#1900). Only the
425
+ // retirements are handed over: the reference rewrites below swap
426
+ // one name for another rather than deleting a declaration, and a
427
+ // rewritten call to a RENAMED centralized import would otherwise
428
+ // read as unbinding the very import the file is told to keep.
429
+ const orphaned = (0, importRemoval_1.planOrphanedBindingRemoval)(sourceCode, removals.map(({ start, end }) => [start, end]), retirementCarriesOrphan);
430
+ if (!orphaned) {
431
+ return null;
432
+ }
433
+ const orphanRemovals = orphaned.map(([start, end]) => ({ start, end, text: '' }));
395
434
  // Replace custom mockFirestore references with the standard one
396
435
  const replacements = [];
397
436
  // Add replacements for custom mockFirestore names
@@ -412,11 +451,15 @@ exports.enforceCentralizedMockFirestore = (0, createRule_1.createRule)({
412
451
  text: 'mockFirestore',
413
452
  });
414
453
  });
415
- // A reference inside a retired declaration goes away with it, so
454
+ // A reference inside a deleted range goes away with it, so
416
455
  // rewriting it would only fight the removal for the same range.
417
- const survivingReplacements = replacements.filter((replacement) => !removals.some((removal) => replacement.start >= removal.start &&
456
+ const deletions = [...removals, ...orphanRemovals];
457
+ const survivingReplacements = replacements.filter((replacement) => !deletions.some((removal) => replacement.start >= removal.start &&
418
458
  replacement.end <= removal.end));
419
- const edits = mergeEdits([...removals, ...survivingReplacements]);
459
+ const edits = mergeEdits([
460
+ ...deletions,
461
+ ...survivingReplacements,
462
+ ]);
420
463
  // Every edit is bounded to the characters it owns. Rebuilding
421
464
  // the file and writing it over the `Program` node instead would
422
465
  // emit whatever precedes `Program.range[0]` — a header comment,
@@ -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 &&