eslint-plugin-react-x 5.14.1 → 5.14.5
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/dist/index.js +303 -202
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -5,9 +5,9 @@ import * as core from "@eslint-react/core";
|
|
|
5
5
|
import { merge } from "@eslint-react/eslint";
|
|
6
6
|
import { JsxDetectionHint, findParentAttribute, getElementFullType, hasAttribute } from "@eslint-react/jsx";
|
|
7
7
|
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
8
|
+
import { computeObjectType, isAssignmentTargetEqual, isInitializedFromReact, resolve, resolveEnclosingAssignmentTarget } from "@eslint-react/var";
|
|
8
9
|
import { DefinitionType, ScopeType } from "@typescript-eslint/scope-manager";
|
|
9
10
|
import { findVariable, getStaticValue } from "@typescript-eslint/utils/ast-utils";
|
|
10
|
-
import { computeObjectType, isAssignmentTargetEqual, isInitializedFromReact, resolve, resolveEnclosingAssignmentTarget } from "@eslint-react/var";
|
|
11
11
|
import { P, isMatching, match } from "ts-pattern";
|
|
12
12
|
import { compare } from "compare-versions";
|
|
13
13
|
import { getConstrainedTypeAtLocation } from "@typescript-eslint/type-utils";
|
|
@@ -144,7 +144,7 @@ const rules$6 = {
|
|
|
144
144
|
//#endregion
|
|
145
145
|
//#region package.json
|
|
146
146
|
var name$6 = "eslint-plugin-react-x";
|
|
147
|
-
var version = "5.14.
|
|
147
|
+
var version = "5.14.5";
|
|
148
148
|
|
|
149
149
|
//#endregion
|
|
150
150
|
//#region src/utils/create-rule.ts
|
|
@@ -1157,6 +1157,67 @@ const MUTATING_ARRAY_METHODS = /* @__PURE__ */ new Set([
|
|
|
1157
1157
|
"splice",
|
|
1158
1158
|
"unshift"
|
|
1159
1159
|
]);
|
|
1160
|
+
/**
|
|
1161
|
+
* Return whether an identifier is an unresolved global or is declared in the
|
|
1162
|
+
* global/module scope.
|
|
1163
|
+
*/
|
|
1164
|
+
function isGlobalVariable(context, node) {
|
|
1165
|
+
const variable = findVariable(context.sourceCode.getScope(node), node);
|
|
1166
|
+
if (variable == null || variable.defs.length === 0) return true;
|
|
1167
|
+
return variable.scope.type === ScopeType.global || variable.scope.type === ScopeType.module;
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Resolve an object expression to the global/module binding it aliases.
|
|
1171
|
+
*
|
|
1172
|
+
* This intentionally follows only stable `const alias = value` declarations.
|
|
1173
|
+
* It gives the rule useful Alias effects without pretending to perform full
|
|
1174
|
+
* control-flow analysis for reassigned locals.
|
|
1175
|
+
*/
|
|
1176
|
+
function resolveGlobalOrigin(context, node, seen = /* @__PURE__ */ new Set()) {
|
|
1177
|
+
const expression = Extract.unwrap(node);
|
|
1178
|
+
if (seen.has(expression)) return null;
|
|
1179
|
+
seen.add(expression);
|
|
1180
|
+
if (expression.type === AST_NODE_TYPES.MemberExpression) return resolveGlobalOrigin(context, expression.object, seen);
|
|
1181
|
+
if (expression.type !== AST_NODE_TYPES.Identifier) return null;
|
|
1182
|
+
if (isGlobalVariable(context, expression)) return expression;
|
|
1183
|
+
const variable = findVariable(context.sourceCode.getScope(expression), expression);
|
|
1184
|
+
const definition = variable?.defs.length === 1 ? variable.defs[0] : null;
|
|
1185
|
+
if (definition?.type !== DefinitionType.Variable) return null;
|
|
1186
|
+
if (definition.node.id.type !== AST_NODE_TYPES.Identifier || definition.node.init == null) return null;
|
|
1187
|
+
if (definition.node.parent.kind !== "const") return null;
|
|
1188
|
+
const initializer = Extract.unwrap(definition.node.init);
|
|
1189
|
+
if (initializer.type !== AST_NODE_TYPES.Identifier && initializer.type !== AST_NODE_TYPES.MemberExpression) return null;
|
|
1190
|
+
return resolveGlobalOrigin(context, initializer, seen);
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Collect every write target in an assignment, including destructuring
|
|
1194
|
+
* patterns such as `[local, globalValue] = source`.
|
|
1195
|
+
*/
|
|
1196
|
+
function getAssignmentTargets(node) {
|
|
1197
|
+
const target = Extract.unwrap(node);
|
|
1198
|
+
switch (target.type) {
|
|
1199
|
+
case AST_NODE_TYPES.Identifier:
|
|
1200
|
+
case AST_NODE_TYPES.MemberExpression: return [target];
|
|
1201
|
+
case AST_NODE_TYPES.ArrayPattern: return target.elements.flatMap((element) => element == null ? [] : getAssignmentTargets(element));
|
|
1202
|
+
case AST_NODE_TYPES.AssignmentPattern: return getAssignmentTargets(target.left);
|
|
1203
|
+
case AST_NODE_TYPES.ObjectPattern: return target.properties.flatMap((property) => {
|
|
1204
|
+
if (property.type === AST_NODE_TYPES.RestElement) return getAssignmentTargets(property.argument);
|
|
1205
|
+
return getAssignmentTargets(property.value);
|
|
1206
|
+
});
|
|
1207
|
+
case AST_NODE_TYPES.RestElement: return getAssignmentTargets(target.argument);
|
|
1208
|
+
default: return [];
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
/** Resolve a direct call target, following simple function aliases. */
|
|
1212
|
+
function resolveToFunction(context, node, seen = /* @__PURE__ */ new Set()) {
|
|
1213
|
+
const expression = Extract.unwrap(node);
|
|
1214
|
+
if (Check.isFunction(expression)) return expression;
|
|
1215
|
+
if (expression.type !== AST_NODE_TYPES.Identifier || seen.has(expression)) return null;
|
|
1216
|
+
seen.add(expression);
|
|
1217
|
+
const resolved = resolve(context, expression);
|
|
1218
|
+
if (resolved == null) return null;
|
|
1219
|
+
return resolveToFunction(context, resolved, seen);
|
|
1220
|
+
}
|
|
1160
1221
|
|
|
1161
1222
|
//#endregion
|
|
1162
1223
|
//#region src/rules/globals/globals.ts
|
|
@@ -1179,101 +1240,83 @@ var globals_default = createRule({
|
|
|
1179
1240
|
function create$49(context) {
|
|
1180
1241
|
const hc = core.getHookCollector(context);
|
|
1181
1242
|
const fc = core.getFunctionComponentCollector(context);
|
|
1182
|
-
const
|
|
1183
|
-
|
|
1184
|
-
* Return true when `id` refers to a variable defined in the global or module
|
|
1185
|
-
* scope (i.e. outside any function), or when it has no known definition.
|
|
1186
|
-
*/
|
|
1187
|
-
function isGlobalOrModuleVariable(id) {
|
|
1188
|
-
const variable = findVariable(context.sourceCode.getScope(id), id);
|
|
1189
|
-
if (variable == null) return true;
|
|
1190
|
-
if (variable.defs.length === 0) return true;
|
|
1191
|
-
const scopeType = variable.scope.type;
|
|
1192
|
-
return scopeType === ScopeType.global || scopeType === ScopeType.module;
|
|
1193
|
-
}
|
|
1194
|
-
/**
|
|
1195
|
-
* Return the nearest enclosing function for `node`.
|
|
1196
|
-
*/
|
|
1243
|
+
const directEffects = /* @__PURE__ */ new Map();
|
|
1244
|
+
const callGraph = /* @__PURE__ */ new Map();
|
|
1197
1245
|
function getEnclosingFunction(node) {
|
|
1198
1246
|
return Traverse.findParent(node, Check.isFunction);
|
|
1199
1247
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
if (func == null) return;
|
|
1206
|
-
violations.push({
|
|
1248
|
+
function recordEffect(node, messageId, data) {
|
|
1249
|
+
const enclosing = getEnclosingFunction(node);
|
|
1250
|
+
if (enclosing == null) return;
|
|
1251
|
+
const effects = directEffects.get(enclosing) ?? [];
|
|
1252
|
+
effects.push({
|
|
1207
1253
|
data,
|
|
1208
|
-
func,
|
|
1209
1254
|
messageId,
|
|
1210
1255
|
node
|
|
1211
1256
|
});
|
|
1257
|
+
directEffects.set(enclosing, effects);
|
|
1258
|
+
}
|
|
1259
|
+
function recordWrite(node, target) {
|
|
1260
|
+
if (target.type === AST_NODE_TYPES.Identifier) {
|
|
1261
|
+
if (!isGlobalVariable(context, target)) return;
|
|
1262
|
+
recordEffect(node, "mutatingGlobal", { name: target.name });
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
if (resolveGlobalOrigin(context, target.object) == null) return;
|
|
1266
|
+
recordEffect(node, "mutatingGlobalProperty", { name: context.sourceCode.getText(target) });
|
|
1267
|
+
}
|
|
1268
|
+
function recordCallEdge(node) {
|
|
1269
|
+
const caller = getEnclosingFunction(node);
|
|
1270
|
+
if (caller == null) return;
|
|
1271
|
+
const callee = resolveToFunction(context, node.callee);
|
|
1272
|
+
if (callee == null) return;
|
|
1273
|
+
const callees = callGraph.get(caller) ?? /* @__PURE__ */ new Set();
|
|
1274
|
+
callees.add(callee);
|
|
1275
|
+
callGraph.set(caller, callees);
|
|
1212
1276
|
}
|
|
1213
1277
|
return merge(hc.visitor, fc.visitor, {
|
|
1214
|
-
/**
|
|
1215
|
-
* Detect `renderCount++`, `renderCount--`
|
|
1216
|
-
*/
|
|
1217
|
-
UpdateExpression(node) {
|
|
1218
|
-
const arg = Extract.unwrap(node.argument);
|
|
1219
|
-
if (arg.type === AST_NODE_TYPES.Identifier) {
|
|
1220
|
-
if (!isGlobalOrModuleVariable(arg)) return;
|
|
1221
|
-
recordViolation(node, "mutatingGlobal", { name: arg.name });
|
|
1222
|
-
return;
|
|
1223
|
-
}
|
|
1224
|
-
if (arg.type === AST_NODE_TYPES.MemberExpression) {
|
|
1225
|
-
const rootId = Extract.getRootIdentifier(arg);
|
|
1226
|
-
if (rootId == null) return;
|
|
1227
|
-
if (!isGlobalOrModuleVariable(rootId)) return;
|
|
1228
|
-
recordViolation(node, "mutatingGlobalProperty", { name: context.sourceCode.getText(arg) });
|
|
1229
|
-
}
|
|
1230
|
-
},
|
|
1231
|
-
/**
|
|
1232
|
-
* Detect `renderCount = 1`, `window.currentUser = id`, `cache[id] = value`
|
|
1233
|
-
*/
|
|
1234
1278
|
AssignmentExpression(node) {
|
|
1235
|
-
const
|
|
1236
|
-
if (left.type === AST_NODE_TYPES.Identifier) {
|
|
1237
|
-
if (!isGlobalOrModuleVariable(left)) return;
|
|
1238
|
-
recordViolation(node, "mutatingGlobal", { name: left.name });
|
|
1239
|
-
return;
|
|
1240
|
-
}
|
|
1241
|
-
if (left.type === AST_NODE_TYPES.MemberExpression) {
|
|
1242
|
-
const rootId = Extract.getRootIdentifier(left);
|
|
1243
|
-
if (rootId == null) return;
|
|
1244
|
-
if (!isGlobalOrModuleVariable(rootId)) return;
|
|
1245
|
-
recordViolation(node, "mutatingGlobalProperty", { name: context.sourceCode.getText(left) });
|
|
1246
|
-
}
|
|
1279
|
+
for (const target of getAssignmentTargets(node.left)) recordWrite(node, target);
|
|
1247
1280
|
},
|
|
1248
|
-
/**
|
|
1249
|
-
* Detect `events.push(event)`, `cache.sort()`, etc.
|
|
1250
|
-
*/
|
|
1251
1281
|
CallExpression(node) {
|
|
1282
|
+
recordCallEdge(node);
|
|
1252
1283
|
const callee = Extract.unwrap(node.callee);
|
|
1253
1284
|
if (callee.type !== AST_NODE_TYPES.MemberExpression) return;
|
|
1254
|
-
const
|
|
1255
|
-
if (
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
name: rootId.name,
|
|
1262
|
-
method: property.name
|
|
1285
|
+
const method = Extract.getPropertyName(callee.property);
|
|
1286
|
+
if (method == null || !MUTATING_ARRAY_METHODS.has(method)) return;
|
|
1287
|
+
const origin = resolveGlobalOrigin(context, callee.object);
|
|
1288
|
+
if (origin == null) return;
|
|
1289
|
+
recordEffect(node, "mutatingGlobalArrayMethod", {
|
|
1290
|
+
name: origin.name,
|
|
1291
|
+
method
|
|
1263
1292
|
});
|
|
1264
1293
|
},
|
|
1265
|
-
"Program:exit"(
|
|
1266
|
-
const
|
|
1267
|
-
const
|
|
1268
|
-
const
|
|
1269
|
-
|
|
1270
|
-
if (
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1294
|
+
"Program:exit"(program) {
|
|
1295
|
+
const renderFunctions = [...fc.api.getAllComponents(program), ...hc.api.getAllHooks(program)];
|
|
1296
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1297
|
+
const reported = /* @__PURE__ */ new Set();
|
|
1298
|
+
function applyFunctionEffects(func) {
|
|
1299
|
+
if (visited.has(func)) return;
|
|
1300
|
+
visited.add(func);
|
|
1301
|
+
for (const effect of directEffects.get(func) ?? []) {
|
|
1302
|
+
if (reported.has(effect)) continue;
|
|
1303
|
+
reported.add(effect);
|
|
1304
|
+
context.report(effect);
|
|
1305
|
+
}
|
|
1306
|
+
for (const callee of callGraph.get(func) ?? []) applyFunctionEffects(callee);
|
|
1276
1307
|
}
|
|
1308
|
+
for (const { node } of renderFunctions) applyFunctionEffects(node);
|
|
1309
|
+
},
|
|
1310
|
+
UnaryExpression(node) {
|
|
1311
|
+
if (node.operator !== "delete") return;
|
|
1312
|
+
const argument = Extract.unwrap(node.argument);
|
|
1313
|
+
if (argument.type !== AST_NODE_TYPES.MemberExpression) return;
|
|
1314
|
+
recordWrite(node, argument);
|
|
1315
|
+
},
|
|
1316
|
+
UpdateExpression(node) {
|
|
1317
|
+
const argument = Extract.unwrap(node.argument);
|
|
1318
|
+
if (argument.type !== AST_NODE_TYPES.Identifier && argument.type !== AST_NODE_TYPES.MemberExpression) return;
|
|
1319
|
+
recordWrite(node, argument);
|
|
1277
1320
|
}
|
|
1278
1321
|
});
|
|
1279
1322
|
}
|
|
@@ -1327,6 +1370,24 @@ function resolveToFunctionNode(context, node, seen = /* @__PURE__ */ new Set())
|
|
|
1327
1370
|
return resolveToFunctionNode(context, resolved, seen);
|
|
1328
1371
|
}
|
|
1329
1372
|
/**
|
|
1373
|
+
* Follow identifier-only variable-declarator aliases to their originating
|
|
1374
|
+
* binding. Assignment aliases are intentionally left for the value-flow phase.
|
|
1375
|
+
* @param context The ESLint rule context.
|
|
1376
|
+
* @param variable The binding whose initializer aliases should be followed.
|
|
1377
|
+
* @param seen Bindings already visited, to guard against cycles.
|
|
1378
|
+
*/
|
|
1379
|
+
function resolveVariableOrigin(context, variable, seen = /* @__PURE__ */ new Set()) {
|
|
1380
|
+
if (seen.has(variable)) return variable;
|
|
1381
|
+
seen.add(variable);
|
|
1382
|
+
const definition = variable.defs.length === 1 ? variable.defs[0] : null;
|
|
1383
|
+
if (definition?.type !== DefinitionType.Variable || definition.node.init == null) return variable;
|
|
1384
|
+
const initializer = Extract.unwrap(definition.node.init);
|
|
1385
|
+
if (initializer.type !== AST_NODE_TYPES.Identifier) return variable;
|
|
1386
|
+
const source = findVariable(context.sourceCode.getScope(initializer), initializer);
|
|
1387
|
+
if (source == null) return variable;
|
|
1388
|
+
return resolveVariableOrigin(context, source, seen);
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1330
1391
|
* Check if a name is ref-like ("ref" or ends with "Ref").
|
|
1331
1392
|
* Refs are mutable by design and exempted from immutability checks.
|
|
1332
1393
|
* @param name The identifier name to check.
|
|
@@ -1349,20 +1410,28 @@ function hasRefLikeNameInChain(node) {
|
|
|
1349
1410
|
return false;
|
|
1350
1411
|
}
|
|
1351
1412
|
/**
|
|
1352
|
-
* Check if the root identifier of a member-expression chain
|
|
1353
|
-
*
|
|
1354
|
-
* `const mounted = useRef(false); mounted.current = true;`.
|
|
1355
|
-
*
|
|
1356
|
-
* This catches refs regardless of naming convention, complementing
|
|
1357
|
-
* {@link hasRefLikeNameInChain}.
|
|
1413
|
+
* Check if the root identifier of a member-expression chain is initialized
|
|
1414
|
+
* from a `useRef()` call, following variable-declarator aliases.
|
|
1358
1415
|
* @param context The ESLint rule context.
|
|
1359
1416
|
* @param node The AST node to inspect (an identifier or member-expression chain).
|
|
1360
1417
|
*/
|
|
1361
1418
|
function isInitializedFromUseRef(context, node) {
|
|
1362
|
-
const
|
|
1363
|
-
if (
|
|
1364
|
-
const
|
|
1365
|
-
|
|
1419
|
+
const root = node.type === AST_NODE_TYPES.Identifier ? node : Extract.getRootIdentifier(node);
|
|
1420
|
+
if (root == null) return false;
|
|
1421
|
+
const variable = findVariable(context.sourceCode.getScope(root), root);
|
|
1422
|
+
if (variable == null) return false;
|
|
1423
|
+
return isVariableInitializedFromUseRef(context, variable, /* @__PURE__ */ new Set());
|
|
1424
|
+
}
|
|
1425
|
+
function isVariableInitializedFromUseRef(context, variable, seen) {
|
|
1426
|
+
if (seen.has(variable)) return false;
|
|
1427
|
+
seen.add(variable);
|
|
1428
|
+
const definition = variable.defs.length === 1 ? variable.defs[0] : null;
|
|
1429
|
+
if (definition?.type !== DefinitionType.Variable || definition.node.init == null) return false;
|
|
1430
|
+
const initializer = Extract.unwrap(definition.node.init);
|
|
1431
|
+
if (initializer.type === AST_NODE_TYPES.CallExpression) return core.isUseRefCall(context, initializer);
|
|
1432
|
+
if (initializer.type !== AST_NODE_TYPES.Identifier) return false;
|
|
1433
|
+
const source = findVariable(context.sourceCode.getScope(initializer), initializer);
|
|
1434
|
+
return source != null && isVariableInitializedFromUseRef(context, source, seen);
|
|
1366
1435
|
}
|
|
1367
1436
|
/**
|
|
1368
1437
|
* Check if a mutated expression chain should be exempt from immutability
|
|
@@ -1376,6 +1445,122 @@ function isRefLikeChain(context, node) {
|
|
|
1376
1445
|
return hasRefLikeNameInChain(node) || isInitializedFromUseRef(context, node);
|
|
1377
1446
|
}
|
|
1378
1447
|
|
|
1448
|
+
//#endregion
|
|
1449
|
+
//#region src/rules/immutability/collect.ts
|
|
1450
|
+
function createImmutabilityCollector() {
|
|
1451
|
+
const facts = {
|
|
1452
|
+
mutations: [],
|
|
1453
|
+
sinks: []
|
|
1454
|
+
};
|
|
1455
|
+
function pushMutation(kind, node, target, root) {
|
|
1456
|
+
const enclosingFunction = Traverse.findParent(node, Check.isFunction);
|
|
1457
|
+
if (enclosingFunction == null) return;
|
|
1458
|
+
facts.mutations.push({
|
|
1459
|
+
kind,
|
|
1460
|
+
enclosingFunction,
|
|
1461
|
+
node,
|
|
1462
|
+
root,
|
|
1463
|
+
target
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
return {
|
|
1467
|
+
facts,
|
|
1468
|
+
visitor: {
|
|
1469
|
+
AssignmentExpression(node) {
|
|
1470
|
+
const target = Extract.unwrap(node.left);
|
|
1471
|
+
switch (target.type) {
|
|
1472
|
+
case AST_NODE_TYPES.Identifier:
|
|
1473
|
+
pushMutation("binding", node, target, target);
|
|
1474
|
+
return;
|
|
1475
|
+
case AST_NODE_TYPES.MemberExpression: {
|
|
1476
|
+
const root = Extract.getRootIdentifier(target);
|
|
1477
|
+
if (root != null) pushMutation("value", node, target, root);
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
},
|
|
1482
|
+
CallExpression(node) {
|
|
1483
|
+
const callee = Extract.unwrap(node.callee);
|
|
1484
|
+
if (callee.type === AST_NODE_TYPES.MemberExpression) {
|
|
1485
|
+
const property = Extract.getPropertyName(callee.property);
|
|
1486
|
+
if (property != null && MUTATING_METHODS.has(property)) {
|
|
1487
|
+
const root = Extract.getRootIdentifier(callee.object);
|
|
1488
|
+
if (root != null) pushMutation("value", node, callee.object, root);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
if (!core.isHookCall(node)) return;
|
|
1492
|
+
for (const argument of node.arguments) {
|
|
1493
|
+
if (argument.type === AST_NODE_TYPES.SpreadElement) continue;
|
|
1494
|
+
facts.sinks.push({
|
|
1495
|
+
kind: "hook-argument",
|
|
1496
|
+
expression: argument
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
},
|
|
1500
|
+
JSXAttribute(node) {
|
|
1501
|
+
if (node.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) return;
|
|
1502
|
+
const expression = node.value.expression;
|
|
1503
|
+
if (expression.type === AST_NODE_TYPES.JSXEmptyExpression) return;
|
|
1504
|
+
facts.sinks.push({
|
|
1505
|
+
kind: "jsx-prop",
|
|
1506
|
+
expression
|
|
1507
|
+
});
|
|
1508
|
+
},
|
|
1509
|
+
UnaryExpression(node) {
|
|
1510
|
+
if (node.operator !== "delete") return;
|
|
1511
|
+
const target = Extract.unwrap(node.argument);
|
|
1512
|
+
if (target.type !== AST_NODE_TYPES.MemberExpression) return;
|
|
1513
|
+
const root = Extract.getRootIdentifier(target);
|
|
1514
|
+
if (root != null) pushMutation("value", node, target, root);
|
|
1515
|
+
},
|
|
1516
|
+
UpdateExpression(node) {
|
|
1517
|
+
const target = Extract.unwrap(node.argument);
|
|
1518
|
+
switch (target.type) {
|
|
1519
|
+
case AST_NODE_TYPES.Identifier:
|
|
1520
|
+
pushMutation("binding", node, target, target);
|
|
1521
|
+
return;
|
|
1522
|
+
case AST_NODE_TYPES.MemberExpression: {
|
|
1523
|
+
const root = Extract.getRootIdentifier(target);
|
|
1524
|
+
if (root != null) pushMutation("value", node, target, root);
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
//#endregion
|
|
1534
|
+
//#region src/rules/immutability/effects.ts
|
|
1535
|
+
function isGlobalOrModuleVariable(variable) {
|
|
1536
|
+
return variable.defs.length === 0 || variable.scope.type === ScopeType.global || variable.scope.type === ScopeType.module;
|
|
1537
|
+
}
|
|
1538
|
+
function isRefMutation(context, mutation) {
|
|
1539
|
+
if (mutation.target.type === AST_NODE_TYPES.Identifier) return isRefLikeName$1(mutation.target.name);
|
|
1540
|
+
return isRefLikeChain(context, mutation.target);
|
|
1541
|
+
}
|
|
1542
|
+
function inferMutableFunctions(context, mutations) {
|
|
1543
|
+
const mutableFunctions = /* @__PURE__ */ new Map();
|
|
1544
|
+
for (const mutation of mutations) {
|
|
1545
|
+
if (isRefMutation(context, mutation)) continue;
|
|
1546
|
+
const variable = findVariable(context.sourceCode.getScope(mutation.root), mutation.root);
|
|
1547
|
+
if (variable == null) continue;
|
|
1548
|
+
const origin = mutation.kind === "binding" ? variable : resolveVariableOrigin(context, variable);
|
|
1549
|
+
if (isGlobalOrModuleVariable(origin)) continue;
|
|
1550
|
+
const declaration = origin.identifiers.at(0) ?? null;
|
|
1551
|
+
let current = mutation.enclosingFunction;
|
|
1552
|
+
while (current != null) {
|
|
1553
|
+
if (declaration != null && isNodeWithin(declaration, current)) break;
|
|
1554
|
+
if (!mutableFunctions.has(current)) mutableFunctions.set(current, {
|
|
1555
|
+
name: origin.name,
|
|
1556
|
+
node: mutation.node
|
|
1557
|
+
});
|
|
1558
|
+
current = Traverse.findParent(current, Check.isFunction);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
return mutableFunctions;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1379
1564
|
//#endregion
|
|
1380
1565
|
//#region src/rules/immutability/immutability.ts
|
|
1381
1566
|
const RULE_NAME$48 = "immutability";
|
|
@@ -1394,120 +1579,36 @@ var immutability_default = createRule({
|
|
|
1394
1579
|
defaultOptions: []
|
|
1395
1580
|
});
|
|
1396
1581
|
function create$48(context) {
|
|
1397
|
-
const
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
mutationSites.push({
|
|
1404
|
-
enclosing,
|
|
1405
|
-
node,
|
|
1406
|
-
root
|
|
1582
|
+
const hooks = core.getHookCollector(context);
|
|
1583
|
+
const collector = createImmutabilityCollector();
|
|
1584
|
+
return merge(hooks.visitor, collector.visitor, { "Program:exit"(program) {
|
|
1585
|
+
for (const hook of hooks.api.getAllHooks(program)) for (const expression of hook.rets) if (expression != null) collector.facts.sinks.push({
|
|
1586
|
+
kind: "hook-return",
|
|
1587
|
+
expression
|
|
1407
1588
|
});
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
const propName = Extract.getPropertyName(callee.property);
|
|
1430
|
-
if (propName != null && MUTATING_METHODS.has(propName) && !isRefLikeChain(context, callee.object)) {
|
|
1431
|
-
const rootId = Extract.getRootIdentifier(callee.object);
|
|
1432
|
-
if (rootId != null) pushMutationSite(node, rootId);
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
if (core.isHookCall(node)) for (const arg of node.arguments) {
|
|
1436
|
-
if (arg.type === AST_NODE_TYPES.SpreadElement) continue;
|
|
1437
|
-
sinkCandidates.push(arg);
|
|
1438
|
-
}
|
|
1439
|
-
},
|
|
1440
|
-
JSXAttribute(node) {
|
|
1441
|
-
if (node.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) return;
|
|
1442
|
-
const expr = node.value.expression;
|
|
1443
|
-
if (expr.type === AST_NODE_TYPES.JSXEmptyExpression) return;
|
|
1444
|
-
sinkCandidates.push(expr);
|
|
1445
|
-
},
|
|
1446
|
-
"Program:exit"(program) {
|
|
1447
|
-
const mutableFunctions = /* @__PURE__ */ new Map();
|
|
1448
|
-
for (const { enclosing, node, root } of mutationSites) {
|
|
1449
|
-
const variable = findVariable(context.sourceCode.getScope(root), root);
|
|
1450
|
-
if (variable == null) continue;
|
|
1451
|
-
const declId = variable.identifiers.at(0) ?? null;
|
|
1452
|
-
let current = enclosing;
|
|
1453
|
-
while (current != null) {
|
|
1454
|
-
if (declId != null && isNodeWithin(declId, current)) break;
|
|
1455
|
-
if (!mutableFunctions.has(current)) mutableFunctions.set(current, {
|
|
1456
|
-
name: variable.name,
|
|
1457
|
-
node
|
|
1458
|
-
});
|
|
1459
|
-
current = Traverse.findParent(current, Check.isFunction);
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
if (mutableFunctions.size === 0) return;
|
|
1463
|
-
const reported = /* @__PURE__ */ new Set();
|
|
1464
|
-
function checkSink(expr) {
|
|
1465
|
-
if (expr == null || reported.has(expr)) return;
|
|
1466
|
-
const fn = resolveToFunctionNode(context, expr);
|
|
1467
|
-
if (fn == null) return;
|
|
1468
|
-
const mutation = mutableFunctions.get(fn);
|
|
1469
|
-
if (mutation == null) return;
|
|
1470
|
-
reported.add(expr);
|
|
1471
|
-
context.report({
|
|
1472
|
-
data: { name: mutation.name },
|
|
1473
|
-
messageId: "default",
|
|
1474
|
-
node: expr
|
|
1475
|
-
});
|
|
1476
|
-
context.report({
|
|
1477
|
-
data: { name: mutation.name },
|
|
1478
|
-
messageId: "mutates",
|
|
1479
|
-
node: mutation.node
|
|
1480
|
-
});
|
|
1481
|
-
}
|
|
1482
|
-
for (const candidate of sinkCandidates) checkSink(candidate);
|
|
1483
|
-
for (const hook of hc.api.getAllHooks(program)) for (const ret of hook.rets) checkSink(ret);
|
|
1484
|
-
},
|
|
1485
|
-
UnaryExpression(node) {
|
|
1486
|
-
if (node.operator !== "delete") return;
|
|
1487
|
-
const arg = Extract.unwrap(node.argument);
|
|
1488
|
-
if (arg.type !== AST_NODE_TYPES.MemberExpression) return;
|
|
1489
|
-
if (isRefLikeChain(context, arg)) return;
|
|
1490
|
-
const rootId = Extract.getRootIdentifier(arg);
|
|
1491
|
-
if (rootId == null) return;
|
|
1492
|
-
pushMutationSite(node, rootId);
|
|
1493
|
-
},
|
|
1494
|
-
UpdateExpression(node) {
|
|
1495
|
-
const arg = Extract.unwrap(node.argument);
|
|
1496
|
-
switch (arg.type) {
|
|
1497
|
-
case AST_NODE_TYPES.Identifier:
|
|
1498
|
-
if (isRefLikeName$1(arg.name)) return;
|
|
1499
|
-
pushMutationSite(node, arg);
|
|
1500
|
-
return;
|
|
1501
|
-
case AST_NODE_TYPES.MemberExpression: {
|
|
1502
|
-
if (isRefLikeChain(context, arg)) return;
|
|
1503
|
-
const rootId = Extract.getRootIdentifier(arg);
|
|
1504
|
-
if (rootId == null) return;
|
|
1505
|
-
pushMutationSite(node, rootId);
|
|
1506
|
-
return;
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1589
|
+
const mutableFunctions = inferMutableFunctions(context, collector.facts.mutations);
|
|
1590
|
+
if (mutableFunctions.size === 0) return;
|
|
1591
|
+
const reported = /* @__PURE__ */ new Set();
|
|
1592
|
+
for (const sink of collector.facts.sinks) {
|
|
1593
|
+
const expression = sink.expression;
|
|
1594
|
+
if (reported.has(expression)) continue;
|
|
1595
|
+
const fn = resolveToFunctionNode(context, expression);
|
|
1596
|
+
if (fn == null) continue;
|
|
1597
|
+
const mutation = mutableFunctions.get(fn);
|
|
1598
|
+
if (mutation == null) continue;
|
|
1599
|
+
reported.add(expression);
|
|
1600
|
+
context.report({
|
|
1601
|
+
data: { name: mutation.name },
|
|
1602
|
+
messageId: "default",
|
|
1603
|
+
node: expression
|
|
1604
|
+
});
|
|
1605
|
+
context.report({
|
|
1606
|
+
data: { name: mutation.name },
|
|
1607
|
+
messageId: "mutates",
|
|
1608
|
+
node: mutation.node
|
|
1609
|
+
});
|
|
1509
1610
|
}
|
|
1510
|
-
});
|
|
1611
|
+
} });
|
|
1511
1612
|
}
|
|
1512
1613
|
|
|
1513
1614
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-react-x",
|
|
3
|
-
"version": "5.14.
|
|
3
|
+
"version": "5.14.5",
|
|
4
4
|
"description": "A set of composable ESLint rules for libraries and frameworks that use React as a UI runtime.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
"string-ts": "^2.3.1",
|
|
46
46
|
"ts-api-utils": "^2.5.0",
|
|
47
47
|
"ts-pattern": "^5.9.0",
|
|
48
|
-
"@eslint-react/
|
|
49
|
-
"@eslint-react/
|
|
50
|
-
"@eslint-react/core": "5.14.
|
|
51
|
-
"@eslint-react/
|
|
52
|
-
"@eslint-react/shared": "5.14.
|
|
53
|
-
"@eslint-react/var": "5.14.
|
|
48
|
+
"@eslint-react/eslint": "5.14.5",
|
|
49
|
+
"@eslint-react/ast": "5.14.5",
|
|
50
|
+
"@eslint-react/core": "5.14.5",
|
|
51
|
+
"@eslint-react/jsx": "5.14.5",
|
|
52
|
+
"@eslint-react/shared": "5.14.5",
|
|
53
|
+
"@eslint-react/var": "5.14.5"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/react": "^19.2.17",
|