mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
[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: 3e8b5a0a7010d0c484bbb417fb258e76bf4e32bc Pull Request resolved: https://github.com/facebook/react/pull/30888
This commit is contained in:
@@ -33,6 +33,7 @@ import {
|
||||
Type,
|
||||
ValidatedIdentifier,
|
||||
ValueKind,
|
||||
getHookKindForType,
|
||||
makeBlockId,
|
||||
makeIdentifierId,
|
||||
makeIdentifierName,
|
||||
@@ -737,6 +738,8 @@ export class Environment {
|
||||
this.#globals,
|
||||
this.#shapes,
|
||||
moduleConfig,
|
||||
moduleName,
|
||||
loc,
|
||||
);
|
||||
} else {
|
||||
moduleType = null;
|
||||
@@ -794,6 +797,21 @@ export class Environment {
|
||||
binding.imported,
|
||||
);
|
||||
if (importedType != null) {
|
||||
/*
|
||||
* Check that hook-like export names are hook types, and non-hook names are non-hook types.
|
||||
* The user-assigned alias isn't decidable by the type provider, so we ignore that for the check.
|
||||
* Thus we allow `import {fooNonHook as useFoo} from ...` because the name and type both say
|
||||
* that it's not a hook.
|
||||
*/
|
||||
const expectHook = isHookName(binding.imported);
|
||||
const isHook = getHookKindForType(this, importedType) != null;
|
||||
if (expectHook !== isHook) {
|
||||
CompilerError.throwInvalidConfig({
|
||||
reason: `Invalid type configuration for module`,
|
||||
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`,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return importedType;
|
||||
}
|
||||
}
|
||||
@@ -822,13 +840,30 @@ export class Environment {
|
||||
} else {
|
||||
const moduleType = this.#resolveModuleType(binding.module, loc);
|
||||
if (moduleType !== null) {
|
||||
let importedType: Type | null = null;
|
||||
if (binding.kind === 'ImportDefault') {
|
||||
const defaultType = this.getPropertyType(moduleType, 'default');
|
||||
if (defaultType !== null) {
|
||||
return defaultType;
|
||||
importedType = defaultType;
|
||||
}
|
||||
} else {
|
||||
return moduleType;
|
||||
importedType = moduleType;
|
||||
}
|
||||
if (importedType !== null) {
|
||||
/*
|
||||
* Check that the hook-like modules are defined as types, and non hook-like modules are not typed as hooks.
|
||||
* So `import Foo from 'useFoo'` is expected to be a hook based on the module name
|
||||
*/
|
||||
const expectHook = isHookName(binding.module);
|
||||
const isHook = getHookKindForType(this, importedType) != null;
|
||||
if (expectHook !== isHook) {
|
||||
CompilerError.throwInvalidConfig({
|
||||
reason: `Invalid type configuration for module`,
|
||||
description: `Expected type for \`import ... from '${binding.module}'\` ${expectHook ? 'to be a hook' : 'not to be a hook'} based on the module name`,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return importedType;
|
||||
}
|
||||
}
|
||||
return isHookName(binding.name) ? this.#getCustomHookType() : null;
|
||||
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
import {BuiltInType, PolyType} from './Types';
|
||||
import {TypeConfig} from './TypeSchema';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {isHookName} from './Environment';
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
|
||||
/*
|
||||
* This file exports types and defaults for JavaScript global objects.
|
||||
@@ -535,6 +537,8 @@ export function installTypeConfig(
|
||||
globals: GlobalRegistry,
|
||||
shapes: ShapeRegistry,
|
||||
typeConfig: TypeConfig,
|
||||
moduleName: string,
|
||||
loc: SourceLocation,
|
||||
): Global {
|
||||
switch (typeConfig.kind) {
|
||||
case 'type': {
|
||||
@@ -567,7 +571,13 @@ export function installTypeConfig(
|
||||
positionalParams: typeConfig.positionalParams,
|
||||
restParam: typeConfig.restParam,
|
||||
calleeEffect: typeConfig.calleeEffect,
|
||||
returnType: installTypeConfig(globals, shapes, typeConfig.returnType),
|
||||
returnType: installTypeConfig(
|
||||
globals,
|
||||
shapes,
|
||||
typeConfig.returnType,
|
||||
moduleName,
|
||||
loc,
|
||||
),
|
||||
returnValueKind: typeConfig.returnValueKind,
|
||||
noAlias: typeConfig.noAlias === true,
|
||||
mutableOnlyIfOperandsAreMutable:
|
||||
@@ -580,7 +590,13 @@ export function installTypeConfig(
|
||||
positionalParams: typeConfig.positionalParams ?? [],
|
||||
restParam: typeConfig.restParam ?? Effect.Freeze,
|
||||
calleeEffect: Effect.Read,
|
||||
returnType: installTypeConfig(globals, shapes, typeConfig.returnType),
|
||||
returnType: installTypeConfig(
|
||||
globals,
|
||||
shapes,
|
||||
typeConfig.returnType,
|
||||
moduleName,
|
||||
loc,
|
||||
),
|
||||
returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen,
|
||||
noAlias: typeConfig.noAlias === true,
|
||||
});
|
||||
@@ -589,10 +605,31 @@ export function installTypeConfig(
|
||||
return addObject(
|
||||
shapes,
|
||||
null,
|
||||
Object.entries(typeConfig.properties ?? {}).map(([key, value]) => [
|
||||
key,
|
||||
installTypeConfig(globals, shapes, value),
|
||||
]),
|
||||
Object.entries(typeConfig.properties ?? {}).map(([key, value]) => {
|
||||
const type = installTypeConfig(
|
||||
globals,
|
||||
shapes,
|
||||
value,
|
||||
moduleName,
|
||||
loc,
|
||||
);
|
||||
const expectHook = isHookName(key);
|
||||
let isHook = false;
|
||||
if (type.kind === 'Function' && type.shapeId !== null) {
|
||||
const functionType = shapes.get(type.shapeId);
|
||||
if (functionType?.functionType?.hookKind !== null) {
|
||||
isHook = true;
|
||||
}
|
||||
}
|
||||
if (expectHook !== isHook) {
|
||||
CompilerError.throwInvalidConfig({
|
||||
reason: `Invalid type configuration for module`,
|
||||
description: `Expected type for object property '${key}' from module '${moduleName}' ${expectHook ? 'to be a hook' : 'not to be a hook'} based on the property name`,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return [key, type];
|
||||
}),
|
||||
);
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import ReactCompilerTest from 'ReactCompilerTest';
|
||||
|
||||
function Component() {
|
||||
return ReactCompilerTest.useHookNotTypedAsHook();
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
2 |
|
||||
3 | function Component() {
|
||||
> 4 | return ReactCompilerTest.useHookNotTypedAsHook();
|
||||
| ^^^^^^^^^^^^^^^^^ InvalidConfig: Invalid type configuration for module. Expected type for object property 'useHookNotTypedAsHook' from module 'ReactCompilerTest' to be a hook based on the property name (4:4)
|
||||
5 | }
|
||||
6 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import ReactCompilerTest from 'ReactCompilerTest';
|
||||
|
||||
function Component() {
|
||||
return ReactCompilerTest.useHookNotTypedAsHook();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useHookNotTypedAsHook} from 'ReactCompilerTest';
|
||||
|
||||
function Component() {
|
||||
return useHookNotTypedAsHook();
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
2 |
|
||||
3 | function Component() {
|
||||
> 4 | return useHookNotTypedAsHook();
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ InvalidConfig: Invalid type configuration for module. Expected type for object property 'useHookNotTypedAsHook' from module 'ReactCompilerTest' to be a hook based on the property name (4:4)
|
||||
5 | }
|
||||
6 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import {useHookNotTypedAsHook} from 'ReactCompilerTest';
|
||||
|
||||
function Component() {
|
||||
return useHookNotTypedAsHook();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import foo from 'useDefaultExportNotTypedAsHook';
|
||||
|
||||
function Component() {
|
||||
return <div>{foo()}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
2 |
|
||||
3 | function Component() {
|
||||
> 4 | return <div>{foo()}</div>;
|
||||
| ^^^ InvalidConfig: Invalid type configuration for module. Expected type for `import ... from 'useDefaultExportNotTypedAsHook'` to be a hook based on the module name (4:4)
|
||||
5 | }
|
||||
6 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import foo from 'useDefaultExportNotTypedAsHook';
|
||||
|
||||
function Component() {
|
||||
return <div>{foo()}</div>;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {notAhookTypedAsHook} from 'ReactCompilerTest';
|
||||
|
||||
function Component() {
|
||||
return <div>{notAhookTypedAsHook()}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
2 |
|
||||
3 | function Component() {
|
||||
> 4 | return <div>{notAhookTypedAsHook()}</div>;
|
||||
| ^^^^^^^^^^^^^^^^^^^ InvalidConfig: Invalid type configuration for module. Expected type for object property 'useHookNotTypedAsHook' from module 'ReactCompilerTest' to be a hook based on the property name (4:4)
|
||||
5 | }
|
||||
6 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import {notAhookTypedAsHook} from 'ReactCompilerTest';
|
||||
|
||||
function Component() {
|
||||
return <div>{notAhookTypedAsHook()}</div>;
|
||||
}
|
||||
@@ -18,60 +18,92 @@ export function makeSharedRuntimeTypeProvider({
|
||||
return function sharedRuntimeTypeProvider(
|
||||
moduleName: string,
|
||||
): TypeConfig | null {
|
||||
if (moduleName !== 'shared-runtime') {
|
||||
return null;
|
||||
if (moduleName === 'shared-runtime') {
|
||||
return {
|
||||
kind: 'object',
|
||||
properties: {
|
||||
default: {
|
||||
kind: 'function',
|
||||
calleeEffect: EffectEnum.Read,
|
||||
positionalParams: [],
|
||||
restParam: EffectEnum.Read,
|
||||
returnType: {kind: 'type', name: 'Primitive'},
|
||||
returnValueKind: ValueKindEnum.Primitive,
|
||||
},
|
||||
graphql: {
|
||||
kind: 'function',
|
||||
calleeEffect: EffectEnum.Read,
|
||||
positionalParams: [],
|
||||
restParam: EffectEnum.Read,
|
||||
returnType: {kind: 'type', name: 'Primitive'},
|
||||
returnValueKind: ValueKindEnum.Primitive,
|
||||
},
|
||||
typedArrayPush: {
|
||||
kind: 'function',
|
||||
calleeEffect: EffectEnum.Read,
|
||||
positionalParams: [EffectEnum.Store, EffectEnum.Capture],
|
||||
restParam: EffectEnum.Capture,
|
||||
returnType: {kind: 'type', name: 'Primitive'},
|
||||
returnValueKind: ValueKindEnum.Primitive,
|
||||
},
|
||||
typedLog: {
|
||||
kind: 'function',
|
||||
calleeEffect: EffectEnum.Read,
|
||||
positionalParams: [],
|
||||
restParam: EffectEnum.Read,
|
||||
returnType: {kind: 'type', name: 'Primitive'},
|
||||
returnValueKind: ValueKindEnum.Primitive,
|
||||
},
|
||||
useFreeze: {
|
||||
kind: 'hook',
|
||||
returnType: {kind: 'type', name: 'Any'},
|
||||
},
|
||||
useFragment: {
|
||||
kind: 'hook',
|
||||
returnType: {kind: 'type', name: 'MixedReadonly'},
|
||||
noAlias: true,
|
||||
},
|
||||
useNoAlias: {
|
||||
kind: 'hook',
|
||||
returnType: {kind: 'type', name: 'Any'},
|
||||
returnValueKind: ValueKindEnum.Mutable,
|
||||
noAlias: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (moduleName === 'ReactCompilerTest') {
|
||||
/**
|
||||
* Fake module used for testing validation that type providers return hook
|
||||
* types for hook names and non-hook types for non-hook names
|
||||
*/
|
||||
return {
|
||||
kind: 'object',
|
||||
properties: {
|
||||
useHookNotTypedAsHook: {
|
||||
kind: 'type',
|
||||
name: 'Any',
|
||||
},
|
||||
notAhookTypedAsHook: {
|
||||
kind: 'hook',
|
||||
returnType: {kind: 'type', name: 'Any'},
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (moduleName === 'useDefaultExportNotTypedAsHook') {
|
||||
/**
|
||||
* Fake module used for testing validation that type providers return hook
|
||||
* types for hook names and non-hook types for non-hook names
|
||||
*/
|
||||
return {
|
||||
kind: 'object',
|
||||
properties: {
|
||||
default: {
|
||||
kind: 'type',
|
||||
name: 'Any',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: 'object',
|
||||
properties: {
|
||||
default: {
|
||||
kind: 'function',
|
||||
calleeEffect: EffectEnum.Read,
|
||||
positionalParams: [],
|
||||
restParam: EffectEnum.Read,
|
||||
returnType: {kind: 'type', name: 'Primitive'},
|
||||
returnValueKind: ValueKindEnum.Primitive,
|
||||
},
|
||||
graphql: {
|
||||
kind: 'function',
|
||||
calleeEffect: EffectEnum.Read,
|
||||
positionalParams: [],
|
||||
restParam: EffectEnum.Read,
|
||||
returnType: {kind: 'type', name: 'Primitive'},
|
||||
returnValueKind: ValueKindEnum.Primitive,
|
||||
},
|
||||
typedArrayPush: {
|
||||
kind: 'function',
|
||||
calleeEffect: EffectEnum.Read,
|
||||
positionalParams: [EffectEnum.Store, EffectEnum.Capture],
|
||||
restParam: EffectEnum.Capture,
|
||||
returnType: {kind: 'type', name: 'Primitive'},
|
||||
returnValueKind: ValueKindEnum.Primitive,
|
||||
},
|
||||
typedLog: {
|
||||
kind: 'function',
|
||||
calleeEffect: EffectEnum.Read,
|
||||
positionalParams: [],
|
||||
restParam: EffectEnum.Read,
|
||||
returnType: {kind: 'type', name: 'Primitive'},
|
||||
returnValueKind: ValueKindEnum.Primitive,
|
||||
},
|
||||
useFreeze: {
|
||||
kind: 'hook',
|
||||
returnType: {kind: 'type', name: 'Any'},
|
||||
},
|
||||
useFragment: {
|
||||
kind: 'hook',
|
||||
returnType: {kind: 'type', name: 'MixedReadonly'},
|
||||
noAlias: true,
|
||||
},
|
||||
useNoAlias: {
|
||||
kind: 'hook',
|
||||
returnType: {kind: 'type', name: 'Any'},
|
||||
returnValueKind: ValueKindEnum.Mutable,
|
||||
noAlias: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user