eslint-plugin-react-x 5.20.4 → 5.20.6
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 +343 -83
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ 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
7
|
import { P, isMatching, match } from "ts-pattern";
|
|
8
|
-
import { isAssignmentTargetEqual, isInitializedFromReact,
|
|
8
|
+
import { isAssignmentTargetEqual, isInitializedFromReact, resolveEnclosingAssignmentTarget, resolveObjectType, resolveOrigin } from "@eslint-react/var";
|
|
9
9
|
import { DefinitionType, ScopeType } from "@typescript-eslint/scope-manager";
|
|
10
10
|
import { findVariable, getStaticValue } from "@typescript-eslint/utils/ast-utils";
|
|
11
11
|
import { findParentAttribute, getElementFullType, hasAttribute } from "@eslint-react/jsx";
|
|
@@ -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.20.
|
|
148
|
+
var version = "5.20.6";
|
|
149
149
|
|
|
150
150
|
//#endregion
|
|
151
151
|
//#region src/utils/create-rule.ts
|
|
@@ -1142,32 +1142,13 @@ const MUTATING_ARRAY_METHODS = /* @__PURE__ */ new Set([
|
|
|
1142
1142
|
"splice",
|
|
1143
1143
|
"unshift"
|
|
1144
1144
|
]);
|
|
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
1145
|
/** Resolve a direct call target, following simple function aliases. */
|
|
1165
1146
|
function resolveToFunction(context, node, seen = /* @__PURE__ */ new Set()) {
|
|
1166
1147
|
const expression = Extract.unwrap(node);
|
|
1167
1148
|
if (Check.isFunction(expression)) return expression;
|
|
1168
1149
|
if (!Check.isIdentifier(expression) || seen.has(expression)) return null;
|
|
1169
1150
|
seen.add(expression);
|
|
1170
|
-
const resolved =
|
|
1151
|
+
const resolved = resolveOrigin(context, expression);
|
|
1171
1152
|
if (resolved == null) return null;
|
|
1172
1153
|
return resolveToFunction(context, resolved, seen);
|
|
1173
1154
|
}
|
|
@@ -1196,7 +1177,7 @@ function createFactCollector$2() {
|
|
|
1196
1177
|
facts,
|
|
1197
1178
|
visitor: {
|
|
1198
1179
|
AssignmentExpression(node) {
|
|
1199
|
-
for (const target of getAssignmentTargets(node.left)) pushWrite(node, target);
|
|
1180
|
+
for (const target of Extract.getAssignmentTargets(node.left)) pushWrite(node, target);
|
|
1200
1181
|
},
|
|
1201
1182
|
CallExpression(node) {
|
|
1202
1183
|
const caller = getEnclosingFunction(node);
|
|
@@ -1216,6 +1197,14 @@ function createFactCollector$2() {
|
|
|
1216
1197
|
receiver: callee.object
|
|
1217
1198
|
});
|
|
1218
1199
|
},
|
|
1200
|
+
ForInStatement(node) {
|
|
1201
|
+
if (node.left.type === AST_NODE_TYPES.VariableDeclaration) return;
|
|
1202
|
+
for (const target of Extract.getAssignmentTargets(node.left)) pushWrite(node, target);
|
|
1203
|
+
},
|
|
1204
|
+
ForOfStatement(node) {
|
|
1205
|
+
if (node.left.type === AST_NODE_TYPES.VariableDeclaration) return;
|
|
1206
|
+
for (const target of Extract.getAssignmentTargets(node.left)) pushWrite(node, target);
|
|
1207
|
+
},
|
|
1219
1208
|
UnaryExpression(node) {
|
|
1220
1209
|
if (node.operator !== "delete") return;
|
|
1221
1210
|
const argument = Extract.unwrap(node.argument);
|
|
@@ -1445,7 +1434,7 @@ function resolveToFunctionNode(context, node, seen = /* @__PURE__ */ new Set())
|
|
|
1445
1434
|
if (Check.isFunction(expr)) return expr;
|
|
1446
1435
|
if (!Check.isIdentifier(expr) || seen.has(expr)) return null;
|
|
1447
1436
|
seen.add(expr);
|
|
1448
|
-
const resolved =
|
|
1437
|
+
const resolved = resolveOrigin(context, expr);
|
|
1449
1438
|
return resolved == null ? null : resolveToFunctionNode(context, resolved, seen);
|
|
1450
1439
|
}
|
|
1451
1440
|
function resolveVariableOrigin(context, variable, seen = /* @__PURE__ */ new Set()) {
|
|
@@ -1514,19 +1503,18 @@ function createFactCollector$1() {
|
|
|
1514
1503
|
if (root == null || !Check.isIdentifier(root)) return;
|
|
1515
1504
|
pushMutation("value", node, target, root);
|
|
1516
1505
|
}
|
|
1506
|
+
function pushTargetMutation(node, target) {
|
|
1507
|
+
if (Check.isIdentifier(target)) {
|
|
1508
|
+
pushMutation("binding", node, target, target);
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
pushMemberMutation(node, target);
|
|
1512
|
+
}
|
|
1517
1513
|
return {
|
|
1518
1514
|
facts,
|
|
1519
1515
|
visitor: {
|
|
1520
1516
|
AssignmentExpression(node) {
|
|
1521
|
-
const target
|
|
1522
|
-
switch (target.type) {
|
|
1523
|
-
case AST_NODE_TYPES.Identifier:
|
|
1524
|
-
pushMutation("binding", node, target, target);
|
|
1525
|
-
return;
|
|
1526
|
-
case AST_NODE_TYPES.MemberExpression:
|
|
1527
|
-
pushMemberMutation(node, target);
|
|
1528
|
-
return;
|
|
1529
|
-
}
|
|
1517
|
+
for (const target of Extract.getAssignmentTargets(node.left)) pushTargetMutation(node, target);
|
|
1530
1518
|
},
|
|
1531
1519
|
CallExpression(node) {
|
|
1532
1520
|
const callee = Extract.unwrap(node.callee);
|
|
@@ -1543,6 +1531,14 @@ function createFactCollector$1() {
|
|
|
1543
1531
|
});
|
|
1544
1532
|
}
|
|
1545
1533
|
},
|
|
1534
|
+
ForInStatement(node) {
|
|
1535
|
+
if (node.left.type === AST_NODE_TYPES.VariableDeclaration) return;
|
|
1536
|
+
for (const target of Extract.getAssignmentTargets(node.left)) pushTargetMutation(node, target);
|
|
1537
|
+
},
|
|
1538
|
+
ForOfStatement(node) {
|
|
1539
|
+
if (node.left.type === AST_NODE_TYPES.VariableDeclaration) return;
|
|
1540
|
+
for (const target of Extract.getAssignmentTargets(node.left)) pushTargetMutation(node, target);
|
|
1541
|
+
},
|
|
1546
1542
|
JSXAttribute(node) {
|
|
1547
1543
|
if (node.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) return;
|
|
1548
1544
|
const expression = node.value.expression;
|
|
@@ -4871,8 +4867,159 @@ const IMPURE_CTORS = /* @__PURE__ */ new Set([
|
|
|
4871
4867
|
"XMLHttpRequest"
|
|
4872
4868
|
]);
|
|
4873
4869
|
/**
|
|
4874
|
-
*
|
|
4875
|
-
|
|
4870
|
+
* Known pure functions
|
|
4871
|
+
*/
|
|
4872
|
+
const PURE_FUNCS = /* @__PURE__ */ new Map([
|
|
4873
|
+
["Array", /* @__PURE__ */ new Set([
|
|
4874
|
+
"from",
|
|
4875
|
+
"isArray",
|
|
4876
|
+
"of"
|
|
4877
|
+
])],
|
|
4878
|
+
["Atomics", /* @__PURE__ */ new Set(["isLockFree"])],
|
|
4879
|
+
["BigInt", /* @__PURE__ */ new Set(["asIntN", "asUintN"])],
|
|
4880
|
+
["Date", /* @__PURE__ */ new Set(["parse", "UTC"])],
|
|
4881
|
+
["globalThis", /* @__PURE__ */ new Set([
|
|
4882
|
+
"atob",
|
|
4883
|
+
"Boolean",
|
|
4884
|
+
"btoa",
|
|
4885
|
+
"decodeURI",
|
|
4886
|
+
"decodeURIComponent",
|
|
4887
|
+
"encodeURI",
|
|
4888
|
+
"encodeURIComponent",
|
|
4889
|
+
"escape",
|
|
4890
|
+
"isFinite",
|
|
4891
|
+
"isNaN",
|
|
4892
|
+
"Number",
|
|
4893
|
+
"parseFloat",
|
|
4894
|
+
"parseInt",
|
|
4895
|
+
"String",
|
|
4896
|
+
"unescape"
|
|
4897
|
+
])],
|
|
4898
|
+
["JSON", /* @__PURE__ */ new Set(["parse", "stringify"])],
|
|
4899
|
+
["Math", /* @__PURE__ */ new Set([
|
|
4900
|
+
"abs",
|
|
4901
|
+
"acos",
|
|
4902
|
+
"acosh",
|
|
4903
|
+
"asin",
|
|
4904
|
+
"asinh",
|
|
4905
|
+
"atan",
|
|
4906
|
+
"atan2",
|
|
4907
|
+
"atanh",
|
|
4908
|
+
"cbrt",
|
|
4909
|
+
"ceil",
|
|
4910
|
+
"clz32",
|
|
4911
|
+
"cos",
|
|
4912
|
+
"cosh",
|
|
4913
|
+
"exp",
|
|
4914
|
+
"expm1",
|
|
4915
|
+
"floor",
|
|
4916
|
+
"fround",
|
|
4917
|
+
"hypot",
|
|
4918
|
+
"imul",
|
|
4919
|
+
"log",
|
|
4920
|
+
"log1p",
|
|
4921
|
+
"log2",
|
|
4922
|
+
"log10",
|
|
4923
|
+
"max",
|
|
4924
|
+
"min",
|
|
4925
|
+
"pow",
|
|
4926
|
+
"round",
|
|
4927
|
+
"sign",
|
|
4928
|
+
"sin",
|
|
4929
|
+
"sinh",
|
|
4930
|
+
"sqrt",
|
|
4931
|
+
"tan",
|
|
4932
|
+
"tanh",
|
|
4933
|
+
"trunc"
|
|
4934
|
+
])],
|
|
4935
|
+
["Number", /* @__PURE__ */ new Set([
|
|
4936
|
+
"isFinite",
|
|
4937
|
+
"isInteger",
|
|
4938
|
+
"isNaN",
|
|
4939
|
+
"isSafeInteger",
|
|
4940
|
+
"parseFloat",
|
|
4941
|
+
"parseInt"
|
|
4942
|
+
])],
|
|
4943
|
+
["Object", /* @__PURE__ */ new Set([
|
|
4944
|
+
"entries",
|
|
4945
|
+
"fromEntries",
|
|
4946
|
+
"getOwnPropertyDescriptor",
|
|
4947
|
+
"getOwnPropertyDescriptors",
|
|
4948
|
+
"getOwnPropertyNames",
|
|
4949
|
+
"getOwnPropertySymbols",
|
|
4950
|
+
"getPrototypeOf",
|
|
4951
|
+
"groupBy",
|
|
4952
|
+
"hasOwn",
|
|
4953
|
+
"is",
|
|
4954
|
+
"isExtensible",
|
|
4955
|
+
"isFrozen",
|
|
4956
|
+
"isSealed",
|
|
4957
|
+
"keys",
|
|
4958
|
+
"values"
|
|
4959
|
+
])],
|
|
4960
|
+
["Reflect", /* @__PURE__ */ new Set([
|
|
4961
|
+
"apply",
|
|
4962
|
+
"construct",
|
|
4963
|
+
"get",
|
|
4964
|
+
"getOwnPropertyDescriptor",
|
|
4965
|
+
"getPrototypeOf",
|
|
4966
|
+
"has",
|
|
4967
|
+
"isExtensible",
|
|
4968
|
+
"ownKeys"
|
|
4969
|
+
])],
|
|
4970
|
+
["String", /* @__PURE__ */ new Set([
|
|
4971
|
+
"fromCharCode",
|
|
4972
|
+
"fromCodePoint",
|
|
4973
|
+
"raw"
|
|
4974
|
+
])]
|
|
4975
|
+
]);
|
|
4976
|
+
function isCatalogObject(name) {
|
|
4977
|
+
return IMPURE_FUNCS.has(name) || PURE_FUNCS.has(name);
|
|
4978
|
+
}
|
|
4979
|
+
/**
|
|
4980
|
+
* Find the property key under which `name` is bound in an object destructuring
|
|
4981
|
+
* pattern, e.g. `"random"` for `random` in `const { random } = Math` or for
|
|
4982
|
+
* `r` in `const { random: r } = Math`.
|
|
4983
|
+
*/
|
|
4984
|
+
function getDestructuredPropertyName(pattern, name) {
|
|
4985
|
+
for (const property of pattern.properties) {
|
|
4986
|
+
if (property.type !== AST_NODE_TYPES.Property) continue;
|
|
4987
|
+
if ((property.value.type === AST_NODE_TYPES.AssignmentPattern ? property.value.left : property.value) !== name) continue;
|
|
4988
|
+
return Extract.getPropertyName(property, "max");
|
|
4989
|
+
}
|
|
4990
|
+
return null;
|
|
4991
|
+
}
|
|
4992
|
+
/**
|
|
4993
|
+
* Resolve a member expression to a builtin object name and property, e.g.
|
|
4994
|
+
* `window.Math.random` resolves to `{ object: "Math", property: "random" }`.
|
|
4995
|
+
* Intermediate segments are followed only while they name known catalog
|
|
4996
|
+
* objects (`window`, `Math`, ...), so unknown-global chains like
|
|
4997
|
+
* `foo.Math.random` do not resolve.
|
|
4998
|
+
*/
|
|
4999
|
+
function resolveBuiltinMember(context, node, seen = /* @__PURE__ */ new Set()) {
|
|
5000
|
+
const chain = Extract.getMemberChain(node);
|
|
5001
|
+
const rootId = chain.at(0);
|
|
5002
|
+
if (rootId == null || !Check.isIdentifier(rootId)) return null;
|
|
5003
|
+
const segments = chain.slice(1);
|
|
5004
|
+
if (segments.length === 0 || !segments.every((segment) => Check.isIdentifier(segment))) return null;
|
|
5005
|
+
const resolved = resolveBuiltinObjectName(context, rootId, seen);
|
|
5006
|
+
if (resolved == null) return null;
|
|
5007
|
+
let object = resolved.property ?? resolved.object;
|
|
5008
|
+
const names = segments.map((segment) => segment.name);
|
|
5009
|
+
if (isCatalogObject(object)) for (const name of names.slice(0, -1)) {
|
|
5010
|
+
if (!isCatalogObject(name)) break;
|
|
5011
|
+
object = name;
|
|
5012
|
+
}
|
|
5013
|
+
return {
|
|
5014
|
+
object,
|
|
5015
|
+
property: names.at(-1) ?? null
|
|
5016
|
+
};
|
|
5017
|
+
}
|
|
5018
|
+
/**
|
|
5019
|
+
* Recursively resolve an identifier to a builtin global object name and
|
|
5020
|
+
* optional property. Follows simple assignment chains like `const M = Math`,
|
|
5021
|
+
* member-function aliases like `const random = Math.random`, and destructured
|
|
5022
|
+
* aliases like `const { random } = Math`.
|
|
4876
5023
|
* Returns `null` if the identifier is locally defined (parameter, import, function declaration, etc.)
|
|
4877
5024
|
* or resolves to a non-builtin source.
|
|
4878
5025
|
* @param context - The rule context.
|
|
@@ -4884,17 +5031,33 @@ function resolveBuiltinObjectName(context, node, seen = /* @__PURE__ */ new Set(
|
|
|
4884
5031
|
seen.add(node.name);
|
|
4885
5032
|
const scope = context.sourceCode.getScope(node);
|
|
4886
5033
|
const variable = findVariable(scope, node);
|
|
4887
|
-
if (variable == null) return
|
|
5034
|
+
if (variable == null) return {
|
|
5035
|
+
object: node.name,
|
|
5036
|
+
property: null
|
|
5037
|
+
};
|
|
4888
5038
|
const def = variable.defs[0];
|
|
4889
|
-
if (def == null) return
|
|
4890
|
-
|
|
5039
|
+
if (def == null) return {
|
|
5040
|
+
object: node.name,
|
|
5041
|
+
property: null
|
|
5042
|
+
};
|
|
5043
|
+
if (def.type === DefinitionType.ImplicitGlobalVariable) return {
|
|
5044
|
+
object: node.name,
|
|
5045
|
+
property: null
|
|
5046
|
+
};
|
|
4891
5047
|
if (def.type === DefinitionType.Variable && def.node.init != null) {
|
|
4892
5048
|
const init = Extract.unwrap(def.node.init);
|
|
4893
|
-
|
|
4894
|
-
if (
|
|
4895
|
-
|
|
4896
|
-
|
|
5049
|
+
const resolved = Check.isIdentifier(init) ? resolveBuiltinObjectName(context, init, seen) : init.type === AST_NODE_TYPES.MemberExpression ? resolveBuiltinMember(context, init, seen) : null;
|
|
5050
|
+
if (resolved == null) return null;
|
|
5051
|
+
if (def.node.id.type === AST_NODE_TYPES.ObjectPattern) {
|
|
5052
|
+
const key = getDestructuredPropertyName(def.node.id, def.name);
|
|
5053
|
+
if (key == null) return null;
|
|
5054
|
+
return {
|
|
5055
|
+
object: resolved.property ?? resolved.object,
|
|
5056
|
+
property: key
|
|
5057
|
+
};
|
|
4897
5058
|
}
|
|
5059
|
+
if (def.node.id.type === AST_NODE_TYPES.ArrayPattern) return null;
|
|
5060
|
+
return resolved;
|
|
4898
5061
|
}
|
|
4899
5062
|
return null;
|
|
4900
5063
|
}
|
|
@@ -4923,10 +5086,9 @@ function create$7(context) {
|
|
|
4923
5086
|
const expr = Extract.unwrap(node.callee);
|
|
4924
5087
|
switch (true) {
|
|
4925
5088
|
case Check.isIdentifier(expr): {
|
|
4926
|
-
const
|
|
4927
|
-
if (
|
|
4928
|
-
|
|
4929
|
-
if (globalThisImpure == null || !globalThisImpure.has(builtinName)) return;
|
|
5089
|
+
const resolved = resolveBuiltinObjectName(context, expr);
|
|
5090
|
+
if (resolved == null) return;
|
|
5091
|
+
if (!(resolved.property != null ? IMPURE_FUNCS.get(resolved.object)?.has(resolved.property) ?? false : IMPURE_FUNCS.get("globalThis")?.has(resolved.object) ?? false)) return;
|
|
4930
5092
|
const func = Traverse.findParent(node, Check.isFunction);
|
|
4931
5093
|
if (func == null) return;
|
|
4932
5094
|
cEntries.push({
|
|
@@ -4936,13 +5098,9 @@ function create$7(context) {
|
|
|
4936
5098
|
break;
|
|
4937
5099
|
}
|
|
4938
5100
|
case expr.type === AST_NODE_TYPES.MemberExpression && Check.isIdentifier(expr.property): {
|
|
4939
|
-
const
|
|
4940
|
-
if (
|
|
4941
|
-
|
|
4942
|
-
if (objectName == null) return;
|
|
4943
|
-
const propertyName = expr.property.name;
|
|
4944
|
-
const objectImpure = IMPURE_FUNCS.get(objectName);
|
|
4945
|
-
if (objectImpure == null || !objectImpure.has(propertyName)) return;
|
|
5101
|
+
const resolved = resolveBuiltinMember(context, expr);
|
|
5102
|
+
if (resolved == null || resolved.property == null) return;
|
|
5103
|
+
if (!IMPURE_FUNCS.get(resolved.object)?.has(resolved.property)) return;
|
|
4946
5104
|
const func = Traverse.findParent(node, Check.isFunction);
|
|
4947
5105
|
if (func == null) return;
|
|
4948
5106
|
cEntries.push({
|
|
@@ -4956,10 +5114,11 @@ function create$7(context) {
|
|
|
4956
5114
|
NewExpression(node) {
|
|
4957
5115
|
const expr = Extract.unwrap(node.callee);
|
|
4958
5116
|
if (!Check.isIdentifier(expr)) return;
|
|
4959
|
-
const
|
|
4960
|
-
if (
|
|
4961
|
-
|
|
4962
|
-
if (
|
|
5117
|
+
const resolved = resolveBuiltinObjectName(context, expr);
|
|
5118
|
+
if (resolved == null) return;
|
|
5119
|
+
const ctorName = resolved.property != null && isCatalogObject(resolved.object) ? resolved.property : resolved.object;
|
|
5120
|
+
if (!IMPURE_CTORS.has(ctorName)) return;
|
|
5121
|
+
if (ctorName === "Date" && node.arguments.length > 0) return;
|
|
4963
5122
|
const func = Traverse.findParent(node, Check.isFunction);
|
|
4964
5123
|
if (func == null) return;
|
|
4965
5124
|
nEntries.push({
|
|
@@ -5029,14 +5188,47 @@ function isReachedThroughFunctions(node, boundary, reached) {
|
|
|
5029
5188
|
function getRefAccess(node) {
|
|
5030
5189
|
let parent = node.parent;
|
|
5031
5190
|
while (Check.isTypeExpression(parent)) parent = parent.parent;
|
|
5032
|
-
const isDirectWrite = parent.type === AST_NODE_TYPES.AssignmentExpression ? parent.left === node || Extract.unwrap(parent.left) === node : parent.type === AST_NODE_TYPES.UpdateExpression ? parent.argument === node || Extract.unwrap(parent.argument) === node : false;
|
|
5191
|
+
const isDirectWrite = parent.type === AST_NODE_TYPES.AssignmentExpression ? parent.left === node || Extract.unwrap(parent.left) === node : parent.type === AST_NODE_TYPES.UpdateExpression ? parent.argument === node || Extract.unwrap(parent.argument) === node : parent.type === AST_NODE_TYPES.UnaryExpression && parent.operator === "delete" ? parent.argument === node || Extract.unwrap(parent.argument) === node : false;
|
|
5033
5192
|
return {
|
|
5034
5193
|
isInitializationWrite: parent.type === AST_NODE_TYPES.AssignmentExpression && parent.operator === "=" && (parent.left === node || Extract.unwrap(parent.left) === node),
|
|
5035
|
-
isWrite: isDirectWrite || isNestedRefCurrentWrite(node),
|
|
5194
|
+
isWrite: isDirectWrite || isNestedRefCurrentWrite(node) || isPatternWriteTarget(node),
|
|
5036
5195
|
node
|
|
5037
5196
|
};
|
|
5038
5197
|
}
|
|
5039
5198
|
/**
|
|
5199
|
+
* Check whether `node` (a `ref.current` MemberExpression) is assigned through a
|
|
5200
|
+
* destructuring pattern or a for-in/of loop target, e.g.
|
|
5201
|
+
* `({ a: ref.current } = value)`, `[ref.current] = value`,
|
|
5202
|
+
* `({ a: ref.current.x } = value)`, or `for (ref.current of items)`. Defaults
|
|
5203
|
+
* and computed keys inside patterns are reads, not writes.
|
|
5204
|
+
*/
|
|
5205
|
+
function isPatternWriteTarget(node) {
|
|
5206
|
+
let child = node;
|
|
5207
|
+
let current = node.parent;
|
|
5208
|
+
while (true) {
|
|
5209
|
+
switch (current.type) {
|
|
5210
|
+
case AST_NODE_TYPES.MemberExpression:
|
|
5211
|
+
if (current.object !== child && Extract.unwrap(current.object) !== child) return false;
|
|
5212
|
+
break;
|
|
5213
|
+
case AST_NODE_TYPES.Property:
|
|
5214
|
+
if (current.value !== child) return false;
|
|
5215
|
+
break;
|
|
5216
|
+
case AST_NODE_TYPES.ObjectPattern:
|
|
5217
|
+
case AST_NODE_TYPES.ArrayPattern:
|
|
5218
|
+
case AST_NODE_TYPES.RestElement: break;
|
|
5219
|
+
case AST_NODE_TYPES.AssignmentPattern:
|
|
5220
|
+
if (current.left !== child) return false;
|
|
5221
|
+
break;
|
|
5222
|
+
case AST_NODE_TYPES.AssignmentExpression: return current.operator === "=" && current.left === child;
|
|
5223
|
+
case AST_NODE_TYPES.ForInStatement:
|
|
5224
|
+
case AST_NODE_TYPES.ForOfStatement: return current.left === child;
|
|
5225
|
+
default: return false;
|
|
5226
|
+
}
|
|
5227
|
+
child = current;
|
|
5228
|
+
current = current.parent;
|
|
5229
|
+
}
|
|
5230
|
+
}
|
|
5231
|
+
/**
|
|
5040
5232
|
* Check whether `node` (a `ref.current` MemberExpression) is being written to indirectly through
|
|
5041
5233
|
* a nested property write, e.g. `ref.current.inner = value` or `ref.current.inner++`.
|
|
5042
5234
|
*/
|
|
@@ -5054,6 +5246,7 @@ function isNestedRefCurrentWrite(node) {
|
|
|
5054
5246
|
if (!sawNesting) return false;
|
|
5055
5247
|
if (parent.type === AST_NODE_TYPES.AssignmentExpression) return parent.left === outer || Extract.unwrap(parent.left) === outer;
|
|
5056
5248
|
if (parent.type === AST_NODE_TYPES.UpdateExpression) return parent.argument === outer || Extract.unwrap(parent.argument) === outer;
|
|
5249
|
+
if (parent.type === AST_NODE_TYPES.UnaryExpression && parent.operator === "delete") return parent.argument === outer || Extract.unwrap(parent.argument) === outer;
|
|
5057
5250
|
return false;
|
|
5058
5251
|
}
|
|
5059
5252
|
}
|
|
@@ -5137,15 +5330,17 @@ function isAfterTerminatingNonNullGuard(node, getNullBranch) {
|
|
|
5137
5330
|
/**
|
|
5138
5331
|
* Collect the raw facts the refs rule reasons about: identifier/member/function
|
|
5139
5332
|
* bindings with their source positions, JSX `ref` attribute targets, call
|
|
5140
|
-
* expressions,
|
|
5141
|
-
*
|
|
5142
|
-
* deferred to
|
|
5333
|
+
* expressions, ref pass sites (calls, constructor calls, tagged templates), and
|
|
5334
|
+
* every `.current` member access. Facts outside any function are never
|
|
5335
|
+
* reachable during render and are not collected. All inference is deferred to
|
|
5336
|
+
* `Program:exit`.
|
|
5143
5337
|
*/
|
|
5144
5338
|
function createFactCollector() {
|
|
5145
5339
|
const facts = {
|
|
5146
5340
|
bindings: [],
|
|
5147
5341
|
callEdges: [],
|
|
5148
5342
|
jsxRefs: [],
|
|
5343
|
+
passSites: [],
|
|
5149
5344
|
refAccesses: []
|
|
5150
5345
|
};
|
|
5151
5346
|
return {
|
|
@@ -5180,6 +5375,10 @@ function createFactCollector() {
|
|
|
5180
5375
|
caller,
|
|
5181
5376
|
node
|
|
5182
5377
|
});
|
|
5378
|
+
facts.passSites.push({
|
|
5379
|
+
caller,
|
|
5380
|
+
node
|
|
5381
|
+
});
|
|
5183
5382
|
},
|
|
5184
5383
|
FunctionDeclaration(node) {
|
|
5185
5384
|
if (node.id != null) facts.bindings.push({
|
|
@@ -5204,6 +5403,22 @@ function createFactCollector() {
|
|
|
5204
5403
|
enclosingFunction
|
|
5205
5404
|
});
|
|
5206
5405
|
},
|
|
5406
|
+
NewExpression(node) {
|
|
5407
|
+
const caller = Traverse.findParent(node, Check.isFunction);
|
|
5408
|
+
if (caller == null) return;
|
|
5409
|
+
facts.passSites.push({
|
|
5410
|
+
caller,
|
|
5411
|
+
node
|
|
5412
|
+
});
|
|
5413
|
+
},
|
|
5414
|
+
TaggedTemplateExpression(node) {
|
|
5415
|
+
const caller = Traverse.findParent(node, Check.isFunction);
|
|
5416
|
+
if (caller == null) return;
|
|
5417
|
+
facts.passSites.push({
|
|
5418
|
+
caller,
|
|
5419
|
+
node
|
|
5420
|
+
});
|
|
5421
|
+
},
|
|
5207
5422
|
VariableDeclarator(node) {
|
|
5208
5423
|
if (!Check.isIdentifier(node.id) || node.init == null) return;
|
|
5209
5424
|
facts.bindings.push({
|
|
@@ -5322,21 +5537,43 @@ function inferRefViolations(accesses, boundaries, reachability, resolver) {
|
|
|
5322
5537
|
return violations;
|
|
5323
5538
|
}
|
|
5324
5539
|
/**
|
|
5540
|
+
* Get the statically known name of a pass-site callee, whether it is a plain
|
|
5541
|
+
* call, a constructor call, or a tagged template.
|
|
5542
|
+
*/
|
|
5543
|
+
function getPassSiteCalleeName(node) {
|
|
5544
|
+
const callee = Extract.unwrap(node.type === AST_NODE_TYPES.TaggedTemplateExpression ? node.tag : node.callee);
|
|
5545
|
+
if (Check.isIdentifier(callee)) return callee.name;
|
|
5546
|
+
if (callee.type === AST_NODE_TYPES.MemberExpression && !callee.computed && Check.isIdentifier(callee.property)) return callee.property.name;
|
|
5547
|
+
return null;
|
|
5548
|
+
}
|
|
5549
|
+
/**
|
|
5550
|
+
* The `mergeRefs` exemption is name-based but alias-aware: a simple variable
|
|
5551
|
+
* alias of a `mergeRefs` binding keeps the exemption.
|
|
5552
|
+
*/
|
|
5553
|
+
function isMergeRefsCallee(callee, calleeName, position, resolver) {
|
|
5554
|
+
if (calleeName === "mergeRefs") return true;
|
|
5555
|
+
if (callee.type !== AST_NODE_TYPES.Identifier) return false;
|
|
5556
|
+
const variable = resolver.getVariable(callee);
|
|
5557
|
+
return variable != null && resolver.resolveAliasTarget(variable, position).name === "mergeRefs";
|
|
5558
|
+
}
|
|
5559
|
+
/**
|
|
5325
5560
|
* Passing a ref to an unknown function can expose its value during render.
|
|
5326
5561
|
* Hook callbacks, mergeRefs, and the existing render-prop compatibility case
|
|
5327
5562
|
* remain exempt.
|
|
5328
5563
|
*/
|
|
5329
|
-
function inferRefPassViolations(
|
|
5564
|
+
function inferRefPassViolations(passSites, boundaries, reachability, resolver) {
|
|
5330
5565
|
const violations = [];
|
|
5331
|
-
for (const
|
|
5332
|
-
const boundary = getBoundaryOf(
|
|
5333
|
-
if (boundary == null || !isReachedDuringRender(
|
|
5334
|
-
const
|
|
5335
|
-
const callee = Extract.unwrap(
|
|
5336
|
-
const calleeName =
|
|
5337
|
-
const
|
|
5338
|
-
if (
|
|
5339
|
-
|
|
5566
|
+
for (const site of passSites) {
|
|
5567
|
+
const boundary = getBoundaryOf(site.caller, boundaries);
|
|
5568
|
+
if (boundary == null || !isReachedDuringRender(site.node, boundary, reachability)) continue;
|
|
5569
|
+
const node = site.node;
|
|
5570
|
+
const callee = Extract.unwrap(node.type === AST_NODE_TYPES.TaggedTemplateExpression ? node.tag : node.callee);
|
|
5571
|
+
const calleeName = getPassSiteCalleeName(node);
|
|
5572
|
+
const args = node.type === AST_NODE_TYPES.TaggedTemplateExpression ? node.quasi.expressions : node.arguments;
|
|
5573
|
+
if (node.type === AST_NODE_TYPES.CallExpression && core.isHookCall(node)) continue;
|
|
5574
|
+
if (isMergeRefsCallee(callee, calleeName, node.range[0], resolver)) continue;
|
|
5575
|
+
if (calleeName === "render" && callee.type === AST_NODE_TYPES.MemberExpression) continue;
|
|
5576
|
+
for (const argument of args) {
|
|
5340
5577
|
if (argument.type === AST_NODE_TYPES.SpreadElement) continue;
|
|
5341
5578
|
const value = Extract.unwrap(argument);
|
|
5342
5579
|
if (!Check.isIdentifier(value)) continue;
|
|
@@ -5450,6 +5687,18 @@ function createBindingResolver(context, facts) {
|
|
|
5450
5687
|
case "unknown": return null;
|
|
5451
5688
|
}
|
|
5452
5689
|
}
|
|
5690
|
+
/**
|
|
5691
|
+
* Follow simple variable-to-variable aliases to the terminal variable, e.g.
|
|
5692
|
+
* `const combine = mergeRefs` resolves to the `mergeRefs` variable itself.
|
|
5693
|
+
* Position-aware, so a reassigned alias resolves to its latest source.
|
|
5694
|
+
*/
|
|
5695
|
+
function resolveAliasTarget(variable, position, seen = /* @__PURE__ */ new Set()) {
|
|
5696
|
+
if (seen.has(variable)) return variable;
|
|
5697
|
+
seen.add(variable);
|
|
5698
|
+
const event = getLatestValue(bindings.get(variable), position);
|
|
5699
|
+
if (event != null && event.value.kind === "variable") return resolveAliasTarget(event.value.variable, event.position, seen);
|
|
5700
|
+
return variable;
|
|
5701
|
+
}
|
|
5453
5702
|
function resolveCallable(node, position) {
|
|
5454
5703
|
const callee = Extract.unwrap(node);
|
|
5455
5704
|
if (isFunctionExpressionLike(callee)) return callee;
|
|
@@ -5499,6 +5748,7 @@ function createBindingResolver(context, facts) {
|
|
|
5499
5748
|
getNullBranch,
|
|
5500
5749
|
getRefTarget,
|
|
5501
5750
|
getVariable,
|
|
5751
|
+
resolveAliasTarget,
|
|
5502
5752
|
resolveCallable,
|
|
5503
5753
|
resolveRef
|
|
5504
5754
|
};
|
|
@@ -5540,7 +5790,7 @@ function create$6(context) {
|
|
|
5540
5790
|
const resolver = createBindingResolver(context, facts.facts);
|
|
5541
5791
|
const callGraph = inferCallGraph(boundaries, facts.facts.callEdges, resolver.resolveCallable);
|
|
5542
5792
|
const reachability = collectReachableFunctions(boundaries, callGraph);
|
|
5543
|
-
const violations = [...inferRefViolations(facts.facts.refAccesses, boundaries, reachability, resolver), ...inferRefPassViolations(facts.facts.
|
|
5793
|
+
const violations = [...inferRefViolations(facts.facts.refAccesses, boundaries, reachability, resolver), ...inferRefPassViolations(facts.facts.passSites, boundaries, reachability, resolver)];
|
|
5544
5794
|
for (const violation of violations) context.report({
|
|
5545
5795
|
messageId: match(violation).with({ kind: "duplicate-init" }, () => "duplicateRefInit").with({ kind: "pass" }, () => "refPassedToFunction").with({ kind: "read" }, () => "readDuringRender").with({ kind: "write" }, () => "writeDuringRender").exhaustive(),
|
|
5546
5796
|
node: violation.node
|
|
@@ -7887,7 +8137,7 @@ function create$5(context) {
|
|
|
7887
8137
|
}
|
|
7888
8138
|
}
|
|
7889
8139
|
function isIdFromUseStateCall(id, at) {
|
|
7890
|
-
const initNode =
|
|
8140
|
+
const initNode = resolveOrigin(context, id);
|
|
7891
8141
|
if (initNode == null) return false;
|
|
7892
8142
|
if (initNode.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
7893
8143
|
if (!isUseStateCall(initNode)) return false;
|
|
@@ -8012,7 +8262,7 @@ function create$5(context) {
|
|
|
8012
8262
|
const getSetStateCalls = (context, id) => {
|
|
8013
8263
|
const variable = findVariable(context.sourceCode.getScope(id), id);
|
|
8014
8264
|
if (variable != null && variable.defs.some((def) => def.type === DefinitionType.Parameter)) return [];
|
|
8015
|
-
const node =
|
|
8265
|
+
const node = resolveOrigin(context, id);
|
|
8016
8266
|
switch (node?.type) {
|
|
8017
8267
|
case AST_NODE_TYPES.ArrowFunctionExpression:
|
|
8018
8268
|
case AST_NODE_TYPES.FunctionDeclaration:
|
|
@@ -8112,7 +8362,7 @@ function create$4(context) {
|
|
|
8112
8362
|
return core.isUseStateLikeCall(node, additionalStateHooks);
|
|
8113
8363
|
}
|
|
8114
8364
|
function isIdFromUseStateCall(topLevelId, at) {
|
|
8115
|
-
const initNode =
|
|
8365
|
+
const initNode = resolveOrigin(context, topLevelId);
|
|
8116
8366
|
if (initNode == null) return false;
|
|
8117
8367
|
if (initNode.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
8118
8368
|
if (!isUseStateCall(initNode)) return false;
|
|
@@ -8473,18 +8723,28 @@ function create$1(context) {
|
|
|
8473
8723
|
function validateNoOuterVariableReassignment(callback) {
|
|
8474
8724
|
const violations = [];
|
|
8475
8725
|
if (callback.body == null) return violations;
|
|
8476
|
-
|
|
8477
|
-
if (
|
|
8478
|
-
const left = Extract.unwrap(node.left);
|
|
8479
|
-
if (!Check.isIdentifier(left)) return;
|
|
8726
|
+
function checkWriteTarget(node, target) {
|
|
8727
|
+
if (!Check.isIdentifier(target)) return;
|
|
8480
8728
|
if (Traverse.findParent(node, Check.isFunction, (n) => n === callback) != null) return;
|
|
8481
|
-
const scope = context.sourceCode.getScope(
|
|
8482
|
-
const variable = findVariable(scope,
|
|
8729
|
+
const scope = context.sourceCode.getScope(target);
|
|
8730
|
+
const variable = findVariable(scope, target);
|
|
8483
8731
|
if (variable != null && variable.defs.length > 0 && isDeclaredInsideCallback(variable, callback)) return;
|
|
8484
8732
|
violations.push({
|
|
8485
8733
|
messageId: "noReassigningOuterVariables",
|
|
8486
|
-
node:
|
|
8734
|
+
node: target
|
|
8487
8735
|
});
|
|
8736
|
+
}
|
|
8737
|
+
simpleTraverse(callback.body, { enter(node) {
|
|
8738
|
+
switch (node.type) {
|
|
8739
|
+
case AST_NODE_TYPES.AssignmentExpression:
|
|
8740
|
+
for (const target of Extract.getAssignmentTargets(node.left)) checkWriteTarget(node, target);
|
|
8741
|
+
return;
|
|
8742
|
+
case AST_NODE_TYPES.ForInStatement:
|
|
8743
|
+
case AST_NODE_TYPES.ForOfStatement:
|
|
8744
|
+
if (node.left.type === AST_NODE_TYPES.VariableDeclaration) return;
|
|
8745
|
+
for (const target of Extract.getAssignmentTargets(node.left)) checkWriteTarget(node, target);
|
|
8746
|
+
return;
|
|
8747
|
+
}
|
|
8488
8748
|
} });
|
|
8489
8749
|
return violations;
|
|
8490
8750
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-react-x",
|
|
3
|
-
"version": "5.20.
|
|
3
|
+
"version": "5.20.6",
|
|
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,12 +36,12 @@
|
|
|
36
36
|
"dist"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@eslint-react/ast": "5.20.
|
|
40
|
-
"@eslint-react/core": "5.20.
|
|
41
|
-
"@eslint-react/eslint": "5.20.
|
|
42
|
-
"@eslint-react/jsx": "5.20.
|
|
43
|
-
"@eslint-react/shared": "5.20.
|
|
44
|
-
"@eslint-react/var": "5.20.
|
|
39
|
+
"@eslint-react/ast": "5.20.6",
|
|
40
|
+
"@eslint-react/core": "5.20.6",
|
|
41
|
+
"@eslint-react/eslint": "5.20.6",
|
|
42
|
+
"@eslint-react/jsx": "5.20.6",
|
|
43
|
+
"@eslint-react/shared": "5.20.6",
|
|
44
|
+
"@eslint-react/var": "5.20.6",
|
|
45
45
|
"@typescript-eslint/scope-manager": "^8.70.0",
|
|
46
46
|
"@typescript-eslint/type-utils": "^8.70.0",
|
|
47
47
|
"@typescript-eslint/types": "^8.70.0",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
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.6",
|
|
67
67
|
"typescript": "6.0.3"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|