@openrewrite/recipes-react 0.2.9

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 (251) hide show
  1. package/dist/index.d.ts +4 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +130 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/migration/change-component-prop-value.d.ts +20 -0
  6. package/dist/migration/change-component-prop-value.d.ts.map +1 -0
  7. package/dist/migration/change-component-prop-value.js +217 -0
  8. package/dist/migration/change-component-prop-value.js.map +1 -0
  9. package/dist/react-native/view-prop-types.d.ts +8 -0
  10. package/dist/react-native/view-prop-types.d.ts.map +1 -0
  11. package/dist/react-native/view-prop-types.js +65 -0
  12. package/dist/react-native/view-prop-types.js.map +1 -0
  13. package/dist/react16/error-boundaries.d.ts +8 -0
  14. package/dist/react16/error-boundaries.d.ts.map +1 -0
  15. package/dist/react16/error-boundaries.js +42 -0
  16. package/dist/react16/error-boundaries.js.map +1 -0
  17. package/dist/react16/find-dom-node.d.ts +8 -0
  18. package/dist/react16/find-dom-node.d.ts.map +1 -0
  19. package/dist/react16/find-dom-node.js +48 -0
  20. package/dist/react16/find-dom-node.js.map +1 -0
  21. package/dist/react16/react-dom-factories.d.ts +8 -0
  22. package/dist/react16/react-dom-factories.d.ts.map +1 -0
  23. package/dist/react16/react-dom-factories.js +72 -0
  24. package/dist/react16/react-dom-factories.js.map +1 -0
  25. package/dist/react16/react-prop-types.d.ts +8 -0
  26. package/dist/react16/react-prop-types.d.ts.map +1 -0
  27. package/dist/react16/react-prop-types.js +69 -0
  28. package/dist/react16/react-prop-types.js.map +1 -0
  29. package/dist/react16/react-to-react-dom.d.ts +8 -0
  30. package/dist/react16/react-to-react-dom.d.ts.map +1 -0
  31. package/dist/react16/react-to-react-dom.js +97 -0
  32. package/dist/react16/react-to-react-dom.js.map +1 -0
  33. package/dist/react16/replace-create-factory.d.ts +8 -0
  34. package/dist/react16/replace-create-factory.d.ts.map +1 -0
  35. package/dist/react16/replace-create-factory.js +69 -0
  36. package/dist/react16/replace-create-factory.js.map +1 -0
  37. package/dist/react16/upgrade-to-react-16.d.ts +8 -0
  38. package/dist/react16/upgrade-to-react-16.d.ts.map +1 -0
  39. package/dist/react16/upgrade-to-react-16.js +41 -0
  40. package/dist/react16/upgrade-to-react-16.js.map +1 -0
  41. package/dist/react17/remove-event-persist.d.ts +8 -0
  42. package/dist/react17/remove-event-persist.d.ts.map +1 -0
  43. package/dist/react17/remove-event-persist.js +114 -0
  44. package/dist/react17/remove-event-persist.js.map +1 -0
  45. package/dist/react17/rename-unsafe-lifecycles.d.ts +8 -0
  46. package/dist/react17/rename-unsafe-lifecycles.d.ts.map +1 -0
  47. package/dist/react17/rename-unsafe-lifecycles.js +48 -0
  48. package/dist/react17/rename-unsafe-lifecycles.js.map +1 -0
  49. package/dist/react17/update-react-imports.d.ts +8 -0
  50. package/dist/react17/update-react-imports.d.ts.map +1 -0
  51. package/dist/react17/update-react-imports.js +40 -0
  52. package/dist/react17/update-react-imports.js.map +1 -0
  53. package/dist/react17/upgrade-to-react-17.d.ts +8 -0
  54. package/dist/react17/upgrade-to-react-17.d.ts.map +1 -0
  55. package/dist/react17/upgrade-to-react-17.js +37 -0
  56. package/dist/react17/upgrade-to-react-17.js.map +1 -0
  57. package/dist/react18/remove-unstable-batched-updates.d.ts +8 -0
  58. package/dist/react18/remove-unstable-batched-updates.d.ts.map +1 -0
  59. package/dist/react18/remove-unstable-batched-updates.js +170 -0
  60. package/dist/react18/remove-unstable-batched-updates.js.map +1 -0
  61. package/dist/react18/replace-reactdom-render.d.ts +8 -0
  62. package/dist/react18/replace-reactdom-render.d.ts.map +1 -0
  63. package/dist/react18/replace-reactdom-render.js +55 -0
  64. package/dist/react18/replace-reactdom-render.js.map +1 -0
  65. package/dist/react18/replace-render-callback.d.ts +8 -0
  66. package/dist/react18/replace-render-callback.d.ts.map +1 -0
  67. package/dist/react18/replace-render-callback.js +60 -0
  68. package/dist/react18/replace-render-callback.js.map +1 -0
  69. package/dist/react18/replace-unmount-component-at-node.d.ts +8 -0
  70. package/dist/react18/replace-unmount-component-at-node.d.ts.map +1 -0
  71. package/dist/react18/replace-unmount-component-at-node.js +54 -0
  72. package/dist/react18/replace-unmount-component-at-node.js.map +1 -0
  73. package/dist/react18/upgrade-to-react-18.d.ts +8 -0
  74. package/dist/react18/upgrade-to-react-18.d.ts.map +1 -0
  75. package/dist/react18/upgrade-to-react-18.js +39 -0
  76. package/dist/react18/upgrade-to-react-18.js.map +1 -0
  77. package/dist/react19/deprecated-react-types.d.ts +8 -0
  78. package/dist/react19/deprecated-react-types.d.ts.map +1 -0
  79. package/dist/react19/deprecated-react-types.js +135 -0
  80. package/dist/react19/deprecated-react-types.js.map +1 -0
  81. package/dist/react19/find-context-consumer.d.ts +9 -0
  82. package/dist/react19/find-context-consumer.d.ts.map +1 -0
  83. package/dist/react19/find-context-consumer.js +128 -0
  84. package/dist/react19/find-context-consumer.js.map +1 -0
  85. package/dist/react19/find-deprecated-reactdom-apis.d.ts +9 -0
  86. package/dist/react19/find-deprecated-reactdom-apis.d.ts.map +1 -0
  87. package/dist/react19/find-deprecated-reactdom-apis.js +132 -0
  88. package/dist/react19/find-deprecated-reactdom-apis.js.map +1 -0
  89. package/dist/react19/find-element-ref.d.ts +9 -0
  90. package/dist/react19/find-element-ref.d.ts.map +1 -0
  91. package/dist/react19/find-element-ref.js +88 -0
  92. package/dist/react19/find-element-ref.js.map +1 -0
  93. package/dist/react19/find-legacy-context-api.d.ts +9 -0
  94. package/dist/react19/find-legacy-context-api.d.ts.map +1 -0
  95. package/dist/react19/find-legacy-context-api.js +163 -0
  96. package/dist/react19/find-legacy-context-api.js.map +1 -0
  97. package/dist/react19/no-implicit-ref-callback-return.d.ts +8 -0
  98. package/dist/react19/no-implicit-ref-callback-return.d.ts.map +1 -0
  99. package/dist/react19/no-implicit-ref-callback-return.js +107 -0
  100. package/dist/react19/no-implicit-ref-callback-return.js.map +1 -0
  101. package/dist/react19/remove-context-provider.d.ts +8 -0
  102. package/dist/react19/remove-context-provider.d.ts.map +1 -0
  103. package/dist/react19/remove-context-provider.js +59 -0
  104. package/dist/react19/remove-context-provider.js.map +1 -0
  105. package/dist/react19/remove-forward-ref.d.ts +8 -0
  106. package/dist/react19/remove-forward-ref.d.ts.map +1 -0
  107. package/dist/react19/remove-forward-ref.js +73 -0
  108. package/dist/react19/remove-forward-ref.js.map +1 -0
  109. package/dist/react19/remove-prop-types.d.ts +8 -0
  110. package/dist/react19/remove-prop-types.d.ts.map +1 -0
  111. package/dist/react19/remove-prop-types.js +76 -0
  112. package/dist/react19/remove-prop-types.js.map +1 -0
  113. package/dist/react19/remove-react-fc.d.ts +8 -0
  114. package/dist/react19/remove-react-fc.d.ts.map +1 -0
  115. package/dist/react19/remove-react-fc.js +149 -0
  116. package/dist/react19/remove-react-fc.js.map +1 -0
  117. package/dist/react19/replace-act-import.d.ts +9 -0
  118. package/dist/react19/replace-act-import.d.ts.map +1 -0
  119. package/dist/react19/replace-act-import.js +34 -0
  120. package/dist/react19/replace-act-import.js.map +1 -0
  121. package/dist/react19/replace-default-props.d.ts +8 -0
  122. package/dist/react19/replace-default-props.d.ts.map +1 -0
  123. package/dist/react19/replace-default-props.js +195 -0
  124. package/dist/react19/replace-default-props.js.map +1 -0
  125. package/dist/react19/replace-react-shallow-renderer.d.ts +8 -0
  126. package/dist/react19/replace-react-shallow-renderer.d.ts.map +1 -0
  127. package/dist/react19/replace-react-shallow-renderer.js +69 -0
  128. package/dist/react19/replace-react-shallow-renderer.js.map +1 -0
  129. package/dist/react19/replace-reactdom-hydrate.d.ts +8 -0
  130. package/dist/react19/replace-reactdom-hydrate.d.ts.map +1 -0
  131. package/dist/react19/replace-reactdom-hydrate.js +55 -0
  132. package/dist/react19/replace-reactdom-hydrate.js.map +1 -0
  133. package/dist/react19/replace-string-ref.d.ts +8 -0
  134. package/dist/react19/replace-string-ref.d.ts.map +1 -0
  135. package/dist/react19/replace-string-ref.js +75 -0
  136. package/dist/react19/replace-string-ref.js.map +1 -0
  137. package/dist/react19/replace-use-form-state.d.ts +8 -0
  138. package/dist/react19/replace-use-form-state.d.ts.map +1 -0
  139. package/dist/react19/replace-use-form-state.js +54 -0
  140. package/dist/react19/replace-use-form-state.js.map +1 -0
  141. package/dist/react19/upgrade-to-react-19.d.ts +8 -0
  142. package/dist/react19/upgrade-to-react-19.d.ts.map +1 -0
  143. package/dist/react19/upgrade-to-react-19.js +59 -0
  144. package/dist/react19/upgrade-to-react-19.js.map +1 -0
  145. package/dist/react19/use-context-hook.d.ts +8 -0
  146. package/dist/react19/use-context-hook.d.ts.map +1 -0
  147. package/dist/react19/use-context-hook.js +54 -0
  148. package/dist/react19/use-context-hook.js.map +1 -0
  149. package/dist/react19/use-ref-required-initial.d.ts +8 -0
  150. package/dist/react19/use-ref-required-initial.d.ts.map +1 -0
  151. package/dist/react19/use-ref-required-initial.js +74 -0
  152. package/dist/react19/use-ref-required-initial.js.map +1 -0
  153. package/dist/refactoring/class-to-functional.d.ts +8 -0
  154. package/dist/refactoring/class-to-functional.d.ts.map +1 -0
  155. package/dist/refactoring/class-to-functional.js +205 -0
  156. package/dist/refactoring/class-to-functional.js.map +1 -0
  157. package/dist/refactoring/create-class-to-es6.d.ts +8 -0
  158. package/dist/refactoring/create-class-to-es6.d.ts.map +1 -0
  159. package/dist/refactoring/create-class-to-es6.js +289 -0
  160. package/dist/refactoring/create-class-to-es6.js.map +1 -0
  161. package/dist/refactoring/create-element-to-jsx.d.ts +8 -0
  162. package/dist/refactoring/create-element-to-jsx.d.ts.map +1 -0
  163. package/dist/refactoring/create-element-to-jsx.js +167 -0
  164. package/dist/refactoring/create-element-to-jsx.js.map +1 -0
  165. package/dist/refactoring/manual-bind-to-arrow.d.ts +8 -0
  166. package/dist/refactoring/manual-bind-to-arrow.d.ts.map +1 -0
  167. package/dist/refactoring/manual-bind-to-arrow.js +134 -0
  168. package/dist/refactoring/manual-bind-to-arrow.js.map +1 -0
  169. package/dist/refactoring/pure-render-mixin.d.ts +8 -0
  170. package/dist/refactoring/pure-render-mixin.d.ts.map +1 -0
  171. package/dist/refactoring/pure-render-mixin.js +253 -0
  172. package/dist/refactoring/pure-render-mixin.js.map +1 -0
  173. package/dist/refactoring/sort-comp.d.ts +8 -0
  174. package/dist/refactoring/sort-comp.d.ts.map +1 -0
  175. package/dist/refactoring/sort-comp.js +128 -0
  176. package/dist/refactoring/sort-comp.js.map +1 -0
  177. package/dist/search/find-hook-usage.d.ts +9 -0
  178. package/dist/search/find-hook-usage.d.ts.map +1 -0
  179. package/dist/search/find-hook-usage.js +262 -0
  180. package/dist/search/find-hook-usage.js.map +1 -0
  181. package/dist/search/find-prop-usage.d.ts +15 -0
  182. package/dist/search/find-prop-usage.d.ts.map +1 -0
  183. package/dist/search/find-prop-usage.js +177 -0
  184. package/dist/search/find-prop-usage.js.map +1 -0
  185. package/dist/search/find-react-component.d.ts +15 -0
  186. package/dist/search/find-react-component.d.ts.map +1 -0
  187. package/dist/search/find-react-component.js +260 -0
  188. package/dist/search/find-react-component.js.map +1 -0
  189. package/dist/search/find-server-rendering-usage.d.ts +9 -0
  190. package/dist/search/find-server-rendering-usage.d.ts.map +1 -0
  191. package/dist/search/find-server-rendering-usage.js +131 -0
  192. package/dist/search/find-server-rendering-usage.js.map +1 -0
  193. package/dist/simplify-object-pattern-property.d.ts +8 -0
  194. package/dist/simplify-object-pattern-property.d.ts.map +1 -0
  195. package/dist/simplify-object-pattern-property.js +59 -0
  196. package/dist/simplify-object-pattern-property.js.map +1 -0
  197. package/dist/simplify-react-imports.d.ts +8 -0
  198. package/dist/simplify-react-imports.d.ts.map +1 -0
  199. package/dist/simplify-react-imports.js +199 -0
  200. package/dist/simplify-react-imports.js.map +1 -0
  201. package/package.json +39 -0
  202. package/src/index.ts +149 -0
  203. package/src/migration/change-component-prop-value.ts +268 -0
  204. package/src/react-native/view-prop-types.ts +63 -0
  205. package/src/react16/error-boundaries.ts +46 -0
  206. package/src/react16/find-dom-node.ts +55 -0
  207. package/src/react16/react-dom-factories.ts +99 -0
  208. package/src/react16/react-prop-types.ts +71 -0
  209. package/src/react16/react-to-react-dom.ts +104 -0
  210. package/src/react16/replace-create-factory.ts +96 -0
  211. package/src/react16/upgrade-to-react-16.ts +37 -0
  212. package/src/react17/remove-event-persist.ts +121 -0
  213. package/src/react17/rename-unsafe-lifecycles.ts +57 -0
  214. package/src/react17/update-react-imports.ts +50 -0
  215. package/src/react17/upgrade-to-react-17.ts +30 -0
  216. package/src/react18/remove-unstable-batched-updates.ts +192 -0
  217. package/src/react18/replace-reactdom-render.ts +68 -0
  218. package/src/react18/replace-render-callback.ts +66 -0
  219. package/src/react18/replace-unmount-component-at-node.ts +66 -0
  220. package/src/react18/upgrade-to-react-18.ts +33 -0
  221. package/src/react19/deprecated-react-types.ts +120 -0
  222. package/src/react19/find-context-consumer.ts +127 -0
  223. package/src/react19/find-deprecated-reactdom-apis.ts +125 -0
  224. package/src/react19/find-element-ref.ts +86 -0
  225. package/src/react19/find-legacy-context-api.ts +157 -0
  226. package/src/react19/no-implicit-ref-callback-return.ts +123 -0
  227. package/src/react19/remove-context-provider.ts +87 -0
  228. package/src/react19/remove-forward-ref.ts +69 -0
  229. package/src/react19/remove-prop-types.ts +86 -0
  230. package/src/react19/remove-react-fc.ts +247 -0
  231. package/src/react19/replace-act-import.ts +36 -0
  232. package/src/react19/replace-default-props.ts +220 -0
  233. package/src/react19/replace-react-shallow-renderer.ts +75 -0
  234. package/src/react19/replace-reactdom-hydrate.ts +67 -0
  235. package/src/react19/replace-string-ref.ts +89 -0
  236. package/src/react19/replace-use-form-state.ts +66 -0
  237. package/src/react19/upgrade-to-react-19.ts +66 -0
  238. package/src/react19/use-context-hook.ts +67 -0
  239. package/src/react19/use-ref-required-initial.ts +75 -0
  240. package/src/refactoring/class-to-functional.ts +229 -0
  241. package/src/refactoring/create-class-to-es6.ts +309 -0
  242. package/src/refactoring/create-element-to-jsx.ts +200 -0
  243. package/src/refactoring/manual-bind-to-arrow.ts +139 -0
  244. package/src/refactoring/pure-render-mixin.ts +346 -0
  245. package/src/refactoring/sort-comp.ts +135 -0
  246. package/src/search/find-hook-usage.ts +226 -0
  247. package/src/search/find-prop-usage.ts +176 -0
  248. package/src/search/find-react-component.ts +254 -0
  249. package/src/search/find-server-rendering-usage.ts +120 -0
  250. package/src/simplify-object-pattern-property.ts +71 -0
  251. package/src/simplify-react-imports.ts +241 -0
@@ -0,0 +1,135 @@
1
+ import {ExecutionContext, Recipe, TreeVisitor} from "@openrewrite/rewrite";
2
+ import {JavaScriptVisitor} from "@openrewrite/rewrite/javascript";
3
+ import {J} from "@openrewrite/rewrite/java";
4
+
5
+ const LIFECYCLE_METHODS = [
6
+ 'displayName',
7
+ 'propTypes',
8
+ 'contextTypes',
9
+ 'childContextTypes',
10
+ 'mixins',
11
+ 'statics',
12
+ 'defaultProps',
13
+ 'constructor',
14
+ 'getDefaultProps',
15
+ 'state',
16
+ 'getInitialState',
17
+ 'getChildContext',
18
+ 'getDerivedStateFromProps',
19
+ 'componentWillMount',
20
+ 'UNSAFE_componentWillMount',
21
+ 'componentDidMount',
22
+ 'componentWillReceiveProps',
23
+ 'UNSAFE_componentWillReceiveProps',
24
+ 'shouldComponentUpdate',
25
+ 'componentWillUpdate',
26
+ 'UNSAFE_componentWillUpdate',
27
+ 'getSnapshotBeforeUpdate',
28
+ 'componentDidUpdate',
29
+ 'componentDidCatch',
30
+ 'componentWillUnmount',
31
+ ];
32
+
33
+ /**
34
+ * Reorders React component methods to match ESLint `react/sort-comp` ordering.
35
+ *
36
+ * Default order:
37
+ * 1. Static methods and properties
38
+ * 2. Lifecycle methods
39
+ * 3. Event handlers (on*)
40
+ * 4. Getter/setter methods
41
+ * 5. Other instance methods
42
+ * 6. render()
43
+ *
44
+ * @see https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/sort-comp.md
45
+ */
46
+ export class SortComp extends Recipe {
47
+ readonly name = "org.openrewrite.react.refactoring.sort-comp";
48
+ readonly displayName: string = "Sort React component methods";
49
+ readonly description: string = "Reorders React component methods to follow the recommended lifecycle ordering convention.";
50
+
51
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
52
+ return new class extends JavaScriptVisitor<ExecutionContext> {
53
+ override async visitClassDeclaration(classDecl: J.ClassDeclaration, ctx: ExecutionContext): Promise<J | undefined> {
54
+ let cd = await super.visitClassDeclaration(classDecl, ctx) as J.ClassDeclaration;
55
+
56
+ if (!cd.body || cd.body.statements.length <= 1) {
57
+ return cd;
58
+ }
59
+
60
+ const stmts = cd.body.statements;
61
+ const sorted = [...stmts].sort((a, b) => {
62
+ const nameA = this.getMethodName(a.element);
63
+ const nameB = this.getMethodName(b.element);
64
+ const orderA = this.getSortOrder(nameA, a.element);
65
+ const orderB = this.getSortOrder(nameB, b.element);
66
+ if (orderA !== orderB) return orderA - orderB;
67
+ return 0; // preserve original order within same category
68
+ });
69
+
70
+ // Check if order actually changed
71
+ const changed = sorted.some((s, i) => s !== stmts[i]);
72
+ if (!changed) return cd;
73
+
74
+ return {
75
+ ...cd,
76
+ body: {
77
+ ...cd.body,
78
+ statements: sorted
79
+ }
80
+ } as any as J.ClassDeclaration;
81
+ }
82
+
83
+ private getMethodName(node: J | undefined): string {
84
+ if (!node) return '';
85
+ if (node.kind === J.Kind.MethodDeclaration) {
86
+ return (node as J.MethodDeclaration).name.simpleName;
87
+ }
88
+ if (node.kind === J.Kind.VariableDeclarations) {
89
+ const vars = node as J.VariableDeclarations;
90
+ if (vars.variables.length > 0) {
91
+ const name = vars.variables[0].element.name;
92
+ if (name.kind === J.Kind.Identifier) {
93
+ return (name as J.Identifier).simpleName;
94
+ }
95
+ }
96
+ }
97
+ return '';
98
+ }
99
+
100
+ private isStatic(node: J | undefined): boolean {
101
+ if (!node) return false;
102
+ if (node.kind === J.Kind.MethodDeclaration) {
103
+ const method = node as J.MethodDeclaration;
104
+ return method.modifiers?.some(m => m.keyword === 'static') ?? false;
105
+ }
106
+ if (node.kind === J.Kind.VariableDeclarations) {
107
+ const vars = node as J.VariableDeclarations;
108
+ return vars.modifiers?.some(m => m.keyword === 'static') ?? false;
109
+ }
110
+ return false;
111
+ }
112
+
113
+ private getSortOrder(name: string, node: J | undefined): number {
114
+ // 0: static methods/properties
115
+ if (this.isStatic(node)) return 0;
116
+
117
+ // 1: lifecycle methods
118
+ const lifecycleIndex = LIFECYCLE_METHODS.indexOf(name);
119
+ if (lifecycleIndex >= 0) return 100 + lifecycleIndex;
120
+
121
+ // 5: render is always last
122
+ if (name === 'render') return 500;
123
+
124
+ // 2: event handlers (on*)
125
+ if (/^on[A-Z]/.test(name)) return 200;
126
+
127
+ // 3: getters/setters
128
+ if (/^(get|set)[A-Z]/.test(name)) return 300;
129
+
130
+ // 4: everything else
131
+ return 400;
132
+ }
133
+ }();
134
+ }
135
+ }
@@ -0,0 +1,226 @@
1
+ import {Column, DataTable, ExecutionContext, foundSearchResult, Recipe, TreeVisitor} from "@openrewrite/rewrite";
2
+ import {JavaScriptVisitor, JS} from "@openrewrite/rewrite/javascript";
3
+ import {isIdentifier, J} from "@openrewrite/rewrite/java";
4
+
5
+ const BUILT_IN_HOOKS = new Set([
6
+ "useState", "useEffect", "useContext", "useReducer", "useCallback",
7
+ "useMemo", "useRef", "useImperativeHandle", "useLayoutEffect",
8
+ "useDebugValue", "useDeferredValue", "useTransition", "useId",
9
+ "useSyncExternalStore", "useInsertionEffect", "useOptimistic",
10
+ "useFormStatus", "useFormState", "useActionState", "use"
11
+ ]);
12
+
13
+ const HOOK_CATEGORIES: Record<string, string> = {
14
+ useState: "state", useReducer: "state",
15
+ useEffect: "effect", useLayoutEffect: "effect", useInsertionEffect: "effect",
16
+ useContext: "context", use: "context",
17
+ useRef: "ref", useImperativeHandle: "ref",
18
+ useMemo: "memo",
19
+ useCallback: "callback",
20
+ useId: "id",
21
+ useDeferredValue: "concurrency", useTransition: "concurrency",
22
+ useSyncExternalStore: "sync",
23
+ useDebugValue: "debug",
24
+ useOptimistic: "state", useFormStatus: "state", useFormState: "state", useActionState: "state"
25
+ };
26
+
27
+ class FindHookUsageRow {
28
+ @Column({displayName: "Hook name", description: "The name of the hook"})
29
+ hookName!: string;
30
+
31
+ @Column({displayName: "File path", description: "The file path where the hook is used"})
32
+ filePath!: string;
33
+
34
+ @Column({displayName: "Component name", description: "The name of the enclosing component or custom hook"})
35
+ componentName!: string;
36
+
37
+ @Column({displayName: "Hook type", description: "Whether the hook is built-in or custom"})
38
+ hookType!: string;
39
+
40
+ @Column({displayName: "Hook category", description: "The category of the hook (state, effect, ref, memo, callback, context, custom)"})
41
+ hookCategory!: string;
42
+
43
+ @Column({displayName: "Is conditional", description: "Whether the hook is called inside a conditional block"})
44
+ isConditional!: boolean;
45
+
46
+ @Column({displayName: "Is in loop", description: "Whether the hook is called inside a loop"})
47
+ isInLoop!: boolean;
48
+ }
49
+
50
+ /**
51
+ * Finds React hook usage across source files.
52
+ *
53
+ * Detects both built-in hooks (useState, useEffect, etc.) and custom hooks
54
+ * (functions starting with "use" followed by an uppercase letter).
55
+ *
56
+ * Reports violations of the Rules of Hooks:
57
+ * - Hooks called inside conditionals
58
+ * - Hooks called inside loops
59
+ */
60
+ export class FindHookUsage extends Recipe {
61
+ readonly name = "org.openrewrite.react.search.find-hook-usage";
62
+ readonly displayName: string = "Find React hook usage";
63
+ readonly description: string = "Finds all React hook usages including built-in and custom hooks, and detects Rules of Hooks violations.";
64
+
65
+ private dataTable = new DataTable<FindHookUsageRow>(
66
+ "org.openrewrite.react.search.FindHookUsageTable",
67
+ "React hook usages",
68
+ "Table of all React hook usages found in the codebase.",
69
+ FindHookUsageRow
70
+ );
71
+
72
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
73
+ const dataTable = this.dataTable;
74
+
75
+ return new class extends JavaScriptVisitor<ExecutionContext> {
76
+ private filePath = "";
77
+ private componentStack: string[] = [];
78
+ private conditionalDepth = 0;
79
+ private loopDepth = 0;
80
+
81
+ override async visitJsCompilationUnit(cu: JS.CompilationUnit, ctx: ExecutionContext): Promise<J | undefined> {
82
+ this.filePath = cu.sourcePath ?? "";
83
+ this.componentStack = [];
84
+ this.conditionalDepth = 0;
85
+ this.loopDepth = 0;
86
+ return super.visitJsCompilationUnit(cu, ctx);
87
+ }
88
+
89
+ // Track component/function scope for reporting
90
+ override async visitMethodDeclaration(method: J.MethodDeclaration, ctx: ExecutionContext): Promise<J | undefined> {
91
+ const name = method.name.simpleName;
92
+ this.componentStack.push(name);
93
+ const result = await super.visitMethodDeclaration(method, ctx);
94
+ this.componentStack.pop();
95
+ return result;
96
+ }
97
+
98
+ override async visitLambda(lambda: J.Lambda, ctx: ExecutionContext): Promise<J | undefined> {
99
+ // Try to get the variable name this lambda is assigned to
100
+ const varName = this.getLambdaName();
101
+ if (varName) {
102
+ this.componentStack.push(varName);
103
+ }
104
+ const result = await super.visitLambda(lambda, ctx);
105
+ if (varName) {
106
+ this.componentStack.pop();
107
+ }
108
+ return result;
109
+ }
110
+
111
+ // Track conditionals
112
+ override async visitIf(ifStmt: J.If, ctx: ExecutionContext): Promise<J | undefined> {
113
+ this.conditionalDepth++;
114
+ const result = await super.visitIf(ifStmt, ctx);
115
+ this.conditionalDepth--;
116
+ return result;
117
+ }
118
+
119
+ override async visitTernary(ternary: J.Ternary, ctx: ExecutionContext): Promise<J | undefined> {
120
+ this.conditionalDepth++;
121
+ const result = await super.visitTernary(ternary, ctx);
122
+ this.conditionalDepth--;
123
+ return result;
124
+ }
125
+
126
+ // Track loops
127
+ override async visitForLoop(forLoop: J.ForLoop, ctx: ExecutionContext): Promise<J | undefined> {
128
+ this.loopDepth++;
129
+ const result = await super.visitForLoop(forLoop, ctx);
130
+ this.loopDepth--;
131
+ return result;
132
+ }
133
+
134
+ override async visitForEachLoop(forEachLoop: J.ForEachLoop, ctx: ExecutionContext): Promise<J | undefined> {
135
+ this.loopDepth++;
136
+ const result = await super.visitForEachLoop(forEachLoop, ctx);
137
+ this.loopDepth--;
138
+ return result;
139
+ }
140
+
141
+ override async visitWhileLoop(whileLoop: J.WhileLoop, ctx: ExecutionContext): Promise<J | undefined> {
142
+ this.loopDepth++;
143
+ const result = await super.visitWhileLoop(whileLoop, ctx);
144
+ this.loopDepth--;
145
+ return result;
146
+ }
147
+
148
+ override async visitDoWhileLoop(doWhileLoop: J.DoWhileLoop, ctx: ExecutionContext): Promise<J | undefined> {
149
+ this.loopDepth++;
150
+ const result = await super.visitDoWhileLoop(doWhileLoop, ctx);
151
+ this.loopDepth--;
152
+ return result;
153
+ }
154
+
155
+ // Detect hook calls
156
+ override async visitMethodInvocation(method: J.MethodInvocation, ctx: ExecutionContext): Promise<J | undefined> {
157
+ let m = await super.visitMethodInvocation(method, ctx) as J.MethodInvocation;
158
+
159
+ const hookName = this.getHookName(m);
160
+ if (!hookName) return m;
161
+
162
+ const isBuiltIn = BUILT_IN_HOOKS.has(hookName);
163
+ const hookType = isBuiltIn ? "built-in" : "custom";
164
+ const hookCategory = HOOK_CATEGORIES[hookName] ?? "custom";
165
+ const componentName = this.componentStack.length > 0
166
+ ? this.componentStack[this.componentStack.length - 1]
167
+ : "<module>";
168
+
169
+ dataTable.insertRow(ctx, {
170
+ hookName,
171
+ filePath: this.filePath,
172
+ componentName,
173
+ hookType,
174
+ hookCategory,
175
+ isConditional: this.conditionalDepth > 0,
176
+ isInLoop: this.loopDepth > 0
177
+ });
178
+
179
+ return foundSearchResult(m, hookName);
180
+ }
181
+
182
+ private getHookName(method: J.MethodInvocation): string | null {
183
+ // Direct call: useState(...)
184
+ const name = method.name.simpleName;
185
+ if (this.isHookName(name) && !method.select) {
186
+ return name;
187
+ }
188
+
189
+ // Namespaced call: React.useState(...)
190
+ if (this.isHookName(name) && method.select) {
191
+ const select = (method.select as any).element ?? method.select;
192
+ if (isIdentifier(select)) {
193
+ return name;
194
+ }
195
+ }
196
+
197
+ return null;
198
+ }
199
+
200
+ private isHookName(name: string): boolean {
201
+ // React hooks convention: starts with "use" followed by uppercase letter
202
+ return name.length > 3 && name.startsWith("use") && name[3] === name[3].toUpperCase();
203
+ }
204
+
205
+ private getLambdaName(): string | null {
206
+ // Walk up the cursor to find a variable declaration
207
+ let cursor = this.cursor;
208
+ while (cursor.parent) {
209
+ const node = cursor.parent.value;
210
+ if (!node || typeof node !== 'object') break;
211
+ if ((node as any).kind === J.Kind.NamedVariable) {
212
+ const namedVar = node as J.VariableDeclarations.NamedVariable;
213
+ const name = namedVar.name;
214
+ if (isIdentifier(name)) {
215
+ return name.simpleName;
216
+ }
217
+ }
218
+ if ((node as any).kind === JS.Kind.ScopedVariableDeclarations) break;
219
+ if ((node as any).kind === J.Kind.MethodDeclaration) break;
220
+ cursor = cursor.parent;
221
+ }
222
+ return null;
223
+ }
224
+ }();
225
+ }
226
+ }
@@ -0,0 +1,176 @@
1
+ import {Column, DataTable, ExecutionContext, foundSearchResult, Option, Recipe, TreeVisitor} from "@openrewrite/rewrite";
2
+ import {JavaScriptVisitor, JS, JSX} from "@openrewrite/rewrite/javascript";
3
+ import {Expression, isIdentifier, J} from "@openrewrite/rewrite/java";
4
+
5
+ class FindPropUsageRow {
6
+ @Column({displayName: "Component name", description: "The name of the JSX element receiving the prop"})
7
+ componentName!: string;
8
+
9
+ @Column({displayName: "File path", description: "The file path where the prop usage is found"})
10
+ filePath!: string;
11
+
12
+ @Column({displayName: "Prop name", description: "The name of the prop"})
13
+ propName!: string;
14
+
15
+ @Column({displayName: "Prop value type", description: "The type of value passed to the prop (string, expression, boolean, spread)"})
16
+ propValueType!: string;
17
+
18
+ @Column({displayName: "Prop value", description: "The literal value if available"})
19
+ propValue!: string;
20
+
21
+ @Column({displayName: "Is spread", description: "Whether this prop comes from a spread attribute"})
22
+ isSpread!: boolean;
23
+ }
24
+
25
+ /**
26
+ * Finds prop usages across React components.
27
+ *
28
+ * Detects:
29
+ * - String literal props: `<Button variant="primary" />`
30
+ * - Expression props: `<Button onClick={handler} />`
31
+ * - Boolean shorthand props: `<Button disabled />`
32
+ * - Spread props: `<Button {...props} />`
33
+ *
34
+ * Can optionally filter by component name and/or prop name.
35
+ */
36
+ export class FindPropUsage extends Recipe {
37
+ readonly name = "org.openrewrite.react.search.find-prop-usage";
38
+ readonly displayName: string = "Find React prop usage";
39
+ readonly description: string = "Finds all prop usages on React JSX elements, with optional filtering by component and prop name.";
40
+
41
+ @Option({
42
+ displayName: "Component name",
43
+ description: "Optional component name to filter (e.g., 'Button'). If not specified, finds props on all components.",
44
+ required: false,
45
+ example: "Button"
46
+ })
47
+ componentName?: string;
48
+
49
+ @Option({
50
+ displayName: "Prop name",
51
+ description: "Optional prop name to filter (e.g., 'onClick'). If not specified, finds all props.",
52
+ required: false,
53
+ example: "onClick"
54
+ })
55
+ propName?: string;
56
+
57
+ private dataTable = new DataTable<FindPropUsageRow>(
58
+ "org.openrewrite.react.search.FindPropUsageTable",
59
+ "React prop usages",
60
+ "Table of all React prop usages found in the codebase.",
61
+ FindPropUsageRow
62
+ );
63
+
64
+ constructor(options?: { componentName?: string; propName?: string }) {
65
+ super(options);
66
+ }
67
+
68
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
69
+ const filterComponent = this.componentName;
70
+ const filterProp = this.propName;
71
+ const dataTable = this.dataTable;
72
+
73
+ return new class extends JavaScriptVisitor<ExecutionContext> {
74
+ private filePath = "";
75
+
76
+ override async visitJsCompilationUnit(cu: JS.CompilationUnit, ctx: ExecutionContext): Promise<J | undefined> {
77
+ this.filePath = cu.sourcePath ?? "";
78
+ return super.visitJsCompilationUnit(cu, ctx);
79
+ }
80
+
81
+ override async visitJsxAttribute(attribute: JSX.Attribute, ctx: ExecutionContext): Promise<J | undefined> {
82
+ const attr = await super.visitJsxAttribute(attribute, ctx) as JSX.Attribute;
83
+
84
+ // Get the enclosing JSX tag
85
+ const tag = this.cursor.firstEnclosing((t): t is JSX.Tag =>
86
+ t.kind === JS.Kind.JsxTag
87
+ );
88
+ if (!tag) return attr;
89
+
90
+ const tagName = this.getTagName(tag);
91
+
92
+ // Apply component filter
93
+ if (filterComponent && tagName !== filterComponent) return attr;
94
+
95
+ // Get prop name
96
+ const key = attr.key;
97
+ if (!isIdentifier(key)) return attr;
98
+ const propNameStr = key.simpleName;
99
+
100
+ // Apply prop name filter
101
+ if (filterProp && propNameStr !== filterProp) return attr;
102
+
103
+ // Determine value type and value
104
+ let propValueType: string;
105
+ let propValue: string;
106
+
107
+ if (!attr.value) {
108
+ // Boolean shorthand: <Button disabled />
109
+ propValueType = "boolean";
110
+ propValue = "true";
111
+ } else {
112
+ const valueExpr = attr.value.element;
113
+ if (valueExpr.kind === J.Kind.Literal) {
114
+ const literal = valueExpr as J.Literal;
115
+ propValueType = typeof literal.value === 'string' ? "string" : String(typeof literal.value);
116
+ propValue = String(literal.value ?? "");
117
+ } else {
118
+ propValueType = "expression";
119
+ propValue = "";
120
+ }
121
+ }
122
+
123
+ dataTable.insertRow(ctx, {
124
+ componentName: tagName,
125
+ filePath: this.filePath,
126
+ propName: propNameStr,
127
+ propValueType,
128
+ propValue,
129
+ isSpread: false
130
+ });
131
+
132
+ return foundSearchResult(attr, propNameStr);
133
+ }
134
+
135
+ override async visitJsxSpreadAttribute(spread: JSX.SpreadAttribute, ctx: ExecutionContext): Promise<J | undefined> {
136
+ const s = await super.visitJsxSpreadAttribute(spread, ctx) as JSX.SpreadAttribute;
137
+
138
+ // Get the enclosing JSX tag
139
+ const tag = this.cursor.firstEnclosing((t): t is JSX.Tag =>
140
+ t.kind === JS.Kind.JsxTag
141
+ );
142
+ if (!tag) return s;
143
+
144
+ const tagName = this.getTagName(tag);
145
+ if (filterComponent && tagName !== filterComponent) return s;
146
+
147
+ // For spreads, we don't apply prop name filter since spread contains multiple props
148
+ if (filterProp) return s;
149
+
150
+ dataTable.insertRow(ctx, {
151
+ componentName: tagName,
152
+ filePath: this.filePath,
153
+ propName: "...",
154
+ propValueType: "spread",
155
+ propValue: "",
156
+ isSpread: true
157
+ });
158
+
159
+ return foundSearchResult(s, "...");
160
+ }
161
+
162
+ private getTagName(tag: JSX.Tag): string {
163
+ const openName = tag.openName.element;
164
+ if (isIdentifier(openName)) {
165
+ return openName.simpleName;
166
+ } else if (openName.kind === J.Kind.FieldAccess) {
167
+ const fa = openName as J.FieldAccess;
168
+ const target = isIdentifier(fa.target) ? fa.target.simpleName + "." : "";
169
+ const name = fa.name.element;
170
+ return target + (isIdentifier(name) ? name.simpleName : "");
171
+ }
172
+ return "";
173
+ }
174
+ }();
175
+ }
176
+ }