[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',