-
Notifications
You must be signed in to change notification settings - Fork 48.7k
[compiler] Avoid failing builds when import specifiers conflict or shadow vars #32663
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
Conversation
compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
Outdated
Show resolved
Hide resolved
@@ -271,6 +276,137 @@ function isFilePartOfSources( | |||
return false; | |||
} | |||
|
|||
function getExternalFunctions( |
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.
This function is the primary change in this PR. When collecting external functions imports to insert into the program, we also generate uids for each import.
In cases we cannot generate uids (e.g. program shadows global variables), we bail out without modifying the program (see error.emit-freeze-conflicting-global.js
)
This makes sense but I wonder if there's something a bit simpler. An idea I had played around with is creating an ImportContext at the program level to track all imports we need to create, and then passing this context to each environment that we create to compile functions. The ImportContext would hold a reference to the program's Scope, such that it could be used to call Babel's helper to generate unique identifier ids. Individual passes can then do As an extension, we could also traverse top-level imports and pre-populate the import context map, so that we can reuse existing imports and don't have to recreate those. What do you think? |
Yeah that makes a lot of sense! That would also help clean up the logic in It could also generalize as a generated variable name registry -- one thing I didn't love about this approach was that it renames hook imports to class ProgramContext {
uids: Set<string> = new Set();
hookNameConfig: string;
scope: t.Scope;
constructor(program: NodePath<t.Program>, hookNameConfig: string) {
this.hookNameConfig = hookNameConfig;
this.scope = program.scope;
}
addImportSpecifier(module: string, specifier: string): NonLocalBinding {
if (isHookName(specifier, this.hookNameConfig)) {
let maybeName = specifier;
let i = 0;
while (scope.hasReference(maybeName)) {
maybeName = `${specifier}_${i++}`;
}
}
...
}
makeUid(nameHint?: string): string {
const name = .generateUid(nameHint);
}
} |
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.
yesss this is awesome
this.scope.hasBinding(name) || | ||
this.scope.hasGlobal(name) || | ||
this.scope.hasReference(name) |
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.
this also prevents names that might be shadowed in local scopes, right?
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.
Yep, I took this from Babel's implementation (https://github.com/babel/babel/blob/main/packages/babel-traverse/src/scope/index.ts#L510-L513).
1 | // @enableEmitFreeze @instrumentForget | ||
2 | function useFoo(props) { | ||
> 3 | const __DEV__ = 'conflicting global'; | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Todo: Encountered conflicting global in generated program. Conflict from local binding __DEV__ (3:3) |
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.
why is this one an error rather than renaming?
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.
This one is trickier -- we could synthesize a read off of globalThis
or something similar, but I'm not familiar with browser / node version support. Note that this is a normal bailout (not a build failure)
Clean up jest-e2e setup since #32663 and other features need program context (e.g. changing imports)
Updates:
|
Clean up jest-e2e setup since #32663 and other features need program context (e.g. changing imports) --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32706). * #32663 * __->__ #32706
…adow vars Avoid failing builds when imported function specifiers conflict by using babel's `generateUid`. Failing a build is very disruptive, as it usually presents to developers similar to a javascript parse error. ```js import {logRender as _logRender} from 'instrument-runtime'; const logRender = () => { /* local conflicting implementation */ } function Component_optimized() { _logRender(); // inserted by compiler } ``` Currently, we fail builds (even in `panicThreshold:none` cases) when import specifiers are detected to conflict with existing local variables. The reason we destructively throw (instead of bailing out) is because (1) we first generate identifier references to the conflicting name in compiled functions, (2) replaced original functions with compiled functions, and then (3) finally check for conflicts. When we finally check for conflicts, it's too late to bail out. ```js // import {logRender} from 'instrument-runtime'; const logRender = () => { /* local conflicting implementation */ } function Component_optimized() { logRender(); // inserted by compiler } ```
…adow vars (#32663) Avoid failing builds when imported function specifiers conflict by using babel's `generateUid`. Failing a build is very disruptive, as it usually presents to developers similar to a javascript parse error. ```js import {logRender as _logRender} from 'instrument-runtime'; const logRender = () => { /* local conflicting implementation */ } function Component_optimized() { _logRender(); // inserted by compiler } ``` Currently, we fail builds (even in `panicThreshold:none` cases) when import specifiers are detected to conflict with existing local variables. The reason we destructively throw (instead of bailing out) is because (1) we first generate identifier references to the conflicting name in compiled functions, (2) replaced original functions with compiled functions, and then (3) finally check for conflicts. When we finally check for conflicts, it's too late to bail out. ```js // import {logRender} from 'instrument-runtime'; const logRender = () => { /* local conflicting implementation */ } function Component_optimized() { logRender(); // inserted by compiler } ``` DiffTrain build for [c61e75b](c61e75b)
Avoid failing builds when imported function specifiers conflict by using babel's
generateUid
. Failing a build is very disruptive, as it usually presents to developers similar to a javascript parse error.Currently, we fail builds (even in
panicThreshold:none
cases) when import specifiers are detected to conflict with existing local variables. The reason we destructively throw (instead of bailing out) is because (1) we first generate identifier references to the conflicting name in compiled functions, (2) replaced original functions with compiled functions, and then (3) finally check for conflicts.When we finally check for conflicts, it's too late to bail out.