eslint-plugin-react-x 4.0.2-beta.5 → 4.0.3-beta.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 +70 -21
  2. package/package.json +6 -6
package/dist/index.js CHANGED
@@ -2,11 +2,11 @@ import { DEFAULT_ESLINT_REACT_SETTINGS, IMPURE_CTORS, IMPURE_FUNCS, WEBSITE_URL,
2
2
  import * as ast from "@eslint-react/ast";
3
3
  import * as core from "@eslint-react/core";
4
4
  import { isUseRefCall } from "@eslint-react/core";
5
+ import { AST_NODE_TYPES } from "@typescript-eslint/types";
5
6
  import { ESLintUtils } from "@typescript-eslint/utils";
6
7
  import { P, isMatching, match } from "ts-pattern";
7
8
  import ts from "typescript";
8
9
  import { JsxDetectionHint, findParentAttribute, getElementFullType, hasAttribute, isJsxLike } from "@eslint-react/jsx";
9
- import { AST_NODE_TYPES } from "@typescript-eslint/types";
10
10
  import { computeObjectType, findEnclosingAssignmentTarget, isAssignmentTargetEqual, resolve } from "@eslint-react/var";
11
11
  import { DefinitionType } from "@typescript-eslint/scope-manager";
12
12
  import { findVariable, getStaticValue, isIdentifier, isVariableDeclarator } from "@typescript-eslint/utils/ast-utils";
@@ -147,7 +147,7 @@ const rules$7 = {
147
147
  //#endregion
148
148
  //#region package.json
149
149
  var name$6 = "eslint-plugin-react-x";
150
- var version = "4.0.2-beta.5";
150
+ var version = "4.0.3-beta.0";
151
151
 
152
152
  //#endregion
153
153
  //#region src/utils/create-rule.ts
@@ -319,6 +319,51 @@ var component_hook_factories_default = createRule({
319
319
  create: create$56,
320
320
  defaultOptions: []
321
321
  });
322
+ /**
323
+ * Check if a function parameter name looks like a React component (PascalCase).
324
+ */
325
+ function isComponentLikeParamName(name) {
326
+ return /^[A-Z]/.test(name);
327
+ }
328
+ /**
329
+ * Check if a function parameter has a type annotation that looks like a React component type.
330
+ * Matches types like ComponentType, React.ComponentType, FC, React.FC, etc.
331
+ */
332
+ function hasComponentTypeAnnotation(param) {
333
+ if (param.type !== AST_NODE_TYPES.Identifier || param.typeAnnotation == null) return false;
334
+ const annotation = param.typeAnnotation.typeAnnotation;
335
+ if (annotation.type === AST_NODE_TYPES.TSTypeReference) return isComponentTypeName(annotation.typeName);
336
+ return false;
337
+ }
338
+ /**
339
+ * Check if a type name refers to a known React component type.
340
+ */
341
+ function isComponentTypeName(typeName) {
342
+ if (typeName.type === AST_NODE_TYPES.Identifier) return /^(ComponentType|FC|ComponentClass|FunctionComponent|Component)$/.test(typeName.name);
343
+ if (typeName.type === AST_NODE_TYPES.TSQualifiedName) {
344
+ if (typeName.left.type === AST_NODE_TYPES.Identifier && typeName.left.name === "React") return isComponentTypeName(typeName.right);
345
+ }
346
+ return false;
347
+ }
348
+ /**
349
+ * Heuristically check if a function is a Higher Order Component (HOC) based on its parameters.
350
+ * Considers a function an HOC if it takes a parameter that looks like a React component
351
+ * (by name or type annotation). This does not validate that the function actually returns
352
+ * a React component.
353
+ */
354
+ function isHigherOrderComponent(fn) {
355
+ return fn.params.some((param) => {
356
+ if (param.type === AST_NODE_TYPES.Identifier && isComponentLikeParamName(param.name)) return true;
357
+ if (hasComponentTypeAnnotation(param)) return true;
358
+ return false;
359
+ });
360
+ }
361
+ /**
362
+ * Check if a node is inside a test mock callback (vi.mock or jest.mock).
363
+ */
364
+ function isInsideTestMockCallback(node) {
365
+ return ast.findParent(node, ast.isTestMockCallback) != null;
366
+ }
322
367
  function create$56(context) {
323
368
  const hint = core.ComponentDetectionHint.DoNotIncludeJsxWithNumberValue | core.ComponentDetectionHint.DoNotIncludeJsxWithBooleanValue | core.ComponentDetectionHint.DoNotIncludeJsxWithNullValue | core.ComponentDetectionHint.DoNotIncludeJsxWithStringValue | core.ComponentDetectionHint.DoNotIncludeJsxWithUndefinedValue | core.ComponentDetectionHint.RequireBothSidesOfLogicalExpressionToBeJsx | core.ComponentDetectionHint.RequireBothBranchesOfConditionalExpressionToBeJsx | core.ComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayPatternElement | core.ComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayExpressionElement | core.ComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayMapCallback;
324
369
  const fCollector = core.getComponentCollector(context, { hint });
@@ -331,7 +376,10 @@ function create$56(context) {
331
376
  const hooks = [...hCollector.api.getAllHooks(program)];
332
377
  for (const { name, node } of fComponents) {
333
378
  if (name == null) continue;
334
- if (ast.findParent(node, ast.isFunction) == null) continue;
379
+ const parentFn = ast.findParent(node, ast.isFunction);
380
+ if (parentFn == null) continue;
381
+ if (isInsideTestMockCallback(node)) continue;
382
+ if (isHigherOrderComponent(parentFn)) continue;
335
383
  if (reported.has(node)) continue;
336
384
  context.report({
337
385
  data: { name },
@@ -341,7 +389,10 @@ function create$56(context) {
341
389
  reported.add(node);
342
390
  }
343
391
  for (const { name = "unknown", node } of cComponents) {
344
- if (ast.findParent(node, ast.isFunction) == null) continue;
392
+ const parentFn = ast.findParent(node, ast.isFunction);
393
+ if (parentFn == null) continue;
394
+ if (isInsideTestMockCallback(node)) continue;
395
+ if (isHigherOrderComponent(parentFn)) continue;
345
396
  context.report({
346
397
  data: { name },
347
398
  messageId: "component",
@@ -350,6 +401,7 @@ function create$56(context) {
350
401
  }
351
402
  for (const { name, node } of hooks) {
352
403
  if (ast.findParent(node, ast.isFunction) == null) continue;
404
+ if (isInsideTestMockCallback(node)) continue;
353
405
  if (reported.has(node)) continue;
354
406
  context.report({
355
407
  data: { name },
@@ -2952,8 +3004,8 @@ const RULE_NAME$27 = "no-nested-lazy-component-declarations";
2952
3004
  var no_nested_lazy_component_declarations_default = createRule({
2953
3005
  meta: {
2954
3006
  type: "problem",
2955
- docs: { description: "Disallows nesting lazy component declarations inside other components." },
2956
- messages: { default: "Do not declare lazy components inside other components. Instead, always declare them at the top level of your module." },
3007
+ docs: { description: "Disallows nesting lazy component declarations inside other components or hooks." },
3008
+ messages: { default: "Do not declare lazy components inside other components or hooks. Instead, always declare them at the top level of your module." },
2957
3009
  schema: []
2958
3010
  },
2959
3011
  name: RULE_NAME$27,
@@ -2961,25 +3013,22 @@ var no_nested_lazy_component_declarations_default = createRule({
2961
3013
  defaultOptions: []
2962
3014
  });
2963
3015
  function create$27(context) {
2964
- const hint = core.ComponentDetectionHint.None;
2965
- const collector = core.getComponentCollector(context, { hint });
2966
- const collectorLegacy = core.getComponentCollectorLegacy(context);
2967
- const lazyComponentDeclarations = /* @__PURE__ */ new Set();
2968
- return defineRuleListener(collector.visitor, collectorLegacy.visitor, {
3016
+ const fCollector = core.getComponentCollector(context);
3017
+ const cCollector = core.getComponentCollectorLegacy(context);
3018
+ const hCollector = core.getHookCollector(context);
3019
+ const lazyCalls = /* @__PURE__ */ new Set();
3020
+ return defineRuleListener(fCollector.visitor, cCollector.visitor, hCollector.visitor, {
2969
3021
  ImportExpression(node) {
2970
3022
  const lazyCall = ast.findParent(node, (n) => core.isLazyCall(context, n));
2971
- if (lazyCall != null) lazyComponentDeclarations.add(lazyCall);
3023
+ if (lazyCall != null) lazyCalls.add(lazyCall);
2972
3024
  },
2973
3025
  "Program:exit"(program) {
2974
- const functionComponents = collector.api.getAllComponents(program);
2975
- const classComponents = collectorLegacy.api.getAllComponents(program);
2976
- for (const lazy of lazyComponentDeclarations) if (ast.findParent(lazy, (n) => {
2977
- if (ast.isJSX(n)) return true;
2978
- if (n.type === AST_NODE_TYPES.CallExpression) return core.isHookCall(n) || core.isCreateElementCall(context, n) || core.isCreateContextCall(context, n);
2979
- if (ast.isFunction(n)) return functionComponents.some((c) => c.node === n);
2980
- if (ast.isClass(n)) return classComponents.some((c) => c.node === n);
2981
- return false;
2982
- }) != null) context.report({
3026
+ const significantParents = [
3027
+ ...fCollector.api.getAllComponents(program),
3028
+ ...hCollector.api.getAllHooks(program),
3029
+ ...cCollector.api.getAllComponents(program)
3030
+ ];
3031
+ for (const lazy of lazyCalls) if (ast.findParent(lazy, (n) => significantParents.some((p) => p.node === n))) context.report({
2983
3032
  messageId: "default",
2984
3033
  node: lazy
2985
3034
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-react-x",
3
- "version": "4.0.2-beta.5",
3
+ "version": "4.0.3-beta.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,11 +45,11 @@
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": "4.0.2-beta.5",
49
- "@eslint-react/jsx": "4.0.2-beta.5",
50
- "@eslint-react/core": "4.0.2-beta.5",
51
- "@eslint-react/var": "4.0.2-beta.5",
52
- "@eslint-react/shared": "4.0.2-beta.5"
48
+ "@eslint-react/ast": "4.0.3-beta.0",
49
+ "@eslint-react/core": "4.0.3-beta.0",
50
+ "@eslint-react/shared": "4.0.3-beta.0",
51
+ "@eslint-react/jsx": "4.0.3-beta.0",
52
+ "@eslint-react/var": "4.0.3-beta.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/react": "^19.2.14",