@samitouri / QOS-React / commits / 68d59d43d5

[compiler][ez] Fix reanimated custom type defs for imports (#31137)

When we added support for Reanimated, we didn't distinguish between true globals (i.e. identifiers with no static resolutions), module types, and imports #29188. For the past 3-4 months, Reanimated imports were not being matched to the correct hook / function shape we match globals and module imports against two different registries. This PR fixes our support for Reanimated library functions imported under `react-native-reanimated`. See test fixtures for details

mofeiZ committed Oct 7, 2024 at 13:09 UTC 68d59d43d5640f7e44b46bfa7ee758de063767b4
9 files changed +149 -16
compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
+8 -5
@@ -16,7 +16,7 @@ import {
16 DEFAULT_SHAPES,
17 Global,
18 GlobalRegistry,
19 - installReAnimatedTypes,
19 + getReanimatedModuleType,
20 installTypeConfig,
21 } from './Globals';
22 import {
@@ -688,7 +688,8 @@ export class Environment {
688 }
689
690 if (config.enableCustomTypeDefinitionForReanimated) {
691 - installReAnimatedTypes(this.#globals, this.#shapes);
691 + const reanimatedModuleType = getReanimatedModuleType(this.#shapes);
692 + this.#moduleTypes.set(REANIMATED_MODULE_NAME, reanimatedModuleType);
693 }
694
695 this.#contextIdentifiers = contextIdentifiers;
@@ -734,11 +735,11 @@ export class Environment {
735 }
736
737 #resolveModuleType(moduleName: string, loc: SourceLocation): Global | null {
737 - if (this.config.moduleTypeProvider == null) {
738 - return null;
739 - }
738 let moduleType = this.#moduleTypes.get(moduleName);
739 if (moduleType === undefined) {
740 + if (this.config.moduleTypeProvider == null) {
741 + return null;
742 + }
743 const unparsedModuleConfig = this.config.moduleTypeProvider(moduleName);
744 if (unparsedModuleConfig != null) {
745 const parsedModuleConfig = TypeSchema.safeParse(unparsedModuleConfig);
@@ -957,6 +958,8 @@ export class Environment {
958 }
959 }
960
961 +const REANIMATED_MODULE_NAME = 'react-native-reanimated';
962 +
963 // From https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js#LL18C1-L23C2
964 export function isHookName(name: string): boolean {
965 return /^use[A-Z0-9]/.test(name);
compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts
+11 -11
@@ -25,7 +25,7 @@ import {
25 addHook,
26 addObject,
27 } from './ObjectShape';
28 -import {BuiltInType, PolyType} from './Types';
28 +import {BuiltInType, ObjectType, PolyType} from './Types';
29 import {TypeConfig} from './TypeSchema';
30 import {assertExhaustive} from '../Utils/utils';
31 import {isHookName} from './Environment';
@@ -652,10 +652,7 @@ export function installTypeConfig(
652 }
653 }
654
655 -export function installReAnimatedTypes(
656 - globals: GlobalRegistry,
657 - registry: ShapeRegistry,
658 -): void {
655 +export function getReanimatedModuleType(registry: ShapeRegistry): ObjectType {
656 // hooks that freeze args and return frozen value
657 const frozenHooks = [
658 'useFrameCallback',
@@ -665,8 +662,9 @@ export function installReAnimatedTypes(
662 'useAnimatedReaction',
663 'useWorkletCallback',
664 ];
665 + const reanimatedType: Array<[string, BuiltInType]> = [];
666 for (const hook of frozenHooks) {
669 - globals.set(
667 + reanimatedType.push([
668 hook,
669 addHook(registry, {
670 positionalParams: [],
@@ -677,7 +675,7 @@ export function installReAnimatedTypes(
675 calleeEffect: Effect.Read,
676 hookKind: 'Custom',
677 }),
680 - );
678 + ]);
679 }
680
681 /**
@@ -686,7 +684,7 @@ export function installReAnimatedTypes(
684 */
685 const mutableHooks = ['useSharedValue', 'useDerivedValue'];
686 for (const hook of mutableHooks) {
689 - globals.set(
687 + reanimatedType.push([
688 hook,
689 addHook(registry, {
690 positionalParams: [],
@@ -697,7 +695,7 @@ export function installReAnimatedTypes(
695 calleeEffect: Effect.Read,
696 hookKind: 'Custom',
697 }),
700 - );
698 + ]);
699 }
700
701 // functions that return mutable value
@@ -711,7 +709,7 @@ export function installReAnimatedTypes(
709 'executeOnUIRuntimeSync',
710 ];
711 for (const fn of funcs) {
714 - globals.set(
712 + reanimatedType.push([
713 fn,
714 addFunction(registry, [], {
715 positionalParams: [],
@@ -721,6 +719,8 @@ export function installReAnimatedTypes(
719 returnValueKind: ValueKind.Mutable,
720 noAlias: true,
721 }),
724 - );
722 + ]);
723 }
724 +
725 + return addObject(registry, null, reanimatedType);
726 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-non-imported-reanimated-shared-value-writes.expect.md new
+36
@@ -0,0 +1,36 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableCustomTypeDefinitionForReanimated
6 +
7 +/**
8 + * Test that a global (i.e. non-imported) useSharedValue is treated as an
9 + * unknown hook.
10 + */
11 +function SomeComponent() {
12 + const sharedVal = useSharedValue(0);
13 + return (
14 + <Button
15 + onPress={() => (sharedVal.value = Math.random())}
16 + title="Randomize"
17 + />
18 + );
19 +}
20 +
21 +```
22 +
23 +
24 +## Error
25 +
26 +```
27 + 9 | return (
28 + 10 | <Button
29 +> 11 | onPress={() => (sharedVal.value = Math.random())}
30 + | ^^^^^^^^^ InvalidReact: Mutating a value returned from a function whose return value should not be mutated. Found mutation of `sharedVal` (11:11)
31 + 12 | title="Randomize"
32 + 13 | />
33 + 14 | );
34 +```
35 +
36 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-non-imported-reanimated-shared-value-writes.jsx new
+15
@@ -0,0 +1,15 @@
1 +// @enableCustomTypeDefinitionForReanimated
2 +
3 +/**
4 + * Test that a global (i.e. non-imported) useSharedValue is treated as an
5 + * unknown hook.
6 + */
7 +function SomeComponent() {
8 + const sharedVal = useSharedValue(0);
9 + return (
10 + <Button
11 + onPress={() => (sharedVal.value = Math.random())}
12 + title="Randomize"
13 + />
14 + );
15 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reanimated-no-memo-arg.expect.md
+2
@@ -3,6 +3,7 @@
3
4 ```javascript
5 // @enableCustomTypeDefinitionForReanimated
6 +import {useAnimatedProps} from 'react-native-reanimated';
7 function Component() {
8 const radius = useSharedValue(50);
9
@@ -38,6 +39,7 @@ export const FIXTURE_ENTRYPOINT = {
39
40 ```javascript
41 import { c as _c } from "react/compiler-runtime"; // @enableCustomTypeDefinitionForReanimated
42 +import { useAnimatedProps } from "react-native-reanimated";
43 function Component() {
44 const $ = _c(2);
45 const radius = useSharedValue(50);
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reanimated-no-memo-arg.js
+1
@@ -1,4 +1,5 @@
1 // @enableCustomTypeDefinitionForReanimated
2 +import {useAnimatedProps} from 'react-native-reanimated';
3 function Component() {
4 const radius = useSharedValue(50);
5
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reanimated-shared-value-writes.expect.md new
+57
@@ -0,0 +1,57 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableCustomTypeDefinitionForReanimated
6 +import {useSharedValue} from 'react-native-reanimated';
7 +
8 +/**
9 + * https://docs.swmansion.com/react-native-reanimated/docs/2.x/api/hooks/useSharedValue/
10 + *
11 + * Test that shared values are treated as ref-like, i.e. allowing writes outside
12 + * of render
13 + */
14 +function SomeComponent() {
15 + const sharedVal = useSharedValue(0);
16 + return (
17 + <Button
18 + onPress={() => (sharedVal.value = Math.random())}
19 + title="Randomize"
20 + />
21 + );
22 +}
23 +
24 +```
25 +
26 +## Code
27 +
28 +```javascript
29 +import { c as _c } from "react/compiler-runtime"; // @enableCustomTypeDefinitionForReanimated
30 +import { useSharedValue } from "react-native-reanimated";
31 +
32 +/**
33 + * https://docs.swmansion.com/react-native-reanimated/docs/2.x/api/hooks/useSharedValue/
34 + *
35 + * Test that shared values are treated as ref-like, i.e. allowing writes outside
36 + * of render
37 + */
38 +function SomeComponent() {
39 + const $ = _c(3);
40 + const sharedVal = useSharedValue(0);
41 +
42 + const T0 = Button;
43 + const t0 = () => (sharedVal.value = Math.random());
44 + let t1;
45 + if ($[0] !== T0 || $[1] !== t0) {
46 + t1 = <T0 onPress={t0} title="Randomize" />;
47 + $[0] = T0;
48 + $[1] = t0;
49 + $[2] = t1;
50 + } else {
51 + t1 = $[2];
52 + }
53 + return t1;
54 +}
55 +
56 +```
57 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reanimated-shared-value-writes.jsx new
+18
@@ -0,0 +1,18 @@
1 +// @enableCustomTypeDefinitionForReanimated
2 +import {useSharedValue} from 'react-native-reanimated';
3 +
4 +/**
5 + * https://docs.swmansion.com/react-native-reanimated/docs/2.x/api/hooks/useSharedValue/
6 + *
7 + * Test that shared values are treated as ref-like, i.e. allowing writes outside
8 + * of render
9 + */
10 +function SomeComponent() {
11 + const sharedVal = useSharedValue(0);
12 + return (
13 + <Button
14 + onPress={() => (sharedVal.value = Math.random())}
15 + title="Randomize"
16 + />
17 + );
18 +}
compiler/packages/snap/src/SproutTodoFilter.ts
+1
@@ -434,6 +434,7 @@ const skipFilter = new Set([
434 'todo.useContext-mutate-context-in-callback',
435 'loop-unused-let',
436 'reanimated-no-memo-arg',
437 + 'reanimated-shared-value-writes',
438
439 'userspace-use-memo-cache',
440 'transitive-freeze-function-expressions',