eslint-plugin-react-x 5.10.3 → 5.11.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 +201 -104
  2. package/package.json +7 -7
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { AST_NODE_TYPES } from "@typescript-eslint/types";
8
8
  import { DefinitionType, ScopeType } from "@typescript-eslint/scope-manager";
9
9
  import { findVariable, getStaticValue } from "@typescript-eslint/utils/ast-utils";
10
10
  import { computeObjectType, isAssignmentTargetEqual, isInitializedFromReact, resolve, resolveEnclosingAssignmentTarget } from "@eslint-react/var";
11
- import { P, match } from "ts-pattern";
11
+ import { P, isMatching, match } from "ts-pattern";
12
12
  import { compare } from "compare-versions";
13
13
  import { getConstrainedTypeAtLocation } from "@typescript-eslint/type-utils";
14
14
  import { unionConstituents } from "ts-api-utils";
@@ -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.10.3";
147
+ var version = "5.11.0";
148
148
 
149
149
  //#endregion
150
150
  //#region src/utils/create-rule.ts
@@ -2692,7 +2692,7 @@ function create$27(context) {
2692
2692
  }, ({ left, right }) => {
2693
2693
  if (left.type === AST_NODE_TYPES.UnaryExpression && left.operator === "!") return getReportDescriptor(right, visited);
2694
2694
  const initialScope = context.sourceCode.getScope(left);
2695
- if (Check.isIdentifier("NaN")(left) || getStaticValue(left, initialScope)?.value === "NaN") return {
2695
+ if (Check.isIdentifier(left, "NaN") || getStaticValue(left, initialScope)?.value === "NaN") return {
2696
2696
  data: { value: context.sourceCode.getText(left) },
2697
2697
  messageId: "default",
2698
2698
  node: left
@@ -2967,8 +2967,12 @@ function isProcessEnvNodeEnvCompare(context, node, operator, value) {
2967
2967
  if (node == null) return false;
2968
2968
  if (node.type !== AST_NODE_TYPES.BinaryExpression) return false;
2969
2969
  if (node.operator !== operator) return false;
2970
- if (isProcessEnvNodeEnv(context, node.left) && Check.isStringLiteral(node.right)) return node.right.value === value;
2971
- if (Check.isStringLiteral(node.left) && isProcessEnvNodeEnv(context, node.right)) return node.left.value === value;
2970
+ const isStringLiteral = isMatching({
2971
+ type: AST_NODE_TYPES.Literal,
2972
+ value: P.string
2973
+ });
2974
+ if (isProcessEnvNodeEnv(context, node.left) && isStringLiteral(node.right)) return node.right.value === value;
2975
+ if (isStringLiteral(node.left) && isProcessEnvNodeEnv(context, node.right)) return node.left.value === value;
2972
2976
  return false;
2973
2977
  }
2974
2978
  function isDevelopmentOnlyCheck(context, node) {
@@ -3428,7 +3432,7 @@ var no_unstable_context_value_default = createRule({
3428
3432
  function create$13(context) {
3429
3433
  const { compilationMode, version } = getSettingsFromContext(context);
3430
3434
  if (compilationMode === "infer" || compilationMode === "all") return {};
3431
- if (compilationMode === "annotation" && context.sourceCode.ast.body.some(Check.isDirective("use memo"))) return {};
3435
+ if (compilationMode === "annotation" && context.sourceCode.ast.body.some((stmt) => Check.isDirective(stmt, "use memo"))) return {};
3432
3436
  const isReact18OrBelow = compare(version, "19.0.0", "<");
3433
3437
  const { api, visitor } = core.getFunctionComponentCollector(context);
3434
3438
  const constructions = /* @__PURE__ */ new WeakMap();
@@ -3527,7 +3531,7 @@ function extractIdentifier(node) {
3527
3531
  function create$12(context, [options]) {
3528
3532
  const { compilationMode } = getSettingsFromContext(context);
3529
3533
  if (compilationMode === "infer" || compilationMode === "all") return {};
3530
- if (compilationMode === "annotation" && context.sourceCode.ast.body.some(Check.isDirective("use memo"))) return {};
3534
+ if (compilationMode === "annotation" && context.sourceCode.ast.body.some((stmt) => Check.isDirective(stmt, "use memo"))) return {};
3531
3535
  const { api, visitor } = core.getFunctionComponentCollector(context);
3532
3536
  const declarators = /* @__PURE__ */ new WeakMap();
3533
3537
  const { safeDefaultProps = [] } = options;
@@ -4379,6 +4383,7 @@ var refs_default = createRule({
4379
4383
  type: "problem",
4380
4384
  docs: { description: "Validates correct usage of refs by checking that 'ref.current' is not read or written during render." },
4381
4385
  messages: {
4386
+ duplicateRefInit: "Ref is initialized more than once during render. Only a single 'if (ref.current == null)' initialization is allowed; move any additional initialization into an effect or event handler.",
4382
4387
  readDuringRender: "Do not read 'ref.current' during render. Refs are not available during rendering and their values may be stale or inconsistent. Move this read into an effect or event handler.",
4383
4388
  refPassedToFunction: "Passing a ref to a function may cause its value to be read during render. Pass 'ref.current' instead if the function only needs the value, or move the call into an effect.",
4384
4389
  writeDuringRender: "Do not write to 'ref.current' during render. Refs should only be mutated in effects or event handlers. Move this write into an effect or event handler."
@@ -4397,6 +4402,50 @@ function resolveAlias(name, aliases) {
4397
4402
  }
4398
4403
  return name;
4399
4404
  }
4405
+ function isFunctionExpressionLike(node) {
4406
+ return node.type === AST_NODE_TYPES.FunctionExpression || node.type === AST_NODE_TYPES.ArrowFunctionExpression;
4407
+ }
4408
+ /**
4409
+ * Phase 2 (part): compute the set of functions that are "reached" during render for a given
4410
+ * component/hook boundary. A function is reached if it is the boundary itself, or if it is
4411
+ * directly called (possibly through a simple variable alias, e.g. `const b = a; b();`) from
4412
+ * somewhere that is itself reached. This lets us detect ref mutations/reads that happen inside
4413
+ * a helper function which is invoked synchronously during render (see "Ref Mutation in Called
4414
+ * Function" in refs.spec.md), as opposed to functions that are merely defined and handed off
4415
+ * to be called later (e.g. event handlers, effect callbacks).
4416
+ */
4417
+ function computeReachedFunctions(boundary, isCompOrHookFn, functionVarBindings, directCallSites, aliases) {
4418
+ const reached = /* @__PURE__ */ new Set([boundary]);
4419
+ const relevantCalls = directCallSites.filter((c) => Traverse.findParent(c.node, isCompOrHookFn) === boundary);
4420
+ let changed = true;
4421
+ for (let iterations = 0; changed && iterations < 50; iterations++) {
4422
+ changed = false;
4423
+ for (const { calleeName, node: callNode } of relevantCalls) {
4424
+ const hostFn = Traverse.findParent(callNode, Check.isFunction) ?? boundary;
4425
+ if (!reached.has(hostFn)) continue;
4426
+ const resolvedName = resolveAlias(calleeName, aliases);
4427
+ const target = functionVarBindings.get(resolvedName);
4428
+ if (target == null || reached.has(target)) continue;
4429
+ if (Traverse.findParent(target, isCompOrHookFn) !== boundary) continue;
4430
+ reached.add(target);
4431
+ changed = true;
4432
+ }
4433
+ }
4434
+ return reached;
4435
+ }
4436
+ /**
4437
+ * Phase 2 (part): determine whether a ref access node is reached unconditionally during render,
4438
+ * i.e. every function boundary between the access and the component/hook `boundary` is itself
4439
+ * a function that gets invoked during render (see `computeReachedFunctions`).
4440
+ */
4441
+ function isReachedDuringRender(node, boundary, reached) {
4442
+ let current = node.parent;
4443
+ while (current != null && current !== boundary) {
4444
+ if (Check.isFunction(current) && !reached.has(current)) return false;
4445
+ current = current.parent;
4446
+ }
4447
+ return true;
4448
+ }
4400
4449
  function create$6(context) {
4401
4450
  const hc = core.getHookCollector(context);
4402
4451
  const fc = core.getFunctionComponentCollector(context);
@@ -4404,17 +4453,27 @@ function create$6(context) {
4404
4453
  const jsxRefIdentifiers = /* @__PURE__ */ new Set();
4405
4454
  const aliases = /* @__PURE__ */ new Map();
4406
4455
  const refPassedToFunctions = [];
4456
+ const functionVarBindings = /* @__PURE__ */ new Map();
4457
+ const directCallSites = [];
4407
4458
  return merge(hc.visitor, fc.visitor, {
4408
4459
  AssignmentExpression(node) {
4409
4460
  if (node.operator !== "=") return;
4410
4461
  const left = Extract.unwrap(node.left);
4411
4462
  const right = Extract.unwrap(node.right);
4412
- if (left.type !== AST_NODE_TYPES.Identifier || right.type !== AST_NODE_TYPES.Identifier) return;
4413
- aliases.set(left.name, right.name);
4463
+ if (left.type !== AST_NODE_TYPES.Identifier) return;
4464
+ if (right.type === AST_NODE_TYPES.Identifier) {
4465
+ aliases.set(left.name, right.name);
4466
+ return;
4467
+ }
4468
+ if (isFunctionExpressionLike(right)) functionVarBindings.set(left.name, right);
4414
4469
  },
4415
4470
  CallExpression(node) {
4416
4471
  const callee = Extract.unwrap(node.callee);
4417
4472
  const calleeName = callee.type === AST_NODE_TYPES.Identifier ? callee.name : callee.type === AST_NODE_TYPES.MemberExpression ? Extract.getPropertyName(callee.property) : null;
4473
+ if (callee.type === AST_NODE_TYPES.Identifier) directCallSites.push({
4474
+ calleeName: callee.name,
4475
+ node
4476
+ });
4418
4477
  if (calleeName != null && (calleeName.startsWith("use") || calleeName === "mergeRefs")) return;
4419
4478
  for (const arg of node.arguments) {
4420
4479
  const unwrapped = Extract.unwrap(arg);
@@ -4451,26 +4510,46 @@ function create$6(context) {
4451
4510
  const hooks = hc.api.getAllHooks(program);
4452
4511
  const funcs = /* @__PURE__ */ new Set([...comps.map((c) => c.node), ...hooks.map((h) => h.node)]);
4453
4512
  const isCompOrHookFn = (n) => Check.isFunction(n) && funcs.has(n);
4513
+ const reachedCache = /* @__PURE__ */ new Map();
4514
+ function getReached(boundary) {
4515
+ let reached = reachedCache.get(boundary);
4516
+ if (reached == null) {
4517
+ reached = computeReachedFunctions(boundary, isCompOrHookFn, functionVarBindings, directCallSites, aliases);
4518
+ reachedCache.set(boundary, reached);
4519
+ }
4520
+ return reached;
4521
+ }
4522
+ const lazyInitSeen = /* @__PURE__ */ new Map();
4454
4523
  for (const { isWrite, node } of refAccesses) {
4455
4524
  const obj = Extract.unwrap(node.object);
4456
- if (obj.type !== AST_NODE_TYPES.Identifier) continue;
4457
- const resolvedName = resolveAlias(obj.name, aliases);
4458
- switch (true) {
4459
- case resolvedName === "ref" || resolvedName.endsWith("Ref"):
4460
- case jsxRefIdentifiers.has(resolvedName):
4461
- case isInitializedFromRef$1(context, resolvedName, context.sourceCode.getScope(node.object)): break;
4462
- default: continue;
4463
- }
4525
+ let refName = null;
4526
+ if (obj.type === AST_NODE_TYPES.Identifier) {
4527
+ const resolvedName = resolveAlias(obj.name, aliases);
4528
+ switch (true) {
4529
+ case resolvedName === "ref" || resolvedName.endsWith("Ref"):
4530
+ case jsxRefIdentifiers.has(resolvedName):
4531
+ case isInitializedFromRef$1(context, resolvedName, context.sourceCode.getScope(node.object)):
4532
+ refName = resolvedName;
4533
+ break;
4534
+ default: continue;
4535
+ }
4536
+ } else if (obj.type === AST_NODE_TYPES.MemberExpression) {
4537
+ const propName = Extract.getPropertyName(obj.property);
4538
+ if (propName == null || propName !== "ref" && !propName.endsWith("Ref")) continue;
4539
+ } else continue;
4464
4540
  const boundary = Traverse.findParent(node, isCompOrHookFn);
4465
4541
  if (boundary == null) continue;
4466
- if (Traverse.findParent(node, Check.isFunction, (n) => n === boundary) != null) continue;
4467
- const refName = resolvedName;
4468
- let isLazyInit = isInNullCheckTest(node);
4469
- if (!isLazyInit) {
4542
+ if (!isReachedDuringRender(node, boundary, getReached(boundary))) continue;
4543
+ let isLazyInit = refName != null && isInNullCheckTest(node);
4544
+ let matchedGuardIf = false;
4545
+ if (!isLazyInit && refName != null) {
4470
4546
  let current = node.parent;
4471
4547
  findIf: for (;;) {
4472
4548
  if (current.type === AST_NODE_TYPES.IfStatement) {
4473
- if (isRefCurrentNullCheck(current.test, refName)) isLazyInit = true;
4549
+ if (isRefCurrentNullCheck(current.test, refName)) {
4550
+ isLazyInit = true;
4551
+ matchedGuardIf = true;
4552
+ }
4474
4553
  break;
4475
4554
  }
4476
4555
  switch (current.type) {
@@ -4492,7 +4571,7 @@ function create$6(context) {
4492
4571
  current = current.parent;
4493
4572
  }
4494
4573
  }
4495
- if (!isLazyInit && isWrite) {
4574
+ if (!isLazyInit && isWrite && refName != null) {
4496
4575
  let stmt = node;
4497
4576
  while (stmt.parent != null && stmt.parent.type !== AST_NODE_TYPES.BlockStatement) stmt = stmt.parent;
4498
4577
  if (stmt.parent?.type === AST_NODE_TYPES.BlockStatement) {
@@ -4508,6 +4587,18 @@ function create$6(context) {
4508
4587
  }
4509
4588
  }
4510
4589
  }
4590
+ if (isLazyInit && matchedGuardIf && isWrite && refName != null) {
4591
+ const seen = lazyInitSeen.get(boundary) ?? /* @__PURE__ */ new Set();
4592
+ lazyInitSeen.set(boundary, seen);
4593
+ if (seen.has(refName)) {
4594
+ context.report({
4595
+ messageId: "duplicateRefInit",
4596
+ node
4597
+ });
4598
+ continue;
4599
+ }
4600
+ seen.add(refName);
4601
+ }
4511
4602
  if (isLazyInit) continue;
4512
4603
  context.report({
4513
4604
  messageId: isWrite ? "writeDuringRender" : "readDuringRender",
@@ -4517,7 +4608,7 @@ function create$6(context) {
4517
4608
  for (const { node } of refPassedToFunctions) {
4518
4609
  const boundary = Traverse.findParent(node, isCompOrHookFn);
4519
4610
  if (boundary == null) continue;
4520
- if (Traverse.findParent(node, Check.isFunction, (n) => n === boundary) != null) continue;
4611
+ if (!isReachedDuringRender(node, boundary, getReached(boundary))) continue;
4521
4612
  context.report({
4522
4613
  messageId: "refPassedToFunction",
4523
4614
  node
@@ -4527,9 +4618,13 @@ function create$6(context) {
4527
4618
  VariableDeclarator(node) {
4528
4619
  if (node.init == null) return;
4529
4620
  const id = node.id;
4621
+ if (id.type !== AST_NODE_TYPES.Identifier) return;
4530
4622
  const init = Extract.unwrap(node.init);
4531
- if (id.type !== AST_NODE_TYPES.Identifier || init.type !== AST_NODE_TYPES.Identifier) return;
4532
- aliases.set(id.name, init.name);
4623
+ if (init.type === AST_NODE_TYPES.Identifier) {
4624
+ aliases.set(id.name, init.name);
4625
+ return;
4626
+ }
4627
+ if (isFunctionExpressionLike(init)) functionVarBindings.set(id.name, init);
4533
4628
  }
4534
4629
  });
4535
4630
  }
@@ -7193,34 +7288,49 @@ function create$4(context) {
7193
7288
 
7194
7289
  //#endregion
7195
7290
  //#region src/rules/static-components/lib.ts
7196
- function resolveDynamicValue(context, node, isInsideRender, seen) {
7291
+ /**
7292
+ * Expression kinds that always produce a brand-new value on every evaluation, and are
7293
+ * therefore considered "dynamically created" when used as a component's value.
7294
+ */
7295
+ const DYNAMIC_EXPRESSION_TYPES = /* @__PURE__ */ new Set([
7296
+ AST_NODE_TYPES.ArrowFunctionExpression,
7297
+ AST_NODE_TYPES.CallExpression,
7298
+ AST_NODE_TYPES.ClassExpression,
7299
+ AST_NODE_TYPES.FunctionExpression,
7300
+ AST_NODE_TYPES.NewExpression
7301
+ ]);
7302
+ function findVariableForIdentifier(context, identifier) {
7303
+ return findVariable(context.sourceCode.getScope(identifier), identifier.name);
7304
+ }
7305
+ /**
7306
+ * Resolves an expression down to the node that dynamically created its value, if any.
7307
+ * Recurses through identifier references and both branches of ternaries.
7308
+ */
7309
+ function findDynamicCreationSite(context, node, isInsideRender, seen) {
7197
7310
  const expr = Extract.unwrap(node);
7311
+ if (DYNAMIC_EXPRESSION_TYPES.has(expr.type)) return expr;
7198
7312
  switch (expr.type) {
7199
- case AST_NODE_TYPES.FunctionExpression:
7200
- case AST_NODE_TYPES.ArrowFunctionExpression:
7201
- case AST_NODE_TYPES.NewExpression:
7202
- case AST_NODE_TYPES.CallExpression:
7203
- case AST_NODE_TYPES.ClassExpression: return expr;
7204
- case AST_NODE_TYPES.ConditionalExpression: {
7205
- const consequent = resolveDynamicValue(context, expr.consequent, isInsideRender, seen);
7206
- if (consequent != null) return consequent;
7207
- return resolveDynamicValue(context, expr.alternate, isInsideRender, seen);
7208
- }
7313
+ case AST_NODE_TYPES.ConditionalExpression: return findDynamicCreationSite(context, expr.consequent, isInsideRender, seen) ?? findDynamicCreationSite(context, expr.alternate, isInsideRender, seen);
7209
7314
  case AST_NODE_TYPES.Identifier:
7210
7315
  case AST_NODE_TYPES.JSXIdentifier: {
7211
- const resolved = findVariableForIdentifier(context, expr);
7212
- if (resolved == null) return null;
7213
- return getDynamicComponentSource(context, resolved, isInsideRender, seen).creationNode;
7316
+ const variable = findVariableForIdentifier(context, expr);
7317
+ if (variable == null) return null;
7318
+ return getDynamicComponentSource(context, variable, isInsideRender, seen).creationNode;
7214
7319
  }
7215
7320
  default: return null;
7216
7321
  }
7217
7322
  }
7218
- function findVariableForIdentifier(context, identifier) {
7219
- let scope = context.sourceCode.getScope(identifier);
7220
- while (scope != null) {
7221
- const variable = scope.variables.find((v) => v.name === identifier.name);
7222
- if (variable != null) return variable;
7223
- scope = scope.upper;
7323
+ /**
7324
+ * Looks for a reassignment of `variable` (e.g. `Component = createComponent();`) whose
7325
+ * right-hand side is dynamically created.
7326
+ */
7327
+ function findReassignmentCreationSite(context, variable, isInsideRender, seen) {
7328
+ for (const ref of variable.references) {
7329
+ if (!ref.isWrite()) continue;
7330
+ const { identifier } = ref;
7331
+ if (identifier.parent.type !== AST_NODE_TYPES.AssignmentExpression || identifier.parent.left !== identifier) continue;
7332
+ const source = findDynamicCreationSite(context, identifier.parent.right, isInsideRender, seen);
7333
+ if (source != null) return source;
7224
7334
  }
7225
7335
  return null;
7226
7336
  }
@@ -7233,34 +7343,21 @@ function getDynamicComponentSource(context, variable, isInsideRender, seen = /*
7233
7343
  for (const def of variable.defs) {
7234
7344
  const defNode = def.node;
7235
7345
  if (!isInsideRender(defNode)) continue;
7236
- if (defNode.type === AST_NODE_TYPES.FunctionDeclaration) return {
7346
+ if (defNode.type === AST_NODE_TYPES.FunctionDeclaration || defNode.type === AST_NODE_TYPES.ClassDeclaration) return {
7237
7347
  creationNode: defNode,
7238
7348
  isDynamic: true
7239
7349
  };
7240
- if (defNode.type === AST_NODE_TYPES.ClassDeclaration) return {
7241
- creationNode: defNode,
7350
+ if (defNode.type !== AST_NODE_TYPES.VariableDeclarator) continue;
7351
+ const initSource = defNode.init == null ? null : findDynamicCreationSite(context, defNode.init, isInsideRender, seen);
7352
+ if (initSource != null) return {
7353
+ creationNode: initSource,
7354
+ isDynamic: true
7355
+ };
7356
+ const reassignmentSource = findReassignmentCreationSite(context, variable, isInsideRender, seen);
7357
+ if (reassignmentSource != null) return {
7358
+ creationNode: reassignmentSource,
7242
7359
  isDynamic: true
7243
7360
  };
7244
- if (defNode.type === AST_NODE_TYPES.VariableDeclarator) {
7245
- if (defNode.init != null) {
7246
- const source = resolveDynamicValue(context, defNode.init, isInsideRender, seen);
7247
- if (source != null) return {
7248
- creationNode: source,
7249
- isDynamic: true
7250
- };
7251
- }
7252
- for (const ref of variable.references) {
7253
- if (!ref.isWrite()) continue;
7254
- const id = ref.identifier;
7255
- if (id.parent.type === AST_NODE_TYPES.AssignmentExpression && id.parent.left === id) {
7256
- const source = resolveDynamicValue(context, id.parent.right, isInsideRender, seen);
7257
- if (source != null) return {
7258
- creationNode: source,
7259
- isDynamic: true
7260
- };
7261
- }
7262
- }
7263
- }
7264
7361
  }
7265
7362
  return {
7266
7363
  creationNode: null,
@@ -7289,52 +7386,52 @@ function create$3(context) {
7289
7386
  const hint = core.FunctionComponentDetectionHint.DoNotIncludeJsxWithNumberValue | core.FunctionComponentDetectionHint.DoNotIncludeJsxWithBooleanValue | core.FunctionComponentDetectionHint.DoNotIncludeJsxWithNullValue | core.FunctionComponentDetectionHint.DoNotIncludeJsxWithStringValue | core.FunctionComponentDetectionHint.DoNotIncludeJsxWithUndefinedValue | core.FunctionComponentDetectionHint.RequireBothSidesOfLogicalExpressionToBeJsx | core.FunctionComponentDetectionHint.RequireBothBranchesOfConditionalExpressionToBeJsx | core.FunctionComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayPatternElement | core.FunctionComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayExpressionElement | core.FunctionComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayMapCallback;
7290
7387
  const fc = core.getFunctionComponentCollector(context, { hint });
7291
7388
  const cc = core.getClassComponentCollector(context);
7292
- const jsxCandidates = [];
7389
+ const candidates = [];
7293
7390
  return merge(fc.visitor, cc.visitor, {
7294
7391
  JSXOpeningElement(node) {
7295
7392
  if (node.name.type !== AST_NODE_TYPES.JSXIdentifier) return;
7296
7393
  const name = node.name.name;
7297
7394
  if (!core.isFunctionComponentName(name)) return;
7298
- jsxCandidates.push({
7395
+ candidates.push({
7299
7396
  name,
7300
- node
7397
+ identifier: node.name
7301
7398
  });
7302
7399
  },
7303
7400
  "Program:exit"(program) {
7304
- const fComponents = [...fc.api.getAllComponents(program)];
7305
- const cComponents = [...cc.api.getAllComponents(program)];
7306
- function getEnclosingComponent(node) {
7307
- return Traverse.findParent(node, (n) => {
7308
- if (Check.isFunction(n)) return fComponents.some((c) => c.node === n);
7309
- if (Check.isClass(n)) return cComponents.some((c) => c.node === n);
7310
- return false;
7311
- });
7312
- }
7313
- const isInsideRender = (node) => getEnclosingComponent(node) != null;
7314
- for (const { name, node: jsxNode } of jsxCandidates) {
7315
- const jsxName = jsxNode.name;
7316
- const variable = findVariableForIdentifier(context, jsxName);
7317
- if (variable == null || variable.defs.length === 0) continue;
7318
- const def = variable.defs.at(0);
7319
- if (def == null) continue;
7320
- const defNode = def.node;
7321
- if (getEnclosingComponent(defNode) == null) continue;
7322
- const result = getDynamicComponentSource(context, variable, isInsideRender);
7323
- if (!result.isDynamic) continue;
7324
- context.report({
7325
- data: { name },
7326
- messageId: "default",
7327
- node: jsxNode.name
7328
- });
7329
- if (result.creationNode != null) context.report({
7330
- data: { name },
7331
- messageId: "createdHere",
7332
- node: result.creationNode
7333
- });
7334
- }
7401
+ const isInsideRender = createRenderBoundaryChecker(fc, cc, program);
7402
+ for (const candidate of candidates) reportIfCreatedDuringRender(context, candidate, isInsideRender);
7335
7403
  }
7336
7404
  });
7337
7405
  }
7406
+ /**
7407
+ * Builds a predicate that tells whether a given node lies within the body of a
7408
+ * previously-collected function or class component, i.e. whether it is "created during render".
7409
+ */
7410
+ function createRenderBoundaryChecker(fc, cc, program) {
7411
+ const componentNodes = new Set([...fc.api.getAllComponents(program), ...cc.api.getAllComponents(program)].map((component) => component.node));
7412
+ const getEnclosingComponent = (node) => Traverse.findParent(node, (n) => (Check.isFunction(n) || Check.isClass(n)) && componentNodes.has(n));
7413
+ return (node) => getEnclosingComponent(node) != null;
7414
+ }
7415
+ function reportIfCreatedDuringRender(context, candidate, isInsideRender) {
7416
+ const { name, identifier } = candidate;
7417
+ const variable = findVariableForIdentifier(context, identifier);
7418
+ if (variable == null) return;
7419
+ const def = variable.defs.at(0);
7420
+ if (def == null) return;
7421
+ if (!isInsideRender(def.node)) return;
7422
+ const { creationNode, isDynamic } = getDynamicComponentSource(context, variable, isInsideRender);
7423
+ if (!isDynamic) return;
7424
+ context.report({
7425
+ data: { name },
7426
+ messageId: "default",
7427
+ node: identifier
7428
+ });
7429
+ if (creationNode != null) context.report({
7430
+ data: { name },
7431
+ messageId: "createdHere",
7432
+ node: creationNode
7433
+ });
7434
+ }
7338
7435
 
7339
7436
  //#endregion
7340
7437
  //#region src/rules/unsupported-syntax/lib.ts
@@ -7343,7 +7440,7 @@ function isEvalCall(node) {
7343
7440
  switch (callee.type) {
7344
7441
  case AST_NODE_TYPES.Identifier: return callee.name === "eval";
7345
7442
  case AST_NODE_TYPES.MemberExpression:
7346
- if (!Check.isIdentifier("globalThis")(Extract.unwrap(callee.object))) return false;
7443
+ if (!Check.isIdentifier(Extract.unwrap(callee.object), "globalThis")) return false;
7347
7444
  return Extract.getPropertyName(callee.property, (n) => callee.computed ? null : n.name) === "eval";
7348
7445
  default: return false;
7349
7446
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-react-x",
3
- "version": "5.10.3",
3
+ "version": "5.11.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.10.3",
49
- "@eslint-react/core": "5.10.3",
50
- "@eslint-react/shared": "5.10.3",
51
- "@eslint-react/jsx": "5.10.3",
52
- "@eslint-react/var": "5.10.3",
53
- "@eslint-react/eslint": "5.10.3"
48
+ "@eslint-react/core": "5.11.0",
49
+ "@eslint-react/ast": "5.11.0",
50
+ "@eslint-react/jsx": "5.11.0",
51
+ "@eslint-react/shared": "5.11.0",
52
+ "@eslint-react/eslint": "5.11.0",
53
+ "@eslint-react/var": "5.11.0"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/react": "^19.2.17",