mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Option to infer React functions to compile
Adds a new option to infer which functions to compile, based on React's ESLint rule. The main difference is that in addition to checking the function name we also check that it creates JSX or calls a hook. This should cover a significant majority of components and reduce the chance of accidentally targeting non-components, but it will leave some false negatives. Note that some cases that the ESLint plugin infers as React functions don't work yet: we don't compile FunctionExpressions, only ArrowFunctionExpressions, and the way we handle ArrowFunctionExpression doesn't work with things like forwardRef or variable declarations. We'll need more updates to fully handle all these cases, which I'll do later in the stack.
This commit is contained in:
@@ -94,6 +94,12 @@ export type PluginOptions = {
|
||||
*
|
||||
*/
|
||||
enableOnlyOnReactScript: boolean;
|
||||
|
||||
/**
|
||||
* Enable to make Forget infer which components to compile, based on the same rules
|
||||
* that React's ESLint rules use to detect components.
|
||||
*/
|
||||
enableInferReactFunctions: boolean;
|
||||
};
|
||||
|
||||
export type Logger = {
|
||||
@@ -103,6 +109,7 @@ export type Logger = {
|
||||
export const defaultOptions: PluginOptions = {
|
||||
enableOnlyOnReactScript: false,
|
||||
enableOnlyOnUseForgetDirective: false,
|
||||
enableInferReactFunctions: false,
|
||||
panicOnBailout: true,
|
||||
environment: null,
|
||||
logger: null,
|
||||
|
||||
@@ -332,6 +332,11 @@ function shouldVisitNode(
|
||||
}
|
||||
}
|
||||
|
||||
if (pass.opts.enableInferReactFunctions) {
|
||||
const isReactLike = isReactFunctionLike(fn);
|
||||
return isReactLike;
|
||||
}
|
||||
|
||||
return fn.scope.getProgramParent() === fn.scope.parent;
|
||||
}
|
||||
|
||||
@@ -419,3 +424,171 @@ function buildBlockStatement(
|
||||
|
||||
return body.node;
|
||||
}
|
||||
|
||||
function isHookName(s: string): boolean {
|
||||
return /^use[A-Z0-9]/.test(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* We consider hooks to be a hook name identifier or a member expression
|
||||
* containing a hook name.
|
||||
*/
|
||||
|
||||
function isHook(path: NodePath<t.Expression | t.PrivateName>): boolean {
|
||||
if (path.isIdentifier()) {
|
||||
return isHookName(path.node.name);
|
||||
} else if (
|
||||
path.isMemberExpression() &&
|
||||
!path.node.computed &&
|
||||
isHook(path.get("property"))
|
||||
) {
|
||||
const obj = path.get("object").node;
|
||||
const isPascalCaseNameSpace = /^[A-Z].*/;
|
||||
return obj.type === "Identifier" && isPascalCaseNameSpace.test(obj.name);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the node is a React component name. React component names must
|
||||
* always start with an uppercase letter.
|
||||
*/
|
||||
|
||||
function isComponentName(path: NodePath<t.Expression>): boolean {
|
||||
return path.isIdentifier() && /^[A-Z]/.test(path.node.name);
|
||||
}
|
||||
|
||||
function isReactFunction(
|
||||
path: NodePath<t.Expression | t.PrivateName | t.V8IntrinsicIdentifier>,
|
||||
functionName: string
|
||||
): boolean {
|
||||
const node = path.node;
|
||||
return (
|
||||
(node.type === "Identifier" && node.name === functionName) ||
|
||||
(node.type === "MemberExpression" &&
|
||||
node.object.type === "Identifier" &&
|
||||
node.object.name === "React" &&
|
||||
node.property.type === "Identifier" &&
|
||||
node.property.name === functionName)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the node is a callback argument of forwardRef. This render function
|
||||
* should follow the rules of hooks.
|
||||
*/
|
||||
|
||||
function isForwardRefCallback(path: NodePath<t.Expression>): boolean {
|
||||
return !!(
|
||||
path.parentPath.isCallExpression() &&
|
||||
path.parentPath.get("callee").isExpression() &&
|
||||
isReactFunction(path.parentPath.get("callee"), "forwardRef")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the node is a callback argument of React.memo. This anonymous
|
||||
* functional component should follow the rules of hooks.
|
||||
*/
|
||||
|
||||
function isMemoCallback(path: NodePath<t.Expression>): boolean {
|
||||
return !!(
|
||||
path.parentPath.isCallExpression() &&
|
||||
path.parentPath.get("callee").isExpression() &&
|
||||
isReactFunction(path.parentPath.get("callee"), "memo")
|
||||
);
|
||||
}
|
||||
|
||||
function isReactFunctionLike(
|
||||
node: NodePath<t.FunctionDeclaration | t.ArrowFunctionExpression>
|
||||
): boolean {
|
||||
const functionName = getFunctionName(node);
|
||||
if (functionName !== null) {
|
||||
if (!isComponentName(functionName) && !isHook(functionName)) {
|
||||
return false;
|
||||
}
|
||||
} else if (
|
||||
node.isExpression() &&
|
||||
!isForwardRefCallback(node) &&
|
||||
!isMemoCallback(node)
|
||||
) {
|
||||
return false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
let invokesHooks = false;
|
||||
let createsJsx = false;
|
||||
node.traverse({
|
||||
JSX() {
|
||||
createsJsx = true;
|
||||
},
|
||||
CallExpression(call) {
|
||||
const callee = call.get("callee");
|
||||
if (callee.isExpression() && isHook(callee)) {
|
||||
invokesHooks = true;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return invokesHooks || createsJsx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the static name of a function AST node. For function declarations it is
|
||||
* easy. For anonymous function expressions it is much harder. If you search for
|
||||
* `IsAnonymousFunctionDefinition()` in the ECMAScript spec you'll find places
|
||||
* where JS gives anonymous function expressions names. We roughly detect the
|
||||
* same AST nodes with some exceptions to better fit our use case.
|
||||
*/
|
||||
|
||||
function getFunctionName(
|
||||
path: NodePath<t.FunctionDeclaration | t.ArrowFunctionExpression>
|
||||
): NodePath<t.Expression> | null {
|
||||
if (path.isFunctionDeclaration()) {
|
||||
const id = path.get("id");
|
||||
if (id.isIdentifier()) {
|
||||
return id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
let id: NodePath<t.LVal | t.Expression | t.PrivateName> | null = null;
|
||||
const parent = path.parentPath;
|
||||
if (parent.isVariableDeclarator() && parent.get("init").node === path.node) {
|
||||
// const useHook = () => {};
|
||||
id = parent.get("id");
|
||||
} else if (
|
||||
parent.isAssignmentExpression() &&
|
||||
parent.get("right").node === path.node &&
|
||||
parent.get("operator") === "="
|
||||
) {
|
||||
// useHook = () => {};
|
||||
id = parent.get("left");
|
||||
} else if (
|
||||
parent.isProperty() &&
|
||||
parent.get("value").node === path.node &&
|
||||
!parent.get("computed") &&
|
||||
parent.get("key").isLVal()
|
||||
) {
|
||||
// {useHook: () => {}}
|
||||
// {useHook() {}}
|
||||
id = parent.get("key");
|
||||
} else if (
|
||||
parent.isAssignmentPattern() &&
|
||||
parent.get("right").node === path.node &&
|
||||
!parent.get("computed")
|
||||
) {
|
||||
// const {useHook = () => {}} = {};
|
||||
// ({useHook = () => {}} = {});
|
||||
//
|
||||
// Kinda clowny, but we'd said we'd follow spec convention for
|
||||
// `IsAnonymousFunctionDefinition()` usage.
|
||||
id = parent.get("left");
|
||||
}
|
||||
if (id !== null && (id.isIdentifier() || id.isMemberExpression())) {
|
||||
return id;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableInferReactFunctions
|
||||
const Component = (props) => {
|
||||
return <div />;
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Duplicate declaration "Component" (This is an error on an internal node. Probably an internal error.)
|
||||
```
|
||||
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
// @enableInferReactFunctions
|
||||
const Component = (props) => {
|
||||
return <div />;
|
||||
};
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableInferReactFunctions
|
||||
React.memo((props) => {
|
||||
return <div />;
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @enableInferReactFunctions
|
||||
React.memo((props) => {
|
||||
return <div />;
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
// @enableInferReactFunctions
|
||||
React.memo((props) => {
|
||||
return <div />;
|
||||
});
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableInferReactFunctions
|
||||
React.forwardRef((props) => {
|
||||
return <div />;
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @enableInferReactFunctions
|
||||
React.forwardRef((props) => {
|
||||
return <div />;
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
// @enableInferReactFunctions
|
||||
React.forwardRef((props) => {
|
||||
return <div />;
|
||||
});
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableInferReactFunctions
|
||||
function Component(props) {
|
||||
const [state, _] = useState(null);
|
||||
return [state];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { unstable_useMemoCache as useMemoCache } from "react"; // @enableInferReactFunctions
|
||||
function Component(props) {
|
||||
const $ = useMemoCache(2);
|
||||
const [state] = useState(null);
|
||||
const c_0 = $[0] !== state;
|
||||
let t0;
|
||||
if (c_0) {
|
||||
t0 = [state];
|
||||
$[0] = state;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// @enableInferReactFunctions
|
||||
function Component(props) {
|
||||
const [state, _] = useState(null);
|
||||
return [state];
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableInferReactFunctions
|
||||
function Component(props) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { unstable_useMemoCache as useMemoCache } from "react"; // @enableInferReactFunctions
|
||||
function Component(props) {
|
||||
const $ = useMemoCache(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <div />;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
// @enableInferReactFunctions
|
||||
function Component(props) {
|
||||
return <div />;
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableInferReactFunctions
|
||||
function useStateValue(props) {
|
||||
const [state, _] = useState(null);
|
||||
return [state];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { unstable_useMemoCache as useMemoCache } from "react"; // @enableInferReactFunctions
|
||||
function useStateValue(props) {
|
||||
const $ = useMemoCache(2);
|
||||
const [state] = useState(null);
|
||||
const c_0 = $[0] !== state;
|
||||
let t0;
|
||||
if (c_0) {
|
||||
t0 = [state];
|
||||
$[0] = state;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// @enableInferReactFunctions
|
||||
function useStateValue(props) {
|
||||
const [state, _] = useState(null);
|
||||
return [state];
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableInferReactFunctions
|
||||
function useDiv(props) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { unstable_useMemoCache as useMemoCache } from "react"; // @enableInferReactFunctions
|
||||
function useDiv(props) {
|
||||
const $ = useMemoCache(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <div />;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
// @enableInferReactFunctions
|
||||
function useDiv(props) {
|
||||
return <div />;
|
||||
}
|
||||
@@ -33,6 +33,7 @@ export function transformFixtureInput(
|
||||
let validateNoSetStateInRender = true;
|
||||
let enableEmitFreeze = null;
|
||||
let enableOnlyOnReactScript = false;
|
||||
let enableInferReactFunctions = false;
|
||||
|
||||
if (firstLine.indexOf("@forgetDirective") !== -1) {
|
||||
enableOnlyOnUseForgetDirective = true;
|
||||
@@ -80,6 +81,20 @@ export function transformFixtureInput(
|
||||
enableOnlyOnReactScript = true;
|
||||
language = "flow";
|
||||
}
|
||||
if (firstLine.indexOf("@enableInferReactFunctions") !== -1) {
|
||||
enableInferReactFunctions = true;
|
||||
}
|
||||
if (
|
||||
[
|
||||
enableInferReactFunctions,
|
||||
enableOnlyOnReactScript,
|
||||
enableOnlyOnUseForgetDirective,
|
||||
].filter((x) => x === true).length > 1
|
||||
) {
|
||||
throw new Error(
|
||||
"Cannot enable more than one of @enableInferReactFunctions, @enableOnlyOnReactScript, and @enableOnlyOnUseForgetDirective at once"
|
||||
);
|
||||
}
|
||||
|
||||
return pluginFn(
|
||||
input,
|
||||
@@ -111,6 +126,7 @@ export function transformFixtureInput(
|
||||
},
|
||||
enableOnlyOnUseForgetDirective,
|
||||
enableOnlyOnReactScript,
|
||||
enableInferReactFunctions,
|
||||
logger: null,
|
||||
gating,
|
||||
instrumentForget,
|
||||
|
||||
@@ -438,6 +438,13 @@ const skipFilter = new Set([
|
||||
"rules-of-hooks/rules-of-hooks-e66a744cffbe",
|
||||
"rules-of-hooks/rules-of-hooks-eacfcaa6ef89",
|
||||
"rules-of-hooks/rules-of-hooks-fe6042f7628b",
|
||||
"infer-functions-component-with-jsx",
|
||||
"infer-function-forwardRef",
|
||||
"infer-function-React-memo",
|
||||
"infer-functions-component-with-hook-call",
|
||||
"infer-functions-component-with-jsx",
|
||||
"infer-functions-hook-with-hook-call",
|
||||
"infer-functions-hook-with-jsx",
|
||||
]);
|
||||
|
||||
export default skipFilter;
|
||||
|
||||
Reference in New Issue
Block a user