eslint-plugin-react-x 5.20.0 → 5.20.2

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.
Files changed (2) hide show
  1. package/dist/index.js +408 -290
  2. package/package.json +8 -8
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.0";
148
+ var version = "5.20.2";
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 createGlobalsCollector() {
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 globs = createGlobalsCollector();
1371
- return merge(hooks.visitor, comps.visitor, globs.visitor, { "Program:exit"(program) {
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, globs.facts);
1374
- const callGraph = inferCallGraph(context, globs.facts.callEdges);
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,
@@ -1493,7 +1493,7 @@ function isRefLikeChain(context, node) {
1493
1493
 
1494
1494
  //#endregion
1495
1495
  //#region src/rules/immutability/collect.ts
1496
- function createImmutabilityCollector() {
1496
+ function createFactCollector$1() {
1497
1497
  const facts = {
1498
1498
  mutations: [],
1499
1499
  sinks: []
@@ -1750,17 +1750,17 @@ var immutability_default = createRule({
1750
1750
  function create$48(context) {
1751
1751
  const hooks = core.getHookCollector(context);
1752
1752
  const comps = core.getFunctionComponentCollector(context);
1753
- const immut = createImmutabilityCollector();
1754
- return merge(hooks.visitor, comps.visitor, immut.visitor, { "Program:exit"(program) {
1755
- for (const hook of hooks.api.getAllHooks(program)) for (const expression of hook.rets) if (expression != null) immut.facts.sinks.push({
1753
+ const facts = createFactCollector$1();
1754
+ return merge(hooks.visitor, comps.visitor, facts.visitor, { "Program:exit"(program) {
1755
+ for (const hook of hooks.api.getAllHooks(program)) for (const expression of hook.rets) if (expression != null) facts.facts.sinks.push({
1756
1756
  kind: "hook-return",
1757
1757
  expression
1758
1758
  });
1759
1759
  const reportedMutations = /* @__PURE__ */ new Set();
1760
- const mutableFunctions = inferMutableFunctions(context, immut.facts.mutations);
1760
+ const mutableFunctions = inferMutableFunctions(context, facts.facts.mutations);
1761
1761
  if (mutableFunctions.size > 0) {
1762
1762
  const reportedSinks = /* @__PURE__ */ new Set();
1763
- for (const sink of immut.facts.sinks) {
1763
+ for (const sink of facts.facts.sinks) {
1764
1764
  const expression = sink.expression;
1765
1765
  if (reportedSinks.has(expression)) continue;
1766
1766
  const fn = resolveToFunctionNode(context, expression);
@@ -1782,7 +1782,7 @@ function create$48(context) {
1782
1782
  }
1783
1783
  }
1784
1784
  const funcs = comps.api.getAllComponents(program).map((comp) => comp.node);
1785
- for (const mutation of inferDirectMutations(context, immut.facts.mutations, funcs)) {
1785
+ for (const mutation of inferDirectMutations(context, facts.facts.mutations, funcs)) {
1786
1786
  if (reportedMutations.has(mutation.node)) continue;
1787
1787
  reportedMutations.add(mutation.node);
1788
1788
  context.report({
@@ -4988,158 +4988,6 @@ const SYNC_ARRAY_CALLBACKS = /* @__PURE__ */ new Set([
4988
4988
  "some",
4989
4989
  "sort"
4990
4990
  ]);
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
4991
  function isFunctionExpressionLike(node) {
5144
4992
  return node.type === AST_NODE_TYPES.FunctionExpression || node.type === AST_NODE_TYPES.ArrowFunctionExpression;
5145
4993
  }
@@ -5272,6 +5120,386 @@ function isAfterTerminatingNonNullGuard(node, getNullBranch) {
5272
5120
  return false;
5273
5121
  }
5274
5122
 
5123
+ //#endregion
5124
+ //#region src/rules/refs/collect.ts
5125
+ /**
5126
+ * Collect the raw facts the refs rule reasons about: identifier/member/function
5127
+ * bindings with their source positions, JSX `ref` attribute targets, call
5128
+ * expressions, and every `.current` member access. Facts outside any function
5129
+ * are never reachable during render and are not collected. All inference is
5130
+ * deferred to `Program:exit`.
5131
+ */
5132
+ function createFactCollector() {
5133
+ const facts = {
5134
+ bindings: [],
5135
+ callEdges: [],
5136
+ jsxRefs: [],
5137
+ refAccesses: []
5138
+ };
5139
+ return {
5140
+ facts,
5141
+ visitor: {
5142
+ AssignmentExpression(node) {
5143
+ if (node.operator !== "=") return;
5144
+ const left = Extract.unwrap(node.left);
5145
+ if (Check.isIdentifier(left)) {
5146
+ facts.bindings.push({
5147
+ id: left,
5148
+ kind: "identifier",
5149
+ position: node.range[0],
5150
+ value: node.right
5151
+ });
5152
+ return;
5153
+ }
5154
+ if (left.type !== AST_NODE_TYPES.MemberExpression || !Check.isIdentifier(left.property)) return;
5155
+ const object = Extract.unwrap(left.object);
5156
+ if (Check.isIdentifier(object)) facts.bindings.push({
5157
+ kind: "member",
5158
+ object,
5159
+ position: node.range[0],
5160
+ property: left.property.name,
5161
+ value: node.right
5162
+ });
5163
+ },
5164
+ CallExpression(node) {
5165
+ const caller = Traverse.findParent(node, Check.isFunction);
5166
+ if (caller == null) return;
5167
+ facts.callEdges.push({
5168
+ caller,
5169
+ node
5170
+ });
5171
+ },
5172
+ FunctionDeclaration(node) {
5173
+ if (node.id != null) facts.bindings.push({
5174
+ id: node.id,
5175
+ kind: "function",
5176
+ node,
5177
+ position: -1
5178
+ });
5179
+ },
5180
+ JSXAttribute(node) {
5181
+ if (node.name.type !== AST_NODE_TYPES.JSXIdentifier || node.name.name !== "ref" || node.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) return;
5182
+ const expression = Extract.unwrap(node.value.expression);
5183
+ if (!Check.isIdentifier(expression)) return;
5184
+ facts.jsxRefs.push(expression);
5185
+ },
5186
+ MemberExpression(node) {
5187
+ if (!Check.isIdentifier(node.property, "current")) return;
5188
+ const enclosingFunction = Traverse.findParent(node, Check.isFunction);
5189
+ if (enclosingFunction == null) return;
5190
+ facts.refAccesses.push({
5191
+ ...getRefAccess(node),
5192
+ enclosingFunction
5193
+ });
5194
+ },
5195
+ VariableDeclarator(node) {
5196
+ if (!Check.isIdentifier(node.id) || node.init == null) return;
5197
+ facts.bindings.push({
5198
+ id: node.id,
5199
+ kind: "identifier",
5200
+ position: node.range[0],
5201
+ value: node.init
5202
+ });
5203
+ }
5204
+ }
5205
+ };
5206
+ }
5207
+
5208
+ //#endregion
5209
+ //#region src/rules/refs/effects.ts
5210
+ /** The nearest strict ancestor of `node` that is a component or hook. */
5211
+ function findBoundary(node, boundaries) {
5212
+ return Traverse.findParent(node, (candidate) => {
5213
+ return Check.isFunction(candidate) && boundaries.has(candidate);
5214
+ });
5215
+ }
5216
+ /** The boundary containing `node`, where `node` is the nearest enclosing function of the fact. */
5217
+ function getBoundaryOf(node, boundaries) {
5218
+ return boundaries.has(node) ? node : findBoundary(node, boundaries);
5219
+ }
5220
+ function isReachedDuringRender(node, boundary, reachability) {
5221
+ const reached = reachability.get(boundary);
5222
+ return reached != null && isReachedThroughFunctions(node, boundary, reached);
5223
+ }
5224
+ /**
5225
+ * Resolve the collected call edges into a per-boundary render call graph.
5226
+ * Direct calls, IIFEs, known synchronous callbacks, function declarations,
5227
+ * aliases, and simple object-property bindings all share the same
5228
+ * identity-based resolver. Each call target is resolved exactly once.
5229
+ */
5230
+ function inferCallGraph(boundaries, callEdges, resolveCallable) {
5231
+ const callGraph = /* @__PURE__ */ new Map();
5232
+ for (const edge of callEdges) {
5233
+ const boundary = getBoundaryOf(edge.caller, boundaries);
5234
+ if (boundary == null) continue;
5235
+ const targets = [resolveCallable(edge.node.callee, edge.node.range[0])];
5236
+ for (const index of getSynchronousCallbackIndexes(edge.node)) {
5237
+ const argument = edge.node.arguments[index];
5238
+ if (argument == null || argument.type === AST_NODE_TYPES.SpreadElement) continue;
5239
+ targets.push(resolveCallable(argument, edge.node.range[0]));
5240
+ }
5241
+ for (const target of targets) {
5242
+ if (target == null || findBoundary(target, boundaries) !== boundary) continue;
5243
+ const hosts = callGraph.get(boundary) ?? /* @__PURE__ */ new Map();
5244
+ callGraph.set(boundary, hosts);
5245
+ const callees = hosts.get(edge.caller) ?? /* @__PURE__ */ new Set();
5246
+ callees.add(target);
5247
+ hosts.set(edge.caller, callees);
5248
+ }
5249
+ }
5250
+ return callGraph;
5251
+ }
5252
+ /**
5253
+ * Walk the call graph from each render boundary and gather every function
5254
+ * reached during that boundary's render.
5255
+ */
5256
+ function collectReachableFunctions(boundaries, callGraph) {
5257
+ const reachedByBoundary = /* @__PURE__ */ new Map();
5258
+ for (const boundary of boundaries) {
5259
+ const reached = /* @__PURE__ */ new Set([boundary]);
5260
+ reachedByBoundary.set(boundary, reached);
5261
+ const hosts = callGraph.get(boundary);
5262
+ if (hosts == null) continue;
5263
+ const queue = [boundary];
5264
+ for (let host = queue.pop(); host != null; host = queue.pop()) for (const target of hosts.get(host) ?? []) {
5265
+ if (reached.has(target)) continue;
5266
+ reached.add(target);
5267
+ queue.push(target);
5268
+ }
5269
+ }
5270
+ return reachedByBoundary;
5271
+ }
5272
+ /**
5273
+ * Validate the reached `.current` accesses and track the one permitted lazy
5274
+ * initialization per ref.
5275
+ */
5276
+ function inferRefViolations(accesses, boundaries, reachability, resolver) {
5277
+ const violations = [];
5278
+ const initializedRefs = /* @__PURE__ */ new Map();
5279
+ for (const access of accesses.toSorted((a, b) => a.node.range[0] - b.node.range[0])) {
5280
+ const target = resolver.getRefTarget(access.node);
5281
+ if (target == null) continue;
5282
+ const boundary = getBoundaryOf(access.enclosingFunction, boundaries);
5283
+ if (boundary == null || !isReachedDuringRender(access.node, boundary, reachability)) continue;
5284
+ let isLazyInitialization = false;
5285
+ if (target.identity != null) {
5286
+ const identity = target.identity;
5287
+ const getTargetNullBranch = (test) => resolver.getNullBranch(test, identity);
5288
+ if (isGuardTestAccess(access.node, getTargetNullBranch)) continue;
5289
+ const guard = getGuardDisposition(access.node, getTargetNullBranch);
5290
+ if (guard != null) {
5291
+ if (guard.inNullBranch && access.isInitializationWrite) isLazyInitialization = true;
5292
+ else if (!guard.inNullBranch && !access.isWrite) continue;
5293
+ } else if (access.isInitializationWrite && isAfterTerminatingNonNullGuard(access.node, getTargetNullBranch)) isLazyInitialization = true;
5294
+ }
5295
+ if (isLazyInitialization && target.identity != null) {
5296
+ const initialized = initializedRefs.get(boundary) ?? /* @__PURE__ */ new Set();
5297
+ initializedRefs.set(boundary, initialized);
5298
+ if (initialized.has(target.identity)) violations.push({
5299
+ kind: "duplicate-init",
5300
+ node: access.node
5301
+ });
5302
+ else initialized.add(target.identity);
5303
+ continue;
5304
+ }
5305
+ violations.push({
5306
+ kind: access.isWrite ? "write" : "read",
5307
+ node: access.node
5308
+ });
5309
+ }
5310
+ return violations;
5311
+ }
5312
+ /**
5313
+ * Passing a ref to an unknown function can expose its value during render.
5314
+ * Hook callbacks, mergeRefs, and the existing render-prop compatibility case
5315
+ * remain exempt.
5316
+ */
5317
+ function inferRefPassViolations(callEdges, boundaries, reachability, resolver) {
5318
+ const violations = [];
5319
+ for (const edge of callEdges) {
5320
+ const boundary = getBoundaryOf(edge.caller, boundaries);
5321
+ if (boundary == null || !isReachedDuringRender(edge.node, boundary, reachability)) continue;
5322
+ const call = edge.node;
5323
+ const callee = Extract.unwrap(call.callee);
5324
+ const calleeName = Extract.getCalleeName(call);
5325
+ const callArguments = call.arguments;
5326
+ if (core.isHookCall(call) || calleeName === "mergeRefs" || calleeName === "render" && callee.type === AST_NODE_TYPES.MemberExpression) continue;
5327
+ for (const argument of callArguments) {
5328
+ if (argument.type === AST_NODE_TYPES.SpreadElement) continue;
5329
+ const value = Extract.unwrap(argument);
5330
+ if (!Check.isIdentifier(value)) continue;
5331
+ const variable = resolver.getVariable(value);
5332
+ if (variable == null || resolver.resolveRef(variable, value.range[0]) == null) continue;
5333
+ violations.push({
5334
+ kind: "pass",
5335
+ node: value
5336
+ });
5337
+ }
5338
+ }
5339
+ return violations;
5340
+ }
5341
+
5342
+ //#endregion
5343
+ //#region src/rules/refs/origins.ts
5344
+ /**
5345
+ * Resolve which variables hold refs and which resolve to callable functions.
5346
+ * Maps are keyed by ESLint variables, not source names, so shadowed
5347
+ * identifiers and separate components cannot contaminate each other.
5348
+ */
5349
+ function createBindingResolver(context, facts) {
5350
+ const { additionalRefHooks } = getSettingsFromContext(context);
5351
+ const bindings = /* @__PURE__ */ new Map();
5352
+ const memberBindings = /* @__PURE__ */ new Map();
5353
+ const jsxRefs = /* @__PURE__ */ new Set();
5354
+ function getVariable(node) {
5355
+ return findVariable(context.sourceCode.getScope(node), node) ?? null;
5356
+ }
5357
+ function getBindingValue(node) {
5358
+ const value = Extract.unwrap(node);
5359
+ if (Check.isIdentifier(value)) {
5360
+ const variable = getVariable(value);
5361
+ return variable == null ? { kind: "unknown" } : {
5362
+ kind: "variable",
5363
+ variable
5364
+ };
5365
+ }
5366
+ if (isFunctionExpressionLike(value)) return {
5367
+ kind: "function",
5368
+ node: value
5369
+ };
5370
+ if (value.type === AST_NODE_TYPES.CallExpression && (core.isUseRefLikeCall(value, additionalRefHooks) || core.isCreateRefCall(context, value))) return { kind: "ref" };
5371
+ if (value.type === AST_NODE_TYPES.MemberExpression && Check.isIdentifier(value.property)) {
5372
+ if (isRefLikeName(value.property.name)) return { kind: "ref" };
5373
+ }
5374
+ return { kind: "unknown" };
5375
+ }
5376
+ function addBinding(variable, value, position) {
5377
+ const events = bindings.get(variable) ?? [];
5378
+ bindings.set(variable, events);
5379
+ events.push({
5380
+ position,
5381
+ value
5382
+ });
5383
+ }
5384
+ for (const fact of facts.bindings) switch (fact.kind) {
5385
+ case "function": {
5386
+ const variable = getVariable(fact.id);
5387
+ if (variable != null) addBinding(variable, {
5388
+ kind: "function",
5389
+ node: fact.node
5390
+ }, fact.position);
5391
+ break;
5392
+ }
5393
+ case "identifier": {
5394
+ const variable = getVariable(fact.id);
5395
+ if (variable != null) addBinding(variable, getBindingValue(fact.value), fact.position);
5396
+ break;
5397
+ }
5398
+ case "member": {
5399
+ const variable = getVariable(fact.object);
5400
+ if (variable == null) break;
5401
+ const properties = memberBindings.get(variable) ?? /* @__PURE__ */ new Map();
5402
+ memberBindings.set(variable, properties);
5403
+ const events = properties.get(fact.property) ?? [];
5404
+ properties.set(fact.property, events);
5405
+ events.push({
5406
+ position: fact.position,
5407
+ value: getBindingValue(fact.value)
5408
+ });
5409
+ break;
5410
+ }
5411
+ }
5412
+ for (const node of facts.jsxRefs) {
5413
+ const variable = getVariable(node);
5414
+ if (variable != null) jsxRefs.add(variable);
5415
+ }
5416
+ function resolveRef(variable, position, seen = /* @__PURE__ */ new Set()) {
5417
+ if (seen.has(variable)) return null;
5418
+ seen.add(variable);
5419
+ if (jsxRefs.has(variable) || isRefLikeName(variable.name)) return variable;
5420
+ const event = getLatestValue(bindings.get(variable), position);
5421
+ if (event != null) switch (event.value.kind) {
5422
+ case "ref": return variable;
5423
+ case "variable": return resolveRef(event.value.variable, event.position, seen);
5424
+ case "function":
5425
+ case "unknown": return null;
5426
+ }
5427
+ return null;
5428
+ }
5429
+ function resolveFunction(variable, position, seen = /* @__PURE__ */ new Set()) {
5430
+ if (seen.has(variable)) return null;
5431
+ seen.add(variable);
5432
+ const event = getLatestValue(bindings.get(variable), position);
5433
+ if (event == null) return null;
5434
+ switch (event.value.kind) {
5435
+ case "function": return event.value.node;
5436
+ case "variable": return resolveFunction(event.value.variable, event.position, seen);
5437
+ case "ref":
5438
+ case "unknown": return null;
5439
+ }
5440
+ }
5441
+ function resolveCallable(node, position) {
5442
+ const callee = Extract.unwrap(node);
5443
+ if (isFunctionExpressionLike(callee)) return callee;
5444
+ if (Check.isIdentifier(callee)) {
5445
+ const variable = getVariable(callee);
5446
+ return variable == null ? null : resolveFunction(variable, position);
5447
+ }
5448
+ if (callee.type !== AST_NODE_TYPES.MemberExpression || !Check.isIdentifier(callee.property)) return null;
5449
+ const object = Extract.unwrap(callee.object);
5450
+ const property = callee.property.name;
5451
+ if (!Check.isIdentifier(object)) return null;
5452
+ const variable = getVariable(object);
5453
+ if (variable == null) return null;
5454
+ const event = getLatestValue(memberBindings.get(variable)?.get(property), position);
5455
+ if (event == null) return null;
5456
+ if (event.value.kind === "function") return event.value.node;
5457
+ if (event.value.kind === "variable") return resolveFunction(event.value.variable, event.position);
5458
+ return null;
5459
+ }
5460
+ function getRefTarget(node) {
5461
+ const object = Extract.unwrap(node.object);
5462
+ if (Check.isIdentifier(object)) {
5463
+ const variable = getVariable(object);
5464
+ if (variable == null) return null;
5465
+ const identity = resolveRef(variable, node.range[0]);
5466
+ return identity == null ? null : { identity };
5467
+ }
5468
+ if (object.type === AST_NODE_TYPES.MemberExpression && Check.isIdentifier(object.property)) {
5469
+ if (isRefLikeName(object.property.name)) return { identity: null };
5470
+ }
5471
+ return null;
5472
+ }
5473
+ function getNullBranch(test, identity) {
5474
+ return getNullCheckBranch(test, (candidate) => {
5475
+ if (candidate.type !== AST_NODE_TYPES.MemberExpression) return false;
5476
+ if (!Check.isIdentifier(candidate.property, "current")) return false;
5477
+ return getRefTarget(candidate)?.identity === identity;
5478
+ }, (candidate) => {
5479
+ if (candidate.type === AST_NODE_TYPES.Literal) return candidate.value == null;
5480
+ if (candidate.type === AST_NODE_TYPES.UnaryExpression && candidate.operator === "void") return true;
5481
+ if (!Check.isIdentifier(candidate, "undefined")) return false;
5482
+ const variable = getVariable(candidate);
5483
+ return variable == null || variable.defs.length === 0;
5484
+ });
5485
+ }
5486
+ return {
5487
+ getNullBranch,
5488
+ getRefTarget,
5489
+ getVariable,
5490
+ resolveCallable,
5491
+ resolveRef
5492
+ };
5493
+ }
5494
+ function getLatestValue(events, position) {
5495
+ let latest = null;
5496
+ for (const event of events ?? []) {
5497
+ if (event.position > position) continue;
5498
+ if (latest == null || event.position >= latest.position) latest = event;
5499
+ }
5500
+ return latest;
5501
+ }
5502
+
5275
5503
  //#endregion
5276
5504
  //#region src/rules/refs/refs.ts
5277
5505
  const RULE_NAME$6 = "refs";
@@ -5292,130 +5520,20 @@ var refs_default = createRule({
5292
5520
  defaultOptions: []
5293
5521
  });
5294
5522
  function create$6(context) {
5295
- const hc = core.getHookCollector(context);
5296
- const fc = core.getFunctionComponentCollector(context);
5297
- const { addFunctionBinding, addIdentifierBinding, addJsxRef, addMemberBinding, getNullBranch, getRefTarget, getVariable, resolveCallable, resolveRef } = createBindingResolver(context);
5298
- const calls = [];
5299
- const refAccesses = [];
5300
- return merge(hc.visitor, fc.visitor, {
5301
- AssignmentExpression(node) {
5302
- if (node.operator !== "=") return;
5303
- const left = Extract.unwrap(node.left);
5304
- if (Check.isIdentifier(left)) {
5305
- addIdentifierBinding(left, node.right, node.range[0]);
5306
- return;
5307
- }
5308
- if (left.type !== AST_NODE_TYPES.MemberExpression || !Check.isIdentifier(left.property)) return;
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
- });
5523
+ const hooks = core.getHookCollector(context);
5524
+ const comps = core.getFunctionComponentCollector(context);
5525
+ const facts = createFactCollector();
5526
+ return merge(hooks.visitor, comps.visitor, facts.visitor, { "Program:exit"(program) {
5527
+ const boundaries = /* @__PURE__ */ new Set([...comps.api.getAllComponents(program).map((component) => component.node), ...hooks.api.getAllHooks(program).map((hook) => hook.node)]);
5528
+ const resolver = createBindingResolver(context, facts.facts);
5529
+ const callGraph = inferCallGraph(boundaries, facts.facts.callEdges, resolver.resolveCallable);
5530
+ const reachability = collectReachableFunctions(boundaries, callGraph);
5531
+ const violations = [...inferRefViolations(facts.facts.refAccesses, boundaries, reachability, resolver), ...inferRefPassViolations(facts.facts.callEdges, boundaries, reachability, resolver)];
5532
+ for (const violation of violations) context.report({
5533
+ messageId: match(violation).with({ kind: "duplicate-init" }, () => "duplicateRefInit").with({ kind: "pass" }, () => "refPassedToFunction").with({ kind: "read" }, () => "readDuringRender").with({ kind: "write" }, () => "writeDuringRender").exhaustive(),
5534
+ node: violation.node
5535
+ });
5536
+ } });
5419
5537
  }
5420
5538
 
5421
5539
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-react-x",
3
- "version": "5.20.0",
3
+ "version": "5.20.2",
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.0",
40
- "@eslint-react/core": "5.20.0",
41
- "@eslint-react/eslint": "5.20.0",
42
- "@eslint-react/jsx": "5.20.0",
43
- "@eslint-react/shared": "5.20.0",
44
- "@eslint-react/var": "5.20.0",
39
+ "@eslint-react/ast": "5.20.2",
40
+ "@eslint-react/core": "5.20.2",
41
+ "@eslint-react/eslint": "5.20.2",
42
+ "@eslint-react/jsx": "5.20.2",
43
+ "@eslint-react/shared": "5.20.2",
44
+ "@eslint-react/var": "5.20.2",
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",
@@ -58,7 +58,7 @@
58
58
  "@types/react": "^19.3.0",
59
59
  "@types/react-dom": "^19.3.0",
60
60
  "dedent": "^1.7.2",
61
- "eslint": "^10.10.0",
61
+ "eslint": "^10.11.0",
62
62
  "react": "^19.3.0",
63
63
  "react-dom": "^19.3.0",
64
64
  "tsdown": "^0.23.0",