@blumintinc/eslint-plugin-blumint 1.20.108 → 1.20.109
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
|
@@ -6,6 +6,85 @@ const createRule_1 = require("../utils/createRule");
|
|
|
6
6
|
// Define the operations that are considered reads and writes
|
|
7
7
|
const READ_OPERATIONS = new Set(['get']);
|
|
8
8
|
const WRITE_OPERATIONS = new Set(['set', 'update', 'delete']);
|
|
9
|
+
/**
|
|
10
|
+
* Helpers that validate a property key and hand back that very key, so a call
|
|
11
|
+
* to one names exactly the method its argument names. `enforce-assert-safe-object-key`
|
|
12
|
+
* is `error` in the same recommended config and its fixer wraps computed keys in
|
|
13
|
+
* `assertSafe(...)`, which means this shape is machine-generated from ordinary
|
|
14
|
+
* `transaction[methodName]` code rather than hand-written.
|
|
15
|
+
*/
|
|
16
|
+
const KEY_ASSERTION_HELPERS = new Set(['assertSafe']);
|
|
17
|
+
/**
|
|
18
|
+
* Strips wrappers that erase at compile time or resolve to the key itself.
|
|
19
|
+
* `k as string`, `k satisfies string`, `<string>k` and `k!` emit nothing, and
|
|
20
|
+
* `await k` yields the same key, so none of them changes which method the
|
|
21
|
+
* lookup selects.
|
|
22
|
+
*
|
|
23
|
+
* The peel repeats because the wrappers nest: `(k as any)!`.
|
|
24
|
+
*/
|
|
25
|
+
function unwrapErasedKey(node) {
|
|
26
|
+
let current = node;
|
|
27
|
+
for (;;) {
|
|
28
|
+
switch (current.type) {
|
|
29
|
+
case utils_1.AST_NODE_TYPES.TSAsExpression:
|
|
30
|
+
case utils_1.AST_NODE_TYPES.TSSatisfiesExpression:
|
|
31
|
+
case utils_1.AST_NODE_TYPES.TSNonNullExpression:
|
|
32
|
+
case utils_1.AST_NODE_TYPES.TSTypeAssertion:
|
|
33
|
+
current = current.expression;
|
|
34
|
+
break;
|
|
35
|
+
case utils_1.AST_NODE_TYPES.AwaitExpression:
|
|
36
|
+
current = current.argument;
|
|
37
|
+
break;
|
|
38
|
+
default:
|
|
39
|
+
return current;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** True for `assertSafe(k)` and for a namespaced `utils.assertSafe(k)`. */
|
|
44
|
+
function isKeyAssertionCall(node) {
|
|
45
|
+
const { callee } = node;
|
|
46
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
47
|
+
return KEY_ASSERTION_HELPERS.has(callee.name);
|
|
48
|
+
}
|
|
49
|
+
return (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
50
|
+
!callee.computed &&
|
|
51
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
52
|
+
KEY_ASSERTION_HELPERS.has(callee.property.name));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Resolves the method a computed access selects: `transaction[<key>]()`.
|
|
56
|
+
*
|
|
57
|
+
* A key assertion returns its argument untouched, so reading through one is
|
|
58
|
+
* information-free — `transaction[assertSafe(k)]` must land on exactly the
|
|
59
|
+
* verdict `transaction[k]` lands on, definite name and all. Every other call
|
|
60
|
+
* may return something other than what it was handed, so its argument proves
|
|
61
|
+
* nothing about the method and the key counts as unresolved. That keeps the
|
|
62
|
+
* safety net on for any wrapper (`transaction[String(k)]`) while never minting
|
|
63
|
+
* a definite read/write verdict out of a call whose result is unknown.
|
|
64
|
+
*/
|
|
65
|
+
function resolveComputedKey(property) {
|
|
66
|
+
let current = unwrapErasedKey(property);
|
|
67
|
+
for (;;) {
|
|
68
|
+
if (current.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
69
|
+
typeof current.value === 'string') {
|
|
70
|
+
return { kind: 'name', name: current.value };
|
|
71
|
+
}
|
|
72
|
+
if (current.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
73
|
+
return { kind: 'unresolved' };
|
|
74
|
+
}
|
|
75
|
+
if (current.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
76
|
+
const [argument] = current.arguments;
|
|
77
|
+
if (!isKeyAssertionCall(current) ||
|
|
78
|
+
current.arguments.length !== 1 ||
|
|
79
|
+
argument.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
80
|
+
return { kind: 'unresolved' };
|
|
81
|
+
}
|
|
82
|
+
current = unwrapErasedKey(argument);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
return { kind: 'opaque' };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
9
88
|
exports.firestoreTransactionReadsBeforeWrites = (0, createRule_1.createRule)({
|
|
10
89
|
name: 'firestore-transaction-reads-before-writes',
|
|
11
90
|
meta: {
|
|
@@ -119,18 +198,19 @@ exports.firestoreTransactionReadsBeforeWrites = (0, createRule_1.createRule)({
|
|
|
119
198
|
// Normal property access: transaction.get()
|
|
120
199
|
methodName = property.name;
|
|
121
200
|
}
|
|
122
|
-
else if (callee.computed
|
|
123
|
-
property
|
|
124
|
-
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
201
|
+
else if (callee.computed) {
|
|
202
|
+
// Computed property access: transaction['get'], transaction[methodName],
|
|
203
|
+
// transaction[assertSafe(methodName)]. A key that survives resolution
|
|
204
|
+
// as a definite string names the method; one that cannot be resolved
|
|
205
|
+
// could be any method, so it is answered conservatively and the caller
|
|
206
|
+
// decides.
|
|
207
|
+
const resolved = resolveComputedKey(property);
|
|
208
|
+
if (resolved.kind === 'unresolved') {
|
|
209
|
+
return { isRead: true, isWrite: true, methodName: null };
|
|
210
|
+
}
|
|
211
|
+
if (resolved.kind === 'name') {
|
|
212
|
+
methodName = resolved.name;
|
|
213
|
+
}
|
|
134
214
|
}
|
|
135
215
|
if (!methodName) {
|
|
136
216
|
return { isRead: false, isWrite: false, methodName: null };
|
|
@@ -154,7 +234,14 @@ exports.firestoreTransactionReadsBeforeWrites = (0, createRule_1.createRule)({
|
|
|
154
234
|
if (callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
155
235
|
return `${objectName}[${callee.property.name}]`;
|
|
156
236
|
}
|
|
157
|
-
|
|
237
|
+
// A wrapped key such as assertSafe(methodName) is quoted verbatim so
|
|
238
|
+
// the message names text that exists in the file and can be searched
|
|
239
|
+
// for, rather than the resolved key the reader never wrote.
|
|
240
|
+
const keyText = context
|
|
241
|
+
.getSourceCode()
|
|
242
|
+
.getText(callee.property)
|
|
243
|
+
.replace(/\s+/g, ' ');
|
|
244
|
+
return `${objectName}[${keyText}]`;
|
|
158
245
|
}
|
|
159
246
|
if (callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
160
247
|
return `${objectName}.${callee.property.name}`;
|
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.noRedundantUseCallbackWrapper = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const LATEST_CALLBACK_MODULE = 'use-latest-callback';
|
|
7
|
+
const LATEST_CALLBACK_HOOK = 'useLatestCallback';
|
|
6
8
|
function isHookLikeName(name) {
|
|
7
9
|
return name.startsWith('use');
|
|
8
10
|
}
|
|
@@ -100,7 +102,10 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
|
|
|
100
102
|
},
|
|
101
103
|
],
|
|
102
104
|
messages: {
|
|
103
|
-
|
|
105
|
+
// The wrapper is named rather than hardcoded: the rule reports
|
|
106
|
+
// `useLatestCallback` too, which has no dependency array, so a message
|
|
107
|
+
// asserting one would describe code the reader cannot find.
|
|
108
|
+
redundantWrapper: '{{wrapper}} is wrapping memoized callback "{{callbackName}}", adding a redundant memoization layer without improving stability. Pass the hook/context callback directly so React keeps the original stable reference and avoids wrapper allocations and dependency drift.',
|
|
104
109
|
},
|
|
105
110
|
},
|
|
106
111
|
defaultOptions: [{}],
|
|
@@ -109,10 +114,59 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
|
|
|
109
114
|
const knownHooks = new Set(option.memoizedHookNames ?? []);
|
|
110
115
|
const assumeAllUseAreMemoized = option.assumeAllUseAreMemoized === true;
|
|
111
116
|
const sourceCode = context.sourceCode;
|
|
117
|
+
// Every callee that memoizes the callback handed to it. `useLatestCallback`
|
|
118
|
+
// belongs here because `use-latest-callback` — 'error' in the same
|
|
119
|
+
// recommended config, and fixable — rewrites every `useCallback(fn, deps)`
|
|
120
|
+
// into `useLatestCallback(fn)`. The wrapper it produces is the very
|
|
121
|
+
// construct this rule objects to, still allocating a fresh arrow around an
|
|
122
|
+
// already stable callback, so without this entry one `eslint --fix` renames
|
|
123
|
+
// the violation out of view while leaving it byte-for-byte intact — and the
|
|
124
|
+
// config mandating that spelling means it is also written by hand (#1726).
|
|
125
|
+
const wrapperNames = new Set(['useCallback', LATEST_CALLBACK_HOOK]);
|
|
126
|
+
/**
|
|
127
|
+
* The wrapper's name if this callee is one, else null. Reading the name
|
|
128
|
+
* rather than a boolean lets the report say which wrapper it found, since
|
|
129
|
+
* the local binding need not be spelled `useLatestCallback` at all.
|
|
130
|
+
*/
|
|
131
|
+
const wrapperNameOf = (callee) => {
|
|
132
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
133
|
+
return wrapperNames.has(callee.name) ? callee.name : null;
|
|
134
|
+
}
|
|
135
|
+
// Namespaced spelling, e.g. React.useCallback
|
|
136
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
137
|
+
!callee.computed &&
|
|
138
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
139
|
+
return wrapperNames.has(callee.property.name)
|
|
140
|
+
? callee.property.name
|
|
141
|
+
: null;
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
};
|
|
112
145
|
// Track identifiers coming from hook-like calls
|
|
113
146
|
const hookReturnObjects = new Set(); // variables assigned to a hook call result (object or function)
|
|
114
147
|
const hookReturnProps = new Set(); // properties destructured from a hook call result
|
|
115
148
|
return {
|
|
149
|
+
ImportDeclaration(node) {
|
|
150
|
+
// The module's sole export is the hook, so its DEFAULT specifier binds
|
|
151
|
+
// it under whatever local name the file chose — a shape a set of bare
|
|
152
|
+
// hook names cannot see. `use-latest-callback`'s own fixer picks that
|
|
153
|
+
// name with `freeImportName`, falling back to `useLatestCallback2` when
|
|
154
|
+
// `useLatestCallback` is already taken in the file, so the alias is
|
|
155
|
+
// authored by the sibling fixer rather than being hypothetical.
|
|
156
|
+
if (node.source.value !== LATEST_CALLBACK_MODULE ||
|
|
157
|
+
(node.importKind && node.importKind !== 'value')) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
for (const specifier of node.specifiers) {
|
|
161
|
+
if (specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier ||
|
|
162
|
+
(specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
163
|
+
specifier.importKind !== 'type' &&
|
|
164
|
+
specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
165
|
+
specifier.imported.name === LATEST_CALLBACK_HOOK)) {
|
|
166
|
+
wrapperNames.add(specifier.local.name);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
},
|
|
116
170
|
VariableDeclarator(node) {
|
|
117
171
|
if (!node.init)
|
|
118
172
|
return;
|
|
@@ -145,17 +199,13 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
|
|
|
145
199
|
}
|
|
146
200
|
},
|
|
147
201
|
CallExpression(node) {
|
|
148
|
-
// Detect
|
|
202
|
+
// Detect memoization wrappers (including React.useCallback and the
|
|
203
|
+
// useLatestCallback spelling the config's own fixer produces)
|
|
149
204
|
const calleeNode = unwrapChainExpression(node.callee);
|
|
150
205
|
if (!calleeNode)
|
|
151
206
|
return;
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
(calleeNode.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
155
|
-
!calleeNode.computed &&
|
|
156
|
-
calleeNode.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
157
|
-
calleeNode.property.name === 'useCallback');
|
|
158
|
-
if (isUseCallback && node.arguments.length >= 1) {
|
|
207
|
+
const wrapper = wrapperNameOf(calleeNode);
|
|
208
|
+
if (wrapper && node.arguments.length >= 1) {
|
|
159
209
|
const arg = node.arguments[0];
|
|
160
210
|
const unwrappedArg = unwrapChainExpression(arg);
|
|
161
211
|
// Case 1: useCallback(memoizedFn, ...) or useCallback(ctx.memoized, ...)
|
|
@@ -173,7 +223,10 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
|
|
|
173
223
|
context.report({
|
|
174
224
|
node,
|
|
175
225
|
messageId: 'redundantWrapper',
|
|
176
|
-
data: {
|
|
226
|
+
data: {
|
|
227
|
+
wrapper,
|
|
228
|
+
callbackName: sourceCode.getText(unwrappedArg),
|
|
229
|
+
},
|
|
177
230
|
fix: (fixer) => fixer.replaceText(node, replaceText),
|
|
178
231
|
});
|
|
179
232
|
}
|
|
@@ -182,7 +235,10 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
|
|
|
182
235
|
context.report({
|
|
183
236
|
node,
|
|
184
237
|
messageId: 'redundantWrapper',
|
|
185
|
-
data: {
|
|
238
|
+
data: {
|
|
239
|
+
wrapper,
|
|
240
|
+
callbackName: sourceCode.getText(unwrappedArg),
|
|
241
|
+
},
|
|
186
242
|
});
|
|
187
243
|
}
|
|
188
244
|
}
|
|
@@ -214,7 +270,10 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
|
|
|
214
270
|
context.report({
|
|
215
271
|
node,
|
|
216
272
|
messageId: 'redundantWrapper',
|
|
217
|
-
data: {
|
|
273
|
+
data: {
|
|
274
|
+
wrapper,
|
|
275
|
+
callbackName: sourceCode.getText(callee),
|
|
276
|
+
},
|
|
218
277
|
fix: (fixer) => fixer.replaceText(node, replaceText),
|
|
219
278
|
});
|
|
220
279
|
}
|
|
@@ -223,7 +282,10 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
|
|
|
223
282
|
context.report({
|
|
224
283
|
node,
|
|
225
284
|
messageId: 'redundantWrapper',
|
|
226
|
-
data: {
|
|
285
|
+
data: {
|
|
286
|
+
wrapper,
|
|
287
|
+
callbackName: sourceCode.getText(callee),
|
|
288
|
+
},
|
|
227
289
|
});
|
|
228
290
|
}
|
|
229
291
|
}
|
|
@@ -272,7 +334,10 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
|
|
|
272
334
|
context.report({
|
|
273
335
|
node,
|
|
274
336
|
messageId: 'redundantWrapper',
|
|
275
|
-
data: {
|
|
337
|
+
data: {
|
|
338
|
+
wrapper,
|
|
339
|
+
callbackName: sourceCode.getText(callee),
|
|
340
|
+
},
|
|
276
341
|
fix: (fixer) => fixer.replaceText(node, replaceText),
|
|
277
342
|
});
|
|
278
343
|
}
|
|
@@ -281,7 +346,10 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
|
|
|
281
346
|
context.report({
|
|
282
347
|
node,
|
|
283
348
|
messageId: 'redundantWrapper',
|
|
284
|
-
data: {
|
|
349
|
+
data: {
|
|
350
|
+
wrapper,
|
|
351
|
+
callbackName: sourceCode.getText(callee),
|
|
352
|
+
},
|
|
285
353
|
});
|
|
286
354
|
}
|
|
287
355
|
}
|
|
@@ -91,20 +91,60 @@ function typeAnnotationReferencesFirestoreType(typeAnnotation, firestoreTypeName
|
|
|
91
91
|
const names = collectTypeReferenceNames(typeAnnotation);
|
|
92
92
|
return names.some((n) => firestoreTypeNames.has(n));
|
|
93
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* True for the assertion wrappers that leave the underlying expression intact:
|
|
96
|
+
* `x as T` and `x satisfies T`.
|
|
97
|
+
*/
|
|
98
|
+
function isCastExpression(node) {
|
|
99
|
+
return (node.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
100
|
+
node.type ===
|
|
101
|
+
utils_1.AST_NODE_TYPES
|
|
102
|
+
.TSSatisfiesExpression);
|
|
103
|
+
}
|
|
94
104
|
/**
|
|
95
105
|
* Returns the inner expression, unwrapping TSAsExpression / TSSatisfiesExpression
|
|
96
106
|
* chains, so we can inspect what lies under a cast.
|
|
97
107
|
*/
|
|
98
108
|
function unwrapCast(node) {
|
|
99
109
|
let current = node;
|
|
100
|
-
while (current
|
|
101
|
-
current.type ===
|
|
102
|
-
utils_1.AST_NODE_TYPES
|
|
103
|
-
.TSSatisfiesExpression) {
|
|
110
|
+
while (isCastExpression(current)) {
|
|
104
111
|
current = current.expression;
|
|
105
112
|
}
|
|
106
113
|
return current;
|
|
107
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* True when some `as` cast in the wrapper chain targets a Firestore type. Those
|
|
117
|
+
* casts are the TSAsExpression visitor's own entry point, so the annotation- and
|
|
118
|
+
* return-type-driven visitors must stand down on them or the same `new Date()`
|
|
119
|
+
* is reported twice.
|
|
120
|
+
*/
|
|
121
|
+
function castChainTargetsFirestoreType(node, firestoreTypeNames) {
|
|
122
|
+
let current = node;
|
|
123
|
+
while (isCastExpression(current)) {
|
|
124
|
+
const cast = current;
|
|
125
|
+
if (cast.type === utils_1.AST_NODE_TYPES.TSAsExpression &&
|
|
126
|
+
typeAnnotationReferencesFirestoreType(cast.typeAnnotation, firestoreTypeNames)) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
current = cast.expression;
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Resolves an expression in a Firestore-typed position to the object literal it
|
|
135
|
+
* ultimately denotes, seeing through `as const` and other assertion wrappers.
|
|
136
|
+
* A wrapper is a syntactic no-op — `{ createdAt: new Date() } as const` still
|
|
137
|
+
* stamps the document with the client clock — so it must not hide the object.
|
|
138
|
+
* Returns null when the chain is already owned by the TSAsExpression visitor.
|
|
139
|
+
*/
|
|
140
|
+
function resolveObjectLiteral(node, firestoreTypeNames) {
|
|
141
|
+
if (!node)
|
|
142
|
+
return null;
|
|
143
|
+
if (castChainTargetsFirestoreType(node, firestoreTypeNames))
|
|
144
|
+
return null;
|
|
145
|
+
const inner = unwrapCast(node);
|
|
146
|
+
return inner.type === utils_1.AST_NODE_TYPES.ObjectExpression ? inner : null;
|
|
147
|
+
}
|
|
108
148
|
/**
|
|
109
149
|
* Checks whether an expression is `new Date(...)` (possibly wrapped in casts).
|
|
110
150
|
*/
|
|
@@ -343,19 +383,22 @@ exports.requireServerTimestampForFirestoreDates = (0, createRule_1.createRule)({
|
|
|
343
383
|
VariableDeclarator(node) {
|
|
344
384
|
if (firestoreTypeNames.size === 0)
|
|
345
385
|
return;
|
|
346
|
-
// Pattern: const x: FirestoreType = { ... }
|
|
386
|
+
// Pattern: const x: FirestoreType = { ... }, including when the literal
|
|
387
|
+
// is wrapped in `as const` or another assertion.
|
|
347
388
|
const typeAnnotation = node.id.typeAnnotation?.typeAnnotation;
|
|
348
|
-
if (typeAnnotation
|
|
349
|
-
typeAnnotationReferencesFirestoreType(typeAnnotation, firestoreTypeNames)
|
|
350
|
-
|
|
351
|
-
node.init.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
352
|
-
// Exempt local render seeds handed to React state, never written to
|
|
353
|
-
// Firestore — being typed as a Firestore doc is not a write.
|
|
354
|
-
if (context.getDeclaredVariables(node).some(isLocalRenderSeedVariable)) {
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
reportNewDatesInObject(node.init, context);
|
|
389
|
+
if (!typeAnnotation ||
|
|
390
|
+
!typeAnnotationReferencesFirestoreType(typeAnnotation, firestoreTypeNames)) {
|
|
391
|
+
return;
|
|
358
392
|
}
|
|
393
|
+
const object = resolveObjectLiteral(node.init, firestoreTypeNames);
|
|
394
|
+
if (!object)
|
|
395
|
+
return;
|
|
396
|
+
// Exempt local render seeds handed to React state, never written to
|
|
397
|
+
// Firestore — being typed as a Firestore doc is not a write.
|
|
398
|
+
if (context.getDeclaredVariables(node).some(isLocalRenderSeedVariable)) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
reportNewDatesInObject(object, context);
|
|
359
402
|
},
|
|
360
403
|
// Pattern: { ... } as FirestoreType or { ... } satisfies FirestoreType
|
|
361
404
|
TSAsExpression(node) {
|
|
@@ -384,21 +427,24 @@ exports.requireServerTimestampForFirestoreDates = (0, createRule_1.createRule)({
|
|
|
384
427
|
ArrowFunctionExpression(node) {
|
|
385
428
|
if (firestoreTypeNames.size === 0)
|
|
386
429
|
return;
|
|
387
|
-
if (node.body.type
|
|
430
|
+
if (node.body.type === utils_1.AST_NODE_TYPES.BlockStatement)
|
|
388
431
|
return;
|
|
389
432
|
const returnType = node.returnType?.typeAnnotation;
|
|
390
|
-
if (returnType
|
|
391
|
-
typeAnnotationReferencesFirestoreType(returnType, firestoreTypeNames)) {
|
|
392
|
-
|
|
433
|
+
if (!returnType ||
|
|
434
|
+
!typeAnnotationReferencesFirestoreType(returnType, firestoreTypeNames)) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const object = resolveObjectLiteral(node.body, firestoreTypeNames);
|
|
438
|
+
if (object) {
|
|
439
|
+
reportNewDatesInObject(object, context);
|
|
393
440
|
}
|
|
394
441
|
},
|
|
395
442
|
// Return statements in functions with explicit Firestore return type annotation
|
|
396
443
|
ReturnStatement(node) {
|
|
397
444
|
if (firestoreTypeNames.size === 0)
|
|
398
445
|
return;
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
if (node.argument.type !== utils_1.AST_NODE_TYPES.ObjectExpression)
|
|
446
|
+
const object = resolveObjectLiteral(node.argument, firestoreTypeNames);
|
|
447
|
+
if (!object)
|
|
402
448
|
return;
|
|
403
449
|
// Walk up to find the enclosing function and check its return type
|
|
404
450
|
let ancestor = node.parent;
|
|
@@ -409,7 +455,7 @@ exports.requireServerTimestampForFirestoreDates = (0, createRule_1.createRule)({
|
|
|
409
455
|
const returnType = ancestor.returnType?.typeAnnotation;
|
|
410
456
|
if (returnType &&
|
|
411
457
|
typeAnnotationReferencesFirestoreType(returnType, firestoreTypeNames)) {
|
|
412
|
-
reportNewDatesInObject(
|
|
458
|
+
reportNewDatesInObject(object, context);
|
|
413
459
|
}
|
|
414
460
|
break;
|
|
415
461
|
}
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,34 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.109",
|
|
4
|
+
"date": "2026-08-05T07:42:30.150Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "firestore-transaction-reads-before-writes",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1728
|
|
11
|
+
],
|
|
12
|
+
"summary": "resolve a call-wrapped computed key (closes #1728)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "no-redundant-usecallback-wrapper",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
1726
|
|
19
|
+
],
|
|
20
|
+
"summary": "see the useLatestCallback spelling (closes #1726)"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "require-server-timestamp-for-firestore-dates",
|
|
24
|
+
"changeType": "fix",
|
|
25
|
+
"issues": [
|
|
26
|
+
1727
|
|
27
|
+
],
|
|
28
|
+
"summary": "look through cast wrappers (closes #1727)"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
},
|
|
2
32
|
{
|
|
3
33
|
"version": "1.20.108",
|
|
4
34
|
"date": "2026-08-05T05:55:32.346Z",
|