eslint-plugin-react-x 5.19.0 → 5.19.1
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 +217 -134
- package/package.json +16 -16
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.19.
|
|
148
|
+
var version = "5.19.1";
|
|
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
|
|
1300
|
+
});
|
|
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
|
|
1191
1310
|
});
|
|
1192
|
-
case AST_NODE_TYPES.RestElement: return getAssignmentTargets(target.argument);
|
|
1193
|
-
default: return [];
|
|
1194
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,36 +1578,23 @@ 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
1584
|
* `useReducer` calls, or a shallow copy (spread literal) of either.
|
|
1505
1585
|
* @param context The rule context.
|
|
1506
1586
|
* @param variable The variable to classify.
|
|
1587
|
+
* @param components The confirmed function component nodes in the file.
|
|
1507
1588
|
* @param seen Variables already visited during spread recursion.
|
|
1508
1589
|
* @returns The frozen origin, or `null` when the variable is not derived from one.
|
|
1509
1590
|
*/
|
|
1510
|
-
function classifyFrozenOrigin(context, variable, seen = /* @__PURE__ */ new Set()) {
|
|
1591
|
+
function classifyFrozenOrigin(context, variable, components, seen = /* @__PURE__ */ new Set()) {
|
|
1511
1592
|
if (seen.has(variable)) return null;
|
|
1512
1593
|
seen.add(variable);
|
|
1513
1594
|
const origin = resolveVariableOrigin(context, variable);
|
|
1514
1595
|
const def = origin.defs.length === 1 ? origin.defs[0] : null;
|
|
1515
1596
|
if (def == null) return null;
|
|
1516
|
-
if (isComponentPropsDefinition(
|
|
1597
|
+
if (isComponentPropsDefinition(def, components)) return {
|
|
1517
1598
|
kind: "props",
|
|
1518
1599
|
name: origin.name
|
|
1519
1600
|
};
|
|
@@ -1542,7 +1623,7 @@ function classifyFrozenOrigin(context, variable, seen = /* @__PURE__ */ new Set(
|
|
|
1542
1623
|
if (!Check.isIdentifier(argument)) continue;
|
|
1543
1624
|
const source = findVariable(context.sourceCode.getScope(argument), argument);
|
|
1544
1625
|
if (source == null) continue;
|
|
1545
|
-
const inner = classifyFrozenOrigin(context, source, seen);
|
|
1626
|
+
const inner = classifyFrozenOrigin(context, source, components, seen);
|
|
1546
1627
|
if (inner != null) return {
|
|
1547
1628
|
kind: "shallow-copy",
|
|
1548
1629
|
name: origin.name,
|
|
@@ -1591,7 +1672,7 @@ function getMutatedObject(mutation) {
|
|
|
1591
1672
|
if (mutation.node.type === AST_NODE_TYPES.CallExpression) return target;
|
|
1592
1673
|
return target.type === AST_NODE_TYPES.MemberExpression ? Extract.unwrap(target.object) : target;
|
|
1593
1674
|
}
|
|
1594
|
-
function inferDirectMutations(context, mutations) {
|
|
1675
|
+
function inferDirectMutations(context, mutations, components) {
|
|
1595
1676
|
const directMutations = [];
|
|
1596
1677
|
for (const mutation of mutations) {
|
|
1597
1678
|
if (mutation.kind !== "value") continue;
|
|
@@ -1599,7 +1680,7 @@ function inferDirectMutations(context, mutations) {
|
|
|
1599
1680
|
if (isRefMutation(context, mutation)) continue;
|
|
1600
1681
|
const variable = findVariable(context.sourceCode.getScope(mutation.root), mutation.root);
|
|
1601
1682
|
if (variable == null) continue;
|
|
1602
|
-
const origin = classifyFrozenOrigin(context, variable);
|
|
1683
|
+
const origin = classifyFrozenOrigin(context, variable, components);
|
|
1603
1684
|
if (origin == null) continue;
|
|
1604
1685
|
switch (origin.kind) {
|
|
1605
1686
|
case "props":
|
|
@@ -1648,17 +1729,18 @@ var immutability_default = createRule({
|
|
|
1648
1729
|
});
|
|
1649
1730
|
function create$48(context) {
|
|
1650
1731
|
const hooks = core.getHookCollector(context);
|
|
1651
|
-
const
|
|
1652
|
-
|
|
1653
|
-
|
|
1732
|
+
const comps = core.getFunctionComponentCollector(context);
|
|
1733
|
+
const immut = createImmutabilityCollector();
|
|
1734
|
+
return merge(hooks.visitor, comps.visitor, immut.visitor, { "Program:exit"(program) {
|
|
1735
|
+
for (const hook of hooks.api.getAllHooks(program)) for (const expression of hook.rets) if (expression != null) immut.facts.sinks.push({
|
|
1654
1736
|
kind: "hook-return",
|
|
1655
1737
|
expression
|
|
1656
1738
|
});
|
|
1657
1739
|
const reportedMutations = /* @__PURE__ */ new Set();
|
|
1658
|
-
const mutableFunctions = inferMutableFunctions(context,
|
|
1740
|
+
const mutableFunctions = inferMutableFunctions(context, immut.facts.mutations);
|
|
1659
1741
|
if (mutableFunctions.size > 0) {
|
|
1660
1742
|
const reportedSinks = /* @__PURE__ */ new Set();
|
|
1661
|
-
for (const sink of
|
|
1743
|
+
for (const sink of immut.facts.sinks) {
|
|
1662
1744
|
const expression = sink.expression;
|
|
1663
1745
|
if (reportedSinks.has(expression)) continue;
|
|
1664
1746
|
const fn = resolveToFunctionNode(context, expression);
|
|
@@ -1679,7 +1761,8 @@ function create$48(context) {
|
|
|
1679
1761
|
});
|
|
1680
1762
|
}
|
|
1681
1763
|
}
|
|
1682
|
-
|
|
1764
|
+
const funcs = comps.api.getAllComponents(program).map((comp) => comp.node);
|
|
1765
|
+
for (const mutation of inferDirectMutations(context, immut.facts.mutations, funcs)) {
|
|
1683
1766
|
if (reportedMutations.has(mutation.node)) continue;
|
|
1684
1767
|
reportedMutations.add(mutation.node);
|
|
1685
1768
|
context.report({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-react-x",
|
|
3
|
-
"version": "5.19.
|
|
3
|
+
"version": "5.19.1",
|
|
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.19.
|
|
40
|
-
"@eslint-react/core": "5.19.
|
|
41
|
-
"@eslint-react/eslint": "5.19.
|
|
42
|
-
"@eslint-react/jsx": "5.19.
|
|
43
|
-
"@eslint-react/shared": "5.19.
|
|
44
|
-
"@eslint-react/var": "5.19.
|
|
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.19.1",
|
|
40
|
+
"@eslint-react/core": "5.19.1",
|
|
41
|
+
"@eslint-react/eslint": "5.19.1",
|
|
42
|
+
"@eslint-react/jsx": "5.19.1",
|
|
43
|
+
"@eslint-react/shared": "5.19.1",
|
|
44
|
+
"@eslint-react/var": "5.19.1",
|
|
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,12 +55,12 @@
|
|
|
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
66
|
"tsl-dx": "^0.13.3",
|