@blumintinc/eslint-plugin-blumint 1.18.3 → 1.18.5
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-props-argument-name.js +84 -0
- package/lib/rules/enforce-singular-type-names.js +70 -3
- package/lib/rules/prefer-sx-prop-over-system-props.js +23 -2
- package/lib/rules/react-memoize-literals.js +39 -0
- package/lib/rules/require-server-timestamp-for-firestore-dates.d.ts +2 -1
- package/lib/rules/require-server-timestamp-for-firestore-dates.js +109 -0
- package/package.json +1 -1
- package/release-manifest.json +52 -0
package/lib/index.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.enforcePropsArgumentName = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
6
7
|
exports.enforcePropsArgumentName = (0, createRule_1.createRule)({
|
|
7
8
|
name: 'enforce-props-argument-name',
|
|
8
9
|
meta: {
|
|
@@ -195,15 +196,89 @@ exports.enforcePropsArgumentName = (0, createRule_1.createRule)({
|
|
|
195
196
|
}
|
|
196
197
|
});
|
|
197
198
|
}
|
|
199
|
+
// Determine whether renaming a constructor parameter property is unsafe to
|
|
200
|
+
// autofix. A parameter property (`private readonly foo: T`) creates BOTH a
|
|
201
|
+
// constructor-local binding and a `this.foo` class field, so a
|
|
202
|
+
// declaration-only rename would leave dangling references: plain `foo`
|
|
203
|
+
// usages inside the constructor (e.g. `super(foo)`) and `this.foo` accesses
|
|
204
|
+
// anywhere in the class. Mirrors #1123 — when a rename cannot be applied
|
|
205
|
+
// everywhere syntactically, emit no fix rather than corrupt the code.
|
|
206
|
+
function parameterPropertyRenameIsUnsafe(classNode, name, declarationId) {
|
|
207
|
+
let unsafe = false;
|
|
208
|
+
const visit = (node) => {
|
|
209
|
+
if (unsafe) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (node.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
213
|
+
node.object.type === utils_1.AST_NODE_TYPES.ThisExpression &&
|
|
214
|
+
node.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
215
|
+
node.property.name === name) {
|
|
216
|
+
unsafe = true;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (node.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
220
|
+
node.name === name &&
|
|
221
|
+
node !== declarationId) {
|
|
222
|
+
unsafe = true;
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
for (const key of Object.keys(node)) {
|
|
226
|
+
if (key === 'parent') {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const value = node[key];
|
|
230
|
+
if (Array.isArray(value)) {
|
|
231
|
+
for (const child of value) {
|
|
232
|
+
if (ASTHelpers_1.ASTHelpers.isNode(child)) {
|
|
233
|
+
visit(child);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
else if (ASTHelpers_1.ASTHelpers.isNode(value)) {
|
|
238
|
+
visit(value);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
visit(classNode);
|
|
243
|
+
return unsafe;
|
|
244
|
+
}
|
|
245
|
+
// Find the class (declaration or expression) that owns a method definition.
|
|
246
|
+
function getEnclosingClass(node) {
|
|
247
|
+
const classBody = node.parent;
|
|
248
|
+
if (classBody && classBody.type === utils_1.AST_NODE_TYPES.ClassBody) {
|
|
249
|
+
const classNode = classBody.parent;
|
|
250
|
+
if (classNode &&
|
|
251
|
+
(classNode.type === utils_1.AST_NODE_TYPES.ClassDeclaration ||
|
|
252
|
+
classNode.type === utils_1.AST_NODE_TYPES.ClassExpression)) {
|
|
253
|
+
return classNode;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
198
258
|
// Check class method parameters (including constructors)
|
|
199
259
|
function checkClassMethod(node) {
|
|
200
260
|
const method = node.value;
|
|
201
261
|
const sourceCode = context.sourceCode;
|
|
202
262
|
const propsParams = getPropsParams(method.params);
|
|
263
|
+
// When the enclosing class extends a base class, a constructor parameter
|
|
264
|
+
// property (e.g. `private readonly fullProps: SubProps`) cannot be safely
|
|
265
|
+
// renamed to `props`: the base class may already declare a private `props`
|
|
266
|
+
// field/parameter-property, so the rename would create a TS2415 private-
|
|
267
|
+
// field collision, and it would also leave `super(fullProps)` / `this.
|
|
268
|
+
// fullProps` references dangling. The rule is purely syntactic and cannot
|
|
269
|
+
// inspect the base class, so it defers to TypeScript correctness here and
|
|
270
|
+
// does not report on parameter properties in subclasses (this repo prefers
|
|
271
|
+
// false negatives over false positives).
|
|
272
|
+
const enclosingClass = getEnclosingClass(node);
|
|
273
|
+
const classExtendsBase = !!enclosingClass?.superClass;
|
|
203
274
|
method.params.forEach((param) => {
|
|
204
275
|
if (isDestructuredParameter(param)) {
|
|
205
276
|
return;
|
|
206
277
|
}
|
|
278
|
+
if (classExtendsBase &&
|
|
279
|
+
param.type === utils_1.AST_NODE_TYPES.TSParameterProperty) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
207
282
|
const id = getIdFromParam(param);
|
|
208
283
|
if (id && id.typeAnnotation && id.typeAnnotation.typeAnnotation) {
|
|
209
284
|
const typeName = getTypeName(id.typeAnnotation.typeAnnotation);
|
|
@@ -221,6 +296,15 @@ exports.enforcePropsArgumentName = (0, createRule_1.createRule)({
|
|
|
221
296
|
suggestedName,
|
|
222
297
|
},
|
|
223
298
|
fix: (fixer) => {
|
|
299
|
+
// A parameter-property rename touches both the parameter and
|
|
300
|
+
// the `this.<name>` field; refuse to autofix when the name is
|
|
301
|
+
// referenced elsewhere, since a declaration-only rename would
|
|
302
|
+
// leave dangling references.
|
|
303
|
+
if (param.type === utils_1.AST_NODE_TYPES.TSParameterProperty &&
|
|
304
|
+
enclosingClass &&
|
|
305
|
+
parameterPropertyRenameIsUnsafe(enclosingClass, id.name, id)) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
224
308
|
const token = sourceCode.getFirstToken(id);
|
|
225
309
|
if (!token)
|
|
226
310
|
return null;
|
|
@@ -25,6 +25,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
26
|
exports.enforceSingularTypeNames = void 0;
|
|
27
27
|
const createRule_1 = require("../utils/createRule");
|
|
28
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
28
29
|
const pluralize = __importStar(require("pluralize"));
|
|
29
30
|
const NON_PLURALIZABLE_SUFFIXES = [
|
|
30
31
|
'Props',
|
|
@@ -33,6 +34,66 @@ const NON_PLURALIZABLE_SUFFIXES = [
|
|
|
33
34
|
'Settings',
|
|
34
35
|
'Data',
|
|
35
36
|
];
|
|
37
|
+
/**
|
|
38
|
+
* Generic references whose instantiation is itself a container: `Array<T>` and
|
|
39
|
+
* `ReadonlyArray<T>`. `Readonly<T>` is handled separately — it is an identity
|
|
40
|
+
* wrapper that preserves T's shape, so we recurse into T rather than treating
|
|
41
|
+
* the reference itself as a container.
|
|
42
|
+
*/
|
|
43
|
+
const ARRAY_GENERIC_NAMES = new Set(['Array', 'ReadonlyArray']);
|
|
44
|
+
/**
|
|
45
|
+
* Returns true when the type alias RHS resolves to a container shape — a
|
|
46
|
+
* `TSArrayType` (`Foo[]`) or `TSTupleType` (`[A, B]`) — for which a plural name
|
|
47
|
+
* is the correct, self-documenting choice. Sees through identity-ish wrappers
|
|
48
|
+
* over the same shape: the `readonly` type operator, parenthesized types, and
|
|
49
|
+
* the `Readonly<T>` utility type; `Array<T>`/`ReadonlyArray<T>` are containers
|
|
50
|
+
* outright.
|
|
51
|
+
*/
|
|
52
|
+
function resolvesToContainerType(node) {
|
|
53
|
+
let current = node;
|
|
54
|
+
// Fixpoint loop: peel identity wrappers until a concrete shape is reached.
|
|
55
|
+
// Wrappers are finite; the cap only guards against a pathological cycle.
|
|
56
|
+
for (let i = 0; i < 10; i++) {
|
|
57
|
+
switch (current.type) {
|
|
58
|
+
case utils_1.AST_NODE_TYPES.TSArrayType:
|
|
59
|
+
case utils_1.AST_NODE_TYPES.TSTupleType:
|
|
60
|
+
return true;
|
|
61
|
+
case utils_1.AST_NODE_TYPES.TSTypeOperator: {
|
|
62
|
+
const operator = current;
|
|
63
|
+
if (operator.operator !== 'readonly' || !operator.typeAnnotation) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
current = operator.typeAnnotation;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
case utils_1.AST_NODE_TYPES.TSTypeReference: {
|
|
70
|
+
const ref = current;
|
|
71
|
+
if (ref.typeName.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
72
|
+
return false;
|
|
73
|
+
if (ARRAY_GENERIC_NAMES.has(ref.typeName.name))
|
|
74
|
+
return true;
|
|
75
|
+
if (ref.typeName.name === 'Readonly' &&
|
|
76
|
+
ref.typeParameters &&
|
|
77
|
+
ref.typeParameters.params.length > 0) {
|
|
78
|
+
current = ref.typeParameters.params[0];
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
default: {
|
|
84
|
+
// Parentheses (`(Foo[])`) — the parser may emit TSParenthesizedType.
|
|
85
|
+
// Matched by string since the node type is not always in the enum.
|
|
86
|
+
if (current.type === 'TSParenthesizedType' &&
|
|
87
|
+
'typeAnnotation' in current) {
|
|
88
|
+
current = current.typeAnnotation;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
36
97
|
exports.enforceSingularTypeNames = (0, createRule_1.createRule)({
|
|
37
98
|
create(context) {
|
|
38
99
|
/**
|
|
@@ -82,9 +143,15 @@ exports.enforceSingularTypeNames = (0, createRule_1.createRule)({
|
|
|
82
143
|
// Check type aliases
|
|
83
144
|
TSTypeAliasDeclaration(node) {
|
|
84
145
|
const name = node.id.name;
|
|
85
|
-
if (isPlural(name))
|
|
86
|
-
|
|
87
|
-
|
|
146
|
+
if (!isPlural(name))
|
|
147
|
+
return;
|
|
148
|
+
// A plural name correctly models a container type (array/tuple),
|
|
149
|
+
// including through identity-ish wrappers (Readonly<>, readonly, parens),
|
|
150
|
+
// so exempt it — matching the rule message's "leaves plural names for
|
|
151
|
+
// container types" promise.
|
|
152
|
+
if (resolvesToContainerType(node.typeAnnotation))
|
|
153
|
+
return;
|
|
154
|
+
reportPluralName(node.id, name, getSingularForm(name));
|
|
88
155
|
},
|
|
89
156
|
// Check interfaces
|
|
90
157
|
TSInterfaceDeclaration(node) {
|
|
@@ -139,6 +139,21 @@ const DEFAULT_MUI_COMPONENTS = new Set([
|
|
|
139
139
|
'AppBar',
|
|
140
140
|
'Toolbar',
|
|
141
141
|
]);
|
|
142
|
+
/**
|
|
143
|
+
* Components whose public prop API defines `color` as a closed semantic enum
|
|
144
|
+
* (a palette / variant selector like `'primary' | 'secondary' | 'error' | …`),
|
|
145
|
+
* not a CSS-forwarded system style prop. On these, `color` feeds
|
|
146
|
+
* `ownerState.color`, selecting theme variants and MUI's internal
|
|
147
|
+
* `.Mui*-color*` class selectors — moving it into `sx` both drops the variant
|
|
148
|
+
* selection and produces an invalid CSS `color` value. So `color` here is a
|
|
149
|
+
* first-class component prop, never a deprecated system prop.
|
|
150
|
+
*/
|
|
151
|
+
const COMPONENT_COLOR_IS_SEMANTIC = new Set([
|
|
152
|
+
'Button',
|
|
153
|
+
'IconButton',
|
|
154
|
+
'Chip',
|
|
155
|
+
'Badge',
|
|
156
|
+
]);
|
|
142
157
|
/**
|
|
143
158
|
* Props that must never be moved to `sx` because they are genuine component
|
|
144
159
|
* API props, not MUI system styling shorthands. `direction` and `spacing` are
|
|
@@ -268,7 +283,13 @@ exports.preferSxPropOverSystemProps = (0, createRule_1.createRule)({
|
|
|
268
283
|
return true;
|
|
269
284
|
return false;
|
|
270
285
|
}
|
|
271
|
-
function isSystemProp(name) {
|
|
286
|
+
function isSystemProp(name, componentName) {
|
|
287
|
+
// `color` is a semantic enum prop (not a CSS system prop) on components
|
|
288
|
+
// like Button/IconButton/Chip/Badge — exempt it there so the autofix
|
|
289
|
+
// never rewrites a variant selector into an invalid CSS color.
|
|
290
|
+
if (name === 'color' && COMPONENT_COLOR_IS_SEMANTIC.has(componentName)) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
272
293
|
return MUI_SYSTEM_PROPS.has(name) && !isAllowedProp(name);
|
|
273
294
|
}
|
|
274
295
|
return {
|
|
@@ -291,7 +312,7 @@ exports.preferSxPropOverSystemProps = (0, createRule_1.createRule)({
|
|
|
291
312
|
if (name === 'sx') {
|
|
292
313
|
sxAttr = attr;
|
|
293
314
|
}
|
|
294
|
-
else if (isSystemProp(name)) {
|
|
315
|
+
else if (isSystemProp(name, componentName)) {
|
|
295
316
|
systemPropAttrs.push(attr);
|
|
296
317
|
}
|
|
297
318
|
}
|
|
@@ -711,6 +711,39 @@ exports.reactMemoizeLiterals = (0, createRule_1.createRule)({
|
|
|
711
711
|
}
|
|
712
712
|
return false;
|
|
713
713
|
}
|
|
714
|
+
/**
|
|
715
|
+
* True when the literal is a variable initializer whose every usage resolves
|
|
716
|
+
* to a style JSX attribute value (sx/style). This extends the inline
|
|
717
|
+
* `isStyleJSXAttributeValue` exemption across a variable: extracting an
|
|
718
|
+
* `sx`/`style` object to a local for readability is common, and the library
|
|
719
|
+
* consumes it by merging/normalizing on each render, not by reference — so
|
|
720
|
+
* memoization adds no stability benefit whether the object is inline or
|
|
721
|
+
* lifted one line above the JSX.
|
|
722
|
+
*
|
|
723
|
+
* The exemption holds only if *every* reference is a style value. If any
|
|
724
|
+
* usage flows through a call, spread, or non-style prop, that consumer can
|
|
725
|
+
* observe the reference, so the literal stays reported — the
|
|
726
|
+
* variable-mediated analogue of the inline `sx={makeSx({ … })}` guard.
|
|
727
|
+
*/
|
|
728
|
+
function isStyleVariableInitializer(node) {
|
|
729
|
+
const parent = node.parent;
|
|
730
|
+
if (!parent ||
|
|
731
|
+
parent.type !== utils_1.AST_NODE_TYPES.VariableDeclarator ||
|
|
732
|
+
parent.init !== node) {
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
const variables = ASTHelpers_1.ASTHelpers.getDeclaredVariables(context, parent);
|
|
736
|
+
if (variables.length === 0) {
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
const usages = variables[0].references.filter((ref) => !ref.init);
|
|
740
|
+
// No usages (dead code): can't prove the literal only feeds a style
|
|
741
|
+
// attribute, so leave it reported (mirrors isVariableAlwaysThrown).
|
|
742
|
+
if (usages.length === 0) {
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
745
|
+
return usages.every((ref) => isStyleJSXAttributeValue(ref.identifier));
|
|
746
|
+
}
|
|
714
747
|
function reportLiteral(node) {
|
|
715
748
|
const descriptor = getLiteralDescriptor(node);
|
|
716
749
|
if (!descriptor)
|
|
@@ -731,6 +764,12 @@ exports.reactMemoizeLiterals = (0, createRule_1.createRule)({
|
|
|
731
764
|
if (isStyleJSXAttributeValue(node)) {
|
|
732
765
|
return;
|
|
733
766
|
}
|
|
767
|
+
// Same rationale, followed across a variable: a literal assigned to a
|
|
768
|
+
// local whose every usage is a style JSX attribute value gains nothing
|
|
769
|
+
// from memoization.
|
|
770
|
+
if (isStyleVariableInitializer(node)) {
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
734
773
|
const hookCall = findEnclosingHookCall(node);
|
|
735
774
|
if (hookCall) {
|
|
736
775
|
if (hookCall.isDirectArgument ||
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
1
2
|
type Options = [
|
|
2
3
|
{
|
|
3
4
|
firestoreTypePaths?: string[];
|
|
@@ -5,5 +6,5 @@ type Options = [
|
|
|
5
6
|
ignoreTestFiles?: boolean;
|
|
6
7
|
}
|
|
7
8
|
];
|
|
8
|
-
export declare const requireServerTimestampForFirestoreDates:
|
|
9
|
+
export declare const requireServerTimestampForFirestoreDates: TSESLint.RuleModule<"useServerTimestamp", Options, TSESLint.RuleListener>;
|
|
9
10
|
export {};
|
|
@@ -168,6 +168,101 @@ context) {
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
|
+
// Bare Firestore write methods invoked on a document reference / DocSetter,
|
|
172
|
+
// e.g. `docRef.set(payload)`, `batch.update(payload)`, `collection.add(payload)`.
|
|
173
|
+
const FIRESTORE_WRITE_METHOD_NAMES = new Set(['set', 'update', 'add']);
|
|
174
|
+
// Standalone Firestore write functions. These are `set`-prefixed but are writes,
|
|
175
|
+
// not React state setters, so they must NOT be mistaken for a render-seed sink.
|
|
176
|
+
const FIRESTORE_WRITE_FN_NAMES = new Set([
|
|
177
|
+
'setDoc',
|
|
178
|
+
'updateDoc',
|
|
179
|
+
'addDoc',
|
|
180
|
+
'setDocument',
|
|
181
|
+
]);
|
|
182
|
+
/**
|
|
183
|
+
* Walks up from an identifier reference through the value positions of object /
|
|
184
|
+
* array literals, spreads and casts to find the CallExpression it is an
|
|
185
|
+
* argument to. Returns that call, or null if the identifier does not flow into
|
|
186
|
+
* a call argument (e.g. it is the callee, or sits in some other position).
|
|
187
|
+
*/
|
|
188
|
+
function findEnclosingCallArgument(idNode) {
|
|
189
|
+
let node = idNode;
|
|
190
|
+
let parent = node.parent;
|
|
191
|
+
while (parent) {
|
|
192
|
+
if (parent.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
193
|
+
return parent.arguments.includes(node)
|
|
194
|
+
? parent
|
|
195
|
+
: null;
|
|
196
|
+
}
|
|
197
|
+
const canAscend = (parent.type === utils_1.AST_NODE_TYPES.Property && parent.value === node) ||
|
|
198
|
+
parent.type === utils_1.AST_NODE_TYPES.ObjectExpression ||
|
|
199
|
+
parent.type === utils_1.AST_NODE_TYPES.ArrayExpression ||
|
|
200
|
+
parent.type === utils_1.AST_NODE_TYPES.SpreadElement ||
|
|
201
|
+
parent.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
202
|
+
parent.type ===
|
|
203
|
+
utils_1.AST_NODE_TYPES
|
|
204
|
+
.TSSatisfiesExpression ||
|
|
205
|
+
parent.type === utils_1.AST_NODE_TYPES.TSNonNullExpression;
|
|
206
|
+
if (!canAscend)
|
|
207
|
+
return null;
|
|
208
|
+
node = parent;
|
|
209
|
+
parent = parent.parent;
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* True when the call is a Firestore write: a `.set()`/`.update()`/`.add()`
|
|
215
|
+
* member call, or a standalone `setDoc`/`updateDoc`/`addDoc`/`setDocument`.
|
|
216
|
+
*/
|
|
217
|
+
function isFirestoreWriteCall(call) {
|
|
218
|
+
const callee = call.callee;
|
|
219
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
220
|
+
!callee.computed &&
|
|
221
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
222
|
+
FIRESTORE_WRITE_METHOD_NAMES.has(callee.property.name)) {
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
return (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
226
|
+
FIRESTORE_WRITE_FN_NAMES.has(callee.name));
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* True when the call hands its argument to a React state sink: a `setXxx`
|
|
230
|
+
* setter (the `useState` convention) or `useState` itself. Firestore write
|
|
231
|
+
* functions are excluded even though some share the `set` prefix.
|
|
232
|
+
*/
|
|
233
|
+
function isStateSetterCall(call) {
|
|
234
|
+
const callee = call.callee;
|
|
235
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
236
|
+
return false;
|
|
237
|
+
if (callee.name === 'useState')
|
|
238
|
+
return true;
|
|
239
|
+
return (/^set[A-Z]/.test(callee.name) && !FIRESTORE_WRITE_FN_NAMES.has(callee.name));
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* A local render seed is a variable that is handed to a React state setter (or
|
|
243
|
+
* useState) and never flows into a Firestore write. Such objects are annotated
|
|
244
|
+
* with a Firestore document type only so React state can be seeded with a
|
|
245
|
+
* document-shaped value; the client clock is correct for an optimistic render,
|
|
246
|
+
* and the authoritative timestamp arrives when the Firestore subscription
|
|
247
|
+
* replaces the seed. Being *typed* as a Firestore document is not evidence that
|
|
248
|
+
* the object is *written* to Firestore.
|
|
249
|
+
*/
|
|
250
|
+
function isLocalRenderSeedVariable(variable) {
|
|
251
|
+
let flowsToStateSetter = false;
|
|
252
|
+
let flowsToWrite = false;
|
|
253
|
+
for (const ref of variable.references) {
|
|
254
|
+
const call = findEnclosingCallArgument(ref.identifier);
|
|
255
|
+
if (!call)
|
|
256
|
+
continue;
|
|
257
|
+
if (isFirestoreWriteCall(call)) {
|
|
258
|
+
flowsToWrite = true;
|
|
259
|
+
}
|
|
260
|
+
else if (isStateSetterCall(call)) {
|
|
261
|
+
flowsToStateSetter = true;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return flowsToStateSetter && !flowsToWrite;
|
|
265
|
+
}
|
|
171
266
|
exports.requireServerTimestampForFirestoreDates = (0, createRule_1.createRule)({
|
|
172
267
|
name: 'require-server-timestamp-for-firestore-dates',
|
|
173
268
|
meta: {
|
|
@@ -254,6 +349,11 @@ exports.requireServerTimestampForFirestoreDates = (0, createRule_1.createRule)({
|
|
|
254
349
|
typeAnnotationReferencesFirestoreType(typeAnnotation, firestoreTypeNames) &&
|
|
255
350
|
node.init &&
|
|
256
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
|
+
}
|
|
257
357
|
reportNewDatesInObject(node.init, context);
|
|
258
358
|
}
|
|
259
359
|
},
|
|
@@ -265,6 +365,15 @@ exports.requireServerTimestampForFirestoreDates = (0, createRule_1.createRule)({
|
|
|
265
365
|
const typeAnnotation = node.typeAnnotation;
|
|
266
366
|
if (!typeAnnotationReferencesFirestoreType(typeAnnotation, firestoreTypeNames))
|
|
267
367
|
return;
|
|
368
|
+
// Exempt `const seed = { ... } as FirestoreType` when `seed` is a local
|
|
369
|
+
// render seed handed to React state and never written to Firestore.
|
|
370
|
+
const parent = node.parent;
|
|
371
|
+
if (parent &&
|
|
372
|
+
parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
373
|
+
parent.init === node &&
|
|
374
|
+
context.getDeclaredVariables(parent).some(isLocalRenderSeedVariable)) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
268
377
|
const inner = unwrapCast(node.expression);
|
|
269
378
|
if (inner.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
270
379
|
reportNewDatesInObject(inner, context);
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,56 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.18.5",
|
|
4
|
+
"date": "2026-07-09T03:26:45.486Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-props-argument-name",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1276
|
|
11
|
+
],
|
|
12
|
+
"summary": "exempt subclass parameter properties to avoid TS2415/TS2304 (closes #1276)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.18.4",
|
|
18
|
+
"date": "2026-07-08T07:14:09.487Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "enforce-singular-type-names",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
1275
|
|
25
|
+
],
|
|
26
|
+
"summary": "exempt container (array/tuple) type aliases (closes #1275)"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "prefer-sx-prop-over-system-props",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
1273
|
|
33
|
+
],
|
|
34
|
+
"summary": "exempt semantic `color` on Button/IconButton/Chip/Badge (closes #1273)"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "react-memoize-literals",
|
|
38
|
+
"changeType": "fix",
|
|
39
|
+
"issues": [
|
|
40
|
+
1274
|
|
41
|
+
],
|
|
42
|
+
"summary": "follow sx/style exemption through variable-mediated values (closes #1274)"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "require-server-timestamp-for-firestore-dates",
|
|
46
|
+
"changeType": "fix",
|
|
47
|
+
"issues": [
|
|
48
|
+
1272
|
|
49
|
+
],
|
|
50
|
+
"summary": "exempt local render seeds passed to React state setters (closes #1272)"
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
2
54
|
{
|
|
3
55
|
"version": "1.18.3",
|
|
4
56
|
"date": "2026-07-06T05:16:06.067Z",
|