From 989eaafa434f55d00473d5c2159ac335c1f066f6 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Tue, 3 Oct 2023 08:47:06 -0700 Subject: [PATCH] Support hooks/methods on the React namespace Allows using hooks/methods off of the `React` namespace, for example `React.useState(sathya)`. Thanks to the previous PR we correctly handle things like validation of hooks called via propertyload syntax. The main change here is to teach the compiler about the `React` namespace. This is a bit of a hack since we treat it as a global, but we're transforming React code so this seems reasonable (?). There are a few additional touch-ups which I'll do in subsequent PRs to make review easier. For example, we need to teach our useMemo/useCallback flattening logic to also handle the case of `React.useMemo()` etc. --- .../src/HIR/BuildHIR.ts | 14 ---- .../src/HIR/Globals.ts | 6 +- ...-existing-react-namespace-import.expect.md | 64 +++++++++++++++ ... babel-existing-react-namespace-import.js} | 11 ++- ...-existing-react-namespace-import.expect.md | 32 -------- ...error.hooks-with-React-namespace.expect.md | 19 ----- .../error.hooks-with-React-namespace.js | 4 - .../hooks-with-React-namespace.expect.md | 31 +++++++ .../compiler/hooks-with-React-namespace.js | 9 +++ .../compiler/react-namespace.expect.md | 80 +++++++++++++++++++ .../fixtures/compiler/react-namespace.js | 18 +++++ .../packages/sprout/src/shared-runtime.ts | 4 + 12 files changed, 215 insertions(+), 77 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/babel-existing-react-namespace-import.expect.md rename compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/{error.babel-existing-react-namespace-import.js => babel-existing-react-namespace-import.js} (54%) delete mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.babel-existing-react-namespace-import.expect.md delete mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.hooks-with-React-namespace.expect.md delete mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.hooks-with-React-namespace.js create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hooks-with-React-namespace.expect.md create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hooks-with-React-namespace.js create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/react-namespace.expect.md create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/react-namespace.js diff --git a/compiler/packages/babel-plugin-react-forget/src/HIR/BuildHIR.ts b/compiler/packages/babel-plugin-react-forget/src/HIR/BuildHIR.ts index 1ec3d31a5a..44883a8b28 100644 --- a/compiler/packages/babel-plugin-react-forget/src/HIR/BuildHIR.ts +++ b/compiler/packages/babel-plugin-react-forget/src/HIR/BuildHIR.ts @@ -2660,20 +2660,6 @@ function lowerMemberExpression( const object = loweredObject ?? lowerExpressionToTemporary(builder, objectNode); - if (objectNode.isIdentifier() && objectNode.node.name === "React") { - builder.errors.push({ - reason: `(BuildHIR::lowerMemberExpression) Handle loading properties from React namespace`, - severity: ErrorSeverity.Todo, - loc: propertyNode.node.loc ?? null, - suggestions: null, - }); - return { - object, - property: propertyNode.toString(), - value: { kind: "UnsupportedNode", node: exprNode, loc: exprLoc }, - }; - } - if (!expr.node.computed) { if (!propertyNode.isIdentifier()) { builder.errors.push({ diff --git a/compiler/packages/babel-plugin-react-forget/src/HIR/Globals.ts b/compiler/packages/babel-plugin-react-forget/src/HIR/Globals.ts index c8a8291486..788abd726a 100644 --- a/compiler/packages/babel-plugin-react-forget/src/HIR/Globals.ts +++ b/compiler/packages/babel-plugin-react-forget/src/HIR/Globals.ts @@ -17,7 +17,7 @@ import { addHook, addObject, } from "./ObjectShape"; -import { BuiltInType, FunctionType, PolyType } from "./Types"; +import { BuiltInType, PolyType } from "./Types"; /** * This file exports types and defaults for JavaScript global objects. @@ -234,7 +234,7 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [ // TODO(mofeiZ): We currently only store rest param effects for hooks // until FeatureFlag `enableTreatHooksAsFunctions` is removed -const BUILTIN_HOOKS: Array<[string, FunctionType]> = [ +const BUILTIN_HOOKS: Array<[string, BuiltInType]> = [ [ "useContext", addHook(DEFAULT_SHAPES, [], { @@ -314,6 +314,8 @@ const BUILTIN_HOOKS: Array<[string, FunctionType]> = [ ], ]; +TYPED_GLOBALS.push(["React", addObject(DEFAULT_SHAPES, null, BUILTIN_HOOKS)]); + export type Global = BuiltInType | PolyType; export type GlobalRegistry = Map; export const DEFAULT_GLOBALS: GlobalRegistry = new Map(BUILTIN_HOOKS); diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/babel-existing-react-namespace-import.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/babel-existing-react-namespace-import.expect.md new file mode 100644 index 0000000000..4f53132de5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/babel-existing-react-namespace-import.expect.md @@ -0,0 +1,64 @@ + +## Input + +```javascript +import * as React from "react"; +import { calculateExpensiveNumber } from "shared-runtime"; + +function Component(props) { + const [x] = React.useState(0); + const expensiveNumber = React.useMemo(() => calculateExpensiveNumber(x), [x]); + + return
{expensiveNumber}
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; + +``` + +## Code + +```javascript +import { unstable_useMemoCache as useMemoCache } from "react"; +import * as React from "react"; +import { calculateExpensiveNumber } from "shared-runtime"; + +function Component(props) { + const $ = useMemoCache(5); + const [x] = React.useState(0); + const c_0 = $[0] !== x; + let t0; + let t1; + if (c_0) { + t0 = () => calculateExpensiveNumber(x); + t1 = [x]; + $[0] = x; + $[1] = t0; + $[2] = t1; + } else { + t0 = $[1]; + t1 = $[2]; + } + const expensiveNumber = React.useMemo(t0, t1); + const c_3 = $[3] !== expensiveNumber; + let t2; + if (c_3) { + t2 =
{expensiveNumber}
; + $[3] = expensiveNumber; + $[4] = t2; + } else { + t2 = $[4]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; + +``` + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.babel-existing-react-namespace-import.js b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/babel-existing-react-namespace-import.js similarity index 54% rename from compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.babel-existing-react-namespace-import.js rename to compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/babel-existing-react-namespace-import.js index 231a979838..a706938691 100644 --- a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.babel-existing-react-namespace-import.js +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/babel-existing-react-namespace-import.js @@ -1,4 +1,5 @@ import * as React from "react"; +import { calculateExpensiveNumber } from "shared-runtime"; function Component(props) { const [x] = React.useState(0); @@ -7,9 +8,7 @@ function Component(props) { return
{expensiveNumber}
; } -function Component2(props) { - const [x] = React.useState(0); - const expensiveNumber = React.useMemo(() => calculateExpensiveNumber(x), [x]); - - return
{expensiveNumber}
; -} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.babel-existing-react-namespace-import.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.babel-existing-react-namespace-import.expect.md deleted file mode 100644 index 14c60a6ec2..0000000000 --- a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.babel-existing-react-namespace-import.expect.md +++ /dev/null @@ -1,32 +0,0 @@ - -## Input - -```javascript -import * as React from "react"; - -function Component(props) { - const [x] = React.useState(0); - const expensiveNumber = React.useMemo(() => calculateExpensiveNumber(x), [x]); - - return
{expensiveNumber}
; -} - -function Component2(props) { - const [x] = React.useState(0); - const expensiveNumber = React.useMemo(() => calculateExpensiveNumber(x), [x]); - - return
{expensiveNumber}
; -} - -``` - - -## Error - -``` -[ReactForget] Todo: (BuildHIR::lowerMemberExpression) Handle loading properties from React namespace (4:4) - -[ReactForget] Todo: (BuildHIR::lowerMemberExpression) Handle loading properties from React namespace (5:5) -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.hooks-with-React-namespace.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.hooks-with-React-namespace.expect.md deleted file mode 100644 index 6e9f92625a..0000000000 --- a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.hooks-with-React-namespace.expect.md +++ /dev/null @@ -1,19 +0,0 @@ - -## Input - -```javascript -function Foo() { - const [x, setX] = React.useState(1); - return x; -} - -``` - - -## Error - -``` -[ReactForget] Todo: (BuildHIR::lowerMemberExpression) Handle loading properties from React namespace (2:2) -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.hooks-with-React-namespace.js b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.hooks-with-React-namespace.js deleted file mode 100644 index 41a3271cee..0000000000 --- a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.hooks-with-React-namespace.js +++ /dev/null @@ -1,4 +0,0 @@ -function Foo() { - const [x, setX] = React.useState(1); - return x; -} diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hooks-with-React-namespace.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hooks-with-React-namespace.expect.md new file mode 100644 index 0000000000..0522b1ec85 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hooks-with-React-namespace.expect.md @@ -0,0 +1,31 @@ + +## Input + +```javascript +function Component() { + const [x, setX] = React.useState(1); + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; + +``` + +## Code + +```javascript +function Component() { + const [x] = React.useState(1); + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; + +``` + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hooks-with-React-namespace.js b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hooks-with-React-namespace.js new file mode 100644 index 0000000000..70f18c6cf1 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hooks-with-React-namespace.js @@ -0,0 +1,9 @@ +function Component() { + const [x, setX] = React.useState(1); + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/react-namespace.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/react-namespace.expect.md new file mode 100644 index 0000000000..6f406320f1 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/react-namespace.expect.md @@ -0,0 +1,80 @@ + +## Input + +```javascript +const FooContext = React.createContext({ current: null }); + +function Component(props) { + const foo = React.useContext(FooContext); + const ref = React.useRef(); + const [x, setX] = React.useState(false); + const onClick = () => { + setX(true); + ref.current = true; + foo.current = true; + }; + return
{React.cloneElement(props.children)}
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ children:
Hello
}], +}; + +``` + +## Code + +```javascript +import { unstable_useMemoCache as useMemoCache } from "react"; +const FooContext = React.createContext({ current: null }); + +function Component(props) { + const $ = useMemoCache(7); + const foo = React.useContext(FooContext); + const ref = React.useRef(); + const [x, setX] = React.useState(false); + const c_0 = $[0] !== foo.current; + let t0; + if (c_0) { + t0 = () => { + setX(true); + ref.current = true; + foo.current = true; + }; + $[0] = foo.current; + $[1] = t0; + } else { + t0 = $[1]; + } + const onClick = t0; + const c_2 = $[2] !== props.children; + let t1; + if (c_2) { + t1 = React.cloneElement(props.children); + $[2] = props.children; + $[3] = t1; + } else { + t1 = $[3]; + } + const c_4 = $[4] !== onClick; + const c_5 = $[5] !== t1; + let t2; + if (c_4 || c_5) { + t2 =
{t1}
; + $[4] = onClick; + $[5] = t1; + $[6] = t2; + } else { + t2 = $[6]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ children:
Hello
}], +}; + +``` + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/react-namespace.js b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/react-namespace.js new file mode 100644 index 0000000000..c11b953143 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/react-namespace.js @@ -0,0 +1,18 @@ +const FooContext = React.createContext({ current: null }); + +function Component(props) { + const foo = React.useContext(FooContext); + const ref = React.useRef(); + const [x, setX] = React.useState(false); + const onClick = () => { + setX(true); + ref.current = true; + foo.current = true; + }; + return
{React.cloneElement(props.children)}
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ children:
Hello
}], +}; diff --git a/compiler/packages/sprout/src/shared-runtime.ts b/compiler/packages/sprout/src/shared-runtime.ts index 28e541b7ae..56cb5eaa08 100644 --- a/compiler/packages/sprout/src/shared-runtime.ts +++ b/compiler/packages/sprout/src/shared-runtime.ts @@ -54,6 +54,10 @@ export function getNumber(): number { return 4; } +export function calculateExpensiveNumber(x: number): number { + return x; +} + /** * Functions that do not mutate their parameters */