main
ts 293 lines 7.93 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 import type * as BabelCore from '@babel/core';
9 import {NodePath} from '@babel/core';
10 import * as t from '@babel/types';
11
12 export default function AnnotateReactCodeBabelPlugin(
13 _babel: typeof BabelCore,
14 ): BabelCore.PluginObj {
15 return {
16 name: 'annotate-react-code',
17 visitor: {
18 Program(prog): void {
19 annotate(prog);
20 },
21 },
22 };
23 }
24
25 function annotate(program: NodePath<t.Program>): void {
26 function traverseFn(fn: BabelFn): void {
27 if (!shouldVisit(fn)) {
28 return;
29 }
30
31 fn.skip();
32
33 const body = fn.node.body;
34 if (t.isBlockStatement(body)) {
35 body.body.unshift(buildTypeOfReactForget());
36 }
37 }
38
39 program.traverse({
40 FunctionDeclaration: traverseFn,
41 FunctionExpression: traverseFn,
42 ArrowFunctionExpression: traverseFn,
43 });
44 }
45
46 function shouldVisit(fn: BabelFn): boolean {
47 return (
48 // Component declarations are known components
49 (fn.isFunctionDeclaration() && isComponentDeclaration(fn.node)) ||
50 // Otherwise check if this is a component or hook-like function
51 isComponentOrHookLike(fn)
52 );
53 }
54
55 function buildTypeOfReactForget(): t.Statement {
56 // typeof globalThis[Symbol.for("react_forget")]
57 return t.expressionStatement(
58 t.unaryExpression(
59 'typeof',
60 t.memberExpression(
61 t.identifier('globalThis'),
62 t.callExpression(
63 t.memberExpression(
64 t.identifier('Symbol'),
65 t.identifier('for'),
66 false,
67 false,
68 ),
69 [t.stringLiteral('react_forget')],
70 ),
71 true,
72 false,
73 ),
74 true,
75 ),
76 );
77 }
78
79 /**
80 * COPIED FROM babel-plugin-react-compiler/src/Entrypoint/BabelUtils.ts
81 */
82 type ComponentDeclaration = t.FunctionDeclaration & {
83 __componentDeclaration: boolean;
84 };
85
86 type BabelFn =
87 | NodePath<t.FunctionDeclaration>
88 | NodePath<t.FunctionExpression>
89 | NodePath<t.ArrowFunctionExpression>;
90
91 export function isComponentDeclaration(
92 node: t.FunctionDeclaration,
93 ): node is ComponentDeclaration {
94 return Object.prototype.hasOwnProperty.call(node, '__componentDeclaration');
95 }
96
97 /*
98 * Adapted from the ESLint rule at
99 * https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js#L90-L103
100 */
101 function isComponentOrHookLike(
102 node: NodePath<
103 t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
104 >,
105 ): boolean {
106 const functionName = getFunctionName(node);
107 // Check if the name is component or hook like:
108 if (functionName !== null && isComponentName(functionName)) {
109 return (
110 // As an added check we also look for hook invocations or JSX
111 callsHooksOrCreatesJsx(node) &&
112 /*
113 * and avoid helper functions that take more than one argument
114 * helpers are _usually_ named with lowercase, but some code may
115 * violate this rule
116 */
117 node.get('params').length <= 1
118 );
119 } else if (functionName !== null && isHook(functionName)) {
120 // Hooks have hook invocations or JSX, but can take any # of arguments
121 return callsHooksOrCreatesJsx(node);
122 }
123
124 /*
125 * Otherwise for function or arrow function expressions, check if they
126 * appear as the argument to React.forwardRef() or React.memo():
127 */
128 if (node.isFunctionExpression() || node.isArrowFunctionExpression()) {
129 if (isForwardRefCallback(node) || isMemoCallback(node)) {
130 // As an added check we also look for hook invocations or JSX
131 return callsHooksOrCreatesJsx(node);
132 } else {
133 return false;
134 }
135 }
136 return false;
137 }
138
139 function isHookName(s: string): boolean {
140 return /^use[A-Z0-9]/.test(s);
141 }
142
143 /*
144 * We consider hooks to be a hook name identifier or a member expression
145 * containing a hook name.
146 */
147
148 function isHook(path: NodePath<t.Expression | t.PrivateName>): boolean {
149 if (path.isIdentifier()) {
150 return isHookName(path.node.name);
151 } else if (
152 path.isMemberExpression() &&
153 !path.node.computed &&
154 isHook(path.get('property'))
155 ) {
156 const obj = path.get('object').node;
157 const isPascalCaseNameSpace = /^[A-Z].*/;
158 return obj.type === 'Identifier' && isPascalCaseNameSpace.test(obj.name);
159 } else {
160 return false;
161 }
162 }
163
164 /*
165 * Checks if the node is a React component name. React component names must
166 * always start with an uppercase letter.
167 */
168
169 function isComponentName(path: NodePath<t.Expression>): boolean {
170 return path.isIdentifier() && /^[A-Z]/.test(path.node.name);
171 }
172 /*
173 * Checks if the node is a callback argument of forwardRef. This render function
174 * should follow the rules of hooks.
175 */
176
177 function isForwardRefCallback(path: NodePath<t.Expression>): boolean {
178 return !!(
179 path.parentPath.isCallExpression() &&
180 path.parentPath.get('callee').isExpression() &&
181 isReactAPI(path.parentPath.get('callee'), 'forwardRef')
182 );
183 }
184
185 /*
186 * Checks if the node is a callback argument of React.memo. This anonymous
187 * functional component should follow the rules of hooks.
188 */
189
190 function isMemoCallback(path: NodePath<t.Expression>): boolean {
191 return (
192 path.parentPath.isCallExpression() &&
193 path.parentPath.get('callee').isExpression() &&
194 isReactAPI(path.parentPath.get('callee'), 'memo')
195 );
196 }
197
198 function isReactAPI(
199 path: NodePath<t.Expression | t.PrivateName | t.V8IntrinsicIdentifier>,
200 functionName: string,
201 ): boolean {
202 const node = path.node;
203 return (
204 (node.type === 'Identifier' && node.name === functionName) ||
205 (node.type === 'MemberExpression' &&
206 node.object.type === 'Identifier' &&
207 node.object.name === 'React' &&
208 node.property.type === 'Identifier' &&
209 node.property.name === functionName)
210 );
211 }
212
213 function callsHooksOrCreatesJsx(node: NodePath<t.Node>): boolean {
214 let invokesHooks = false;
215 let createsJsx = false;
216 node.traverse({
217 JSX() {
218 createsJsx = true;
219 },
220 CallExpression(call) {
221 const callee = call.get('callee');
222 if (callee.isExpression() && isHook(callee)) {
223 invokesHooks = true;
224 }
225 },
226 });
227
228 return invokesHooks || createsJsx;
229 }
230
231 /*
232 * Gets the static name of a function AST node. For function declarations it is
233 * easy. For anonymous function expressions it is much harder. If you search for
234 * `IsAnonymousFunctionDefinition()` in the ECMAScript spec you'll find places
235 * where JS gives anonymous function expressions names. We roughly detect the
236 * same AST nodes with some exceptions to better fit our use case.
237 */
238
239 function getFunctionName(
240 path: NodePath<
241 t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
242 >,
243 ): NodePath<t.Expression> | null {
244 if (path.isFunctionDeclaration()) {
245 const id = path.get('id');
246 if (id.isIdentifier()) {
247 return id;
248 }
249 return null;
250 }
251 let id: NodePath<t.LVal | t.Expression | t.PrivateName> | null = null;
252 const parent = path.parentPath;
253 if (parent.isVariableDeclarator() && parent.get('init').node === path.node) {
254 // const useHook = () => {};
255 id = parent.get('id');
256 } else if (
257 parent.isAssignmentExpression() &&
258 parent.get('right').node === path.node &&
259 parent.get('operator') === '='
260 ) {
261 // useHook = () => {};
262 id = parent.get('left');
263 } else if (
264 parent.isProperty() &&
265 parent.get('value').node === path.node &&
266 !parent.get('computed') &&
267 parent.get('key').isLVal()
268 ) {
269 /*
270 * {useHook: () => {}}
271 * {useHook() {}}
272 */
273 id = parent.get('key');
274 } else if (
275 parent.isAssignmentPattern() &&
276 parent.get('right').node === path.node &&
277 !parent.get('computed')
278 ) {
279 /*
280 * const {useHook = () => {}} = {};
281 * ({useHook = () => {}} = {});
282 *
283 * Kinda clowny, but we'd said we'd follow spec convention for
284 * `IsAnonymousFunctionDefinition()` usage.
285 */
286 id = parent.get('left');
287 }
288 if (id !== null && (id.isIdentifier() || id.isMemberExpression())) {
289 return id;
290 } else {
291 return null;
292 }
293 }