@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.
- package/lib/index.js +1 -1
- package/lib/rules/consistent-callback-naming.js +13 -5
- package/lib/rules/enforce-dynamic-imports.js +3 -1
- package/lib/rules/enforce-early-destructuring.js +13 -3
- package/lib/rules/enforce-microdiff.js +169 -72
- package/lib/rules/enforce-object-literal-as-const.js +185 -4
- package/lib/rules/enforce-timestamp-now.d.ts +2 -1
- package/lib/rules/enforce-timestamp-now.js +226 -71
- package/lib/rules/fast-deep-equal-over-microdiff.js +83 -38
- package/lib/rules/no-class-instance-destructuring.d.ts +2 -1
- package/lib/rules/no-class-instance-destructuring.js +240 -32
- package/lib/utils/fastDeepEqualModules.d.ts +30 -0
- package/lib/utils/fastDeepEqualModules.js +40 -0
- package/lib/utils/microdiffModules.d.ts +15 -0
- package/lib/utils/microdiffModules.js +22 -0
- package/package.json +1 -1
- package/release-manifest.json +84 -0
|
@@ -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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
}
|