-
Notifications
You must be signed in to change notification settings - Fork 46.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[compiler] Validate type configs for hooks/non-hooks #30888
Changes from all commits
23b6f20
6e4115a
7a6efb2
0a8bea2
c37ac41
c26ca80
9df57a0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we can enforce that the isHookName of the exported name and the local alias are the same? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that makes sense, but we should do that everywhere regardless of whether we have a type provider for the module or not. Let's explore that in a follow-up. |
||
* 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; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | | ||
``` | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import ReactCompilerTest from 'ReactCompilerTest'; | ||
|
||
function Component() { | ||
return ReactCompilerTest.useHookNotTypedAsHook(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | | ||
``` | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import {useHookNotTypedAsHook} from 'ReactCompilerTest'; | ||
|
||
function Component() { | ||
return useHookNotTypedAsHook(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | | ||
``` | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import foo from 'useDefaultExportNotTypedAsHook'; | ||
|
||
function Component() { | ||
return <div>{foo()}</div>; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | | ||
``` | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import {notAhookTypedAsHook} from 'ReactCompilerTest'; | ||
|
||
function Component() { | ||
return <div>{notAhookTypedAsHook()}</div>; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe factor this out into a helper?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm it's kind of tedious to get customize the error message if we extract to a helper, i'm gonna leave it