eslint-plugin-react-x 3.0.0-next.53 → 3.0.0-next.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 +99 -137
  2. package/package.json +8 -8
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { ESLintUtils } from "@typescript-eslint/utils";
5
5
  import { P, isMatching, match } from "ts-pattern";
6
6
  import ts from "typescript";
7
7
  import { AST_NODE_TYPES } from "@typescript-eslint/types";
8
- import { findEnclosingAssignmentTarget, findVariable, getChildScopes, getObjectType, getVariableInitializer, isAssignmentTargetEqual } from "@eslint-react/var";
8
+ import { findEnclosingAssignmentTarget, findVariable, getObjectType, getVariableInitializer, isAssignmentTargetEqual } from "@eslint-react/var";
9
9
  import { DefinitionType } from "@typescript-eslint/scope-manager";
10
10
  import { constFalse, constTrue, constVoid, flow, getOrElseUpdate, identity, not, unit } from "@eslint-react/eff";
11
11
  import { compare } from "compare-versions";
@@ -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-next.53";
71
+ var version = "3.0.0-next.55";
72
72
 
73
73
  //#endregion
74
74
  //#region src/utils/create-rule.ts
@@ -3173,6 +3173,9 @@ function create$21(context) {
3173
3173
  return initNode;
3174
3174
  }).otherwise(() => null);
3175
3175
  if (arg0Node == null) return;
3176
+ function getChildScopes(scope) {
3177
+ return scope.childScopes.reduce((acc, child) => [...acc, ...getChildScopes(child)], [scope]);
3178
+ }
3176
3179
  if (!getChildScopes(context.sourceCode.getScope(arg0Node)).flatMap((x) => x.references).some((x) => x.resolved?.scope.block === component)) {
3177
3180
  context.report({
3178
3181
  messageId: "default",
@@ -3254,6 +3257,9 @@ function create$20(context) {
3254
3257
  return variableNode;
3255
3258
  }).otherwise(() => null);
3256
3259
  if (arg0Node == null) return;
3260
+ function getChildScopes(scope) {
3261
+ return scope.childScopes.reduce((acc, child) => [...acc, ...getChildScopes(child)], [scope]);
3262
+ }
3257
3263
  if (!getChildScopes(context.sourceCode.getScope(arg0Node)).flatMap((x) => x.references).some((x) => x.resolved?.scope.block === component)) {
3258
3264
  context.report({
3259
3265
  messageId: "default",
@@ -4219,126 +4225,6 @@ function create$6(context) {
4219
4225
  //#endregion
4220
4226
  //#region src/rules/refs/refs.ts
4221
4227
  const RULE_NAME$5 = "refs";
4222
- function isWriteAccess(node) {
4223
- const { parent } = node;
4224
- if (parent.type === AST_NODE_TYPES.AssignmentExpression && parent.left === node) return true;
4225
- if (parent.type === AST_NODE_TYPES.UpdateExpression && parent.argument === node) return true;
4226
- return false;
4227
- }
4228
- function isInsideNestedFunction(node, boundary) {
4229
- let current = node.parent;
4230
- while (current != null && current !== boundary) {
4231
- if (ast.isFunction(current)) return true;
4232
- current = current.parent;
4233
- }
4234
- return false;
4235
- }
4236
- /**
4237
- * Check if a ref.current access is part of a lazy initialization pattern.
4238
- *
4239
- * Standard:
4240
- * if (ref.current === null) { ref.current = value; }
4241
- *
4242
- * Inverted (with early return):
4243
- * if (ref.current !== null) { return ...; }
4244
- * ref.current = computeValue();
4245
- * @param node The MemberExpression node for ref.current
4246
- * @param isWrite Whether this access is a write (assignment/update) or a read
4247
- * @returns true if this access is part of a lazy initialization pattern and should be allowed during render
4248
- */
4249
- function isPartOfLazyInitialization(node, isWrite) {
4250
- if (node.object.type !== AST_NODE_TYPES.Identifier) return false;
4251
- const refName = node.object.name;
4252
- if (isInNullCheckTest(node)) return true;
4253
- if (findEnclosingRefNullCheckIf(node, refName) != null) return true;
4254
- if (isWrite && isWriteAfterNullCheckIf(node, refName)) return true;
4255
- return false;
4256
- }
4257
- function isNullCheckOperator(operator) {
4258
- return operator === "===" || operator === "==" || operator === "!==" || operator === "!=";
4259
- }
4260
- /**
4261
- * Check if a test expression is a null check on `ref.current` for a given ref name.
4262
- * @param test The test expression to check
4263
- * @param refName The name of the ref variable (e.g. "myRef") to check against
4264
- * @returns true if the test is of the form `ref.current === null` or `null === ref.current`
4265
- */
4266
- function isRefCurrentNullCheck(test, refName) {
4267
- if (test.type !== AST_NODE_TYPES.BinaryExpression) return false;
4268
- if (!isNullCheckOperator(test.operator)) return false;
4269
- const { left, right } = test;
4270
- 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";
4271
- const isNullSide = (side) => side.type === AST_NODE_TYPES.Literal && side.value == null;
4272
- return isRefSide(left) && isNullSide(right) || isRefSide(right) && isNullSide(left);
4273
- }
4274
- /**
4275
- * Check if the node is the operand of a `ref.current === null` test inside an IfStatement.
4276
- * @param node The MemberExpression node for ref.current
4277
- * @returns true if the node is part of a null check test in an if statement
4278
- */
4279
- function isInNullCheckTest(node) {
4280
- const { parent } = node;
4281
- if (parent.type !== AST_NODE_TYPES.BinaryExpression) return false;
4282
- if (!isNullCheckOperator(parent.operator)) return false;
4283
- const otherSide = parent.left === node ? parent.right : parent.left;
4284
- if (otherSide.type !== AST_NODE_TYPES.Literal || otherSide.value != null) return false;
4285
- return parent.parent.type === AST_NODE_TYPES.IfStatement && parent.parent.test === parent;
4286
- }
4287
- /**
4288
- * Walk up from the node to find a containing IfStatement whose test is a null-check
4289
- * on `ref.current` with the given ref name.
4290
- * @param node The MemberExpression node for ref.current
4291
- * @param refName The name of the ref variable (e.g. "myRef") to check against
4292
- * @returns the enclosing IfStatement node if found, or null if not found
4293
- */
4294
- function findEnclosingRefNullCheckIf(node, refName) {
4295
- let current = node.parent;
4296
- while (current != null) {
4297
- if (current.type === AST_NODE_TYPES.IfStatement) return isRefCurrentNullCheck(current.test, refName) ? current : null;
4298
- switch (current.type) {
4299
- case AST_NODE_TYPES.ExpressionStatement:
4300
- case AST_NODE_TYPES.BlockStatement:
4301
- case AST_NODE_TYPES.ReturnStatement:
4302
- case AST_NODE_TYPES.JSXExpressionContainer:
4303
- case AST_NODE_TYPES.JSXElement:
4304
- case AST_NODE_TYPES.JSXOpeningElement:
4305
- case AST_NODE_TYPES.JSXClosingElement:
4306
- case AST_NODE_TYPES.AssignmentExpression:
4307
- case AST_NODE_TYPES.VariableDeclaration:
4308
- case AST_NODE_TYPES.VariableDeclarator:
4309
- case AST_NODE_TYPES.MemberExpression:
4310
- case AST_NODE_TYPES.ChainExpression:
4311
- case AST_NODE_TYPES.CallExpression: break;
4312
- default: return null;
4313
- }
4314
- current = current.parent;
4315
- }
4316
- return null;
4317
- }
4318
- /**
4319
- * Check if a write to `ref.current` comes after a sibling if-statement that null-checks
4320
- * the same ref (inverted lazy init with early return pattern):
4321
- *
4322
- * if (ref.current !== null) { return ...; }
4323
- * ref.current = value; // ← this write
4324
- * @param node The MemberExpression node for ref.current being written to
4325
- * @param refName The name of the ref variable (e.g. "myRef") to check against
4326
- * @returns true if there is a preceding sibling if-statement that null-checks the same ref
4327
- */
4328
- function isWriteAfterNullCheckIf(node, refName) {
4329
- let stmt = node;
4330
- while (stmt.parent != null && stmt.parent.type !== AST_NODE_TYPES.BlockStatement) stmt = stmt.parent;
4331
- if (stmt.parent?.type !== AST_NODE_TYPES.BlockStatement) return false;
4332
- const block = stmt.parent;
4333
- const stmtIdx = block.body.indexOf(stmt);
4334
- if (stmtIdx < 0) return false;
4335
- for (let i = stmtIdx - 1; i >= 0; i--) {
4336
- const sibling = block.body[i];
4337
- if (sibling == null) continue;
4338
- if (sibling.type === AST_NODE_TYPES.IfStatement && isRefCurrentNullCheck(sibling.test, refName)) return true;
4339
- }
4340
- return false;
4341
- }
4342
4228
  var refs_default = createRule({
4343
4229
  meta: {
4344
4230
  type: "problem",
@@ -4358,34 +4244,110 @@ function create$5(context) {
4358
4244
  const cCollector = core.useComponentCollector(context);
4359
4245
  const refAccesses = [];
4360
4246
  const jsxRefIdentifiers = /* @__PURE__ */ new Set();
4361
- function isRefIdentifier(node) {
4362
- if (node.type !== AST_NODE_TYPES.Identifier) return false;
4363
- if (core.isRefLikeName(node.name)) return true;
4364
- if (jsxRefIdentifiers.has(node.name)) return true;
4365
- 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);
4366
4273
  }
4367
4274
  return defineRuleListener(hCollector.visitor, cCollector.visitor, {
4368
4275
  JSXAttribute(node) {
4369
- 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
+ }
4370
4281
  },
4371
4282
  MemberExpression(node) {
4372
- if (node.property.type !== AST_NODE_TYPES.Identifier || node.property.name !== "current") return;
4283
+ if (!ast.isIdentifier(node.property, "current")) return;
4373
4284
  refAccesses.push({
4374
4285
  node,
4375
- 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)
4376
4287
  });
4377
4288
  },
4378
4289
  "Program:exit"(program) {
4379
- const components = cCollector.ctx.getAllComponents(program);
4290
+ const comps = cCollector.ctx.getAllComponents(program);
4380
4291
  const hooks = hCollector.ctx.getAllHooks(program);
4381
- const componentAndHookFns = new Set([...components.map((c) => c.node), ...hooks.map((h) => h.node)]);
4382
- 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);
4383
4294
  for (const { node, isWrite } of refAccesses) {
4384
- if (!isRefIdentifier(node.object)) continue;
4385
- 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);
4386
4304
  if (boundary == null) continue;
4387
- if (isInsideNestedFunction(node, boundary)) continue;
4388
- 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;
4389
4351
  context.report({
4390
4352
  messageId: isWrite ? "writeDuringRender" : "readDuringRender",
4391
4353
  node
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-react-x",
3
- "version": "3.0.0-next.53",
3
+ "version": "3.0.0-next.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,17 +45,17 @@
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-next.53",
49
- "@eslint-react/core": "3.0.0-next.53",
50
- "@eslint-react/eff": "3.0.0-next.53",
51
- "@eslint-react/shared": "3.0.0-next.53",
52
- "@eslint-react/var": "3.0.0-next.53"
48
+ "@eslint-react/ast": "3.0.0-next.55",
49
+ "@eslint-react/eff": "3.0.0-next.55",
50
+ "@eslint-react/shared": "3.0.0-next.55",
51
+ "@eslint-react/var": "3.0.0-next.55",
52
+ "@eslint-react/core": "3.0.0-next.55"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/react": "^19.2.14",
56
56
  "@types/react-dom": "^19.2.3",
57
- "tsdown": "^0.20.3",
58
- "tsl-dx": "^0.7.2",
57
+ "tsdown": "^0.21.0-beta.1",
58
+ "tsl-dx": "^0.8.0",
59
59
  "@local/configs": "0.0.0"
60
60
  },
61
61
  "peerDependencies": {