diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index 2b007f9659b99..4134fadf1d7d7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -33,6 +33,7 @@ import { Type, ValidatedIdentifier, ValueKind, + getHookKindForType, makeBlockId, makeIdentifierId, makeIdentifierName, @@ -788,14 +789,9 @@ export class Environment { ); } else { const moduleType = this.#resolveModuleType(binding.module, loc); + let propertyType: Type | null = null; if (moduleType !== null) { - const importedType = this.getPropertyType( - moduleType, - binding.imported, - ); - if (importedType != null) { - return importedType; - } + propertyType = this.getPropertyType(moduleType, binding.imported); } /** @@ -806,9 +802,18 @@ export class Environment { * `import {useHook as foo} ...` * `import {foo as useHook} ...` */ - return isHookName(binding.imported) || isHookName(binding.name) - ? this.#getCustomHookType() - : null; + const expectHook = + isHookName(binding.imported) || isHookName(binding.name); + if (expectHook) { + if ( + propertyType && + getHookKindForType(this, propertyType) !== null + ) { + return propertyType; + } + return this.#getCustomHookType(); + } + return propertyType; } } case 'ImportDefault': @@ -821,17 +826,27 @@ export class Environment { ); } else { const moduleType = this.#resolveModuleType(binding.module, loc); + let importedType: Type | null = null; if (moduleType !== null) { if (binding.kind === 'ImportDefault') { const defaultType = this.getPropertyType(moduleType, 'default'); if (defaultType !== null) { - return defaultType; + importedType = defaultType; } } else { - return moduleType; + importedType = moduleType; + } + } + if (isHookName(binding.name)) { + if ( + importedType !== null && + getHookKindForType(this, importedType) !== null + ) { + return importedType; } + return this.#getCustomHookType(); } - return isHookName(binding.name) ? this.#getCustomHookType() : null; + return importedType; } } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-hooklike-name-not-typed-as-hook-import.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-hooklike-name-not-typed-as-hook-import.expect.md new file mode 100644 index 0000000000000..297056ffb7780 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-hooklike-name-not-typed-as-hook-import.expect.md @@ -0,0 +1,123 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import { + useArrayConcatNotTypedAsHook, + ValidateMemoization, +} from 'shared-runtime'; + +export function Component({a, b}) { + const item1 = useMemo(() => [a], [a]); + const item2 = useMemo(() => [b], [b]); + const item3 = useArrayConcatNotTypedAsHook(item1, item2); + + return ( + <> + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 1, b: 0}, + {a: 1, b: 1}, + {a: 1, b: 2}, + {a: 2, b: 2}, + {a: 3, b: 2}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import { + useArrayConcatNotTypedAsHook, + ValidateMemoization, +} from "shared-runtime"; + +export function Component(t0) { + const $ = _c(10); + const { a, b } = t0; + let t1; + let t2; + if ($[0] !== a) { + t2 = [a]; + $[0] = a; + $[1] = t2; + } else { + t2 = $[1]; + } + t1 = t2; + const item1 = t1; + let t3; + let t4; + if ($[2] !== b) { + t4 = [b]; + $[2] = b; + $[3] = t4; + } else { + t4 = $[3]; + } + t3 = t4; + const item2 = t3; + const item3 = useArrayConcatNotTypedAsHook(item1, item2); + let t5; + if ($[4] !== a || $[5] !== b) { + t5 = [a, b]; + $[4] = a; + $[5] = b; + $[6] = t5; + } else { + t5 = $[6]; + } + let t6; + if ($[7] !== t5 || $[8] !== item3) { + t6 = ( + <> + + + ); + $[7] = t5; + $[8] = item3; + $[9] = t6; + } else { + t6 = $[9]; + } + return t6; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 1, b: 0 }, + { a: 1, b: 1 }, + { a: 1, b: 2 }, + { a: 2, b: 2 }, + { a: 3, b: 2 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0,0],"output":[0,0]}
+
{"inputs":[1,0],"output":[1,0]}
+
{"inputs":[1,1],"output":[1,1]}
+
{"inputs":[1,2],"output":[1,2]}
+
{"inputs":[2,2],"output":[2,2]}
+
{"inputs":[3,2],"output":[3,2]}
+
{"inputs":[0,0],"output":[0,0]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-hooklike-name-not-typed-as-hook-import.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-hooklike-name-not-typed-as-hook-import.tsx new file mode 100644 index 0000000000000..8cd065b228fc7 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-hooklike-name-not-typed-as-hook-import.tsx @@ -0,0 +1,31 @@ +import {useMemo} from 'react'; +import { + useArrayConcatNotTypedAsHook, + ValidateMemoization, +} from 'shared-runtime'; + +export function Component({a, b}) { + const item1 = useMemo(() => [a], [a]); + const item2 = useMemo(() => [b], [b]); + const item3 = useArrayConcatNotTypedAsHook(item1, item2); + + return ( + <> + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 1, b: 0}, + {a: 1, b: 1}, + {a: 1, b: 2}, + {a: 2, b: 2}, + {a: 3, b: 2}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-import-object-typed-module-as-hook-name.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-import-object-typed-module-as-hook-name.expect.md new file mode 100644 index 0000000000000..7ef2ee7e26fb9 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-import-object-typed-module-as-hook-name.expect.md @@ -0,0 +1,145 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import useHook from 'shared-runtime'; + +export function Component({a, b}) { + const item1 = useMemo(() => ({a}), [a]); + const item2 = useMemo(() => ({b}), [b]); + useHook(item1, item2); + + return ( + <> + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 1, b: 0}, + {a: 1, b: 1}, + {a: 1, b: 2}, + {a: 2, b: 2}, + {a: 3, b: 2}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import useHook from "shared-runtime"; + +export function Component(t0) { + const $ = _c(17); + const { a, b } = t0; + let t1; + let t2; + if ($[0] !== a) { + t2 = { a }; + $[0] = a; + $[1] = t2; + } else { + t2 = $[1]; + } + t1 = t2; + const item1 = t1; + let t3; + let t4; + if ($[2] !== b) { + t4 = { b }; + $[2] = b; + $[3] = t4; + } else { + t4 = $[3]; + } + t3 = t4; + const item2 = t3; + useHook(item1, item2); + let t5; + if ($[4] !== a) { + t5 = [a]; + $[4] = a; + $[5] = t5; + } else { + t5 = $[5]; + } + let t6; + if ($[6] !== t5 || $[7] !== item1) { + t6 = ; + $[6] = t5; + $[7] = item1; + $[8] = t6; + } else { + t6 = $[8]; + } + let t7; + if ($[9] !== b) { + t7 = [b]; + $[9] = b; + $[10] = t7; + } else { + t7 = $[10]; + } + let t8; + if ($[11] !== t7 || $[12] !== item2) { + t8 = ; + $[11] = t7; + $[12] = item2; + $[13] = t8; + } else { + t8 = $[13]; + } + let t9; + if ($[14] !== t6 || $[15] !== t8) { + t9 = ( + <> + {t6} + {t8} + + ); + $[14] = t6; + $[15] = t8; + $[16] = t9; + } else { + t9 = $[16]; + } + return t9; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 1, b: 0 }, + { a: 1, b: 1 }, + { a: 1, b: 2 }, + { a: 2, b: 2 }, + { a: 3, b: 2 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok) [[ (exception in render) ReferenceError: ValidateMemoization is not defined ]] +[[ (exception in render) ReferenceError: ValidateMemoization is not defined ]] +[[ (exception in render) ReferenceError: ValidateMemoization is not defined ]] +[[ (exception in render) ReferenceError: ValidateMemoization is not defined ]] +[[ (exception in render) ReferenceError: ValidateMemoization is not defined ]] +[[ (exception in render) ReferenceError: ValidateMemoization is not defined ]] +[[ (exception in render) ReferenceError: ValidateMemoization is not defined ]] +logs: [{ a: 0 },{ b: 0 },{ a: 0 },{ b: 0 },{ a: 1 },{ b: 1 },{ a: 1 },{ b: 2 },{ a: 2 },{ b: 2 },{ a: 3 },{ b: 2 },{ a: 0 },{ b: 0 }] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-import-object-typed-module-as-hook-name.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-import-object-typed-module-as-hook-name.js new file mode 100644 index 0000000000000..fb2eb66dee8b2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-import-object-typed-module-as-hook-name.js @@ -0,0 +1,29 @@ +import {useMemo} from 'react'; +import useHook from 'shared-runtime'; + +export function Component({a, b}) { + const item1 = useMemo(() => ({a}), [a]); + const item2 = useMemo(() => ({b}), [b]); + useHook(item1, item2); + + return ( + <> + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 1, b: 0}, + {a: 1, b: 1}, + {a: 1, b: 2}, + {a: 2, b: 2}, + {a: 3, b: 2}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-unknown-import-from-typed-module.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-unknown-import-from-typed-module.expect.md new file mode 100644 index 0000000000000..3cf8a9f6e3e63 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-unknown-import-from-typed-module.expect.md @@ -0,0 +1,144 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import {useHook, ValidateMemoization} from 'shared-runtime'; + +export function Component({a, b}) { + const item1 = useMemo(() => ({a}), [a]); + const item2 = useMemo(() => ({b}), [b]); + useHook(item1, item2); + + return ( + <> + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 1, b: 0}, + {a: 1, b: 1}, + {a: 1, b: 2}, + {a: 2, b: 2}, + {a: 3, b: 2}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import { useHook, ValidateMemoization } from "shared-runtime"; + +export function Component(t0) { + const $ = _c(17); + const { a, b } = t0; + let t1; + let t2; + if ($[0] !== a) { + t2 = { a }; + $[0] = a; + $[1] = t2; + } else { + t2 = $[1]; + } + t1 = t2; + const item1 = t1; + let t3; + let t4; + if ($[2] !== b) { + t4 = { b }; + $[2] = b; + $[3] = t4; + } else { + t4 = $[3]; + } + t3 = t4; + const item2 = t3; + useHook(item1, item2); + let t5; + if ($[4] !== a) { + t5 = [a]; + $[4] = a; + $[5] = t5; + } else { + t5 = $[5]; + } + let t6; + if ($[6] !== t5 || $[7] !== item1) { + t6 = ; + $[6] = t5; + $[7] = item1; + $[8] = t6; + } else { + t6 = $[8]; + } + let t7; + if ($[9] !== b) { + t7 = [b]; + $[9] = b; + $[10] = t7; + } else { + t7 = $[10]; + } + let t8; + if ($[11] !== t7 || $[12] !== item2) { + t8 = ; + $[11] = t7; + $[12] = item2; + $[13] = t8; + } else { + t8 = $[13]; + } + let t9; + if ($[14] !== t6 || $[15] !== t8) { + t9 = ( + <> + {t6} + {t8} + + ); + $[14] = t6; + $[15] = t8; + $[16] = t9; + } else { + t9 = $[16]; + } + return t9; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 1, b: 0 }, + { a: 1, b: 1 }, + { a: 1, b: 2 }, + { a: 2, b: 2 }, + { a: 3, b: 2 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0],"output":{"a":0}}
{"inputs":[0],"output":{"b":0}}
+
{"inputs":[1],"output":{"a":1}}
{"inputs":[0],"output":{"b":0}}
+
{"inputs":[1],"output":{"a":1}}
{"inputs":[1],"output":{"b":1}}
+
{"inputs":[1],"output":{"a":1}}
{"inputs":[2],"output":{"b":2}}
+
{"inputs":[2],"output":{"a":2}}
{"inputs":[2],"output":{"b":2}}
+
{"inputs":[3],"output":{"a":3}}
{"inputs":[2],"output":{"b":2}}
+
{"inputs":[0],"output":{"a":0}}
{"inputs":[0],"output":{"b":0}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-unknown-import-from-typed-module.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-unknown-import-from-typed-module.js new file mode 100644 index 0000000000000..96902ffacfad5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-unknown-import-from-typed-module.js @@ -0,0 +1,29 @@ +import {useMemo} from 'react'; +import {useHook, ValidateMemoization} from 'shared-runtime'; + +export function Component({a, b}) { + const item1 = useMemo(() => ({a}), [a]); + const item2 = useMemo(() => ({b}), [b]); + useHook(item1, item2); + + return ( + <> + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 1, b: 0}, + {a: 1, b: 1}, + {a: 1, b: 2}, + {a: 2, b: 2}, + {a: 3, b: 2}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts b/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts index fb0877d11474f..9a07902dbc2f5 100644 --- a/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts +++ b/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts @@ -48,6 +48,14 @@ export function makeSharedRuntimeTypeProvider({ returnType: {kind: 'type', name: 'Primitive'}, returnValueKind: ValueKindEnum.Primitive, }, + useArrayConcatNotTypedAsHook: { + kind: 'function', + calleeEffect: EffectEnum.Read, + positionalParams: [], + restParam: EffectEnum.Capture, + returnType: {kind: 'type', name: 'Array'}, + returnValueKind: ValueKindEnum.Primitive, + }, useFreeze: { kind: 'hook', returnType: {kind: 'type', name: 'Any'}, diff --git a/compiler/packages/snap/src/sprout/shared-runtime.ts b/compiler/packages/snap/src/sprout/shared-runtime.ts index bb1c65a6574ac..5b64e5ab46fb9 100644 --- a/compiler/packages/snap/src/sprout/shared-runtime.ts +++ b/compiler/packages/snap/src/sprout/shared-runtime.ts @@ -6,7 +6,7 @@ */ import {IntlVariations, IntlViewerContext, init} from 'fbt'; -import React, {FunctionComponent} from 'react'; +import React, {FunctionComponent, useMemo} from 'react'; /** * This file is meant for use by `runner-evaluator` and fixture tests. @@ -355,4 +355,14 @@ export function typedArrayPush(array: Array, item: T): void { export function typedLog(...values: Array): void { console.log(...values); } + +export function useArrayConcatNotTypedAsHook( + array1: Array, + array2: Array, +): Array { + return useMemo(() => { + return [...array1, ...array2]; + }, [array1, array2]); +} + export default typedLog;