@discourse/lint-configs 2.40.0 → 2.41.0
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/eslint-rules/no-discourse-computed/discourse-computed-analysis.mjs +614 -0
- package/eslint-rules/no-discourse-computed/discourse-computed-fixer.mjs +319 -0
- package/eslint-rules/no-discourse-computed.mjs +388 -0
- package/eslint-rules/utils/analyze-imports.mjs +65 -0
- package/eslint-rules/utils/property-path.mjs +78 -0
- package/eslint.mjs +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Analysis helpers for the `no-discourse-computed` ESLint rule.
|
|
3
|
+
*
|
|
4
|
+
* These helpers are intentionally isolated so they can be reused by other
|
|
5
|
+
* rules or tests. They perform read-only AST traversal and return detailed
|
|
6
|
+
* information about @discourseComputed usages to determine auto-fixability.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} UsageInfo
|
|
11
|
+
* @property {string} [messageId] - The suggested messageId if not fixable
|
|
12
|
+
* @property {Object} [reportData] - Data for the report message
|
|
13
|
+
* @property {boolean} canAutoFix - Whether this specific usage is auto-fixable
|
|
14
|
+
* @property {boolean} isClassic - Whether this is a classic Ember class usage
|
|
15
|
+
* @property {Array} [simpleReassignments] - List of simple reassignments for the fixer
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {Object} DiscourseComputedInfo
|
|
20
|
+
* @property {boolean} hasFixableDecorators
|
|
21
|
+
* @property {boolean} hasClassicClassDecorators
|
|
22
|
+
* @property {boolean} hasParameterReassignments
|
|
23
|
+
* @property {boolean} hasParametersInSpread
|
|
24
|
+
* @property {boolean} hasUnsafeOptionalChaining
|
|
25
|
+
* @property {boolean} hasParameterInNestedFunction
|
|
26
|
+
* @property {Map<import('estree').Node, UsageInfo>} usageMap - Map of nodes to their detailed usage info
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Analyze the source AST to detect various usages of `@discourseComputed` that
|
|
31
|
+
* determine whether decorators are safe to auto-fix.
|
|
32
|
+
*
|
|
33
|
+
* @param {import('eslint').SourceCode} sourceCode - ESLint SourceCode instance
|
|
34
|
+
* @param {string|null} discourseComputedLocalName - local identifier name used for the discourseComputed import
|
|
35
|
+
* @returns {DiscourseComputedInfo}
|
|
36
|
+
*/
|
|
37
|
+
export function analyzeDiscourseComputedUsage(
|
|
38
|
+
sourceCode,
|
|
39
|
+
discourseComputedLocalName
|
|
40
|
+
) {
|
|
41
|
+
const info = {
|
|
42
|
+
hasFixableDecorators: false,
|
|
43
|
+
hasClassicClassDecorators: false,
|
|
44
|
+
hasParameterReassignments: false,
|
|
45
|
+
hasParametersInSpread: false,
|
|
46
|
+
hasUnsafeOptionalChaining: false,
|
|
47
|
+
hasParameterInNestedFunction: false,
|
|
48
|
+
usageMap: new Map(),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (!discourseComputedLocalName) {
|
|
52
|
+
return info;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Helper to traverse any node recursively
|
|
56
|
+
const traverseNode = (node) => {
|
|
57
|
+
if (!node || typeof node !== "object") {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (node.type === "ClassDeclaration" || node.type === "ClassExpression") {
|
|
62
|
+
analyzeClassBody(node.body, info);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (
|
|
66
|
+
node.type === "CallExpression" &&
|
|
67
|
+
node.callee &&
|
|
68
|
+
node.callee.type === "MemberExpression" &&
|
|
69
|
+
node.callee.property &&
|
|
70
|
+
node.callee.property.name === "extend" &&
|
|
71
|
+
node.callee.object &&
|
|
72
|
+
node.callee.object.type === "Identifier" &&
|
|
73
|
+
/^(Component|Controller|Route|EmberObject|Service|Object)$/.test(
|
|
74
|
+
node.callee.object.name
|
|
75
|
+
)
|
|
76
|
+
) {
|
|
77
|
+
node.arguments.forEach((arg) => {
|
|
78
|
+
if (arg.type === "ObjectExpression") {
|
|
79
|
+
analyzeObjectExpression(arg, info);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Handle direct CallExpression of discourseComputed (classic classes)
|
|
85
|
+
if (
|
|
86
|
+
node.type === "CallExpression" &&
|
|
87
|
+
node.callee &&
|
|
88
|
+
node.callee.name === discourseComputedLocalName
|
|
89
|
+
) {
|
|
90
|
+
// Check if this CallExpression is part of a decorator
|
|
91
|
+
const isDecorator = node.parent && node.parent.type === "Decorator";
|
|
92
|
+
if (!isDecorator) {
|
|
93
|
+
// Check if we're inside a .extend() call
|
|
94
|
+
let parent = node.parent;
|
|
95
|
+
let isClassicClass = false;
|
|
96
|
+
while (parent) {
|
|
97
|
+
if (
|
|
98
|
+
parent.type === "CallExpression" &&
|
|
99
|
+
parent.callee &&
|
|
100
|
+
parent.callee.type === "MemberExpression" &&
|
|
101
|
+
parent.callee.property &&
|
|
102
|
+
parent.callee.property.name === "extend" &&
|
|
103
|
+
parent.callee.object &&
|
|
104
|
+
parent.callee.object.type === "Identifier" &&
|
|
105
|
+
/^(Component|Controller|Route|EmberObject|Service|Object)$/.test(
|
|
106
|
+
parent.callee.object.name
|
|
107
|
+
)
|
|
108
|
+
) {
|
|
109
|
+
isClassicClass = true;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
parent = parent.parent;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (isClassicClass) {
|
|
116
|
+
info.hasClassicClassDecorators = true;
|
|
117
|
+
info.usageMap.set(node, {
|
|
118
|
+
canAutoFix: false,
|
|
119
|
+
isClassic: true,
|
|
120
|
+
messageId: "cannotAutoFixClassic",
|
|
121
|
+
reportData: { name: discourseComputedLocalName },
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const key in node) {
|
|
128
|
+
if (key === "parent" || key === "range" || key === "loc") {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const child = node[key];
|
|
132
|
+
if (Array.isArray(child)) {
|
|
133
|
+
child.forEach((item) => traverseNode(item));
|
|
134
|
+
} else {
|
|
135
|
+
traverseNode(child);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Analyze AST body
|
|
141
|
+
sourceCode.ast.body.forEach((statement) => traverseNode(statement));
|
|
142
|
+
|
|
143
|
+
return info;
|
|
144
|
+
|
|
145
|
+
// ---- local helpers ----
|
|
146
|
+
|
|
147
|
+
function analyzeClassBody(classBody, infoObj) {
|
|
148
|
+
if (!classBody || !classBody.body) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
classBody.body.forEach((member) => {
|
|
153
|
+
if (member.type !== "MethodDefinition" || !member.decorators) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const discourseDecorator = member.decorators.find((decorator) => {
|
|
158
|
+
const expr = decorator.expression;
|
|
159
|
+
if (expr.type === "CallExpression") {
|
|
160
|
+
return expr.callee.name === discourseComputedLocalName;
|
|
161
|
+
}
|
|
162
|
+
return expr.name === discourseComputedLocalName;
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (!discourseDecorator) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const usageInfo = analyzeMethodUsage(member, discourseDecorator);
|
|
170
|
+
infoObj.usageMap.set(discourseDecorator, usageInfo);
|
|
171
|
+
|
|
172
|
+
// Update global summary flags
|
|
173
|
+
if (usageInfo.canAutoFix) {
|
|
174
|
+
infoObj.hasFixableDecorators = true;
|
|
175
|
+
} else if (usageInfo.isClassic) {
|
|
176
|
+
infoObj.hasClassicClassDecorators = true;
|
|
177
|
+
} else {
|
|
178
|
+
const mid = usageInfo.messageId;
|
|
179
|
+
if (mid === "cannotAutoFixNestedFunction") {
|
|
180
|
+
infoObj.hasParameterInNestedFunction = true;
|
|
181
|
+
} else if (mid === "cannotAutoFixUnsafeOptionalChaining") {
|
|
182
|
+
infoObj.hasUnsafeOptionalChaining = true;
|
|
183
|
+
} else if (mid === "cannotAutoFixSpread") {
|
|
184
|
+
infoObj.hasParametersInSpread = true;
|
|
185
|
+
} else {
|
|
186
|
+
infoObj.hasParameterReassignments = true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Returns true if funcNode's body references `arguments` at the outer function
|
|
194
|
+
* scope — i.e., not inside a nested non-arrow function (which has its own
|
|
195
|
+
* `arguments` object unrelated to the method's parameters).
|
|
196
|
+
*
|
|
197
|
+
* @param {import('estree').Function} funcNode
|
|
198
|
+
* @returns {boolean}
|
|
199
|
+
*/
|
|
200
|
+
function containsArgumentsReference(funcNode) {
|
|
201
|
+
let found = false;
|
|
202
|
+
function visit(node, insideNestedNonArrow) {
|
|
203
|
+
if (found || !node || typeof node !== "object") {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (
|
|
207
|
+
node.type === "Identifier" &&
|
|
208
|
+
node.name === "arguments" &&
|
|
209
|
+
!insideNestedNonArrow
|
|
210
|
+
) {
|
|
211
|
+
found = true;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const childFlag =
|
|
215
|
+
insideNestedNonArrow ||
|
|
216
|
+
node.type === "FunctionDeclaration" ||
|
|
217
|
+
node.type === "FunctionExpression";
|
|
218
|
+
for (const key of Object.keys(node)) {
|
|
219
|
+
if (key === "parent" || key === "range" || key === "loc") {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const child = node[key];
|
|
223
|
+
if (Array.isArray(child)) {
|
|
224
|
+
child.forEach((item) => visit(item, childFlag));
|
|
225
|
+
} else {
|
|
226
|
+
visit(child, childFlag);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
visit(funcNode.body, false);
|
|
231
|
+
return found;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function analyzeMethodUsage(methodNode, decoratorNode) {
|
|
235
|
+
const decoratorExpression = decoratorNode.expression;
|
|
236
|
+
let decoratorArgs = [];
|
|
237
|
+
if (decoratorExpression.type === "CallExpression") {
|
|
238
|
+
decoratorArgs = decoratorExpression.arguments
|
|
239
|
+
.map((arg) => (arg.type === "Literal" ? arg.value : null))
|
|
240
|
+
.filter(Boolean);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const functionNode = methodNode.value;
|
|
244
|
+
const paramNames = functionNode.params.map((p) => p.name);
|
|
245
|
+
|
|
246
|
+
if (containsArgumentsReference(functionNode)) {
|
|
247
|
+
return {
|
|
248
|
+
canAutoFix: false,
|
|
249
|
+
messageId: "cannotAutoFixArguments",
|
|
250
|
+
reportData: { name: discourseComputedLocalName },
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (paramNames.length === 0) {
|
|
255
|
+
return {
|
|
256
|
+
canAutoFix: true,
|
|
257
|
+
reportData: { name: discourseComputedLocalName },
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Use ESLint scope analysis to find all references to parameters
|
|
262
|
+
const scope = sourceCode.getScope(functionNode);
|
|
263
|
+
const parameterReassignmentInfo = {};
|
|
264
|
+
let hasParameterInSpread = false;
|
|
265
|
+
let spreadParam = null;
|
|
266
|
+
let hasUnsafeOptionalChaining = false;
|
|
267
|
+
let unsafeOptionalChainingParam = null;
|
|
268
|
+
let hasParameterInNestedFunction = false;
|
|
269
|
+
let nestedFunctionParam = null;
|
|
270
|
+
|
|
271
|
+
for (const variable of scope.variables) {
|
|
272
|
+
if (!paramNames.includes(variable.name) || variable.scope !== scope) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const paramIndex = paramNames.indexOf(variable.name);
|
|
277
|
+
const propertyPath = decoratorArgs[paramIndex] || variable.name;
|
|
278
|
+
|
|
279
|
+
for (const reference of variable.references) {
|
|
280
|
+
const refNode = reference.identifier;
|
|
281
|
+
const parent = refNode.parent;
|
|
282
|
+
|
|
283
|
+
// 1. Check for nested function usage (non-arrow)
|
|
284
|
+
// In ESLint scope, reference.from gives the scope where the reference occurs
|
|
285
|
+
let currentScope = reference.from;
|
|
286
|
+
while (currentScope && currentScope !== scope) {
|
|
287
|
+
if (
|
|
288
|
+
currentScope.type === "function" &&
|
|
289
|
+
currentScope.block.type !== "ArrowFunctionExpression"
|
|
290
|
+
) {
|
|
291
|
+
hasParameterInNestedFunction = true;
|
|
292
|
+
nestedFunctionParam = variable.name;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
currentScope = currentScope.upper;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 2. Check for reassignment
|
|
299
|
+
if (reference.isWrite()) {
|
|
300
|
+
if (!parameterReassignmentInfo[variable.name]) {
|
|
301
|
+
parameterReassignmentInfo[variable.name] = {
|
|
302
|
+
assignments: [],
|
|
303
|
+
hasUpdateExpression: false,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (parent.type === "UpdateExpression") {
|
|
308
|
+
parameterReassignmentInfo[variable.name].hasUpdateExpression = true;
|
|
309
|
+
} else if (parent.type === "AssignmentExpression") {
|
|
310
|
+
// Determine nesting depth for reassignment
|
|
311
|
+
let depth = 0;
|
|
312
|
+
let ancestor = parent.parent;
|
|
313
|
+
while (ancestor && ancestor !== functionNode.body) {
|
|
314
|
+
if (
|
|
315
|
+
/^(If|For|While|DoWhile|Switch|Try)Statement$/.test(
|
|
316
|
+
ancestor.type
|
|
317
|
+
)
|
|
318
|
+
) {
|
|
319
|
+
depth++;
|
|
320
|
+
}
|
|
321
|
+
ancestor = ancestor.parent;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
parameterReassignmentInfo[variable.name].assignments.push({
|
|
325
|
+
node: parent,
|
|
326
|
+
depth,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 3. Check for spread usage
|
|
332
|
+
let spreadCheck = parent;
|
|
333
|
+
while (spreadCheck && spreadCheck !== functionNode.body) {
|
|
334
|
+
if (spreadCheck.type === "SpreadElement") {
|
|
335
|
+
// Check if it's a "safe" spread pattern: ...(param || [])
|
|
336
|
+
const arg = spreadCheck.argument;
|
|
337
|
+
const isSafe =
|
|
338
|
+
arg.type === "LogicalExpression" &&
|
|
339
|
+
(arg.operator === "||" || arg.operator === "??") &&
|
|
340
|
+
arg.right.type === "ArrayExpression";
|
|
341
|
+
|
|
342
|
+
if (!isSafe) {
|
|
343
|
+
hasParameterInSpread = true;
|
|
344
|
+
spreadParam = variable.name;
|
|
345
|
+
}
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
spreadCheck = spreadCheck.parent;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 4. Check for unsafe optional chaining
|
|
352
|
+
const isNestedProperty =
|
|
353
|
+
propertyPath.includes(".") ||
|
|
354
|
+
propertyPath.includes("{") ||
|
|
355
|
+
propertyPath.includes("@") ||
|
|
356
|
+
propertyPath.includes("[");
|
|
357
|
+
|
|
358
|
+
if (isNestedProperty) {
|
|
359
|
+
let current = refNode;
|
|
360
|
+
let inUnsafeLogical = false;
|
|
361
|
+
|
|
362
|
+
while (
|
|
363
|
+
current.parent &&
|
|
364
|
+
(current.parent.type === "LogicalExpression" ||
|
|
365
|
+
current.parent.type === "ConditionalExpression" ||
|
|
366
|
+
current.parent.type === "MemberExpression")
|
|
367
|
+
) {
|
|
368
|
+
const p = current.parent;
|
|
369
|
+
|
|
370
|
+
if (p.type === "LogicalExpression") {
|
|
371
|
+
const isSafeFallback =
|
|
372
|
+
(p.operator === "||" || p.operator === "??") &&
|
|
373
|
+
p.right.type === "Literal";
|
|
374
|
+
if (!isSafeFallback) {
|
|
375
|
+
inUnsafeLogical = true;
|
|
376
|
+
}
|
|
377
|
+
} else if (p.type === "ConditionalExpression") {
|
|
378
|
+
inUnsafeLogical = true;
|
|
379
|
+
} else if (p.type === "MemberExpression") {
|
|
380
|
+
if (inUnsafeLogical && p.object === current) {
|
|
381
|
+
hasUnsafeOptionalChaining = true;
|
|
382
|
+
unsafeOptionalChainingParam = variable.name;
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
current = p;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const hasParameterReassignment =
|
|
393
|
+
Object.keys(parameterReassignmentInfo).length > 0;
|
|
394
|
+
|
|
395
|
+
const simpleReassignments = [];
|
|
396
|
+
if (
|
|
397
|
+
hasParameterReassignment &&
|
|
398
|
+
!hasParameterInSpread &&
|
|
399
|
+
functionNode.body.body &&
|
|
400
|
+
functionNode.body.body.length > 0
|
|
401
|
+
) {
|
|
402
|
+
for (let i = 0; i < functionNode.body.body.length; i++) {
|
|
403
|
+
const statement = functionNode.body.body[i];
|
|
404
|
+
|
|
405
|
+
// 1. Handle direct top-level assignment: foo = foo || [];
|
|
406
|
+
if (
|
|
407
|
+
statement.type === "ExpressionStatement" &&
|
|
408
|
+
statement.expression.type === "AssignmentExpression" &&
|
|
409
|
+
statement.expression.left.type === "Identifier" &&
|
|
410
|
+
paramNames.includes(statement.expression.left.name)
|
|
411
|
+
) {
|
|
412
|
+
const paramName = statement.expression.left.name;
|
|
413
|
+
const paramInfo = parameterReassignmentInfo[paramName];
|
|
414
|
+
if (paramInfo && !paramInfo.hasUpdateExpression) {
|
|
415
|
+
const firstAssignment = paramInfo.assignments[0];
|
|
416
|
+
if (
|
|
417
|
+
firstAssignment &&
|
|
418
|
+
firstAssignment.depth === 0 &&
|
|
419
|
+
firstAssignment.node === statement.expression
|
|
420
|
+
) {
|
|
421
|
+
simpleReassignments.push({
|
|
422
|
+
statement,
|
|
423
|
+
paramName,
|
|
424
|
+
info: paramInfo,
|
|
425
|
+
});
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// 2. Handle simple guard clause: if (!foo) { foo = []; }
|
|
432
|
+
if (
|
|
433
|
+
statement.type === "IfStatement" &&
|
|
434
|
+
statement.consequent.type === "BlockStatement" &&
|
|
435
|
+
statement.consequent.body.length === 1 &&
|
|
436
|
+
statement.consequent.body[0].type === "ExpressionStatement" &&
|
|
437
|
+
statement.consequent.body[0].expression.type ===
|
|
438
|
+
"AssignmentExpression" &&
|
|
439
|
+
statement.consequent.body[0].expression.left.type === "Identifier" &&
|
|
440
|
+
paramNames.includes(statement.consequent.body[0].expression.left.name)
|
|
441
|
+
) {
|
|
442
|
+
// Check if the test is a "null check"
|
|
443
|
+
const test = statement.test;
|
|
444
|
+
const isUnaryNegation =
|
|
445
|
+
test.type === "UnaryExpression" &&
|
|
446
|
+
test.operator === "!" &&
|
|
447
|
+
test.argument.type === "Identifier" &&
|
|
448
|
+
paramNames.includes(test.argument.name);
|
|
449
|
+
|
|
450
|
+
const isBinaryNullCheck =
|
|
451
|
+
test.type === "BinaryExpression" &&
|
|
452
|
+
(test.operator === "==" ||
|
|
453
|
+
test.operator === "===" ||
|
|
454
|
+
test.operator === "!=" ||
|
|
455
|
+
test.operator === "!==") &&
|
|
456
|
+
((test.left.type === "Identifier" &&
|
|
457
|
+
paramNames.includes(test.left.name) &&
|
|
458
|
+
(test.right.type === "Literal" ||
|
|
459
|
+
(test.right.type === "Identifier" &&
|
|
460
|
+
test.right.name === "undefined"))) ||
|
|
461
|
+
(test.right.type === "Identifier" &&
|
|
462
|
+
paramNames.includes(test.right.name) &&
|
|
463
|
+
(test.left.type === "Literal" ||
|
|
464
|
+
(test.left.type === "Identifier" &&
|
|
465
|
+
test.left.name === "undefined"))));
|
|
466
|
+
|
|
467
|
+
if (isUnaryNegation || isBinaryNullCheck) {
|
|
468
|
+
const assignment = statement.consequent.body[0].expression;
|
|
469
|
+
const paramName = assignment.left.name;
|
|
470
|
+
const paramInfo = parameterReassignmentInfo[paramName];
|
|
471
|
+
|
|
472
|
+
if (
|
|
473
|
+
paramInfo &&
|
|
474
|
+
!paramInfo.hasUpdateExpression &&
|
|
475
|
+
paramInfo.assignments.length === 1 &&
|
|
476
|
+
paramInfo.assignments[0].depth === 1 &&
|
|
477
|
+
paramInfo.assignments[0].node === assignment
|
|
478
|
+
) {
|
|
479
|
+
simpleReassignments.push({
|
|
480
|
+
statement,
|
|
481
|
+
paramName,
|
|
482
|
+
info: paramInfo,
|
|
483
|
+
isGuard: true,
|
|
484
|
+
});
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const reportData = { name: discourseComputedLocalName };
|
|
495
|
+
const hasSimpleReassignments = simpleReassignments.length > 0;
|
|
496
|
+
|
|
497
|
+
if (hasParameterInNestedFunction) {
|
|
498
|
+
const idx = paramNames.indexOf(nestedFunctionParam);
|
|
499
|
+
return {
|
|
500
|
+
canAutoFix: false,
|
|
501
|
+
messageId: "cannotAutoFixNestedFunction",
|
|
502
|
+
reportData: {
|
|
503
|
+
...reportData,
|
|
504
|
+
param: nestedFunctionParam,
|
|
505
|
+
propertyPath: decoratorArgs[idx] || nestedFunctionParam,
|
|
506
|
+
},
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (hasUnsafeOptionalChaining) {
|
|
511
|
+
const idx = paramNames.indexOf(unsafeOptionalChainingParam);
|
|
512
|
+
return {
|
|
513
|
+
canAutoFix: false,
|
|
514
|
+
messageId: "cannotAutoFixUnsafeOptionalChaining",
|
|
515
|
+
reportData: {
|
|
516
|
+
...reportData,
|
|
517
|
+
param: unsafeOptionalChainingParam,
|
|
518
|
+
propertyPath: decoratorArgs[idx] || unsafeOptionalChainingParam,
|
|
519
|
+
},
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (hasParameterInSpread) {
|
|
524
|
+
const idx = paramNames.indexOf(spreadParam);
|
|
525
|
+
return {
|
|
526
|
+
canAutoFix: false,
|
|
527
|
+
messageId: "cannotAutoFixSpread",
|
|
528
|
+
reportData: {
|
|
529
|
+
...reportData,
|
|
530
|
+
param: spreadParam,
|
|
531
|
+
propertyPath: decoratorArgs[idx] || spreadParam,
|
|
532
|
+
},
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (hasParameterReassignment && !hasSimpleReassignments) {
|
|
537
|
+
const reassignedParam = Object.keys(parameterReassignmentInfo)[0];
|
|
538
|
+
const reassignedInfo = parameterReassignmentInfo[reassignedParam] || {};
|
|
539
|
+
const idx = paramNames.indexOf(reassignedParam);
|
|
540
|
+
const propertyPath = decoratorArgs[idx] || reassignedParam;
|
|
541
|
+
|
|
542
|
+
let messageId = "cannotAutoFixGeneric";
|
|
543
|
+
if (reassignedInfo.hasUpdateExpression) {
|
|
544
|
+
messageId = "cannotAutoFixUpdateExpression";
|
|
545
|
+
} else if (
|
|
546
|
+
reassignedInfo.assignments &&
|
|
547
|
+
reassignedInfo.assignments.length > 0 &&
|
|
548
|
+
reassignedInfo.assignments[0].depth > 0
|
|
549
|
+
) {
|
|
550
|
+
messageId = "cannotAutoFixNestedReassignment";
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return {
|
|
554
|
+
canAutoFix: false,
|
|
555
|
+
messageId,
|
|
556
|
+
reportData: { ...reportData, param: reassignedParam, propertyPath },
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return {
|
|
561
|
+
canAutoFix: true,
|
|
562
|
+
simpleReassignments,
|
|
563
|
+
reportData: { name: discourseComputedLocalName },
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function analyzeObjectExpression(objExpr, infoObj) {
|
|
568
|
+
if (!objExpr || !objExpr.properties) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
objExpr.properties.forEach((prop) => {
|
|
573
|
+
if (
|
|
574
|
+
prop.type === "Property" &&
|
|
575
|
+
prop.decorators &&
|
|
576
|
+
prop.decorators.length > 0
|
|
577
|
+
) {
|
|
578
|
+
const discourseDecorator = prop.decorators.find((decorator) => {
|
|
579
|
+
const expr = decorator.expression;
|
|
580
|
+
if (expr.type === "CallExpression") {
|
|
581
|
+
return expr.callee.name === discourseComputedLocalName;
|
|
582
|
+
}
|
|
583
|
+
return expr.name === discourseComputedLocalName;
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
if (discourseDecorator) {
|
|
587
|
+
infoObj.hasClassicClassDecorators = true;
|
|
588
|
+
infoObj.usageMap.set(discourseDecorator, {
|
|
589
|
+
canAutoFix: false,
|
|
590
|
+
isClassic: true,
|
|
591
|
+
messageId: "cannotAutoFixClassic",
|
|
592
|
+
reportData: { name: discourseComputedLocalName },
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (
|
|
598
|
+
prop.type === "Property" &&
|
|
599
|
+
prop.value &&
|
|
600
|
+
prop.value.type === "CallExpression" &&
|
|
601
|
+
prop.value.callee &&
|
|
602
|
+
prop.value.callee.name === discourseComputedLocalName
|
|
603
|
+
) {
|
|
604
|
+
infoObj.hasClassicClassDecorators = true;
|
|
605
|
+
infoObj.usageMap.set(prop.value, {
|
|
606
|
+
canAutoFix: false,
|
|
607
|
+
isClassic: true,
|
|
608
|
+
messageId: "cannotAutoFixClassic",
|
|
609
|
+
reportData: { name: discourseComputedLocalName },
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|