eslint-plugin-react-x 3.0.0-beta.54 → 3.0.0-beta.55

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 +92 -136
  2. package/package.json +6 -6
package/dist/index.js CHANGED
@@ -68,7 +68,7 @@ const rules$7 = {
68
68
  //#endregion
69
69
  //#region package.json
70
70
  var name$6 = "eslint-plugin-react-x";
71
- var version = "3.0.0-beta.54";
71
+ var version = "3.0.0-beta.55";
72
72
 
73
73
  //#endregion
74
74
  //#region src/utils/create-rule.ts
@@ -4225,126 +4225,6 @@ function create$6(context) {
4225
4225
  //#endregion
4226
4226
  //#region src/rules/refs/refs.ts
4227
4227
  const RULE_NAME$5 = "refs";
4228
- function isWriteAccess(node) {
4229
- const { parent } = node;
4230
- if (parent.type === AST_NODE_TYPES.AssignmentExpression && parent.left === node) return true;
4231
- if (parent.type === AST_NODE_TYPES.UpdateExpression && parent.argument === node) return true;
4232
- return false;
4233
- }
4234
- function isInsideNestedFunction(node, boundary) {
4235
- let current = node.parent;
4236
- while (current != null && current !== boundary) {
4237
- if (ast.isFunction(current)) return true;
4238
- current = current.parent;
4239
- }
4240
- return false;
4241
- }
4242
- /**
4243
- * Check if a ref.current access is part of a lazy initialization pattern.
4244
- *
4245
- * Standard:
4246
- * if (ref.current === null) { ref.current = value; }
4247
- *
4248
- * Inverted (with early return):
4249
- * if (ref.current !== null) { return ...; }
4250
- * ref.current = computeValue();
4251
- * @param node The MemberExpression node for ref.current
4252
- * @param isWrite Whether this access is a write (assignment/update) or a read
4253
- * @returns true if this access is part of a lazy initialization pattern and should be allowed during render
4254
- */
4255
- function isPartOfLazyInitialization(node, isWrite) {
4256
- if (node.object.type !== AST_NODE_TYPES.Identifier) return false;
4257
- const refName = node.object.name;
4258
- if (isInNullCheckTest(node)) return true;
4259
- if (findEnclosingRefNullCheckIf(node, refName) != null) return true;
4260
- if (isWrite && isWriteAfterNullCheckIf(node, refName)) return true;
4261
- return false;
4262
- }
4263
- function isNullCheckOperator(operator) {
4264
- return operator === "===" || operator === "==" || operator === "!==" || operator === "!=";
4265
- }
4266
- /**
4267
- * Check if a test expression is a null check on `ref.current` for a given ref name.
4268
- * @param test The test expression to check
4269
- * @param refName The name of the ref variable (e.g. "myRef") to check against
4270
- * @returns true if the test is of the form `ref.current === null` or `null === ref.current`
4271
- */
4272
- function isRefCurrentNullCheck(test, refName) {
4273
- if (test.type !== AST_NODE_TYPES.BinaryExpression) return false;
4274
- if (!isNullCheckOperator(test.operator)) return false;
4275
- const { left, right } = test;
4276
- const isRefSide = (side) => side.type === AST_NODE_TYPES.MemberExpression && side.object.type === AST_NODE_TYPES.Identifier && side.object.name === refName && side.property.type === AST_NODE_TYPES.Identifier && side.property.name === "current";
4277
- const isNullSide = (side) => side.type === AST_NODE_TYPES.Literal && side.value == null;
4278
- return isRefSide(left) && isNullSide(right) || isRefSide(right) && isNullSide(left);
4279
- }
4280
- /**
4281
- * Check if the node is the operand of a `ref.current === null` test inside an IfStatement.
4282
- * @param node The MemberExpression node for ref.current
4283
- * @returns true if the node is part of a null check test in an if statement
4284
- */
4285
- function isInNullCheckTest(node) {
4286
- const { parent } = node;
4287
- if (parent.type !== AST_NODE_TYPES.BinaryExpression) return false;
4288
- if (!isNullCheckOperator(parent.operator)) return false;
4289
- const otherSide = parent.left === node ? parent.right : parent.left;
4290
- if (otherSide.type !== AST_NODE_TYPES.Literal || otherSide.value != null) return false;
4291
- return parent.parent.type === AST_NODE_TYPES.IfStatement && parent.parent.test === parent;
4292
- }
4293
- /**
4294
- * Walk up from the node to find a containing IfStatement whose test is a null-check
4295
- * on `ref.current` with the given ref name.
4296
- * @param node The MemberExpression node for ref.current
4297
- * @param refName The name of the ref variable (e.g. "myRef") to check against
4298
- * @returns the enclosing IfStatement node if found, or null if not found
4299
- */
4300
- function findEnclosingRefNullCheckIf(node, refName) {
4301
- let current = node.parent;
4302
- while (current != null) {
4303
- if (current.type === AST_NODE_TYPES.IfStatement) return isRefCurrentNullCheck(current.test, refName) ? current : null;
4304
- switch (current.type) {
4305
- case AST_NODE_TYPES.ExpressionStatement:
4306
- case AST_NODE_TYPES.BlockStatement:
4307
- case AST_NODE_TYPES.ReturnStatement:
4308
- case AST_NODE_TYPES.JSXExpressionContainer:
4309
- case AST_NODE_TYPES.JSXElement:
4310
- case AST_NODE_TYPES.JSXOpeningElement:
4311
- case AST_NODE_TYPES.JSXClosingElement:
4312
- case AST_NODE_TYPES.AssignmentExpression:
4313
- case AST_NODE_TYPES.VariableDeclaration:
4314
- case AST_NODE_TYPES.VariableDeclarator:
4315
- case AST_NODE_TYPES.MemberExpression:
4316
- case AST_NODE_TYPES.ChainExpression:
4317
- case AST_NODE_TYPES.CallExpression: break;
4318
- default: return null;
4319
- }
4320
- current = current.parent;
4321
- }
4322
- return null;
4323
- }
4324
- /**
4325
- * Check if a write to `ref.current` comes after a sibling if-statement that null-checks
4326
- * the same ref (inverted lazy init with early return pattern):
4327
- *
4328
- * if (ref.current !== null) { return ...; }
4329
- * ref.current = value; // ← this write
4330
- * @param node The MemberExpression node for ref.current being written to
4331
- * @param refName The name of the ref variable (e.g. "myRef") to check against
4332
- * @returns true if there is a preceding sibling if-statement that null-checks the same ref
4333
- */
4334
- function isWriteAfterNullCheckIf(node, refName) {
4335
- let stmt = node;
4336
- while (stmt.parent != null && stmt.parent.type !== AST_NODE_TYPES.BlockStatement) stmt = stmt.parent;
4337
- if (stmt.parent?.type !== AST_NODE_TYPES.BlockStatement) return false;
4338
- const block = stmt.parent;
4339
- const stmtIdx = block.body.indexOf(stmt);
4340
- if (stmtIdx < 0) return false;
4341
- for (let i = stmtIdx - 1; i >= 0; i--) {
4342
- const sibling = block.body[i];
4343
- if (sibling == null) continue;
4344
- if (sibling.type === AST_NODE_TYPES.IfStatement && isRefCurrentNullCheck(sibling.test, refName)) return true;
4345
- }
4346
- return false;
4347
- }
4348
4228
  var refs_default = createRule({
4349
4229
  meta: {
4350
4230
  type: "problem",
@@ -4364,34 +4244,110 @@ function create$5(context) {
4364
4244
  const cCollector = core.useComponentCollector(context);
4365
4245
  const refAccesses = [];
4366
4246
  const jsxRefIdentifiers = /* @__PURE__ */ new Set();
4367
- function isRefIdentifier(node) {
4368
- if (node.type !== AST_NODE_TYPES.Identifier) return false;
4369
- if (core.isRefLikeName(node.name)) return true;
4370
- if (jsxRefIdentifiers.has(node.name)) return true;
4371
- return core.isInitializedFromRef(node.name, context.sourceCode.getScope(node));
4247
+ /**
4248
+ * Check if the node is the operand of a `ref.current === null` test inside an IfStatement.
4249
+ * @param node The MemberExpression node for ref.current
4250
+ * @returns true if the node is part of a null check test in an if statement
4251
+ */
4252
+ function isInNullCheckTest(node) {
4253
+ const { parent } = node;
4254
+ if (!isMatching({
4255
+ type: AST_NODE_TYPES.BinaryExpression,
4256
+ operator: P.union("===", "==", "!==", "!=")
4257
+ }, parent)) return false;
4258
+ const otherSide = parent.left === node ? parent.right : parent.left;
4259
+ if (otherSide.type !== AST_NODE_TYPES.Literal || otherSide.value != null) return false;
4260
+ return parent.parent.type === AST_NODE_TYPES.IfStatement && parent.parent.test === parent;
4261
+ }
4262
+ /**
4263
+ * Check if a test expression is a null check on `ref.current` for a given ref name.
4264
+ * Matches forms like `ref.current === null`, `null === ref.current`, and their != variants.
4265
+ */
4266
+ function isRefCurrentNullCheck(test, refName) {
4267
+ if (test.type !== AST_NODE_TYPES.BinaryExpression) return false;
4268
+ const op = test.operator;
4269
+ if (op !== "===" && op !== "==" && op !== "!==" && op !== "!=") return false;
4270
+ const { left, right } = test;
4271
+ const checkSides = (a, b) => a.type === AST_NODE_TYPES.MemberExpression && a.object.type === AST_NODE_TYPES.Identifier && a.object.name === refName && a.property.type === AST_NODE_TYPES.Identifier && a.property.name === "current" && b.type === AST_NODE_TYPES.Literal && b.value == null;
4272
+ return checkSides(left, right) || checkSides(right, left);
4372
4273
  }
4373
4274
  return defineRuleListener(hCollector.visitor, cCollector.visitor, {
4374
4275
  JSXAttribute(node) {
4375
- if (node.name.type === AST_NODE_TYPES.JSXIdentifier && node.name.name === "ref" && node.value?.type === AST_NODE_TYPES.JSXExpressionContainer && node.value.expression.type === AST_NODE_TYPES.Identifier) jsxRefIdentifiers.add(node.value.expression.name);
4276
+ switch (true) {
4277
+ case node.name.type === AST_NODE_TYPES.JSXIdentifier && node.name.name === "ref" && node.value?.type === AST_NODE_TYPES.JSXExpressionContainer && node.value.expression.type === AST_NODE_TYPES.Identifier:
4278
+ jsxRefIdentifiers.add(node.value.expression.name);
4279
+ return;
4280
+ }
4376
4281
  },
4377
4282
  MemberExpression(node) {
4378
- if (node.property.type !== AST_NODE_TYPES.Identifier || node.property.name !== "current") return;
4283
+ if (!ast.isIdentifier(node.property, "current")) return;
4379
4284
  refAccesses.push({
4380
4285
  node,
4381
- isWrite: isWriteAccess(node)
4286
+ isWrite: match(node.parent).with({ type: AST_NODE_TYPES.AssignmentExpression }, (p) => p.left === node).with({ type: AST_NODE_TYPES.UpdateExpression }, (p) => p.argument === node).otherwise(() => false)
4382
4287
  });
4383
4288
  },
4384
4289
  "Program:exit"(program) {
4385
- const components = cCollector.ctx.getAllComponents(program);
4290
+ const comps = cCollector.ctx.getAllComponents(program);
4386
4291
  const hooks = hCollector.ctx.getAllHooks(program);
4387
- const componentAndHookFns = new Set([...components.map((c) => c.node), ...hooks.map((h) => h.node)]);
4388
- const isComponentOrHookFn = (n) => ast.isFunction(n) && componentAndHookFns.has(n);
4292
+ const funcs = new Set([...comps.map((c) => c.node), ...hooks.map((h) => h.node)]);
4293
+ const isCompOrHookFn = (n) => ast.isFunction(n) && funcs.has(n);
4389
4294
  for (const { node, isWrite } of refAccesses) {
4390
- if (!isRefIdentifier(node.object)) continue;
4391
- const boundary = ast.findParentNode(node, isComponentOrHookFn);
4295
+ const obj = node.object;
4296
+ if (obj.type !== AST_NODE_TYPES.Identifier) continue;
4297
+ switch (true) {
4298
+ case core.isRefLikeName(obj.name):
4299
+ case jsxRefIdentifiers.has(obj.name):
4300
+ case core.isInitializedFromRef(obj.name, context.sourceCode.getScope(node.object)): break;
4301
+ default: continue;
4302
+ }
4303
+ const boundary = ast.findParentNode(node, isCompOrHookFn);
4392
4304
  if (boundary == null) continue;
4393
- if (isInsideNestedFunction(node, boundary)) continue;
4394
- if (isPartOfLazyInitialization(node, isWrite)) continue;
4305
+ if (ast.findParentNode(node, ast.isFunction) !== boundary) continue;
4306
+ const refName = obj.name;
4307
+ let isLazyInit = isInNullCheckTest(node);
4308
+ if (!isLazyInit) {
4309
+ let current = node.parent;
4310
+ findIf: while (current != null) {
4311
+ if (current.type === AST_NODE_TYPES.IfStatement) {
4312
+ if (isRefCurrentNullCheck(current.test, refName)) isLazyInit = true;
4313
+ break;
4314
+ }
4315
+ switch (current.type) {
4316
+ case AST_NODE_TYPES.ExpressionStatement:
4317
+ case AST_NODE_TYPES.BlockStatement:
4318
+ case AST_NODE_TYPES.ReturnStatement:
4319
+ case AST_NODE_TYPES.JSXExpressionContainer:
4320
+ case AST_NODE_TYPES.JSXElement:
4321
+ case AST_NODE_TYPES.JSXOpeningElement:
4322
+ case AST_NODE_TYPES.JSXClosingElement:
4323
+ case AST_NODE_TYPES.AssignmentExpression:
4324
+ case AST_NODE_TYPES.VariableDeclaration:
4325
+ case AST_NODE_TYPES.VariableDeclarator:
4326
+ case AST_NODE_TYPES.MemberExpression:
4327
+ case AST_NODE_TYPES.ChainExpression:
4328
+ case AST_NODE_TYPES.CallExpression: break;
4329
+ default: break findIf;
4330
+ }
4331
+ current = current.parent;
4332
+ }
4333
+ }
4334
+ if (!isLazyInit && isWrite) {
4335
+ let stmt = node;
4336
+ while (stmt.parent != null && stmt.parent.type !== AST_NODE_TYPES.BlockStatement) stmt = stmt.parent;
4337
+ if (stmt.parent?.type === AST_NODE_TYPES.BlockStatement) {
4338
+ const block = stmt.parent;
4339
+ const stmtIdx = block.body.indexOf(stmt);
4340
+ if (stmtIdx >= 0) for (let i = stmtIdx - 1; i >= 0; i--) {
4341
+ const sibling = block.body[i];
4342
+ if (sibling == null) continue;
4343
+ if (sibling.type === AST_NODE_TYPES.IfStatement && isRefCurrentNullCheck(sibling.test, refName)) {
4344
+ isLazyInit = true;
4345
+ break;
4346
+ }
4347
+ }
4348
+ }
4349
+ }
4350
+ if (isLazyInit) continue;
4395
4351
  context.report({
4396
4352
  messageId: isWrite ? "writeDuringRender" : "readDuringRender",
4397
4353
  node
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-react-x",
3
- "version": "3.0.0-beta.54",
3
+ "version": "3.0.0-beta.55",
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,11 +45,11 @@
45
45
  "string-ts": "^2.3.1",
46
46
  "ts-api-utils": "^2.4.0",
47
47
  "ts-pattern": "^5.9.0",
48
- "@eslint-react/ast": "3.0.0-beta.54",
49
- "@eslint-react/core": "3.0.0-beta.54",
50
- "@eslint-react/eff": "3.0.0-beta.54",
51
- "@eslint-react/var": "3.0.0-beta.54",
52
- "@eslint-react/shared": "3.0.0-beta.54"
48
+ "@eslint-react/eff": "3.0.0-beta.55",
49
+ "@eslint-react/core": "3.0.0-beta.55",
50
+ "@eslint-react/ast": "3.0.0-beta.55",
51
+ "@eslint-react/shared": "3.0.0-beta.55",
52
+ "@eslint-react/var": "3.0.0-beta.55"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/react": "^19.2.14",