Compare commits

..

56 Commits

Author SHA1 Message Date
Mofei Zhang
8c0829bda4 [compiler] Aggregate error reporting, separate eslint rules
All error messages that are not invariants, todos, or config validation errors are now moved to a registry.  This helps us check that error messages and linked documentation is uniform and up to date. This also lets us link error codes to linter reporting categories and break the single `react-compiler` rule up into many smaller rules.


Changes to error reporting in React Compiler:
- instead of writing raw error message, add a new ErrorCode to `CompilerErrorCodes`
- create errors by constructing a `CompilerErrorDetailOptions` with the correct error code, or calling `CompilerErrorDetail.fromCode()` / `CompilerDiagnostic.fromCode()`.

Changes to linter:
- `react-compiler` lint rule is now broken up into many smaller lint rules, each with their own test cases.
- linting no longer fails early when a non-fatal error is find. Instead, we now log and move on to lint for more errors!

Todos:
- Codebases previously using the eslint plugin may already have `react-compiler` eslint suppressions. Since this PR removes this rule, these codebases may get new eslint warnings on already-suppressed ranges. Some options I can think of:
  - Force everyone to add the new suppressions
  - Release a script to rewrite previous suppressions to the new suppressions. Codebases that had incomplete suppressions / waited to update may benefit here, as this still surfaces errors on previously-unseen suppressions
  - Hard code logic in our eslint plugin to avoid reporting if we see a `eslint-disable-next-line react-compiler`. This doesn't feel ideal
- Align on eslint plugin rule names and error code mappings
  - I aggregated a few error codes here. For example, reassigning and modifying local variables after render now have the same error code + message. See changes fixtures for more examples
- More test cases for linter rules. Will add these once lint rules are more finalized
2025-08-11 13:48:58 -07:00
Sebastian "Sebbie" Silbermann
ac7820a99e Create fresh Offscreen instance when replaying (#34127) 2025-08-11 20:55:48 +02:00
Sebastian Markbåge
3c67bbe5f9 [DevTools] Track suspensey CSS on "suspended by" (#34166)
We need to track that Suspensey CSS (Host Resources) can contribute to
the loading state. We can pick up the start/end time from the
Performance Observer API since we know which resource was loaded.

If DOM nodes are not filtered there's a link to the `<link>` instance.
The `"awaited by"` stack is the callsite of the JSX creating the
`<link>`.

<img width="591" height="447" alt="Screenshot 2025-08-11 at 1 35 21 AM"
src="https://github.com/user-attachments/assets/63af0ca9-de8d-4c74-a797-af0a009b5d73"
/>

Inspecting the link itself:

<img width="592" height="344" alt="Screenshot 2025-08-11 at 1 31 43 AM"
src="https://github.com/user-attachments/assets/89603dbc-6721-4bbf-8b58-6010719b29e3"
/>

In this approach I only include it if the page currently matches the
media query. It might contribute in some other scenario but we're not
showing every possible state but every possible scenario that might
suspend if timing changes in the current state.
2025-08-11 12:28:32 -04:00
Sebastian Markbåge
2c9a42dfd7 [DevTools] If the await doesn't have a stack use the stack from use() if any (#34162)
Stacked on #34148.

This picks up the stack for the await from the `use()` Hook if one was
used to get this async info.

When you select a component that used hooks, we already collect this
information.

If you select a Suspense boundary, this lazily invokes the first
component that awaited this data to inspects its hooks and produce a
stack trace for the use().

When all we have for the name is "Promise" I also use the name of the
first callsite in the stack trace if there's more than one. Which in
practice will be the name of the custom Hook that called it. Ideally
we'd use source mapping and ignore listing for this but that would
require suspending the display. We could maybe make the SuspendedByRow
wrapped in a Suspense boundary for this case.

<img width="438" height="401" alt="Screenshot 2025-08-10 at 10 07 55 PM"
src="https://github.com/user-attachments/assets/2a68917d-c27b-4c00-84aa-0ceb51c4e541"
/>
2025-08-11 12:28:10 -04:00
Jan Kassens
f1e70b5e0a [easy] remove leftover reference to disableDefaultPropsExceptForClasses (#34169)
Noticed that I missed this in some earlier cleanup diff.

Test Plan:
grep for disableDefaultPropsExceptForClasses
2025-08-11 12:13:33 -04:00
Sebastian Markbåge
d587434c35 [DevTools] Pick up suspended by info from use() (#34148)
Similar to #34144 but for `use()`.

`use()` dependencies don't get added to the `fiber._debugInfo` set
because that just models the things blocking the children, and not the
Fiber component itself. This picks up any debug info from the thenable
state that we stashed onto `_debugThenableState` so that we know it used
`use()`.

<img width="593" height="425" alt="Screenshot 2025-08-09 at 4 03 40 PM"
src="https://github.com/user-attachments/assets/c7e06884-4efd-47fa-a76b-132935db6ddc"
/>

Without #34146 this doesn't pick up uninstrumented promises but after
it, it'll pick those up as well. An instrumented promise that doesn't
have anything in its debug info is not picked up. For example, if it
didn't depend on any I/O on the server.

This doesn't yet pick up the stack trace of the `use()` call. That
information is in the Hooks information but needs a follow up to extract
it.
2025-08-11 12:10:05 -04:00
Sebastian Markbåge
ca292f7a57 [DevTools] Don't show "awaited by" if there's nothing to show (#34163)
E.g. if the owner is null or the same as current component and no stack.
This happens for example when you return a plain Promise in the child
position and inspect the component it was returned in since there's no
hook stack and the owner is the same as the instance itself so there's
nothing new to link to.

Before:

<img width="267" height="99" alt="Screenshot 2025-08-10 at 10 28 32 PM"
src="https://github.com/user-attachments/assets/23341ab2-2888-457d-a1d1-128f3e0bd5ec"
/>

After:

<img width="253" height="91" alt="Screenshot 2025-08-10 at 10 29 04 PM"
src="https://github.com/user-attachments/assets/b33bb38b-891a-4f46-bc16-15604b033cdb"
/>
2025-08-11 11:48:09 -04:00
Sebastian Markbåge
62a634b972 [DebugTools] Use thenables from the _debugThenableState if available (#34161)
In the case where a Promise is not cached, then the thenable state might
contain an older version. This version is the one that was actually
observed by the committed render, so that's the version we'll want to
inspect.

We used to not store the thenable state but now we have it on
`_debugThenableState` in DEV.

<img width="593" height="359" alt="Screenshot 2025-08-10 at 8 26 04 PM"
src="https://github.com/user-attachments/assets/51ee53f3-a31a-4e3f-a4cf-bb20b6efe0cb"
/>
2025-08-11 11:46:27 -04:00
Sebastian Markbåge
53d07944df [Fiber] Assign implicit debug info to used thenables (#34146)
Similar to #34137 but for Promises.

This lets us pick up the debug info from a raw Promise as a child which
is not covered by `_debugThenables`. Currently ChildFiber doesn't stash
its thenables so we can't pick them up from devtools after the fact
without some debug info added to the parent.

It also lets us track some approximate start/end time of use():ed
promises based on the first time we saw this particular Promise.
2025-08-11 11:44:05 -04:00
Sebastian Markbåge
34ce3acafd [DevTools] Pick up suspended by info from React.lazy in type position (#34144)
Normally, we pick up debug info from instrumented Promise or React.Lazy
while we're reconciling in ReactChildFiber when they appear in the child
position. We add those to the `_debugInfo` of the Fiber.

However, we don't do that for for Lazy in the Component type position.
Instead, we have to pick up the debug info from it explicitly in
DevTools. Likely this is the info added by #34137. Older versions
wouldn't be covered by this particular mechanism but more generally from
throwing a Promise.


<img width="592" height="449" alt="Screenshot 2025-08-08 at 11 32 33 PM"
src="https://github.com/user-attachments/assets/87211c64-a7df-47b7-a784-5cdc7c5fae16"
/>
2025-08-11 11:42:59 -04:00
Sebastian Markbåge
6445b3154e [Fiber] Add additional debugInfo to React.lazy constructors in DEV (#34137)
This creates a debug info object for the React.lazy call when it's
called on the client. We have some additional information we can track
for these since they're created by React earlier.

We can track the stack trace where `React.lazy` was called to associate
it back to something useful. We can track the start time when we
initialized it for the first time and the end time when it resolves. The
name from the promise if available.

This data is currently only picked up in child position and not
component position. The component position is in a follow up.

<img width="592" height="451" alt="Screenshot 2025-08-08 at 2 49 33 PM"
src="https://github.com/user-attachments/assets/913d2629-6df5-40f6-b036-ae13631379b9"
/>

This begs for ignore listing in the front end since these stacks aren't
filtered on the server.
2025-08-11 11:42:23 -04:00
Sebastian Markbåge
ab5238d5a4 [DevTools] Show name prop of Suspense / Activity in the Components Tree view (#34135)
The name prop will be used in the Suspense tab to help identity a
boundary. Activity will also allow names. A custom component can be
identified by the name of the component but built-ins doesn't have that.

This PR adds it to the Components Tree View as well since otherwise you
only have the key to go on. Normally we don't add all the props to avoid
making this view too noisy but this is an exception along with key to
help identify a boundary quickly in the tree.

Unlike the SuspenseNode store, this wouldn't ever have a name inferred
by owner since that kind of context already exists in this view.

<img width="600" height="161" alt="Screenshot 2025-08-08 at 1 20 36 PM"
src="https://github.com/user-attachments/assets/fe50d624-887a-4b9d-9186-75f131f83195"
/>

I also made both the key and name prop searchable.

<img width="608" height="206" alt="Screenshot 2025-08-08 at 1 32 27 PM"
src="https://github.com/user-attachments/assets/d3502d9c-7614-45fc-b973-57f06dd9cddc"
/>
2025-08-11 11:41:46 -04:00
Sebastian Markbåge
7a934a16b8 [DevTools] Show Owner Stacks in "rendered by" View (#34130)
This shows the stack trace of the JSX at each level so now you can also
jump to the code location for the JSX callsite. The visual is similar to
the owner stacks with `createTask` except when you click the `<...>` you
jump to the Instance in the Components panel.

<img width="593" height="450" alt="Screenshot 2025-08-08 at 12 19 21 AM"
src="https://github.com/user-attachments/assets/dac35faf-9d99-46ce-8b41-7c6fe24625d2"
/>

I'm not sure it's really necessary to have all the JSX stacks of every
owner. We could just have it for the current component and then the rest
of the owners you could get to if you just click that owner instance.

As a bonus, I also use the JSX callsite as the fallback for the "View
Source" button. This is primarily useful for built-ins like `<div>` and
`<Suspense>` that don't have any implementation to jump to anyway. It's
useful to be able to jump to where a boundary was defined.
2025-08-11 11:41:30 -04:00
Sebastian Markbåge
59ef3c4baf [DevTools] Allow Introspection of React Elements and React.lazy (#34129)
With RSC it's common to get React.lazy objects in the children position.
This first formats them nicely.

Then it adds introspection support for both lazy and elements.

Unfortunately because of quirks with the hydration mechanism we have to
expose it under the name `_payload` instead of something direct. Also
because the name "type" is taken we can't expose the type field on an
element neither. That whole algorithm could use a rewrite.

<img width="422" height="137" alt="Screenshot 2025-08-07 at 11 37 03 PM"
src="https://github.com/user-attachments/assets/a6f65f58-dbc4-4b8f-928b-d7f629fc51b2"
/>

<img width="516" height="275" alt="Screenshot 2025-08-07 at 11 36 36 PM"
src="https://github.com/user-attachments/assets/650bafdb-a633-4d78-9487-a750a18074ce"
/>

For JSX an alternative or additional feature might be instead to jump to
the first Instance that was rendered using that JSX. We know that based
on the equality of the memoizedProps on the Fiber. It's just a matter of
whether we do that eagerly or more lazily when you click but you may not
have a match so would be nice to indicate that before you click.
2025-08-11 11:41:14 -04:00
Sebastian "Sebbie" Silbermann
72965f3615 [DevTools] Restore reconciling Suspense stack after fallback was reconciled (#34168) 2025-08-11 17:12:39 +02:00
Sebastian Markbåge
594fb5e9ab [DevTools] Always skip 1 frame (#34167)
Follow up to #34093.

There's an issue where the skipFrames argument isn't part of the cache
key so the other parsers that expect skipping one frame might skip zero
and show the internal `fakeJSXDEV` callsite. Ideally we should include
the skipFrames as part of the cache key but we can also always just skip
one.
2025-08-11 01:50:26 -04:00
Sebastian "Sebbie" Silbermann
98286cf8e3 [DevTools] Send suspense nodes to frontend store (#34070) 2025-08-10 10:12:20 +02:00
Sophie Alpert
cf6e502ed2 Hot reloading: Avoid stack overflow on wide trees (#34145)
Every sibling added to the stack here. Not sure this needs to be
recursive at all but certainly for siblings this can just be a loop.
2025-08-09 08:02:22 -07:00
Sebastian Markbåge
3958d5d84b [Flight] Copy the name field of a serialized function debug value (#34085)
This ensures that if the name is set manually after the declaration,
then we get that name when we log the value. For example Node.js
`Response` is declared as `_Response` and then later assigned a new
name.

We should probably really serialize all static enumerable properties but
"name" is non-enumerable so it's still a special case.
2025-08-07 10:55:01 -04:00
Sebastian Markbåge
738aebdbac [DevTools] Add Badge to Owners and sometimes stack traces (#34106)
Stacked on #34101.

This adds a badge to owners if they are different from the currently
selected component's environment.

<img width="590" height="566" alt="Screenshot 2025-08-04 at 5 15 02 PM"
src="https://github.com/user-attachments/assets/e898254f-1b4c-498e-8713-978d90545340"
/>

We also add one to the end of stack traces if the stack trace has a
different environment than the owner which can happen when you call a
function (without rendering a component) into a third party environment
but the owner component was in the first party.

One awkward thing is that Suspense boundaries are always in the client
environment so their Server Components are always badged.
2025-08-07 10:39:08 -04:00
Sebastian Markbåge
4c9c109cea [Fiber] Try to give a stack trace to every entry in the Scheduler Performance Track (#34123)
For "render" and "commit" phases we don't give any specific stack atm.
This tries to always provide something useful to say the cause of the
render.

For normal renders this will now show the same thing as the "Event" and
"Update" entries already showed. We stash the task that was used for
those and use them throughout the render and commit phases.

For Suspense (Retry lane) and Idle (Offscreen lane), we don't have any
updates. Instead for those there's a component that left work behind in
previous passes. For those I use the debugTask of the `<Suspense>` or
`<Activity>` boundary to indicate that this was the root of the render.

Similarly when an Action is invoked on a `<form action={...}>` component
using the built-in submit handler, there's no actionable stack in user
space that called it. So we use the stack of the JSX for the form
instead.
2025-08-07 10:26:30 -04:00
Ruslan Lesiutin
552a5dadcf [DevTools] fix: handle store mutations synchronously in TreeContext (#34119)
If there is a commit that removes the currently inspected (selected)
elements in the Components tree, we are going to kick off the transition
to re-render the Tree. The elements will be re-rendered with the
previous inspectedElementID, which was just removed and all consecutive
calls to store object with this id would produce errors, since this
element was just removed.

We should handle store mutations synchronously. Doesn't make sense to
start a transition in this case, because Elements depend on the
TreeState and could make calls to store in render function.

Before:
<img width="2286" height="1734" alt="Screenshot 2025-08-06 at 17 41 14"
src="https://github.com/user-attachments/assets/97d92220-3488-47b2-aa6b-70fa39345f6b"
/>


After:


https://github.com/user-attachments/assets/3da36aff-6987-4b76-b741-ca59f829f8e6
2025-08-07 14:05:56 +01:00
Joseph Savona
f468d37739 [compiler] remove use of inspect module (#34124) 2025-08-06 23:59:55 -07:00
Joseph Savona
c403a7c548 [compiler] Upstream experimental flow integration (#34121)
all credit on the Flood/ code goes to @mvitousek and @jbrown215, i'm
just the one upstreaming it
2025-08-06 15:58:07 -07:00
Sebastian Markbåge
fa212fc2b1 [DevTools] Measure the Rectangle of Suspense boundaries as we reconcile (#34090)
Stacked on #34089.

This measures the client rects of the direct children of Suspense
boundaries as we reconcile. This will be used by the Suspense tab to
visualize the boundaries given their outlines.

We could ask for this more lazily just in case we're currently looking
at the Suspense tab. We could also do something like monitor the sizes
using a ResizeObserver to cover when they change.

However, it should be pretty cheap to this in the reconciliation phase
since we're already mostly visiting these nodes on the way down. We have
also already done all the layouts at this point since it was part of the
commit phase and paint already. So we're just reading cached values in
this phase. We can also infer that things are expected to change when
parents or sibling changes. Similar technique as ViewTransitions.
2025-08-06 14:56:52 -04:00
Sebastian Markbåge
b080063331 [DevTools] Source Map Stack Traces such in await locations (#34094)
Stacked on #34093.

Instead of using the original `ReactStackTrace` that has the call sites
on the server, this parses the `Error` object which has the virtual call
sites on the client. We'll need this technique for things stack traces
suspending on the client anyway like `use()`.

We can then use these callsites to source map in the front end.

We currently don't source map function names but might be useful for
this use case as well as getting original component names from prod.

One thing this doesn't do yet is that it doesn't ignore list the stack
traces on the client using the source map's ignore list setting. It's
not super important since we expect to have already ignore listed on the
server but this will become important for client stack traces like
`use()`.
2025-08-06 13:45:06 -04:00
Sebastian Markbåge
66f09bd054 [DevTools] Sort "Suspended By" view by the start time (#34105)
or end time if they have the same start time.

<img width="517" height="411" alt="Screenshot 2025-08-04 at 4 00 23 PM"
src="https://github.com/user-attachments/assets/b99be67b-5727-4e24-98c0-ee064fb21e2f"
/>

They would typically appear in this order naturally but not always.
Especially in Suspense boundaries where the order can also be depended
on when the components are discovered.
2025-08-06 11:23:00 -04:00
Sebastian Markbåge
0825d019be [DevTools] Prefer I/O stack and show await stack after only if it's a different owner (#34101)
Stacked on #34094.

This shows the I/O stack if available. If it's not available or if it
has a different owner (like if it was passed in) then we show the
`"awaited at:"` stack below it so you can see where it started and where
it was awaited. If it's the same owner this tends to be unnecessary
noise. We could maybe be smarter if the stacks are very different then
you might want to show both even with the same owner.

<img width="517" height="478" alt="Screenshot 2025-08-04 at 11 57 28 AM"
src="https://github.com/user-attachments/assets/2dbfbed4-4671-4a5f-8e6e-ebec6fe8a1b7"
/>

Additionally, this adds an inferred await if there's no owner and no
stack for the await. The inferred await of a function/class component is
just the owner. No stack. Because the stack trace would be the return
value. This will also be the case if you use throw-a-Promise. The
inferred await in the child position of a built-in is the JSX location
of that await like if you pass a promise to a child. This inference
already happens when you pass a Promise from RSC so in this case it
already has an await - so this is mainly for client promises.
2025-08-06 11:21:01 -04:00
Sebastian Markbåge
c97ec75324 [DevTools] Disconnect and Reconnect children of Suspense boundaries instead of Unmounting and Mounting (#34089)
Stacked on #34082.

This keeps the DevToolsInstance children alive inside Offscreen trees
while they're hidden. However, they're sent as unmounted to the front
end store.

This allows DevTools state to be preserved between these two states.

Such as it keeps the "suspended by" set on the SuspenseNode alive since
the children are still mounted. So now you when you resuspend, you can
see what in the children was suspended. This is useful when you're
simulating a suspense but can also be a bit misleading when something
suspended for real since it'll only show the previous suspended set and
not what is currently suspending it since that hasn't committed yet.

SuspenseNodes inside resuspended trees are now kept alive too. That way
they can contribute to the timeline even when resuspended. We can choose
whether to keep them visible in the rects while hidden or not.

In the future we'll also need to add more special cases around Activity.
Because right now if SuspenseNodes are kept alive in the Suspense tab UI
while hidden, then they're also alive inside Activity that are hidden
which maybe we don't want. Maybe simplest would be that they both
disappear from the Suspense tab UI but can be considered for the
timeline.

Another case is that when Activity goes hidden, Fiber will no longer
cause its content to suspend the parent but that's not modeled here. So
hidden Activity will show up as "suspended by" in a parent Suspense.
When they disconnect, they should really be removed from the "suspended
by" set of the parent (and perhaps be shown only on the Activity
boundary itself).
2025-08-06 11:05:19 -04:00
Sebastian Markbåge
99fd4f2ac1 [DevTools] Reorder moved filtered Fibers with backing DevToolsInstance (#34104)
Instead, we just continue to collect the unfiltered children.

---------

Co-authored-by: Sebastian Sebbie Silbermann <sebastian.silbermann@vercel.com>
2025-08-05 12:39:45 -04:00
Joseph Savona
7deda941f7 [compiler] Delete PropagatePhiTypes (#34107)
We moved this logic into InferTypes a long time ago and the PRs to clean
it up keep getting lost in the shuffle.
2025-08-04 15:15:51 -07:00
Joseph Savona
d3b26b2953 [compiler] rebase #32285 (#34102)
Redo of #32285 which was created with ghstack and is tedious to rebase
with sapling.
2025-08-04 12:04:44 -07:00
lauren
b211d7023c [compiler] Add repros for various invariants (#34099)
We received some bug reports about invariants reported by the compiler
in their codebase. Adding them as repros.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34099).
* #34100
* __->__ #34099
2025-08-04 14:36:12 -04:00
Sebastian Markbåge
ba4bdb2ab5 [DevTools] Consume SuspenseNodes that were skipped when we're bailing out of a subtree (#34082)
This searches through the remaining children to see if any of them were
children of the bailed out FiberInstance and if so we should reuse them
in the new set. It's faster to do this than search through children of
the FiberInstance for Suspense boundaries.
2025-08-04 13:04:47 -04:00
Sebastian Markbåge
be11cb5c4b [DevTools] Tweak the presentation of the Promise value (#34097)
Show the value as "fulfilled: Type" or "rejected: Type" immediately
instead of having to expand it twice. We could show all the properties
of the object immediately like we do in the Performance Track but it's
not always particularly interesting data in the value that isn't already
in the header.

I also moved it to the end after the stack traces since I think the
stack is more interesting but I'm also visually trying to connect the
stack trace with the "name" since typically the "name" will come from
part of the stack trace.

Before:

<img width="517" height="433" alt="Screenshot 2025-08-03 at 11 39 49 PM"
src="https://github.com/user-attachments/assets/ad28d8a2-c149-4957-a393-20ff3932a819"
/>

After:

<img width="520" height="476" alt="Screenshot 2025-08-03 at 11 58 35 PM"
src="https://github.com/user-attachments/assets/53a755b0-bb68-4305-9d16-d6fac7ca4910"
/>
2025-08-04 09:42:48 -04:00
Sebastian Markbåge
557745eb0b [DevTools] Add structure full stack parsing to DevTools (#34093)
We'll need complete parsing of stack traces for both owner stacks and
async debug info so we need to expand the stack parsing capabilities a
bit. This refactors the source location extraction to use some helpers
we can use for other things too.

This is a fork of `ReactFlightStackConfigV8` which also supports
DevTools requirements like checking both `react_stack_bottom_frame` and
`react-stack-bottom-frame` as well as supporting Firefox stacks.

It also supports extracting the first frame of a component stack or the
last frame of an owner stack for the source location.
2025-08-04 09:37:46 -04:00
Sebastian Markbåge
d3f800d47a [DevTools] Style clickable Owner components with angle brackets and bold (#34096)
We have two type of links that appear next to each other now. One type
of link jumps to a Component instance in the DevTools. The other opens a
source location - e.g. in your editor.

This clarifies that something will jump to the Component instance by
marking it as bold and using angle brackets around the name.

This can be seen in the "rendered by" list of owner as well as in the
async stack traces when the stack was in a different owner than the one
currently selected.

<img width="516" height="387" alt="Screenshot 2025-08-03 at 11 27 38 PM"
src="https://github.com/user-attachments/assets/5da50262-1e74-4e46-a6f8-96b4c1e4db31"
/>

The idea is to connect this styling to the owner stacks using
`createTask` where this same pattern occurs (albeit the task name is not
clickable):

<img width="454" height="188" alt="Screenshot 2025-08-03 at 11 23 45 PM"
src="https://github.com/user-attachments/assets/81a55c8f-963a-4fda-846a-97f49ef0c469"
/>

In fact, I was going to add the stack traces to the "rendered by" list
to give the ability to jump to the JSX location in the owner stack so
that it becomes this same view.
2025-08-04 09:28:31 -04:00
Sebastian Markbåge
8e3db095aa [DevTools] Make a non-editable name of KeyValue clickable (#34095)
This has been bothering me. You can click the arrow and the value to
expand/collapse a KeyValue row but not the name.

When the name is not editable it should be clickable. Such as when
inspecting a Promise value.
2025-08-04 09:27:37 -04:00
Sebastian Markbåge
041754697c [DevTools] Only show state for ClassComponents (#34091)
The only thing that uses `memoizedState` as a public API is
ClassComponents. Everything else uses it as internals. We shouldn't ever
show those internals.

Before those internals showed up for example on a suspended Suspense
boundary:

<img width="436" height="370" alt="Screenshot 2025-08-03 at 8 13 37 PM"
src="https://github.com/user-attachments/assets/7fe275a7-d5da-421d-a000-523825916630"
/>
2025-08-04 09:26:12 -04:00
Ruslan Lesiutin
30fca45c1c fix: apply initial horizontal offset on tree mount (#34088)
When the element is pre-selected and the Tree component is mounted,
right now we are only applying initial vertical offset, but not the
horizontal one.

Because of this, if the DOM element was selected on Elements panel and
then user opens Components panel for the first time of the browser
DevTools session, depending on the element's depth, it could be hidden.

Similarly to vertical offset, apply horizontal one, but via ref setter.

### Before:

https://github.com/user-attachments/assets/0ab3cca9-93c1-4e9e-8d23-88330d438912

### After:

https://github.com/user-attachments/assets/10de153a-1e55-4cf7-b1ff-4cc7cb35ba10
2025-08-04 12:12:53 +01:00
Sebastian Markbåge
c499adf8c8 [Flight] Allow Temporary References to be awaited (#34084)
Fixes #33534.

`.then` method can be tested when you await a value that's not a
Promise. For regular Client References we have a way to mark those as
"async" and yield a reference to the unwrapped value in case it's a
Promise on the Client.

However, the realization is that we never serialize Promises as opaque
when passed from the client to the server. If a Promise is passed, then
it would've been deserialized as a Promise (while still registered as a
temporary reference) and not one of these Proxy objects.

Technically it could be a non-function value on the client which would
be wrong but you're not supposed to dot into it in the first place.

So we can just assume it's `undefined`.
2025-08-02 18:44:20 -04:00
Dennis Kats
1d163962b2 Allow returning a temporary reference inside an async function (#33761)
<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

Before submitting a pull request, please make sure the following is
done:

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

Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->

## Summary

Fixes `await`-ing and returning temporary references in `async`
functions. These two operations invoke `.then()` under the hood if it is
available, which currently results in an "Cannot access then on the
server. You cannot dot into a temporary client reference..." error. This
can easily be reproduced by returning a temporary reference from a
server function.

Fixes #33534 

## How did you test this change?
I added a test in a new test file. I wasn't sure where else to put it.
<img width="771" height="138" alt="image"
src="https://github.com/user-attachments/assets/09ffe6eb-271a-4842-a9fe-c68e17b3fb41"
/>


<!--
Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes the user
interface.
How exactly did you verify that your PR solves the issue you wanted to
solve?
  If you leave this empty, your PR will very likely be closed.
-->
2025-08-02 18:11:54 -04:00
Joseph Savona
ddf8bc3fba [compiler] Improve merging of scopes that invalidate together (#34049)
We try to merge consecutive reactive scopes that will always invalidate
together, but there's one common case that isn't handled.

```js
const y = [[x]];
```

Here we'll create two consecutive scopes for the inner and outer array
expressions. Because the input to the second scope is a temporary,
they'll merge into one scope.

But if we name the inner array, the merging stops:

```js
const array = [x];
const y = [array];
```

This is because the merging logic checks if all the dependencies of the
second scope are outputs of the first scope, but doesn't account for
renaming due to LoadLocal/StoreLocal. The fix is to track these
temporaries.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34049).
* __->__ #34049
* #34047
* #34044
2025-08-01 13:00:01 -07:00
Joseph Savona
0860b9cc1f [compiler] Add definitions for Object entries/keys/values (#34047)
Fixes remaining issue in #32261, where passing a previously useMemo()-d
value to `Object.entries()` makes the compiler think the value is
mutated and fail validatePreserveExistingMemo. While I was there I added
Object.keys() and Object.values() too.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34047).
* #34049
* __->__ #34047
* #34044
2025-08-01 12:59:49 -07:00
Sebastian Markbåge
538ac7ae4b [Flight] Fix debug info leaking to outer handler (#34081)
The `waitForReference` call for debug info can trigger inside a
different object's initializingHandler. In that case, we can get
confused by which one is the root object.

We have this special case to detect if the initializing handler's object
is `null` and we have an empty string key, then we should replace the
root object's value with the resolved value.


52612a7cbd/packages/react-client/src/ReactFlightClient.js (L1374)

However, if the initializing handler actually should have the value
`null` then we might get confused by this and replace it with the
resolved value from a debug object. This fixes it by just using a
non-empty string as the key for the waitForReference on debug value
since we're not going to use it anyway.

It used to be impossible to get into this state since a `null` value at
the root couldn't have any reference inside itself but now the debug
info for a `null` value can have outstanding references.

However, a better fix might be using a placeholder marker object instead
of null or better yet ensuring that we know which root we're
initializing in the debug model.
2025-08-01 15:44:48 -04:00
lauren
52612a7cbd [compiler] Emit more specific error when making identifiers with reserved words (#34080)
This currently throws an invariant which may be misleading. I checked
the ecma262 spec and used the same list of reserved words in our check.
To err on the side of being conservative, we also error when strict mode
reserved words are used.
2025-08-01 15:10:34 -04:00
Sebastian "Sebbie" Silbermann
bdb4a96f62 [DevTools] Lazily compute initial Tree state (#34078) 2025-08-01 17:49:25 +02:00
Sebastian Markbåge
c260b38d0a [DevTools] Clean up Virtual Instances from id map (#34063)
This was a pretty glaring memory leak. 🙈

I forgot to clean up the VirtualInstances from the id map so the Server
Component instances always leaked in DEV.
2025-07-31 10:30:31 -04:00
Sebastian Markbåge
5bbf9be246 [DevTools] Model Hidden Offscreen Boundaries as Unmounts (#34062)
This is modeling Offscreen boundaries as the thing that unmounts a tree
in the frontend. This will let us model this as a "hide" that preserves
state instead in a follow up but not yet.

By doing it this way, we don't have to special case suspended Suspense
boundaries, at least not for the modern versions that use Offscreen as
the internal node. It's still special cased for the old React versions.
Instead, this is handled by the Offscreen fiber getting hidden.

By giving this fiber an FilteredFiberInstance, we also have somewhere to
store the children on (separately from the parent children set which can
include other siblings too like the loading state).

One consequence is that Activity boundary content now disappears when
they're hidden which is probably a good thing since otherwise it would
be confusing and noisy when it's used to render multiple pages at once.
2025-07-31 10:30:10 -04:00
Josh Story
8de7aed892 [Fizz] Count Boundary bytes that may contribute to the preamble in the request byteSize (#34059)
Stacked on #34058

When tracking how large the shell is we currently only track the bytes
of everything above Suspense boundaries. However since Boundaries that
contribute to the preamble will always be inlined when the shell flushes
they should also be considered as part of the request byteSize since
they always flush alongside the shell. This change adds this tracking
2025-07-30 18:18:57 -07:00
Josh Story
98773466ce [Fizz] Don't outline Boundaries that may contribute to the preamble (#34058)
Suspense boundaries that may have contributed to the preamble should not
be outlined due to size because these boundaries are only meant to be in
fallback state if the boundary actually errors. This change excludes any
boundary which has the potential to contribute to the preamble. We could
alternatively track which boundaries actually contributed to the
preamble but in practice there will be very few and I think this is
sufficient.

One problem with this approach is it makes Suspense above body opt out
of the mode where we omit rel="expect" for large shells. In essence
Suspense above body has the semantics of a Shell (it blocks flushing
until resolved) but it doesn't get tracked as request bytes and thus we
will not opt users into the skipped blocking shell for very large
boundaries.

This will be fixed in a followup
2025-07-30 18:06:47 -07:00
Sebastian Markbåge
9784cb379e [DevTools] No suspending above the root (#34055)
Follow up to #34050.

It's not actually possible to suspend *above* the root since even if you
suspend in the first child position, you're still suspending the
HostRoot which always has a corresponding FiberInstance and
SuspenseNode.
2025-07-30 11:31:27 -04:00
Sebastian Markbåge
dcf2a6f665 [DevTools] Keep a Suspense Tree Parellel to the Instance tree in the Backend (#34050)
This keeps a data structure of Suspense boundaries and the root which
can keep track which boundaries might participate in a loading sequence
and everything that suspends them. This will power the Suspense tab.

Now when you select a `<Suspense>` boundary the "suspended by" section
shows the whole boundary instead of just that component.

In the future, we'll likely need to add "Activity" boundaries to this
tree as well, so that we can track what suspended the root of an
Activity when filtering a subtree. Similar to how the root SuspenseNode
now tracks suspending at the root. Maybe it's ok to just traverse to
collect this information on-demand when you select one though since this
doesn't contribute to the deduping.

We'll also need to add implicit Suspense boundaries for the rows of a
SuspenseList with `tail=hidden/collapsed`.
2025-07-30 09:55:09 -04:00
Sebastian "Sebbie" Silbermann
36c63d4f9c [DevTools] Layout for Suspense tab (#34042) 2025-07-30 07:12:18 +02:00
Joseph Savona
88b40f6e41 Enable ref validation in linter (#34044)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34044).
* #34027
* __->__ #34044
2025-07-29 12:30:29 -07:00
Joseph Savona
04a7a61918 [compiler] Allow assigning ref-accessing functions to objects if not mutated (#34026)
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.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34026).
* #34027
* __->__ #34026
2025-07-29 10:57:26 -07:00
277 changed files with 13244 additions and 3249 deletions

View File

@@ -10,48 +10,22 @@ import {codeFrameColumns} from '@babel/code-frame';
import type {SourceLocation} from './HIR';
import {Err, Ok, Result} from './Utils/Result';
import {assertExhaustive} from './Utils/utils';
export enum ErrorSeverity {
/**
* Invalid JS syntax, or valid syntax that is semantically invalid which may indicate some
* misunderstanding on the users part.
*/
InvalidJS = 'InvalidJS',
/**
* JS syntax that is not supported and which we do not plan to support. Developers should
* rewrite to use supported forms.
*/
UnsupportedJS = 'UnsupportedJS',
/**
* Code that breaks the rules of React.
*/
InvalidReact = 'InvalidReact',
/**
* Incorrect configuration of the compiler.
*/
InvalidConfig = 'InvalidConfig',
/**
* Code that can reasonably occur and that doesn't break any rules, but is unsafe to preserve
* memoization.
*/
CannotPreserveMemoization = 'CannotPreserveMemoization',
/**
* Unhandled syntax that we don't support yet.
*/
Todo = 'Todo',
/**
* An unexpected internal error in the compiler that indicates critical issues that can panic
* the compiler.
*/
Invariant = 'Invariant',
}
import {ErrorSeverity} from './Utils/CompilerErrorSeverity';
import {
ErrorCode,
ErrorCodeDetails,
LinterCategory,
} from './Utils/CompilerErrorCodes';
export {ErrorSeverity};
export {ErrorCode, ErrorCodeDetails, LinterCategory};
export type CompilerDiagnosticOptions = {
severity: ErrorSeverity;
category: string;
description: string;
description?: string | null | undefined;
details: Array<CompilerDiagnosticDetail>;
suggestions?: Array<CompilerSuggestion> | null | undefined;
linterCategory?: LinterCategory | null | undefined;
};
export type CompilerDiagnosticDetail =
@@ -86,13 +60,28 @@ export type CompilerSuggestion =
description: string;
};
export type CompilerErrorDetailOptions = {
export type PlainCompilerErrorDetailOptions = {
errorCode?: void;
reason: string;
description?: string | null | undefined;
severity: ErrorSeverity;
severity:
| ErrorSeverity.Invariant
| ErrorSeverity.Todo
| ErrorSeverity.InvalidConfig;
loc: SourceLocation | null;
suggestions?: Array<CompilerSuggestion> | null | undefined;
};
export type CodedCompilerErrorDetailOptions = {
errorCode: ErrorCode;
description?: string | null | undefined;
loc: SourceLocation | null;
suggestions?: Array<CompilerSuggestion> | null | undefined;
linterCategory?: LinterCategory | null | undefined;
};
export type CompilerErrorDetailOptions =
| PlainCompilerErrorDetailOptions
| CodedCompilerErrorDetailOptions;
export type PrintErrorMessageOptions = {
/**
@@ -102,19 +91,72 @@ export type PrintErrorMessageOptions = {
eslint: boolean;
};
export function makeCompilerDiagnostic(
code: ErrorCode,
options?: {
description?: string;
suggestions?: Array<CompilerSuggestion> | null | undefined;
},
): CompilerDiagnostic {
return makeCompilerDiagnostic(code, options);
}
export class CompilerDiagnostic {
options: CompilerDiagnosticOptions;
constructor(options: CompilerDiagnosticOptions) {
/**
* Constructor is private to enforce that we either only create invariant diagnostics
* or use ErrorCodes
*/
private constructor(options: CompilerDiagnosticOptions) {
this.options = options;
}
static create(
options: Omit<CompilerDiagnosticOptions, 'details'>,
): CompilerDiagnostic {
static create<
T extends CompilerDiagnosticOptions & {severity: ErrorSeverity.Invariant},
>(options: Omit<T, 'details'>): CompilerDiagnostic {
return new CompilerDiagnostic({...options, details: []});
}
static fromCode(
code: ErrorCode,
options?: {
description?: string;
suggestions?: Array<CompilerSuggestion> | null | undefined;
details?: Array<CompilerDiagnosticDetail> | null | undefined;
},
): CompilerDiagnostic {
const errorEntry = ErrorCodeDetails[code];
let description = undefined;
if (errorEntry.description != null) {
description = errorEntry.description;
}
if (options?.description != null && options.description.length > 0) {
if (description != null && description.length > 0) {
description += ' ';
} else {
description = '';
}
description += options.description;
}
const diagnosticOptions: CompilerDiagnosticOptions = {
severity: errorEntry.severity,
category: errorEntry.reason,
description,
linterCategory: errorEntry.linterCategory,
suggestions: options?.suggestions,
details: options?.details ?? [],
};
return new CompilerDiagnostic(diagnosticOptions);
}
// TODO: remove after converting test fixtures to use printErrorMessage
serialize(): unknown {
return {options: {...this.options, linterCategory: undefined}};
}
get category(): CompilerDiagnosticOptions['category'] {
return this.options.category;
}
@@ -127,6 +169,9 @@ export class CompilerDiagnostic {
get suggestions(): CompilerDiagnosticOptions['suggestions'] {
return this.options.suggestions;
}
get linterCategory(): CompilerDiagnosticOptions['linterCategory'] {
return this.options.linterCategory;
}
withDetail(detail: CompilerDiagnosticDetail): CompilerDiagnostic {
this.options.details.push(detail);
@@ -138,11 +183,10 @@ export class CompilerDiagnostic {
}
printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
const buffer = [
printErrorSummary(this.severity, this.category),
'\n\n',
this.description,
];
const buffer = [printErrorSummary(this.severity, this.category)];
if (this.description != null) {
buffer.push(`\n\n${this.description}`);
}
for (const detail of this.options.details) {
switch (detail.kind) {
case 'error': {
@@ -202,13 +246,65 @@ export class CompilerErrorDetail {
this.options = options;
}
get reason(): CompilerErrorDetailOptions['reason'] {
return this.options.reason;
static fromCode(
code: ErrorCode,
details?: {
description?: string | null;
loc?: SourceLocation | null;
suggestions?: Array<CompilerSuggestion> | null | undefined;
},
): CompilerErrorDetail {
return new CompilerErrorDetail({
...details,
errorCode: code,
} as CodedCompilerErrorDetailOptions);
}
get description(): CompilerErrorDetailOptions['description'] {
// TODO: remove after converting test fixtures to use printErrorMessage
serialize(): unknown {
return {
options: {
reason: this.reason,
description: this.description,
severity: this.severity,
loc: this.loc,
suggestions: this.suggestions,
},
};
}
get reason(): string {
if (this.options.errorCode != null) {
return ErrorCodeDetails[this.options.errorCode].reason;
} else {
return this.options.reason;
}
}
get description(): string | null | undefined {
if (this.options.errorCode != null) {
let description = undefined;
if (ErrorCodeDetails[this.options.errorCode].description != null) {
description = ErrorCodeDetails[this.options.errorCode].description;
}
if (
this.options.description != null &&
this.options.description.length > 0
) {
if (description != null && description.length > 0) {
description += '. ';
} else {
description = '';
}
description += this.options.description;
}
return description;
}
return this.options.description;
}
get severity(): CompilerErrorDetailOptions['severity'] {
get severity(): ErrorSeverity {
if (this.options.errorCode != null) {
return ErrorCodeDetails[this.options.errorCode].severity;
}
return this.options.severity;
}
get loc(): CompilerErrorDetailOptions['loc'] {
@@ -217,6 +313,12 @@ export class CompilerErrorDetail {
get suggestions(): CompilerErrorDetailOptions['suggestions'] {
return this.options.suggestions;
}
get linterCategory(): LinterCategory | null | undefined {
if (this.options.errorCode != null) {
return ErrorCodeDetails[this.options.errorCode].linterCategory;
}
return undefined;
}
primaryLocation(): SourceLocation | null {
return this.loc;
@@ -266,7 +368,7 @@ export class CompilerError extends Error {
static invariant(
condition: unknown,
options: Omit<CompilerErrorDetailOptions, 'severity'>,
options: Omit<PlainCompilerErrorDetailOptions, 'severity'>,
): asserts condition {
if (!condition) {
const errors = new CompilerError();
@@ -280,14 +382,14 @@ export class CompilerError extends Error {
}
}
static throwDiagnostic(options: CompilerDiagnosticOptions): never {
static throwDiagnostic(diagnostic: CompilerDiagnostic): never {
const errors = new CompilerError();
errors.pushDiagnostic(new CompilerDiagnostic(options));
errors.pushDiagnostic(diagnostic);
throw errors;
}
static throwTodo(
options: Omit<CompilerErrorDetailOptions, 'severity'>,
options: Omit<PlainCompilerErrorDetailOptions, 'severity'>,
): never {
const errors = new CompilerError();
errors.pushErrorDetail(
@@ -296,34 +398,21 @@ export class CompilerError extends Error {
throw errors;
}
static throwInvalidJS(
options: Omit<CompilerErrorDetailOptions, 'severity'>,
static throwFromCode(
code: ErrorCode,
options?: {
description?: string | null;
loc?: SourceLocation | null;
suggestions?: Array<CompilerSuggestion> | null | undefined;
},
): never {
const errors = new CompilerError();
errors.pushErrorDetail(
new CompilerErrorDetail({
...options,
severity: ErrorSeverity.InvalidJS,
}),
);
throw errors;
}
static throwInvalidReact(
options: Omit<CompilerErrorDetailOptions, 'severity'>,
): never {
const errors = new CompilerError();
errors.pushErrorDetail(
new CompilerErrorDetail({
...options,
severity: ErrorSeverity.InvalidReact,
}),
);
errors.pushErrorDetail(CompilerErrorDetail.fromCode(code, options));
throw errors;
}
static throwInvalidConfig(
options: Omit<CompilerErrorDetailOptions, 'severity'>,
options: Omit<PlainCompilerErrorDetailOptions, 'severity'>,
): never {
const errors = new CompilerError();
errors.pushErrorDetail(
@@ -392,13 +481,10 @@ export class CompilerError extends Error {
}
push(options: CompilerErrorDetailOptions): CompilerErrorDetail {
const detail = new CompilerErrorDetail({
reason: options.reason,
description: options.description ?? null,
severity: options.severity,
suggestions: options.suggestions,
loc: typeof options.loc === 'symbol' ? null : options.loc,
});
if (options instanceof CompilerErrorDetail) {
return this.pushErrorDetail(options);
}
const detail = new CompilerErrorDetail(options);
return this.pushErrorDetail(detail);
}
@@ -407,6 +493,19 @@ export class CompilerError extends Error {
return detail;
}
pushErrorCode(
code: ErrorCode,
details?: {
description?: string | null;
loc?: SourceLocation | null;
suggestions?: Array<CompilerSuggestion> | null | undefined;
},
): CompilerErrorDetail {
const detail = CompilerErrorDetail.fromCode(code, details);
this.details.push(detail);
return detail;
}
hasErrors(): boolean {
return this.details.length > 0;
}

View File

@@ -38,10 +38,10 @@ export function validateRestrictedImports(
ImportDeclaration(importDeclPath) {
if (restrictedImports.has(importDeclPath.node.source.value)) {
error.push({
severity: ErrorSeverity.Todo,
reason: 'Bailing out due to blocklisted import',
description: `Import from module ${importDeclPath.node.source.value}`,
loc: importDeclPath.node.loc ?? null,
severity: ErrorSeverity.Todo,
});
}
},
@@ -205,10 +205,10 @@ export class ProgramContext {
}
const error = new CompilerError();
error.push({
severity: ErrorSeverity.Todo,
reason: 'Encountered conflicting global in generated program',
description: `Conflict from local binding ${name}`,
loc: scope.getBinding(name)?.path.node.loc ?? null,
severity: ErrorSeverity.Todo,
suggestions: null,
});
return Err(error);

View File

@@ -11,7 +11,7 @@ import {
CompilerDiagnostic,
CompilerError,
CompilerErrorDetail,
CompilerErrorDetailOptions,
PlainCompilerErrorDetailOptions,
} from '../CompilerError';
import {
EnvironmentConfig,
@@ -107,6 +107,8 @@ export type PluginOptions = {
* passes.
*
* Defaults to false
*
* TODO: rename this to lintOnly or something similar
*/
noEmit: boolean;
@@ -234,7 +236,10 @@ export type CompileErrorEvent = {
export type CompileDiagnosticEvent = {
kind: 'CompileDiagnostic';
fnLoc: t.SourceLocation | null;
detail: Omit<Omit<CompilerErrorDetailOptions, 'severity'>, 'suggestions'>;
detail: Omit<
Omit<PlainCompilerErrorDetailOptions, 'severity'>,
'suggestions'
>;
};
export type CompileSuccessEvent = {
kind: 'CompileSuccess';

View File

@@ -92,7 +92,6 @@ import {
} from '../Validation';
import {validateLocalsNotReassignedAfterRender} from '../Validation/ValidateLocalsNotReassignedAfterRender';
import {outlineFunctions} from '../Optimization/OutlineFunctions';
import {propagatePhiTypes} from '../TypeInference/PropagatePhiTypes';
import {lowerContextAccess} from '../Optimization/LowerContextAccess';
import {validateNoSetStateInEffects} from '../Validation/ValidateNoSetStateInEffects';
import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement';
@@ -101,11 +100,11 @@ import {outlineJSX} from '../Optimization/OutlineJsx';
import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls';
import {transformFire} from '../Transform';
import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureFunctionsInRender';
import {CompilerError} from '..';
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -174,7 +173,7 @@ function runWithEnvironment(
!env.config.disableMemoizationForDebugging &&
!env.config.enableChangeDetectionForDebugging
) {
dropManualMemoization(hir).unwrap();
env.logOrThrowErrors(dropManualMemoization(hir));
log({kind: 'hir', name: 'DropManualMemoization', value: hir});
}
@@ -207,10 +206,10 @@ function runWithEnvironment(
if (env.isInferredMemoEnabled) {
if (env.config.validateHooksUsage) {
validateHooksUsage(hir).unwrap();
env.logOrThrowErrors(validateHooksUsage(hir));
}
if (env.config.validateNoCapitalizedCalls) {
validateNoCapitalizedCalls(hir).unwrap();
if (env.config.validateNoCapitalizedCalls != null) {
env.logOrThrowErrors(validateNoCapitalizedCalls(hir));
}
}
@@ -230,20 +229,17 @@ function runWithEnvironment(
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
if (!env.config.enableNewMutationAliasingModel) {
const fnEffectErrors = inferReferenceEffects(hir);
const fnEffectResult = inferReferenceEffects(hir);
if (env.isInferredMemoEnabled) {
if (fnEffectErrors.length > 0) {
CompilerError.throw(fnEffectErrors[0]);
}
env.logOrThrowErrors(fnEffectResult);
}
log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
} else {
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
if (env.isInferredMemoEnabled) {
if (mutabilityAliasingErrors.isErr()) {
throw mutabilityAliasingErrors.unwrapErr();
}
env.logOrThrowErrors(mutabilityAliasingErrors);
}
}
@@ -272,10 +268,8 @@ function runWithEnvironment(
});
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
if (env.isInferredMemoEnabled) {
if (mutabilityAliasingErrors.isErr()) {
throw mutabilityAliasingErrors.unwrapErr();
}
validateLocalsNotReassignedAfterRender(hir);
env.logOrThrowErrors(mutabilityAliasingErrors.map(() => undefined));
env.logOrThrowErrors(validateLocalsNotReassignedAfterRender(hir));
}
}
@@ -285,11 +279,15 @@ function runWithEnvironment(
}
if (env.config.validateRefAccessDuringRender) {
validateNoRefAccessInRender(hir).unwrap();
env.logOrThrowErrors(validateNoRefAccessInRender(hir));
}
if (env.config.validateNoSetStateInRender) {
validateNoSetStateInRender(hir).unwrap();
env.logOrThrowErrors(validateNoSetStateInRender(hir));
}
if (env.config.validateNoDerivedComputationsInEffects) {
env.logOrThrowErrors(validateNoDerivedComputationsInEffects(hir));
}
if (env.config.validateNoSetStateInEffects) {
@@ -299,16 +297,18 @@ function runWithEnvironment(
if (env.config.validateNoJSXInTryStatements) {
env.logErrors(validateNoJSXInTryStatement(hir));
}
if (env.config.validateNoImpureFunctionsInRender) {
validateNoImpureFunctionsInRender(hir).unwrap();
if (
env.config.validateNoImpureFunctionsInRender &&
!env.config.enableNewMutationAliasingModel
) {
env.logOrThrowErrors(validateNoImpureFunctionsInRender(hir));
}
if (
env.config.validateNoFreezingKnownMutableFunctions ||
env.config.enableNewMutationAliasingModel
) {
validateNoFreezingKnownMutableFunctions(hir).unwrap();
env.logOrThrowErrors(validateNoFreezingKnownMutableFunctions(hir));
}
}
@@ -322,13 +322,6 @@ function runWithEnvironment(
value: hir,
});
propagatePhiTypes(hir);
log({
kind: 'hir',
name: 'PropagatePhiTypes',
value: hir,
});
if (env.isInferredMemoEnabled) {
if (env.config.validateStaticComponents) {
env.logErrors(validateStaticComponents(hir));

View File

@@ -7,11 +7,7 @@
import {NodePath} from '@babel/core';
import * as t from '@babel/types';
import {
CompilerError,
CompilerErrorDetail,
ErrorSeverity,
} from '../CompilerError';
import {CompilerError, ErrorSeverity} from '../CompilerError';
import {ExternalFunction, ReactFunctionType} from '../HIR/Environment';
import {CodegenFunction} from '../ReactiveScopes';
import {isComponentDeclaration} from '../Utils/ComponentDeclaration';
@@ -32,6 +28,7 @@ import {
} from './Suppression';
import {GeneratedSource} from '../HIR';
import {Err, Ok, Result} from '../Utils/Result';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
export type CompilerPass = {
opts: PluginOptions;
@@ -101,12 +98,9 @@ function findDirectivesDynamicGating(
if (t.isValidIdentifier(maybeMatch[1])) {
result.push({directive, match: maybeMatch[1]});
} else {
errors.push({
reason: `Dynamic gating directive is not a valid JavaScript identifier`,
errors.pushErrorCode(ErrorCode.DYNAMIC_GATING_IS_NOT_IDENTIFIER, {
description: `Found '${directive.value.value}'`,
severity: ErrorSeverity.InvalidReact,
loc: directive.loc ?? null,
suggestions: null,
});
}
}
@@ -115,14 +109,11 @@ function findDirectivesDynamicGating(
return Err(errors);
} else if (result.length > 1) {
const error = new CompilerError();
error.push({
reason: `Multiple dynamic gating directives found`,
error.pushErrorCode(ErrorCode.DYNAMIC_GATING_MULTIPLE_DIRECTIVES, {
description: `Expected a single directive but found [${result
.map(r => r.directive.value.value)
.join(', ')}]`,
severity: ErrorSeverity.InvalidReact,
loc: result[0].directive.loc ?? null,
suggestions: null,
});
return Err(error);
} else if (result.length === 1) {
@@ -451,14 +442,12 @@ export function compileProgram(
if (programContext.hasModuleScopeOptOut) {
if (compiledFns.length > 0) {
const error = new CompilerError();
error.pushErrorDetail(
new CompilerErrorDetail({
reason:
'Unexpected compiled functions when module scope opt-out is present',
severity: ErrorSeverity.Invariant,
loc: null,
}),
);
error.push({
reason:
'Unexpected compiled functions when module scope opt-out is present',
severity: ErrorSeverity.Invariant,
loc: null,
});
handleError(error, programContext, null);
}
return null;
@@ -591,9 +580,7 @@ function processFn(
let compiledFn: CodegenFunction;
const compileResult = tryCompileFunction(fn, fnType, programContext);
if (compileResult.kind === 'error') {
if (directives.optOut != null) {
logError(compileResult.error, programContext, fn.node.loc ?? null);
} else {
if (directives.optOut == null) {
handleError(compileResult.error, programContext, fn.node.loc ?? null);
}
const retryResult = retryCompileFunction(fn, fnType, programContext);
@@ -692,7 +679,7 @@ function tryCompileFunction(
fn,
programContext.opts.environment,
fnType,
'all_features',
programContext.opts.noEmit ? 'lint_only' : 'all_features',
programContext,
programContext.opts.logger,
programContext.filename,
@@ -805,15 +792,7 @@ function shouldSkipCompilation(
if (pass.opts.sources) {
if (pass.filename === null) {
const error = new CompilerError();
error.pushErrorDetail(
new CompilerErrorDetail({
reason: `Expected a filename but found none.`,
description:
"When the 'sources' config options is specified, the React compiler will only compile files with a name",
severity: ErrorSeverity.InvalidConfig,
loc: null,
}),
);
error.pushErrorCode(ErrorCode.FILENAME_NOT_SET);
handleError(error, pass, null);
return true;
}

View File

@@ -11,7 +11,7 @@ import {
CompilerDiagnostic,
CompilerError,
CompilerSuggestionOperation,
ErrorSeverity,
ErrorCode,
} from '../CompilerError';
import {assertExhaustive} from '../Utils/utils';
import {GeneratedSource} from '../HIR';
@@ -165,14 +165,12 @@ export function suppressionsToCompilerError(
let reason, suggestion;
switch (suppressionRange.source) {
case 'Eslint':
reason =
'React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled';
reason = ErrorCode.BAILOUT_ESLINT_SUPPRESSION;
suggestion =
'Remove the ESLint suppression and address the React error';
break;
case 'Flow':
reason =
'React Compiler has skipped optimizing this component because one or more React rule violations were reported by Flow';
reason = ErrorCode.BAILOUT_FLOW_SUPPRESSION;
suggestion = 'Remove the Flow suppression and address the React error';
break;
default:
@@ -182,10 +180,8 @@ export function suppressionsToCompilerError(
);
}
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,
CompilerDiagnostic.fromCode(reason, {
description: `Found suppression \`${suppressionRange.disableComment.value.trim()}\``,
suggestions: [
{
description: suggestion,

View File

@@ -8,27 +8,24 @@
import {NodePath} from '@babel/core';
import * as t from '@babel/types';
import {CompilerError, EnvironmentConfig, ErrorSeverity, Logger} from '..';
import {CompilerError, EnvironmentConfig, Logger} from '..';
import {getOrInsertWith} from '../Utils/utils';
import {Environment, GeneratedSource} from '../HIR';
import {DEFAULT_EXPORT} from '../HIR/Environment';
import {CompileProgramMetadata} from './Program';
import {CompilerDiagnostic, CompilerDiagnosticOptions} from '../CompilerError';
import {CompilerDiagnostic} from '../CompilerError';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
function throwInvalidReact(
options: Omit<CompilerDiagnosticOptions, 'severity'>,
function logAndThrowDiagnostic(
diagnostic: CompilerDiagnostic,
{logger, filename}: TraversalState,
): never {
const detail: CompilerDiagnosticOptions = {
severity: ErrorSeverity.InvalidReact,
...options,
};
logger?.logEvent(filename, {
kind: 'CompileError',
fnLoc: null,
detail: new CompilerDiagnostic(detail),
detail: diagnostic,
});
CompilerError.throwDiagnostic(detail);
CompilerError.throwDiagnostic(diagnostic);
}
function isAutodepsSigil(
@@ -90,10 +87,8 @@ function assertValidEffectImportReference(
* as it may have already been transformed by the compiler (and not
* memoized).
*/
throwInvalidReact(
{
category:
'Cannot infer dependencies of this effect. This will break your build!',
logAndThrowDiagnostic(
CompilerDiagnostic.fromCode(ErrorCode.DID_NOT_INFER_DEPS, {
description:
'To resolve, either pass a dependency array or fix reported compiler bailout diagnostics.' +
(maybeErrorDiagnostic ? ` ${maybeErrorDiagnostic}` : ''),
@@ -104,7 +99,7 @@ function assertValidEffectImportReference(
loc: parent.node.loc ?? GeneratedSource,
},
],
},
}),
context,
);
}
@@ -121,10 +116,8 @@ function assertValidFireImportReference(
paths[0],
context.transformErrors,
);
throwInvalidReact(
{
category:
'[Fire] Untransformed reference to compiler-required feature.',
logAndThrowDiagnostic(
CompilerDiagnostic.fromCode(ErrorCode.CANNOT_COMPILE_FIRE, {
description:
'Either remove this `fire` call or ensure it is successfully transformed by the compiler' +
maybeErrorDiagnostic
@@ -137,7 +130,7 @@ function assertValidFireImportReference(
loc: paths[0].node.loc ?? GeneratedSource,
},
],
},
}),
context,
);
}

View File

@@ -0,0 +1,752 @@
/**
* TypeScript definitions for Flow type JSON representations
* Based on the output of /data/sandcastle/boxes/fbsource/fbcode/flow/src/typing/convertTypes.ml
*/
// Base type for all Flow types with a kind field
export interface BaseFlowType {
kind: string;
}
// Type for representing polarity
export type Polarity = 'positive' | 'negative' | 'neutral';
// Type for representing a name that might be null
export type OptionalName = string | null;
// Open type
export interface OpenType extends BaseFlowType {
kind: 'Open';
}
// Def type
export interface DefType extends BaseFlowType {
kind: 'Def';
def: DefT;
}
// Eval type
export interface EvalType extends BaseFlowType {
kind: 'Eval';
type: FlowType;
destructor: Destructor;
}
// Generic type
export interface GenericType extends BaseFlowType {
kind: 'Generic';
name: string;
bound: FlowType;
no_infer: boolean;
}
// ThisInstance type
export interface ThisInstanceType extends BaseFlowType {
kind: 'ThisInstance';
instance: InstanceT;
is_this: boolean;
name: string;
}
// ThisTypeApp type
export interface ThisTypeAppType extends BaseFlowType {
kind: 'ThisTypeApp';
t1: FlowType;
t2: FlowType;
t_list?: Array<FlowType>;
}
// TypeApp type
export interface TypeAppType extends BaseFlowType {
kind: 'TypeApp';
type: FlowType;
targs: Array<FlowType>;
from_value: boolean;
use_desc: boolean;
}
// FunProto type
export interface FunProtoType extends BaseFlowType {
kind: 'FunProto';
}
// ObjProto type
export interface ObjProtoType extends BaseFlowType {
kind: 'ObjProto';
}
// NullProto type
export interface NullProtoType extends BaseFlowType {
kind: 'NullProto';
}
// FunProtoBind type
export interface FunProtoBindType extends BaseFlowType {
kind: 'FunProtoBind';
}
// Intersection type
export interface IntersectionType extends BaseFlowType {
kind: 'Intersection';
members: Array<FlowType>;
}
// Union type
export interface UnionType extends BaseFlowType {
kind: 'Union';
members: Array<FlowType>;
}
// Maybe type
export interface MaybeType extends BaseFlowType {
kind: 'Maybe';
type: FlowType;
}
// Optional type
export interface OptionalType extends BaseFlowType {
kind: 'Optional';
type: FlowType;
use_desc: boolean;
}
// Keys type
export interface KeysType extends BaseFlowType {
kind: 'Keys';
type: FlowType;
}
// Annot type
export interface AnnotType extends BaseFlowType {
kind: 'Annot';
type: FlowType;
use_desc: boolean;
}
// Opaque type
export interface OpaqueType extends BaseFlowType {
kind: 'Opaque';
opaquetype: {
opaque_id: string;
underlying_t: FlowType | null;
super_t: FlowType | null;
opaque_type_args: Array<{
name: string;
type: FlowType;
polarity: Polarity;
}>;
opaque_name: string;
};
}
// Namespace type
export interface NamespaceType extends BaseFlowType {
kind: 'Namespace';
namespace_symbol: {
symbol: string;
};
values_type: FlowType;
types_tmap: PropertyMap;
}
// Any type
export interface AnyType extends BaseFlowType {
kind: 'Any';
}
// StrUtil type
export interface StrUtilType extends BaseFlowType {
kind: 'StrUtil';
op: 'StrPrefix' | 'StrSuffix';
prefix?: string;
suffix?: string;
remainder?: FlowType;
}
// TypeParam definition
export interface TypeParam {
name: string;
bound: FlowType;
polarity: Polarity;
default: FlowType | null;
}
// EnumInfo types
export type EnumInfo = ConcreteEnum | AbstractEnum;
export interface ConcreteEnum {
kind: 'ConcreteEnum';
enum_name: string;
enum_id: string;
members: Array<string>;
representation_t: FlowType;
has_unknown_members: boolean;
}
export interface AbstractEnum {
kind: 'AbstractEnum';
representation_t: FlowType;
}
// CanonicalRendersForm types
export type CanonicalRendersForm =
| InstrinsicRenders
| NominalRenders
| StructuralRenders
| DefaultRenders;
export interface InstrinsicRenders {
kind: 'InstrinsicRenders';
name: string;
}
export interface NominalRenders {
kind: 'NominalRenders';
renders_id: string;
renders_name: string;
renders_super: FlowType;
}
export interface StructuralRenders {
kind: 'StructuralRenders';
renders_variant: 'RendersNormal' | 'RendersMaybe' | 'RendersStar';
renders_structural_type: FlowType;
}
export interface DefaultRenders {
kind: 'DefaultRenders';
}
// InstanceT definition
export interface InstanceT {
inst: InstType;
static: FlowType;
super: FlowType;
implements: Array<FlowType>;
}
// InstType definition
export interface InstType {
class_name: string | null;
class_id: string;
type_args: Array<{
name: string;
type: FlowType;
polarity: Polarity;
}>;
own_props: PropertyMap;
proto_props: PropertyMap;
call_t: null | {
id: number;
call: FlowType;
};
}
// DefT types
export type DefT =
| NumGeneralType
| StrGeneralType
| BoolGeneralType
| BigIntGeneralType
| EmptyType
| MixedType
| NullType
| VoidType
| SymbolType
| FunType
| ObjType
| ArrType
| ClassType
| InstanceType
| SingletonStrType
| NumericStrKeyType
| SingletonNumType
| SingletonBoolType
| SingletonBigIntType
| TypeType
| PolyType
| ReactAbstractComponentType
| RendersType
| EnumValueType
| EnumObjectType;
export interface NumGeneralType extends BaseFlowType {
kind: 'NumGeneral';
}
export interface StrGeneralType extends BaseFlowType {
kind: 'StrGeneral';
}
export interface BoolGeneralType extends BaseFlowType {
kind: 'BoolGeneral';
}
export interface BigIntGeneralType extends BaseFlowType {
kind: 'BigIntGeneral';
}
export interface EmptyType extends BaseFlowType {
kind: 'Empty';
}
export interface MixedType extends BaseFlowType {
kind: 'Mixed';
}
export interface NullType extends BaseFlowType {
kind: 'Null';
}
export interface VoidType extends BaseFlowType {
kind: 'Void';
}
export interface SymbolType extends BaseFlowType {
kind: 'Symbol';
}
export interface FunType extends BaseFlowType {
kind: 'Fun';
static: FlowType;
funtype: FunTypeObj;
}
export interface ObjType extends BaseFlowType {
kind: 'Obj';
objtype: ObjTypeObj;
}
export interface ArrType extends BaseFlowType {
kind: 'Arr';
arrtype: ArrTypeObj;
}
export interface ClassType extends BaseFlowType {
kind: 'Class';
type: FlowType;
}
export interface InstanceType extends BaseFlowType {
kind: 'Instance';
instance: InstanceT;
}
export interface SingletonStrType extends BaseFlowType {
kind: 'SingletonStr';
from_annot: boolean;
value: string;
}
export interface NumericStrKeyType extends BaseFlowType {
kind: 'NumericStrKey';
number: string;
string: string;
}
export interface SingletonNumType extends BaseFlowType {
kind: 'SingletonNum';
from_annot: boolean;
number: string;
string: string;
}
export interface SingletonBoolType extends BaseFlowType {
kind: 'SingletonBool';
from_annot: boolean;
value: boolean;
}
export interface SingletonBigIntType extends BaseFlowType {
kind: 'SingletonBigInt';
from_annot: boolean;
value: string;
}
export interface TypeType extends BaseFlowType {
kind: 'Type';
type_kind: TypeTKind;
type: FlowType;
}
export type TypeTKind =
| 'TypeAliasKind'
| 'TypeParamKind'
| 'OpaqueKind'
| 'ImportTypeofKind'
| 'ImportClassKind'
| 'ImportEnumKind'
| 'InstanceKind'
| 'RenderTypeKind';
export interface PolyType extends BaseFlowType {
kind: 'Poly';
tparams: Array<TypeParam>;
t_out: FlowType;
id: string;
}
export interface ReactAbstractComponentType extends BaseFlowType {
kind: 'ReactAbstractComponent';
config: FlowType;
renders: FlowType;
instance: ComponentInstance;
component_kind: ComponentKind;
}
export type ComponentInstance =
| {kind: 'RefSetterProp'; type: FlowType}
| {kind: 'Omitted'};
export type ComponentKind =
| {kind: 'Structural'}
| {kind: 'Nominal'; id: string; name: string; types: Array<FlowType> | null};
export interface RendersType extends BaseFlowType {
kind: 'Renders';
form: CanonicalRendersForm;
}
export interface EnumValueType extends BaseFlowType {
kind: 'EnumValue';
enum_info: EnumInfo;
}
export interface EnumObjectType extends BaseFlowType {
kind: 'EnumObject';
enum_value_t: FlowType;
enum_info: EnumInfo;
}
// ObjKind types
export type ObjKind =
| {kind: 'Exact'}
| {kind: 'Inexact'}
| {kind: 'Indexed'; dicttype: DictType};
// DictType definition
export interface DictType {
dict_name: string | null;
key: FlowType;
value: FlowType;
dict_polarity: Polarity;
}
// ArrType types
export type ArrTypeObj = ArrayAT | TupleAT | ROArrayAT;
export interface ArrayAT {
kind: 'ArrayAT';
elem_t: FlowType;
}
export interface TupleAT {
kind: 'TupleAT';
elem_t: FlowType;
elements: Array<TupleElement>;
min_arity: number;
max_arity: number;
inexact: boolean;
}
export interface ROArrayAT {
kind: 'ROArrayAT';
elem_t: FlowType;
}
// TupleElement definition
export interface TupleElement {
name: string | null;
t: FlowType;
polarity: Polarity;
optional: boolean;
}
// Flags definition
export interface Flags {
obj_kind: ObjKind;
}
// Property types
export type Property =
| FieldProperty
| GetProperty
| SetProperty
| GetSetProperty
| MethodProperty;
export interface FieldProperty {
kind: 'Field';
type: FlowType;
polarity: Polarity;
}
export interface GetProperty {
kind: 'Get';
type: FlowType;
}
export interface SetProperty {
kind: 'Set';
type: FlowType;
}
export interface GetSetProperty {
kind: 'GetSet';
get_type: FlowType;
set_type: FlowType;
}
export interface MethodProperty {
kind: 'Method';
type: FlowType;
}
// PropertyMap definition
export interface PropertyMap {
[key: string]: Property; // For other properties in the map
}
// ObjType definition
export interface ObjTypeObj {
flags: Flags;
props: PropertyMap;
proto_t: FlowType;
call_t: number | null;
}
// FunType definition
export interface FunTypeObj {
this_t: {
type: FlowType;
status: ThisStatus;
};
params: Array<{
name: string | null;
type: FlowType;
}>;
rest_param: null | {
name: string | null;
type: FlowType;
};
return_t: FlowType;
type_guard: null | {
inferred: boolean;
param_name: string;
type_guard: FlowType;
one_sided: boolean;
};
effect: Effect;
}
// ThisStatus types
export type ThisStatus =
| {kind: 'This_Method'; unbound: boolean}
| {kind: 'This_Function'};
// Effect types
export type Effect =
| {kind: 'HookDecl'; id: string}
| {kind: 'HookAnnot'}
| {kind: 'ArbitraryEffect'}
| {kind: 'AnyEffect'};
// Destructor types
export type Destructor =
| NonMaybeTypeDestructor
| PropertyTypeDestructor
| ElementTypeDestructor
| OptionalIndexedAccessNonMaybeTypeDestructor
| OptionalIndexedAccessResultTypeDestructor
| ExactTypeDestructor
| ReadOnlyTypeDestructor
| PartialTypeDestructor
| RequiredTypeDestructor
| SpreadTypeDestructor
| SpreadTupleTypeDestructor
| RestTypeDestructor
| ValuesTypeDestructor
| ConditionalTypeDestructor
| TypeMapDestructor
| ReactElementPropsTypeDestructor
| ReactElementConfigTypeDestructor
| ReactCheckComponentConfigDestructor
| ReactDRODestructor
| MakeHooklikeDestructor
| MappedTypeDestructor
| EnumTypeDestructor;
export interface NonMaybeTypeDestructor {
kind: 'NonMaybeType';
}
export interface PropertyTypeDestructor {
kind: 'PropertyType';
name: string;
}
export interface ElementTypeDestructor {
kind: 'ElementType';
index_type: FlowType;
}
export interface OptionalIndexedAccessNonMaybeTypeDestructor {
kind: 'OptionalIndexedAccessNonMaybeType';
index: OptionalIndexedAccessIndex;
}
export type OptionalIndexedAccessIndex =
| {kind: 'StrLitIndex'; name: string}
| {kind: 'TypeIndex'; type: FlowType};
export interface OptionalIndexedAccessResultTypeDestructor {
kind: 'OptionalIndexedAccessResultType';
}
export interface ExactTypeDestructor {
kind: 'ExactType';
}
export interface ReadOnlyTypeDestructor {
kind: 'ReadOnlyType';
}
export interface PartialTypeDestructor {
kind: 'PartialType';
}
export interface RequiredTypeDestructor {
kind: 'RequiredType';
}
export interface SpreadTypeDestructor {
kind: 'SpreadType';
target: SpreadTarget;
operands: Array<SpreadOperand>;
operand_slice: Slice | null;
}
export type SpreadTarget =
| {kind: 'Value'; make_seal: 'Sealed' | 'Frozen' | 'As_Const'}
| {kind: 'Annot'; make_exact: boolean};
export type SpreadOperand = {kind: 'Type'; type: FlowType} | Slice;
export interface Slice {
kind: 'Slice';
prop_map: PropertyMap;
generics: Array<string>;
dict: DictType | null;
reachable_targs: Array<{
type: FlowType;
polarity: Polarity;
}>;
}
export interface SpreadTupleTypeDestructor {
kind: 'SpreadTupleType';
inexact: boolean;
resolved_rev: string;
unresolved: string;
}
export interface RestTypeDestructor {
kind: 'RestType';
merge_mode: RestMergeMode;
type: FlowType;
}
export type RestMergeMode =
| {kind: 'SpreadReversal'}
| {kind: 'ReactConfigMerge'; polarity: Polarity}
| {kind: 'Omit'};
export interface ValuesTypeDestructor {
kind: 'ValuesType';
}
export interface ConditionalTypeDestructor {
kind: 'ConditionalType';
distributive_tparam_name: string | null;
infer_tparams: string;
extends_t: FlowType;
true_t: FlowType;
false_t: FlowType;
}
export interface TypeMapDestructor {
kind: 'ObjectKeyMirror';
}
export interface ReactElementPropsTypeDestructor {
kind: 'ReactElementPropsType';
}
export interface ReactElementConfigTypeDestructor {
kind: 'ReactElementConfigType';
}
export interface ReactCheckComponentConfigDestructor {
kind: 'ReactCheckComponentConfig';
props: {
[key: string]: Property;
};
}
export interface ReactDRODestructor {
kind: 'ReactDRO';
dro_type:
| 'HookReturn'
| 'HookArg'
| 'Props'
| 'ImmutableAnnot'
| 'DebugAnnot';
}
export interface MakeHooklikeDestructor {
kind: 'MakeHooklike';
}
export interface MappedTypeDestructor {
kind: 'MappedType';
homomorphic: Homomorphic;
distributive_tparam_name: string | null;
property_type: FlowType;
mapped_type_flags: {
variance: Polarity;
optional: 'MakeOptional' | 'RemoveOptional' | 'KeepOptionality';
};
}
export type Homomorphic =
| {kind: 'Homomorphic'}
| {kind: 'Unspecialized'}
| {kind: 'SemiHomomorphic'; type: FlowType};
export interface EnumTypeDestructor {
kind: 'EnumType';
}
// Union of all possible Flow types
export type FlowType =
| OpenType
| DefType
| EvalType
| GenericType
| ThisInstanceType
| ThisTypeAppType
| TypeAppType
| FunProtoType
| ObjProtoType
| NullProtoType
| FunProtoBindType
| IntersectionType
| UnionType
| MaybeType
| OptionalType
| KeysType
| AnnotType
| OpaqueType
| NamespaceType
| AnyType
| StrUtilType;

View File

@@ -0,0 +1,132 @@
import {CompilerError, SourceLocation} from '..';
import {ErrorCode} from '../CompilerError';
import {
ConcreteType,
printConcrete,
printType,
StructuralValue,
Type,
VariableId,
} from './Types';
export function unsupportedLanguageFeature(
desc: string,
loc: SourceLocation,
): never {
CompilerError.throwFromCode(ErrorCode.INVALID_JAVASCRIPT_AST, {
description: `Typedchecker does not currently support language feature: ${desc}`,
loc,
});
}
export type UnificationError =
| {
kind: 'TypeUnification';
left: ConcreteType<Type>;
right: ConcreteType<Type>;
}
| {
kind: 'StructuralUnification';
left: StructuralValue;
right: ConcreteType<Type>;
};
function printUnificationError(err: UnificationError): string {
if (err.kind === 'TypeUnification') {
return `${printConcrete(err.left, printType)} is incompatible with ${printConcrete(err.right, printType)}`;
} else {
return `structural ${err.left.kind} is incompatible with ${printConcrete(err.right, printType)}`;
}
}
export function raiseUnificationErrors(
errs: null | Array<UnificationError>,
loc: SourceLocation,
): void {
if (errs != null) {
if (errs.length === 0) {
CompilerError.invariant(false, {
reason: 'Should not have array of zero errors',
loc,
});
} else if (errs.length === 1) {
CompilerError.throwFromCode(ErrorCode.INVALID_JAVASCRIPT_AST, {
description: `Unable to unify types because ${printUnificationError(errs[0])}`,
loc,
});
} else {
const messages = errs
.map(err => `\t* ${printUnificationError(err)}`)
.join('\n');
CompilerError.throwFromCode(ErrorCode.INVALID_JAVASCRIPT_AST, {
description: `Unable to unify types because:\n${messages}`,
loc,
});
}
}
}
export function unresolvableTypeVariable(
id: VariableId,
loc: SourceLocation,
): never {
CompilerError.throwFromCode(ErrorCode.INVALID_JAVASCRIPT_AST, {
description: `Unable to resolve free variable ${id} to a concrete type`,
loc,
});
}
export function cannotAddVoid(explicit: boolean, loc: SourceLocation): never {
if (explicit) {
CompilerError.throwFromCode(ErrorCode.INVALID_JAVASCRIPT_AST, {
description: `Undefined is not a valid operand of \`+\``,
loc,
});
} else {
CompilerError.throwFromCode(ErrorCode.INVALID_JAVASCRIPT_AST, {
description: `Value may be undefined, which is not a valid operand of \`+\``,
loc,
});
}
}
export function unsupportedTypeAnnotation(
desc: string,
loc: SourceLocation,
): never {
CompilerError.throwFromCode(ErrorCode.INVALID_JAVASCRIPT_AST, {
description: `Typedchecker does not currently support type annotation: ${desc}`,
loc,
});
}
export function checkTypeArgumentArity(
desc: string,
expected: number,
actual: number,
loc: SourceLocation,
): void {
if (expected !== actual) {
CompilerError.throwFromCode(ErrorCode.INVALID_JAVASCRIPT_AST, {
description: `Expected ${desc} to have ${expected} type parameters, got ${actual}`,
loc,
});
}
}
export function notAFunction(desc: string, loc: SourceLocation): void {
CompilerError.throwFromCode(ErrorCode.INVALID_JAVASCRIPT_AST, {
description: `Cannot call ${desc} because it is not a function`,
loc,
});
}
export function notAPolymorphicFunction(
desc: string,
loc: SourceLocation,
): void {
CompilerError.throwFromCode(ErrorCode.INVALID_JAVASCRIPT_AST, {
description: `Cannot call ${desc} with type arguments because it is not a polymorphic function`,
loc,
});
}

View File

@@ -0,0 +1,312 @@
import {GeneratedSource} from '../HIR';
import {assertExhaustive} from '../Utils/utils';
import {unsupportedLanguageFeature} from './TypeErrors';
import {
ConcreteType,
ResolvedType,
TypeParameter,
TypeParameterId,
DEBUG,
printConcrete,
printType,
} from './Types';
export function substitute(
type: ConcreteType<ResolvedType>,
typeParameters: Array<TypeParameter<ResolvedType>>,
typeArguments: Array<ResolvedType>,
): ResolvedType {
const substMap = new Map<TypeParameterId, ResolvedType>();
for (let i = 0; i < typeParameters.length; i++) {
// TODO: Length checks to make sure type params match up with args
const typeParameter = typeParameters[i];
const typeArgument = typeArguments[i];
substMap.set(typeParameter.id, typeArgument);
}
const substitutionFunction = (t: ResolvedType): ResolvedType => {
// TODO: We really want a stateful mapper or visitor here so that we can model nested polymorphic types
if (t.type.kind === 'Generic' && substMap.has(t.type.id)) {
const substitutedType = substMap.get(t.type.id)!;
return substitutedType;
}
return {
kind: 'Concrete',
type: mapType(substitutionFunction, t.type),
platform: t.platform,
};
};
const substituted = mapType(substitutionFunction, type);
if (DEBUG) {
let substs = '';
for (let i = 0; i < typeParameters.length; i++) {
const typeParameter = typeParameters[i];
const typeArgument = typeArguments[i];
substs += `[${typeParameter.name}${typeParameter.id} := ${printType(typeArgument)}]`;
}
console.log(
`${printConcrete(type, printType)}${substs} = ${printConcrete(substituted, printType)}`,
);
}
return {kind: 'Concrete', type: substituted, platform: /* TODO */ 'shared'};
}
export function mapType<T, U>(
f: (t: T) => U,
type: ConcreteType<T>,
): ConcreteType<U> {
switch (type.kind) {
case 'Mixed':
case 'Number':
case 'String':
case 'Boolean':
case 'Void':
return type;
case 'Nullable':
return {
kind: 'Nullable',
type: f(type.type),
};
case 'Array':
return {
kind: 'Array',
element: f(type.element),
};
case 'Set':
return {
kind: 'Set',
element: f(type.element),
};
case 'Map':
return {
kind: 'Map',
key: f(type.key),
value: f(type.value),
};
case 'Function':
return {
kind: 'Function',
typeParameters:
type.typeParameters?.map(param => ({
id: param.id,
name: param.name,
bound: f(param.bound),
})) ?? null,
params: type.params.map(f),
returnType: f(type.returnType),
};
case 'Component': {
return {
kind: 'Component',
children: type.children != null ? f(type.children) : null,
props: new Map([...type.props.entries()].map(([k, v]) => [k, f(v)])),
};
}
case 'Generic':
return {
kind: 'Generic',
id: type.id,
bound: f(type.bound),
};
case 'Object':
return type;
case 'Tuple':
return {
kind: 'Tuple',
id: type.id,
members: type.members.map(f),
};
case 'Structural':
return type;
case 'Enum':
case 'Union':
case 'Instance':
unsupportedLanguageFeature(type.kind, GeneratedSource);
default:
assertExhaustive(type, 'Unknown type kind');
}
}
export function diff<R, T>(
a: ConcreteType<T>,
b: ConcreteType<T>,
onChild: (a: T, b: T) => R,
onChildMismatch: (child: R, cur: R) => R,
onMismatch: (a: ConcreteType<T>, b: ConcreteType<T>, cur: R) => R,
init: R,
): R {
let errors = init;
// Check if kinds match
if (a.kind !== b.kind) {
errors = onMismatch(a, b, errors);
return errors;
}
// Based on kind, check other properties
switch (a.kind) {
case 'Mixed':
case 'Number':
case 'String':
case 'Boolean':
case 'Void':
// Simple types, no further checks needed
break;
case 'Nullable':
// Check the nested type
errors = onChildMismatch(onChild(a.type, (b as typeof a).type), errors);
break;
case 'Array':
case 'Set':
// Check the element type
errors = onChildMismatch(
onChild(a.element, (b as typeof a).element),
errors,
);
break;
case 'Map':
// Check both key and value types
errors = onChildMismatch(onChild(a.key, (b as typeof a).key), errors);
errors = onChildMismatch(onChild(a.value, (b as typeof a).value), errors);
break;
case 'Function': {
const bFunc = b as typeof a;
// Check type parameters
if ((a.typeParameters == null) !== (bFunc.typeParameters == null)) {
errors = onMismatch(a, b, errors);
}
if (a.typeParameters != null && bFunc.typeParameters != null) {
if (a.typeParameters.length !== bFunc.typeParameters.length) {
errors = onMismatch(a, b, errors);
}
// Type parameters are just numbers, so we can compare them directly
for (let i = 0; i < a.typeParameters.length; i++) {
if (a.typeParameters[i] !== bFunc.typeParameters[i]) {
errors = onMismatch(a, b, errors);
}
}
}
// Check parameters
if (a.params.length !== bFunc.params.length) {
errors = onMismatch(a, b, errors);
}
for (let i = 0; i < a.params.length; i++) {
errors = onChildMismatch(onChild(a.params[i], bFunc.params[i]), errors);
}
// Check return type
errors = onChildMismatch(onChild(a.returnType, bFunc.returnType), errors);
break;
}
case 'Component': {
const bComp = b as typeof a;
// Check children
if (a.children !== bComp.children) {
errors = onMismatch(a, b, errors);
}
// Check props
if (a.props.size !== bComp.props.size) {
errors = onMismatch(a, b, errors);
}
for (const [k, v] of a.props) {
const bProp = bComp.props.get(k);
if (bProp == null) {
errors = onMismatch(a, b, errors);
} else {
errors = onChildMismatch(onChild(v, bProp), errors);
}
}
break;
}
case 'Generic': {
// Check that the type parameter IDs match
if (a.id !== (b as typeof a).id) {
errors = onMismatch(a, b, errors);
}
break;
}
case 'Structural': {
const bStruct = b as typeof a;
// Check that the structural IDs match
if (a.id !== bStruct.id) {
errors = onMismatch(a, b, errors);
}
break;
}
case 'Object': {
const bNom = b as typeof a;
// Check that the nominal IDs match
if (a.id !== bNom.id) {
errors = onMismatch(a, b, errors);
}
break;
}
case 'Tuple': {
const bTuple = b as typeof a;
// Check that the tuple IDs match
if (a.id !== bTuple.id) {
errors = onMismatch(a, b, errors);
}
for (let i = 0; i < a.members.length; i++) {
errors = onChildMismatch(
onChild(a.members[i], bTuple.members[i]),
errors,
);
}
break;
}
case 'Enum':
case 'Instance':
case 'Union': {
unsupportedLanguageFeature(a.kind, GeneratedSource);
}
default:
assertExhaustive(a, 'Unknown type kind');
}
return errors;
}
export function filterOptional(t: ResolvedType): ResolvedType {
if (t.kind === 'Concrete' && t.type.kind === 'Nullable') {
return t.type.type;
}
return t;
}

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,7 @@ import {
CompilerDiagnostic,
CompilerError,
CompilerSuggestionOperation,
ErrorCode,
ErrorSeverity,
} from '../CompilerError';
import {Err, Ok, Result} from '../Utils/Result';
@@ -170,14 +171,12 @@ export function lower(
);
} else {
builder.errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.Todo,
category: `Handle ${param.node.type} parameters`,
CompilerDiagnostic.fromCode(ErrorCode.UNKNOWN_FUNCTION_PARAMETERS, {
description: `[BuildHIR] Add support for ${param.node.type} parameters.`,
}).withDetail({
kind: 'error',
loc: param.node.loc ?? null,
message: 'Unsupported parameter type',
message: `Unsupported parameter type: ${param.node.type}`,
}),
);
}
@@ -201,14 +200,12 @@ export function lower(
directives = body.get('directives').map(d => d.node.value.value);
} else {
builder.errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidJS,
category: `Unexpected function body kind`,
CompilerDiagnostic.fromCode(ErrorCode.INVALID_JAVASCRIPT_AST, {
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',
message: 'Change this to a block statement or expression',
}),
);
}
@@ -769,12 +766,12 @@ function lowerStatement(
const testExpr = case_.get('test');
if (testExpr.node == null) {
if (hasDefault) {
builder.errors.push({
reason: `Expected at most one \`default\` branch in a switch statement, this code should have failed to parse`,
severity: ErrorSeverity.InvalidJS,
loc: case_.node.loc ?? null,
suggestions: null,
});
builder.errors.pushErrorCode(
ErrorCode.INVALID_SYNTAX_MULTIPLE_DEFAULTS,
{
loc: case_.node.loc ?? null,
},
);
break;
}
hasDefault = true;
@@ -886,19 +883,20 @@ function lowerStatement(
if (builder.isContextIdentifier(id)) {
if (kind === InstructionKind.Const) {
const declRangeStart = declaration.parentPath.node.start!;
builder.errors.push({
reason: `Expect \`const\` declaration not to be reassigned`,
severity: ErrorSeverity.InvalidJS,
loc: id.node.loc ?? null,
suggestions: [
{
description: 'Change to a `let` declaration',
op: CompilerSuggestionOperation.Replace,
range: [declRangeStart, declRangeStart + 5], // "const".length
text: 'let',
},
],
});
builder.errors.pushErrorCode(
ErrorCode.INVALID_SYNTAX_REASSIGNED_CONST,
{
loc: id.node.loc ?? null,
suggestions: [
{
description: 'Change to a `let` declaration',
op: CompilerSuggestionOperation.Replace,
range: [declRangeStart, declRangeStart + 5], // "const".length
text: 'let',
},
],
},
);
}
lowerValueToTemporary(builder, {
kind: 'DeclareContext',
@@ -932,13 +930,13 @@ function lowerStatement(
}
}
} else {
builder.errors.push({
reason: `Expected variable declaration to be an identifier if no initializer was provided`,
description: `Got a \`${id.type}\``,
severity: ErrorSeverity.InvalidJS,
loc: stmt.node.loc ?? null,
suggestions: null,
});
builder.errors.pushErrorCode(
ErrorCode.INVALID_SYNTAX_BAD_VARIABLE_DECL,
{
description: `Got a \`${id.type}\``,
loc: stmt.node.loc ?? null,
},
);
}
}
return;
@@ -1374,12 +1372,8 @@ function lowerStatement(
return;
}
case 'WithStatement': {
builder.errors.push({
reason: `JavaScript 'with' syntax is not supported`,
description: `'with' syntax is considered deprecated and removed from JavaScript standards, consider alternatives`,
severity: ErrorSeverity.UnsupportedJS,
builder.errors.pushErrorCode(ErrorCode.UNSUPPORTED_WITH, {
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
@@ -1394,12 +1388,8 @@ function lowerStatement(
* and complex enough to support that we don't anticipate supporting anytime soon. Developers
* are encouraged to lift classes out of component/hook declarations.
*/
builder.errors.push({
reason: 'Inline `class` declarations are not supported',
description: `Move class declarations outside of components/hooks`,
severity: ErrorSeverity.UnsupportedJS,
builder.errors.pushErrorCode(ErrorCode.UNSUPPORTED_INNER_CLASS, {
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
@@ -1423,12 +1413,8 @@ function lowerStatement(
case 'ImportDeclaration':
case 'TSExportAssignment':
case 'TSImportEqualsDeclaration': {
builder.errors.push({
reason:
'JavaScript `import` and `export` statements may only appear at the top level of a module',
severity: ErrorSeverity.InvalidJS,
builder.errors.pushErrorCode(ErrorCode.INVALID_IMPORT_EXPORT, {
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
@@ -1438,12 +1424,8 @@ function lowerStatement(
return;
}
case 'TSNamespaceExportDeclaration': {
builder.errors.push({
reason:
'TypeScript `namespace` statements may only appear at the top level of a module',
severity: ErrorSeverity.InvalidJS,
builder.errors.pushErrorCode(ErrorCode.INVALID_TS_NAMESPACE, {
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
@@ -1698,12 +1680,8 @@ function lowerExpression(
const expr = exprPath as NodePath<t.NewExpression>;
const calleePath = expr.get('callee');
if (!calleePath.isExpression()) {
builder.errors.push({
reason: `Expected an expression as the \`new\` expression receiver (v8 intrinsics are not supported)`,
description: `Got a \`${calleePath.node.type}\``,
severity: ErrorSeverity.InvalidJS,
builder.errors.pushErrorCode(ErrorCode.UNSUPPORTED_NEW_EXPRESSION, {
loc: calleePath.node.loc ?? null,
suggestions: null,
});
return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
}
@@ -1800,12 +1778,12 @@ function lowerExpression(
last = lowerExpressionToTemporary(builder, item);
}
if (last === null) {
builder.errors.push({
reason: `Expected sequence expression to have at least one expression`,
severity: ErrorSeverity.InvalidJS,
loc: expr.node.loc ?? null,
suggestions: null,
});
builder.errors.pushErrorCode(
ErrorCode.UNSUPPORTED_EMPTY_SEQUENCE_EXPRESSION,
{
loc: expr.node.loc ?? null,
},
);
} else {
lowerValueToTemporary(builder, {
kind: 'StoreLocal',
@@ -2289,18 +2267,18 @@ function lowerExpression(
});
for (const [name, locations] of Object.entries(fbtLocations)) {
if (locations.length > 1) {
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,
};
CompilerError.throwDiagnostic(
CompilerDiagnostic.fromCode(ErrorCode.TODO_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,
};
}),
}),
});
);
}
}
}
@@ -2389,11 +2367,8 @@ function lowerExpression(
const quasis = expr.get('quasis');
if (subexprs.length !== quasis.length - 1) {
builder.errors.push({
reason: `Unexpected quasi and subexpression lengths in template literal`,
severity: ErrorSeverity.InvalidJS,
builder.errors.pushErrorCode(ErrorCode.INVALID_QUASI_LENGTHS, {
loc: exprPath.node.loc ?? null,
suggestions: null,
});
return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
}
@@ -2441,24 +2416,23 @@ function lowerExpression(
};
}
} else {
builder.errors.push({
reason: `Only object properties can be deleted`,
severity: ErrorSeverity.InvalidJS,
loc: expr.node.loc ?? null,
suggestions: [
{
description: 'Remove this line',
range: [expr.node.start!, expr.node.end!],
op: CompilerSuggestionOperation.Remove,
},
],
});
builder.errors.pushErrorCode(
ErrorCode.INVALID_SYNTAX_DELETE_EXPRESSION,
{
loc: expr.node.loc ?? null,
suggestions: [
{
description: 'Remove this line',
range: [expr.node.start!, expr.node.end!],
op: CompilerSuggestionOperation.Remove,
},
],
},
);
return {kind: 'UnsupportedNode', node: expr.node, loc: exprLoc};
}
} else if (expr.node.operator === 'throw') {
builder.errors.push({
reason: `Throw expressions are not supported`,
severity: ErrorSeverity.InvalidJS,
builder.errors.pushErrorCode(ErrorCode.UNSUPPORTED_THROW_EXPRESSION, {
loc: expr.node.loc ?? null,
suggestions: [
{
@@ -3285,10 +3259,8 @@ function lowerJsxElementName(
const name = exprPath.node.name.name;
const tag = `${namespace}:${name}`;
if (namespace.indexOf(':') !== -1 || name.indexOf(':') !== -1) {
builder.errors.push({
reason: `Expected JSXNamespacedName to have no colons in the namespace or name`,
builder.errors.pushErrorCode(ErrorCode.INVALID_JSX_NAMESPACED_NAME, {
description: `Got \`${namespace}\` : \`${name}\``,
severity: ErrorSeverity.InvalidJS,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -3583,11 +3555,7 @@ function lowerIdentifier(
}
default: {
if (binding.kind === 'Global' && binding.name === 'eval') {
builder.errors.push({
reason: `The 'eval' function is not supported`,
description:
'Eval is an anti-pattern in JavaScript, and the code executed cannot be evaluated by React Compiler',
severity: ErrorSeverity.UnsupportedJS,
builder.errors.pushErrorCode(ErrorCode.UNSUPPORTED_EVAL, {
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -3653,9 +3621,7 @@ function lowerIdentifierForAssignment(
binding.bindingKind === 'const' &&
kind === InstructionKind.Reassign
) {
builder.errors.push({
reason: `Cannot reassign a \`const\` variable`,
severity: ErrorSeverity.InvalidJS,
builder.errors.pushErrorCode(ErrorCode.INVALID_SYNTAX_REASSIGNED_CONST, {
loc: path.node.loc ?? null,
description:
binding.identifier.name != null
@@ -3710,12 +3676,13 @@ function lowerAssignment(
let temporary;
if (builder.isContextIdentifier(lvalue)) {
if (kind === InstructionKind.Const && !isHoistedIdentifier) {
builder.errors.push({
reason: `Expected \`const\` declaration not to be reassigned`,
severity: ErrorSeverity.InvalidJS,
loc: lvalue.node.loc ?? null,
suggestions: null,
});
builder.errors.pushErrorCode(
ErrorCode.INVALID_SYNTAX_REASSIGNED_CONST,
{
loc: lvalue.node.loc ?? null,
suggestions: null,
},
);
}
if (
@@ -3726,7 +3693,8 @@ function lowerAssignment(
) {
builder.errors.push({
reason: `Unexpected context variable kind`,
severity: ErrorSeverity.InvalidJS,
description: `Expected one of Const, Reassign, Let, Function, got ${kind}`,
severity: ErrorSeverity.Invariant,
loc: lvalue.node.loc ?? null,
suggestions: null,
});

View File

@@ -49,6 +49,7 @@ import {
} from './ObjectShape';
import {Scope as BabelScope, NodePath} from '@babel/traverse';
import {TypeSchema} from './TypeSchema';
import {FlowTypeEnv} from '../Flood/Types';
export const ReactElementSymbolSchema = z.object({
elementSymbol: z.union([
@@ -92,7 +93,7 @@ export const MacroSchema = z.union([
z.tuple([z.string(), z.array(MacroMethodSchema)]),
]);
export type CompilerMode = 'all_features' | 'no_inferred_memo';
export type CompilerMode = 'all_features' | 'no_inferred_memo' | 'lint_only';
export type Macro = z.infer<typeof MacroSchema>;
export type MacroMethod = z.infer<typeof MacroMethodSchema>;
@@ -243,6 +244,12 @@ export const EnvironmentConfigSchema = z.object({
*/
enableUseTypeAnnotations: z.boolean().default(false),
/**
* Allows specifying a function that can populate HIR with type information from
* Flow
*/
flowTypeProvider: z.nullable(z.function().args(z.string())).default(null),
/**
* Enable a new model for mutability and aliasing inference
*/
@@ -323,6 +330,12 @@ export const EnvironmentConfigSchema = z.object({
*/
validateNoSetStateInEffects: z.boolean().default(false),
/**
* Validates that effects are not used to calculate derived data which could instead be computed
* during render.
*/
validateNoDerivedComputationsInEffects: z.boolean().default(false),
/**
* Validates against creating JSX within a try block and recommends using an error boundary
* instead.
@@ -691,6 +704,8 @@ export class Environment {
#hoistedIdentifiers: Set<t.Identifier>;
parentFunction: NodePath<t.Function>;
#flowTypeEnvironment: FlowTypeEnv | null;
constructor(
scope: BabelScope,
fnType: ReactFunctionType,
@@ -759,6 +774,26 @@ export class Environment {
this.parentFunction = parentFunction;
this.#contextIdentifiers = contextIdentifiers;
this.#hoistedIdentifiers = new Set();
if (config.flowTypeProvider != null) {
this.#flowTypeEnvironment = new FlowTypeEnv();
CompilerError.invariant(code != null, {
reason:
'Expected Environment to be initialized with source code when a Flow type provider is specified',
loc: null,
});
this.#flowTypeEnvironment.init(this, code);
} else {
this.#flowTypeEnvironment = null;
}
}
get typeContext(): FlowTypeEnv {
CompilerError.invariant(this.#flowTypeEnvironment != null, {
reason: 'Flow type environment not initialized',
loc: null,
});
return this.#flowTypeEnvironment;
}
get isInferredMemoEnabled(): boolean {
@@ -794,6 +829,14 @@ export class Environment {
}
}
logOrThrowErrors(errors: Result<void, CompilerError>): void {
if (this.compilerMode === 'lint_only') {
this.logErrors(errors);
} else {
errors.unwrap();
}
}
isContextIdentifier(node: t.Identifier): boolean {
return this.#contextIdentifiers.has(node);
}

View File

@@ -114,6 +114,99 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
returnValueKind: ValueKind.Mutable,
}),
],
[
'entries',
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [Effect.Capture],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
aliasing: {
receiver: '@receiver',
params: ['@object'],
rest: null,
returns: '@returns',
temporaries: [],
effects: [
{
kind: 'Create',
into: '@returns',
reason: ValueReason.KnownReturnSignature,
value: ValueKind.Mutable,
},
// Object values are captured into the return
{
kind: 'Capture',
from: '@object',
into: '@returns',
},
],
},
}),
],
[
'keys',
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [Effect.Read],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
aliasing: {
receiver: '@receiver',
params: ['@object'],
rest: null,
returns: '@returns',
temporaries: [],
effects: [
{
kind: 'Create',
into: '@returns',
reason: ValueReason.KnownReturnSignature,
value: ValueKind.Mutable,
},
// Only keys are captured, and keys are immutable
{
kind: 'ImmutableCapture',
from: '@object',
into: '@returns',
},
],
},
}),
],
[
'values',
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [Effect.Capture],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
aliasing: {
receiver: '@receiver',
params: ['@object'],
rest: null,
returns: '@returns',
temporaries: [],
effects: [
{
kind: 'Create',
into: '@returns',
reason: ValueReason.KnownReturnSignature,
value: ValueKind.Mutable,
},
// Object values are captured into the return
{
kind: 'Capture',
from: '@object',
into: '@returns',
},
],
},
}),
],
]),
],
[

View File

@@ -14,6 +14,8 @@ import type {HookKind} from './ObjectShape';
import {Type, makeType} from './Types';
import {z} from 'zod';
import type {AliasingEffect} from '../Inference/AliasingEffects';
import {isReservedWord} from '../Utils/Keyword';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
/*
* *******************************************************************************************
@@ -1320,12 +1322,21 @@ export function forkTemporaryIdentifier(
* original source code.
*/
export function makeIdentifierName(name: string): ValidatedIdentifier {
CompilerError.invariant(t.isValidIdentifier(name), {
reason: `Expected a valid identifier name`,
loc: GeneratedSource,
description: `\`${name}\` is not a valid JavaScript identifier`,
suggestions: null,
});
if (isReservedWord(name)) {
CompilerError.throwFromCode(
ErrorCode.INVALID_SYNTAX_RESERVED_VARIABLE_NAME,
{
loc: GeneratedSource,
description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`,
},
);
} else {
CompilerError.invariant(t.isValidIdentifier(name), {
reason: `Expected a valid identifier name`,
loc: GeneratedSource,
description: `\`${name}\` is not a valid JavaScript identifier`,
});
}
return {
kind: 'named',
value: name as ValidIdentifierName,

View File

@@ -7,7 +7,7 @@
import {Binding, NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {CompilerError, ErrorSeverity} from '../CompilerError';
import {CompilerDiagnostic, CompilerError} from '../CompilerError';
import {Environment} from './Environment';
import {
BasicBlock,
@@ -37,6 +37,7 @@ import {
mapTerminalSuccessors,
terminalFallthrough,
} from './visitors';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
/*
* *******************************************************************************************
@@ -308,19 +309,17 @@ export default class HIRBuilder {
resolveBinding(node: t.Identifier): Identifier {
if (node.name === 'fbt') {
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,
},
],
});
CompilerError.throwDiagnostic(
CompilerDiagnostic.fromCode(ErrorCode.TODO_CONFLICTING_FBT_IDENTIFIER, {
details: [
{
kind: 'error',
message: 'Rename to avoid conflict with fbt plugin',
loc: node.loc ?? GeneratedSource,
},
],
}),
);
}
const originalName = node.name;
let name = originalName;

View File

@@ -142,6 +142,7 @@ function parseAliasingSignatureConfig(
const effects = typeConfig.effects.map(
(effect: AliasingEffectConfig): AliasingEffect => {
switch (effect.kind) {
case 'ImmutableCapture':
case 'CreateFrom':
case 'Capture':
case 'Alias':

View File

@@ -111,6 +111,19 @@ export const AliasEffectSchema: z.ZodType<AliasEffectConfig> = z.object({
into: LifetimeIdSchema,
});
export type ImmutableCaptureEffectConfig = {
kind: 'ImmutableCapture';
from: string;
into: string;
};
export const ImmutableCaptureEffectSchema: z.ZodType<ImmutableCaptureEffectConfig> =
z.object({
kind: z.literal('ImmutableCapture'),
from: LifetimeIdSchema,
into: LifetimeIdSchema,
});
export type CaptureEffectConfig = {
kind: 'Capture';
from: string;
@@ -187,6 +200,7 @@ export type AliasingEffectConfig =
| AssignEffectConfig
| AliasEffectConfig
| CaptureEffectConfig
| ImmutableCaptureEffectConfig
| ImpureEffectConfig
| MutateEffectConfig
| MutateTransitiveConditionallyConfig
@@ -199,6 +213,7 @@ export const AliasingEffectSchema: z.ZodType<AliasingEffectConfig> = z.union([
AssignEffectSchema,
AliasEffectSchema,
CaptureEffectSchema,
ImmutableCaptureEffectSchema,
ImpureEffectSchema,
MutateEffectSchema,
MutateTransitiveConditionallySchema,

View File

@@ -5,12 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {
CompilerDiagnostic,
CompilerError,
ErrorSeverity,
SourceLocation,
} from '..';
import {CompilerDiagnostic, CompilerError, SourceLocation} from '..';
import {
CallExpression,
Effect,
@@ -35,6 +30,7 @@ import {
makeInstructionId,
} from '../HIR';
import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
import {Result} from '../Utils/Result';
type ManualMemoCallee = {
@@ -299,12 +295,11 @@ function extractManualMemoizationArgs(
>;
if (fnPlace == 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({
CompilerDiagnostic.fromCode(
kind === 'useMemo'
? ErrorCode.INVALID_USE_MEMO_NO_ARG0
: ErrorCode.INVALID_USE_CALLBACK_NO_ARG0,
).withDetail({
kind: 'error',
loc: instr.value.loc,
message: `Expected a callback function to be passed to ${kind}`,
@@ -314,12 +309,11 @@ function extractManualMemoizationArgs(
}
if (fnPlace.kind === 'Spread' || depsListPlace?.kind === 'Spread') {
errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: `Unexpected spread argument to ${kind}`,
description: `Unexpected spread argument to ${kind}`,
suggestions: null,
}).withDetail({
CompilerDiagnostic.fromCode(
fnPlace.kind === 'Spread'
? ErrorCode.DYNAMIC_USE_MEMO_SPREAD_ARGUMENT
: ErrorCode.DYNAMIC_USE_CALLBACK_SPREAD_ARGUMENT,
).withDetail({
kind: 'error',
loc: instr.value.loc,
message: `Unexpected spread argument to ${kind}`,
@@ -334,12 +328,9 @@ function extractManualMemoizationArgs(
);
if (maybeDepsList == null) {
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({
CompilerDiagnostic.fromCode(
ErrorCode.DYNAMIC_MANUAL_MEMO_DEPENDENCY_LIST,
).withDetail({
kind: 'error',
loc: depsListPlace.loc,
message: `Expected the dependency list for ${kind} to be an array literal`,
@@ -352,12 +343,9 @@ function extractManualMemoizationArgs(
const maybeDep = sidemap.maybeDeps.get(dep.identifier.id);
if (maybeDep == null) {
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({
CompilerDiagnostic.fromCode(
ErrorCode.COMPLEX_MANUAL_MEMO_DEPENDENCY_LIST_ENTRY,
).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\`)`,
@@ -457,16 +445,16 @@ export function dropManualMemoization(
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({
CompilerDiagnostic.fromCode(
ErrorCode.INVALID_USE_MEMO_CALLBACK_RETURN,
{
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.`,
},
).withDetail({
kind: 'error',
loc: instr.value.loc,
message: 'useMemo() callbacks must return a value',
@@ -497,12 +485,9 @@ export function dropManualMemoization(
*/
if (!sidemap.functions.has(fnPlace.identifier.id)) {
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({
CompilerDiagnostic.fromCode(
ErrorCode.DYNAMIC_MANUAL_MEMO_CALLBACK,
).withDetail({
kind: 'error',
loc: fnPlace.loc,
message: `Expected the first argument to be an inline function expression`,

View File

@@ -25,6 +25,7 @@ import {
isRefOrRefValue,
} from '../HIR';
import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
import {assertExhaustive} from '../Utils/utils';
interface State {
@@ -62,22 +63,20 @@ function inferOperandEffect(state: State, place: Place): null | FunctionEffect {
// We ignore mutations of primitives since this is not a React-specific problem
value.kind !== ValueKind.Primitive
) {
let reason = getWriteErrorReason(value);
let errorCode = getWriteErrorReason(value);
return {
kind:
value.reason.size === 1 && value.reason.has(ValueReason.Global)
? 'GlobalMutation'
: 'ReactMutation',
error: {
reason,
errorCode,
description:
place.identifier.name !== null &&
place.identifier.name.kind === 'named'
? `Found mutation of \`${place.identifier.name.value}\``
: null,
loc: place.loc,
suggestions: null,
severity: ErrorSeverity.InvalidReact,
},
};
}
@@ -266,11 +265,8 @@ export function inferInstructionFunctionEffects(
functionEffects.push({
kind: 'GlobalMutation',
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)',
errorCode: ErrorCode.INVALID_WRITE_GLOBAL,
loc: instr.loc,
suggestions: null,
severity: ErrorSeverity.InvalidReact,
},
});
break;
@@ -324,28 +320,28 @@ function isEffectSafeOutsideRender(effect: FunctionEffect): boolean {
return effect.kind === 'GlobalMutation';
}
export function getWriteErrorReason(abstractValue: AbstractValue): string {
export function getWriteErrorReason(abstractValue: AbstractValue): ErrorCode {
if (abstractValue.reason.has(ValueReason.Global)) {
return 'Modifying a variable defined outside a component or hook is not allowed. Consider using an effect';
return ErrorCode.INVALID_WRITE_GLOBAL;
} else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
return 'Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX';
return ErrorCode.INVALID_WRITE_FROZEN_VALUE_JSX;
} else if (abstractValue.reason.has(ValueReason.Context)) {
return `Modifying a value returned from 'useContext()' is not allowed.`;
return ErrorCode.INVALID_WRITE_IMMUTABLE_VALUE_USE_CONTEXT;
} else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
return 'Modifying a value returned from a function whose return value should not be mutated';
return ErrorCode.INVALID_WRITE_IMMUTABLE_VALUE_KNOWN_SIGNATURE;
} else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
return 'Modifying component props or hook arguments is not allowed. Consider using a local variable instead';
return ErrorCode.INVALID_WRITE_IMMUTABLE_ARGS;
} else if (abstractValue.reason.has(ValueReason.State)) {
return "Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead";
return ErrorCode.INVALID_WRITE_STATE;
} else if (abstractValue.reason.has(ValueReason.ReducerState)) {
return "Modifying a value returned from 'useReducer()', which should not be modified directly. Use the dispatch function to update instead";
return ErrorCode.INVALID_WRITE_REDUCER_STATE;
} else if (abstractValue.reason.has(ValueReason.Effect)) {
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()';
return ErrorCode.INVALID_WRITE_EFFECT_DEPENDENCY;
} else if (abstractValue.reason.has(ValueReason.HookCaptured)) {
return 'Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook';
return ErrorCode.INVALID_WRITE_HOOK_CAPTURED;
} else if (abstractValue.reason.has(ValueReason.HookReturn)) {
return 'Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed';
return ErrorCode.INVALID_WRITE_HOOK_RETURN;
} else {
return 'This modifies a variable that React considers immutable';
return ErrorCode.INVALID_WRITE_GENERIC;
}
}

View File

@@ -9,7 +9,6 @@ import {
CompilerDiagnostic,
CompilerError,
Effect,
ErrorSeverity,
SourceLocation,
ValueKind,
} from '..';
@@ -69,6 +68,7 @@ import {getWriteErrorReason} from './InferFunctionEffects';
import prettyFormat from 'pretty-format';
import {createTemporaryPlace} from '../HIR/HIRBuilder';
import {AliasingEffect, AliasingSignature, hashEffect} from './AliasingEffects';
import {ErrorCode, ErrorCodeDetails} from '../Utils/CompilerErrorCodes';
const DEBUG = false;
@@ -442,7 +442,7 @@ function applySignature(
const value = state.kind(effect.value);
switch (value.kind) {
case ValueKind.Frozen: {
const reason = getWriteErrorReason({
const errorCode = getWriteErrorReason({
kind: value.kind,
reason: value.reason,
context: new Set(),
@@ -455,10 +455,9 @@ function applySignature(
effects.push({
kind: 'MutateFrozen',
place: effect.value,
error: CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: 'This value cannot be modified',
description: `${reason}.`,
// TODO: remove ERROR_CODE.INVALID_WRITE and update test fixtures
error: CompilerDiagnostic.fromCode(ErrorCode.INVALID_WRITE, {
description: ErrorCodeDetails[errorCode].reason + '.',
}).withDetail({
kind: 'error',
loc: effect.value.loc,
@@ -1026,22 +1025,20 @@ function applyEffect(
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.`,
});
const diagnostic = CompilerDiagnostic.fromCode(
ErrorCode.INVALID_ACCESS_BEFORE_INIT,
);
if (hoistedAccess != null && hoistedAccess.loc != effect.value.loc) {
diagnostic.withDetail({
kind: 'error',
loc: hoistedAccess.loc,
message: `${variable ?? 'variable'} accessed before it is declared`,
message: `${variable ?? 'This variable'} is accessed before it is declared`,
});
}
diagnostic.withDetail({
kind: 'error',
loc: effect.value.loc,
message: `${variable ?? 'variable'} is declared here`,
message: `${variable ?? 'This variable'} is declared here`,
});
applyEffect(
@@ -1056,7 +1053,7 @@ function applyEffect(
effects,
);
} else {
const reason = getWriteErrorReason({
const errorCode = getWriteErrorReason({
kind: value.kind,
reason: value.reason,
context: new Set(),
@@ -1075,10 +1072,9 @@ function applyEffect(
? 'MutateFrozen'
: 'MutateGlobal',
place: effect.value,
error: CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: 'This value cannot be modified',
description: `${reason}.`,
// TODO: remove ERROR_CODE.INVALID_WRITE and update test fixtures
error: CompilerDiagnostic.fromCode(ErrorCode.INVALID_WRITE, {
description: ErrorCodeDetails[errorCode].reason + '.',
}).withDetail({
kind: 'error',
loc: effect.value.loc,
@@ -2006,15 +2002,12 @@ function computeSignatureForInstruction(
effects.push({
kind: 'MutateGlobal',
place: value.value,
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({
error: CompilerDiagnostic.fromCode(
ErrorCode.INVALID_WRITE_GLOBAL,
).withDetail({
kind: 'error',
loc: instr.loc,
message: `${variable} cannot be reassigned`,
message: `${variable} should not be reassigned`,
}),
});
effects.push({kind: 'Assign', from: value.value, into: lvalue});
@@ -2105,19 +2098,16 @@ function computeEffectsForLegacySignature(
effects.push({
kind: 'Impure',
place: receiver,
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,
message: 'Cannot call impure function',
}),
error: CompilerDiagnostic.fromCode(ErrorCode.IMPURE_FUNCTIONS).withDetail(
{
kind: 'error',
loc,
message:
signature.canonicalName != null
? `\`${signature.canonicalName}\` is an impure function. `
: 'This is an impure function.',
},
),
});
}
const stores: Array<Place> = [];

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError';
import {CompilerError} from '../CompilerError';
import {Environment} from '../HIR';
import {
AbstractValue,
@@ -48,6 +48,7 @@ import {
eachTerminalOperand,
eachTerminalSuccessor,
} from '../HIR/visitors';
import {Err, Ok, Result} from '../Utils/Result';
import {assertExhaustive, Set_isSuperset} from '../Utils/utils';
import {
inferTerminalFunctionEffects,
@@ -106,7 +107,7 @@ const UndefinedValue: InstructionValue = {
export default function inferReferenceEffects(
fn: HIRFunction,
options: {isFunctionExpression: boolean} = {isFunctionExpression: false},
): Array<CompilerErrorDetailOptions> {
): Result<void, CompilerError> {
/*
* Initial state contains function params
* TODO: include module declarations here as well
@@ -247,10 +248,17 @@ export default function inferReferenceEffects(
if (options.isFunctionExpression) {
fn.effects = functionEffects;
return [];
} else {
return transformFunctionEffectErrors(functionEffects);
const errors = transformFunctionEffectErrors(functionEffects);
if (errors.length > 0) {
const compilerError = new CompilerError();
for (const detail of errors) {
compilerError.push(detail);
}
return Err(compilerError);
}
}
return Ok(void 0);
}
type FreezeAction = {values: Set<InstructionValue>; reason: Set<ValueReason>};

View File

@@ -119,6 +119,7 @@ class FindLastUsageVisitor extends ReactiveFunctionVisitor<void> {
class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | null> {
lastUsage: Map<DeclarationId, InstructionId>;
temporaries: Map<DeclarationId, DeclarationId> = new Map();
constructor(lastUsage: Map<DeclarationId, InstructionId>) {
super();
@@ -215,6 +216,12 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
current.lvalues.add(
instr.instruction.lvalue.identifier.declarationId,
);
if (instr.instruction.value.kind === 'LoadLocal') {
this.temporaries.set(
instr.instruction.lvalue.identifier.declarationId,
instr.instruction.value.place.identifier.declarationId,
);
}
}
break;
}
@@ -236,6 +243,13 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
)) {
current.lvalues.add(lvalue.identifier.declarationId);
}
this.temporaries.set(
instr.instruction.value.lvalue.place.identifier
.declarationId,
this.temporaries.get(
instr.instruction.value.value.identifier.declarationId,
) ?? instr.instruction.value.value.identifier.declarationId,
);
} else {
log(
`Reset scope @${current.block.scope.id} from StoreLocal in [${instr.instruction.id}]`,
@@ -260,7 +274,7 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
case 'scope': {
if (
current !== null &&
canMergeScopes(current.block, instr) &&
canMergeScopes(current.block, instr, this.temporaries) &&
areLValuesLastUsedByScope(
instr.scope,
current.lvalues,
@@ -426,6 +440,7 @@ function areLValuesLastUsedByScope(
function canMergeScopes(
current: ReactiveScopeBlock,
next: ReactiveScopeBlock,
temporaries: Map<DeclarationId, DeclarationId>,
): boolean {
// Don't merge scopes with reassignments
if (
@@ -465,11 +480,14 @@ function canMergeScopes(
(next.scope.dependencies.size !== 0 &&
[...next.scope.dependencies].every(
dep =>
dep.path.length === 0 &&
isAlwaysInvalidatingType(dep.identifier.type) &&
Iterable_some(
current.scope.declarations.values(),
decl =>
decl.identifier.declarationId === dep.identifier.declarationId,
decl.identifier.declarationId === dep.identifier.declarationId ||
decl.identifier.declarationId ===
temporaries.get(dep.identifier.declarationId),
),
))
) {
@@ -477,12 +495,16 @@ function canMergeScopes(
return true;
}
log(` cannot merge scopes:`);
log(` ${printReactiveScopeSummary(current.scope)}`);
log(` ${printReactiveScopeSummary(next.scope)}`);
log(
` ${printReactiveScopeSummary(current.scope)} ${[...current.scope.declarations.values()].map(decl => decl.identifier.declarationId)}`,
);
log(
` ${printReactiveScopeSummary(next.scope)} ${[...next.scope.dependencies].map(dep => `${dep.identifier.declarationId} ${temporaries.get(dep.identifier.declarationId) ?? dep.identifier.declarationId}`)}`,
);
return false;
}
function isAlwaysInvalidatingType(type: Type): boolean {
export function isAlwaysInvalidatingType(type: Type): boolean {
switch (type.kind) {
case 'Object': {
switch (type.shapeId) {

View File

@@ -42,6 +42,7 @@ import {
import {eachInstructionOperand} from '../HIR/visitors';
import {printSourceLocationLine} from '../HIR/PrintHIR';
import {USE_FIRE_FUNCTION_NAME} from '../HIR/Environment';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
/*
* TODO(jmbrown):
@@ -50,8 +51,6 @@ import {USE_FIRE_FUNCTION_NAME} from '../HIR/Environment';
* - React.useEffect calls
*/
const CANNOT_COMPILE_FIRE = 'Cannot compile `fire`';
export function transformFire(fn: HIRFunction): void {
const context = new Context(fn.env);
replaceFireFunctions(fn, context);
@@ -178,9 +177,7 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
loc: value.args[1].loc,
description:
'You must use an array literal for an effect dependency array when that effect uses `fire()`',
severity: ErrorSeverity.Invariant,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
errorCode: ErrorCode.CANNOT_COMPILE_FIRE,
});
}
} else if (value.args.length > 1 && value.args[1].kind === 'Spread') {
@@ -188,9 +185,7 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
loc: value.args[1].place.loc,
description:
'You must use an array literal for an effect dependency array when that effect uses `fire()`',
severity: ErrorSeverity.Invariant,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
errorCode: ErrorCode.CANNOT_COMPILE_FIRE,
});
}
}
@@ -243,11 +238,9 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
} else {
context.pushError({
loc: value.loc,
errorCode: ErrorCode.CANNOT_COMPILE_FIRE,
description:
'`fire()` can only receive a function call such as `fire(fn(a,b)). Method calls and other expressions are not allowed',
severity: ErrorSeverity.InvalidReact,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
}
} else {
@@ -262,10 +255,8 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
}
context.pushError({
loc: value.loc,
errorCode: ErrorCode.CANNOT_COMPILE_FIRE,
description,
severity: ErrorSeverity.InvalidReact,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
}
} else if (value.kind === 'CallExpression') {
@@ -394,9 +385,7 @@ function ensureNoRemainingCalleeCaptures(
description: `All uses of ${calleeName} must be either used with a fire() call in \
this effect or not used with a fire() call at all. ${calleeName} was used with fire() on line \
${printSourceLocationLine(calleeInfo.fireLoc)} in this effect`,
severity: ErrorSeverity.InvalidReact,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
errorCode: ErrorCode.CANNOT_COMPILE_FIRE,
});
}
}
@@ -411,9 +400,7 @@ function ensureNoMoreFireUses(fn: HIRFunction, context: Context): void {
context.pushError({
loc: place.identifier.loc,
description: 'Cannot use `fire` outside of a useEffect function',
severity: ErrorSeverity.Invariant,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
errorCode: ErrorCode.CANNOT_COMPILE_FIRE,
});
}
}

View File

@@ -1,110 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {HIRFunction, IdentifierId, Type, typeEquals} from '../HIR';
/**
* Temporary workaround for InferTypes not propagating the types of phis.
* Previously, LeaveSSA would replace all the identifiers for each phi (operands and
* the phi itself) with a single "canonical" identifier, generally chosen as the first
* operand to flow into the phi. In case of a phi whose operand was a phi, this could
* sometimes be an operand from the earlier phi.
*
* As a result, even though InferTypes did not propagate types for phis, LeaveSSA
* could end up replacing the phi Identifier with another identifer from an operand,
* which _did_ have a type inferred.
*
* This didn't affect the initial construction of mutable ranges because InferMutableRanges
* runs before LeaveSSA - thus, the types propagated by LeaveSSA only affected later optimizations,
* notably MergeScopesThatInvalidateTogether which uses type to determine if a scope's output
* will always invalidate with its input.
*
* The long-term correct approach is to update InferTypes to infer the types of phis,
* but this is complicated because InferMutableRanges inadvertently depends on phis
* never having a known type, such that a Store effect cannot occur on a phi value.
* Once we fix InferTypes to infer phi types, then we'll also have to update InferMutableRanges
* to handle this case.
*
* As a temporary workaround, this pass propagates the type of phis and can be called
* safely *after* InferMutableRanges. Unlike LeaveSSA, this pass only propagates the
* type if all operands have the same type, it's its more correct.
*/
export function propagatePhiTypes(fn: HIRFunction): void {
/**
* We track which SSA ids have had their types propagated to handle nested ternaries,
* see the StoreLocal handling below
*/
const propagated = new Set<IdentifierId>();
for (const [, block] of fn.body.blocks) {
for (const phi of block.phis) {
/*
* We replicate the previous LeaveSSA behavior and only propagate types for
* unnamed variables. LeaveSSA would have chosen one of the operands as the
* canonical id and taken its type as the type of all identifiers. We're
* more conservative and only propagate if the types are the same and the
* phi didn't have a type inferred.
*
* Note that this can change output slightly in cases such as
* `cond ? <div /> : null`.
*
* Previously the first operand's type (BuiltInJsx) would have been propagated,
* and this expression may have been merged with subsequent reactive scopes
* since it appears (based on that type) to always invalidate.
*
* But the correct type is `BuiltInJsx | null`, which we can't express and
* so leave as a generic `Type`, which does not always invalidate and therefore
* does not merge with subsequent scopes.
*
* We also don't propagate scopes for named variables, to preserve compatibility
* with previous LeaveSSA behavior.
*/
if (
phi.place.identifier.type.kind !== 'Type' ||
phi.place.identifier.name !== null
) {
continue;
}
let type: Type | null = null;
for (const [, operand] of phi.operands) {
if (type === null) {
type = operand.identifier.type;
} else if (!typeEquals(type, operand.identifier.type)) {
type = null;
break;
}
}
if (type !== null) {
phi.place.identifier.type = type;
propagated.add(phi.place.identifier.id);
}
}
for (const instr of block.instructions) {
const {value} = instr;
switch (value.kind) {
case 'StoreLocal': {
/**
* Nested ternaries can lower to a form with an intermediate StoreLocal where
* the value.lvalue is the temporary of the outer ternary, and the value.value
* is the result of the inner ternary.
*
* This is a common pattern in practice and easy enough to support. Again, the
* long-term approach is to update InferTypes and InferMutableRanges.
*/
const lvalue = value.lvalue.place;
if (
propagated.has(value.value.identifier.id) &&
lvalue.identifier.type.kind === 'Type' &&
lvalue.identifier.name === null
) {
lvalue.identifier.type = value.value.identifier.type;
propagated.add(lvalue.identifier.id);
}
}
}
}
}
}

View File

@@ -0,0 +1,622 @@
import {ErrorSeverity} from './CompilerErrorSeverity';
export enum LinterCategory {
RULES_OF_HOOKS = 'rules-of-hooks',
IMPURE_FUNCTIONS = 'impure-functions',
JSX_IN_TRY = 'jsx-in-try',
NO_REF_ACCESS_IN_RENDER = 'no-ref-access-in-render',
VALIDATE_MANUAL_MEMO = 'validate-manual-memo',
DYNAMIC_MANUAL_MEMO = 'dynamic-manual-memo',
EXHAUSTIVE_DEPS = 'exhaustive-deps',
MEMOIZED_DEPENDENCIES = 'memoized-dependencies', // not real!
INVALID_WRITE = 'invalid-write',
CAPITALIZED_CALLS = 'capitalized-calls',
STATIC_COMPONENTS = 'static-components',
NO_SET_STATE_IN_RENDER = 'no-set-state-in-render',
NO_SET_STATE_IN_EFFECTS = 'no-set-state-in-effects',
UNNECESSARY_EFFECTS = 'unnecessary-effects',
UNSUPPORTED_SYNTAX = 'unsupported-syntax',
TODO_SYNTAX = 'todo-syntax',
COMPILER_CONFIG = 'compiler-config',
}
export enum ErrorCode {
HOOK_CALL_STATIC,
HOOK_INVALID_REFERENCE,
HOOK_CALL_REACTIVE,
HOOK_CALL_NOT_TOP_LEVEL,
IMPURE_FUNCTIONS,
JSX_IN_TRY,
NO_REF_ACCESS_IN_RENDER,
WRITE_AFTER_RENDER,
REASSIGN_AFTER_RENDER,
REASSIGN_IN_ASYNC,
INVALID_WRITE,
CAPITALIZED_CALLS,
STATIC_COMPONENTS,
INVALID_USE_MEMO_CALLBACK_RETURN,
INVALID_USE_MEMO_CALLBACK_PARAMETERS,
INVALID_USE_MEMO_CALLBACK_ASYNC,
INVALID_USE_MEMO_NO_ARG0,
INVALID_USE_CALLBACK_NO_ARG0,
DYNAMIC_MANUAL_MEMO_CALLBACK,
DYNAMIC_USE_MEMO_SPREAD_ARGUMENT,
DYNAMIC_USE_CALLBACK_SPREAD_ARGUMENT,
DYNAMIC_MANUAL_MEMO_DEPENDENCY_LIST,
COMPLEX_MANUAL_MEMO_DEPENDENCY_LIST_ENTRY,
INVALID_SET_STATE_IN_RENDER,
INVALID_SET_STATE_IN_MEMO,
INVALID_SET_STATE_IN_EFFECTS,
NO_DERIVED_COMPUTATIONS_IN_EFFECTS,
DYNAMIC_GATING_IS_NOT_IDENTIFIER,
DYNAMIC_GATING_MULTIPLE_DIRECTIVES,
FILENAME_NOT_SET,
INVALID_WRITE_GLOBAL,
INVALID_WRITE_FROZEN_VALUE_JSX,
INVALID_WRITE_IMMUTABLE_VALUE_USE_CONTEXT,
INVALID_WRITE_IMMUTABLE_VALUE_KNOWN_SIGNATURE,
INVALID_WRITE_IMMUTABLE_ARGS,
INVALID_WRITE_STATE,
INVALID_WRITE_REDUCER_STATE,
INVALID_WRITE_EFFECT_DEPENDENCY,
INVALID_WRITE_HOOK_CAPTURED,
INVALID_WRITE_HOOK_RETURN,
INVALID_ACCESS_BEFORE_INIT,
INVALID_WRITE_GENERIC,
MEMOIZED_EFFECT_DEPENDENCIES,
INVALID_SYNTAX_MULTIPLE_DEFAULTS,
INVALID_SYNTAX_REASSIGNED_CONST,
INVALID_SYNTAX_BAD_VARIABLE_DECL,
UNSUPPORTED_WITH,
UNSUPPORTED_INNER_CLASS,
INVALID_IMPORT_EXPORT,
INVALID_TS_NAMESPACE,
UNSUPPORTED_NEW_EXPRESSION,
UNSUPPORTED_EMPTY_SEQUENCE_EXPRESSION,
INVALID_QUASI_LENGTHS,
INVALID_SYNTAX_DELETE_EXPRESSION,
UNSUPPORTED_THROW_EXPRESSION,
INVALID_JSX_NAMESPACED_NAME,
UNSUPPORTED_EVAL,
INVALID_SYNTAX_RESERVED_VARIABLE_NAME,
INVALID_JAVASCRIPT_AST,
BAILOUT_ESLINT_SUPPRESSION,
BAILOUT_FLOW_SUPPRESSION,
MANUAL_MEMO_MUTATED_LATER,
MANUAL_MEMO_REMOVED,
MANUAL_MEMO_DEPENDENCIES_CONFLICT,
CANNOT_COMPILE_FIRE,
DID_NOT_INFER_DEPS,
/** Todo syntax */
TODO_CONFLICTING_FBT_IDENTIFIER,
TODO_DUPLICATE_FBT_TAGS,
UNKNOWN_FUNCTION_PARAMETERS,
}
type ErrorCodeType = {
code: ErrorCode;
description?: string;
severity: ErrorSeverity;
reason: string;
linterCategory: LinterCategory | null;
};
export const ErrorCodeDetails: Record<ErrorCode, ErrorCodeType> = {
[ErrorCode.HOOK_CALL_STATIC]: {
code: ErrorCode.HOOK_CALL_STATIC,
severity: ErrorSeverity.InvalidReact,
reason:
'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)',
linterCategory: LinterCategory.RULES_OF_HOOKS,
},
[ErrorCode.HOOK_INVALID_REFERENCE]: {
code: ErrorCode.HOOK_INVALID_REFERENCE,
severity: ErrorSeverity.InvalidReact,
reason:
'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',
linterCategory: LinterCategory.RULES_OF_HOOKS,
},
[ErrorCode.HOOK_CALL_REACTIVE]: {
code: ErrorCode.HOOK_CALL_REACTIVE,
severity: ErrorSeverity.InvalidReact,
reason:
'Hooks must be the same function on every render, but this value may change over time to a different function. See https://react.dev/reference/rules/react-calls-components-and-hooks#dont-dynamically-use-hooks',
linterCategory: LinterCategory.RULES_OF_HOOKS,
},
[ErrorCode.HOOK_CALL_NOT_TOP_LEVEL]: {
code: ErrorCode.HOOK_CALL_NOT_TOP_LEVEL,
severity: ErrorSeverity.InvalidReact,
reason:
'Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)',
linterCategory: LinterCategory.RULES_OF_HOOKS,
},
[ErrorCode.IMPURE_FUNCTIONS]: {
code: ErrorCode.IMPURE_FUNCTIONS,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot call impure functions during render',
description:
'Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).',
linterCategory: LinterCategory.IMPURE_FUNCTIONS,
},
[ErrorCode.JSX_IN_TRY]: {
code: ErrorCode.JSX_IN_TRY,
severity: ErrorSeverity.InvalidReact,
reason: '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)`,
linterCategory: LinterCategory.JSX_IN_TRY,
},
[ErrorCode.NO_REF_ACCESS_IN_RENDER]: {
code: ErrorCode.NO_REF_ACCESS_IN_RENDER,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot access refs during render',
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)',
linterCategory: LinterCategory.NO_REF_ACCESS_IN_RENDER,
},
[ErrorCode.INVALID_SET_STATE_IN_EFFECTS]: {
code: ErrorCode.INVALID_SET_STATE_IN_EFFECTS,
severity: ErrorSeverity.InvalidReact,
reason:
'Calling setState synchronously within an effect can trigger cascading renders',
description:
'Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. ' +
'In general, the body of an effect should do one or both of the following:\n' +
'* Update external systems with the latest state from React.\n' +
'* Subscribe for updates from some external system, calling setState in a callback function when external state changes.\n\n' +
'Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. ' +
'(https://react.dev/learn/you-might-not-need-an-effect)',
linterCategory: LinterCategory.NO_SET_STATE_IN_EFFECTS,
},
[ErrorCode.WRITE_AFTER_RENDER]: {
code: ErrorCode.WRITE_AFTER_RENDER,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot modify local variables after render completes',
description: `This argument is a function which may reassign or mutate a variable after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.`,
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.REASSIGN_AFTER_RENDER]: {
code: ErrorCode.REASSIGN_AFTER_RENDER,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot reassign local variables after render completes',
description: `Reassigning local variables after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.`,
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.REASSIGN_IN_ASYNC]: {
code: ErrorCode.REASSIGN_IN_ASYNC,
severity: ErrorSeverity.InvalidReact,
reason: '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',
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.INVALID_WRITE]: {
code: ErrorCode.INVALID_WRITE,
severity: ErrorSeverity.InvalidReact,
reason: 'This value cannot be modified',
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.CAPITALIZED_CALLS]: {
code: ErrorCode.CAPITALIZED_CALLS,
severity: ErrorSeverity.InvalidReact,
reason:
'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',
linterCategory: LinterCategory.CAPITALIZED_CALLS,
},
[ErrorCode.STATIC_COMPONENTS]: {
code: ErrorCode.STATIC_COMPONENTS,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot create components during render',
linterCategory: LinterCategory.STATIC_COMPONENTS,
},
[ErrorCode.INVALID_USE_MEMO_NO_ARG0]: {
code: ErrorCode.INVALID_USE_MEMO_NO_ARG0,
severity: ErrorSeverity.InvalidReact,
reason: `Expected a callback function to be passed to useMemo`,
linterCategory: LinterCategory.VALIDATE_MANUAL_MEMO,
},
[ErrorCode.INVALID_USE_CALLBACK_NO_ARG0]: {
code: ErrorCode.INVALID_USE_CALLBACK_NO_ARG0,
severity: ErrorSeverity.InvalidReact,
reason: `Expected a callback function to be passed to useCallback`,
linterCategory: LinterCategory.VALIDATE_MANUAL_MEMO,
},
[ErrorCode.DYNAMIC_USE_MEMO_SPREAD_ARGUMENT]: {
code: ErrorCode.DYNAMIC_USE_MEMO_SPREAD_ARGUMENT,
severity: ErrorSeverity.InvalidReact,
reason: 'Unexpected spread argument to useMemo',
linterCategory: LinterCategory.DYNAMIC_MANUAL_MEMO,
},
[ErrorCode.DYNAMIC_USE_CALLBACK_SPREAD_ARGUMENT]: {
code: ErrorCode.DYNAMIC_USE_CALLBACK_SPREAD_ARGUMENT,
severity: ErrorSeverity.InvalidReact,
reason: 'Unexpected spread argument to useCallback',
linterCategory: LinterCategory.DYNAMIC_MANUAL_MEMO,
},
[ErrorCode.INVALID_USE_MEMO_CALLBACK_PARAMETERS]: {
code: ErrorCode.INVALID_USE_MEMO_CALLBACK_PARAMETERS,
severity: ErrorSeverity.InvalidReact,
reason: '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.',
linterCategory: LinterCategory.VALIDATE_MANUAL_MEMO,
},
[ErrorCode.INVALID_USE_MEMO_CALLBACK_ASYNC]: {
code: ErrorCode.INVALID_USE_MEMO_CALLBACK_ASYNC,
severity: ErrorSeverity.InvalidReact,
reason: 'useMemo() callbacks may not be async or generator functions',
description:
'useMemo() callbacks are called once and must synchronously return a value.',
linterCategory: LinterCategory.VALIDATE_MANUAL_MEMO,
},
[ErrorCode.INVALID_USE_MEMO_CALLBACK_RETURN]: {
code: ErrorCode.INVALID_USE_MEMO_CALLBACK_RETURN,
severity: ErrorSeverity.InvalidReact,
reason: 'useMemo() callbacks must return a value',
linterCategory: LinterCategory.VALIDATE_MANUAL_MEMO,
},
[ErrorCode.DYNAMIC_MANUAL_MEMO_CALLBACK]: {
code: ErrorCode.DYNAMIC_MANUAL_MEMO_CALLBACK,
severity: ErrorSeverity.InvalidReact,
reason: `Expected the first argument to be an inline function expression`,
linterCategory: LinterCategory.DYNAMIC_MANUAL_MEMO,
},
[ErrorCode.DYNAMIC_MANUAL_MEMO_DEPENDENCY_LIST]: {
code: ErrorCode.DYNAMIC_MANUAL_MEMO_DEPENDENCY_LIST,
severity: ErrorSeverity.InvalidReact,
reason: `Expected the dependency list of useMemo or useCallback to be an array literal`,
linterCategory: LinterCategory.DYNAMIC_MANUAL_MEMO,
},
[ErrorCode.COMPLEX_MANUAL_MEMO_DEPENDENCY_LIST_ENTRY]: {
code: ErrorCode.COMPLEX_MANUAL_MEMO_DEPENDENCY_LIST_ENTRY,
severity: ErrorSeverity.InvalidReact,
reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
linterCategory: LinterCategory.DYNAMIC_MANUAL_MEMO,
},
[ErrorCode.INVALID_SET_STATE_IN_RENDER]: {
code: ErrorCode.INVALID_SET_STATE_IN_RENDER,
severity: ErrorSeverity.InvalidReact,
reason: 'Calling setState during render may trigger an infinite loop',
description:
'Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState)',
linterCategory: LinterCategory.NO_SET_STATE_IN_RENDER,
},
[ErrorCode.INVALID_SET_STATE_IN_MEMO]: {
code: ErrorCode.INVALID_SET_STATE_IN_MEMO,
severity: ErrorSeverity.InvalidReact,
reason: '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)',
linterCategory: LinterCategory.NO_SET_STATE_IN_RENDER,
},
[ErrorCode.NO_DERIVED_COMPUTATIONS_IN_EFFECTS]: {
code: ErrorCode.NO_DERIVED_COMPUTATIONS_IN_EFFECTS,
severity: ErrorSeverity.InvalidReact,
reason:
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
linterCategory: LinterCategory.UNNECESSARY_EFFECTS,
},
/** Invalid writes */
[ErrorCode.INVALID_WRITE_GLOBAL]: {
code: ErrorCode.INVALID_WRITE_GLOBAL,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot reassign variables declared outside of the component/hook',
description:
'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)',
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.INVALID_WRITE_FROZEN_VALUE_JSX]: {
code: ErrorCode.INVALID_WRITE_FROZEN_VALUE_JSX,
severity: ErrorSeverity.InvalidReact,
reason:
'Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX',
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.INVALID_WRITE_IMMUTABLE_VALUE_USE_CONTEXT]: {
code: ErrorCode.INVALID_WRITE_IMMUTABLE_VALUE_USE_CONTEXT,
severity: ErrorSeverity.InvalidReact,
reason: `Modifying a value returned from 'useContext()' is not allowed.`,
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.INVALID_WRITE_IMMUTABLE_VALUE_KNOWN_SIGNATURE]: {
code: ErrorCode.INVALID_WRITE_IMMUTABLE_VALUE_KNOWN_SIGNATURE,
severity: ErrorSeverity.InvalidReact,
reason:
'Modifying a value returned from a function whose return value should not be mutated',
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.INVALID_WRITE_IMMUTABLE_ARGS]: {
code: ErrorCode.INVALID_WRITE_IMMUTABLE_ARGS,
severity: ErrorSeverity.InvalidReact,
reason:
'Modifying component props or hook arguments is not allowed. Consider using a local variable instead',
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.INVALID_WRITE_STATE]: {
code: ErrorCode.INVALID_WRITE_STATE,
severity: ErrorSeverity.InvalidReact,
reason:
"Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead",
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.INVALID_WRITE_REDUCER_STATE]: {
code: ErrorCode.INVALID_WRITE_REDUCER_STATE,
severity: ErrorSeverity.InvalidReact,
reason:
"Modifying a value returned from 'useReducer()', which should not be modified directly. Use the dispatch function to update instead",
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.INVALID_WRITE_EFFECT_DEPENDENCY]: {
code: ErrorCode.INVALID_WRITE_EFFECT_DEPENDENCY,
severity: ErrorSeverity.InvalidReact,
reason:
'Modifying a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the modification before calling useEffect()',
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.INVALID_WRITE_HOOK_CAPTURED]: {
code: ErrorCode.INVALID_WRITE_HOOK_CAPTURED,
severity: ErrorSeverity.InvalidReact,
reason:
'Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook',
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.INVALID_WRITE_HOOK_RETURN]: {
code: ErrorCode.INVALID_WRITE_HOOK_RETURN,
severity: ErrorSeverity.InvalidReact,
reason:
'Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed',
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.INVALID_ACCESS_BEFORE_INIT]: {
code: ErrorCode.INVALID_ACCESS_BEFORE_INIT,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot access variable before it is declared',
description: `Reading a variable before it is initialized will prevent the earlier access from updating when this value changes over time. Instead, move the variable access to after it has been initialized`,
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.INVALID_WRITE_GENERIC]: {
code: ErrorCode.INVALID_WRITE_GENERIC,
severity: ErrorSeverity.InvalidReact,
reason: 'This modifies a variable that React considers immutable',
linterCategory: LinterCategory.INVALID_WRITE,
},
/** Compiler Config */
[ErrorCode.DYNAMIC_GATING_IS_NOT_IDENTIFIER]: {
code: ErrorCode.DYNAMIC_GATING_IS_NOT_IDENTIFIER,
severity: ErrorSeverity.InvalidReact,
reason: 'Dynamic gating directive is not a valid JavaScript identifier',
linterCategory: LinterCategory.COMPILER_CONFIG,
},
[ErrorCode.DYNAMIC_GATING_MULTIPLE_DIRECTIVES]: {
code: ErrorCode.DYNAMIC_GATING_MULTIPLE_DIRECTIVES,
severity: ErrorSeverity.InvalidReact,
reason: 'Expected a single dynamic gating directive',
linterCategory: LinterCategory.COMPILER_CONFIG,
},
[ErrorCode.FILENAME_NOT_SET]: {
code: ErrorCode.FILENAME_NOT_SET,
severity: ErrorSeverity.InvalidConfig,
reason: `Expected a filename but found none.`,
description:
"When the 'sources' config options is specified, the React compiler will only compile files with a name",
linterCategory: LinterCategory.COMPILER_CONFIG,
},
/** Effect dependencies */
[ErrorCode.MEMOIZED_EFFECT_DEPENDENCIES]: {
code: ErrorCode.MEMOIZED_EFFECT_DEPENDENCIES,
reason:
'React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior',
severity: ErrorSeverity.CannotPreserveMemoization,
linterCategory: LinterCategory.MEMOIZED_DEPENDENCIES,
},
/** Invalid / unsupported syntax */
[ErrorCode.INVALID_SYNTAX_MULTIPLE_DEFAULTS]: {
code: ErrorCode.INVALID_SYNTAX_MULTIPLE_DEFAULTS,
reason: `Expected at most one \`default\` branch in a switch statement, this code should have failed to parse`,
severity: ErrorSeverity.InvalidJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.INVALID_SYNTAX_REASSIGNED_CONST]: {
code: ErrorCode.INVALID_SYNTAX_REASSIGNED_CONST,
reason: `Expect \`const\` declaration not to be reassigned`,
severity: ErrorSeverity.InvalidJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.INVALID_SYNTAX_BAD_VARIABLE_DECL]: {
code: ErrorCode.INVALID_SYNTAX_BAD_VARIABLE_DECL,
reason: `Expected variable declaration to be an identifier if no initializer was provided`,
severity: ErrorSeverity.InvalidJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.UNSUPPORTED_WITH]: {
code: ErrorCode.UNSUPPORTED_WITH,
reason: `JavaScript 'with' syntax is not supported`,
description: `'with' syntax is considered deprecated and removed from JavaScript standards, consider alternatives`,
severity: ErrorSeverity.UnsupportedJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.UNSUPPORTED_INNER_CLASS]: {
code: ErrorCode.UNSUPPORTED_INNER_CLASS,
reason: 'Inline `class` declarations are not supported',
description: `Move class declarations outside of components/hooks`,
severity: ErrorSeverity.UnsupportedJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.INVALID_IMPORT_EXPORT]: {
code: ErrorCode.INVALID_IMPORT_EXPORT,
reason:
'JavaScript `import` and `export` statements may only appear at the top level of a module',
severity: ErrorSeverity.InvalidJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.INVALID_TS_NAMESPACE]: {
code: ErrorCode.INVALID_TS_NAMESPACE,
reason:
'TypeScript `namespace` statements may only appear at the top level of a module',
severity: ErrorSeverity.InvalidJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.UNSUPPORTED_NEW_EXPRESSION]: {
code: ErrorCode.UNSUPPORTED_NEW_EXPRESSION,
reason: `Expected an expression as the \`new\` expression receiver (v8 intrinsics are not supported)`,
severity: ErrorSeverity.InvalidJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.UNSUPPORTED_EMPTY_SEQUENCE_EXPRESSION]: {
code: ErrorCode.UNSUPPORTED_EMPTY_SEQUENCE_EXPRESSION,
reason: `Expected sequence expression to have at least one expression`,
severity: ErrorSeverity.InvalidJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.INVALID_QUASI_LENGTHS]: {
code: ErrorCode.INVALID_QUASI_LENGTHS,
reason: `Unexpected quasi and subexpression lengths in template literal`,
severity: ErrorSeverity.InvalidJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.INVALID_SYNTAX_DELETE_EXPRESSION]: {
code: ErrorCode.INVALID_SYNTAX_DELETE_EXPRESSION,
reason: `Only object properties can be deleted`,
severity: ErrorSeverity.InvalidJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.UNSUPPORTED_THROW_EXPRESSION]: {
code: ErrorCode.UNSUPPORTED_THROW_EXPRESSION,
reason: `Throw expressions are not supported`,
severity: ErrorSeverity.InvalidJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.INVALID_JSX_NAMESPACED_NAME]: {
code: ErrorCode.INVALID_JSX_NAMESPACED_NAME,
reason: `Expected JSXNamespacedName to have no colons in the namespace or name`,
severity: ErrorSeverity.InvalidJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.UNSUPPORTED_EVAL]: {
code: ErrorCode.UNSUPPORTED_EVAL,
reason: `The 'eval' function is not supported`,
description:
'Eval is an anti-pattern in JavaScript, and the code executed cannot be evaluated by React Compiler',
severity: ErrorSeverity.UnsupportedJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.INVALID_SYNTAX_RESERVED_VARIABLE_NAME]: {
code: ErrorCode.INVALID_SYNTAX_RESERVED_VARIABLE_NAME,
reason: 'Expected a non-reserved identifier name',
severity: ErrorSeverity.InvalidJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.INVALID_JAVASCRIPT_AST]: {
code: ErrorCode.INVALID_JAVASCRIPT_AST,
reason: 'Encountered invalid JavaScript',
severity: ErrorSeverity.InvalidJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.BAILOUT_ESLINT_SUPPRESSION]: {
code: ErrorCode.BAILOUT_ESLINT_SUPPRESSION,
reason:
'React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled',
description:
'React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior',
severity: ErrorSeverity.InvalidReact,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.MANUAL_MEMO_REMOVED]: {
code: ErrorCode.MANUAL_MEMO_REMOVED,
reason:
'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.',
severity: ErrorSeverity.CannotPreserveMemoization,
linterCategory: LinterCategory.TODO_SYNTAX,
},
/**
* This is left vague as fire is very experimental
*/
[ErrorCode.CANNOT_COMPILE_FIRE]: {
code: ErrorCode.CANNOT_COMPILE_FIRE,
reason: 'Cannot compile `fire`',
severity: ErrorSeverity.InvalidReact,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.DID_NOT_INFER_DEPS]: {
code: ErrorCode.DID_NOT_INFER_DEPS,
reason:
'Cannot infer dependencies of this effect. This will break your build!',
severity: ErrorSeverity.InvalidReact,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.BAILOUT_FLOW_SUPPRESSION]: {
code: ErrorCode.BAILOUT_FLOW_SUPPRESSION,
reason:
'React Compiler has skipped optimizing this component because one or more React rule violations were reported by Flow',
description:
'React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior',
severity: ErrorSeverity.InvalidReact,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
/** Syntax React Compiler may eventually support */
[ErrorCode.TODO_CONFLICTING_FBT_IDENTIFIER]: {
code: ErrorCode.TODO_CONFLICTING_FBT_IDENTIFIER,
reason: 'Support local variables named `fbt`',
description:
'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',
severity: ErrorSeverity.Todo,
linterCategory: LinterCategory.TODO_SYNTAX,
},
[ErrorCode.TODO_DUPLICATE_FBT_TAGS]: {
code: ErrorCode.TODO_DUPLICATE_FBT_TAGS,
reason: 'Support duplicate fbt tags',
severity: ErrorSeverity.Todo,
linterCategory: LinterCategory.TODO_SYNTAX,
},
[ErrorCode.UNKNOWN_FUNCTION_PARAMETERS]: {
code: ErrorCode.UNKNOWN_FUNCTION_PARAMETERS,
severity: ErrorSeverity.Todo,
reason: 'Currently unsupported function parameter syntax',
linterCategory: LinterCategory.TODO_SYNTAX,
},
[ErrorCode.MANUAL_MEMO_MUTATED_LATER]: {
code: ErrorCode.MANUAL_MEMO_MUTATED_LATER,
reason:
'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(''),
severity: ErrorSeverity.CannotPreserveMemoization,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.MANUAL_MEMO_DEPENDENCIES_CONFLICT]: {
code: ErrorCode.MANUAL_MEMO_DEPENDENCIES_CONFLICT,
reason:
'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.',
severity: ErrorSeverity.CannotPreserveMemoization,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
};

View File

@@ -0,0 +1,34 @@
export enum ErrorSeverity {
/**
* Invalid JS syntax, or valid syntax that is semantically invalid which may indicate some
* misunderstanding on the users part.
*/
InvalidJS = 'InvalidJS',
/**
* JS syntax that is not supported and which we do not plan to support. Developers should
* rewrite to use supported forms.
*/
UnsupportedJS = 'UnsupportedJS',
/**
* Code that breaks the rules of React.
*/
InvalidReact = 'InvalidReact',
/**
* Incorrect configuration of the compiler.
*/
InvalidConfig = 'InvalidConfig',
/**
* Code that can reasonably occur and that doesn't break any rules, but is unsafe to preserve
* memoization.
*/
CannotPreserveMemoization = 'CannotPreserveMemoization',
/**
* Unhandled syntax that we don't support yet.
*/
Todo = 'Todo',
/**
* An unexpected internal error in the compiler that indicates critical issues that can panic
* the compiler.
*/
Invariant = 'Invariant',
}

View File

@@ -78,6 +78,10 @@ export default class DisjointSet<T> {
return root;
}
has(item: T): boolean {
return this.#entries.has(item);
}
/*
* Forces the set into canonical form, ie with all items pointing directly to
* their root, and returns a Map representing the mapping of items to their roots.

View File

@@ -0,0 +1,87 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* https://tc39.es/ecma262/multipage/ecmascript-language-lexical-grammar.html#sec-keywords-and-reserved-words
*/
/**
* Note: `await` and `yield` are contextually allowed as identifiers.
* await: reserved inside async functions and modules
* yield: reserved inside generator functions
*
* Note: `async` is not reserved.
*/
const RESERVED_WORDS = new Set([
'break',
'case',
'catch',
'class',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'else',
'enum',
'export',
'extends',
'false',
'finally',
'for',
'function',
'if',
'import',
'in',
'instanceof',
'new',
'null',
'return',
'super',
'switch',
'this',
'throw',
'true',
'try',
'typeof',
'var',
'void',
'while',
'with',
]);
/**
* Reserved when a module has a 'use strict' directive.
*/
const STRICT_MODE_RESERVED_WORDS = new Set([
'let',
'static',
'implements',
'interface',
'package',
'private',
'protected',
'public',
]);
/**
* The names arguments and eval are not keywords, but they are subject to some restrictions in
* strict mode code.
*/
const STRICT_MODE_RESTRICTED_WORDS = new Set(['eval', 'arguments']);
/**
* Conservative check for whether an identifer name is reserved or not. We assume that code is
* written with strict mode.
*/
export function isReservedWord(identifierName: string): boolean {
return (
RESERVED_WORDS.has(identifierName) ||
STRICT_MODE_RESERVED_WORDS.has(identifierName) ||
STRICT_MODE_RESTRICTED_WORDS.has(identifierName)
);
}

View File

@@ -33,12 +33,12 @@ export function assertExhaustive(_: never, errorMsg: string): never {
// Modifies @param array in place, retaining only the items where the predicate returns true.
export function retainWhere<T>(
array: Array<T>,
predicate: (item: T) => boolean,
predicate: (item: T, index: number) => boolean,
): void {
let writeIndex = 0;
for (let readIndex = 0; readIndex < array.length; readIndex++) {
const item = array[readIndex];
if (predicate(item) === true) {
if (predicate(item, readIndex) === true) {
array[writeIndex++] = item;
}
}

View File

@@ -6,11 +6,7 @@
*/
import * as t from '@babel/types';
import {
CompilerError,
CompilerErrorDetail,
ErrorSeverity,
} from '../CompilerError';
import {CompilerError, CompilerErrorDetail} from '../CompilerError';
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
import {isHookName} from '../HIR/Environment';
import {
@@ -27,6 +23,7 @@ import {
} from '../HIR/visitors';
import {assertExhaustive} from '../Utils/utils';
import {Result} from '../Utils/Result';
import {ErrorCode, ErrorCodeDetails} from '../Utils/CompilerErrorCodes';
/**
* Represents the possible kinds of value which may be stored at a given Place during
@@ -111,8 +108,6 @@ export function validateHooksUsage(
// Once a particular hook has a conditional call error, don't report any further issues for this hook
setKind(place, Kind.Error);
const reason =
'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)';
const previousError =
typeof place.loc !== 'symbol' ? errorsByPlace.get(place.loc) : undefined;
@@ -120,15 +115,15 @@ export function validateHooksUsage(
* In some circumstances such as optional calls, we may first encounter a "hook may not be referenced as normal values" error.
* If that same place is also used as a conditional call, upgrade the error to a conditonal hook error
*/
if (previousError === undefined || previousError.reason !== reason) {
if (
previousError === undefined ||
previousError.reason !==
ErrorCodeDetails[ErrorCode.HOOK_CALL_STATIC].reason
) {
recordError(
place.loc,
new CompilerErrorDetail({
description: null,
reason,
CompilerErrorDetail.fromCode(ErrorCode.HOOK_CALL_STATIC, {
loc: place.loc,
severity: ErrorSeverity.InvalidReact,
suggestions: null,
}),
);
}
@@ -139,13 +134,8 @@ export function validateHooksUsage(
if (previousError === undefined) {
recordError(
place.loc,
new CompilerErrorDetail({
description: null,
reason:
'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',
CompilerErrorDetail.fromCode(ErrorCode.HOOK_INVALID_REFERENCE, {
loc: place.loc,
severity: ErrorSeverity.InvalidReact,
suggestions: null,
}),
);
}
@@ -156,14 +146,12 @@ export function validateHooksUsage(
if (previousError === undefined) {
recordError(
place.loc,
new CompilerErrorDetail({
description: null,
reason:
'Hooks must be the same function on every render, but this value may change over time to a different function. See https://react.dev/reference/rules/react-calls-components-and-hooks#dont-dynamically-use-hooks',
loc: place.loc,
severity: ErrorSeverity.InvalidReact,
suggestions: null,
}),
CompilerErrorDetail.fromCode(
ErrorCodeDetails[ErrorCode.HOOK_CALL_REACTIVE].code,
{
loc: place.loc,
},
),
);
}
}
@@ -424,7 +412,7 @@ export function validateHooksUsage(
}
for (const [, error] of errorsByPlace) {
errors.push(error);
errors.pushErrorDetail(error);
}
return errors.asResult();
}
@@ -446,16 +434,10 @@ function visitFunctionExpression(errors: CompilerError, fn: HIRFunction): void {
: instr.value.property;
const hookKind = getHookKind(fn.env, callee.identifier);
if (hookKind != null) {
errors.pushErrorDetail(
new CompilerErrorDetail({
severity: ErrorSeverity.InvalidReact,
reason:
'Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)',
loc: callee.loc,
description: `Cannot call ${hookKind === 'Custom' ? 'hook' : hookKind} within a function expression`,
suggestions: null,
}),
);
errors.pushErrorCode(ErrorCode.HOOK_CALL_NOT_TOP_LEVEL, {
loc: callee.loc,
description: `Cannot call ${hookKind === 'Custom' ? 'hook' : hookKind} within a function expression`,
});
}
break;
}

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..';
import {CompilerDiagnostic, CompilerError, Effect} from '..';
import {HIRFunction, IdentifierId, Place} from '../HIR';
import {
eachInstructionLValue,
@@ -13,13 +13,17 @@ import {
eachTerminalOperand,
} from '../HIR/visitors';
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
import {Ok, Result} from '../Utils/Result';
/**
* Validates that local variables cannot be reassigned after render.
* This prevents a category of bugs in which a closure captures a
* binding from one render but does not update
*/
export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
export function validateLocalsNotReassignedAfterRender(
fn: HIRFunction,
): Result<void, CompilerError> {
const contextVariables = new Set<IdentifierId>();
const reassignment = getContextReassignment(
fn,
@@ -35,18 +39,15 @@ export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
? `\`${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({
CompilerDiagnostic.fromCode(ErrorCode.REASSIGN_AFTER_RENDER).withDetail({
kind: 'error',
loc: reassignment.loc,
message: `Cannot reassign ${variable} after render completes`,
}),
);
throw errors;
return errors.asResult();
}
return Ok(undefined);
}
function getContextReassignment(
@@ -90,12 +91,9 @@ function getContextReassignment(
? `\`${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({
CompilerDiagnostic.fromCode(
ErrorCode.REASSIGN_IN_ASYNC,
).withDetail({
kind: 'error',
loc: reassignment.loc,
message: `Cannot reassign ${variable}`,

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, ErrorSeverity} from '..';
import {CompilerError} from '..';
import {
Identifier,
Instruction,
@@ -22,6 +22,7 @@ import {
ReactiveFunctionVisitor,
visitReactiveFunction,
} from '../ReactiveScopes/visitors';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
import {Result} from '../Utils/Result';
/**
@@ -108,12 +109,8 @@ class Visitor extends ReactiveFunctionVisitor<CompilerError> {
isUnmemoized(deps.identifier, this.scopes))
) {
state.push({
reason:
'React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior',
description: null,
severity: ErrorSeverity.CannotPreserveMemoization,
errorCode: ErrorCode.MEMOIZED_EFFECT_DEPENDENCIES,
loc: typeof instruction.loc !== 'symbol' ? instruction.loc : null,
suggestions: null,
});
}
}

View File

@@ -5,9 +5,10 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, EnvironmentConfig, ErrorSeverity} from '..';
import {CompilerError, EnvironmentConfig} from '..';
import {HIRFunction, IdentifierId} from '../HIR';
import {DEFAULT_GLOBALS} from '../HIR/Globals';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
import {Result} from '../Utils/Result';
export function validateNoCapitalizedCalls(
@@ -33,8 +34,6 @@ export function validateNoCapitalizedCalls(
const errors = new CompilerError();
const capitalLoadGlobals = new Map<IdentifierId, string>();
const capitalizedProperties = new Map<IdentifierId, string>();
const reason =
'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';
for (const [, block] of fn.body.blocks) {
for (const {lvalue, value} of block.instructions) {
switch (value.kind) {
@@ -55,11 +54,9 @@ export function validateNoCapitalizedCalls(
const calleeIdentifier = value.callee.identifier.id;
const calleeName = capitalLoadGlobals.get(calleeIdentifier);
if (calleeName != null) {
CompilerError.throwInvalidReact({
reason,
errors.pushErrorCode(ErrorCode.CAPITALIZED_CALLS, {
description: `${calleeName} may be a component.`,
loc: value.loc,
suggestions: null,
});
}
break;
@@ -78,12 +75,9 @@ export function validateNoCapitalizedCalls(
const propertyIdentifier = value.property.identifier.id;
const propertyName = capitalizedProperties.get(propertyIdentifier);
if (propertyName != null) {
errors.push({
severity: ErrorSeverity.InvalidReact,
reason,
errors.pushErrorCode(ErrorCode.CAPITALIZED_CALLS, {
description: `${propertyName} may be a component.`,
loc: value.loc,
suggestions: null,
});
}
break;

View File

@@ -0,0 +1,225 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, SourceLocation} from '..';
import {
ArrayExpression,
BlockId,
FunctionExpression,
HIRFunction,
IdentifierId,
isSetStateType,
isUseEffectHookType,
} from '../HIR';
import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
import {Result} from '../Utils/Result';
/**
* Validates that useEffect is not used for derived computations which could/should
* be performed in render.
*
* See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state
*
* Example:
*
* ```
* // 🔴 Avoid: redundant state and unnecessary Effect
* const [fullName, setFullName] = useState('');
* useEffect(() => {
* setFullName(firstName + ' ' + lastName);
* }, [firstName, lastName]);
* ```
*
* Instead use:
*
* ```
* // ✅ Good: calculated during rendering
* const fullName = firstName + ' ' + lastName;
* ```
*/
export function validateNoDerivedComputationsInEffects(
fn: HIRFunction,
): Result<void, CompilerError> {
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
const functions: Map<IdentifierId, FunctionExpression> = new Map();
const locals: Map<IdentifierId, IdentifierId> = new Map();
const errors = new CompilerError();
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {lvalue, value} = instr;
if (value.kind === 'LoadLocal') {
locals.set(lvalue.identifier.id, value.place.identifier.id);
} else if (value.kind === 'ArrayExpression') {
candidateDependencies.set(lvalue.identifier.id, value);
} else if (value.kind === 'FunctionExpression') {
functions.set(lvalue.identifier.id, value);
} else if (
value.kind === 'CallExpression' ||
value.kind === 'MethodCall'
) {
const callee =
value.kind === 'CallExpression' ? value.callee : value.property;
if (
isUseEffectHookType(callee.identifier) &&
value.args.length === 2 &&
value.args[0].kind === 'Identifier' &&
value.args[1].kind === 'Identifier'
) {
const effectFunction = functions.get(value.args[0].identifier.id);
const deps = candidateDependencies.get(value.args[1].identifier.id);
if (
effectFunction != null &&
deps != null &&
deps.elements.length !== 0 &&
deps.elements.every(element => element.kind === 'Identifier')
) {
const dependencies: Array<IdentifierId> = deps.elements.map(dep => {
CompilerError.invariant(dep.kind === 'Identifier', {
reason: `Dependency is checked as a place above`,
loc: value.loc,
});
return locals.get(dep.identifier.id) ?? dep.identifier.id;
});
validateEffect(
effectFunction.loweredFunc.func,
dependencies,
errors,
);
}
}
}
}
}
return errors.asResult();
}
function validateEffect(
effectFunction: HIRFunction,
effectDeps: Array<IdentifierId>,
errors: CompilerError,
): void {
for (const operand of effectFunction.context) {
if (isSetStateType(operand.identifier)) {
continue;
} else if (effectDeps.find(dep => dep === operand.identifier.id) != null) {
continue;
} else {
// Captured something other than the effect dep or setState
return;
}
}
for (const dep of effectDeps) {
if (
effectFunction.context.find(operand => operand.identifier.id === dep) ==
null
) {
// effect dep wasn't actually used in the function
return;
}
}
const seenBlocks: Set<BlockId> = new Set();
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
for (const dep of effectDeps) {
values.set(dep, [dep]);
}
const setStateLocations: Array<SourceLocation> = [];
for (const block of effectFunction.body.blocks.values()) {
for (const pred of block.preds) {
if (!seenBlocks.has(pred)) {
// skip if block has a back edge
return;
}
}
for (const phi of block.phis) {
const aggregateDeps: Set<IdentifierId> = new Set();
for (const operand of phi.operands.values()) {
const deps = values.get(operand.identifier.id);
if (deps != null) {
for (const dep of deps) {
aggregateDeps.add(dep);
}
}
}
if (aggregateDeps.size !== 0) {
values.set(phi.place.identifier.id, Array.from(aggregateDeps));
}
}
for (const instr of block.instructions) {
switch (instr.value.kind) {
case 'Primitive':
case 'JSXText':
case 'LoadGlobal': {
break;
}
case 'LoadLocal': {
const deps = values.get(instr.value.place.identifier.id);
if (deps != null) {
values.set(instr.lvalue.identifier.id, deps);
}
break;
}
case 'ComputedLoad':
case 'PropertyLoad':
case 'BinaryExpression':
case 'TemplateLiteral':
case 'CallExpression':
case 'MethodCall': {
const aggregateDeps: Set<IdentifierId> = new Set();
for (const operand of eachInstructionValueOperand(instr.value)) {
const deps = values.get(operand.identifier.id);
if (deps != null) {
for (const dep of deps) {
aggregateDeps.add(dep);
}
}
}
if (aggregateDeps.size !== 0) {
values.set(instr.lvalue.identifier.id, Array.from(aggregateDeps));
}
if (
instr.value.kind === 'CallExpression' &&
isSetStateType(instr.value.callee.identifier) &&
instr.value.args.length === 1 &&
instr.value.args[0].kind === 'Identifier'
) {
const deps = values.get(instr.value.args[0].identifier.id);
if (deps != null && new Set(deps).size === effectDeps.length) {
setStateLocations.push(instr.value.callee.loc);
} else {
// doesn't depend on any deps
return;
}
}
break;
}
default: {
return;
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
if (values.has(operand.identifier.id)) {
//
return;
}
}
seenBlocks.add(block.id);
}
for (const loc of setStateLocations) {
errors.pushErrorCode(ErrorCode.NO_DERIVED_COMPUTATIONS_IN_EFFECTS, {loc});
}
}

View File

@@ -5,7 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..';
import {CompilerDiagnostic, CompilerError, Effect} from '..';
import {ErrorCode} from '../CompilerError';
import {
FunctionEffect,
HIRFunction,
@@ -65,11 +66,7 @@ export function validateNoFreezingKnownMutableFunctions(
? `\`${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.`,
})
CompilerDiagnostic.fromCode(ErrorCode.WRITE_AFTER_RENDER)
.withDetail({
kind: 'error',
loc: operand.loc,

View File

@@ -5,9 +5,10 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
import {CompilerDiagnostic, CompilerError} from '..';
import {HIRFunction} from '../HIR';
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
import {Result} from '../Utils/Result';
/**
@@ -35,19 +36,13 @@ export function validateNoImpureFunctionsInRender(
);
if (signature != null && signature.impure === true) {
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({
CompilerDiagnostic.fromCode(ErrorCode.IMPURE_FUNCTIONS).withDetail({
kind: 'error',
loc: callee.loc,
message: 'Cannot call impure function',
message:
signature.canonicalName != null
? `\`${signature.canonicalName}\` is an impure function. `
: 'This is an impure function.',
}),
);
}

View File

@@ -5,8 +5,9 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
import {CompilerDiagnostic, CompilerError} from '..';
import {BlockId, HIRFunction} from '../HIR';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
import {Result} from '../Utils/Result';
import {retainWhere} from '../Utils/utils';
@@ -35,11 +36,7 @@ export function validateNoJSXInTryStatement(
case 'JsxExpression':
case 'JsxFragment': {
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({
CompilerDiagnostic.fromCode(ErrorCode.JSX_IN_TRY).withDetail({
kind: 'error',
loc: value.loc,
message: 'Avoid constructing JSX within try/catch',

View File

@@ -5,11 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {
CompilerDiagnostic,
CompilerError,
ErrorSeverity,
} from '../CompilerError';
import {CompilerDiagnostic, CompilerError} from '../CompilerError';
import {
BlockId,
HIRFunction,
@@ -26,6 +22,7 @@ import {
eachPatternOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
import {Err, Ok, Result} from '../Utils/Result';
import {retainWhere} from '../Utils/utils';
@@ -467,11 +464,9 @@ function validateNoRefAccessInRenderImpl(
if (fnType.fn.readRefEffect) {
didError = true;
errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetail({
CompilerDiagnostic.fromCode(
ErrorCode.NO_REF_ACCESS_IN_RENDER,
).withDetail({
kind: 'error',
loc: callee.loc,
message: `This function accesses a ref value`,
@@ -730,15 +725,13 @@ function destructure(
function guardCheck(errors: CompilerError, operand: Place, env: Env): void {
if (env.get(operand.identifier.id)?.kind === 'Guard') {
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`,
}),
CompilerDiagnostic.fromCode(ErrorCode.NO_REF_ACCESS_IN_RENDER).withDetail(
{
kind: 'error',
loc: operand.loc,
message: `Cannot access ref value during render`,
},
),
);
}
}
@@ -754,15 +747,13 @@ function validateNoRefValueAccess(
(type?.kind === 'Structure' && type.fn?.readRefEffect)
) {
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`,
}),
CompilerDiagnostic.fromCode(ErrorCode.NO_REF_ACCESS_IN_RENDER).withDetail(
{
kind: 'error',
loc: (type.kind === 'RefValue' && type.loc) || operand.loc,
message: `Cannot access ref value during render`,
},
),
);
}
}
@@ -780,15 +771,13 @@ function validateNoRefPassedToFunction(
(type?.kind === 'Structure' && type.fn?.readRefEffect)
) {
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`,
}),
CompilerDiagnostic.fromCode(ErrorCode.NO_REF_ACCESS_IN_RENDER).withDetail(
{
kind: 'error',
loc: (type.kind === 'RefValue' && type.loc) || loc,
message: `Passing a ref to a function may read its value during render`,
},
),
);
}
}
@@ -802,15 +791,13 @@ function validateNoRefUpdate(
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`,
}),
CompilerDiagnostic.fromCode(ErrorCode.NO_REF_ACCESS_IN_RENDER).withDetail(
{
kind: 'error',
loc: (type.kind === 'RefValue' && type.loc) || loc,
message: `Cannot update ref during render`,
},
),
);
}
}
@@ -823,21 +810,13 @@ function validateNoDirectRefValueAccess(
const type = destructure(env.get(operand.identifier.id));
if (type?.kind === 'RefValue') {
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`,
}),
CompilerDiagnostic.fromCode(ErrorCode.NO_REF_ACCESS_IN_RENDER).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,11 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {
CompilerDiagnostic,
CompilerError,
ErrorSeverity,
} from '../CompilerError';
import {CompilerDiagnostic, CompilerError} from '../CompilerError';
import {
HIRFunction,
IdentifierId,
@@ -20,6 +16,7 @@ import {
Place,
} from '../HIR';
import {eachInstructionValueOperand} from '../HIR/visitors';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
import {Result} from '../Utils/Result';
/**
@@ -95,19 +92,9 @@ export function validateNoSetStateInEffects(
const setState = setStateFunctions.get(arg.identifier.id);
if (setState !== undefined) {
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({
CompilerDiagnostic.fromCode(
ErrorCode.INVALID_SET_STATE_IN_EFFECTS,
).withDetail({
kind: 'error',
loc: setState.loc,
message:

View File

@@ -5,14 +5,11 @@
* LICENSE file in the root directory of this source tree.
*/
import {
CompilerDiagnostic,
CompilerError,
ErrorSeverity,
} from '../CompilerError';
import {CompilerDiagnostic, CompilerError} from '../CompilerError';
import {HIRFunction, IdentifierId, isSetStateType} from '../HIR';
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
import {eachInstructionValueOperand} from '../HIR/visitors';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
import {Result} from '../Utils/Result';
/**
@@ -127,14 +124,9 @@ function validateNoSetStateInRenderImpl(
) {
if (activeManualMemoId !== 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({
CompilerDiagnostic.fromCode(
ErrorCode.INVALID_SET_STATE_IN_MEMO,
).withDetail({
kind: 'error',
loc: callee.loc,
message: 'Found setState() within useMemo()',
@@ -142,17 +134,12 @@ function validateNoSetStateInRenderImpl(
);
} else if (unconditionalBlocks.has(block.id)) {
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({
CompilerDiagnostic.fromCode(
ErrorCode.INVALID_SET_STATE_IN_RENDER,
).withDetail({
kind: 'error',
loc: callee.loc,
message: 'Found setState() within useMemo()',
message: 'Found setState() call here',
}),
);
}

View File

@@ -5,11 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {
CompilerDiagnostic,
CompilerError,
ErrorSeverity,
} from '../CompilerError';
import {CompilerDiagnostic, CompilerError, ErrorCode} from '../CompilerError';
import {
DeclarationId,
Effect,
@@ -280,13 +276,8 @@ function validateInferredDep(
}
}
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. ',
CompilerDiagnostic.fromCode(ErrorCode.MANUAL_MEMO_DEPENDENCIES_CONFLICT, {
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')
@@ -300,9 +291,6 @@ function validateInferredDep(
: 'Inferred dependency not present in source'
}.`
: '',
]
.join('')
.trim(),
suggestions: null,
}).withDetail({
kind: 'error',
@@ -534,15 +522,9 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
!this.prunedScopes.has(identifier.scope.id)
) {
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({
CompilerDiagnostic.fromCode(
ErrorCode.MANUAL_MEMO_MUTATED_LATER,
).withDetail({
kind: 'error',
loc,
message: 'This dependency may be modified later',
@@ -582,18 +564,10 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
for (const identifier of decls) {
if (isUnmemoized(identifier, this.scopes)) {
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(),
CompilerDiagnostic.fromCode(ErrorCode.MANUAL_MEMO_REMOVED, {
description: DEBUG
? `${printIdentifier(identifier)} was not memoized.`
: '',
}).withDetail({
kind: 'error',
loc,

View File

@@ -5,12 +5,9 @@
* LICENSE file in the root directory of this source tree.
*/
import {
CompilerDiagnostic,
CompilerError,
ErrorSeverity,
} from '../CompilerError';
import {CompilerDiagnostic, CompilerError} from '../CompilerError';
import {HIRFunction, IdentifierId, SourceLocation} from '../HIR';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
import {Result} from '../Utils/Result';
/**
@@ -64,9 +61,7 @@ export function validateStaticComponents(
);
if (location != null) {
error.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: 'Cannot create components during render',
CompilerDiagnostic.fromCode(ErrorCode.STATIC_COMPONENTS, {
description: `Components created during render will reset their state each time they are created. Declare components outside of render. `,
})
.withDetail({

View File

@@ -5,12 +5,9 @@
* LICENSE file in the root directory of this source tree.
*/
import {
CompilerDiagnostic,
CompilerError,
ErrorSeverity,
} from '../CompilerError';
import {CompilerDiagnostic, CompilerError} from '../CompilerError';
import {FunctionExpression, HIRFunction, IdentifierId} from '../HIR';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
import {Result} from '../Utils/Result';
export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
@@ -73,13 +70,9 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
? 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({
CompilerDiagnostic.fromCode(
ErrorCode.INVALID_USE_MEMO_CALLBACK_PARAMETERS,
).withDetail({
kind: 'error',
loc,
message: 'Callbacks with parameters are not supported',
@@ -89,14 +82,9 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
if (body.loweredFunc.func.async || body.loweredFunc.func.generator) {
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({
CompilerDiagnostic.fromCode(
ErrorCode.INVALID_USE_MEMO_CALLBACK_ASYNC,
).withDetail({
kind: 'error',
loc: body.loc,
message: 'Async and generator functions are not supported',

View File

@@ -19,7 +19,7 @@ function Component(props) {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(7);
const $ = _c(5);
let t0;
if ($[0] !== props.x) {
t0 = foo(props.x);
@@ -31,26 +31,19 @@ function Component(props) {
const x = t0;
let t1;
if ($[2] !== props || $[3] !== x) {
t1 = function () {
const fn = function () {
const arr = [...bar(props)];
return arr.at(x);
};
t1 = fn();
$[2] = props;
$[3] = x;
$[4] = t1;
} else {
t1 = $[4];
}
const fn = t1;
let t2;
if ($[5] !== fn) {
t2 = fn();
$[5] = fn;
$[6] = t2;
} else {
t2 = $[6];
}
const fnResult = t2;
const fnResult = t1;
return fnResult;
}

View File

@@ -23,34 +23,18 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(6);
const $ = _c(2);
let t0;
if ($[0] !== props.a) {
t0 = { a: props.a };
const item = { a: props.a };
const items = [item];
t0 = items.map(_temp);
$[0] = props.a;
$[1] = t0;
} else {
t0 = $[1];
}
const item = t0;
let t1;
if ($[2] !== item) {
t1 = [item];
$[2] = item;
$[3] = t1;
} else {
t1 = $[3];
}
const items = t1;
let t2;
if ($[4] !== items) {
t2 = items.map(_temp);
$[4] = items;
$[5] = t2;
} else {
t2 = $[5];
}
const mapped = t2;
const mapped = t0;
return mapped;
}
function _temp(item_0) {

View File

@@ -21,26 +21,18 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(4);
const $ = _c(2);
const f = _temp;
let t0;
if ($[0] !== props.items) {
t0 = [...props.items].map(f);
const x = [...props.items].map(f);
t0 = [x, f];
$[0] = props.items;
$[1] = t0;
} else {
t0 = $[1];
}
const x = t0;
let t1;
if ($[2] !== x) {
t1 = [x, f];
$[2] = x;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
return t0;
}
function _temp(item) {
return item;

View File

@@ -23,27 +23,19 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function component(a) {
const $ = _c(4);
const $ = _c(2);
let t0;
if ($[0] !== a) {
t0 = { a };
const z = { a };
t0 = () => {
console.log(z);
};
$[0] = a;
$[1] = t0;
} else {
t0 = $[1];
}
const z = t0;
let t1;
if ($[2] !== z) {
t1 = () => {
console.log(z);
};
$[2] = z;
$[3] = t1;
} else {
t1 = $[3];
}
const x = t1;
const x = t0;
return x;
}

View File

@@ -23,27 +23,19 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function component(a) {
const $ = _c(4);
const $ = _c(2);
let t0;
if ($[0] !== a) {
t0 = { a };
const z = { a };
t0 = function () {
console.log(z);
};
$[0] = a;
$[1] = t0;
} else {
t0 = $[1];
}
const z = t0;
let t1;
if ($[2] !== z) {
t1 = function () {
console.log(z);
};
$[2] = z;
$[3] = t1;
} else {
t1 = $[3];
}
const x = t1;
const x = t0;
return x;
}

View File

@@ -22,35 +22,19 @@ export const FIXTURE_ENTRYPOINT = {
import { c as _c } from "react/compiler-runtime";
import { Stringify } from "shared-runtime";
function Component(t0) {
const $ = _c(6);
const $ = _c(2);
const { a } = t0;
let t1;
if ($[0] !== a) {
t1 = { a };
const z = { a };
const p = () => <Stringify>{z}</Stringify>;
t1 = p();
$[0] = a;
$[1] = t1;
} else {
t1 = $[1];
}
const z = t1;
let t2;
if ($[2] !== z) {
t2 = () => <Stringify>{z}</Stringify>;
$[2] = z;
$[3] = t2;
} else {
t2 = $[3];
}
const p = t2;
let t3;
if ($[4] !== p) {
t3 = p();
$[4] = p;
$[5] = t3;
} else {
t3 = $[5];
}
return t3;
return t1;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -25,27 +25,19 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function component(a) {
const $ = _c(4);
const $ = _c(2);
let t0;
if ($[0] !== a) {
t0 = { a };
const z = { a };
t0 = function () {
console.log(z);
};
$[0] = a;
$[1] = t0;
} else {
t0 = $[1];
}
const z = t0;
let t1;
if ($[2] !== z) {
t1 = function () {
console.log(z);
};
$[2] = z;
$[3] = t1;
} else {
t1 = $[3];
}
const x = t1;
const x = t0;
return x;
}

View File

@@ -25,29 +25,21 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function component(a) {
const $ = _c(4);
const $ = _c(2);
let t0;
if ($[0] !== a) {
t0 = { a };
const z = { a };
t0 = function () {
(function () {
console.log(z);
})();
};
$[0] = a;
$[1] = t0;
} else {
t0 = $[1];
}
const z = t0;
let t1;
if ($[2] !== z) {
t1 = function () {
(function () {
console.log(z);
})();
};
$[2] = z;
$[3] = t1;
} else {
t1 = $[3];
}
const x = t1;
const x = t0;
return x;
}

View File

@@ -0,0 +1,32 @@
## Input
```javascript
import {useRef} from 'react';
function useThing(fn) {
const fnRef = useRef(fn);
const ref = useRef(null);
if (ref.current === null) {
ref.current = function (this: unknown, ...args) {
return fnRef.current.call(this, ...args);
};
}
return ref.current;
}
```
## Error
```
Found 1 error:
Error: Expected a non-reserved identifier name
`this` is a reserved word in JavaScript and cannot be used as an identifier name.
```

View File

@@ -0,0 +1,13 @@
import {useRef} from 'react';
function useThing(fn) {
const fnRef = useRef(fn);
const ref = useRef(null);
if (ref.current === null) {
ref.current = function (this: unknown, ...args) {
return fnRef.current.call(this, ...args);
};
}
return ref.current;
}

View File

@@ -19,13 +19,13 @@ 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)
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;
| ^^^^^^^^^^ `someGlobal` cannot be reassigned
| ^^^^^^^^^^ `someGlobal` should not be reassigned
4 | };
5 | return <Foo />;
6 | }

View File

@@ -22,13 +22,13 @@ 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)
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;
| ^^^^^^^^^^ `someGlobal` cannot be reassigned
| ^^^^^^^^^^ `someGlobal` should not be reassigned
4 | };
5 | // Children are generally access/called during render, so
6 | // modifying a global in a children function is almost

View File

@@ -18,13 +18,15 @@ function Component() {
```
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: Cannot reassign variables 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-spread-attribute.ts:4:4
2 | function Component() {
3 | const foo = () => {
> 4 | someGlobal = true;
| ^^^^^^^^^^ 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)
| ^^^^^^^^^^ Cannot reassign variables declared outside of the component/hook
5 | };
6 | return <div {...foo} />;
7 | }

View File

@@ -20,7 +20,7 @@ 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]`
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 |

View File

@@ -23,7 +23,7 @@ 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`
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"]
@@ -36,7 +36,7 @@ error.bailout-on-suppression-of-custom-rule.ts:3:0
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`
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';

View File

@@ -0,0 +1,46 @@
## Input
```javascript
import {useCallback, useRef} from 'react';
export default function useThunkDispatch(state, dispatch, extraArg) {
const stateRef = useRef(state);
stateRef.current = state;
return useCallback(
function thunk(action) {
if (typeof action === 'function') {
return action(thunk, () => stateRef.current, extraArg);
} else {
dispatch(action);
return undefined;
}
},
[dispatch, extraArg]
);
}
```
## Error
```
Found 1 error:
Invariant: [InferMutationAliasingEffects] Expected value kind to be initialized
<unknown> thunk$14.
error.bug-infer-mutation-aliasing-effects.ts:10:22
8 | function thunk(action) {
9 | if (typeof action === 'function') {
> 10 | return action(thunk, () => stateRef.current, extraArg);
| ^^^^^ [InferMutationAliasingEffects] Expected value kind to be initialized
11 | } else {
12 | dispatch(action);
13 | return undefined;
```

View File

@@ -0,0 +1,18 @@
import {useCallback, useRef} from 'react';
export default function useThunkDispatch(state, dispatch, extraArg) {
const stateRef = useRef(state);
stateRef.current = state;
return useCallback(
function thunk(action) {
if (typeof action === 'function') {
return action(thunk, () => stateRef.current, extraArg);
} else {
dispatch(action);
return undefined;
}
},
[dispatch, extraArg]
);
}

View File

@@ -0,0 +1,31 @@
## Input
```javascript
const YearsAndMonthsSince = () => {
const diff = foo();
const months = Math.floor(diff.bar());
return <>{months}</>;
};
```
## Error
```
Found 1 error:
Invariant: [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier`
error.bug-invariant-codegen-methodcall.ts:3:17
1 | const YearsAndMonthsSince = () => {
2 | const diff = foo();
> 3 | const months = Math.floor(diff.bar());
| ^^^^^^^^^^ [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier`
4 | return <>{months}</>;
5 | };
6 |
```

View File

@@ -0,0 +1,5 @@
const YearsAndMonthsSince = () => {
const diff = foo();
const months = Math.floor(diff.bar());
return <>{months}</>;
};

View File

@@ -0,0 +1,37 @@
## Input
```javascript
import {useEffect} from 'react';
export function Foo() {
useEffect(() => {
try {
// do something
} catch ({status}) {
// do something
}
}, []);
}
```
## Error
```
Found 1 error:
Invariant: (BuildHIR::lowerAssignment) Could not find binding for declaration.
error.bug-invariant-couldnt-find-binding-for-decl.ts:7:14
5 | try {
6 | // do something
> 7 | } catch ({status}) {
| ^^^^^^ (BuildHIR::lowerAssignment) Could not find binding for declaration.
8 | // do something
9 | }
10 | }, []);
```

View File

@@ -0,0 +1,11 @@
import {useEffect} from 'react';
export function Foo() {
useEffect(() => {
try {
// do something
} catch ({status}) {
// do something
}
}, []);
}

View File

@@ -0,0 +1,32 @@
## Input
```javascript
import {useMemo} from 'react';
export default function useFoo(text) {
return useMemo(() => {
try {
let formattedText = '';
try {
formattedText = format(text);
} catch {
console.log('error');
}
return formattedText || '';
} catch (e) {}
}, [text]);
}
```
## Error
```
Found 1 error:
Invariant: Expected a break target
```

View File

@@ -0,0 +1,15 @@
import {useMemo} from 'react';
export default function useFoo(text) {
return useMemo(() => {
try {
let formattedText = '';
try {
formattedText = format(text);
} catch {
console.log('error');
}
return formattedText || '';
} catch (e) {}
}, [text]);
}

View File

@@ -0,0 +1,44 @@
## Input
```javascript
import {useMemo} from 'react';
import {useFoo, formatB, Baz} from './lib';
export const Example = ({data}) => {
let a;
let b;
if (data) {
({a, b} = data);
}
const foo = useFoo(a);
const bar = useMemo(() => formatB(b), [b]);
return <Baz foo={foo} bar={bar} />;
};
```
## Error
```
Found 1 error:
Invariant: Expected consistent kind for destructuring
Other places were `Reassign` but 'mutate? #t8$46[7:9]{reactive}' is const.
error.bug-invariant-expected-consistent-destructuring.ts:9:9
7 |
8 | if (data) {
> 9 | ({a, b} = data);
| ^ Expected consistent kind for destructuring
10 | }
11 |
12 | const foo = useFoo(a);
```

View File

@@ -0,0 +1,16 @@
import {useMemo} from 'react';
import {useFoo, formatB, Baz} from './lib';
export const Example = ({data}) => {
let a;
let b;
if (data) {
({a, b} = data);
}
const foo = useFoo(a);
const bar = useMemo(() => formatB(b), [b]);
return <Baz foo={foo} bar={bar} />;
};

View File

@@ -0,0 +1,46 @@
## Input
```javascript
import {useState} from 'react';
import {bar} from './bar';
export const useFoot = () => {
const [, setState] = useState(null);
try {
const {data} = bar();
setState({
data,
error: null,
});
} catch (err) {
setState(_prevState => ({
loading: false,
error: err,
}));
}
};
```
## Error
```
Found 1 error:
Invariant: Expected all references to a variable to be consistently local or context references
Identifier <unknown> err$7 is referenced as a context variable, but was previously referenced as a [object Object] variable.
error.bug-invariant-local-or-context-references.ts:15:13
13 | setState(_prevState => ({
14 | loading: false,
> 15 | error: err,
| ^^^ Expected all references to a variable to be consistently local or context references
16 | }));
17 | }
18 | };
```

View File

@@ -0,0 +1,18 @@
import {useState} from 'react';
import {bar} from './bar';
export const useFoot = () => {
const [, setState] = useState(null);
try {
const {data} = bar();
setState({
data,
error: null,
});
} catch (err) {
setState(_prevState => ({
loading: false,
error: err,
}));
}
};

View File

@@ -0,0 +1,34 @@
## Input
```javascript
const Foo = ({json}) => {
try {
const foo = JSON.parse(json)?.foo;
return <span>{foo}</span>;
} catch {
return null;
}
};
```
## Error
```
Found 1 error:
Invariant: Unexpected terminal in optional
error.bug-invariant-unexpected-terminal-in-optional.ts:3:16
1 | const Foo = ({json}) => {
2 | try {
> 3 | const foo = JSON.parse(json)?.foo;
| ^^^^ Unexpected terminal in optional
4 | return <span>{foo}</span>;
5 | } catch {
6 | return null;
```

View File

@@ -0,0 +1,8 @@
const Foo = ({json}) => {
try {
const foo = JSON.parse(json)?.foo;
return <span>{foo}</span>;
} catch {
return null;
}
};

View File

@@ -0,0 +1,30 @@
## Input
```javascript
import Bar from './Bar';
export function Foo() {
return (
<Bar
renderer={(...props) => {
return <span {...props}>{displayValue}</span>;
}}
/>
);
}
```
## Error
```
Found 1 error:
Invariant: Expected temporaries to be promoted to named identifiers in an earlier pass
identifier 15 is unnamed.
```

View File

@@ -0,0 +1,11 @@
import Bar from './Bar';
export function Foo() {
return (
<Bar
renderer={(...props) => {
return <span {...props}>{displayValue}</span>;
}}
/>
);
}

View File

@@ -40,7 +40,7 @@ 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.
This argument is a function which may reassign or mutate a 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 | );

View File

@@ -30,9 +30,9 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
Error: Cannot reassign variable after render completes
Error: Cannot reassign local variables after render completes
Reassigning `x` after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.
Reassigning local variables 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 | };

View File

@@ -19,9 +19,9 @@ function Component() {
```
Found 1 error:
Error: Cannot reassign variable after render completes
Error: Cannot reassign local variables after render completes
Reassigning `x` after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.
Reassigning local variables 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;

View File

@@ -17,9 +17,9 @@ function Component() {
```
Found 1 error:
Error: Cannot reassign variable after render completes
Error: Cannot reassign local variables after render completes
Reassigning `callback` after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.
Reassigning local variables 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() {

View File

@@ -0,0 +1,39 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
function BadExample() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(capitalize(firstName + ' ' + lastName));
}, [firstName, lastName]);
return <div>{fullName}</div>;
}
```
## Error
```
Found 1 error:
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
error.invalid-derived-computation-in-effect.ts:9:4
7 | const [fullName, setFullName] = useState('');
8 | useEffect(() => {
> 9 | setFullName(capitalize(firstName + ' ' + lastName));
| ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
10 | }, [firstName, lastName]);
11 |
12 | return <div>{fullName}</div>;
```

View File

@@ -0,0 +1,13 @@
// @validateNoDerivedComputationsInEffects
function BadExample() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(capitalize(firstName + ' ' + lastName));
}, [firstName, lastName]);
return <div>{fullName}</div>;
}

View File

@@ -17,12 +17,12 @@ Found 1 error:
Error: Cannot reassign variables declared outside of the component/hook
Variable `x` 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)
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.invalid-destructure-assignment-to-global.ts:2:3
1 | function useFoo(props) {
> 2 | [x] = props;
| ^ `x` cannot be reassigned
| ^ `x` should not be reassigned
3 | return {x};
4 | }
5 |

View File

@@ -19,13 +19,13 @@ Found 1 error:
Error: Cannot reassign variables declared outside of the component/hook
Variable `b` 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)
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.invalid-destructure-to-local-global-variables.ts:3:6
1 | function Component(props) {
2 | let a;
> 3 | [a, b] = props.value;
| ^ `b` cannot be reassigned
| ^ `b` should not be reassigned
4 |
5 | return [a, b];
6 | }

View File

@@ -39,13 +39,13 @@ 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)
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.invalid-global-reassignment-indirect.ts:9:4
7 |
8 | const setGlobal = () => {
> 9 | someGlobal = true;
| ^^^^^^^^^^ `someGlobal` cannot be reassigned
| ^^^^^^^^^^ `someGlobal` should not be reassigned
10 | };
11 | const indirectSetGlobal = () => {
12 | setGlobal();

View File

@@ -42,13 +42,13 @@ Found 1 error:
Error: Cannot access variable before it is declared
`setState` is accessed before it is declared, which prevents the earlier access from updating when this value changes over time.
Reading a variable before it is initialized will prevent the earlier access from updating when this value changes over time. Instead, move the variable access to after it has been initialized
error.invalid-hoisting-setstate.ts:19:18
17 | * $2 = Function context=setState
18 | */
> 19 | useEffect(() => setState(2), []);
| ^^^^^^^^ `setState` accessed before it is declared
| ^^^^^^^^ `setState` is accessed before it is declared
20 |
21 | const [state, setState] = useState(0);
22 | return <Stringify state={state} />;

View File

@@ -21,7 +21,7 @@ Found 1 error:
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `cache` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
This argument is a function which may reassign or mutate a variable after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.invalid-hook-function-argument-mutates-local-variable.ts:5:10
3 | function useFoo() {

View File

@@ -19,41 +19,41 @@ function Component() {
```
Found 3 errors:
Error: Cannot call impure function during render
Error: Cannot call impure functions during render
`Date.now` 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)
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
error.invalid-impure-functions-in-render.ts:4:15
2 |
3 | function Component() {
> 4 | const date = Date.now();
| ^^^^^^^^^^ Cannot call impure function
| ^^^^^^^^^^ `Date.now` is an impure function.
5 | const now = performance.now();
6 | const rand = Math.random();
7 | return <Foo date={date} now={now} rand={rand} />;
Error: Cannot call impure function during render
Error: Cannot call impure functions during render
`performance.now` 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)
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
error.invalid-impure-functions-in-render.ts:5:14
3 | function Component() {
4 | const date = Date.now();
> 5 | const now = performance.now();
| ^^^^^^^^^^^^^^^^^ Cannot call impure function
| ^^^^^^^^^^^^^^^^^ `performance.now` is an impure function.
6 | const rand = Math.random();
7 | return <Foo date={date} now={now} rand={rand} />;
8 | }
Error: Cannot call impure function during render
Error: Cannot call impure functions during render
`Math.random` 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)
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
error.invalid-impure-functions-in-render.ts:6:15
4 | const date = Date.now();
5 | const now = performance.now();
> 6 | const rand = Math.random();
| ^^^^^^^^^^^^^ Cannot call impure function
| ^^^^^^^^^^^^^ `Math.random` is an impure function.
7 | return <Foo date={date} now={now} rand={rand} />;
8 | }
9 |

View File

@@ -23,7 +23,7 @@ Found 1 error:
Error: This value cannot be modified
Modifying a variable defined outside a component or hook is not allowed. Consider using an effect.
Cannot reassign variables declared outside of the component/hook.
error.invalid-mutation-of-possible-props-phi-indirect.ts:4:4
2 | let x = cond ? someGlobal : props.foo;

View File

@@ -48,9 +48,9 @@ function Component() {
```
Found 1 error:
Error: Cannot reassign variable after render completes
Error: Cannot reassign local variables after render completes
Reassigning `local` after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.
Reassigning local variables after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.invalid-nested-function-reassign-local-variable-in-effect.ts:7:6
5 | // Create the reassignment function inside another function, then return it

View File

@@ -21,7 +21,7 @@ Found 1 error:
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `cache` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
This argument is a function which may reassign or mutate a variable after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.invalid-pass-mutable-function-as-prop.ts:7:18
5 | cache.set('key', 'value');

View File

@@ -15,7 +15,7 @@ function Component() {
```
Found 1 error:
Error: Cannot reassign a `const` variable
Error: Expect `const` declaration not to be reassigned
`x` is declared as const.
@@ -23,7 +23,7 @@ error.invalid-reassign-const.ts:3:2
1 | function Component() {
2 | const x = 0;
> 3 | x = 1;
| ^ Cannot reassign a `const` variable
| ^ Expect `const` declaration not to be reassigned
4 | }
5 |
```

View File

@@ -17,9 +17,9 @@ function useFoo() {
```
Found 1 error:
Error: Cannot reassign variable after render completes
Error: Cannot reassign local variables after render completes
Reassigning `x` after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.
Reassigning local variables after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.invalid-reassign-local-in-hook-return-value.ts:4:4
2 | let x = 0;

View File

@@ -49,9 +49,9 @@ function Component() {
```
Found 1 error:
Error: Cannot reassign variable after render completes
Error: Cannot reassign local variables after render completes
Reassigning `local` after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.
Reassigning local variables after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.invalid-reassign-local-variable-in-effect.ts:7:4
5 |

View File

@@ -50,9 +50,9 @@ function Component() {
```
Found 1 error:
Error: Cannot reassign variable after render completes
Error: Cannot reassign local variables after render completes
Reassigning `local` after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.
Reassigning local variables after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.invalid-reassign-local-variable-in-hook-argument.ts:8:4
6 |

View File

@@ -43,9 +43,9 @@ function Component() {
```
Found 1 error:
Error: Cannot reassign variable after render completes
Error: Cannot reassign local variables after render completes
Reassigning `local` after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.
Reassigning local variables after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.invalid-reassign-local-variable-in-jsx-callback.ts:5:4
3 |

View File

@@ -23,7 +23,7 @@ Found 1 error:
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `cache` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
This argument is a function which may reassign or mutate a variable after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.invalid-return-mutable-function-from-hook.ts:7:9
5 | useHook(); // for inference to kick in

View File

@@ -21,7 +21,7 @@ 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 react-hooks/rules-of-hooks`
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 react-hooks/rules-of-hooks`
error.invalid-sketchy-code-use-forget.ts:1:0
> 1 | /* eslint-disable react-hooks/rules-of-hooks */
@@ -32,7 +32,7 @@ error.invalid-sketchy-code-use-forget.ts:1:0
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 react-hooks/rules-of-hooks`
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 react-hooks/rules-of-hooks`
error.invalid-sketchy-code-use-forget.ts:5:2
3 | 'use forget';

View File

@@ -51,7 +51,7 @@ Found 1 error:
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `cache` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
This argument is a function which may reassign or mutate a variable after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
19 | map: TInput => TOutput
20 | ): TInput => TOutput {

View File

@@ -40,7 +40,7 @@ Found 1 error:
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 react-hooks/rules-of-hooks`
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 react-hooks/rules-of-hooks`
error.invalid-unclosed-eslint-suppression.ts:2:0
1 | // Note: Everything below this is sketchy

View File

@@ -29,7 +29,7 @@ error.invalid-unconditional-set-state-in-render.ts:6:2
4 | const aliased = setX;
5 |
> 6 | setX(1);
| ^^^^ Found setState() within useMemo()
| ^^^^ Found setState() call here
7 | aliased(2);
8 |
9 | return x;
@@ -42,7 +42,7 @@ error.invalid-unconditional-set-state-in-render.ts:7:2
5 |
6 | setX(1);
> 7 | aliased(2);
| ^^^^^^^ Found setState() within useMemo()
| ^^^^^^^ Found setState() call here
8 |
9 | return x;
10 | }

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