@blumintinc/eslint-plugin-blumint 1.20.151 → 1.20.153
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-boolean-naming-prefixes.js +173 -0
- package/lib/rules/enforce-object-literal-as-const.js +31 -15
- package/lib/rules/enforce-querykey-ts.js +31 -31
- package/lib/rules/global-const-style.js +149 -3
- package/lib/rules/no-explicit-return-type.js +136 -0
- package/lib/rules/prefer-clone-deep.js +38 -0
- package/package.json +1 -1
- package/release-manifest.json +60 -0
package/lib/index.js
CHANGED
|
@@ -69,6 +69,111 @@ function memberNameOf(key) {
|
|
|
69
69
|
}
|
|
70
70
|
return undefined;
|
|
71
71
|
}
|
|
72
|
+
const EQUALITY_OPERATORS = new Set([
|
|
73
|
+
'===',
|
|
74
|
+
'!==',
|
|
75
|
+
'==',
|
|
76
|
+
'!=',
|
|
77
|
+
]);
|
|
78
|
+
/**
|
|
79
|
+
* The text of a literal string, written either way round: `'string'` and
|
|
80
|
+
* `` `string` `` assert the same thing about the operand beside them.
|
|
81
|
+
*/
|
|
82
|
+
function stringLiteralValueOf(node) {
|
|
83
|
+
if (node.type === utils_1.AST_NODE_TYPES.Literal && typeof node.value === 'string') {
|
|
84
|
+
return node.value;
|
|
85
|
+
}
|
|
86
|
+
if (node.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
|
|
87
|
+
node.expressions.length === 0 &&
|
|
88
|
+
node.quasis.length === 1) {
|
|
89
|
+
return node.quasis[0].value.cooked;
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The operand an equality comparison holds opposite `operand`, so operand
|
|
95
|
+
* order carries no meaning: `typeof x === 'string'` and
|
|
96
|
+
* `'string' === typeof x` are the same assertion.
|
|
97
|
+
*/
|
|
98
|
+
function comparedAgainst(comparison, operand) {
|
|
99
|
+
if (!EQUALITY_OPERATORS.has(comparison.operator))
|
|
100
|
+
return undefined;
|
|
101
|
+
if (comparison.left === operand)
|
|
102
|
+
return comparison.right;
|
|
103
|
+
if (comparison.right === operand)
|
|
104
|
+
return comparison.left;
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The outermost node standing for the same value, so a contradiction written
|
|
109
|
+
* around `verdict!` or `verdict as string` is a contradiction about
|
|
110
|
+
* `verdict`.
|
|
111
|
+
*/
|
|
112
|
+
function passthroughValueOf(node) {
|
|
113
|
+
let current = node;
|
|
114
|
+
while (current.parent) {
|
|
115
|
+
const { parent } = current;
|
|
116
|
+
const wraps = (parent.type === utils_1.AST_NODE_TYPES.TSNonNullExpression ||
|
|
117
|
+
parent.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
118
|
+
parent.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression ||
|
|
119
|
+
parent.type === utils_1.AST_NODE_TYPES.TSTypeAssertion ||
|
|
120
|
+
parent.type === utils_1.AST_NODE_TYPES.ChainExpression) &&
|
|
121
|
+
parent.expression ===
|
|
122
|
+
current;
|
|
123
|
+
if (!wraps)
|
|
124
|
+
break;
|
|
125
|
+
current = parent;
|
|
126
|
+
}
|
|
127
|
+
return current;
|
|
128
|
+
}
|
|
129
|
+
/** `Error`, `TypeError` and any `…Error` class take a string message. */
|
|
130
|
+
function isErrorConstructor(callee) {
|
|
131
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
132
|
+
return callee.name.endsWith('Error');
|
|
133
|
+
}
|
|
134
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
135
|
+
const member = memberNameOf(callee.property);
|
|
136
|
+
return !!member && member.name.endsWith('Error');
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Whether this reference uses the value in a way a boolean could not be
|
|
142
|
+
* used, which disproves a booleanness read off a name.
|
|
143
|
+
*/
|
|
144
|
+
function referenceContradictsBoolean(reference) {
|
|
145
|
+
const value = passthroughValueOf(reference);
|
|
146
|
+
const { parent } = value;
|
|
147
|
+
if (!parent)
|
|
148
|
+
return false;
|
|
149
|
+
// `typeof verdict === 'string'`. A tag of `'boolean'` AFFIRMS the boolean
|
|
150
|
+
// reading whichever equality operator carries it — `!== 'boolean'` is how
|
|
151
|
+
// a boolean guard is spelled — so only some other tag contradicts.
|
|
152
|
+
if (parent.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
|
|
153
|
+
parent.operator === 'typeof' &&
|
|
154
|
+
parent.argument === value) {
|
|
155
|
+
const comparison = parent.parent;
|
|
156
|
+
if (comparison?.type !== utils_1.AST_NODE_TYPES.BinaryExpression)
|
|
157
|
+
return false;
|
|
158
|
+
const other = comparedAgainst(comparison, parent);
|
|
159
|
+
const tag = other ? stringLiteralValueOf(other) : undefined;
|
|
160
|
+
return tag !== undefined && tag !== 'boolean';
|
|
161
|
+
}
|
|
162
|
+
// `verdict === 'occupied'` — a value compared with a string is not a
|
|
163
|
+
// boolean, since no boolean is ever equal to one.
|
|
164
|
+
if (parent.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
|
|
165
|
+
const other = comparedAgainst(parent, value);
|
|
166
|
+
return !!other && stringLiteralValueOf(other) !== undefined;
|
|
167
|
+
}
|
|
168
|
+
// `throw new Error(verdict)` — the message parameter is a string, so the
|
|
169
|
+
// binding carries the failure reason rather than a verdict flag.
|
|
170
|
+
if (parent.type === utils_1.AST_NODE_TYPES.NewExpression &&
|
|
171
|
+
parent.arguments[0] === value &&
|
|
172
|
+
isErrorConstructor(parent.callee)) {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
72
177
|
exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
73
178
|
name: 'enforce-boolean-naming-prefixes',
|
|
74
179
|
meta: {
|
|
@@ -914,6 +1019,66 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
914
1019
|
}
|
|
915
1020
|
return calleeReturnEvaluation(calleeName) !== 'nonBoolean';
|
|
916
1021
|
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Whether the only evidence that a binding holds a boolean is a NAME.
|
|
1024
|
+
*
|
|
1025
|
+
* `calleeReturnEvaluation` answers "does this callee demonstrably return a
|
|
1026
|
+
* non-boolean?"; its 'indeterminate' verdict is the case where the callee's
|
|
1027
|
+
* body is out of reach (an import, a parameter, a value read off a builder
|
|
1028
|
+
* chain) and the callee's `is`/`has`/`can` prefix is all that is left. A
|
|
1029
|
+
* boolean-sounding property (`state.isValid`) is the same kind of evidence.
|
|
1030
|
+
*
|
|
1031
|
+
* Everything else — an explicit `: boolean` annotation, a boolean literal, a
|
|
1032
|
+
* comparison or negation, a `Boolean()` coercion, a resolvable declaration
|
|
1033
|
+
* whose return classifies as boolean — is evidence about the VALUE, which no
|
|
1034
|
+
* use site is allowed to outrank.
|
|
1035
|
+
*/
|
|
1036
|
+
function booleanEvidenceIsNameOnly(declarator) {
|
|
1037
|
+
if (declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
1038
|
+
hasBooleanTypeAnnotation(declarator.id) ||
|
|
1039
|
+
!declarator.init) {
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1042
|
+
const restsOnName = (expression) => {
|
|
1043
|
+
const value = unwrapChainExpression(expression);
|
|
1044
|
+
// A property name is the whole of the evidence in
|
|
1045
|
+
// `isLikelyBooleanByMemberExpression`, the only path that reads one.
|
|
1046
|
+
if (value.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
1047
|
+
return true;
|
|
1048
|
+
}
|
|
1049
|
+
if (value.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
1050
|
+
value.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
1051
|
+
return (!isGlobalBooleanCall(value) &&
|
|
1052
|
+
calleeReturnEvaluation(value.callee.name) === 'indeterminate');
|
|
1053
|
+
}
|
|
1054
|
+
// `isFoo(x) || fallback` reaches booleanness through its left operand.
|
|
1055
|
+
if (value.type === utils_1.AST_NODE_TYPES.LogicalExpression &&
|
|
1056
|
+
value.operator === '||') {
|
|
1057
|
+
return restsOnName(value.left);
|
|
1058
|
+
}
|
|
1059
|
+
return false;
|
|
1060
|
+
};
|
|
1061
|
+
return restsOnName(declarator.init);
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Whether any use of the binding contradicts booleanness.
|
|
1065
|
+
*
|
|
1066
|
+
* Validator families built on `ValidatorPipeline` return `true | string` —
|
|
1067
|
+
* `true` for a pass, the failure message for a fail — while
|
|
1068
|
+
* `enforce-is-prefix-validators` requires the validator itself to be
|
|
1069
|
+
* `is`-prefixed. Inferring the result's booleanness from that mandated
|
|
1070
|
+
* prefix makes the two rules unsatisfiable together, so a use site that
|
|
1071
|
+
* reads the value as a string settles it against the name.
|
|
1072
|
+
*
|
|
1073
|
+
* References come from the scope manager, never from matching the name as
|
|
1074
|
+
* text: a contradiction must belong to THIS binding, not to a shadowing
|
|
1075
|
+
* inner one, a sibling scope's binding, or an unrelated same-named value.
|
|
1076
|
+
*/
|
|
1077
|
+
function useSiteContradictsBoolean(declarator) {
|
|
1078
|
+
return context
|
|
1079
|
+
.getDeclaredVariables(declarator)
|
|
1080
|
+
.some((variable) => variable.references.some((reference) => referenceContradictsBoolean(reference.identifier)));
|
|
1081
|
+
}
|
|
917
1082
|
/**
|
|
918
1083
|
* Check if a variable is used in a while loop condition and is likely a DOM element or tree node
|
|
919
1084
|
* This helps identify variables like 'parent', 'element', 'node', etc. that are used
|
|
@@ -1233,6 +1398,14 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
1233
1398
|
utils_1.AST_NODE_TYPES.TSBooleanKeyword) {
|
|
1234
1399
|
isBooleanVar = true;
|
|
1235
1400
|
}
|
|
1401
|
+
// A booleanness read off a name loses to a use site that treats the value
|
|
1402
|
+
// as something else, which is what keeps this rule satisfiable alongside
|
|
1403
|
+
// `enforce-is-prefix-validators` for `true | string` validator verdicts.
|
|
1404
|
+
if (isBooleanVar &&
|
|
1405
|
+
booleanEvidenceIsNameOnly(node) &&
|
|
1406
|
+
useSiteContradictsBoolean(node)) {
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1236
1409
|
if (isBooleanVar && !hasApprovedPrefix(variableName)) {
|
|
1237
1410
|
context.report({
|
|
1238
1411
|
node: node.id,
|
|
@@ -207,25 +207,41 @@ exports.enforceObjectLiteralAsConst = (0, createRule_1.createRule)({
|
|
|
207
207
|
return undefined;
|
|
208
208
|
}
|
|
209
209
|
/**
|
|
210
|
-
* `as const` turns an array literal into a readonly *tuple*,
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
210
|
+
* `as const` turns an array literal into a fixed-length readonly *tuple*,
|
|
211
|
+
* strictly narrower than the mutable array the literal otherwise gets. Two
|
|
212
|
+
* separate breakages follow from that narrowing, and neither is visible at
|
|
213
|
+
* the literal:
|
|
214
|
+
*
|
|
215
|
+
* - Where the enclosing signature declares a mutable array or tuple, TS4104
|
|
216
|
+
* refuses the assignment, so appending `as const` breaks the build. No
|
|
217
|
+
* edit at the literal satisfies the rule — honouring it means rewriting
|
|
218
|
+
* the signature, a call the author has to make (#1526).
|
|
219
|
+
* - Where the signature is inferred, the frozen arity becomes part of the
|
|
220
|
+
* return type and every caller inherits it: `.length` narrows to a literal
|
|
221
|
+
* number (TS2367 against any other length), `.includes` narrows its
|
|
222
|
+
* parameter to the element union — `never` for `[]` — (TS2345), and the
|
|
223
|
+
* value stops satisfying a mutable `T[]` parameter. The break lands in a
|
|
224
|
+
* different function than the one edited, and the callers are beyond what
|
|
225
|
+
* the rule can see (#2015).
|
|
226
|
+
*
|
|
227
|
+
* So an array literal is left alone unless the enclosing signature states a
|
|
228
|
+
* type that accepts a readonly tuple. An annotation the rule cannot resolve
|
|
229
|
+
* still counts as accepting, per `acceptsReadonlyArray`: the annotation, not
|
|
230
|
+
* the literal, is what callers read, so the arity never escapes.
|
|
217
231
|
*
|
|
218
232
|
* Object literals are unaffected: `readonly` property modifiers do not
|
|
219
233
|
* enter assignability, so `{ a: 1 } as const` still satisfies a mutable
|
|
220
|
-
* `{ a: number }
|
|
234
|
+
* `{ a: number }`, and freezing one fixes no arity.
|
|
221
235
|
*/
|
|
222
|
-
function
|
|
236
|
+
function freezingArrayIsUnsafe(literal, ancestors) {
|
|
223
237
|
if (!isArrayLiteral(literal)) {
|
|
224
238
|
return false;
|
|
225
239
|
}
|
|
226
240
|
const enclosingFunction = enclosingFunctionOf(ancestors);
|
|
227
|
-
|
|
228
|
-
|
|
241
|
+
// With no declared return type in view, the inferred tuple is what the
|
|
242
|
+
// callers get.
|
|
243
|
+
if (!enclosingFunction || !declaredReturnTypeOf(enclosingFunction)) {
|
|
244
|
+
return true;
|
|
229
245
|
}
|
|
230
246
|
const returnedValueType = returnedValueTypeOf(enclosingFunction);
|
|
231
247
|
return !!returnedValueType && !acceptsReadonlyArray(returnedValueType);
|
|
@@ -285,10 +301,10 @@ exports.enforceObjectLiteralAsConst = (0, createRule_1.createRule)({
|
|
|
285
301
|
if (isInsideReactHook(ancestors) && isArrayLiteral(literal)) {
|
|
286
302
|
return;
|
|
287
303
|
}
|
|
288
|
-
// Skip arrays
|
|
289
|
-
//
|
|
290
|
-
// (#
|
|
291
|
-
if (
|
|
304
|
+
// Skip arrays whose enclosing signature does not accept the readonly
|
|
305
|
+
// tuple `as const` produces — declared mutable (#1526) or inferred, in
|
|
306
|
+
// which case the frozen arity reaches every caller (#2015)
|
|
307
|
+
if (freezingArrayIsUnsafe(literal, ancestors)) {
|
|
292
308
|
return;
|
|
293
309
|
}
|
|
294
310
|
// Report the issue and provide a fix
|
|
@@ -123,20 +123,19 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
123
123
|
]);
|
|
124
124
|
const sourceCode = context.getSourceCode();
|
|
125
125
|
/**
|
|
126
|
-
* Reports are buffered until the whole file has been walked so
|
|
127
|
-
*
|
|
128
|
-
*
|
|
126
|
+
* Reports are buffered until the whole file has been walked so each fix is
|
|
127
|
+
* planned against the file's complete import picture: what a substituted
|
|
128
|
+
* constant resolves to, and whether the fix has to bring an import with it,
|
|
129
|
+
* are questions about the whole file rather than about the text above the
|
|
130
|
+
* violation.
|
|
129
131
|
*/
|
|
130
132
|
const pendingReports = [];
|
|
131
133
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
* Resolving suppression here keeps a suppressed violation out of the plan
|
|
138
|
-
* entirely: it neither claims the carrier slot nor contributes a specifier
|
|
139
|
-
* to the import, which would otherwise be imported and never used.
|
|
134
|
+
* A violation the author has waived with an inline disable directive is
|
|
135
|
+
* left out of the fix plan, so no constant is resolved for it and no import
|
|
136
|
+
* is written for it (#1410). This is the decision ESLint reaches anyway
|
|
137
|
+
* when it discards a suppressed message together with its fix, taken one
|
|
138
|
+
* step earlier where the rule can see it.
|
|
140
139
|
*/
|
|
141
140
|
const isReportSuppressed = (0, disableDirectives_1.createSuppressionChecker)(context);
|
|
142
141
|
/**
|
|
@@ -264,10 +263,13 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
264
263
|
: queryKeysSpecifier;
|
|
265
264
|
}
|
|
266
265
|
/**
|
|
267
|
-
* Make
|
|
266
|
+
* Make a substituted constant resolve: extend the file's queryKeys import
|
|
268
267
|
* when there is one to extend, otherwise add a fresh import statement.
|
|
268
|
+
*
|
|
269
|
+
* The caller asks for this only where the constant binds to nothing, so the
|
|
270
|
+
* emitted specifier is never a duplicate of one the file already has.
|
|
269
271
|
*/
|
|
270
|
-
function buildImportFix(fixer,
|
|
272
|
+
function buildImportFix(fixer, constant) {
|
|
271
273
|
const importDeclarations = importDeclarationsOf();
|
|
272
274
|
const queryKeysDeclarations = queryKeysDeclarationsOf();
|
|
273
275
|
const reusable = queryKeysDeclarations.find((declaration) => declaration.importKind !== 'type' &&
|
|
@@ -275,7 +277,7 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
275
277
|
if (reusable) {
|
|
276
278
|
const namedSpecifiers = reusable.specifiers.filter(isValueImportSpecifier);
|
|
277
279
|
const lastSpecifier = namedSpecifiers[namedSpecifiers.length - 1];
|
|
278
|
-
return fixer.insertTextAfter(lastSpecifier,
|
|
280
|
+
return fixer.insertTextAfter(lastSpecifier, `, ${constant}`);
|
|
279
281
|
}
|
|
280
282
|
// A namespace or type-only queryKeys import cannot take named value
|
|
281
283
|
// specifiers, but its path is proof of how this file reaches the module.
|
|
@@ -283,7 +285,7 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
283
285
|
if (source === null) {
|
|
284
286
|
return null;
|
|
285
287
|
}
|
|
286
|
-
const importText = `import { ${
|
|
288
|
+
const importText = `import { ${constant} } from '${source}';\n`;
|
|
287
289
|
const anchor = (0, importInsertion_1.importInsertionAnchor)(sourceCode);
|
|
288
290
|
if (importDeclarations.length) {
|
|
289
291
|
// The statement joins an import block, and the anchor is that block's
|
|
@@ -310,7 +312,6 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
310
312
|
}
|
|
311
313
|
function flushReports() {
|
|
312
314
|
const resolutions = new Map();
|
|
313
|
-
const missingConstants = [];
|
|
314
315
|
const canImport = importSourceOf() !== null;
|
|
315
316
|
// The location handed to `context.report` below is what ESLint matches a
|
|
316
317
|
// directive against, so suppression is resolved from exactly that node.
|
|
@@ -329,20 +330,7 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
329
330
|
continue;
|
|
330
331
|
}
|
|
331
332
|
resolutions.set(report, { name, state });
|
|
332
|
-
if (state === 'missing' && !missingConstants.includes(name)) {
|
|
333
|
-
missingConstants.push(name);
|
|
334
|
-
}
|
|
335
333
|
}
|
|
336
|
-
// The first applied substitution carries the import for every other one:
|
|
337
|
-
// its fix range then starts at the top of the file and ends before the
|
|
338
|
-
// remaining literals, so no two fixes of this rule overlap in a pass.
|
|
339
|
-
// Suppressed reports are skipped so the slot falls to a survivor.
|
|
340
|
-
const importCarrier = pendingReports.find((report) => {
|
|
341
|
-
const resolution = resolutions.get(report);
|
|
342
|
-
return (!suppressed.has(report) &&
|
|
343
|
-
resolution !== undefined &&
|
|
344
|
-
resolution.state !== 'conflict');
|
|
345
|
-
});
|
|
346
334
|
// Suppressed violations are still reported: ESLint discards them, and
|
|
347
335
|
// reporting keeps the user's directive "used" so that
|
|
348
336
|
// `--report-unused-disable-directives` does not flag it.
|
|
@@ -363,8 +351,20 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
363
351
|
const fixes = [
|
|
364
352
|
fixer.replaceText(substitution.keyNode, resolution.name),
|
|
365
353
|
];
|
|
366
|
-
|
|
367
|
-
|
|
354
|
+
// Every fix stands on its own: the one that writes a constant is
|
|
355
|
+
// the one that imports it. Concentrating the file's imports into a
|
|
356
|
+
// single carrier fix made every other fix depend on that one being
|
|
357
|
+
// applied, and ESLint drops a fix whose range overlaps a fix it has
|
|
358
|
+
// already taken from another rule — so the carrier lost that race
|
|
359
|
+
// while the literal-only fixes still landed, leaving the file
|
|
360
|
+
// naming an identifier nothing imports (#2012).
|
|
361
|
+
//
|
|
362
|
+
// Two such fixes both reach for the import declaration, so within a
|
|
363
|
+
// pass they overlap and ESLint applies one of them. That converges:
|
|
364
|
+
// the unapplied violation is re-reported on the next pass and its
|
|
365
|
+
// constant then binds to, or extends, the import that landed.
|
|
366
|
+
if (resolution.state === 'missing') {
|
|
367
|
+
const importFix = buildImportFix(fixer, resolution.name);
|
|
368
368
|
if (!importFix) {
|
|
369
369
|
return null;
|
|
370
370
|
}
|
|
@@ -108,6 +108,139 @@ const isComponentFactoryCall = (node) => {
|
|
|
108
108
|
// the same terms as `const Row = (props) => {...}` (Issue #1681).
|
|
109
109
|
const isFunctionValue = (node) => node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
110
110
|
node.type === utils_1.AST_NODE_TYPES.FunctionExpression;
|
|
111
|
+
// `as const` does more than pin literal types: it makes the value deeply
|
|
112
|
+
// `readonly`. A binding that is written through after its declaration therefore
|
|
113
|
+
// cannot carry the assertion at all — appending it turns compiling code into
|
|
114
|
+
// `TS2339: Property 'push' does not exist on type 'readonly []'` for an array
|
|
115
|
+
// and `TS2540: Cannot assign to 'a' because it is a read-only property` for an
|
|
116
|
+
// object (Issue #2013). These are the built-in methods that mutate their
|
|
117
|
+
// receiver rather than returning a fresh value, so a call to one of them is a
|
|
118
|
+
// write even though no assignment target names the binding.
|
|
119
|
+
const MUTATING_METHOD_NAMES = new Set([
|
|
120
|
+
'push',
|
|
121
|
+
'pop',
|
|
122
|
+
'shift',
|
|
123
|
+
'unshift',
|
|
124
|
+
'splice',
|
|
125
|
+
'sort',
|
|
126
|
+
'reverse',
|
|
127
|
+
'fill',
|
|
128
|
+
'copyWithin',
|
|
129
|
+
]);
|
|
130
|
+
/**
|
|
131
|
+
* Climbs out of the wrappers that denote the same value as `node` — type
|
|
132
|
+
* wrappers (`(X as any).push()`, `X!.push()`) and the `ChainExpression` an
|
|
133
|
+
* optional access hangs on the outside of the whole chain (`delete X?.a`). The
|
|
134
|
+
* role a node plays in its statement is decided by the outermost such wrapper,
|
|
135
|
+
* so a classifier that reads `node.parent` directly answers for the wrapper
|
|
136
|
+
* instead of the access.
|
|
137
|
+
*/
|
|
138
|
+
const outermostValueOf = (node) => {
|
|
139
|
+
let current = node;
|
|
140
|
+
for (;;) {
|
|
141
|
+
const parent = current.parent;
|
|
142
|
+
if (parent &&
|
|
143
|
+
((isValueWrapper(parent) && parent.expression === current) ||
|
|
144
|
+
(parent.type === utils_1.AST_NODE_TYPES.ChainExpression &&
|
|
145
|
+
parent.expression === current))) {
|
|
146
|
+
current = parent;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
return current;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* The outermost property-access path rooted at `identifier`: `X` in `X.a.b`
|
|
154
|
+
* yields the `X.a.b` member expression. Returns `null` when the identifier is
|
|
155
|
+
* not the base of any access, which is every reference that merely reads the
|
|
156
|
+
* binding as a value — `other.push(X)` passes it as an ARGUMENT, so the
|
|
157
|
+
* mutation happens to `other`, not to `X`.
|
|
158
|
+
*
|
|
159
|
+
* The climb stops at the first parent that is not a member access on the
|
|
160
|
+
* current node, so `X.map(f).push(1)` yields `X.map`: the mutated receiver
|
|
161
|
+
* there is the array `map` returned, not `X`.
|
|
162
|
+
*/
|
|
163
|
+
const accessPathOf = (identifier) => {
|
|
164
|
+
let current = outermostValueOf(identifier);
|
|
165
|
+
let path = null;
|
|
166
|
+
for (;;) {
|
|
167
|
+
const parent = current.parent;
|
|
168
|
+
if (!parent ||
|
|
169
|
+
parent.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
|
|
170
|
+
parent.object !== current) {
|
|
171
|
+
return path;
|
|
172
|
+
}
|
|
173
|
+
path = parent;
|
|
174
|
+
current = outermostValueOf(parent);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
/** The property name an access reads, for `X.push` and `X['push']` alike. */
|
|
178
|
+
const accessedPropertyName = (path) => {
|
|
179
|
+
if (!path.computed && path.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
180
|
+
return path.property.name;
|
|
181
|
+
}
|
|
182
|
+
if (path.computed &&
|
|
183
|
+
path.property.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
184
|
+
typeof path.property.value === 'string') {
|
|
185
|
+
return path.property.value;
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
};
|
|
189
|
+
const isMutatingMethodCall = (path) => {
|
|
190
|
+
const propertyName = accessedPropertyName(path);
|
|
191
|
+
if (propertyName === null || !MUTATING_METHOD_NAMES.has(propertyName)) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
const callee = outermostValueOf(path);
|
|
195
|
+
return (callee.parent?.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
196
|
+
callee.parent.callee === callee);
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* Whether `node` sits in a position that writes to it: the left of an
|
|
200
|
+
* assignment (plain or compound), the operand of `++`/`--` or `delete`, the
|
|
201
|
+
* loop variable of `for…in`/`for…of`, or a slot in a destructuring assignment
|
|
202
|
+
* target (`[X.a] = […]`, `({ p: X.a } = …)`).
|
|
203
|
+
*/
|
|
204
|
+
const isWriteTarget = (node) => {
|
|
205
|
+
const value = outermostValueOf(node);
|
|
206
|
+
const parent = value.parent;
|
|
207
|
+
if (!parent) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
switch (parent.type) {
|
|
211
|
+
case utils_1.AST_NODE_TYPES.AssignmentExpression:
|
|
212
|
+
return parent.left === value;
|
|
213
|
+
case utils_1.AST_NODE_TYPES.UpdateExpression:
|
|
214
|
+
return parent.argument === value;
|
|
215
|
+
case utils_1.AST_NODE_TYPES.UnaryExpression:
|
|
216
|
+
return parent.operator === 'delete' && parent.argument === value;
|
|
217
|
+
case utils_1.AST_NODE_TYPES.ForInStatement:
|
|
218
|
+
case utils_1.AST_NODE_TYPES.ForOfStatement:
|
|
219
|
+
return parent.left === value;
|
|
220
|
+
// Destructuring targets nest, so the answer belongs to the pattern's own
|
|
221
|
+
// position. The same node types appear in ObjectExpression/ArrayExpression
|
|
222
|
+
// VALUES, where the recursion reaches a non-assignment parent and stops.
|
|
223
|
+
case utils_1.AST_NODE_TYPES.ArrayPattern:
|
|
224
|
+
case utils_1.AST_NODE_TYPES.ObjectPattern:
|
|
225
|
+
case utils_1.AST_NODE_TYPES.Property:
|
|
226
|
+
case utils_1.AST_NODE_TYPES.RestElement:
|
|
227
|
+
case utils_1.AST_NODE_TYPES.AssignmentPattern:
|
|
228
|
+
return isWriteTarget(parent);
|
|
229
|
+
default:
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
/**
|
|
234
|
+
* Whether the binding is written through anywhere in the file. Answered from
|
|
235
|
+
* the scope manager's reference list rather than a textual search for the
|
|
236
|
+
* name, so a same-named binding in another scope (`const arr` shadowed inside a
|
|
237
|
+
* callback) contributes nothing, and a same-named method on an unrelated
|
|
238
|
+
* receiver (`other.push(1)`) is never even visited.
|
|
239
|
+
*/
|
|
240
|
+
const isBindingMutated = (variable) => variable.references.some((reference) => {
|
|
241
|
+
const path = accessPathOf(reference.identifier);
|
|
242
|
+
return path !== null && (isMutatingMethodCall(path) || isWriteTarget(path));
|
|
243
|
+
});
|
|
111
244
|
/**
|
|
112
245
|
* Walks the scope chain upward from `scope` (inclusive) and reports whether
|
|
113
246
|
* `targetName` is bound anywhere between `scope` and `stopScope` (inclusive).
|
|
@@ -366,9 +499,22 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
366
499
|
(target.value === null || typeof target.value === 'boolean')) {
|
|
367
500
|
return false;
|
|
368
501
|
}
|
|
369
|
-
|
|
370
|
-
target.type
|
|
371
|
-
target.type
|
|
502
|
+
if (target.type !== utils_1.AST_NODE_TYPES.Literal &&
|
|
503
|
+
target.type !== utils_1.AST_NODE_TYPES.ArrayExpression &&
|
|
504
|
+
target.type !== utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
// A binding that is mutated later can never take the assertion:
|
|
508
|
+
// `as const` types the value `readonly`, so the appended text
|
|
509
|
+
// turns working code into TS2339/TS2540 (Issue #2013). The
|
|
510
|
+
// report is withheld rather than merely the fix, on the same
|
|
511
|
+
// terms as the `null`/boolean carve-out above — a violation no
|
|
512
|
+
// legal edit can clear is not a violation. The rename is a
|
|
513
|
+
// separate concern and still applies.
|
|
514
|
+
const declaredVariable = context
|
|
515
|
+
.getDeclaredVariables(declaration)
|
|
516
|
+
.find((variable) => variable.name === name);
|
|
517
|
+
return !declaredVariable || !isBindingMutated(declaredVariable);
|
|
372
518
|
};
|
|
373
519
|
if (shouldHaveAsConst(init)) {
|
|
374
520
|
context.report({
|
|
@@ -713,6 +713,104 @@ function declaresVoidResult(returnType) {
|
|
|
713
713
|
return (typeArguments?.length === 1 &&
|
|
714
714
|
typeArguments[0].type === utils_1.AST_NODE_TYPES.TSVoidKeyword);
|
|
715
715
|
}
|
|
716
|
+
// TypeScript's built-in decorator signatures. A factory annotated with one of
|
|
717
|
+
// these is the one shape where the annotation is WIDER than what inference
|
|
718
|
+
// produces rather than a restatement of it: `MethodDecorator` accepts three
|
|
719
|
+
// parameters, the returned closure typically declares none, and a decoration
|
|
720
|
+
// site requires the declared arity. Stripping the annotation therefore turns
|
|
721
|
+
// every `@Factory()` use into TS1329 (#2014).
|
|
722
|
+
const DECORATOR_TYPE_NAMES = new Set([
|
|
723
|
+
'ClassDecorator',
|
|
724
|
+
'MethodDecorator',
|
|
725
|
+
'ParameterDecorator',
|
|
726
|
+
'PropertyDecorator',
|
|
727
|
+
]);
|
|
728
|
+
/**
|
|
729
|
+
* The identifier a type name resolves to. A qualified name (`ts.MethodDecorator`)
|
|
730
|
+
* denotes the type its right-most segment names, so that segment is what decides
|
|
731
|
+
* — a substring test over the printed annotation would equally match
|
|
732
|
+
* `MyMethodDecoratorConfig`, which is an unrelated user type.
|
|
733
|
+
*/
|
|
734
|
+
function rightmostTypeName(typeName) {
|
|
735
|
+
if (typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
736
|
+
return typeName.name;
|
|
737
|
+
}
|
|
738
|
+
if (typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
|
|
739
|
+
return rightmostTypeName(typeName.right);
|
|
740
|
+
}
|
|
741
|
+
return undefined;
|
|
742
|
+
}
|
|
743
|
+
function namesDecoratorType(annotation) {
|
|
744
|
+
if (annotation.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
|
|
745
|
+
const name = rightmostTypeName(annotation.typeName);
|
|
746
|
+
return name !== undefined && DECORATOR_TYPE_NAMES.has(name);
|
|
747
|
+
}
|
|
748
|
+
// A factory usable in more than one position (`ClassDecorator &
|
|
749
|
+
// MethodDecorator`) still owes every decoration site the declared shape.
|
|
750
|
+
if (annotation.type === utils_1.AST_NODE_TYPES.TSUnionType ||
|
|
751
|
+
annotation.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
|
|
752
|
+
return annotation.types.some(namesDecoratorType);
|
|
753
|
+
}
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* The identifier a CALLED decorator invokes: `Log` for `@Log()` and `@Log()()`.
|
|
758
|
+
*
|
|
759
|
+
* Only a called decorator identifies a factory, and only a factory's return type
|
|
760
|
+
* is what the decoration site consumes. A bare `@Log` names the decorator
|
|
761
|
+
* itself, whose annotation restates the value it returns exactly as inference
|
|
762
|
+
* would — so it stays reportable rather than being silenced by proximity to a
|
|
763
|
+
* decorator.
|
|
764
|
+
*
|
|
765
|
+
* An owner-qualified decorator (`@registry.log()`) names a property rather than
|
|
766
|
+
* a binding, and matching it by property name alone would silence the rule on
|
|
767
|
+
* every unrelated method of the same name, so it yields nothing.
|
|
768
|
+
*/
|
|
769
|
+
function decoratorFactoryIdentifier(expression) {
|
|
770
|
+
if (expression.type !== utils_1.AST_NODE_TYPES.CallExpression)
|
|
771
|
+
return undefined;
|
|
772
|
+
const callee = expression.callee;
|
|
773
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
774
|
+
return callee;
|
|
775
|
+
}
|
|
776
|
+
return decoratorFactoryIdentifier(callee);
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* The declarations invoked by a decorator in this file.
|
|
780
|
+
*
|
|
781
|
+
* This catches the factory whose annotation is a user-defined decorator type
|
|
782
|
+
* (`type Cached = (t: object, k: string, d: PropertyDescriptor) => void`), which
|
|
783
|
+
* no name test can recognise. Each identifier is resolved through the scope
|
|
784
|
+
* manager rather than compared by name, so a same-named binding elsewhere in the
|
|
785
|
+
* file cannot silence the rule on a function no decorator actually reaches.
|
|
786
|
+
*/
|
|
787
|
+
function decoratorReferencedDeclarations(source, visitorKeys) {
|
|
788
|
+
const heads = new Set();
|
|
789
|
+
const stack = [source.ast];
|
|
790
|
+
while (stack.length > 0) {
|
|
791
|
+
const current = stack.pop();
|
|
792
|
+
if (current.type === utils_1.AST_NODE_TYPES.Decorator) {
|
|
793
|
+
const head = decoratorFactoryIdentifier(current.expression);
|
|
794
|
+
if (head) {
|
|
795
|
+
heads.add(head);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
pushChildren(current, visitorKeys, stack);
|
|
799
|
+
}
|
|
800
|
+
const declarations = new Set();
|
|
801
|
+
if (heads.size === 0)
|
|
802
|
+
return declarations;
|
|
803
|
+
for (const scope of source.scopeManager?.scopes ?? []) {
|
|
804
|
+
for (const reference of scope.references) {
|
|
805
|
+
if (!heads.has(reference.identifier))
|
|
806
|
+
continue;
|
|
807
|
+
for (const definition of reference.resolved?.defs ?? []) {
|
|
808
|
+
declarations.add(definition.node);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return declarations;
|
|
813
|
+
}
|
|
716
814
|
function containsRange(outer, inner) {
|
|
717
815
|
return inner[0] >= outer[0] && inner[1] <= outer[1];
|
|
718
816
|
}
|
|
@@ -967,6 +1065,40 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
|
|
|
967
1065
|
// Edges are resolved lazily, and only for functions a direct
|
|
968
1066
|
// self-reference has already failed to explain.
|
|
969
1067
|
const participatesInReturnCycle = createReturnCycleResolver(visitorKeys);
|
|
1068
|
+
// Decorators are visited after the functions they name — a class body is
|
|
1069
|
+
// walked long after the top-level factory it decorates with — so the
|
|
1070
|
+
// answer is computed from the whole tree rather than accumulated during
|
|
1071
|
+
// the walk, and memoised because most files hold no decorator at all.
|
|
1072
|
+
let decoratedDeclarations;
|
|
1073
|
+
const declarationsNamedByDecorators = () => {
|
|
1074
|
+
decoratedDeclarations ??= decoratorReferencedDeclarations(sourceCode, visitorKeys);
|
|
1075
|
+
return decoratedDeclarations;
|
|
1076
|
+
};
|
|
1077
|
+
/**
|
|
1078
|
+
* True when the annotation is what makes the function usable in a
|
|
1079
|
+
* decorator position. TypeScript infers the concrete closure the factory
|
|
1080
|
+
* returns — `() => void` for `return () => {};` — which declares fewer
|
|
1081
|
+
* parameters than a decoration site passes, so removing the annotation
|
|
1082
|
+
* turns every `@Factory()` use into TS1329 (#2014).
|
|
1083
|
+
*
|
|
1084
|
+
* The question is answered syntactically. A `RuleTester` fixture carries
|
|
1085
|
+
* no `parserOptions.project`, so a type-based answer would be untestable
|
|
1086
|
+
* and would silently no-op wherever consumers lint without a program.
|
|
1087
|
+
*/
|
|
1088
|
+
function isDecoratorFactory(node, returnType) {
|
|
1089
|
+
if (namesDecoratorType(returnType.typeAnnotation))
|
|
1090
|
+
return true;
|
|
1091
|
+
const declarations = declarationsNamedByDecorators();
|
|
1092
|
+
if (declarations.size === 0)
|
|
1093
|
+
return false;
|
|
1094
|
+
if (declarations.has(node))
|
|
1095
|
+
return true;
|
|
1096
|
+
// `const Log = (): Cached => ...` is bound by its declarator, which is
|
|
1097
|
+
// what a decorator's identifier resolves to.
|
|
1098
|
+
const parent = node.parent;
|
|
1099
|
+
return (parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
1100
|
+
declarations.has(parent));
|
|
1101
|
+
}
|
|
970
1102
|
/**
|
|
971
1103
|
* True when TypeScript cannot infer the return type because the function
|
|
972
1104
|
* is referenced from within its own return expression (TS7023). Removing
|
|
@@ -1100,6 +1232,7 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
|
|
|
1100
1232
|
if (isTypeGuardFunction(node) ||
|
|
1101
1233
|
isReadonlyWideningReturnType(returnType) ||
|
|
1102
1234
|
isAllowedVoidReturnType(returnType) ||
|
|
1235
|
+
isDecoratorFactory(node, returnType) ||
|
|
1103
1236
|
(mergedOptions.allowRecursiveFunctions &&
|
|
1104
1237
|
isRecursiveFunction(node)) ||
|
|
1105
1238
|
isReturnTypeRequiredByRecursion(node)) {
|
|
@@ -1117,6 +1250,7 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
|
|
|
1117
1250
|
if (isTypeGuardFunction(node) ||
|
|
1118
1251
|
isReadonlyWideningReturnType(returnType) ||
|
|
1119
1252
|
isAllowedVoidReturnType(returnType) ||
|
|
1253
|
+
isDecoratorFactory(node, returnType) ||
|
|
1120
1254
|
(mergedOptions.allowRecursiveFunctions &&
|
|
1121
1255
|
isRecursiveFunction(node)) ||
|
|
1122
1256
|
isReturnTypeRequiredByRecursion(node)) {
|
|
@@ -1131,6 +1265,7 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
|
|
|
1131
1265
|
if (isTypeGuardFunction(node) ||
|
|
1132
1266
|
isReadonlyWideningReturnType(returnType) ||
|
|
1133
1267
|
isAllowedVoidReturnType(returnType) ||
|
|
1268
|
+
isDecoratorFactory(node, returnType) ||
|
|
1134
1269
|
isReturnTypeRequiredByRecursion(node)) {
|
|
1135
1270
|
return;
|
|
1136
1271
|
}
|
|
@@ -1156,6 +1291,7 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
|
|
|
1156
1291
|
if (isTypeGuardFunction(node.value) ||
|
|
1157
1292
|
isReadonlyWideningReturnType(returnType) ||
|
|
1158
1293
|
isAllowedVoidReturnType(returnType) ||
|
|
1294
|
+
isDecoratorFactory(node, returnType) ||
|
|
1159
1295
|
(mergedOptions.allowAbstractMethodSignatures &&
|
|
1160
1296
|
isInterfaceOrAbstractMethodSignature(node)) ||
|
|
1161
1297
|
isReturnTypeRequiredByRecursion(node)) {
|
|
@@ -25,6 +25,41 @@ function isCloneDeepModule(source) {
|
|
|
25
25
|
source.startsWith('@/') ||
|
|
26
26
|
source.endsWith('util/cloneDeep'));
|
|
27
27
|
}
|
|
28
|
+
function isConstAssertion(node) {
|
|
29
|
+
return (node.type === utils_1.AST_NODE_TYPES.TSAsExpression &&
|
|
30
|
+
node.typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
31
|
+
node.typeAnnotation.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
32
|
+
node.typeAnnotation.typeName.name === 'const');
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A `const` assertion is legal only on a literal, so leaving one wrapped around
|
|
36
|
+
* the emitted `cloneDeep(...)` call yields TS1355 and turns a compiling file
|
|
37
|
+
* into a broken one (#2011). The fix is declined there rather than absorbing
|
|
38
|
+
* the assertion, which is the conservative reading of a `const` the author
|
|
39
|
+
* asked for on a value this rule replaces.
|
|
40
|
+
*
|
|
41
|
+
* The whole assertion chain is walked because each of its links still applies
|
|
42
|
+
* to the emitted call: `as Foo as const`, `satisfies Foo as const` and
|
|
43
|
+
* `! as const` are TS1355 just the same. The walk stops at the first parent
|
|
44
|
+
* that is not an assertion, which keeps `as const` on an ENCLOSING literal
|
|
45
|
+
* fixable — that assertion still has a literal to apply to.
|
|
46
|
+
*
|
|
47
|
+
* Only a `const` assertion is disqualifying: `as Foo` and `satisfies Foo` are
|
|
48
|
+
* legal on a call expression and keep their fix.
|
|
49
|
+
*/
|
|
50
|
+
function isConstAsserted(node) {
|
|
51
|
+
let current = node.parent;
|
|
52
|
+
while (current &&
|
|
53
|
+
(current.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
54
|
+
current.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression ||
|
|
55
|
+
current.type === utils_1.AST_NODE_TYPES.TSNonNullExpression)) {
|
|
56
|
+
if (isConstAssertion(current)) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
current = current.parent;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
28
63
|
exports.preferCloneDeep = (0, createRule_1.createRule)({
|
|
29
64
|
name: 'prefer-clone-deep',
|
|
30
65
|
meta: {
|
|
@@ -444,6 +479,9 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
|
|
|
444
479
|
return null;
|
|
445
480
|
}
|
|
446
481
|
for (const target of targets) {
|
|
482
|
+
if (isConstAsserted(target)) {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
447
485
|
const call = buildCloneDeepCall(target);
|
|
448
486
|
if (call === null) {
|
|
449
487
|
return null;
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,64 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.153",
|
|
4
|
+
"date": "2026-08-14T20:21:23.691Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-boolean-naming-prefixes",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2016
|
|
11
|
+
],
|
|
12
|
+
"summary": "decline when the use site contradicts the callee's name (closes #2016)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "enforce-object-literal-as-const",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
2015
|
|
19
|
+
],
|
|
20
|
+
"summary": "keep an unannotated returned array unfrozen (closes #2015)"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "global-const-style",
|
|
24
|
+
"changeType": "fix",
|
|
25
|
+
"issues": [
|
|
26
|
+
2013
|
|
27
|
+
],
|
|
28
|
+
"summary": "decline the as const when the binding is mutated later (closes #2013)"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "no-explicit-return-type",
|
|
32
|
+
"changeType": "fix",
|
|
33
|
+
"issues": [
|
|
34
|
+
2014
|
|
35
|
+
],
|
|
36
|
+
"summary": "keep a decorator factory's annotation (closes #2014)"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"version": "1.20.152",
|
|
42
|
+
"date": "2026-08-14T10:28:08.765Z",
|
|
43
|
+
"rules": [
|
|
44
|
+
{
|
|
45
|
+
"name": "enforce-querykey-ts",
|
|
46
|
+
"changeType": "fix",
|
|
47
|
+
"issues": [
|
|
48
|
+
2012
|
|
49
|
+
],
|
|
50
|
+
"summary": "carry each key's import on its own fix (closes #2012)"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "prefer-clone-deep",
|
|
54
|
+
"changeType": "fix",
|
|
55
|
+
"issues": [
|
|
56
|
+
2011
|
|
57
|
+
],
|
|
58
|
+
"summary": "decline the fix under a const assertion (closes #2011)"
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
},
|
|
2
62
|
{
|
|
3
63
|
"version": "1.20.151",
|
|
4
64
|
"date": "2026-08-14T02:27:13.012Z",
|