eslint-plugin-react-x 5.12.1 → 5.13.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 (2) hide show
  1. package/dist/index.js +87 -37
  2. package/package.json +7 -7
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.12.1";
147
+ var version = "5.13.0";
148
148
 
149
149
  //#endregion
150
150
  //#region src/utils/create-rule.ts
@@ -4239,6 +4239,17 @@ function create$7(context) {
4239
4239
 
4240
4240
  //#endregion
4241
4241
  //#region src/rules/refs/lib.ts
4242
+ function isFunctionExpressionLike(node) {
4243
+ return node.type === AST_NODE_TYPES.FunctionExpression || node.type === AST_NODE_TYPES.ArrowFunctionExpression;
4244
+ }
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
+ }
4242
4253
  /**
4243
4254
  * Check if the node is the operand of a `ref.current === null` test inside an IfStatement.
4244
4255
  * @param node The MemberExpression node for ref.current
@@ -4266,37 +4277,62 @@ function isInNullCheckTest(node) {
4266
4277
  if (parent.type === AST_NODE_TYPES.UnaryExpression && parent.operator === "!") return isIfTest(parent);
4267
4278
  return false;
4268
4279
  }
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;
4286
+ }
4269
4287
  function isBinaryNullCheck(test, refName) {
4270
4288
  if (test.type !== AST_NODE_TYPES.BinaryExpression) return false;
4271
4289
  if (!/^(===|==|!==|!=)$/.test(test.operator)) return false;
4272
4290
  const { left, right } = test;
4273
- const checkSides = (a, b) => {
4274
- a = Check.isTypeExpression(a) ? Extract.unwrap(a) : a;
4275
- if (a.type !== AST_NODE_TYPES.MemberExpression) return false;
4276
- if (Extract.getPropertyName(a.property) !== "current") return false;
4277
- const obj = Extract.unwrap(a.object);
4278
- return obj.type === AST_NODE_TYPES.Identifier && obj.name === refName && b.type === AST_NODE_TYPES.Literal && b.value == null;
4279
- };
4280
- return checkSides(left, right) || checkSides(right, left);
4291
+ return isBinaryNullCheckOperand(left, right, refName) || isBinaryNullCheckOperand(right, left, refName);
4281
4292
  }
4282
4293
  /**
4283
- * Check if a test expression is a null check on `ref.current` for a given ref name.
4284
- * Matches forms like `ref.current === null`, `null === ref.current`, `!ref.current`,
4285
- * `!(ref.current === null)`, and their != variants.
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`.
4286
4296
  * @param test The test expression to check.
4287
4297
  * @param refName The name of the ref variable.
4288
4298
  */
4289
- function isRefCurrentNullCheck(test, refName) {
4290
- if (isBinaryNullCheck(test, refName)) return true;
4299
+ function getRefCurrentNullCheckBranch(test, refName) {
4300
+ if (isBinaryNullCheck(test, refName)) return test.operator === "===" || test.operator === "==" ? "consequent" : "alternate";
4291
4301
  if (test.type === AST_NODE_TYPES.UnaryExpression && test.operator === "!") {
4292
4302
  const arg = Extract.unwrap(test.argument);
4293
4303
  if (arg.type === AST_NODE_TYPES.MemberExpression) {
4294
4304
  const obj = Extract.unwrap(arg.object);
4295
- return obj.type === AST_NODE_TYPES.Identifier && obj.name === refName && Extract.getPropertyName(arg.property) === "current";
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";
4296
4311
  }
4297
- return isBinaryNullCheck(arg, refName);
4298
4312
  }
4299
- return false;
4313
+ return null;
4314
+ }
4315
+ /**
4316
+ * Check whether `node` (a `ref.current` MemberExpression) is being written to indirectly through
4317
+ * a nested property write, e.g. `ref.current.inner = value` or `ref.current.inner++`.
4318
+ * @param node The MemberExpression node for ref.current
4319
+ */
4320
+ function isNestedRefCurrentWrite(node) {
4321
+ let outer = node;
4322
+ let sawNesting = false;
4323
+ while (true) {
4324
+ let parent = outer.parent;
4325
+ while (Check.isTypeExpression(parent)) parent = parent.parent;
4326
+ if (parent.type === AST_NODE_TYPES.MemberExpression && (parent.object === outer || Extract.unwrap(parent.object) === outer)) {
4327
+ outer = parent;
4328
+ sawNesting = true;
4329
+ continue;
4330
+ }
4331
+ if (!sawNesting) return false;
4332
+ if (parent.type === AST_NODE_TYPES.AssignmentExpression) return parent.left === outer || Extract.unwrap(parent.left) === outer;
4333
+ if (parent.type === AST_NODE_TYPES.UpdateExpression) return parent.argument === outer || Extract.unwrap(parent.argument) === outer;
4334
+ return false;
4335
+ }
4300
4336
  }
4301
4337
  function isInitializedFromRef$1(context, name, initialScope) {
4302
4338
  for (const { node } of findVariable(initialScope, name)?.defs ?? []) {
@@ -4334,17 +4370,6 @@ var refs_default = createRule({
4334
4370
  create: create$6,
4335
4371
  defaultOptions: []
4336
4372
  });
4337
- function resolveAlias(name, aliases) {
4338
- const seen = /* @__PURE__ */ new Set();
4339
- while (aliases.has(name) && !seen.has(name)) {
4340
- seen.add(name);
4341
- name = aliases.get(name);
4342
- }
4343
- return name;
4344
- }
4345
- function isFunctionExpressionLike(node) {
4346
- return node.type === AST_NODE_TYPES.FunctionExpression || node.type === AST_NODE_TYPES.ArrowFunctionExpression;
4347
- }
4348
4373
  /**
4349
4374
  * Phase 2 (part): compute the set of functions that are "reached" during render for a given
4350
4375
  * component/hook boundary. A function is reached if it is the boundary itself, or if it is
@@ -4378,10 +4403,10 @@ function computeReachedFunctions(boundary, isCompOrHookFn, functionVarBindings,
4378
4403
  * i.e. every function boundary between the access and the component/hook `boundary` is itself
4379
4404
  * a function that gets invoked during render (see `computeReachedFunctions`).
4380
4405
  */
4381
- function isReachedDuringRender(node, boundary, reached) {
4406
+ function isReachedDuringRender(node, boundary, reached, alwaysReached) {
4382
4407
  let current = node.parent;
4383
4408
  while (current != null && current !== boundary) {
4384
- if (Check.isFunction(current) && !reached.has(current)) return false;
4409
+ if (Check.isFunction(current) && !reached.has(current) && !alwaysReached.has(current)) return false;
4385
4410
  current = current.parent;
4386
4411
  }
4387
4412
  return true;
@@ -4395,11 +4420,18 @@ function create$6(context) {
4395
4420
  const refPassedToFunctions = [];
4396
4421
  const functionVarBindings = /* @__PURE__ */ new Map();
4397
4422
  const directCallSites = [];
4423
+ const stateInitializerFunctions = /* @__PURE__ */ new Set();
4398
4424
  return merge(hc.visitor, fc.visitor, {
4399
4425
  AssignmentExpression(node) {
4400
4426
  if (node.operator !== "=") return;
4401
4427
  const left = Extract.unwrap(node.left);
4402
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
+ }
4403
4435
  if (left.type !== AST_NODE_TYPES.Identifier) return;
4404
4436
  if (right.type === AST_NODE_TYPES.Identifier) {
4405
4437
  aliases.set(left.name, right.name);
@@ -4414,7 +4446,21 @@ function create$6(context) {
4414
4446
  calleeName: callee.name,
4415
4447
  node
4416
4448
  });
4417
- if (calleeName != null && (calleeName.startsWith("use") || calleeName === "mergeRefs")) return;
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;
4418
4464
  for (const arg of node.arguments) {
4419
4465
  const unwrapped = Extract.unwrap(arg);
4420
4466
  if (unwrapped.type !== AST_NODE_TYPES.Identifier) continue;
@@ -4440,7 +4486,7 @@ function create$6(context) {
4440
4486
  isWrite: (() => {
4441
4487
  let parent = node.parent;
4442
4488
  while (Check.isTypeExpression(parent)) parent = parent.parent;
4443
- 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);
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);
4444
4490
  })(),
4445
4491
  node
4446
4492
  });
@@ -4479,16 +4525,19 @@ function create$6(context) {
4479
4525
  } else continue;
4480
4526
  const boundary = Traverse.findParent(node, isCompOrHookFn);
4481
4527
  if (boundary == null) continue;
4482
- if (!isReachedDuringRender(node, boundary, getReached(boundary))) continue;
4528
+ if (!isReachedDuringRender(node, boundary, getReached(boundary), stateInitializerFunctions)) continue;
4483
4529
  let isLazyInit = refName != null && isInNullCheckTest(node);
4484
4530
  let matchedGuardIf = false;
4485
4531
  if (!isLazyInit && refName != null) {
4486
4532
  let current = node.parent;
4533
+ let prevChild = node;
4487
4534
  findLoop: while (true) {
4488
4535
  if (current.type === AST_NODE_TYPES.IfStatement) {
4489
- if (isRefCurrentNullCheck(current.test, refName)) {
4490
- isLazyInit = true;
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) {
4491
4539
  matchedGuardIf = true;
4540
+ isLazyInit = actualBranch === nullBranch ? isWrite : !isWrite;
4492
4541
  }
4493
4542
  break;
4494
4543
  }
@@ -4508,6 +4557,7 @@ function create$6(context) {
4508
4557
  case AST_NODE_TYPES.CallExpression: break;
4509
4558
  default: break findLoop;
4510
4559
  }
4560
+ prevChild = current;
4511
4561
  current = current.parent;
4512
4562
  }
4513
4563
  }
@@ -4520,7 +4570,7 @@ function create$6(context) {
4520
4570
  if (stmtIdx >= 0) for (let i = stmtIdx - 1; i >= 0; i--) {
4521
4571
  const sibling = block.body[i];
4522
4572
  if (sibling == null) continue;
4523
- if (sibling.type === AST_NODE_TYPES.IfStatement && isRefCurrentNullCheck(sibling.test, refName)) {
4573
+ if (sibling.type === AST_NODE_TYPES.IfStatement && getRefCurrentNullCheckBranch(sibling.test, refName) === "alternate") {
4524
4574
  isLazyInit = true;
4525
4575
  break;
4526
4576
  }
@@ -4548,7 +4598,7 @@ function create$6(context) {
4548
4598
  for (const { node } of refPassedToFunctions) {
4549
4599
  const boundary = Traverse.findParent(node, isCompOrHookFn);
4550
4600
  if (boundary == null) continue;
4551
- if (!isReachedDuringRender(node, boundary, getReached(boundary))) continue;
4601
+ if (!isReachedDuringRender(node, boundary, getReached(boundary), stateInitializerFunctions)) continue;
4552
4602
  context.report({
4553
4603
  messageId: "refPassedToFunction",
4554
4604
  node
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-react-x",
3
- "version": "5.12.1",
3
+ "version": "5.13.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.12.1",
49
- "@eslint-react/jsx": "5.12.1",
50
- "@eslint-react/core": "5.12.1",
51
- "@eslint-react/var": "5.12.1",
52
- "@eslint-react/shared": "5.12.1",
53
- "@eslint-react/eslint": "5.12.1"
48
+ "@eslint-react/core": "5.13.0",
49
+ "@eslint-react/ast": "5.13.0",
50
+ "@eslint-react/eslint": "5.13.0",
51
+ "@eslint-react/var": "5.13.0",
52
+ "@eslint-react/jsx": "5.13.0",
53
+ "@eslint-react/shared": "5.13.0"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/react": "^19.2.17",