@blumintinc/eslint-plugin-blumint 1.20.49 → 1.20.51

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.
@@ -3,6 +3,143 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.enforceObjectLiteralAsConst = void 0;
4
4
  const createRule_1 = require("../utils/createRule");
5
5
  const ASTHelpers_1 = require("../utils/ASTHelpers");
6
+ const FUNCTION_TYPES = new Set([
7
+ 'FunctionDeclaration',
8
+ 'FunctionExpression',
9
+ 'ArrowFunctionExpression',
10
+ ]);
11
+ /**
12
+ * A `return` inside a generator yields the generator type's *second* type
13
+ * argument; the first types the `yield`s. `IterableIterator` and friends leave
14
+ * `TReturn` unparameterised, so they carry no constraint on the returned value.
15
+ */
16
+ const GENERATOR_TYPE_NAMES = new Set(['Generator', 'AsyncGenerator']);
17
+ const PROMISE_TYPE_NAMES = new Set(['Promise', 'PromiseLike']);
18
+ /**
19
+ * A readonly tuple is assignable to none of these, so a union member spelled
20
+ * this way cannot rescue an `as const` the rest of the union rejects.
21
+ */
22
+ const NON_ARRAY_KEYWORDS = new Set([
23
+ 'TSBigIntKeyword',
24
+ 'TSBooleanKeyword',
25
+ 'TSLiteralType',
26
+ 'TSNeverKeyword',
27
+ 'TSNullKeyword',
28
+ 'TSNumberKeyword',
29
+ 'TSStringKeyword',
30
+ 'TSSymbolKeyword',
31
+ 'TSUndefinedKeyword',
32
+ 'TSVoidKeyword',
33
+ ]);
34
+ /**
35
+ * Type arguments are `typeParameters` on this parser version and
36
+ * `typeArguments` on newer ones; both spell the same `<T>` after the name.
37
+ */
38
+ function typeArgumentsOf(node) {
39
+ const withTypeArgs = node;
40
+ return ((withTypeArgs.typeArguments ?? withTypeArgs.typeParameters)?.params ?? []);
41
+ }
42
+ function typeReferenceNameOf(node) {
43
+ if (node.type !== 'TSTypeReference' || node.typeName.type !== 'Identifier') {
44
+ return undefined;
45
+ }
46
+ return node.typeName.name;
47
+ }
48
+ /**
49
+ * Whether a readonly tuple — what `as const` makes of an array literal — can be
50
+ * assigned to this annotation, judged from syntax alone.
51
+ *
52
+ * Only shapes the annotation states outright are treated as hostile. Anything
53
+ * the rule cannot resolve (a type reference, a type parameter, an object type)
54
+ * counts as accepting, because declining on no evidence would silence the rule
55
+ * across most annotated code.
56
+ */
57
+ function acceptsReadonlyArray(typeNode) {
58
+ switch (typeNode.type) {
59
+ // `string[]` and `[string, number]` are mutable: TS4104 rejects a readonly
60
+ // tuple assigned to either.
61
+ case 'TSArrayType':
62
+ case 'TSTupleType':
63
+ return false;
64
+ // `readonly string[]` / `readonly [string, number]`.
65
+ case 'TSTypeOperator':
66
+ return typeNode.operator === 'readonly';
67
+ case 'TSTypeReference':
68
+ return typeReferenceNameOf(typeNode) !== 'Array';
69
+ // Assignable to the union as a whole iff assignable to some member.
70
+ case 'TSUnionType':
71
+ return typeNode.types.some(acceptsReadonlyArray);
72
+ case 'TSIntersectionType':
73
+ return typeNode.types.every(acceptsReadonlyArray);
74
+ default:
75
+ return !NON_ARRAY_KEYWORDS.has(typeNode.type);
76
+ }
77
+ }
78
+ /**
79
+ * The type a function type annotation declares for its return value, or
80
+ * `undefined` when the annotation is not a function type (a type reference to
81
+ * an aliased signature, say) and so states nothing resolvable here.
82
+ */
83
+ function returnTypeOfFunctionType(typeNode) {
84
+ if (typeNode?.type !== 'TSFunctionType') {
85
+ return undefined;
86
+ }
87
+ return typeNode.returnType?.typeAnnotation;
88
+ }
89
+ /**
90
+ * The declared return type visible for `fn`, whether written on the function
91
+ * itself (`function f(): string[]`) or on the site that declares it — a typed
92
+ * variable, a typed class property, or an assertion on the function expression.
93
+ *
94
+ * A callback passed as a call argument is deliberately not resolved: its
95
+ * contextual type lives on the callee's declaration, which is usually in
96
+ * another file, and the in-file shapes that do reach here (`useMemo`, `.map`)
97
+ * annotate their callbacks generically rather than with a mutable array.
98
+ */
99
+ function declaredReturnTypeOf(fn) {
100
+ if (fn.returnType) {
101
+ return fn.returnType.typeAnnotation;
102
+ }
103
+ const { parent } = fn;
104
+ if (!parent) {
105
+ return undefined;
106
+ }
107
+ if (parent.type === 'VariableDeclarator') {
108
+ return parent.id.type === 'Identifier'
109
+ ? returnTypeOfFunctionType(parent.id.typeAnnotation?.typeAnnotation)
110
+ : undefined;
111
+ }
112
+ if (parent.type === 'PropertyDefinition') {
113
+ return returnTypeOfFunctionType(parent.typeAnnotation?.typeAnnotation);
114
+ }
115
+ if (parent.type === 'TSAsExpression') {
116
+ return returnTypeOfFunctionType(parent.typeAnnotation);
117
+ }
118
+ return undefined;
119
+ }
120
+ /**
121
+ * The type the *returned expression* must satisfy. For an async function or a
122
+ * generator the declared return type wraps that expression's type, so the
123
+ * wrapper is peeled off before the annotation is judged.
124
+ */
125
+ function returnedValueTypeOf(fn) {
126
+ const declared = declaredReturnTypeOf(fn);
127
+ if (!declared) {
128
+ return undefined;
129
+ }
130
+ const referenceName = typeReferenceNameOf(declared);
131
+ if (fn.generator) {
132
+ return referenceName && GENERATOR_TYPE_NAMES.has(referenceName)
133
+ ? typeArgumentsOf(declared)[1]
134
+ : undefined;
135
+ }
136
+ if (fn.async) {
137
+ return referenceName && PROMISE_TYPE_NAMES.has(referenceName)
138
+ ? typeArgumentsOf(declared)[0]
139
+ : undefined;
140
+ }
141
+ return declared;
142
+ }
6
143
  exports.enforceObjectLiteralAsConst = (0, createRule_1.createRule)({
7
144
  name: 'enforce-object-literal-as-const',
8
145
  meta: {
@@ -55,6 +192,44 @@ exports.enforceObjectLiteralAsConst = (0, createRule_1.createRule)({
55
192
  function isArrayLiteral(node) {
56
193
  return node.type === 'ArrayExpression';
57
194
  }
195
+ /**
196
+ * The function the `return` belongs to — the nearest one, so a `return`
197
+ * inside a nested callback is judged against that callback's annotation
198
+ * rather than the outer function's.
199
+ */
200
+ function enclosingFunctionOf(ancestors) {
201
+ for (let i = ancestors.length - 1; i >= 0; i--) {
202
+ const ancestor = ancestors[i];
203
+ if (FUNCTION_TYPES.has(ancestor.type)) {
204
+ return ancestor;
205
+ }
206
+ }
207
+ return undefined;
208
+ }
209
+ /**
210
+ * `as const` turns an array literal into a readonly *tuple*, which TS4104
211
+ * refuses to assign to a mutable array or tuple. Where the annotation says
212
+ * the value must be mutable, appending `as const` breaks the build, and no
213
+ * edit at the literal can satisfy the rule — honouring it would mean
214
+ * rewriting the signature, a call the author has to make. So the rule stays
215
+ * silent rather than reporting something the developer cannot act on
216
+ * (#1526).
217
+ *
218
+ * Object literals are unaffected: `readonly` property modifiers do not
219
+ * enter assignability, so `{ a: 1 } as const` still satisfies a mutable
220
+ * `{ a: number }`.
221
+ */
222
+ function conflictsWithDeclaredType(literal, ancestors) {
223
+ if (!isArrayLiteral(literal)) {
224
+ return false;
225
+ }
226
+ const enclosingFunction = enclosingFunctionOf(ancestors);
227
+ if (!enclosingFunction) {
228
+ return false;
229
+ }
230
+ const returnedValueType = returnedValueTypeOf(enclosingFunction);
231
+ return !!returnedValueType && !acceptsReadonlyArray(returnedValueType);
232
+ }
58
233
  return {
59
234
  ReturnStatement(node) {
60
235
  // Skip if there's no argument in the return statement
@@ -102,12 +277,18 @@ exports.enforceObjectLiteralAsConst = (0, createRule_1.createRule)({
102
277
  argument.elements.some((elem) => elem !== null && elem.type === 'SpreadElement'))) {
103
278
  return;
104
279
  }
280
+ const literal = argument.type === 'TSAsExpression'
281
+ ? argument.expression
282
+ : argument;
105
283
  // Skip arrays returned from React hooks (memoized data/prop lists that
106
284
  // must not be frozen into readonly tuples — see #511 and #1324)
107
- if (isInsideReactHook(ancestors) &&
108
- isArrayLiteral(argument.type === 'TSAsExpression'
109
- ? argument.expression
110
- : argument)) {
285
+ if (isInsideReactHook(ancestors) && isArrayLiteral(literal)) {
286
+ return;
287
+ }
288
+ // Skip arrays the enclosing signature declares mutable: `as const`
289
+ // cannot compile there and the developer cannot act on the report
290
+ // (#1526)
291
+ if (conflictsWithDeclaredType(literal, ancestors)) {
111
292
  return;
112
293
  }
113
294
  // Report the issue and provide a fix
@@ -1 +1,2 @@
1
- export declare const enforceTimestampNow: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"preferTimestampNow", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
1
+ import { TSESLint } from '@typescript-eslint/utils';
2
+ export declare const enforceTimestampNow: TSESLint.RuleModule<"preferTimestampNow", [], TSESLint.RuleListener>;
@@ -3,6 +3,31 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.enforceTimestampNow = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
+ const FIRESTORE_MODULES = new Set([
7
+ 'firebase-admin/firestore',
8
+ 'firebase/firestore',
9
+ ]);
10
+ /**
11
+ * Members whose meaning survives the `new Date()` → `Timestamp.now()` rewrite,
12
+ * taken from the shipped typings rather than from `Date`'s surface:
13
+ * `@google-cloud/firestore` (what `firebase-admin/firestore` re-exports)
14
+ * declares `seconds`, `nanoseconds`, `toDate()`, `toMillis()`, `isEqual()` and
15
+ * `valueOf()`, and `@firebase/firestore` adds `toString()` and `toJSON()`.
16
+ *
17
+ * `valueOf`, `toString` and `toJSON` are excluded on purpose even though
18
+ * `Timestamp` declares them, because their contracts differ from `Date`'s:
19
+ * `Timestamp#valueOf()` returns an encoded `string` where `Date#valueOf()`
20
+ * returns a `number`, and the string forms render `Timestamp(seconds=…,
21
+ * nanoseconds=…)` instead of a date. Those call sites keep compiling while the
22
+ * value silently changes, which is harder to catch than a type error.
23
+ */
24
+ const TIMESTAMP_COMPATIBLE_MEMBERS = new Set([
25
+ 'toDate',
26
+ 'toMillis',
27
+ 'isEqual',
28
+ 'seconds',
29
+ 'nanoseconds',
30
+ ]);
6
31
  exports.enforceTimestampNow = (0, createRule_1.createRule)({
7
32
  name: 'enforce-timestamp-now',
8
33
  meta: {
@@ -35,8 +60,121 @@ exports.enforceTimestampNow = (0, createRule_1.createRule)({
35
60
  if (filename.includes('.test.') || filename.includes('.spec.')) {
36
61
  return {};
37
62
  }
38
- // Track Timestamp imports and aliases
63
+ // Names that may denote the Firestore `Timestamp` class when matching
64
+ // `X.fromDate(new Date())` / `X.fromMillis(Date.now())`. Seeded with the
65
+ // default name so detection still works when the class reaches the file
66
+ // through a re-export or a `require()` the rule cannot see. Those reports
67
+ // rewrite an expression whose object identifier is already written in the
68
+ // source, so a seeded name can never produce an unbound reference.
39
69
  const timestampAliases = new Set(['Timestamp']);
70
+ // Names actually bound by an observed `Timestamp` import. Tracked apart
71
+ // from `timestampAliases` because the `new Date()` fix synthesizes an
72
+ // identifier the original code never mentions: with no real import the
73
+ // rewrite emits an unbound `Timestamp` and turns compiling code into
74
+ // TS2304 (issue #1521).
75
+ const importedTimestampAliases = [];
76
+ function recordTimestampAlias(localName) {
77
+ timestampAliases.add(localName);
78
+ if (!importedTimestampAliases.includes(localName)) {
79
+ importedTimestampAliases.push(localName);
80
+ }
81
+ }
82
+ /**
83
+ * Local names a static Firestore import binds to `Timestamp` as a *value*.
84
+ *
85
+ * A type-only binding is erased at emit, so referencing it from the
86
+ * synthesized `Timestamp.now()` turns compiling code into TS1361 (issue
87
+ * #1530). This is the same shape as #1521: the gate has to prove not just
88
+ * that a binding exists but that it is the kind of binding the emitted code
89
+ * requires. Both spellings are type-only and neither implies the other —
90
+ * `import type { Timestamp }` marks the declaration while leaving its
91
+ * specifier `value`, and `import { type Timestamp }` marks the specifier
92
+ * while leaving the declaration `value`.
93
+ */
94
+ function staticTimestampAliases(node) {
95
+ if (typeof node.source.value !== 'string' ||
96
+ !FIRESTORE_MODULES.has(node.source.value) ||
97
+ node.importKind === 'type') {
98
+ return [];
99
+ }
100
+ return node.specifiers
101
+ .filter((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
102
+ specifier.importKind !== 'type' &&
103
+ specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
104
+ specifier.imported.name === 'Timestamp')
105
+ .map((specifier) => specifier.local.name);
106
+ }
107
+ /**
108
+ * Local names a `const { Timestamp } = await import(...)` declarator binds
109
+ * to `Timestamp`.
110
+ *
111
+ * No type-only screen is needed here the way the static path needs one
112
+ * (#1530): a destructured `await import(...)` is a runtime value binding by
113
+ * construction. TypeScript spells a type-only dynamic import as the
114
+ * `import('...').Timestamp` type node, which never appears as a variable
115
+ * initializer, and `ImportExpression` carries no `importKind` to check.
116
+ */
117
+ function dynamicTimestampAliases(node) {
118
+ if (node.init?.type !== utils_1.AST_NODE_TYPES.AwaitExpression ||
119
+ node.init.argument.type !== utils_1.AST_NODE_TYPES.ImportExpression ||
120
+ node.id.type !== utils_1.AST_NODE_TYPES.ObjectPattern) {
121
+ return [];
122
+ }
123
+ const source = node.init.argument.source;
124
+ if (source.type !== utils_1.AST_NODE_TYPES.Literal ||
125
+ typeof source.value !== 'string' ||
126
+ !FIRESTORE_MODULES.has(source.value)) {
127
+ return [];
128
+ }
129
+ const aliases = [];
130
+ node.id.properties.forEach((prop) => {
131
+ if (prop.type === utils_1.AST_NODE_TYPES.Property &&
132
+ prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
133
+ prop.key.name === 'Timestamp' &&
134
+ prop.value.type === utils_1.AST_NODE_TYPES.Identifier) {
135
+ aliases.push(prop.value.name);
136
+ }
137
+ });
138
+ return aliases;
139
+ }
140
+ /** Whether a resolved binding is the Firestore `Timestamp` class itself. */
141
+ function isTimestampImportBinding(variable) {
142
+ return variable.defs.some((def) => {
143
+ if (def.node.type === utils_1.AST_NODE_TYPES.ImportSpecifier) {
144
+ const declaration = def.node.parent;
145
+ return (declaration?.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
146
+ staticTimestampAliases(declaration).includes(variable.name));
147
+ }
148
+ if (def.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
149
+ return dynamicTimestampAliases(def.node).includes(variable.name);
150
+ }
151
+ return false;
152
+ });
153
+ }
154
+ /** The binding a name resolves to at the node currently being visited. */
155
+ function resolveBinding(name) {
156
+ let scope = context.getScope();
157
+ while (scope) {
158
+ const binding = scope.variables.find((variable) => variable.name === name);
159
+ if (binding) {
160
+ return binding;
161
+ }
162
+ scope = scope.upper;
163
+ }
164
+ return undefined;
165
+ }
166
+ // A synthesized `Timestamp.now()` is only safe when the alias resolves to
167
+ // the import at the rewrite site. Resolution has to run per name from the
168
+ // innermost scope outward: an alias bound by a dynamic import inside another
169
+ // function is unreachable here, and an inner binding of the same name would
170
+ // capture the emitted reference and silently swap in a different value
171
+ // (issues #1455/#1456).
172
+ function findTimestampAliasInScope() {
173
+ return importedTimestampAliases.find((alias) => {
174
+ const binding = resolveBinding(alias);
175
+ return !!binding && isTimestampImportBinding(binding);
176
+ });
177
+ }
40
178
  function isTimestampFromDateWithNewDate(node) {
41
179
  // Check if it's a Timestamp.fromDate(new Date()) call
42
180
  if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
@@ -95,64 +233,77 @@ exports.enforceTimestampNow = (0, createRule_1.createRule)({
95
233
  node.callee.name === 'Date' &&
96
234
  node.arguments.length === 0);
97
235
  }
98
- // Check if a Date object is being modified (e.g., futureDate.setDate())
99
- function isDateBeingModified(dateVar) {
100
- // Look through the scope to find if this variable is modified
101
- const scope = context.getScope();
102
- const variable = scope.variables.find((v) => v.name === dateVar);
103
- if (!variable)
236
+ /**
237
+ * Whether a single use of the rewritten binding would still type-check and
238
+ * mean the same thing once its initializer is a `Timestamp`.
239
+ *
240
+ * Only a read of a member that `Timestamp` shares with `Date` qualifies.
241
+ * Every other shape — a write, an argument, a return, a comparison, an
242
+ * interpolation, a computed access — hands the value to a position whose
243
+ * expected type this rule cannot see, so it cannot be shown safe.
244
+ */
245
+ function isTimestampCompatibleReference(reference) {
246
+ // The declaration's own initializer write is the site being rewritten,
247
+ // not a use of the resulting value.
248
+ if (reference.init) {
249
+ return true;
250
+ }
251
+ // A later assignment rebinds the variable to a value typed elsewhere,
252
+ // which the rewritten initializer no longer matches.
253
+ if (!reference.isReadOnly()) {
104
254
  return false;
105
- // Check if any references to this variable are followed by property access and modification
106
- return variable.references.some((ref) => {
107
- const id = ref.identifier;
108
- const parent = id.parent;
109
- // Check for patterns like dateVar.setDate(), dateVar.setHours(), etc.
110
- return (parent &&
111
- parent.type === utils_1.AST_NODE_TYPES.MemberExpression &&
112
- parent.object === id &&
113
- parent.property.type === utils_1.AST_NODE_TYPES.Identifier &&
114
- (parent.property.name.startsWith('set') ||
115
- parent.property.name === 'toISOString' ||
116
- parent.property.name === 'toLocaleString' ||
117
- parent.property.name === 'toString'));
118
- });
255
+ }
256
+ const identifier = reference.identifier;
257
+ const parent = identifier.parent;
258
+ if (!parent ||
259
+ parent.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
260
+ parent.object !== identifier ||
261
+ parent.computed ||
262
+ parent.property.type !== utils_1.AST_NODE_TYPES.Identifier) {
263
+ return false;
264
+ }
265
+ return TIMESTAMP_COMPATIBLE_MEMBERS.has(parent.property.name);
266
+ }
267
+ /**
268
+ * Whether every use of a `new Date()` binding survives the rewrite to
269
+ * `Timestamp.now()`.
270
+ *
271
+ * `Timestamp` shares almost none of `Date`'s surface, so rewriting the
272
+ * initializer turns each `getX`/`setX`/`toLocaleX` call on the binding into
273
+ * TS2339 (issue #1528). A denylist of the `Date` members `Timestamp` lacks
274
+ * can only ever be incomplete, so the question is inverted: the fix is
275
+ * offered only when the whole use set is provably part of the `Timestamp`
276
+ * API. Declining on an unrecognized use trades a missed rewrite for never
277
+ * breaking the build, which is the trade this repo prefers.
278
+ */
279
+ function usesOnlyTimestampCompatibleMembers(declarator) {
280
+ // An exported binding is read by files this rule never sees, so its use
281
+ // set cannot be enumerated and the rewrite would break importers instead.
282
+ if (declarator.parent?.parent?.type ===
283
+ utils_1.AST_NODE_TYPES.ExportNamedDeclaration) {
284
+ return false;
285
+ }
286
+ const [variable] = context.getDeclaredVariables(declarator);
287
+ if (!variable) {
288
+ return false;
289
+ }
290
+ return variable.references.every(isTimestampCompatibleReference);
119
291
  }
120
292
  return {
121
- ImportDeclaration(node) {
122
- // Track Timestamp imports from Firebase
123
- if (node.source.value === 'firebase-admin/firestore' ||
124
- node.source.value === 'firebase/firestore') {
125
- node.specifiers.forEach((specifier) => {
126
- if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
127
- specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
128
- specifier.imported.name === 'Timestamp') {
129
- timestampAliases.add(specifier.local.name);
130
- }
131
- });
132
- }
293
+ Program(node) {
294
+ // Collect static imports before any usage is visited. An import is
295
+ // hoisted and module-scoped, so it binds `Timestamp` for the whole file
296
+ // regardless of where it sits; visiting imports in traversal order
297
+ // would make the guard depend on the import preceding the usage.
298
+ node.body.forEach((statement) => {
299
+ if (statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration) {
300
+ staticTimestampAliases(statement).forEach(recordTimestampAlias);
301
+ }
302
+ });
133
303
  },
134
304
  VariableDeclarator(node) {
135
305
  // Track dynamic imports of Timestamp
136
- if (node.init?.type === utils_1.AST_NODE_TYPES.AwaitExpression &&
137
- node.init.argument.type === utils_1.AST_NODE_TYPES.ImportExpression) {
138
- const importSource = node.init.argument.source;
139
- if (importSource.type === utils_1.AST_NODE_TYPES.Literal &&
140
- (importSource.value === 'firebase-admin/firestore' ||
141
- importSource.value === 'firebase/firestore')) {
142
- // Handle destructured imports
143
- if (node.id.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
144
- node.id.properties.forEach((prop) => {
145
- if (prop.type === utils_1.AST_NODE_TYPES.Property &&
146
- prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
147
- prop.key.name === 'Timestamp') {
148
- if (prop.value.type === utils_1.AST_NODE_TYPES.Identifier) {
149
- timestampAliases.add(prop.value.name);
150
- }
151
- }
152
- });
153
- }
154
- }
155
- }
306
+ dynamicTimestampAliases(node).forEach(recordTimestampAlias);
156
307
  },
157
308
  CallExpression(node) {
158
309
  if (isTimestampFromDateWithNewDate(node)) {
@@ -211,27 +362,31 @@ exports.enforceTimestampNow = (0, createRule_1.createRule)({
211
362
  varName.includes('date') ||
212
363
  varName.includes('created') ||
213
364
  varName.includes('updated')) {
214
- // Check if the Date object is being modified
215
- if (isDateBeingModified(parent.id.name)) {
216
- // If the Date is being modified, don't flag it
365
+ // Stay silent when any use of the binding relies on the `Date`
366
+ // API, since the rewrite would strip it (issue #1528).
367
+ if (!usesOnlyTimestampCompatibleMembers(parent)) {
217
368
  return;
218
369
  }
219
- // Check if we have a Timestamp import before suggesting
220
- if (timestampAliases.size > 0) {
221
- const timestampName = Array.from(timestampAliases)[0];
222
- const expressionText = sourceCode.getText(node);
223
- context.report({
224
- node,
225
- messageId: 'preferTimestampNow',
226
- data: {
227
- expression: expressionText,
228
- timestampAlias: timestampName,
229
- },
230
- fix(fixer) {
231
- return fixer.replaceText(node, `${timestampName}.now()`);
232
- },
233
- });
370
+ // Stay silent unless a real `Timestamp` binding is in scope. The
371
+ // rewrite names an identifier the source never mentions, and a
372
+ // file with no Firestore import is almost certainly using the
373
+ // `Date` for something other than a Firestore document anyway.
374
+ const timestampName = findTimestampAliasInScope();
375
+ if (!timestampName) {
376
+ return;
234
377
  }
378
+ const expressionText = sourceCode.getText(node);
379
+ context.report({
380
+ node,
381
+ messageId: 'preferTimestampNow',
382
+ data: {
383
+ expression: expressionText,
384
+ timestampAlias: timestampName,
385
+ },
386
+ fix(fixer) {
387
+ return fixer.replaceText(node, `${timestampName}.now()`);
388
+ },
389
+ });
235
390
  }
236
391
  }
237
392
  }