eslint-plugin-react-x 5.18.0 → 5.18.2

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 +97 -40
  2. package/package.json +11 -11
package/dist/index.js CHANGED
@@ -145,7 +145,7 @@ const rules$6 = {
145
145
  //#endregion
146
146
  //#region package.json
147
147
  var name$6 = "eslint-plugin-react-x";
148
- var version = "5.18.0";
148
+ var version = "5.18.2";
149
149
 
150
150
  //#endregion
151
151
  //#region src/utils/create-rule.ts
@@ -171,27 +171,6 @@ var error_boundaries_default = createRule({
171
171
  create: create$50,
172
172
  defaultOptions: []
173
173
  });
174
- /**
175
- * Finds the nearest TryStatement whose `try` block (not catch/finally)
176
- * encloses the given node.
177
- * @param node The node to check.
178
- * @returns The enclosing TryStatement, or null if none is found.
179
- */
180
- function getEnclosingTryBlock(node) {
181
- let current = node.parent;
182
- while (current != null) {
183
- if (current.type === AST_NODE_TYPES.TryStatement) {
184
- let n = node;
185
- while (n != null && n !== current) {
186
- if (n === current.block) return current;
187
- n = n.parent;
188
- }
189
- }
190
- if (current.type === AST_NODE_TYPES.Program) return null;
191
- current = current.parent;
192
- }
193
- return null;
194
- }
195
174
  function create$50(context) {
196
175
  if (!context.sourceCode.text.includes("try")) return {};
197
176
  const hint = core.JsxDetectionHint.DoNotIncludeJsxWithNullValue | core.JsxDetectionHint.DoNotIncludeJsxWithNumberValue | core.JsxDetectionHint.DoNotIncludeJsxWithBigIntValue | core.JsxDetectionHint.DoNotIncludeJsxWithStringValue | core.JsxDetectionHint.DoNotIncludeJsxWithBooleanValue | core.JsxDetectionHint.DoNotIncludeJsxWithUndefinedValue | core.JsxDetectionHint.DoNotIncludeJsxWithEmptyArrayValue;
@@ -209,7 +188,7 @@ function create$50(context) {
209
188
  const hooks = hc.api.getAllHooks(node);
210
189
  const funcs = [...comps, ...hooks];
211
190
  for (const call of useCalls) {
212
- const stmt = getEnclosingTryBlock(call);
191
+ const stmt = Traverse.findEnclosingTryBlock(call);
213
192
  const func = Traverse.findParent(stmt, (n) => funcs.some((f) => f.node === n));
214
193
  if (stmt != null && func != null && !reported.has(stmt)) {
215
194
  context.report({
@@ -222,7 +201,7 @@ function create$50(context) {
222
201
  for (const { rets } of funcs) for (const ret of rets) {
223
202
  if (ret == null) continue;
224
203
  if (!core.isJsxLike(context, ret, hint)) continue;
225
- const stmt = getEnclosingTryBlock(ret);
204
+ const stmt = Traverse.findEnclosingTryBlock(ret);
226
205
  if (stmt != null && !reported.has(stmt)) {
227
206
  context.report({
228
207
  messageId: "tryCatchWithJsx",
@@ -3418,21 +3397,79 @@ function create$23(context) {
3418
3397
  //#endregion
3419
3398
  //#region src/rules/no-nested-component-definitions/lib.ts
3420
3399
  /**
3421
- * Determine whether the node is inside JSX attribute value
3422
- * @param node The AST node to check
3423
- * @returns `true` if the node is inside JSX attribute value
3400
+ * Well-known component wrapper function names, matched by exact or `.`-suffixed fully
3401
+ * qualified name (e.g. `memo`, `React.memo`, `mobx.observer`, `connect` from react-redux,
3402
+ * or Relay's `create*Container` helpers). Only wrappers whose argument is a render function
3403
+ * are listed; wrappers taking a component identifier (`styled`, `motion`) or a loader
3404
+ * (`lazy`, `dynamic`) are irrelevant for name resolution.
3424
3405
  */
3425
- function isInsideJSXAttributeValue(node) {
3426
- return node.parent.type === AST_NODE_TYPES.JSXAttribute || findParentAttribute(node, (n) => n.value?.type === AST_NODE_TYPES.JSXExpressionContainer) != null;
3406
+ const WELL_KNOWN_COMPONENT_WRAPPERS = [
3407
+ "connect",
3408
+ "createFragmentContainer",
3409
+ "createPaginationContainer",
3410
+ "createRefetchContainer",
3411
+ "forwardRef",
3412
+ "graphql",
3413
+ "memo",
3414
+ "observer",
3415
+ "useCallback"
3416
+ ];
3417
+ /** Matches the HOC naming convention shared by recompose, react-router v5, Formik and custom HOCs (e.g. `withProps`, `withRouter`, `withFormik`, `withAuth`). */
3418
+ const RE_HOC_WRAPPER_NAME = /^with[A-Z]/;
3419
+ /**
3420
+ * Check if a call expression is a well-known component wrapper call.
3421
+ * Only calls whose callee's fully qualified name is (or ends with) a well-known wrapper
3422
+ * name (e.g. `memo`, `React.memo`, `React.useCallback`, `mobx.observer`) are treated as
3423
+ * wrappers; anything else, including member calls on data objects (e.g. `items.map`,
3424
+ * `Array.from`), is not, so array method callbacks are never mistaken for wrapped components.
3425
+ * Curried wrappers like `connect(...)(Component)` or `withFormik(...)(Component)` are
3426
+ * recognized by unwrapping nested callee call expressions.
3427
+ * @param context The rule context
3428
+ * @param node The call expression to check
3429
+ * @returns `true` if the call is a well-known component wrapper call
3430
+ */
3431
+ function isWellKnownComponentWrapperCall(context, node) {
3432
+ let callee = Extract.unwrap(node.callee);
3433
+ while (callee.type === AST_NODE_TYPES.CallExpression) callee = Extract.unwrap(callee.callee);
3434
+ const name = Extract.getFullyQualifiedName(callee, (n) => context.sourceCode.getText(n));
3435
+ const baseName = name.slice(name.lastIndexOf(".") + 1);
3436
+ return RE_HOC_WRAPPER_NAME.test(baseName) || WELL_KNOWN_COMPONENT_WRAPPERS.some((wrapper) => name === wrapper || name.endsWith(`.${wrapper}`));
3427
3437
  }
3428
3438
  /**
3429
- * Check whether a given node is declared inside a class component's render block
3430
- * Ex: class C extends React.Component { render() { const Nested = () => <div />; } }
3431
- * @param node The AST node being checked
3432
- * @returns `true` if the node is inside a class component's render block
3439
+ * Check if a call expression is a component wrapper call for name resolution purposes.
3440
+ * @param context The rule context
3441
+ * @param call The call expression to check
3442
+ * @param arg The function node passed to the call
3443
+ * @returns `true` if the call is a component wrapper call
3433
3444
  */
3434
- function isInsideRenderMethod(node) {
3435
- return Traverse.findParent(node, (n) => core.isRenderMethodLike(n) && core.isClassComponent(n.parent.parent)) != null;
3445
+ function isComponentWrapperCall(context, call, arg) {
3446
+ if (Extract.unwrap(call.callee) === arg) return false;
3447
+ return isWellKnownComponentWrapperCall(context, call);
3448
+ }
3449
+ /**
3450
+ * Resolve the name a function is bound to through a chain of wrapping call expressions,
3451
+ * e.g. `const Component = useCallback(() => <div />, [])` resolves to `Component`.
3452
+ * @param context The rule context
3453
+ * @param node The function node to resolve the bound name for
3454
+ * @returns The bound name if the call chain ends at a variable declarator with an identifier, `null` otherwise
3455
+ */
3456
+ function getWrapperCallBoundName(context, node) {
3457
+ let current = node;
3458
+ let parent = current.parent;
3459
+ while (Check.isTypeExpression(parent)) {
3460
+ current = parent;
3461
+ parent = parent.parent;
3462
+ }
3463
+ while (parent.type === AST_NODE_TYPES.CallExpression && isComponentWrapperCall(context, parent, current)) {
3464
+ current = parent;
3465
+ parent = parent.parent;
3466
+ while (Check.isTypeExpression(parent)) {
3467
+ current = parent;
3468
+ parent = parent.parent;
3469
+ }
3470
+ }
3471
+ if (parent.type !== AST_NODE_TYPES.VariableDeclarator || parent.id.type !== AST_NODE_TYPES.Identifier) return null;
3472
+ return parent.id.name;
3436
3473
  }
3437
3474
  /**
3438
3475
  * Determine whether the node is inside `createElement`'s props argument
@@ -3447,6 +3484,23 @@ function isInsideCreateElementProps(context, node) {
3447
3484
  if (prop == null) return false;
3448
3485
  return prop === call.arguments[1];
3449
3486
  }
3487
+ /**
3488
+ * Determine whether the node is inside JSX attribute value
3489
+ * @param node The AST node to check
3490
+ * @returns `true` if the node is inside JSX attribute value
3491
+ */
3492
+ function isInsideJSXAttributeValue(node) {
3493
+ return node.parent.type === AST_NODE_TYPES.JSXAttribute || findParentAttribute(node, (n) => n.value?.type === AST_NODE_TYPES.JSXExpressionContainer) != null;
3494
+ }
3495
+ /**
3496
+ * Check whether a given node is declared inside a class component's render block
3497
+ * Ex: class C extends React.Component { render() { const Nested = () => <div />; } }
3498
+ * @param node The AST node being checked
3499
+ * @returns `true` if the node is inside a class component's render block
3500
+ */
3501
+ function isInsideRenderMethod(node) {
3502
+ return Traverse.findParent(node, (n) => core.isRenderMethodLike(n) && core.isClassComponent(n.parent.parent)) != null;
3503
+ }
3450
3504
 
3451
3505
  //#endregion
3452
3506
  //#region src/rules/no-nested-component-definitions/no-nested-component-definitions.ts
@@ -3463,21 +3517,24 @@ var no_nested_component_definitions_default = createRule({
3463
3517
  defaultOptions: []
3464
3518
  });
3465
3519
  function create$22(context) {
3466
- 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;
3520
+ 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 | core.FunctionComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayFlatMapCallback;
3467
3521
  const fc = core.getFunctionComponentCollector(context, { hint });
3468
3522
  const cc = core.getClassComponentCollector(context);
3469
3523
  return merge(fc.visitor, cc.visitor, { "Program:exit"(program) {
3470
3524
  const fComponents = [...fc.api.getAllComponents(program)];
3471
3525
  const cComponents = [...cc.api.getAllComponents(program)];
3526
+ const fComponentNodes = new Set(fComponents.map((c) => c.node));
3527
+ const cComponentNodes = new Set(cComponents.map((c) => c.node));
3472
3528
  function findEnclosingComponent(node) {
3473
3529
  return Traverse.findParent(node, (n) => {
3474
- if (Check.isFunction(n)) return fComponents.some((c) => c.node === n);
3475
- if (Check.isClass(n)) return cComponents.some((c) => c.node === n);
3530
+ if (Check.isFunction(n)) return fComponentNodes.has(n);
3531
+ if (Check.isClass(n)) return cComponentNodes.has(n);
3476
3532
  return false;
3477
3533
  });
3478
3534
  }
3479
- for (const { name, node: component } of fComponents) {
3480
- if (name == null) continue;
3535
+ for (let { name, node: component } of fComponents) {
3536
+ name ??= getWrapperCallBoundName(context, component);
3537
+ if (name == null || !core.isFunctionComponentNameLoose(name)) continue;
3481
3538
  if (isInsideJSXAttributeValue(component)) {
3482
3539
  context.report({
3483
3540
  data: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-react-x",
3
- "version": "5.18.0",
3
+ "version": "5.18.2",
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,21 +45,21 @@
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.18.0",
49
- "@eslint-react/eslint": "5.18.0",
50
- "@eslint-react/core": "5.18.0",
51
- "@eslint-react/jsx": "5.18.0",
52
- "@eslint-react/shared": "5.18.0",
53
- "@eslint-react/var": "5.18.0"
48
+ "@eslint-react/ast": "5.18.2",
49
+ "@eslint-react/core": "5.18.2",
50
+ "@eslint-react/eslint": "5.18.2",
51
+ "@eslint-react/jsx": "5.18.2",
52
+ "@eslint-react/var": "5.18.2",
53
+ "@eslint-react/shared": "5.18.2"
54
54
  },
55
55
  "devDependencies": {
56
- "@types/react": "^19.2.17",
57
- "@types/react-dom": "^19.2.3",
56
+ "@types/react": "^19.2.18",
57
+ "@types/react-dom": "^19.2.4",
58
58
  "dedent": "^1.7.2",
59
- "eslint": "^10.7.0",
59
+ "eslint": "^10.8.0",
60
60
  "react": "^19.2.8",
61
61
  "react-dom": "^19.2.8",
62
- "tsdown": "^0.22.13",
62
+ "tsdown": "^0.22.14",
63
63
  "tsl": "^1.0.30",
64
64
  "tsl-dx": "^0.13.3",
65
65
  "typescript": "6.0.3",