eslint-plugin-react-x 5.20.1 → 5.20.3
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 +446 -316
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -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.3";
|
|
149
149
|
|
|
150
150
|
//#endregion
|
|
151
151
|
//#region src/utils/create-rule.ts
|
|
@@ -1174,7 +1174,7 @@ function resolveToFunction(context, node, seen = /* @__PURE__ */ new Set()) {
|
|
|
1174
1174
|
|
|
1175
1175
|
//#endregion
|
|
1176
1176
|
//#region src/rules/globals/collect.ts
|
|
1177
|
-
function
|
|
1177
|
+
function createFactCollector$2() {
|
|
1178
1178
|
const facts = {
|
|
1179
1179
|
callEdges: [],
|
|
1180
1180
|
methodCalls: [],
|
|
@@ -1312,7 +1312,7 @@ function inferGlobalMutations(context, facts) {
|
|
|
1312
1312
|
return directEffects;
|
|
1313
1313
|
}
|
|
1314
1314
|
/** Resolve the collected call edges into a function-to-function call graph. */
|
|
1315
|
-
function inferCallGraph(context, callEdges) {
|
|
1315
|
+
function inferCallGraph$1(context, callEdges) {
|
|
1316
1316
|
const callGraph = /* @__PURE__ */ new Map();
|
|
1317
1317
|
for (const edge of callEdges) {
|
|
1318
1318
|
const callee = resolveToFunction(context, edge.callee);
|
|
@@ -1367,11 +1367,11 @@ var globals_default = createRule({
|
|
|
1367
1367
|
function create$49(context) {
|
|
1368
1368
|
const hooks = core.getHookCollector(context);
|
|
1369
1369
|
const comps = core.getFunctionComponentCollector(context);
|
|
1370
|
-
const
|
|
1371
|
-
return merge(hooks.visitor, comps.visitor,
|
|
1370
|
+
const facts = createFactCollector$2();
|
|
1371
|
+
return merge(hooks.visitor, comps.visitor, facts.visitor, { "Program:exit"(program) {
|
|
1372
1372
|
const renderFunctions = [...comps.api.getAllComponents(program), ...hooks.api.getAllHooks(program)].map(({ node }) => node);
|
|
1373
|
-
const directEffects = inferGlobalMutations(context,
|
|
1374
|
-
const callGraph = inferCallGraph(context,
|
|
1373
|
+
const directEffects = inferGlobalMutations(context, facts.facts);
|
|
1374
|
+
const callGraph = inferCallGraph$1(context, facts.facts.callEdges);
|
|
1375
1375
|
for (const effect of collectReachableEffects(renderFunctions, directEffects, callGraph)) {
|
|
1376
1376
|
const data = effect.method == null ? { name: effect.name } : {
|
|
1377
1377
|
name: effect.name,
|
|
@@ -1462,13 +1462,13 @@ function isRefLikeName$1(name) {
|
|
|
1462
1462
|
return name === "ref" || name.endsWith("Ref");
|
|
1463
1463
|
}
|
|
1464
1464
|
function hasRefLikeNameInChain(node) {
|
|
1465
|
-
|
|
1466
|
-
if (
|
|
1467
|
-
return Check.isIdentifier(
|
|
1465
|
+
const unwrapped = Extract.unwrap(node);
|
|
1466
|
+
if (!Check.isExpression(unwrapped)) return false;
|
|
1467
|
+
return Extract.getMemberChain(unwrapped).some((member) => Check.isIdentifier(member) && isRefLikeName$1(member.name));
|
|
1468
1468
|
}
|
|
1469
1469
|
function isInitializedFromCall(context, node, isCall) {
|
|
1470
|
-
const root =
|
|
1471
|
-
if (root == null) return false;
|
|
1470
|
+
const root = Extract.getMemberChain(node).at(0);
|
|
1471
|
+
if (root == null || !Check.isIdentifier(root)) return false;
|
|
1472
1472
|
const variable = findVariable(context.sourceCode.getScope(root), root);
|
|
1473
1473
|
if (variable == null) return false;
|
|
1474
1474
|
const origin = resolveVariableOrigin(context, variable);
|
|
@@ -1493,7 +1493,7 @@ function isRefLikeChain(context, node) {
|
|
|
1493
1493
|
|
|
1494
1494
|
//#endregion
|
|
1495
1495
|
//#region src/rules/immutability/collect.ts
|
|
1496
|
-
function
|
|
1496
|
+
function createFactCollector$1() {
|
|
1497
1497
|
const facts = {
|
|
1498
1498
|
mutations: [],
|
|
1499
1499
|
sinks: []
|
|
@@ -1509,6 +1509,11 @@ function createImmutabilityCollector() {
|
|
|
1509
1509
|
target
|
|
1510
1510
|
});
|
|
1511
1511
|
}
|
|
1512
|
+
function pushMemberMutation(node, target) {
|
|
1513
|
+
const root = Extract.getMemberChain(target).at(0);
|
|
1514
|
+
if (root == null || !Check.isIdentifier(root)) return;
|
|
1515
|
+
pushMutation("value", node, target, root);
|
|
1516
|
+
}
|
|
1512
1517
|
return {
|
|
1513
1518
|
facts,
|
|
1514
1519
|
visitor: {
|
|
@@ -1518,21 +1523,16 @@ function createImmutabilityCollector() {
|
|
|
1518
1523
|
case AST_NODE_TYPES.Identifier:
|
|
1519
1524
|
pushMutation("binding", node, target, target);
|
|
1520
1525
|
return;
|
|
1521
|
-
case AST_NODE_TYPES.MemberExpression:
|
|
1522
|
-
|
|
1523
|
-
if (root != null) pushMutation("value", node, target, root);
|
|
1526
|
+
case AST_NODE_TYPES.MemberExpression:
|
|
1527
|
+
pushMemberMutation(node, target);
|
|
1524
1528
|
return;
|
|
1525
|
-
}
|
|
1526
1529
|
}
|
|
1527
1530
|
},
|
|
1528
1531
|
CallExpression(node) {
|
|
1529
1532
|
const callee = Extract.unwrap(node.callee);
|
|
1530
1533
|
if (callee.type === AST_NODE_TYPES.MemberExpression) {
|
|
1531
1534
|
const method = Extract.getCalleeName(node);
|
|
1532
|
-
if (method != null && KNOWN_MUTATING_METHODS.has(method))
|
|
1533
|
-
const root = Extract.getIdentifierAt(callee.object, 0);
|
|
1534
|
-
if (root != null) pushMutation("value", node, callee.object, root);
|
|
1535
|
-
}
|
|
1535
|
+
if (method != null && KNOWN_MUTATING_METHODS.has(method)) pushMemberMutation(node, callee.object);
|
|
1536
1536
|
}
|
|
1537
1537
|
if (!core.isHookCall(node)) return;
|
|
1538
1538
|
for (const argument of node.arguments) {
|
|
@@ -1556,8 +1556,7 @@ function createImmutabilityCollector() {
|
|
|
1556
1556
|
if (node.operator !== "delete") return;
|
|
1557
1557
|
const target = Extract.unwrap(node.argument);
|
|
1558
1558
|
if (target.type !== AST_NODE_TYPES.MemberExpression) return;
|
|
1559
|
-
|
|
1560
|
-
if (root != null) pushMutation("value", node, target, root);
|
|
1559
|
+
pushMemberMutation(node, target);
|
|
1561
1560
|
},
|
|
1562
1561
|
UpdateExpression(node) {
|
|
1563
1562
|
const target = Extract.unwrap(node.argument);
|
|
@@ -1565,11 +1564,9 @@ function createImmutabilityCollector() {
|
|
|
1565
1564
|
case AST_NODE_TYPES.Identifier:
|
|
1566
1565
|
pushMutation("binding", node, target, target);
|
|
1567
1566
|
return;
|
|
1568
|
-
case AST_NODE_TYPES.MemberExpression:
|
|
1569
|
-
|
|
1570
|
-
if (root != null) pushMutation("value", node, target, root);
|
|
1567
|
+
case AST_NODE_TYPES.MemberExpression:
|
|
1568
|
+
pushMemberMutation(node, target);
|
|
1571
1569
|
return;
|
|
1572
|
-
}
|
|
1573
1570
|
}
|
|
1574
1571
|
}
|
|
1575
1572
|
}
|
|
@@ -1603,8 +1600,8 @@ function classifyFrozenOrigin(context, variable, components, seen = /* @__PURE__
|
|
|
1603
1600
|
if (def.node.init == null) {
|
|
1604
1601
|
const loop = def.node.parent.parent;
|
|
1605
1602
|
if (loop.type !== AST_NODE_TYPES.ForOfStatement || loop.left !== def.node.parent) return null;
|
|
1606
|
-
const root = Extract.
|
|
1607
|
-
if (root == null) return null;
|
|
1603
|
+
const root = Extract.getMemberChain(loop.right).at(0);
|
|
1604
|
+
if (root == null || !Check.isIdentifier(root)) return null;
|
|
1608
1605
|
const source = findVariable(context.sourceCode.getScope(root), root);
|
|
1609
1606
|
if (source == null) return null;
|
|
1610
1607
|
const inner = classifyFrozenOrigin(context, source, components, seen);
|
|
@@ -1750,17 +1747,17 @@ var immutability_default = createRule({
|
|
|
1750
1747
|
function create$48(context) {
|
|
1751
1748
|
const hooks = core.getHookCollector(context);
|
|
1752
1749
|
const comps = core.getFunctionComponentCollector(context);
|
|
1753
|
-
const
|
|
1754
|
-
return merge(hooks.visitor, comps.visitor,
|
|
1755
|
-
for (const hook of hooks.api.getAllHooks(program)) for (const expression of hook.rets) if (expression != null)
|
|
1750
|
+
const facts = createFactCollector$1();
|
|
1751
|
+
return merge(hooks.visitor, comps.visitor, facts.visitor, { "Program:exit"(program) {
|
|
1752
|
+
for (const hook of hooks.api.getAllHooks(program)) for (const expression of hook.rets) if (expression != null) facts.facts.sinks.push({
|
|
1756
1753
|
kind: "hook-return",
|
|
1757
1754
|
expression
|
|
1758
1755
|
});
|
|
1759
1756
|
const reportedMutations = /* @__PURE__ */ new Set();
|
|
1760
|
-
const mutableFunctions = inferMutableFunctions(context,
|
|
1757
|
+
const mutableFunctions = inferMutableFunctions(context, facts.facts.mutations);
|
|
1761
1758
|
if (mutableFunctions.size > 0) {
|
|
1762
1759
|
const reportedSinks = /* @__PURE__ */ new Set();
|
|
1763
|
-
for (const sink of
|
|
1760
|
+
for (const sink of facts.facts.sinks) {
|
|
1764
1761
|
const expression = sink.expression;
|
|
1765
1762
|
if (reportedSinks.has(expression)) continue;
|
|
1766
1763
|
const fn = resolveToFunctionNode(context, expression);
|
|
@@ -1782,7 +1779,7 @@ function create$48(context) {
|
|
|
1782
1779
|
}
|
|
1783
1780
|
}
|
|
1784
1781
|
const funcs = comps.api.getAllComponents(program).map((comp) => comp.node);
|
|
1785
|
-
for (const mutation of inferDirectMutations(context,
|
|
1782
|
+
for (const mutation of inferDirectMutations(context, facts.facts.mutations, funcs)) {
|
|
1786
1783
|
if (reportedMutations.has(mutation.node)) continue;
|
|
1787
1784
|
reportedMutations.add(mutation.node);
|
|
1788
1785
|
context.report({
|
|
@@ -3679,7 +3676,7 @@ const isWithStateCall = core.isAPICall("withState");
|
|
|
3679
3676
|
*/
|
|
3680
3677
|
function isComponentWrapperCall(context, call, arg) {
|
|
3681
3678
|
if (Extract.unwrap(call.callee) === arg) return false;
|
|
3682
|
-
call =
|
|
3679
|
+
call = getInnermostCall(call);
|
|
3683
3680
|
if (isConnectCall(context, call)) return true;
|
|
3684
3681
|
if (isCreateFragmentContainerCall(context, call)) return true;
|
|
3685
3682
|
if (isCreatePaginationContainerCall(context, call)) return true;
|
|
@@ -3698,6 +3695,21 @@ function isComponentWrapperCall(context, call, arg) {
|
|
|
3698
3695
|
return false;
|
|
3699
3696
|
}
|
|
3700
3697
|
/**
|
|
3698
|
+
* Unwrap curried call wrappers like `connect(...)(Component)` to get the innermost call expression.
|
|
3699
|
+
* Type expressions and chain expressions around each callee are unwrapped along the way.
|
|
3700
|
+
* @param node The outermost call expression to inspect.
|
|
3701
|
+
* @returns The innermost call expression, whose callee is not itself a call expression.
|
|
3702
|
+
*/
|
|
3703
|
+
function getInnermostCall(node) {
|
|
3704
|
+
let call = node;
|
|
3705
|
+
let callee = Extract.unwrap(call.callee);
|
|
3706
|
+
while (callee.type === AST_NODE_TYPES.CallExpression) {
|
|
3707
|
+
call = callee;
|
|
3708
|
+
callee = Extract.unwrap(call.callee);
|
|
3709
|
+
}
|
|
3710
|
+
return call;
|
|
3711
|
+
}
|
|
3712
|
+
/**
|
|
3701
3713
|
* Resolve the name a function is bound to through wrapping calls (e.g. `const C = useCallback(...)` → `C`).
|
|
3702
3714
|
* @param context The rule context
|
|
3703
3715
|
* @param node The function node to resolve the bound name for
|
|
@@ -4880,8 +4892,8 @@ function resolveBuiltinObjectName(context, node, seen = /* @__PURE__ */ new Set(
|
|
|
4880
4892
|
const init = Extract.unwrap(def.node.init);
|
|
4881
4893
|
if (Check.isIdentifier(init)) return resolveBuiltinObjectName(context, init, seen);
|
|
4882
4894
|
if (init.type === AST_NODE_TYPES.MemberExpression) {
|
|
4883
|
-
const rootId = Extract.
|
|
4884
|
-
if (rootId != null) return resolveBuiltinObjectName(context, rootId, seen);
|
|
4895
|
+
const rootId = Extract.getMemberChain(init).at(0);
|
|
4896
|
+
if (rootId != null && Check.isIdentifier(rootId)) return resolveBuiltinObjectName(context, rootId, seen);
|
|
4885
4897
|
}
|
|
4886
4898
|
}
|
|
4887
4899
|
return null;
|
|
@@ -4924,8 +4936,8 @@ function create$7(context) {
|
|
|
4924
4936
|
break;
|
|
4925
4937
|
}
|
|
4926
4938
|
case expr.type === AST_NODE_TYPES.MemberExpression && Check.isIdentifier(expr.property): {
|
|
4927
|
-
const rootId = Extract.
|
|
4928
|
-
if (rootId == null) return;
|
|
4939
|
+
const rootId = Extract.getMemberChain(expr.object).at(0);
|
|
4940
|
+
if (rootId == null || !Check.isIdentifier(rootId)) return;
|
|
4929
4941
|
const objectName = resolveBuiltinObjectName(context, rootId);
|
|
4930
4942
|
if (objectName == null) return;
|
|
4931
4943
|
const propertyName = expr.property.name;
|
|
@@ -4988,158 +5000,6 @@ const SYNC_ARRAY_CALLBACKS = /* @__PURE__ */ new Set([
|
|
|
4988
5000
|
"some",
|
|
4989
5001
|
"sort"
|
|
4990
5002
|
]);
|
|
4991
|
-
function createBindingResolver(context) {
|
|
4992
|
-
const { additionalRefHooks } = getSettingsFromContext(context);
|
|
4993
|
-
const bindings = /* @__PURE__ */ new Map();
|
|
4994
|
-
const memberBindings = /* @__PURE__ */ new Map();
|
|
4995
|
-
const jsxRefs = /* @__PURE__ */ new Set();
|
|
4996
|
-
function getVariable(node) {
|
|
4997
|
-
return findVariable(context.sourceCode.getScope(node), node) ?? null;
|
|
4998
|
-
}
|
|
4999
|
-
function getBindingValue(node) {
|
|
5000
|
-
const value = Extract.unwrap(node);
|
|
5001
|
-
if (Check.isIdentifier(value)) {
|
|
5002
|
-
const variable = getVariable(value);
|
|
5003
|
-
return variable == null ? { kind: "unknown" } : {
|
|
5004
|
-
kind: "variable",
|
|
5005
|
-
variable
|
|
5006
|
-
};
|
|
5007
|
-
}
|
|
5008
|
-
if (isFunctionExpressionLike(value)) return {
|
|
5009
|
-
kind: "function",
|
|
5010
|
-
node: value
|
|
5011
|
-
};
|
|
5012
|
-
if (value.type === AST_NODE_TYPES.CallExpression && (core.isUseRefLikeCall(value, additionalRefHooks) || core.isCreateRefCall(context, value))) return { kind: "ref" };
|
|
5013
|
-
if (value.type === AST_NODE_TYPES.MemberExpression && Check.isIdentifier(value.property)) {
|
|
5014
|
-
if (isRefLikeName(value.property.name)) return { kind: "ref" };
|
|
5015
|
-
}
|
|
5016
|
-
return { kind: "unknown" };
|
|
5017
|
-
}
|
|
5018
|
-
function addBinding(variable, value, position) {
|
|
5019
|
-
const events = bindings.get(variable) ?? [];
|
|
5020
|
-
bindings.set(variable, events);
|
|
5021
|
-
events.push({
|
|
5022
|
-
position,
|
|
5023
|
-
value
|
|
5024
|
-
});
|
|
5025
|
-
}
|
|
5026
|
-
function addIdentifierBinding(node, value, position = node.range[0]) {
|
|
5027
|
-
const variable = getVariable(node);
|
|
5028
|
-
if (variable != null) addBinding(variable, getBindingValue(value), position);
|
|
5029
|
-
}
|
|
5030
|
-
function addFunctionBinding(node, value, position) {
|
|
5031
|
-
const variable = getVariable(node);
|
|
5032
|
-
if (variable != null) addBinding(variable, {
|
|
5033
|
-
kind: "function",
|
|
5034
|
-
node: value
|
|
5035
|
-
}, position);
|
|
5036
|
-
}
|
|
5037
|
-
function addMemberBinding(object, property, value, position) {
|
|
5038
|
-
const variable = getVariable(object);
|
|
5039
|
-
if (variable == null) return;
|
|
5040
|
-
const properties = memberBindings.get(variable) ?? /* @__PURE__ */ new Map();
|
|
5041
|
-
memberBindings.set(variable, properties);
|
|
5042
|
-
const events = properties.get(property) ?? [];
|
|
5043
|
-
properties.set(property, events);
|
|
5044
|
-
events.push({
|
|
5045
|
-
position,
|
|
5046
|
-
value: getBindingValue(value)
|
|
5047
|
-
});
|
|
5048
|
-
}
|
|
5049
|
-
function addJsxRef(node) {
|
|
5050
|
-
const variable = getVariable(node);
|
|
5051
|
-
if (variable != null) jsxRefs.add(variable);
|
|
5052
|
-
}
|
|
5053
|
-
function resolveRef(variable, position, seen = /* @__PURE__ */ new Set()) {
|
|
5054
|
-
if (seen.has(variable)) return null;
|
|
5055
|
-
seen.add(variable);
|
|
5056
|
-
if (jsxRefs.has(variable) || isRefLikeName(variable.name)) return variable;
|
|
5057
|
-
const event = getLatestValue(bindings.get(variable), position);
|
|
5058
|
-
if (event != null) switch (event.value.kind) {
|
|
5059
|
-
case "ref": return variable;
|
|
5060
|
-
case "variable": return resolveRef(event.value.variable, event.position, seen);
|
|
5061
|
-
case "function":
|
|
5062
|
-
case "unknown": return null;
|
|
5063
|
-
}
|
|
5064
|
-
return null;
|
|
5065
|
-
}
|
|
5066
|
-
function resolveFunction(variable, position, seen = /* @__PURE__ */ new Set()) {
|
|
5067
|
-
if (seen.has(variable)) return null;
|
|
5068
|
-
seen.add(variable);
|
|
5069
|
-
const event = getLatestValue(bindings.get(variable), position);
|
|
5070
|
-
if (event == null) return null;
|
|
5071
|
-
switch (event.value.kind) {
|
|
5072
|
-
case "function": return event.value.node;
|
|
5073
|
-
case "variable": return resolveFunction(event.value.variable, event.position, seen);
|
|
5074
|
-
case "ref":
|
|
5075
|
-
case "unknown": return null;
|
|
5076
|
-
}
|
|
5077
|
-
}
|
|
5078
|
-
function resolveCallable(node, position) {
|
|
5079
|
-
const callee = Extract.unwrap(node);
|
|
5080
|
-
if (isFunctionExpressionLike(callee)) return callee;
|
|
5081
|
-
if (Check.isIdentifier(callee)) {
|
|
5082
|
-
const variable = getVariable(callee);
|
|
5083
|
-
return variable == null ? null : resolveFunction(variable, position);
|
|
5084
|
-
}
|
|
5085
|
-
if (callee.type !== AST_NODE_TYPES.MemberExpression || !Check.isIdentifier(callee.property)) return null;
|
|
5086
|
-
const object = Extract.unwrap(callee.object);
|
|
5087
|
-
const property = callee.property.name;
|
|
5088
|
-
if (!Check.isIdentifier(object)) return null;
|
|
5089
|
-
const variable = getVariable(object);
|
|
5090
|
-
if (variable == null) return null;
|
|
5091
|
-
const event = getLatestValue(memberBindings.get(variable)?.get(property), position);
|
|
5092
|
-
if (event == null) return null;
|
|
5093
|
-
if (event.value.kind === "function") return event.value.node;
|
|
5094
|
-
if (event.value.kind === "variable") return resolveFunction(event.value.variable, event.position);
|
|
5095
|
-
return null;
|
|
5096
|
-
}
|
|
5097
|
-
function getRefTarget(node) {
|
|
5098
|
-
const object = Extract.unwrap(node.object);
|
|
5099
|
-
if (Check.isIdentifier(object)) {
|
|
5100
|
-
const variable = getVariable(object);
|
|
5101
|
-
if (variable == null) return null;
|
|
5102
|
-
const identity = resolveRef(variable, node.range[0]);
|
|
5103
|
-
return identity == null ? null : { identity };
|
|
5104
|
-
}
|
|
5105
|
-
if (object.type === AST_NODE_TYPES.MemberExpression && Check.isIdentifier(object.property)) {
|
|
5106
|
-
if (isRefLikeName(object.property.name)) return { identity: null };
|
|
5107
|
-
}
|
|
5108
|
-
return null;
|
|
5109
|
-
}
|
|
5110
|
-
function getNullBranch(test, identity) {
|
|
5111
|
-
return getNullCheckBranch(test, (candidate) => {
|
|
5112
|
-
if (candidate.type !== AST_NODE_TYPES.MemberExpression) return false;
|
|
5113
|
-
if (!Check.isIdentifier(candidate.property, "current")) return false;
|
|
5114
|
-
return getRefTarget(candidate)?.identity === identity;
|
|
5115
|
-
}, (candidate) => {
|
|
5116
|
-
if (candidate.type === AST_NODE_TYPES.Literal) return candidate.value == null;
|
|
5117
|
-
if (candidate.type === AST_NODE_TYPES.UnaryExpression && candidate.operator === "void") return true;
|
|
5118
|
-
if (!Check.isIdentifier(candidate, "undefined")) return false;
|
|
5119
|
-
const variable = getVariable(candidate);
|
|
5120
|
-
return variable == null || variable.defs.length === 0;
|
|
5121
|
-
});
|
|
5122
|
-
}
|
|
5123
|
-
return {
|
|
5124
|
-
addFunctionBinding,
|
|
5125
|
-
addIdentifierBinding,
|
|
5126
|
-
addJsxRef,
|
|
5127
|
-
addMemberBinding,
|
|
5128
|
-
getNullBranch,
|
|
5129
|
-
getRefTarget,
|
|
5130
|
-
getVariable,
|
|
5131
|
-
resolveCallable,
|
|
5132
|
-
resolveRef
|
|
5133
|
-
};
|
|
5134
|
-
}
|
|
5135
|
-
function getLatestValue(events, position) {
|
|
5136
|
-
let latest = null;
|
|
5137
|
-
for (const event of events ?? []) {
|
|
5138
|
-
if (event.position > position) continue;
|
|
5139
|
-
if (latest == null || event.position >= latest.position) latest = event;
|
|
5140
|
-
}
|
|
5141
|
-
return latest;
|
|
5142
|
-
}
|
|
5143
5003
|
function isFunctionExpressionLike(node) {
|
|
5144
5004
|
return node.type === AST_NODE_TYPES.FunctionExpression || node.type === AST_NODE_TYPES.ArrowFunctionExpression;
|
|
5145
5005
|
}
|
|
@@ -5272,6 +5132,386 @@ function isAfterTerminatingNonNullGuard(node, getNullBranch) {
|
|
|
5272
5132
|
return false;
|
|
5273
5133
|
}
|
|
5274
5134
|
|
|
5135
|
+
//#endregion
|
|
5136
|
+
//#region src/rules/refs/collect.ts
|
|
5137
|
+
/**
|
|
5138
|
+
* Collect the raw facts the refs rule reasons about: identifier/member/function
|
|
5139
|
+
* bindings with their source positions, JSX `ref` attribute targets, call
|
|
5140
|
+
* expressions, and every `.current` member access. Facts outside any function
|
|
5141
|
+
* are never reachable during render and are not collected. All inference is
|
|
5142
|
+
* deferred to `Program:exit`.
|
|
5143
|
+
*/
|
|
5144
|
+
function createFactCollector() {
|
|
5145
|
+
const facts = {
|
|
5146
|
+
bindings: [],
|
|
5147
|
+
callEdges: [],
|
|
5148
|
+
jsxRefs: [],
|
|
5149
|
+
refAccesses: []
|
|
5150
|
+
};
|
|
5151
|
+
return {
|
|
5152
|
+
facts,
|
|
5153
|
+
visitor: {
|
|
5154
|
+
AssignmentExpression(node) {
|
|
5155
|
+
if (node.operator !== "=") return;
|
|
5156
|
+
const left = Extract.unwrap(node.left);
|
|
5157
|
+
if (Check.isIdentifier(left)) {
|
|
5158
|
+
facts.bindings.push({
|
|
5159
|
+
id: left,
|
|
5160
|
+
kind: "identifier",
|
|
5161
|
+
position: node.range[0],
|
|
5162
|
+
value: node.right
|
|
5163
|
+
});
|
|
5164
|
+
return;
|
|
5165
|
+
}
|
|
5166
|
+
if (left.type !== AST_NODE_TYPES.MemberExpression || !Check.isIdentifier(left.property)) return;
|
|
5167
|
+
const object = Extract.unwrap(left.object);
|
|
5168
|
+
if (Check.isIdentifier(object)) facts.bindings.push({
|
|
5169
|
+
kind: "member",
|
|
5170
|
+
object,
|
|
5171
|
+
position: node.range[0],
|
|
5172
|
+
property: left.property.name,
|
|
5173
|
+
value: node.right
|
|
5174
|
+
});
|
|
5175
|
+
},
|
|
5176
|
+
CallExpression(node) {
|
|
5177
|
+
const caller = Traverse.findParent(node, Check.isFunction);
|
|
5178
|
+
if (caller == null) return;
|
|
5179
|
+
facts.callEdges.push({
|
|
5180
|
+
caller,
|
|
5181
|
+
node
|
|
5182
|
+
});
|
|
5183
|
+
},
|
|
5184
|
+
FunctionDeclaration(node) {
|
|
5185
|
+
if (node.id != null) facts.bindings.push({
|
|
5186
|
+
id: node.id,
|
|
5187
|
+
kind: "function",
|
|
5188
|
+
node,
|
|
5189
|
+
position: -1
|
|
5190
|
+
});
|
|
5191
|
+
},
|
|
5192
|
+
JSXAttribute(node) {
|
|
5193
|
+
if (node.name.type !== AST_NODE_TYPES.JSXIdentifier || node.name.name !== "ref" || node.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) return;
|
|
5194
|
+
const expression = Extract.unwrap(node.value.expression);
|
|
5195
|
+
if (!Check.isIdentifier(expression)) return;
|
|
5196
|
+
facts.jsxRefs.push(expression);
|
|
5197
|
+
},
|
|
5198
|
+
MemberExpression(node) {
|
|
5199
|
+
if (!Check.isIdentifier(node.property, "current")) return;
|
|
5200
|
+
const enclosingFunction = Traverse.findParent(node, Check.isFunction);
|
|
5201
|
+
if (enclosingFunction == null) return;
|
|
5202
|
+
facts.refAccesses.push({
|
|
5203
|
+
...getRefAccess(node),
|
|
5204
|
+
enclosingFunction
|
|
5205
|
+
});
|
|
5206
|
+
},
|
|
5207
|
+
VariableDeclarator(node) {
|
|
5208
|
+
if (!Check.isIdentifier(node.id) || node.init == null) return;
|
|
5209
|
+
facts.bindings.push({
|
|
5210
|
+
id: node.id,
|
|
5211
|
+
kind: "identifier",
|
|
5212
|
+
position: node.range[0],
|
|
5213
|
+
value: node.init
|
|
5214
|
+
});
|
|
5215
|
+
}
|
|
5216
|
+
}
|
|
5217
|
+
};
|
|
5218
|
+
}
|
|
5219
|
+
|
|
5220
|
+
//#endregion
|
|
5221
|
+
//#region src/rules/refs/effects.ts
|
|
5222
|
+
/** The nearest strict ancestor of `node` that is a component or hook. */
|
|
5223
|
+
function findBoundary(node, boundaries) {
|
|
5224
|
+
return Traverse.findParent(node, (candidate) => {
|
|
5225
|
+
return Check.isFunction(candidate) && boundaries.has(candidate);
|
|
5226
|
+
});
|
|
5227
|
+
}
|
|
5228
|
+
/** The boundary containing `node`, where `node` is the nearest enclosing function of the fact. */
|
|
5229
|
+
function getBoundaryOf(node, boundaries) {
|
|
5230
|
+
return boundaries.has(node) ? node : findBoundary(node, boundaries);
|
|
5231
|
+
}
|
|
5232
|
+
function isReachedDuringRender(node, boundary, reachability) {
|
|
5233
|
+
const reached = reachability.get(boundary);
|
|
5234
|
+
return reached != null && isReachedThroughFunctions(node, boundary, reached);
|
|
5235
|
+
}
|
|
5236
|
+
/**
|
|
5237
|
+
* Resolve the collected call edges into a per-boundary render call graph.
|
|
5238
|
+
* Direct calls, IIFEs, known synchronous callbacks, function declarations,
|
|
5239
|
+
* aliases, and simple object-property bindings all share the same
|
|
5240
|
+
* identity-based resolver. Each call target is resolved exactly once.
|
|
5241
|
+
*/
|
|
5242
|
+
function inferCallGraph(boundaries, callEdges, resolveCallable) {
|
|
5243
|
+
const callGraph = /* @__PURE__ */ new Map();
|
|
5244
|
+
for (const edge of callEdges) {
|
|
5245
|
+
const boundary = getBoundaryOf(edge.caller, boundaries);
|
|
5246
|
+
if (boundary == null) continue;
|
|
5247
|
+
const targets = [resolveCallable(edge.node.callee, edge.node.range[0])];
|
|
5248
|
+
for (const index of getSynchronousCallbackIndexes(edge.node)) {
|
|
5249
|
+
const argument = edge.node.arguments[index];
|
|
5250
|
+
if (argument == null || argument.type === AST_NODE_TYPES.SpreadElement) continue;
|
|
5251
|
+
targets.push(resolveCallable(argument, edge.node.range[0]));
|
|
5252
|
+
}
|
|
5253
|
+
for (const target of targets) {
|
|
5254
|
+
if (target == null || findBoundary(target, boundaries) !== boundary) continue;
|
|
5255
|
+
const hosts = callGraph.get(boundary) ?? /* @__PURE__ */ new Map();
|
|
5256
|
+
callGraph.set(boundary, hosts);
|
|
5257
|
+
const callees = hosts.get(edge.caller) ?? /* @__PURE__ */ new Set();
|
|
5258
|
+
callees.add(target);
|
|
5259
|
+
hosts.set(edge.caller, callees);
|
|
5260
|
+
}
|
|
5261
|
+
}
|
|
5262
|
+
return callGraph;
|
|
5263
|
+
}
|
|
5264
|
+
/**
|
|
5265
|
+
* Walk the call graph from each render boundary and gather every function
|
|
5266
|
+
* reached during that boundary's render.
|
|
5267
|
+
*/
|
|
5268
|
+
function collectReachableFunctions(boundaries, callGraph) {
|
|
5269
|
+
const reachedByBoundary = /* @__PURE__ */ new Map();
|
|
5270
|
+
for (const boundary of boundaries) {
|
|
5271
|
+
const reached = /* @__PURE__ */ new Set([boundary]);
|
|
5272
|
+
reachedByBoundary.set(boundary, reached);
|
|
5273
|
+
const hosts = callGraph.get(boundary);
|
|
5274
|
+
if (hosts == null) continue;
|
|
5275
|
+
const queue = [boundary];
|
|
5276
|
+
for (let host = queue.pop(); host != null; host = queue.pop()) for (const target of hosts.get(host) ?? []) {
|
|
5277
|
+
if (reached.has(target)) continue;
|
|
5278
|
+
reached.add(target);
|
|
5279
|
+
queue.push(target);
|
|
5280
|
+
}
|
|
5281
|
+
}
|
|
5282
|
+
return reachedByBoundary;
|
|
5283
|
+
}
|
|
5284
|
+
/**
|
|
5285
|
+
* Validate the reached `.current` accesses and track the one permitted lazy
|
|
5286
|
+
* initialization per ref.
|
|
5287
|
+
*/
|
|
5288
|
+
function inferRefViolations(accesses, boundaries, reachability, resolver) {
|
|
5289
|
+
const violations = [];
|
|
5290
|
+
const initializedRefs = /* @__PURE__ */ new Map();
|
|
5291
|
+
for (const access of accesses.toSorted((a, b) => a.node.range[0] - b.node.range[0])) {
|
|
5292
|
+
const target = resolver.getRefTarget(access.node);
|
|
5293
|
+
if (target == null) continue;
|
|
5294
|
+
const boundary = getBoundaryOf(access.enclosingFunction, boundaries);
|
|
5295
|
+
if (boundary == null || !isReachedDuringRender(access.node, boundary, reachability)) continue;
|
|
5296
|
+
let isLazyInitialization = false;
|
|
5297
|
+
if (target.identity != null) {
|
|
5298
|
+
const identity = target.identity;
|
|
5299
|
+
const getTargetNullBranch = (test) => resolver.getNullBranch(test, identity);
|
|
5300
|
+
if (isGuardTestAccess(access.node, getTargetNullBranch)) continue;
|
|
5301
|
+
const guard = getGuardDisposition(access.node, getTargetNullBranch);
|
|
5302
|
+
if (guard != null) {
|
|
5303
|
+
if (guard.inNullBranch && access.isInitializationWrite) isLazyInitialization = true;
|
|
5304
|
+
else if (!guard.inNullBranch && !access.isWrite) continue;
|
|
5305
|
+
} else if (access.isInitializationWrite && isAfterTerminatingNonNullGuard(access.node, getTargetNullBranch)) isLazyInitialization = true;
|
|
5306
|
+
}
|
|
5307
|
+
if (isLazyInitialization && target.identity != null) {
|
|
5308
|
+
const initialized = initializedRefs.get(boundary) ?? /* @__PURE__ */ new Set();
|
|
5309
|
+
initializedRefs.set(boundary, initialized);
|
|
5310
|
+
if (initialized.has(target.identity)) violations.push({
|
|
5311
|
+
kind: "duplicate-init",
|
|
5312
|
+
node: access.node
|
|
5313
|
+
});
|
|
5314
|
+
else initialized.add(target.identity);
|
|
5315
|
+
continue;
|
|
5316
|
+
}
|
|
5317
|
+
violations.push({
|
|
5318
|
+
kind: access.isWrite ? "write" : "read",
|
|
5319
|
+
node: access.node
|
|
5320
|
+
});
|
|
5321
|
+
}
|
|
5322
|
+
return violations;
|
|
5323
|
+
}
|
|
5324
|
+
/**
|
|
5325
|
+
* Passing a ref to an unknown function can expose its value during render.
|
|
5326
|
+
* Hook callbacks, mergeRefs, and the existing render-prop compatibility case
|
|
5327
|
+
* remain exempt.
|
|
5328
|
+
*/
|
|
5329
|
+
function inferRefPassViolations(callEdges, boundaries, reachability, resolver) {
|
|
5330
|
+
const violations = [];
|
|
5331
|
+
for (const edge of callEdges) {
|
|
5332
|
+
const boundary = getBoundaryOf(edge.caller, boundaries);
|
|
5333
|
+
if (boundary == null || !isReachedDuringRender(edge.node, boundary, reachability)) continue;
|
|
5334
|
+
const call = edge.node;
|
|
5335
|
+
const callee = Extract.unwrap(call.callee);
|
|
5336
|
+
const calleeName = Extract.getCalleeName(call);
|
|
5337
|
+
const callArguments = call.arguments;
|
|
5338
|
+
if (core.isHookCall(call) || calleeName === "mergeRefs" || calleeName === "render" && callee.type === AST_NODE_TYPES.MemberExpression) continue;
|
|
5339
|
+
for (const argument of callArguments) {
|
|
5340
|
+
if (argument.type === AST_NODE_TYPES.SpreadElement) continue;
|
|
5341
|
+
const value = Extract.unwrap(argument);
|
|
5342
|
+
if (!Check.isIdentifier(value)) continue;
|
|
5343
|
+
const variable = resolver.getVariable(value);
|
|
5344
|
+
if (variable == null || resolver.resolveRef(variable, value.range[0]) == null) continue;
|
|
5345
|
+
violations.push({
|
|
5346
|
+
kind: "pass",
|
|
5347
|
+
node: value
|
|
5348
|
+
});
|
|
5349
|
+
}
|
|
5350
|
+
}
|
|
5351
|
+
return violations;
|
|
5352
|
+
}
|
|
5353
|
+
|
|
5354
|
+
//#endregion
|
|
5355
|
+
//#region src/rules/refs/origins.ts
|
|
5356
|
+
/**
|
|
5357
|
+
* Resolve which variables hold refs and which resolve to callable functions.
|
|
5358
|
+
* Maps are keyed by ESLint variables, not source names, so shadowed
|
|
5359
|
+
* identifiers and separate components cannot contaminate each other.
|
|
5360
|
+
*/
|
|
5361
|
+
function createBindingResolver(context, facts) {
|
|
5362
|
+
const { additionalRefHooks } = getSettingsFromContext(context);
|
|
5363
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
5364
|
+
const memberBindings = /* @__PURE__ */ new Map();
|
|
5365
|
+
const jsxRefs = /* @__PURE__ */ new Set();
|
|
5366
|
+
function getVariable(node) {
|
|
5367
|
+
return findVariable(context.sourceCode.getScope(node), node) ?? null;
|
|
5368
|
+
}
|
|
5369
|
+
function getBindingValue(node) {
|
|
5370
|
+
const value = Extract.unwrap(node);
|
|
5371
|
+
if (Check.isIdentifier(value)) {
|
|
5372
|
+
const variable = getVariable(value);
|
|
5373
|
+
return variable == null ? { kind: "unknown" } : {
|
|
5374
|
+
kind: "variable",
|
|
5375
|
+
variable
|
|
5376
|
+
};
|
|
5377
|
+
}
|
|
5378
|
+
if (isFunctionExpressionLike(value)) return {
|
|
5379
|
+
kind: "function",
|
|
5380
|
+
node: value
|
|
5381
|
+
};
|
|
5382
|
+
if (value.type === AST_NODE_TYPES.CallExpression && (core.isUseRefLikeCall(value, additionalRefHooks) || core.isCreateRefCall(context, value))) return { kind: "ref" };
|
|
5383
|
+
if (value.type === AST_NODE_TYPES.MemberExpression && Check.isIdentifier(value.property)) {
|
|
5384
|
+
if (isRefLikeName(value.property.name)) return { kind: "ref" };
|
|
5385
|
+
}
|
|
5386
|
+
return { kind: "unknown" };
|
|
5387
|
+
}
|
|
5388
|
+
function addBinding(variable, value, position) {
|
|
5389
|
+
const events = bindings.get(variable) ?? [];
|
|
5390
|
+
bindings.set(variable, events);
|
|
5391
|
+
events.push({
|
|
5392
|
+
position,
|
|
5393
|
+
value
|
|
5394
|
+
});
|
|
5395
|
+
}
|
|
5396
|
+
for (const fact of facts.bindings) switch (fact.kind) {
|
|
5397
|
+
case "function": {
|
|
5398
|
+
const variable = getVariable(fact.id);
|
|
5399
|
+
if (variable != null) addBinding(variable, {
|
|
5400
|
+
kind: "function",
|
|
5401
|
+
node: fact.node
|
|
5402
|
+
}, fact.position);
|
|
5403
|
+
break;
|
|
5404
|
+
}
|
|
5405
|
+
case "identifier": {
|
|
5406
|
+
const variable = getVariable(fact.id);
|
|
5407
|
+
if (variable != null) addBinding(variable, getBindingValue(fact.value), fact.position);
|
|
5408
|
+
break;
|
|
5409
|
+
}
|
|
5410
|
+
case "member": {
|
|
5411
|
+
const variable = getVariable(fact.object);
|
|
5412
|
+
if (variable == null) break;
|
|
5413
|
+
const properties = memberBindings.get(variable) ?? /* @__PURE__ */ new Map();
|
|
5414
|
+
memberBindings.set(variable, properties);
|
|
5415
|
+
const events = properties.get(fact.property) ?? [];
|
|
5416
|
+
properties.set(fact.property, events);
|
|
5417
|
+
events.push({
|
|
5418
|
+
position: fact.position,
|
|
5419
|
+
value: getBindingValue(fact.value)
|
|
5420
|
+
});
|
|
5421
|
+
break;
|
|
5422
|
+
}
|
|
5423
|
+
}
|
|
5424
|
+
for (const node of facts.jsxRefs) {
|
|
5425
|
+
const variable = getVariable(node);
|
|
5426
|
+
if (variable != null) jsxRefs.add(variable);
|
|
5427
|
+
}
|
|
5428
|
+
function resolveRef(variable, position, seen = /* @__PURE__ */ new Set()) {
|
|
5429
|
+
if (seen.has(variable)) return null;
|
|
5430
|
+
seen.add(variable);
|
|
5431
|
+
if (jsxRefs.has(variable) || isRefLikeName(variable.name)) return variable;
|
|
5432
|
+
const event = getLatestValue(bindings.get(variable), position);
|
|
5433
|
+
if (event != null) switch (event.value.kind) {
|
|
5434
|
+
case "ref": return variable;
|
|
5435
|
+
case "variable": return resolveRef(event.value.variable, event.position, seen);
|
|
5436
|
+
case "function":
|
|
5437
|
+
case "unknown": return null;
|
|
5438
|
+
}
|
|
5439
|
+
return null;
|
|
5440
|
+
}
|
|
5441
|
+
function resolveFunction(variable, position, seen = /* @__PURE__ */ new Set()) {
|
|
5442
|
+
if (seen.has(variable)) return null;
|
|
5443
|
+
seen.add(variable);
|
|
5444
|
+
const event = getLatestValue(bindings.get(variable), position);
|
|
5445
|
+
if (event == null) return null;
|
|
5446
|
+
switch (event.value.kind) {
|
|
5447
|
+
case "function": return event.value.node;
|
|
5448
|
+
case "variable": return resolveFunction(event.value.variable, event.position, seen);
|
|
5449
|
+
case "ref":
|
|
5450
|
+
case "unknown": return null;
|
|
5451
|
+
}
|
|
5452
|
+
}
|
|
5453
|
+
function resolveCallable(node, position) {
|
|
5454
|
+
const callee = Extract.unwrap(node);
|
|
5455
|
+
if (isFunctionExpressionLike(callee)) return callee;
|
|
5456
|
+
if (Check.isIdentifier(callee)) {
|
|
5457
|
+
const variable = getVariable(callee);
|
|
5458
|
+
return variable == null ? null : resolveFunction(variable, position);
|
|
5459
|
+
}
|
|
5460
|
+
if (callee.type !== AST_NODE_TYPES.MemberExpression || !Check.isIdentifier(callee.property)) return null;
|
|
5461
|
+
const object = Extract.unwrap(callee.object);
|
|
5462
|
+
const property = callee.property.name;
|
|
5463
|
+
if (!Check.isIdentifier(object)) return null;
|
|
5464
|
+
const variable = getVariable(object);
|
|
5465
|
+
if (variable == null) return null;
|
|
5466
|
+
const event = getLatestValue(memberBindings.get(variable)?.get(property), position);
|
|
5467
|
+
if (event == null) return null;
|
|
5468
|
+
if (event.value.kind === "function") return event.value.node;
|
|
5469
|
+
if (event.value.kind === "variable") return resolveFunction(event.value.variable, event.position);
|
|
5470
|
+
return null;
|
|
5471
|
+
}
|
|
5472
|
+
function getRefTarget(node) {
|
|
5473
|
+
const object = Extract.unwrap(node.object);
|
|
5474
|
+
if (Check.isIdentifier(object)) {
|
|
5475
|
+
const variable = getVariable(object);
|
|
5476
|
+
if (variable == null) return null;
|
|
5477
|
+
const identity = resolveRef(variable, node.range[0]);
|
|
5478
|
+
return identity == null ? null : { identity };
|
|
5479
|
+
}
|
|
5480
|
+
if (object.type === AST_NODE_TYPES.MemberExpression && Check.isIdentifier(object.property)) {
|
|
5481
|
+
if (isRefLikeName(object.property.name)) return { identity: null };
|
|
5482
|
+
}
|
|
5483
|
+
return null;
|
|
5484
|
+
}
|
|
5485
|
+
function getNullBranch(test, identity) {
|
|
5486
|
+
return getNullCheckBranch(test, (candidate) => {
|
|
5487
|
+
if (candidate.type !== AST_NODE_TYPES.MemberExpression) return false;
|
|
5488
|
+
if (!Check.isIdentifier(candidate.property, "current")) return false;
|
|
5489
|
+
return getRefTarget(candidate)?.identity === identity;
|
|
5490
|
+
}, (candidate) => {
|
|
5491
|
+
if (candidate.type === AST_NODE_TYPES.Literal) return candidate.value == null;
|
|
5492
|
+
if (candidate.type === AST_NODE_TYPES.UnaryExpression && candidate.operator === "void") return true;
|
|
5493
|
+
if (!Check.isIdentifier(candidate, "undefined")) return false;
|
|
5494
|
+
const variable = getVariable(candidate);
|
|
5495
|
+
return variable == null || variable.defs.length === 0;
|
|
5496
|
+
});
|
|
5497
|
+
}
|
|
5498
|
+
return {
|
|
5499
|
+
getNullBranch,
|
|
5500
|
+
getRefTarget,
|
|
5501
|
+
getVariable,
|
|
5502
|
+
resolveCallable,
|
|
5503
|
+
resolveRef
|
|
5504
|
+
};
|
|
5505
|
+
}
|
|
5506
|
+
function getLatestValue(events, position) {
|
|
5507
|
+
let latest = null;
|
|
5508
|
+
for (const event of events ?? []) {
|
|
5509
|
+
if (event.position > position) continue;
|
|
5510
|
+
if (latest == null || event.position >= latest.position) latest = event;
|
|
5511
|
+
}
|
|
5512
|
+
return latest;
|
|
5513
|
+
}
|
|
5514
|
+
|
|
5275
5515
|
//#endregion
|
|
5276
5516
|
//#region src/rules/refs/refs.ts
|
|
5277
5517
|
const RULE_NAME$6 = "refs";
|
|
@@ -5292,130 +5532,20 @@ var refs_default = createRule({
|
|
|
5292
5532
|
defaultOptions: []
|
|
5293
5533
|
});
|
|
5294
5534
|
function create$6(context) {
|
|
5295
|
-
const
|
|
5296
|
-
const
|
|
5297
|
-
const
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
const object = Extract.unwrap(left.object);
|
|
5310
|
-
if (Check.isIdentifier(object)) addMemberBinding(object, left.property.name, node.right, node.range[0]);
|
|
5311
|
-
},
|
|
5312
|
-
CallExpression(node) {
|
|
5313
|
-
calls.push(node);
|
|
5314
|
-
},
|
|
5315
|
-
FunctionDeclaration(node) {
|
|
5316
|
-
if (node.id != null) addFunctionBinding(node.id, node, -1);
|
|
5317
|
-
},
|
|
5318
|
-
JSXAttribute(node) {
|
|
5319
|
-
if (node.name.type !== AST_NODE_TYPES.JSXIdentifier || node.name.name !== "ref" || node.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) return;
|
|
5320
|
-
const expression = Extract.unwrap(node.value.expression);
|
|
5321
|
-
if (!Check.isIdentifier(expression)) return;
|
|
5322
|
-
addJsxRef(expression);
|
|
5323
|
-
},
|
|
5324
|
-
MemberExpression(node) {
|
|
5325
|
-
if (!Check.isIdentifier(node.property, "current")) return;
|
|
5326
|
-
refAccesses.push(getRefAccess(node));
|
|
5327
|
-
},
|
|
5328
|
-
"Program:exit"(program) {
|
|
5329
|
-
const boundaries = /* @__PURE__ */ new Set([...fc.api.getAllComponents(program).map((component) => component.node), ...hc.api.getAllHooks(program).map((hook) => hook.node)]);
|
|
5330
|
-
const isBoundary = (node) => {
|
|
5331
|
-
return Check.isFunction(node) && boundaries.has(node);
|
|
5332
|
-
};
|
|
5333
|
-
const reachedByBoundary = /* @__PURE__ */ new Map();
|
|
5334
|
-
for (const boundary of boundaries) reachedByBoundary.set(boundary, /* @__PURE__ */ new Set([boundary]));
|
|
5335
|
-
let changed = true;
|
|
5336
|
-
while (changed) {
|
|
5337
|
-
changed = false;
|
|
5338
|
-
for (const call of calls) {
|
|
5339
|
-
const boundary = Traverse.findParent(call, isBoundary);
|
|
5340
|
-
if (boundary == null) continue;
|
|
5341
|
-
const reached = reachedByBoundary.get(boundary);
|
|
5342
|
-
if (reached == null) continue;
|
|
5343
|
-
const host = Traverse.findParent(call, Check.isFunction) ?? boundary;
|
|
5344
|
-
if (!reached.has(host)) continue;
|
|
5345
|
-
const targets = [resolveCallable(call.callee, call.range[0])];
|
|
5346
|
-
for (const index of getSynchronousCallbackIndexes(call)) {
|
|
5347
|
-
const argument = call.arguments[index];
|
|
5348
|
-
if (argument == null || argument.type === AST_NODE_TYPES.SpreadElement) continue;
|
|
5349
|
-
targets.push(resolveCallable(argument, call.range[0]));
|
|
5350
|
-
}
|
|
5351
|
-
for (const target of targets) {
|
|
5352
|
-
if (target == null || reached.has(target) || Traverse.findParent(target, isBoundary) !== boundary) continue;
|
|
5353
|
-
reached.add(target);
|
|
5354
|
-
changed = true;
|
|
5355
|
-
}
|
|
5356
|
-
}
|
|
5357
|
-
}
|
|
5358
|
-
function isReachedDuringRender(node, boundary) {
|
|
5359
|
-
const reached = reachedByBoundary.get(boundary);
|
|
5360
|
-
return reached != null && isReachedThroughFunctions(node, boundary, reached);
|
|
5361
|
-
}
|
|
5362
|
-
const initializedRefs = /* @__PURE__ */ new Map();
|
|
5363
|
-
for (const access of refAccesses.toSorted((a, b) => a.node.range[0] - b.node.range[0])) {
|
|
5364
|
-
const target = getRefTarget(access.node);
|
|
5365
|
-
if (target == null) continue;
|
|
5366
|
-
const boundary = Traverse.findParent(access.node, isBoundary);
|
|
5367
|
-
if (boundary == null || !isReachedDuringRender(access.node, boundary)) continue;
|
|
5368
|
-
let isLazyInitialization = false;
|
|
5369
|
-
if (target.identity != null) {
|
|
5370
|
-
const identity = target.identity;
|
|
5371
|
-
const getTargetNullBranch = (test) => getNullBranch(test, identity);
|
|
5372
|
-
if (isGuardTestAccess(access.node, getTargetNullBranch)) continue;
|
|
5373
|
-
const guard = getGuardDisposition(access.node, getTargetNullBranch);
|
|
5374
|
-
if (guard != null) {
|
|
5375
|
-
if (guard.inNullBranch && access.isInitializationWrite) isLazyInitialization = true;
|
|
5376
|
-
else if (!guard.inNullBranch && !access.isWrite) continue;
|
|
5377
|
-
} else if (access.isInitializationWrite && isAfterTerminatingNonNullGuard(access.node, getTargetNullBranch)) isLazyInitialization = true;
|
|
5378
|
-
}
|
|
5379
|
-
if (isLazyInitialization && target.identity != null) {
|
|
5380
|
-
const initialized = initializedRefs.get(boundary) ?? /* @__PURE__ */ new Set();
|
|
5381
|
-
initializedRefs.set(boundary, initialized);
|
|
5382
|
-
if (initialized.has(target.identity)) context.report({
|
|
5383
|
-
messageId: "duplicateRefInit",
|
|
5384
|
-
node: access.node
|
|
5385
|
-
});
|
|
5386
|
-
else initialized.add(target.identity);
|
|
5387
|
-
continue;
|
|
5388
|
-
}
|
|
5389
|
-
context.report({
|
|
5390
|
-
messageId: access.isWrite ? "writeDuringRender" : "readDuringRender",
|
|
5391
|
-
node: access.node
|
|
5392
|
-
});
|
|
5393
|
-
}
|
|
5394
|
-
for (const call of calls) {
|
|
5395
|
-
const boundary = Traverse.findParent(call, isBoundary);
|
|
5396
|
-
if (boundary == null || !isReachedDuringRender(call, boundary)) continue;
|
|
5397
|
-
const callee = Extract.unwrap(call.callee);
|
|
5398
|
-
const calleeName = Extract.getCalleeName(call);
|
|
5399
|
-
const callArguments = call.arguments;
|
|
5400
|
-
if (core.isHookCall(call) || calleeName === "mergeRefs" || calleeName === "render" && callee.type === AST_NODE_TYPES.MemberExpression) continue;
|
|
5401
|
-
for (const argument of callArguments) {
|
|
5402
|
-
if (argument.type === AST_NODE_TYPES.SpreadElement) continue;
|
|
5403
|
-
const value = Extract.unwrap(argument);
|
|
5404
|
-
if (!Check.isIdentifier(value)) continue;
|
|
5405
|
-
const variable = getVariable(value);
|
|
5406
|
-
if (variable == null || resolveRef(variable, value.range[0]) == null) continue;
|
|
5407
|
-
context.report({
|
|
5408
|
-
messageId: "refPassedToFunction",
|
|
5409
|
-
node: value
|
|
5410
|
-
});
|
|
5411
|
-
}
|
|
5412
|
-
}
|
|
5413
|
-
},
|
|
5414
|
-
VariableDeclarator(node) {
|
|
5415
|
-
if (!Check.isIdentifier(node.id) || node.init == null) return;
|
|
5416
|
-
addIdentifierBinding(node.id, node.init, node.range[0]);
|
|
5417
|
-
}
|
|
5418
|
-
});
|
|
5535
|
+
const hooks = core.getHookCollector(context);
|
|
5536
|
+
const comps = core.getFunctionComponentCollector(context);
|
|
5537
|
+
const facts = createFactCollector();
|
|
5538
|
+
return merge(hooks.visitor, comps.visitor, facts.visitor, { "Program:exit"(program) {
|
|
5539
|
+
const boundaries = /* @__PURE__ */ new Set([...comps.api.getAllComponents(program).map((component) => component.node), ...hooks.api.getAllHooks(program).map((hook) => hook.node)]);
|
|
5540
|
+
const resolver = createBindingResolver(context, facts.facts);
|
|
5541
|
+
const callGraph = inferCallGraph(boundaries, facts.facts.callEdges, resolver.resolveCallable);
|
|
5542
|
+
const reachability = collectReachableFunctions(boundaries, callGraph);
|
|
5543
|
+
const violations = [...inferRefViolations(facts.facts.refAccesses, boundaries, reachability, resolver), ...inferRefPassViolations(facts.facts.callEdges, boundaries, reachability, resolver)];
|
|
5544
|
+
for (const violation of violations) context.report({
|
|
5545
|
+
messageId: match(violation).with({ kind: "duplicate-init" }, () => "duplicateRefInit").with({ kind: "pass" }, () => "refPassedToFunction").with({ kind: "read" }, () => "readDuringRender").with({ kind: "write" }, () => "writeDuringRender").exhaustive(),
|
|
5546
|
+
node: violation.node
|
|
5547
|
+
});
|
|
5548
|
+
} });
|
|
5419
5549
|
}
|
|
5420
5550
|
|
|
5421
5551
|
//#endregion
|
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.3",
|
|
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.3",
|
|
40
|
+
"@eslint-react/core": "5.20.3",
|
|
41
|
+
"@eslint-react/eslint": "5.20.3",
|
|
42
|
+
"@eslint-react/jsx": "5.20.3",
|
|
43
|
+
"@eslint-react/shared": "5.20.3",
|
|
44
|
+
"@eslint-react/var": "5.20.3",
|
|
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",
|