eslint-plugin-react-x 5.19.0 → 5.20.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/dist/index.js +241 -138
- package/package.json +17 -17
package/dist/index.js
CHANGED
|
@@ -4,12 +4,12 @@ import { Check, Compare, Extract, Traverse } from "@eslint-react/ast";
|
|
|
4
4
|
import * as core from "@eslint-react/core";
|
|
5
5
|
import { merge } from "@eslint-react/eslint";
|
|
6
6
|
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
7
|
+
import { P, isMatching, match } from "ts-pattern";
|
|
7
8
|
import { isAssignmentTargetEqual, isInitializedFromReact, resolve, resolveEnclosingAssignmentTarget, resolveObjectType } 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
11
|
import { findParentAttribute, getElementFullType, hasAttribute } from "@eslint-react/jsx";
|
|
11
12
|
import { compare } from "compare-versions";
|
|
12
|
-
import { P, isMatching, match } from "ts-pattern";
|
|
13
13
|
import { getConstrainedTypeAtLocation } from "@typescript-eslint/type-utils";
|
|
14
14
|
import { unionConstituents } from "ts-api-utils";
|
|
15
15
|
import "typescript";
|
|
@@ -145,7 +145,7 @@ const rules$6 = {
|
|
|
145
145
|
//#endregion
|
|
146
146
|
//#region package.json
|
|
147
147
|
var name$6 = "eslint-plugin-react-x";
|
|
148
|
-
var version = "5.
|
|
148
|
+
var version = "5.20.0";
|
|
149
149
|
|
|
150
150
|
//#endregion
|
|
151
151
|
//#region src/utils/create-rule.ts
|
|
@@ -1143,6 +1143,97 @@ const MUTATING_ARRAY_METHODS = /* @__PURE__ */ new Set([
|
|
|
1143
1143
|
"unshift"
|
|
1144
1144
|
]);
|
|
1145
1145
|
/**
|
|
1146
|
+
* Collect every write target in an assignment, including destructuring
|
|
1147
|
+
* patterns such as `[local, globalValue] = source`.
|
|
1148
|
+
*/
|
|
1149
|
+
function getAssignmentTargets(node) {
|
|
1150
|
+
const target = Extract.unwrap(node);
|
|
1151
|
+
switch (target.type) {
|
|
1152
|
+
case AST_NODE_TYPES.Identifier:
|
|
1153
|
+
case AST_NODE_TYPES.MemberExpression: return [target];
|
|
1154
|
+
case AST_NODE_TYPES.ArrayPattern: return target.elements.flatMap((element) => element == null ? [] : getAssignmentTargets(element));
|
|
1155
|
+
case AST_NODE_TYPES.AssignmentPattern: return getAssignmentTargets(target.left);
|
|
1156
|
+
case AST_NODE_TYPES.ObjectPattern: return target.properties.flatMap((property) => {
|
|
1157
|
+
if (property.type === AST_NODE_TYPES.RestElement) return getAssignmentTargets(property.argument);
|
|
1158
|
+
return getAssignmentTargets(property.value);
|
|
1159
|
+
});
|
|
1160
|
+
case AST_NODE_TYPES.RestElement: return getAssignmentTargets(target.argument);
|
|
1161
|
+
default: return [];
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
/** Resolve a direct call target, following simple function aliases. */
|
|
1165
|
+
function resolveToFunction(context, node, seen = /* @__PURE__ */ new Set()) {
|
|
1166
|
+
const expression = Extract.unwrap(node);
|
|
1167
|
+
if (Check.isFunction(expression)) return expression;
|
|
1168
|
+
if (!Check.isIdentifier(expression) || seen.has(expression)) return null;
|
|
1169
|
+
seen.add(expression);
|
|
1170
|
+
const resolved = resolve(context, expression);
|
|
1171
|
+
if (resolved == null) return null;
|
|
1172
|
+
return resolveToFunction(context, resolved, seen);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
//#endregion
|
|
1176
|
+
//#region src/rules/globals/collect.ts
|
|
1177
|
+
function createGlobalsCollector() {
|
|
1178
|
+
const facts = {
|
|
1179
|
+
callEdges: [],
|
|
1180
|
+
methodCalls: [],
|
|
1181
|
+
writes: []
|
|
1182
|
+
};
|
|
1183
|
+
function getEnclosingFunction(node) {
|
|
1184
|
+
return Traverse.findParent(node, Check.isFunction);
|
|
1185
|
+
}
|
|
1186
|
+
function pushWrite(node, target) {
|
|
1187
|
+
const enclosingFunction = getEnclosingFunction(node);
|
|
1188
|
+
if (enclosingFunction == null) return;
|
|
1189
|
+
facts.writes.push({
|
|
1190
|
+
enclosingFunction,
|
|
1191
|
+
node,
|
|
1192
|
+
target
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
return {
|
|
1196
|
+
facts,
|
|
1197
|
+
visitor: {
|
|
1198
|
+
AssignmentExpression(node) {
|
|
1199
|
+
for (const target of getAssignmentTargets(node.left)) pushWrite(node, target);
|
|
1200
|
+
},
|
|
1201
|
+
CallExpression(node) {
|
|
1202
|
+
const caller = getEnclosingFunction(node);
|
|
1203
|
+
if (caller != null) facts.callEdges.push({
|
|
1204
|
+
callee: node.callee,
|
|
1205
|
+
caller
|
|
1206
|
+
});
|
|
1207
|
+
const callee = Extract.unwrap(node.callee);
|
|
1208
|
+
if (callee.type !== AST_NODE_TYPES.MemberExpression) return;
|
|
1209
|
+
const method = Extract.getCalleeName(node);
|
|
1210
|
+
if (method == null || !MUTATING_ARRAY_METHODS.has(method)) return;
|
|
1211
|
+
if (caller == null) return;
|
|
1212
|
+
facts.methodCalls.push({
|
|
1213
|
+
enclosingFunction: caller,
|
|
1214
|
+
method,
|
|
1215
|
+
node,
|
|
1216
|
+
receiver: callee.object
|
|
1217
|
+
});
|
|
1218
|
+
},
|
|
1219
|
+
UnaryExpression(node) {
|
|
1220
|
+
if (node.operator !== "delete") return;
|
|
1221
|
+
const argument = Extract.unwrap(node.argument);
|
|
1222
|
+
if (argument.type !== AST_NODE_TYPES.MemberExpression) return;
|
|
1223
|
+
pushWrite(node, argument);
|
|
1224
|
+
},
|
|
1225
|
+
UpdateExpression(node) {
|
|
1226
|
+
const argument = Extract.unwrap(node.argument);
|
|
1227
|
+
if (argument.type !== AST_NODE_TYPES.Identifier && argument.type !== AST_NODE_TYPES.MemberExpression) return;
|
|
1228
|
+
pushWrite(node, argument);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
//#endregion
|
|
1235
|
+
//#region src/rules/globals/origins.ts
|
|
1236
|
+
/**
|
|
1146
1237
|
* Return whether an identifier is an unresolved global or is declared in the
|
|
1147
1238
|
* global/module scope.
|
|
1148
1239
|
*/
|
|
@@ -1174,34 +1265,85 @@ function resolveGlobalOrigin(context, node, seen = /* @__PURE__ */ new Set()) {
|
|
|
1174
1265
|
if (initializer.type !== AST_NODE_TYPES.Identifier && initializer.type !== AST_NODE_TYPES.MemberExpression) return null;
|
|
1175
1266
|
return resolveGlobalOrigin(context, initializer, seen);
|
|
1176
1267
|
}
|
|
1268
|
+
|
|
1269
|
+
//#endregion
|
|
1270
|
+
//#region src/rules/globals/effects.ts
|
|
1177
1271
|
/**
|
|
1178
|
-
*
|
|
1179
|
-
*
|
|
1272
|
+
* Classify the collected writes and mutating method calls, keeping only the
|
|
1273
|
+
* ones that reach a global/module binding, grouped by the function that
|
|
1274
|
+
* performs them.
|
|
1180
1275
|
*/
|
|
1181
|
-
function
|
|
1182
|
-
const
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1276
|
+
function inferGlobalMutations(context, facts) {
|
|
1277
|
+
const directEffects = /* @__PURE__ */ new Map();
|
|
1278
|
+
function pushEffect(enclosingFunction, effect) {
|
|
1279
|
+
const effects = directEffects.get(enclosingFunction) ?? [];
|
|
1280
|
+
effects.push(effect);
|
|
1281
|
+
directEffects.set(enclosingFunction, effects);
|
|
1282
|
+
}
|
|
1283
|
+
for (const write of facts.writes) {
|
|
1284
|
+
if (Check.isIdentifier(write.target)) {
|
|
1285
|
+
if (!isGlobalVariable(context, write.target)) continue;
|
|
1286
|
+
pushEffect(write.enclosingFunction, {
|
|
1287
|
+
kind: "global",
|
|
1288
|
+
name: write.target.name,
|
|
1289
|
+
method: null,
|
|
1290
|
+
node: write.node
|
|
1291
|
+
});
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
1294
|
+
if (resolveGlobalOrigin(context, write.target.object) == null) continue;
|
|
1295
|
+
pushEffect(write.enclosingFunction, {
|
|
1296
|
+
kind: "property",
|
|
1297
|
+
name: context.sourceCode.getText(write.target),
|
|
1298
|
+
method: null,
|
|
1299
|
+
node: write.node
|
|
1191
1300
|
});
|
|
1192
|
-
case AST_NODE_TYPES.RestElement: return getAssignmentTargets(target.argument);
|
|
1193
|
-
default: return [];
|
|
1194
1301
|
}
|
|
1302
|
+
for (const call of facts.methodCalls) {
|
|
1303
|
+
const origin = resolveGlobalOrigin(context, call.receiver);
|
|
1304
|
+
if (origin == null) continue;
|
|
1305
|
+
pushEffect(call.enclosingFunction, {
|
|
1306
|
+
kind: "method",
|
|
1307
|
+
name: origin.name,
|
|
1308
|
+
method: call.method,
|
|
1309
|
+
node: call.node
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
return directEffects;
|
|
1195
1313
|
}
|
|
1196
|
-
/** Resolve
|
|
1197
|
-
function
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1314
|
+
/** Resolve the collected call edges into a function-to-function call graph. */
|
|
1315
|
+
function inferCallGraph(context, callEdges) {
|
|
1316
|
+
const callGraph = /* @__PURE__ */ new Map();
|
|
1317
|
+
for (const edge of callEdges) {
|
|
1318
|
+
const callee = resolveToFunction(context, edge.callee);
|
|
1319
|
+
if (callee == null) continue;
|
|
1320
|
+
const callees = callGraph.get(edge.caller) ?? /* @__PURE__ */ new Set();
|
|
1321
|
+
callees.add(callee);
|
|
1322
|
+
callGraph.set(edge.caller, callees);
|
|
1323
|
+
}
|
|
1324
|
+
return callGraph;
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Like the SPEC's function signatures, these summaries keep creation of an
|
|
1328
|
+
* effect separate from applying it in a component or hook render: walk the
|
|
1329
|
+
* call graph from each render function and gather every reachable effect once.
|
|
1330
|
+
*/
|
|
1331
|
+
function collectReachableEffects(renderFunctions, directEffects, callGraph) {
|
|
1332
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1333
|
+
const reported = /* @__PURE__ */ new Set();
|
|
1334
|
+
const reachable = [];
|
|
1335
|
+
function applyFunctionEffects(func) {
|
|
1336
|
+
if (visited.has(func)) return;
|
|
1337
|
+
visited.add(func);
|
|
1338
|
+
for (const effect of directEffects.get(func) ?? []) {
|
|
1339
|
+
if (reported.has(effect)) continue;
|
|
1340
|
+
reported.add(effect);
|
|
1341
|
+
reachable.push(effect);
|
|
1342
|
+
}
|
|
1343
|
+
for (const callee of callGraph.get(func) ?? []) applyFunctionEffects(callee);
|
|
1344
|
+
}
|
|
1345
|
+
for (const func of renderFunctions) applyFunctionEffects(func);
|
|
1346
|
+
return reachable;
|
|
1205
1347
|
}
|
|
1206
1348
|
|
|
1207
1349
|
//#endregion
|
|
@@ -1223,87 +1365,25 @@ var globals_default = createRule({
|
|
|
1223
1365
|
defaultOptions: []
|
|
1224
1366
|
});
|
|
1225
1367
|
function create$49(context) {
|
|
1226
|
-
const
|
|
1227
|
-
const
|
|
1228
|
-
const
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
directEffects.set(enclosing, effects);
|
|
1243
|
-
}
|
|
1244
|
-
function recordWrite(node, target) {
|
|
1245
|
-
if (Check.isIdentifier(target)) {
|
|
1246
|
-
if (!isGlobalVariable(context, target)) return;
|
|
1247
|
-
recordEffect(node, "mutatingGlobal", { name: target.name });
|
|
1248
|
-
return;
|
|
1249
|
-
}
|
|
1250
|
-
if (resolveGlobalOrigin(context, target.object) == null) return;
|
|
1251
|
-
recordEffect(node, "mutatingGlobalProperty", { name: context.sourceCode.getText(target) });
|
|
1252
|
-
}
|
|
1253
|
-
function recordCallEdge(node) {
|
|
1254
|
-
const caller = getEnclosingFunction(node);
|
|
1255
|
-
if (caller == null) return;
|
|
1256
|
-
const callee = resolveToFunction(context, node.callee);
|
|
1257
|
-
if (callee == null) return;
|
|
1258
|
-
const callees = callGraph.get(caller) ?? /* @__PURE__ */ new Set();
|
|
1259
|
-
callees.add(callee);
|
|
1260
|
-
callGraph.set(caller, callees);
|
|
1261
|
-
}
|
|
1262
|
-
return merge(hc.visitor, fc.visitor, {
|
|
1263
|
-
AssignmentExpression(node) {
|
|
1264
|
-
for (const target of getAssignmentTargets(node.left)) recordWrite(node, target);
|
|
1265
|
-
},
|
|
1266
|
-
CallExpression(node) {
|
|
1267
|
-
recordCallEdge(node);
|
|
1268
|
-
const callee = Extract.unwrap(node.callee);
|
|
1269
|
-
if (callee.type !== AST_NODE_TYPES.MemberExpression) return;
|
|
1270
|
-
const method = Extract.getCalleeName(node);
|
|
1271
|
-
if (method == null || !MUTATING_ARRAY_METHODS.has(method)) return;
|
|
1272
|
-
const origin = resolveGlobalOrigin(context, callee.object);
|
|
1273
|
-
if (origin == null) return;
|
|
1274
|
-
recordEffect(node, "mutatingGlobalArrayMethod", {
|
|
1275
|
-
name: origin.name,
|
|
1276
|
-
method
|
|
1368
|
+
const hooks = core.getHookCollector(context);
|
|
1369
|
+
const comps = core.getFunctionComponentCollector(context);
|
|
1370
|
+
const globs = createGlobalsCollector();
|
|
1371
|
+
return merge(hooks.visitor, comps.visitor, globs.visitor, { "Program:exit"(program) {
|
|
1372
|
+
const renderFunctions = [...comps.api.getAllComponents(program), ...hooks.api.getAllHooks(program)].map(({ node }) => node);
|
|
1373
|
+
const directEffects = inferGlobalMutations(context, globs.facts);
|
|
1374
|
+
const callGraph = inferCallGraph(context, globs.facts.callEdges);
|
|
1375
|
+
for (const effect of collectReachableEffects(renderFunctions, directEffects, callGraph)) {
|
|
1376
|
+
const data = effect.method == null ? { name: effect.name } : {
|
|
1377
|
+
name: effect.name,
|
|
1378
|
+
method: effect.method
|
|
1379
|
+
};
|
|
1380
|
+
context.report({
|
|
1381
|
+
data,
|
|
1382
|
+
messageId: match(effect).with({ kind: "global" }, () => "mutatingGlobal").with({ kind: "method" }, () => "mutatingGlobalArrayMethod").with({ kind: "property" }, () => "mutatingGlobalProperty").exhaustive(),
|
|
1383
|
+
node: effect.node
|
|
1277
1384
|
});
|
|
1278
|
-
},
|
|
1279
|
-
"Program:exit"(program) {
|
|
1280
|
-
const renderFunctions = [...fc.api.getAllComponents(program), ...hc.api.getAllHooks(program)];
|
|
1281
|
-
const visited = /* @__PURE__ */ new Set();
|
|
1282
|
-
const reported = /* @__PURE__ */ new Set();
|
|
1283
|
-
function applyFunctionEffects(func) {
|
|
1284
|
-
if (visited.has(func)) return;
|
|
1285
|
-
visited.add(func);
|
|
1286
|
-
for (const effect of directEffects.get(func) ?? []) {
|
|
1287
|
-
if (reported.has(effect)) continue;
|
|
1288
|
-
reported.add(effect);
|
|
1289
|
-
context.report(effect);
|
|
1290
|
-
}
|
|
1291
|
-
for (const callee of callGraph.get(func) ?? []) applyFunctionEffects(callee);
|
|
1292
|
-
}
|
|
1293
|
-
for (const { node } of renderFunctions) applyFunctionEffects(node);
|
|
1294
|
-
},
|
|
1295
|
-
UnaryExpression(node) {
|
|
1296
|
-
if (node.operator !== "delete") return;
|
|
1297
|
-
const argument = Extract.unwrap(node.argument);
|
|
1298
|
-
if (argument.type !== AST_NODE_TYPES.MemberExpression) return;
|
|
1299
|
-
recordWrite(node, argument);
|
|
1300
|
-
},
|
|
1301
|
-
UpdateExpression(node) {
|
|
1302
|
-
const argument = Extract.unwrap(node.argument);
|
|
1303
|
-
if (argument.type !== AST_NODE_TYPES.Identifier && argument.type !== AST_NODE_TYPES.MemberExpression) return;
|
|
1304
|
-
recordWrite(node, argument);
|
|
1305
1385
|
}
|
|
1306
|
-
});
|
|
1386
|
+
} });
|
|
1307
1387
|
}
|
|
1308
1388
|
|
|
1309
1389
|
//#endregion
|
|
@@ -1314,7 +1394,7 @@ function create$49(context) {
|
|
|
1314
1394
|
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
|
|
1315
1395
|
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set
|
|
1316
1396
|
*/
|
|
1317
|
-
const
|
|
1397
|
+
const KNOWN_MUTATING_METHODS = /* @__PURE__ */ new Set([
|
|
1318
1398
|
"add",
|
|
1319
1399
|
"clear",
|
|
1320
1400
|
"copyWithin",
|
|
@@ -1330,9 +1410,9 @@ const MUTATING_METHODS = /* @__PURE__ */ new Set([
|
|
|
1330
1410
|
"unshift"
|
|
1331
1411
|
]);
|
|
1332
1412
|
/**
|
|
1333
|
-
* Known
|
|
1413
|
+
* Known mutating hooks.
|
|
1334
1414
|
*/
|
|
1335
|
-
const
|
|
1415
|
+
const KNOWN_MUTATING_HOOKS = /* @__PURE__ */ new Set([
|
|
1336
1416
|
"useHistory",
|
|
1337
1417
|
"useNavigate",
|
|
1338
1418
|
"useNavigation",
|
|
@@ -1346,6 +1426,20 @@ function isNodeWithin(node, ancestor) {
|
|
|
1346
1426
|
}
|
|
1347
1427
|
return false;
|
|
1348
1428
|
}
|
|
1429
|
+
function isComponentPropsDefinition(def, components) {
|
|
1430
|
+
if (def.type !== DefinitionType.Parameter) return false;
|
|
1431
|
+
const fn = def.node;
|
|
1432
|
+
if (!Check.isFunction(fn)) return false;
|
|
1433
|
+
const firstParam = fn.params.at(0);
|
|
1434
|
+
if (firstParam == null || !isNodeWithin(def.name, firstParam)) return false;
|
|
1435
|
+
return components.includes(fn);
|
|
1436
|
+
}
|
|
1437
|
+
function getStateHookName(context, init) {
|
|
1438
|
+
const { additionalStateHooks } = getSettingsFromContext(context);
|
|
1439
|
+
if (core.isUseStateLikeCall(init, additionalStateHooks)) return Extract.getCalleeName(init) ?? "useState";
|
|
1440
|
+
if (core.isUseReducerCall(context, init)) return "useReducer";
|
|
1441
|
+
return null;
|
|
1442
|
+
}
|
|
1349
1443
|
function resolveToFunctionNode(context, node, seen = /* @__PURE__ */ new Set()) {
|
|
1350
1444
|
const expr = Extract.unwrap(node);
|
|
1351
1445
|
if (Check.isFunction(expr)) return expr;
|
|
@@ -1390,7 +1484,7 @@ function isInitializedFromUseRef(context, node) {
|
|
|
1390
1484
|
function isKnownNonMutatingMethodCall(context, node) {
|
|
1391
1485
|
const callee = Extract.unwrap(node.callee);
|
|
1392
1486
|
return Check.isExpression(callee) && isInitializedFromCall(context, callee, (init) => {
|
|
1393
|
-
return
|
|
1487
|
+
return KNOWN_MUTATING_HOOKS.values().some((hook) => core.isAPICall(hook)(context, init));
|
|
1394
1488
|
});
|
|
1395
1489
|
}
|
|
1396
1490
|
function isRefLikeChain(context, node) {
|
|
@@ -1435,7 +1529,7 @@ function createImmutabilityCollector() {
|
|
|
1435
1529
|
const callee = Extract.unwrap(node.callee);
|
|
1436
1530
|
if (callee.type === AST_NODE_TYPES.MemberExpression) {
|
|
1437
1531
|
const method = Extract.getCalleeName(node);
|
|
1438
|
-
if (method != null &&
|
|
1532
|
+
if (method != null && KNOWN_MUTATING_METHODS.has(method)) {
|
|
1439
1533
|
const root = Extract.getIdentifierAt(callee.object, 0);
|
|
1440
1534
|
if (root != null) pushMutation("value", node, callee.object, root);
|
|
1441
1535
|
}
|
|
@@ -1484,42 +1578,43 @@ function createImmutabilityCollector() {
|
|
|
1484
1578
|
|
|
1485
1579
|
//#endregion
|
|
1486
1580
|
//#region src/rules/immutability/origins.ts
|
|
1487
|
-
function isComponentPropsDefinition(context, def) {
|
|
1488
|
-
if (def.type !== DefinitionType.Parameter) return false;
|
|
1489
|
-
const fn = def.node;
|
|
1490
|
-
if (!Check.isFunction(fn)) return false;
|
|
1491
|
-
const firstParam = fn.params.at(0);
|
|
1492
|
-
if (firstParam == null || !isNodeWithin(def.name, firstParam)) return false;
|
|
1493
|
-
return core.isFunctionComponentDefinition(context, fn, core.DEFAULT_COMPONENT_DETECTION_HINT);
|
|
1494
|
-
}
|
|
1495
|
-
function getStateHookName(context, init) {
|
|
1496
|
-
const { additionalStateHooks } = getSettingsFromContext(context);
|
|
1497
|
-
if (core.isUseStateLikeCall(init, additionalStateHooks)) return Extract.getCalleeName(init) ?? "useState";
|
|
1498
|
-
if (core.isUseReducerCall(context, init)) return "useReducer";
|
|
1499
|
-
return null;
|
|
1500
|
-
}
|
|
1501
1581
|
/**
|
|
1502
1582
|
* Classify whether a variable ultimately holds a value that must be treated as
|
|
1503
1583
|
* immutable: a component's props, a state value returned from `useState`-like or
|
|
1504
|
-
* `useReducer` calls,
|
|
1584
|
+
* `useReducer` calls, a shallow copy (spread literal) of either, or a `for...of`
|
|
1585
|
+
* iterator variable whose iterated collection resolves to one of those.
|
|
1505
1586
|
* @param context The rule context.
|
|
1506
1587
|
* @param variable The variable to classify.
|
|
1507
|
-
* @param
|
|
1588
|
+
* @param components The confirmed function component nodes in the file.
|
|
1589
|
+
* @param seen Variables already visited during spread/iterator recursion.
|
|
1508
1590
|
* @returns The frozen origin, or `null` when the variable is not derived from one.
|
|
1509
1591
|
*/
|
|
1510
|
-
function classifyFrozenOrigin(context, variable, seen = /* @__PURE__ */ new Set()) {
|
|
1592
|
+
function classifyFrozenOrigin(context, variable, components, seen = /* @__PURE__ */ new Set()) {
|
|
1511
1593
|
if (seen.has(variable)) return null;
|
|
1512
1594
|
seen.add(variable);
|
|
1513
1595
|
const origin = resolveVariableOrigin(context, variable);
|
|
1514
1596
|
const def = origin.defs.length === 1 ? origin.defs[0] : null;
|
|
1515
1597
|
if (def == null) return null;
|
|
1516
|
-
if (isComponentPropsDefinition(
|
|
1598
|
+
if (isComponentPropsDefinition(def, components)) return {
|
|
1517
1599
|
kind: "props",
|
|
1518
1600
|
name: origin.name
|
|
1519
1601
|
};
|
|
1520
1602
|
if (def.type !== DefinitionType.Variable) return null;
|
|
1521
|
-
|
|
1522
|
-
|
|
1603
|
+
if (def.node.init == null) {
|
|
1604
|
+
const loop = def.node.parent.parent;
|
|
1605
|
+
if (loop.type !== AST_NODE_TYPES.ForOfStatement || loop.left !== def.node.parent) return null;
|
|
1606
|
+
const root = Extract.getIdentifierAt(loop.right, 0);
|
|
1607
|
+
if (root == null) return null;
|
|
1608
|
+
const source = findVariable(context.sourceCode.getScope(root), root);
|
|
1609
|
+
if (source == null) return null;
|
|
1610
|
+
const inner = classifyFrozenOrigin(context, source, components, seen);
|
|
1611
|
+
return inner == null ? null : {
|
|
1612
|
+
kind: "iterator",
|
|
1613
|
+
name: origin.name,
|
|
1614
|
+
original: inner.name
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
const init = Extract.unwrap(def.node.init);
|
|
1523
1618
|
switch (init.type) {
|
|
1524
1619
|
case AST_NODE_TYPES.CallExpression: {
|
|
1525
1620
|
const hook = getStateHookName(context, init);
|
|
@@ -1542,7 +1637,7 @@ function classifyFrozenOrigin(context, variable, seen = /* @__PURE__ */ new Set(
|
|
|
1542
1637
|
if (!Check.isIdentifier(argument)) continue;
|
|
1543
1638
|
const source = findVariable(context.sourceCode.getScope(argument), argument);
|
|
1544
1639
|
if (source == null) continue;
|
|
1545
|
-
const inner = classifyFrozenOrigin(context, source, seen);
|
|
1640
|
+
const inner = classifyFrozenOrigin(context, source, components, seen);
|
|
1546
1641
|
if (inner != null) return {
|
|
1547
1642
|
kind: "shallow-copy",
|
|
1548
1643
|
name: origin.name,
|
|
@@ -1591,7 +1686,7 @@ function getMutatedObject(mutation) {
|
|
|
1591
1686
|
if (mutation.node.type === AST_NODE_TYPES.CallExpression) return target;
|
|
1592
1687
|
return target.type === AST_NODE_TYPES.MemberExpression ? Extract.unwrap(target.object) : target;
|
|
1593
1688
|
}
|
|
1594
|
-
function inferDirectMutations(context, mutations) {
|
|
1689
|
+
function inferDirectMutations(context, mutations, components) {
|
|
1595
1690
|
const directMutations = [];
|
|
1596
1691
|
for (const mutation of mutations) {
|
|
1597
1692
|
if (mutation.kind !== "value") continue;
|
|
@@ -1599,7 +1694,7 @@ function inferDirectMutations(context, mutations) {
|
|
|
1599
1694
|
if (isRefMutation(context, mutation)) continue;
|
|
1600
1695
|
const variable = findVariable(context.sourceCode.getScope(mutation.root), mutation.root);
|
|
1601
1696
|
if (variable == null) continue;
|
|
1602
|
-
const origin = classifyFrozenOrigin(context, variable);
|
|
1697
|
+
const origin = classifyFrozenOrigin(context, variable, components);
|
|
1603
1698
|
if (origin == null) continue;
|
|
1604
1699
|
switch (origin.kind) {
|
|
1605
1700
|
case "props":
|
|
@@ -1623,6 +1718,12 @@ function inferDirectMutations(context, mutations) {
|
|
|
1623
1718
|
detail: `It is a shallow copy of '${origin.original}'; mutating nested values through it mutates '${origin.original}' in place.`,
|
|
1624
1719
|
node: mutation.node
|
|
1625
1720
|
});
|
|
1721
|
+
break;
|
|
1722
|
+
case "iterator": directMutations.push({
|
|
1723
|
+
name: origin.name,
|
|
1724
|
+
detail: `It is an element of '${origin.original}' and must be treated as immutable.`,
|
|
1725
|
+
node: mutation.node
|
|
1726
|
+
});
|
|
1626
1727
|
}
|
|
1627
1728
|
}
|
|
1628
1729
|
return directMutations;
|
|
@@ -1648,17 +1749,18 @@ var immutability_default = createRule({
|
|
|
1648
1749
|
});
|
|
1649
1750
|
function create$48(context) {
|
|
1650
1751
|
const hooks = core.getHookCollector(context);
|
|
1651
|
-
const
|
|
1652
|
-
|
|
1653
|
-
|
|
1752
|
+
const comps = core.getFunctionComponentCollector(context);
|
|
1753
|
+
const immut = createImmutabilityCollector();
|
|
1754
|
+
return merge(hooks.visitor, comps.visitor, immut.visitor, { "Program:exit"(program) {
|
|
1755
|
+
for (const hook of hooks.api.getAllHooks(program)) for (const expression of hook.rets) if (expression != null) immut.facts.sinks.push({
|
|
1654
1756
|
kind: "hook-return",
|
|
1655
1757
|
expression
|
|
1656
1758
|
});
|
|
1657
1759
|
const reportedMutations = /* @__PURE__ */ new Set();
|
|
1658
|
-
const mutableFunctions = inferMutableFunctions(context,
|
|
1760
|
+
const mutableFunctions = inferMutableFunctions(context, immut.facts.mutations);
|
|
1659
1761
|
if (mutableFunctions.size > 0) {
|
|
1660
1762
|
const reportedSinks = /* @__PURE__ */ new Set();
|
|
1661
|
-
for (const sink of
|
|
1763
|
+
for (const sink of immut.facts.sinks) {
|
|
1662
1764
|
const expression = sink.expression;
|
|
1663
1765
|
if (reportedSinks.has(expression)) continue;
|
|
1664
1766
|
const fn = resolveToFunctionNode(context, expression);
|
|
@@ -1679,7 +1781,8 @@ function create$48(context) {
|
|
|
1679
1781
|
});
|
|
1680
1782
|
}
|
|
1681
1783
|
}
|
|
1682
|
-
|
|
1784
|
+
const funcs = comps.api.getAllComponents(program).map((comp) => comp.node);
|
|
1785
|
+
for (const mutation of inferDirectMutations(context, immut.facts.mutations, funcs)) {
|
|
1683
1786
|
if (reportedMutations.has(mutation.node)) continue;
|
|
1684
1787
|
reportedMutations.add(mutation.node);
|
|
1685
1788
|
context.report({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-react-x",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.20.0",
|
|
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",
|
|
@@ -36,17 +36,17 @@
|
|
|
36
36
|
"dist"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@eslint-react/ast": "5.
|
|
40
|
-
"@eslint-react/core": "5.
|
|
41
|
-
"@eslint-react/eslint": "5.
|
|
42
|
-
"@eslint-react/jsx": "5.
|
|
43
|
-
"@eslint-react/shared": "5.
|
|
44
|
-
"@eslint-react/var": "5.
|
|
45
|
-
"@typescript-eslint/scope-manager": "^8.
|
|
46
|
-
"@typescript-eslint/type-utils": "^8.
|
|
47
|
-
"@typescript-eslint/types": "^8.
|
|
48
|
-
"@typescript-eslint/typescript-estree": "^8.
|
|
49
|
-
"@typescript-eslint/utils": "^8.
|
|
39
|
+
"@eslint-react/ast": "5.20.0",
|
|
40
|
+
"@eslint-react/core": "5.20.0",
|
|
41
|
+
"@eslint-react/eslint": "5.20.0",
|
|
42
|
+
"@eslint-react/jsx": "5.20.0",
|
|
43
|
+
"@eslint-react/shared": "5.20.0",
|
|
44
|
+
"@eslint-react/var": "5.20.0",
|
|
45
|
+
"@typescript-eslint/scope-manager": "^8.70.0",
|
|
46
|
+
"@typescript-eslint/type-utils": "^8.70.0",
|
|
47
|
+
"@typescript-eslint/types": "^8.70.0",
|
|
48
|
+
"@typescript-eslint/typescript-estree": "^8.70.0",
|
|
49
|
+
"@typescript-eslint/utils": "^8.70.0",
|
|
50
50
|
"compare-versions": "^6.1.1",
|
|
51
51
|
"string-ts": "^2.3.1",
|
|
52
52
|
"ts-api-utils": "^2.5.0",
|
|
@@ -55,15 +55,15 @@
|
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@local/configs": "0.0.0",
|
|
57
57
|
"@local/eff": "0.0.0",
|
|
58
|
-
"@types/react": "^19.
|
|
59
|
-
"@types/react-dom": "^19.
|
|
58
|
+
"@types/react": "^19.3.0",
|
|
59
|
+
"@types/react-dom": "^19.3.0",
|
|
60
60
|
"dedent": "^1.7.2",
|
|
61
61
|
"eslint": "^10.10.0",
|
|
62
|
-
"react": "^19.
|
|
63
|
-
"react-dom": "^19.
|
|
62
|
+
"react": "^19.3.0",
|
|
63
|
+
"react-dom": "^19.3.0",
|
|
64
64
|
"tsdown": "^0.23.0",
|
|
65
65
|
"tsl": "^1.0.30",
|
|
66
|
-
"tsl-dx": "^0.13.
|
|
66
|
+
"tsl-dx": "^0.13.5",
|
|
67
67
|
"typescript": "6.0.3"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|