@blumintinc/eslint-plugin-blumint 1.20.176 → 1.20.178
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-firestore-facade.js +232 -64
- package/lib/rules/jsdoc-above-field.js +57 -31
- package/package.json +1 -1
- package/release-manifest.json +28 -0
package/lib/index.js
CHANGED
|
@@ -23,18 +23,43 @@ const CLIENT_BATCH_FACTORY = 'writeBatch';
|
|
|
23
23
|
// while the admin SDK only exposes the `db.runTransaction(cb)` method, so the
|
|
24
24
|
// callee shape, not the name, decides which SDK a transaction belongs to.
|
|
25
25
|
const TRANSACTION_RUNNER = 'runTransaction';
|
|
26
|
+
// Realtime Database modules. DocSetter/DocSetterTransaction wrap Firestore and
|
|
27
|
+
// have no Realtime Database counterpart, so reporting a RealtimeDB receiver
|
|
28
|
+
// prescribes a remedy that cannot exist. Keying the carve-out on the import
|
|
29
|
+
// source rather than on the bare type name keeps an unrelated local type named
|
|
30
|
+
// `Reference` from silencing a genuine Firestore write.
|
|
31
|
+
const REALTIME_DB_MODULES = new Set([
|
|
32
|
+
'firebase-admin/database',
|
|
33
|
+
'firebase/database',
|
|
34
|
+
'@firebase/database',
|
|
35
|
+
]);
|
|
36
|
+
// Exports of those modules that type a RealtimeDB node reference.
|
|
37
|
+
const REALTIME_DB_REFERENCE_TYPES = new Set(['Reference', 'ThenableReference']);
|
|
38
|
+
// Exports that type the database handle a reference is obtained from.
|
|
39
|
+
const REALTIME_DB_HANDLE_TYPES = new Set(['Database']);
|
|
40
|
+
// The modular accessor that returns a handle, so `getDatabase().ref(...)` is
|
|
41
|
+
// recognized without relying on the receiver being spelled `realtimeDb`.
|
|
42
|
+
const REALTIME_DB_HANDLE_FACTORIES = new Set(['getDatabase']);
|
|
26
43
|
const isMemberExpression = (node) => node.type === utils_1.AST_NODE_TYPES.MemberExpression;
|
|
27
44
|
const isCallExpression = (node) => node.type === utils_1.AST_NODE_TYPES.CallExpression;
|
|
28
45
|
const isIdentifier = (node) => node.type === utils_1.AST_NODE_TYPES.Identifier;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
46
|
+
// A TS-only wrapper changes the type of an expression, never the receiver it
|
|
47
|
+
// evaluates to, so every shape check has to look through one. Shared by both
|
|
48
|
+
// the Firestore and the RealtimeDB arms so the two cannot drift apart.
|
|
49
|
+
const unwrapTypeWrappers = (node) => {
|
|
50
|
+
switch (node.type) {
|
|
51
|
+
case utils_1.AST_NODE_TYPES.TSAsExpression:
|
|
52
|
+
case utils_1.AST_NODE_TYPES.TSSatisfiesExpression:
|
|
53
|
+
case utils_1.AST_NODE_TYPES.TSNonNullExpression:
|
|
54
|
+
case utils_1.AST_NODE_TYPES.TSTypeAssertion:
|
|
55
|
+
return unwrapTypeWrappers(node.expression);
|
|
56
|
+
default:
|
|
57
|
+
return node;
|
|
32
58
|
}
|
|
33
|
-
return node;
|
|
34
59
|
};
|
|
35
60
|
const getLeftmostIdentifier = (node) => {
|
|
36
61
|
let current = node
|
|
37
|
-
?
|
|
62
|
+
? unwrapTypeWrappers(node)
|
|
38
63
|
: null;
|
|
39
64
|
while (current) {
|
|
40
65
|
if (isIdentifier(current)) {
|
|
@@ -104,6 +129,19 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
|
|
|
104
129
|
create(context) {
|
|
105
130
|
const realtimeDbRefVariables = new Set();
|
|
106
131
|
const realtimeDbChildVariables = new Set();
|
|
132
|
+
// Bindings holding a RealtimeDB `Database`, from which `.ref(...)` yields a
|
|
133
|
+
// RealtimeDB reference regardless of how the handle is spelled.
|
|
134
|
+
const realtimeDbHandleVariables = new Set();
|
|
135
|
+
// Local names of RealtimeDB type exports, so a binding is classified by its
|
|
136
|
+
// declared type rather than by the receiver's spelling. Aliases
|
|
137
|
+
// (`import type { Reference as RtdbRef }`) are carried by the local name.
|
|
138
|
+
const realtimeDbReferenceTypeLocals = new Set();
|
|
139
|
+
const realtimeDbHandleTypeLocals = new Set();
|
|
140
|
+
// Namespace bindings for a RealtimeDB module (`import * as database`),
|
|
141
|
+
// which put the same types behind a qualified name.
|
|
142
|
+
const realtimeDbNamespaceLocals = new Set();
|
|
143
|
+
// Local names of value exports that return a handle (`getDatabase`).
|
|
144
|
+
const realtimeDbHandleFactoryLocals = new Set();
|
|
107
145
|
const collectionObjectVariables = new Set();
|
|
108
146
|
const firestoreCollectionVariables = new Set();
|
|
109
147
|
const firestoreDocRefVariables = new Set();
|
|
@@ -132,7 +170,7 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
|
|
|
132
170
|
clientFirestoreVariables.delete(name);
|
|
133
171
|
};
|
|
134
172
|
const isClientSdkExportReference = (node, exportName, allowUntracedSpelling) => {
|
|
135
|
-
const callee =
|
|
173
|
+
const callee = unwrapTypeWrappers(node);
|
|
136
174
|
if (isIdentifier(callee)) {
|
|
137
175
|
if (clientSdkImportedLocals.get(callee.name) === exportName) {
|
|
138
176
|
return true;
|
|
@@ -151,7 +189,7 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
|
|
|
151
189
|
};
|
|
152
190
|
// `writeBatch(firestore)` / `fs.writeBatch(firestore)` — client SDK only.
|
|
153
191
|
const isClientBatchFactoryCall = (node) => {
|
|
154
|
-
const candidate =
|
|
192
|
+
const candidate = unwrapTypeWrappers(node);
|
|
155
193
|
return (isCallExpression(candidate) &&
|
|
156
194
|
isClientSdkExportReference(candidate.callee, CLIENT_BATCH_FACTORY, true));
|
|
157
195
|
};
|
|
@@ -160,7 +198,7 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
|
|
|
160
198
|
// could share it, so the binding must be traced back to the SDK.
|
|
161
199
|
const isClientTransactionRunnerCall = (node) => isClientSdkExportReference(node.callee, TRANSACTION_RUNNER, false);
|
|
162
200
|
const recordFirestoreVariable = (varName, expression) => {
|
|
163
|
-
const target =
|
|
201
|
+
const target = unwrapTypeWrappers(expression);
|
|
164
202
|
if (target.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
|
|
165
203
|
const matchedConsequent = recordFirestoreVariable(varName, target.consequent);
|
|
166
204
|
const matchedAlternate = recordFirestoreVariable(varName, target.alternate);
|
|
@@ -232,7 +270,7 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
|
|
|
232
270
|
return false;
|
|
233
271
|
};
|
|
234
272
|
const isFirestoreCollectionCall = (node) => {
|
|
235
|
-
const candidate =
|
|
273
|
+
const candidate = unwrapTypeWrappers(node);
|
|
236
274
|
if (!isCallExpression(candidate))
|
|
237
275
|
return false;
|
|
238
276
|
if (!isMemberExpression(candidate.callee))
|
|
@@ -243,7 +281,7 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
|
|
|
243
281
|
return isFirestoreRoot(candidate.callee.object, firestoreCollectionVariables, firestoreDocRefVariables);
|
|
244
282
|
};
|
|
245
283
|
const isFirestoreDocumentReference = (node) => {
|
|
246
|
-
const candidate =
|
|
284
|
+
const candidate = unwrapTypeWrappers(node);
|
|
247
285
|
if (!isCallExpression(candidate))
|
|
248
286
|
return false;
|
|
249
287
|
if (!isMemberExpression(candidate.callee))
|
|
@@ -271,10 +309,36 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
|
|
|
271
309
|
const varName = node.left.name;
|
|
272
310
|
const right = node.right;
|
|
273
311
|
clearFirestoreTrackingFor(varName);
|
|
312
|
+
// A RealtimeDB binding declared elsewhere keeps its role: the write it
|
|
313
|
+
// receives here has no Firestore facade to route through either way.
|
|
314
|
+
if (registerRealtimeDbValue(varName, right))
|
|
315
|
+
return;
|
|
274
316
|
recordFirestoreVariable(varName, right);
|
|
275
317
|
};
|
|
276
|
-
|
|
277
|
-
|
|
318
|
+
// A `null` imported name marks a namespace or default binding, which puts
|
|
319
|
+
// the module's exports behind a qualifier.
|
|
320
|
+
const recordRealtimeDbImport = (localName, importedName, source) => {
|
|
321
|
+
if (!REALTIME_DB_MODULES.has(source))
|
|
322
|
+
return;
|
|
323
|
+
if (importedName === null) {
|
|
324
|
+
realtimeDbNamespaceLocals.add(localName);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (REALTIME_DB_REFERENCE_TYPES.has(importedName)) {
|
|
328
|
+
realtimeDbReferenceTypeLocals.add(localName);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (REALTIME_DB_HANDLE_TYPES.has(importedName)) {
|
|
332
|
+
realtimeDbHandleTypeLocals.add(localName);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (REALTIME_DB_HANDLE_FACTORIES.has(importedName)) {
|
|
336
|
+
realtimeDbHandleFactoryLocals.add(localName);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
const recordImportBinding = (localName, importedName, source) => {
|
|
340
|
+
recordRealtimeDbImport(localName, importedName, source);
|
|
341
|
+
if (!CLIENT_SDK_MODULES.has(source)) {
|
|
278
342
|
nonClientImportedLocals.add(localName);
|
|
279
343
|
return;
|
|
280
344
|
}
|
|
@@ -285,7 +349,6 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
|
|
|
285
349
|
clientSdkImportedLocals.set(localName, importedName);
|
|
286
350
|
};
|
|
287
351
|
const recordImportPattern = (pattern, source) => {
|
|
288
|
-
const isClientSource = CLIENT_SDK_MODULES.has(source);
|
|
289
352
|
if (pattern.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
290
353
|
for (const property of pattern.properties) {
|
|
291
354
|
if (property.type !== utils_1.AST_NODE_TYPES.Property)
|
|
@@ -293,16 +356,16 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
|
|
|
293
356
|
if (!isIdentifier(property.key) || !isIdentifier(property.value)) {
|
|
294
357
|
continue;
|
|
295
358
|
}
|
|
296
|
-
recordImportBinding(property.value.name, property.key.name,
|
|
359
|
+
recordImportBinding(property.value.name, property.key.name, source);
|
|
297
360
|
}
|
|
298
361
|
return;
|
|
299
362
|
}
|
|
300
363
|
if (isIdentifier(pattern)) {
|
|
301
|
-
recordImportBinding(pattern.name, null,
|
|
364
|
+
recordImportBinding(pattern.name, null, source);
|
|
302
365
|
}
|
|
303
366
|
};
|
|
304
367
|
const getImportExpressionSource = (node) => {
|
|
305
|
-
const expression =
|
|
368
|
+
const expression = unwrapTypeWrappers(node);
|
|
306
369
|
if (expression.type !== utils_1.AST_NODE_TYPES.ImportExpression)
|
|
307
370
|
return null;
|
|
308
371
|
const source = expression.source;
|
|
@@ -324,7 +387,7 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
|
|
|
324
387
|
const init = node.init;
|
|
325
388
|
if (!init || init.type !== utils_1.AST_NODE_TYPES.AwaitExpression)
|
|
326
389
|
return;
|
|
327
|
-
const awaited =
|
|
390
|
+
const awaited = unwrapTypeWrappers(init.argument);
|
|
328
391
|
const directSource = getImportExpressionSource(awaited);
|
|
329
392
|
if (directSource !== null) {
|
|
330
393
|
recordImportPattern(node.id, directSource);
|
|
@@ -377,59 +440,157 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
|
|
|
377
440
|
firestoreTransactionVariables.add(param.name);
|
|
378
441
|
}
|
|
379
442
|
};
|
|
380
|
-
const
|
|
381
|
-
if (
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
return
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
443
|
+
const classifyRealtimeDbTypeName = (typeName) => {
|
|
444
|
+
if (isIdentifier(typeName)) {
|
|
445
|
+
if (realtimeDbReferenceTypeLocals.has(typeName.name)) {
|
|
446
|
+
return 'reference';
|
|
447
|
+
}
|
|
448
|
+
return realtimeDbHandleTypeLocals.has(typeName.name) ? 'handle' : null;
|
|
449
|
+
}
|
|
450
|
+
if (typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
|
|
451
|
+
const qualifier = getLeftmostIdentifier(typeName.left);
|
|
452
|
+
if (!qualifier || !realtimeDbNamespaceLocals.has(qualifier.name)) {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
if (REALTIME_DB_REFERENCE_TYPES.has(typeName.right.name)) {
|
|
456
|
+
return 'reference';
|
|
457
|
+
}
|
|
458
|
+
return REALTIME_DB_HANDLE_TYPES.has(typeName.right.name)
|
|
459
|
+
? 'handle'
|
|
460
|
+
: null;
|
|
461
|
+
}
|
|
462
|
+
return null;
|
|
463
|
+
};
|
|
464
|
+
// `Reference | null` is the ordinary spelling for a ref held across calls,
|
|
465
|
+
// so a union member carries the signal just as a bare annotation does.
|
|
466
|
+
const classifyRealtimeDbType = (typeNode) => {
|
|
467
|
+
if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
|
|
468
|
+
return classifyRealtimeDbTypeName(typeNode.typeName);
|
|
469
|
+
}
|
|
470
|
+
if (typeNode.type === utils_1.AST_NODE_TYPES.TSUnionType ||
|
|
471
|
+
typeNode.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
|
|
472
|
+
for (const member of typeNode.types) {
|
|
473
|
+
const kind = classifyRealtimeDbType(member);
|
|
474
|
+
if (kind)
|
|
475
|
+
return kind;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return null;
|
|
479
|
+
};
|
|
480
|
+
// An assertion states the author's intent as plainly as an annotation, and
|
|
481
|
+
// is the shape RealtimeDB refs take at call sites that narrow away `null`.
|
|
482
|
+
const classifyRealtimeDbAssertion = (node) => {
|
|
483
|
+
let current = node;
|
|
484
|
+
for (;;) {
|
|
485
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSNonNullExpression) {
|
|
486
|
+
current = current.expression;
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
490
|
+
current.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression ||
|
|
491
|
+
current.type === utils_1.AST_NODE_TYPES.TSTypeAssertion) {
|
|
492
|
+
const kind = classifyRealtimeDbType(current.typeAnnotation);
|
|
493
|
+
if (kind)
|
|
494
|
+
return kind;
|
|
495
|
+
current = current.expression;
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
const registerRealtimeDbBinding = (name, kind) => {
|
|
502
|
+
if (kind === 'reference') {
|
|
503
|
+
realtimeDbRefVariables.add(name);
|
|
395
504
|
return true;
|
|
396
505
|
}
|
|
397
|
-
if (
|
|
398
|
-
|
|
399
|
-
isIdentifier(init.callee.property) &&
|
|
400
|
-
init.callee.property.name === 'child' &&
|
|
401
|
-
isIdentifier(init.callee.object) &&
|
|
402
|
-
realtimeDbRefVariables.has(init.callee.object.name) &&
|
|
403
|
-
isIdentifier(node.id)) {
|
|
404
|
-
realtimeDbChildVariables.add(node.id.name);
|
|
506
|
+
if (kind === 'handle') {
|
|
507
|
+
realtimeDbHandleVariables.add(name);
|
|
405
508
|
return true;
|
|
406
509
|
}
|
|
407
510
|
return false;
|
|
408
511
|
};
|
|
512
|
+
const isRealtimeDbHandleFactoryCall = (node) => {
|
|
513
|
+
const target = unwrapTypeWrappers(node);
|
|
514
|
+
if (!isCallExpression(target))
|
|
515
|
+
return false;
|
|
516
|
+
const callee = target.callee;
|
|
517
|
+
if (isIdentifier(callee)) {
|
|
518
|
+
return realtimeDbHandleFactoryLocals.has(callee.name);
|
|
519
|
+
}
|
|
520
|
+
return (isMemberExpression(callee) &&
|
|
521
|
+
isIdentifier(callee.property) &&
|
|
522
|
+
REALTIME_DB_HANDLE_FACTORIES.has(callee.property.name) &&
|
|
523
|
+
isIdentifier(callee.object) &&
|
|
524
|
+
realtimeDbNamespaceLocals.has(callee.object.name));
|
|
525
|
+
};
|
|
526
|
+
const isRealtimeDbHandle = (node) => {
|
|
527
|
+
if (classifyRealtimeDbAssertion(node) === 'handle')
|
|
528
|
+
return true;
|
|
529
|
+
if (isRealtimeDbHandleFactoryCall(node))
|
|
530
|
+
return true;
|
|
531
|
+
const target = unwrapTypeWrappers(node);
|
|
532
|
+
return (isIdentifier(target) &&
|
|
533
|
+
(target.name.includes('realtimeDb') ||
|
|
534
|
+
realtimeDbHandleVariables.has(target.name)));
|
|
535
|
+
};
|
|
536
|
+
const isRealtimeDbMethodCallOn = (node, method, isRealtimeDbReceiver) => {
|
|
537
|
+
const target = unwrapTypeWrappers(node);
|
|
538
|
+
return (isCallExpression(target) &&
|
|
539
|
+
isMemberExpression(target.callee) &&
|
|
540
|
+
isIdentifier(target.callee.property) &&
|
|
541
|
+
target.callee.property.name === method &&
|
|
542
|
+
isRealtimeDbReceiver(target.callee.object));
|
|
543
|
+
};
|
|
409
544
|
const isRealtimeDbReference = (node) => {
|
|
410
|
-
if (node
|
|
411
|
-
isMemberExpression(node.callee) &&
|
|
412
|
-
isIdentifier(node.callee.property) &&
|
|
413
|
-
node.callee.property.name === 'ref' &&
|
|
414
|
-
isIdentifier(node.callee.object) &&
|
|
415
|
-
(node.callee.object.name === 'realtimeDb' ||
|
|
416
|
-
node.callee.object.name.includes('realtimeDb'))) {
|
|
545
|
+
if (classifyRealtimeDbAssertion(node) === 'reference')
|
|
417
546
|
return true;
|
|
547
|
+
const target = unwrapTypeWrappers(node);
|
|
548
|
+
if (isIdentifier(target)) {
|
|
549
|
+
return (realtimeDbRefVariables.has(target.name) ||
|
|
550
|
+
realtimeDbChildVariables.has(target.name));
|
|
418
551
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
552
|
+
return (isRealtimeDbMethodCallOn(target, 'ref', isRealtimeDbHandle) ||
|
|
553
|
+
isRealtimeDbMethodCallOn(target, 'child', isRealtimeDbReference));
|
|
554
|
+
};
|
|
555
|
+
// Returns the RealtimeDB role a value carries, so the two registration
|
|
556
|
+
// sites (declarator init and later assignment) share one classification.
|
|
557
|
+
const classifyRealtimeDbValue = (node) => {
|
|
558
|
+
const asserted = classifyRealtimeDbAssertion(node);
|
|
559
|
+
if (asserted)
|
|
560
|
+
return asserted;
|
|
561
|
+
if (isRealtimeDbHandleFactoryCall(node))
|
|
562
|
+
return 'handle';
|
|
563
|
+
if (isRealtimeDbMethodCallOn(node, 'ref', isRealtimeDbHandle)) {
|
|
564
|
+
return 'reference';
|
|
565
|
+
}
|
|
566
|
+
return isRealtimeDbMethodCallOn(node, 'child', isRealtimeDbReference)
|
|
567
|
+
? 'child'
|
|
568
|
+
: null;
|
|
569
|
+
};
|
|
570
|
+
const registerRealtimeDbValue = (name, value) => {
|
|
571
|
+
const kind = classifyRealtimeDbValue(value);
|
|
572
|
+
if (kind === 'child') {
|
|
573
|
+
realtimeDbChildVariables.add(name);
|
|
430
574
|
return true;
|
|
431
575
|
}
|
|
432
|
-
return
|
|
576
|
+
return registerRealtimeDbBinding(name, kind);
|
|
577
|
+
};
|
|
578
|
+
const isRealtimeDbRefAssignment = (node) => {
|
|
579
|
+
if (node.type !== utils_1.AST_NODE_TYPES.VariableDeclarator)
|
|
580
|
+
return false;
|
|
581
|
+
if (!isIdentifier(node.id))
|
|
582
|
+
return false;
|
|
583
|
+
const varName = node.id.name;
|
|
584
|
+
// Registering from the annotation covers `let ref: Reference | null`,
|
|
585
|
+
// where the RealtimeDB call arrives later as an assignment rather than as
|
|
586
|
+
// this declarator's init.
|
|
587
|
+
const annotation = node.id.typeAnnotation?.typeAnnotation;
|
|
588
|
+
if (annotation &&
|
|
589
|
+
registerRealtimeDbBinding(varName, classifyRealtimeDbType(annotation))) {
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
const init = node.init;
|
|
593
|
+
return !!init && registerRealtimeDbValue(varName, init);
|
|
433
594
|
};
|
|
434
595
|
const isTrackedFirestoreName = (name) => firestoreDocRefVariables.has(name) ||
|
|
435
596
|
firestoreBatchVariables.has(name) ||
|
|
@@ -505,7 +666,7 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
|
|
|
505
666
|
}
|
|
506
667
|
let current = object;
|
|
507
668
|
while (current) {
|
|
508
|
-
const unwrapped =
|
|
669
|
+
const unwrapped = unwrapTypeWrappers(current);
|
|
509
670
|
if (isCallExpression(unwrapped) &&
|
|
510
671
|
isMemberExpression(unwrapped.callee) &&
|
|
511
672
|
isIdentifier(unwrapped.callee.property)) {
|
|
@@ -539,16 +700,23 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
|
|
|
539
700
|
};
|
|
540
701
|
return {
|
|
541
702
|
ImportDeclaration(node) {
|
|
542
|
-
const source = node.source.value;
|
|
543
|
-
const isClientSource = typeof source === 'string' && CLIENT_SDK_MODULES.has(source);
|
|
703
|
+
const source = typeof node.source.value === 'string' ? node.source.value : '';
|
|
544
704
|
for (const specifier of node.specifiers) {
|
|
545
705
|
if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier) {
|
|
546
|
-
recordImportBinding(specifier.local.name, specifier.imported.name,
|
|
706
|
+
recordImportBinding(specifier.local.name, specifier.imported.name, source);
|
|
547
707
|
continue;
|
|
548
708
|
}
|
|
549
|
-
recordImportBinding(specifier.local.name, null,
|
|
709
|
+
recordImportBinding(specifier.local.name, null, source);
|
|
550
710
|
}
|
|
551
711
|
},
|
|
712
|
+
// Parameters and other annotated bindings declare their RealtimeDB role
|
|
713
|
+
// without ever holding a `ref(...)` call of their own.
|
|
714
|
+
Identifier(node) {
|
|
715
|
+
const annotation = node.typeAnnotation?.typeAnnotation;
|
|
716
|
+
if (!annotation)
|
|
717
|
+
return;
|
|
718
|
+
registerRealtimeDbBinding(node.name, classifyRealtimeDbType(annotation));
|
|
719
|
+
},
|
|
552
720
|
VariableDeclarator(node) {
|
|
553
721
|
recordDynamicImportBindings(node);
|
|
554
722
|
isRealtimeDbRefAssignment(node);
|
|
@@ -108,22 +108,62 @@ exports.jsdocAboveField = (0, createRule_1.createRule)({
|
|
|
108
108
|
: node.range[1],
|
|
109
109
|
};
|
|
110
110
|
};
|
|
111
|
+
const containerOf = (node) => {
|
|
112
|
+
const parent = node.parent;
|
|
113
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
|
|
114
|
+
return { node: parent, members: parent.members, separator: ';' };
|
|
115
|
+
}
|
|
116
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.TSInterfaceBody) {
|
|
117
|
+
return { node: parent, members: parent.body, separator: ';' };
|
|
118
|
+
}
|
|
119
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.ClassBody) {
|
|
120
|
+
return { node: parent, members: parent.body, separator: ';' };
|
|
121
|
+
}
|
|
122
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
123
|
+
return { node: parent, members: parent.properties, separator: ',' };
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Whether nothing but the container's closing brace follows the block, so
|
|
129
|
+
* the field it trails is the only member it can belong to.
|
|
130
|
+
*/
|
|
131
|
+
const closesContainer = (node, comment) => {
|
|
132
|
+
const container = containerOf(node);
|
|
133
|
+
if (!container) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
const closeBrace = sourceCode.getLastToken(container.node);
|
|
137
|
+
if (!closeBrace || comment.range[1] > closeBrace.range[0]) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
return !container.members.some((member) => member.range[0] >= comment.range[1]);
|
|
141
|
+
};
|
|
111
142
|
/**
|
|
112
143
|
* Attaches a trailing JSDoc block by token order rather than by line.
|
|
113
144
|
*
|
|
114
|
-
* Prettier
|
|
115
|
-
* line
|
|
116
|
-
* `timeout: number;`
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
145
|
+
* Prettier is not idempotent on a multi-line block that trails a field. One
|
|
146
|
+
* pass reflows it onto its own line ahead of the member's separator, so the
|
|
147
|
+
* block that followed `timeout: number;` sits between `timeout: number` and
|
|
148
|
+
* its `;`; the next pass moves the separator back in front of it, and that
|
|
149
|
+
* separator-first spelling is the fixed point formatted source converges
|
|
150
|
+
* to. Both intermediates and the fixed point document the field above them,
|
|
151
|
+
* so keying on the comment sharing the field's line — or on the separator
|
|
152
|
+
* still following it — leaves the rule inert on the shape it exists to
|
|
153
|
+
* police.
|
|
154
|
+
*
|
|
155
|
+
* Past the separator the member has ended and position alone stops naming
|
|
156
|
+
* an owner: an own-line block there reads as the leading documentation of
|
|
157
|
+
* the next field. That reading needs a next field, so it is unavailable on
|
|
158
|
+
* the last member of a container, where the preceding field is the only
|
|
159
|
+
* candidate left.
|
|
120
160
|
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
161
|
+
* A blank line is the one signal that survives the round trip intact:
|
|
162
|
+
* prettier preserves an authored one and never inserts one while reflowing,
|
|
163
|
+
* so a block held off by an empty line is a deliberate note about the
|
|
164
|
+
* enclosing shape rather than displaced documentation.
|
|
125
165
|
*/
|
|
126
|
-
const trailingJSDocFor = (span) => {
|
|
166
|
+
const trailingJSDocFor = (node, span) => {
|
|
127
167
|
return allComments.find((comment) => {
|
|
128
168
|
if (!isJSDocBlock(comment)) {
|
|
129
169
|
return false;
|
|
@@ -131,11 +171,13 @@ exports.jsdocAboveField = (0, createRule_1.createRule)({
|
|
|
131
171
|
if (comment.range[0] < span.offset) {
|
|
132
172
|
return false;
|
|
133
173
|
}
|
|
174
|
+
const between = sourceCode.text.slice(span.offset, comment.range[0]);
|
|
134
175
|
const precedesSeparator = comment.range[1] <= span.separatorEnd;
|
|
135
176
|
if (!precedesSeparator && comment.loc.start.line !== span.line) {
|
|
136
|
-
|
|
177
|
+
if (!closesContainer(node, comment) || /\n[^\S\n]*\n/.test(between)) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
137
180
|
}
|
|
138
|
-
const between = sourceCode.text.slice(span.offset, comment.range[0]);
|
|
139
181
|
return /^[\s;,]*$/.test(between);
|
|
140
182
|
});
|
|
141
183
|
};
|
|
@@ -236,22 +278,6 @@ exports.jsdocAboveField = (0, createRule_1.createRule)({
|
|
|
236
278
|
*/
|
|
237
279
|
const lineIndentOf = (node) => /^[ \t]*/.exec(sourceCode.lines[node.loc.start.line - 1] ?? '')?.[0] ??
|
|
238
280
|
'';
|
|
239
|
-
const containerOf = (node) => {
|
|
240
|
-
const parent = node.parent;
|
|
241
|
-
if (parent?.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
|
|
242
|
-
return { node: parent, members: parent.members, separator: ';' };
|
|
243
|
-
}
|
|
244
|
-
if (parent?.type === utils_1.AST_NODE_TYPES.TSInterfaceBody) {
|
|
245
|
-
return { node: parent, members: parent.body, separator: ';' };
|
|
246
|
-
}
|
|
247
|
-
if (parent?.type === utils_1.AST_NODE_TYPES.ClassBody) {
|
|
248
|
-
return { node: parent, members: parent.body, separator: ';' };
|
|
249
|
-
}
|
|
250
|
-
if (parent?.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
251
|
-
return { node: parent, members: parent.properties, separator: ',' };
|
|
252
|
-
}
|
|
253
|
-
return undefined;
|
|
254
|
-
};
|
|
255
281
|
/**
|
|
256
282
|
* A member's separator sits inside its range for type and class members
|
|
257
283
|
* but outside it for object literal properties, so both spellings have to
|
|
@@ -314,7 +340,7 @@ exports.jsdocAboveField = (0, createRule_1.createRule)({
|
|
|
314
340
|
jsdoc: member === node
|
|
315
341
|
? comment
|
|
316
342
|
: isRelevantNode(member)
|
|
317
|
-
? trailingJSDocFor(fieldSpanOf(member))
|
|
343
|
+
? trailingJSDocFor(member, fieldSpanOf(member))
|
|
318
344
|
: undefined,
|
|
319
345
|
};
|
|
320
346
|
});
|
|
@@ -420,7 +446,7 @@ exports.jsdocAboveField = (0, createRule_1.createRule)({
|
|
|
420
446
|
return;
|
|
421
447
|
}
|
|
422
448
|
const span = fieldSpanOf(node);
|
|
423
|
-
const jsdocComment = trailingJSDocFor(span);
|
|
449
|
+
const jsdocComment = trailingJSDocFor(node, span);
|
|
424
450
|
if (!jsdocComment) {
|
|
425
451
|
return;
|
|
426
452
|
}
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.178",
|
|
4
|
+
"date": "2026-08-26T22:11:53.342Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-firestore-facade",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2149
|
|
11
|
+
],
|
|
12
|
+
"summary": "key the RealtimeDB carve-out on declared type (closes #2149)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.20.177",
|
|
18
|
+
"date": "2026-08-26T16:36:01.459Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "jsdoc-above-field",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
2145
|
|
25
|
+
],
|
|
26
|
+
"summary": "claim a trailing block that closes its container (closes #2145)"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
2
30
|
{
|
|
3
31
|
"version": "1.20.176",
|
|
4
32
|
"date": "2026-08-26T11:56:32.903Z",
|