@eslint-react/core 2.7.5-next.8 → 2.8.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.
package/dist/index.d.ts CHANGED
@@ -251,20 +251,12 @@ interface ClassComponentSemanticNode extends SemanticNode {
251
251
  type ComponentSemanticNode = ClassComponentSemanticNode | FunctionComponentSemanticNode;
252
252
  //#endregion
253
253
  //#region src/component/component-collector.d.ts
254
- type FunctionEntry$1 = {
255
- key: string;
256
- node: AST.TSESTreeFunction;
257
- hookCalls: TSESTree.CallExpression[];
258
- isComponent: boolean;
254
+ interface FunctionEntry$1 extends FunctionComponentSemanticNode {
259
255
  isComponentDefinition: boolean;
260
- isExportDefault: boolean;
261
- isExportDefaultDeclaration: boolean;
262
- rets: TSESTree.ReturnStatement["argument"][];
263
- };
256
+ }
264
257
  declare namespace useComponentCollector {
265
258
  type Options = {
266
259
  collectDisplayName?: boolean;
267
- collectHookCalls?: boolean;
268
260
  hint?: ComponentDetectionHint;
269
261
  };
270
262
  type ReturnType = {
@@ -540,7 +532,7 @@ type FindEnclosingComponentOrHookFilter = (n: TSESTree.Node, name: string | null
540
532
  * @param test Optional test function to customize component or hook identification
541
533
  * @returns The enclosing component or hook node, or `null` if none is found
542
534
  */
543
- declare function findEnclosingComponentOrHook(node: TSESTree.Node | unit, test?: FindEnclosingComponentOrHookFilter): TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression | TSESTree.FunctionDeclarationWithName | TSESTree.FunctionDeclarationWithOptionalName | undefined;
535
+ declare function findEnclosingComponentOrHook(node: TSESTree.Node | unit, test?: FindEnclosingComponentOrHookFilter): TSESTree.ArrowFunctionExpression | TSESTree.FunctionDeclarationWithName | TSESTree.FunctionDeclarationWithOptionalName | TSESTree.FunctionExpression | undefined;
544
536
  //#endregion
545
537
  //#region src/hierarchy/is-inside-component-or-hook.d.ts
546
538
  /**
@@ -574,7 +566,6 @@ interface HookSemanticNode extends SemanticNode {
574
566
  type FunctionEntry = {
575
567
  key: string;
576
568
  node: AST.TSESTreeFunction;
577
- isHook: boolean;
578
569
  };
579
570
  declare namespace useHookCollector {
580
571
  type ReturnType = {
@@ -724,7 +715,7 @@ declare function resolveJsxAttributeValue(context: RuleContext, attribute: AST.T
724
715
  readonly toStatic: () => string | number | bigint | boolean | RegExp | null;
725
716
  } | {
726
717
  readonly kind: "expression";
727
- readonly node: TSESTree.Expression | TSESTree.JSXEmptyExpression;
718
+ readonly node: TSESTree.JSXEmptyExpression | TSESTree.Expression;
728
719
  readonly toStatic: () => unknown;
729
720
  } | {
730
721
  readonly kind: "element";
@@ -732,7 +723,7 @@ declare function resolveJsxAttributeValue(context: RuleContext, attribute: AST.T
732
723
  readonly toStatic: () => undefined;
733
724
  } | {
734
725
  readonly kind: "spreadChild";
735
- readonly node: TSESTree.Expression | TSESTree.JSXEmptyExpression;
726
+ readonly node: TSESTree.JSXEmptyExpression | TSESTree.Expression;
736
727
  readonly toStatic: () => undefined;
737
728
  } | {
738
729
  readonly kind: "spreadProps";
package/dist/index.js CHANGED
@@ -269,28 +269,20 @@ function useHookCollector(context) {
269
269
  const onFunctionEnter = (node) => {
270
270
  const id = AST.getFunctionId(node);
271
271
  const key = idGen$2.next();
272
- if (id != null && isHookId(id)) {
273
- functionEntries.push({
274
- key,
275
- node,
276
- isHook: true
277
- });
278
- hooks.set(key, {
279
- id,
280
- key,
281
- kind: "function",
282
- name: AST.toStringFormat(id, getText),
283
- node,
284
- flag: 0n,
285
- hint: 0n,
286
- hookCalls: []
287
- });
288
- return;
289
- }
290
272
  functionEntries.push({
291
273
  key,
274
+ node
275
+ });
276
+ if (id == null || !isHookId(id)) return;
277
+ hooks.set(key, {
278
+ id,
279
+ key,
280
+ kind: "function",
281
+ name: AST.toStringFormat(id, getText),
292
282
  node,
293
- isHook: false
283
+ flag: 0n,
284
+ hint: 0n,
285
+ hookCalls: []
294
286
  });
295
287
  };
296
288
  const onFunctionExit = () => {
@@ -309,11 +301,9 @@ function useHookCollector(context) {
309
301
  ":function:exit": onFunctionExit,
310
302
  CallExpression(node) {
311
303
  if (!isHookCall(node)) return;
312
- const fEntry = getCurrentEntry();
313
- if (fEntry?.key == null) return;
314
- const hook = hooks.get(fEntry.key);
315
- if (hook == null) return;
316
- hook.hookCalls.push(node);
304
+ const entry = getCurrentEntry();
305
+ if (entry == null) return;
306
+ hooks.get(entry.key)?.hookCalls.push(node);
317
307
  }
318
308
  }
319
309
  };
@@ -721,6 +711,99 @@ function isPureComponent(node) {
721
711
  return false;
722
712
  }
723
713
 
714
+ //#endregion
715
+ //#region src/component/component-wrapper.ts
716
+ /**
717
+ * Check if the node is a call expression for a component wrapper
718
+ * @param context The ESLint rule context
719
+ * @param node The node to check
720
+ * @returns `true` if the node is a call expression for a component wrapper
721
+ */
722
+ function isComponentWrapperCall(context, node) {
723
+ if (node.type !== AST_NODE_TYPES.CallExpression) return false;
724
+ return isMemoCall(context, node) || isForwardRefCall(context, node);
725
+ }
726
+ /**
727
+ * Check if the node is a call expression for a component wrapper loosely
728
+ * @param context The ESLint rule context
729
+ * @param node The node to check
730
+ * @returns `true` if the node is a call expression for a component wrapper loosely
731
+ */
732
+ function isComponentWrapperCallLoose(context, node) {
733
+ if (node.type !== AST_NODE_TYPES.CallExpression) return false;
734
+ return isComponentWrapperCall(context, node) || isUseCallbackCall(node);
735
+ }
736
+ /**
737
+ * Check if the node is a callback function passed to a component wrapper
738
+ * @param context The ESLint rule context
739
+ * @param node The node to check
740
+ * @returns `true` if the node is a callback function passed to a component wrapper
741
+ */
742
+ function isComponentWrapperCallback(context, node) {
743
+ if (!AST.isFunction(node)) return false;
744
+ const parent = node.parent;
745
+ if (parent.type !== AST_NODE_TYPES.CallExpression) return false;
746
+ return isComponentWrapperCall(context, parent);
747
+ }
748
+ /**
749
+ * Check if the node is a callback function passed to a component wrapper loosely
750
+ * @param context The ESLint rule context
751
+ * @param node The node to check
752
+ * @returns `true` if the node is a callback function passed to a component wrapper loosely
753
+ */
754
+ function isComponentWrapperCallbackLoose(context, node) {
755
+ if (!AST.isFunction(node)) return false;
756
+ const parent = node.parent;
757
+ if (parent.type !== AST_NODE_TYPES.CallExpression) return false;
758
+ return isComponentWrapperCallLoose(context, parent);
759
+ }
760
+
761
+ //#endregion
762
+ //#region src/component/component-id.ts
763
+ /**
764
+ * Get function component identifier from `const Component = memo(() => {});`
765
+ * @param context The rule context
766
+ * @param node The function node to analyze
767
+ * @returns The function identifier or `unit` if not found
768
+ */
769
+ function getFunctionComponentId(context, node) {
770
+ const functionId = AST.getFunctionId(node);
771
+ if (functionId != null) return functionId;
772
+ const { parent } = node;
773
+ if (parent.type === AST_NODE_TYPES.CallExpression && isComponentWrapperCallLoose(context, parent) && parent.parent.type === AST_NODE_TYPES.VariableDeclarator) return parent.parent.id;
774
+ if (parent.type === AST_NODE_TYPES.CallExpression && isComponentWrapperCallLoose(context, parent) && parent.parent.type === AST_NODE_TYPES.CallExpression && isComponentWrapperCallLoose(context, parent.parent) && parent.parent.parent.type === AST_NODE_TYPES.VariableDeclarator) return parent.parent.parent.id;
775
+ return unit;
776
+ }
777
+
778
+ //#endregion
779
+ //#region src/component/component-name.ts
780
+ /**
781
+ * Check if a string matches the strict component name pattern
782
+ * @param name The name to check
783
+ */
784
+ function isComponentName(name) {
785
+ return RE_COMPONENT_NAME.test(name);
786
+ }
787
+ /**
788
+ * Check if a string matches the loose component name pattern
789
+ * @param name The name to check
790
+ */
791
+ function isComponentNameLoose(name) {
792
+ return RE_COMPONENT_NAME_LOOSE.test(name);
793
+ }
794
+ /**
795
+ * Check if the function has no name or a loose component name
796
+ * @param context The rule context
797
+ * @param fn The function node
798
+ */
799
+ function hasNoneOrLooseComponentName(context, fn) {
800
+ const id = getFunctionComponentId(context, fn);
801
+ if (id == null) return true;
802
+ if (id.type === AST_NODE_TYPES.Identifier) return isComponentNameLoose(id.name);
803
+ if (id.type === AST_NODE_TYPES.MemberExpression && id.property.type === AST_NODE_TYPES.Identifier) return isComponentNameLoose(id.property.name);
804
+ return false;
805
+ }
806
+
724
807
  //#endregion
725
808
  //#region src/component/component-render-method.ts
726
809
  /**
@@ -799,6 +882,7 @@ function isChildrenOfCreateElement(context, node) {
799
882
  * @returns `true` if the node is considered a component definition
800
883
  */
801
884
  function isComponentDefinition(context, node, hint) {
885
+ if (!hasNoneOrLooseComponentName(context, node)) return false;
802
886
  if (isChildrenOfCreateElement(context, node) || isRenderMethodCallback(node)) return false;
803
887
  if (shouldExcludeBasedOnHint(node, hint)) return false;
804
888
  const significantParent = AST.findParentNode(node, AST.isOneOf([
@@ -813,70 +897,6 @@ function isComponentDefinition(context, node, hint) {
813
897
  return true;
814
898
  }
815
899
 
816
- //#endregion
817
- //#region src/component/component-wrapper.ts
818
- /**
819
- * Check if the node is a call expression for a component wrapper
820
- * @param context The ESLint rule context
821
- * @param node The node to check
822
- * @returns `true` if the node is a call expression for a component wrapper
823
- */
824
- function isComponentWrapperCall(context, node) {
825
- if (node.type !== AST_NODE_TYPES.CallExpression) return false;
826
- return isMemoCall(context, node) || isForwardRefCall(context, node);
827
- }
828
- /**
829
- * Check if the node is a call expression for a component wrapper loosely
830
- * @param context The ESLint rule context
831
- * @param node The node to check
832
- * @returns `true` if the node is a call expression for a component wrapper loosely
833
- */
834
- function isComponentWrapperCallLoose(context, node) {
835
- if (node.type !== AST_NODE_TYPES.CallExpression) return false;
836
- return isComponentWrapperCall(context, node) || isUseCallbackCall(node);
837
- }
838
- /**
839
- * Check if the node is a callback function passed to a component wrapper
840
- * @param context The ESLint rule context
841
- * @param node The node to check
842
- * @returns `true` if the node is a callback function passed to a component wrapper
843
- */
844
- function isComponentWrapperCallback(context, node) {
845
- if (!AST.isFunction(node)) return false;
846
- const parent = node.parent;
847
- if (parent.type !== AST_NODE_TYPES.CallExpression) return false;
848
- return isComponentWrapperCall(context, parent);
849
- }
850
- /**
851
- * Check if the node is a callback function passed to a component wrapper loosely
852
- * @param context The ESLint rule context
853
- * @param node The node to check
854
- * @returns `true` if the node is a callback function passed to a component wrapper loosely
855
- */
856
- function isComponentWrapperCallbackLoose(context, node) {
857
- if (!AST.isFunction(node)) return false;
858
- const parent = node.parent;
859
- if (parent.type !== AST_NODE_TYPES.CallExpression) return false;
860
- return isComponentWrapperCallLoose(context, parent);
861
- }
862
-
863
- //#endregion
864
- //#region src/component/component-id.ts
865
- /**
866
- * Get function component identifier from `const Component = memo(() => {});`
867
- * @param context The rule context
868
- * @param node The function node to analyze
869
- * @returns The function identifier or `unit` if not found
870
- */
871
- function getFunctionComponentId(context, node) {
872
- const functionId = AST.getFunctionId(node);
873
- if (functionId != null) return functionId;
874
- const { parent } = node;
875
- if (parent.type === AST_NODE_TYPES.CallExpression && isComponentWrapperCallLoose(context, parent) && parent.parent.type === AST_NODE_TYPES.VariableDeclarator) return parent.parent.id;
876
- if (parent.type === AST_NODE_TYPES.CallExpression && isComponentWrapperCallLoose(context, parent) && parent.parent.type === AST_NODE_TYPES.CallExpression && isComponentWrapperCallLoose(context, parent.parent) && parent.parent.parent.type === AST_NODE_TYPES.VariableDeclarator) return parent.parent.parent.id;
877
- return unit;
878
- }
879
-
880
900
  //#endregion
881
901
  //#region src/component/component-flag.ts
882
902
  /**
@@ -904,35 +924,6 @@ function getComponentFlagFromInitPath(initPath) {
904
924
  return flag;
905
925
  }
906
926
 
907
- //#endregion
908
- //#region src/component/component-name.ts
909
- /**
910
- * Check if a string matches the strict component name pattern
911
- * @param name The name to check
912
- */
913
- function isComponentName(name) {
914
- return RE_COMPONENT_NAME.test(name);
915
- }
916
- /**
917
- * Check if a string matches the loose component name pattern
918
- * @param name The name to check
919
- */
920
- function isComponentNameLoose(name) {
921
- return RE_COMPONENT_NAME_LOOSE.test(name);
922
- }
923
- /**
924
- * Check if the function has no name or a loose component name
925
- * @param context The rule context
926
- * @param fn The function node
927
- */
928
- function hasNoneOrLooseComponentName(context, fn) {
929
- const id = getFunctionComponentId(context, fn);
930
- if (id == null) return true;
931
- if (id.type === AST_NODE_TYPES.Identifier) return isComponentNameLoose(id.name);
932
- if (id.type === AST_NODE_TYPES.MemberExpression && id.property.type === AST_NODE_TYPES.Identifier) return isComponentNameLoose(id.property.name);
933
- return false;
934
- }
935
-
936
927
  //#endregion
937
928
  //#region src/component/component-collector.ts
938
929
  const idGen$1 = new IdGenerator("function_component_");
@@ -943,7 +934,7 @@ const idGen$1 = new IdGenerator("function_component_");
943
934
  * @returns The ctx and visitor of the collector
944
935
  */
945
936
  function useComponentCollector(context, options = {}) {
946
- const { collectDisplayName = false, collectHookCalls = false, hint = DEFAULT_COMPONENT_DETECTION_HINT } = options;
937
+ const { collectDisplayName = false, hint = DEFAULT_COMPONENT_DETECTION_HINT } = options;
947
938
  const functionEntries = [];
948
939
  const components = /* @__PURE__ */ new Map();
949
940
  const getText = (n) => context.sourceCode.getText(n);
@@ -953,11 +944,20 @@ function useComponentCollector(context, options = {}) {
953
944
  const exp = AST.findParentNode(node, (n) => n.type === AST_NODE_TYPES.ExportDefaultDeclaration);
954
945
  const isExportDefault = exp != null;
955
946
  const isExportDefaultDeclaration = exp != null && AST.getUnderlyingExpression(exp.declaration) === node;
947
+ const id = getFunctionComponentId(context, node);
948
+ const name = id == null ? unit : AST.toStringFormat(id, getText);
949
+ const initPath = AST.getFunctionInitPath(node);
956
950
  functionEntries.push({
951
+ id: getFunctionComponentId(context, node),
957
952
  key,
953
+ kind: "function",
954
+ name,
958
955
  node,
956
+ displayName: unit,
957
+ flag: getComponentFlagFromInitPath(initPath),
958
+ hint,
959
959
  hookCalls: [],
960
- isComponent: false,
960
+ initPath,
961
961
  isComponentDefinition: isComponentDefinition(context, node, hint),
962
962
  isExportDefault,
963
963
  isExportDefaultDeclaration,
@@ -983,29 +983,12 @@ function useComponentCollector(context, options = {}) {
983
983
  "ArrowFunctionExpression[body.type!='BlockStatement']"() {
984
984
  const entry = getCurrentEntry();
985
985
  if (entry == null) return;
986
- if (!entry.isComponentDefinition) return;
987
986
  const { body } = entry.node;
988
987
  if (body.type === AST_NODE_TYPES.BlockStatement) return;
989
- if (!(hasNoneOrLooseComponentName(context, entry.node) && isJsxLike(context.sourceCode, body, hint))) return;
990
- const initPath = AST.getFunctionInitPath(entry.node);
991
- const id = getFunctionComponentId(context, entry.node);
992
- const key = entry.key;
993
- const name = id == null ? unit : AST.toStringFormat(id, getText);
994
- components.set(key, {
995
- id,
996
- key,
997
- kind: "function",
998
- name,
999
- node: entry.node,
1000
- displayName: unit,
1001
- flag: getComponentFlagFromInitPath(initPath),
1002
- hint,
1003
- hookCalls: entry.hookCalls,
1004
- initPath,
1005
- isExportDefault: entry.isExportDefault,
1006
- isExportDefaultDeclaration: entry.isExportDefaultDeclaration,
1007
- rets: [body]
1008
- });
988
+ entry.rets.push(body);
989
+ if (!entry.isComponentDefinition) return;
990
+ if (!components.has(entry.key) && !isJsxLike(context.sourceCode, body, hint)) return;
991
+ components.set(entry.key, entry);
1009
992
  },
1010
993
  ...collectDisplayName ? { [AST.SEL_DISPLAY_NAME_ASSIGNMENT_EXPRESSION](node) {
1011
994
  const { left, right } = node;
@@ -1015,39 +998,22 @@ function useComponentCollector(context, options = {}) {
1015
998
  if (component == null) return;
1016
999
  component.displayName = right;
1017
1000
  } } : {},
1018
- ...collectHookCalls ? { "CallExpression:exit"(node) {
1001
+ CallExpression(node) {
1019
1002
  if (!isHookCall(node)) return;
1020
1003
  const entry = getCurrentEntry();
1021
1004
  if (entry == null) return;
1022
1005
  entry.hookCalls.push(node);
1023
- } } : {},
1006
+ if (!entry.isComponentDefinition) return;
1007
+ components.set(entry.key, entry);
1008
+ },
1024
1009
  ReturnStatement(node) {
1025
1010
  const entry = getCurrentEntry();
1026
1011
  if (entry == null) return;
1012
+ entry.rets.push(node.argument);
1027
1013
  if (!entry.isComponentDefinition) return;
1028
1014
  const { argument } = node;
1029
- entry.rets.push(argument);
1030
- if (!(hasNoneOrLooseComponentName(context, entry.node) && isJsxLike(context.sourceCode, argument, hint))) return;
1031
- entry.isComponent = true;
1032
- const initPath = AST.getFunctionInitPath(entry.node);
1033
- const id = getFunctionComponentId(context, entry.node);
1034
- const key = entry.key;
1035
- const name = id == null ? unit : AST.toStringFormat(id, getText);
1036
- components.set(key, {
1037
- id,
1038
- key,
1039
- kind: "function",
1040
- name,
1041
- node: entry.node,
1042
- displayName: unit,
1043
- flag: getComponentFlagFromInitPath(initPath),
1044
- hint,
1045
- hookCalls: entry.hookCalls,
1046
- initPath,
1047
- isExportDefault: entry.isExportDefault,
1048
- isExportDefaultDeclaration: entry.isExportDefaultDeclaration,
1049
- rets: entry.rets
1050
- });
1015
+ if (!components.has(entry.key) && !isJsxLike(context.sourceCode, argument, hint)) return;
1016
+ components.set(entry.key, entry);
1051
1017
  }
1052
1018
  }
1053
1019
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eslint-react/core",
3
- "version": "2.7.5-next.8",
3
+ "version": "2.8.0",
4
4
  "description": "ESLint React's ESLint utility module for static analysis of React core APIs and patterns.",
5
5
  "homepage": "https://github.com/Rel1cx/eslint-react",
6
6
  "bugs": {
@@ -34,10 +34,10 @@
34
34
  "@typescript-eslint/types": "^8.54.0",
35
35
  "@typescript-eslint/utils": "^8.54.0",
36
36
  "ts-pattern": "^5.9.0",
37
- "@eslint-react/ast": "2.7.5-next.8",
38
- "@eslint-react/eff": "2.7.5-next.8",
39
- "@eslint-react/shared": "2.7.5-next.8",
40
- "@eslint-react/var": "2.7.5-next.8"
37
+ "@eslint-react/ast": "2.8.0",
38
+ "@eslint-react/eff": "2.8.0",
39
+ "@eslint-react/shared": "2.8.0",
40
+ "@eslint-react/var": "2.8.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "tsdown": "^0.20.1",