@blumintinc/eslint-plugin-blumint 1.20.49 → 1.20.50
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-early-destructuring.js +13 -3
- 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 +129 -48
- package/lib/rules/no-class-instance-destructuring.d.ts +2 -1
- package/lib/rules/no-class-instance-destructuring.js +240 -32
- package/package.json +1 -1
- package/release-manifest.json +46 -0
package/lib/index.js
CHANGED
|
@@ -52,7 +52,7 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
52
52
|
messages: {
|
|
53
53
|
callbackPropPrefix: 'Callback prop "{{propName}}" is a function but lacks the "on" prefix. ' +
|
|
54
54
|
'Consistent "on" prefixes signal event handlers to consumers and distinguish callbacks from data props. ' +
|
|
55
|
-
'Rename to "on{{eventName}}".',
|
|
55
|
+
'Rename to "on{{eventName}}" here, in the props type that declares it, and in every reader of that prop.',
|
|
56
56
|
callbackFunctionPrefix: 'Function "{{functionName}}" uses the "handle" prefix. ' +
|
|
57
57
|
'The "handle" prefix is redundant and less descriptive than action-oriented verb phrases. ' +
|
|
58
58
|
'Rename using a descriptive verb (e.g., click instead of handleClick).',
|
|
@@ -252,6 +252,18 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
252
252
|
!isRenderFunction(node.value.expression) &&
|
|
253
253
|
!isReactComponentType(node.value.expression)) {
|
|
254
254
|
const eventName = propName.charAt(0).toUpperCase() + propName.slice(1);
|
|
255
|
+
// Reported without an autofix (Bug #1522). A JSX attribute name is
|
|
256
|
+
// one end of a props contract: the other end is the declaration
|
|
257
|
+
// that binds the name — a props `type`/`interface`, a
|
|
258
|
+
// `JSX.IntrinsicElements` augmentation for host elements — plus
|
|
259
|
+
// every reader of that member (`props.validate`, destructuring) and
|
|
260
|
+
// every other call site of the component. Rewriting only the
|
|
261
|
+
// attribute yields TS2322, and renaming the local declaration
|
|
262
|
+
// as well merely relocates the break: readers in the same file fail
|
|
263
|
+
// with TS2551 and call sites in other files (which a single-file
|
|
264
|
+
// fixer cannot see, let alone edit atomically) fail with TS2322.
|
|
265
|
+
// No subset of the rename is safe to apply in isolation, so the
|
|
266
|
+
// report carries the full instruction instead of a broken fix.
|
|
255
267
|
context.report({
|
|
256
268
|
node,
|
|
257
269
|
messageId: 'callbackPropPrefix',
|
|
@@ -259,10 +271,6 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
259
271
|
propName,
|
|
260
272
|
eventName,
|
|
261
273
|
},
|
|
262
|
-
fix(fixer) {
|
|
263
|
-
// Convert camelCase to PascalCase for the event name
|
|
264
|
-
return fixer.replaceText(node.name, `on${eventName}`);
|
|
265
|
-
},
|
|
266
274
|
});
|
|
267
275
|
}
|
|
268
276
|
}
|
|
@@ -407,8 +407,9 @@ function renderArrayPatternWithDefaults(pattern, sourceCode) {
|
|
|
407
407
|
return `${leftText} = ${sourceCode.getText(element.right)}`;
|
|
408
408
|
}
|
|
409
409
|
if (element.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
410
|
-
|
|
411
|
-
|
|
410
|
+
// No synthesized `= {}` here either, for the reason spelled out in
|
|
411
|
+
// formatPropertyText: the default is checked against the bindings under it.
|
|
412
|
+
return renderObjectPatternWithDefaults(element, sourceCode);
|
|
412
413
|
}
|
|
413
414
|
if (element.type === utils_1.AST_NODE_TYPES.ArrayPattern) {
|
|
414
415
|
const nested = renderArrayPatternWithDefaults(element, sourceCode);
|
|
@@ -441,10 +442,19 @@ function formatPropertyText(property, sourceCode) {
|
|
|
441
442
|
return renderPropertyWithAssignment(property, value, keyText, sourceCode);
|
|
442
443
|
}
|
|
443
444
|
if (value.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
445
|
+
// A nested object pattern is re-emitted as authored, without a synthesized
|
|
446
|
+
// `= {}`. TypeScript checks such a default against every binding element
|
|
447
|
+
// beneath it, so `{ profile: { name, age } = {} }` reports TS2525 once per
|
|
448
|
+
// name: `{}` supplies no value and the names carry no defaults of their own.
|
|
449
|
+
// The default also only ever guarded a nullish parent (an explicitly `null`
|
|
450
|
+
// one still throws), so dropping it costs a partial runtime guard and buys
|
|
451
|
+
// back the invariant that compiling input yields compiling output.
|
|
444
452
|
const nested = renderObjectPatternWithDefaults(value, sourceCode);
|
|
445
|
-
return `${keyText}: ${nested}
|
|
453
|
+
return `${keyText}: ${nested}`;
|
|
446
454
|
}
|
|
447
455
|
if (value.type === utils_1.AST_NODE_TYPES.ArrayPattern) {
|
|
456
|
+
// An array pattern's `= []` default is safe to synthesize: TypeScript does
|
|
457
|
+
// not push it down onto the element bindings the way it does for objects.
|
|
448
458
|
const nested = renderArrayPatternWithDefaults(value, sourceCode);
|
|
449
459
|
return `${keyText}: ${nested} = []`;
|
|
450
460
|
}
|
|
@@ -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,10 @@ 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
|
+
]);
|
|
6
10
|
exports.enforceTimestampNow = (0, createRule_1.createRule)({
|
|
7
11
|
name: 'enforce-timestamp-now',
|
|
8
12
|
meta: {
|
|
@@ -35,8 +39,102 @@ exports.enforceTimestampNow = (0, createRule_1.createRule)({
|
|
|
35
39
|
if (filename.includes('.test.') || filename.includes('.spec.')) {
|
|
36
40
|
return {};
|
|
37
41
|
}
|
|
38
|
-
//
|
|
42
|
+
// Names that may denote the Firestore `Timestamp` class when matching
|
|
43
|
+
// `X.fromDate(new Date())` / `X.fromMillis(Date.now())`. Seeded with the
|
|
44
|
+
// default name so detection still works when the class reaches the file
|
|
45
|
+
// through a re-export or a `require()` the rule cannot see. Those reports
|
|
46
|
+
// rewrite an expression whose object identifier is already written in the
|
|
47
|
+
// source, so a seeded name can never produce an unbound reference.
|
|
39
48
|
const timestampAliases = new Set(['Timestamp']);
|
|
49
|
+
// Names actually bound by an observed `Timestamp` import. Tracked apart
|
|
50
|
+
// from `timestampAliases` because the `new Date()` fix synthesizes an
|
|
51
|
+
// identifier the original code never mentions: with no real import the
|
|
52
|
+
// rewrite emits an unbound `Timestamp` and turns compiling code into
|
|
53
|
+
// TS2304 (issue #1521).
|
|
54
|
+
const importedTimestampAliases = [];
|
|
55
|
+
function recordTimestampAlias(localName) {
|
|
56
|
+
timestampAliases.add(localName);
|
|
57
|
+
if (!importedTimestampAliases.includes(localName)) {
|
|
58
|
+
importedTimestampAliases.push(localName);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Local names a static Firestore import binds to `Timestamp`. */
|
|
62
|
+
function staticTimestampAliases(node) {
|
|
63
|
+
if (typeof node.source.value !== 'string' ||
|
|
64
|
+
!FIRESTORE_MODULES.has(node.source.value)) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
return node.specifiers
|
|
68
|
+
.filter((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
69
|
+
specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
70
|
+
specifier.imported.name === 'Timestamp')
|
|
71
|
+
.map((specifier) => specifier.local.name);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Local names a `const { Timestamp } = await import(...)` declarator binds
|
|
75
|
+
* to `Timestamp`.
|
|
76
|
+
*/
|
|
77
|
+
function dynamicTimestampAliases(node) {
|
|
78
|
+
if (node.init?.type !== utils_1.AST_NODE_TYPES.AwaitExpression ||
|
|
79
|
+
node.init.argument.type !== utils_1.AST_NODE_TYPES.ImportExpression ||
|
|
80
|
+
node.id.type !== utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
const source = node.init.argument.source;
|
|
84
|
+
if (source.type !== utils_1.AST_NODE_TYPES.Literal ||
|
|
85
|
+
typeof source.value !== 'string' ||
|
|
86
|
+
!FIRESTORE_MODULES.has(source.value)) {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
const aliases = [];
|
|
90
|
+
node.id.properties.forEach((prop) => {
|
|
91
|
+
if (prop.type === utils_1.AST_NODE_TYPES.Property &&
|
|
92
|
+
prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
93
|
+
prop.key.name === 'Timestamp' &&
|
|
94
|
+
prop.value.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
95
|
+
aliases.push(prop.value.name);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return aliases;
|
|
99
|
+
}
|
|
100
|
+
/** Whether a resolved binding is the Firestore `Timestamp` class itself. */
|
|
101
|
+
function isTimestampImportBinding(variable) {
|
|
102
|
+
return variable.defs.some((def) => {
|
|
103
|
+
if (def.node.type === utils_1.AST_NODE_TYPES.ImportSpecifier) {
|
|
104
|
+
const declaration = def.node.parent;
|
|
105
|
+
return (declaration?.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
106
|
+
staticTimestampAliases(declaration).includes(variable.name));
|
|
107
|
+
}
|
|
108
|
+
if (def.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
|
|
109
|
+
return dynamicTimestampAliases(def.node).includes(variable.name);
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/** The binding a name resolves to at the node currently being visited. */
|
|
115
|
+
function resolveBinding(name) {
|
|
116
|
+
let scope = context.getScope();
|
|
117
|
+
while (scope) {
|
|
118
|
+
const binding = scope.variables.find((variable) => variable.name === name);
|
|
119
|
+
if (binding) {
|
|
120
|
+
return binding;
|
|
121
|
+
}
|
|
122
|
+
scope = scope.upper;
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
// A synthesized `Timestamp.now()` is only safe when the alias resolves to
|
|
127
|
+
// the import at the rewrite site. Resolution has to run per name from the
|
|
128
|
+
// innermost scope outward: an alias bound by a dynamic import inside another
|
|
129
|
+
// function is unreachable here, and an inner binding of the same name would
|
|
130
|
+
// capture the emitted reference and silently swap in a different value
|
|
131
|
+
// (issues #1455/#1456).
|
|
132
|
+
function findTimestampAliasInScope() {
|
|
133
|
+
return importedTimestampAliases.find((alias) => {
|
|
134
|
+
const binding = resolveBinding(alias);
|
|
135
|
+
return !!binding && isTimestampImportBinding(binding);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
40
138
|
function isTimestampFromDateWithNewDate(node) {
|
|
41
139
|
// Check if it's a Timestamp.fromDate(new Date()) call
|
|
42
140
|
if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
@@ -118,41 +216,20 @@ exports.enforceTimestampNow = (0, createRule_1.createRule)({
|
|
|
118
216
|
});
|
|
119
217
|
}
|
|
120
218
|
return {
|
|
121
|
-
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
});
|
|
132
|
-
}
|
|
219
|
+
Program(node) {
|
|
220
|
+
// Collect static imports before any usage is visited. An import is
|
|
221
|
+
// hoisted and module-scoped, so it binds `Timestamp` for the whole file
|
|
222
|
+
// regardless of where it sits; visiting imports in traversal order
|
|
223
|
+
// would make the guard depend on the import preceding the usage.
|
|
224
|
+
node.body.forEach((statement) => {
|
|
225
|
+
if (statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration) {
|
|
226
|
+
staticTimestampAliases(statement).forEach(recordTimestampAlias);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
133
229
|
},
|
|
134
230
|
VariableDeclarator(node) {
|
|
135
231
|
// 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
|
-
}
|
|
232
|
+
dynamicTimestampAliases(node).forEach(recordTimestampAlias);
|
|
156
233
|
},
|
|
157
234
|
CallExpression(node) {
|
|
158
235
|
if (isTimestampFromDateWithNewDate(node)) {
|
|
@@ -216,22 +293,26 @@ exports.enforceTimestampNow = (0, createRule_1.createRule)({
|
|
|
216
293
|
// If the Date is being modified, don't flag it
|
|
217
294
|
return;
|
|
218
295
|
}
|
|
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
|
-
});
|
|
296
|
+
// Stay silent unless a real `Timestamp` binding is in scope. The
|
|
297
|
+
// rewrite names an identifier the source never mentions, and a
|
|
298
|
+
// file with no Firestore import is almost certainly using the
|
|
299
|
+
// `Date` for something other than a Firestore document anyway.
|
|
300
|
+
const timestampName = findTimestampAliasInScope();
|
|
301
|
+
if (!timestampName) {
|
|
302
|
+
return;
|
|
234
303
|
}
|
|
304
|
+
const expressionText = sourceCode.getText(node);
|
|
305
|
+
context.report({
|
|
306
|
+
node,
|
|
307
|
+
messageId: 'preferTimestampNow',
|
|
308
|
+
data: {
|
|
309
|
+
expression: expressionText,
|
|
310
|
+
timestampAlias: timestampName,
|
|
311
|
+
},
|
|
312
|
+
fix(fixer) {
|
|
313
|
+
return fixer.replaceText(node, `${timestampName}.now()`);
|
|
314
|
+
},
|
|
315
|
+
});
|
|
235
316
|
}
|
|
236
317
|
}
|
|
237
318
|
}
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
export declare const noClassInstanceDestructuring: TSESLint.RuleModule<"noClassInstanceDestructuring", [], TSESLint.RuleListener>;
|
|
@@ -3,6 +3,94 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.noClassInstanceDestructuring = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
/**
|
|
7
|
+
* A synthesized binding may not be a keyword, and may not be one of the
|
|
8
|
+
* bare-word globals whose rebinding would be legal but ruinous.
|
|
9
|
+
*/
|
|
10
|
+
const UNUSABLE_NAMES = new Set([
|
|
11
|
+
'break',
|
|
12
|
+
'case',
|
|
13
|
+
'catch',
|
|
14
|
+
'class',
|
|
15
|
+
'const',
|
|
16
|
+
'continue',
|
|
17
|
+
'debugger',
|
|
18
|
+
'default',
|
|
19
|
+
'delete',
|
|
20
|
+
'do',
|
|
21
|
+
'else',
|
|
22
|
+
'enum',
|
|
23
|
+
'export',
|
|
24
|
+
'extends',
|
|
25
|
+
'false',
|
|
26
|
+
'finally',
|
|
27
|
+
'for',
|
|
28
|
+
'function',
|
|
29
|
+
'if',
|
|
30
|
+
'implements',
|
|
31
|
+
'import',
|
|
32
|
+
'in',
|
|
33
|
+
'instanceof',
|
|
34
|
+
'interface',
|
|
35
|
+
'let',
|
|
36
|
+
'new',
|
|
37
|
+
'null',
|
|
38
|
+
'package',
|
|
39
|
+
'private',
|
|
40
|
+
'protected',
|
|
41
|
+
'public',
|
|
42
|
+
'return',
|
|
43
|
+
'static',
|
|
44
|
+
'super',
|
|
45
|
+
'switch',
|
|
46
|
+
'this',
|
|
47
|
+
'throw',
|
|
48
|
+
'true',
|
|
49
|
+
'try',
|
|
50
|
+
'typeof',
|
|
51
|
+
'var',
|
|
52
|
+
'void',
|
|
53
|
+
'while',
|
|
54
|
+
'with',
|
|
55
|
+
'yield',
|
|
56
|
+
'await',
|
|
57
|
+
'arguments',
|
|
58
|
+
'eval',
|
|
59
|
+
'undefined',
|
|
60
|
+
'NaN',
|
|
61
|
+
'Infinity',
|
|
62
|
+
]);
|
|
63
|
+
/**
|
|
64
|
+
* Node types whose children are statements, so a single statement may be
|
|
65
|
+
* replaced by several. Anything else (a `for` initializer, an unbraced `if`
|
|
66
|
+
* body, a label) can hold exactly one.
|
|
67
|
+
*/
|
|
68
|
+
const STATEMENT_CONTAINERS = new Set([
|
|
69
|
+
utils_1.AST_NODE_TYPES.Program,
|
|
70
|
+
utils_1.AST_NODE_TYPES.BlockStatement,
|
|
71
|
+
utils_1.AST_NODE_TYPES.StaticBlock,
|
|
72
|
+
utils_1.AST_NODE_TYPES.SwitchCase,
|
|
73
|
+
utils_1.AST_NODE_TYPES.TSModuleBlock,
|
|
74
|
+
]);
|
|
75
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
76
|
+
function isUsableName(name) {
|
|
77
|
+
return IDENTIFIER_PATTERN.test(name) && !UNUSABLE_NAMES.has(name);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* `Person` -> `person`, `URL` -> `url`, `URLParser` -> `urlParser`. The leading
|
|
81
|
+
* run of capitals is treated as one word so an acronym does not become `uRL`.
|
|
82
|
+
*/
|
|
83
|
+
function lowerFirstWord(name) {
|
|
84
|
+
const leadingCapitals = /^[A-Z]+/.exec(name);
|
|
85
|
+
if (!leadingCapitals)
|
|
86
|
+
return name;
|
|
87
|
+
const run = leadingCapitals[0];
|
|
88
|
+
if (run.length === name.length)
|
|
89
|
+
return name.toLowerCase();
|
|
90
|
+
if (run.length === 1)
|
|
91
|
+
return name[0].toLowerCase() + name.slice(1);
|
|
92
|
+
return run.slice(0, -1).toLowerCase() + name.slice(run.length - 1);
|
|
93
|
+
}
|
|
6
94
|
exports.noClassInstanceDestructuring = (0, createRule_1.createRule)({
|
|
7
95
|
name: 'no-class-instance-destructuring',
|
|
8
96
|
meta: {
|
|
@@ -41,12 +129,12 @@ exports.noClassInstanceDestructuring = (0, createRule_1.createRule)({
|
|
|
41
129
|
}
|
|
42
130
|
return 'member';
|
|
43
131
|
}
|
|
44
|
-
function buildAccessPath(
|
|
132
|
+
function buildAccessPath(receiverText, prop) {
|
|
45
133
|
const keyText = sourceCode.getText(prop.key);
|
|
46
134
|
if (prop.key.type === utils_1.AST_NODE_TYPES.Identifier && !prop.computed) {
|
|
47
|
-
return `${
|
|
135
|
+
return `${receiverText}.${keyText}`;
|
|
48
136
|
}
|
|
49
|
-
return `${
|
|
137
|
+
return `${receiverText}[${keyText}]`;
|
|
50
138
|
}
|
|
51
139
|
function formatMembers(properties) {
|
|
52
140
|
const memberNames = properties.map(describeMember).filter(Boolean);
|
|
@@ -54,12 +142,12 @@ exports.noClassInstanceDestructuring = (0, createRule_1.createRule)({
|
|
|
54
142
|
return '`<members>`';
|
|
55
143
|
return memberNames.map((name) => `\`${name}\``).join(', ');
|
|
56
144
|
}
|
|
57
|
-
function formatAccessExamples(properties,
|
|
145
|
+
function formatAccessExamples(properties, receiverText) {
|
|
58
146
|
const accessPaths = properties
|
|
59
147
|
.filter((prop) => prop.type === utils_1.AST_NODE_TYPES.Property)
|
|
60
|
-
.map((prop) => buildAccessPath(
|
|
148
|
+
.map((prop) => buildAccessPath(receiverText, prop));
|
|
61
149
|
if (accessPaths.length === 0) {
|
|
62
|
-
return `\`${
|
|
150
|
+
return `\`${receiverText}.<member>\``;
|
|
63
151
|
}
|
|
64
152
|
return accessPaths.map((path) => `\`${path}\``).join(', ');
|
|
65
153
|
}
|
|
@@ -81,20 +169,134 @@ exports.noClassInstanceDestructuring = (0, createRule_1.createRule)({
|
|
|
81
169
|
}
|
|
82
170
|
return false;
|
|
83
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* `new Person` and `new Person()` construct alike, but only the latter can
|
|
174
|
+
* carry a member access: `new Person.name` reads `name` off the class and
|
|
175
|
+
* constructs that instead.
|
|
176
|
+
*/
|
|
177
|
+
function callableInitText(init) {
|
|
178
|
+
const text = sourceCode.getText(init);
|
|
179
|
+
if (init.type === utils_1.AST_NODE_TYPES.NewExpression && init.arguments) {
|
|
180
|
+
const lastToken = sourceCode.getLastToken(init);
|
|
181
|
+
if (lastToken?.value !== ')')
|
|
182
|
+
return `${text}()`;
|
|
183
|
+
}
|
|
184
|
+
return text;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Every name that is visible where the temp binding would be inserted,
|
|
188
|
+
* including names only referenced (globals, imports resolved elsewhere) and
|
|
189
|
+
* names bound by nested closures that read through this scope. Reusing any
|
|
190
|
+
* of them would either redeclare or shadow a live binding — see the
|
|
191
|
+
* shadow-capture guard in `src/tests/fixer-shadow-capture.test.ts`.
|
|
192
|
+
*/
|
|
193
|
+
function collectVisibleNames(scope) {
|
|
194
|
+
const names = new Set();
|
|
195
|
+
for (let current = scope; current; current = current.upper) {
|
|
196
|
+
for (const variable of current.variables) {
|
|
197
|
+
names.add(variable.name);
|
|
198
|
+
}
|
|
199
|
+
for (const reference of current.through) {
|
|
200
|
+
names.add(reference.identifier.name);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return names;
|
|
204
|
+
}
|
|
205
|
+
function calleeName(init) {
|
|
206
|
+
const { callee } = init;
|
|
207
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier)
|
|
208
|
+
return callee.name;
|
|
209
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
210
|
+
!callee.computed &&
|
|
211
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
212
|
+
return callee.property.name;
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
function baseNameFor(init) {
|
|
217
|
+
if (init.type !== utils_1.AST_NODE_TYPES.NewExpression)
|
|
218
|
+
return 'instance';
|
|
219
|
+
const raw = calleeName(init);
|
|
220
|
+
if (!raw)
|
|
221
|
+
return 'instance';
|
|
222
|
+
const lowered = lowerFirstWord(raw);
|
|
223
|
+
// An already-lowercase callee would have the temp shadow the class itself.
|
|
224
|
+
if (lowered !== raw && isUsableName(lowered))
|
|
225
|
+
return lowered;
|
|
226
|
+
const suffixed = `${lowered}Instance`;
|
|
227
|
+
return isUsableName(suffixed) ? suffixed : 'instance';
|
|
228
|
+
}
|
|
229
|
+
function uniqueName(base, taken) {
|
|
230
|
+
if (!taken.has(base))
|
|
231
|
+
return base;
|
|
232
|
+
for (let suffix = 2; suffix <= taken.size + 2; suffix++) {
|
|
233
|
+
const candidate = `${base}${suffix}`;
|
|
234
|
+
if (!taken.has(candidate))
|
|
235
|
+
return candidate;
|
|
236
|
+
}
|
|
237
|
+
return base;
|
|
238
|
+
}
|
|
239
|
+
function indentOf(node) {
|
|
240
|
+
const line = sourceCode.lines[node.loc.start.line - 1] ?? '';
|
|
241
|
+
const leading = /^[\t ]*/.exec(line);
|
|
242
|
+
return leading ? leading[0] : '';
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* The statement that may be swapped for several declarations, or `null`
|
|
246
|
+
* when the declarator does not own a whole statement of its own.
|
|
247
|
+
*/
|
|
248
|
+
function resolveStatement(node) {
|
|
249
|
+
const declaration = node.parent;
|
|
250
|
+
if (declaration?.type !== utils_1.AST_NODE_TYPES.VariableDeclaration)
|
|
251
|
+
return null;
|
|
252
|
+
// Splitting one declarator out of `const {a, b} = inst, c = 1;` would have
|
|
253
|
+
// to hoist emitted lines past declarators this fix does not own.
|
|
254
|
+
if (declaration.declarations.length !== 1)
|
|
255
|
+
return null;
|
|
256
|
+
const exportDeclaration = declaration.parent?.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration
|
|
257
|
+
? declaration.parent
|
|
258
|
+
: null;
|
|
259
|
+
const statement = exportDeclaration ?? declaration;
|
|
260
|
+
if (!STATEMENT_CONTAINERS.has(statement.parent?.type ?? ''))
|
|
261
|
+
return null;
|
|
262
|
+
const text = sourceCode.getText(statement);
|
|
263
|
+
return {
|
|
264
|
+
statement,
|
|
265
|
+
kind: declaration.kind,
|
|
266
|
+
// Re-exporting each extracted member keeps the module's public surface,
|
|
267
|
+
// while the temp binding stays private.
|
|
268
|
+
exportPrefix: exportDeclaration ? 'export ' : '',
|
|
269
|
+
indent: indentOf(statement),
|
|
270
|
+
// Matching the source's own terminator keeps semicolon-free files intact.
|
|
271
|
+
terminator: text.endsWith(';') ? ';' : '',
|
|
272
|
+
};
|
|
273
|
+
}
|
|
84
274
|
return {
|
|
85
275
|
VariableDeclarator(node) {
|
|
86
276
|
if (node.id.type === utils_1.AST_NODE_TYPES.ObjectPattern &&
|
|
87
277
|
node.init &&
|
|
88
278
|
isClassInstance(node.init)) {
|
|
89
279
|
const objectPattern = node.id;
|
|
90
|
-
const
|
|
280
|
+
const init = node.init;
|
|
281
|
+
const initText = sourceCode.getText(init);
|
|
282
|
+
const propertyCount = objectPattern.properties.filter((prop) => prop.type === utils_1.AST_NODE_TYPES.Property).length;
|
|
283
|
+
// Reading two members off `new Person()` twice would construct twice,
|
|
284
|
+
// so the instance is bound once and read from. A plain identifier is
|
|
285
|
+
// already such a binding, and a lone member read constructs once.
|
|
286
|
+
const needsTempBinding = init.type !== utils_1.AST_NODE_TYPES.Identifier && propertyCount > 1;
|
|
287
|
+
const tempName = needsTempBinding
|
|
288
|
+
? uniqueName(baseNameFor(init), collectVisibleNames(context.getScope()))
|
|
289
|
+
: null;
|
|
290
|
+
const receiverText = tempName ?? callableInitText(init);
|
|
91
291
|
context.report({
|
|
92
292
|
node,
|
|
93
293
|
messageId: 'noClassInstanceDestructuring',
|
|
94
294
|
data: {
|
|
95
295
|
members: formatMembers(objectPattern.properties),
|
|
96
296
|
instance: `\`${initText}\``,
|
|
97
|
-
suggestion:
|
|
297
|
+
suggestion: tempName
|
|
298
|
+
? `\`const ${tempName} = ${initText};\` then ${formatAccessExamples(objectPattern.properties, tempName)}`
|
|
299
|
+
: formatAccessExamples(objectPattern.properties, receiverText),
|
|
98
300
|
},
|
|
99
301
|
fix(fixer) {
|
|
100
302
|
const properties = objectPattern.properties;
|
|
@@ -108,33 +310,39 @@ exports.noClassInstanceDestructuring = (0, createRule_1.createRule)({
|
|
|
108
310
|
// the code to something more weakly typed than the author wrote.
|
|
109
311
|
if (objectPattern.typeAnnotation)
|
|
110
312
|
return null;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
: sourceCode.getText(prop.value);
|
|
118
|
-
const accessPath = buildAccessPath(initText, prop);
|
|
119
|
-
return fixer.replaceText(node, `${value} = ${accessPath}`);
|
|
120
|
-
}
|
|
313
|
+
if (properties.length === 0)
|
|
314
|
+
return null;
|
|
315
|
+
// A rest element collects the members no other property names, a
|
|
316
|
+
// set only the type checker knows, so it cannot become a member
|
|
317
|
+
// read. Rewriting the siblings alone would delete the binding.
|
|
318
|
+
if (properties.some((prop) => prop.type !== utils_1.AST_NODE_TYPES.Property)) {
|
|
121
319
|
return null;
|
|
122
320
|
}
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const value = prop.value.type === utils_1.AST_NODE_TYPES.Identifier
|
|
128
|
-
? prop.value.name
|
|
129
|
-
: sourceCode.getText(prop.value);
|
|
130
|
-
const accessPath = buildAccessPath(initText, prop);
|
|
131
|
-
return `${value} = ${accessPath}`;
|
|
132
|
-
})
|
|
133
|
-
.join(';\nconst ');
|
|
134
|
-
// Only apply the fix if we have valid declarations
|
|
135
|
-
if (!declarations)
|
|
321
|
+
const props = properties;
|
|
322
|
+
// `const {a = 1} = inst` applies the default only when the member
|
|
323
|
+
// is `undefined`; a plain member read cannot express that.
|
|
324
|
+
if (props.some((prop) => prop.value.type === utils_1.AST_NODE_TYPES.AssignmentPattern)) {
|
|
136
325
|
return null;
|
|
137
|
-
|
|
326
|
+
}
|
|
327
|
+
const targetOf = (prop) => prop.value.type === utils_1.AST_NODE_TYPES.Identifier
|
|
328
|
+
? prop.value.name
|
|
329
|
+
: sourceCode.getText(prop.value);
|
|
330
|
+
// A single binding stays a single declarator, so it can be
|
|
331
|
+
// rewritten in place wherever the declaration sits.
|
|
332
|
+
if (props.length === 1) {
|
|
333
|
+
return fixer.replaceText(node, `${targetOf(props[0])} = ${buildAccessPath(receiverText, props[0])}`);
|
|
334
|
+
}
|
|
335
|
+
const target = resolveStatement(node);
|
|
336
|
+
if (!target)
|
|
337
|
+
return null;
|
|
338
|
+
const lines = tempName ? [`const ${tempName} = ${initText}`] : [];
|
|
339
|
+
for (const prop of props) {
|
|
340
|
+
lines.push(`${target.exportPrefix}${target.kind} ${targetOf(prop)} = ${buildAccessPath(receiverText, prop)}`);
|
|
341
|
+
}
|
|
342
|
+
// One replacement, so the whole rewrite lands or none of it does.
|
|
343
|
+
return fixer.replaceText(target.statement, lines
|
|
344
|
+
.map((line) => `${line}${target.terminator}`)
|
|
345
|
+
.join(`\n${target.indent}`));
|
|
138
346
|
},
|
|
139
347
|
});
|
|
140
348
|
}
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,50 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.50",
|
|
4
|
+
"date": "2026-07-31T12:53:09.792Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "consistent-callback-naming",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1522
|
|
11
|
+
],
|
|
12
|
+
"summary": "stop autofixing prop renames, which broke compilation (closes #1522)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "enforce-early-destructuring",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
1523
|
|
19
|
+
],
|
|
20
|
+
"summary": "drop the synthesized `= {}` on nested object patterns (closes #1523)"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "enforce-object-literal-as-const",
|
|
24
|
+
"changeType": "fix",
|
|
25
|
+
"issues": [
|
|
26
|
+
1526
|
|
27
|
+
],
|
|
28
|
+
"summary": "skip array literals a signature declares mutable (closes #1526)"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "enforce-timestamp-now",
|
|
32
|
+
"changeType": "fix",
|
|
33
|
+
"issues": [
|
|
34
|
+
1521
|
|
35
|
+
],
|
|
36
|
+
"summary": "gate the new Date() autofix on an in-scope Timestamp import (closes #1521)"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "no-class-instance-destructuring",
|
|
40
|
+
"changeType": "fix",
|
|
41
|
+
"issues": [
|
|
42
|
+
1524
|
|
43
|
+
],
|
|
44
|
+
"summary": "bind the instance once instead of reconstructing per property (closes #1524)"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
},
|
|
2
48
|
{
|
|
3
49
|
"version": "1.20.49",
|
|
4
50
|
"date": "2026-07-31T10:04:24.469Z",
|