Compare commits

...

96 Commits

Author SHA1 Message Date
Joe Savona
f9929a43ca [compiler] More claude config 2026-01-15 18:22:04 -08:00
Joe Savona
7f292dea2e [compiler] Improve snap, no more testfilter.txt file 2026-01-15 18:22:02 -08:00
Joe Savona
28f77b42dd [compiler] Improve impurity/ref tracking
note: This implements the idea discussed in https://github.com/reactwg/react/discussions/389#discussioncomment-14252280

Currently we create `Impure` effects for impure functions like `Date.now()` or `Math.random()`, and then throw if the effect is reachable during render. However, impurity is a property of the resulting value: if the value isn't accessed during render then it's okay: maybe you're console-logging the time while debugging (fine), or storing the impure value into a ref and only accessing it in an effect or event handler (totally ok).

This PR updates to validate that impure values are not transitively consumed during render, building on the `Render` effect. The validation currently uses an algorithm very similar to that of InferReactivePlaces - building a set of known impure values, starting with `Impure` effects as the sources and then flowing outward via data flow and mutations. If a transitively impure value reaches a `Render` effect, it's an error. We record both the source of the impure value and where it gets rendered to make it easier to understand and fix errors:

```
Error: Cannot access impure value during render

Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).

error.invalid-impure-functions-in-render-via-render-helper.ts:10:25
   8 |     const array = makeArray(now);
   9 |     const hasDate = identity(array);
> 10 |     return <Bar hasDate={hasDate} />;
     |                          ^^^^^^^ Cannot access impure value during render
  11 |   };
  12 |   return <Foo renderItem={renderItem} />;
  13 | }

error.invalid-impure-functions-in-render-via-render-helper.ts:6:14
  4 |
  5 | function Component() {
> 6 |   const now = Date.now();
    |               ^^^^^^^^^^ `Date.now` is an impure function.
  7 |   const renderItem = () => {
  8 |     const array = makeArray(now);
  9 |     const hasDate = identity(array);
```

Impure values are allowed to flow into refs, meaning that we now allow `useRef(Date.now())` or `useRef(localFunctionThatReturnsMathDotRandom())` which would have errored previously. The next PR reuses this improved impurity tracking to validate ref access in render as well.
2026-01-15 18:22:00 -08:00
Joe Savona
d963d422d0 [compiler] Update snap to support --nodebug
Passing `--nodebug` will disable debug mode, even if only a single fixture is filtered. This can help when running tests one by one and checking if their output is correct - debug mode causes tests to fail because there is extra/different error output.
2026-01-15 18:12:25 -08:00
Joe Savona
79badc68c4 [compiler] Claude file/settings 2026-01-15 18:12:14 -08:00
Sebastian "Sebbie" Silbermann
5aec1b2a8d [DevTools] Attach async info in filtered fallback to parent of Suspense (#35456) 2026-01-10 11:33:48 +01:00
lauren
d6cae440e3 [ci] Add size-balanced test sequencer for better shard distribution (#35458)
Jest's default test sequencer sorts alphabetically, causing large test
files
(eg ReactDOMFloat-test.js at 9k lines,
ReactHooksWithNoopRenderer-test.js at 4k
lines) to cluster in shard 3/5. This made shard 3/5 average 117s vs 77s
for
other shards, a 52% slowdown. I'm using filesize as a rough proxy for
number of tests.

This custom sequencer sorts tests by file size and distributes large
files evenly across all shards
instead of clustering them together.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35458).
* __->__ #35458
* #35459
2026-01-06 21:29:22 -05:00
lauren
00908be9ff [ci] Increase DevTools test shards and bump timeout (#35459)
[ci] Increase DevTools test shards and bump timeout

- Increase DevTools test shards from 3 to 5
- Bump timeout to 20s

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35459).
* #35458
* __->__ #35459
2026-01-06 21:23:05 -05:00
lauren
0e180141bf [ci] Separate DevTools test-build into dedicated job with fewer shards (#35457)
DevTools has ~45 test files which don't distribute well across 10
shards,
causing shard 3 to run 2x slower than others (104s vs ~50s). This moves
DevTools build tests to a separate job with 3 shards for better load
balancing.
2026-01-06 20:23:40 -05:00
Jon Jensen
65eec428c4 Use FormData submitter parameter (#29028) 2025-12-18 11:34:15 +01:00
Hendrik Liebau
454fc41fc7 [test] Add tests for cyclic arrays in Flight and Flight Reply (#35347)
We already had tests for cyclic objects, but not for cyclic arrays.
2025-12-17 18:08:16 +01:00
Sebastian Markbåge
f93b9fd44b Skip hydration errors when a view transition has been applied (#35380)
When the Fizz runtime runs a view-transition we apply
`view-transition-name` and `view-transition-class` to the `style`. These
can be observed by Fiber when hydrating which incorrectly leads to
hydration errors.

More over, even after we remove them, the `style` attribute has now been
normalized which we are unable to diff because we diff against the SSR
generated `style` attribute string and not the normalized form. So if
there are other inline styles defined, we have to skip diffing them in
this scenario.
2025-12-17 09:37:43 -05:00
Christian Van
b731fe28cc Improve cyclic thenable detection in ReactFlightReplyServer (#35369)
## Summary

This PR improves cyclic thenable detection in
`ReactFlightReplyServer.js`. Fixes #35368.
The previous fix only detected direct self-references (`inspectedValue
=== chunk`) and relied on the `cycleProtection` counter to eventually
bail out of longer cycles. This change keeps the existing
MAX_THENABLE_CYCLE_DEPTH ($1000$) `cycleProtection` cap as a hard
guardrail and adds a visited set so that we can detect self-cycles and
multi-node cycles as soon as any `ReactPromise` is revisited and while
still bounding the amount of work we do for deep acyclic chains via
`cycleProtection`.

## How did you test this change?

- Ran the existing test suite for the server renderer:

  ```bash
  yarn test react-server
  yarn test --prod react-server
  yarn flow dom-node
  yarn linc
  ```

---------

Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
2025-12-17 12:22:26 +01:00
Jack Pope
88ee1f5955 Add reporting modes for react-hooks/exhaustive-effect-dependencies and temporarily enable (#35365)
`react-hooks/exhaustive-effect-dependencies` from
`ValidateExhaustiveDeps` reports errors for both missing and extra
effect deps. We already have `react-hooks/exhaustive-deps` that errors
on missing dependencies. In the future we'd like to consolidate this all
to the compiler based error, but for now there's a lot of overlap. Let's
enable testing the extra dep warning by splitting out reporting modes.

This PR
- Creates `on`, `off`, `missing-only`, and `extra-only` reporting modes
for the effect dep validation flag
- Temporarily enables the new rule with `extra-only` in
`eslint-plugin-react-hooks`
- Adds additional null checking to `manualMemoLoc` to fix a bug found
when running against the fixture
2025-12-15 18:59:27 -05:00
emily8rown
bcf97c7564 Devtools disable log dimming strict mode setting (#35207)
<!--

1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
  2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).

-->

## Summary

Currently, every second console log is dimmed, receiving a special style
that indicates to user that it was raising because of [React Strict
Mode](https://react.dev/reference/react/StrictMode) second rendering.
This introduces a setting to disable this.

## How did you test this change?
Test in console-test.js


https://github.com/user-attachments/assets/af6663ac-f79b-4824-95c0-d46b0c8dec12

Browser extension react devtools


https://github.com/user-attachments/assets/7e2ecb7a-fbdf-4c72-ab45-7e3a1c6e5e44

React native dev tools:


https://github.com/user-attachments/assets/d875b3ac-1f27-43f8-8d9d-12b2d65fa6e6

---------

Co-authored-by: Ruslan Lesiutin <28902667+hoxyq@users.noreply.github.com>
2025-12-15 13:41:43 +00:00
Sebastian "Sebbie" Silbermann
ba5b843692 [test] Exclude repository root from assertions (#35361) 2025-12-15 11:45:17 +01:00
Jack Pope
b061b597f7 Upgrade nextjs for compiler playground (#35353)
Upgrading due to CVE-2025-55183 and CVE-2025-67779
2025-12-12 09:06:31 -05:00
Jorge Cabiedes
38a6f4e4a1 [compiler] Only run validations with env.logErrors on outputMode: 'lint' (#35216)
Summary:
These validations are not essential for compilation, with this we only
run that logic when outputMode is 'lint'

Test Plan:
Update fixtures and run tests
2025-12-11 16:36:36 -08:00
Nathan
b85cf6af3d [compiler] Fix VariableDeclarator source location (#35348)
Putting up https://github.com/facebook/react/pull/35129 again
Reverted in https://github.com/facebook/react/pull/35346 after breaking
main before security patch

This change impacts output formatting in a lot of snaps, so is very
sensitive to additions in main to the fixtures resulting in broken tests
after merging, so we should try merge quickly after rebasing or do a
fast follow to the merge with a snap update.
2025-12-11 18:02:05 -05:00
Sebastian Markbåge
b45bb335db [Flight] Add extra loop protection (#35351)
In case we get into loops.
2025-12-11 17:23:01 -05:00
Sebastian Markbåge
80cb7a9925 Revert "[compiler] Fix VariableDeclarator source location (#35129)" (#35346)
This broke main.
2025-12-11 15:27:07 -05:00
Sebastian Markbåge
894bc73cb4 [Flight] Patch Promise cycles and toString on Server Functions (#35345)
Server Functions can be stringified (sometimes implicitly) when passed
as data. This adds an override to hide the source code in that case -
just in case someone puts sensitive information in there.

Note that this still preserves the `name` field but this is also
available on the export but in practice is likely minified anyway.
There's nothing else on these referenes we'd consider unsafe unless you
explicitly expose expandos which are part of the `"use server"` export.

This adds a safety check to ensure you don't encode cyclic Promises.
This isn't a parser bug per se. Promises do have a safety mechanism that
avoids them infinite looping. However, since we use custom Thenables,
what can happen is that every time a native Promise awaits it, another
Promise wrapper is created around the Thenable which foils the
ECMAScript Promise cycle detection which can lead to an infinite loop.

This also ensures that embedded `ReadableStream` and `AsyncIterable`
streams are properly closed if the source stream closes early both on
the Server and Client. This doesn't cause an infinite loop but just to
make sure resource clean up can proceed properly.

We're also adding some more explicit clear errors for invalid payloads
since we no longer need to obfuscate the original issue.
2025-12-11 15:24:24 -05:00
Nathan
d3eb566291 [compiler] Fix VariableDeclarator source location (#35129)
### What
Fixes source locations for VariableDeclarator in the generated AST.
Fixes a number of the errors in the snapshot I added yesterday in the
source loc validator PR https://github.com/facebook/react/pull/35109

I'm not entirely sure why, but a side effect of the fix has resulted in
a ton of snaps needing updating, with some empty lines no longer present
in the generated output. I broke the change up into 2 separate commits.
The [first
commit](f4e4dc0f44)
has the core change and the update to the missing source locations test
expectation, and the [second
commit](cd4d9e944c)
has the rest of the snapshot updates.

### How
- Add location for variable declarators in ast codegen.
- We don't actually have the location preserved in HIR, since when we
lower the declarations we pass through the location for the
VariableDeclaration. Since VariableDeclarator is just a container for
each of the assignments, the start of the `id` and end of the `init` can
be used to accurately reconstruct it when generating the AST.
- Add source locations for object/array patterns for destructuring
assignment source location support
2025-12-11 14:35:03 -05:00
Ruslan Lesiutin
37bcdcde04 fix[devtools]: feature-check document with typeof instead of direct reference (#35343)
Follow-up to https://github.com/facebook/react/pull/35296.

We can get `ReferenceError` if this is unavailable. Using `typeof` check
instead for safety.
2025-12-11 12:15:00 +00:00
Ruslan Lesiutin
5a970933c0 fix[devtools]: feature-check structure stack trace methods (#35293)
`Error.prepareStackTrace` is non-standard feature and not all JavaScript
runtimes implement the methods that we are using in React DevTools
backend.

This PR adds additional checks for the presence of the methods that we
are using.
2025-12-10 19:21:54 +00:00
Ruslan Lesiutin
5d80124345 fix[devtools]: still show overlay, if getClientRects is not implemented (#35294)
Follow-up to https://github.com/facebook/react/pull/34653.

React Native doesn't implement `getClientRect`, since this is applicable
to CSS box, which is not a concept for Native (maybe yet).

I am loosening the condition that gates `showOverlay()` call to pass if
`getClientRect` is not implemented.

Conceptually, everything that is inside `react-devtools-shared/backend`
should be Host-agnostic, because both on Web and Native it is installed
inside the Host JavaScript runtime, be it main frame of the page, or RN
instance. Since overlay & highlighting logic also lives there, it should
also follow these principles.
2025-12-10 19:21:28 +00:00
Jack Pope
eade0d0fb7 Attach instance handle to DOM in DEV for enableInternalInstanceMap (#35341)
Continue attaching `internalInstanceKey` to DOM nodes in DEV. This
prevents breaking some internal dev tooling while we experiment with the
broader change. Note that this does not reference the DOM handle within
the flag, just attaches it and deletes it. Internals tracking is still
done through the private map.
2025-12-10 13:35:20 -05:00
emily8rown
d763f3131e [Devtools] Navigating commits performance panel hotkey (#35238)
## Summary
Add keyboard shortcuts (Cmd/Ctrl + Left/Right arrow keys) to navigate
between commits in the Profiler's snapshot view.

Moved `filteredCommitIndices` management and commit navigation logic
(`selectNextCommitIndex`, `selectPrevCommitIndex`) from
`SnapshotSelector` into `useCommitFilteringAndNavigation` used by
`ProfilerContext` to enable keyboard shortcuts from the top-level
Profiler component.

## How did you test this change?
- New tests in ProfilerContext-tests
- Built browser extension: `yarn build:<browser name>`
- tested in browser: `yarn run test:<browser name>`
- Manually verified Left/Right arrow navigation cycles through commits
- Verified navigation respects commit duration filter
- Verified reload-and-profile button unaffected

Chrome:


https://github.com/user-attachments/assets/01d2a749-13dc-4d08-8bcb-3d4d45a5f97c

Edge with duration filter:


https://github.com/user-attachments/assets/a7f76ff7-2a0b-4b9c-a0ce-d4449373308b

firefox mixing hotkey with clicking arrow buttons:


https://github.com/user-attachments/assets/48912d68-7c75-40f2-a203-5e6d7e6b2d99
2025-12-10 13:07:35 +00:00
lauren
734f1bf1ac [eprh] Enable enableUseKeyedState and enableVerboseNoSetStateInEffect (#35338)
Temporarily enables these 2 flags for internal testing.
2025-12-09 14:20:22 -05:00
dan
61331f3c9e Fix ViewTransition crash in Mobile Safari (#35337)
Speculative fix to https://github.com/facebook/react/issues/35336
written by Claude.

I have verified that applying a similar patch locally to the repro from
#35336 does fix the crash.

I'm not familiar enough with the underlying APIs to tell whether the fix
is correct or sufficient.
2025-12-10 03:35:15 +09:00
Sebastian "Sebbie" Silbermann
55480b4d22 [playground] Downgrade Next.js to a secure version (#35317)
Was bumped to a canary in https://github.com/facebook/react/pull/34499/
which got never released as stable.

Presumeably to use `Activity` which only made it into Activity in later
Next.js releases. However, `Activity` never ended up being used due to
incompatibilities with Monaco Editor. Downgrading should be safe.

Downgrading to fix
https://github.com/vercel/next.js/security/advisories/GHSA-9qr9-h5gf-34mp.
This will allow new deploys since Vercel is currently blocking new
deploys of unsafe version

---------

Co-authored-by: Eugene Choi <4eugenechoi@gmail.com>
2025-12-08 14:06:57 -05:00
lauren
3640f38a72 [compiler] Add enableVerboseNoSetStateInEffect to suggest options to user/agent (#35306)
The current `validateNoSetStateInEffects` error has potential false
positives because
we cannot fully statically detect patterns where calling setState in an
effect is
actually valid. This flag `enableVerboseNoSetStateInEffect` adds a
verbose error mode that presents multiple possible
use-cases, allowing an agent to reason about which fix is appropriate
before acting:

1. Non-local derived data - suggests restructuring state ownership
2. Derived event pattern - suggests requesting an event callback from
parent
3. Force update / external sync - suggests using `useSyncExternalStore`

This gives agents the context needed to make informed decisions rather
than
blindly applying a fix that may not be correct for the specific
situation.
2025-12-08 12:16:28 -05:00
Joseph Savona
ec9cc003d2 [compiler][poc] Reuse ValidateExhaustiveDeps for effect dep validation (#35285)
Alternative approach to #35282 for validating effect deps in the
compiler that builds on the machinery in ValidateExhaustiveDependencies.
Key changes to that pass:

* Refactor to track the dependencies of array expressions as temporaries
so we can look them up later if they appear as effect deps.
* Instead of not storing temporaries for LoadLocals of locally created
variables, we store the temporary but also propagate the local-ness
through. This allows us to record deps at the top level, necessary for
effect deps. Previously the pass was only ever concerned with tracking
deps within function expressions.
* Refactor the bulk of the dependency-checking logic from
`onFinishMemoize()` into a standalone helper to use it for the new
`onEffect()` helper as well.
* Add a new ErrorCategory for effect deps, use it for errors on
effects
* Put the effect dep validation behind a feature flag
* Adjust the error reason for effect errors

---------

Co-authored-by: Jack Pope <jackpope1@gmail.com>
2025-12-08 10:58:38 -05:00
Sebastian "Sebbie" Silbermann
380778d296 [test] Cleanup stack assertions in tests mixing React Server and Client (#35316) 2025-12-07 17:45:06 +01:00
Sebastian "Sebbie" Silbermann
41745339cd Run CI for backport releases (#35313) 2025-12-07 11:46:19 +01:00
Sebastian "Sebbie" Silbermann
c0b7c0d31f [scripts] Remove perf-counters (#35308) 2025-12-06 12:33:42 +01:00
Joseph Savona
2cb08e65b3 [compiler] Fix bug w functions depending on hoisted primitives (#35284)
Fixes an edge case where a function expression would fail to take a
dependency if it referenced a hoisted `const` inferred as a primitive
value. We were incorrectly skipping primitve-typed operands when
determing scopes for merging in InferReactiveScopeVariables.

This was super tricky to debug, for posterity the trick is that Context
variables (StoreContext etc) are modeled just like a mutable object,
where assignment to the variable is equivalent to `object.value = ...`
and reading the variable is equivalent to `object.value` property
access. Comparing to an equivalent version of the repro case replaced
with an object and property read/writes showed that everything was
exactly right, except that InferReactiveScopeVariables wasn't merging
the scopes of the function and the context variable, which led me right
to the problematic line.

Closes #35122
2025-12-05 14:29:06 -05:00
Ruslan Lesiutin
ad5971febd fix[devtools]: no-op unsupported backend bridge events (#35296)
Follow-up to https://github.com/facebook/react/pull/34641.
Similar to https://github.com/facebook/react/pull/35293,
https://github.com/facebook/react/pull/35294.

React DevTools backend can be used in non-DOM environments, so we have
to feature-check some DOM APIs.
For now I am just no-oping newly added commands for Native, we should
revisit this decision once we would roll out Suspense panel there, if
needed. I am not sure if scrolling will be required as much as it is
needed on Web.

`isReactNativeEnvironment()` check is kinda clowny, but we've been
relying on it for quite some time already.
2025-12-05 16:41:58 +00:00
Sebastian "Sebbie" Silbermann
378973b387 [Flight] Move react-server-dom-webpack/*.unbundled to private react-server-dom-unbundled (#35290)
Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
2025-12-05 03:59:21 +01:00
Sebastian Markbåge
3016ff87d8 [Flight] Never parse "then" functions (#35289)
AFAIK this is not needed to prevent any exploit but we don't really need
this. We allow functions on pretty much any other object anyway but
never on the "then" property since those would be serialized as Promises
by the client anyway.
2025-12-04 19:05:52 -05:00
lauren
f99241b2e6 [compiler] Add enableUseKeyedState flag and improve setState-in-render errors (#35230)
Adds a new `enableUseKeyedState` compiler flag that changes the error
message for unconditional setState calls during render.

When `enableUseKeyedState` is enabled, the error recommends using
`useKeyedState(initialState, key)` to reset state when dependencies
change. When disabled (the default), it links to the React docs for the
manual pattern of storing previous values in state.

Both error messages now include helpful bullet points explaining the two
main alternatives:
1. Use useKeyedState (or manual pattern) to reset state when other
state/props change
2. Compute derived data directly during render without using state
2025-12-04 18:29:10 -05:00
Kyℓe Hensel
66ae640b36 [eprh] fix react-compiler rules missing meta.docs.url property (#35258)
## Summary

To help people access the documentation easier, we can [add
`meta.docs.url`](https://eslint.org/docs/latest/extend/custom-rules#:~:text=Specifies%20the%20URL)
to the new react-compiler rules. This allows IDEs to make the rule name
a clickable link.

## How did you test this change?

`yarn test`, `yarn prettier`, `yarn lint` and in a separate project
[using file:// URLs](https://stackoverflow.com/a/38417065)
2025-12-04 15:28:13 -05:00
Felipe Cardozo
bf1afade8d [react-dom/server] Fix hanging on Deno (#35235) 2025-12-04 06:50:27 +01:00
Sebastian "Sebbie" Silbermann
0526c799d4 Update changelog with latest releases (#35279) 2025-12-03 18:13:43 +01:00
Sebastian Markbåge
7dc903cd29 Patch FlightReplyServer with fixes from ReactFlightClient (#35277)
FlightReplyServer are for client->server and ReactFlightClient is for
server->client. They're not 100% symmetrical.

We did a number of refactors to ReactFlightClient in PRs like #29823 and
#33664 to change the structure of the resolution. This PR brings those
changes to synchronize the two approaches. Which addresses deep
resolution of cycles and deferred error handling.

This also fixes a critical security vulnerability.
2025-12-03 10:41:19 -05:00
Sebastian "Sebbie" Silbermann
36df5e8b42 [release] Allow building single release channel with processed versions (#35270) 2025-12-02 22:05:10 +01:00
Jack Pope
09f05694a2 [compiler] Extend setState in effect validation to useEffectEvent (#35214)
ValidateNoSetStateInEffects already supports transitive setter
functions. This PR marks any synchonous state setter useEffectEvent
function so we can validate that uEE isn't being used only as
misdirection to avoid the validation within an effect body.

The error points to the call of the effect event.

Example:

```js
export default function MyApp() {
  const [count, setCount] = useState(0)
  const effectEvent = useEffectEvent(() => {
    setCount(10)
  })
  useEffect(() => {
    effectEvent()
  }, [])
  return <div>{count}</div>;
```

```
Found 1 error:

Error: Calling setState synchronously within an effect can trigger cascading renders

Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
* Update external systems with the latest state from React.
* Subscribe for updates from some external system, calling setState in a callback function when external state changes.

Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).

   5 |   })
   6 |   useEffect(() => {
>  7 |     effectEvent()
     |     ^^^^^^^^^^^ Avoid calling setState() directly within an effect
   8 |   }, [])
   9 |   return <div>{count}</div>;
  10 | }
```
2025-12-01 14:55:42 -05:00
Sebastian "Sebbie" Silbermann
0af4fd80ed [test] Update ESLint e2e tests (#35233) 2025-12-01 19:03:59 +01:00
Sebastian "Sebbie" Silbermann
1721e73e14 [test] Fix Error Proxy in Node.js 21+ (#35227) 2025-11-29 16:52:39 +01:00
Sebastian "Sebbie" Silbermann
6875c3eab4 [test] Only run tests overriding fallback and error states in supported versions (#35234) 2025-11-27 16:15:14 +01:00
Sebastian "Sebbie" Silbermann
74fa1667a7 [DevTools] Move "Back to full tree view" into Activity list item (#35164) 2025-11-26 10:42:18 +01:00
Joseph Savona
627b583650 [compiler][snap] Fix for filter mode with nested files, 'error.' prefix (#35215)
Fixes some issues i ran into w my recent snap changes:
* Correctly match against patterns that contain subdirectories, eg
`fbt/fbt-call`
* When checking if the input pattern has an extension, only prune known
supported extensions. Our convention of `error.<name>` for fixtures that
error makes the rest of the test name look like an extension to
`path.extname()`.

Tested with lots of different patterns including `error.` examples at
the top level and in nested directories, etc.
2025-11-25 15:39:07 -08:00
Joseph Savona
fb18ad3fd3 [compiler] Exhaustive deps: extra tests, improve diagnostic (#35213)
First, this adds some more tests and organizes them into an
`exhaustive-deps/` subdirectory.

Second, the diagnostics are overhauled. For each memo block we now
report a single diagnostic which summarizes the issue, plus individual
errors for each missing/extra dependency. Within the extra deps, we
distinguish whether it's truly extra vs whether its just a more (too)
precise version of an inferred dep. For example, if you depend on
`x.y.z` but the inferred dep was `x.y`. Finally, we print the full
inferred deps at the end as a hint (it's also a suggestion, but this
makes it more clear what would be suggested).
2025-11-25 12:09:09 -08:00
Joseph Savona
ddff35441a [compiler] Enable validateExhaustiveMemoizationDependencies by default (#35201)
Enables `@validateExhaustiveMemoizationDependencies` feature flag by
default, and disables it in select tests that failed due to the change.
Some of our tests intentionally use incorrect memo dependencies in order
to test edge cases.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35201).
* #35213
* __->__ #35201
2025-11-25 12:07:55 -08:00
Joseph Savona
d39a1d6b63 [compiler] Distingush optional/extraneous deps (#35204)
In ValidateExhaustiveDependencies, I previously changed to allow
extraneous dependencies as long as they were non-reactive. Here we make
that more precise, and distinguish between values that are definitely
referenced in the memo function but optional as dependencies vs values
that are not even referenced in the memo function. The latter now error
as extraneous even if they're non-reactive. This also turned up a case
where constant-folded primitives could show up as false positives of the
latter category, so now we track manual deps which quality for constant
folding and don't error on them.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35204).
* #35213
* #35201
* __->__ #35204
2025-11-25 12:06:25 -08:00
Joseph Savona
16e16ec6ff [compiler] Script to enable a feature by default and update tests (#35202)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35202).
* #35201
* __->__ #35202
2025-11-24 12:21:35 -08:00
Joseph Savona
9599e7a787 [compiler] Adjustments to exhaustive deps messages, disable the lint rule (#35192)
Similar to ValidateHookUsage, we implement this check in the compiler
for safety but (for now) continue to rely on the existing rule for
actually reporting errors to users.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35192).
* #35201
* #35202
* __->__ #35192
2025-11-24 12:20:12 -08:00
Joseph Savona
67c1487ffd [compiler] Allow extraneous non-reactive locals (#35190)
The existing exhaustive-deps rule allows omitting non-reactive
dependencies, even if they're not memoized. Conceptually, if a value is
non-reactive then it cannot semantically change. Even if the value is a
new object, that object represents the exact same value and doesn't
necessitate redoing downstream computation. Thus its fine to exclude
nonreactive dependencies, whether they're a stable type or not.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35190).
* #35201
* #35202
* #35192
* __->__ #35190
2025-11-24 12:18:49 -08:00
Joseph Savona
454e01e603 [compiler] Allow manual dependencies to have different optionality than inferred deps (#35186)
Since adding this validation we've already changed our inference to use
knowledge from manual memoization to inform when values are frozen and
which values are non-nullable. To align with that, if the user chooses
to use different optionality btw the deps and the memo block/callback,
that's fine. The key is that eg `x?.y` will invalidate whenever `x.y`
would, so from a memoization correctness perspective its fine. It's not
our job to be a type checker: if a value is potentially nullable, it
should likely use a nullable property access in both places but
TypeScript/Flow can check that.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35186).
* #35201
* #35202
* #35192
* #35190
* __->__ #35186
2025-11-24 12:17:03 -08:00
Joseph Savona
c9a8cf3411 [compiler] Allow nonreactive stable types as extraneous deps (#35185)
When checking ValidateExhaustiveDeps internally, this seems to be the
most common case that it flags. The current exhaustive-deps rule allows
extraneous deps if they are a set of stable types. So here we reuse our
existing isStableType() util in the compiler to allow this case.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35185).
* #35201
* #35202
* #35192
* #35190
* #35186
* __->__ #35185
2025-11-24 12:15:06 -08:00
Joseph Savona
fca172e3f3 [compiler] Ignore ESLint suppressions when ValidateMemoDeps enabled (#35184)
With `ValidateExhaustiveMemoDependencies` we can now check exhaustive
dependencies for useMemo and useCallback within the compiler, without
relying on the separate exhaustive-deps rule. Until now we've bailed out
of any component/hook that suppresses this rule, since the suppression
_might_ affect a memoization value. Compiling code with incorrect memo
deps can change behavior so this wasn't safe. The downside was that a
suppression within a useEffect could prevent memoization, even though
non-exhaustive deps for effects do not cause problems for memoization
specifically.

So here, we change to ignore ESLint suppressions if we have both the
compiler's hooks validation and memo deps validations enabled.

Now we just have to test out the new validation and refine before we can
enable this by default.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35184).
* #35201
* #35202
* #35192
* #35190
* #35186
* #35185
* __->__ #35184
2025-11-24 12:12:49 -08:00
emily8rown
fd524fe02a [DevTools] hotkey to start/stop profiling (#35160)
## Summary

The built-in browser profiler supports starting/stopping with Cmd+E. For
Symmetry this adds the same hotkey for react devtools profiler.

## How did you test this change?
yarn build:\<browser name\> 
yarn run test:\<browser name\>

<img width="483" height="135" alt="Screenshot 2025-11-17 at 14 30 34"
src="https://github.com/user-attachments/assets/426939aa-15da-4c21-87a4-e949e6949482"
/>

firefox:

https://github.com/user-attachments/assets/6f225b90-828f-4e79-a364-59d6bc942f83

edge:

https://github.com/user-attachments/assets/5b2e9242-f0e8-481b-99a2-2dd78099f3ac

chrome:

https://github.com/user-attachments/assets/790aab02-2867-4499-aec1-e32e38c763f9

---------

Co-authored-by: Ruslan Lesiutin <28902667+hoxyq@users.noreply.github.com>
2025-11-21 11:37:10 -05:00
Joseph Savona
40b4a5bf71 [compiler] ValidateExhaustiveDeps disallows unnecessary non-reactive deps (#34472)
Just to be consistent, we disallow unnecessary deps even if they're
known to be non-reactive.
2025-11-20 19:30:35 -08:00
Joseph Savona
df75af4edc [compiler] Auto-fix for non-exhaustive deps (#34471)
Records more information in DropManualMemoization so that we know the
full span of the manual dependencies array (if present). This allows
ValidateExhaustiveDeps to include a suggestion with the correct deps.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34471).
* #34472
* __->__ #34471
2025-11-20 19:28:08 -08:00
Joseph Savona
bcc3fd8b05 [compiler] Implement exhaustive dependency checking for manual memoization (#34394)
The compiler currently drops manual memoization and rewrites it using
its own inference. If the existing manual memo dependencies has missing
or extra dependencies, compilation can change behavior by running the
computation more often (if deps were missing) or less often (if there
were extra deps). We currently address this by relying on the developer
to use the ESLint plugin and have `eslint-disable-next-line
react-hooks/exhaustive-deps` suppressions in their code. If a
suppression exists, we skip compilation.

But not everyone is using the linter! Relying on the linter is also
imprecise since it forces us to bail out on exhaustive-deps checks that
only effect (ahem) effects — and while it isn't good to have incorrect
deps on effects, it isn't a problem for compilation.

So this PR is a rough sketch of validating manual memoization
dependencies in the compiler. Long-term we could use this to also check
effect deps and replace the ExhaustiveDeps lint rule, but for now I'm
focused specifically on manual memoization use-cases. If this works, we
can stop bailing out on ESLint suppressions, since the compiler will
implement all the appropriate checks (we already check rules of hooks).

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34394).
* #34472
* #34471
* __->__ #34394
2025-11-20 19:26:26 -08:00
Joseph Savona
50e7ec8a69 [compiler] Deprecate noEmit, add outputMode (#35112)
This deprecates the `noEmit: boolean` flag and adds `outputMode:
'client' | 'client-no-memo' | 'ssr' | 'lint'` as the replacement.
OutputMode defaults to null and takes precedence if specified, otherwise
we use 'client' mode for noEmit=false and 'lint' mode for noEmit=true.

Key points:
* Retrying failed compilation switches from 'client' mode to
'client-no-memo'
* Validations are enabled behind
Environment.proto.shouldEnableValidations, enabled for all modes except
'client-no-memo'. Similar for dropping manual memoization.
* OptimizeSSR is now gated by the outputMode==='ssr', not a feature flag
* Creation of reactive scopes, and related codegen logic, is now gated
by outputMode==='client'
2025-11-20 15:12:40 -08:00
Joseph Savona
4cf770d7e1 [compiler][poc] Quick experiment with SSR-optimization pass (#35102)
Just a quick poc:
* Inline useState when the initializer is known to not be a function.
The heuristic could be improved but will handle a large number of cases
already.
* Prune effects
* Prune useRef if the ref is unused, by pruning 'ref' props on primitive
components. Then DCE does the rest of the work - with a small change to
allow `useRef()` calls to be dropped since function calls aren't
normally eligible for dropping.
* Prune event handlers, by pruning props whose names start w "on" from
primitive components. Then DCE removes the functions themselves.

Per the fixture, this gets pretty far.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35102).
* #35112
* __->__ #35102
2025-11-20 15:02:38 -08:00
Jorge Cabiedes
7d67591041 [compiler] Remove useState argument constraint. no-derived-computations-in-effects (#35174)
Summary:
I missed this conditional messing things up for undefined useState()
calls. We should be tracking them.

I also missed a test that expect an error was not throwing.

Test Plan:
Update broken test

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35174).
* __->__ #35174
* #35173
2025-11-20 10:45:17 -08:00
Jorge Cabiedes
7ee974de92 [compiler] Prevent innaccurate derivation recording on FunctionExpressions on no-derived-computation-in-effects (#35173)
Summary:
The operands of a function expression are the elements passed as
context. This means that it doesn't make sense to record mutations for
them.

The relevant mutations will happen in the function body, so we need to
prevent FunctionExpression type instruction from running the logic for
effect mutations.

This was also causing some values to depend on themselves in some cases
triggering an infinite loop. Also added n invariant to prevent this
issue

Test Plan:
Added fixture test

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35173).
* #35174
* __->__ #35173
2025-11-20 10:44:45 -08:00
Sebastian "Sebbie" Silbermann
8ac5f4eb36 Fix form status reset when component state is updated (#34075)
Co-authored-by: Vordgi <sasha2822222@gmail.com>
2025-11-19 18:22:07 +01:00
Sebastian Markbåge
eb89912ee5 Add expertimental optimisticKey behind a flag (#35162)
When dealing with optimistic state, a common problem is not knowing the
id of the thing we're waiting on. Items in lists need keys (and single
items should often have keys too to reset their state). As a result you
have to generate fake keys. It's a pain to manage those and when the
real item comes in, you often end up rendering that with a different
`key` which resets the state of the component tree. That in turns works
against the grain of React and a lot of negatives fall out of it.

This adds a special `optimisticKey` symbol that can be used in place of
a `string` key.

```js
import {optimisticKey} from 'react';
...
const [optimisticItems, setOptimisticItems] = useOptimistic([]);
const children = savedItems.concat(
  optimisticItems.map(item =>
    <Item key={optimisticKey} item={item} />
  )
);
return <div>{children}</div>;
```

The semantics of this `optimisticKey` is that the assumption is that the
newly saved item will be rendered in the same slot as the previous
optimistic items. State is transferred into whatever real key ends up in
the same slot.

This might lead to some incorrect transferring of state in some cases
where things don't end up lining up - but it's worth it for simplicity
in many cases since dealing with true matching of optimistic state is
often very complex for something that only lasts a blink of an eye.

If a new item matches a `key` elsewhere in the set, then that's favored
over reconciling against the old slot.

One quirk with the current algorithm is if the `savedItems` has items
removed, then the slots won't line up by index anymore and will be
skewed. We might be able to add something where the optimistic set is
always reconciled against the end. However, it's probably better to just
assume that the set will line up perfectly and otherwise it's just best
effort that can lead to weird artifacts.

An `optimisticKey` will match itself for updates to the same slot, but
it will not match any existing slot that is not an `optimisticKey`. So
it's not an `any`, which I originally called it, because it doesn't
match existing real keys against new optimistic keys. Only one
direction.
2025-11-18 16:29:18 -05:00
Ricky
0972e23908 [compiler] Consider setter from useOptimistic non-reactive (#35141)
Closes https://github.com/facebook/react/issues/35138
2025-11-18 10:50:43 -05:00
Sebastian "Sebbie" Silbermann
194c12d949 [DevTools] Name root "Transition" when focusing on Activity (#35108) 2025-11-18 10:16:58 +01:00
Sebastian "Sebbie" Silbermann
7f1a085b28 [DevTools] Show list of named Activities in Suspense tab (#35092) 2025-11-18 09:52:44 +01:00
Joseph Savona
ea4899e13f [compiler][snap] Support pattern of files to test as CLI argument (#35148)
I've been trying out LLM agents for compiler development, and one thing
i found is that the agent naturally wants to run `yarn snap <pattern>`
to test a specific fixture, and I want to be able to tell it (directly
or in rules/skills) to do this in order to get the debug output from all
the compiler passes. Agents can figure out our current testfilter.txt
file system but that's just tedious. So here we add support for `yarn
snap -p <pattern>`. If you pass in a pattern with an extension, we
target that extension specifically. If you pass in a .expect.md file, we
look at that specific fixture. And if the pattern doesn't have
extensions, we search for `<pattern>{.js,.jsx,.ts,.tsx}`. When patterns
are enabled we automatically log as in debug mode (if there is a single
match), and disable watch mode.

Open to feedback!
2025-11-17 12:09:09 -08:00
Joseph Savona
b946a249b5 [compiler] Improve setState-in-effects rule to account for ref-gated conditionals (#35147)
Conditionally calling setState in an effect is sometimes necessary, but
should generally follow the pattern of using a "previous vaue" ref to
manually compare and ensure that the setState is idempotent. See fixture
for an example.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35147).
* #35148
* __->__ #35147
2025-11-17 12:07:43 -08:00
Joseph Savona
d6b1a0573b [compiler] Extract reusable logic for control dominators (#35146)
The next PR needs to check if a block is controlled by a value derived
from a ref.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35146).
* #35148
* #35147
* __->__ #35146
2025-11-17 12:05:52 -08:00
Joseph Savona
b315a0f713 [compiler] Fix for destructuring with mixed declaration/reassignment (#35144)
Destructing statements that start off as declarations can end up
becoming reassignments if the variable is a scope declaration, so we
have existing logic to handle cases where some parts of a destructure
need to be converted into new locals, with a reassignment to the hoisted
scope variable afterwards. However, there is an edge case where all of
the values are reassigned, in which case we don't need to rewrite and
can just set the instruction kind to reassign.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35144).
* #35148
* #35147
* #35146
* __->__ #35144
2025-11-17 11:34:49 -08:00
Sebastian "Sebbie" Silbermann
7df96b0c1a [Flight] Complete list of Node.js' internal Promise awaits (#35161) 2025-11-17 19:28:26 +01:00
Sebastian "Sebbie" Silbermann
45bc3c9f04 [Flight] Reduce risk of maximum call stack exceeded when emitting async sequence (#35159) 2025-11-17 18:54:13 +01:00
Hendrik Liebau
fb2177c153 [Flight] Fix pending chunks count for streams & async iterables in DEV (#35143)
In DEV, we need to prevent the response from being GC'd while there are
still pending chunks for ReadableSteams or pending results for
AsyncIterables.

Co-authored-by: Sebastian "Sebbie" Silbermann <silbermann.sebastian@gmail.com>
2025-11-14 23:52:11 +01:00
Joseph Savona
647e13366c [compiler] fix bad rebase from sapling (#35145)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35145).
* #35144
* __->__ #35145
2025-11-14 14:50:38 -08:00
Joseph Savona
19b769fa5f [compiler] Fix for inferring props-derived-value as mutable (#35140)
Fix for the repro from the previous PR. A `Capture x -> y` effect should
downgrade to `ImmutableCapture` when the source value is maybe-frozen.
MaybeFrozen represents the union of a frozen value with a non-frozen
value.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35140).
* __->__ #35140
* #35139
2025-11-14 12:14:34 -08:00
Joseph Savona
dbf2538355 [compiler] Repro for false positive mutation of a value derived from props (#35139)
Repro from the compiler WG (Thanks Cody!) of a case where the compiler
incorrectly thinks a value is mutable.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35139).
* #35140
* __->__ #35139
2025-11-14 12:14:23 -08:00
Eliot Pontarelli
21f282425c [compiler] Allow ref access in callbacks passed to event handler props (#35062)
## Summary

Fixes #35040. The React compiler incorrectly flags ref access within
event handlers as ref access at render time. For example, this code
would fail to compile with error "Cannot access refs during render":

```tsx
  const onSubmit = async (data) => {
    const file = ref.current?.toFile(); // Incorrectly flagged as error
  };

  <form onSubmit={handleSubmit(onSubmit)}>
```
This is a false positive because any built-in DOM event handler is
guaranteed not to run at render time. This PR only supports built-in
event handlers because there are no guarantees that user-made event
handlers will not run at render time.

## How did you test this change?

I created 4 test fixtures which validate this change:
* allow-ref-access-in-event-handler-wrapper.tsx - Sync handler test
input
* allow-ref-access-in-event-handler-wrapper.expect.md - Sync handler
expected output
* allow-ref-access-in-async-event-handler-wrapper.tsx - Async handler
test input
* allow-ref-access-in-async-event-handler-wrapper.expect.md - Async
handler expected output

All linters and test suites also pass.
2025-11-14 09:00:33 -08:00
Jorge Cabiedes
257b033fc7 [Compiler] Avoid capturing global setStates for no-derived-computations lint (#35135)
Summary:
This only matters when enableTreatSetIdentifiersAsStateSetters=true

This pattern is still bad. But Right now the validation can only
recommend to move stuff to "calculate in render"

A global setState should not be moved to render, not even conditionally
and you can't remove state without crossing Component boundaries, which
makes this a different kind of fix.

So while we are only suggesting "calculate in render" as a fix we should
disallow the lint from throwing in this case IMO

Test Plan:
Added a fixture

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35135).
* __->__ #35135
* #35134
2025-11-13 22:56:06 -08:00
Jorge Cabiedes
de97ef9ad5 [Compiler] Don't count a setState in the dependency array of the effect it is called on as a usage (#35134)
Summary:
The validation only allows setState declaration as a usage outside of
the effect.

Another edge case is that if you add the setState being validated in the
dependency array you also make the validation opt out since it counts as
a usage outside of the effect.

Added a bit of logic to consider the effect's deps when creating the
cache for setState usages within the effect

Test Plan:
Added a fixture

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35134).
* #35135
* __->__ #35134
2025-11-13 22:52:23 -08:00
Hendrik Liebau
93fc57400b [Flight] Fix broken byte stream parsing caused by buffer detachment (#35127)
This PR fixes a critical bug where `ReadableStream({type: 'bytes'})`
instances passed through React Server Components (RSC) would stall after
reading only the first chunk or the first few chunks in the client. This
issue was masked by using `web-streams-polyfill` in tests, but manifests
with native Web Streams implementations.

The root cause is that when a chunk is enqueued to a
`ReadableByteStreamController`, the spec requires the underlying
ArrayBuffer to be synchronously transferred/detached. In the React
Flight Client's chunk parsing, embedded byte stream chunks are created
as views into the incoming RSC stream chunk buffer using `new
Uint8Array(chunk.buffer, offset, length)`. When embedded byte stream
chunks are enqueued, they can detach the shared buffer, leaving the RSC
stream parsing in a broken state.

The fix is to copy embedded byte stream chunks before enqueueing them,
preventing buffer detachment from affecting subsequent parsing. To not
affect performance too much, we use a zero-copy optimization: when a
chunk ends exactly at the end of the RSC stream chunk, or when the row
spans into the next RSC chunk, no further parsing will access that
buffer, so we can safely enqueue the view directly without copying.

We now also enqueue embedded byte stream chunks immediately as they are
parsed, without waiting for the full row to complete.

To simplify the logic in the client, we introduce a new `'b'` protocol
tag specifically for byte stream chunks. The server now emits `'b'`
instead of `'o'` for `Uint8Array` chunks from byte streams (detected via
`supportsBYOB`). This allows the client to recognize byte stream chunks
without needing to track stream IDs.

Tests now use the proper Jest environment with native Web Streams
instead of polyfills, exposing and validating the fix for this issue.
2025-11-13 21:23:02 +01:00
Sebastian "Sebbie" Silbermann
093b3246e1 [react-dom] Batch updates from resize until next frame (#35117) 2025-11-13 13:30:21 +01:00
Nathan
3a495ae722 [compiler] source location validator (#35109)
@josephsavona this was briefly discussed in an old thread, lmk your
thoughts on the approach. I have some fixes ready as well but wanted to
get this test case in first... there's some things I don't _love_ about
this approach, but end of the day it's just a tool for the test suite
rather than something for end user folks so even if it does a 70% good
enough job that's fine.

### refresher on the problem
when we generate coverage reports with jest (istanbul), our coverage
ends up completely out of whack due to the AST missing a ton of (let's
call them "important") source locations after the compiler pipeline has
run.

At the moment to get around this, we've been doing something a bit
unorthodox and also running our test suite with istanbul running before
the compiler -- which results in its own set of issues (for eg, things
being memoized differently, or the compiler completely bailing out on
the instrumented code, etc).

before getting in fixes, I wanted to set up a test case to start
chipping away on as you had recommended.

### how it works

The validator basically:
1. Traverses the original AST and collects the source locations for some
"important" node types
- (excludes useMemo/useCallback calls, as those are stripped out by the
compiler)
3. Traverses the generated AST and looks for nodes with matching source
locations.
4. Generates errors for source locations missing nodes in the generated
AST

### caveats/drawbacks

There are some things that don't work super well with this approach. A
more natural test fit I think would be just having some explicit
assertions made against an AST in a test file, as you can just bake all
of the assumptions/nuance in there that are difficult to handle in a
generic manner. However, this is maybe "good enough" for now.

1. Have to be careful what you put into the test fixture. If you put in
some code that the compiler just removes (for eg, a variable assignment
that is unused), you're creating a failure case that's impossible to
fix. I added a skip for useMemo/useCallback.
2. "Important" locations must exactly match for validation to pass.
- Might get tricky making sure things are mapped correctly when a node
type is completely changed, for eg, when a block statement arrow
function body gets turned into an implicit return via the body just
being an expression/identifier.
- This can/could result in scenarios where more changes are needed to
shuttle the locations through due to HIR not having a 1:1 mapping all
the babel nuances, even if some combination of other data might be good
enough even if not 10000% accurate. This might be the _right_ thing
anyways so we don't end up with edge cases having incorrect source
locations.
2025-11-12 19:02:46 -08:00
Ricky
bbe3f4d322 [flags] disableLegacyMode in native-fb (#35120)
this is failing test too because we need the legacy mode in the react
package until we fix the tests
2025-11-12 15:38:58 -05:00
Sebastian "Sebbie" Silbermann
1ea46df8ba [DevTools] Batch updates when updating component filters (#35093) 2025-11-11 23:20:22 +01:00
Sebastian "Sebbie" Silbermann
8c15edd57c [DevTools] Send root unmount as a regular removal operation (#35107) 2025-11-11 23:08:54 +01:00
Jorge Cabiedes
5e94655cbb [compiler] _exp version of ValidateNoDerivedComputationsInEffects take precedence over stable version when enabled (#35099)
Summary:
We should only run one version of the validation. I think it makes sense
that if the exp version is enable it takes precedence over the stable
one

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35099).
* __->__ #35099
* #35100
2025-11-11 10:16:20 -08:00
Jorge Cabiedes
db8273c12f [compiler] Update test snap to include fixture comment (#35100)
Summary:
I missed this test case failing and now having @loggerTestOnly after
landing some other PRs good to know they're not land blocking

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35100).
* #35099
* __->__ #35100
2025-11-11 10:16:04 -08:00
Ricky
04ee54cd12 [tests] add more portal activity tests (#35095)
I copied some tests from
[`Activity-test.js`](1d68bce19c/packages/react-reconciler/src/__tests__/Activity-test.js)
and made them portal specific just to confirm my understanding of how
Portals + Activity interact is correct. Seems good to include them.
2025-11-11 12:47:56 -05:00
766 changed files with 19379 additions and 7067 deletions

View File

@@ -331,6 +331,7 @@ module.exports = {
'packages/react-server-dom-turbopack/**/*.js',
'packages/react-server-dom-parcel/**/*.js',
'packages/react-server-dom-fb/**/*.js',
'packages/react-server-dom-unbundled/**/*.js',
'packages/react-test-renderer/**/*.js',
'packages/react-debug-tools/**/*.js',
'packages/react-devtools-extensions/**/*.js',
@@ -634,6 +635,7 @@ module.exports = {
FocusOptions: 'readonly',
OptionalEffectTiming: 'readonly',
__REACT_ROOT_PATH_TEST__: 'readonly',
spyOnDev: 'readonly',
spyOnDevAndProd: 'readonly',
spyOnProd: 'readonly',

View File

@@ -3,6 +3,10 @@ name: (Runtime) Build and Test
on:
push:
branches: [main]
tags:
# To get CI for backport releases.
# This will duplicate CI for releases from main which is acceptable
- "v*"
pull_request:
paths-ignore:
- compiler/**
@@ -41,7 +45,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
lookup-only: true
- uses: actions/setup-node@v4
if: steps.node_modules.outputs.cache-hit != 'true'
@@ -55,10 +59,8 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Save cache
@@ -67,7 +69,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
runtime_compiler_node_modules_cache:
name: Cache Runtime, Compiler node_modules
@@ -82,7 +84,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
lookup-only: true
- uses: actions/setup-node@v4
if: steps.node_modules.outputs.cache-hit != 'true'
@@ -98,10 +100,8 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
restore-keys: |
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-and-compiler-node_modules-v6-
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd compiler install --frozen-lockfile
@@ -112,7 +112,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# ----- FLOW -----
discover_flow_inline_configs:
@@ -154,10 +154,8 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -184,10 +182,8 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -216,7 +212,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -274,10 +270,8 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
restore-keys: |
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-and-compiler-node_modules-v6-
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -306,7 +300,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
- name: Install runtime dependencies
run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
@@ -349,10 +343,8 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
restore-keys: |
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-and-compiler-node_modules-v6-
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -390,9 +382,6 @@ jobs:
-r=experimental --env=development,
-r=experimental --env=production,
# Dev Tools
--project=devtools -r=experimental,
# TODO: Update test config to support www build tests
# - "-r=www-classic --env=development --variant=false"
# - "-r=www-classic --env=production --variant=false"
@@ -440,10 +429,8 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
restore-keys: |
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-and-compiler-node_modules-v6-
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -460,6 +447,50 @@ jobs:
run: ls -R build
- run: yarn test --build ${{ matrix.test_params }} --shard=${{ matrix.shard }} --ci
test_build_devtools:
name: yarn test-build (devtools)
needs: [build_and_lint, runtime_node_modules_cache]
strategy:
fail-fast: false
matrix:
shard:
- 1/5
- 2/5
- 3/5
- 4/5
- 5/5
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache/restore@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Restore archived build
uses: actions/download-artifact@v4
with:
pattern: _build_*
path: build
merge-multiple: true
- name: Display structure of build
run: ls -R build
- run: yarn test --build --project=devtools -r=experimental --shard=${{ matrix.shard }} --ci
process_artifacts_combined:
name: Process artifacts combined
needs: [build_and_lint, runtime_node_modules_cache]
@@ -483,10 +514,8 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -548,10 +577,8 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -588,10 +615,8 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -740,10 +765,8 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -802,10 +825,8 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile

View File

@@ -1,3 +1,9 @@
## 19.2.1 (Dec 3, 2025)
### React Server Components
- Bring React Server Component fixes to Server Actions (@sebmarkbage [#35277](https://github.com/facebook/react/pull/35277))
## 19.2.0 (October 1st, 2025)
Below is a list of all new features, APIs, and bug fixes.
@@ -71,6 +77,12 @@ Read the [React 19.2 release post](https://react.dev/blog/2025/10/01/react-19-2)
- [createContainer](https://github.com/facebook/react/blob/v19.2.0/packages/react-reconciler/src/ReactFiberReconciler.js#L255-L261) and [createHydrationContainer](https://github.com/facebook/react/blob/v19.2.0/packages/react-reconciler/src/ReactFiberReconciler.js#L305-L312) had their parameter order adjusted after `on*` handlers to account for upcoming experimental APIs
## 19.1.2 (Dec 3, 2025)
### React Server Components
- Bring React Server Component fixes to Server Actions (@sebmarkbage [#35277](https://github.com/facebook/react/pull/35277))
## 19.1.1 (July 28, 2025)
### React
@@ -123,6 +135,12 @@ An Owner Stack is a string representing the components that are directly respons
* Exposed `registerServerReference` in client builds to handle server references in different environments. [#32534](https://github.com/facebook/react/pull/32534)
* Added react-server-dom-parcel package which integrates Server Components with the [Parcel bundler](https://parceljs.org/) [#31725](https://github.com/facebook/react/pull/31725), [#32132](https://github.com/facebook/react/pull/32132), [#31799](https://github.com/facebook/react/pull/31799), [#32294](https://github.com/facebook/react/pull/32294), [#31741](https://github.com/facebook/react/pull/31741)
## 19.0.1 (Dec 3, 2025)
### React Server Components
- Bring React Server Component fixes to Server Actions (@sebmarkbage [#35277](https://github.com/facebook/react/pull/35277))
## 19.0.0 (December 5, 2024)
Below is a list of all new features, APIs, deprecations, and breaking changes. Read [React 19 release post](https://react.dev/blog/2024/04/25/react-19) and [React 19 upgrade guide](https://react.dev/blog/2024/04/25/react-19-upgrade-guide) for more information.

View File

@@ -0,0 +1,20 @@
{
"permissions": {
"allow": [
"Bash(node scripts/enable-feature-flag.js:*)",
"Bash(yarn snap:*)",
"Bash(for test in \"error.invalid-access-ref-during-render\" \"error.invalid-ref-in-callback-invoked-during-render\" \"error.invalid-impure-functions-in-render-via-render-helper\")",
"Bash(do)",
"Bash(echo:*)",
"Bash(done)",
"Bash(cat:*)",
"Bash(sl revert:*)",
"Bash(yarn workspace snap run build:*)",
"Bash(yarn tsc:*)",
"Bash(yarn snap:build)",
"Bash(timeout 30 yarn snap:*)"
],
"deny": [],
"ask": []
}
}

245
compiler/CLAUDE.md Normal file
View File

@@ -0,0 +1,245 @@
# React Compiler Knowledge Base
This document contains knowledge about the React Compiler gathered during development sessions. It serves as a reference for understanding the codebase architecture and key concepts.
## Project Structure
- `packages/babel-plugin-react-compiler/` - Main compiler package
- `src/HIR/` - High-level Intermediate Representation types and utilities
- `src/Inference/` - Effect inference passes (aliasing, mutation, etc.)
- `src/Validation/` - Validation passes that check for errors
- `src/Entrypoint/Pipeline.ts` - Main compilation pipeline with pass ordering
- `src/__tests__/fixtures/compiler/` - Test fixtures
- `error.*.js` - Fixtures that should produce compilation errors
- `*.expect.md` - Expected output for each fixture
## Running Tests
```bash
# Run all tests
yarn snap
# Run specific test by pattern
yarn snap -p <pattern>
# Update fixture outputs
yarn snap -u
```
## Version Control
This repository uses Sapling (`sl`) for version control. Unlike git, Sapling does not require explicitly adding files to the staging area.
```bash
# Check status
sl status
# Commit all changes
sl commit -m "Your commit message"
# Commit with multi-line message using heredoc
sl commit -m "$(cat <<'EOF'
Summary line
Detailed description here
EOF
)"
```
## Key Concepts
### HIR (High-level Intermediate Representation)
The compiler converts source code to HIR for analysis. Key types in `src/HIR/HIR.ts`:
- **HIRFunction** - A function being compiled
- `body.blocks` - Map of BasicBlocks
- `context` - Captured variables from outer scope
- `params` - Function parameters
- `returns` - The function's return place
- `aliasingEffects` - Effects that describe the function's behavior when called
- **Instruction** - A single operation
- `lvalue` - The place being assigned to
- `value` - The instruction kind (CallExpression, FunctionExpression, LoadLocal, etc.)
- `effects` - Array of AliasingEffects for this instruction
- **Terminal** - Block terminators (return, branch, etc.)
- `effects` - Array of AliasingEffects
- **Place** - A reference to a value
- `identifier.id` - Unique IdentifierId
- **Phi nodes** - Join points for values from different control flow paths
- Located at `block.phis`
- `phi.place` - The result place
- `phi.operands` - Map of predecessor block to source place
### AliasingEffects System
Effects describe data flow and operations. Defined in `src/Inference/AliasingEffects.ts`:
**Data Flow Effects:**
- `Impure` - Marks a place as containing an impure value (e.g., Date.now() result, ref.current)
- `Capture a -> b` - Value from `a` is captured into `b` (mutable capture)
- `Alias a -> b` - `b` aliases `a`
- `ImmutableCapture a -> b` - Immutable capture (like Capture but read-only)
- `Assign a -> b` - Direct assignment
- `MaybeAlias a -> b` - Possible aliasing
- `CreateFrom a -> b` - Created from source
**Mutation Effects:**
- `Mutate value` - Value is mutated
- `MutateTransitive value` - Value and transitive captures are mutated
- `MutateConditionally value` - May mutate
- `MutateTransitiveConditionally value` - May mutate transitively
**Other Effects:**
- `Render place` - Place is used in render context (JSX props, component return)
- `Freeze place` - Place is frozen (made immutable)
- `Create place` - New value created
- `CreateFunction` - Function expression created, includes `captures` array
- `Apply` - Function application with receiver, function, args, and result
### Hook Aliasing Signatures
Located in `src/HIR/Globals.ts`, hooks can define custom aliasing signatures to control how data flows through them.
**Structure:**
```typescript
aliasing: {
receiver: '@receiver', // The hook function itself
params: ['@param0'], // Named positional parameters
rest: '@rest', // Rest parameters (or null)
returns: '@returns', // Return value
temporaries: [], // Temporary values during execution
effects: [ // Array of effects to apply when hook is called
{kind: 'Freeze', value: '@param0', reason: ValueReason.HookCaptured},
{kind: 'Assign', from: '@param0', into: '@returns'},
],
}
```
**Common patterns:**
1. **RenderHookAliasing** (useState, useContext, useMemo, useCallback):
- Freezes arguments (`Freeze @rest`)
- Marks arguments as render-time (`Render @rest`)
- Creates frozen return value
- Aliases arguments to return
2. **EffectHookAliasing** (useEffect, useLayoutEffect, useInsertionEffect):
- Freezes function and deps
- Creates internal effect object
- Captures function and deps into effect
- Returns undefined
3. **Event handler hooks** (useEffectEvent):
- Freezes callback (`Freeze @fn`)
- Aliases input to return (`Assign @fn -> @returns`)
- NO Render effect (callback not called during render)
**Example: useEffectEvent**
```typescript
const UseEffectEventHook = addHook(
DEFAULT_SHAPES,
{
positionalParams: [Effect.Freeze], // Takes one positional param
restParam: null,
returnType: {kind: 'Function', ...},
calleeEffect: Effect.Read,
hookKind: 'useEffectEvent',
returnValueKind: ValueKind.Frozen,
aliasing: {
receiver: '@receiver',
params: ['@fn'], // Name for the callback parameter
rest: null,
returns: '@returns',
temporaries: [],
effects: [
{kind: 'Freeze', value: '@fn', reason: ValueReason.HookCaptured},
{kind: 'Assign', from: '@fn', into: '@returns'},
// Note: NO Render effect - callback is not called during render
],
},
},
BuiltInUseEffectEventId,
);
// Add as both names for compatibility
['useEffectEvent', UseEffectEventHook],
['experimental_useEffectEvent', UseEffectEventHook],
```
**Key insight:** If a hook is missing an `aliasing` config, it falls back to `DefaultNonmutatingHook` which includes a `Render` effect on all arguments. This can cause false positives for hooks like `useEffectEvent` whose callbacks are not called during render.
### Effect Inference Pipeline
Effects are populated by `InferMutationAliasingEffects` (runs before validation):
1. For `Date.now()`, `Math.random()` etc. - adds `Impure` effect (controlled by `validateNoImpureFunctionsInRender` config)
2. For `ref.current` access - adds `Impure` effect (controlled by `validateRefAccessDuringRender` config)
3. For return terminals - adds `Alias` from return value to `fn.returns`
4. For component/JSX returns - adds `Render` effect
5. For function expressions - adds `CreateFunction` effect with captures
### Validation: validateNoImpureValuesInRender
Located at `src/Validation/ValidateNoImpureValuesInRender.ts`
**Purpose:** Detect when impure values (refs, Date.now results, etc.) flow into render context.
**Algorithm:**
1. Track impure values in a Map<IdentifierId, ImpureEffect>
2. Track functions with impure returns separately (they're not impure values themselves)
3. Fixed-point iteration over all blocks:
- Process phi nodes (any impure operand makes result impure)
- Process instruction effects
- Process terminal effects
- Backwards propagation for mutated LoadLocal values
4. Validate: check all Render effects against impure values
**Key patterns:**
- `Impure` effect marks the target as impure
- `Capture/Alias/etc` propagates impurity from source to target
- `Apply` propagates impurity from args/receiver to result
- `CreateFunction` propagates impurity from captured values (but NOT from body effects)
- If a value has both `Render` and `Capture` in same instruction, only error on Render (don't cascade)
**Tracking functions with impure returns:**
- Separate from the `impure` map (function values aren't impure, just their returns)
- Populated when analyzing FunctionExpression bodies
- Used when:
1. Calling the function - mark call result as impure
2. Capturing the function - mark target as impure (for object.foo = impureFunc cases)
**Backwards propagation:**
- When `$x = LoadLocal y` and `$x` is mutated with impure content, mark `y` as impure
- This handles: `const arr = []; arr.push(impure); render(arr)`
## Known Issues / Edge Cases
### Function Outlining
After `OutlineFunctions` pass, inner functions are replaced with `LoadGlobal(_temp)`. The validation runs BEFORE outlining, so it sees the original FunctionExpression. But be aware that test output shows post-outlining HIR.
### SSA and LoadLocal
In SSA form, each `LoadLocal` creates a new identifier. When a loaded value is mutated:
- `$x = LoadLocal y`
- `mutate($x, impure)`
- `$z = LoadLocal y` (different from $x!)
- `render($z)`
The impurity in $x must propagate back to y, then forward to $z. This requires backwards propagation in the fixed-point loop.
## Configuration Flags
In `Environment.ts` / test directives:
- `validateNoImpureFunctionsInRender` - Enable impure function validation (Date.now, Math.random, etc.)
- `validateRefAccessDuringRender` - Enable ref access validation
## Debugging Tips
1. Run `yarn snap -p <fixture>` to see full HIR output with effects
2. Look for `@aliasingEffects=` on FunctionExpressions
3. Look for `Impure`, `Render`, `Capture` effects on instructions
4. Check the pass ordering in Pipeline.ts to understand when effects are populated vs validated

View File

@@ -12,6 +12,7 @@
# next.js
/.next/
/out/
/next-env.d.ts
# production
/build

View File

@@ -14,7 +14,6 @@ import React, {
unstable_ViewTransition as ViewTransition,
unstable_addTransitionType as addTransitionType,
startTransition,
Activity,
} from 'react';
import {Resizable} from 're-resizable';
import {useStore, useStoreDispatch} from '../StoreContext';
@@ -34,9 +33,14 @@ export default function ConfigEditor({
}): React.ReactElement {
const [isExpanded, setIsExpanded] = useState(false);
// TODO: Add back <Activity> after upgrading next.js
return (
<>
<Activity mode={isExpanded ? 'visible' : 'hidden'}>
<div
style={{
display: isExpanded ? 'block' : 'none',
}}>
{/* <Activity mode={isExpanded ? 'visible' : 'hidden'}> */}
<ExpandedEditor
onToggle={() => {
startTransition(() => {
@@ -46,8 +50,13 @@ export default function ConfigEditor({
}}
formattedAppliedConfig={formattedAppliedConfig}
/>
</Activity>
<Activity mode={isExpanded ? 'hidden' : 'visible'}>
</div>
<div
style={{
display: !isExpanded ? 'block' : 'none',
}}>
{/* </Activity>
<Activity mode={isExpanded ? 'hidden' : 'visible'}></Activity> */}
<CollapsedEditor
onToggle={() => {
startTransition(() => {
@@ -56,7 +65,8 @@ export default function ConfigEditor({
});
}}
/>
</Activity>
</div>
{/* </Activity> */}
</>
);
}
@@ -116,8 +126,9 @@ function ExpandedEditor({
return (
<ViewTransition
enter={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}
exit={{[CONFIG_PANEL_TRANSITION]: 'slide-out', default: 'none'}}>
update={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}>
{/* enter={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}
exit={{[CONFIG_PANEL_TRANSITION]: 'slide-out', default: 'none'}}> */}
<Resizable
minWidth={300}
maxWidth={600}

View File

@@ -1,6 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -35,13 +35,13 @@
"lru-cache": "^11.2.2",
"lz-string": "^1.5.0",
"monaco-editor": "^0.52.0",
"next": "15.6.0-canary.7",
"next": "15.5.9",
"notistack": "^3.0.0-alpha.7",
"prettier": "^3.3.3",
"pretty-format": "^29.3.1",
"re-resizable": "^6.9.16",
"react": "19.2",
"react-dom": "19.2"
"react": "19.2.3",
"react-dom": "19.2.3"
},
"devDependencies": {
"@types/node": "18.11.9",

View File

@@ -19,7 +19,7 @@
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"jsx": "preserve",
"incremental": true,
"plugins": [
{

View File

@@ -715,10 +715,10 @@
dependencies:
"@monaco-editor/loader" "^1.6.1"
"@next/env@15.6.0-canary.7":
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.6.0-canary.7.tgz#cdbf2967a9437ef09eef755e203f315acc4d8d8f"
integrity sha512-LNZ7Yd3Cl9rKvjYdeJmszf2HmSDP76SQmfafKep2Ux16ZXKoN5OjwVHFTltKNdsB3vt2t+XJzLP2rhw5lBoFBA==
"@next/env@15.5.9":
version "15.5.9"
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.5.9.tgz#53c2c34dc17cd87b61f70c6cc211e303123b2ab8"
integrity sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==
"@next/eslint-plugin-next@15.5.2":
version "15.5.2"
@@ -727,45 +727,45 @@
dependencies:
fast-glob "3.3.1"
"@next/swc-darwin-arm64@15.6.0-canary.7":
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.6.0-canary.7.tgz#628cd34ce9120000f1cb5b08963426431174fc57"
integrity sha512-POsBrxhrR3qvqXV+JZ6ZoBc8gJf8rhYe+OedceI1piPVqtJYOJa3EB4eaqcc+kMsllKRrH/goNlhLwtyhE+0Qg==
"@next/swc-darwin-arm64@15.5.7":
version "15.5.7"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz#f0c9ccfec2cd87cbd4b241ce4c779a7017aed958"
integrity sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==
"@next/swc-darwin-x64@15.6.0-canary.7":
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.6.0-canary.7.tgz#37d4ebab14da74a2f8028daf6d76aab410153e06"
integrity sha512-lmk9ysBuSiPlAJZTCo/3O4mXNFosg6EDIf4GrmynIwCG2as6/KxzyD1WqFp56Exp8eFDjP7SFapD10sV43vCsA==
"@next/swc-darwin-x64@15.5.7":
version "15.5.7"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz#18009e9fcffc5c0687cc9db24182ddeac56280d9"
integrity sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==
"@next/swc-linux-arm64-gnu@15.6.0-canary.7":
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.6.0-canary.7.tgz#ce700cc0e0d24763136838223105a524b36694fa"
integrity sha512-why8k6d0SBm3AKoOD5S7ir3g+BF34l9oFKIoZrLaZaKBvNGpFcjc7Ovc2TunNMeaMJzv9k1dHYSap0EI5oSuzg==
"@next/swc-linux-arm64-gnu@15.5.7":
version "15.5.7"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz#fe7c7e08264cf522d4e524299f6d3e63d68d579a"
integrity sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==
"@next/swc-linux-arm64-musl@15.6.0-canary.7":
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.6.0-canary.7.tgz#c791b8e15bf2c338b4cc0387fe7afb3ef83ecfcf"
integrity sha512-HzvTRsKvYj32Va4YuJN3n3xOxvk+6QwB63d/EsgmdkeA/vrqciUAmJDYpuzZEvRc3Yp2nyPq8KZxtHAr6ISZ2Q==
"@next/swc-linux-arm64-musl@15.5.7":
version "15.5.7"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz#94228fe293475ec34a5a54284e1056876f43a3cf"
integrity sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==
"@next/swc-linux-x64-gnu@15.6.0-canary.7":
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.6.0-canary.7.tgz#c01c3a3d8e71660c49298dd053d078379b6b5919"
integrity sha512-6yRFrg2qWXOqa+1BI53J9EmHWFzKg9U2r+5R7n7BFUp8PH5SC92WBsmYTnh/RkvAYvdupiVzMervwwswCs6kFg==
"@next/swc-linux-x64-gnu@15.5.7":
version "15.5.7"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz#078c71201dfe7fcfb8fa6dc92aae6c94bc011cdc"
integrity sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==
"@next/swc-linux-x64-musl@15.6.0-canary.7":
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.6.0-canary.7.tgz#3f4b39faef4a5f88b13e4c726b008ddc9717f819"
integrity sha512-O/JjvOvNK/Wao/OIQaA6evDkxkmFFQgJ1/hI1dVk6/PAeKmW2/Q+6Dodh97eAkOwedS1ZdQl2mojf87TzLvzdQ==
"@next/swc-linux-x64-musl@15.5.7":
version "15.5.7"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz#72947f5357f9226292353e0bb775643da3c7a182"
integrity sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==
"@next/swc-win32-arm64-msvc@15.6.0-canary.7":
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.6.0-canary.7.tgz#9bc5da0907b7ce67eedda02a6d56a09d9a539ccf"
integrity sha512-p9DvrDgnePofZCtiWVY7qZtwXxiOGJlAyy2LoGPYSGOUDhjbTG8j6XMUFXpV9UwpH+l7st522psO1BVzbpT8IQ==
"@next/swc-win32-arm64-msvc@15.5.7":
version "15.5.7"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz#397b912cd51c6a80e32b9c0507ecd82514353941"
integrity sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==
"@next/swc-win32-x64-msvc@15.6.0-canary.7":
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.6.0-canary.7.tgz#5b271c591ccbe67d5fa966dd22db86c547414fd1"
integrity sha512-f1ywT3xWu4StWKA1mZRyGfelu/h+W0OEEyBxQNXzXyYa0VGZb9LyCNb5cYoNKBm0Bw18Hp1PVe0bHuusemGCcw==
"@next/swc-win32-x64-msvc@15.5.7":
version "15.5.7"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz#e02b543d9dc6c1631d4ac239cb1177245dfedfe4"
integrity sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
@@ -3204,25 +3204,25 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
next@15.6.0-canary.7:
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/next/-/next-15.6.0-canary.7.tgz#bfc2ac3c9a78e23d550c303d18247a263e6b5bc1"
integrity sha512-4ukX2mxat9wWT6E0Gw/3TOR9ULV1q399E42F86cwsPSFgTWa04ABhcTqO0r9J/QR1YWPR8WEgh9qUzmWA/1yEw==
next@15.5.9:
version "15.5.9"
resolved "https://registry.yarnpkg.com/next/-/next-15.5.9.tgz#1b80d05865cc27e710fb4dcfc6fd9e726ed12ad4"
integrity sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==
dependencies:
"@next/env" "15.6.0-canary.7"
"@next/env" "15.5.9"
"@swc/helpers" "0.5.15"
caniuse-lite "^1.0.30001579"
postcss "8.4.31"
styled-jsx "5.1.6"
optionalDependencies:
"@next/swc-darwin-arm64" "15.6.0-canary.7"
"@next/swc-darwin-x64" "15.6.0-canary.7"
"@next/swc-linux-arm64-gnu" "15.6.0-canary.7"
"@next/swc-linux-arm64-musl" "15.6.0-canary.7"
"@next/swc-linux-x64-gnu" "15.6.0-canary.7"
"@next/swc-linux-x64-musl" "15.6.0-canary.7"
"@next/swc-win32-arm64-msvc" "15.6.0-canary.7"
"@next/swc-win32-x64-msvc" "15.6.0-canary.7"
"@next/swc-darwin-arm64" "15.5.7"
"@next/swc-darwin-x64" "15.5.7"
"@next/swc-linux-arm64-gnu" "15.5.7"
"@next/swc-linux-arm64-musl" "15.5.7"
"@next/swc-linux-x64-gnu" "15.5.7"
"@next/swc-linux-x64-musl" "15.5.7"
"@next/swc-win32-arm64-msvc" "15.5.7"
"@next/swc-win32-x64-msvc" "15.5.7"
sharp "^0.34.3"
node-releases@^2.0.18:
@@ -3582,10 +3582,10 @@ re-resizable@^6.9.16:
resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.10.0.tgz#d684a096ab438f1a93f59ad3a580a206b0ce31ee"
integrity sha512-hysSK0xmA5nz24HBVztlk4yCqCLCvS32E6ZpWxVKop9x3tqCa4yAj1++facrmkOf62JsJHjmjABdKxXofYioCw==
react-dom@19.2:
version "19.2.0"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz#00ed1e959c365e9a9d48f8918377465466ec3af8"
integrity sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==
react-dom@19.2.3:
version "19.2.3"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.3.tgz#f0b61d7e5c4a86773889fcc1853af3ed5f215b17"
integrity sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==
dependencies:
scheduler "^0.27.0"
@@ -3599,10 +3599,10 @@ react-is@^18.0.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
react@19.2:
version "19.2.0"
resolved "https://registry.npmjs.org/react/-/react-19.2.0.tgz#d33dd1721698f4376ae57a54098cb47fc75d93a5"
integrity sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==
react@19.2.3:
version "19.2.3"
resolved "https://registry.yarnpkg.com/react/-/react-19.2.3.tgz#d83e5e8e7a258cf6b4fe28640515f99b87cd19b8"
integrity sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==
read-cache@^1.0.0:
version "1.0.0"

View File

@@ -132,6 +132,12 @@ export class CompilerDiagnostic {
return new CompilerDiagnostic({...options, details: []});
}
clone(): CompilerDiagnostic {
const cloned = CompilerDiagnostic.create({...this.options});
cloned.options.details = [...this.options.details];
return cloned;
}
get reason(): CompilerDiagnosticOptions['reason'] {
return this.options.reason;
}
@@ -304,6 +310,30 @@ export class CompilerError extends Error {
disabledDetails: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
printedMessage: string | null = null;
static simpleInvariant(
condition: unknown,
options: {
reason: CompilerDiagnosticOptions['reason'];
description?: CompilerDiagnosticOptions['description'];
loc: SourceLocation;
},
): asserts condition {
if (!condition) {
const errors = new CompilerError();
errors.pushDiagnostic(
CompilerDiagnostic.create({
reason: options.reason,
description: options.description ?? null,
category: ErrorCategory.Invariant,
}).withDetails({
kind: 'error',
loc: options.loc,
message: options.reason,
}),
);
throw errors;
}
}
static invariant(
condition: unknown,
options: Omit<CompilerDiagnosticOptions, 'category'>,
@@ -576,7 +606,9 @@ function printErrorSummary(category: ErrorCategory, message: string): string {
case ErrorCategory.Suppression:
case ErrorCategory.Syntax:
case ErrorCategory.UseMemo:
case ErrorCategory.VoidUseMemo: {
case ErrorCategory.VoidUseMemo:
case ErrorCategory.MemoDependencies:
case ErrorCategory.EffectExhaustiveDependencies: {
heading = 'Error';
break;
}
@@ -634,6 +666,10 @@ export enum ErrorCategory {
* Checks that manual memoization is preserved
*/
PreserveManualMemo = 'PreserveManualMemo',
/**
* Checks for exhaustive useMemo/useCallback dependencies without extraneous values
*/
MemoDependencies = 'MemoDependencies',
/**
* Checks for known incompatible libraries
*/
@@ -654,6 +690,10 @@ export enum ErrorCategory {
* Checks for memoized effect deps
*/
EffectDependencies = 'EffectDependencies',
/**
* Checks for exhaustive and extraneous effect dependencies
*/
EffectExhaustiveDependencies = 'EffectExhaustiveDependencies',
/**
* Checks for no setState in effect bodies
*/
@@ -809,6 +849,16 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
preset: LintRulePreset.Off,
};
}
case ErrorCategory.EffectExhaustiveDependencies: {
return {
category,
severity: ErrorSeverity.Error,
name: 'exhaustive-effect-dependencies',
description:
'Validates that effect dependencies are exhaustive and without extraneous values',
preset: LintRulePreset.Off,
};
}
case ErrorCategory.EffectDerivationsOfState: {
return {
category,
@@ -825,7 +875,9 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
severity: ErrorSeverity.Error,
name: 'set-state-in-effect',
description:
'Validates against calling setState synchronously in an effect, which can lead to re-renders that degrade performance',
'Validates against calling setState synchronously in an effect. ' +
'This can indicate non-local derived data, a derived event pattern, or ' +
'improper external data synchronization.',
preset: LintRulePreset.Recommended,
};
}
@@ -1031,6 +1083,24 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
preset: LintRulePreset.RecommendedLatest,
};
}
case ErrorCategory.MemoDependencies: {
return {
category,
severity: ErrorSeverity.Error,
name: 'memo-dependencies',
description:
'Validates that useMemo() and useCallback() specify comprehensive dependencies without extraneous values. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
/**
* TODO: the "MemoDependencies" rule largely reimplements the "exhaustive-deps" non-compiler rule,
* allowing the compiler to ensure it does not regress change behavior due to different dependencies.
* We previously relied on the source having ESLint suppressions for any exhaustive-deps violations,
* but it's more reliable to verify it within the compiler.
*
* Long-term we should de-duplicate these implementations.
*/
preset: LintRulePreset.Off,
};
}
case ErrorCategory.IncompatibleLibrary: {
return {
category,

View File

@@ -102,14 +102,25 @@ export type PluginOptions = Partial<{
panicThreshold: PanicThresholdOptions;
/*
/**
* @deprecated
*
* When enabled, Forget will continue statically analyzing and linting code, but skip over codegen
* passes.
*
* NOTE: ignored if `outputMode` is specified
*
* Defaults to false
*/
noEmit: boolean;
/**
* If specified, overrides `noEmit` and controls the output mode of the compiler.
*
* Defaults to null
*/
outputMode: CompilerOutputMode | null;
/*
* Determines the strategy for determining which functions to compile. Note that regardless of
* which mode is enabled, a component can be opted out by adding the string literal
@@ -212,6 +223,19 @@ const CompilationModeSchema = z.enum([
export type CompilationMode = z.infer<typeof CompilationModeSchema>;
const CompilerOutputModeSchema = z.enum([
// Build optimized for SSR, with client features removed
'ssr',
// Build optimized for the client, with auto memoization
'client',
// Build optimized for the client without auto memo
'client-no-memo',
// Lint mode, the output is unused but validations should run
'lint',
]);
export type CompilerOutputMode = z.infer<typeof CompilerOutputModeSchema>;
/**
* Represents 'events' that may occur during compilation. Events are only
* recorded when a logger is set (through the config).
@@ -293,6 +317,7 @@ export const defaultOptions: ParsedPluginOptions = {
logger: null,
gating: null,
noEmit: false,
outputMode: null,
dynamicGating: null,
eslintSuppressionRules: null,
flowSuppressions: true,

View File

@@ -8,7 +8,7 @@
import {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import prettyFormat from 'pretty-format';
import {Logger, ProgramContext} from '.';
import {CompilerOutputMode, Logger, ProgramContext} from '.';
import {
HIRFunction,
ReactiveFunction,
@@ -24,7 +24,6 @@ import {
pruneUnusedLabelsHIR,
} from '../HIR';
import {
CompilerMode,
Environment,
EnvironmentConfig,
ReactFunctionType,
@@ -97,7 +96,6 @@ import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHI
import {outlineJSX} from '../Optimization/OutlineJsx';
import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls';
import {transformFire} from '../Transform';
import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureFunctionsInRender';
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
@@ -105,6 +103,10 @@ import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRan
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
import {validateNoDerivedComputationsInEffects_exp} from '../Validation/ValidateNoDerivedComputationsInEffects_exp';
import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
import {optimizeForSSR} from '../Optimization/OptimizeForSSR';
import {validateExhaustiveDependencies} from '../Validation/ValidateExhaustiveDependencies';
import {validateSourceLocations} from '../Validation/ValidateSourceLocations';
import {validateNoImpureValuesInRender} from '../Validation/ValidateNoImpureValuesInRender';
export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -118,7 +120,7 @@ function run(
>,
config: EnvironmentConfig,
fnType: ReactFunctionType,
mode: CompilerMode,
mode: CompilerOutputMode,
programContext: ProgramContext,
logger: Logger | null,
filename: string | null,
@@ -168,7 +170,7 @@ function runWithEnvironment(
validateUseMemo(hir).unwrap();
if (
env.isInferredMemoEnabled &&
env.enableDropManualMemoization &&
!env.config.enablePreserveExistingManualUseMemo &&
!env.config.disableMemoizationForDebugging &&
!env.config.enableChangeDetectionForDebugging
@@ -204,7 +206,7 @@ function runWithEnvironment(
inferTypes(hir);
log({kind: 'hir', name: 'InferTypes', value: hir});
if (env.isInferredMemoEnabled) {
if (env.enableValidations) {
if (env.config.validateHooksUsage) {
validateHooksUsage(hir).unwrap();
}
@@ -230,12 +232,17 @@ function runWithEnvironment(
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
if (env.isInferredMemoEnabled) {
if (env.enableValidations) {
if (mutabilityAliasingErrors.isErr()) {
throw mutabilityAliasingErrors.unwrapErr();
}
}
if (env.outputMode === 'ssr') {
optimizeForSSR(hir);
log({kind: 'hir', name: 'OptimizeForSSR', value: hir});
}
// Note: Has to come after infer reference effects because "dead" code may still affect inference
deadCodeElimination(hir);
log({kind: 'hir', name: 'DeadCodeElimination', value: hir});
@@ -252,44 +259,48 @@ function runWithEnvironment(
isFunctionExpression: false,
});
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
if (env.isInferredMemoEnabled) {
if (env.enableValidations) {
if (mutabilityAliasingRangeErrors.isErr()) {
throw mutabilityAliasingRangeErrors.unwrapErr();
}
validateLocalsNotReassignedAfterRender(hir);
}
if (env.isInferredMemoEnabled) {
if (env.enableValidations) {
if (env.config.assertValidMutableRanges) {
assertValidMutableRanges(hir);
}
if (env.config.validateRefAccessDuringRender) {
validateNoRefAccessInRender(hir).unwrap();
}
if (env.config.validateNoSetStateInRender) {
validateNoSetStateInRender(hir).unwrap();
}
if (env.config.validateNoDerivedComputationsInEffects) {
if (
env.config.validateNoDerivedComputationsInEffects_exp &&
env.outputMode === 'lint'
) {
env.logErrors(validateNoDerivedComputationsInEffects_exp(hir));
} else if (env.config.validateNoDerivedComputationsInEffects) {
validateNoDerivedComputationsInEffects(hir);
}
if (env.config.validateNoDerivedComputationsInEffects_exp) {
env.logErrors(validateNoDerivedComputationsInEffects_exp(hir));
}
if (env.config.validateNoSetStateInEffects) {
if (env.config.validateNoSetStateInEffects && env.outputMode === 'lint') {
env.logErrors(validateNoSetStateInEffects(hir, env));
}
if (env.config.validateNoJSXInTryStatements) {
if (env.config.validateNoJSXInTryStatements && env.outputMode === 'lint') {
env.logErrors(validateNoJSXInTryStatement(hir));
}
if (env.config.validateNoImpureFunctionsInRender) {
validateNoImpureFunctionsInRender(hir).unwrap();
if (
env.config.validateNoImpureFunctionsInRender ||
env.config.validateRefAccessDuringRender
) {
validateNoImpureValuesInRender(hir).unwrap();
}
if (env.config.validateRefAccessDuringRender) {
validateNoRefAccessInRender(hir).unwrap();
}
validateNoFreezingKnownMutableFunctions(hir).unwrap();
@@ -298,6 +309,16 @@ function runWithEnvironment(
inferReactivePlaces(hir);
log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
if (env.enableValidations) {
if (
env.config.validateExhaustiveMemoizationDependencies ||
env.config.validateExhaustiveEffectDependencies
) {
// NOTE: this relies on reactivity inference running first
validateExhaustiveDependencies(hir).unwrap();
}
}
rewriteInstructionKindsBasedOnReassignment(hir);
log({
kind: 'hir',
@@ -305,11 +326,15 @@ function runWithEnvironment(
value: hir,
});
if (env.isInferredMemoEnabled) {
if (env.config.validateStaticComponents) {
env.logErrors(validateStaticComponents(hir));
}
if (
env.enableValidations &&
env.config.validateStaticComponents &&
env.outputMode === 'lint'
) {
env.logErrors(validateStaticComponents(hir));
}
if (env.enableMemoization) {
/**
* Only create reactive scopes (which directly map to generated memo blocks)
* if inferred memoization is enabled. This makes all later passes which
@@ -559,6 +584,10 @@ function runWithEnvironment(
log({kind: 'ast', name: 'Codegen (outlined)', value: outlined.fn});
}
if (env.config.validateSourceLocations) {
validateSourceLocations(func, ast).unwrap();
}
/**
* This flag should be only set for unit / fixture tests to check
* that Forget correctly handles unexpected errors (e.g. exceptions
@@ -577,7 +606,7 @@ export function compileFn(
>,
config: EnvironmentConfig,
fnType: ReactFunctionType,
mode: CompilerMode,
mode: CompilerOutputMode,
programContext: ProgramContext,
logger: Logger | null,
filename: string | null,

View File

@@ -24,6 +24,7 @@ import {
validateRestrictedImports,
} from './Imports';
import {
CompilerOutputMode,
CompilerReactTarget,
ParsedPluginOptions,
PluginOptions,
@@ -399,7 +400,15 @@ export function compileProgram(
*/
const suppressions = findProgramSuppressions(
pass.comments,
pass.opts.eslintSuppressionRules ?? DEFAULT_ESLINT_SUPPRESSIONS,
/*
* If the compiler is validating hooks rules and exhaustive memo dependencies, we don't need to check
* for React ESLint suppressions
*/
pass.opts.environment.validateExhaustiveMemoizationDependencies &&
pass.opts.environment.validateHooksUsage
? null
: (pass.opts.eslintSuppressionRules ?? DEFAULT_ESLINT_SUPPRESSIONS),
// Always bail on Flow suppressions
pass.opts.flowSuppressions,
);
@@ -421,9 +430,17 @@ export function compileProgram(
);
const compiledFns: Array<CompileResult> = [];
// outputMode takes precedence if specified
const outputMode: CompilerOutputMode =
pass.opts.outputMode ?? (pass.opts.noEmit ? 'lint' : 'client');
while (queue.length !== 0) {
const current = queue.shift()!;
const compiled = processFn(current.fn, current.fnType, programContext);
const compiled = processFn(
current.fn,
current.fnType,
programContext,
outputMode,
);
if (compiled != null) {
for (const outlined of compiled.outlined) {
@@ -581,6 +598,7 @@ function processFn(
fn: BabelFn,
fnType: ReactFunctionType,
programContext: ProgramContext,
outputMode: CompilerOutputMode,
): null | CodegenFunction {
let directives: {
optIn: t.Directive | null;
@@ -616,18 +634,27 @@ function processFn(
}
let compiledFn: CodegenFunction;
const compileResult = tryCompileFunction(fn, fnType, programContext);
const compileResult = tryCompileFunction(
fn,
fnType,
programContext,
outputMode,
);
if (compileResult.kind === 'error') {
if (directives.optOut != null) {
logError(compileResult.error, programContext, fn.node.loc ?? null);
} else {
handleError(compileResult.error, programContext, fn.node.loc ?? null);
}
const retryResult = retryCompileFunction(fn, fnType, programContext);
if (retryResult == null) {
if (outputMode === 'client') {
const retryResult = retryCompileFunction(fn, fnType, programContext);
if (retryResult == null) {
return null;
}
compiledFn = retryResult;
} else {
return null;
}
compiledFn = retryResult;
} else {
compiledFn = compileResult.compiledFn;
}
@@ -663,7 +690,7 @@ function processFn(
if (programContext.hasModuleScopeOptOut) {
return null;
} else if (programContext.opts.noEmit) {
} else if (programContext.opts.outputMode === 'lint') {
/**
* inferEffectDependencies + noEmit is currently only used for linting. In
* this mode, add source locations for where the compiler *can* infer effect
@@ -693,6 +720,7 @@ function tryCompileFunction(
fn: BabelFn,
fnType: ReactFunctionType,
programContext: ProgramContext,
outputMode: CompilerOutputMode,
):
| {kind: 'compile'; compiledFn: CodegenFunction}
| {kind: 'error'; error: unknown} {
@@ -719,7 +747,7 @@ function tryCompileFunction(
fn,
programContext.opts.environment,
fnType,
'all_features',
outputMode,
programContext,
programContext.opts.logger,
programContext.filename,
@@ -757,7 +785,7 @@ function retryCompileFunction(
fn,
environment,
fnType,
'no_inferred_memo',
'client-no-memo',
programContext,
programContext.opts.logger,
programContext.filename,

View File

@@ -78,7 +78,7 @@ export function filterSuppressionsThatAffectFunction(
export function findProgramSuppressions(
programComments: Array<t.Comment>,
ruleNames: Array<string>,
ruleNames: Array<string> | null,
flowSuppressions: boolean,
): Array<SuppressionRange> {
const suppressionRanges: Array<SuppressionRange> = [];
@@ -89,7 +89,7 @@ export function findProgramSuppressions(
let disableNextLinePattern: RegExp | null = null;
let disablePattern: RegExp | null = null;
let enablePattern: RegExp | null = null;
if (ruleNames.length !== 0) {
if (ruleNames != null && ruleNames.length !== 0) {
const rulePattern = `(${ruleNames.join('|')})`;
disableNextLinePattern = new RegExp(
`eslint-disable-next-line ${rulePattern}`,

View File

@@ -4026,6 +4026,7 @@ function lowerAssignment(
pattern: {
kind: 'ArrayPattern',
items,
loc: lvalue.node.loc ?? GeneratedSource,
},
},
value,
@@ -4203,6 +4204,7 @@ function lowerAssignment(
pattern: {
kind: 'ObjectPattern',
properties,
loc: lvalue.node.loc ?? GeneratedSource,
},
},
value,

View File

@@ -9,7 +9,7 @@ import * as t from '@babel/types';
import {ZodError, z} from 'zod/v4';
import {fromZodError} from 'zod-validation-error/v4';
import {CompilerError} from '../CompilerError';
import {Logger, ProgramContext} from '../Entrypoint';
import {CompilerOutputMode, Logger, ProgramContext} from '../Entrypoint';
import {Err, Ok, Result} from '../Utils/Result';
import {
DEFAULT_GLOBALS,
@@ -51,6 +51,7 @@ import {Scope as BabelScope, NodePath} from '@babel/traverse';
import {TypeSchema} from './TypeSchema';
import {FlowTypeEnv} from '../Flood/Types';
import {defaultModuleTypeProvider} from './DefaultModuleTypeProvider';
import {assertExhaustive} from '../Utils/utils';
export const ReactElementSymbolSchema = z.object({
elementSymbol: z.union([
@@ -217,6 +218,23 @@ export const EnvironmentConfigSchema = z.object({
*/
validatePreserveExistingMemoizationGuarantees: z.boolean().default(true),
/**
* Validate that dependencies supplied to manual memoization calls are exhaustive.
*/
validateExhaustiveMemoizationDependencies: z.boolean().default(true),
/**
* Validate that dependencies supplied to effect hooks are exhaustive.
* Can be:
* - 'off': No validation (default)
* - 'all': Validate and report both missing and extra dependencies
* - 'missing-only': Only report missing dependencies
* - 'extra-only': Only report extra/unnecessary dependencies
*/
validateExhaustiveEffectDependencies: z
.enum(['off', 'all', 'missing-only', 'extra-only'])
.default('off'),
/**
* When this is true, rather than pruning existing manual memoization but ensuring or validating
* that the memoized values remain memoized, the compiler will simply not prune existing calls to
@@ -312,6 +330,12 @@ export const EnvironmentConfigSchema = z.object({
*/
validateNoSetStateInRender: z.boolean().default(true),
/**
* When enabled, changes the behavior of validateNoSetStateInRender to recommend
* using useKeyedState instead of the manual pattern for resetting state.
*/
enableUseKeyedState: z.boolean().default(false),
/**
* Validates that setState is not called synchronously within an effect (useEffect and friends).
* Scheduling a setState (with an event listener, subscription, etc) is valid.
@@ -364,6 +388,13 @@ export const EnvironmentConfigSchema = z.object({
validateNoCapitalizedCalls: z.nullable(z.array(z.string())).default(null),
validateBlocklistedImports: z.nullable(z.array(z.string())).default(null),
/**
* Validates that AST nodes generated during codegen have proper source locations.
* This is useful for debugging issues with source maps and Istanbul coverage.
* When enabled, the compiler will error if important source locations are missing in the generated AST.
*/
validateSourceLocations: z.boolean().default(false),
/**
* Validate against impure functions called during render
*/
@@ -665,11 +696,35 @@ export const EnvironmentConfigSchema = z.object({
validateNoDynamicallyCreatedComponentsOrHooks: z.boolean().default(false),
/**
* When enabled, allows setState calls in effects when the value being set is
* derived from a ref. This is useful for patterns where initial layout measurements
* from refs need to be stored in state during mount.
* When enabled, allows setState calls in effects based on valid patterns involving refs:
* - Allow setState where the value being set is derived from a ref. This is useful where
* state needs to take into account layer information, and a layout effect reads layout
* data from a ref and sets state.
* - Allow conditionally calling setState after manually comparing previous/new values
* for changes via a ref. Relying on effect deps is insufficient for non-primitive values,
* so a ref is generally required to manually track previous values and compare prev/next
* for meaningful changes before setting state.
*/
enableAllowSetStateFromRefsInEffects: z.boolean().default(true),
/**
* When enabled, provides verbose error messages for setState calls within effects,
* presenting multiple possible fixes to the user/agent since we cannot statically
* determine which specific use-case applies:
* 1. Non-local derived data - requires restructuring state ownership
* 2. Derived event pattern - detecting when a prop changes
* 3. Force update / external sync - should use useSyncExternalStore
*/
enableVerboseNoSetStateInEffect: z.boolean().default(false),
/**
* Enables inference of event handler types for JSX props on built-in DOM elements.
* When enabled, functions passed to event handler props (props starting with "on")
* on primitive JSX tags are inferred to have the BuiltinEventHandlerId type, which
* allows ref access within those functions since DOM event handlers are guaranteed
* by React to only execute in response to events, not during render.
*/
enableInferEventHandlers: z.boolean().default(false),
});
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
@@ -709,7 +764,7 @@ export class Environment {
code: string | null;
config: EnvironmentConfig;
fnType: ReactFunctionType;
compilerMode: CompilerMode;
outputMode: CompilerOutputMode;
programContext: ProgramContext;
hasFireRewrite: boolean;
hasInferredEffect: boolean;
@@ -724,7 +779,7 @@ export class Environment {
constructor(
scope: BabelScope,
fnType: ReactFunctionType,
compilerMode: CompilerMode,
outputMode: CompilerOutputMode,
config: EnvironmentConfig,
contextIdentifiers: Set<t.Identifier>,
parentFunction: NodePath<t.Function>, // the outermost function being compiled
@@ -735,7 +790,7 @@ export class Environment {
) {
this.#scope = scope;
this.fnType = fnType;
this.compilerMode = compilerMode;
this.outputMode = outputMode;
this.config = config;
this.filename = filename;
this.code = code;
@@ -831,8 +886,65 @@ export class Environment {
return this.#flowTypeEnvironment;
}
get isInferredMemoEnabled(): boolean {
return this.compilerMode !== 'no_inferred_memo';
get enableDropManualMemoization(): boolean {
switch (this.outputMode) {
case 'lint': {
// linting drops to be more compatible with compiler analysis
return true;
}
case 'client':
case 'ssr': {
return true;
}
case 'client-no-memo': {
return false;
}
default: {
assertExhaustive(
this.outputMode,
`Unexpected output mode '${this.outputMode}'`,
);
}
}
}
get enableMemoization(): boolean {
switch (this.outputMode) {
case 'client':
case 'lint': {
// linting also enables memoization so that we can check if manual memoization is preserved
return true;
}
case 'ssr':
case 'client-no-memo': {
return false;
}
default: {
assertExhaustive(
this.outputMode,
`Unexpected output mode '${this.outputMode}'`,
);
}
}
}
get enableValidations(): boolean {
switch (this.outputMode) {
case 'client':
case 'lint':
case 'ssr': {
return true;
}
case 'client-no-memo': {
return false;
}
default: {
assertExhaustive(
this.outputMode,
`Unexpected output mode '${this.outputMode}'`,
);
}
}
}
get nextIdentifierId(): IdentifierId {

View File

@@ -23,13 +23,14 @@ import {
BuiltInUseInsertionEffectHookId,
BuiltInUseLayoutEffectHookId,
BuiltInUseOperatorId,
BuiltInUseOptimisticId,
BuiltInUseReducerId,
BuiltInUseRefId,
BuiltInUseStateId,
BuiltInUseTransitionId,
BuiltInWeakMapId,
BuiltInWeakSetId,
BuiltinEffectEventId,
BuiltInEffectEventId,
ReanimatedSharedValueId,
ShapeRegistry,
addFunction,
@@ -37,7 +38,7 @@ import {
addObject,
} from './ObjectShape';
import {BuiltInType, ObjectType, PolyType} from './Types';
import {TypeConfig} from './TypeSchema';
import {AliasingSignatureConfig, TypeConfig} from './TypeSchema';
import {assertExhaustive} from '../Utils/utils';
import {isHookName} from './Environment';
import {CompilerError, SourceLocation} from '..';
@@ -625,11 +626,136 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
// TODO: rest of Global objects
];
const RenderHookAliasing: (
reason: ValueReason,
) => AliasingSignatureConfig = reason => ({
receiver: '@receiver',
params: [],
rest: '@rest',
returns: '@returns',
temporaries: [],
effects: [
// Freeze the arguments
{
kind: 'Freeze',
value: '@rest',
reason: ValueReason.HookCaptured,
},
// Render the arguments
{
kind: 'Render',
place: '@rest',
},
// Returns a frozen value
{
kind: 'Create',
into: '@returns',
value: ValueKind.Frozen,
reason,
},
// May alias any arguments into the return
{
kind: 'Alias',
from: '@rest',
into: '@returns',
},
],
});
const EffectHookAliasing: AliasingSignatureConfig = {
receiver: '@receiver',
params: ['@fn', '@deps'],
rest: '@rest',
returns: '@returns',
temporaries: ['@effect'],
effects: [
// Freezes the function and deps
{
kind: 'Freeze',
value: '@rest',
reason: ValueReason.Effect,
},
{
kind: 'Freeze',
value: '@fn',
reason: ValueReason.Effect,
},
{
kind: 'Freeze',
value: '@deps',
reason: ValueReason.Effect,
},
// Deps are accessed during render
{
kind: 'Render',
place: '@deps',
},
// Internally creates an effect object that captures the function and deps
{
kind: 'Create',
into: '@effect',
value: ValueKind.Frozen,
reason: ValueReason.KnownReturnSignature,
},
// The effect stores the function and dependencies
{
kind: 'Capture',
from: '@rest',
into: '@effect',
},
{
kind: 'Capture',
from: '@fn',
into: '@effect',
},
// Returns undefined
{
kind: 'Create',
into: '@returns',
value: ValueKind.Primitive,
reason: ValueReason.KnownReturnSignature,
},
],
};
/*
* TODO(mofeiZ): We currently only store rest param effects for hooks.
* now that FeatureFlag `enableTreatHooksAsFunctions` is removed we can
* use positional params too (?)
*/
const useEffectEvent = addHook(
DEFAULT_SHAPES,
{
positionalParams: [],
restParam: Effect.Freeze,
returnType: {
kind: 'Function',
return: {kind: 'Poly'},
shapeId: BuiltInEffectEventId,
isConstructor: false,
},
calleeEffect: Effect.Read,
hookKind: 'useEffectEvent',
// Frozen because it should not mutate any locally-bound values
returnValueKind: ValueKind.Frozen,
aliasing: {
receiver: '@receiver',
params: ['@value'],
rest: null,
returns: '@return',
temporaries: [],
effects: [
{kind: 'Assign', from: '@value', into: '@return'},
{
kind: 'Freeze',
value: '@value',
reason: ValueReason.HookCaptured,
},
],
},
},
BuiltInUseEffectEventId,
);
const REACT_APIS: Array<[string, BuiltInType]> = [
[
'useContext',
@@ -643,6 +769,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
hookKind: 'useContext',
returnValueKind: ValueKind.Frozen,
returnValueReason: ValueReason.Context,
aliasing: RenderHookAliasing(ValueReason.Context),
},
BuiltInUseContextHookId,
),
@@ -657,6 +784,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
hookKind: 'useState',
returnValueKind: ValueKind.Frozen,
returnValueReason: ValueReason.State,
aliasing: RenderHookAliasing(ValueReason.State),
}),
],
[
@@ -669,6 +797,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
hookKind: 'useActionState',
returnValueKind: ValueKind.Frozen,
returnValueReason: ValueReason.State,
aliasing: RenderHookAliasing(ValueReason.HookCaptured),
}),
],
[
@@ -681,6 +810,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
hookKind: 'useReducer',
returnValueKind: ValueKind.Frozen,
returnValueReason: ValueReason.ReducerState,
aliasing: RenderHookAliasing(ValueReason.ReducerState),
}),
],
[
@@ -692,6 +822,22 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
calleeEffect: Effect.Read,
hookKind: 'useRef',
returnValueKind: ValueKind.Mutable,
aliasing: {
receiver: '@receiver',
params: [],
rest: '@rest',
returns: '@return',
temporaries: [],
effects: [
{
kind: 'Create',
into: '@return',
value: ValueKind.Mutable,
reason: ValueReason.KnownReturnSignature,
},
{kind: 'Capture', from: '@rest', into: '@return'},
],
},
}),
],
[
@@ -714,6 +860,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
calleeEffect: Effect.Read,
hookKind: 'useMemo',
returnValueKind: ValueKind.Frozen,
aliasing: RenderHookAliasing(ValueReason.HookCaptured),
}),
],
[
@@ -721,10 +868,16 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
addHook(DEFAULT_SHAPES, {
positionalParams: [],
restParam: Effect.Freeze,
returnType: {kind: 'Poly'},
returnType: {
kind: 'Function',
isConstructor: false,
return: {kind: 'Poly'},
shapeId: null,
},
calleeEffect: Effect.Read,
hookKind: 'useCallback',
returnValueKind: ValueKind.Frozen,
aliasing: RenderHookAliasing(ValueReason.HookCaptured),
}),
],
[
@@ -738,41 +891,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
calleeEffect: Effect.Read,
hookKind: 'useEffect',
returnValueKind: ValueKind.Frozen,
aliasing: {
receiver: '@receiver',
params: [],
rest: '@rest',
returns: '@returns',
temporaries: ['@effect'],
effects: [
// Freezes the function and deps
{
kind: 'Freeze',
value: '@rest',
reason: ValueReason.Effect,
},
// Internally creates an effect object that captures the function and deps
{
kind: 'Create',
into: '@effect',
value: ValueKind.Frozen,
reason: ValueReason.KnownReturnSignature,
},
// The effect stores the function and dependencies
{
kind: 'Capture',
from: '@rest',
into: '@effect',
},
// Returns undefined
{
kind: 'Create',
into: '@returns',
value: ValueKind.Primitive,
reason: ValueReason.KnownReturnSignature,
},
],
},
aliasing: EffectHookAliasing,
},
BuiltInUseEffectHookId,
),
@@ -788,6 +907,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
calleeEffect: Effect.Read,
hookKind: 'useLayoutEffect',
returnValueKind: ValueKind.Frozen,
aliasing: EffectHookAliasing,
},
BuiltInUseLayoutEffectHookId,
),
@@ -803,6 +923,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
calleeEffect: Effect.Read,
hookKind: 'useInsertionEffect',
returnValueKind: ValueKind.Frozen,
aliasing: EffectHookAliasing,
},
BuiltInUseInsertionEffectHookId,
),
@@ -816,6 +937,20 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
calleeEffect: Effect.Read,
hookKind: 'useTransition',
returnValueKind: ValueKind.Frozen,
aliasing: RenderHookAliasing(ValueReason.HookCaptured),
}),
],
[
'useOptimistic',
addHook(DEFAULT_SHAPES, {
positionalParams: [],
restParam: Effect.Freeze,
returnType: {kind: 'Object', shapeId: BuiltInUseOptimisticId},
calleeEffect: Effect.Read,
hookKind: 'useOptimistic',
returnValueKind: ValueKind.Frozen,
returnValueReason: ValueReason.State,
aliasing: RenderHookAliasing(ValueReason.HookCaptured),
}),
],
[
@@ -829,6 +964,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
returnType: {kind: 'Poly'},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Frozen,
aliasing: RenderHookAliasing(ValueReason.HookCaptured),
},
BuiltInUseOperatorId,
),
@@ -853,27 +989,8 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
BuiltInFireId,
),
],
[
'useEffectEvent',
addHook(
DEFAULT_SHAPES,
{
positionalParams: [],
restParam: Effect.Freeze,
returnType: {
kind: 'Function',
return: {kind: 'Poly'},
shapeId: BuiltinEffectEventId,
isConstructor: false,
},
calleeEffect: Effect.Read,
hookKind: 'useEffectEvent',
// Frozen because it should not mutate any locally-bound values
returnValueKind: ValueKind.Frozen,
},
BuiltInUseEffectEventId,
),
],
['useEffectEvent', useEffectEvent],
['experimental_useEffectEvent', useEffectEvent],
['AUTODEPS', addObject(DEFAULT_SHAPES, BuiltInAutodepsId, [])],
];

View File

@@ -694,11 +694,13 @@ export type SpreadPattern = {
export type ArrayPattern = {
kind: 'ArrayPattern';
items: Array<Place | SpreadPattern | Hole>;
loc: SourceLocation;
};
export type ObjectPattern = {
kind: 'ObjectPattern';
properties: Array<ObjectProperty | SpreadPattern>;
loc: SourceLocation;
};
export type ObjectPropertyKey =
@@ -803,9 +805,11 @@ export type ManualMemoDependency = {
| {
kind: 'NamedLocal';
value: Place;
constant: boolean;
}
| {kind: 'Global'; identifierName: string};
path: DependencyPath;
loc: SourceLocation;
};
export type StartMemoize = {
@@ -817,6 +821,11 @@ export type StartMemoize = {
* (e.g. useMemo without a second arg)
*/
deps: Array<ManualMemoDependency> | null;
/**
* The source location of the dependencies argument. Used for
* emitting diagnostics with a suggested replacement
*/
depsLoc: SourceLocation | null;
loc: SourceLocation;
};
export type FinishMemoize = {
@@ -1680,6 +1689,28 @@ export function areEqualPaths(a: DependencyPath, b: DependencyPath): boolean {
)
);
}
export function isSubPath(
subpath: DependencyPath,
path: DependencyPath,
): boolean {
return (
subpath.length <= path.length &&
subpath.every(
(item, ix) =>
item.property === path[ix].property &&
item.optional === path[ix].optional,
)
);
}
export function isSubPathIgnoringOptionals(
subpath: DependencyPath,
path: DependencyPath,
): boolean {
return (
subpath.length <= path.length &&
subpath.every((item, ix) => item.property === path[ix].property)
);
}
export function getPlaceScope(
id: InstructionId,
@@ -1823,6 +1854,10 @@ export function isPrimitiveType(id: Identifier): boolean {
return id.type.kind === 'Primitive';
}
export function isPlainObjectType(id: Identifier): boolean {
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInObject';
}
export function isArrayType(id: Identifier): boolean {
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInArray';
}
@@ -1844,7 +1879,15 @@ export function isRefValueType(id: Identifier): boolean {
}
export function isUseRefType(id: Identifier): boolean {
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseRefId';
return isUseRefType_(id.type);
}
export function isUseRefType_(type: Type): boolean {
return (
(type.kind === 'Object' && type.shapeId === 'BuiltInUseRefId') ||
(type.kind === 'Phi' &&
type.operands.some(operand => isUseRefType_(operand)))
);
}
export function isUseStateType(id: Identifier): boolean {
@@ -1855,6 +1898,13 @@ export function isJsxType(type: Type): boolean {
return type.kind === 'Object' && type.shapeId === 'BuiltInJsx';
}
export function isJsxOrJsxUnionType(type: Type): boolean {
return (
(type.kind === 'Object' && type.shapeId === 'BuiltInJsx') ||
(type.kind === 'Phi' && type.operands.some(op => isJsxOrJsxUnionType(op)))
);
}
export function isRefOrRefValue(id: Identifier): boolean {
return isUseRefType(id) || isRefValueType(id);
}
@@ -1887,6 +1937,18 @@ export function isStartTransitionType(id: Identifier): boolean {
);
}
export function isUseOptimisticType(id: Identifier): boolean {
return (
id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseOptimistic'
);
}
export function isSetOptimisticType(id: Identifier): boolean {
return (
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetOptimistic'
);
}
export function isSetActionStateType(id: Identifier): boolean {
return (
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetActionState'
@@ -1920,7 +1982,8 @@ export function isStableType(id: Identifier): boolean {
isSetActionStateType(id) ||
isDispatcherType(id) ||
isUseRefType(id) ||
isStartTransitionType(id)
isStartTransitionType(id) ||
isSetOptimisticType(id)
);
}
@@ -1931,8 +1994,9 @@ export function isStableTypeContainer(id: Identifier): boolean {
}
return (
isUseStateType(id) || // setState
type_.shapeId === 'BuiltInUseActionState' || // setActionState
isUseActionStateType(id) || // setActionState
isUseReducerType(id) || // dispatcher
isUseOptimisticType(id) || // setOptimistic
type_.shapeId === 'BuiltInUseTransition' // startTransition
);
}
@@ -1952,6 +2016,7 @@ export function evaluatesToStableTypeOrContainer(
case 'useActionState':
case 'useRef':
case 'useTransition':
case 'useOptimistic':
return true;
}
}
@@ -1975,6 +2040,11 @@ export function isUseInsertionEffectHookType(id: Identifier): boolean {
id.type.shapeId === 'BuiltInUseInsertionEffectHook'
);
}
export function isUseEffectEventType(id: Identifier): boolean {
return (
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInUseEffectEvent'
);
}
export function isUseContextHookType(id: Identifier): boolean {
return (
@@ -2003,4 +2073,23 @@ export function getHookKindForType(
return null;
}
export function areEqualSourceLocations(
loc1: SourceLocation,
loc2: SourceLocation,
): boolean {
if (typeof loc1 === 'symbol' || typeof loc2 === 'symbol') {
return false;
}
return (
loc1.filename === loc2.filename &&
loc1.identifierName === loc2.identifierName &&
loc1.start.line === loc2.start.line &&
loc1.start.column === loc2.start.column &&
loc1.start.index === loc2.start.index &&
loc1.end.line === loc2.end.line &&
loc1.end.column === loc2.end.column &&
loc1.end.index === loc2.end.index
);
}
export * from './Types';

View File

@@ -988,7 +988,7 @@ export function createTemporaryPlace(
identifier: makeTemporaryIdentifier(env.nextIdentifierId, loc),
reactive: false,
effect: Effect.Unknown,
loc: GeneratedSource,
loc,
};
}

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError} from '../CompilerError';
import {CompilerError, ErrorCategory} from '../CompilerError';
import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects';
import {assertExhaustive} from '../Utils/utils';
import {
@@ -190,14 +190,22 @@ function parseAliasingSignatureConfig(
};
}
case 'Impure': {
const place = lookup(effect.place);
const into = lookup(effect.into);
return {
kind: 'Impure',
into,
category: ErrorCategory.Purity,
description: effect.description,
reason: effect.reason,
sourceMessage: effect.sourceMessage,
usageMessage: effect.usageMessage,
};
}
case 'Render': {
const place = lookup(effect.place);
return {
kind: 'Render',
place,
error: CompilerError.throwTodo({
reason: 'Support impure effect declarations',
loc: GeneratedSource,
}),
};
}
case 'Apply': {
@@ -304,6 +312,7 @@ export type HookKind =
| 'useTransition'
| 'useImperativeHandle'
| 'useEffectEvent'
| 'useOptimistic'
| 'Custom';
/*
@@ -399,12 +408,15 @@ export const BuiltInUseReducerId = 'BuiltInUseReducer';
export const BuiltInDispatchId = 'BuiltInDispatch';
export const BuiltInUseContextHookId = 'BuiltInUseContextHook';
export const BuiltInUseTransitionId = 'BuiltInUseTransition';
export const BuiltInUseOptimisticId = 'BuiltInUseOptimistic';
export const BuiltInSetOptimisticId = 'BuiltInSetOptimistic';
export const BuiltInStartTransitionId = 'BuiltInStartTransition';
export const BuiltInFireId = 'BuiltInFire';
export const BuiltInFireFunctionId = 'BuiltInFireFunction';
export const BuiltInUseEffectEventId = 'BuiltInUseEffectEvent';
export const BuiltinEffectEventId = 'BuiltInEffectEventFunction';
export const BuiltInEffectEventId = 'BuiltInEffectEventFunction';
export const BuiltInAutodepsId = 'BuiltInAutoDepsId';
export const BuiltInEventHandlerId = 'BuiltInEventHandlerId';
// See getReanimatedModuleType() in Globals.ts — this is part of supporting Reanimated's ref-like types
export const ReanimatedSharedValueId = 'ReanimatedSharedValueId';
@@ -1185,6 +1197,25 @@ addObject(BUILTIN_SHAPES, BuiltInUseTransitionId, [
],
]);
addObject(BUILTIN_SHAPES, BuiltInUseOptimisticId, [
['0', {kind: 'Poly'}],
[
'1',
addFunction(
BUILTIN_SHAPES,
[],
{
positionalParams: [],
restParam: Effect.Freeze,
returnType: PRIMITIVE_TYPE,
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
},
BuiltInSetOptimisticId,
),
],
]);
addObject(BUILTIN_SHAPES, BuiltInUseActionStateId, [
['0', {kind: 'Poly'}],
[
@@ -1243,7 +1274,20 @@ addFunction(
calleeEffect: Effect.ConditionallyMutate,
returnValueKind: ValueKind.Mutable,
},
BuiltinEffectEventId,
BuiltInEffectEventId,
);
addFunction(
BUILTIN_SHAPES,
[],
{
positionalParams: [],
restParam: Effect.ConditionallyMutate,
returnType: {kind: 'Poly'},
calleeEffect: Effect.ConditionallyMutate,
returnValueKind: ValueKind.Mutable,
},
BuiltInEventHandlerId,
);
/**
@@ -1477,6 +1521,11 @@ export const DefaultNonmutatingHook = addHook(
value: '@rest',
reason: ValueReason.HookCaptured,
},
// Render the arguments
{
kind: 'Render',
place: '@rest',
},
// Returns a frozen value
{
kind: 'Create',

View File

@@ -983,15 +983,15 @@ export function printAliasingEffect(effect: AliasingEffect): string {
return `...${printPlaceForAliasEffect(arg.place)}`;
})
.join(', ');
let signature = '';
if (effect.signature != null) {
if (effect.signature.aliasing != null) {
signature = printAliasingSignature(effect.signature.aliasing);
} else {
signature = JSON.stringify(effect.signature, null, 2);
}
}
return `Apply ${printPlaceForAliasEffect(effect.into)} = ${receiverCallee}(${args})${signature != '' ? '\n ' : ''}${signature}`;
// let signature = '';
// if (effect.signature != null) {
// if (effect.signature.aliasing != null) {
// signature = printAliasingSignature(effect.signature.aliasing);
// } else {
// signature = JSON.stringify(effect.signature, null, 2);
// }
// }
return `Apply ${printPlaceForAliasEffect(effect.into)} = ${receiverCallee}(${args})`;
}
case 'Freeze': {
return `Freeze ${printPlaceForAliasEffect(effect.value)} ${effect.reason}`;
@@ -1009,7 +1009,7 @@ export function printAliasingEffect(effect: AliasingEffect): string {
return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
}
case 'Impure': {
return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
return `Impure ${printPlaceForAliasEffect(effect.into)} reason=${effect.reason} description=${effect.description}`;
}
case 'Render': {
return `Render ${printPlaceForAliasEffect(effect.place)}`;

View File

@@ -185,11 +185,29 @@ export const ApplyEffectSchema: z.ZodType<ApplyEffectConfig> = z.object({
export type ImpureEffectConfig = {
kind: 'Impure';
place: string;
into: string;
reason: string;
description: string;
sourceMessage: string;
usageMessage: string;
};
export const ImpureEffectSchema: z.ZodType<ImpureEffectConfig> = z.object({
kind: z.literal('Impure'),
into: LifetimeIdSchema,
reason: z.string(),
description: z.string(),
sourceMessage: z.string(),
usageMessage: z.string(),
});
export type RenderEffectConfig = {
kind: 'Render';
place: string;
};
export const RenderEffectSchema: z.ZodType<RenderEffectConfig> = z.object({
kind: z.literal('Render'),
place: LifetimeIdSchema,
});
@@ -204,7 +222,8 @@ export type AliasingEffectConfig =
| ImpureEffectConfig
| MutateEffectConfig
| MutateTransitiveConditionallyConfig
| ApplyEffectConfig;
| ApplyEffectConfig
| RenderEffectConfig;
export const AliasingEffectSchema: z.ZodType<AliasingEffectConfig> = z.union([
FreezeEffectSchema,

View File

@@ -11,6 +11,7 @@ import {
BasicBlock,
BlockId,
Instruction,
InstructionKind,
InstructionValue,
makeInstructionId,
Pattern,
@@ -32,6 +33,32 @@ export function* eachInstructionLValue(
yield* eachInstructionValueLValue(instr.value);
}
export function* eachInstructionLValueWithKind(
instr: ReactiveInstruction,
): Iterable<[Place, InstructionKind]> {
switch (instr.value.kind) {
case 'DeclareContext':
case 'StoreContext':
case 'DeclareLocal':
case 'StoreLocal': {
yield [instr.value.lvalue.place, instr.value.lvalue.kind];
break;
}
case 'Destructure': {
const kind = instr.value.lvalue.kind;
for (const place of eachPatternOperand(instr.value.lvalue.pattern)) {
yield [place, kind];
}
break;
}
case 'PostfixUpdate':
case 'PrefixUpdate': {
yield [instr.value.lvalue, InstructionKind.Reassign];
break;
}
}
}
export function* eachInstructionValueLValue(
value: ReactiveValue,
): Iterable<Place> {

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerDiagnostic} from '../CompilerError';
import {CompilerDiagnostic, ErrorCategory} from '../CompilerError';
import {
FunctionExpression,
GeneratedSource,
@@ -162,7 +162,15 @@ export type AliasingEffect =
/**
* Indicates a side-effect that is not safe during render
*/
| {kind: 'Impure'; place: Place; error: CompilerDiagnostic}
| {
kind: 'Impure';
into: Place;
category: ErrorCategory;
reason: string;
description: string;
usageMessage: string;
sourceMessage: string;
}
/**
* Indicates that a given place is accessed during render. Used to distingush
* hook arguments that are known to be called immediately vs those used for
@@ -222,6 +230,14 @@ export function hashEffect(effect: AliasingEffect): string {
return [effect.kind, effect.value.identifier.id, effect.reason].join(':');
}
case 'Impure':
return [
effect.kind,
effect.into.identifier.id,
effect.reason,
effect.description,
effect.usageMessage,
effect.sourceMessage,
].join(':');
case 'Render': {
return [effect.kind, effect.place.identifier.id].join(':');
}

View File

@@ -0,0 +1,114 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {BlockId, computePostDominatorTree, HIRFunction, Place} from '../HIR';
import {PostDominator} from '../HIR/Dominator';
export type ControlDominators = (id: BlockId) => Place | null;
/**
* Returns an object that lazily calculates whether particular blocks are controlled
* by values of interest. Which values matter are up to the caller.
*/
export function createControlDominators(
fn: HIRFunction,
isControlVariable: (place: Place) => boolean,
): ControlDominators {
const postDominators = computePostDominatorTree(fn, {
includeThrowsAsExitNode: false,
});
const postDominatorFrontierCache = new Map<BlockId, Set<BlockId>>();
function isControlledBlock(id: BlockId): Place | null {
let controlBlocks = postDominatorFrontierCache.get(id);
if (controlBlocks === undefined) {
controlBlocks = postDominatorFrontier(fn, postDominators, id);
postDominatorFrontierCache.set(id, controlBlocks);
}
for (const blockId of controlBlocks) {
const controlBlock = fn.body.blocks.get(blockId)!;
switch (controlBlock.terminal.kind) {
case 'if':
case 'branch': {
if (isControlVariable(controlBlock.terminal.test)) {
return controlBlock.terminal.test;
}
break;
}
case 'switch': {
if (isControlVariable(controlBlock.terminal.test)) {
return controlBlock.terminal.test;
}
for (const case_ of controlBlock.terminal.cases) {
if (case_.test !== null && isControlVariable(case_.test)) {
return case_.test;
}
}
break;
}
}
}
return null;
}
return isControlledBlock;
}
/*
* Computes the post-dominator frontier of @param block. These are immediate successors of nodes that
* post-dominate @param targetId and from which execution may not reach @param block. Intuitively, these
* are the earliest blocks from which execution branches such that it may or may not reach the target block.
*/
function postDominatorFrontier(
fn: HIRFunction,
postDominators: PostDominator<BlockId>,
targetId: BlockId,
): Set<BlockId> {
const visited = new Set<BlockId>();
const frontier = new Set<BlockId>();
const targetPostDominators = postDominatorsOf(fn, postDominators, targetId);
for (const blockId of [...targetPostDominators, targetId]) {
if (visited.has(blockId)) {
continue;
}
visited.add(blockId);
const block = fn.body.blocks.get(blockId)!;
for (const pred of block.preds) {
if (!targetPostDominators.has(pred)) {
// The predecessor does not always reach this block, we found an item on the frontier!
frontier.add(pred);
}
}
}
return frontier;
}
function postDominatorsOf(
fn: HIRFunction,
postDominators: PostDominator<BlockId>,
targetId: BlockId,
): Set<BlockId> {
const result = new Set<BlockId>();
const visited = new Set<BlockId>();
const queue = [targetId];
while (queue.length) {
const currentId = queue.shift()!;
if (visited.has(currentId)) {
continue;
}
visited.add(currentId);
const current = fn.body.blocks.get(currentId)!;
for (const pred of current.preds) {
const predPostDominator = postDominators.get(pred) ?? pred;
if (predPostDominator === targetId || result.has(predPostDominator)) {
result.add(pred);
}
queue.push(pred);
}
}
return result;
}

View File

@@ -42,7 +42,7 @@ type IdentifierSidemap = {
functions: Map<IdentifierId, TInstruction<FunctionExpression>>;
manualMemos: Map<IdentifierId, ManualMemoCallee>;
react: Set<IdentifierId>;
maybeDepsLists: Map<IdentifierId, Array<Place>>;
maybeDepsLists: Map<IdentifierId, {loc: SourceLocation; deps: Array<Place>}>;
maybeDeps: Map<IdentifierId, ManualMemoDependency>;
optionals: Set<IdentifierId>;
};
@@ -65,6 +65,7 @@ export function collectMaybeMemoDependencies(
identifierName: value.binding.name,
},
path: [],
loc: value.loc,
};
}
case 'PropertyLoad': {
@@ -74,6 +75,7 @@ export function collectMaybeMemoDependencies(
root: object.root,
// TODO: determine if the access is optional
path: [...object.path, {property: value.property, optional}],
loc: value.loc,
};
}
break;
@@ -92,8 +94,10 @@ export function collectMaybeMemoDependencies(
root: {
kind: 'NamedLocal',
value: {...value.place},
constant: false,
},
path: [],
loc: value.place.loc,
};
}
break;
@@ -159,10 +163,10 @@ function collectTemporaries(
}
case 'ArrayExpression': {
if (value.elements.every(e => e.kind === 'Identifier')) {
sidemap.maybeDepsLists.set(
instr.lvalue.identifier.id,
value.elements as Array<Place>,
);
sidemap.maybeDepsLists.set(instr.lvalue.identifier.id, {
loc: value.loc,
deps: value.elements as Array<Place>,
});
}
break;
}
@@ -182,6 +186,7 @@ function makeManualMemoizationMarkers(
fnExpr: Place,
env: Environment,
depsList: Array<ManualMemoDependency> | null,
depsLoc: SourceLocation | null,
memoDecl: Place,
manualMemoId: number,
): [TInstruction<StartMemoize>, TInstruction<FinishMemoize>] {
@@ -197,6 +202,7 @@ function makeManualMemoizationMarkers(
* as dependencies
*/
deps: depsList,
depsLoc,
loc: fnExpr.loc,
},
effects: null,
@@ -287,86 +293,85 @@ function extractManualMemoizationArgs(
sidemap: IdentifierSidemap,
errors: CompilerError,
): {
fnPlace: Place | null;
fnPlace: Place;
depsList: Array<ManualMemoDependency> | null;
} {
depsLoc: SourceLocation | null;
} | null {
const [fnPlace, depsListPlace] = instr.value.args as Array<
Place | SpreadPattern | undefined
>;
if (fnPlace == null) {
if (fnPlace == null || fnPlace.kind !== 'Identifier') {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected a callback function to be passed to ${kind}`,
description: `Expected a callback function to be passed to ${kind}`,
description:
kind === 'useCallback'
? 'The first argument to useCallback() must be a function to cache'
: 'The first argument to useMemo() must be a function that calculates a result to cache',
suggestions: null,
}).withDetails({
kind: 'error',
loc: instr.value.loc,
message: `Expected a callback function to be passed to ${kind}`,
message:
kind === 'useCallback'
? `Expected a callback function`
: `Expected a memoization function`,
}),
);
return {fnPlace: null, depsList: null};
return null;
}
if (fnPlace.kind === 'Spread' || depsListPlace?.kind === 'Spread') {
if (depsListPlace == null) {
return {
fnPlace,
depsList: null,
depsLoc: null,
};
}
const maybeDepsList =
depsListPlace.kind === 'Identifier'
? sidemap.maybeDepsLists.get(depsListPlace.identifier.id)
: null;
if (maybeDepsList == null) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Unexpected spread argument to ${kind}`,
description: `Unexpected spread argument to ${kind}`,
reason: `Expected the dependency list for ${kind} to be an array literal`,
description: `Expected the dependency list for ${kind} to be an array literal`,
suggestions: null,
}).withDetails({
kind: 'error',
loc: instr.value.loc,
message: `Unexpected spread argument to ${kind}`,
loc:
depsListPlace?.kind === 'Identifier' ? depsListPlace.loc : instr.loc,
message: `Expected the dependency list for ${kind} to be an array literal`,
}),
);
return {fnPlace: null, depsList: null};
return null;
}
let depsList: Array<ManualMemoDependency> | null = null;
if (depsListPlace != null) {
const maybeDepsList = sidemap.maybeDepsLists.get(
depsListPlace.identifier.id,
);
if (maybeDepsList == null) {
const depsList: Array<ManualMemoDependency> = [];
for (const dep of maybeDepsList.deps) {
const maybeDep = sidemap.maybeDeps.get(dep.identifier.id);
if (maybeDep == null) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected the dependency list for ${kind} to be an array literal`,
description: `Expected the dependency list for ${kind} to be an array literal`,
reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
description: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
suggestions: null,
}).withDetails({
kind: 'error',
loc: depsListPlace.loc,
message: `Expected the dependency list for ${kind} to be an array literal`,
loc: dep.loc,
message: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
}),
);
return {fnPlace, depsList: null};
}
depsList = [];
for (const dep of maybeDepsList) {
const maybeDep = sidemap.maybeDeps.get(dep.identifier.id);
if (maybeDep == null) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
description: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
suggestions: null,
}).withDetails({
kind: 'error',
loc: dep.loc,
message: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
}),
);
} else {
depsList.push(maybeDep);
}
} else {
depsList.push(maybeDep);
}
}
return {
fnPlace,
depsList,
depsLoc: maybeDepsList.loc,
};
}
@@ -427,16 +432,17 @@ export function dropManualMemoization(
const manualMemo = sidemap.manualMemos.get(id);
if (manualMemo != null) {
const {fnPlace, depsList} = extractManualMemoizationArgs(
const memoDetails = extractManualMemoizationArgs(
instr as TInstruction<CallExpression> | TInstruction<MethodCall>,
manualMemo.kind,
sidemap,
errors,
);
if (fnPlace == null) {
if (memoDetails == null) {
continue;
}
const {fnPlace, depsList, depsLoc} = memoDetails;
instr.value = getManualMemoizationReplacement(
fnPlace,
@@ -487,6 +493,7 @@ export function dropManualMemoization(
fnPlace,
func.env,
depsList,
depsLoc,
memoDecl,
nextManualMemoId++,
);

View File

@@ -27,11 +27,13 @@ import {
InstructionKind,
InstructionValue,
isArrayType,
isJsxType,
isJsxOrJsxUnionType,
isMapType,
isMutableEffect,
isPrimitiveType,
isRefOrRefValue,
isSetType,
isUseRefType,
makeIdentifierId,
Phi,
Place,
@@ -70,6 +72,7 @@ import {
MutationReason,
} from './AliasingEffects';
import {ErrorCategory} from '../CompilerError';
import {REF_ERROR_DESCRIPTION} from '../Validation/ValidateNoRefAccessInRender';
const DEBUG = false;
@@ -569,14 +572,32 @@ function inferBlock(
terminal.effects = effects.length !== 0 ? effects : null;
}
} else if (terminal.kind === 'return') {
terminal.effects = [
context.internEffect({
kind: 'Alias',
from: terminal.value,
into: context.fn.returns,
}),
];
if (!context.isFuctionExpression) {
terminal.effects = [
terminal.effects.push(
context.internEffect({
kind: 'Freeze',
value: terminal.value,
reason: ValueReason.JsxCaptured,
}),
];
);
}
if (
context.fn.fnType === 'Component' ||
isJsxOrJsxUnionType(context.fn.returns.identifier.type)
) {
terminal.effects.push(
context.internEffect({
kind: 'Render',
place: terminal.value,
}),
);
}
}
}
@@ -749,17 +770,7 @@ function applyEffect(
break;
}
case 'ImmutableCapture': {
const kind = state.kind(effect.from).kind;
switch (kind) {
case ValueKind.Global:
case ValueKind.Primitive: {
// no-op: we don't need to track data flow for copy types
break;
}
default: {
effects.push(effect);
}
}
effects.push(effect);
break;
}
case 'CreateFrom': {
@@ -954,6 +965,7 @@ function applyEffect(
case ValueKind.Primitive: {
break;
}
case ValueKind.MaybeFrozen:
case ValueKind.Frozen: {
sourceType = 'frozen';
break;
@@ -1060,6 +1072,17 @@ function applyEffect(
reason: new Set(fromValue.reason),
});
state.define(effect.into, value);
applyEffect(
context,
state,
{
kind: 'ImmutableCapture',
from: effect.from,
into: effect.into,
},
initialized,
effects,
);
break;
}
default: {
@@ -1071,6 +1094,8 @@ function applyEffect(
break;
}
case 'Apply': {
effects.push(effect);
const functionValues = state.values(effect.function);
if (
functionValues.length === 1 &&
@@ -1965,6 +1990,11 @@ function computeSignatureForInstruction(
value: ValueKind.Primitive,
reason: ValueReason.Other,
});
effects.push({
kind: 'ImmutableCapture',
from: value.object,
into: lvalue,
});
} else {
effects.push({
kind: 'CreateFrom',
@@ -1972,6 +2002,20 @@ function computeSignatureForInstruction(
into: lvalue,
});
}
if (
env.config.validateRefAccessDuringRender &&
isUseRefType(value.object.identifier)
) {
effects.push({
kind: 'Impure',
into: lvalue,
category: ErrorCategory.Refs,
reason: `Cannot access ref value during render`,
description: REF_ERROR_DESCRIPTION,
sourceMessage: `Ref is initially accessed`,
usageMessage: `Ref value is used during render`,
});
}
break;
}
case 'PropertyStore':
@@ -2136,6 +2180,15 @@ function computeSignatureForInstruction(
into: lvalue,
});
}
if (value.children != null) {
// Children are typically called during render, not used as an event/effect callback
for (const child of value.children) {
effects.push({
kind: 'Render',
place: child,
});
}
}
if (value.kind === 'JsxExpression') {
if (value.tag.kind === 'Identifier') {
// Tags are render function, by definition they're called during render
@@ -2144,29 +2197,17 @@ function computeSignatureForInstruction(
place: value.tag,
});
}
if (value.children != null) {
// Children are typically called during render, not used as an event/effect callback
for (const child of value.children) {
effects.push({
kind: 'Render',
place: child,
});
}
}
for (const prop of value.props) {
if (
prop.kind === 'JsxAttribute' &&
prop.place.identifier.type.kind === 'Function' &&
(isJsxType(prop.place.identifier.type.return) ||
(prop.place.identifier.type.return.kind === 'Phi' &&
prop.place.identifier.type.return.operands.some(operand =>
isJsxType(operand),
)))
) {
// Any props which return jsx are assumed to be called during render
const place =
prop.kind === 'JsxAttribute' ? prop.place : prop.argument;
if (isUseRefType(place.identifier)) {
continue;
}
if (place.identifier.type.kind !== 'Function') {
// Functions are checked independently
effects.push({
kind: 'Render',
place: prop.place,
place,
});
}
}
@@ -2202,6 +2243,11 @@ function computeSignatureForInstruction(
value: ValueKind.Primitive,
reason: ValueReason.Other,
});
effects.push({
kind: 'ImmutableCapture',
from: value.value,
into: place,
});
} else if (patternItem.kind === 'Identifier') {
effects.push({
kind: 'CreateFrom',
@@ -2383,15 +2429,46 @@ function computeSignatureForInstruction(
});
break;
}
case 'BinaryExpression': {
effects.push({
kind: 'Create',
into: lvalue,
value: ValueKind.Primitive,
reason: ValueReason.Other,
});
effects.push({
kind: 'ImmutableCapture',
into: lvalue,
from: value.left,
});
effects.push({
kind: 'ImmutableCapture',
into: lvalue,
from: value.right,
});
break;
}
case 'UnaryExpression': {
effects.push({
kind: 'Create',
into: lvalue,
value: ValueKind.Primitive,
reason: ValueReason.Other,
});
effects.push({
kind: 'ImmutableCapture',
into: lvalue,
from: value.value,
});
break;
}
case 'TaggedTemplateExpression':
case 'BinaryExpression':
case 'Debugger':
case 'JSXText':
case 'MetaProperty':
case 'Primitive':
case 'RegExpLiteral':
case 'TemplateLiteral':
case 'UnaryExpression':
case 'UnsupportedNode': {
effects.push({
kind: 'Create',
@@ -2422,7 +2499,7 @@ function computeEffectsForLegacySignature(
lvalue: Place,
receiver: Place,
args: Array<Place | SpreadPattern | Hole>,
loc: SourceLocation,
_loc: SourceLocation,
): Array<AliasingEffect> {
const returnValueReason = signature.returnValueReason ?? ValueReason.Other;
const effects: Array<AliasingEffect> = [];
@@ -2435,23 +2512,21 @@ function computeEffectsForLegacySignature(
if (signature.impure && state.env.config.validateNoImpureFunctionsInRender) {
effects.push({
kind: 'Impure',
place: receiver,
error: CompilerDiagnostic.create({
category: ErrorCategory.Purity,
reason: 'Cannot call impure function during render',
description:
(signature.canonicalName != null
? `\`${signature.canonicalName}\` is an impure function. `
: '') +
'Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
}).withDetails({
kind: 'error',
loc,
message: 'Cannot call impure function',
}),
into: lvalue,
category: ErrorCategory.Purity,
reason: 'Cannot access impure value during render',
description:
'Calling an impure function can produce unstable results that update ' +
'unpredictably when the component happens to re-render. ' +
'(https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
sourceMessage:
signature.canonicalName != null
? `\`${signature.canonicalName}\` is an impure function.`
: 'This function is impure',
usageMessage: 'Cannot access impure value during render',
});
}
if (signature.knownIncompatible != null && state.env.isInferredMemoEnabled) {
if (signature.knownIncompatible != null && state.env.enableValidations) {
const errors = new CompilerError();
errors.pushDiagnostic(
CompilerDiagnostic.create({
@@ -2747,7 +2822,23 @@ function computeEffectsForSignature(
}
break;
}
case 'Impure':
case 'Impure': {
if (env.config.validateNoImpureFunctionsInRender) {
const values = substitutions.get(effect.into.identifier.id) ?? [];
for (const value of values) {
effects.push({
kind: effect.kind,
into: value,
category: effect.category,
reason: effect.reason,
description: effect.description,
sourceMessage: effect.sourceMessage,
usageMessage: effect.usageMessage,
});
}
}
break;
}
case 'MutateFrozen':
case 'MutateGlobal': {
const values = substitutions.get(effect.place.identifier.id) ?? [];

View File

@@ -19,6 +19,8 @@ import {
ValueReason,
Place,
isPrimitiveType,
isUseRefType,
isJsxOrJsxUnionType,
} from '../HIR/HIR';
import {
eachInstructionLValue,
@@ -28,6 +30,9 @@ import {
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
import {Err, Ok, Result} from '../Utils/Result';
import {AliasingEffect, MutationReason} from './AliasingEffects';
import {printIdentifier, printType} from '../HIR/PrintHIR';
const DEBUG = false;
/**
* This pass builds an abstract model of the heap and interprets the effects of the
@@ -104,7 +109,6 @@ export function inferMutationAliasingRanges(
reason: MutationReason | null;
}> = [];
const renders: Array<{index: number; place: Place}> = [];
let index = 0;
const errors = new CompilerError();
@@ -197,14 +201,12 @@ export function inferMutationAliasingRanges(
});
} else if (
effect.kind === 'MutateFrozen' ||
effect.kind === 'MutateGlobal' ||
effect.kind === 'Impure'
effect.kind === 'MutateGlobal'
) {
errors.pushDiagnostic(effect.error);
functionEffects.push(effect);
} else if (effect.kind === 'Render') {
renders.push({index: index++, place: effect.place});
functionEffects.push(effect);
}
}
}
@@ -214,10 +216,6 @@ export function inferMutationAliasingRanges(
state.assign(index, from, into);
}
}
if (block.terminal.kind === 'return') {
state.assign(index++, block.terminal.value, fn.returns);
}
if (
(block.terminal.kind === 'maybe-throw' ||
block.terminal.kind === 'return') &&
@@ -227,23 +225,31 @@ export function inferMutationAliasingRanges(
if (effect.kind === 'Alias') {
state.assign(index++, effect.from, effect.into);
} else {
CompilerError.invariant(effect.kind === 'Freeze', {
reason: `Unexpected '${effect.kind}' effect for MaybeThrow terminal`,
description: null,
details: [
{
kind: 'error',
loc: block.terminal.loc,
message: null,
},
],
});
CompilerError.invariant(
effect.kind === 'Freeze' || effect.kind === 'Render',
{
reason: `Unexpected '${effect.kind}' effect for MaybeThrow terminal`,
description: null,
details: [
{
kind: 'error',
loc: block.terminal.loc,
message: null,
},
],
},
);
}
}
}
}
for (const mutation of mutations) {
if (DEBUG) {
console.log(
`[${mutation.index}] mutate ${printIdentifier(mutation.place.identifier)}`,
);
}
state.mutate(
mutation.index,
mutation.place.identifier,
@@ -255,8 +261,16 @@ export function inferMutationAliasingRanges(
errors,
);
}
if (DEBUG) {
console.log(state.debug());
}
for (const render of renders) {
state.render(render.index, render.place.identifier, errors);
if (DEBUG) {
console.log(
`[${render.index}] render ${printIdentifier(render.place.identifier)}`,
);
}
state.render(render.index, render.place, errors);
}
for (const param of [...fn.context, ...fn.params]) {
const place = param.kind === 'Identifier' ? param : param.place;
@@ -383,17 +397,7 @@ export function inferMutationAliasingRanges(
break;
}
case 'Apply': {
CompilerError.invariant(false, {
reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,
description: null,
details: [
{
kind: 'error',
loc: effect.function.loc,
message: null,
},
],
});
break;
}
case 'MutateTransitive':
case 'MutateConditionally':
@@ -515,6 +519,13 @@ export function inferMutationAliasingRanges(
const ignoredErrors = new CompilerError();
for (const param of [...fn.params, ...fn.context, fn.returns]) {
const place = param.kind === 'Identifier' ? param : param.place;
const node = state.nodes.get(place.identifier);
if (node != null && node.render != null) {
functionEffects.push({
kind: 'Render',
place: place,
});
}
tracked.push(place);
}
for (const into of tracked) {
@@ -568,7 +579,12 @@ export function inferMutationAliasingRanges(
}
}
if (errors.hasAnyErrors() && !isFunctionExpression) {
if (
errors.hasAnyErrors() &&
(fn.fnType === 'Component' ||
isJsxOrJsxUnionType(fn.returns.identifier.type) ||
!isFunctionExpression)
) {
return Err(errors);
}
return Ok(functionEffects);
@@ -577,7 +593,6 @@ export function inferMutationAliasingRanges(
function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
for (const effect of fn.aliasingEffects ?? []) {
switch (effect.kind) {
case 'Impure':
case 'MutateFrozen':
case 'MutateGlobal': {
errors.pushDiagnostic(effect.error);
@@ -612,10 +627,74 @@ type Node = {
| {kind: 'Object'}
| {kind: 'Phi'}
| {kind: 'Function'; function: HIRFunction};
render: Place | null;
};
function _printNode(node: Node): string {
const out: Array<string> = [];
debugNode(out, node);
return out.join('\n');
}
function debugNode(out: Array<string>, node: Node): void {
out.push(
printIdentifier(node.id) +
printType(node.id.type) +
` lastMutated=[${node.lastMutated}]`,
);
if (node.transitive != null) {
out.push(` transitive=${node.transitive.kind}`);
}
if (node.local != null) {
out.push(` local=${node.local.kind}`);
}
if (node.mutationReason != null) {
out.push(` mutationReason=${node.mutationReason?.kind}`);
}
const edges: Array<{
index: number;
direction: '<=' | '=>';
kind: string;
id: Identifier;
}> = [];
for (const [alias, index] of node.createdFrom) {
edges.push({index, direction: '<=', kind: 'createFrom', id: alias});
}
for (const [alias, index] of node.aliases) {
edges.push({index, direction: '<=', kind: 'alias', id: alias});
}
for (const [alias, index] of node.maybeAliases) {
edges.push({index, direction: '<=', kind: 'alias?', id: alias});
}
for (const [alias, index] of node.captures) {
edges.push({index, direction: '<=', kind: 'capture', id: alias});
}
for (const edge of node.edges) {
edges.push({
index: edge.index,
direction: '=>',
kind: edge.kind,
id: edge.node,
});
}
edges.sort((a, b) => a.index - b.index);
for (const edge of edges) {
out.push(
` [${edge.index}] ${edge.direction} ${edge.kind} ${printIdentifier(edge.id)}`,
);
}
}
class AliasingState {
nodes: Map<Identifier, Node> = new Map();
debug(): string {
const items: Array<string> = [];
for (const [_id, node] of this.nodes) {
debugNode(items, node);
}
return items.join('\n');
}
create(place: Place, value: Node['value']): void {
this.nodes.set(place.identifier, {
id: place.identifier,
@@ -629,6 +708,7 @@ class AliasingState {
lastMutated: 0,
mutationReason: null,
value,
render: null,
});
}
@@ -681,9 +761,9 @@ class AliasingState {
}
}
render(index: number, start: Identifier, errors: CompilerError): void {
render(index: number, start: Place, errors: CompilerError): void {
const seen = new Set<Identifier>();
const queue: Array<Identifier> = [start];
const queue: Array<Identifier> = [start.identifier];
while (queue.length !== 0) {
const current = queue.pop()!;
if (seen.has(current)) {
@@ -691,11 +771,34 @@ class AliasingState {
}
seen.add(current);
const node = this.nodes.get(current);
if (node == null || node.transitive != null || node.local != null) {
if (node == null || isUseRefType(node.id)) {
if (DEBUG) {
console.log(` render ${printIdentifier(current)}: skip mutated/ref`);
}
continue;
}
if (node.value.kind === 'Function') {
appendFunctionErrors(errors, node.value.function);
if (
node.local == null &&
node.transitive == null &&
node.value.kind === 'Function'
) {
const returns = node.value.function.returns;
if (
isJsxType(returns.identifier.type) ||
(returns.identifier.type.kind === 'Phi' &&
returns.identifier.type.operands.some(operand =>
isJsxType(operand),
))
) {
appendFunctionErrors(errors, node.value.function);
}
if (DEBUG) {
console.log(` render ${printIdentifier(current)}: skip function`);
}
continue;
}
if (node.render == null) {
node.render = start;
}
for (const [alias, when] of node.createdFrom) {
if (when >= index) {
@@ -709,6 +812,12 @@ class AliasingState {
}
queue.push(alias);
}
for (const [alias, when] of node.maybeAliases) {
if (when >= index) {
continue;
}
queue.push(alias);
}
for (const [capture, when] of node.captures) {
if (when >= index) {
continue;

View File

@@ -7,7 +7,6 @@
import {CompilerError} from '..';
import {
BlockId,
Effect,
Environment,
HIRFunction,
@@ -15,14 +14,12 @@ import {
IdentifierId,
Instruction,
Place,
computePostDominatorTree,
evaluatesToStableTypeOrContainer,
getHookKind,
isStableType,
isStableTypeContainer,
isUseOperator,
} from '../HIR';
import {PostDominator} from '../HIR/Dominator';
import {
eachInstructionLValue,
eachInstructionOperand,
@@ -35,6 +32,7 @@ import {
} from '../ReactiveScopes/InferReactiveScopeVariables';
import DisjointSet from '../Utils/DisjointSet';
import {assertExhaustive} from '../Utils/utils';
import {createControlDominators} from './ControlDominators';
/**
* Side map to track and propagate sources of stability (i.e. hook calls such as
@@ -212,45 +210,9 @@ export function inferReactivePlaces(fn: HIRFunction): void {
reactiveIdentifiers.markReactive(place);
}
const postDominators = computePostDominatorTree(fn, {
includeThrowsAsExitNode: false,
});
const postDominatorFrontierCache = new Map<BlockId, Set<BlockId>>();
function isReactiveControlledBlock(id: BlockId): boolean {
let controlBlocks = postDominatorFrontierCache.get(id);
if (controlBlocks === undefined) {
controlBlocks = postDominatorFrontier(fn, postDominators, id);
postDominatorFrontierCache.set(id, controlBlocks);
}
for (const blockId of controlBlocks) {
const controlBlock = fn.body.blocks.get(blockId)!;
switch (controlBlock.terminal.kind) {
case 'if':
case 'branch': {
if (reactiveIdentifiers.isReactive(controlBlock.terminal.test)) {
return true;
}
break;
}
case 'switch': {
if (reactiveIdentifiers.isReactive(controlBlock.terminal.test)) {
return true;
}
for (const case_ of controlBlock.terminal.cases) {
if (
case_.test !== null &&
reactiveIdentifiers.isReactive(case_.test)
) {
return true;
}
}
break;
}
}
}
return false;
}
const isReactiveControlledBlock = createControlDominators(fn, place =>
reactiveIdentifiers.isReactive(place),
);
do {
for (const [, block] of fn.body.blocks) {
@@ -411,61 +373,6 @@ export function inferReactivePlaces(fn: HIRFunction): void {
propagateReactivityToInnerFunctions(fn, true);
}
/*
* Computes the post-dominator frontier of @param block. These are immediate successors of nodes that
* post-dominate @param targetId and from which execution may not reach @param block. Intuitively, these
* are the earliest blocks from which execution branches such that it may or may not reach the target block.
*/
function postDominatorFrontier(
fn: HIRFunction,
postDominators: PostDominator<BlockId>,
targetId: BlockId,
): Set<BlockId> {
const visited = new Set<BlockId>();
const frontier = new Set<BlockId>();
const targetPostDominators = postDominatorsOf(fn, postDominators, targetId);
for (const blockId of [...targetPostDominators, targetId]) {
if (visited.has(blockId)) {
continue;
}
visited.add(blockId);
const block = fn.body.blocks.get(blockId)!;
for (const pred of block.preds) {
if (!targetPostDominators.has(pred)) {
// The predecessor does not always reach this block, we found an item on the frontier!
frontier.add(pred);
}
}
}
return frontier;
}
function postDominatorsOf(
fn: HIRFunction,
postDominators: PostDominator<BlockId>,
targetId: BlockId,
): Set<BlockId> {
const result = new Set<BlockId>();
const visited = new Set<BlockId>();
const queue = [targetId];
while (queue.length) {
const currentId = queue.shift()!;
if (visited.has(currentId)) {
continue;
}
visited.add(currentId);
const current = fn.body.blocks.get(currentId)!;
for (const pred of current.preds) {
const predPostDominator = postDominators.get(pred) ?? pred;
if (predPostDominator === targetId || result.has(predPostDominator)) {
result.add(pred);
}
queue.push(pred);
}
}
return result;
}
class ReactivityMap {
hasChanges: boolean = false;
reactive: Set<IdentifierId> = new Set();

View File

@@ -609,6 +609,19 @@ function evaluateInstruction(
constantPropagationImpl(value.loweredFunc.func, constants);
return null;
}
case 'StartMemoize': {
if (value.deps != null) {
for (const dep of value.deps) {
if (dep.root.kind === 'NamedLocal') {
const placeValue = read(constants, dep.root.value);
if (placeValue != null && placeValue.kind === 'Primitive') {
dep.root.constant = true;
}
}
}
}
return null;
}
default: {
// TODO: handle more cases
return null;

View File

@@ -7,6 +7,8 @@
import {
BlockId,
Environment,
getHookKind,
HIRFunction,
Identifier,
IdentifierId,
@@ -68,9 +70,14 @@ export function deadCodeElimination(fn: HIRFunction): void {
}
class State {
env: Environment;
named: Set<string> = new Set();
identifiers: Set<IdentifierId> = new Set();
constructor(env: Environment) {
this.env = env;
}
// Mark the identifier as being referenced (not dead code)
reference(identifier: Identifier): void {
this.identifiers.add(identifier.id);
@@ -112,7 +119,7 @@ function findReferencedIdentifiers(fn: HIRFunction): State {
const hasLoop = hasBackEdge(fn);
const reversedBlocks = [...fn.body.blocks.values()].reverse();
const state = new State();
const state = new State(fn.env);
let size = state.count;
do {
size = state.count;
@@ -310,12 +317,27 @@ function pruneableValue(value: InstructionValue, state: State): boolean {
// explicitly retain debugger statements to not break debugging workflows
return false;
}
case 'Await':
case 'CallExpression':
case 'MethodCall': {
if (state.env.outputMode === 'ssr') {
const calleee =
value.kind === 'CallExpression' ? value.callee : value.property;
const hookKind = getHookKind(state.env, calleee.identifier);
switch (hookKind) {
case 'useState':
case 'useReducer':
case 'useRef': {
// unused refs can be removed
return true;
}
}
}
return false;
}
case 'Await':
case 'ComputedDelete':
case 'ComputedStore':
case 'PropertyDelete':
case 'MethodCall':
case 'PropertyStore':
case 'StoreGlobal': {
/*

View File

@@ -0,0 +1,269 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError} from '..';
import {
CallExpression,
getHookKind,
HIRFunction,
IdentifierId,
InstructionValue,
isArrayType,
isPlainObjectType,
isPrimitiveType,
isSetStateType,
isStartTransitionType,
LoadLocal,
StoreLocal,
} from '../HIR';
import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {retainWhere} from '../Utils/utils';
/**
* Optimizes the code for running specifically in an SSR environment. This optimization
* asssumes that setState will not be called during render during initial mount, which
* allows inlining useState/useReducer.
*
* Optimizations:
* - Inline useState/useReducer
* - Remove effects
* - Remove refs where known to be unused during render (eg directly passed to a dom node)
* - Remove event handlers
*
* Note that an earlier pass already inlines useMemo/useCallback
*/
export function optimizeForSSR(fn: HIRFunction): void {
const inlinedState = new Map<IdentifierId, InstructionValue>();
/**
* First pass identifies useState/useReducer which can be safely inlined. Any use
* of the hook return other than destructuring (with a specific pattern) prevents
* inlining.
*
* Supported cases:
* - `const [state, ] = useState( <primitive-array-or-object> )`
* - `const [state, ] = useReducer(..., <value>)`
* - `const [state, ] = useReducer[..., <value>, <init>]`
*/
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {value} = instr;
switch (value.kind) {
case 'Destructure': {
if (
inlinedState.has(value.value.identifier.id) &&
value.lvalue.pattern.kind === 'ArrayPattern' &&
value.lvalue.pattern.items.length >= 1 &&
value.lvalue.pattern.items[0].kind === 'Identifier'
) {
// Allow destructuring of inlined states
continue;
}
break;
}
case 'MethodCall':
case 'CallExpression': {
const calleee =
value.kind === 'CallExpression' ? value.callee : value.property;
const hookKind = getHookKind(fn.env, calleee.identifier);
switch (hookKind) {
case 'useReducer': {
if (
value.args.length === 2 &&
value.args[1].kind === 'Identifier'
) {
const arg = value.args[1];
const replace: LoadLocal = {
kind: 'LoadLocal',
place: arg,
loc: arg.loc,
};
inlinedState.set(instr.lvalue.identifier.id, replace);
} else if (
value.args.length === 3 &&
value.args[1].kind === 'Identifier' &&
value.args[2].kind === 'Identifier'
) {
const arg = value.args[1];
const initializer = value.args[2];
const replace: CallExpression = {
kind: 'CallExpression',
callee: initializer,
args: [arg],
loc: value.loc,
};
inlinedState.set(instr.lvalue.identifier.id, replace);
}
break;
}
case 'useState': {
if (
value.args.length === 1 &&
value.args[0].kind === 'Identifier'
) {
const arg = value.args[0];
if (
isPrimitiveType(arg.identifier) ||
isPlainObjectType(arg.identifier) ||
isArrayType(arg.identifier)
) {
const replace: LoadLocal = {
kind: 'LoadLocal',
place: arg,
loc: arg.loc,
};
inlinedState.set(instr.lvalue.identifier.id, replace);
}
}
break;
}
}
}
}
// Any use of useState/useReducer return besides destructuring prevents inlining
if (inlinedState.size !== 0) {
for (const operand of eachInstructionValueOperand(value)) {
inlinedState.delete(operand.identifier.id);
}
}
}
if (inlinedState.size !== 0) {
for (const operand of eachTerminalOperand(block.terminal)) {
inlinedState.delete(operand.identifier.id);
}
}
}
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {value} = instr;
switch (value.kind) {
case 'FunctionExpression': {
if (hasKnownNonRenderCall(value.loweredFunc.func)) {
instr.value = {
kind: 'Primitive',
value: undefined,
loc: value.loc,
};
}
break;
}
case 'JsxExpression': {
if (
value.tag.kind === 'BuiltinTag' &&
value.tag.name.indexOf('-') === -1
) {
const tag = value.tag.name;
retainWhere(value.props, prop => {
return (
prop.kind === 'JsxSpreadAttribute' ||
(!isKnownEventHandler(tag, prop.name) && prop.name !== 'ref')
);
});
}
break;
}
case 'Destructure': {
if (inlinedState.has(value.value.identifier.id)) {
// Canonical check is part of determining if state can inline, this is for TS
CompilerError.invariant(
value.lvalue.pattern.kind === 'ArrayPattern' &&
value.lvalue.pattern.items.length >= 1 &&
value.lvalue.pattern.items[0].kind === 'Identifier',
{
reason:
'Expected a valid destructuring pattern for inlined state',
description: null,
details: [
{
kind: 'error',
message: 'Expected a valid destructuring pattern',
loc: value.loc,
},
],
},
);
const store: StoreLocal = {
kind: 'StoreLocal',
loc: value.loc,
type: null,
lvalue: {
kind: value.lvalue.kind,
place: value.lvalue.pattern.items[0],
},
value: value.value,
};
instr.value = store;
}
break;
}
case 'MethodCall':
case 'CallExpression': {
const calleee =
value.kind === 'CallExpression' ? value.callee : value.property;
const hookKind = getHookKind(fn.env, calleee.identifier);
switch (hookKind) {
case 'useEffectEvent': {
if (
value.args.length === 1 &&
value.args[0].kind === 'Identifier'
) {
const load: LoadLocal = {
kind: 'LoadLocal',
place: value.args[0],
loc: value.loc,
};
instr.value = load;
}
break;
}
case 'useEffect':
case 'useLayoutEffect':
case 'useInsertionEffect': {
// Drop effects
instr.value = {
kind: 'Primitive',
value: undefined,
loc: value.loc,
};
break;
}
case 'useReducer':
case 'useState': {
const replace = inlinedState.get(instr.lvalue.identifier.id);
if (replace != null) {
instr.value = replace;
}
break;
}
}
}
}
}
}
}
function hasKnownNonRenderCall(fn: HIRFunction): boolean {
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
if (
instr.value.kind === 'CallExpression' &&
(isSetStateType(instr.value.callee.identifier) ||
isStartTransitionType(instr.value.callee.identifier))
) {
return true;
}
}
}
return false;
}
const EVENT_HANDLER_PATTERN = /^on[A-Z]/;
function isKnownEventHandler(_tag: string, prop: string): boolean {
return EVENT_HANDLER_PATTERN.test(prop);
}

View File

@@ -515,6 +515,7 @@ function emitDestructureProps(
pattern: {
kind: 'ObjectPattern',
properties,
loc: GeneratedSource,
},
kind: InstructionKind.Let,
},

View File

@@ -159,7 +159,7 @@ export function codegenFunction(
const compiled = compileResult.unwrap();
const hookGuard = fn.env.config.enableEmitHookGuards;
if (hookGuard != null && fn.env.isInferredMemoEnabled) {
if (hookGuard != null && fn.env.outputMode === 'client') {
compiled.body = t.blockStatement([
createHookGuard(
hookGuard,
@@ -259,7 +259,7 @@ export function codegenFunction(
if (
emitInstrumentForget != null &&
fn.id != null &&
fn.env.isInferredMemoEnabled
fn.env.outputMode === 'client'
) {
/*
* Technically, this is a conditional hook call. However, we expect
@@ -591,7 +591,10 @@ function codegenBlockNoReset(
}
function wrapCacheDep(cx: Context, value: t.Expression): t.Expression {
if (cx.env.config.enableEmitFreeze != null && cx.env.isInferredMemoEnabled) {
if (
cx.env.config.enableEmitFreeze != null &&
cx.env.outputMode === 'client'
) {
const emitFreezeIdentifier = cx.env.programContext.addImportSpecifier(
cx.env.config.enableEmitFreeze,
).name;
@@ -699,7 +702,7 @@ function codegenReactiveScope(
outputComments.push(name.name);
if (!cx.hasDeclared(identifier)) {
statements.push(
t.variableDeclaration('let', [t.variableDeclarator(name)]),
t.variableDeclaration('let', [createVariableDeclarator(name, null)]),
);
}
cacheLoads.push({name, index, value: wrapCacheDep(cx, name)});
@@ -1359,8 +1362,6 @@ function codegenInstructionNullable(
value = null;
} else {
lvalue = instr.value.lvalue.pattern;
let hasReassign = false;
let hasDeclaration = false;
for (const place of eachPatternOperand(lvalue)) {
if (
kind !== InstructionKind.Reassign &&
@@ -1368,26 +1369,6 @@ function codegenInstructionNullable(
) {
cx.temp.set(place.identifier.declarationId, null);
}
const isDeclared = cx.hasDeclared(place.identifier);
hasReassign ||= isDeclared;
hasDeclaration ||= !isDeclared;
}
if (hasReassign && hasDeclaration) {
CompilerError.invariant(false, {
reason:
'Encountered a destructuring operation where some identifiers are already declared (reassignments) but others are not (declarations)',
description: null,
details: [
{
kind: 'error',
loc: instr.loc,
message: null,
},
],
suggestions: null,
});
} else if (hasReassign) {
kind = InstructionKind.Reassign;
}
value = codegenPlaceToExpression(cx, instr.value.value);
}
@@ -1406,7 +1387,7 @@ function codegenInstructionNullable(
suggestions: null,
});
return createVariableDeclaration(instr.loc, 'const', [
t.variableDeclarator(codegenLValue(cx, lvalue), value),
createVariableDeclarator(codegenLValue(cx, lvalue), value),
]);
}
case InstructionKind.Function: {
@@ -1470,7 +1451,7 @@ function codegenInstructionNullable(
suggestions: null,
});
return createVariableDeclaration(instr.loc, 'let', [
t.variableDeclarator(codegenLValue(cx, lvalue), value),
createVariableDeclarator(codegenLValue(cx, lvalue), value),
]);
}
case InstructionKind.Reassign: {
@@ -1710,6 +1691,9 @@ function withLoc<T extends (...args: Array<any>) => t.Node>(
};
}
const createIdentifier = withLoc(t.identifier);
const createArrayPattern = withLoc(t.arrayPattern);
const createObjectPattern = withLoc(t.objectPattern);
const createBinaryExpression = withLoc(t.binaryExpression);
const createExpressionStatement = withLoc(t.expressionStatement);
const _createLabelledStatement = withLoc(t.labeledStatement);
@@ -1741,6 +1725,31 @@ const createTryStatement = withLoc(t.tryStatement);
const createBreakStatement = withLoc(t.breakStatement);
const createContinueStatement = withLoc(t.continueStatement);
function createVariableDeclarator(
id: t.LVal,
init?: t.Expression | null,
): t.VariableDeclarator {
const node = t.variableDeclarator(id, init);
/*
* The variable declarator location is not preserved in HIR, however, we can use the
* start location of the id and the end location of the init to recreate the
* exact original variable declarator location.
*
* Or if init is null, we likely have a declaration without an initializer, so we can use the id.loc.end as the end location.
*/
if (id.loc && (init === null || init?.loc)) {
node.loc = {
start: id.loc.start,
end: init?.loc?.end ?? id.loc.end,
filename: id.loc.filename,
identifierName: undefined,
};
}
return node;
}
function createHookGuard(
guard: ExternalFunction,
context: ProgramContext,
@@ -1794,7 +1803,7 @@ function createCallExpression(
}
const hookGuard = env.config.enableEmitHookGuards;
if (hookGuard != null && isHook && env.isInferredMemoEnabled) {
if (hookGuard != null && isHook && env.outputMode === 'client') {
const iife = t.functionExpression(
null,
[],
@@ -1848,7 +1857,7 @@ function codegenInstruction(
);
} else {
return createVariableDeclaration(instr.loc, 'const', [
t.variableDeclarator(
createVariableDeclarator(
convertIdentifier(instr.lvalue.identifier),
expressionValue,
),
@@ -2775,7 +2784,7 @@ function codegenArrayPattern(
): t.ArrayPattern {
const hasHoles = !pattern.items.every(e => e.kind !== 'Hole');
if (hasHoles) {
const result = t.arrayPattern([]);
const result = createArrayPattern(pattern.loc, []);
/*
* Older versions of babel have a validation bug fixed by
* https://github.com/babel/babel/pull/10917
@@ -2796,7 +2805,8 @@ function codegenArrayPattern(
}
return result;
} else {
return t.arrayPattern(
return createArrayPattern(
pattern.loc,
pattern.items.map(item => {
if (item.kind === 'Hole') {
return null;
@@ -2816,7 +2826,8 @@ function codegenLValue(
return codegenArrayPattern(cx, pattern);
}
case 'ObjectPattern': {
return t.objectPattern(
return createObjectPattern(
pattern.loc,
pattern.properties.map(property => {
if (property.kind === 'ObjectProperty') {
const key = codegenObjectPropertyKey(cx, property.key);
@@ -2935,7 +2946,7 @@ function convertIdentifier(identifier: Identifier): t.Identifier {
suggestions: null,
},
);
return t.identifier(identifier.name.value);
return createIdentifier(identifier.loc, identifier.name.value);
}
function compareScopeDependency(

View File

@@ -19,7 +19,11 @@ import {
promoteTemporary,
} from '../HIR';
import {clonePlaceToTemporary} from '../HIR/HIRBuilder';
import {eachPatternOperand, mapPatternOperands} from '../HIR/visitors';
import {
eachInstructionLValueWithKind,
eachPatternOperand,
mapPatternOperands,
} from '../HIR/visitors';
import {
ReactiveFunctionTransform,
Transformed,
@@ -113,6 +117,9 @@ class Visitor extends ReactiveFunctionTransform<State> {
): Transformed<ReactiveStatement> {
this.visitInstruction(instruction, state);
let instructionsToProcess: Array<ReactiveInstruction> = [instruction];
let result: Transformed<ReactiveStatement> = {kind: 'keep'};
if (instruction.value.kind === 'Destructure') {
const transformed = transformDestructuring(
state,
@@ -120,7 +127,8 @@ class Visitor extends ReactiveFunctionTransform<State> {
instruction.value,
);
if (transformed) {
return {
instructionsToProcess = transformed;
result = {
kind: 'replace-many',
value: transformed.map(instruction => ({
kind: 'instruction',
@@ -129,7 +137,17 @@ class Visitor extends ReactiveFunctionTransform<State> {
};
}
}
return {kind: 'keep'};
// Update state.declared with declarations from the instruction(s)
for (const instr of instructionsToProcess) {
for (const [place, kind] of eachInstructionLValueWithKind(instr)) {
if (kind !== InstructionKind.Reassign) {
state.declared.add(place.identifier.declarationId);
}
}
}
return result;
}
}
@@ -144,10 +162,13 @@ function transformDestructuring(
const isDeclared = state.declared.has(place.identifier.declarationId);
if (isDeclared) {
reassigned.add(place.identifier.id);
} else {
hasDeclaration = true;
}
hasDeclaration ||= !isDeclared;
}
if (reassigned.size === 0 || !hasDeclaration) {
if (!hasDeclaration) {
// all reassignments
destructure.lvalue.kind = InstructionKind.Reassign;
return null;
}
/*

View File

@@ -389,14 +389,6 @@ export function findDisjointMutableValues(
*/
operand.identifier.mutableRange.start > 0
) {
if (
instr.value.kind === 'FunctionExpression' ||
instr.value.kind === 'ObjectMethod'
) {
if (operand.identifier.type.kind === 'Primitive') {
continue;
}
}
operands.push(operand.identifier);
}
}

View File

@@ -25,6 +25,7 @@ import {
} from '../HIR/HIR';
import {
BuiltInArrayId,
BuiltInEventHandlerId,
BuiltInFunctionId,
BuiltInJsxId,
BuiltInMixedReadonlyId,
@@ -471,6 +472,41 @@ function* generateInstructionTypes(
}
}
}
if (env.config.enableInferEventHandlers) {
if (
value.kind === 'JsxExpression' &&
value.tag.kind === 'BuiltinTag' &&
!value.tag.name.includes('-')
) {
/*
* Infer event handler types for built-in DOM elements.
* Props starting with "on" (e.g., onClick, onSubmit) on primitive tags
* are inferred as event handlers. This allows functions with ref access
* to be passed to these props, since DOM event handlers are guaranteed
* by React to only execute in response to events, never during render.
*
* We exclude tags with hyphens to avoid web components (custom elements),
* which are required by the HTML spec to contain a hyphen. Web components
* may call event handler props during their lifecycle methods (e.g.,
* connectedCallback), which would be unsafe for ref access.
*/
for (const prop of value.props) {
if (
prop.kind === 'JsxAttribute' &&
prop.name.startsWith('on') &&
prop.name.length > 2 &&
prop.name[2] === prop.name[2].toUpperCase()
) {
yield equation(prop.place.identifier.type, {
kind: 'Function',
shapeId: BuiltInEventHandlerId,
return: makeType(),
isConstructor: false,
});
}
}
}
}
yield equation(left, {kind: 'Object', shapeId: BuiltInJsxId});
break;
}

View File

@@ -167,6 +167,14 @@ export function Set_filter<T>(
return result;
}
export function Set_subtract<T>(
source: ReadonlySet<T>,
other: Iterable<T>,
): Set<T> {
const otherSet = other instanceof Set ? other : new Set(other);
return Set_filter(source, item => !otherSet.has(item));
}
export function hasNode<T>(
input: NodePath<T | null | undefined>,
): input is NodePath<NonNullable<T>> {

View File

@@ -22,6 +22,7 @@ import {
BasicBlock,
isUseRefType,
SourceLocation,
ArrayExpression,
} from '../HIR';
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
@@ -36,15 +37,23 @@ type DerivationMetadata = {
isStateSource: boolean;
};
type EffectMetadata = {
effect: HIRFunction;
dependencies: ArrayExpression;
};
type ValidationContext = {
readonly functions: Map<IdentifierId, FunctionExpression>;
readonly candidateDependencies: Map<IdentifierId, ArrayExpression>;
readonly errors: CompilerError;
readonly derivationCache: DerivationCache;
readonly effects: Set<HIRFunction>;
readonly effectsCache: Map<IdentifierId, EffectMetadata>;
readonly setStateLoads: Map<IdentifierId, IdentifierId | null>;
readonly setStateUsages: Map<IdentifierId, Set<SourceLocation>>;
};
const MAX_FIXPOINT_ITERATIONS = 100;
class DerivationCache {
hasChanges: boolean = false;
cache: Map<IdentifierId, DerivationMetadata> = new Map();
@@ -175,18 +184,20 @@ export function validateNoDerivedComputationsInEffects_exp(
fn: HIRFunction,
): Result<void, CompilerError> {
const functions: Map<IdentifierId, FunctionExpression> = new Map();
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
const derivationCache = new DerivationCache();
const errors = new CompilerError();
const effects: Set<HIRFunction> = new Set();
const effectsCache: Map<IdentifierId, EffectMetadata> = new Map();
const setStateLoads: Map<IdentifierId, IdentifierId> = new Map();
const setStateUsages: Map<IdentifierId, Set<SourceLocation>> = new Map();
const context: ValidationContext = {
functions,
candidateDependencies,
errors,
derivationCache,
effects,
effectsCache,
setStateLoads,
setStateUsages,
};
@@ -215,6 +226,7 @@ export function validateNoDerivedComputationsInEffects_exp(
}
let isFirstPass = true;
let iterationCount = 0;
do {
context.derivationCache.takeSnapshot();
@@ -227,10 +239,23 @@ export function validateNoDerivedComputationsInEffects_exp(
context.derivationCache.checkForChanges();
isFirstPass = false;
iterationCount++;
CompilerError.invariant(iterationCount < MAX_FIXPOINT_ITERATIONS, {
reason:
'[ValidateNoDerivedComputationsInEffects] Fixpoint iteration failed to converge.',
description: `Fixpoint iteration exceeded ${MAX_FIXPOINT_ITERATIONS} iterations while tracking derivations. This suggests a cyclic dependency in the derivation cache.`,
details: [
{
kind: 'error',
loc: fn.loc,
message: `Exceeded ${MAX_FIXPOINT_ITERATIONS} iterations in ValidateNoDerivedComputationsInEffects`,
},
],
});
} while (context.derivationCache.snapshot());
for (const effect of effects) {
validateEffect(effect, context);
for (const [, effect] of effectsCache) {
validateEffect(effect.effect, effect.dependencies, context);
}
return errors.asResult();
@@ -354,10 +379,16 @@ function recordInstructionDerivations(
value.args[1].kind === 'Identifier'
) {
const effectFunction = context.functions.get(value.args[0].identifier.id);
if (effectFunction != null) {
context.effects.add(effectFunction.loweredFunc.func);
const deps = context.candidateDependencies.get(
value.args[1].identifier.id,
);
if (effectFunction != null && deps != null) {
context.effectsCache.set(value.args[0].identifier.id, {
effect: effectFunction.loweredFunc.func,
dependencies: deps,
});
}
} else if (isUseStateType(lvalue.identifier) && value.args.length > 0) {
} else if (isUseStateType(lvalue.identifier)) {
typeOfValue = 'fromState';
context.derivationCache.addDerivationEntry(
lvalue,
@@ -367,6 +398,8 @@ function recordInstructionDerivations(
);
return;
}
} else if (value.kind === 'ArrayExpression') {
context.candidateDependencies.set(lvalue.identifier.id, value);
}
for (const operand of eachInstructionOperand(instr)) {
@@ -405,6 +438,14 @@ function recordInstructionDerivations(
);
}
if (value.kind === 'FunctionExpression') {
/*
* We don't want to record effect mutations of FunctionExpressions the mutations will happen in the
* function body and we will record them there.
*/
return;
}
for (const operand of eachInstructionOperand(instr)) {
switch (operand.effect) {
case Effect.Capture:
@@ -495,6 +536,19 @@ function buildTreeNode(
const namedSiblings: Set<string> = new Set();
for (const childId of sourceMetadata.sourcesIds) {
CompilerError.invariant(childId !== sourceId, {
reason:
'Unexpected self-reference: a value should not have itself as a source',
description: null,
details: [
{
kind: 'error',
loc: sourceMetadata.place.loc,
message: null,
},
],
});
const childNodes = buildTreeNode(
childId,
context,
@@ -596,6 +650,7 @@ function getFnLocalDeps(
function validateEffect(
effectFunction: HIRFunction,
dependencies: ArrayExpression,
context: ValidationContext,
): void {
const seenBlocks: Set<BlockId> = new Set();
@@ -612,6 +667,16 @@ function validateEffect(
Set<SourceLocation>
> = new Map();
// Consider setStates in the effect's dependency array as being part of effectSetStateUsages
for (const dep of dependencies.elements) {
if (dep.kind === 'Identifier') {
const root = getRootSetState(dep.identifier.id, context.setStateLoads);
if (root !== null) {
effectSetStateUsages.set(root, new Set([dep.loc]));
}
}
}
let cleanUpFunctionDeps: Set<IdentifierId> | undefined;
const globals: Set<IdentifierId> = new Set();
@@ -662,6 +727,18 @@ function validateEffect(
instr.value.args.length === 1 &&
instr.value.args[0].kind === 'Identifier'
) {
const calleeMetadata = context.derivationCache.cache.get(
instr.value.callee.identifier.id,
);
/*
* If the setState comes from a source other than local state skip
* since the fix is not to calculate in render
*/
if (calleeMetadata?.typeOfValue != 'fromState') {
continue;
}
const argMetadata = context.derivationCache.cache.get(
instr.value.args[0].identifier.id,
);

View File

@@ -1,59 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {CompilerDiagnostic, CompilerError} from '..';
import {ErrorCategory} from '../CompilerError';
import {HIRFunction} from '../HIR';
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
import {Result} from '../Utils/Result';
/**
* Checks that known-impure functions are not called during render. Examples of invalid functions to
* call during render are `Math.random()` and `Date.now()`. Users may extend this set of
* impure functions via a module type provider and specifying functions with `impure: true`.
*
* TODO: add best-effort analysis of functions which are called during render. We have variations of
* this in several of our validation passes and should unify those analyses into a reusable helper
* and use it here.
*/
export function validateNoImpureFunctionsInRender(
fn: HIRFunction,
): Result<void, CompilerError> {
const errors = new CompilerError();
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
const value = instr.value;
if (value.kind === 'MethodCall' || value.kind == 'CallExpression') {
const callee =
value.kind === 'MethodCall' ? value.property : value.callee;
const signature = getFunctionCallSignature(
fn.env,
callee.identifier.type,
);
if (signature != null && signature.impure === true) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Purity,
reason: 'Cannot call impure function during render',
description:
(signature.canonicalName != null
? `\`${signature.canonicalName}\` is an impure function. `
: '') +
'Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
suggestions: null,
}).withDetails({
kind: 'error',
loc: callee.loc,
message: 'Cannot call impure function',
}),
);
}
}
}
}
return errors.asResult();
}

View File

@@ -0,0 +1,387 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {CompilerDiagnostic, CompilerError, Effect} from '..';
import {
areEqualSourceLocations,
HIRFunction,
IdentifierId,
InstructionId,
isJsxType,
isUseRefType,
} from '../HIR';
import {AliasingEffect, hashEffect} from '../Inference/AliasingEffects';
import {createControlDominators} from '../Inference/ControlDominators';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
import {Err, Ok, Result} from '../Utils/Result';
import {getOrInsertWith} from '../Utils/utils';
import {printFunction} from '../HIR/PrintHIR';
type ImpureEffect = Extract<AliasingEffect, {kind: 'Impure'}>;
type RenderEffect = Extract<AliasingEffect, {kind: 'Render'}>;
type FunctionCache = Map<HIRFunction, Map<string, ImpuritySignature>>;
type ImpuritySignature = {
effects: Map<IdentifierId, ImpureEffect>;
error: CompilerError;
returns: IdentifierId;
};
export function validateNoImpureValuesInRender(
fn: HIRFunction,
): Result<void, CompilerError> {
const impure = new Map<IdentifierId, ImpureEffect>();
const impureFunctions = new Map<IdentifierId, ImpuritySignature>();
const result = inferImpureValues(fn, impure, impureFunctions, new Map());
if (result.error.hasAnyErrors()) {
return Err(result.error);
}
return Ok(undefined);
}
function inferFunctionExpressionMemo(
fn: HIRFunction,
impure: Map<IdentifierId, ImpureEffect>,
impureFunctions: Map<IdentifierId, ImpuritySignature>,
cache: FunctionCache,
): ImpuritySignature {
const key = fn.context
.map(
place =>
`${place.identifier.id}:${impure.has(place.identifier.id)}:${Array.from(
impureFunctions.get(place.identifier.id)?.effects ?? new Map(),
)
.map(([id, effect]) => `${id}=>${effect.into.identifier.id}`)
.join(',')}`,
)
.join(',');
return getOrInsertWith(
getOrInsertWith(cache, fn, () => new Map()),
key,
() => inferImpureValues(fn, impure, impureFunctions, cache),
);
}
function processEffects(
id: InstructionId,
effects: Array<AliasingEffect>,
impure: Map<IdentifierId, ImpureEffect>,
impureFunctions: Map<IdentifierId, ImpuritySignature>,
cache: FunctionCache,
): boolean {
let hasChanges = false;
const rendered: Set<IdentifierId> = new Set();
for (const effect of effects) {
if (effect.kind === 'Render') {
rendered.add(effect.place.identifier.id);
}
}
for (const effect of effects) {
switch (effect.kind) {
case 'Alias':
case 'Assign':
case 'Capture':
case 'CreateFrom':
case 'ImmutableCapture': {
const sourceEffect = impure.get(effect.from.identifier.id);
if (
sourceEffect != null &&
!impure.has(effect.into.identifier.id) &&
!rendered.has(effect.from.identifier.id) &&
!isUseRefType(effect.into.identifier) &&
!isJsxType(effect.into.identifier.type)
) {
// console.log(
// `${effect.kind} $${effect.into.identifier.id} <= $${effect.from.identifier.id} ($${sourceEffect.into.identifier.id} forward)`,
// );
impure.set(effect.into.identifier.id, sourceEffect);
hasChanges = true;
}
if (
sourceEffect == null &&
(effect.kind === 'Assign' || effect.kind === 'Capture') &&
!impure.has(effect.from.identifier.id) &&
!rendered.has(effect.from.identifier.id) &&
!isUseRefType(effect.from.identifier) &&
isMutable({id}, effect.into)
) {
const destinationEffect = impure.get(effect.into.identifier.id);
if (destinationEffect != null) {
// console.log(
// `${effect.kind} $${effect.into.identifier.id} => $${effect.from.identifier.id} ($${destinationEffect.into.identifier.id} backward)`,
// );
impure.set(effect.from.identifier.id, destinationEffect);
hasChanges = true;
}
}
if (
(effect.kind === 'Alias' ||
effect.kind === 'Assign' ||
effect.kind === 'ImmutableCapture') &&
!rendered.has(effect.into.identifier.id) &&
!isJsxType(effect.into.identifier.type)
) {
const functionEffect = impureFunctions.get(effect.from.identifier.id);
if (
functionEffect != null &&
!impureFunctions.has(effect.into.identifier.id)
// ||
// !areEqualFunctionSignatures(
// impureFunctions.get(effect.into.identifier.id)!.effects,
// functionEffect.effects,
// )
) {
// console.log(
// `${effect.kind} $${effect.into.identifier.id} <= $${effect.from.identifier.id} (function)`,
// );
impureFunctions.set(effect.into.identifier.id, functionEffect);
hasChanges = true;
}
}
break;
}
case 'Impure': {
if (!impure.has(effect.into.identifier.id)) {
// console.log(`Impure $${effect.into.identifier.id}`);
impure.set(effect.into.identifier.id, effect);
hasChanges = true;
}
break;
}
case 'Render': {
break;
}
case 'CreateFunction': {
const result = inferFunctionExpressionMemo(
effect.function.loweredFunc.func,
impure,
impureFunctions,
cache,
);
if (result.error.hasAnyErrors()) {
break;
}
const previousResult = impureFunctions.get(effect.into.identifier.id);
if (
previousResult == null ||
!areEqualFunctionSignatures(result.effects, previousResult.effects)
) {
// console.log(`Function $${effect.into.identifier.id}`);
impureFunctions.set(effect.into.identifier.id, result);
hasChanges = true;
}
break;
}
case 'Apply': {
const functionSignature = impureFunctions.get(
effect.function.identifier.id,
);
if (functionSignature != null) {
for (const [id, functionEffect] of functionSignature.effects) {
if (!impure.has(id)) {
impure.set(id, functionEffect);
hasChanges = true;
}
if (
id === functionSignature.returns &&
!impure.has(effect.into.identifier.id)
) {
impure.set(effect.into.identifier.id, functionEffect);
hasChanges = true;
}
}
}
break;
}
case 'MaybeAlias':
case 'Create':
case 'Freeze':
case 'Mutate':
case 'MutateConditionally':
case 'MutateFrozen':
case 'MutateGlobal':
case 'MutateTransitive':
case 'MutateTransitiveConditionally': {
break;
}
}
}
return hasChanges;
}
function inferImpureValues(
fn: HIRFunction,
impure: Map<IdentifierId, ImpureEffect>,
impureFunctions: Map<IdentifierId, ImpuritySignature>,
cache: FunctionCache,
): ImpuritySignature {
const getBlockControl = createControlDominators(fn, place => {
return impure.has(place.identifier.id);
});
let hasChanges = false;
let iterations = 0;
do {
hasChanges = false;
if (iterations++ > 100) {
throw new Error('too many iterations');
}
for (const block of fn.body.blocks.values()) {
const controlPlace = getBlockControl(block.id);
const controlImpureEffect =
controlPlace != null ? impure.get(controlPlace.identifier.id) : null;
for (const phi of block.phis) {
if (impure.has(phi.place.identifier.id)) {
// Already marked impure on a previous pass
continue;
}
let impureEffect = null;
for (const [, operand] of phi.operands) {
const operandEffect = impure.get(operand.identifier.id);
if (operandEffect != null) {
impureEffect = operandEffect;
break;
}
}
if (impureEffect != null) {
impure.set(phi.place.identifier.id, impureEffect);
hasChanges = true;
} else {
for (const [pred] of phi.operands) {
const predControl = getBlockControl(pred);
if (predControl != null) {
const predEffect = impure.get(predControl.identifier.id);
if (predEffect != null) {
impure.set(phi.place.identifier.id, predEffect);
hasChanges = true;
break;
}
}
}
}
}
for (const instr of block.instructions) {
const _impure = new Set(impure.keys());
hasChanges =
processEffects(
instr.id,
instr.effects ?? [],
impure,
impureFunctions,
cache,
) || hasChanges;
}
if (block.terminal.kind === 'return' && block.terminal.effects != null) {
hasChanges =
processEffects(
block.terminal.id,
block.terminal.effects,
impure,
impureFunctions,
cache,
) || hasChanges;
}
}
} while (hasChanges);
fn.env.logger?.debugLogIRs?.({
kind: 'debug',
name: 'ValidateNoImpureValuesInRender',
value: JSON.stringify(Array.from(impure.keys()).sort(), null, 2),
});
fn.env.logger?.debugLogIRs?.({
kind: 'debug',
name: 'ValidateNoImpureValuesInRender (function)',
value: JSON.stringify(Array.from(impureFunctions.keys()).sort(), null, 2),
});
const error = new CompilerError();
function validateRenderEffect(effect: RenderEffect): void {
let impureEffect = impure.get(effect.place.identifier.id);
if (impureEffect == null) {
const functionSignature = impureFunctions.get(effect.place.identifier.id);
impureEffect = functionSignature?.effects.get(functionSignature.returns);
}
if (impureEffect == null) {
return;
}
const diagnostic = CompilerDiagnostic.create({
category: impureEffect.category,
reason: impureEffect.reason,
description: impureEffect.description,
}).withDetails({
kind: 'error',
loc: effect.place.loc,
message: impureEffect.usageMessage,
});
if (!areEqualSourceLocations(effect.place.loc, impureEffect.into.loc)) {
diagnostic.withDetails({
kind: 'error',
loc: impureEffect.into.loc,
message: impureEffect.sourceMessage,
});
}
error.pushDiagnostic(diagnostic);
}
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const value = instr.value;
if (
value.kind === 'FunctionExpression' ||
value.kind === 'ObjectMethod'
) {
const result = inferFunctionExpressionMemo(
value.loweredFunc.func,
impure,
impureFunctions,
cache,
);
if (result.error.hasAnyErrors()) {
error.merge(result.error);
}
}
for (const effect of instr.effects ?? []) {
if (effect.kind === 'Render') {
validateRenderEffect(effect);
}
}
}
if (block.terminal.kind === 'return' && block.terminal.effects != null) {
for (const effect of block.terminal.effects) {
if (effect.kind === 'Render') {
validateRenderEffect(effect);
}
}
}
}
const impureEffects: Map<IdentifierId, ImpureEffect> = new Map();
for (const param of [...fn.context, ...fn.params, fn.returns]) {
const place = param.kind === 'Identifier' ? param : param.place;
const impureEffect = impure.get(place.identifier.id);
if (impureEffect != null) {
impureEffects.set(place.identifier.id, impureEffect);
}
}
return {effects: impureEffects, error, returns: fn.returns.identifier.id};
}
function areEqualFunctionSignatures(
sig1: Map<IdentifierId, ImpureEffect>,
sig2: Map<IdentifierId, ImpureEffect>,
): boolean {
return (
sig1.size === sig2.size &&
Array.from(sig1).every(
([id, effect]) =>
sig2.has(id) && hashEffect(effect) === hashEffect(sig2.get(id)!),
)
);
}

View File

@@ -0,0 +1,137 @@
# ValidateNoRefAccessInRender
This document summarizes the design and key learnings for the ref mutation validation pass.
## Purpose
Validates that a function does not mutate a ref value during render. This ensures React components follow the rules of React by not writing to `ref.current` during the render phase.
## Key Concepts
### Ref vs RefValue
- **Ref**: The ref object itself (e.g., `useRef()` return value). Has type `React.RefObject<T>`.
- **RefValue**: The `.current` property of a ref. This is the mutable value that should not be accessed during render.
The validation tracks both using a `RefInfo` type with a `refId` that correlates refs with their `.current` values.
### What Constitutes a Mutation
A mutation is any `PropertyStore` or `ComputedStore` instruction where:
1. The target object is a known ref (tracked in the `refs` map)
2. OR the target object has a ref type (`isUseRefType`)
### Allowed Patterns
1. **Event handlers and effect callbacks**: Functions that are not called at the top level during render can mutate refs freely.
2. **Null-guard initialization**: The pattern `if (ref.current == null) { ref.current = value; }` is allowed because it's a common lazy initialization pattern that only runs once.
## Algorithm: Single Forward Data-Flow Pass
The validation uses a single forward pass over all blocks:
### Phase 1: Track Refs
- Initialize refs from function params and context (captured variables)
- Process phi nodes to propagate ref info through control flow joins
- Track refs through LoadLocal, StoreLocal, PropertyLoad operations
### Phase 2: Detect Null Guards
- Track nullable values (null literals, undefined)
- Track binary comparisons of `ref.current` to null (`==`, `===`, `!=`, `!==`)
- Mark blocks as "safe" for specific refs when inside null-guard branches
- Propagate safety through control flow until fallthrough
### Phase 3: Validate Mutations
- For PropertyStore/ComputedStore on refs:
- If inside a null-guard for this ref: allow (but track for duplicate detection)
- If at top level: error immediately
- If in nested function: track for later (error if function is called)
### Phase 4: Track Ref-Mutating Functions
- When a FunctionExpression mutates a ref, track it in `refMutatingFunctions`
- When such a function is called at top level, report the error at the mutation site
## Key Data Structures
```typescript
// Correlates refs with their .current values
type RefInfo = {
kind: 'Ref' | 'RefValue';
refId: number;
};
// Tracks null-guard conditions
type GuardInfo = {
refId: number;
isEquality: boolean; // true for ==, ===; false for !=, !==
};
// Information about a mutation (for error reporting)
type MutationInfo = {
loc: SourceLocation;
isCurrentProperty: boolean;
};
```
## Error Reporting
### Error Location
Errors highlight the **entire instruction** (e.g., `ref.current = value`), not just the ref identifier. This is achieved by using `instr.loc` instead of `value.object.loc`.
### Duplicate Initialization
When a ref is initialized more than once inside a null-guard:
1. Primary error: Points to the second initialization
2. Secondary error: Points to the first initialization with "Ref was first initialized here"
### Transitive Mutations
When a function that mutates refs is called during render:
- The error points to the mutation site inside the function
- Not the call site (the call site is what triggers the check)
## Edge Cases and Patterns
### Unary NOT on Guards
The `!` operator inverts guard polarity:
```javascript
if (!ref.current) { ... } // Same as: if (ref.current == null)
```
### Nested Functions
Functions defined during render but not called are allowed to mutate refs:
```javascript
// OK - onClick is not called during render
const onClick = () => { ref.current = value; };
return <button onClick={onClick} />;
```
### Props with Ref Type
Refs can come from props. The validation handles `props.ref` by checking type information.
## Limitations / Known Gaps
The following patterns are NOT currently validated by this pass:
1. **Impure values in render**: `Date.now()`, `Math.random()` flowing into render context (handled by `ValidateNoImpureValuesInRender`)
2. **useState/useReducer callbacks**: These hooks call their initializer functions during render, so ref access inside them should error. This requires special hook semantics.
3. **Ref reads during render**: This pass focuses on mutations. Ref reads are handled separately.
## Testing
Test fixtures use naming conventions:
- `error.*.ts` - Fixtures expected to produce compilation errors
- Regular names - Fixtures expected to compile successfully
Run tests with:
```bash
yarn snap -p <pattern> --nodebug # Run specific tests
yarn snap -p <pattern> --nodebug -u # Update expected output
```

View File

@@ -16,18 +16,23 @@ import {
IdentifierId,
isSetStateType,
isUseEffectHookType,
isUseEffectEventType,
isUseInsertionEffectHookType,
isUseLayoutEffectHookType,
isUseRefType,
isRefValueType,
Place,
Effect,
BlockId,
} from '../HIR';
import {
eachInstructionLValue,
eachInstructionValueOperand,
} from '../HIR/visitors';
import {createControlDominators} from '../Inference/ControlDominators';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
import {Result} from '../Utils/Result';
import {Iterable_some} from '../Utils/utils';
import {assertExhaustive, Iterable_some} from '../Utils/utils';
/**
* Validates against calling setState in the body of an effect (useEffect and friends),
@@ -94,7 +99,20 @@ export function validateNoSetStateInEffects(
instr.value.kind === 'MethodCall'
? instr.value.receiver
: instr.value.callee;
if (
if (isUseEffectEventType(callee.identifier)) {
const arg = instr.value.args[0];
if (arg !== undefined && arg.kind === 'Identifier') {
const setState = setStateFunctions.get(arg.identifier.id);
if (setState !== undefined) {
/**
* This effect event function calls setState synchonously,
* treat it as a setState function for transitive tracking
*/
setStateFunctions.set(instr.lvalue.identifier.id, setState);
}
}
} else if (
isUseEffectHookType(callee.identifier) ||
isUseLayoutEffectHookType(callee.identifier) ||
isUseInsertionEffectHookType(callee.identifier)
@@ -103,26 +121,58 @@ export function validateNoSetStateInEffects(
if (arg !== undefined && arg.kind === 'Identifier') {
const setState = setStateFunctions.get(arg.identifier.id);
if (setState !== undefined) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.EffectSetState,
reason:
'Calling setState synchronously within an effect can trigger cascading renders',
description:
'Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. ' +
'In general, the body of an effect should do one or both of the following:\n' +
'* Update external systems with the latest state from React.\n' +
'* Subscribe for updates from some external system, calling setState in a callback function when external state changes.\n\n' +
'Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. ' +
'(https://react.dev/learn/you-might-not-need-an-effect)',
suggestions: null,
}).withDetails({
kind: 'error',
loc: setState.loc,
message:
'Avoid calling setState() directly within an effect',
}),
);
const enableVerbose =
env.config.enableVerboseNoSetStateInEffect;
if (enableVerbose) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.EffectSetState,
reason:
'Calling setState synchronously within an effect can trigger cascading renders',
description:
'Effects are intended to synchronize state between React and external systems. ' +
'Calling setState synchronously causes cascading renders that hurt performance.\n\n' +
'This pattern may indicate one of several issues:\n\n' +
'**1. Non-local derived data**: If the value being set could be computed from props/state ' +
'but requires data from a parent component, consider restructuring state ownership so the ' +
'derivation can happen during render in the component that owns the relevant state.\n\n' +
"**2. Derived event pattern**: If you're detecting when a prop changes (e.g., `isPlaying` " +
'transitioning from false to true), this often indicates the parent should provide an event ' +
'callback (like `onPlay`) instead of just the current state. Request access to the original event.\n\n' +
"**3. Force update / external sync**: If you're forcing a re-render to sync with an external " +
'data source (mutable values outside React), use `useSyncExternalStore` to properly subscribe ' +
'to external state changes.\n\n' +
'See: https://react.dev/learn/you-might-not-need-an-effect',
suggestions: null,
}).withDetails({
kind: 'error',
loc: setState.loc,
message:
'Avoid calling setState() directly within an effect',
}),
);
} else {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.EffectSetState,
reason:
'Calling setState synchronously within an effect can trigger cascading renders',
description:
'Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. ' +
'In general, the body of an effect should do one or both of the following:\n' +
'* Update external systems with the latest state from React.\n' +
'* Subscribe for updates from some external system, calling setState in a callback function when external state changes.\n\n' +
'Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. ' +
'(https://react.dev/learn/you-might-not-need-an-effect)',
suggestions: null,
}).withDetails({
kind: 'error',
loc: setState.loc,
message:
'Avoid calling setState() directly within an effect',
}),
);
}
}
}
}
@@ -140,6 +190,8 @@ function getSetStateCall(
setStateFunctions: Map<IdentifierId, Place>,
env: Environment,
): Place | null {
const enableAllowSetStateFromRefsInEffects =
env.config.enableAllowSetStateFromRefsInEffects;
const refDerivedValues: Set<IdentifierId> = new Set();
const isDerivedFromRef = (place: Place): boolean => {
@@ -150,9 +202,38 @@ function getSetStateCall(
);
};
const isRefControlledBlock: (id: BlockId) => Place | null =
enableAllowSetStateFromRefsInEffects
? createControlDominators(fn, place => isDerivedFromRef(place))
: (): Place | null => null;
for (const [, block] of fn.body.blocks) {
if (enableAllowSetStateFromRefsInEffects) {
for (const phi of block.phis) {
if (isDerivedFromRef(phi.place)) {
continue;
}
let isPhiDerivedFromRef = false;
for (const [, operand] of phi.operands) {
if (isDerivedFromRef(operand)) {
isPhiDerivedFromRef = true;
break;
}
}
if (isPhiDerivedFromRef) {
refDerivedValues.add(phi.place.identifier.id);
} else {
for (const [pred] of phi.operands) {
if (isRefControlledBlock(pred) != null) {
refDerivedValues.add(phi.place.identifier.id);
break;
}
}
}
}
}
for (const instr of block.instructions) {
if (env.config.enableAllowSetStateFromRefsInEffects) {
if (enableAllowSetStateFromRefsInEffects) {
const hasRefOperand = Iterable_some(
eachInstructionValueOperand(instr.value),
isDerivedFromRef,
@@ -162,6 +243,46 @@ function getSetStateCall(
for (const lvalue of eachInstructionLValue(instr)) {
refDerivedValues.add(lvalue.identifier.id);
}
// Ref-derived values can also propagate through mutation
for (const operand of eachInstructionValueOperand(instr.value)) {
switch (operand.effect) {
case Effect.Capture:
case Effect.Store:
case Effect.ConditionallyMutate:
case Effect.ConditionallyMutateIterator:
case Effect.Mutate: {
if (isMutable(instr, operand)) {
refDerivedValues.add(operand.identifier.id);
}
break;
}
case Effect.Freeze:
case Effect.Read: {
// no-op
break;
}
case Effect.Unknown: {
CompilerError.invariant(false, {
reason: 'Unexpected unknown effect',
description: null,
details: [
{
kind: 'error',
loc: operand.loc,
message: null,
},
],
suggestions: null,
});
}
default: {
assertExhaustive(
operand.effect,
`Unexpected effect kind \`${operand.effect}\``,
);
}
}
}
}
if (
@@ -203,7 +324,7 @@ function getSetStateCall(
isSetStateType(callee.identifier) ||
setStateFunctions.has(callee.identifier.id)
) {
if (env.config.enableAllowSetStateFromRefsInEffects) {
if (enableAllowSetStateFromRefsInEffects) {
const arg = instr.value.args.at(0);
if (
arg !== undefined &&
@@ -216,6 +337,8 @@ function getSetStateCall(
* be needed when initial layout measurements from refs need to be stored in state.
*/
return null;
} else if (isRefControlledBlock(block.id) != null) {
continue;
}
}
/*

View File

@@ -155,20 +155,40 @@ function validateNoSetStateInRenderImpl(
}),
);
} else if (unconditionalBlocks.has(block.id)) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.RenderSetState,
reason:
'Calling setState during render may trigger an infinite loop',
description:
'Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState)',
suggestions: null,
}).withDetails({
kind: 'error',
loc: callee.loc,
message: 'Found setState() in render',
}),
);
const enableUseKeyedState = fn.env.config.enableUseKeyedState;
if (enableUseKeyedState) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.RenderSetState,
reason: 'Cannot call setState during render',
description:
'Calling setState during render may trigger an infinite loop.\n' +
'* To reset state when other state/props change, use `const [state, setState] = useKeyedState(initialState, key)` to reset `state` when `key` changes.\n' +
'* To derive data from other state/props, compute the derived data during render without using state',
suggestions: null,
}).withDetails({
kind: 'error',
loc: callee.loc,
message: 'Found setState() in render',
}),
);
} else {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.RenderSetState,
reason: 'Cannot call setState during render',
description:
'Calling setState during render may trigger an infinite loop.\n' +
'* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders\n' +
'* To derive data from other state/props, compute the derived data during render without using state',
suggestions: null,
}).withDetails({
kind: 'error',
loc: callee.loc,
message: 'Found setState() in render',
}),
);
}
}
}
break;

View File

@@ -242,6 +242,7 @@ function validateInferredDep(
normalizedDep = {
root: maybeNormalizedRoot.root,
path: [...maybeNormalizedRoot.path, ...dep.path],
loc: maybeNormalizedRoot.loc,
};
} else {
CompilerError.invariant(dep.identifier.name?.kind === 'named', {
@@ -267,8 +268,10 @@ function validateInferredDep(
effect: Effect.Read,
reactive: false,
},
constant: false,
},
path: [...dep.path],
loc: GeneratedSource,
};
}
for (const decl of declsWithinMemoBlock) {
@@ -379,8 +382,10 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
root: {
kind: 'NamedLocal',
value: storeTarget,
constant: false,
},
path: [],
loc: storeTarget.loc,
});
}
}
@@ -408,8 +413,10 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
root: {
kind: 'NamedLocal',
value: {...lvalue},
constant: false,
},
path: [],
loc: lvalue.loc,
});
}
}

View File

@@ -0,0 +1,296 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {CompilerDiagnostic, CompilerError, ErrorCategory} from '..';
import {CodegenFunction} from '../ReactiveScopes';
import {Result} from '../Utils/Result';
/**
* IMPORTANT: This validation is only intended for use in unit tests.
* It is not intended for use in production.
*
* This validation is used to ensure that the generated AST has proper source locations
* for "important" original nodes.
*
* There's one big gotcha with this validation: it only works if the "important" original nodes
* are not optimized away by the compiler.
*
* When that scenario happens, we should just update the fixture to not include a node that has no
* corresponding node in the generated AST due to being completely removed during compilation.
*/
/**
* Some common node types that are important for coverage tracking.
* Based on istanbul-lib-instrument + some other common nodes we expect to be present in the generated AST.
*
* Note: For VariableDeclaration, VariableDeclarator, and Identifier, we enforce stricter validation
* that requires both the source location AND node type to match in the generated AST. This ensures
* that variable declarations maintain their structural integrity through compilation.
*/
const IMPORTANT_INSTRUMENTED_TYPES = new Set([
'ArrowFunctionExpression',
'AssignmentPattern',
'ObjectMethod',
'ExpressionStatement',
'BreakStatement',
'ContinueStatement',
'ReturnStatement',
'ThrowStatement',
'TryStatement',
'VariableDeclarator',
'IfStatement',
'ForStatement',
'ForInStatement',
'ForOfStatement',
'WhileStatement',
'DoWhileStatement',
'SwitchStatement',
'SwitchCase',
'WithStatement',
'FunctionDeclaration',
'FunctionExpression',
'LabeledStatement',
'ConditionalExpression',
'LogicalExpression',
/**
* Note: these aren't important for coverage tracking,
* but we still want to track them to ensure we aren't regressing them when
* we fix the source location tracking for other nodes.
*/
'VariableDeclaration',
'Identifier',
]);
/**
* Check if a node is a manual memoization call that the compiler optimizes away.
* These include useMemo and useCallback calls, which are intentionally removed
* by the DropManualMemoization pass.
*/
function isManualMemoization(node: t.Node): boolean {
// Check if this is a useMemo/useCallback call expression
if (t.isCallExpression(node)) {
const callee = node.callee;
if (t.isIdentifier(callee)) {
return callee.name === 'useMemo' || callee.name === 'useCallback';
}
if (
t.isMemberExpression(callee) &&
t.isIdentifier(callee.property) &&
t.isIdentifier(callee.object)
) {
return (
callee.object.name === 'React' &&
(callee.property.name === 'useMemo' ||
callee.property.name === 'useCallback')
);
}
}
return false;
}
/**
* Create a location key for comparison. We compare by line/column/source,
* not by object identity.
*/
function locationKey(loc: t.SourceLocation): string {
return `${loc.start.line}:${loc.start.column}-${loc.end.line}:${loc.end.column}`;
}
/**
* Validates that important source locations from the original code are preserved
* in the generated AST. This ensures that Istanbul coverage instrumentation can
* properly map back to the original source code.
*
* The validator:
* 1. Collects locations from "important" nodes in the original AST (those that
* Istanbul instruments for coverage tracking)
* 2. Exempts known compiler optimizations (useMemo/useCallback removal)
* 3. Verifies that all important locations appear somewhere in the generated AST
*
* Missing locations can cause Istanbul to fail to track coverage for certain
* code paths, leading to inaccurate coverage reports.
*/
export function validateSourceLocations(
func: NodePath<
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
>,
generatedAst: CodegenFunction,
): Result<void, CompilerError> {
const errors = new CompilerError();
/*
* Step 1: Collect important locations from the original source
* Note: Multiple node types can share the same location (e.g. VariableDeclarator and Identifier)
*/
const importantOriginalLocations = new Map<
string,
{loc: t.SourceLocation; nodeTypes: Set<string>}
>();
func.traverse({
enter(path) {
const node = path.node;
// Only track node types that Istanbul instruments
if (!IMPORTANT_INSTRUMENTED_TYPES.has(node.type)) {
return;
}
// Skip manual memoization that the compiler intentionally removes
if (isManualMemoization(node)) {
return;
}
// Collect the location if it exists
if (node.loc) {
const key = locationKey(node.loc);
const existing = importantOriginalLocations.get(key);
if (existing) {
existing.nodeTypes.add(node.type);
} else {
importantOriginalLocations.set(key, {
loc: node.loc,
nodeTypes: new Set([node.type]),
});
}
}
},
});
// Step 2: Collect all locations from the generated AST with their node types
const generatedLocations = new Map<string, Set<string>>();
function collectGeneratedLocations(node: t.Node): void {
if (node.loc) {
const key = locationKey(node.loc);
const nodeTypes = generatedLocations.get(key);
if (nodeTypes) {
nodeTypes.add(node.type);
} else {
generatedLocations.set(key, new Set([node.type]));
}
}
// Use Babel's VISITOR_KEYS to traverse only actual node properties
const keys = t.VISITOR_KEYS[node.type as keyof typeof t.VISITOR_KEYS];
if (!keys) {
return;
}
for (const key of keys) {
const value = (node as any)[key];
if (Array.isArray(value)) {
for (const item of value) {
if (t.isNode(item)) {
collectGeneratedLocations(item);
}
}
} else if (t.isNode(value)) {
collectGeneratedLocations(value);
}
}
}
// Collect from main function body
collectGeneratedLocations(generatedAst.body);
// Collect from outlined functions
for (const outlined of generatedAst.outlined) {
collectGeneratedLocations(outlined.fn.body);
}
/*
* Step 3: Validate that all important locations are preserved
* For certain node types, also validate that the node type matches
*/
const strictNodeTypes = new Set([
'VariableDeclaration',
'VariableDeclarator',
'Identifier',
]);
const reportMissingLocation = (
loc: t.SourceLocation,
nodeType: string,
): void => {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Todo,
reason: 'Important source location missing in generated code',
description:
`Source location for ${nodeType} is missing in the generated output. This can cause coverage instrumentation ` +
`to fail to track this code properly, resulting in inaccurate coverage reports.`,
}).withDetails({
kind: 'error',
loc,
message: null,
}),
);
};
const reportWrongNodeType = (
loc: t.SourceLocation,
expectedType: string,
actualTypes: Set<string>,
): void => {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Todo,
reason:
'Important source location has wrong node type in generated code',
description:
`Source location for ${expectedType} exists in the generated output but with wrong node type(s): ${Array.from(actualTypes).join(', ')}. ` +
`This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports.`,
}).withDetails({
kind: 'error',
loc,
message: null,
}),
);
};
for (const [key, {loc, nodeTypes}] of importantOriginalLocations) {
const generatedNodeTypes = generatedLocations.get(key);
if (!generatedNodeTypes) {
// Location is completely missing
reportMissingLocation(loc, Array.from(nodeTypes).join(', '));
} else {
// Location exists, check each node type
for (const nodeType of nodeTypes) {
if (
strictNodeTypes.has(nodeType) &&
!generatedNodeTypes.has(nodeType)
) {
/*
* For strict node types, the specific node type must be present
* Check if any generated node type is also an important original node type
*/
const hasValidNodeType = Array.from(generatedNodeTypes).some(
genType => nodeTypes.has(genType),
);
if (hasValidNodeType) {
// At least one generated node type is valid (also in original), so this is just missing
reportMissingLocation(loc, nodeType);
} else {
// None of the generated node types are in original - this is wrong node type
reportWrongNodeType(loc, nodeType, generatedNodeTypes);
}
}
}
}
}
return errors.asResult();
}

View File

@@ -12,4 +12,5 @@ export {validateNoCapitalizedCalls} from './ValidateNoCapitalizedCalls';
export {validateNoRefAccessInRender} from './ValidateNoRefAccessInRender';
export {validateNoSetStateInRender} from './ValidateNoSetStateInRender';
export {validatePreservedManualMemoization} from './ValidatePreservedManualMemoization';
export {validateSourceLocations} from './ValidateSourceLocations';
export {validateUseMemo} from './ValidateUseMemo';

View File

@@ -0,0 +1,51 @@
## Input
```javascript
import {useRef} from 'react';
/**
* Allowed: we don't have sufficient type information to be sure that
* this accesses an impure value during render.
*/
function Component() {
const ref = useRef(null);
const object = {};
object.foo = () => ref.current;
const refValue = object.foo();
return <div>{refValue}</div>;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useRef } from "react";
/**
* Allowed: we don't have sufficient type information to be sure that
* this accesses an impure value during render.
*/
function Component() {
const $ = _c(2);
const ref = useRef(null);
const object = {};
object.foo = () => ref.current;
const refValue = object.foo();
let t0;
if ($[0] !== refValue) {
t0 = <div>{refValue}</div>;
$[0] = refValue;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,5 +1,9 @@
import {useRef} from 'react';
/**
* Allowed: we don't have sufficient type information to be sure that
* this accesses an impure value during render.
*/
function Component() {
const ref = useRef(null);
const object = {};

View File

@@ -35,10 +35,8 @@ function Component() {
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const a = makeObject_Primitives();
const x = [];
x.push(a);
mutate(x);
t0 = [x, a];
$[0] = t0;

View File

@@ -33,7 +33,6 @@ function Component() {
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
const x = [];
x.push(a);
t1 = [x, a];
$[1] = t1;
} else {

View File

@@ -85,14 +85,10 @@ function Component(t0) {
let t1;
if ($[0] !== prop) {
const obj = shallowCopy(prop);
const aliasedObj = identity(obj);
const getId = () => obj.id;
mutate(aliasedObj);
setPropertyByKey(aliasedObj, "id", prop.id + 1);
t1 = <Stringify getId={getId} shouldInvokeFns={true} />;
$[0] = prop;
$[1] = t1;

View File

@@ -181,12 +181,9 @@ function Component(t0) {
if ($[0] !== prop) {
const obj = shallowCopy(prop);
const aliasedObj = identity(obj);
const id = [obj.id];
mutate(aliasedObj);
setPropertyByKey(aliasedObj, "id", prop.id + 1);
t1 = <Stringify id={id} />;
$[0] = prop;
$[1] = t1;

View File

@@ -54,7 +54,6 @@ function Foo(t0) {
let t1;
if ($[0] !== cond1 || $[1] !== cond2) {
const arr = makeArray({ a: 2 }, 2, []);
t1 = cond1 ? (
<>
<div>{identity("foo")}</div>

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @enablePreserveExistingMemoizationGuarantees:false
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
const someGlobal = {value: 0};
@@ -33,7 +33,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import { useMemo } from "react";
const someGlobal = { value: 0 };

View File

@@ -1,4 +1,4 @@
// @enablePreserveExistingMemoizationGuarantees:false
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
const someGlobal = {value: 0};

View File

@@ -49,7 +49,6 @@ function Component() {
ref.current = "";
}
};
t0 = () => {
setRef();
};

View File

@@ -49,7 +49,6 @@ function Component() {
ref.current.value = "";
}
};
t0 = () => {
setRef();
};

View File

@@ -0,0 +1,148 @@
## Input
```javascript
// @enableInferEventHandlers
import {useRef} from 'react';
// Simulates react-hook-form's handleSubmit
function handleSubmit<T>(callback: (data: T) => void | Promise<void>) {
return (event: any) => {
event.preventDefault();
callback({} as T);
};
}
// Simulates an upload function
async function upload(file: any): Promise<{blob: {url: string}}> {
return {blob: {url: 'https://example.com/file.jpg'}};
}
interface SignatureRef {
toFile(): any;
}
function Component() {
const ref = useRef<SignatureRef>(null);
const onSubmit = async (value: any) => {
// This should be allowed: accessing ref.current in an async event handler
// that's wrapped and passed to onSubmit prop
let sigUrl: string;
if (value.hasSignature) {
const {blob} = await upload(ref.current?.toFile());
sigUrl = blob?.url || '';
} else {
sigUrl = value.signature;
}
console.log('Signature URL:', sigUrl);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input type="text" name="signature" />
<button type="submit">Submit</button>
</form>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableInferEventHandlers
import { useRef } from "react";
// Simulates react-hook-form's handleSubmit
function handleSubmit(callback) {
const $ = _c(2);
let t0;
if ($[0] !== callback) {
t0 = (event) => {
event.preventDefault();
callback({} as T);
};
$[0] = callback;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
// Simulates an upload function
async function upload(file) {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = { blob: { url: "https://example.com/file.jpg" } };
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
interface SignatureRef {
toFile(): any;
}
function Component() {
const $ = _c(4);
const ref = useRef(null);
const onSubmit = async (value) => {
let sigUrl;
if (value.hasSignature) {
const { blob } = await upload(ref.current?.toFile());
sigUrl = blob?.url || "";
} else {
sigUrl = value.signature;
}
console.log("Signature URL:", sigUrl);
};
const t0 = handleSubmit(onSubmit);
let t1;
let t2;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <input type="text" name="signature" />;
t2 = <button type="submit">Submit</button>;
$[0] = t1;
$[1] = t2;
} else {
t1 = $[0];
t2 = $[1];
}
let t3;
if ($[2] !== t0) {
t3 = (
<form onSubmit={t0}>
{t1}
{t2}
</form>
);
$[2] = t0;
$[3] = t3;
} else {
t3 = $[3];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
### Eval output
(kind: ok) <form><input type="text" name="signature"><button type="submit">Submit</button></form>

View File

@@ -0,0 +1,48 @@
// @enableInferEventHandlers
import {useRef} from 'react';
// Simulates react-hook-form's handleSubmit
function handleSubmit<T>(callback: (data: T) => void | Promise<void>) {
return (event: any) => {
event.preventDefault();
callback({} as T);
};
}
// Simulates an upload function
async function upload(file: any): Promise<{blob: {url: string}}> {
return {blob: {url: 'https://example.com/file.jpg'}};
}
interface SignatureRef {
toFile(): any;
}
function Component() {
const ref = useRef<SignatureRef>(null);
const onSubmit = async (value: any) => {
// This should be allowed: accessing ref.current in an async event handler
// that's wrapped and passed to onSubmit prop
let sigUrl: string;
if (value.hasSignature) {
const {blob} = await upload(ref.current?.toFile());
sigUrl = blob?.url || '';
} else {
sigUrl = value.signature;
}
console.log('Signature URL:', sigUrl);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input type="text" name="signature" />
<button type="submit">Submit</button>
</form>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};

View File

@@ -0,0 +1,100 @@
## Input
```javascript
// @enableInferEventHandlers
import {useRef} from 'react';
// Simulates react-hook-form's handleSubmit or similar event handler wrappers
function handleSubmit<T>(callback: (data: T) => void) {
return (event: any) => {
event.preventDefault();
callback({} as T);
};
}
function Component() {
const ref = useRef<HTMLInputElement>(null);
const onSubmit = (data: any) => {
// This should be allowed: accessing ref.current in an event handler
// that's wrapped by handleSubmit and passed to onSubmit prop
if (ref.current !== null) {
console.log(ref.current.value);
}
};
return (
<>
<input ref={ref} />
<form onSubmit={handleSubmit(onSubmit)}>
<button type="submit">Submit</button>
</form>
</>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableInferEventHandlers
import { useRef } from "react";
// Simulates react-hook-form's handleSubmit or similar event handler wrappers
function handleSubmit(callback) {
const $ = _c(2);
let t0;
if ($[0] !== callback) {
t0 = (event) => {
event.preventDefault();
callback({} as T);
};
$[0] = callback;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
function Component() {
const $ = _c(1);
const ref = useRef(null);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const onSubmit = (data) => {
if (ref.current !== null) {
console.log(ref.current.value);
}
};
t0 = (
<>
<input ref={ref} />
<form onSubmit={handleSubmit(onSubmit)}>
<button type="submit">Submit</button>
</form>
</>
);
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
### Eval output
(kind: ok) <input><form><button type="submit">Submit</button></form>

View File

@@ -0,0 +1,36 @@
// @enableInferEventHandlers
import {useRef} from 'react';
// Simulates react-hook-form's handleSubmit or similar event handler wrappers
function handleSubmit<T>(callback: (data: T) => void) {
return (event: any) => {
event.preventDefault();
callback({} as T);
};
}
function Component() {
const ref = useRef<HTMLInputElement>(null);
const onSubmit = (data: any) => {
// This should be allowed: accessing ref.current in an event handler
// that's wrapped by handleSubmit and passed to onSubmit prop
if (ref.current !== null) {
console.log(ref.current.value);
}
};
return (
<>
<input ref={ref} />
<form onSubmit={handleSubmit(onSubmit)}>
<button type="submit">Submit</button>
</form>
</>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};

View File

@@ -36,7 +36,6 @@ function useArrayOfRef() {
const callback = (value) => {
ref.current = value;
};
t0 = [callback];
$[0] = t0;
} else {

View File

@@ -35,7 +35,6 @@ function Component(props) {
const arr = [...bar(props)];
return arr.at(x);
};
t1 = fn();
$[2] = props;
$[3] = x;

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
@@ -36,7 +36,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
import { useMemo } from "react";
import { makeObject_Primitives, ValidateMemoization } from "shared-runtime";

View File

@@ -1,4 +1,4 @@
// @validatePreserveExistingMemoizationGuarantees
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';

View File

@@ -61,7 +61,6 @@ function useBar(t0) {
if ($[0] !== arg) {
const s = new Set([1, 5, 4]);
const mutableIterator = s.values();
t1 = [arg, ...mutableIterator];
$[0] = arg;
$[1] = t1;

View File

@@ -2,6 +2,7 @@
## Input
```javascript
// @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
function Component(props) {
@@ -30,7 +31,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies:false
import { useMemo } from "react";
function Component(props) {

View File

@@ -1,3 +1,4 @@
// @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
function Component(props) {

View File

@@ -28,7 +28,6 @@ function Component(props) {
const a = [];
const b = {};
foo(a, b);
foo(b);
t0 = <div a={a} b={b} />;
$[0] = t0;

View File

@@ -45,7 +45,6 @@ function useKeyCommand() {
const nextPosition = direction === "left" ? addOne(position) : position;
currentPosition.current = nextPosition;
};
const moveLeft = { handler: handleKey("left") };
const moveRight = { handler: handleKey("right") };
t0 = [moveLeft, moveRight];

View File

@@ -0,0 +1,74 @@
## Input
```javascript
import {useRef} from 'react';
import {addOne} from 'shared-runtime';
/**
* Allowed: we don't have sufficient type information to be sure that
* this accesses an impure value during render.
*/
function useKeyCommand() {
const currentPosition = useRef(0);
const handleKey = direction => () => {
const position = currentPosition.current;
const nextPosition = direction === 'left' ? addOne(position) : position;
currentPosition.current = nextPosition;
};
const moveLeft = {
handler: handleKey('left')(),
};
const moveRight = {
handler: handleKey('right')(),
};
return [moveLeft, moveRight];
}
export const FIXTURE_ENTRYPOINT = {
fn: useKeyCommand,
params: [],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useRef } from "react";
import { addOne } from "shared-runtime";
/**
* Allowed: we don't have sufficient type information to be sure that
* this accesses an impure value during render.
*/
function useKeyCommand() {
const $ = _c(1);
const currentPosition = useRef(0);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const handleKey = (direction) => () => {
const position = currentPosition.current;
const nextPosition = direction === "left" ? addOne(position) : position;
currentPosition.current = nextPosition;
};
const moveLeft = { handler: handleKey("left")() };
const moveRight = { handler: handleKey("right")() };
t0 = [moveLeft, moveRight];
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: useKeyCommand,
params: [],
};
```
### Eval output
(kind: ok) [{},{}]

View File

@@ -1,6 +1,10 @@
import {useRef} from 'react';
import {addOne} from 'shared-runtime';
/**
* Allowed: we don't have sufficient type information to be sure that
* this accesses an impure value during render.
*/
function useKeyCommand() {
const currentPosition = useRef(0);
const handleKey = direction => () => {

View File

@@ -45,7 +45,6 @@ function Component(t0) {
z.a = 2;
mutate(y.b);
};
x();
t1 = [y, z];
$[0] = a;

View File

@@ -5,6 +5,10 @@
import {useRef} from 'react';
import {Stringify} from 'shared-runtime';
/**
* Allowed: we don't have sufficient type information to be sure that
* this accesses an impure value during render.
*/
function Component(props) {
const ref = useRef(props.value);
const object = {};
@@ -26,6 +30,10 @@ import { c as _c } from "react/compiler-runtime";
import { useRef } from "react";
import { Stringify } from "shared-runtime";
/**
* Allowed: we don't have sufficient type information to be sure that
* this accesses an impure value during render.
*/
function Component(props) {
const $ = _c(1);
const ref = useRef(props.value);

View File

@@ -1,6 +1,10 @@
import {useRef} from 'react';
import {Stringify} from 'shared-runtime';
/**
* Allowed: we don't have sufficient type information to be sure that
* this accesses an impure value during render.
*/
function Component(props) {
const ref = useRef(props.value);
const object = {};

View File

@@ -29,7 +29,6 @@ function MyComponentName(props) {
const x = {};
foo(x, props.a);
foo(x, props.b);
y = [];
y.push(x);
$[0] = props.a;

View File

@@ -34,7 +34,6 @@ function useTest() {
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
let w = {};
const t1 = (w = 42);
const t2 = w;

View File

@@ -34,7 +34,6 @@ function useTest() {
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const w = {};
const t1 = (w.x = 42);
const t2 = w.x;

View File

@@ -44,11 +44,9 @@ function ComponentA(props) {
if (b) {
a.push(props.p0);
}
if (props.p1) {
b.push(props.p2);
}
t0 = <Foo a={a} b={b} />;
$[0] = props.p0;
$[1] = props.p1;
@@ -69,11 +67,9 @@ function ComponentB(props) {
if (mayMutate(b)) {
a.push(props.p0);
}
if (props.p1) {
b.push(props.p2);
}
t0 = <Foo a={a} b={b} />;
$[0] = props.p0;
$[1] = props.p1;

View File

@@ -28,7 +28,6 @@ function Component(props) {
const a = [];
const b = {};
new Foo(a, b);
new Foo(b);
t0 = <div a={a} b={b} />;
$[0] = t0;

View File

@@ -11,7 +11,7 @@ function Component(props) {
Component = useMemo(() => {
return Component;
});
}, [Component]);
return <Component {...props} />;
}
@@ -36,6 +36,7 @@ function Component(props) {
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
Component = Stringify;
Component;
Component = Component;
$[0] = Component;
} else {

View File

@@ -7,7 +7,7 @@ function Component(props) {
Component = useMemo(() => {
return Component;
});
}, [Component]);
return <Component {...props} />;
}

View File

@@ -34,7 +34,6 @@ function Component(props) {
const callback = () => {
console.log(x);
};
x = {};
t0 = <Stringify callback={callback} shouldInvokeFns={true} />;
$[0] = t0;

View File

@@ -75,7 +75,6 @@ function Component(props) {
let t0;
if ($[0] !== post) {
const allUrls = [];
const { media: t1, comments: t2, urls: t3 } = post;
const media = t1 === undefined ? null : t1;
let t4;
@@ -102,7 +101,6 @@ function Component(props) {
if (!comments.length) {
return;
}
console.log(comments.length);
};
$[6] = comments.length;
@@ -111,7 +109,6 @@ function Component(props) {
t6 = $[7];
}
const onClick = t6;
allUrls.push(...urls);
t0 = <Stringify media={media} allUrls={allUrls} onClick={onClick} />;
$[0] = post;

View File

@@ -53,7 +53,6 @@ function Component(props) {
let t0;
if ($[0] !== post) {
const allUrls = [];
const { media, comments, urls } = post;
let t1;
if ($[2] !== comments.length) {
@@ -61,7 +60,6 @@ function Component(props) {
if (!comments.length) {
return;
}
console.log(comments.length);
};
$[2] = comments.length;
@@ -70,7 +68,6 @@ function Component(props) {
t1 = $[3];
}
const onClick = t1;
allUrls.push(...urls);
t0 = <Media media={media} onClick={onClick} />;
$[0] = post;

View File

@@ -57,7 +57,6 @@ function Component(t0) {
let y;
if ($[0] !== a || $[1] !== b || $[2] !== c) {
x = [];
if (a) {
let t1;
if ($[5] !== b) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
import {useEffect, useState} from 'react';
function Component({value, enabled}) {
@@ -29,43 +29,21 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
import { useEffect, useState } from "react";
function Component(t0) {
const $ = _c(6);
const { value, enabled } = t0;
function Component({ value, enabled }) {
const [localValue, setLocalValue] = useState("");
let t1;
let t2;
if ($[0] !== enabled || $[1] !== value) {
t1 = () => {
if (enabled) {
setLocalValue(value);
} else {
setLocalValue("disabled");
}
};
t2 = [value, enabled];
$[0] = enabled;
$[1] = value;
$[2] = t1;
$[3] = t2;
} else {
t1 = $[2];
t2 = $[3];
}
useEffect(t1, t2);
let t3;
if ($[4] !== localValue) {
t3 = <div>{localValue}</div>;
$[4] = localValue;
$[5] = t3;
} else {
t3 = $[5];
}
return t3;
useEffect(() => {
if (enabled) {
setLocalValue(value);
} else {
setLocalValue("disabled");
}
}, [value, enabled]);
return <div>{localValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
@@ -78,8 +56,8 @@ export const FIXTURE_ENTRYPOINT = {
## Logs
```
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [value]\n\nData Flow Tree:\n└── value (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":9,"column":6,"index":244},"end":{"line":9,"column":19,"index":257},"filename":"derived-state-conditionally-in-effect.ts","identifierName":"setLocalValue"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":16,"column":1,"index":378},"filename":"derived-state-conditionally-in-effect.ts"},"fnName":"Component","memoSlots":6,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [value]\n\nData Flow Tree:\n└── value (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":9,"column":6,"index":263},"end":{"line":9,"column":19,"index":276},"filename":"derived-state-conditionally-in-effect.ts","identifierName":"setLocalValue"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":126},"end":{"line":16,"column":1,"index":397},"filename":"derived-state-conditionally-in-effect.ts"},"fnName":"Component","memoSlots":6,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
import {useEffect, useState} from 'react';
function Component({value, enabled}) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
import {useEffect, useState} from 'react';
export default function Component({input = 'empty'}) {
@@ -26,38 +26,18 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
import { useEffect, useState } from "react";
export default function Component(t0) {
const $ = _c(5);
const { input: t1 } = t0;
const input = t1 === undefined ? "empty" : t1;
export default function Component({ input = "empty" }) {
const [currInput, setCurrInput] = useState(input);
let t2;
let t3;
if ($[0] !== input) {
t2 = () => {
setCurrInput(input + "local const");
};
t3 = [input, "local const"];
$[0] = input;
$[1] = t2;
$[2] = t3;
} else {
t2 = $[1];
t3 = $[2];
}
useEffect(t2, t3);
let t4;
if ($[3] !== currInput) {
t4 = <div>{currInput}</div>;
$[3] = currInput;
$[4] = t4;
} else {
t4 = $[4];
}
return t4;
const localConst = "local const";
useEffect(() => {
setCurrInput(input + localConst);
}, [input, localConst]);
return <div>{currInput}</div>;
}
export const FIXTURE_ENTRYPOINT = {
@@ -70,8 +50,8 @@ export const FIXTURE_ENTRYPOINT = {
## Logs
```
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [input]\n\nData Flow Tree:\n└── input (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":9,"column":4,"index":276},"end":{"line":9,"column":16,"index":288},"filename":"derived-state-from-default-props.ts","identifierName":"setCurrInput"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":15,"index":122},"end":{"line":13,"column":1,"index":372},"filename":"derived-state-from-default-props.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [input]\n\nData Flow Tree:\n└── input (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":9,"column":4,"index":295},"end":{"line":9,"column":16,"index":307},"filename":"derived-state-from-default-props.ts","identifierName":"setCurrInput"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":15,"index":141},"end":{"line":13,"column":1,"index":391},"filename":"derived-state-from-default-props.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
import {useEffect, useState} from 'react';
export default function Component({input = 'empty'}) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
import {useEffect, useState} from 'react';
@@ -23,45 +23,20 @@ function Component({shouldChange}) {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
import { useEffect, useState } from "react";
function Component(t0) {
const $ = _c(7);
const { shouldChange } = t0;
function Component({ shouldChange }) {
const [count, setCount] = useState(0);
let t1;
if ($[0] !== count || $[1] !== shouldChange) {
t1 = () => {
if (shouldChange) {
setCount(count + 1);
}
};
$[0] = count;
$[1] = shouldChange;
$[2] = t1;
} else {
t1 = $[2];
}
let t2;
if ($[3] !== count) {
t2 = [count];
$[3] = count;
$[4] = t2;
} else {
t2 = $[4];
}
useEffect(t1, t2);
let t3;
if ($[5] !== count) {
t3 = <div>{count}</div>;
$[5] = count;
$[6] = t3;
} else {
t3 = $[6];
}
return t3;
useEffect(() => {
if (shouldChange) {
setCount(count + 1);
}
}, [count]);
return <div>{count}</div>;
}
```
@@ -69,8 +44,8 @@ function Component(t0) {
## Logs
```
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nState: [count]\n\nData Flow Tree:\n└── count (State)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":10,"column":6,"index":237},"end":{"line":10,"column":14,"index":245},"filename":"derived-state-from-local-state-in-effect.ts","identifierName":"setCount"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":108},"end":{"line":15,"column":1,"index":310},"filename":"derived-state-from-local-state-in-effect.ts"},"fnName":"Component","memoSlots":7,"memoBlocks":3,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nState: [count]\n\nData Flow Tree:\n└── count (State)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":10,"column":6,"index":256},"end":{"line":10,"column":14,"index":264},"filename":"derived-state-from-local-state-in-effect.ts","identifierName":"setCount"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":127},"end":{"line":15,"column":1,"index":329},"filename":"derived-state-from-local-state-in-effect.ts"},"fnName":"Component","memoSlots":7,"memoBlocks":3,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
import {useEffect, useState} from 'react';

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
import {useEffect, useState} from 'react';
function Component({firstName}) {
@@ -33,68 +33,25 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
import { useEffect, useState } from "react";
function Component(t0) {
const $ = _c(12);
const { firstName } = t0;
function Component({ firstName }) {
const [lastName, setLastName] = useState("Doe");
const [fullName, setFullName] = useState("John");
let t1;
let t2;
if ($[0] !== firstName || $[1] !== lastName) {
t1 = () => {
setFullName(firstName + " " + "D." + " " + lastName);
};
t2 = [firstName, "D.", lastName];
$[0] = firstName;
$[1] = lastName;
$[2] = t1;
$[3] = t2;
} else {
t1 = $[2];
t2 = $[3];
}
useEffect(t1, t2);
let t3;
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
t3 = (e) => setLastName(e.target.value);
$[4] = t3;
} else {
t3 = $[4];
}
let t4;
if ($[5] !== lastName) {
t4 = <input value={lastName} onChange={t3} />;
$[5] = lastName;
$[6] = t4;
} else {
t4 = $[6];
}
let t5;
if ($[7] !== fullName) {
t5 = <div>{fullName}</div>;
$[7] = fullName;
$[8] = t5;
} else {
t5 = $[8];
}
let t6;
if ($[9] !== t4 || $[10] !== t5) {
t6 = (
<div>
{t4}
{t5}
</div>
);
$[9] = t4;
$[10] = t5;
$[11] = t6;
} else {
t6 = $[11];
}
return t6;
const middleName = "D.";
useEffect(() => {
setFullName(firstName + " " + middleName + " " + lastName);
}, [firstName, middleName, lastName]);
return (
<div>
<input value={lastName} onChange={(e) => setLastName(e.target.value)} />
<div>{fullName}</div>
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
@@ -107,8 +64,8 @@ export const FIXTURE_ENTRYPOINT = {
## Logs
```
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [firstName]\nState: [lastName]\n\nData Flow Tree:\n├── firstName (Prop)\n└── lastName (State)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":11,"column":4,"index":297},"end":{"line":11,"column":15,"index":308},"filename":"derived-state-from-prop-local-state-and-component-scope.ts","identifierName":"setFullName"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":20,"column":1,"index":542},"filename":"derived-state-from-prop-local-state-and-component-scope.ts"},"fnName":"Component","memoSlots":12,"memoBlocks":5,"memoValues":6,"prunedMemoBlocks":0,"prunedMemoValues":0}
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [firstName]\nState: [lastName]\n\nData Flow Tree:\n├── firstName (Prop)\n└── lastName (State)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":11,"column":4,"index":316},"end":{"line":11,"column":15,"index":327},"filename":"derived-state-from-prop-local-state-and-component-scope.ts","identifierName":"setFullName"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":126},"end":{"line":20,"column":1,"index":561},"filename":"derived-state-from-prop-local-state-and-component-scope.ts"},"fnName":"Component","memoSlots":12,"memoBlocks":5,"memoValues":6,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
import {useEffect, useState} from 'react';
function Component({firstName}) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
import {useEffect, useState} from 'react';
function Component({initialName}) {
@@ -29,48 +29,21 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
import { useEffect, useState } from "react";
function Component(t0) {
const $ = _c(6);
const { initialName } = t0;
function Component({ initialName }) {
const [name, setName] = useState("");
let t1;
let t2;
if ($[0] !== initialName) {
t1 = () => {
setName(initialName);
};
t2 = [initialName];
$[0] = initialName;
$[1] = t1;
$[2] = t2;
} else {
t1 = $[1];
t2 = $[2];
}
useEffect(t1, t2);
let t3;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t3 = (e) => setName(e.target.value);
$[3] = t3;
} else {
t3 = $[3];
}
let t4;
if ($[4] !== name) {
t4 = (
<div>
<input value={name} onChange={t3} />
</div>
);
$[4] = name;
$[5] = t4;
} else {
t4 = $[5];
}
return t4;
useEffect(() => {
setName(initialName);
}, [initialName]);
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
@@ -83,7 +56,7 @@ export const FIXTURE_ENTRYPOINT = {
## Logs
```
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":16,"column":1,"index":359},"filename":"derived-state-from-prop-setter-call-outside-effect-no-error.ts"},"fnName":"Component","memoSlots":6,"memoBlocks":3,"memoValues":4,"prunedMemoBlocks":0,"prunedMemoValues":0}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":126},"end":{"line":16,"column":1,"index":378},"filename":"derived-state-from-prop-setter-call-outside-effect-no-error.ts"},"fnName":"Component","memoSlots":6,"memoBlocks":3,"memoValues":4,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output

Some files were not shown because too many files have changed in this diff Show More