Commit Graph

1446 Commits

Author SHA1 Message Date
Sathya Gunasekaran 42fa01ec87 [test] Add failing test for component syntax with refs
The desugaring of Component syntax with refs is not compatible with our gating 
lowering.
2023-11-28 14:18:24 +00:00
Sathya Gunasekaran be03439687 [yarn] Update hermes-parser
Need new version for component syntax updates
2023-11-28 14:18:24 +00:00
Sathya Gunasekaran 1ebb7ae376 [eslint] Assume external modules are non side-effecting
This lets us tree shake out the `chalk` module entirely
2023-11-28 14:10:48 +00:00
Sathya Gunasekaran 0ba3065812 [eslint] Don't rollup zod 2023-11-28 14:10:48 +00:00
Joe Savona 445baf9ae4 Extend effect dep validation to handle pruned memoization
Extends the validation that effect deps are memoized to handle an additional 
case that @gsathya  pointed out: when a dependency has a reactive scope but that 
scope ends up being pruned. We track reactive scopes which actually exist in the 
ReactiveFunction, and reject useEffect deps that have an associated reactive 
scope but where that scope does not exist (bc it got pruned).
2023-11-27 12:31:27 -08:00
Joe Savona d7db416167 [RFC] useEffect dependency memoization check
This is one approach to testing whether useEffect dependencies are memoized. The 
idea is based off the observation that the only reason dependencies wouldn't be 
memoized (other than compiler bugs) is that they are mutated later. If they're 
mutated later, then the dep array will have a mutable range which encompasses 
the InstructionId of the useEffect call. So we look for that pattern and throw a 
validation error. 

The downside of this approach is that we might reject code that happens to be 
valid: specifically, that the lack of memoization isn't a problem in practice 
because the effect won't trigger a loop. But (per test plan) this doesn't seem 
to introduce that many new bailouts on www. Rather than implement a complex 
validation that checks whether we un-memoized something that was memoized in the 
input, it seems more practical to: 

1. Enable this more comprehensive validation against any form of un-memo'd 
effect dependency 

2. Flip the default for hooks (to assume they follow the rules), which will fix 
the primary cause of Forget pessimistically not memoizing dependencies. 

## Test Plan 

Synced to www and checked output via the upgrade script: a few components stop 
getting memoized bc they have un-memoized effect dependencies. Let's chat!
2023-11-27 10:20:43 -08:00
Mofei Zhang 5b8e94c6d8 [ez][wip] Make playground more resilient to crashes
--- 

<img width="1031" alt="image" 
src="https://github.com/facebook/react-forget/assets/34200447/8e475472-45c3-4ef0-aa4b-d187e72b999c"> 

Behold! No instacrash on `useMemo()`
2023-11-21 09:30:29 -06:00
Mofei Zhang 6a6ef0ab59 [repro] Forget outputs invalid code when we bailout
Sprout output: 

``` 

$ sprout --filter --verbose 

FAIL: bug-invalid-code-when-bailout 

Difference in forget and non-forget results. 

Expected result: { 

"kind": "ok", 

"value": "{}", 

"logs": [] 

} 

Found: { 

"kind": "exception", 

"value": "Cannot access 'bar' before initialization", 

"logs": [ 

"'The above error occurred in one of your React components:\\n' +\n  '\\n' +\n  
'    at globalThis.WrapperTestComponent 
(/Users/feifei0/fb/react-forget/packages/sprout/dist/runner-evaluator.js:30:26)\\n' 
+\n  '\\n' +\n  'Consider adding an error boundary to your tree to customize 
error handling behavior.\\n' +\n  'Visit 
https://reactjs.org/link/error-boundaries to learn more about error 
boundaries.'" 

] 

} 

```
2023-11-21 09:06:15 -06:00
Mofei Zhang bfefbf3fce [repro] dce bug for JSX memberexpr tags in lambda 2023-11-21 09:06:14 -06:00
Joe Savona 8176ebb546 BuildHIR lowers FunctionDecl instead of rewriting to FuncExpr
We were modifying the Babel AST as a shortcut to lowering function declarations, 
instead we can explicitly lower them equivalently to a `let <id> = 
<function-expression>`.
2023-11-16 15:41:19 -08:00
Lauren Tan a1e3891189 [rfc][babel] InvalidConfig always throws
When you have your panic threshold set to "NONE" as we recommend, it's easy to 
miss that your config is wrong (which makes everything not compile) because 
those errors were being silenced. This made debugging FluentUI and the 
forget-feedback testapp pretty difficult to figure out at first, and defeats the 
purpose of having config validation in the first place. 

This pr makes it so InvalidConfig errors always throw, regardless of the panic 
threshold set. In general our plugin should never throw at build time due to 
component bailouts, but because an InvalidConfig would bailout everything from 
being compiled at all, it seems reasonable to throw here
2023-11-17 12:01:20 -05:00
Lauren Tan 6d101435d4 [Babel] Fix up eslint suppression logic
Babel doesn't attach Comment nodes to anything, so they dangle off of 

the Program node while only specifying a range. This meant that 

previously we first had to traverse all of the Program's comments to 

find an eslint suppression of the rules of React, then during traversal 

of the individual functions, we would check if there were any *global* 

eslint suppressions, then bailout all components. 

This PR updates our logic to determine if individual functions are 

affected by an eslint suppression range: 

- If an eslint suppression range falls within its body; or 

- If an eslint suppression wraps the function
2023-11-17 10:22:46 -05:00
Lauren Tan fbcc21c37a Add repro for bug with 'use no forget'
For some reason, when there are other hooks/components defined in the file, the 
'use no forget' directive stops working
2023-11-17 10:22:45 -05:00
Mofei Zhang 96f10e058b [be][tests] Change fixtures to evaluate successfully instead of throwing
--- 

Not dependent on changes from #2366, but now the fix is easy to review
2023-11-16 18:12:00 -05:00
Mofei Zhang 66748b00f2 [be][sprout] Snapshot files for sprout with shared utils
--- 

16 out of ~150 recently added sprout fixtures have exceptions that reflect 
easy-to-miss mistakes in the fixture input, like forgetting to import 
`useState`. 

This PR adds snapshot files for sprout to prevent these mistakes (or catch them 
at diff review time). This describes what it implements, but happy to take other 
suggestions as well! 

1. One sprout snapshot file for each input. 

This significantly increases the number of files we have, but makes it clear 
what the fixture is testing. I was hoping to expand on these snapshots to record 
the result of multiple re-renders, or mounts (with different parameters), but 
the tradeoff is that adding / changing fixtures will now require running `yarn 
sprout --mode update`. 

Some alternatives I've considered: 

- Only record snapshots for fixtures that error (`result.kind == "exception"`). 
This change would be pretty easy. This doesn't help sanity check sprout results 
for general mistakes though ("am I testing what I want to test?) 

- Warn loudly for fixtures that error -- seems like these are easy to ignore, 
but.. worth it for the convenience? 

- Some github action that runs and comments on your PR with sprout fixture 
changes; i.e. never manually update sprout files? 

2. Sprout and snap share a common snapshot file, with some parsing hackery. 

Previously, the implementation added new files to a separate sprout snapshot 
directory, assuming that devs don't need to read these often. 

After feedback from @josephsavona, I updated to have snap and sprout write to 
the same file for ease of reviewing / debugging (to be able to see the input / 
output side by side). 

- `snap --mode update` will update snap's part of the file. 

- `sprout --mode update` will update sprout's part 

- `snap` and `sprout` will both compare oldSnapshot (read from the file) with 
`newSnapshot = merge(oldSnapshot, newData)` to determine if a test pass. 

3. Some hacks 😅 Absolute paths to project directory (e.g. stack traces) are 
replaced with `<project_root>` in a mocked `console.log`
2023-11-16 18:12:00 -05:00
Mofei Zhang fa07eb0b3b [be] make evaluator and worker easier to debug in sprout
--- 

jsdom and other libraries seem to cause jest workers to exit with 
`forceExit:true`. Not sure what option I set (or global I've overwritten) but 
console logs aren't flushed as a result, making debugging a bit confusing. 

This PR: 

- Moves some code from `eval(...)` to a typechecked real js function. I always 
had trouble debugging the `eval`ed code, so smaller code snippet is better here. 

- waits for jest workers to end before exiting
2023-11-16 18:12:00 -05:00
Joe Savona 94348be2d9 Enable ValidateNoSetStateInRender in ESLint plugin
Same as previous: this rule is working accurately with no false positives, let's 
enable it.
2023-11-16 07:52:13 -08:00
Joe Savona 7b6b370638 Consolidate ValidateNoSetStateInRender flags
I ran the plugin with the extended version of ValidateNoSetStateInRender enabled 
(incl. function expressions) and there are no false positives. Let's remove the 
flag for the function expression case since the whole rule is working 
accurately.
2023-11-15 13:05:24 -08:00
Joe Savona 6a1343c82b Repro of undefined expressioncontainer expression
Repro from T169063835. This works, but since it came up it seems good to add a 
test case for it just in case there's something funny here that we could regress 
on w/o realizing it.
2023-11-15 14:23:53 -08:00
Joe Savona df42058237 Fix variable-resolution hoisting issues
Fixes one category of bugs with const hoisting. The algorithm finds all consts 
that need to be hoisted, then looks through the statements of a block to find 
the first statement which references that const, delaying the emission of the 
HoistedConst instruction until its actually used. To determine if a statement 
references a const we find every identifier in the statement and check if its 
binding is one of the hoisted bindings. 

There's a very small bug here: when we resolve the binding of each identifier, 
we need to resolve it in its own scope. We're currently resolving these 
identifiers agains the outer block statement's scope, which can cause us to 
misattribute identifiers when there is shadowing: 

``` 

const items = props.items.map(x => x); // we scan this statement, resolve 'x' in 
the block statement scope, and mis-attribute it to the outer x. 

const x = 42; // (1) this x is a candidate for hoisting, so the binding is in 
the set of hoisted consts 

```
2023-11-15 16:55:01 -08:00
Joe Savona b55ccb1b84 Repro for hoisting bug
I had added a repro for this earlier but hadn't realized it was due to const 
hoisting. Renaming this test to clarify what's causing the problem and to make 
it easier to find.
2023-11-15 16:34:28 -08:00
Sathya Gunasekaran eacf189eca [playground] Remove button from error message
We show the entire error in panel, no need to click to console.log 

This makes it easier to copy the error now. 

Before: 


https://github.com/facebook/react-forget/assets/565765/4c13abfe-a06d-4580-b3d7-b02792f53e57 

After: 


https://github.com/facebook/react-forget/assets/565765/0dff95cc-5207-48a5-a0b6-2055cadcb780
2023-11-16 12:01:59 +00:00
Sathya Gunasekaran 369c315ac4 [hir] Update error message to say global
This is non ideal but at least it's a step in the right direction. 

Getting the correct error requires us to track every identifier and global, 
which seems a bit excessive for now. 

We can revisit and improve this error if this is starting to confuse folks.
2023-11-15 17:03:55 +00:00
Sathya Gunasekaran 392a4fd9da [test] Add tests for mutating a global
The error is thrown correctly but the error message is incorrect.
2023-11-15 17:03:51 +00:00
Sathya Gunasekaran 306fe03e78 [printer] Print function name if available 2023-11-15 09:36:32 +00:00
Sathya Gunasekaran 58947d9075 [babel] Rename isReactFunction to isReactAPI
Disambiguates better between this and the existing `isReactFunctionLike` 
function.
2023-11-15 09:36:31 +00:00
Joe Savona 696434fed8 Fix ValidateNoSetStateInRender for loops 2023-11-14 11:33:39 -08:00
Mofei Zhang c434ef4b70 [validation] Patch false positives for hook calls after loops
--- 

We were throwing `InvalidReact` errors on valid inputs. 

```js 

// Input 

function Foo() { 

log("block0"); 

for (const _ of foo) { 

log("loop"); 

} 

useBar(); 

} 

// IR 

bb0: 

// log("block0"); 

ForOf init=bb2 loop=bb3 fallthrough=bb1 

bb2: 

// init 

Branch: then:bb3 else:bb1 

bb3: 

// log("loop") 

Goto(Continue) bb2 

bb1: 

// useBar(); 

Return 

``` 

We correctly compute post dominators here. 

```js 

// In validateUnconditionalHooks 

console.log(dominators.debug()); 

/* Output: 

(read x => y as x is the post-dominator for y (all paths from x to the exit must 
go through y)) 

"bb4" => "bb4", 

"bb1" => "bb4", 

"bb2" => "bb1", 

"bb3" => "bb2", 

"bb0" => "bb2", 

*/ 

``` 

However, `findBlocksWithBackEdges` prevented us from adding `bb1` to the 
`unconditionalBlocks` set as `bb2` (its post dominator) has a back edge. I'm not 
sure what the `findBlocksWithBackEdges` was doing previously, so I replaced it 
with an invariant asserting that the loop terminates.
2023-11-14 13:04:31 -05:00
Mofei Zhang 00abc5acf5 [be][tests] Run todo fixtures
--- 

Snap should compile all fixtures to record changes in results, even `todo` 
prefixed ones. Previously, they were skipped as we noted the correlation of `// 
@skip` pragmas and file naming. 

Now, no fixture should be skipped as our compiler pipeline should be able to 
handle every kind of error.
2023-11-14 13:04:28 -05:00
Sathya Gunasekaran 2efd85f145 Convert CompilationModeScham to zod type 2023-11-14 11:46:26 +00:00
Sathya Gunasekaran 084e4325b9 Convert PanicThresholdOptions to zod type 2023-11-14 11:46:23 +00:00
Sathya Gunasekaran 0821a57fa8 [playground] Capture Babel parsing errors
Before: 

<img width="1416" alt="Screenshot 2023-11-14 at 9 09 53 AM" 
src="https://github.com/facebook/react-forget/assets/565765/f53daf34-080a-4804-9a61-77c141c05d21"> 

After: 

<img width="1444" alt="Screenshot 2023-11-14 at 9 09 45 AM" 
src="https://github.com/facebook/react-forget/assets/565765/8861c266-9a49-4ff7-99cd-7240ca1750b1">
2023-11-14 18:24:38 +00:00
Sathya Gunasekaran 07d6c6d8aa [playground] Show error when compiling unsupported functions
Before: 

<img width="1117" alt="Screenshot 2023-11-14 at 9 11 28 AM" 
src="https://github.com/facebook/react-forget/assets/565765/d1ec2b5f-1bc5-4d29-9811-effcd39581e4"> 

After: 

<img width="1115" alt="Screenshot 2023-11-14 at 9 11 18 AM" 
src="https://github.com/facebook/react-forget/assets/565765/9a41889c-cf7b-4f31-8b6e-e26bfb204883">
2023-11-14 09:10:31 +00:00
Sathya Gunasekaran 0794aacdbf Turn off validateNoSetStateInRender in babel-plugin
There's false positives that we found in the Eslint plugin
2023-11-14 16:59:10 +00:00
Sathya Gunasekaran 1641f94a86 [eslint] Turn off validations
We're seeing false positives
2023-11-14 16:59:09 +00:00
Sathya Gunasekaran 3da32312e7 [globals] type useEffect return value as primitive
useEffect returns undefined
2023-11-13 10:51:08 +00:00
Lauren Tan 480c11bdb1 [hoisting] Make hoisting related errors consolidatable
Noticed from our paste that we weren't correctly rolling up hoisting related 
errors due to specific information being in the error title, so this PR moves 
them into description instead.
2023-11-13 16:54:54 -05:00
Joe Savona 800b874ed1 fix main
Forgot to update snapshots when addressing PR feedback.
2023-11-13 13:14:27 -08:00
Joe Savona e6c5c9a005 Improve NoSetStateInRender function expression check
Updates the approach used in ValidateNoSetStateInRender to detect function 
expressions called during render. We now do the following: 

* Track function expression which are known to unconditionally call setState 
themselves- if these functions get called, that’s equivalent to calling 
setState. We call the validation recursively to compute this. 

* Track LoadLocal/StoreLocal indirections for such function expressions. 

* Check CallExpressions where the callee is either a known SetState (via type 
info) _or_ (new) where the callee is in the set of known-to-setState function 
expressions. 

The Set is shared throughout the analysis, so we can even find multiple levels 
of indirection (see new test case).
2023-11-13 11:28:16 -08:00
Joe Savona dcbdf06491 Extra test case related to memoization "within" freeze
I found an interesting edge case in the previous diff with mutation of a value 
that appears in the expression of an object key: 

```javascript 

const key = {} 

const object = { 

[mutateAndReturnOtherValue(key)]: 42, 

}; 

mutate(key); 

``` 

We analyze and represent this correctly all the way through to codegen, but then 
we hit the bug that @mofeiZ has noticed before: the temporary for `t = 
mutateAndReturnOtherValue(key)` isn't emitted immediately (bc its a temporary). 
It gets emitted inside the memo block for `object`, which is incorrect. 

I tried to reproduce that here with JSX and it works as expected. It's an 
interesting case though so let's land this to ensure we don't regress.
2023-11-13 11:03:00 -08:00
Joe Savona 8d41529600 Support computed object keys 2023-11-13 09:55:52 -08:00
Mofei Zhang f47685c602 [be] Flag and test for unexpected exceptions during compilations 2023-11-12 14:56:10 -05:00
Mofei Zhang 7db2388fff [be] tsconfig: useUnknownInCatchVariables 2023-11-12 14:44:15 -05:00
Joe Savona 3d2a05490f Update eslint plugin to enable more safe validations 2023-11-10 17:08:26 -08:00
Joe Savona fc85f24865 Separate flag for ref access violation within function expressions
Adds a separate compiler flag for enabling the incomplete validation of ref 
access within function expressions. Unlike the previous PR for 
set-state-in-render validation, ref access in render can be okay in some 
circumstances so i'm leaving this off by default. The point of splitting this up 
is that our linting will be able to enable the rule without risk of false 
positives.
2023-11-10 17:07:21 -08:00
Joe Savona 6e038f9699 Partially enable validateNoSetStateInRender
The approach i initially took to validating function expressions was to try to 
extend the mutable range if they are called during render, and then use the 
mutable range of a function to determine if it's called during render later. 
However there are cases where the range can be extended for other reasons, as 
@poteto discovered, so we can't rely on the range extension. We've had several 
of our validations completely off as a result of this. 

In this PR i'm re-enabling @poteto's ValidateNoSetStateInRender pass by default, 
but making the function expression checking use a separate compiler flag. This 
means we'll have some false negatives, but should guarantee that we avoid false 
positives. This means we can definitely catch things like: 

``` 

const [state, setState] = useState(false); 

setState(true); 

``` 

Which we would have allowed by default before.
2023-11-10 16:52:29 -08:00
Mofei Zhang c08b7be34e [be] Add comments for LoggerEvent type 2023-11-10 17:09:31 -05:00
Mofei Zhang 9965db70bd Revert "[babel] Remove unused PipelineError"
This reverts commit 10d129a8406e9d226abdb6943bf8512e34ce91db 

--- 

Reverts #2311 due to undocumented assumptions being broken. I also added some 
comments to `LoggerEvents` to explain each event type. 

In `Program.ts`, we have something like the following code. `compile` could 
produce any number of errors (not just expected errors / instances of 
`CompilerError`). As an example, we sometimes error in `Codegen` due to babel 
version incompatibilities (`Error: ObjectMethod: Too many arguments passed. 
Received 7 but can receive no more than 5`). 

```js 

try { 

// any error could be thrown here 

compile(input); 

} catch (e) { 

// unknown type for e 

handleError(e, ...); 

} 

```
2023-11-10 17:09:29 -05:00
Sathya Gunasekaran bd3661020e Add test for bailouts skipping compilation 2023-11-09 12:00:37 +00:00
Sathya Gunasekaran 69eb9ccccc [snap] Add option to set panicThreshold 2023-11-09 12:00:34 +00:00