Skip to content

Commit dfad575

Browse files
committed
[compiler] Validate type configs for hooks/non-hooks
Alternative to #30868. The goal is to ensure that the types coming out of moduleTypeProvider are valid wrt to hook typing. If something is named like a hook, then it must be typed as a hook (or don't type it). ghstack-source-id: fc389f3 Pull Request resolved: #30888
1 parent 4c58fce commit dfad575

8 files changed

+209
-54
lines changed

compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
Type,
3434
ValidatedIdentifier,
3535
ValueKind,
36+
getHookKindForType,
3637
makeBlockId,
3738
makeIdentifierId,
3839
makeIdentifierName,
@@ -794,6 +795,21 @@ export class Environment {
794795
binding.imported,
795796
);
796797
if (importedType != null) {
798+
/*
799+
* Check that hook-like export names are hook types, and non-hook names are non-hook types.
800+
* The user-assigned alias isn't decidable by the type provider, so we ignore that for the check.
801+
* Thus we allow `import {fooNonHook as useFoo} from ...` because the name and type both say
802+
* that it's not a hook.
803+
*/
804+
const expectHook = isHookName(binding.imported);
805+
const isHook = getHookKindForType(this, importedType) != null;
806+
if (expectHook !== isHook) {
807+
CompilerError.throwInvalidConfig({
808+
reason: `Invalid type configuration for module`,
809+
description: `Expected type for \`import {${binding.imported}} from '${binding.module}'\` ${expectHook ? 'to be a hook' : 'not to be a hook'} based on the exported name`,
810+
loc,
811+
});
812+
}
797813
return importedType;
798814
}
799815
}
@@ -822,13 +838,30 @@ export class Environment {
822838
} else {
823839
const moduleType = this.#resolveModuleType(binding.module, loc);
824840
if (moduleType !== null) {
841+
let importedType: Type | null = null;
825842
if (binding.kind === 'ImportDefault') {
826843
const defaultType = this.getPropertyType(moduleType, 'default');
827844
if (defaultType !== null) {
828-
return defaultType;
845+
importedType = defaultType;
829846
}
830847
} else {
831-
return moduleType;
848+
importedType = moduleType;
849+
}
850+
if (importedType !== null) {
851+
/*
852+
* Check that the hook-like modules are defined as types, and non hook-like modules are not typed as hooks.
853+
* So `import Foo from 'useFoo'` is expected to be a hook based on the module name
854+
*/
855+
const expectHook = isHookName(binding.module);
856+
const isHook = getHookKindForType(this, importedType) != null;
857+
if (expectHook !== isHook) {
858+
CompilerError.throwInvalidConfig({
859+
reason: `Invalid type configuration for module`,
860+
description: `Expected type for \`import ... from '${binding.module}'\` ${expectHook ? 'to be a hook' : 'not to be a hook'} based on the module name`,
861+
loc,
862+
});
863+
}
864+
return importedType;
832865
}
833866
}
834867
return isHookName(binding.name) ? this.#getCustomHookType() : null;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
## Input
3+
4+
```javascript
5+
import {useHookNotTypedAsHook} from 'ReactCompilerTest';
6+
7+
function Component() {
8+
return useHookNotTypedAsHook();
9+
}
10+
11+
```
12+
13+
14+
## Error
15+
16+
```
17+
2 |
18+
3 | function Component() {
19+
> 4 | return useHookNotTypedAsHook();
20+
| ^^^^^^^^^^^^^^^^^^^^^ InvalidConfig: Invalid type configuration for module. Expected type for `import {useHookNotTypedAsHook} from 'ReactCompilerTest'` to be a hook based on the exported name (4:4)
21+
5 | }
22+
6 |
23+
```
24+
25+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import {useHookNotTypedAsHook} from 'ReactCompilerTest';
2+
3+
function Component() {
4+
return useHookNotTypedAsHook();
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
## Input
3+
4+
```javascript
5+
import foo from 'useDefaultExportNotTypedAsHook';
6+
7+
function Component() {
8+
return <div>{foo()}</div>;
9+
}
10+
11+
```
12+
13+
14+
## Error
15+
16+
```
17+
2 |
18+
3 | function Component() {
19+
> 4 | return <div>{foo()}</div>;
20+
| ^^^ InvalidConfig: Invalid type configuration for module. Expected type for `import ... from 'useDefaultExportNotTypedAsHook'` to be a hook based on the module name (4:4)
21+
5 | }
22+
6 |
23+
```
24+
25+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import foo from 'useDefaultExportNotTypedAsHook';
2+
3+
function Component() {
4+
return <div>{foo()}</div>;
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
## Input
3+
4+
```javascript
5+
import {notAhookTypedAsHook} from 'ReactCompilerTest';
6+
7+
function Component() {
8+
return <div>{notAhookTypedAsHook()}</div>;
9+
}
10+
11+
```
12+
13+
14+
## Error
15+
16+
```
17+
2 |
18+
3 | function Component() {
19+
> 4 | return <div>{notAhookTypedAsHook()}</div>;
20+
| ^^^^^^^^^^^^^^^^^^^ InvalidConfig: Invalid type configuration for module. Expected type for `import {notAhookTypedAsHook} from 'ReactCompilerTest'` not to be a hook based on the exported name (4:4)
21+
5 | }
22+
6 |
23+
```
24+
25+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import {notAhookTypedAsHook} from 'ReactCompilerTest';
2+
3+
function Component() {
4+
return <div>{notAhookTypedAsHook()}</div>;
5+
}

compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts

Lines changed: 84 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -18,60 +18,92 @@ export function makeSharedRuntimeTypeProvider({
1818
return function sharedRuntimeTypeProvider(
1919
moduleName: string,
2020
): TypeConfig | null {
21-
if (moduleName !== 'shared-runtime') {
22-
return null;
23-
}
24-
return {
25-
kind: 'object',
26-
properties: {
27-
default: {
28-
kind: 'function',
29-
calleeEffect: EffectEnum.Read,
30-
positionalParams: [],
31-
restParam: EffectEnum.Read,
32-
returnType: {kind: 'type', name: 'Primitive'},
33-
returnValueKind: ValueKindEnum.Primitive,
34-
},
35-
graphql: {
36-
kind: 'function',
37-
calleeEffect: EffectEnum.Read,
38-
positionalParams: [],
39-
restParam: EffectEnum.Read,
40-
returnType: {kind: 'type', name: 'Primitive'},
41-
returnValueKind: ValueKindEnum.Primitive,
42-
},
43-
typedArrayPush: {
44-
kind: 'function',
45-
calleeEffect: EffectEnum.Read,
46-
positionalParams: [EffectEnum.Store, EffectEnum.Capture],
47-
restParam: EffectEnum.Capture,
48-
returnType: {kind: 'type', name: 'Primitive'},
49-
returnValueKind: ValueKindEnum.Primitive,
21+
if (moduleName === 'shared-runtime') {
22+
return {
23+
kind: 'object',
24+
properties: {
25+
default: {
26+
kind: 'function',
27+
calleeEffect: EffectEnum.Read,
28+
positionalParams: [],
29+
restParam: EffectEnum.Read,
30+
returnType: {kind: 'type', name: 'Primitive'},
31+
returnValueKind: ValueKindEnum.Primitive,
32+
},
33+
graphql: {
34+
kind: 'function',
35+
calleeEffect: EffectEnum.Read,
36+
positionalParams: [],
37+
restParam: EffectEnum.Read,
38+
returnType: {kind: 'type', name: 'Primitive'},
39+
returnValueKind: ValueKindEnum.Primitive,
40+
},
41+
typedArrayPush: {
42+
kind: 'function',
43+
calleeEffect: EffectEnum.Read,
44+
positionalParams: [EffectEnum.Store, EffectEnum.Capture],
45+
restParam: EffectEnum.Capture,
46+
returnType: {kind: 'type', name: 'Primitive'},
47+
returnValueKind: ValueKindEnum.Primitive,
48+
},
49+
typedLog: {
50+
kind: 'function',
51+
calleeEffect: EffectEnum.Read,
52+
positionalParams: [],
53+
restParam: EffectEnum.Read,
54+
returnType: {kind: 'type', name: 'Primitive'},
55+
returnValueKind: ValueKindEnum.Primitive,
56+
},
57+
useFreeze: {
58+
kind: 'hook',
59+
returnType: {kind: 'type', name: 'Any'},
60+
},
61+
useFragment: {
62+
kind: 'hook',
63+
returnType: {kind: 'type', name: 'MixedReadonly'},
64+
noAlias: true,
65+
},
66+
useNoAlias: {
67+
kind: 'hook',
68+
returnType: {kind: 'type', name: 'Any'},
69+
returnValueKind: ValueKindEnum.Mutable,
70+
noAlias: true,
71+
},
5072
},
51-
typedLog: {
52-
kind: 'function',
53-
calleeEffect: EffectEnum.Read,
54-
positionalParams: [],
55-
restParam: EffectEnum.Read,
56-
returnType: {kind: 'type', name: 'Primitive'},
57-
returnValueKind: ValueKindEnum.Primitive,
73+
};
74+
} else if (moduleName === 'ReactCompilerTest') {
75+
/**
76+
* Fake module used for testing validation that type providers return hook
77+
* types for hook names and non-hook types for non-hook names
78+
*/
79+
return {
80+
kind: 'object',
81+
properties: {
82+
useHookNotTypedAsHook: {
83+
kind: 'type',
84+
name: 'Any',
85+
},
86+
notAhookTypedAsHook: {
87+
kind: 'hook',
88+
returnType: {kind: 'type', name: 'Any'},
89+
},
5890
},
59-
useFreeze: {
60-
kind: 'hook',
61-
returnType: {kind: 'type', name: 'Any'},
91+
};
92+
} else if (moduleName === 'useDefaultExportNotTypedAsHook') {
93+
/**
94+
* Fake module used for testing validation that type providers return hook
95+
* types for hook names and non-hook types for non-hook names
96+
*/
97+
return {
98+
kind: 'object',
99+
properties: {
100+
default: {
101+
kind: 'type',
102+
name: 'Any',
103+
},
62104
},
63-
useFragment: {
64-
kind: 'hook',
65-
returnType: {kind: 'type', name: 'MixedReadonly'},
66-
noAlias: true,
67-
},
68-
useNoAlias: {
69-
kind: 'hook',
70-
returnType: {kind: 'type', name: 'Any'},
71-
returnValueKind: ValueKindEnum.Mutable,
72-
noAlias: true,
73-
},
74-
},
75-
};
105+
};
106+
}
107+
return null;
76108
};
77109
}

0 commit comments

Comments
 (0)