eslint-plugin-react-x 5.13.1 → 5.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +0 -1
  2. package/dist/index.js +380 -302
  3. package/package.json +10 -10
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { ESLint, Linter } from "eslint";
2
-
3
2
  //#region src/index.d.ts
4
3
  type ConfigName = "disable-experimental" | "disable-type-checked" | "disable-conflict-eslint-plugin-react" | "disable-conflict-eslint-plugin-react-hooks" | "recommended" | "recommended-type-checked" | "recommended-typescript" | "strict" | "strict-type-checked" | "strict-typescript";
5
4
  declare const finalPlugin: ESLint.Plugin & {
package/dist/index.js CHANGED
@@ -144,7 +144,7 @@ const rules$6 = {
144
144
  //#endregion
145
145
  //#region package.json
146
146
  var name$6 = "eslint-plugin-react-x";
147
- var version = "5.13.1";
147
+ var version = "5.14.0";
148
148
 
149
149
  //#endregion
150
150
  //#region src/utils/create-rule.ts
@@ -1331,7 +1331,7 @@ function resolveToFunctionNode(context, node, seen = /* @__PURE__ */ new Set())
1331
1331
  * Refs are mutable by design and exempted from immutability checks.
1332
1332
  * @param name The identifier name to check.
1333
1333
  */
1334
- function isRefLikeName(name) {
1334
+ function isRefLikeName$1(name) {
1335
1335
  return name === "ref" || name.endsWith("Ref");
1336
1336
  }
1337
1337
  /**
@@ -1340,14 +1340,41 @@ function isRefLikeName(name) {
1340
1340
  * @param node The AST node to inspect.
1341
1341
  */
1342
1342
  function hasRefLikeNameInChain(node) {
1343
- if (node.type === AST_NODE_TYPES.Identifier) return isRefLikeName(node.name);
1343
+ if (node.type === AST_NODE_TYPES.Identifier) return isRefLikeName$1(node.name);
1344
1344
  if (node.type === AST_NODE_TYPES.MemberExpression) {
1345
1345
  const propName = Extract.getPropertyName(node.property);
1346
- if (propName != null && isRefLikeName(propName)) return true;
1346
+ if (propName != null && isRefLikeName$1(propName)) return true;
1347
1347
  return hasRefLikeNameInChain(node.object);
1348
1348
  }
1349
1349
  return false;
1350
1350
  }
1351
+ /**
1352
+ * Check if the root identifier of a member-expression chain (or the
1353
+ * identifier itself) is initialized directly from a `useRef()` call, e.g.
1354
+ * `const mounted = useRef(false); mounted.current = true;`.
1355
+ *
1356
+ * This catches refs regardless of naming convention, complementing
1357
+ * {@link hasRefLikeNameInChain}.
1358
+ * @param context The ESLint rule context.
1359
+ * @param node The AST node to inspect (an identifier or member-expression chain).
1360
+ */
1361
+ function isInitializedFromUseRef(context, node) {
1362
+ const rootId = node.type === AST_NODE_TYPES.Identifier ? node : Extract.getRootIdentifier(node);
1363
+ if (rootId == null) return false;
1364
+ const initNode = resolve(context, rootId);
1365
+ return initNode != null && initNode.type === AST_NODE_TYPES.CallExpression && core.isUseRefCall(context, initNode);
1366
+ }
1367
+ /**
1368
+ * Check if a mutated expression chain should be exempt from immutability
1369
+ * checks because it is rooted at a ref: either by naming convention
1370
+ * ({@link hasRefLikeNameInChain}) or because it is initialized from a
1371
+ * `useRef()` call ({@link isInitializedFromUseRef}).
1372
+ * @param context The ESLint rule context.
1373
+ * @param node The AST node to inspect (an identifier or member-expression chain).
1374
+ */
1375
+ function isRefLikeChain(context, node) {
1376
+ return hasRefLikeNameInChain(node) || isInitializedFromUseRef(context, node);
1377
+ }
1351
1378
 
1352
1379
  //#endregion
1353
1380
  //#region src/rules/immutability/immutability.ts
@@ -1384,11 +1411,11 @@ function create$48(context) {
1384
1411
  const left = Extract.unwrap(node.left);
1385
1412
  switch (left.type) {
1386
1413
  case AST_NODE_TYPES.Identifier:
1387
- if (isRefLikeName(left.name)) return;
1414
+ if (isRefLikeName$1(left.name)) return;
1388
1415
  pushMutationSite(node, left);
1389
1416
  return;
1390
1417
  case AST_NODE_TYPES.MemberExpression: {
1391
- if (hasRefLikeNameInChain(left)) return;
1418
+ if (isRefLikeChain(context, left)) return;
1392
1419
  const rootId = Extract.getRootIdentifier(left);
1393
1420
  if (rootId == null) return;
1394
1421
  pushMutationSite(node, rootId);
@@ -1400,7 +1427,7 @@ function create$48(context) {
1400
1427
  const callee = Extract.unwrap(node.callee);
1401
1428
  if (callee.type === AST_NODE_TYPES.MemberExpression) {
1402
1429
  const propName = Extract.getPropertyName(callee.property);
1403
- if (propName != null && MUTATING_METHODS.has(propName) && !hasRefLikeNameInChain(callee.object)) {
1430
+ if (propName != null && MUTATING_METHODS.has(propName) && !isRefLikeChain(context, callee.object)) {
1404
1431
  const rootId = Extract.getRootIdentifier(callee.object);
1405
1432
  if (rootId != null) pushMutationSite(node, rootId);
1406
1433
  }
@@ -1459,7 +1486,7 @@ function create$48(context) {
1459
1486
  if (node.operator !== "delete") return;
1460
1487
  const arg = Extract.unwrap(node.argument);
1461
1488
  if (arg.type !== AST_NODE_TYPES.MemberExpression) return;
1462
- if (hasRefLikeNameInChain(arg)) return;
1489
+ if (isRefLikeChain(context, arg)) return;
1463
1490
  const rootId = Extract.getRootIdentifier(arg);
1464
1491
  if (rootId == null) return;
1465
1492
  pushMutationSite(node, rootId);
@@ -1468,11 +1495,11 @@ function create$48(context) {
1468
1495
  const arg = Extract.unwrap(node.argument);
1469
1496
  switch (arg.type) {
1470
1497
  case AST_NODE_TYPES.Identifier:
1471
- if (isRefLikeName(arg.name)) return;
1498
+ if (isRefLikeName$1(arg.name)) return;
1472
1499
  pushMutationSite(node, arg);
1473
1500
  return;
1474
1501
  case AST_NODE_TYPES.MemberExpression: {
1475
- if (hasRefLikeNameInChain(arg)) return;
1502
+ if (isRefLikeChain(context, arg)) return;
1476
1503
  const rootId = Extract.getRootIdentifier(arg);
1477
1504
  if (rootId == null) return;
1478
1505
  pushMutationSite(node, rootId);
@@ -4239,83 +4266,64 @@ function create$7(context) {
4239
4266
 
4240
4267
  //#endregion
4241
4268
  //#region src/rules/refs/lib.ts
4269
+ const SYNC_ARRAY_CALLBACKS = /* @__PURE__ */ new Set([
4270
+ "every",
4271
+ "filter",
4272
+ "find",
4273
+ "findIndex",
4274
+ "findLast",
4275
+ "findLastIndex",
4276
+ "flatMap",
4277
+ "forEach",
4278
+ "map",
4279
+ "reduce",
4280
+ "reduceRight",
4281
+ "some",
4282
+ "sort"
4283
+ ]);
4284
+ function getLatestValue(events, position) {
4285
+ let latest = null;
4286
+ for (const event of events ?? []) {
4287
+ if (event.position > position) continue;
4288
+ if (latest == null || event.position >= latest.position) latest = event;
4289
+ }
4290
+ return latest;
4291
+ }
4242
4292
  function isFunctionExpressionLike(node) {
4243
4293
  return node.type === AST_NODE_TYPES.FunctionExpression || node.type === AST_NODE_TYPES.ArrowFunctionExpression;
4244
4294
  }
4245
- function resolveAlias(name, aliases) {
4246
- const seen = /* @__PURE__ */ new Set();
4247
- while (aliases.has(name) && !seen.has(name)) {
4248
- seen.add(name);
4249
- name = aliases.get(name);
4250
- }
4251
- return name;
4252
- }
4253
- /**
4254
- * Check if the node is the operand of a `ref.current === null` test inside an IfStatement.
4255
- * @param node The MemberExpression node for ref.current
4256
- * @returns true if the node is part of a null check test in an if statement
4257
- */
4258
- function isInNullCheckTest(node) {
4259
- let parent = node.parent;
4260
- while (Check.isTypeExpression(parent)) parent = parent.parent;
4261
- const isNullCompareBinary = (n) => {
4262
- return n.type === AST_NODE_TYPES.BinaryExpression && /^(===|==|!==|!=)$/.test(n.operator);
4263
- };
4264
- const isLiteralNull = (n) => {
4265
- return n.type === AST_NODE_TYPES.Literal && n.value == null;
4266
- };
4267
- const isIfTest = (testNode) => {
4268
- return testNode.parent?.type === AST_NODE_TYPES.IfStatement && testNode.parent.test === testNode;
4269
- };
4270
- if (isNullCompareBinary(parent)) {
4271
- if (!isLiteralNull(parent.left === node || Extract.unwrap(parent.left) === node ? parent.right : parent.left)) return false;
4272
- if (isIfTest(parent)) return true;
4273
- const grandparent = parent.parent;
4274
- if (grandparent.type === AST_NODE_TYPES.UnaryExpression && grandparent.operator === "!" && isIfTest(grandparent)) return true;
4275
- return false;
4276
- }
4277
- if (parent.type === AST_NODE_TYPES.UnaryExpression && parent.operator === "!") return isIfTest(parent);
4278
- return false;
4295
+ function isRefLikeName(name) {
4296
+ return name === "ref" || name.endsWith("Ref");
4279
4297
  }
4280
- function isBinaryNullCheckOperand(a, b, refName) {
4281
- a = Check.isTypeExpression(a) ? Extract.unwrap(a) : a;
4282
- if (a.type !== AST_NODE_TYPES.MemberExpression) return false;
4283
- if (Extract.getPropertyName(a.property) !== "current") return false;
4284
- const obj = Extract.unwrap(a.object);
4285
- return obj.type === AST_NODE_TYPES.Identifier && obj.name === refName && b.type === AST_NODE_TYPES.Literal && b.value == null;
4298
+ function getCalleeName(node) {
4299
+ const callee = Extract.unwrap(node.callee);
4300
+ if (callee.type === AST_NODE_TYPES.Identifier) return callee.name;
4301
+ if (callee.type === AST_NODE_TYPES.MemberExpression) return Extract.getPropertyName(callee.property);
4302
+ return null;
4286
4303
  }
4287
- function isBinaryNullCheck(test, refName) {
4288
- if (test.type !== AST_NODE_TYPES.BinaryExpression) return false;
4289
- if (!/^(===|==|!==|!=)$/.test(test.operator)) return false;
4290
- const { left, right } = test;
4291
- return isBinaryNullCheckOperand(left, right, refName) || isBinaryNullCheckOperand(right, left, refName);
4304
+ function getSynchronousCallbackIndexes(node) {
4305
+ const callee = Extract.unwrap(node.callee);
4306
+ const name = getCalleeName(node);
4307
+ if (name == null) return [];
4308
+ if (callee.type === AST_NODE_TYPES.MemberExpression && SYNC_ARRAY_CALLBACKS.has(name)) return [0];
4309
+ switch (name) {
4310
+ case "useMemo":
4311
+ case "useState": return [0];
4312
+ case "useReducer": return [0, 2];
4313
+ default: return [];
4314
+ }
4292
4315
  }
4293
- /**
4294
- * Determine which branch of an `if` statement is guaranteed to see `ref.current` as null, given
4295
- * a test expression. Returns `null` if the test isn't a recognized null check on `refName`.
4296
- * @param test The test expression to check.
4297
- * @param refName The name of the ref variable.
4298
- */
4299
- function getRefCurrentNullCheckBranch(test, refName) {
4300
- if (isBinaryNullCheck(test, refName)) return test.operator === "===" || test.operator === "==" ? "consequent" : "alternate";
4301
- if (test.type === AST_NODE_TYPES.UnaryExpression && test.operator === "!") {
4302
- const arg = Extract.unwrap(test.argument);
4303
- if (arg.type === AST_NODE_TYPES.MemberExpression) {
4304
- const obj = Extract.unwrap(arg.object);
4305
- return obj.type === AST_NODE_TYPES.Identifier && obj.name === refName && Extract.getPropertyName(arg.property) === "current" ? "consequent" : null;
4306
- }
4307
- if (arg.type === AST_NODE_TYPES.BinaryExpression) {
4308
- const inner = getRefCurrentNullCheckBranch(arg, refName);
4309
- if (inner === "consequent") return "alternate";
4310
- if (inner === "alternate") return "consequent";
4311
- }
4316
+ function isReachedThroughFunctions(node, boundary, reached) {
4317
+ let current = node.parent;
4318
+ while (current != null && current !== boundary) {
4319
+ if (Check.isFunction(current) && !reached.has(current)) return false;
4320
+ current = current.parent;
4312
4321
  }
4313
- return null;
4322
+ return current === boundary;
4314
4323
  }
4315
4324
  /**
4316
4325
  * Check whether `node` (a `ref.current` MemberExpression) is being written to indirectly through
4317
4326
  * a nested property write, e.g. `ref.current.inner = value` or `ref.current.inner++`.
4318
- * @param node The MemberExpression node for ref.current
4319
4327
  */
4320
4328
  function isNestedRefCurrentWrite(node) {
4321
4329
  let outer = node;
@@ -4334,19 +4342,87 @@ function isNestedRefCurrentWrite(node) {
4334
4342
  return false;
4335
4343
  }
4336
4344
  }
4337
- function isInitializedFromRef$1(context, name, initialScope) {
4338
- for (const { node } of findVariable(initialScope, name)?.defs ?? []) {
4339
- if (node.type !== AST_NODE_TYPES.VariableDeclarator) continue;
4340
- const init = node.init;
4341
- if (init == null) continue;
4342
- switch (true) {
4343
- case init.type === AST_NODE_TYPES.MemberExpression: {
4344
- const initObj = Extract.unwrap(init.object);
4345
- if (initObj.type === AST_NODE_TYPES.Identifier && (initObj.name === "ref" || initObj.name.endsWith("Ref"))) return true;
4346
- break;
4347
- }
4348
- case init.type === AST_NODE_TYPES.CallExpression && core.isUseRefCall(context, init): return true;
4345
+ function getRefAccess(node) {
4346
+ let parent = node.parent;
4347
+ while (Check.isTypeExpression(parent)) parent = parent.parent;
4348
+ 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;
4349
+ return {
4350
+ isInitializationWrite: parent.type === AST_NODE_TYPES.AssignmentExpression && parent.operator === "=" && (parent.left === node || Extract.unwrap(parent.left) === node),
4351
+ isWrite: isDirectWrite || isNestedRefCurrentWrite(node),
4352
+ node
4353
+ };
4354
+ }
4355
+ /**
4356
+ * Parse an exact nullish check and return the branch where the checked value is null/undefined.
4357
+ * Truthiness checks such as `!ref.current` are deliberately excluded: falsy ref values are not
4358
+ * necessarily uninitialized.
4359
+ */
4360
+ function getNullCheckBranch(test, isCheckedValue, isNullishValue) {
4361
+ const unwrapped = Extract.unwrap(test);
4362
+ if (unwrapped.type === AST_NODE_TYPES.UnaryExpression && unwrapped.operator === "!") {
4363
+ const inner = Extract.unwrap(unwrapped.argument);
4364
+ if (inner.type !== AST_NODE_TYPES.BinaryExpression) return null;
4365
+ const branch = getNullCheckBranch(inner, isCheckedValue, isNullishValue);
4366
+ if (branch === "consequent") return "alternate";
4367
+ if (branch === "alternate") return "consequent";
4368
+ return null;
4369
+ }
4370
+ if (unwrapped.type !== AST_NODE_TYPES.BinaryExpression || !/^(===|==|!==|!=)$/.test(unwrapped.operator)) return null;
4371
+ const matches = (left, right) => {
4372
+ return isCheckedValue(Extract.unwrap(left)) && isNullishValue(Extract.unwrap(right));
4373
+ };
4374
+ if (!matches(unwrapped.left, unwrapped.right) && !matches(unwrapped.right, unwrapped.left)) return null;
4375
+ return unwrapped.operator === "===" || unwrapped.operator === "==" ? "consequent" : "alternate";
4376
+ }
4377
+ function isGuardTestAccess(node, getNullBranch) {
4378
+ let current = node;
4379
+ while (Check.isTypeExpression(current.parent)) current = current.parent;
4380
+ if (current.parent.type === AST_NODE_TYPES.BinaryExpression) current = current.parent;
4381
+ if (current.parent.type === AST_NODE_TYPES.UnaryExpression && current.parent.operator === "!") current = current.parent;
4382
+ return current.parent.type === AST_NODE_TYPES.IfStatement && current.parent.test === current && getNullBranch(current.parent.test) != null;
4383
+ }
4384
+ function getGuardDisposition(node, getNullBranch) {
4385
+ let child = node;
4386
+ let current = node.parent;
4387
+ while (current != null && !Check.isFunction(current)) {
4388
+ if (current.type === AST_NODE_TYPES.IfStatement) {
4389
+ const actualBranch = current.consequent === child ? "consequent" : current.alternate === child ? "alternate" : null;
4390
+ const nullBranch = getNullBranch(current.test);
4391
+ if (actualBranch != null && nullBranch != null) return { inNullBranch: actualBranch === nullBranch };
4392
+ }
4393
+ child = current;
4394
+ current = current.parent;
4395
+ }
4396
+ return null;
4397
+ }
4398
+ /** Return whether every path through a statement exits the current function. */
4399
+ function isUnconditionallyTerminating(statement) {
4400
+ switch (statement.type) {
4401
+ case AST_NODE_TYPES.ReturnStatement:
4402
+ case AST_NODE_TYPES.ThrowStatement: return true;
4403
+ case AST_NODE_TYPES.BlockStatement: {
4404
+ const last = statement.body.at(-1);
4405
+ return last != null && isUnconditionallyTerminating(last);
4349
4406
  }
4407
+ case AST_NODE_TYPES.IfStatement: return statement.alternate != null && isUnconditionallyTerminating(statement.consequent) && isUnconditionallyTerminating(statement.alternate);
4408
+ default: return false;
4409
+ }
4410
+ }
4411
+ function isAfterTerminatingNonNullGuard(node, getNullBranch) {
4412
+ let statement = node;
4413
+ while (statement.parent?.type !== AST_NODE_TYPES.BlockStatement) {
4414
+ if (statement.parent == null || Check.isFunction(statement.parent)) return false;
4415
+ statement = statement.parent;
4416
+ }
4417
+ const block = statement.parent;
4418
+ const index = block.body.indexOf(statement);
4419
+ for (let i = index - 1; i >= 0; i--) {
4420
+ const sibling = block.body[i];
4421
+ if (sibling?.type !== AST_NODE_TYPES.IfStatement) continue;
4422
+ const nullBranch = getNullBranch(sibling.test);
4423
+ if (nullBranch == null) continue;
4424
+ const nonNullBranch = nullBranch === "alternate" ? sibling.consequent : sibling.alternate;
4425
+ return nonNullBranch != null && isUnconditionallyTerminating(nonNullBranch);
4350
4426
  }
4351
4427
  return false;
4352
4428
  }
@@ -4370,251 +4446,253 @@ var refs_default = createRule({
4370
4446
  create: create$6,
4371
4447
  defaultOptions: []
4372
4448
  });
4373
- /**
4374
- * Phase 2 (part): compute the set of functions that are "reached" during render for a given
4375
- * component/hook boundary. A function is reached if it is the boundary itself, or if it is
4376
- * directly called (possibly through a simple variable alias, e.g. `const b = a; b();`) from
4377
- * somewhere that is itself reached. This lets us detect ref mutations/reads that happen inside
4378
- * a helper function which is invoked synchronously during render (see "Ref Mutation in Called
4379
- * Function" in refs.spec.md), as opposed to functions that are merely defined and handed off
4380
- * to be called later (e.g. event handlers, effect callbacks).
4381
- */
4382
- function computeReachedFunctions(boundary, isCompOrHookFn, functionVarBindings, directCallSites, aliases) {
4383
- const reached = /* @__PURE__ */ new Set([boundary]);
4384
- const relevantCalls = directCallSites.filter((c) => Traverse.findParent(c.node, isCompOrHookFn) === boundary);
4385
- let changed = true;
4386
- for (let iterations = 0; changed && iterations < 50; iterations++) {
4387
- changed = false;
4388
- for (const { calleeName, node: callNode } of relevantCalls) {
4389
- const hostFn = Traverse.findParent(callNode, Check.isFunction) ?? boundary;
4390
- if (!reached.has(hostFn)) continue;
4391
- const resolvedName = resolveAlias(calleeName, aliases);
4392
- const target = functionVarBindings.get(resolvedName);
4393
- if (target == null || reached.has(target)) continue;
4394
- if (Traverse.findParent(target, isCompOrHookFn) !== boundary) continue;
4395
- reached.add(target);
4396
- changed = true;
4397
- }
4398
- }
4399
- return reached;
4400
- }
4401
- /**
4402
- * Phase 2 (part): determine whether a ref access node is reached unconditionally during render,
4403
- * i.e. every function boundary between the access and the component/hook `boundary` is itself
4404
- * a function that gets invoked during render (see `computeReachedFunctions`).
4405
- */
4406
- function isReachedDuringRender(node, boundary, reached, alwaysReached) {
4407
- let current = node.parent;
4408
- while (current != null && current !== boundary) {
4409
- if (Check.isFunction(current) && !reached.has(current) && !alwaysReached.has(current)) return false;
4410
- current = current.parent;
4411
- }
4412
- return true;
4413
- }
4414
4449
  function create$6(context) {
4415
4450
  const hc = core.getHookCollector(context);
4416
4451
  const fc = core.getFunctionComponentCollector(context);
4452
+ const bindings = /* @__PURE__ */ new Map();
4453
+ const memberBindings = /* @__PURE__ */ new Map();
4454
+ const jsxRefs = /* @__PURE__ */ new Set();
4455
+ const calls = [];
4417
4456
  const refAccesses = [];
4418
- const jsxRefIdentifiers = /* @__PURE__ */ new Set();
4419
- const aliases = /* @__PURE__ */ new Map();
4420
- const refPassedToFunctions = [];
4421
- const functionVarBindings = /* @__PURE__ */ new Map();
4422
- const directCallSites = [];
4423
- const stateInitializerFunctions = /* @__PURE__ */ new Set();
4457
+ function getVariable(node) {
4458
+ return findVariable(context.sourceCode.getScope(node), node) ?? null;
4459
+ }
4460
+ function addBinding(variable, value, position) {
4461
+ const events = bindings.get(variable) ?? [];
4462
+ bindings.set(variable, events);
4463
+ events.push({
4464
+ position,
4465
+ value
4466
+ });
4467
+ }
4468
+ function addIdentifierBinding(node, value, position = node.range[0]) {
4469
+ const variable = getVariable(node);
4470
+ if (variable != null) addBinding(variable, value, position);
4471
+ }
4472
+ function addMemberBinding(object, property, value, position) {
4473
+ const variable = getVariable(object);
4474
+ if (variable == null) return;
4475
+ const properties = memberBindings.get(variable) ?? /* @__PURE__ */ new Map();
4476
+ memberBindings.set(variable, properties);
4477
+ const events = properties.get(property) ?? [];
4478
+ properties.set(property, events);
4479
+ events.push({
4480
+ position,
4481
+ value
4482
+ });
4483
+ }
4484
+ function bindingValueFrom(node) {
4485
+ const value = Extract.unwrap(node);
4486
+ if (value.type === AST_NODE_TYPES.Identifier) {
4487
+ const variable = getVariable(value);
4488
+ return variable == null ? { kind: "unknown" } : {
4489
+ kind: "variable",
4490
+ variable
4491
+ };
4492
+ }
4493
+ if (isFunctionExpressionLike(value)) return {
4494
+ kind: "function",
4495
+ node: value
4496
+ };
4497
+ if (value.type === AST_NODE_TYPES.CallExpression && (core.isUseRefCall(context, value) || core.isCreateRefCall(context, value))) return { kind: "ref" };
4498
+ if (value.type === AST_NODE_TYPES.MemberExpression) {
4499
+ const property = Extract.getPropertyName(value.property);
4500
+ if (property != null && isRefLikeName(property)) return { kind: "ref" };
4501
+ }
4502
+ return { kind: "unknown" };
4503
+ }
4504
+ function resolveRef(variable, position, seen = /* @__PURE__ */ new Set()) {
4505
+ if (seen.has(variable)) return null;
4506
+ seen.add(variable);
4507
+ if (jsxRefs.has(variable) || isRefLikeName(variable.name)) return variable;
4508
+ const event = getLatestValue(bindings.get(variable), position);
4509
+ if (event != null) switch (event.value.kind) {
4510
+ case "ref": return variable;
4511
+ case "variable": return resolveRef(event.value.variable, event.position, seen);
4512
+ case "function":
4513
+ case "unknown": return null;
4514
+ }
4515
+ return null;
4516
+ }
4517
+ function resolveFunction(variable, position, seen = /* @__PURE__ */ new Set()) {
4518
+ if (seen.has(variable)) return null;
4519
+ seen.add(variable);
4520
+ const event = getLatestValue(bindings.get(variable), position);
4521
+ if (event == null) return null;
4522
+ switch (event.value.kind) {
4523
+ case "function": return event.value.node;
4524
+ case "variable": return resolveFunction(event.value.variable, event.position, seen);
4525
+ case "ref":
4526
+ case "unknown": return null;
4527
+ }
4528
+ }
4529
+ function resolveCallable(node, position) {
4530
+ const callee = Extract.unwrap(node);
4531
+ if (isFunctionExpressionLike(callee)) return callee;
4532
+ if (callee.type === AST_NODE_TYPES.Identifier) {
4533
+ const variable = getVariable(callee);
4534
+ return variable == null ? null : resolveFunction(variable, position);
4535
+ }
4536
+ if (callee.type !== AST_NODE_TYPES.MemberExpression) return null;
4537
+ const object = Extract.unwrap(callee.object);
4538
+ const property = Extract.getPropertyName(callee.property);
4539
+ if (object.type !== AST_NODE_TYPES.Identifier || property == null) return null;
4540
+ const variable = getVariable(object);
4541
+ if (variable == null) return null;
4542
+ const event = getLatestValue(memberBindings.get(variable)?.get(property), position);
4543
+ if (event == null) return null;
4544
+ if (event.value.kind === "function") return event.value.node;
4545
+ if (event.value.kind === "variable") return resolveFunction(event.value.variable, event.position);
4546
+ return null;
4547
+ }
4548
+ function getRefTarget(node) {
4549
+ const object = Extract.unwrap(node.object);
4550
+ if (object.type === AST_NODE_TYPES.Identifier) {
4551
+ const variable = getVariable(object);
4552
+ if (variable == null) return null;
4553
+ const identity = resolveRef(variable, node.range[0]);
4554
+ return identity == null ? null : { identity };
4555
+ }
4556
+ if (object.type === AST_NODE_TYPES.MemberExpression) {
4557
+ const property = Extract.getPropertyName(object.property);
4558
+ if (property != null && isRefLikeName(property)) return { identity: null };
4559
+ }
4560
+ return null;
4561
+ }
4562
+ function getNullBranch(test, identity) {
4563
+ return getNullCheckBranch(test, (candidate) => {
4564
+ if (candidate.type !== AST_NODE_TYPES.MemberExpression || Extract.getPropertyName(candidate.property) !== "current") return false;
4565
+ return getRefTarget(candidate)?.identity === identity;
4566
+ }, (candidate) => {
4567
+ if (candidate.type === AST_NODE_TYPES.Literal) return candidate.value == null;
4568
+ if (candidate.type === AST_NODE_TYPES.UnaryExpression && candidate.operator === "void") return true;
4569
+ if (candidate.type !== AST_NODE_TYPES.Identifier || candidate.name !== "undefined") return false;
4570
+ const variable = getVariable(candidate);
4571
+ return variable == null || variable.defs.length === 0;
4572
+ });
4573
+ }
4424
4574
  return merge(hc.visitor, fc.visitor, {
4425
4575
  AssignmentExpression(node) {
4426
4576
  if (node.operator !== "=") return;
4427
4577
  const left = Extract.unwrap(node.left);
4428
- const right = Extract.unwrap(node.right);
4429
- if (left.type === AST_NODE_TYPES.MemberExpression) {
4430
- const leftObj = Extract.unwrap(left.object);
4431
- const propName = Extract.getPropertyName(left.property);
4432
- if (leftObj.type === AST_NODE_TYPES.Identifier && propName != null && isFunctionExpressionLike(right)) functionVarBindings.set(`${leftObj.name}.${propName}`, right);
4433
- return;
4434
- }
4435
- if (left.type !== AST_NODE_TYPES.Identifier) return;
4436
- if (right.type === AST_NODE_TYPES.Identifier) {
4437
- aliases.set(left.name, right.name);
4578
+ if (left.type === AST_NODE_TYPES.Identifier) {
4579
+ addIdentifierBinding(left, bindingValueFrom(node.right), node.range[0]);
4438
4580
  return;
4439
4581
  }
4440
- if (isFunctionExpressionLike(right)) functionVarBindings.set(left.name, right);
4582
+ if (left.type !== AST_NODE_TYPES.MemberExpression) return;
4583
+ const object = Extract.unwrap(left.object);
4584
+ const property = Extract.getPropertyName(left.property);
4585
+ if (object.type === AST_NODE_TYPES.Identifier && property != null) addMemberBinding(object, property, bindingValueFrom(node.right), node.range[0]);
4441
4586
  },
4442
4587
  CallExpression(node) {
4443
- const callee = Extract.unwrap(node.callee);
4444
- const calleeName = callee.type === AST_NODE_TYPES.Identifier ? callee.name : callee.type === AST_NODE_TYPES.MemberExpression ? Extract.getPropertyName(callee.property) : null;
4445
- if (callee.type === AST_NODE_TYPES.Identifier) directCallSites.push({
4446
- calleeName: callee.name,
4588
+ calls.push(node);
4589
+ },
4590
+ FunctionDeclaration(node) {
4591
+ if (node.id != null) addIdentifierBinding(node.id, {
4592
+ kind: "function",
4447
4593
  node
4448
- });
4449
- else if (callee.type === AST_NODE_TYPES.MemberExpression) {
4450
- const calleeObj = Extract.unwrap(callee.object);
4451
- if (calleeObj.type === AST_NODE_TYPES.Identifier && calleeName != null) directCallSites.push({
4452
- calleeName: `${calleeObj.name}.${calleeName}`,
4453
- node
4454
- });
4455
- }
4456
- if (calleeName === "useState") {
4457
- const [firstArg] = node.arguments;
4458
- if (firstArg != null) {
4459
- const unwrappedFirstArg = Extract.unwrap(firstArg);
4460
- if (isFunctionExpressionLike(unwrappedFirstArg)) stateInitializerFunctions.add(unwrappedFirstArg);
4461
- }
4462
- }
4463
- if (calleeName != null && (calleeName.startsWith("use") || calleeName === "mergeRefs" || calleeName === "render")) return;
4464
- for (const arg of node.arguments) {
4465
- const unwrapped = Extract.unwrap(arg);
4466
- if (unwrapped.type !== AST_NODE_TYPES.Identifier) continue;
4467
- const resolvedName = resolveAlias(unwrapped.name, aliases);
4468
- if (resolvedName === "ref" || resolvedName.endsWith("Ref") || jsxRefIdentifiers.has(resolvedName) || isInitializedFromRef$1(context, resolvedName, context.sourceCode.getScope(unwrapped))) refPassedToFunctions.push({
4469
- callNode: node,
4470
- node: unwrapped
4471
- });
4472
- }
4594
+ }, -1);
4473
4595
  },
4474
4596
  JSXAttribute(node) {
4475
- switch (true) {
4476
- case node.name.type === AST_NODE_TYPES.JSXIdentifier && node.name.name === "ref" && node.value?.type === AST_NODE_TYPES.JSXExpressionContainer: {
4477
- const expr = Extract.unwrap(node.value.expression);
4478
- if (expr.type === AST_NODE_TYPES.Identifier) jsxRefIdentifiers.add(expr.name);
4479
- return;
4480
- }
4481
- }
4597
+ if (node.name.type !== AST_NODE_TYPES.JSXIdentifier || node.name.name !== "ref" || node.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) return;
4598
+ const expression = Extract.unwrap(node.value.expression);
4599
+ if (expression.type !== AST_NODE_TYPES.Identifier) return;
4600
+ const variable = getVariable(expression);
4601
+ if (variable != null) jsxRefs.add(variable);
4482
4602
  },
4483
4603
  MemberExpression(node) {
4484
4604
  if (Extract.getPropertyName(node.property) !== "current") return;
4485
- refAccesses.push({
4486
- isWrite: (() => {
4487
- let parent = node.parent;
4488
- while (Check.isTypeExpression(parent)) parent = parent.parent;
4489
- return match(parent).with({ type: AST_NODE_TYPES.AssignmentExpression }, (p) => p.left === node || Extract.unwrap(p.left) === node).with({ type: AST_NODE_TYPES.UpdateExpression }, (p) => p.argument === node || Extract.unwrap(p.argument) === node).otherwise(() => false) || isNestedRefCurrentWrite(node);
4490
- })(),
4491
- node
4492
- });
4605
+ refAccesses.push(getRefAccess(node));
4493
4606
  },
4494
4607
  "Program:exit"(program) {
4495
- const comps = fc.api.getAllComponents(program);
4496
- const hooks = hc.api.getAllHooks(program);
4497
- const funcs = /* @__PURE__ */ new Set([...comps.map((c) => c.node), ...hooks.map((h) => h.node)]);
4498
- const isCompOrHookFn = (n) => Check.isFunction(n) && funcs.has(n);
4499
- const reachedCache = /* @__PURE__ */ new Map();
4500
- function getReached(boundary) {
4501
- let reached = reachedCache.get(boundary);
4502
- if (reached == null) {
4503
- reached = computeReachedFunctions(boundary, isCompOrHookFn, functionVarBindings, directCallSites, aliases);
4504
- reachedCache.set(boundary, reached);
4505
- }
4506
- return reached;
4507
- }
4508
- const lazyInitSeen = /* @__PURE__ */ new Map();
4509
- for (const { isWrite, node } of refAccesses) {
4510
- const obj = Extract.unwrap(node.object);
4511
- let refName = null;
4512
- if (obj.type === AST_NODE_TYPES.Identifier) {
4513
- const resolvedName = resolveAlias(obj.name, aliases);
4514
- switch (true) {
4515
- case resolvedName === "ref" || resolvedName.endsWith("Ref"):
4516
- case jsxRefIdentifiers.has(resolvedName):
4517
- case isInitializedFromRef$1(context, resolvedName, context.sourceCode.getScope(node.object)):
4518
- refName = resolvedName;
4519
- break;
4520
- default: continue;
4608
+ const boundaries = /* @__PURE__ */ new Set([...fc.api.getAllComponents(program).map((component) => component.node), ...hc.api.getAllHooks(program).map((hook) => hook.node)]);
4609
+ const isBoundary = (node) => {
4610
+ return Check.isFunction(node) && boundaries.has(node);
4611
+ };
4612
+ const reachedByBoundary = /* @__PURE__ */ new Map();
4613
+ for (const boundary of boundaries) reachedByBoundary.set(boundary, /* @__PURE__ */ new Set([boundary]));
4614
+ let changed = true;
4615
+ while (changed) {
4616
+ changed = false;
4617
+ for (const call of calls) {
4618
+ const boundary = Traverse.findParent(call, isBoundary);
4619
+ if (boundary == null) continue;
4620
+ const reached = reachedByBoundary.get(boundary);
4621
+ if (reached == null) continue;
4622
+ const host = Traverse.findParent(call, Check.isFunction) ?? boundary;
4623
+ if (!reached.has(host)) continue;
4624
+ const targets = [resolveCallable(call.callee, call.range[0])];
4625
+ for (const index of getSynchronousCallbackIndexes(call)) {
4626
+ const argument = call.arguments[index];
4627
+ if (argument == null || argument.type === AST_NODE_TYPES.SpreadElement) continue;
4628
+ targets.push(resolveCallable(argument, call.range[0]));
4521
4629
  }
4522
- } else if (obj.type === AST_NODE_TYPES.MemberExpression) {
4523
- const propName = Extract.getPropertyName(obj.property);
4524
- if (propName == null || propName !== "ref" && !propName.endsWith("Ref")) continue;
4525
- } else continue;
4526
- const boundary = Traverse.findParent(node, isCompOrHookFn);
4527
- if (boundary == null) continue;
4528
- if (!isReachedDuringRender(node, boundary, getReached(boundary), stateInitializerFunctions)) continue;
4529
- let isLazyInit = refName != null && isInNullCheckTest(node);
4530
- let matchedGuardIf = false;
4531
- if (!isLazyInit && refName != null) {
4532
- let current = node.parent;
4533
- let prevChild = node;
4534
- findLoop: while (true) {
4535
- if (current.type === AST_NODE_TYPES.IfStatement) {
4536
- const nullBranch = getRefCurrentNullCheckBranch(current.test, refName);
4537
- const actualBranch = current.consequent === prevChild ? "consequent" : current.alternate === prevChild ? "alternate" : null;
4538
- if (nullBranch != null && actualBranch != null) {
4539
- matchedGuardIf = true;
4540
- isLazyInit = actualBranch === nullBranch ? isWrite : !isWrite;
4541
- }
4542
- break;
4543
- }
4544
- switch (current.type) {
4545
- case AST_NODE_TYPES.ExpressionStatement:
4546
- case AST_NODE_TYPES.BlockStatement:
4547
- case AST_NODE_TYPES.ReturnStatement:
4548
- case AST_NODE_TYPES.JSXExpressionContainer:
4549
- case AST_NODE_TYPES.JSXElement:
4550
- case AST_NODE_TYPES.JSXOpeningElement:
4551
- case AST_NODE_TYPES.JSXClosingElement:
4552
- case AST_NODE_TYPES.AssignmentExpression:
4553
- case AST_NODE_TYPES.VariableDeclaration:
4554
- case AST_NODE_TYPES.VariableDeclarator:
4555
- case AST_NODE_TYPES.MemberExpression:
4556
- case AST_NODE_TYPES.ChainExpression:
4557
- case AST_NODE_TYPES.CallExpression: break;
4558
- default: break findLoop;
4559
- }
4560
- prevChild = current;
4561
- current = current.parent;
4630
+ for (const target of targets) {
4631
+ if (target == null || reached.has(target) || Traverse.findParent(target, isBoundary) !== boundary) continue;
4632
+ reached.add(target);
4633
+ changed = true;
4562
4634
  }
4563
4635
  }
4564
- if (!isLazyInit && isWrite && refName != null) {
4565
- let stmt = node;
4566
- while (stmt.parent != null && stmt.parent.type !== AST_NODE_TYPES.BlockStatement) stmt = stmt.parent;
4567
- if (stmt.parent?.type === AST_NODE_TYPES.BlockStatement) {
4568
- const block = stmt.parent;
4569
- const stmtIdx = block.body.indexOf(stmt);
4570
- if (stmtIdx >= 0) for (let i = stmtIdx - 1; i >= 0; i--) {
4571
- const sibling = block.body[i];
4572
- if (sibling == null) continue;
4573
- if (sibling.type === AST_NODE_TYPES.IfStatement && getRefCurrentNullCheckBranch(sibling.test, refName) === "alternate") {
4574
- isLazyInit = true;
4575
- break;
4576
- }
4577
- }
4578
- }
4636
+ }
4637
+ function isReachedDuringRender(node, boundary) {
4638
+ const reached = reachedByBoundary.get(boundary);
4639
+ return reached != null && isReachedThroughFunctions(node, boundary, reached);
4640
+ }
4641
+ const initializedRefs = /* @__PURE__ */ new Map();
4642
+ for (const access of refAccesses.toSorted((a, b) => a.node.range[0] - b.node.range[0])) {
4643
+ const target = getRefTarget(access.node);
4644
+ if (target == null) continue;
4645
+ const boundary = Traverse.findParent(access.node, isBoundary);
4646
+ if (boundary == null || !isReachedDuringRender(access.node, boundary)) continue;
4647
+ let isLazyInitialization = false;
4648
+ if (target.identity != null) {
4649
+ const identity = target.identity;
4650
+ const getTargetNullBranch = (test) => getNullBranch(test, identity);
4651
+ if (isGuardTestAccess(access.node, getTargetNullBranch)) continue;
4652
+ const guard = getGuardDisposition(access.node, getTargetNullBranch);
4653
+ if (guard != null) {
4654
+ if (guard.inNullBranch && access.isInitializationWrite) isLazyInitialization = true;
4655
+ else if (!guard.inNullBranch && !access.isWrite) continue;
4656
+ } else if (access.isInitializationWrite && isAfterTerminatingNonNullGuard(access.node, getTargetNullBranch)) isLazyInitialization = true;
4579
4657
  }
4580
- if (isLazyInit && matchedGuardIf && isWrite && refName != null) {
4581
- const seen = lazyInitSeen.get(boundary) ?? /* @__PURE__ */ new Set();
4582
- lazyInitSeen.set(boundary, seen);
4583
- if (seen.has(refName)) {
4584
- context.report({
4585
- messageId: "duplicateRefInit",
4586
- node
4587
- });
4588
- continue;
4589
- }
4590
- seen.add(refName);
4658
+ if (isLazyInitialization && target.identity != null) {
4659
+ const initialized = initializedRefs.get(boundary) ?? /* @__PURE__ */ new Set();
4660
+ initializedRefs.set(boundary, initialized);
4661
+ if (initialized.has(target.identity)) context.report({
4662
+ messageId: "duplicateRefInit",
4663
+ node: access.node
4664
+ });
4665
+ else initialized.add(target.identity);
4666
+ continue;
4591
4667
  }
4592
- if (isLazyInit) continue;
4593
4668
  context.report({
4594
- messageId: isWrite ? "writeDuringRender" : "readDuringRender",
4595
- node
4669
+ messageId: access.isWrite ? "writeDuringRender" : "readDuringRender",
4670
+ node: access.node
4596
4671
  });
4597
4672
  }
4598
- for (const { node } of refPassedToFunctions) {
4599
- const boundary = Traverse.findParent(node, isCompOrHookFn);
4600
- if (boundary == null) continue;
4601
- if (!isReachedDuringRender(node, boundary, getReached(boundary), stateInitializerFunctions)) continue;
4602
- context.report({
4603
- messageId: "refPassedToFunction",
4604
- node
4605
- });
4673
+ for (const call of calls) {
4674
+ const boundary = Traverse.findParent(call, isBoundary);
4675
+ if (boundary == null || !isReachedDuringRender(call, boundary)) continue;
4676
+ const callee = Extract.unwrap(call.callee);
4677
+ const calleeName = getCalleeName(call);
4678
+ const callArguments = call.arguments;
4679
+ if (core.isHookCall(call) || calleeName === "mergeRefs" || calleeName === "render" && callee.type === AST_NODE_TYPES.MemberExpression) continue;
4680
+ for (const argument of callArguments) {
4681
+ if (argument.type === AST_NODE_TYPES.SpreadElement) continue;
4682
+ const value = Extract.unwrap(argument);
4683
+ if (value.type !== AST_NODE_TYPES.Identifier) continue;
4684
+ const variable = getVariable(value);
4685
+ if (variable == null || resolveRef(variable, value.range[0]) == null) continue;
4686
+ context.report({
4687
+ messageId: "refPassedToFunction",
4688
+ node: value
4689
+ });
4690
+ }
4606
4691
  }
4607
4692
  },
4608
4693
  VariableDeclarator(node) {
4609
- if (node.init == null) return;
4610
- const id = node.id;
4611
- if (id.type !== AST_NODE_TYPES.Identifier) return;
4612
- const init = Extract.unwrap(node.init);
4613
- if (init.type === AST_NODE_TYPES.Identifier) {
4614
- aliases.set(id.name, init.name);
4615
- return;
4616
- }
4617
- if (isFunctionExpressionLike(init)) functionVarBindings.set(id.name, init);
4694
+ if (node.id.type !== AST_NODE_TYPES.Identifier || node.init == null) return;
4695
+ addIdentifierBinding(node.id, bindingValueFrom(node.init), node.range[0]);
4618
4696
  }
4619
4697
  });
4620
4698
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-react-x",
3
- "version": "5.13.1",
3
+ "version": "5.14.0",
4
4
  "description": "A set of composable ESLint rules for libraries and frameworks that use React as a UI runtime.",
5
5
  "keywords": [
6
6
  "react",
@@ -45,12 +45,12 @@
45
45
  "string-ts": "^2.3.1",
46
46
  "ts-api-utils": "^2.5.0",
47
47
  "ts-pattern": "^5.9.0",
48
- "@eslint-react/ast": "5.13.1",
49
- "@eslint-react/core": "5.13.1",
50
- "@eslint-react/eslint": "5.13.1",
51
- "@eslint-react/var": "5.13.1",
52
- "@eslint-react/shared": "5.13.1",
53
- "@eslint-react/jsx": "5.13.1"
48
+ "@eslint-react/ast": "5.14.0",
49
+ "@eslint-react/eslint": "5.14.0",
50
+ "@eslint-react/jsx": "5.14.0",
51
+ "@eslint-react/core": "5.14.0",
52
+ "@eslint-react/shared": "5.14.0",
53
+ "@eslint-react/var": "5.14.0"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/react": "^19.2.17",
@@ -59,12 +59,12 @@
59
59
  "eslint": "^10.6.0",
60
60
  "react": "^19.2.7",
61
61
  "react-dom": "^19.2.7",
62
- "tsdown": "^0.22.3",
62
+ "tsdown": "^0.22.4",
63
63
  "tsl": "^1.0.30",
64
64
  "tsl-dx": "^0.13.2",
65
65
  "typescript": "6.0.3",
66
- "@local/configs": "0.0.0",
67
- "@local/eff": "0.0.0"
66
+ "@local/eff": "0.0.0",
67
+ "@local/configs": "0.0.0"
68
68
  },
69
69
  "peerDependencies": {
70
70
  "eslint": "*",