Compare commits

..

51 Commits

Author SHA1 Message Date
Joe Savona
e1f907d68d [compiler] Allow assigning ref-accessing functions to objects if not mutated
Allows assigning a ref-accessing function to an object so long as that object is not subsequently transitively mutated. We should likely rewrite the ref validation to use the new mutation/aliasing effects, which would provide a more consistent behavior across instruction types and require fewer special cases like this.
2025-07-29 10:57:08 -07:00
Joseph Savona
c2326b1336 [compiler] disallow ref access in state initializer, reducer/initializer (#34025)
Per title, disallow ref access in `useState()` initializer function,
`useReducer()` reducer, and `useReducer()` init function.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34025).
* #34027
* #34026
* __->__ #34025
2025-07-29 10:56:04 -07:00
Joseph Savona
4395689980 [compiler] ref guards apply up to fallthrough of the test (#34024)
Fixes #30782

When developers do an `if (ref.current == null)` guard for lazy ref
initialization, the "safe" blocks should extend up to the if's
fallthrough. Previously we only allowed writing to the ref in the if
consequent, but this meant that you couldn't use a ternary, logical, etc
in the if body.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34024).
* #34027
* #34026
* #34025
* __->__ #34024
2025-07-29 10:53:13 -07:00
Joseph Savona
6891dcb87d [compiler] treat ref-like identifiers as refs by default (#34005)
`@enableTreatRefLikeIdentifiersAsRefs` is now on by default. I made one
small fix to the render helper logic as part of this, uncovered by
including more tests.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34005).
* #34027
* #34026
* #34025
* #34024
* __->__ #34005
2025-07-29 10:51:10 -07:00
Joseph Savona
3f40eb73a8 [compiler] Allow passing refs to render helpers (#34006)
We infer render helpers as functions whose result is immediately
interpolated into jsx. This is a very conservative approximation, to
help with common cases like `<Foo>{props.renderItem(ref)}</Foo>`. The
idea is similar to hooks that it's ultimately on the developer to catch
ref-in-render validations (and the runtime detects them too), so we can
be a bit more relaxed since there are valid reasons to use this pattern.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34006).
* #34027
* #34026
* #34025
* #34024
* #34005
* __->__ #34006
* #34004
2025-07-29 10:06:23 -07:00
Joseph Savona
1d7e942da7 [compiler] Allow mergeRefs pattern (and detect refs passed as ref prop) (#34004)
Two related changes:
* ValidateNoRefAccessInRender now allows the mergeRefs pattern, ie a
function that aggregates multiple refs into a new ref. This is the main
case where we have seen false positive no-ref-in-render errors.
* Behind `@enableTreatRefLikeIdentifiersAsRefs`, we infer values passed
as the `ref` prop to some JSX as refs.

The second change is potentially helpful for situations such as

```js
function Component({ref: parentRef}) {
  const childRef = useRef(null);
  const mergedRef = mergeRefs(parentRef, childRef);
  useEffect(() => {
    // generally accesses childRef, not mergedRef
  }, []);
  return <Foo ref={mergedRef} />;
}
```

Ie where you create a merged ref but don't access its `.current`
property. Without inferring `ref` props as refs, we'd fail to allow this
merge refs case.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34004).
* #34027
* #34026
* #34025
* #34024
* #34005
* #34006
* __->__ #34004
2025-07-29 10:06:11 -07:00
Joseph Savona
79dc706498 [compiler] Improve ref validation error message (#34003)
Improves the error message for ValidateNoRefAccessInRender, using the
new diagnostic type as well as providing a longer but succinct summary
of what refs are for and why they're unsafe to access in render.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34003).
* #34027
* #34026
* #34025
* #34024
* #34005
* #34006
* #34004
* __->__ #34003
2025-07-29 10:03:28 -07:00
Joseph Savona
85bbe39ef8 [compiler] Fixes to enableTreatRefLikeIdentifiersAsRefs (#34000)
We added the `@enableTreatRefLikeIdentifiersAsRefs` feature a while back
but never enabled it. Since then we've continued to see examples that
motivate this mode, so here we're fixing it up to prepare to enable by
default. It now works as follows:

* If we find a property load or property store where both a) the
object's name is ref-like (`ref` or `-Ref`) and b) the property is
`current`, we infer the object itself as a ref and the value of the
property as a ref value. Originally the feature only detected property
loads, not stores.
* Inferred refs are not considered stable (this is a change from the
original implementation). The only way to get a stable ref is by calling
`useRef()`. We've seen issues with assuming refs are stable.

With this change, cases like the following now correctly error:

```js
function Foo(props) {
  const fooRef = props.fooRef;
  fooRef.current = true;
  ^^^^^^^^^^^^^^ cannot modify ref in render
}
```

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34000).
* #34027
* #34026
* #34025
* #34024
* #34005
* #34006
* #34004
* #34003
* __->__ #34000
2025-07-29 09:57:48 -07:00
lauren
820af20971 [eslint] Disallow use within try/catch blocks (#34040)
Follow up to #34032. The linter now ensures that `use` cannot be used
within try/catch.
2025-07-29 12:33:42 -04:00
Sebastian Markbåge
9be531cd37 [Fiber] Treat unwrapping React.lazy more like a use() (#34031)
While we want to get rid of React.lazy's special wrapper type and just
use a Promise for the type, we still have the wrapper.

However, this is still conceptually the same as a Usable in that it
should be have the same if you `use(promise)` or render a Promise as a
child or type position.

This PR makes it behave like a `use()` when we unwrap them. We could
move to a model where it actually reaches the internal of the Lazy's
Promise when it unwraps but for now I leave the lazy API signature
intact by just catching the Promise and then "use()" that.

This lets us align on the semantics with `use()` such as the suspense
yield optimization. It also lets us warn or fork based on legacy
throw-a-Promise behavior where as `React.lazy` is not deprecated.
2025-07-29 11:50:12 -04:00
Sebastian "Sebbie" Silbermann
b1cbb482d5 [DevTools] More robust resize handling (#34036) 2025-07-29 17:45:00 +02:00
Sebastian "Sebbie" Silbermann
9c9136b441 [DevTools] Swap Components tab layout based on container size (#34035) 2025-07-29 17:23:35 +02:00
Sebastian "Sebbie" Silbermann
33a2bf78c4 [DevTools] Silence unactionable bundle warnings in shell (#34034) 2025-07-29 11:18:47 +02:00
Sebastian Markbåge
5d7e8b90e2 [DevTools] Use use() instead of throwing a Promise in Caches (#34033) 2025-07-29 03:45:56 -04:00
Sebastian Markbåge
71236c9409 [DevTools] Include the description derived from the promise (#34017)
Stacked on #34016.

This is using the same thing we already do for the performance track to
provide a description of the I/O based on the content of the resolved
Promise. E.g. a Response's URL.

<img width="375" height="388" alt="Screenshot 2025-07-28 at 1 09 49 AM"
src="https://github.com/user-attachments/assets/f3fdc40f-4e21-4e83-b49e-21c7ec975137"
/>
2025-07-28 15:11:04 -04:00
lauren
7ee7571212 [compiler] Enable validateNoVoidUseMemo in eslint & playground (#34022)
Enables `validateNoVoidUseMemo` by default only in eslint (it defaults
to false otherwise) as well as the playground.
2025-07-28 13:42:14 -04:00
lauren
6b22f31f1a [compiler] Aggregate all errors reported from DropManualMemoization (#34002)
Noticed this from my previous PR that this pass was throwing on the
first error. This PR is a small refactor to aggregate every violation
and report them all at once.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34002).
* #34022
* __->__ #34002
2025-07-28 13:25:25 -04:00
lauren
b5c1637109 [compiler] Reuse DropManualMemoization for ValidateNoVoidUseMemo (#34001)
Much of the logic in the new validation pass is already implemented in
DropManualMemoization, so let's combine them. I opted to keep the
environment flag so we can more precisely control the rollout.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34001).
* #34022
* #34002
* __->__ #34001
2025-07-28 12:54:43 -04:00
lauren
c60eebffea [compiler] Add new ValidateNoVoidUseMemo pass (#33990)
Adds a new validation pass to validate against `useMemo`s that don't
return anything. This usually indicates some kind of "useEffect"-like
code that has side effects that need to be memoized to prevent
overfiring, and is an anti-pattern.

A follow up validation could also look at the return value of `useMemo`s
to see if they are being used.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33990).
* #34022
* #34002
* #34001
* __->__ #33990
* #33989
2025-07-28 12:46:42 -04:00
lauren
5dd622eabe [compiler] Disambiguate between void, implicit, and explicit returns (#33989)
Adds a new property to ReturnTerminals to disambiguate whether it was
explicit, implicit (arrow function expressions), or void (where it was
omitted). I will use this property in the next PR adding a new
validation pass.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33989).
* #34022
* #34002
* #34001
* #33990
* __->__ #33989
2025-07-28 12:46:30 -04:00
dan
904989f044 Clean up 19.1.1 changelog (#34023)
See
https://github.com/facebook/react/pull/34021#issuecomment-3128006800.

The purpose of the changelog is to communicate to React users what
changed in the release.

Therefore, it is important that the changelog is written oriented
towards React end users. Historically this means that we omit
internal-only changes, i.e. changes that have no effect on the end user
behavior. If internal changes are mentioned in the changelog (e.g. if
they affect end user behavior), they should be phrased in a way that is
understandable to the end user — in particular, they should not refer to
internal API names or concepts.

We also try to group changes according to the publicly known packages.

In this PR:

- Make #33680 an actual link (otherwise it isn't linkified in
CHANGELOG.md on GitHub).
- Remove two changelog entries listed under "React" that don't affect
anyone who upgrades the "React" package, that are phrased using
terminology and internal function names unfamiliar to React users, and
that seem to be RN-specific changes (so should probably go into the RN
changelog that goes out with the next renderer sync that includes these
changes).
2025-07-28 17:32:23 +01:00
Sebastian "Sebbie" Silbermann
ab2681af03 [DevTools] Skeleton for Suspense tab (#34020) 2025-07-28 18:26:55 +02:00
Sebastian Markbåge
101b20b663 [DevTools] Add a little bar indicating time span of an async entry relative to others (#34016)
Stacked on #34012.

This shows a time track for when some I/O started and when it finished
relative to other I/O in the same component (or later in the same
suspense boundary).

This is not meant to be a precise visualization since the data might be
misleading if you're running this in dev which has other perf
characteristics anyway. It's just meant to be a general way to orient
yourself in the data.

We can also highlight rejected promises here.

The color scheme is the same as Chrome's current Performance Track
colors to add continuity but those could change.

<img width="478" height="480" alt="Screenshot 2025-07-27 at 11 48 03 PM"
src="https://github.com/user-attachments/assets/545dd591-a91f-4c47-be96-41d80f09a94a"
/>
2025-07-28 12:22:33 -04:00
Jack Pope
eaee5308cc Add changelog entry for 19.1.1 (#34021)
Add changelog details matching release notes:
https://github.com/facebook/react/releases/tag/v19.1.1
2025-07-28 12:09:56 -04:00
Sebastian Markbåge
4a58b63865 [DevTools] Add "suspended by" Section to Component Inspector Sidebar (#34012)
This collects the ReactAsyncInfo between instances. It associates it
with the parent. Typically this would be a Server Component's Promise
return value but it can also be Promises in a fragment. It can also be
associated with a client component when you pass a Promise into the
child position e.g. `<div>{promise}</div>` then it's associated with the
div. If an instance is filtered, then it gets associated with the parent
of that's unfiltered.

The stack trace currently isn't source mapped. I'll do that in a follow
up.

We also need to add a "short name" from the Promise for the description
(e.g. url). I'll also add a little marker showing the relative time span
of each entry.

<img width="447" height="591" alt="Screenshot 2025-07-26 at 7 56 00 PM"
src="https://github.com/user-attachments/assets/7c966540-7b1b-4568-8cb9-f25cefd5a918"
/>
<img width="446" height="570" alt="Screenshot 2025-07-26 at 7 55 23 PM"
src="https://github.com/user-attachments/assets/4eac235b-e735-41e8-9c6e-a7633af64e4b"
/>
2025-07-28 12:05:56 -04:00
Hiroshi Ogawa
cc015840ef fix: React.use inside React.lazy-ed component on SSR (#33941) 2025-07-28 10:36:08 +02:00
Sebastian "Sebbie" Silbermann
19baee813c [Runtime] Fix CI (#33999) 2025-07-25 21:04:35 +02:00
Joseph Savona
2aa5f9d4e3 [compiler] fix false positive "mutate frozen" validation with refs (#33993)
The test case here previously reported a "Cannot modify local variables
after render completes" error (from
ValidateNoFreezingKnownMutableFunctions). This happens because one of
the functions passed to a hook clearly mutates a ref — except that we
try to ignore mutations of refs! The problem in this case is that the
`const ref = ...` was getting converted to a context variable since the
ref is accessed in a function before its declaration. We don't infer
types for context variables at all, and our ref handling is based on
types, so we failed to ignore this ref mutation.

The fix is to recognize that `StoreLocal const ...` is a special case:
the variable may be referenced in code before the declaration, but at
runtime it's either a TDZ error or the variable will have the type from
the declaration. So we can safely infer a type.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33993).
* __->__ #33993
* #33991
* #33984
2025-07-25 10:08:09 -07:00
Joseph Savona
8c587a2a41 [compiler] clarify text for setState-in-effect error (#33991)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33991).
* #33993
* __->__ #33991
* #33984
2025-07-25 10:07:55 -07:00
Joseph Savona
12483a119b [compiler] Fix for edge cases of mutation of potentially frozen values (#33984)
Fixes two related cases of mutation of potentially frozen values.

The first is method calls on frozen values. Previously, we modeled
unknown function calls as potentially aliasing their receiver+args into
the return value. If the receiver or argument were known to be frozen,
then we would downgrade the `Alias` effect into an `ImmutableCapture`.
However, within a function expression it's possible to call a function
using a frozen value as an argument (that gets `Alias`-ed into the
return) but where we don't have the context locally to know that the
value is frozen.

This results in cases like this:

```js
const frozen = useContext(...);
useEffect(() => {
  frozen.method().property = true;
  ^^^^^^^^^^^^^^^^^^^^^^^^ cannot mutate frozen value
}, [...]);
```

Within the function we would infer:

```
t0 = MethodCall ...
  Create t0 = mutable
  Alias t0 <- frozen
t1 = PropertyStore ...
  Mutate t0
```

And then transitively infer the function expression as having a `Mutate
'frozen'` effect, which when evaluated against the outer context
(`frozen` is frozen) is an error.

The fix is to model unknown function calls as _maybe_ aliasing their
receiver/args in the return, and then considering mutations of a
maybe-aliased value to only be a conditional mutation of the source:


```
t0 = MethodCall ...
  Create t0 = mutable
  MaybeAlias t0 <- frozen // maybe alias now
t1 = PropertyStore ...
  Mutate t0
```

Then, the `Mutate t0` turns into a `MutateConditional 'frozen'`, which
just gets ignored when we process the outer context.

The second, related fix is for known mutation of phis that may be a
frozen value. The previous inference model correctly recorded these as
errors, the new model does not. We now correctly report a validation
error for this case in the new model.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33984).
* #33993
* #33991
* __->__ #33984
2025-07-25 10:07:24 -07:00
Sebastian Markbåge
b2c30493ce [DevTools] Use the hard coded url instead of the local storage url for presets (and make VSCode default) (#33995)
Stacked on #33983.

Previously, the source of truth is the url stored in local storage but
that means if we change the presets then they don't take effect (e.g.
#33994). This PR uses the hardcoded value instead when a preset is
selected.

This also has the benefit that if you switch between custom and vs code
in the selector, then the custom url is preserved instead of getting
reset when you checkout other options.

Currently the default is custom with empty string, which means that
there's no code editor configured at all by default. It doesn't make a
lot of sense that we have it not working by default when so many people
use VS Code. So this also makes VS Code the default if there's no
EDITOR_URL env specified.
2025-07-25 10:27:27 -04:00
Sebastian Markbåge
36c2bf5c3e [DevTools] Allow all file links in Chrome DevTools to open in external editor (#33985)
Stacked on #33983.

Allow React to be configured as the default handler of all links in
Chrome DevTools. To do this you need to configure the Chrome DevTools
setting for "Link Handling:" to be set to "React Developer Tools". By
default this doesn't do anything but if you then check the box added in
#33983 it starts open local files directly in the external editor.

This needs docs to show how to enable that option.

(As far as I can tell this broke in Chrome Canary 🙄 but hopefully fixed
before stable.)
2025-07-25 10:27:09 -04:00
Sebastian Markbåge
190758e623 [DevTools] Add column to vscode editor preset (#33994)
We should jump to the right column.

Unfortunately, the way presets are set up now you have to switch off and
switch to the preset for this to take effect.
2025-07-25 10:21:00 -04:00
Sebastian Markbåge
b1a6f03f8a [DevTools] Rerender when the browser theme changes (#33992)
When the browser theme changes, we don't immediately rerender the UI so
we don't pick up the new theme if the React devtools are set to auto.

This picks up the change immediately.
2025-07-25 10:19:09 -04:00
Sebastian Markbåge
142fd27bf6 [DevTools] Add Option to Open Local Files directly in External Editor (#33983)
The `useOpenResource` hook is now used to open links. Currently, the
`<>` icon for the component stacks and the link in the bottom of the
components stack. But it'll also be used for many new links like stacks.
If this new option is configured, and this is a local file then this is
opened directly in the external editor. Otherwise it fallbacks to open
in the Sources tab or whatever the standalone or inline is configured to
use.

<img width="453" height="252" alt="Screenshot 2025-07-24 at 4 09 09 PM"
src="https://github.com/user-attachments/assets/04cae170-dd30-4485-a9ee-e8fe1612978e"
/>

I prominently surface this option in the Source pane to make it
discoverable.

<img width="588" height="144" alt="Screenshot 2025-07-24 at 4 03 48 PM"
src="https://github.com/user-attachments/assets/0f3a7da9-2fae-4b5b-90ec-769c5a9c5361"
/>

When this is configured, the "Open in Editor" is hidden since that's
just the default. I plan on deprecating this button to avoid having the
two buttons going forward.

Notably there's one exception where this doesn't work. When you click an
Action or Event listener it takes you to the Sources tab and you have to
open in editor from there. That's because we use the `inspect()`
mechanism instead of extracting the source location. That's because we
can't do the "throw trick" since these can have side-effects. The Chrome
debugger protocol would solve this but it pops up an annoying dialog. We
could maybe only attach the debugger only for that case. Especially if
the dialog disappears before you focus on the browser again.
2025-07-25 10:16:43 -04:00
Sebastian "Sebbie" Silbermann
7ca2d4cd2e Work around Chrome DevTools crash on performance.measure (#33997) 2025-07-25 12:32:30 +02:00
Sebastian Markbåge
99be14c883 [Flight] Promote enableAsyncDebugInfo to stable without enableComponentPerformanceTrack (#33996)
There's a lot of overlap between `enableComponentPerformanceTrack` and
`enableAsyncDebugInfo` because they both rely on timing information. The
former is mainly emit timestamps for how long server components and
awaits took. The latter how long I/O took.

`enableAsyncDebugInfo` is currently primarily for the component
performance track but its meta data is useful for other debug tools too.
This promotes that flag to stable.

However, `enableComponentPerformanceTrack` needs more work due to
performance concerns with Chrome DevTools so I need to separate them.
This keeps doing most of the timing tracking on the server but doesn't
emit the per-server component time stamps when
`enableComponentPerformanceTrack` is false.
2025-07-25 04:59:46 -04:00
Josh Story
5a04619f60 [Flight] Properly close stream when no chunks need to be written after prerender (#33982)
There is an edge case when prerendering where if you have nothing to
write you can end up in a state where the prerender is in status closed
before you can provide a destination. In this case the destination is
never closed becuase it assumes it already would have been.

This condition can happen now because of the introduction of the deubg
stream. Before this a request would never entere closed status if there
was no active destination. When a destination was added it would perform
a flush and possibly close the stream. Now, it is possible to flush
without a destination because you might have debug chunks to stream and
you can end up closing the stream independent of an active destination.

There are a number of ways we can solve this but the one that seems to
adhere best to the original design is to only set the status to CLOSED
when a destination is active. This means that if you don't have an
active destination when the pendingChunks count hits zero it will not
enter CLOSED status until you startFlowing.
2025-07-24 19:38:31 -07:00
Joseph Savona
129aa85e16 [compiler] Use diagnostic for "found suppression" error (#33981) 2025-07-24 15:54:24 -07:00
Joseph Savona
bcea86945c [compiler][rfc] Enable more validations in playground. (#33777)
This is mostly to kick off conversation, i think we should go with a
modified version of the implemented approach that i'll describe here.

The playground currently serves two roles. The primary one we think
about is for verifying compiler output. We use it for this sometimes,
and developers frequently use it for this, including to send us repros
if they have a potential bug. The second mode is to help developers
learn about React. Part of that includes learning how to use React
correctly — where it's helpful to see feedback about problematic code —
and also to understand what kind of tools we provide compared to other
frameworks, to make an informed choice about what tools they want to
use.

Currently we primarily think about the first role, but I think we should
emphasize the second more. In this PR i'm doing the worst of both:
enabling all the validations used by both the compiler and the linter by
default. This means that code that would actually compile can fail with
validations, which isn't great.

What I think we should actually do is compile twice, one in
"compilation" mode and once in "linter" mode, and combine the results as
follows:
* If "compilation" mode succeeds, show the compiled output _and_ any
linter errors.
* If "compilation" mode fails, show only the compilation mode failures.

We should also distinguish which case it is when we show errors:
"Compilation succeeded", "Compilation succeeded with linter errors",
"Compilation failed".

This lets developers continue to verify compiler output, while also
turning the playground into a much more useful tool for learning React.
Thoughts?

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33777).
* #33981
* __->__ #33777
2025-07-24 15:52:45 -07:00
Joseph Savona
2ae8b3dacf [compiler] Use new diagnostic printing in playground (#33767)
Per title

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33767).
* #33981
* #33777
* __->__ #33767
2025-07-24 15:47:56 -07:00
Joseph Savona
7f510554ad [compiler] Cleanup diagnostic messages (#33765)
Minor sytlistic cleanup

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33765).
* #33981
* #33777
* #33767
* __->__ #33765
2025-07-24 15:45:17 -07:00
Joseph Savona
a39da6c61f [compiler] Use new diagnostics for core inference errors (#33760)
Uses the new diagnostic type for errors created during mutation/aliasing
inference, such as errors for mutating immutable values like props or
state, reassigning globals, etc.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33760).
* #33981
* #33777
* #33767
* #33765
* __->__ #33760
2025-07-24 15:43:08 -07:00
Joseph Savona
48bc166428 [compiler] Update diagnostics for ValidatePreservedManualMemoization (#33759)
Uses the new diagnostic infrastructure for this validation, which lets
us provide a more targeted message on the text that we highlight (eg
"This dependency may be mutated later") separately from the overall
error message.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33759).
* #33981
* #33777
* #33767
* #33765
* #33760
* __->__ #33759
* #33758
2025-07-24 15:39:53 -07:00
Joseph Savona
72848027a5 [compiler] Improve more error messages (#33758)
This PR uses the new diagnostic type for most of the error messages
produced in our explicit validation passes (`Validation/` directory).
One of the validations produced multiple errors as a hack to showing
multiple related locations, which we can now consolidate into a single
diagnostic.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33758).
* #33981
* #33777
* #33767
* #33765
* #33760
* #33759
* __->__ #33758
2025-07-24 15:39:42 -07:00
Joseph Savona
707e321f8f [compiler][wip] Improve diagnostic infra (#33751)
Work in progress, i'm experimenting with revamping our diagnostic infra.
Starting with a better format for representing errors, with an ability
to point ot multiple locations, along with better printing of errors. Of
course, Babel still controls the printing in the majority case so this
still needs more work.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33751).
* #33981
* #33777
* #33767
* #33765
* #33760
* #33759
* #33758
* __->__ #33751
* #33752
* #33753
2025-07-24 15:37:06 -07:00
Joseph Savona
0d39496eab [compiler] Enable additional lints by default (#33752)
Enable more validations to help catch bad patterns, but only in the
linter. These rules are already enabled by default in the compiler _if_
violations could produce unsafe output.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33752).
* #33981
* #33777
* #33767
* #33765
* #33760
* #33759
* #33758
* #33751
* __->__ #33752
* #33753
2025-07-24 15:36:54 -07:00
Joseph Savona
6f4294af9b [compiler] Validate against setState in all effect types (#33753)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33753).
* #33981
* #33777
* #33767
* #33765
* #33760
* #33759
* #33758
* #33751
* #33752
* __->__ #33753
2025-07-24 15:36:40 -07:00
Joseph Savona
448f781a52 [compiler] Fix for false positive mutation of destructured spread object (#33786)
When destructuring, spread creates a new mutable object that _captures_
part of the original rvalue. This new value is safe to modify.

When making this change I realized that we weren't inferring array
pattern spread as creating an array (in type inference) so I also added
that here.
2025-07-24 15:16:28 -07:00
Sebastian Markbåge
5020d48d28 [DevTools] Feature detect createSidebarPane (#33988)
Same as #33987 but for the sidebar pane creation.
2025-07-24 17:42:06 -04:00
Sebastian Markbåge
3082604bdc [DevTools] Feature detect sources panel (#33987)
I broke Firefox DevTools extension in #33968.

It turns out the Firefox has a placeholder object for the sources panel
which is empty. We need to detect the actual event handler.
2025-07-24 17:38:12 -04:00
517 changed files with 9191 additions and 2687 deletions

View File

@@ -608,6 +608,7 @@ module.exports = {
symbol: 'readonly',
SyntheticEvent: 'readonly',
SyntheticMouseEvent: 'readonly',
SyntheticPointerEvent: 'readonly',
Thenable: 'readonly',
TimeoutID: 'readonly',
WheelEventHandler: 'readonly',

View File

@@ -1,3 +1,8 @@
## 19.1.1 (July 28, 2025)
### React
* Fixed Owner Stacks to work with ES2015 function.name semantics ([#33680](https://github.com/facebook/react/pull/33680) by @hoxyq)
## 19.1.0 (March 28, 2025)
### Owner Stack

View File

@@ -1,5 +1,5 @@
import { c as _c } from "react/compiler-runtime"; // 
        @compilationMode:"all"
@compilationMode:"all"
function nonReactFn() {
  const $ = _c(1);
  let t0;

View File

@@ -11,6 +11,7 @@ import * as t from '@babel/types';
import BabelPluginReactCompiler, {
CompilerError,
CompilerErrorDetail,
CompilerDiagnostic,
Effect,
ErrorSeverity,
parseConfigPragmaForTests,
@@ -141,10 +142,13 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
],
];
function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
function compile(
source: string,
mode: 'compiler' | 'linter',
): [CompilerOutput, 'flow' | 'typescript'] {
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
const error = new CompilerError();
const otherErrors: Array<CompilerErrorDetail> = [];
const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
const entry = results.get(result.name);
if (Array.isArray(entry)) {
@@ -203,6 +207,23 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
};
const parsedOptions = parseConfigPragmaForTests(pragma, {
compilationMode: 'infer',
environment:
mode === 'linter'
? {
// enabled in compiler
validateRefAccessDuringRender: false,
// enabled in linter
validateNoSetStateInRender: true,
validateNoSetStateInEffects: true,
validateNoJSXInTryStatements: true,
validateNoImpureFunctionsInRender: true,
validateStaticComponents: true,
validateNoFreezingKnownMutableFunctions: true,
validateNoVoidUseMemo: true,
}
: {
/* use defaults for compiler mode */
},
});
const opts: PluginOptions = parsePluginOptions({
...parsedOptions,
@@ -214,7 +235,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
debugLogIRs: logIR,
logEvent: (_filename: string | null, event: LoggerEvent) => {
if (event.kind === 'CompileError') {
otherErrors.push(new CompilerErrorDetail(event.detail));
otherErrors.push(event.detail);
}
},
},
@@ -226,7 +247,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
* (i.e. object shape that is not CompilerError)
*/
if (err instanceof CompilerError && err.details.length > 0) {
error.details.push(...err.details);
error.merge(err);
} else {
/**
* Handle unexpected failures by logging (to get a stack trace)
@@ -245,12 +266,15 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
}
// Only include logger errors if there weren't other errors
if (!error.hasErrors() && otherErrors.length !== 0) {
otherErrors.forEach(e => error.push(e));
otherErrors.forEach(e => error.details.push(e));
}
if (error.hasErrors()) {
return [{kind: 'err', results, error: error}, language];
return [{kind: 'err', results, error}, language];
}
return [{kind: 'ok', results, transformOutput}, language];
return [
{kind: 'ok', results, transformOutput, errors: error.details},
language,
];
}
export default function Editor(): JSX.Element {
@@ -259,7 +283,11 @@ export default function Editor(): JSX.Element {
const dispatchStore = useStoreDispatch();
const {enqueueSnackbar} = useSnackbar();
const [compilerOutput, language] = useMemo(
() => compile(deferredStore.source),
() => compile(deferredStore.source, 'compiler'),
[deferredStore.source],
);
const [linterOutput] = useMemo(
() => compile(deferredStore.source, 'linter'),
[deferredStore.source],
);
@@ -285,19 +313,26 @@ export default function Editor(): JSX.Element {
});
});
let mergedOutput: CompilerOutput;
let errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
if (compilerOutput.kind === 'ok') {
errors = linterOutput.kind === 'ok' ? [] : linterOutput.error.details;
mergedOutput = {
...compilerOutput,
errors,
};
} else {
mergedOutput = compilerOutput;
errors = compilerOutput.error.details;
}
return (
<>
<div className="relative flex basis top-14">
<div className={clsx('relative sm:basis-1/4')}>
<Input
language={language}
errors={
compilerOutput.kind === 'err' ? compilerOutput.error.details : []
}
/>
<Input language={language} errors={errors} />
</div>
<div className={clsx('flex sm:flex flex-wrap')}>
<Output store={deferredStore} compilerOutput={compilerOutput} />
<Output store={deferredStore} compilerOutput={mergedOutput} />
</div>
</div>
</>

View File

@@ -36,13 +36,18 @@ export default function Input({errors, language}: Props): JSX.Element {
const uri = monaco.Uri.parse(`file:///index.js`);
const model = monaco.editor.getModel(uri);
invariant(model, 'Model must exist for the selected input file.');
renderReactCompilerMarkers({monaco, model, details: errors});
renderReactCompilerMarkers({
monaco,
model,
details: errors,
source: store.source,
});
/**
* N.B. that `tabSize` is a model property, not an editor property.
* So, the tab size has to be set per model.
*/
model.updateOptions({tabSize: 2});
}, [monaco, errors]);
}, [monaco, errors, store.source]);
useEffect(() => {
/**

View File

@@ -11,7 +11,11 @@ import {
InformationCircleIcon,
} from '@heroicons/react/outline';
import MonacoEditor, {DiffEditor} from '@monaco-editor/react';
import {type CompilerError} from 'babel-plugin-react-compiler';
import {
CompilerErrorDetail,
CompilerDiagnostic,
type CompilerError,
} from 'babel-plugin-react-compiler';
import parserBabel from 'prettier/plugins/babel';
import * as prettierPluginEstree from 'prettier/plugins/estree';
import * as prettier from 'prettier/standalone';
@@ -44,6 +48,7 @@ export type CompilerOutput =
kind: 'ok';
transformOutput: CompilerTransformOutput;
results: Map<string, Array<PrintedCompilerPipelineValue>>;
errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
}
| {
kind: 'err';
@@ -123,10 +128,36 @@ async function tabify(
parser: transformOutput.language === 'flow' ? 'babel-flow' : 'babel-ts',
plugins: [parserBabel, prettierPluginEstree],
});
let output: string;
let language: string;
if (compilerOutput.errors.length === 0) {
output = code;
language = 'javascript';
} else {
language = 'markdown';
output = `
# Summary
React Compiler compiled this function successfully, but there are lint errors that indicate potential issues with the original code.
## ${compilerOutput.errors.length} Lint Errors
${compilerOutput.errors.map(e => e.printErrorMessage(source, {eslint: false})).join('\n\n')}
## Output
\`\`\`js
${code}
\`\`\`
`.trim();
}
reorderedTabs.set(
'JS',
'Output',
<TextTabContent
output={code}
output={output}
language={language}
diff={null}
showInfoPanel={false}></TextTabContent>,
);
@@ -142,6 +173,18 @@ async function tabify(
</>,
);
}
} else if (compilerOutput.kind === 'err') {
const errors = compilerOutput.error.printErrorMessage(source, {
eslint: false,
});
reorderedTabs.set(
'Output',
<TextTabContent
output={errors}
language="markdown"
diff={null}
showInfoPanel={false}></TextTabContent>,
);
}
tabs.forEach((tab, name) => {
reorderedTabs.set(name, tab);
@@ -162,17 +205,32 @@ function getSourceMapUrl(code: string, map: string): string | null {
}
function Output({store, compilerOutput}: Props): JSX.Element {
const [tabsOpen, setTabsOpen] = useState<Set<string>>(() => new Set(['JS']));
const [tabsOpen, setTabsOpen] = useState<Set<string>>(
() => new Set(['Output']),
);
const [tabs, setTabs] = useState<Map<string, React.ReactNode>>(
() => new Map(),
);
/*
* Update the active tab back to the output or errors tab when the compilation state
* changes between success/failure.
*/
const [previousOutputKind, setPreviousOutputKind] = useState(
compilerOutput.kind,
);
if (compilerOutput.kind !== previousOutputKind) {
setPreviousOutputKind(compilerOutput.kind);
setTabsOpen(new Set(['Output']));
}
useEffect(() => {
tabify(store.source, compilerOutput).then(tabs => {
setTabs(tabs);
});
}, [store.source, compilerOutput]);
const changedPasses: Set<string> = new Set(['JS', 'HIR']); // Initial and final passes should always be bold
const changedPasses: Set<string> = new Set(['Output', 'HIR']); // Initial and final passes should always be bold
let lastResult: string = '';
for (const [passName, results] of compilerOutput.results) {
for (const result of results) {
@@ -196,20 +254,6 @@ function Output({store, compilerOutput}: Props): JSX.Element {
tabs={tabs}
changedPasses={changedPasses}
/>
{compilerOutput.kind === 'err' ? (
<div
className="flex flex-wrap absolute bottom-0 bg-white grow border-y border-grey-200 transition-all ease-in"
style={{width: 'calc(100vw - 650px)'}}>
<div className="w-full p-4 basis-full border-b">
<h2>COMPILER ERRORS</h2>
</div>
<pre
className="p-4 basis-full text-red-600 overflow-y-scroll whitespace-pre-wrap"
style={{width: 'calc(100vw - 650px)', height: '150px'}}>
<code>{compilerOutput.error.toString()}</code>
</pre>
</div>
) : null}
</>
);
}
@@ -218,10 +262,12 @@ function TextTabContent({
output,
diff,
showInfoPanel,
language,
}: {
output: string;
diff: string | null;
showInfoPanel: boolean;
language: string;
}): JSX.Element {
const [diffMode, setDiffMode] = useState(false);
return (
@@ -272,7 +318,7 @@ function TextTabContent({
/>
) : (
<MonacoEditor
defaultLanguage="javascript"
language={language ?? 'javascript'}
value={output}
options={{
...monacoOptions,

View File

@@ -28,5 +28,5 @@ export const monacoOptions: Partial<EditorProps['options']> = {
automaticLayout: true,
wordWrap: 'on',
wrappingIndent: 'deepIndent',
wrappingIndent: 'same',
};

View File

@@ -6,7 +6,11 @@
*/
import {Monaco} from '@monaco-editor/react';
import {CompilerErrorDetail, ErrorSeverity} from 'babel-plugin-react-compiler';
import {
CompilerDiagnostic,
CompilerErrorDetail,
ErrorSeverity,
} from 'babel-plugin-react-compiler';
import {MarkerSeverity, type editor} from 'monaco-editor';
function mapReactCompilerSeverityToMonaco(
@@ -22,38 +26,46 @@ function mapReactCompilerSeverityToMonaco(
}
function mapReactCompilerDiagnosticToMonacoMarker(
detail: CompilerErrorDetail,
detail: CompilerErrorDetail | CompilerDiagnostic,
monaco: Monaco,
source: string,
): editor.IMarkerData | null {
if (detail.loc == null || typeof detail.loc === 'symbol') {
const loc = detail.primaryLocation();
if (loc == null || typeof loc === 'symbol') {
return null;
}
const severity = mapReactCompilerSeverityToMonaco(detail.severity, monaco);
let message = detail.printErrorMessage();
let message = detail.printErrorMessage(source, {eslint: true});
return {
severity,
message,
startLineNumber: detail.loc.start.line,
startColumn: detail.loc.start.column + 1,
endLineNumber: detail.loc.end.line,
endColumn: detail.loc.end.column + 1,
startLineNumber: loc.start.line,
startColumn: loc.start.column + 1,
endLineNumber: loc.end.line,
endColumn: loc.end.column + 1,
};
}
type ReactCompilerMarkerConfig = {
monaco: Monaco;
model: editor.ITextModel;
details: Array<CompilerErrorDetail>;
details: Array<CompilerErrorDetail | CompilerDiagnostic>;
source: string;
};
let decorations: Array<string> = [];
export function renderReactCompilerMarkers({
monaco,
model,
details,
source,
}: ReactCompilerMarkerConfig): void {
const markers: Array<editor.IMarkerData> = [];
for (const detail of details) {
const marker = mapReactCompilerDiagnosticToMonacoMarker(detail, monaco);
const marker = mapReactCompilerDiagnosticToMonacoMarker(
detail,
monaco,
source,
);
if (marker == null) {
continue;
}

View File

@@ -12,6 +12,7 @@ import {
pipelineUsesReanimatedPlugin,
} from '../Entrypoint/Reanimated';
import validateNoUntransformedReferences from '../Entrypoint/ValidateNoUntransformedReferences';
import {CompilerError} from '..';
const ENABLE_REACT_COMPILER_TIMINGS =
process.env['ENABLE_REACT_COMPILER_TIMINGS'] === '1';
@@ -34,51 +35,58 @@ export default function BabelPluginReactCompiler(
*/
Program: {
enter(prog, pass): void {
const filename = pass.filename ?? 'unknown';
if (ENABLE_REACT_COMPILER_TIMINGS === true) {
performance.mark(`${filename}:start`, {
detail: 'BabelPlugin:Program:start',
});
}
let opts = parsePluginOptions(pass.opts);
const isDev =
(typeof __DEV__ !== 'undefined' && __DEV__ === true) ||
process.env['NODE_ENV'] === 'development';
if (
opts.enableReanimatedCheck === true &&
pipelineUsesReanimatedPlugin(pass.file.opts.plugins)
) {
opts = injectReanimatedFlag(opts);
}
if (
opts.environment.enableResetCacheOnSourceFileChanges !== false &&
isDev
) {
opts = {
...opts,
environment: {
...opts.environment,
enableResetCacheOnSourceFileChanges: true,
},
};
}
const result = compileProgram(prog, {
opts,
filename: pass.filename ?? null,
comments: pass.file.ast.comments ?? [],
code: pass.file.code,
});
validateNoUntransformedReferences(
prog,
pass.filename ?? null,
opts.logger,
opts.environment,
result,
);
if (ENABLE_REACT_COMPILER_TIMINGS === true) {
performance.mark(`${filename}:end`, {
detail: 'BabelPlugin:Program:end',
try {
const filename = pass.filename ?? 'unknown';
if (ENABLE_REACT_COMPILER_TIMINGS === true) {
performance.mark(`${filename}:start`, {
detail: 'BabelPlugin:Program:start',
});
}
let opts = parsePluginOptions(pass.opts);
const isDev =
(typeof __DEV__ !== 'undefined' && __DEV__ === true) ||
process.env['NODE_ENV'] === 'development';
if (
opts.enableReanimatedCheck === true &&
pipelineUsesReanimatedPlugin(pass.file.opts.plugins)
) {
opts = injectReanimatedFlag(opts);
}
if (
opts.environment.enableResetCacheOnSourceFileChanges !== false &&
isDev
) {
opts = {
...opts,
environment: {
...opts.environment,
enableResetCacheOnSourceFileChanges: true,
},
};
}
const result = compileProgram(prog, {
opts,
filename: pass.filename ?? null,
comments: pass.file.ast.comments ?? [],
code: pass.file.code,
});
validateNoUntransformedReferences(
prog,
pass.filename ?? null,
opts.logger,
opts.environment,
result,
);
if (ENABLE_REACT_COMPILER_TIMINGS === true) {
performance.mark(`${filename}:end`, {
detail: 'BabelPlugin:Program:end',
});
}
} catch (e) {
if (e instanceof CompilerError) {
throw e.withPrintedMessage(pass.file.code, {eslint: false});
}
throw e;
}
},
exit(_, pass): void {

View File

@@ -5,6 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
import * as t from '@babel/types';
import {codeFrameColumns} from '@babel/code-frame';
import type {SourceLocation} from './HIR';
import {Err, Ok, Result} from './Utils/Result';
import {assertExhaustive} from './Utils/utils';
@@ -44,6 +46,24 @@ export enum ErrorSeverity {
Invariant = 'Invariant',
}
export type CompilerDiagnosticOptions = {
severity: ErrorSeverity;
category: string;
description: string;
details: Array<CompilerDiagnosticDetail>;
suggestions?: Array<CompilerSuggestion> | null | undefined;
};
export type CompilerDiagnosticDetail =
/**
* A/the source of the error
*/
{
kind: 'error';
loc: SourceLocation | null;
message: string;
};
export enum CompilerSuggestionOperation {
InsertBefore,
InsertAfter,
@@ -74,6 +94,103 @@ export type CompilerErrorDetailOptions = {
suggestions?: Array<CompilerSuggestion> | null | undefined;
};
export type PrintErrorMessageOptions = {
/**
* ESLint uses 1-indexed columns and prints one error at a time
* So it doesn't require the "Found # error(s)" text
*/
eslint: boolean;
};
export class CompilerDiagnostic {
options: CompilerDiagnosticOptions;
constructor(options: CompilerDiagnosticOptions) {
this.options = options;
}
static create(
options: Omit<CompilerDiagnosticOptions, 'details'>,
): CompilerDiagnostic {
return new CompilerDiagnostic({...options, details: []});
}
get category(): CompilerDiagnosticOptions['category'] {
return this.options.category;
}
get description(): CompilerDiagnosticOptions['description'] {
return this.options.description;
}
get severity(): CompilerDiagnosticOptions['severity'] {
return this.options.severity;
}
get suggestions(): CompilerDiagnosticOptions['suggestions'] {
return this.options.suggestions;
}
withDetail(detail: CompilerDiagnosticDetail): CompilerDiagnostic {
this.options.details.push(detail);
return this;
}
primaryLocation(): SourceLocation | null {
return this.options.details.filter(d => d.kind === 'error')[0]?.loc ?? null;
}
printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
const buffer = [
printErrorSummary(this.severity, this.category),
'\n\n',
this.description,
];
for (const detail of this.options.details) {
switch (detail.kind) {
case 'error': {
const loc = detail.loc;
if (loc == null || typeof loc === 'symbol') {
continue;
}
let codeFrame: string;
try {
codeFrame = printCodeFrame(source, loc, detail.message);
} catch (e) {
codeFrame = detail.message;
}
buffer.push('\n\n');
if (loc.filename != null) {
const line = loc.start.line;
const column = options.eslint
? loc.start.column + 1
: loc.start.column;
buffer.push(`${loc.filename}:${line}:${column}\n`);
}
buffer.push(codeFrame);
break;
}
default: {
assertExhaustive(
detail.kind,
`Unexpected detail kind ${(detail as any).kind}`,
);
}
}
}
return buffer.join('');
}
toString(): string {
const buffer = [printErrorSummary(this.severity, this.category)];
if (this.description != null) {
buffer.push(`. ${this.description}.`);
}
const loc = this.primaryLocation();
if (loc != null && typeof loc !== 'symbol') {
buffer.push(` (${loc.start.line}:${loc.start.column})`);
}
return buffer.join('');
}
}
/*
* Each bailout or invariant in HIR lowering creates an {@link CompilerErrorDetail}, which is then
* aggregated into a single {@link CompilerError} later.
@@ -101,24 +218,51 @@ export class CompilerErrorDetail {
return this.options.suggestions;
}
printErrorMessage(): string {
const buffer = [`${this.severity}: ${this.reason}`];
primaryLocation(): SourceLocation | null {
return this.loc;
}
printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
const buffer = [printErrorSummary(this.severity, this.reason)];
if (this.description != null) {
buffer.push(`. ${this.description}`);
buffer.push(`\n\n${this.description}.`);
}
if (this.loc != null && typeof this.loc !== 'symbol') {
buffer.push(` (${this.loc.start.line}:${this.loc.end.line})`);
const loc = this.loc;
if (loc != null && typeof loc !== 'symbol') {
let codeFrame: string;
try {
codeFrame = printCodeFrame(source, loc, this.reason);
} catch (e) {
codeFrame = '';
}
buffer.push(`\n\n`);
if (loc.filename != null) {
const line = loc.start.line;
const column = options.eslint ? loc.start.column + 1 : loc.start.column;
buffer.push(`${loc.filename}:${line}:${column}\n`);
}
buffer.push(codeFrame);
buffer.push('\n\n');
}
return buffer.join('');
}
toString(): string {
return this.printErrorMessage();
const buffer = [printErrorSummary(this.severity, this.reason)];
if (this.description != null) {
buffer.push(`. ${this.description}.`);
}
const loc = this.loc;
if (loc != null && typeof loc !== 'symbol') {
buffer.push(` (${loc.start.line}:${loc.start.column})`);
}
return buffer.join('');
}
}
export class CompilerError extends Error {
details: Array<CompilerErrorDetail> = [];
details: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
printedMessage: string | null = null;
static invariant(
condition: unknown,
@@ -136,6 +280,12 @@ export class CompilerError extends Error {
}
}
static throwDiagnostic(options: CompilerDiagnosticOptions): never {
const errors = new CompilerError();
errors.pushDiagnostic(new CompilerDiagnostic(options));
throw errors;
}
static throwTodo(
options: Omit<CompilerErrorDetailOptions, 'severity'>,
): never {
@@ -198,18 +348,49 @@ export class CompilerError extends Error {
}
override get message(): string {
return this.toString();
return this.printedMessage ?? this.toString();
}
override set message(_message: string) {}
override toString(): string {
if (this.printedMessage) {
return this.printedMessage;
}
if (Array.isArray(this.details)) {
return this.details.map(detail => detail.toString()).join('\n\n');
}
return this.name;
}
withPrintedMessage(
source: string,
options: PrintErrorMessageOptions,
): CompilerError {
this.printedMessage = this.printErrorMessage(source, options);
return this;
}
printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
if (options.eslint && this.details.length === 1) {
return this.details[0].printErrorMessage(source, options);
}
return (
`Found ${this.details.length} error${this.details.length === 1 ? '' : 's'}:\n\n` +
this.details
.map(detail => detail.printErrorMessage(source, options).trim())
.join('\n\n')
);
}
merge(other: CompilerError): void {
this.details.push(...other.details);
}
pushDiagnostic(diagnostic: CompilerDiagnostic): void {
this.details.push(diagnostic);
}
push(options: CompilerErrorDetailOptions): CompilerErrorDetail {
const detail = new CompilerErrorDetail({
reason: options.reason,
@@ -260,3 +441,55 @@ export class CompilerError extends Error {
});
}
}
function printCodeFrame(
source: string,
loc: t.SourceLocation,
message: string,
): string {
return codeFrameColumns(
source,
{
start: {
line: loc.start.line,
column: loc.start.column + 1,
},
end: {
line: loc.end.line,
column: loc.end.column + 1,
},
},
{
message,
},
);
}
function printErrorSummary(severity: ErrorSeverity, message: string): string {
let severityCategory: string;
switch (severity) {
case ErrorSeverity.InvalidConfig:
case ErrorSeverity.InvalidJS:
case ErrorSeverity.InvalidReact:
case ErrorSeverity.UnsupportedJS: {
severityCategory = 'Error';
break;
}
case ErrorSeverity.CannotPreserveMemoization: {
severityCategory = 'Memoization';
break;
}
case ErrorSeverity.Invariant: {
severityCategory = 'Invariant';
break;
}
case ErrorSeverity.Todo: {
severityCategory = 'Todo';
break;
}
default: {
assertExhaustive(severity, `Unexpected severity '${severity}'`);
}
}
return `${severityCategory}: ${message}`;
}

View File

@@ -7,7 +7,12 @@
import * as t from '@babel/types';
import {z} from 'zod';
import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError';
import {
CompilerDiagnostic,
CompilerError,
CompilerErrorDetail,
CompilerErrorDetailOptions,
} from '../CompilerError';
import {
EnvironmentConfig,
ExternalFunction,
@@ -224,7 +229,7 @@ export type LoggerEvent =
export type CompileErrorEvent = {
kind: 'CompileError';
fnLoc: t.SourceLocation | null;
detail: CompilerErrorDetailOptions;
detail: CompilerErrorDetail | CompilerDiagnostic;
};
export type CompileDiagnosticEvent = {
kind: 'CompileDiagnostic';

View File

@@ -174,7 +174,7 @@ function runWithEnvironment(
!env.config.disableMemoizationForDebugging &&
!env.config.enableChangeDetectionForDebugging
) {
dropManualMemoization(hir);
dropManualMemoization(hir).unwrap();
log({kind: 'hir', name: 'DropManualMemoization', value: hir});
}

View File

@@ -181,7 +181,7 @@ function logError(
context.opts.logger.logEvent(context.filename, {
kind: 'CompileError',
fnLoc,
detail: detail.options,
detail,
});
}
} else {

View File

@@ -8,8 +8,8 @@
import {NodePath} from '@babel/core';
import * as t from '@babel/types';
import {
CompilerDiagnostic,
CompilerError,
CompilerErrorDetail,
CompilerSuggestionOperation,
ErrorSeverity,
} from '../CompilerError';
@@ -181,12 +181,11 @@ export function suppressionsToCompilerError(
'Unhandled suppression source',
);
}
error.pushErrorDetail(
new CompilerErrorDetail({
reason: `${reason}. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior`,
description: suppressionRange.disableComment.value.trim(),
error.pushDiagnostic(
CompilerDiagnostic.create({
category: reason,
description: `React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression \`${suppressionRange.disableComment.value.trim()}\``,
severity: ErrorSeverity.InvalidReact,
loc: suppressionRange.disableComment.loc ?? null,
suggestions: [
{
description: suggestion,
@@ -197,6 +196,10 @@ export function suppressionsToCompilerError(
op: CompilerSuggestionOperation.Remove,
},
],
}).withDetail({
kind: 'error',
loc: suppressionRange.disableComment.loc ?? null,
message: 'Found React rule suppression',
}),
);
}

View File

@@ -8,32 +8,27 @@
import {NodePath} from '@babel/core';
import * as t from '@babel/types';
import {
CompilerError,
CompilerErrorDetailOptions,
EnvironmentConfig,
ErrorSeverity,
Logger,
} from '..';
import {CompilerError, EnvironmentConfig, ErrorSeverity, Logger} from '..';
import {getOrInsertWith} from '../Utils/utils';
import {Environment} from '../HIR';
import {Environment, GeneratedSource} from '../HIR';
import {DEFAULT_EXPORT} from '../HIR/Environment';
import {CompileProgramMetadata} from './Program';
import {CompilerDiagnostic, CompilerDiagnosticOptions} from '../CompilerError';
function throwInvalidReact(
options: Omit<CompilerErrorDetailOptions, 'severity'>,
options: Omit<CompilerDiagnosticOptions, 'severity'>,
{logger, filename}: TraversalState,
): never {
const detail: CompilerErrorDetailOptions = {
...options,
const detail: CompilerDiagnosticOptions = {
severity: ErrorSeverity.InvalidReact,
...options,
};
logger?.logEvent(filename, {
kind: 'CompileError',
fnLoc: null,
detail,
detail: new CompilerDiagnostic(detail),
});
CompilerError.throw(detail);
CompilerError.throwDiagnostic(detail);
}
function isAutodepsSigil(
@@ -97,14 +92,18 @@ function assertValidEffectImportReference(
*/
throwInvalidReact(
{
reason:
'[InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. ' +
'This will break your build! ' +
'To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics.',
description: maybeErrorDiagnostic
? `(Bailout reason: ${maybeErrorDiagnostic})`
: null,
loc: parent.node.loc ?? null,
category:
'Cannot infer dependencies of this effect. This will break your build!',
description:
'To resolve, either pass a dependency array or fix reported compiler bailout diagnostics.' +
(maybeErrorDiagnostic ? ` ${maybeErrorDiagnostic}` : ''),
details: [
{
kind: 'error',
message: 'Cannot infer dependencies',
loc: parent.node.loc ?? GeneratedSource,
},
],
},
context,
);
@@ -124,13 +123,20 @@ function assertValidFireImportReference(
);
throwInvalidReact(
{
reason:
'[Fire] Untransformed reference to compiler-required feature. ' +
'Either remove this `fire` call or ensure it is successfully transformed by the compiler',
description: maybeErrorDiagnostic
? `(Bailout reason: ${maybeErrorDiagnostic})`
: null,
loc: paths[0].node.loc ?? null,
category:
'[Fire] Untransformed reference to compiler-required feature.',
description:
'Either remove this `fire` call or ensure it is successfully transformed by the compiler' +
maybeErrorDiagnostic
? ` ${maybeErrorDiagnostic}`
: '',
details: [
{
kind: 'error',
message: 'Untransformed `fire` call',
loc: paths[0].node.loc ?? GeneratedSource,
},
],
},
context,
);

View File

@@ -9,6 +9,7 @@ import {NodePath, Scope} from '@babel/traverse';
import * as t from '@babel/types';
import invariant from 'invariant';
import {
CompilerDiagnostic,
CompilerError,
CompilerSuggestionOperation,
ErrorSeverity,
@@ -104,12 +105,17 @@ export function lower(
if (param.isIdentifier()) {
const binding = builder.resolveIdentifier(param);
if (binding.kind !== 'Identifier') {
builder.errors.push({
reason: `(BuildHIR::lower) Could not find binding for param \`${param.node.name}\``,
severity: ErrorSeverity.Invariant,
loc: param.node.loc ?? null,
suggestions: null,
});
builder.errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.Invariant,
category: 'Could not find binding',
description: `[BuildHIR] Could not find binding for param \`${param.node.name}\`.`,
}).withDetail({
kind: 'error',
loc: param.node.loc ?? null,
message: 'Could not find binding',
}),
);
return;
}
const place: Place = {
@@ -163,12 +169,17 @@ export function lower(
'Assignment',
);
} else {
builder.errors.push({
reason: `(BuildHIR::lower) Handle ${param.node.type} params`,
severity: ErrorSeverity.Todo,
loc: param.node.loc ?? null,
suggestions: null,
});
builder.errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.Todo,
category: `Handle ${param.node.type} parameters`,
description: `[BuildHIR] Add support for ${param.node.type} parameters.`,
}).withDetail({
kind: 'error',
loc: param.node.loc ?? null,
message: 'Unsupported parameter type',
}),
);
}
});
@@ -178,6 +189,7 @@ export function lower(
const fallthrough = builder.reserve('block');
const terminal: ReturnTerminal = {
kind: 'return',
returnVariant: 'Implicit',
loc: GeneratedSource,
value: lowerExpressionToTemporary(builder, body),
id: makeInstructionId(0),
@@ -188,13 +200,17 @@ export function lower(
lowerStatement(builder, body);
directives = body.get('directives').map(d => d.node.value.value);
} else {
builder.errors.push({
severity: ErrorSeverity.InvalidJS,
reason: `Unexpected function body kind`,
description: `Expected function body to be an expression or a block statement, got \`${body.type}\``,
loc: body.node.loc ?? null,
suggestions: null,
});
builder.errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidJS,
category: `Unexpected function body kind`,
description: `Expected function body to be an expression or a block statement, got \`${body.type}\`.`,
}).withDetail({
kind: 'error',
loc: body.node.loc ?? null,
message: 'Expected a block statement or expression',
}),
);
}
if (builder.errors.hasErrors()) {
@@ -204,6 +220,7 @@ export function lower(
builder.terminate(
{
kind: 'return',
returnVariant: 'Void',
loc: GeneratedSource,
value: lowerValueToTemporary(builder, {
kind: 'Primitive',
@@ -287,6 +304,7 @@ function lowerStatement(
}
const terminal: ReturnTerminal = {
kind: 'return',
returnVariant: 'Explicit',
loc: stmt.node.loc ?? GeneratedSource,
value,
id: makeInstructionId(0),
@@ -2271,11 +2289,17 @@ function lowerExpression(
});
for (const [name, locations] of Object.entries(fbtLocations)) {
if (locations.length > 1) {
CompilerError.throwTodo({
reason: `Support <${tagName}> tags with multiple <${tagName}:${name}> values`,
loc: locations.at(-1) ?? GeneratedSource,
description: null,
suggestions: null,
CompilerError.throwDiagnostic({
severity: ErrorSeverity.Todo,
category: 'Support duplicate fbt tags',
description: `Support \`<${tagName}>\` tags with multiple \`<${tagName}:${name}>\` values`,
details: locations.map(loc => {
return {
kind: 'error',
message: `Multiple \`<${tagName}:${name}>\` tags found`,
loc,
};
}),
});
}
}
@@ -3503,9 +3527,8 @@ function lowerFunction(
);
let loweredFunc: HIRFunction;
if (lowering.isErr()) {
lowering
.unwrapErr()
.details.forEach(detail => builder.errors.pushErrorDetail(detail));
const functionErrors = lowering.unwrapErr();
builder.errors.merge(functionErrors);
return null;
}
loweredFunc = lowering.unwrap();

View File

@@ -608,7 +608,7 @@ export const EnvironmentConfigSchema = z.object({
*
* Here the variables `ref` and `myRef` will be typed as Refs.
*/
enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(false),
enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(true),
/*
* If specified a value, the compiler lowers any calls to `useContext` to use
@@ -631,6 +631,17 @@ export const EnvironmentConfigSchema = z.object({
* ```
*/
lowerContextAccess: ExternalFunctionSchema.nullable().default(null),
/**
* If enabled, will validate useMemos that don't return any values:
*
* Valid:
* useMemo(() => foo, [foo]);
* useMemo(() => { return foo }, [foo]);
* Invalid:
* useMemo(() => { ... }, [...]);
*/
validateNoVoidUseMemo: z.boolean().default(false),
});
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;

View File

@@ -446,8 +446,20 @@ export type ThrowTerminal = {
};
export type Case = {test: Place | null; block: BlockId};
export type ReturnVariant = 'Void' | 'Implicit' | 'Explicit';
export type ReturnTerminal = {
kind: 'return';
/**
* Void:
* () => { ... }
* function() { ... }
* Implicit (ArrowFunctionExpression only):
* () => foo
* Explicit:
* () => { return ... }
* function () { return ... }
*/
returnVariant: ReturnVariant;
loc: SourceLocation;
value: Place;
id: InstructionId;

View File

@@ -7,7 +7,7 @@
import {Binding, NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {CompilerError} from '../CompilerError';
import {CompilerError, ErrorSeverity} from '../CompilerError';
import {Environment} from './Environment';
import {
BasicBlock,
@@ -308,9 +308,18 @@ export default class HIRBuilder {
resolveBinding(node: t.Identifier): Identifier {
if (node.name === 'fbt') {
CompilerError.throwTodo({
reason: 'Support local variables named "fbt"',
loc: node.loc ?? null,
CompilerError.throwDiagnostic({
severity: ErrorSeverity.Todo,
category: 'Support local variables named `fbt`',
description:
'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',
details: [
{
kind: 'error',
message: 'Rename to avoid conflict with fbt plugin',
loc: node.loc ?? GeneratedSource,
},
],
});
}
const originalName = node.name;

View File

@@ -1211,6 +1211,8 @@ addObject(BUILTIN_SHAPES, BuiltInRefValueId, [
['*', {kind: 'Object', shapeId: BuiltInRefValueId}],
]);
addObject(BUILTIN_SHAPES, ReanimatedSharedValueId, []);
addFunction(
BUILTIN_SHAPES,
[],

View File

@@ -215,7 +215,7 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
break;
}
case 'return': {
value = `[${terminal.id}] Return${
value = `[${terminal.id}] Return ${terminal.returnVariant}${
terminal.value != null ? ' ' + printPlace(terminal.value) : ''
}`;
if (terminal.effects != null) {
@@ -943,7 +943,10 @@ export function printAliasingEffect(effect: AliasingEffect): string {
return `Assign ${printPlaceForAliasEffect(effect.into)} = ${printPlaceForAliasEffect(effect.from)}`;
}
case 'Alias': {
return `Alias ${printPlaceForAliasEffect(effect.into)} = ${printPlaceForAliasEffect(effect.from)}`;
return `Alias ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
}
case 'MaybeAlias': {
return `MaybeAlias ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
}
case 'Capture': {
return `Capture ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
@@ -995,13 +998,13 @@ export function printAliasingEffect(effect: AliasingEffect): string {
return `${effect.kind} ${printPlaceForAliasEffect(effect.value)}`;
}
case 'MutateFrozen': {
return `MutateFrozen ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
return `MutateFrozen ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.category)}`;
}
case 'MutateGlobal': {
return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.category)}`;
}
case 'Impure': {
return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.category)}`;
}
case 'Render': {
return `Render ${printPlaceForAliasEffect(effect.place)}`;

View File

@@ -345,6 +345,51 @@ export function* eachPatternOperand(pattern: Pattern): Iterable<Place> {
}
}
export function* eachPatternItem(
pattern: Pattern,
): Iterable<Place | SpreadPattern> {
switch (pattern.kind) {
case 'ArrayPattern': {
for (const item of pattern.items) {
if (item.kind === 'Identifier') {
yield item;
} else if (item.kind === 'Spread') {
yield item;
} else if (item.kind === 'Hole') {
continue;
} else {
assertExhaustive(
item,
`Unexpected item kind \`${(item as any).kind}\``,
);
}
}
break;
}
case 'ObjectPattern': {
for (const property of pattern.properties) {
if (property.kind === 'ObjectProperty') {
yield property.place;
} else if (property.kind === 'Spread') {
yield property;
} else {
assertExhaustive(
property,
`Unexpected item kind \`${(property as any).kind}\``,
);
}
}
break;
}
default: {
assertExhaustive(
pattern,
`Unexpected pattern kind \`${(pattern as any).kind}\``,
);
}
}
}
export function mapInstructionLValues(
instr: Instruction,
fn: (place: Place) => Place,
@@ -732,6 +777,7 @@ export function mapTerminalSuccessors(
case 'return': {
return {
kind: 'return',
returnVariant: terminal.returnVariant,
loc: terminal.loc,
value: terminal.value,
id: makeInstructionId(0),

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerErrorDetailOptions} from '../CompilerError';
import {CompilerDiagnostic} from '../CompilerError';
import {
FunctionExpression,
GeneratedSource,
@@ -90,6 +90,23 @@ export type AliasingEffect =
* c could be mutating a.
*/
| {kind: 'Alias'; from: Place; into: Place}
/**
* Indicates the potential for information flow from `from` to `into`. This is used for a specific
* case: functions with unknown signatures. If the compiler sees a call such as `foo(x)`, it has to
* consider several possibilities (which may depend on the arguments):
* - foo(x) returns a new mutable value that does not capture any information from x.
* - foo(x) returns a new mutable value that *does* capture information from x.
* - foo(x) returns x itself, ie foo is the identity function
*
* The same is true of functions that take multiple arguments: `cond(a, b, c)` could conditionally
* return b or c depending on the value of a.
*
* To represent this case, MaybeAlias represents the fact that an aliasing relationship could exist.
* Any mutations that flow through this relationship automatically become conditional.
*/
| {kind: 'MaybeAlias'; from: Place; into: Place}
/**
* Records direct assignment: `into = from`.
*/
@@ -133,19 +150,19 @@ export type AliasingEffect =
/**
* Mutation of a value known to be immutable
*/
| {kind: 'MutateFrozen'; place: Place; error: CompilerErrorDetailOptions}
| {kind: 'MutateFrozen'; place: Place; error: CompilerDiagnostic}
/**
* Mutation of a global
*/
| {
kind: 'MutateGlobal';
place: Place;
error: CompilerErrorDetailOptions;
error: CompilerDiagnostic;
}
/**
* Indicates a side-effect that is not safe during render
*/
| {kind: 'Impure'; place: Place; error: CompilerErrorDetailOptions}
| {kind: 'Impure'; place: Place; error: CompilerDiagnostic}
/**
* 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
@@ -183,7 +200,8 @@ export function hashEffect(effect: AliasingEffect): string {
case 'ImmutableCapture':
case 'Assign':
case 'Alias':
case 'Capture': {
case 'Capture':
case 'MaybeAlias': {
return [
effect.kind,
effect.from.identifier.id,
@@ -211,9 +229,9 @@ export function hashEffect(effect: AliasingEffect): string {
effect.kind,
effect.place.identifier.id,
effect.error.severity,
effect.error.reason,
effect.error.category,
effect.error.description,
printSourceLocation(effect.error.loc ?? GeneratedSource),
printSourceLocation(effect.error.primaryLocation() ?? GeneratedSource),
].join(':');
}
case 'Mutate':

View File

@@ -85,7 +85,8 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
case 'Assign':
case 'Alias':
case 'Capture':
case 'CreateFrom': {
case 'CreateFrom':
case 'MaybeAlias': {
capturedOrMutated.add(effect.from.identifier.id);
break;
}

View File

@@ -5,7 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, SourceLocation} from '..';
import {
CompilerDiagnostic,
CompilerError,
ErrorSeverity,
SourceLocation,
} from '..';
import {
CallExpression,
Effect,
@@ -30,6 +35,7 @@ import {
makeInstructionId,
} from '../HIR';
import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
import {Result} from '../Utils/Result';
type ManualMemoCallee = {
kind: 'useMemo' | 'useCallback';
@@ -283,26 +289,43 @@ function extractManualMemoizationArgs(
instr: TInstruction<CallExpression> | TInstruction<MethodCall>,
kind: 'useCallback' | 'useMemo',
sidemap: IdentifierSidemap,
errors: CompilerError,
): {
fnPlace: Place;
fnPlace: Place | null;
depsList: Array<ManualMemoDependency> | null;
} {
const [fnPlace, depsListPlace] = instr.value.args as Array<
Place | SpreadPattern | undefined
>;
if (fnPlace == null) {
CompilerError.throwInvalidReact({
reason: `Expected a callback function to be passed to ${kind}`,
loc: instr.value.loc,
suggestions: null,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: `Expected a callback function to be passed to ${kind}`,
description: `Expected a callback function to be passed to ${kind}`,
suggestions: null,
}).withDetail({
kind: 'error',
loc: instr.value.loc,
message: `Expected a callback function to be passed to ${kind}`,
}),
);
return {fnPlace: null, depsList: null};
}
if (fnPlace.kind === 'Spread' || depsListPlace?.kind === 'Spread') {
CompilerError.throwInvalidReact({
reason: `Unexpected spread argument to ${kind}`,
loc: instr.value.loc,
suggestions: null,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: `Unexpected spread argument to ${kind}`,
description: `Unexpected spread argument to ${kind}`,
suggestions: null,
}).withDetail({
kind: 'error',
loc: instr.value.loc,
message: `Unexpected spread argument to ${kind}`,
}),
);
return {fnPlace: null, depsList: null};
}
let depsList: Array<ManualMemoDependency> | null = null;
if (depsListPlace != null) {
@@ -310,23 +333,40 @@ function extractManualMemoizationArgs(
depsListPlace.identifier.id,
);
if (maybeDepsList == null) {
CompilerError.throwInvalidReact({
reason: `Expected the dependency list for ${kind} to be an array literal`,
suggestions: null,
loc: depsListPlace.loc,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: `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,
}).withDetail({
kind: 'error',
loc: depsListPlace.loc,
message: `Expected the dependency list for ${kind} to be an array literal`,
}),
);
return {fnPlace, depsList: null};
}
depsList = maybeDepsList.map(dep => {
depsList = [];
for (const dep of maybeDepsList) {
const maybeDep = sidemap.maybeDeps.get(dep.identifier.id);
if (maybeDep == null) {
CompilerError.throwInvalidReact({
reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
suggestions: null,
loc: dep.loc,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: `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,
}).withDetail({
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);
}
return maybeDep;
});
}
}
return {
fnPlace,
@@ -341,8 +381,14 @@ function extractManualMemoizationArgs(
* rely on type inference to find useMemo/useCallback invocations, and instead does basic tracking
* of globals and property loads to find both direct calls as well as usage via the React namespace,
* eg `React.useMemo()`.
*
* This pass also validates that useMemo callbacks return a value (not void), ensuring that useMemo
* is only used for memoizing values and not for running arbitrary side effects.
*/
export function dropManualMemoization(func: HIRFunction): void {
export function dropManualMemoization(
func: HIRFunction,
): Result<void, CompilerError> {
const errors = new CompilerError();
const isValidationEnabled =
func.env.config.validatePreserveExistingMemoizationGuarantees ||
func.env.config.validateNoSetStateInRender ||
@@ -389,7 +435,47 @@ export function dropManualMemoization(func: HIRFunction): void {
instr as TInstruction<CallExpression> | TInstruction<MethodCall>,
manualMemo.kind,
sidemap,
errors,
);
if (fnPlace == null) {
continue;
}
/**
* Bailout on void return useMemos. This is an anti-pattern where code might be using
* useMemo like useEffect: running arbirtary side-effects synced to changes in specific
* values.
*/
if (
func.env.config.validateNoVoidUseMemo &&
manualMemo.kind === 'useMemo'
) {
const funcToCheck = sidemap.functions.get(
fnPlace.identifier.id,
)?.value;
if (funcToCheck !== undefined && funcToCheck.loweredFunc.func) {
if (!hasNonVoidReturn(funcToCheck.loweredFunc.func)) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: 'useMemo() callbacks must return a value',
description: `This ${
manualMemo.loadInstr.value.kind === 'PropertyLoad'
? 'React.useMemo'
: 'useMemo'
} callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects.`,
suggestions: null,
}).withDetail({
kind: 'error',
loc: instr.value.loc,
message: 'useMemo() callbacks must return a value',
}),
);
}
}
}
instr.value = getManualMemoizationReplacement(
fnPlace,
instr.value.loc,
@@ -410,11 +496,19 @@ export function dropManualMemoization(func: HIRFunction): void {
* is rare and likely sketchy.
*/
if (!sidemap.functions.has(fnPlace.identifier.id)) {
CompilerError.throwInvalidReact({
reason: `Expected the first argument to be an inline function expression`,
suggestions: [],
loc: fnPlace.loc,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: `Expected the first argument to be an inline function expression`,
description: `Expected the first argument to be an inline function expression`,
suggestions: [],
}).withDetail({
kind: 'error',
loc: fnPlace.loc,
message: `Expected the first argument to be an inline function expression`,
}),
);
continue;
}
const memoDecl: Place =
manualMemo.kind === 'useMemo'
@@ -486,6 +580,8 @@ export function dropManualMemoization(func: HIRFunction): void {
markInstructionIds(func.body);
}
}
return errors.asResult();
}
function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
@@ -530,3 +626,17 @@ function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
}
return optionals;
}
function hasNonVoidReturn(func: HIRFunction): boolean {
for (const [, block] of func.body.blocks) {
if (block.terminal.kind === 'return') {
if (
block.terminal.returnVariant === 'Explicit' ||
block.terminal.returnVariant === 'Implicit'
) {
return true;
}
}
}
return false;
}

View File

@@ -326,26 +326,26 @@ function isEffectSafeOutsideRender(effect: FunctionEffect): boolean {
export function getWriteErrorReason(abstractValue: AbstractValue): string {
if (abstractValue.reason.has(ValueReason.Global)) {
return 'Writing to a variable defined outside a component or hook is not allowed. Consider using an effect';
return 'Modifying a variable defined outside a component or hook is not allowed. Consider using an effect';
} else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
return 'Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX';
return 'Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX';
} else if (abstractValue.reason.has(ValueReason.Context)) {
return `Mutating a value returned from 'useContext()', which should not be mutated`;
return `Modifying a value returned from 'useContext()' is not allowed.`;
} else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
return 'Mutating a value returned from a function whose return value should not be mutated';
return 'Modifying a value returned from a function whose return value should not be mutated';
} else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
return 'Mutating component props or hook arguments is not allowed. Consider using a local variable instead';
return 'Modifying component props or hook arguments is not allowed. Consider using a local variable instead';
} else if (abstractValue.reason.has(ValueReason.State)) {
return "Mutating a value returned from 'useState()', which should not be mutated. Use the setter function to update instead";
return "Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead";
} else if (abstractValue.reason.has(ValueReason.ReducerState)) {
return "Mutating a value returned from 'useReducer()', which should not be mutated. Use the dispatch function to update instead";
return "Modifying a value returned from 'useReducer()', which should not be modified directly. Use the dispatch function to update instead";
} else if (abstractValue.reason.has(ValueReason.Effect)) {
return 'Updating a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the mutation before calling useEffect()';
return 'Modifying a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the modification before calling useEffect()';
} else if (abstractValue.reason.has(ValueReason.HookCaptured)) {
return 'Updating a value previously passed as an argument to a hook is not allowed. Consider moving the mutation before calling the hook';
return 'Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook';
} else if (abstractValue.reason.has(ValueReason.HookReturn)) {
return 'Updating a value returned from a hook is not allowed. Consider moving the mutation into the hook where the value is constructed';
return 'Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed';
} else {
return 'This mutates a variable that React considers immutable';
return 'This modifies a variable that React considers immutable';
}
}

View File

@@ -6,6 +6,7 @@
*/
import {
CompilerDiagnostic,
CompilerError,
Effect,
ErrorSeverity,
@@ -36,8 +37,8 @@ import {
ValueReason,
} from '../HIR';
import {
eachInstructionValueLValue,
eachInstructionValueOperand,
eachPatternItem,
eachTerminalOperand,
eachTerminalSuccessor,
} from '../HIR/visitors';
@@ -446,20 +447,23 @@ function applySignature(
reason: value.reason,
context: new Set(),
});
const variable =
effect.value.identifier.name !== null &&
effect.value.identifier.name.kind === 'named'
? `\`${effect.value.identifier.name.value}\``
: 'value';
effects.push({
kind: 'MutateFrozen',
place: effect.value,
error: {
error: CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
reason,
description:
effect.value.identifier.name !== null &&
effect.value.identifier.name.kind === 'named'
? `Found mutation of \`${effect.value.identifier.name.value}\``
: null,
category: 'This value cannot be modified',
description: `${reason}.`,
}).withDetail({
kind: 'error',
loc: effect.value.loc,
suggestions: null,
},
message: `${variable} cannot be modified`,
}),
});
}
}
@@ -687,6 +691,7 @@ function applyEffect(
}
break;
}
case 'MaybeAlias':
case 'Alias':
case 'Capture': {
CompilerError.invariant(
@@ -951,7 +956,7 @@ function applyEffect(
context,
state,
// OK: recording information flow
{kind: 'Alias', from: operand, into: effect.into},
{kind: 'MaybeAlias', from: operand, into: effect.into},
initialized,
effects,
);
@@ -1013,33 +1018,31 @@ function applyEffect(
effect.value.identifier.declarationId,
)
) {
const description =
const variable =
effect.value.identifier.name !== null &&
effect.value.identifier.name.kind === 'named'
? `Variable \`${effect.value.identifier.name.value}\` is accessed before it is declared`
? `\`${effect.value.identifier.name.value}\``
: null;
const hoistedAccess = context.hoistedContextDeclarations.get(
effect.value.identifier.declarationId,
);
const diagnostic = CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: 'Cannot access variable before it is declared',
description: `${variable ?? 'This variable'} is accessed before it is declared, which prevents the earlier access from updating when this value changes over time.`,
});
if (hoistedAccess != null && hoistedAccess.loc != effect.value.loc) {
applyEffect(
context,
state,
{
kind: 'MutateFrozen',
place: effect.value,
error: {
severity: ErrorSeverity.InvalidReact,
reason: `This variable is accessed before it is declared, which may prevent it from updating as the assigned value changes over time`,
description,
loc: hoistedAccess.loc,
suggestions: null,
},
},
initialized,
effects,
);
diagnostic.withDetail({
kind: 'error',
loc: hoistedAccess.loc,
message: `${variable ?? 'variable'} accessed before it is declared`,
});
}
diagnostic.withDetail({
kind: 'error',
loc: effect.value.loc,
message: `${variable ?? 'variable'} is declared here`,
});
applyEffect(
context,
@@ -1047,13 +1050,7 @@ function applyEffect(
{
kind: 'MutateFrozen',
place: effect.value,
error: {
severity: ErrorSeverity.InvalidReact,
reason: `This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time`,
description,
loc: effect.value.loc,
suggestions: null,
},
error: diagnostic,
},
initialized,
effects,
@@ -1064,11 +1061,11 @@ function applyEffect(
reason: value.reason,
context: new Set(),
});
const description =
const variable =
effect.value.identifier.name !== null &&
effect.value.identifier.name.kind === 'named'
? `Found mutation of \`${effect.value.identifier.name.value}\``
: null;
? `\`${effect.value.identifier.name.value}\``
: 'value';
applyEffect(
context,
state,
@@ -1078,13 +1075,15 @@ function applyEffect(
? 'MutateFrozen'
: 'MutateGlobal',
place: effect.value,
error: {
error: CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
reason,
description,
category: 'This value cannot be modified',
description: `${reason}.`,
}).withDetail({
kind: 'error',
loc: effect.value.loc,
suggestions: null,
},
message: `${variable} cannot be modified`,
}),
},
initialized,
effects,
@@ -1325,7 +1324,7 @@ class InferenceState {
return 'mutate-global';
}
case ValueKind.MaybeFrozen: {
return 'none';
return 'mutate-frozen';
}
default: {
assertExhaustive(kind, `Unexpected kind ${kind}`);
@@ -1864,19 +1863,34 @@ function computeSignatureForInstruction(
break;
}
case 'Destructure': {
for (const patternLValue of eachInstructionValueLValue(value)) {
if (isPrimitiveType(patternLValue.identifier)) {
for (const patternItem of eachPatternItem(value.lvalue.pattern)) {
const place =
patternItem.kind === 'Identifier' ? patternItem : patternItem.place;
if (isPrimitiveType(place.identifier)) {
effects.push({
kind: 'Create',
into: patternLValue,
into: place,
value: ValueKind.Primitive,
reason: ValueReason.Other,
});
} else {
} else if (patternItem.kind === 'Identifier') {
effects.push({
kind: 'CreateFrom',
from: value.value,
into: patternLValue,
into: place,
});
} else {
// Spread creates a new object/array that captures from the RValue
effects.push({
kind: 'Create',
into: place,
reason: ValueReason.Other,
value: ValueKind.Mutable,
});
effects.push({
kind: 'Capture',
from: value.value,
into: place,
});
}
}
@@ -1988,16 +2002,20 @@ function computeSignatureForInstruction(
break;
}
case 'StoreGlobal': {
const variable = `\`${value.name}\``;
effects.push({
kind: 'MutateGlobal',
place: value.value,
error: {
reason:
'Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)',
loc: instr.loc,
suggestions: null,
error: CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
},
category:
'Cannot reassign variables declared outside of the component/hook',
description: `Variable ${variable} is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)`,
}).withDetail({
kind: 'error',
loc: instr.loc,
message: `${variable} cannot be reassigned`,
}),
});
effects.push({kind: 'Assign', from: value.value, into: lvalue});
break;
@@ -2087,17 +2105,19 @@ function computeEffectsForLegacySignature(
effects.push({
kind: 'Impure',
place: receiver,
error: {
reason:
'Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
description:
signature.canonicalName != null
? `\`${signature.canonicalName}\` is an impure function whose results may change on every call`
: null,
error: CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: '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)',
}).withDetail({
kind: 'error',
loc,
suggestions: null,
},
message: 'Cannot call impure function',
}),
});
}
const stores: Array<Place> = [];
@@ -2357,6 +2377,7 @@ function computeEffectsForSignature(
// Apply substitutions
for (const effect of signature.effects) {
switch (effect.kind) {
case 'MaybeAlias':
case 'Assign':
case 'ImmutableCapture':
case 'Alias':

View File

@@ -160,6 +160,8 @@ export function inferMutationAliasingRanges(
state.assign(index++, effect.from, effect.into);
} else if (effect.kind === 'Alias') {
state.assign(index++, effect.from, effect.into);
} else if (effect.kind === 'MaybeAlias') {
state.maybeAlias(index++, effect.from, effect.into);
} else if (effect.kind === 'Capture') {
state.capture(index++, effect.from, effect.into);
} else if (
@@ -195,7 +197,7 @@ export function inferMutationAliasingRanges(
effect.kind === 'MutateGlobal' ||
effect.kind === 'Impure'
) {
errors.push(effect.error);
errors.pushDiagnostic(effect.error);
functionEffects.push(effect);
} else if (effect.kind === 'Render') {
renders.push({index: index++, place: effect.place});
@@ -247,6 +249,7 @@ export function inferMutationAliasingRanges(
}
for (const param of [...fn.context, ...fn.params]) {
const place = param.kind === 'Identifier' ? param : param.place;
const node = state.nodes.get(place.identifier);
if (node == null) {
continue;
@@ -346,7 +349,8 @@ export function inferMutationAliasingRanges(
case 'Assign':
case 'Alias':
case 'Capture':
case 'CreateFrom': {
case 'CreateFrom':
case 'MaybeAlias': {
const isMutatedOrReassigned =
effect.into.identifier.mutableRange.end > instr.id;
if (isMutatedOrReassigned) {
@@ -549,7 +553,7 @@ function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
case 'Impure':
case 'MutateFrozen':
case 'MutateGlobal': {
errors.push(effect.error);
errors.pushDiagnostic(effect.error);
break;
}
}
@@ -567,7 +571,12 @@ type Node = {
createdFrom: Map<Identifier, number>;
captures: Map<Identifier, number>;
aliases: Map<Identifier, number>;
edges: Array<{index: number; node: Identifier; kind: 'capture' | 'alias'}>;
maybeAliases: Map<Identifier, number>;
edges: Array<{
index: number;
node: Identifier;
kind: 'capture' | 'alias' | 'maybeAlias';
}>;
transitive: {kind: MutationKind; loc: SourceLocation} | null;
local: {kind: MutationKind; loc: SourceLocation} | null;
lastMutated: number;
@@ -585,6 +594,7 @@ class AliasingState {
createdFrom: new Map(),
captures: new Map(),
aliases: new Map(),
maybeAliases: new Map(),
edges: [],
transitive: null,
local: null,
@@ -630,6 +640,18 @@ class AliasingState {
}
}
maybeAlias(index: number, from: Place, into: Place): void {
const fromNode = this.nodes.get(from.identifier);
const toNode = this.nodes.get(into.identifier);
if (fromNode == null || toNode == null) {
return;
}
fromNode.edges.push({index, node: into.identifier, kind: 'maybeAlias'});
if (!toNode.maybeAliases.has(from.identifier)) {
toNode.maybeAliases.set(from.identifier, index);
}
}
render(index: number, start: Identifier, errors: CompilerError): void {
const seen = new Set<Identifier>();
const queue: Array<Identifier> = [start];
@@ -673,22 +695,24 @@ class AliasingState {
// Null is used for simulated mutations
end: InstructionId | null,
transitive: boolean,
kind: MutationKind,
startKind: MutationKind,
loc: SourceLocation,
errors: CompilerError,
): void {
const seen = new Set<Identifier>();
const seen = new Map<Identifier, MutationKind>();
const queue: Array<{
place: Identifier;
transitive: boolean;
direction: 'backwards' | 'forwards';
}> = [{place: start, transitive, direction: 'backwards'}];
kind: MutationKind;
}> = [{place: start, transitive, direction: 'backwards', kind: startKind}];
while (queue.length !== 0) {
const {place: current, transitive, direction} = queue.pop()!;
if (seen.has(current)) {
const {place: current, transitive, direction, kind} = queue.pop()!;
const previousKind = seen.get(current);
if (previousKind != null && previousKind >= kind) {
continue;
}
seen.add(current);
seen.set(current, kind);
const node = this.nodes.get(current);
if (node == null) {
continue;
@@ -724,13 +748,18 @@ class AliasingState {
if (edge.index >= index) {
break;
}
queue.push({place: edge.node, transitive, direction: 'forwards'});
queue.push({place: edge.node, transitive, direction: 'forwards', kind});
}
for (const [alias, when] of node.createdFrom) {
if (when >= index) {
continue;
}
queue.push({place: alias, transitive: true, direction: 'backwards'});
queue.push({
place: alias,
transitive: true,
direction: 'backwards',
kind,
});
}
if (direction === 'backwards' || node.value.kind !== 'Phi') {
/**
@@ -747,7 +776,25 @@ class AliasingState {
if (when >= index) {
continue;
}
queue.push({place: alias, transitive, direction: 'backwards'});
queue.push({place: alias, transitive, direction: 'backwards', kind});
}
/**
* MaybeAlias indicates potential data flow from unknown function calls,
* so we downgrade mutations through these aliases to consider them
* conditional. This means we'll consider them for mutation *range*
* purposes but not report validation errors for mutations, since
* we aren't sure that the `from` value could actually be aliased.
*/
for (const [alias, when] of node.maybeAliases) {
if (when >= index) {
continue;
}
queue.push({
place: alias,
transitive,
direction: 'backwards',
kind: MutationKind.Conditional,
});
}
}
/**
@@ -758,7 +805,12 @@ class AliasingState {
if (when >= index) {
continue;
}
queue.push({place: capture, transitive, direction: 'backwards'});
queue.push({
place: capture,
transitive,
direction: 'backwards',
kind,
});
}
}
}

View File

@@ -21,7 +21,6 @@ import {
isStableType,
isStableTypeContainer,
isUseOperator,
isUseRefType,
} from '../HIR';
import {PostDominator} from '../HIR/Dominator';
import {
@@ -70,13 +69,6 @@ class StableSidemap {
isStable: false,
});
}
} else if (
this.env.config.enableTreatRefLikeIdentifiersAsRefs &&
isUseRefType(lvalue.identifier)
) {
this.map.set(lvalue.identifier.id, {
isStable: true,
});
}
break;
}

View File

@@ -153,6 +153,10 @@ This is somewhat the inverse of `Capture`. The `CreateFrom` effect describes tha
Describes immutable data flow from one value to another. This is not currently used for anything, but is intended to eventually power a more sophisticated escape analysis.
### MaybeAlias
Describes potential data flow that the compiler knows may occur behind a function call, but cannot be sure about. For example, `foo(x)` _may_ be the identity function and return `x`, or `cond(a, b, c)` may conditionally return `b` or `c` depending on the value of `a`, but those functions could just as easily return new mutable values and not capture any information from their arguments. MaybeAlias represents that we have to consider the potential for data flow when deciding mutable ranges, but should be conservative about reporting errors. For example, `foo(someFrozenValue).property = true` should not error since we don't know for certain that foo returns its input.
### State-Changing Effects
The following effects describe state changes to specific values, not data flow. In many cases, JavaScript semantics will involve a combination of both data-flow effects *and* state-change effects. For example, `object.property = value` has data flow (`Capture object <- value`) and mutation (`Mutate object`).
@@ -347,6 +351,17 @@ a.b = b; // capture
mutate(a); // can transitively mutate b
```
### MaybeAlias makes mutation conditional
Because we don't know for certain that the aliasing occurs, we consider the mutation conditional against the source.
```
MaybeAlias a <- b
Mutate a
=>
MutateConditional b
```
### Freeze Does Not Freeze the Value
Freeze does not freeze the value itself:

View File

@@ -237,6 +237,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
terminal: {
id: makeInstructionId(0),
kind: 'return',
returnVariant: 'Explicit',
loc: GeneratedSource,
value: arrayInstr.lvalue,
effects: null,

View File

@@ -352,6 +352,7 @@ function emitOutlinedFn(
terminal: {
id: makeInstructionId(0),
kind: 'return',
returnVariant: 'Explicit',
loc: GeneratedSource,
value: instructions.at(-1)!.lvalue,
effects: null,

View File

@@ -14,6 +14,7 @@ import {
Identifier,
IdentifierId,
Instruction,
InstructionKind,
makePropertyLiteral,
makeType,
PropType,
@@ -194,12 +195,29 @@ function* generateInstructionTypes(
break;
}
// We intentionally do not infer types for context variables
// We intentionally do not infer types for most context variables
case 'DeclareContext':
case 'StoreContext':
case 'LoadContext': {
break;
}
case 'StoreContext': {
/**
* The caveat is StoreContext const, where we know the value is
* assigned once such that everywhere the value is accessed, it
* must have the same type from the rvalue.
*
* A concrete example where this is useful is `const ref = useRef()`
* where the ref is referenced before its declaration in a function
* expression, causing it to be converted to a const context variable.
*/
if (value.lvalue.kind === InstructionKind.Const) {
yield equation(
value.lvalue.place.identifier.type,
value.value.identifier.type,
);
}
break;
}
case 'StoreLocal': {
if (env.config.enableUseTypeAnnotations) {
@@ -360,6 +378,12 @@ function* generateInstructionTypes(
value: makePropertyLiteral(propertyName),
},
});
} else if (item.kind === 'Spread') {
// Array pattern spread always creates an array
yield equation(item.place.identifier.type, {
kind: 'Object',
shapeId: BuiltInArrayId,
});
} else {
break;
}
@@ -427,6 +451,18 @@ function* generateInstructionTypes(
case 'JsxExpression':
case 'JsxFragment': {
if (env.config.enableTreatRefLikeIdentifiersAsRefs) {
if (value.kind === 'JsxExpression') {
for (const prop of value.props) {
if (prop.kind === 'JsxAttribute' && prop.name === 'ref') {
yield equation(prop.place.identifier.type, {
kind: 'Object',
shapeId: BuiltInUseRefId,
});
}
}
}
}
yield equation(left, {kind: 'Object', shapeId: BuiltInJsxId});
break;
}
@@ -442,7 +478,36 @@ function* generateInstructionTypes(
yield equation(left, returnType);
break;
}
case 'PropertyStore':
case 'PropertyStore': {
/**
* Infer types based on assignments to known object properties
* This is important for refs, where assignment to `<maybeRef>.current`
* can help us infer that an object itself is a ref
*/
yield equation(
/**
* Our property type declarations are best-effort and we haven't tested
* using them to drive inference of rvalues from lvalues. We want to emit
* a Property type in order to infer refs from `.current` accesses, but
* stay conservative by not otherwise inferring anything about rvalues.
* So we use a dummy type here.
*
* TODO: consider using the rvalue type here
*/
makeType(),
// unify() only handles properties in the second position
{
kind: 'Property',
objectType: value.object.identifier.type,
objectName: getName(names, value.object.identifier.id),
propertyName: {
kind: 'literal',
value: value.property,
},
},
);
break;
}
case 'DeclareLocal':
case 'RegExpLiteral':
case 'MetaProperty':

View File

@@ -113,8 +113,13 @@ function* splitPragma(
*/
function parseConfigPragmaEnvironmentForTest(
pragma: string,
defaultConfig: PartialEnvironmentConfig,
): EnvironmentConfig {
const maybeConfig: Partial<Record<keyof EnvironmentConfig, unknown>> = {};
// throw early if the defaults are invalid
EnvironmentConfigSchema.parse(defaultConfig);
const maybeConfig: Partial<Record<keyof EnvironmentConfig, unknown>> =
defaultConfig;
for (const {key, value: val} of splitPragma(pragma)) {
if (!hasOwnProperty(EnvironmentConfigSchema.shape, key)) {
@@ -174,9 +179,13 @@ export function parseConfigPragmaForTests(
pragma: string,
defaults: {
compilationMode: CompilationMode;
environment?: PartialEnvironmentConfig;
},
): PluginOptions {
const environment = parseConfigPragmaEnvironmentForTest(pragma);
const environment = parseConfigPragmaEnvironmentForTest(
pragma,
defaults.environment ?? {},
);
const options: Record<keyof PluginOptions, unknown> = {
...defaultOptions,
panicThreshold: 'all_errors',

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, Effect} from '..';
import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..';
import {HIRFunction, IdentifierId, Place} from '../HIR';
import {
eachInstructionLValue,
@@ -28,16 +28,24 @@ export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
false,
);
if (reassignment !== null) {
CompilerError.throwInvalidReact({
reason:
'Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead',
description:
reassignment.identifier.name !== null &&
reassignment.identifier.name.kind === 'named'
? `Variable \`${reassignment.identifier.name.value}\` cannot be reassigned after render`
: '',
loc: reassignment.loc,
});
const errors = new CompilerError();
const variable =
reassignment.identifier.name != null &&
reassignment.identifier.name.kind === 'named'
? `\`${reassignment.identifier.name.value}\``
: 'variable';
errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: 'Cannot reassign variable after render completes',
description: `Reassigning ${variable} after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.`,
}).withDetail({
kind: 'error',
loc: reassignment.loc,
message: `Cannot reassign ${variable} after render completes`,
}),
);
throw errors;
}
}
@@ -75,16 +83,25 @@ function getContextReassignment(
// if the function or its depends reassign, propagate that fact on the lvalue
if (reassignment !== null) {
if (isAsync || value.loweredFunc.func.async) {
CompilerError.throwInvalidReact({
reason:
'Reassigning a variable in an async function can cause inconsistent behavior on subsequent renders. Consider using state instead',
description:
reassignment.identifier.name !== null &&
reassignment.identifier.name.kind === 'named'
? `Variable \`${reassignment.identifier.name.value}\` cannot be reassigned after render`
: '',
loc: reassignment.loc,
});
const errors = new CompilerError();
const variable =
reassignment.identifier.name !== null &&
reassignment.identifier.name.kind === 'named'
? `\`${reassignment.identifier.name.value}\``
: 'variable';
errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: 'Cannot reassign variable in async function',
description:
'Reassigning a variable in an async function can cause inconsistent behavior on subsequent renders. Consider using state instead',
}).withDetail({
kind: 'error',
loc: reassignment.loc,
message: `Cannot reassign ${variable}`,
}),
);
throw errors;
}
reassigningFunctions.set(lvalue.identifier.id, reassignment);
}

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, Effect, ErrorSeverity} from '..';
import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..';
import {
FunctionEffect,
HIRFunction,
@@ -57,16 +57,30 @@ export function validateNoFreezingKnownMutableFunctions(
if (operand.effect === Effect.Freeze) {
const effect = contextMutationEffects.get(operand.identifier.id);
if (effect != null) {
errors.push({
reason: `This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead`,
loc: operand.loc,
severity: ErrorSeverity.InvalidReact,
});
errors.push({
reason: `The function modifies a local variable here`,
loc: effect.loc,
severity: ErrorSeverity.InvalidReact,
});
const place = [...effect.places][0];
const variable =
place != null &&
place.identifier.name != null &&
place.identifier.name.kind === 'named'
? `\`${place.identifier.name.value}\``
: 'a local variable';
errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: 'Cannot modify local variables after render completes',
description: `This argument is a function which may reassign or mutate ${variable} after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.`,
})
.withDetail({
kind: 'error',
loc: operand.loc,
message: `This function may (indirectly) reassign or modify ${variable} after render`,
})
.withDetail({
kind: 'error',
loc: effect.loc,
message: `This modifies ${variable}`,
}),
);
}
}
}

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, ErrorSeverity} from '..';
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
import {HIRFunction} from '../HIR';
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
import {Result} from '../Utils/Result';
@@ -34,17 +34,22 @@ export function validateNoImpureFunctionsInRender(
callee.identifier.type,
);
if (signature != null && signature.impure === true) {
errors.push({
reason:
'Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
description:
signature.canonicalName != null
? `\`${signature.canonicalName}\` is an impure function whose results may change on every call`
: null,
severity: ErrorSeverity.InvalidReact,
loc: callee.loc,
suggestions: null,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: '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)',
severity: ErrorSeverity.InvalidReact,
suggestions: null,
}).withDetail({
kind: 'error',
loc: callee.loc,
message: 'Cannot call impure function',
}),
);
}
}
}

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, ErrorSeverity} from '..';
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
import {BlockId, HIRFunction} from '../HIR';
import {Result} from '../Utils/Result';
import {retainWhere} from '../Utils/utils';
@@ -34,11 +34,17 @@ export function validateNoJSXInTryStatement(
switch (value.kind) {
case 'JsxExpression':
case 'JsxFragment': {
errors.push({
severity: ErrorSeverity.InvalidReact,
reason: `Unexpected JSX element within a try statement. To catch errors in rendering a given component, wrap that component in an error boundary. (https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)`,
loc: value.loc,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: 'Avoid constructing JSX within try/catch',
description: `React does not immediately render components when JSX is rendered, so any errors from this component will not be caught by the try/catch. To catch errors in rendering a given component, wrap that component in an error boundary. (https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)`,
}).withDetail({
kind: 'error',
loc: value.loc,
message: 'Avoid constructing JSX within try/catch',
}),
);
break;
}
}

View File

@@ -5,7 +5,11 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, ErrorSeverity} from '../CompilerError';
import {
CompilerDiagnostic,
CompilerError,
ErrorSeverity,
} from '../CompilerError';
import {
BlockId,
HIRFunction,
@@ -23,6 +27,7 @@ import {
eachTerminalOperand,
} from '../HIR/visitors';
import {Err, Ok, Result} from '../Utils/Result';
import {retainWhere} from '../Utils/utils';
/**
* Validates that a function does not access a ref value during render. This includes a partial check
@@ -75,8 +80,18 @@ type RefAccessRefType =
type RefFnType = {readRefEffect: boolean; returnType: RefAccessType};
class Env extends Map<IdentifierId, RefAccessType> {
class Env {
#changed = false;
#data: Map<IdentifierId, RefAccessType> = new Map();
#temporaries: Map<IdentifierId, Place> = new Map();
lookup(place: Place): Place {
return this.#temporaries.get(place.identifier.id) ?? place;
}
define(place: Place, value: Place): void {
this.#temporaries.set(place.identifier.id, value);
}
resetChanged(): void {
this.#changed = false;
@@ -86,8 +101,14 @@ class Env extends Map<IdentifierId, RefAccessType> {
return this.#changed;
}
override set(key: IdentifierId, value: RefAccessType): this {
const cur = this.get(key);
get(key: IdentifierId): RefAccessType | undefined {
const operandId = this.#temporaries.get(key)?.identifier.id ?? key;
return this.#data.get(operandId);
}
set(key: IdentifierId, value: RefAccessType): this {
const operandId = this.#temporaries.get(key)?.identifier.id ?? key;
const cur = this.#data.get(operandId);
const widenedValue = joinRefAccessTypes(value, cur ?? {kind: 'None'});
if (
!(cur == null && widenedValue.kind === 'None') &&
@@ -95,7 +116,8 @@ class Env extends Map<IdentifierId, RefAccessType> {
) {
this.#changed = true;
}
return super.set(key, widenedValue);
this.#data.set(operandId, widenedValue);
return this;
}
}
@@ -103,9 +125,48 @@ export function validateNoRefAccessInRender(
fn: HIRFunction,
): Result<void, CompilerError> {
const env = new Env();
collectTemporariesSidemap(fn, env);
return validateNoRefAccessInRenderImpl(fn, env).map(_ => undefined);
}
function collectTemporariesSidemap(fn: HIRFunction, env: Env): void {
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {lvalue, value} = instr;
switch (value.kind) {
case 'LoadLocal': {
const temp = env.lookup(value.place);
if (temp != null) {
env.define(lvalue, temp);
}
break;
}
case 'StoreLocal': {
const temp = env.lookup(value.value);
if (temp != null) {
env.define(lvalue, temp);
env.define(value.lvalue.place, temp);
}
break;
}
case 'PropertyLoad': {
if (
isUseRefType(value.object.identifier) &&
value.property === 'current'
) {
continue;
}
const temp = env.lookup(value.object);
if (temp != null) {
env.define(lvalue, temp);
}
break;
}
}
}
}
}
function refTypeOfType(place: Place): RefAccessType {
if (isRefValueType(place.identifier)) {
return {kind: 'RefValue'};
@@ -258,12 +319,27 @@ function validateNoRefAccessInRenderImpl(
env.set(place.identifier.id, type);
}
const interpolatedAsJsx = new Set<IdentifierId>();
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {value} = instr;
if (value.kind === 'JsxExpression' || value.kind === 'JsxFragment') {
if (value.children != null) {
for (const child of value.children) {
interpolatedAsJsx.add(child.identifier.id);
}
}
}
}
}
for (let i = 0; (i == 0 || env.hasChanged()) && i < 10; i++) {
env.resetChanged();
returnValues = [];
const safeBlocks = new Map<BlockId, RefId>();
const safeBlocks: Array<{block: BlockId; ref: RefId}> = [];
const errors = new CompilerError();
for (const [, block] of fn.body.blocks) {
retainWhere(safeBlocks, entry => entry.block !== block.id);
for (const phi of block.phis) {
env.set(
phi.place.identifier.id,
@@ -385,28 +461,79 @@ function validateNoRefAccessInRenderImpl(
const hookKind = getHookKindForType(fn.env, callee.identifier.type);
let returnType: RefAccessType = {kind: 'None'};
const fnType = env.get(callee.identifier.id);
let didError = false;
if (fnType?.kind === 'Structure' && fnType.fn !== null) {
returnType = fnType.fn.returnType;
if (fnType.fn.readRefEffect) {
errors.push({
severity: ErrorSeverity.InvalidReact,
reason:
'This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)',
loc: callee.loc,
description:
callee.identifier.name !== null &&
callee.identifier.name.kind === 'named'
? `Function \`${callee.identifier.name.value}\` accesses a ref`
: null,
suggestions: null,
});
didError = true;
errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetail({
kind: 'error',
loc: callee.loc,
message: `This function accesses a ref value`,
}),
);
}
}
for (const operand of eachInstructionValueOperand(instr.value)) {
if (hookKind != null) {
validateNoDirectRefValueAccess(errors, operand, env);
} else {
validateNoRefAccess(errors, env, operand, operand.loc);
/*
* If we already reported an error on this instruction, don't report
* duplicate errors
*/
if (!didError) {
const isRefLValue = isUseRefType(instr.lvalue.identifier);
for (const operand of eachInstructionValueOperand(instr.value)) {
/**
* By default we check that function call operands are not refs,
* ref values, or functions that can access refs.
*/
if (
isRefLValue ||
(hookKind != null &&
hookKind !== 'useState' &&
hookKind !== 'useReducer')
) {
/**
* Special cases:
*
* 1. the lvalue is a ref
* In general passing a ref to a function may access that ref
* value during render, so we disallow it.
*
* The main exception is the "mergeRefs" pattern, ie a function
* that accepts multiple refs as arguments (or an array of refs)
* and returns a new, aggregated ref. If the lvalue is a ref,
* we assume that the user is doing this pattern and allow passing
* refs.
*
* Eg `const mergedRef = mergeRefs(ref1, ref2)`
*
* 2. calling hooks
*
* Hooks are independently checked to ensure they don't access refs
* during render.
*/
validateNoDirectRefValueAccess(errors, operand, env);
} else if (interpolatedAsJsx.has(instr.lvalue.identifier.id)) {
/**
* Special case: the lvalue is passed as a jsx child
*
* For example `<Foo>{renderHelper(ref)}</Foo>`. Here we have more
* context and infer that the ref is being passed to a component-like
* render function which attempts to obey the rules.
*/
validateNoRefValueAccess(errors, env, operand);
} else {
validateNoRefPassedToFunction(
errors,
env,
operand,
operand.loc,
);
}
}
}
env.set(instr.lvalue.identifier.id, returnType);
@@ -439,23 +566,39 @@ function validateNoRefAccessInRenderImpl(
case 'PropertyStore':
case 'ComputedDelete':
case 'ComputedStore': {
const safe = safeBlocks.get(block.id);
const target = env.get(instr.value.object.identifier.id);
let safe: (typeof safeBlocks)['0'] | null | undefined = null;
if (
instr.value.kind === 'PropertyStore' &&
safe != null &&
target?.kind === 'Ref' &&
target.refId === safe
target != null &&
target.kind === 'Ref'
) {
safeBlocks.delete(block.id);
} else {
validateNoRefAccess(errors, env, instr.value.object, instr.loc);
safe = safeBlocks.find(entry => entry.ref === target.refId);
}
for (const operand of eachInstructionValueOperand(instr.value)) {
if (operand === instr.value.object) {
continue;
if (safe != null) {
retainWhere(safeBlocks, entry => entry !== safe);
} else {
validateNoRefUpdate(errors, env, instr.value.object, instr.loc);
}
if (
instr.value.kind === 'ComputedDelete' ||
instr.value.kind === 'ComputedStore'
) {
validateNoRefValueAccess(errors, env, instr.value.property);
}
if (
instr.value.kind === 'ComputedStore' ||
instr.value.kind === 'PropertyStore'
) {
validateNoDirectRefValueAccess(errors, instr.value.value, env);
const type = env.get(instr.value.value.identifier.id);
if (type != null && type.kind === 'Structure') {
let objectType: RefAccessType = type;
if (target != null) {
objectType = joinRefAccessTypes(objectType, target);
}
env.set(instr.value.object.identifier.id, objectType);
}
validateNoRefValueAccess(errors, env, operand);
}
break;
}
@@ -535,8 +678,11 @@ function validateNoRefAccessInRenderImpl(
if (block.terminal.kind === 'if') {
const test = env.get(block.terminal.test.identifier.id);
if (test?.kind === 'Guard') {
safeBlocks.set(block.terminal.consequent, test.refId);
if (
test?.kind === 'Guard' &&
safeBlocks.find(entry => entry.ref === test.refId) == null
) {
safeBlocks.push({block: block.terminal.fallthrough, ref: test.refId});
}
}
@@ -583,18 +729,17 @@ function destructure(
function guardCheck(errors: CompilerError, operand: Place, env: Env): void {
if (env.get(operand.identifier.id)?.kind === 'Guard') {
errors.push({
severity: ErrorSeverity.InvalidReact,
reason:
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
loc: operand.loc,
description:
operand.identifier.name !== null &&
operand.identifier.name.kind === 'named'
? `Cannot access ref value \`${operand.identifier.name.value}\``
: null,
suggestions: null,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetail({
kind: 'error',
loc: operand.loc,
message: `Cannot access ref value during render`,
}),
);
}
}
@@ -608,22 +753,21 @@ function validateNoRefValueAccess(
type?.kind === 'RefValue' ||
(type?.kind === 'Structure' && type.fn?.readRefEffect)
) {
errors.push({
severity: ErrorSeverity.InvalidReact,
reason:
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
loc: (type.kind === 'RefValue' && type.loc) || operand.loc,
description:
operand.identifier.name !== null &&
operand.identifier.name.kind === 'named'
? `Cannot access ref value \`${operand.identifier.name.value}\``
: null,
suggestions: null,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetail({
kind: 'error',
loc: (type.kind === 'RefValue' && type.loc) || operand.loc,
message: `Cannot access ref value during render`,
}),
);
}
}
function validateNoRefAccess(
function validateNoRefPassedToFunction(
errors: CompilerError,
env: Env,
operand: Place,
@@ -635,18 +779,39 @@ function validateNoRefAccess(
type?.kind === 'RefValue' ||
(type?.kind === 'Structure' && type.fn?.readRefEffect)
) {
errors.push({
severity: ErrorSeverity.InvalidReact,
reason:
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
loc: (type.kind === 'RefValue' && type.loc) || loc,
description:
operand.identifier.name !== null &&
operand.identifier.name.kind === 'named'
? `Cannot access ref value \`${operand.identifier.name.value}\``
: null,
suggestions: null,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetail({
kind: 'error',
loc: (type.kind === 'RefValue' && type.loc) || loc,
message: `Passing a ref to a function may read its value during render`,
}),
);
}
}
function validateNoRefUpdate(
errors: CompilerError,
env: Env,
operand: Place,
loc: SourceLocation,
): void {
const type = destructure(env.get(operand.identifier.id));
if (type?.kind === 'Ref' || type?.kind === 'RefValue') {
errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetail({
kind: 'error',
loc: (type.kind === 'RefValue' && type.loc) || loc,
message: `Cannot update ref during render`,
}),
);
}
}
@@ -657,17 +822,22 @@ function validateNoDirectRefValueAccess(
): void {
const type = destructure(env.get(operand.identifier.id));
if (type?.kind === 'RefValue') {
errors.push({
severity: ErrorSeverity.InvalidReact,
reason:
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
loc: type.loc ?? operand.loc,
description:
operand.identifier.name !== null &&
operand.identifier.name.kind === 'named'
? `Cannot access ref value \`${operand.identifier.name.value}\``
: null,
suggestions: null,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetail({
kind: 'error',
loc: type.loc ?? operand.loc,
message: `Cannot access ref value during render`,
}),
);
}
}
const ERROR_DESCRIPTION =
'React refs are values that are not needed for rendering. Refs should only be accessed ' +
'outside of render, such as in event handlers or effects. ' +
'Accessing a ref value (the `current` property) during render can cause your component ' +
'not to update as expected (https://react.dev/reference/react/useRef)';

View File

@@ -5,7 +5,11 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, ErrorSeverity} from '../CompilerError';
import {
CompilerDiagnostic,
CompilerError,
ErrorSeverity,
} from '../CompilerError';
import {
HIRFunction,
IdentifierId,
@@ -90,14 +94,26 @@ export function validateNoSetStateInEffects(
if (arg !== undefined && arg.kind === 'Identifier') {
const setState = setStateFunctions.get(arg.identifier.id);
if (setState !== undefined) {
errors.push({
reason:
'Calling setState directly within a useEffect causes cascading renders and is not recommended. Consider alternatives to useEffect. (https://react.dev/learn/you-might-not-need-an-effect)',
description: null,
severity: ErrorSeverity.InvalidReact,
loc: setState.loc,
suggestions: null,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
category:
'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)',
severity: ErrorSeverity.InvalidReact,
suggestions: null,
}).withDetail({
kind: 'error',
loc: setState.loc,
message:
'Avoid calling setState() directly within an effect',
}),
);
}
}
}

View File

@@ -5,7 +5,11 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, ErrorSeverity} from '../CompilerError';
import {
CompilerDiagnostic,
CompilerError,
ErrorSeverity,
} from '../CompilerError';
import {HIRFunction, IdentifierId, isSetStateType} from '../HIR';
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
import {eachInstructionValueOperand} from '../HIR/visitors';
@@ -122,23 +126,35 @@ function validateNoSetStateInRenderImpl(
unconditionalSetStateFunctions.has(callee.identifier.id)
) {
if (activeManualMemoId !== null) {
errors.push({
reason:
'Calling setState from useMemo may trigger an infinite loop. (https://react.dev/reference/react/useState)',
description: null,
severity: ErrorSeverity.InvalidReact,
loc: callee.loc,
suggestions: null,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
category:
'Calling setState from useMemo may trigger an infinite loop',
description:
'Each time the memo callback is evaluated it will change state. This can cause a memoization dependency to change, running the memo function again and causing an infinite loop. Instead of setting state in useMemo(), prefer deriving the value during render. (https://react.dev/reference/react/useState)',
severity: ErrorSeverity.InvalidReact,
suggestions: null,
}).withDetail({
kind: 'error',
loc: callee.loc,
message: 'Found setState() within useMemo()',
}),
);
} else if (unconditionalBlocks.has(block.id)) {
errors.push({
reason:
'This is an unconditional set state during render, which will trigger an infinite loop. (https://react.dev/reference/react/useState)',
description: null,
severity: ErrorSeverity.InvalidReact,
loc: callee.loc,
suggestions: null,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
category:
'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)',
severity: ErrorSeverity.InvalidReact,
suggestions: null,
}).withDetail({
kind: 'error',
loc: callee.loc,
message: 'Found setState() within useMemo()',
}),
);
}
}
break;

View File

@@ -5,7 +5,11 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, ErrorSeverity} from '../CompilerError';
import {
CompilerDiagnostic,
CompilerError,
ErrorSeverity,
} from '../CompilerError';
import {
DeclarationId,
Effect,
@@ -275,27 +279,37 @@ function validateInferredDep(
errorDiagnostic = merge(errorDiagnostic ?? compareResult, compareResult);
}
}
errorState.push({
severity: ErrorSeverity.CannotPreserveMemoization,
reason:
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected',
description:
DEBUG ||
// If the dependency is a named variable then we can report it. Otherwise only print in debug mode
(dep.identifier.name != null && dep.identifier.name.kind === 'named')
? `The inferred dependency was \`${prettyPrintScopeDependency(
dep,
)}\`, but the source dependencies were [${validDepsInMemoBlock
.map(dep => printManualMemoDependency(dep, true))
.join(', ')}]. ${
errorDiagnostic
? getCompareDependencyResultDescription(errorDiagnostic)
: 'Inferred dependency not present in source'
}`
: null,
loc: memoLocation,
suggestions: null,
});
errorState.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.CannotPreserveMemoization,
category:
'Compilation skipped because existing memoization could not be preserved',
description: [
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. ',
'The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. ',
DEBUG ||
// If the dependency is a named variable then we can report it. Otherwise only print in debug mode
(dep.identifier.name != null && dep.identifier.name.kind === 'named')
? `The inferred dependency was \`${prettyPrintScopeDependency(
dep,
)}\`, but the source dependencies were [${validDepsInMemoBlock
.map(dep => printManualMemoDependency(dep, true))
.join(', ')}]. ${
errorDiagnostic
? getCompareDependencyResultDescription(errorDiagnostic)
: 'Inferred dependency not present in source'
}.`
: '',
]
.join('')
.trim(),
suggestions: null,
}).withDetail({
kind: 'error',
loc: memoLocation,
message: 'Could not preserve existing manual memoization',
}),
);
}
class Visitor extends ReactiveFunctionVisitor<VisitorState> {
@@ -519,14 +533,21 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
!this.scopes.has(identifier.scope.id) &&
!this.prunedScopes.has(identifier.scope.id)
) {
state.errors.push({
reason:
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly',
description: null,
severity: ErrorSeverity.CannotPreserveMemoization,
loc,
suggestions: null,
});
state.errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.CannotPreserveMemoization,
category:
'Compilation skipped because existing memoization could not be preserved',
description: [
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. ',
'This dependency may be mutated later, which could cause the value to change unexpectedly.',
].join(''),
}).withDetail({
kind: 'error',
loc,
message: 'This dependency may be modified later',
}),
);
}
}
}
@@ -560,16 +581,25 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
for (const identifier of decls) {
if (isUnmemoized(identifier, this.scopes)) {
state.errors.push({
reason:
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.',
description: DEBUG
? `${printIdentifier(identifier)} was not memoized`
: null,
severity: ErrorSeverity.CannotPreserveMemoization,
loc,
suggestions: null,
});
state.errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.CannotPreserveMemoization,
category:
'Compilation skipped because existing memoization could not be preserved',
description: [
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. ',
DEBUG
? `${printIdentifier(identifier)} was not memoized.`
: '',
]
.join('')
.trim(),
}).withDetail({
kind: 'error',
loc,
message: 'Could not preserve existing memoization',
}),
);
}
}
}

View File

@@ -5,7 +5,11 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, ErrorSeverity} from '../CompilerError';
import {
CompilerDiagnostic,
CompilerError,
ErrorSeverity,
} from '../CompilerError';
import {HIRFunction, IdentifierId, SourceLocation} from '../HIR';
import {Result} from '../Utils/Result';
@@ -59,20 +63,23 @@ export function validateStaticComponents(
value.tag.identifier.id,
);
if (location != null) {
error.push({
reason: `Components created during render will reset their state each time they are created. Declare components outside of render. `,
severity: ErrorSeverity.InvalidReact,
loc: value.tag.loc,
description: null,
suggestions: null,
});
error.push({
reason: `The component may be created during render`,
severity: ErrorSeverity.InvalidReact,
loc: location,
description: null,
suggestions: null,
});
error.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: 'Cannot create components during render',
description: `Components created during render will reset their state each time they are created. Declare components outside of render. `,
})
.withDetail({
kind: 'error',
loc: value.tag.loc,
message: 'This component is created during render',
})
.withDetail({
kind: 'error',
loc: location,
message: 'The component is created during render here',
}),
);
}
}
}

View File

@@ -5,7 +5,11 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, ErrorSeverity} from '..';
import {
CompilerDiagnostic,
CompilerError,
ErrorSeverity,
} from '../CompilerError';
import {FunctionExpression, HIRFunction, IdentifierId} from '../HIR';
import {Result} from '../Utils/Result';
@@ -63,24 +67,41 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
}
if (body.loweredFunc.func.params.length > 0) {
errors.push({
severity: ErrorSeverity.InvalidReact,
reason: 'useMemo callbacks may not accept any arguments',
description: null,
loc: body.loc,
suggestions: null,
});
const firstParam = body.loweredFunc.func.params[0];
const loc =
firstParam.kind === 'Identifier'
? firstParam.loc
: firstParam.place.loc;
errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: 'useMemo() callbacks may not accept parameters',
description:
'useMemo() callbacks are called by React to cache calculations across re-renders. They should not take parameters. Instead, directly reference the props, state, or local variables needed for the computation.',
suggestions: null,
}).withDetail({
kind: 'error',
loc,
message: 'Callbacks with parameters are not supported',
}),
);
}
if (body.loweredFunc.func.async || body.loweredFunc.func.generator) {
errors.push({
severity: ErrorSeverity.InvalidReact,
reason:
'useMemo callbacks may not be async or generator functions',
description: null,
loc: body.loc,
suggestions: null,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category:
'useMemo() callbacks may not be async or generator functions',
description:
'useMemo() callbacks are called once and must synchronously return a value.',
suggestions: null,
}).withDetail({
kind: 'error',
loc: body.loc,
message: 'Async and generator functions are not supported',
}),
);
}
break;

View File

@@ -58,7 +58,8 @@ it('logs failed compilation', () => {
expect(event.detail.severity).toEqual('InvalidReact');
//@ts-ignore
const {start, end, identifierName} = event.detail.loc as t.SourceLocation;
const {start, end, identifierName} =
event.detail.primaryLocation() as t.SourceLocation;
expect(start).toEqual({column: 28, index: 28, line: 1});
expect(end).toEqual({column: 33, index: 33, line: 1});
expect(identifierName).toEqual('props');

View File

@@ -20,7 +20,7 @@ describe('parseConfigPragma()', () => {
validateHooksUsage: 1,
} as any);
}).toThrowErrorMatchingInlineSnapshot(
`"InvalidConfig: Could not validate environment config. Update React Compiler config to fix the error. Validation error: Expected boolean, received number at "validateHooksUsage""`,
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: Expected boolean, received number at "validateHooksUsage"."`,
);
});
@@ -38,7 +38,7 @@ describe('parseConfigPragma()', () => {
],
} as any);
}).toThrowErrorMatchingInlineSnapshot(
`"InvalidConfig: Could not validate environment config. Update React Compiler config to fix the error. Validation error: autodepsIndex must be > 0 at "inferEffectDependencies[0].autodepsIndex""`,
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: autodepsIndex must be > 0 at "inferEffectDependencies[0].autodepsIndex"."`,
);
});

View File

@@ -0,0 +1,52 @@
## Input
```javascript
import {useRef} from 'react';
import {Stringify} from 'shared-runtime';
function Component(props) {
const ref = useRef(props.value);
const object = {};
object.foo = () => ref.current;
return <Stringify object={object} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useRef } from "react";
import { Stringify } from "shared-runtime";
function Component(props) {
const $ = _c(1);
const ref = useRef(props.value);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const object = {};
object.foo = () => ref.current;
t0 = <Stringify object={object} shouldInvokeFns={true} />;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 42 }],
};
```
### Eval output
(kind: ok) <div>{"object":{"foo":{"kind":"Function","result":42}},"shouldInvokeFns":true}</div>

View File

@@ -0,0 +1,14 @@
import {useRef} from 'react';
import {Stringify} from 'shared-runtime';
function Component(props) {
const ref = useRef(props.value);
const object = {};
object.foo = () => ref.current;
return <Stringify object={object} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};

View File

@@ -0,0 +1,44 @@
## Input
```javascript
// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
import {useRef} from 'react';
function Component() {
const ref = useRef(null);
const ref2 = useRef(null);
const mergedRef = mergeRefs([ref], ref2);
return <Stringify ref={mergedRef} />;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
import { useRef } from "react";
function Component() {
const $ = _c(1);
const ref = useRef(null);
const ref2 = useRef(null);
const mergedRef = mergeRefs([ref], ref2);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <Stringify ref={mergedRef} />;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,11 @@
// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
import {useRef} from 'react';
function Component() {
const ref = useRef(null);
const ref2 = useRef(null);
const mergedRef = mergeRefs([ref], ref2);
return <Stringify ref={mergedRef} />;
}

View File

@@ -0,0 +1,45 @@
## Input
```javascript
// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
import {useRef} from 'react';
function Component(props) {
const ref = useRef(null);
return <Foo>{props.render({ref})}</Foo>;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
import { useRef } from "react";
function Component(props) {
const $ = _c(3);
const ref = useRef(null);
const T0 = Foo;
const t0 = props.render({ ref });
let t1;
if ($[0] !== T0 || $[1] !== t0) {
t1 = <T0>{t0}</T0>;
$[0] = T0;
$[1] = t0;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,9 @@
// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
import {useRef} from 'react';
function Component(props) {
const ref = useRef(null);
return <Foo>{props.render({ref})}</Foo>;
}

View File

@@ -0,0 +1,49 @@
## Input
```javascript
// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
import {useRef} from 'react';
function Component(props) {
const ref = useRef(null);
return <Foo>{props.render(ref)}</Foo>;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
import { useRef } from "react";
function Component(props) {
const $ = _c(4);
const ref = useRef(null);
let t0;
if ($[0] !== props.render) {
t0 = props.render(ref);
$[0] = props.render;
$[1] = t0;
} else {
t0 = $[1];
}
let t1;
if ($[2] !== t0) {
t1 = <Foo>{t0}</Foo>;
$[2] = t0;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,9 @@
// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
import {useRef} from 'react';
function Component(props) {
const ref = useRef(null);
return <Foo>{props.render(ref)}</Foo>;
}

View File

@@ -27,6 +27,7 @@ function Component() {
}
function Child({ref}) {
'use no memo';
// This violates the rules of React, so we access the ref in a child
// component
return ref.current;
@@ -100,8 +101,10 @@ function Component() {
return t6;
}
function Child(t0) {
const { ref } = t0;
function Child({ ref }) {
"use no memo";
// This violates the rules of React, so we access the ref in a child
// component
return ref.current;
}

View File

@@ -23,6 +23,7 @@ function Component() {
}
function Child({ref}) {
'use no memo';
// This violates the rules of React, so we access the ref in a child
// component
return ref.current;

View File

@@ -23,6 +23,7 @@ function Component() {
}
function Child({ref}) {
'use no memo';
// This violates the rules of React, so we access the ref in a child
// component
return ref.current;
@@ -86,8 +87,10 @@ function Component() {
return t5;
}
function Child(t0) {
const { ref } = t0;
function Child({ ref }) {
"use no memo";
// This violates the rules of React, so we access the ref in a child
// component
return ref.current;
}

View File

@@ -19,6 +19,7 @@ function Component() {
}
function Child({ref}) {
'use no memo';
// This violates the rules of React, so we access the ref in a child
// component
return ref.current;

View File

@@ -25,6 +25,7 @@ function Component() {
}
function Child({ref}) {
'use no memo';
// This violates the rules of React, so we access the ref in a child
// component
return ref.current;
@@ -83,8 +84,10 @@ function Component() {
}
function _temp() {}
function Child(t0) {
const { ref } = t0;
function Child({ ref }) {
"use no memo";
// This violates the rules of React, so we access the ref in a child
// component
return ref.current;
}

View File

@@ -21,6 +21,7 @@ function Component() {
}
function Child({ref}) {
'use no memo';
// This violates the rules of React, so we access the ref in a child
// component
return ref.current;

View File

@@ -0,0 +1,68 @@
## Input
```javascript
// @validateRefAccessDuringRender
import {useRef} from 'react';
function Component(props) {
const ref = useRef(null);
if (ref.current == null) {
// the logical means the ref write is in a different block
// from the if consequent. this tests that the "safe" blocks
// extend up to the if's fallthrough
ref.current = props.unknownKey ?? props.value;
}
return <Child ref={ref} />;
}
function Child({ref}) {
'use no memo';
return ref.current;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender
import { useRef } from "react";
function Component(props) {
const $ = _c(1);
const ref = useRef(null);
if (ref.current == null) {
ref.current = props.unknownKey ?? props.value;
}
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <Child ref={ref} />;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
function Child({ ref }) {
"use no memo";
return ref.current;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 42 }],
};
```
### Eval output
(kind: ok) 42

View File

@@ -0,0 +1,24 @@
// @validateRefAccessDuringRender
import {useRef} from 'react';
function Component(props) {
const ref = useRef(null);
if (ref.current == null) {
// the logical means the ref write is in a different block
// from the if consequent. this tests that the "safe" blocks
// extend up to the if's fallthrough
ref.current = props.unknownKey ?? props.value;
}
return <Child ref={ref} />;
}
function Child({ref}) {
'use no memo';
return ref.current;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};

View File

@@ -0,0 +1,104 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
function Component(props) {
// Should memoize independently
const x = useMemo(() => makeObject_Primitives(), []);
const rest = useMemo(() => {
const [_, ...rest] = props.array;
// Should be inferred as Array.proto.push which doesn't mutate input
rest.push(x);
return rest;
});
return (
<>
<ValidateMemoization inputs={[]} output={x} />
<ValidateMemoization inputs={[props.array]} output={rest} />
</>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{array: [0, 1, 2]}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
import { useMemo } from "react";
import { makeObject_Primitives, ValidateMemoization } from "shared-runtime";
function Component(props) {
const $ = _c(9);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = makeObject_Primitives();
$[0] = t0;
} else {
t0 = $[0];
}
const x = t0;
let rest;
if ($[1] !== props.array) {
[, ...rest] = props.array;
rest.push(x);
$[1] = props.array;
$[2] = rest;
} else {
rest = $[2];
}
const rest_0 = rest;
let t1;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <ValidateMemoization inputs={[]} output={x} />;
$[3] = t1;
} else {
t1 = $[3];
}
let t2;
if ($[4] !== props.array) {
t2 = [props.array];
$[4] = props.array;
$[5] = t2;
} else {
t2 = $[5];
}
let t3;
if ($[6] !== rest_0 || $[7] !== t2) {
t3 = (
<>
{t1}
<ValidateMemoization inputs={t2} output={rest_0} />
</>
);
$[6] = rest_0;
$[7] = t2;
$[8] = t3;
} else {
t3 = $[8];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ array: [0, 1, 2] }],
};
```
### Eval output
(kind: ok) <div>{"inputs":[],"output":{"a":0,"b":"value1","c":true}}</div><div>{"inputs":[[0,1,2]],"output":[1,2,{"a":0,"b":"value1","c":true}]}</div>

View File

@@ -0,0 +1,28 @@
// @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
function Component(props) {
// Should memoize independently
const x = useMemo(() => makeObject_Primitives(), []);
const rest = useMemo(() => {
const [_, ...rest] = props.array;
// Should be inferred as Array.proto.push which doesn't mutate input
rest.push(x);
return rest;
});
return (
<>
<ValidateMemoization inputs={[]} output={x} />
<ValidateMemoization inputs={[props.array]} output={rest} />
</>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{array: [0, 1, 2]}],
};

View File

@@ -15,10 +15,15 @@ function Component(props) {
## Error
```
Found 1 error:
Todo: (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern
error._todo.computed-lval-in-destructure.ts:3:9
1 | function Component(props) {
2 | const computedKey = props.key;
> 3 | const {[computedKey]: x} = props.val;
| ^^^^^^^^^^^^^^^^ Todo: (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern (3:3)
| ^^^^^^^^^^^^^^^^ (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern
4 |
5 | return x;
6 | }

View File

@@ -15,10 +15,17 @@ function Component() {
## Error
```
Found 1 error:
Error: Cannot reassign variables declared outside of the component/hook
Variable `someGlobal` is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)
error.assign-global-in-component-tag-function.ts:3:4
1 | function Component() {
2 | const Foo = () => {
> 3 | someGlobal = true;
| ^^^^^^^^^^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (3:3)
| ^^^^^^^^^^ `someGlobal` cannot be reassigned
4 | };
5 | return <Foo />;
6 | }

View File

@@ -18,10 +18,17 @@ function Component() {
## Error
```
Found 1 error:
Error: Cannot reassign variables declared outside of the component/hook
Variable `someGlobal` is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)
error.assign-global-in-jsx-children.ts:3:4
1 | function Component() {
2 | const foo = () => {
> 3 | someGlobal = true;
| ^^^^^^^^^^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (3:3)
| ^^^^^^^^^^ `someGlobal` cannot be reassigned
4 | };
5 | // Children are generally access/called during render, so
6 | // modifying a global in a children function is almost

View File

@@ -16,10 +16,15 @@ function Component() {
## Error
```
Found 1 error:
Error: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)
error.assign-global-in-jsx-spread-attribute.ts:4:4
2 | function Component() {
3 | const foo = () => {
> 4 | someGlobal = true;
| ^^^^^^^^^^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (4:4)
| ^^^^^^^^^^ Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)
5 | };
6 | return <div {...foo} />;
7 | }

View File

@@ -16,10 +16,17 @@ function Foo(props) {
## Error
```
Found 1 error:
Error: React Compiler has skipped optimizing this component because one or more React rule violations were reported by Flow
React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression `$FlowFixMe[react-rule-hook]`
error.bailout-on-flow-suppression.ts:4:2
2 |
3 | function Foo(props) {
> 4 | // $FlowFixMe[react-rule-hook]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: React Compiler has skipped optimizing this component because one or more React rule violations were reported by Flow. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. $FlowFixMe[react-rule-hook] (4:4)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Found React rule suppression
5 | useX();
6 | return null;
7 | }

View File

@@ -19,15 +19,33 @@ function lowercasecomponent() {
## Error
```
Found 2 errors:
Error: React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled
React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression `eslint-disable my-app/react-rule`
error.bailout-on-suppression-of-custom-rule.ts:3:0
1 | // @eslintSuppressionRules:["my-app","react-rule"]
2 |
> 3 | /* eslint-disable my-app/react-rule */
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. eslint-disable my-app/react-rule (3:3)
InvalidReact: React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. eslint-disable-next-line my-app/react-rule (7:7)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Found React rule suppression
4 | function lowercasecomponent() {
5 | 'use forget';
6 | const x = [];
Error: React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled
React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression `eslint-disable-next-line my-app/react-rule`
error.bailout-on-suppression-of-custom-rule.ts:7:2
5 | 'use forget';
6 | const x = [];
> 7 | // eslint-disable-next-line my-app/react-rule
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Found React rule suppression
8 | return <div>{x}</div>;
9 | }
10 | /* eslint-enable my-app/react-rule */
```

View File

@@ -36,6 +36,13 @@ function Component() {
## Error
```
Found 1 error:
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate a local variable after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.bug-old-inference-false-positive-ref-validation-in-use-effect.ts:20:12
18 | );
19 | const ref = useRef(null);
> 20 | useEffect(() => {
@@ -47,12 +54,19 @@ function Component() {
> 23 | }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 24 | }, [update]);
| ^^^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (20:24)
InvalidReact: The function modifies a local variable here (14:14)
| ^^^^ This function may (indirectly) reassign or modify a local variable after render
25 |
26 | return 'ok';
27 | }
error.bug-old-inference-false-positive-ref-validation-in-use-effect.ts:14:6
12 | ...partialParams,
13 | };
> 14 | nextParams.param = 'value';
| ^^^^^^^^^^ This modifies a local variable
15 | console.log(nextParams);
16 | },
17 | [params]
```

View File

@@ -14,10 +14,15 @@ function Component(props) {
## Error
```
Found 1 error:
Invariant: Const declaration cannot be referenced as an expression
error.call-args-destructuring-asignment-complex.ts:3:9
1 | function Component(props) {
2 | let x = makeObject();
> 3 | x.foo(([[x]] = makeObject()));
| ^^^^^ Invariant: Const declaration cannot be referenced as an expression (3:3)
| ^^^^^ Const declaration cannot be referenced as an expression
4 | return x;
5 | }
6 |

View File

@@ -14,10 +14,17 @@ function Foo() {
## Error
```
Found 1 error:
Error: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config
Bar may be a component..
error.capitalized-function-call-aliased.ts:4:2
2 | function Foo() {
3 | let x = Bar;
> 4 | x(); // ERROR
| ^^^ InvalidReact: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config. Bar may be a component. (4:4)
| ^^^ Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config
5 | }
6 |
```

View File

@@ -15,10 +15,17 @@ function Component() {
## Error
```
Found 1 error:
Error: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config
SomeFunc may be a component..
error.capitalized-function-call.ts:3:12
1 | // @validateNoCapitalizedCalls
2 | function Component() {
> 3 | const x = SomeFunc();
| ^^^^^^^^^^ InvalidReact: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config. SomeFunc may be a component. (3:3)
| ^^^^^^^^^^ Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config
4 |
5 | return x;
6 | }

View File

@@ -15,10 +15,17 @@ function Component() {
## Error
```
Found 1 error:
Error: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config
SomeFunc may be a component..
error.capitalized-method-call.ts:3:12
1 | // @validateNoCapitalizedCalls
2 | function Component() {
> 3 | const x = someGlobal.SomeFunc();
| ^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config. SomeFunc may be a component. (3:3)
| ^^^^^^^^^^^^^^^^^^^^^ Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config
4 |
5 | return x;
6 | }

View File

@@ -32,19 +32,33 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
Found 2 errors:
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef)
error.capture-ref-for-mutation.ts:12:13
10 | };
11 | const moveLeft = {
> 12 | handler: handleKey('left')(),
| ^^^^^^^^^^^^^^^^^ InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) (12:12)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (12:12)
InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) (15:15)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (15:15)
| ^^^^^^^^^^^^^^^^^ This function accesses a ref value
13 | };
14 | const moveRight = {
15 | handler: handleKey('right')(),
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef)
error.capture-ref-for-mutation.ts:15:13
13 | };
14 | const moveRight = {
> 15 | handler: handleKey('right')(),
| ^^^^^^^^^^^^^^^^^^ This function accesses a ref value
16 | };
17 | return [moveLeft, moveRight];
18 | }
```

View File

@@ -16,10 +16,15 @@ function Component(props) {
## Error
```
Found 1 error:
Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
error.conditional-hook-unknown-hook-react-namespace.ts:4:8
2 | let x = null;
3 | if (props.cond) {
> 4 | x = React.useNonexistentHook();
| ^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (4:4)
| ^^^^^^^^^^^^^^^^^^^^^^^^ Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
5 | }
6 | return x;
7 | }

View File

@@ -16,10 +16,15 @@ function Component(props) {
## Error
```
Found 1 error:
Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
error.conditional-hooks-as-method-call.ts:4:8
2 | let x = null;
3 | if (props.cond) {
> 4 | x = Foo.useFoo();
| ^^^^^^^^^^ InvalidReact: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (4:4)
| ^^^^^^^^^^ Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
5 | }
6 | return x;
7 | }

View File

@@ -28,10 +28,17 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
Found 1 error:
Error: Cannot reassign variable after render completes
Reassigning `x` after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.context-variable-only-chained-assign.ts:10:19
8 | };
9 | const fn2 = () => {
> 10 | const copy2 = (x = 4);
| ^ InvalidReact: Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `x` cannot be reassigned after render (10:10)
| ^ Cannot reassign `x` after render completes
11 | return [invoke(fn1), copy2, identity(copy2)];
12 | };
13 | return invoke(fn2);

View File

@@ -17,10 +17,17 @@ function Component() {
## Error
```
Found 1 error:
Error: Cannot reassign variable after render completes
Reassigning `x` after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.declare-reassign-variable-in-function-declaration.ts:4:4
2 | let x = null;
3 | function foo() {
> 4 | x = 9;
| ^ InvalidReact: Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `x` cannot be reassigned after render (4:4)
| ^ Cannot reassign `x` after render completes
5 | }
6 | const y = bar(foo);
7 | return <Child y={y} />;

View File

@@ -22,6 +22,11 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
Found 1 error:
Todo: (BuildHIR::node.lowerReorderableExpression) Expression type `ArrowFunctionExpression` cannot be safely reordered
error.default-param-accesses-local.ts:3:6
1 | function Component(
2 | x,
> 3 | y = () => {
@@ -29,7 +34,7 @@ export const FIXTURE_ENTRYPOINT = {
> 4 | return x;
| ^^^^^^^^^^^^^
> 5 | }
| ^^^^ Todo: (BuildHIR::node.lowerReorderableExpression) Expression type `ArrowFunctionExpression` cannot be safely reordered (3:5)
| ^^^^ (BuildHIR::node.lowerReorderableExpression) Expression type `ArrowFunctionExpression` cannot be safely reordered
6 | ) {
7 | return y();
8 | }

View File

@@ -19,10 +19,17 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
Found 1 error:
Todo: [hoisting] EnterSSA: Expected identifier to be defined before being used
Identifier x$1 is undefined.
error.dont-hoist-inline-reference.ts:3:2
1 | import {identity} from 'shared-runtime';
2 | function useInvalid() {
> 3 | const x = identity(x);
| ^^^^^^^^^^^^^^^^^^^^^^ Todo: [hoisting] EnterSSA: Expected identifier to be defined before being used. Identifier x$1 is undefined (3:3)
| ^^^^^^^^^^^^^^^^^^^^^^ [hoisting] EnterSSA: Expected identifier to be defined before being used
4 | return x;
5 | }
6 |

View File

@@ -15,10 +15,17 @@ function useFoo(props) {
## Error
```
Found 1 error:
Todo: Encountered conflicting global in generated program
Conflict from local binding __DEV__.
error.emit-freeze-conflicting-global.ts:3:8
1 | // @enableEmitFreeze @instrumentForget
2 | function useFoo(props) {
> 3 | const __DEV__ = 'conflicting global';
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Todo: Encountered conflicting global in generated program. Conflict from local binding __DEV__ (3:3)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Encountered conflicting global in generated program
4 | console.log(__DEV__);
5 | return foo(props.x);
6 | }

View File

@@ -15,10 +15,17 @@ function Component() {
## Error
```
Found 1 error:
Error: Cannot reassign variable after render completes
Reassigning `callback` after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.function-expression-references-variable-its-assigned-to.ts:3:4
1 | function Component() {
2 | let callback = () => {
> 3 | callback = null;
| ^^^^^^^^ InvalidReact: Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `callback` cannot be reassigned after render (3:3)
| ^^^^^^^^ Cannot reassign `callback` after render completes
4 | };
5 | return <div onClick={callback} />;
6 | }

View File

@@ -24,6 +24,13 @@ function Component(props) {
## Error
```
Found 1 error:
Memoization: Compilation skipped because existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source.
error.hoist-optional-member-expression-with-conditional-optional.ts:4:23
2 | import {ValidateMemoization} from 'shared-runtime';
3 | function Component(props) {
> 4 | const data = useMemo(() => {
@@ -41,7 +48,7 @@ function Component(props) {
> 10 | return x;
| ^^^^^^^^^^^^^^^^^
> 11 | }, [props?.items, props.cond]);
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source (4:11)
| ^^^^ Could not preserve existing manual memoization
12 | return (
13 | <ValidateMemoization inputs={[props?.items, props.cond]} output={data} />
14 | );

View File

@@ -24,6 +24,13 @@ function Component(props) {
## Error
```
Found 1 error:
Memoization: Compilation skipped because existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source.
error.hoist-optional-member-expression-with-conditional.ts:4:23
2 | import {ValidateMemoization} from 'shared-runtime';
3 | function Component(props) {
> 4 | const data = useMemo(() => {
@@ -41,7 +48,7 @@ function Component(props) {
> 10 | return x;
| ^^^^^^^^^^^^^^^^^
> 11 | }, [props?.items, props.cond]);
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source (4:11)
| ^^^^ Could not preserve existing manual memoization
12 | return (
13 | <ValidateMemoization inputs={[props?.items, props.cond]} output={data} />
14 | );

View File

@@ -24,6 +24,11 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
Found 1 error:
Todo: Support functions with unreachable code that may contain hoisted declarations
error.hoisting-simple-function-declaration.ts:6:2
4 | }
5 | return baz(); // OK: FuncDecls are HoistableDeclarations that have both declaration and value hoisting
> 6 | function baz() {
@@ -31,7 +36,7 @@ export const FIXTURE_ENTRYPOINT = {
> 7 | return bar();
| ^^^^^^^^^^^^^^^^^
> 8 | }
| ^^^^ Todo: Support functions with unreachable code that may contain hoisted declarations (6:8)
| ^^^^ Support functions with unreachable code that may contain hoisted declarations
9 | }
10 |
11 | export const FIXTURE_ENTRYPOINT = {

View File

@@ -29,10 +29,17 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
Found 1 error:
Error: This value cannot be modified
Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook.
error.hook-call-freezes-captured-identifier.ts:13:2
11 | });
12 |
> 13 | x.value += count;
| ^ InvalidReact: Updating a value previously passed as an argument to a hook is not allowed. Consider moving the mutation before calling the hook (13:13)
| ^ value cannot be modified
14 | return <Stringify x={x} cb={cb} />;
15 | }
16 |

View File

@@ -29,10 +29,17 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
Found 1 error:
Error: This value cannot be modified
Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook.
error.hook-call-freezes-captured-memberexpr.ts:13:2
11 | });
12 |
> 13 | x.value += count;
| ^ InvalidReact: Updating a value previously passed as an argument to a hook is not allowed. Consider moving the mutation before calling the hook (13:13)
| ^ value cannot be modified
14 | return <Stringify x={x} cb={cb} />;
15 | }
16 |

View File

@@ -23,15 +23,29 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
Found 2 errors:
Error: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values
error.hook-property-load-local-hook.ts:7:12
5 |
6 | function Foo() {
> 7 | let bar = useFoo.useBar;
| ^^^^^^^^^^^^^ InvalidReact: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values (7:7)
InvalidReact: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values (8:8)
| ^^^^^^^^^^^^^ Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values
8 | return bar();
9 | }
10 |
Error: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values
error.hook-property-load-local-hook.ts:8:9
6 | function Foo() {
7 | let bar = useFoo.useBar;
> 8 | return bar();
| ^^^ Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values
9 | }
10 |
11 | export const FIXTURE_ENTRYPOINT = {
```

View File

@@ -20,12 +20,30 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
Found 2 errors:
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef)
error.hook-ref-value.ts:5:23
3 | function Component(props) {
4 | const ref = useRef();
> 5 | useEffect(() => {}, [ref.current]);
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5)
| ^^^^^^^^^^^ Cannot access ref value during render
6 | }
7 |
8 | export const FIXTURE_ENTRYPOINT = {
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5)
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef)
error.hook-ref-value.ts:5:23
3 | function Component(props) {
4 | const ref = useRef();
> 5 | useEffect(() => {}, [ref.current]);
| ^^^^^^^^^^^ Cannot access ref value during render
6 | }
7 |
8 | export const FIXTURE_ENTRYPOINT = {

View File

@@ -15,13 +15,20 @@ function component(a, b) {
## Error
```
Found 1 error:
Error: useMemo() callbacks may not be async or generator functions
useMemo() callbacks are called once and must synchronously return a value.
error.invalid-ReactUseMemo-async-callback.ts:2:24
1 | function component(a, b) {
> 2 | let x = React.useMemo(async () => {
| ^^^^^^^^^^^^^
> 3 | await a;
| ^^^^^^^^^^^^
> 4 | }, []);
| ^^^^ InvalidReact: useMemo callbacks may not be async or generator functions (2:4)
| ^^^^ Async and generator functions are not supported
5 | return x;
6 | }
7 |

View File

@@ -15,10 +15,17 @@ function Component(props) {
## Error
```
Found 1 error:
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef)
error.invalid-access-ref-during-render.ts:4:16
2 | function Component(props) {
3 | const ref = useRef(null);
> 4 | const value = ref.current;
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4)
| ^^^^^^^^^^^ Cannot access ref value during render
5 | return value;
6 | }
7 |

View File

@@ -0,0 +1,45 @@
## Input
```javascript
import {useReducer, useRef} from 'react';
function Component(props) {
const ref = useRef(props.value);
const [state] = useReducer(
(state, action) => state + action,
0,
init => ref.current
);
return <Stringify state={state} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};
```
## Error
```
Found 1 error:
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef)
error.invalid-access-ref-in-reducer-init.ts:8:4
6 | (state, action) => state + action,
7 | 0,
> 8 | init => ref.current
| ^^^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render
9 | );
10 |
11 | return <Stringify state={state} />;
```

View File

@@ -0,0 +1,17 @@
import {useReducer, useRef} from 'react';
function Component(props) {
const ref = useRef(props.value);
const [state] = useReducer(
(state, action) => state + action,
0,
init => ref.current
);
return <Stringify state={state} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};

View File

@@ -0,0 +1,41 @@
## Input
```javascript
import {useReducer, useRef} from 'react';
function Component(props) {
const ref = useRef(props.value);
const [state] = useReducer(() => ref.current, null);
return <Stringify state={state} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};
```
## Error
```
Found 1 error:
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef)
error.invalid-access-ref-in-reducer.ts:5:29
3 | function Component(props) {
4 | const ref = useRef(props.value);
> 5 | const [state] = useReducer(() => ref.current, null);
| ^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render
6 |
7 | return <Stringify state={state} />;
8 | }
```

View File

@@ -0,0 +1,13 @@
import {useReducer, useRef} from 'react';
function Component(props) {
const ref = useRef(props.value);
const [state] = useReducer(() => ref.current, null);
return <Stringify state={state} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};

View File

@@ -0,0 +1,37 @@
## Input
```javascript
import {useRef} from 'react';
function Component() {
const ref = useRef(null);
const object = {};
object.foo = () => ref.current;
const refValue = object.foo();
return <div>{refValue}</div>;
}
```
## Error
```
Found 1 error:
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef)
error.invalid-access-ref-in-render-mutate-object-with-ref-function.ts:7:19
5 | const object = {};
6 | object.foo = () => ref.current;
> 7 | const refValue = object.foo();
| ^^^^^^^^^^ This function accesses a ref value
8 | return <div>{refValue}</div>;
9 | }
10 |
```

View File

@@ -0,0 +1,9 @@
import {useRef} from 'react';
function Component() {
const ref = useRef(null);
const object = {};
object.foo = () => ref.current;
const refValue = object.foo();
return <div>{refValue}</div>;
}

View File

@@ -0,0 +1,41 @@
## Input
```javascript
import {useRef, useState} from 'react';
function Component(props) {
const ref = useRef(props.value);
const [state] = useState(() => ref.current);
return <Stringify state={state} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};
```
## Error
```
Found 1 error:
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef)
error.invalid-access-ref-in-state-initializer.ts:5:27
3 | function Component(props) {
4 | const ref = useRef(props.value);
> 5 | const [state] = useState(() => ref.current);
| ^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render
6 |
7 | return <Stringify state={state} />;
8 | }
```

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