-
Notifications
You must be signed in to change notification settings - Fork 47.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[compiler] detect and throw on untransformed required features
Traverse program after running compiler transform to find untransformed references to compiler features (e.g. `inferEffectDeps`, `fire`). Hard error to fail the babel pipeline when the compiler fails to transform these features to give predictable runtime semantics. Untransformed calls to functions like `fire` will throw at runtime anyways, so let's fail the build to catch these earlier. Note that with this fails the build *regardless of panicThreshold* This PR also throws retry pipeline errors, which shows better errors than the current generic TODO message
- Loading branch information
Showing
28 changed files
with
695 additions
and
139 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
188 changes: 188 additions & 0 deletions
188
.../packages/babel-plugin-react-compiler/src/Entrypoint/ValidateNoUntransformedReferences.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
import {NodePath} from '@babel/core'; | ||
import * as t from '@babel/types'; | ||
|
||
import {CompilerError, EnvironmentConfig} from '..'; | ||
import {getOrInsertWith} from '../Utils/utils'; | ||
import {Environment} from '../HIR'; | ||
|
||
export default function validateNoUntransformedReferences( | ||
path: NodePath<t.Program>, | ||
env: EnvironmentConfig, | ||
): void { | ||
const moduleLoadChecks = new Map< | ||
string, | ||
Map<string, CheckInvalidReferenceFn> | ||
>(); | ||
if (env.enableFire) { | ||
/** | ||
* Error on any untransformed references to `fire` (e.g. including non-call | ||
* expressions) | ||
*/ | ||
for (const module of Environment.knownReactModules) { | ||
const react = getOrInsertWith(moduleLoadChecks, module, () => new Map()); | ||
react.set('fire', assertNone); | ||
} | ||
} | ||
if (env.inferEffectDependencies) { | ||
/** | ||
* Only error on untransformed references of the form `useMyEffect(...)` or | ||
* `moduleNamespace.useMyEffect(...)`, with matching argument counts. | ||
* TODO: add mode to also hard error on non-call references | ||
*/ | ||
for (const { | ||
function: {source, importSpecifierName}, | ||
numRequiredArgs, | ||
} of env.inferEffectDependencies) { | ||
const module = getOrInsertWith(moduleLoadChecks, source, () => new Map()); | ||
module.set( | ||
importSpecifierName, | ||
assertNoAutoDepCalls.bind(null, numRequiredArgs), | ||
); | ||
} | ||
} | ||
if (moduleLoadChecks.size > 0) { | ||
transformProgram(path, moduleLoadChecks); | ||
} | ||
} | ||
|
||
type TraversalState = { | ||
shouldInvalidateScopes: boolean; | ||
program: NodePath<t.Program>; | ||
}; | ||
type CheckInvalidReferenceFn = (paths: Array<NodePath<t.Node>>) => void; | ||
function validateImportSpecifier( | ||
specifier: NodePath<t.ImportSpecifier>, | ||
importSpecifierChecks: Map<string, CheckInvalidReferenceFn>, | ||
state: TraversalState, | ||
): void { | ||
const imported = specifier.get('imported'); | ||
const specifierName: string = | ||
imported.node.type === 'Identifier' | ||
? imported.node.name | ||
: imported.node.value; | ||
const checkFn = importSpecifierChecks.get(specifierName); | ||
if (checkFn == null) { | ||
return; | ||
} | ||
if (state.shouldInvalidateScopes) { | ||
state.shouldInvalidateScopes = false; | ||
state.program.scope.crawl(); | ||
} | ||
|
||
const local = specifier.get('local'); | ||
const binding = local.scope.getBinding(local.node.name); | ||
CompilerError.invariant(binding != null, { | ||
reason: 'Expected binding to be found for import specifier', | ||
loc: local.node.loc ?? null, | ||
}); | ||
checkFn(binding.referencePaths); | ||
} | ||
|
||
function validateNamespacedImport( | ||
specifier: NodePath<t.ImportNamespaceSpecifier | t.ImportDefaultSpecifier>, | ||
importSpecifierChecks: Map<string, CheckInvalidReferenceFn>, | ||
state: TraversalState, | ||
): void { | ||
if (state.shouldInvalidateScopes) { | ||
state.shouldInvalidateScopes = false; | ||
state.program.scope.crawl(); | ||
} | ||
const local = specifier.get('local'); | ||
const binding = local.scope.getBinding(local.node.name); | ||
|
||
CompilerError.invariant(binding != null, { | ||
reason: 'Expected binding to be found for import specifier', | ||
loc: local.node.loc ?? null, | ||
}); | ||
const filteredReferences = new Map< | ||
CheckInvalidReferenceFn, | ||
Array<NodePath<t.Node>> | ||
>(); | ||
for (const reference of binding.referencePaths) { | ||
const parent = reference.parentPath; | ||
if ( | ||
parent != null && | ||
parent.isMemberExpression() && | ||
parent.get('object') === reference | ||
) { | ||
if (parent.node.computed || parent.node.property.type !== 'Identifier') { | ||
continue; | ||
} | ||
const checkFn = importSpecifierChecks.get(parent.node.property.name); | ||
if (checkFn != null) { | ||
getOrInsertWith(filteredReferences, checkFn, () => []).push(parent); | ||
} | ||
} | ||
} | ||
|
||
for (const [checkFn, references] of filteredReferences) { | ||
checkFn(references); | ||
} | ||
} | ||
function transformProgram( | ||
path: NodePath<t.Program>, | ||
moduleLoadChecks: Map<string, Map<string, CheckInvalidReferenceFn>>, | ||
): void { | ||
const traversalState = {shouldInvalidateScopes: true, program: path}; | ||
path.traverse({ | ||
ImportDeclaration(path: NodePath<t.ImportDeclaration>) { | ||
const importSpecifierChecks = moduleLoadChecks.get( | ||
path.node.source.value, | ||
); | ||
if (importSpecifierChecks == null) { | ||
return; | ||
} | ||
const specifiers = path.get('specifiers'); | ||
for (const specifier of specifiers) { | ||
if (specifier.isImportSpecifier()) { | ||
validateImportSpecifier( | ||
specifier, | ||
importSpecifierChecks, | ||
traversalState, | ||
); | ||
} else { | ||
validateNamespacedImport( | ||
specifier as NodePath< | ||
t.ImportNamespaceSpecifier | t.ImportDefaultSpecifier | ||
>, | ||
importSpecifierChecks, | ||
traversalState, | ||
); | ||
} | ||
} | ||
}, | ||
}); | ||
} | ||
|
||
function assertNoAutoDepCalls( | ||
numArgs: number, | ||
paths: Array<NodePath<t.Node>>, | ||
): void { | ||
for (const path of paths) { | ||
const parent = path.parentPath; | ||
if (parent != null && parent.isCallExpression()) { | ||
const args = parent.get('arguments'); | ||
if (args.length === numArgs) { | ||
/** | ||
* Note that we cannot easily check the type of the first argument here, | ||
* as it may have already been transformed by the compiler (and not | ||
* memoized). | ||
*/ | ||
CompilerError.throwTodo({ | ||
reason: | ||
'Untransformed reference to experimental compiler-only feature', | ||
loc: parent.node.loc ?? null, | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
|
||
function assertNone(paths: Array<NodePath<t.Node>>): void { | ||
if (paths.length > 0) { | ||
CompilerError.throwTodo({ | ||
reason: 'Untransformed reference to experimental compiler-only feature', | ||
loc: paths[0].node.loc ?? null, | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
26 changes: 26 additions & 0 deletions
26
...nfer-effect-dependencies/bailout-retry/error.callsite-in-non-react-fn.expect.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
|
||
## Input | ||
|
||
```javascript | ||
// @inferEffectDependencies @compilationMode(infer) @panicThreshold(none) | ||
import {useEffect} from 'react'; | ||
|
||
function nonReactFn(arg) { | ||
useEffect(() => [1, 2, arg]); | ||
} | ||
|
||
``` | ||
|
||
|
||
## Error | ||
|
||
``` | ||
3 | | ||
4 | function nonReactFn(arg) { | ||
> 5 | useEffect(() => [1, 2, arg]); | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Todo: Untransformed reference to experimental compiler-only feature (5:5) | ||
6 | } | ||
7 | | ||
``` | ||
6 changes: 6 additions & 0 deletions
6
...xtures/compiler/infer-effect-dependencies/bailout-retry/error.callsite-in-non-react-fn.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
// @inferEffectDependencies @compilationMode(infer) @panicThreshold(none) | ||
import {useEffect} from 'react'; | ||
|
||
function nonReactFn(arg) { | ||
useEffect(() => [1, 2, arg]); | ||
} |
41 changes: 41 additions & 0 deletions
41
...r/infer-effect-dependencies/bailout-retry/error.non-inlined-effect-fn.expect.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
|
||
## Input | ||
|
||
```javascript | ||
// @inferEffectDependencies @panicThreshold(none) | ||
import {useEffect} from 'react'; | ||
|
||
/** | ||
* Error on non-inlined effect functions: | ||
* 1. From the effect hook callee's perspective, it only makes sense | ||
* to either | ||
* (a) never hard error (i.e. failing to infer deps is acceptable) or | ||
* (b) always hard error, | ||
* regardless of whether the callback function is an inline fn. | ||
* 2. (Technical detail) it's harder to support detecting cases in which | ||
* function (pre-Forget transform) was inline but becomes memoized | ||
*/ | ||
function Component({foo}) { | ||
function f() { | ||
console.log(foo); | ||
} | ||
|
||
// No inferred dep array, the argument is not a lambda | ||
useEffect(f); | ||
} | ||
|
||
``` | ||
|
||
|
||
## Error | ||
|
||
``` | ||
18 | | ||
19 | // No inferred dep array, the argument is not a lambda | ||
> 20 | useEffect(f); | ||
| ^^^^^^^^^^^^ Todo: Untransformed reference to experimental compiler-only feature (20:20) | ||
21 | } | ||
22 | | ||
``` | ||
21 changes: 21 additions & 0 deletions
21
.../fixtures/compiler/infer-effect-dependencies/bailout-retry/error.non-inlined-effect-fn.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
// @inferEffectDependencies @panicThreshold(none) | ||
import {useEffect} from 'react'; | ||
|
||
/** | ||
* Error on non-inlined effect functions: | ||
* 1. From the effect hook callee's perspective, it only makes sense | ||
* to either | ||
* (a) never hard error (i.e. failing to infer deps is acceptable) or | ||
* (b) always hard error, | ||
* regardless of whether the callback function is an inline fn. | ||
* 2. (Technical detail) it's harder to support detecting cases in which | ||
* function (pre-Forget transform) was inline but becomes memoized | ||
*/ | ||
function Component({foo}) { | ||
function f() { | ||
console.log(foo); | ||
} | ||
|
||
// No inferred dep array, the argument is not a lambda | ||
useEffect(f); | ||
} |
27 changes: 27 additions & 0 deletions
27
...pendencies/bailout-retry/error.todo-import-default-property-useEffect.expect.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
|
||
## Input | ||
|
||
```javascript | ||
// @inferEffectDependencies @panicThreshold(none) | ||
import React from 'react'; | ||
|
||
function NonReactiveDepInEffect() { | ||
const obj = makeObject_Primitives(); | ||
React.useEffect(() => print(obj)); | ||
} | ||
|
||
``` | ||
|
||
|
||
## Error | ||
|
||
``` | ||
4 | function NonReactiveDepInEffect() { | ||
5 | const obj = makeObject_Primitives(); | ||
> 6 | React.useEffect(() => print(obj)); | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Todo: Untransformed reference to experimental compiler-only feature (6:6) | ||
7 | } | ||
8 | | ||
``` | ||
2 changes: 1 addition & 1 deletion
2
...todo.import-default-property-useEffect.js → ...todo-import-default-property-useEffect.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.