Compare commits
345 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
758301f70f | ||
|
|
48b52d896e | ||
|
|
3a669170e9 | ||
|
|
a083344699 | ||
|
|
423c44b886 | ||
|
|
f970d5ff32 | ||
|
|
724e7bfb40 | ||
|
|
ef88c588d5 | ||
|
|
dc485c7303 | ||
|
|
c35f6a3041 | ||
|
|
adbc32de32 | ||
|
|
1324e1bb1f | ||
|
|
7f5ea1bf67 | ||
|
|
0e32da71c7 | ||
|
|
2381ecc290 | ||
|
|
5418d8bdc1 | ||
|
|
ed1351c4fb | ||
|
|
93f8593289 | ||
|
|
dc1becd893 | ||
|
|
d8aa94b0f4 | ||
|
|
03ba0c76e1 | ||
|
|
4e00747378 | ||
|
|
7bd8716acd | ||
|
|
7385d1f61a | ||
|
|
85f415e33b | ||
|
|
903366b8b1 | ||
|
|
0fbb9b3683 | ||
|
|
e096403c59 | ||
|
|
1873ad7960 | ||
|
|
77b2f909f6 | ||
|
|
6773248311 | ||
|
|
5747cadf44 | ||
|
|
751edd6e2c | ||
|
|
6cfc9c1ff3 | ||
|
|
e7984651e4 | ||
|
|
5f2b571878 | ||
|
|
56e846921d | ||
|
|
19b71673b1 | ||
|
|
73507ec457 | ||
|
|
03a62b20fd | ||
|
|
b9ec735de2 | ||
|
|
47905a7950 | ||
|
|
7b971c0a55 | ||
|
|
83ea655a0b | ||
|
|
026abeaa5f | ||
|
|
d7215b4970 | ||
|
|
e2ce64acb9 | ||
|
|
34b1567427 | ||
|
|
b467c6e949 | ||
|
|
93d4458fdc | ||
|
|
1d68bce19c | ||
|
|
ead92181bd | ||
|
|
d44659744f | ||
|
|
8454a32f3c | ||
|
|
06fcc8f380 | ||
|
|
91e5c3daf1 | ||
|
|
4b3e662e4c | ||
|
|
3e1b34dc51 | ||
|
|
7568e71854 | ||
|
|
9724e3e66e | ||
|
|
848e0e3a4f | ||
|
|
5c15c1cd34 | ||
|
|
69b4cb8df4 | ||
|
|
a664f5f2ee | ||
|
|
1384ea8230 | ||
|
|
3025aa3964 | ||
|
|
a4eb2dfa6f | ||
|
|
6a8c7fb6f1 | ||
|
|
b65e6fc58b | ||
|
|
c786258422 | ||
|
|
1be3ce9996 | ||
|
|
3b2a398106 | ||
|
|
62ff1e61fc | ||
|
|
0e79784702 | ||
|
|
a2329c10ff | ||
|
|
d3f84a433a | ||
|
|
bc2356176b | ||
|
|
4fdf7cf249 | ||
|
|
614a945d9d | ||
|
|
d6eb735938 | ||
|
|
71753ac90a | ||
|
|
f24d3bbc70 | ||
|
|
85c427d822 | ||
|
|
02bd4458f7 | ||
|
|
0eebd37041 | ||
|
|
74dee8ef64 | ||
|
|
e866b1d1e9 | ||
|
|
19f65ff179 | ||
|
|
26b177bc5e | ||
|
|
056a586928 | ||
|
|
5cc3d49f72 | ||
|
|
289f070d64 | ||
|
|
6a8a8ef326 | ||
|
|
f89ed71ddf | ||
|
|
7d9f876cbc | ||
|
|
df3562dc7f | ||
|
|
b56907db51 | ||
|
|
c825f03067 | ||
|
|
2e68dc76a4 | ||
|
|
ced705d756 | ||
|
|
70b52beca6 | ||
|
|
4a28227960 | ||
|
|
e4a27db283 | ||
|
|
57d5a59748 | ||
|
|
bc828bf6e3 | ||
|
|
a757cb7667 | ||
|
|
d74f061b69 | ||
|
|
f7254efc5c | ||
|
|
79ca5ae855 | ||
|
|
ae74234eae | ||
|
|
861811347b | ||
|
|
7f9d99749c | ||
|
|
aef8b1b562 | ||
|
|
67e24bc527 | ||
|
|
bbc2d596fa | ||
|
|
1bd1f01f2a | ||
|
|
548235db10 | ||
|
|
1f460f31ee | ||
|
|
2f0649a0b2 | ||
|
|
7bccdbd765 | ||
|
|
5667a41fe4 | ||
|
|
cf884083e0 | ||
|
|
57b16e3788 | ||
|
|
2a04bae651 | ||
|
|
92cfdc3a4e | ||
|
|
a55e98f738 | ||
|
|
063394cf82 | ||
|
|
d8a15c49a4 | ||
|
|
0d8ff4d8c7 | ||
|
|
554a373d7e | ||
|
|
5dd163b49e | ||
|
|
ef8894452b | ||
|
|
e6f2a8a376 | ||
|
|
ba2214e571 | ||
|
|
ecb2ce6c5f | ||
|
|
3580584ba2 | ||
|
|
319a7867d0 | ||
|
|
d15d7fd79e | ||
|
|
8674c3ba28 | ||
|
|
24e260d35b | ||
|
|
2bbb7be0e1 | ||
|
|
dce1f6cd5d | ||
|
|
7c0fff6f2b | ||
|
|
e2d19bf6a9 | ||
|
|
a7d8dddaf3 | ||
|
|
8309724cb4 | ||
|
|
09d3cd8fb5 | ||
|
|
f78b2343cc | ||
|
|
e08f53b182 | ||
|
|
2622487a74 | ||
|
|
8a24ef3e75 | ||
|
|
c552618a82 | ||
|
|
df38ac9a3b | ||
|
|
8bb7241f4c | ||
|
|
8d557a638e | ||
|
|
6a51a9fea6 | ||
|
|
1fd291d3c5 | ||
|
|
047715c4ba | ||
|
|
250f1b20e0 | ||
|
|
b0c1dc01ec | ||
|
|
6eb5d67e9c | ||
|
|
ac2c1a5a58 | ||
|
|
c44fbf43b1 | ||
|
|
8ad773b1f3 | ||
|
|
58d17912e8 | ||
|
|
2c6d92fd80 | ||
|
|
e233218359 | ||
|
|
05b61f812a | ||
|
|
e0c421ab71 | ||
|
|
2ee6147510 | ||
|
|
e02c173fa5 | ||
|
|
24a2ba03fb | ||
|
|
012b371cde | ||
|
|
83c88ad470 | ||
|
|
cad813ac1e | ||
|
|
720bb13069 | ||
|
|
1eca9a2747 | ||
|
|
cd85bb5616 | ||
|
|
07e4974bad | ||
|
|
d91d28c8ba | ||
|
|
b4fe1e6c7e | ||
|
|
b204edda3a | ||
|
|
115e3ec15f | ||
|
|
565eb7888e | ||
|
|
d415fd3ed7 | ||
|
|
5e3cd53f20 | ||
|
|
01cad9eaca | ||
|
|
6eda534718 | ||
|
|
c03a51d836 | ||
|
|
ad578aa01f | ||
|
|
03a96c75db | ||
|
|
755cebad6b | ||
|
|
581321160f | ||
|
|
1bcdd224b1 | ||
|
|
84af9085c1 | ||
|
|
128abcfa01 | ||
|
|
e3c9656d20 | ||
|
|
27b4076ab0 | ||
|
|
81d66927af | ||
|
|
6a4c8f51fa | ||
|
|
16df13b84c | ||
|
|
7899729130 | ||
|
|
a51f925217 | ||
|
|
941cd803a7 | ||
|
|
851bad0c88 | ||
|
|
5e0c951b58 | ||
|
|
348a4e2d44 | ||
|
|
5d49b2b7f4 | ||
|
|
ae22247dce | ||
|
|
e3f191803c | ||
|
|
e12b0bdc3b | ||
|
|
92d7ad5dd9 | ||
|
|
67a44bcd1b | ||
|
|
3fa927b674 | ||
|
|
47664deb8e | ||
|
|
5502d85cc7 | ||
|
|
8a8e9a7edf | ||
|
|
68f00c901c | ||
|
|
93d7aa69b2 | ||
|
|
20e5431747 | ||
|
|
1a27af3607 | ||
|
|
0e10ee906e | ||
|
|
0c813c528d | ||
|
|
a9ad64c852 | ||
|
|
7fc888dde2 | ||
|
|
67415c8c4a | ||
|
|
f3a803617e | ||
|
|
fe84397e81 | ||
|
|
b1c519f3d4 | ||
|
|
8c1501452c | ||
|
|
bd9e6e0bed | ||
|
|
835b00908b | ||
|
|
e2ba45bb39 | ||
|
|
886b3d36d7 | ||
|
|
288d428af1 | ||
|
|
a34c5dff15 | ||
|
|
3bf8ab430e | ||
|
|
acada3035f | ||
|
|
969a9790ad | ||
|
|
665de2ed28 | ||
|
|
eda778b8ae | ||
|
|
1836b46fff | ||
|
|
eec50b17b3 | ||
|
|
a9410fb487 | ||
|
|
6b70072c4f | ||
|
|
b2cff47472 | ||
|
|
8943025358 | ||
|
|
3d9d22cbdb | ||
|
|
d4374b3ae3 | ||
|
|
3f2a42a5de | ||
|
|
294c33f34d | ||
|
|
3fb190f729 | ||
|
|
f5e96b9740 | ||
|
|
78992521a8 | ||
|
|
80d7aa17ad | ||
|
|
474f25842a | ||
|
|
1fef581e1a | ||
|
|
60d9b9740d | ||
|
|
c4e2508dad | ||
|
|
de5a1b203e | ||
|
|
b9a045368b | ||
|
|
e2cc315a1b | ||
|
|
5a31758ed6 | ||
|
|
ba6590dd7c | ||
|
|
2710795a1e | ||
|
|
735e9ac54e | ||
|
|
5d64f74211 | ||
|
|
3302d1f791 | ||
|
|
7697a9f62e | ||
|
|
3168e08f83 | ||
|
|
2805f0ed9e | ||
|
|
ac3e705a18 | ||
|
|
8e60cb7ed5 | ||
|
|
6a58b80020 | ||
|
|
b1b0955f2b | ||
|
|
1549bda33f | ||
|
|
bb6f0c8d2f | ||
|
|
aad7c664ff | ||
|
|
3fe51c9e14 | ||
|
|
4082b0e7d3 | ||
|
|
6b49c449b6 | ||
|
|
872b4fef6d | ||
|
|
c5362a380f | ||
|
|
89a803fcec | ||
|
|
8d7b5e4903 | ||
|
|
3434ff4f4b | ||
|
|
bd5b1b7639 | ||
|
|
0a1f1fcd50 | ||
|
|
b870042915 | ||
|
|
33a1095d72 | ||
|
|
213594860f | ||
|
|
9c2e2b8475 | ||
|
|
4123f6b771 | ||
|
|
cb1e73be04 | ||
|
|
cacc20e37c | ||
|
|
bb7c9c1b8a | ||
|
|
44f8451ede | ||
|
|
ad4ecb6e6e | ||
|
|
26e87b5f15 | ||
|
|
75dc0026d6 | ||
|
|
df10309e2b | ||
|
|
e42f3d30ca | ||
|
|
67e743fba5 | ||
|
|
9eede45646 | ||
|
|
090777d78a | ||
|
|
4049cfeeab | ||
|
|
e67e3bed92 | ||
|
|
06cfa99f37 | ||
|
|
05addfc663 | ||
|
|
d260b0d8b8 | ||
|
|
425ba0ad6d | ||
|
|
6de32a5a07 | ||
|
|
698bb4deb7 | ||
|
|
11d7bcf88c | ||
|
|
a85ec041d6 | ||
|
|
7d29ecbeb2 | ||
|
|
253abc78a1 | ||
|
|
d73b6f1110 | ||
|
|
d5586e2059 | ||
|
|
ec5dd0ab3a | ||
|
|
8120753665 | ||
|
|
3770ff3853 | ||
|
|
873f711299 | ||
|
|
5f06c3d22a | ||
|
|
243a56b9a2 | ||
|
|
83c7379b96 | ||
|
|
c2ac8b4f0e | ||
|
|
03fda05d2c | ||
|
|
0bc71e67ab | ||
|
|
3e20dc8b9c | ||
|
|
ae5c2f82b3 | ||
|
|
0bdb9206b7 | ||
|
|
f508edc83f | ||
|
|
0c89b160f6 | ||
|
|
87a45ae37f | ||
|
|
01ed0e9642 | ||
|
|
b58a8e3c40 | ||
|
|
42b1b33a24 | ||
|
|
7a36dfedc7 | ||
|
|
546bac7281 | ||
|
|
2cb8edbb05 | ||
|
|
431bb0bddb | ||
|
|
5063b3283f | ||
|
|
eaf6adb127 | ||
|
|
6ffcac8558 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"packages": ["packages/react", "packages/react-dom", "packages/react-server-dom-webpack", "packages/scheduler"],
|
||||
"buildCommand": "download-build-in-codesandbox-ci",
|
||||
"node": "18",
|
||||
"node": "20",
|
||||
"publishDirectory": {
|
||||
"react": "build/oss-experimental/react",
|
||||
"react-dom": "build/oss-experimental/react-dom",
|
||||
|
||||
@@ -28,3 +28,6 @@ packages/react-devtools-shared/src/hooks/__tests__/__source__/__untransformed__/
|
||||
packages/react-devtools-shell/dist
|
||||
packages/react-devtools-timeline/dist
|
||||
packages/react-devtools-timeline/static
|
||||
|
||||
# Imported third-party Flow types
|
||||
flow-typed/
|
||||
|
||||
21
.eslintrc.js
21
.eslintrc.js
@@ -468,6 +468,7 @@ module.exports = {
|
||||
files: ['packages/react-server-dom-webpack/**/*.js'],
|
||||
globals: {
|
||||
__webpack_chunk_load__: 'readonly',
|
||||
__webpack_get_script_filename__: 'readonly',
|
||||
__webpack_require__: 'readonly',
|
||||
},
|
||||
},
|
||||
@@ -516,6 +517,14 @@ module.exports = {
|
||||
__IS_INTERNAL_VERSION__: 'readonly',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/react-devtools-*/**/*.js'],
|
||||
excludedFiles: '**/__tests__/**/*.js',
|
||||
plugins: ['eslint-plugin-react-hooks-published'],
|
||||
rules: {
|
||||
'react-hooks-published/rules-of-hooks': ERROR,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/eslint-plugin-react-hooks/src/**/*'],
|
||||
extends: ['plugin:@typescript-eslint/recommended'],
|
||||
@@ -546,13 +555,10 @@ module.exports = {
|
||||
},
|
||||
|
||||
globals: {
|
||||
$Call: 'readonly',
|
||||
$ElementType: 'readonly',
|
||||
$Flow$ModuleRef: 'readonly',
|
||||
$FlowFixMe: 'readonly',
|
||||
$Keys: 'readonly',
|
||||
$NonMaybeType: 'readonly',
|
||||
$PropertyType: 'readonly',
|
||||
$ReadOnly: 'readonly',
|
||||
$ReadOnlyArray: 'readonly',
|
||||
$ArrayBufferView: 'readonly',
|
||||
@@ -567,6 +573,7 @@ module.exports = {
|
||||
BigInt: 'readonly',
|
||||
BigInt64Array: 'readonly',
|
||||
BigUint64Array: 'readonly',
|
||||
CacheType: 'readonly',
|
||||
Class: 'readonly',
|
||||
ClientRect: 'readonly',
|
||||
CopyInspectedElementPath: 'readonly',
|
||||
@@ -578,16 +585,19 @@ module.exports = {
|
||||
$AsyncIterator: 'readonly',
|
||||
Iterator: 'readonly',
|
||||
AsyncIterator: 'readonly',
|
||||
IntervalID: 'readonly',
|
||||
IteratorResult: 'readonly',
|
||||
JSONValue: 'readonly',
|
||||
JSResourceReference: 'readonly',
|
||||
mixin$Animatable: 'readonly',
|
||||
MouseEventHandler: 'readonly',
|
||||
NavigateEvent: 'readonly',
|
||||
PerformanceMeasureOptions: 'readonly',
|
||||
PropagationPhases: 'readonly',
|
||||
PropertyDescriptor: 'readonly',
|
||||
React$AbstractComponent: 'readonly',
|
||||
PropertyDescriptorMap: 'readonly',
|
||||
Proxy$traps: 'readonly',
|
||||
React$Component: 'readonly',
|
||||
React$ComponentType: 'readonly',
|
||||
React$Config: 'readonly',
|
||||
React$Context: 'readonly',
|
||||
React$Element: 'readonly',
|
||||
@@ -619,7 +629,6 @@ module.exports = {
|
||||
PropertyIndexedKeyframes: 'readonly',
|
||||
KeyframeAnimationOptions: 'readonly',
|
||||
GetAnimationsOptions: 'readonly',
|
||||
Animatable: 'readonly',
|
||||
ScrollTimeline: 'readonly',
|
||||
EventListenerOptionsOrUseCapture: 'readonly',
|
||||
FocusOptions: 'readonly',
|
||||
|
||||
2
.github/workflows/compiler_playground.yml
vendored
2
.github/workflows/compiler_playground.yml
vendored
@@ -57,8 +57,6 @@ jobs:
|
||||
key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }}
|
||||
- run: npx playwright install --with-deps chromium
|
||||
if: steps.cache_playwright_browsers.outputs.cache-hit != 'true'
|
||||
- run: npx playwright install-deps
|
||||
if: steps.cache_playwright_browsers.outputs.cache-hit == 'true'
|
||||
- run: CI=true yarn test
|
||||
- run: ls -R test-results
|
||||
if: '!cancelled()'
|
||||
|
||||
11
.github/workflows/compiler_prereleases.yml
vendored
11
.github/workflows/compiler_prereleases.yml
vendored
@@ -19,6 +19,9 @@ on:
|
||||
tag_version:
|
||||
required: false
|
||||
type: string
|
||||
dry_run:
|
||||
required: false
|
||||
type: boolean
|
||||
secrets:
|
||||
NPM_TOKEN:
|
||||
required: true
|
||||
@@ -55,7 +58,13 @@ jobs:
|
||||
key: compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/yarn.lock') }}
|
||||
- run: yarn install --frozen-lockfile
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- name: Publish packages to npm
|
||||
- if: inputs.dry_run == true
|
||||
name: Publish packages to npm (dry run)
|
||||
run: |
|
||||
cp ./scripts/release/ci-npmrc ~/.npmrc
|
||||
scripts/release/publish.js --frfr --debug --ci --versionName=${{ inputs.version_name }} --tag=${{ inputs.dist_tag }} ${{ inputs.tag_version && format('--tagVersion={0}', inputs.tag_version) || '' }}
|
||||
- if: inputs.dry_run != true
|
||||
name: Publish packages to npm
|
||||
run: |
|
||||
cp ./scripts/release/ci-npmrc ~/.npmrc
|
||||
scripts/release/publish.js --frfr --ci --versionName=${{ inputs.version_name }} --tag=${{ inputs.dist_tag }} ${{ inputs.tag_version && format('--tagVersion={0}', inputs.tag_version) || '' }}
|
||||
|
||||
@@ -17,6 +17,9 @@ on:
|
||||
tag_version:
|
||||
required: false
|
||||
type: string
|
||||
dry_run:
|
||||
required: false
|
||||
type: boolean
|
||||
|
||||
permissions: {}
|
||||
|
||||
@@ -33,5 +36,6 @@ jobs:
|
||||
dist_tag: ${{ inputs.dist_tag }}
|
||||
version_name: ${{ inputs.version_name }}
|
||||
tag_version: ${{ inputs.tag_version }}
|
||||
dry_run: ${{ inputs.dry_run }}
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
@@ -19,5 +19,6 @@ jobs:
|
||||
release_channel: experimental
|
||||
dist_tag: experimental
|
||||
version_name: '0.0.0'
|
||||
dry_run: false
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
49
.github/workflows/devtools_discord_notify.yml
vendored
Normal file
49
.github/workflows/devtools_discord_notify.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: (DevTools) Discord Notify
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, ready_for_review]
|
||||
paths:
|
||||
- packages/react-devtools**
|
||||
- .github/workflows/devtools_**.yml
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
check_access:
|
||||
if: ${{ github.event.pull_request.draft == false }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
|
||||
steps:
|
||||
- run: echo ${{ github.event.pull_request.author_association }}
|
||||
- name: Check is member or collaborator
|
||||
id: check_is_member_or_collaborator
|
||||
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}
|
||||
run: echo "is_member_or_collaborator=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
check_maintainer:
|
||||
if: ${{ needs.check_access.outputs.is_member_or_collaborator == 'true' || needs.check_access.outputs.is_member_or_collaborator == true }}
|
||||
needs: [check_access]
|
||||
uses: facebook/react/.github/workflows/shared_check_maintainer.yml@main
|
||||
permissions:
|
||||
# Used by check_maintainer
|
||||
contents: read
|
||||
with:
|
||||
actor: ${{ github.event.pull_request.user.login }}
|
||||
|
||||
notify:
|
||||
if: ${{ needs.check_maintainer.outputs.is_core_team == 'true' }}
|
||||
needs: check_maintainer
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
|
||||
with:
|
||||
webhook-url: ${{ secrets.DEVTOOLS_DISCORD_WEBHOOK_URL }}
|
||||
embed-author-name: ${{ github.event.pull_request.user.login }}
|
||||
embed-author-url: ${{ github.event.pull_request.user.html_url }}
|
||||
embed-author-icon-url: ${{ github.event.pull_request.user.avatar_url }}
|
||||
embed-title: '#${{ github.event.number }} (+${{github.event.pull_request.additions}} -${{github.event.pull_request.deletions}}): ${{ github.event.pull_request.title }}'
|
||||
embed-description: ${{ github.event.pull_request.body }}
|
||||
embed-url: ${{ github.event.pull_request.html_url }}
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: react-devtools
|
||||
path: build/devtools.tgz
|
||||
path: build/devtools
|
||||
if-no-files-found: error
|
||||
# Simplifies getting the extension for local testing
|
||||
- name: Archive chrome extension
|
||||
@@ -201,5 +201,5 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: screenshots
|
||||
path: ./tmp/screenshots
|
||||
path: ./tmp/playwright-artifacts
|
||||
if-no-files-found: warn
|
||||
|
||||
34
.github/workflows/runtime_build_and_test.yml
vendored
34
.github/workflows/runtime_build_and_test.yml
vendored
@@ -194,7 +194,7 @@ jobs:
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- run: |
|
||||
yarn generate-inline-fizz-runtime
|
||||
git diff --quiet || (echo "There was a change to the Fizz runtime. Run `yarn generate-inline-fizz-runtime` and check in the result." && false)
|
||||
git diff --exit-code || (echo "There was a change to the Fizz runtime. Run \`yarn generate-inline-fizz-runtime\` and check in the result." && false)
|
||||
|
||||
# ----- FEATURE FLAGS -----
|
||||
flags:
|
||||
@@ -316,7 +316,7 @@ jobs:
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- run: ./scripts/react-compiler/build-compiler.sh && ./scripts/react-compiler/link-compiler.sh
|
||||
- run: yarn workspace eslint-plugin-react-hooks test
|
||||
|
||||
|
||||
# ----- BUILD -----
|
||||
build_and_lint:
|
||||
name: yarn build and lint
|
||||
@@ -567,7 +567,7 @@ jobs:
|
||||
- name: Search build artifacts for unminified errors
|
||||
run: |
|
||||
yarn extract-errors
|
||||
git diff --quiet || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false)
|
||||
git diff --exit-code || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false)
|
||||
|
||||
check_release_dependencies:
|
||||
name: Check release dependencies
|
||||
@@ -766,6 +766,11 @@ jobs:
|
||||
name: react-devtools-${{ matrix.browser }}-extension
|
||||
path: build/devtools/${{ matrix.browser }}-extension.zip
|
||||
if-no-files-found: error
|
||||
- name: Archive ${{ matrix.browser }} metadata
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: react-devtools-${{ matrix.browser }}-metadata
|
||||
path: build/devtools/webpack-stats.*.json
|
||||
|
||||
merge_devtools_artifacts:
|
||||
name: Merge DevTools artifacts
|
||||
@@ -776,7 +781,7 @@ jobs:
|
||||
uses: actions/upload-artifact/merge@v4
|
||||
with:
|
||||
name: react-devtools
|
||||
pattern: react-devtools-*-extension
|
||||
pattern: react-devtools-*
|
||||
|
||||
run_devtools_e2e_tests:
|
||||
name: Run DevTools e2e tests
|
||||
@@ -811,12 +816,27 @@ jobs:
|
||||
pattern: _build_*
|
||||
path: build
|
||||
merge-multiple: true
|
||||
- run: |
|
||||
npx playwright install
|
||||
sudo npx playwright install-deps
|
||||
- name: Check Playwright version
|
||||
id: playwright_version
|
||||
run: echo "playwright_version=$(npm ls @playwright/test | grep @playwright | sed 's/.*@//' | head -1)" >> "$GITHUB_OUTPUT"
|
||||
- name: Cache Playwright Browsers for version ${{ steps.playwright_version.outputs.playwright_version }}
|
||||
id: cache_playwright_browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }}
|
||||
- name: Playwright install deps
|
||||
if: steps.cache_playwright_browsers.outputs.cache-hit != 'true'
|
||||
run: npx playwright install --with-deps chromium
|
||||
- run: ./scripts/ci/run_devtools_e2e_tests.js
|
||||
env:
|
||||
RELEASE_CHANNEL: experimental
|
||||
- name: Archive Playwright report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: devtools-playwright-artifacts
|
||||
path: tmp/playwright-artifacts
|
||||
if-no-files-found: warn
|
||||
|
||||
# ----- SIZEBOT -----
|
||||
sizebot:
|
||||
|
||||
2
.github/workflows/runtime_discord_notify.yml
vendored
2
.github/workflows/runtime_discord_notify.yml
vendored
@@ -4,8 +4,10 @@ on:
|
||||
pull_request_target:
|
||||
types: [opened, ready_for_review]
|
||||
paths-ignore:
|
||||
- packages/react-devtools**
|
||||
- compiler/**
|
||||
- .github/workflows/compiler_**.yml
|
||||
- .github/workflows/devtools**.yml
|
||||
|
||||
permissions: {}
|
||||
|
||||
|
||||
4
.github/workflows/runtime_prereleases.yml
vendored
4
.github/workflows/runtime_prereleases.yml
vendored
@@ -82,7 +82,6 @@ jobs:
|
||||
run: |
|
||||
scripts/release/publish.js \
|
||||
--ci \
|
||||
--skipTests \
|
||||
--tags=${{ inputs.dist_tag }} \
|
||||
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry' || '' }}
|
||||
@@ -91,11 +90,10 @@ jobs:
|
||||
run: |
|
||||
scripts/release/publish.js \
|
||||
--ci \
|
||||
--skipTests \
|
||||
--tags=${{ inputs.dist_tag }} \
|
||||
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry' || '' }}
|
||||
- if: '${{ !(inputs.skip_packages && inputs.only_packages) }}'
|
||||
- if: '${{ !inputs.skip_packages && !inputs.only_packages }}'
|
||||
name: 'Publish all packages'
|
||||
run: |
|
||||
scripts/release/publish.js \
|
||||
|
||||
@@ -18,6 +18,7 @@ jobs:
|
||||
permissions:
|
||||
# Used to create a review and close PRs
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
- name: Close PR
|
||||
uses: actions/github-script@v7
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,6 +23,7 @@ chrome-user-data
|
||||
.vscode
|
||||
*.swp
|
||||
*.swo
|
||||
/tmp
|
||||
|
||||
packages/react-devtools-core/dist
|
||||
packages/react-devtools-extensions/chrome/build
|
||||
|
||||
73
CHANGELOG.md
73
CHANGELOG.md
@@ -1,3 +1,76 @@
|
||||
## 19.2.0 (October 1st, 2025)
|
||||
|
||||
Below is a list of all new features, APIs, and bug fixes.
|
||||
|
||||
Read the [React 19.2 release post](https://react.dev/blog/2025/10/01/react-19-2) for more information.
|
||||
|
||||
### New React Features
|
||||
|
||||
- [`<Activity>`](https://react.dev/reference/react/Activity): A new API to hide and restore the UI and internal state of its children.
|
||||
- [`useEffectEvent`](https://react.dev/reference/react/useEffectEvent) is a React Hook that lets you extract non-reactive logic into an [Effect Event](https://react.dev/learn/separating-events-from-effects#declaring-an-effect-event).
|
||||
- [`cacheSignal`](https://react.dev/reference/react/cacheSignal) (for RSCs) lets your know when the `cache()` lifetime is over.
|
||||
- [React Performance tracks](https://react.dev/reference/dev-tools/react-performance-tracks) appear on the Performance panel’s timeline in your browser developer tools
|
||||
|
||||
### New React DOM Features
|
||||
|
||||
- Added resume APIs for partial pre-rendering with Web Streams:
|
||||
- [`resume`](https://react.dev/reference/react-dom/server/resume): to resume a prerender to a stream.
|
||||
- [`resumeAndPrerender`](https://react.dev/reference/react-dom/static/resumeAndPrerender): to resume a prerender to HTML.
|
||||
- Added resume APIs for partial pre-rendering with Node Streams:
|
||||
- [`resumeToPipeableStream`](https://react.dev/reference/react-dom/server/resumeToPipeableStream): to resume a prerender to a stream.
|
||||
- [`resumeAndPrerenderToNodeStream`](https://react.dev/reference/react-dom/static/resumeAndPrerenderToNodeStream): to resume a prerender to HTML.
|
||||
- Updated [`prerender`](https://react.dev/reference/react-dom/static/prerender) APIs to return a `postponed` state that can be passed to the `resume` APIs.
|
||||
|
||||
### Notable changes
|
||||
|
||||
- React DOM now batches suspense boundary reveals, matching the behavior of client side rendering. This change is especially noticeable when animating the reveal of Suspense boundaries e.g. with the upcoming `<ViewTransition>` Component. React will batch as much reveals as possible before the first paint while trying to hit popular first-contentful paint metrics.
|
||||
- Add Node Web Streams (`prerender`, `renderToReadableStream`) to server-side-rendering APIs for Node.js
|
||||
- Use underscore instead of `:` IDs generated by useId
|
||||
|
||||
### All Changes
|
||||
|
||||
#### React
|
||||
|
||||
- `<Activity />` was developed over many years, starting before `ClassComponent.setState` (@acdlite @sebmarkbage and many others)
|
||||
- Stringify context as "SomeContext" instead of "SomeContext.Provider" (@kassens [#33507](https://github.com/facebook/react/pull/33507))
|
||||
- Include stack of cause of React instrumentation errors with `%o` placeholder (@eps1lon [#34198](https://github.com/facebook/react/pull/34198))
|
||||
- Fix infinite `useDeferredValue` loop in popstate event (@acdlite [#32821](https://github.com/facebook/react/pull/32821))
|
||||
- Fix a bug when an initial value was passed to `useDeferredValue` (@acdlite [#34376](https://github.com/facebook/react/pull/34376))
|
||||
- Fix a crash when submitting forms with Client Actions (@sebmarkbage [#33055](https://github.com/facebook/react/pull/33055))
|
||||
- Hide/unhide the content of dehydrated suspense boundaries if they resuspend (@sebmarkbage [#32900](https://github.com/facebook/react/pull/32900))
|
||||
- Avoid stack overflow on wide trees during Hot Reload (@sophiebits [#34145](https://github.com/facebook/react/pull/34145))
|
||||
- Improve Owner and Component stacks in various places (@sebmarkbage, @eps1lon: [#33629](https://github.com/facebook/react/pull/33629), [#33724](https://github.com/facebook/react/pull/33724), [#32735](https://github.com/facebook/react/pull/32735), [#33723](https://github.com/facebook/react/pull/33723))
|
||||
- Add `cacheSignal` (@sebmarkbage [#33557](https://github.com/facebook/react/pull/33557))
|
||||
|
||||
#### React DOM
|
||||
|
||||
- Block on Suspensey Fonts during reveal of server-side-rendered content (@sebmarkbage [#33342](https://github.com/facebook/react/pull/33342))
|
||||
- Use underscore instead of `:` for IDs generated by `useId` (@sebmarkbage, @eps1lon: [#32001](https://github.com/facebook/react/pull/32001), [https://github.com/facebook/react/pull/33342](https://github.com/facebook/react/pull/33342)[#33099](https://github.com/facebook/react/pull/33099), [#33422](https://github.com/facebook/react/pull/33422))
|
||||
- Stop warning when ARIA 1.3 attributes are used (@Abdul-Omira [#34264](https://github.com/facebook/react/pull/34264))
|
||||
- Allow `nonce` to be used on hoistable styles (@Andarist [#32461](https://github.com/facebook/react/pull/32461))
|
||||
- Warn for using a React owned node as a Container if it also has text content (@sebmarkbage [#32774](https://github.com/facebook/react/pull/32774))
|
||||
- s/HTML/text for for error messages if text hydration mismatches (@rickhanlonii [#32763](https://github.com/facebook/react/pull/32763))
|
||||
- Fix a bug with `React.use` inside `React.lazy`\-ed Component (@hi-ogawa [#33941](https://github.com/facebook/react/pull/33941))
|
||||
- Enable the `progressiveChunkSize` option for server-side-rendering APIs (@sebmarkbage [#33027](https://github.com/facebook/react/pull/33027))
|
||||
- Fix a bug with deeply nested Suspense inside Suspense fallback when server-side-rendering (@gnoff [#33467](https://github.com/facebook/react/pull/33467))
|
||||
- Avoid hanging when suspending after aborting while rendering (@gnoff [#34192](https://github.com/facebook/react/pull/34192))
|
||||
- Add Node Web Streams to server-side-rendering APIs for Node.js (@sebmarkbage [#33475](https://github.com/facebook/react/pull/33475))
|
||||
|
||||
#### React Server Components
|
||||
|
||||
- Preload `<img>` and `<link>` using hints before they're rendered (@sebmarkbage [#34604](https://github.com/facebook/react/pull/34604))
|
||||
- Log error if production elements are rendered during development (@eps1lon [#34189](https://github.com/facebook/react/pull/34189))
|
||||
- Fix a bug when returning a Temporary reference (e.g. a Client Reference) from Server Functions (@sebmarkbage [#34084](https://github.com/facebook/react/pull/34084), @denk0403 [#33761](https://github.com/facebook/react/pull/33761))
|
||||
- Pass line/column to `filterStackFrame` (@eps1lon [#33707](https://github.com/facebook/react/pull/33707))
|
||||
- Support Async Modules in Turbopack Server References (@lubieowoce [#34531](https://github.com/facebook/react/pull/34531))
|
||||
- Add support for .mjs file extension in Webpack (@jennyscript [#33028](https://github.com/facebook/react/pull/33028))
|
||||
- Fix a wrong missing key warning (@unstubbable [#34350](https://github.com/facebook/react/pull/34350))
|
||||
- Make console log resolve in predictable order (@sebmarkbage [#33665](https://github.com/facebook/react/pull/33665))
|
||||
|
||||
#### React Reconciler
|
||||
|
||||
- [createContainer](https://github.com/facebook/react/blob/v19.2.0/packages/react-reconciler/src/ReactFiberReconciler.js#L255-L261) and [createHydrationContainer](https://github.com/facebook/react/blob/v19.2.0/packages/react-reconciler/src/ReactFiberReconciler.js#L305-L312) had their parameter order adjusted after `on*` handlers to account for upcoming experimental APIs
|
||||
|
||||
## 19.1.1 (July 28, 2025)
|
||||
|
||||
### React
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
acdlite
|
||||
eps1lon
|
||||
EugeneChoi4
|
||||
gaearon
|
||||
gnoff
|
||||
unstubbable
|
||||
|
||||
@@ -7,18 +7,18 @@
|
||||
//
|
||||
// The @latest channel uses the version as-is, e.g.:
|
||||
//
|
||||
// 19.1.0
|
||||
// 19.3.0
|
||||
//
|
||||
// The @canary channel appends additional information, with the scheme
|
||||
// <version>-<label>-<commit_sha>, e.g.:
|
||||
//
|
||||
// 19.1.0-canary-a1c2d3e4
|
||||
// 19.3.0-canary-a1c2d3e4
|
||||
//
|
||||
// The @experimental channel doesn't include a version, only a date and a sha, e.g.:
|
||||
//
|
||||
// 0.0.0-experimental-241c4467e-20200129
|
||||
|
||||
const ReactVersion = '19.2.0';
|
||||
const ReactVersion = '19.3.0';
|
||||
|
||||
// The label used by the @canary channel. Represents the upcoming release's
|
||||
// stability. Most of the time, this will be "canary", but we may temporarily
|
||||
@@ -33,8 +33,8 @@ const canaryChannelLabel = 'canary';
|
||||
const rcNumber = 0;
|
||||
|
||||
const stablePackages = {
|
||||
'eslint-plugin-react-hooks': '6.1.0',
|
||||
'jest-react': '0.17.0',
|
||||
'eslint-plugin-react-hooks': '7.0.0',
|
||||
'jest-react': '0.18.0',
|
||||
react: ReactVersion,
|
||||
'react-art': ReactVersion,
|
||||
'react-dom': ReactVersion,
|
||||
@@ -42,12 +42,12 @@ const stablePackages = {
|
||||
'react-server-dom-turbopack': ReactVersion,
|
||||
'react-server-dom-parcel': ReactVersion,
|
||||
'react-is': ReactVersion,
|
||||
'react-reconciler': '0.33.0',
|
||||
'react-refresh': '0.18.0',
|
||||
'react-reconciler': '0.34.0',
|
||||
'react-refresh': '0.19.0',
|
||||
'react-test-renderer': ReactVersion,
|
||||
'use-subscription': '1.12.0',
|
||||
'use-sync-external-store': '1.6.0',
|
||||
scheduler: '0.27.0',
|
||||
'use-subscription': '1.13.0',
|
||||
'use-sync-external-store': '1.7.0',
|
||||
scheduler: '0.28.0',
|
||||
};
|
||||
|
||||
// These packages do not exist in the @canary or @latest channel, only
|
||||
|
||||
@@ -8,6 +8,7 @@ module.exports = {
|
||||
'@babel/plugin-syntax-jsx',
|
||||
'@babel/plugin-transform-flow-strip-types',
|
||||
['@babel/plugin-transform-class-properties', {loose: true}],
|
||||
['@babel/plugin-transform-private-methods', {loose: true}],
|
||||
'@babel/plugin-transform-classes',
|
||||
],
|
||||
presets: [
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { c as _c } from "react/compiler-runtime"; //
|
||||
@compilationMode:"all"
|
||||
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"all"
|
||||
function nonReactFn() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { PluginOptions } from
|
||||
'babel-plugin-react-compiler/dist';
|
||||
({
|
||||
//compilationMode: "all"
|
||||
} satisfies PluginOptions);
|
||||
@@ -0,0 +1,14 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
export default function TestComponent(t0) {
|
||||
const $ = _c(2);
|
||||
const { x } = t0;
|
||||
let t1;
|
||||
if ($[0] !== x || true) {
|
||||
t1 = <Button>{x}</Button>;
|
||||
$[0] = x;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
@@ -5,8 +5,9 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {expect, test, type Page} from '@playwright/test';
|
||||
import {encodeStore, type Store} from '../../lib/stores';
|
||||
import {defaultConfig} from '../../lib/defaultStore';
|
||||
import {format} from 'prettier';
|
||||
|
||||
function isMonacoLoaded(): boolean {
|
||||
@@ -20,6 +21,16 @@ function formatPrint(data: Array<string>): Promise<string> {
|
||||
return format(data.join(''), {parser: 'babel'});
|
||||
}
|
||||
|
||||
async function expandConfigs(page: Page): Promise<void> {
|
||||
const expandButton = page.locator('[title="Expand config editor"]');
|
||||
await expandButton.click();
|
||||
await page.waitForSelector('.monaco-editor-config', {state: 'visible'});
|
||||
}
|
||||
|
||||
const TEST_SOURCE = `export default function TestComponent({ x }) {
|
||||
return <Button>{x}</Button>;
|
||||
}`;
|
||||
|
||||
const TEST_CASE_INPUTS = [
|
||||
{
|
||||
name: 'module-scope-use-memo',
|
||||
@@ -121,10 +132,9 @@ test('editor should open successfully', async ({page}) => {
|
||||
|
||||
test('editor should compile from hash successfully', async ({page}) => {
|
||||
const store: Store = {
|
||||
source: `export default function TestComponent({ x }) {
|
||||
return <Button>{x}</Button>;
|
||||
}
|
||||
`,
|
||||
source: TEST_SOURCE,
|
||||
config: defaultConfig,
|
||||
showInternals: false,
|
||||
};
|
||||
const hash = encodeStore(store);
|
||||
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
|
||||
@@ -136,7 +146,7 @@ test('editor should compile from hash successfully', async ({page}) => {
|
||||
path: 'test-results/01-compiles-from-hash.png',
|
||||
});
|
||||
const text =
|
||||
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
|
||||
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
|
||||
const output = await formatPrint(text);
|
||||
|
||||
expect(output).not.toEqual('');
|
||||
@@ -145,10 +155,9 @@ test('editor should compile from hash successfully', async ({page}) => {
|
||||
|
||||
test('reset button works', async ({page}) => {
|
||||
const store: Store = {
|
||||
source: `export default function TestComponent({ x }) {
|
||||
return <Button>{x}</Button>;
|
||||
}
|
||||
`,
|
||||
source: TEST_SOURCE,
|
||||
config: defaultConfig,
|
||||
showInternals: false,
|
||||
};
|
||||
const hash = encodeStore(store);
|
||||
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
|
||||
@@ -157,33 +166,201 @@ test('reset button works', async ({page}) => {
|
||||
// Reset button works
|
||||
page.on('dialog', dialog => dialog.accept());
|
||||
await page.getByRole('button', {name: 'Reset'}).click();
|
||||
await expandConfigs(page);
|
||||
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: 'test-results/02-reset-button-works.png',
|
||||
});
|
||||
const text =
|
||||
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
|
||||
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
|
||||
const output = await formatPrint(text);
|
||||
|
||||
const configText =
|
||||
(await page.locator('.monaco-editor-config').allInnerTexts()) ?? [];
|
||||
const configOutput = configText.join('');
|
||||
|
||||
expect(output).not.toEqual('');
|
||||
expect(output).toMatchSnapshot('02-default-output.txt');
|
||||
expect(configOutput).not.toEqual('');
|
||||
expect(configOutput).toMatchSnapshot('default-config.txt');
|
||||
});
|
||||
|
||||
test('defaults load when only source is in Store', async ({page}) => {
|
||||
// Test for backwards compatibility
|
||||
const partial = {
|
||||
source: TEST_SOURCE,
|
||||
};
|
||||
const hash = encodeStore(partial as Store);
|
||||
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
|
||||
await page.waitForFunction(isMonacoLoaded);
|
||||
await expandConfigs(page);
|
||||
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: 'test-results/03-missing-defaults.png',
|
||||
});
|
||||
|
||||
// Config editor has default config
|
||||
const configText =
|
||||
(await page.locator('.monaco-editor-config').allInnerTexts()) ?? [];
|
||||
const configOutput = configText.join('');
|
||||
|
||||
expect(configOutput).not.toEqual('');
|
||||
expect(configOutput).toMatchSnapshot('default-config.txt');
|
||||
|
||||
const checkbox = page.locator('label.show-internals');
|
||||
await expect(checkbox).not.toBeChecked();
|
||||
const ssaTab = page.locator('text=SSA');
|
||||
await expect(ssaTab).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('show internals button toggles correctly', async ({page}) => {
|
||||
await page.goto(`/`, {waitUntil: 'networkidle'});
|
||||
await page.waitForFunction(isMonacoLoaded);
|
||||
|
||||
// show internals should be off
|
||||
const checkbox = page.locator('label.show-internals');
|
||||
await checkbox.click();
|
||||
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: 'test-results/04-show-internals-on.png',
|
||||
});
|
||||
|
||||
await expect(checkbox).toBeChecked();
|
||||
|
||||
const ssaTab = page.locator('text=SSA');
|
||||
await expect(ssaTab).toBeVisible();
|
||||
});
|
||||
|
||||
test('error is displayed when config has syntax error', async ({page}) => {
|
||||
const store: Store = {
|
||||
source: TEST_SOURCE,
|
||||
config: `compilationMode: `,
|
||||
showInternals: false,
|
||||
};
|
||||
const hash = encodeStore(store);
|
||||
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
|
||||
await page.waitForFunction(isMonacoLoaded);
|
||||
await expandConfigs(page);
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: 'test-results/05-config-syntax-error.png',
|
||||
});
|
||||
|
||||
const text =
|
||||
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
|
||||
const output = text.join('');
|
||||
|
||||
// Remove hidden chars
|
||||
expect(output.replace(/\s+/g, ' ')).toContain('Invalid override format');
|
||||
});
|
||||
|
||||
test('error is displayed when config has validation error', async ({page}) => {
|
||||
const store: Store = {
|
||||
source: TEST_SOURCE,
|
||||
config: `import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
|
||||
|
||||
({
|
||||
compilationMode: "123"
|
||||
} satisfies PluginOptions);`,
|
||||
showInternals: false,
|
||||
};
|
||||
const hash = encodeStore(store);
|
||||
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
|
||||
await page.waitForFunction(isMonacoLoaded);
|
||||
await expandConfigs(page);
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: 'test-results/06-config-validation-error.png',
|
||||
});
|
||||
|
||||
const text =
|
||||
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
|
||||
const output = text.join('');
|
||||
|
||||
expect(output.replace(/\s+/g, ' ')).toContain('Unexpected compilationMode');
|
||||
});
|
||||
|
||||
test('disableMemoizationForDebugging flag works as expected', async ({
|
||||
page,
|
||||
}) => {
|
||||
const store: Store = {
|
||||
source: TEST_SOURCE,
|
||||
config: `import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
|
||||
|
||||
({
|
||||
environment: {
|
||||
disableMemoizationForDebugging: true
|
||||
}
|
||||
} satisfies PluginOptions);`,
|
||||
showInternals: false,
|
||||
};
|
||||
const hash = encodeStore(store);
|
||||
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
|
||||
await page.waitForFunction(isMonacoLoaded);
|
||||
await expandConfigs(page);
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: 'test-results/07-config-disableMemoizationForDebugging-flag.png',
|
||||
});
|
||||
|
||||
const text =
|
||||
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
|
||||
const output = await formatPrint(text);
|
||||
|
||||
expect(output).not.toEqual('');
|
||||
expect(output).toMatchSnapshot('disableMemoizationForDebugging-output.txt');
|
||||
});
|
||||
|
||||
test('error is displayed when source has syntax error', async ({page}) => {
|
||||
const syntaxErrorSource = `function TestComponent(props) {
|
||||
const oops = props.
|
||||
return (
|
||||
<>{oops}</>
|
||||
);
|
||||
}`;
|
||||
const store: Store = {
|
||||
source: syntaxErrorSource,
|
||||
config: defaultConfig,
|
||||
showInternals: false,
|
||||
};
|
||||
const hash = encodeStore(store);
|
||||
await page.goto(`/#${hash}`);
|
||||
await page.waitForFunction(isMonacoLoaded);
|
||||
await expandConfigs(page);
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: 'test-results/08-source-syntax-error.png',
|
||||
});
|
||||
|
||||
const text =
|
||||
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
|
||||
const output = text.join('');
|
||||
|
||||
expect(output.replace(/\s+/g, ' ')).toContain(
|
||||
'Expected identifier to be defined before being used',
|
||||
);
|
||||
});
|
||||
|
||||
TEST_CASE_INPUTS.forEach((t, idx) =>
|
||||
test(`playground compiles: ${t.name}`, async ({page}) => {
|
||||
const store: Store = {
|
||||
source: t.input,
|
||||
config: defaultConfig,
|
||||
showInternals: false,
|
||||
};
|
||||
const hash = encodeStore(store);
|
||||
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
|
||||
await page.waitForFunction(isMonacoLoaded);
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: `test-results/03-0${idx}-${t.name}.png`,
|
||||
path: `test-results/08-0${idx}-${t.name}.png`,
|
||||
});
|
||||
|
||||
const text =
|
||||
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
|
||||
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
|
||||
let output: string;
|
||||
if (t.noFormat) {
|
||||
output = text.join('');
|
||||
|
||||
@@ -1,56 +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 type {NextPage} from 'next';
|
||||
import Head from 'next/head';
|
||||
import {SnackbarProvider} from 'notistack';
|
||||
import {Editor, Header, StoreProvider} from '../components';
|
||||
import MessageSnackbar from '../components/Message';
|
||||
|
||||
const Home: NextPage = () => {
|
||||
return (
|
||||
<div className="flex flex-col w-screen h-screen font-light">
|
||||
<Head>
|
||||
<title>
|
||||
{process.env.NODE_ENV === 'development'
|
||||
? '[DEV] React Compiler Playground'
|
||||
: 'React Compiler Playground'}
|
||||
</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"></meta>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/Source-Code-Pro-Regular.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/Optimistic_Display_W_Lt.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
</Head>
|
||||
<StoreProvider>
|
||||
<SnackbarProvider
|
||||
preventDuplicate
|
||||
maxSnack={10}
|
||||
Components={{message: MessageSnackbar}}>
|
||||
<Header />
|
||||
<Editor />
|
||||
</SnackbarProvider>
|
||||
</StoreProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
126
compiler/apps/playground/components/AccordionWindow.tsx
Normal file
126
compiler/apps/playground/components/AccordionWindow.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 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 {Resizable} from 're-resizable';
|
||||
import React, {
|
||||
useId,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
startTransition,
|
||||
} from 'react';
|
||||
import {EXPAND_ACCORDION_TRANSITION} from '../lib/transitionTypes';
|
||||
|
||||
type TabsRecord = Map<string, React.ReactNode>;
|
||||
|
||||
export default function AccordionWindow(props: {
|
||||
defaultTab: string | null;
|
||||
tabs: TabsRecord;
|
||||
tabsOpen: Set<string>;
|
||||
setTabsOpen: (newTab: Set<string>) => void;
|
||||
changedPasses: Set<string>;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<div className="flex-1 min-w-[550px] sm:min-w-0">
|
||||
<div className="flex flex-row h-full">
|
||||
{Array.from(props.tabs.keys()).map(name => {
|
||||
return (
|
||||
<AccordionWindowItem
|
||||
name={name}
|
||||
key={name}
|
||||
tabs={props.tabs}
|
||||
tabsOpen={props.tabsOpen}
|
||||
setTabsOpen={props.setTabsOpen}
|
||||
hasChanged={props.changedPasses.has(name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AccordionWindowItem({
|
||||
name,
|
||||
tabs,
|
||||
tabsOpen,
|
||||
setTabsOpen,
|
||||
hasChanged,
|
||||
}: {
|
||||
name: string;
|
||||
tabs: TabsRecord;
|
||||
tabsOpen: Set<string>;
|
||||
setTabsOpen: (newTab: Set<string>) => void;
|
||||
hasChanged: boolean;
|
||||
isFailure: boolean;
|
||||
}): React.ReactElement {
|
||||
const id = useId();
|
||||
const isShow = tabsOpen.has(name);
|
||||
|
||||
const transitionName = `accordion-window-item-${id}`;
|
||||
|
||||
const toggleTabs = (): void => {
|
||||
startTransition(() => {
|
||||
addTransitionType(EXPAND_ACCORDION_TRANSITION);
|
||||
const nextState = new Set(tabsOpen);
|
||||
if (nextState.has(name)) {
|
||||
nextState.delete(name);
|
||||
} else {
|
||||
nextState.add(name);
|
||||
}
|
||||
setTabsOpen(nextState);
|
||||
});
|
||||
};
|
||||
|
||||
// Replace spaces with non-breaking spaces
|
||||
const displayName = name.replace(/ /g, '\u00A0');
|
||||
|
||||
return (
|
||||
<div key={name} className="flex flex-row">
|
||||
{isShow ? (
|
||||
<ViewTransition
|
||||
name={transitionName}
|
||||
update={{
|
||||
[EXPAND_ACCORDION_TRANSITION]: 'expand-accordion',
|
||||
default: 'none',
|
||||
}}>
|
||||
<Resizable className="border-r" minWidth={550} enable={{right: true}}>
|
||||
<h2
|
||||
title="Minimize tab"
|
||||
aria-label="Minimize tab"
|
||||
onClick={toggleTabs}
|
||||
className={`p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 ${
|
||||
hasChanged ? 'font-bold' : 'font-light'
|
||||
} text-secondary hover:text-link`}>
|
||||
- {displayName}
|
||||
</h2>
|
||||
{tabs.get(name) ?? <div>No output for {name}</div>}
|
||||
</Resizable>
|
||||
</ViewTransition>
|
||||
) : (
|
||||
<ViewTransition
|
||||
name={transitionName}
|
||||
update={{
|
||||
[EXPAND_ACCORDION_TRANSITION]: 'expand-accordion',
|
||||
default: 'none',
|
||||
}}>
|
||||
<div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
|
||||
<button
|
||||
title={`Expand compiler tab: ${name}`}
|
||||
aria-label={`Expand compiler tab: ${name}`}
|
||||
style={{transform: 'rotate(90deg) translate(-50%)'}}
|
||||
onClick={toggleTabs}
|
||||
className={`flex-grow-0 w-5 transition-colors duration-150 ease-in ${
|
||||
hasChanged ? 'font-bold' : 'font-light'
|
||||
} text-secondary hover:text-link`}>
|
||||
{displayName}
|
||||
</button>
|
||||
</div>
|
||||
</ViewTransition>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
215
compiler/apps/playground/components/Editor/ConfigEditor.tsx
Normal file
215
compiler/apps/playground/components/Editor/ConfigEditor.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* 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 MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
|
||||
import type {editor} from 'monaco-editor';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import React, {
|
||||
useState,
|
||||
useRef,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
startTransition,
|
||||
} from 'react';
|
||||
import {Resizable} from 're-resizable';
|
||||
import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
import {monacoConfigOptions} from './monacoOptions';
|
||||
import {IconChevron} from '../Icons/IconChevron';
|
||||
import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes';
|
||||
|
||||
// @ts-expect-error - webpack asset/source loader handles .d.ts files as strings
|
||||
import compilerTypeDefs from 'babel-plugin-react-compiler/dist/index.d.ts';
|
||||
|
||||
loader.config({monaco});
|
||||
|
||||
export default function ConfigEditor({
|
||||
formattedAppliedConfig,
|
||||
}: {
|
||||
formattedAppliedConfig: string;
|
||||
}): React.ReactElement {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
// TODO: Use <Activity> when it is compatible with Monaco: https://github.com/suren-atoyan/monaco-react/issues/753
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: isExpanded ? 'block' : 'none',
|
||||
}}>
|
||||
<ExpandedEditor
|
||||
onToggle={() => {
|
||||
startTransition(() => {
|
||||
addTransitionType(CONFIG_PANEL_TRANSITION);
|
||||
setIsExpanded(false);
|
||||
});
|
||||
}}
|
||||
formattedAppliedConfig={formattedAppliedConfig}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: !isExpanded ? 'block' : 'none',
|
||||
}}>
|
||||
<CollapsedEditor
|
||||
onToggle={() => {
|
||||
startTransition(() => {
|
||||
addTransitionType(CONFIG_PANEL_TRANSITION);
|
||||
setIsExpanded(true);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ExpandedEditor({
|
||||
onToggle,
|
||||
formattedAppliedConfig,
|
||||
}: {
|
||||
onToggle: (expanded: boolean) => void;
|
||||
formattedAppliedConfig: string;
|
||||
}): React.ReactElement {
|
||||
const store = useStore();
|
||||
const dispatchStore = useStoreDispatch();
|
||||
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const handleChange: (value: string | undefined) => void = (
|
||||
value: string | undefined,
|
||||
) => {
|
||||
if (value === undefined) return;
|
||||
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
|
||||
debounceTimerRef.current = setTimeout(() => {
|
||||
dispatchStore({
|
||||
type: 'updateConfig',
|
||||
payload: {
|
||||
config: value,
|
||||
},
|
||||
});
|
||||
}, 500); // 500ms debounce delay
|
||||
};
|
||||
|
||||
const handleMount: (
|
||||
_: editor.IStandaloneCodeEditor,
|
||||
monaco: Monaco,
|
||||
) => void = (_, monaco) => {
|
||||
// Add the babel-plugin-react-compiler type definitions to Monaco
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
||||
//@ts-expect-error - compilerTypeDefs is a string
|
||||
compilerTypeDefs,
|
||||
'file:///node_modules/babel-plugin-react-compiler/dist/index.d.ts',
|
||||
);
|
||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
||||
target: monaco.languages.typescript.ScriptTarget.Latest,
|
||||
allowNonTsExtensions: true,
|
||||
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
||||
module: monaco.languages.typescript.ModuleKind.ESNext,
|
||||
noEmit: true,
|
||||
strict: false,
|
||||
esModuleInterop: true,
|
||||
allowSyntheticDefaultImports: true,
|
||||
jsx: monaco.languages.typescript.JsxEmit.React,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewTransition
|
||||
update={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}>
|
||||
<Resizable
|
||||
minWidth={300}
|
||||
maxWidth={600}
|
||||
defaultSize={{width: 350}}
|
||||
enable={{right: true, bottom: false}}>
|
||||
<div className="bg-blue-10 relative h-full flex flex-col !h-[calc(100vh_-_3.5rem)] border border-gray-300">
|
||||
<div
|
||||
className="absolute w-8 h-16 bg-blue-10 rounded-r-full flex items-center justify-center z-[2] cursor-pointer border border-l-0 border-gray-300"
|
||||
title="Minimize config editor"
|
||||
onClick={onToggle}
|
||||
style={{
|
||||
top: '50%',
|
||||
marginTop: '-32px',
|
||||
right: '-32px',
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
}}>
|
||||
<IconChevron displayDirection="left" className="text-blue-50" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col m-2 mb-2">
|
||||
<div className="pb-2">
|
||||
<h2 className="inline-block text-blue-50 py-1.5 px-1.5 xs:px-3 sm:px-4 text-sm">
|
||||
Config Overrides
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex-1 border border-gray-300">
|
||||
<MonacoEditor
|
||||
path={'config.ts'}
|
||||
language={'typescript'}
|
||||
value={store.config}
|
||||
onMount={handleMount}
|
||||
onChange={handleChange}
|
||||
loading={''}
|
||||
className="monaco-editor-config"
|
||||
options={monacoConfigOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col m-2">
|
||||
<div className="pb-2">
|
||||
<h2 className="inline-block text-blue-50 py-1.5 px-1.5 xs:px-3 sm:px-4 text-sm">
|
||||
Applied Configs
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex-1 border border-gray-300">
|
||||
<MonacoEditor
|
||||
path={'applied-config.js'}
|
||||
language={'javascript'}
|
||||
value={formattedAppliedConfig}
|
||||
loading={''}
|
||||
className="monaco-editor-applied-config"
|
||||
options={{
|
||||
...monacoConfigOptions,
|
||||
readOnly: true,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Resizable>
|
||||
</ViewTransition>
|
||||
);
|
||||
}
|
||||
|
||||
function CollapsedEditor({
|
||||
onToggle,
|
||||
}: {
|
||||
onToggle: () => void;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<div
|
||||
className="w-4 !h-[calc(100vh_-_3.5rem)]"
|
||||
style={{position: 'relative'}}>
|
||||
<div
|
||||
className="absolute w-10 h-16 bg-blue-10 hover:translate-x-2 transition-transform rounded-r-full flex items-center justify-center z-[2] cursor-pointer border border-gray-300"
|
||||
title="Expand config editor"
|
||||
onClick={onToggle}
|
||||
style={{
|
||||
top: '50%',
|
||||
marginTop: '-32px',
|
||||
left: '-8px',
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
}}>
|
||||
<IconChevron displayDirection="right" className="text-blue-50" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,313 +5,30 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {parse as babelParse, ParseResult} from '@babel/parser';
|
||||
import * as HermesParser from 'hermes-parser';
|
||||
import * as t from '@babel/types';
|
||||
import BabelPluginReactCompiler, {
|
||||
CompilerError,
|
||||
import {
|
||||
CompilerErrorDetail,
|
||||
CompilerDiagnostic,
|
||||
Effect,
|
||||
ErrorSeverity,
|
||||
parseConfigPragmaForTests,
|
||||
ValueKind,
|
||||
type Hook,
|
||||
PluginOptions,
|
||||
CompilerPipelineValue,
|
||||
parsePluginOptions,
|
||||
printReactiveFunctionWithOutlined,
|
||||
printFunctionWithOutlined,
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import clsx from 'clsx';
|
||||
import invariant from 'invariant';
|
||||
import {useSnackbar} from 'notistack';
|
||||
import {useDeferredValue, useMemo} from 'react';
|
||||
import {useMountEffect} from '../../hooks';
|
||||
import {defaultStore} from '../../lib/defaultStore';
|
||||
import {
|
||||
createMessage,
|
||||
initStoreFromUrlOrLocalStorage,
|
||||
MessageLevel,
|
||||
MessageSource,
|
||||
type Store,
|
||||
} from '../../lib/stores';
|
||||
import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
import {useDeferredValue, useMemo, useState} from 'react';
|
||||
import {useStore} from '../StoreContext';
|
||||
import ConfigEditor from './ConfigEditor';
|
||||
import Input from './Input';
|
||||
import {
|
||||
CompilerOutput,
|
||||
CompilerTransformOutput,
|
||||
default as Output,
|
||||
PrintedCompilerPipelineValue,
|
||||
} from './Output';
|
||||
import {transformFromAstSync} from '@babel/core';
|
||||
import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint';
|
||||
|
||||
function parseInput(
|
||||
input: string,
|
||||
language: 'flow' | 'typescript',
|
||||
): ParseResult<t.File> {
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
if (language === 'flow') {
|
||||
return HermesParser.parse(input, {
|
||||
babel: true,
|
||||
flow: 'all',
|
||||
sourceType: 'module',
|
||||
enableExperimentalComponentSyntax: true,
|
||||
});
|
||||
} else {
|
||||
return babelParse(input, {
|
||||
plugins: ['typescript', 'jsx'],
|
||||
sourceType: 'module',
|
||||
}) as ParseResult<t.File>;
|
||||
}
|
||||
}
|
||||
|
||||
function invokeCompiler(
|
||||
source: string,
|
||||
language: 'flow' | 'typescript',
|
||||
options: PluginOptions,
|
||||
): CompilerTransformOutput {
|
||||
const ast = parseInput(source, language);
|
||||
let result = transformFromAstSync(ast, source, {
|
||||
filename: '_playgroundFile.js',
|
||||
highlightCode: false,
|
||||
retainLines: true,
|
||||
plugins: [[BabelPluginReactCompiler, options]],
|
||||
ast: true,
|
||||
sourceType: 'module',
|
||||
configFile: false,
|
||||
sourceMaps: true,
|
||||
babelrc: false,
|
||||
});
|
||||
if (result?.ast == null || result?.code == null || result?.map == null) {
|
||||
throw new Error('Expected successful compilation');
|
||||
}
|
||||
return {
|
||||
code: result.code,
|
||||
sourceMaps: result.map,
|
||||
language,
|
||||
};
|
||||
}
|
||||
|
||||
const COMMON_HOOKS: Array<[string, Hook]> = [
|
||||
[
|
||||
'useFragment',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'usePaginationFragment',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'useRefetchableFragment',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'useLazyLoadQuery',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'usePreloadedQuery',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
function compile(
|
||||
source: string,
|
||||
mode: 'compiler' | 'linter',
|
||||
): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
|
||||
const error = new CompilerError();
|
||||
const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
|
||||
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
|
||||
const entry = results.get(result.name);
|
||||
if (Array.isArray(entry)) {
|
||||
entry.push(result);
|
||||
} else {
|
||||
results.set(result.name, [result]);
|
||||
}
|
||||
};
|
||||
let language: 'flow' | 'typescript';
|
||||
if (source.match(/\@flow/)) {
|
||||
language = 'flow';
|
||||
} else {
|
||||
language = 'typescript';
|
||||
}
|
||||
let transformOutput;
|
||||
try {
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
const pragma = source.substring(0, source.indexOf('\n'));
|
||||
const logIR = (result: CompilerPipelineValue): void => {
|
||||
switch (result.kind) {
|
||||
case 'ast': {
|
||||
break;
|
||||
}
|
||||
case 'hir': {
|
||||
upsert({
|
||||
kind: 'hir',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'reactive': {
|
||||
upsert({
|
||||
kind: 'reactive',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printReactiveFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'debug': {
|
||||
upsert({
|
||||
kind: 'debug',
|
||||
fnName: null,
|
||||
name: result.name,
|
||||
value: result.value,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const _: never = result;
|
||||
throw new Error(`Unhandled result ${result}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
const parsedOptions = parseConfigPragmaForTests(pragma, {
|
||||
compilationMode: 'infer',
|
||||
environment:
|
||||
mode === 'linter'
|
||||
? {
|
||||
// enabled in compiler
|
||||
validateRefAccessDuringRender: false,
|
||||
// enabled in linter
|
||||
validateNoSetStateInRender: true,
|
||||
validateNoSetStateInEffects: true,
|
||||
validateNoJSXInTryStatements: true,
|
||||
validateNoImpureFunctionsInRender: true,
|
||||
validateStaticComponents: true,
|
||||
validateNoFreezingKnownMutableFunctions: true,
|
||||
validateNoVoidUseMemo: true,
|
||||
}
|
||||
: {
|
||||
/* use defaults for compiler mode */
|
||||
},
|
||||
});
|
||||
const opts: PluginOptions = parsePluginOptions({
|
||||
...parsedOptions,
|
||||
environment: {
|
||||
...parsedOptions.environment,
|
||||
customHooks: new Map([...COMMON_HOOKS]),
|
||||
},
|
||||
logger: {
|
||||
debugLogIRs: logIR,
|
||||
logEvent: (_filename: string | null, event: LoggerEvent) => {
|
||||
if (event.kind === 'CompileError') {
|
||||
otherErrors.push(event.detail);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
transformOutput = invokeCompiler(source, language, opts);
|
||||
} catch (err) {
|
||||
/**
|
||||
* error might be an invariant violation or other runtime error
|
||||
* (i.e. object shape that is not CompilerError)
|
||||
*/
|
||||
if (err instanceof CompilerError && err.details.length > 0) {
|
||||
error.merge(err);
|
||||
} else {
|
||||
/**
|
||||
* Handle unexpected failures by logging (to get a stack trace)
|
||||
* and reporting
|
||||
*/
|
||||
console.error(err);
|
||||
error.details.push(
|
||||
new CompilerErrorDetail({
|
||||
severity: ErrorSeverity.Invariant,
|
||||
reason: `Unexpected failure when transforming input! ${err}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Only include logger errors if there weren't other errors
|
||||
if (!error.hasErrors() && otherErrors.length !== 0) {
|
||||
otherErrors.forEach(e => error.details.push(e));
|
||||
}
|
||||
if (error.hasErrors()) {
|
||||
return [{kind: 'err', results, error}, language];
|
||||
}
|
||||
return [
|
||||
{kind: 'ok', results, transformOutput, errors: error.details},
|
||||
language,
|
||||
];
|
||||
}
|
||||
import {CompilerOutput, default as Output} from './Output';
|
||||
import {compile} from '../../lib/compilation';
|
||||
import prettyFormat from 'pretty-format';
|
||||
|
||||
export default function Editor(): JSX.Element {
|
||||
const store = useStore();
|
||||
const deferredStore = useDeferredValue(store);
|
||||
const dispatchStore = useStoreDispatch();
|
||||
const {enqueueSnackbar} = useSnackbar();
|
||||
const [compilerOutput, language] = useMemo(
|
||||
() => compile(deferredStore.source, 'compiler'),
|
||||
[deferredStore.source],
|
||||
const [compilerOutput, language, appliedOptions] = useMemo(
|
||||
() => compile(deferredStore.source, 'compiler', deferredStore.config),
|
||||
[deferredStore.source, deferredStore.config],
|
||||
);
|
||||
const [linterOutput] = useMemo(
|
||||
() => compile(deferredStore.source, 'linter'),
|
||||
[deferredStore.source],
|
||||
() => compile(deferredStore.source, 'linter', deferredStore.config),
|
||||
[deferredStore.source, deferredStore.config],
|
||||
);
|
||||
|
||||
useMountEffect(() => {
|
||||
let mountStore: Store;
|
||||
try {
|
||||
mountStore = initStoreFromUrlOrLocalStorage();
|
||||
} catch (e) {
|
||||
invariant(e instanceof Error, 'Only Error may be caught.');
|
||||
enqueueSnackbar(e.message, {
|
||||
variant: 'warning',
|
||||
...createMessage(
|
||||
'Bad URL - fell back to the default Playground.',
|
||||
MessageLevel.Info,
|
||||
MessageSource.Playground,
|
||||
),
|
||||
});
|
||||
mountStore = defaultStore;
|
||||
}
|
||||
dispatchStore({
|
||||
type: 'setStore',
|
||||
payload: {store: mountStore},
|
||||
});
|
||||
});
|
||||
const [formattedAppliedConfig, setFormattedAppliedConfig] = useState('');
|
||||
|
||||
let mergedOutput: CompilerOutput;
|
||||
let errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
|
||||
@@ -325,13 +42,25 @@ export default function Editor(): JSX.Element {
|
||||
mergedOutput = compilerOutput;
|
||||
errors = compilerOutput.error.details;
|
||||
}
|
||||
|
||||
if (appliedOptions) {
|
||||
const formatted = prettyFormat(appliedOptions, {
|
||||
printFunctionName: false,
|
||||
printBasicPrototype: false,
|
||||
});
|
||||
if (formatted !== formattedAppliedConfig) {
|
||||
setFormattedAppliedConfig(formatted);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex basis top-14">
|
||||
<div className={clsx('relative sm:basis-1/4')}>
|
||||
<Input language={language} errors={errors} />
|
||||
<div className="relative flex top-14">
|
||||
<div className="flex-shrink-0">
|
||||
<ConfigEditor formattedAppliedConfig={formattedAppliedConfig} />
|
||||
</div>
|
||||
<div className={clsx('flex sm:flex flex-wrap')}>
|
||||
<div className="flex flex-1 min-w-0">
|
||||
<Input language={language} errors={errors} />
|
||||
<Output store={deferredStore} compilerOutput={mergedOutput} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,22 +6,31 @@
|
||||
*/
|
||||
|
||||
import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
|
||||
import {CompilerErrorDetail} from 'babel-plugin-react-compiler';
|
||||
import {
|
||||
CompilerErrorDetail,
|
||||
CompilerDiagnostic,
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import invariant from 'invariant';
|
||||
import type {editor} from 'monaco-editor';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import {Resizable} from 're-resizable';
|
||||
import {useEffect, useState} from 'react';
|
||||
import {
|
||||
useEffect,
|
||||
useState,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
} from 'react';
|
||||
import {renderReactCompilerMarkers} from '../../lib/reactCompilerMonacoDiagnostics';
|
||||
import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
import TabbedWindow from '../TabbedWindow';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes';
|
||||
|
||||
// @ts-expect-error TODO: Make TS recognize .d.ts files, in addition to loading them with webpack.
|
||||
import React$Types from '../../node_modules/@types/react/index.d.ts';
|
||||
|
||||
loader.config({monaco});
|
||||
|
||||
type Props = {
|
||||
errors: Array<CompilerErrorDetail>;
|
||||
errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
|
||||
language: 'flow' | 'typescript';
|
||||
};
|
||||
|
||||
@@ -42,11 +51,6 @@ export default function Input({errors, language}: Props): JSX.Element {
|
||||
details: errors,
|
||||
source: store.source,
|
||||
});
|
||||
/**
|
||||
* N.B. that `tabSize` is a model property, not an editor property.
|
||||
* So, the tab size has to be set per model.
|
||||
*/
|
||||
model.updateOptions({tabSize: 2});
|
||||
}, [monaco, errors, store.source]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -79,11 +83,11 @@ export default function Input({errors, language}: Props): JSX.Element {
|
||||
});
|
||||
}, [monaco, language]);
|
||||
|
||||
const handleChange: (value: string | undefined) => void = value => {
|
||||
const handleChange: (value: string | undefined) => void = async value => {
|
||||
if (!value) return;
|
||||
|
||||
dispatchStore({
|
||||
type: 'updateFile',
|
||||
type: 'updateSource',
|
||||
payload: {
|
||||
source: value,
|
||||
},
|
||||
@@ -135,30 +139,42 @@ export default function Input({errors, language}: Props): JSX.Element {
|
||||
});
|
||||
};
|
||||
|
||||
const editorContent = (
|
||||
<MonacoEditor
|
||||
path={'index.js'}
|
||||
/**
|
||||
* .js and .jsx files are specified to be TS so that Monaco can actually
|
||||
* check their syntax using its TS language service. They are still JS files
|
||||
* due to their extensions, so TS language features don't work.
|
||||
*/
|
||||
language={'javascript'}
|
||||
value={store.source}
|
||||
onMount={handleMount}
|
||||
onChange={handleChange}
|
||||
className="monaco-editor-input"
|
||||
options={monacoOptions}
|
||||
loading={''}
|
||||
/>
|
||||
);
|
||||
|
||||
const tabs = new Map([['Input', editorContent]]);
|
||||
const [activeTab, setActiveTab] = useState('Input');
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col flex-none border-r border-gray-200">
|
||||
<Resizable
|
||||
minWidth={650}
|
||||
enable={{right: true}}
|
||||
/**
|
||||
* Restrict MonacoEditor's height, since the config autoLayout:true
|
||||
* will grow the editor to fit within parent element
|
||||
*/
|
||||
className="!h-[calc(100vh_-_3.5rem)]">
|
||||
<MonacoEditor
|
||||
path={'index.js'}
|
||||
/**
|
||||
* .js and .jsx files are specified to be TS so that Monaco can actually
|
||||
* check their syntax using its TS language service. They are still JS files
|
||||
* due to their extensions, so TS language features don't work.
|
||||
*/
|
||||
language={'javascript'}
|
||||
value={store.source}
|
||||
onMount={handleMount}
|
||||
onChange={handleChange}
|
||||
options={monacoOptions}
|
||||
/>
|
||||
</Resizable>
|
||||
</div>
|
||||
<ViewTransition
|
||||
update={{
|
||||
[CONFIG_PANEL_TRANSITION]: 'container',
|
||||
default: 'none',
|
||||
}}>
|
||||
<div className="flex-1 min-w-[550px] sm:min-w-0">
|
||||
<div className="flex flex-col h-full !h-[calc(100vh_-_3.5rem)] border-r border-gray-200">
|
||||
<TabbedWindow
|
||||
tabs={tabs}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ViewTransition>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,15 +19,38 @@ import {
|
||||
import parserBabel from 'prettier/plugins/babel';
|
||||
import * as prettierPluginEstree from 'prettier/plugins/estree';
|
||||
import * as prettier from 'prettier/standalone';
|
||||
import {memo, ReactNode, useEffect, useState} from 'react';
|
||||
import {type Store} from '../../lib/stores';
|
||||
import {
|
||||
memo,
|
||||
ReactNode,
|
||||
use,
|
||||
useState,
|
||||
Suspense,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
startTransition,
|
||||
} from 'react';
|
||||
import AccordionWindow from '../AccordionWindow';
|
||||
import TabbedWindow from '../TabbedWindow';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
import {BabelFileResult} from '@babel/core';
|
||||
import {
|
||||
CONFIG_PANEL_TRANSITION,
|
||||
TOGGLE_INTERNALS_TRANSITION,
|
||||
EXPAND_ACCORDION_TRANSITION,
|
||||
} from '../../lib/transitionTypes';
|
||||
import {LRUCache} from 'lru-cache';
|
||||
|
||||
const MemoizedOutput = memo(Output);
|
||||
|
||||
export default MemoizedOutput;
|
||||
|
||||
export const BASIC_OUTPUT_TAB_NAMES = ['Output', 'SourceMap'];
|
||||
|
||||
const tabifyCache = new LRUCache<Store, Promise<Map<string, ReactNode>>>({
|
||||
max: 5,
|
||||
});
|
||||
|
||||
export type PrintedCompilerPipelineValue =
|
||||
| {
|
||||
kind: 'hir';
|
||||
@@ -64,12 +87,16 @@ type Props = {
|
||||
async function tabify(
|
||||
source: string,
|
||||
compilerOutput: CompilerOutput,
|
||||
showInternals: boolean,
|
||||
): Promise<Map<string, ReactNode>> {
|
||||
const tabs = new Map<string, React.ReactNode>();
|
||||
const reorderedTabs = new Map<string, React.ReactNode>();
|
||||
const concattedResults = new Map<string, string>();
|
||||
// Concat all top level function declaration results into a single tab for each pass
|
||||
for (const [passName, results] of compilerOutput.results) {
|
||||
if (!showInternals && !BASIC_OUTPUT_TAB_NAMES.includes(passName)) {
|
||||
continue;
|
||||
}
|
||||
for (const result of results) {
|
||||
switch (result.kind) {
|
||||
case 'hir': {
|
||||
@@ -192,6 +219,25 @@ ${code}
|
||||
return reorderedTabs;
|
||||
}
|
||||
|
||||
function tabifyCached(
|
||||
store: Store,
|
||||
compilerOutput: CompilerOutput,
|
||||
): Promise<Map<string, ReactNode>> {
|
||||
const cached = tabifyCache.get(store);
|
||||
if (cached) return cached;
|
||||
const result = tabify(store.source, compilerOutput, store.showInternals);
|
||||
tabifyCache.set(store, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
function Fallback(): JSX.Element {
|
||||
return (
|
||||
<div className="w-full h-monaco_small sm:h-monaco flex items-center justify-center">
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function utf16ToUTF8(s: string): string {
|
||||
return unescape(encodeURIComponent(s));
|
||||
}
|
||||
@@ -205,12 +251,18 @@ function getSourceMapUrl(code: string, map: string): string | null {
|
||||
}
|
||||
|
||||
function Output({store, compilerOutput}: Props): JSX.Element {
|
||||
return (
|
||||
<Suspense fallback={<Fallback />}>
|
||||
<OutputContent store={store} compilerOutput={compilerOutput} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function OutputContent({store, compilerOutput}: Props): JSX.Element {
|
||||
const [tabsOpen, setTabsOpen] = useState<Set<string>>(
|
||||
() => new Set(['Output']),
|
||||
);
|
||||
const [tabs, setTabs] = useState<Map<string, React.ReactNode>>(
|
||||
() => new Map(),
|
||||
);
|
||||
const [activeTab, setActiveTab] = useState<string>('Output');
|
||||
|
||||
/*
|
||||
* Update the active tab back to the output or errors tab when the compilation state
|
||||
@@ -219,17 +271,19 @@ function Output({store, compilerOutput}: Props): JSX.Element {
|
||||
const [previousOutputKind, setPreviousOutputKind] = useState(
|
||||
compilerOutput.kind,
|
||||
);
|
||||
const isFailure = compilerOutput.kind !== 'ok';
|
||||
|
||||
if (compilerOutput.kind !== previousOutputKind) {
|
||||
setPreviousOutputKind(compilerOutput.kind);
|
||||
setTabsOpen(new Set(['Output']));
|
||||
if (isFailure) {
|
||||
startTransition(() => {
|
||||
addTransitionType(EXPAND_ACCORDION_TRANSITION);
|
||||
setTabsOpen(prev => new Set(prev).add('Output'));
|
||||
setActiveTab('Output');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
tabify(store.source, compilerOutput).then(tabs => {
|
||||
setTabs(tabs);
|
||||
});
|
||||
}, [store.source, compilerOutput]);
|
||||
|
||||
const changedPasses: Set<string> = new Set(['Output', 'HIR']); // Initial and final passes should always be bold
|
||||
let lastResult: string = '';
|
||||
for (const [passName, results] of compilerOutput.results) {
|
||||
@@ -244,17 +298,40 @@ function Output({store, compilerOutput}: Props): JSX.Element {
|
||||
lastResult = currResult;
|
||||
}
|
||||
}
|
||||
const tabs = use(tabifyCached(store, compilerOutput));
|
||||
|
||||
if (!store.showInternals) {
|
||||
return (
|
||||
<ViewTransition
|
||||
update={{
|
||||
[CONFIG_PANEL_TRANSITION]: 'container',
|
||||
[TOGGLE_INTERNALS_TRANSITION]: '',
|
||||
default: 'none',
|
||||
}}>
|
||||
<TabbedWindow
|
||||
tabs={tabs}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
/>
|
||||
</ViewTransition>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabbedWindow
|
||||
defaultTab="HIR"
|
||||
<ViewTransition
|
||||
update={{
|
||||
[CONFIG_PANEL_TRANSITION]: 'accordion-container',
|
||||
[TOGGLE_INTERNALS_TRANSITION]: '',
|
||||
default: 'none',
|
||||
}}>
|
||||
<AccordionWindow
|
||||
defaultTab={store.showInternals ? 'HIR' : 'Output'}
|
||||
setTabsOpen={setTabsOpen}
|
||||
tabsOpen={tabsOpen}
|
||||
tabs={tabs}
|
||||
changedPasses={changedPasses}
|
||||
/>
|
||||
</>
|
||||
</ViewTransition>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -306,20 +383,29 @@ function TextTabContent({
|
||||
<DiffEditor
|
||||
original={diff}
|
||||
modified={output}
|
||||
loading={''}
|
||||
options={{
|
||||
...monacoOptions,
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
},
|
||||
dimension: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
readOnly: true,
|
||||
lineNumbers: 'off',
|
||||
glyphMargin: false,
|
||||
// Undocumented see https://github.com/Microsoft/vscode/issues/30795#issuecomment-410998882
|
||||
lineDecorationsWidth: 0,
|
||||
lineNumbersMinChars: 0,
|
||||
overviewRulerLanes: 0,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<MonacoEditor
|
||||
language={language ?? 'javascript'}
|
||||
value={output}
|
||||
loading={''}
|
||||
className="monaco-editor-output"
|
||||
options={{
|
||||
...monacoOptions,
|
||||
readOnly: true,
|
||||
|
||||
@@ -29,4 +29,17 @@ export const monacoOptions: Partial<EditorProps['options']> = {
|
||||
automaticLayout: true,
|
||||
wordWrap: 'on',
|
||||
wrappingIndent: 'same',
|
||||
|
||||
tabSize: 2,
|
||||
};
|
||||
|
||||
export const monacoConfigOptions: Partial<EditorProps['options']> = {
|
||||
...monacoOptions,
|
||||
lineNumbers: 'off',
|
||||
renderLineHighlight: 'none',
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
fontSize: 12,
|
||||
scrollBeyondLastLine: false,
|
||||
glyphMargin: false,
|
||||
};
|
||||
|
||||
@@ -10,14 +10,20 @@ import {CheckIcon} from '@heroicons/react/solid';
|
||||
import clsx from 'clsx';
|
||||
import Link from 'next/link';
|
||||
import {useSnackbar} from 'notistack';
|
||||
import {useState} from 'react';
|
||||
import {
|
||||
useState,
|
||||
startTransition,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
} from 'react';
|
||||
import {defaultStore} from '../lib/defaultStore';
|
||||
import {IconGitHub} from './Icons/IconGitHub';
|
||||
import Logo from './Logo';
|
||||
import {useStoreDispatch} from './StoreContext';
|
||||
import {useStore, useStoreDispatch} from './StoreContext';
|
||||
import {TOGGLE_INTERNALS_TRANSITION} from '../lib/transitionTypes';
|
||||
|
||||
export default function Header(): JSX.Element {
|
||||
const [showCheck, setShowCheck] = useState(false);
|
||||
const store = useStore();
|
||||
const dispatchStore = useStoreDispatch();
|
||||
const {enqueueSnackbar, closeSnackbar} = useSnackbar();
|
||||
|
||||
@@ -56,6 +62,32 @@ export default function Header(): JSX.Element {
|
||||
<p className="hidden select-none sm:block">React Compiler Playground</p>
|
||||
</div>
|
||||
<div className="flex items-center text-[15px] gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="show-internals relative inline-block w-[34px] h-5">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={store.showInternals}
|
||||
onChange={() =>
|
||||
startTransition(() => {
|
||||
addTransitionType(TOGGLE_INTERNALS_TRANSITION);
|
||||
dispatchStore({type: 'toggleInternals'});
|
||||
})
|
||||
}
|
||||
className="absolute opacity-0 cursor-pointer h-full w-full m-0"
|
||||
/>
|
||||
<span
|
||||
className={clsx(
|
||||
'absolute inset-0 rounded-full cursor-pointer transition-all duration-250',
|
||||
"before:content-[''] before:absolute before:w-4 before:h-4 before:left-0.5 before:bottom-0.5",
|
||||
'before:bg-white before:rounded-full before:transition-transform before:duration-250',
|
||||
'focus-within:shadow-[0_0_1px_#2196F3]',
|
||||
store.showInternals
|
||||
? 'bg-link before:translate-x-3.5'
|
||||
: 'bg-gray-300',
|
||||
)}></span>
|
||||
</label>
|
||||
<span className="text-secondary">Show Internals</span>
|
||||
</div>
|
||||
<button
|
||||
title="Reset Playground"
|
||||
aria-label="Reset Playground"
|
||||
|
||||
41
compiler/apps/playground/components/Icons/IconChevron.tsx
Normal file
41
compiler/apps/playground/components/Icons/IconChevron.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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 {memo} from 'react';
|
||||
|
||||
export const IconChevron = memo<
|
||||
JSX.IntrinsicElements['svg'] & {
|
||||
/**
|
||||
* The direction the arrow should point.
|
||||
*/
|
||||
displayDirection: 'right' | 'left';
|
||||
}
|
||||
>(function IconChevron({className, displayDirection, ...props}) {
|
||||
const rotationClass =
|
||||
displayDirection === 'left' ? 'rotate-90' : '-rotate-90';
|
||||
const classes = className ? `${rotationClass} ${className}` : rotationClass;
|
||||
|
||||
return (
|
||||
<svg
|
||||
className={classes}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
{...props}>
|
||||
<g fill="none" fillRule="evenodd" transform="translate(-446 -398)">
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="nonzero"
|
||||
d="M95.8838835,240.366117 C95.3957281,239.877961 94.6042719,239.877961 94.1161165,240.366117 C93.6279612,240.854272 93.6279612,241.645728 94.1161165,242.133883 L98.6161165,246.633883 C99.1042719,247.122039 99.8957281,247.122039 100.383883,246.633883 L104.883883,242.133883 C105.372039,241.645728 105.372039,240.854272 104.883883,240.366117 C104.395728,239.877961 103.604272,239.877961 103.116117,240.366117 L99.5,243.982233 L95.8838835,240.366117 Z"
|
||||
transform="translate(356.5 164.5)"
|
||||
/>
|
||||
<polygon points="446 418 466 418 466 398 446 398" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
});
|
||||
@@ -6,10 +6,14 @@
|
||||
*/
|
||||
|
||||
import type {Dispatch, ReactNode} from 'react';
|
||||
import {useEffect, useReducer} from 'react';
|
||||
import {useState, useEffect, useReducer} from 'react';
|
||||
import createContext from '../lib/createContext';
|
||||
import {emptyStore} from '../lib/defaultStore';
|
||||
import {saveStore, type Store} from '../lib/stores';
|
||||
import {emptyStore, defaultStore} from '../lib/defaultStore';
|
||||
import {
|
||||
saveStore,
|
||||
initStoreFromUrlOrLocalStorage,
|
||||
type Store,
|
||||
} from '../lib/stores';
|
||||
|
||||
const StoreContext = createContext<Store>();
|
||||
|
||||
@@ -30,6 +34,20 @@ export const useStoreDispatch = StoreDispatchContext.useContext;
|
||||
*/
|
||||
export function StoreProvider({children}: {children: ReactNode}): JSX.Element {
|
||||
const [store, dispatch] = useReducer(storeReducer, emptyStore);
|
||||
const [isPageReady, setIsPageReady] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
let mountStore: Store;
|
||||
try {
|
||||
mountStore = initStoreFromUrlOrLocalStorage();
|
||||
} catch (e) {
|
||||
console.error('Failed to initialize store from URL or local storage', e);
|
||||
mountStore = defaultStore;
|
||||
}
|
||||
dispatch({type: 'setStore', payload: {store: mountStore}});
|
||||
setIsPageReady(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (store !== emptyStore) {
|
||||
saveStore(store);
|
||||
@@ -39,7 +57,7 @@ export function StoreProvider({children}: {children: ReactNode}): JSX.Element {
|
||||
return (
|
||||
<StoreContext.Provider value={store}>
|
||||
<StoreDispatchContext.Provider value={dispatch}>
|
||||
{children}
|
||||
{isPageReady ? children : null}
|
||||
</StoreDispatchContext.Provider>
|
||||
</StoreContext.Provider>
|
||||
);
|
||||
@@ -53,10 +71,19 @@ type ReducerAction =
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'updateFile';
|
||||
type: 'updateSource';
|
||||
payload: {
|
||||
source: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'updateConfig';
|
||||
payload: {
|
||||
config: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'toggleInternals';
|
||||
};
|
||||
|
||||
function storeReducer(store: Store, action: ReducerAction): Store {
|
||||
@@ -65,13 +92,28 @@ function storeReducer(store: Store, action: ReducerAction): Store {
|
||||
const newStore = action.payload.store;
|
||||
return newStore;
|
||||
}
|
||||
case 'updateFile': {
|
||||
const {source} = action.payload;
|
||||
case 'updateSource': {
|
||||
const source = action.payload.source;
|
||||
const newStore = {
|
||||
...store,
|
||||
source,
|
||||
};
|
||||
return newStore;
|
||||
}
|
||||
case 'updateConfig': {
|
||||
const config = action.payload.config;
|
||||
const newStore = {
|
||||
...store,
|
||||
config,
|
||||
};
|
||||
return newStore;
|
||||
}
|
||||
case 'toggleInternals': {
|
||||
const newStore = {
|
||||
...store,
|
||||
showInternals: !store.showInternals,
|
||||
};
|
||||
return newStore;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,103 +4,78 @@
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
import React, {
|
||||
startTransition,
|
||||
useId,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {TOGGLE_TAB_TRANSITION} from '../lib/transitionTypes';
|
||||
|
||||
import {Resizable} from 're-resizable';
|
||||
import React, {useCallback} from 'react';
|
||||
|
||||
type TabsRecord = Map<string, React.ReactNode>;
|
||||
|
||||
export default function TabbedWindow(props: {
|
||||
defaultTab: string | null;
|
||||
tabs: TabsRecord;
|
||||
tabsOpen: Set<string>;
|
||||
setTabsOpen: (newTab: Set<string>) => void;
|
||||
changedPasses: Set<string>;
|
||||
}): React.ReactElement {
|
||||
if (props.tabs.size === 0) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-center"
|
||||
style={{width: 'calc(100vw - 650px)'}}>
|
||||
No compiler output detected, see errors below
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-row">
|
||||
{Array.from(props.tabs.keys()).map(name => {
|
||||
return (
|
||||
<TabbedWindowItem
|
||||
name={name}
|
||||
key={name}
|
||||
tabs={props.tabs}
|
||||
tabsOpen={props.tabsOpen}
|
||||
setTabsOpen={props.setTabsOpen}
|
||||
hasChanged={props.changedPasses.has(name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TabbedWindowItem({
|
||||
name,
|
||||
export default function TabbedWindow({
|
||||
tabs,
|
||||
tabsOpen,
|
||||
setTabsOpen,
|
||||
hasChanged,
|
||||
activeTab,
|
||||
onTabChange,
|
||||
}: {
|
||||
name: string;
|
||||
tabs: TabsRecord;
|
||||
tabsOpen: Set<string>;
|
||||
setTabsOpen: (newTab: Set<string>) => void;
|
||||
hasChanged: boolean;
|
||||
tabs: Map<string, React.ReactNode>;
|
||||
activeTab: string;
|
||||
onTabChange: (tab: string) => void;
|
||||
}): React.ReactElement {
|
||||
const isShow = tabsOpen.has(name);
|
||||
const id = useId();
|
||||
const transitionName = `tab-highlight-${id}`;
|
||||
|
||||
const toggleTabs = useCallback(() => {
|
||||
const nextState = new Set(tabsOpen);
|
||||
if (nextState.has(name)) {
|
||||
nextState.delete(name);
|
||||
} else {
|
||||
nextState.add(name);
|
||||
}
|
||||
setTabsOpen(nextState);
|
||||
}, [tabsOpen, name, setTabsOpen]);
|
||||
|
||||
// Replace spaces with non-breaking spaces
|
||||
const displayName = name.replace(/ /g, '\u00A0');
|
||||
const handleTabChange = (tab: string): void => {
|
||||
startTransition(() => {
|
||||
addTransitionType(TOGGLE_TAB_TRANSITION);
|
||||
onTabChange(tab);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={name} className="flex flex-row">
|
||||
{isShow ? (
|
||||
<Resizable className="border-r" minWidth={550} enable={{right: true}}>
|
||||
<h2
|
||||
title="Minimize tab"
|
||||
aria-label="Minimize tab"
|
||||
onClick={toggleTabs}
|
||||
className={`p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 ${
|
||||
hasChanged ? 'font-bold' : 'font-light'
|
||||
} text-secondary hover:text-link`}>
|
||||
- {displayName}
|
||||
</h2>
|
||||
{tabs.get(name) ?? <div>No output for {name}</div>}
|
||||
</Resizable>
|
||||
) : (
|
||||
<div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
|
||||
<button
|
||||
title={`Expand compiler tab: ${name}`}
|
||||
aria-label={`Expand compiler tab: ${name}`}
|
||||
style={{transform: 'rotate(90deg) translate(-50%)'}}
|
||||
onClick={toggleTabs}
|
||||
className={`flex-grow-0 w-5 transition-colors duration-150 ease-in ${
|
||||
hasChanged ? 'font-bold' : 'font-light'
|
||||
} text-secondary hover:text-link`}>
|
||||
{displayName}
|
||||
</button>
|
||||
<div className="flex-1 min-w-[550px] sm:min-w-0">
|
||||
<div className="flex flex-col h-full max-w-full">
|
||||
<div className="flex p-2 flex-shrink-0">
|
||||
{Array.from(tabs.keys()).map(tab => {
|
||||
const isActive = activeTab === tab;
|
||||
return (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => handleTabChange(tab)}
|
||||
className={clsx(
|
||||
'transition-transform py-1.5 px-1.5 xs:px-3 sm:px-4 rounded-full text-sm relative',
|
||||
isActive ? 'text-link' : 'hover:bg-primary/5',
|
||||
)}>
|
||||
{isActive && (
|
||||
<ViewTransition
|
||||
name={transitionName}
|
||||
enter={{default: 'none'}}
|
||||
exit={{default: 'none'}}
|
||||
share={{
|
||||
[TOGGLE_TAB_TRANSITION]: 'tab-highlight',
|
||||
default: 'none',
|
||||
}}
|
||||
update={{default: 'none'}}>
|
||||
<div className="absolute inset-0 bg-highlight rounded-full" />
|
||||
</ViewTransition>
|
||||
)}
|
||||
<ViewTransition
|
||||
enter={{default: 'none'}}
|
||||
exit={{default: 'none'}}
|
||||
update={{
|
||||
[TOGGLE_TAB_TRANSITION]: 'tab-text',
|
||||
default: 'none',
|
||||
}}>
|
||||
<span className="relative z-1">{tab}</span>
|
||||
</ViewTransition>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 overflow-hidden w-full h-full">
|
||||
{tabs.get(activeTab)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
308
compiler/apps/playground/lib/compilation.ts
Normal file
308
compiler/apps/playground/lib/compilation.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
/**
|
||||
* 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 {parse as babelParse, ParseResult} from '@babel/parser';
|
||||
import * as HermesParser from 'hermes-parser';
|
||||
import * as t from '@babel/types';
|
||||
import BabelPluginReactCompiler, {
|
||||
CompilerError,
|
||||
CompilerErrorDetail,
|
||||
CompilerDiagnostic,
|
||||
Effect,
|
||||
ErrorCategory,
|
||||
parseConfigPragmaForTests,
|
||||
ValueKind,
|
||||
type Hook,
|
||||
PluginOptions,
|
||||
CompilerPipelineValue,
|
||||
parsePluginOptions,
|
||||
printReactiveFunctionWithOutlined,
|
||||
printFunctionWithOutlined,
|
||||
type LoggerEvent,
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import {transformFromAstSync} from '@babel/core';
|
||||
import type {
|
||||
CompilerOutput,
|
||||
CompilerTransformOutput,
|
||||
PrintedCompilerPipelineValue,
|
||||
} from '../components/Editor/Output';
|
||||
|
||||
function parseInput(
|
||||
input: string,
|
||||
language: 'flow' | 'typescript',
|
||||
): ParseResult<t.File> {
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
if (language === 'flow') {
|
||||
return HermesParser.parse(input, {
|
||||
babel: true,
|
||||
flow: 'all',
|
||||
sourceType: 'module',
|
||||
enableExperimentalComponentSyntax: true,
|
||||
});
|
||||
} else {
|
||||
return babelParse(input, {
|
||||
plugins: ['typescript', 'jsx'],
|
||||
sourceType: 'module',
|
||||
}) as ParseResult<t.File>;
|
||||
}
|
||||
}
|
||||
|
||||
function invokeCompiler(
|
||||
source: string,
|
||||
language: 'flow' | 'typescript',
|
||||
options: PluginOptions,
|
||||
): CompilerTransformOutput {
|
||||
const ast = parseInput(source, language);
|
||||
let result = transformFromAstSync(ast, source, {
|
||||
filename: '_playgroundFile.js',
|
||||
highlightCode: false,
|
||||
retainLines: true,
|
||||
plugins: [[BabelPluginReactCompiler, options]],
|
||||
ast: true,
|
||||
sourceType: 'module',
|
||||
configFile: false,
|
||||
sourceMaps: true,
|
||||
babelrc: false,
|
||||
});
|
||||
if (result?.ast == null || result?.code == null || result?.map == null) {
|
||||
throw new Error('Expected successful compilation');
|
||||
}
|
||||
return {
|
||||
code: result.code,
|
||||
sourceMaps: result.map,
|
||||
language,
|
||||
};
|
||||
}
|
||||
|
||||
const COMMON_HOOKS: Array<[string, Hook]> = [
|
||||
[
|
||||
'useFragment',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'usePaginationFragment',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'useRefetchableFragment',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'useLazyLoadQuery',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'usePreloadedQuery',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
function parseOptions(
|
||||
source: string,
|
||||
mode: 'compiler' | 'linter',
|
||||
configOverrides: string,
|
||||
): PluginOptions {
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
const pragma = source.substring(0, source.indexOf('\n'));
|
||||
|
||||
const parsedPragmaOptions = parseConfigPragmaForTests(pragma, {
|
||||
compilationMode: 'infer',
|
||||
environment:
|
||||
mode === 'linter'
|
||||
? {
|
||||
// enabled in compiler
|
||||
validateRefAccessDuringRender: false,
|
||||
// enabled in linter
|
||||
validateNoSetStateInRender: true,
|
||||
validateNoSetStateInEffects: true,
|
||||
validateNoJSXInTryStatements: true,
|
||||
validateNoImpureFunctionsInRender: true,
|
||||
validateStaticComponents: true,
|
||||
validateNoFreezingKnownMutableFunctions: true,
|
||||
validateNoVoidUseMemo: true,
|
||||
}
|
||||
: {
|
||||
/* use defaults for compiler mode */
|
||||
},
|
||||
});
|
||||
|
||||
// Parse config overrides from config editor
|
||||
let configOverrideOptions: any = {};
|
||||
const configMatch = configOverrides.match(/^\s*import.*?\n\n\((.*)\)/s);
|
||||
if (configOverrides.trim()) {
|
||||
if (configMatch && configMatch[1]) {
|
||||
const configString = configMatch[1].replace(/satisfies.*$/, '').trim();
|
||||
configOverrideOptions = new Function(`return (${configString})`)();
|
||||
} else {
|
||||
throw new Error('Invalid override format');
|
||||
}
|
||||
}
|
||||
|
||||
const opts: PluginOptions = parsePluginOptions({
|
||||
...parsedPragmaOptions,
|
||||
...configOverrideOptions,
|
||||
environment: {
|
||||
...parsedPragmaOptions.environment,
|
||||
...configOverrideOptions.environment,
|
||||
customHooks: new Map([...COMMON_HOOKS]),
|
||||
},
|
||||
});
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
export function compile(
|
||||
source: string,
|
||||
mode: 'compiler' | 'linter',
|
||||
configOverrides: string,
|
||||
): [CompilerOutput, 'flow' | 'typescript', PluginOptions | null] {
|
||||
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
|
||||
const error = new CompilerError();
|
||||
const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
|
||||
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
|
||||
const entry = results.get(result.name);
|
||||
if (Array.isArray(entry)) {
|
||||
entry.push(result);
|
||||
} else {
|
||||
results.set(result.name, [result]);
|
||||
}
|
||||
};
|
||||
let language: 'flow' | 'typescript';
|
||||
if (source.match(/\@flow/)) {
|
||||
language = 'flow';
|
||||
} else {
|
||||
language = 'typescript';
|
||||
}
|
||||
let transformOutput;
|
||||
|
||||
let baseOpts: PluginOptions | null = null;
|
||||
try {
|
||||
baseOpts = parseOptions(source, mode, configOverrides);
|
||||
} catch (err) {
|
||||
error.details.push(
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Config,
|
||||
reason: `Unexpected failure when transforming configs! \n${err}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (baseOpts) {
|
||||
try {
|
||||
const logIR = (result: CompilerPipelineValue): void => {
|
||||
switch (result.kind) {
|
||||
case 'ast': {
|
||||
break;
|
||||
}
|
||||
case 'hir': {
|
||||
upsert({
|
||||
kind: 'hir',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'reactive': {
|
||||
upsert({
|
||||
kind: 'reactive',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printReactiveFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'debug': {
|
||||
upsert({
|
||||
kind: 'debug',
|
||||
fnName: null,
|
||||
name: result.name,
|
||||
value: result.value,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const _: never = result;
|
||||
throw new Error(`Unhandled result ${result}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
// Add logger options to the parsed options
|
||||
const opts = {
|
||||
...baseOpts,
|
||||
logger: {
|
||||
debugLogIRs: logIR,
|
||||
logEvent: (_filename: string | null, event: LoggerEvent): void => {
|
||||
if (event.kind === 'CompileError') {
|
||||
otherErrors.push(event.detail);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
transformOutput = invokeCompiler(source, language, opts);
|
||||
} catch (err) {
|
||||
/**
|
||||
* error might be an invariant violation or other runtime error
|
||||
* (i.e. object shape that is not CompilerError)
|
||||
*/
|
||||
if (err instanceof CompilerError && err.details.length > 0) {
|
||||
error.merge(err);
|
||||
} else {
|
||||
/**
|
||||
* Handle unexpected failures by logging (to get a stack trace)
|
||||
* and reporting
|
||||
*/
|
||||
error.details.push(
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Invariant,
|
||||
reason: `Unexpected failure when transforming input! \n${err}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Only include logger errors if there weren't other errors
|
||||
if (!error.hasErrors() && otherErrors.length !== 0) {
|
||||
otherErrors.forEach(e => error.details.push(e));
|
||||
}
|
||||
if (error.hasErrors() || !transformOutput) {
|
||||
return [{kind: 'err', results, error}, language, baseOpts];
|
||||
}
|
||||
return [
|
||||
{kind: 'ok', results, transformOutput, errors: error.details},
|
||||
language,
|
||||
baseOpts,
|
||||
];
|
||||
}
|
||||
@@ -13,10 +13,21 @@ export default function MyApp() {
|
||||
}
|
||||
`;
|
||||
|
||||
export const defaultConfig = `\
|
||||
import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
|
||||
|
||||
({
|
||||
//compilationMode: "all"
|
||||
} satisfies PluginOptions);`;
|
||||
|
||||
export const defaultStore: Store = {
|
||||
source: index,
|
||||
config: defaultConfig,
|
||||
showInternals: false,
|
||||
};
|
||||
|
||||
export const emptyStore: Store = {
|
||||
source: '',
|
||||
config: '',
|
||||
showInternals: false,
|
||||
};
|
||||
|
||||
@@ -10,18 +10,20 @@ import {
|
||||
compressToEncodedURIComponent,
|
||||
decompressFromEncodedURIComponent,
|
||||
} from 'lz-string';
|
||||
import {defaultStore} from '../defaultStore';
|
||||
import {defaultStore, defaultConfig} from '../defaultStore';
|
||||
|
||||
/**
|
||||
* Global Store for Playground
|
||||
*/
|
||||
export interface Store {
|
||||
source: string;
|
||||
config: string;
|
||||
showInternals: boolean;
|
||||
}
|
||||
export function encodeStore(store: Store): string {
|
||||
return compressToEncodedURIComponent(JSON.stringify(store));
|
||||
}
|
||||
export function decodeStore(hash: string): Store {
|
||||
export function decodeStore(hash: string): any {
|
||||
return JSON.parse(decompressFromEncodedURIComponent(hash));
|
||||
}
|
||||
|
||||
@@ -62,8 +64,14 @@ export function initStoreFromUrlOrLocalStorage(): Store {
|
||||
*/
|
||||
if (!encodedSource) return defaultStore;
|
||||
|
||||
const raw = decodeStore(encodedSource);
|
||||
const raw: any = decodeStore(encodedSource);
|
||||
|
||||
invariant(isValidStore(raw), 'Invalid Store');
|
||||
return raw;
|
||||
|
||||
// Make sure all properties are populated
|
||||
return {
|
||||
source: raw.source,
|
||||
config: 'config' in raw && raw['config'] ? raw.config : defaultConfig,
|
||||
showInternals: 'showInternals' in raw ? raw.showInternals : false,
|
||||
};
|
||||
}
|
||||
|
||||
11
compiler/apps/playground/lib/transitionTypes.ts
Normal file
11
compiler/apps/playground/lib/transitionTypes.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const CONFIG_PANEL_TRANSITION = 'config-panel';
|
||||
export const TOGGLE_TAB_TRANSITION = 'toggle-tab';
|
||||
export const TOGGLE_INTERNALS_TRANSITION = 'toggle-internals';
|
||||
export const EXPAND_ACCORDION_TRANSITION = 'open-accordion';
|
||||
1
compiler/apps/playground/next-env.d.ts
vendored
1
compiler/apps/playground/next-env.d.ts
vendored
@@ -1,5 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -11,6 +11,7 @@ const path = require('path');
|
||||
const nextConfig = {
|
||||
experimental: {
|
||||
reactCompiler: true,
|
||||
viewTransition: true,
|
||||
},
|
||||
reactStrictMode: true,
|
||||
webpack: (config, options) => {
|
||||
|
||||
@@ -32,28 +32,33 @@
|
||||
"hermes-eslint": "^0.25.0",
|
||||
"hermes-parser": "^0.25.0",
|
||||
"invariant": "^2.2.4",
|
||||
"lru-cache": "^11.2.2",
|
||||
"lz-string": "^1.5.0",
|
||||
"monaco-editor": "^0.52.0",
|
||||
"next": "^15.2.0-canary.64",
|
||||
"next": "15.6.0-canary.7",
|
||||
"notistack": "^3.0.0-alpha.7",
|
||||
"prettier": "^3.3.3",
|
||||
"pretty-format": "^29.3.1",
|
||||
"re-resizable": "^6.9.16",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.11.9",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@types/react": "19.1.13",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"clsx": "^1.2.1",
|
||||
"concurrently": "^7.4.0",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-next": "^15.0.1",
|
||||
"eslint-config-next": "15.5.2",
|
||||
"monaco-editor-webpack-plugin": "^7.1.0",
|
||||
"postcss": "^8.4.31",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"wait-on": "^7.2.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,12 +55,16 @@ export default defineConfig({
|
||||
// contextOptions: {
|
||||
// ignoreHTTPSErrors: true,
|
||||
// },
|
||||
viewport: {width: 1920, height: 1080},
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {...devices['Desktop Chrome']},
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
viewport: {width: 1920, height: 1080},
|
||||
},
|
||||
},
|
||||
// {
|
||||
// name: 'Desktop Firefox',
|
||||
|
||||
@@ -69,3 +69,66 @@
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
}
|
||||
|
||||
::view-transition-old(.slide-in) {
|
||||
animation-name: slideOutLeft;
|
||||
}
|
||||
::view-transition-new(.slide-in) {
|
||||
animation-name: slideInLeft;
|
||||
}
|
||||
::view-transition-group(.slide-in) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@keyframes slideOutLeft {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
}
|
||||
to {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
||||
@keyframes slideInLeft {
|
||||
from {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
::view-transition-old(.container),
|
||||
::view-transition-new(.container) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
::view-transition-old(.accordion-container),
|
||||
::view-transition-new(.accordion-container) {
|
||||
height: 100%;
|
||||
object-fit: none;
|
||||
object-position: left;
|
||||
}
|
||||
|
||||
::view-transition-old(.tab-highlight),
|
||||
::view-transition-new(.tab-highlight) {
|
||||
height: 100%;
|
||||
}
|
||||
::view-transition-group(.tab-text) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
::view-transition-old(.expand-accordion),
|
||||
::view-transition-new(.expand-accordion) {
|
||||
width: auto;
|
||||
}
|
||||
::view-transition-group(.expand-accordion) {
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
/**
|
||||
* For some reason, the original Monaco editor is still visible to the
|
||||
* left of the DiffEditor. This is a workaround for better visual clarity.
|
||||
*/
|
||||
.monaco-diff-editor .editor.original{
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"types": [
|
||||
"react/experimental"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@@ -16,7 +19,7 @@
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,8 @@
|
||||
"test": "yarn workspaces run test",
|
||||
"snap": "yarn workspace babel-plugin-react-compiler run snap",
|
||||
"snap:build": "yarn workspace snap run build",
|
||||
"npm:publish": "node scripts/release/publish"
|
||||
"npm:publish": "node scripts/release/publish",
|
||||
"eslint-docs": "yarn workspace babel-plugin-react-compiler build && node scripts/build-eslint-docs.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"fs-extra": "^4.0.2",
|
||||
|
||||
@@ -52,8 +52,8 @@
|
||||
"react-dom": "0.0.0-experimental-4beb1fd8-20241118",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"zod": "^3.22.4",
|
||||
"zod-validation-error": "^2.1.0"
|
||||
"zod": "^3.25.0 || ^4.0.0",
|
||||
"zod-validation-error": "^3.5.0 || ^4.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"./**/@babel/parser": "7.7.4",
|
||||
|
||||
@@ -17,7 +17,7 @@ export function runBabelPluginReactCompiler(
|
||||
text: string,
|
||||
file: string,
|
||||
language: 'flow' | 'typescript',
|
||||
options: Partial<PluginOptions> | null,
|
||||
options: PluginOptions | null,
|
||||
includeAst: boolean = false,
|
||||
): BabelCore.BabelFileResult {
|
||||
const ast = BabelParser.parse(text, {
|
||||
|
||||
@@ -7,49 +7,37 @@
|
||||
|
||||
import * as t from '@babel/types';
|
||||
import {codeFrameColumns} from '@babel/code-frame';
|
||||
import type {SourceLocation} from './HIR';
|
||||
import {type SourceLocation} from './HIR';
|
||||
import {Err, Ok, Result} from './Utils/Result';
|
||||
import {assertExhaustive} from './Utils/utils';
|
||||
import invariant from 'invariant';
|
||||
|
||||
export enum ErrorSeverity {
|
||||
/**
|
||||
* Invalid JS syntax, or valid syntax that is semantically invalid which may indicate some
|
||||
* misunderstanding on the user’s part.
|
||||
* An actionable error that the developer can fix. For example, product code errors should be
|
||||
* reported as such.
|
||||
*/
|
||||
InvalidJS = 'InvalidJS',
|
||||
Error = 'Error',
|
||||
/**
|
||||
* JS syntax that is not supported and which we do not plan to support. Developers should
|
||||
* rewrite to use supported forms.
|
||||
* An error that the developer may not necessarily be able to fix. For example, syntax not
|
||||
* supported by the compiler does not indicate any fault in the product code.
|
||||
*/
|
||||
UnsupportedJS = 'UnsupportedJS',
|
||||
Warning = 'Warning',
|
||||
/**
|
||||
* Code that breaks the rules of React.
|
||||
* Not an error. These will not be surfaced in ESLint, but may be surfaced in other ways
|
||||
* (eg Forgive) where informational hints can be shown.
|
||||
*/
|
||||
InvalidReact = 'InvalidReact',
|
||||
Hint = 'Hint',
|
||||
/**
|
||||
* Incorrect configuration of the compiler.
|
||||
* These errors will not be reported anywhere. Useful for work in progress validations.
|
||||
*/
|
||||
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',
|
||||
Off = 'Off',
|
||||
}
|
||||
|
||||
export type CompilerDiagnosticOptions = {
|
||||
severity: ErrorSeverity;
|
||||
category: string;
|
||||
description: string;
|
||||
category: ErrorCategory;
|
||||
reason: string;
|
||||
description: string | null;
|
||||
details: Array<CompilerDiagnosticDetail>;
|
||||
suggestions?: Array<CompilerSuggestion> | null | undefined;
|
||||
};
|
||||
@@ -58,11 +46,15 @@ export type CompilerDiagnosticDetail =
|
||||
/**
|
||||
* A/the source of the error
|
||||
*/
|
||||
{
|
||||
kind: 'error';
|
||||
loc: SourceLocation | null;
|
||||
message: string;
|
||||
};
|
||||
| {
|
||||
kind: 'error';
|
||||
loc: SourceLocation | null;
|
||||
message: string | null;
|
||||
}
|
||||
| {
|
||||
kind: 'hint';
|
||||
message: string;
|
||||
};
|
||||
|
||||
export enum CompilerSuggestionOperation {
|
||||
InsertBefore,
|
||||
@@ -86,10 +78,13 @@ export type CompilerSuggestion =
|
||||
description: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated use {@link CompilerDiagnosticOptions} instead
|
||||
*/
|
||||
export type CompilerErrorDetailOptions = {
|
||||
category: ErrorCategory;
|
||||
reason: string;
|
||||
description?: string | null | undefined;
|
||||
severity: ErrorSeverity;
|
||||
loc: SourceLocation | null;
|
||||
suggestions?: Array<CompilerSuggestion> | null | undefined;
|
||||
};
|
||||
@@ -115,34 +110,41 @@ export class CompilerDiagnostic {
|
||||
return new CompilerDiagnostic({...options, details: []});
|
||||
}
|
||||
|
||||
get category(): CompilerDiagnosticOptions['category'] {
|
||||
return this.options.category;
|
||||
get reason(): CompilerDiagnosticOptions['reason'] {
|
||||
return this.options.reason;
|
||||
}
|
||||
get description(): CompilerDiagnosticOptions['description'] {
|
||||
return this.options.description;
|
||||
}
|
||||
get severity(): CompilerDiagnosticOptions['severity'] {
|
||||
return this.options.severity;
|
||||
get severity(): ErrorSeverity {
|
||||
return getRuleForCategory(this.category).severity;
|
||||
}
|
||||
get suggestions(): CompilerDiagnosticOptions['suggestions'] {
|
||||
return this.options.suggestions;
|
||||
}
|
||||
get category(): ErrorCategory {
|
||||
return this.options.category;
|
||||
}
|
||||
|
||||
withDetail(detail: CompilerDiagnosticDetail): CompilerDiagnostic {
|
||||
this.options.details.push(detail);
|
||||
withDetails(...details: Array<CompilerDiagnosticDetail>): CompilerDiagnostic {
|
||||
this.options.details.push(...details);
|
||||
return this;
|
||||
}
|
||||
|
||||
primaryLocation(): SourceLocation | null {
|
||||
return this.options.details.filter(d => d.kind === 'error')[0]?.loc ?? null;
|
||||
const firstErrorDetail = this.options.details.filter(
|
||||
d => d.kind === 'error',
|
||||
)[0];
|
||||
return firstErrorDetail != null && firstErrorDetail.kind === 'error'
|
||||
? firstErrorDetail.loc
|
||||
: null;
|
||||
}
|
||||
|
||||
printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
|
||||
const buffer = [
|
||||
printErrorSummary(this.severity, this.category),
|
||||
'\n\n',
|
||||
this.description,
|
||||
];
|
||||
const buffer = [printErrorSummary(this.category, this.reason)];
|
||||
if (this.description != null) {
|
||||
buffer.push('\n\n', `${this.description}.`);
|
||||
}
|
||||
for (const detail of this.options.details) {
|
||||
switch (detail.kind) {
|
||||
case 'error': {
|
||||
@@ -152,9 +154,9 @@ export class CompilerDiagnostic {
|
||||
}
|
||||
let codeFrame: string;
|
||||
try {
|
||||
codeFrame = printCodeFrame(source, loc, detail.message);
|
||||
codeFrame = printCodeFrame(source, loc, detail.message ?? '');
|
||||
} catch (e) {
|
||||
codeFrame = detail.message;
|
||||
codeFrame = detail.message ?? '';
|
||||
}
|
||||
buffer.push('\n\n');
|
||||
if (loc.filename != null) {
|
||||
@@ -167,9 +169,14 @@ export class CompilerDiagnostic {
|
||||
buffer.push(codeFrame);
|
||||
break;
|
||||
}
|
||||
case 'hint': {
|
||||
buffer.push('\n\n');
|
||||
buffer.push(detail.message);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
detail.kind,
|
||||
detail,
|
||||
`Unexpected detail kind ${(detail as any).kind}`,
|
||||
);
|
||||
}
|
||||
@@ -179,7 +186,7 @@ export class CompilerDiagnostic {
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
const buffer = [printErrorSummary(this.severity, this.category)];
|
||||
const buffer = [printErrorSummary(this.category, this.reason)];
|
||||
if (this.description != null) {
|
||||
buffer.push(`. ${this.description}.`);
|
||||
}
|
||||
@@ -191,9 +198,11 @@ export class CompilerDiagnostic {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* Each bailout or invariant in HIR lowering creates an {@link CompilerErrorDetail}, which is then
|
||||
* aggregated into a single {@link CompilerError} later.
|
||||
*
|
||||
* @deprecated use {@link CompilerDiagnostic} instead
|
||||
*/
|
||||
export class CompilerErrorDetail {
|
||||
options: CompilerErrorDetailOptions;
|
||||
@@ -208,8 +217,8 @@ export class CompilerErrorDetail {
|
||||
get description(): CompilerErrorDetailOptions['description'] {
|
||||
return this.options.description;
|
||||
}
|
||||
get severity(): CompilerErrorDetailOptions['severity'] {
|
||||
return this.options.severity;
|
||||
get severity(): ErrorSeverity {
|
||||
return getRuleForCategory(this.category).severity;
|
||||
}
|
||||
get loc(): CompilerErrorDetailOptions['loc'] {
|
||||
return this.options.loc;
|
||||
@@ -217,13 +226,16 @@ export class CompilerErrorDetail {
|
||||
get suggestions(): CompilerErrorDetailOptions['suggestions'] {
|
||||
return this.options.suggestions;
|
||||
}
|
||||
get category(): ErrorCategory {
|
||||
return this.options.category;
|
||||
}
|
||||
|
||||
primaryLocation(): SourceLocation | null {
|
||||
return this.loc;
|
||||
}
|
||||
|
||||
printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
|
||||
const buffer = [printErrorSummary(this.severity, this.reason)];
|
||||
const buffer = [printErrorSummary(this.category, this.reason)];
|
||||
if (this.description != null) {
|
||||
buffer.push(`\n\n${this.description}.`);
|
||||
}
|
||||
@@ -248,7 +260,7 @@ export class CompilerErrorDetail {
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
const buffer = [printErrorSummary(this.severity, this.reason)];
|
||||
const buffer = [printErrorSummary(this.category, this.reason)];
|
||||
if (this.description != null) {
|
||||
buffer.push(`. ${this.description}.`);
|
||||
}
|
||||
@@ -260,21 +272,28 @@ export class CompilerErrorDetail {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An aggregate of {@link CompilerDiagnostic}. This allows us to aggregate all issues found by the
|
||||
* compiler into a single error before we throw. Where possible, prefer to push diagnostics into
|
||||
* the error aggregate instead of throwing immediately.
|
||||
*/
|
||||
export class CompilerError extends Error {
|
||||
details: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
|
||||
disabledDetails: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
|
||||
printedMessage: string | null = null;
|
||||
|
||||
static invariant(
|
||||
condition: unknown,
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity'>,
|
||||
options: Omit<CompilerDiagnosticOptions, 'category'>,
|
||||
): asserts condition {
|
||||
if (!condition) {
|
||||
const errors = new CompilerError();
|
||||
errors.pushErrorDetail(
|
||||
new CompilerErrorDetail({
|
||||
...options,
|
||||
severity: ErrorSeverity.Invariant,
|
||||
}),
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
reason: options.reason,
|
||||
description: options.description,
|
||||
category: ErrorCategory.Invariant,
|
||||
}).withDetails(...options.details),
|
||||
);
|
||||
throw errors;
|
||||
}
|
||||
@@ -287,49 +306,45 @@ export class CompilerError extends Error {
|
||||
}
|
||||
|
||||
static throwTodo(
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity'>,
|
||||
options: Omit<CompilerErrorDetailOptions, 'category'>,
|
||||
): never {
|
||||
const errors = new CompilerError();
|
||||
errors.pushErrorDetail(
|
||||
new CompilerErrorDetail({...options, severity: ErrorSeverity.Todo}),
|
||||
new CompilerErrorDetail({
|
||||
...options,
|
||||
category: ErrorCategory.Todo,
|
||||
}),
|
||||
);
|
||||
throw errors;
|
||||
}
|
||||
|
||||
static throwInvalidJS(
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity'>,
|
||||
options: Omit<CompilerErrorDetailOptions, 'category'>,
|
||||
): never {
|
||||
const errors = new CompilerError();
|
||||
errors.pushErrorDetail(
|
||||
new CompilerErrorDetail({
|
||||
...options,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
}),
|
||||
);
|
||||
throw errors;
|
||||
}
|
||||
|
||||
static throwInvalidReact(
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity'>,
|
||||
): never {
|
||||
static throwInvalidReact(options: CompilerErrorDetailOptions): never {
|
||||
const errors = new CompilerError();
|
||||
errors.pushErrorDetail(
|
||||
new CompilerErrorDetail({
|
||||
...options,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
}),
|
||||
);
|
||||
errors.pushErrorDetail(new CompilerErrorDetail(options));
|
||||
throw errors;
|
||||
}
|
||||
|
||||
static throwInvalidConfig(
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity'>,
|
||||
options: Omit<CompilerErrorDetailOptions, 'category'>,
|
||||
): never {
|
||||
const errors = new CompilerError();
|
||||
errors.pushErrorDetail(
|
||||
new CompilerErrorDetail({
|
||||
...options,
|
||||
severity: ErrorSeverity.InvalidConfig,
|
||||
category: ErrorCategory.Config,
|
||||
}),
|
||||
);
|
||||
throw errors;
|
||||
@@ -345,6 +360,7 @@ export class CompilerError extends Error {
|
||||
super(...args);
|
||||
this.name = 'ReactCompilerError';
|
||||
this.details = [];
|
||||
this.disabledDetails = [];
|
||||
}
|
||||
|
||||
override get message(): string {
|
||||
@@ -385,60 +401,93 @@ export class CompilerError extends Error {
|
||||
|
||||
merge(other: CompilerError): void {
|
||||
this.details.push(...other.details);
|
||||
this.disabledDetails.push(...other.disabledDetails);
|
||||
}
|
||||
|
||||
pushDiagnostic(diagnostic: CompilerDiagnostic): void {
|
||||
this.details.push(diagnostic);
|
||||
if (diagnostic.severity === ErrorSeverity.Off) {
|
||||
this.disabledDetails.push(diagnostic);
|
||||
} else {
|
||||
this.details.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use {@link pushDiagnostic} instead
|
||||
*/
|
||||
push(options: CompilerErrorDetailOptions): CompilerErrorDetail {
|
||||
const detail = new CompilerErrorDetail({
|
||||
category: options.category,
|
||||
reason: options.reason,
|
||||
description: options.description ?? null,
|
||||
severity: options.severity,
|
||||
suggestions: options.suggestions,
|
||||
loc: typeof options.loc === 'symbol' ? null : options.loc,
|
||||
});
|
||||
return this.pushErrorDetail(detail);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use {@link pushDiagnostic} instead
|
||||
*/
|
||||
pushErrorDetail(detail: CompilerErrorDetail): CompilerErrorDetail {
|
||||
this.details.push(detail);
|
||||
if (detail.severity === ErrorSeverity.Off) {
|
||||
this.disabledDetails.push(detail);
|
||||
} else {
|
||||
this.details.push(detail);
|
||||
}
|
||||
return detail;
|
||||
}
|
||||
|
||||
hasErrors(): boolean {
|
||||
hasAnyErrors(): boolean {
|
||||
return this.details.length > 0;
|
||||
}
|
||||
|
||||
asResult(): Result<void, CompilerError> {
|
||||
return this.hasErrors() ? Err(this) : Ok(undefined);
|
||||
return this.hasAnyErrors() ? Err(this) : Ok(undefined);
|
||||
}
|
||||
|
||||
/*
|
||||
* An error is critical if it means the compiler has entered into a broken state and cannot
|
||||
* continue safely. Other expected errors such as Todos mean that we can skip over that component
|
||||
* but otherwise continue compiling the rest of the app.
|
||||
/**
|
||||
* Returns true if any of the error details are of severity Error.
|
||||
*/
|
||||
isCritical(): boolean {
|
||||
return this.details.some(detail => {
|
||||
switch (detail.severity) {
|
||||
case ErrorSeverity.Invariant:
|
||||
case ErrorSeverity.InvalidJS:
|
||||
case ErrorSeverity.InvalidReact:
|
||||
case ErrorSeverity.InvalidConfig:
|
||||
case ErrorSeverity.UnsupportedJS: {
|
||||
return true;
|
||||
}
|
||||
case ErrorSeverity.CannotPreserveMemoization:
|
||||
case ErrorSeverity.Todo: {
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(detail.severity, 'Unhandled error severity');
|
||||
}
|
||||
hasErrors(): boolean {
|
||||
for (const detail of this.details) {
|
||||
if (detail.severity === ErrorSeverity.Error) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there are no Errors and there is at least one Warning.
|
||||
*/
|
||||
hasWarning(): boolean {
|
||||
let res = false;
|
||||
for (const detail of this.details) {
|
||||
if (detail.severity === ErrorSeverity.Error) {
|
||||
return false;
|
||||
}
|
||||
if (detail.severity === ErrorSeverity.Warning) {
|
||||
res = true;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
hasHints(): boolean {
|
||||
let res = false;
|
||||
for (const detail of this.details) {
|
||||
if (detail.severity === ErrorSeverity.Error) {
|
||||
return false;
|
||||
}
|
||||
if (detail.severity === ErrorSeverity.Warning) {
|
||||
return false;
|
||||
}
|
||||
if (detail.severity === ErrorSeverity.Hint) {
|
||||
res = true;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,31 +514,500 @@ function printCodeFrame(
|
||||
);
|
||||
}
|
||||
|
||||
function printErrorSummary(severity: ErrorSeverity, message: string): string {
|
||||
let severityCategory: string;
|
||||
switch (severity) {
|
||||
case ErrorSeverity.InvalidConfig:
|
||||
case ErrorSeverity.InvalidJS:
|
||||
case ErrorSeverity.InvalidReact:
|
||||
case ErrorSeverity.UnsupportedJS: {
|
||||
severityCategory = 'Error';
|
||||
function printErrorSummary(category: ErrorCategory, message: string): string {
|
||||
let heading: string;
|
||||
switch (category) {
|
||||
case ErrorCategory.AutomaticEffectDependencies:
|
||||
case ErrorCategory.CapitalizedCalls:
|
||||
case ErrorCategory.Config:
|
||||
case ErrorCategory.EffectDerivationsOfState:
|
||||
case ErrorCategory.EffectSetState:
|
||||
case ErrorCategory.ErrorBoundaries:
|
||||
case ErrorCategory.Factories:
|
||||
case ErrorCategory.FBT:
|
||||
case ErrorCategory.Fire:
|
||||
case ErrorCategory.Gating:
|
||||
case ErrorCategory.Globals:
|
||||
case ErrorCategory.Hooks:
|
||||
case ErrorCategory.Immutability:
|
||||
case ErrorCategory.Purity:
|
||||
case ErrorCategory.Refs:
|
||||
case ErrorCategory.RenderSetState:
|
||||
case ErrorCategory.StaticComponents:
|
||||
case ErrorCategory.Suppression:
|
||||
case ErrorCategory.Syntax:
|
||||
case ErrorCategory.UseMemo:
|
||||
case ErrorCategory.VoidUseMemo: {
|
||||
heading = 'Error';
|
||||
break;
|
||||
}
|
||||
case ErrorSeverity.CannotPreserveMemoization: {
|
||||
severityCategory = 'Memoization';
|
||||
case ErrorCategory.EffectDependencies:
|
||||
case ErrorCategory.IncompatibleLibrary:
|
||||
case ErrorCategory.PreserveManualMemo:
|
||||
case ErrorCategory.UnsupportedSyntax: {
|
||||
heading = 'Compilation Skipped';
|
||||
break;
|
||||
}
|
||||
case ErrorSeverity.Invariant: {
|
||||
severityCategory = 'Invariant';
|
||||
case ErrorCategory.Invariant: {
|
||||
heading = 'Invariant';
|
||||
break;
|
||||
}
|
||||
case ErrorSeverity.Todo: {
|
||||
severityCategory = 'Todo';
|
||||
case ErrorCategory.Todo: {
|
||||
heading = 'Todo';
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(severity, `Unexpected severity '${severity}'`);
|
||||
assertExhaustive(category, `Unhandled category '${category}'`);
|
||||
}
|
||||
}
|
||||
return `${severityCategory}: ${message}`;
|
||||
return `${heading}: ${message}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* See getRuleForCategory() for how these map to ESLint rules
|
||||
*/
|
||||
export enum ErrorCategory {
|
||||
/**
|
||||
* Checking for valid hooks usage (non conditional, non-first class, non reactive, etc)
|
||||
*/
|
||||
Hooks = 'Hooks',
|
||||
/**
|
||||
* Checking for no capitalized calls (not definitively an error, hence separating)
|
||||
*/
|
||||
CapitalizedCalls = 'CapitalizedCalls',
|
||||
/**
|
||||
* Checking for static components
|
||||
*/
|
||||
StaticComponents = 'StaticComponents',
|
||||
/**
|
||||
* Checking for valid usage of manual memoization
|
||||
*/
|
||||
UseMemo = 'UseMemo',
|
||||
/**
|
||||
* Checking that useMemos always return a value
|
||||
*/
|
||||
VoidUseMemo = 'VoidUseMemo',
|
||||
/**
|
||||
* Checking for higher order functions acting as factories for components/hooks
|
||||
*/
|
||||
Factories = 'Factories',
|
||||
/**
|
||||
* Checks that manual memoization is preserved
|
||||
*/
|
||||
PreserveManualMemo = 'PreserveManualMemo',
|
||||
/**
|
||||
* Checks for known incompatible libraries
|
||||
*/
|
||||
IncompatibleLibrary = 'IncompatibleLibrary',
|
||||
/**
|
||||
* Checking for no mutations of props, hook arguments, hook return values
|
||||
*/
|
||||
Immutability = 'Immutability',
|
||||
/**
|
||||
* Checking for assignments to globals
|
||||
*/
|
||||
Globals = 'Globals',
|
||||
/**
|
||||
* Checking for valid usage of refs, ie no access during render
|
||||
*/
|
||||
Refs = 'Refs',
|
||||
/**
|
||||
* Checks for memoized effect deps
|
||||
*/
|
||||
EffectDependencies = 'EffectDependencies',
|
||||
/**
|
||||
* Checks for no setState in effect bodies
|
||||
*/
|
||||
EffectSetState = 'EffectSetState',
|
||||
EffectDerivationsOfState = 'EffectDerivationsOfState',
|
||||
/**
|
||||
* Validates against try/catch in place of error boundaries
|
||||
*/
|
||||
ErrorBoundaries = 'ErrorBoundaries',
|
||||
/**
|
||||
* Checking for pure functions
|
||||
*/
|
||||
Purity = 'Purity',
|
||||
/**
|
||||
* Validates against setState in render
|
||||
*/
|
||||
RenderSetState = 'RenderSetState',
|
||||
/**
|
||||
* Internal invariants
|
||||
*/
|
||||
Invariant = 'Invariant',
|
||||
/**
|
||||
* Todos
|
||||
*/
|
||||
Todo = 'Todo',
|
||||
/**
|
||||
* Syntax errors
|
||||
*/
|
||||
Syntax = 'Syntax',
|
||||
/**
|
||||
* Checks for use of unsupported syntax
|
||||
*/
|
||||
UnsupportedSyntax = 'UnsupportedSyntax',
|
||||
/**
|
||||
* Config errors
|
||||
*/
|
||||
Config = 'Config',
|
||||
/**
|
||||
* Gating error
|
||||
*/
|
||||
Gating = 'Gating',
|
||||
/**
|
||||
* Suppressions
|
||||
*/
|
||||
Suppression = 'Suppression',
|
||||
/**
|
||||
* Issues with auto deps
|
||||
*/
|
||||
AutomaticEffectDependencies = 'AutomaticEffectDependencies',
|
||||
/**
|
||||
* Issues with `fire`
|
||||
*/
|
||||
Fire = 'Fire',
|
||||
/**
|
||||
* fbt-specific issues
|
||||
*/
|
||||
FBT = 'FBT',
|
||||
}
|
||||
|
||||
export enum LintRulePreset {
|
||||
/**
|
||||
* Rules that are stable and included in the `recommended` preset.
|
||||
*/
|
||||
Recommended = 'recommended',
|
||||
/**
|
||||
* Rules that are more experimental and only included in the `recommended-latest` preset.
|
||||
*/
|
||||
RecommendedLatest = 'recommended-latest',
|
||||
/**
|
||||
* Rules that are disabled.
|
||||
*/
|
||||
Off = 'off',
|
||||
}
|
||||
|
||||
export type LintRule = {
|
||||
// Stores the category the rule corresponds to, used to filter errors when reporting
|
||||
category: ErrorCategory;
|
||||
|
||||
// Stores the severity of the error, which is used to map to lint levels such as error/warning.
|
||||
severity: ErrorSeverity;
|
||||
|
||||
/**
|
||||
* The "name" of the rule as it will be used by developers to enable/disable, eg
|
||||
* "eslint-disable-nest line <name>"
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* A description of the rule that appears somewhere in ESLint. This does not affect
|
||||
* how error messages are formatted
|
||||
*/
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* Configures the preset in which the rule is enabled. If 'off', the rule will not be included in
|
||||
* any preset.
|
||||
*
|
||||
* NOTE: not all validations are enabled by default! Setting this flag only affects
|
||||
* whether a given rule is part of the recommended set. The corresponding validation
|
||||
* also should be enabled by default if you want the error to actually show up!
|
||||
*/
|
||||
preset: LintRulePreset;
|
||||
};
|
||||
|
||||
const RULE_NAME_PATTERN = /^[a-z]+(-[a-z]+)*$/;
|
||||
|
||||
export function getRuleForCategory(category: ErrorCategory): LintRule {
|
||||
const rule = getRuleForCategoryImpl(category);
|
||||
invariant(
|
||||
RULE_NAME_PATTERN.test(rule.name),
|
||||
`Invalid rule name, got '${rule.name}' but rules must match ${RULE_NAME_PATTERN.toString()}`,
|
||||
);
|
||||
return rule;
|
||||
}
|
||||
|
||||
function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
switch (category) {
|
||||
case ErrorCategory.AutomaticEffectDependencies: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'automatic-effect-dependencies',
|
||||
description:
|
||||
'Verifies that automatic effect dependencies are compiled if opted-in',
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.CapitalizedCalls: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'capitalized-calls',
|
||||
description:
|
||||
'Validates against calling capitalized functions/methods instead of using JSX',
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Config: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'config',
|
||||
description: 'Validates the compiler configuration options',
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectDependencies: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'memoized-effect-dependencies',
|
||||
description: 'Validates that effect dependencies are memoized',
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectDerivationsOfState: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'no-deriving-state-in-effects',
|
||||
description:
|
||||
'Validates against deriving values from state in an effect',
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectSetState: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'set-state-in-effect',
|
||||
description:
|
||||
'Validates against calling setState synchronously in an effect, which can lead to re-renders that degrade performance',
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.ErrorBoundaries: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'error-boundaries',
|
||||
description:
|
||||
'Validates usage of error boundaries instead of try/catch for errors in child components',
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Factories: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'component-hook-factories',
|
||||
description:
|
||||
'Validates against higher order functions defining nested components or hooks. ' +
|
||||
'Components and hooks should be defined at the module level',
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.FBT: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'fbt',
|
||||
description: 'Validates usage of fbt',
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Fire: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'fire',
|
||||
description: 'Validates usage of `fire`',
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Gating: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'gating',
|
||||
description:
|
||||
'Validates configuration of [gating mode](https://react.dev/reference/react-compiler/gating)',
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Globals: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'globals',
|
||||
description:
|
||||
'Validates against assignment/mutation of globals during render, part of ensuring that ' +
|
||||
'[side effects must render outside of render](https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)',
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Hooks: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'hooks',
|
||||
description: 'Validates the rules of hooks',
|
||||
/**
|
||||
* TODO: the "Hooks" rule largely reimplements the "rules-of-hooks" non-compiler rule.
|
||||
* We need to dedeupe these (moving the remaining bits into the compiler) and then enable
|
||||
* this rule.
|
||||
*/
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Immutability: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'immutability',
|
||||
description:
|
||||
'Validates against mutating props, state, and other values that [are immutable](https://react.dev/reference/rules/components-and-hooks-must-be-pure#props-and-state-are-immutable)',
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Invariant: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'invariant',
|
||||
description: 'Internal invariants',
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.PreserveManualMemo: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'preserve-manual-memoization',
|
||||
description:
|
||||
'Validates that existing manual memoized is preserved by the compiler. ' +
|
||||
'React Compiler will only compile components and hooks if its inference ' +
|
||||
'[matches or exceeds the existing manual memoization](https://react.dev/learn/react-compiler/introduction#what-should-i-do-about-usememo-usecallback-and-reactmemo)',
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Purity: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'purity',
|
||||
description:
|
||||
'Validates that [components/hooks are pure](https://react.dev/reference/rules/components-and-hooks-must-be-pure) by checking that they do not call known-impure functions',
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Refs: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'refs',
|
||||
description:
|
||||
'Validates correct usage of refs, not reading/writing during render. See the "pitfalls" section in [`useRef()` usage](https://react.dev/reference/react/useRef#usage)',
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.RenderSetState: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'set-state-in-render',
|
||||
description:
|
||||
'Validates against setting state during render, which can trigger additional renders and potential infinite render loops',
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.StaticComponents: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'static-components',
|
||||
description:
|
||||
'Validates that components are static, not recreated every render. Components that are recreated dynamically can reset state and trigger excessive re-rendering',
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Suppression: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'rule-suppression',
|
||||
description: 'Validates against suppression of other rules',
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Syntax: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'syntax',
|
||||
description: 'Validates against invalid syntax',
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Todo: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Hint,
|
||||
name: 'todo',
|
||||
description: 'Unimplemented features',
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.UnsupportedSyntax: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Warning,
|
||||
name: 'unsupported-syntax',
|
||||
description:
|
||||
'Validates against syntax that we do not plan to support in React Compiler',
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.UseMemo: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'use-memo',
|
||||
description:
|
||||
'Validates usage of the useMemo() hook against common mistakes. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.VoidUseMemo: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'void-use-memo',
|
||||
description:
|
||||
'Validates that useMemos always return a value and that the result of the useMemo is used by the component/hook. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
|
||||
preset: LintRulePreset.RecommendedLatest,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.IncompatibleLibrary: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Warning,
|
||||
name: 'incompatible-library',
|
||||
description:
|
||||
'Validates against usage of libraries which are incompatible with memoization (manual or automatic)',
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(category, `Unsupported category ${category}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const LintRules: Array<LintRule> = Object.keys(ErrorCategory).map(
|
||||
category => getRuleForCategory(category as any),
|
||||
);
|
||||
|
||||
@@ -51,12 +51,26 @@ function insertAdditionalFunctionDeclaration(
|
||||
CompilerError.invariant(originalFnName != null && compiled.id != null, {
|
||||
reason:
|
||||
'Expected function declarations that are referenced elsewhere to have a named identifier',
|
||||
loc: fnPath.node.loc ?? null,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: fnPath.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
CompilerError.invariant(originalFnParams.length === compiledParams.length, {
|
||||
reason:
|
||||
'Expected React Compiler optimized function declarations to have the same number of parameters as source',
|
||||
loc: fnPath.node.loc ?? null,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: fnPath.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const gatingCondition = t.identifier(
|
||||
@@ -140,7 +154,13 @@ export function insertGatedFunctionDeclaration(
|
||||
CompilerError.invariant(compiled.type === 'FunctionDeclaration', {
|
||||
reason: 'Expected compiled node type to match input type',
|
||||
description: `Got ${compiled.type} but expected FunctionDeclaration`,
|
||||
loc: fnPath.node.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: fnPath.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
insertAdditionalFunctionDeclaration(
|
||||
fnPath,
|
||||
|
||||
@@ -9,7 +9,7 @@ import {NodePath} from '@babel/core';
|
||||
import * as t from '@babel/types';
|
||||
import {Scope as BabelScope} from '@babel/traverse';
|
||||
|
||||
import {CompilerError, ErrorSeverity} from '../CompilerError';
|
||||
import {CompilerError, ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
EnvironmentConfig,
|
||||
GeneratedSource,
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import {getOrInsertWith} from '../Utils/utils';
|
||||
import {ExternalFunction, isHookName} from '../HIR/Environment';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {LoggerEvent, PluginOptions} from './Options';
|
||||
import {LoggerEvent, ParsedPluginOptions} from './Options';
|
||||
import {BabelFn, getReactCompilerRuntimeModule} from './Program';
|
||||
import {SuppressionRange} from './Suppression';
|
||||
|
||||
@@ -38,7 +38,7 @@ export function validateRestrictedImports(
|
||||
ImportDeclaration(importDeclPath) {
|
||||
if (restrictedImports.has(importDeclPath.node.source.value)) {
|
||||
error.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
reason: 'Bailing out due to blocklisted import',
|
||||
description: `Import from module ${importDeclPath.node.source.value}`,
|
||||
loc: importDeclPath.node.loc ?? null,
|
||||
@@ -46,7 +46,7 @@ export function validateRestrictedImports(
|
||||
}
|
||||
},
|
||||
});
|
||||
if (error.hasErrors()) {
|
||||
if (error.hasAnyErrors()) {
|
||||
return error;
|
||||
} else {
|
||||
return null;
|
||||
@@ -56,7 +56,7 @@ export function validateRestrictedImports(
|
||||
type ProgramContextOptions = {
|
||||
program: NodePath<t.Program>;
|
||||
suppressions: Array<SuppressionRange>;
|
||||
opts: PluginOptions;
|
||||
opts: ParsedPluginOptions;
|
||||
filename: string | null;
|
||||
code: string | null;
|
||||
hasModuleScopeOptOut: boolean;
|
||||
@@ -66,7 +66,7 @@ export class ProgramContext {
|
||||
* Program and environment context
|
||||
*/
|
||||
scope: BabelScope;
|
||||
opts: PluginOptions;
|
||||
opts: ParsedPluginOptions;
|
||||
filename: string | null;
|
||||
code: string | null;
|
||||
reactRuntimeModule: string;
|
||||
@@ -205,7 +205,7 @@ export class ProgramContext {
|
||||
}
|
||||
const error = new CompilerError();
|
||||
error.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
reason: 'Encountered conflicting global in generated program',
|
||||
description: `Conflict from local binding ${name}`,
|
||||
loc: scope.getBinding(name)?.path.node.loc ?? null,
|
||||
@@ -240,7 +240,7 @@ export function addImportsToProgram(
|
||||
programContext: ProgramContext,
|
||||
): void {
|
||||
const existingImports = getExistingImports(path);
|
||||
const stmts: Array<t.ImportDeclaration> = [];
|
||||
const stmts: Array<t.ImportDeclaration | t.VariableDeclaration> = [];
|
||||
const sortedModules = [...programContext.imports.entries()].sort(([a], [b]) =>
|
||||
a.localeCompare(b),
|
||||
);
|
||||
@@ -256,8 +256,14 @@ export function addImportsToProgram(
|
||||
{
|
||||
reason:
|
||||
'Encountered conflicting import specifiers in generated program',
|
||||
description: `Conflict from import ${loweredImport.module}:(${loweredImport.imported} as ${loweredImport.name}).`,
|
||||
loc: GeneratedSource,
|
||||
description: `Conflict from import ${loweredImport.module}:(${loweredImport.imported} as ${loweredImport.name})`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
@@ -268,7 +274,13 @@ export function addImportsToProgram(
|
||||
reason:
|
||||
'Found inconsistent import specifier. This is an internal bug.',
|
||||
description: `Expected import ${moduleName}:${specifierName} but found ${loweredImport.module}:${loweredImport.imported}`,
|
||||
loc: GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -291,9 +303,29 @@ export function addImportsToProgram(
|
||||
if (maybeExistingImports != null) {
|
||||
maybeExistingImports.pushContainer('specifiers', importSpecifiers);
|
||||
} else {
|
||||
stmts.push(
|
||||
t.importDeclaration(importSpecifiers, t.stringLiteral(moduleName)),
|
||||
);
|
||||
if (path.node.sourceType === 'module') {
|
||||
stmts.push(
|
||||
t.importDeclaration(importSpecifiers, t.stringLiteral(moduleName)),
|
||||
);
|
||||
} else {
|
||||
stmts.push(
|
||||
t.variableDeclaration('const', [
|
||||
t.variableDeclarator(
|
||||
t.objectPattern(
|
||||
sortedImport.map(specifier => {
|
||||
return t.objectProperty(
|
||||
t.identifier(specifier.imported),
|
||||
t.identifier(specifier.name),
|
||||
);
|
||||
}),
|
||||
),
|
||||
t.callExpression(t.identifier('require'), [
|
||||
t.stringLiteral(moduleName),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
path.unshiftContainer('body', stmts);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import * as t from '@babel/types';
|
||||
import {z} from 'zod';
|
||||
import {z} from 'zod/v4';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
tryParseExternalFunction,
|
||||
} from '../HIR/Environment';
|
||||
import {hasOwnProperty} from '../Utils/utils';
|
||||
import {fromZodError} from 'zod-validation-error';
|
||||
import {fromZodError} from 'zod-validation-error/v4';
|
||||
import {CompilerPipelineValue} from './Pipeline';
|
||||
|
||||
const PanicThresholdOptionsSchema = z.enum([
|
||||
@@ -51,8 +51,8 @@ const CustomOptOutDirectiveSchema = z
|
||||
.default(null);
|
||||
type CustomOptOutDirective = z.infer<typeof CustomOptOutDirectiveSchema>;
|
||||
|
||||
export type PluginOptions = {
|
||||
environment: EnvironmentConfig;
|
||||
export type PluginOptions = Partial<{
|
||||
environment: Partial<EnvironmentConfig>;
|
||||
|
||||
logger: Logger | null;
|
||||
|
||||
@@ -135,7 +135,12 @@ export type PluginOptions = {
|
||||
*/
|
||||
eslintSuppressionRules: Array<string> | null | undefined;
|
||||
|
||||
/**
|
||||
* Whether to report "suppression" errors for Flow suppressions. If false, suppression errors
|
||||
* are only emitted for ESLint suppressions
|
||||
*/
|
||||
flowSuppressions: boolean;
|
||||
|
||||
/*
|
||||
* Ignore 'use no forget' annotations. Helpful during testing but should not be used in production.
|
||||
*/
|
||||
@@ -161,7 +166,11 @@ export type PluginOptions = {
|
||||
* a userspace approximation of runtime APIs.
|
||||
*/
|
||||
target: CompilerReactTarget;
|
||||
};
|
||||
}>;
|
||||
|
||||
export type ParsedPluginOptions = Required<
|
||||
Omit<PluginOptions, 'environment'>
|
||||
> & {environment: EnvironmentConfig};
|
||||
|
||||
const CompilerReactTargetSchema = z.union([
|
||||
z.literal('17'),
|
||||
@@ -277,7 +286,7 @@ export type Logger = {
|
||||
debugLogIRs?: (value: CompilerPipelineValue) => void;
|
||||
};
|
||||
|
||||
export const defaultOptions: PluginOptions = {
|
||||
export const defaultOptions: ParsedPluginOptions = {
|
||||
compilationMode: 'infer',
|
||||
panicThreshold: 'none',
|
||||
environment: parseEnvironmentConfig({}).unwrap(),
|
||||
@@ -294,9 +303,9 @@ export const defaultOptions: PluginOptions = {
|
||||
enableReanimatedCheck: true,
|
||||
customOptOutDirectives: null,
|
||||
target: '19',
|
||||
} as const;
|
||||
};
|
||||
|
||||
export function parsePluginOptions(obj: unknown): PluginOptions {
|
||||
export function parsePluginOptions(obj: unknown): ParsedPluginOptions {
|
||||
if (obj == null || typeof obj !== 'object') {
|
||||
return defaultOptions;
|
||||
}
|
||||
|
||||
@@ -33,9 +33,7 @@ import {findContextIdentifiers} from '../HIR/FindContextIdentifiers';
|
||||
import {
|
||||
analyseFunctions,
|
||||
dropManualMemoization,
|
||||
inferMutableRanges,
|
||||
inferReactivePlaces,
|
||||
inferReferenceEffects,
|
||||
inlineImmediatelyInvokedFunctionExpressions,
|
||||
inferEffectDependencies,
|
||||
} from '../Inference';
|
||||
@@ -100,12 +98,12 @@ 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';
|
||||
import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
|
||||
|
||||
export type CompilerPipelineValue =
|
||||
| {kind: 'ast'; name: string; value: CodegenFunction}
|
||||
@@ -229,26 +227,12 @@ function runWithEnvironment(
|
||||
analyseFunctions(hir);
|
||||
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
|
||||
|
||||
if (!env.config.enableNewMutationAliasingModel) {
|
||||
const fnEffectErrors = inferReferenceEffects(hir);
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (fnEffectErrors.length > 0) {
|
||||
CompilerError.throw(fnEffectErrors[0]);
|
||||
}
|
||||
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
|
||||
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (mutabilityAliasingErrors.isErr()) {
|
||||
throw mutabilityAliasingErrors.unwrapErr();
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!env.config.enableNewMutationAliasingModel) {
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
}
|
||||
|
||||
// Note: Has to come after infer reference effects because "dead" code may still affect inference
|
||||
@@ -263,20 +247,15 @@ function runWithEnvironment(
|
||||
pruneMaybeThrows(hir);
|
||||
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
|
||||
|
||||
if (!env.config.enableNewMutationAliasingModel) {
|
||||
inferMutableRanges(hir);
|
||||
log({kind: 'hir', name: 'InferMutableRanges', value: hir});
|
||||
} else {
|
||||
const mutabilityAliasingErrors = inferMutationAliasingRanges(hir, {
|
||||
isFunctionExpression: false,
|
||||
});
|
||||
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (mutabilityAliasingErrors.isErr()) {
|
||||
throw mutabilityAliasingErrors.unwrapErr();
|
||||
}
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
const mutabilityAliasingRangeErrors = inferMutationAliasingRanges(hir, {
|
||||
isFunctionExpression: false,
|
||||
});
|
||||
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (mutabilityAliasingRangeErrors.isErr()) {
|
||||
throw mutabilityAliasingRangeErrors.unwrapErr();
|
||||
}
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
}
|
||||
|
||||
if (env.isInferredMemoEnabled) {
|
||||
@@ -297,7 +276,7 @@ function runWithEnvironment(
|
||||
}
|
||||
|
||||
if (env.config.validateNoSetStateInEffects) {
|
||||
env.logErrors(validateNoSetStateInEffects(hir));
|
||||
env.logErrors(validateNoSetStateInEffects(hir, env));
|
||||
}
|
||||
|
||||
if (env.config.validateNoJSXInTryStatements) {
|
||||
@@ -308,12 +287,7 @@ function runWithEnvironment(
|
||||
validateNoImpureFunctionsInRender(hir).unwrap();
|
||||
}
|
||||
|
||||
if (
|
||||
env.config.validateNoFreezingKnownMutableFunctions ||
|
||||
env.config.enableNewMutationAliasingModel
|
||||
) {
|
||||
validateNoFreezingKnownMutableFunctions(hir).unwrap();
|
||||
}
|
||||
validateNoFreezingKnownMutableFunctions(hir).unwrap();
|
||||
}
|
||||
|
||||
inferReactivePlaces(hir);
|
||||
@@ -351,6 +325,15 @@ function runWithEnvironment(
|
||||
outlineJSX(hir);
|
||||
}
|
||||
|
||||
if (env.config.enableNameAnonymousFunctions) {
|
||||
nameAnonymousFunctions(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'NameAnonymousFunctions',
|
||||
value: hir,
|
||||
});
|
||||
}
|
||||
|
||||
if (env.config.enableFunctionOutlining) {
|
||||
outlineFunctions(hir, fbtOperands);
|
||||
log({kind: 'hir', name: 'OutlineFunctions', value: hir});
|
||||
|
||||
@@ -10,7 +10,7 @@ import * as t from '@babel/types';
|
||||
import {
|
||||
CompilerError,
|
||||
CompilerErrorDetail,
|
||||
ErrorSeverity,
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
import {ExternalFunction, ReactFunctionType} from '../HIR/Environment';
|
||||
import {CodegenFunction} from '../ReactiveScopes';
|
||||
@@ -23,7 +23,11 @@ import {
|
||||
ProgramContext,
|
||||
validateRestrictedImports,
|
||||
} from './Imports';
|
||||
import {CompilerReactTarget, PluginOptions} from './Options';
|
||||
import {
|
||||
CompilerReactTarget,
|
||||
ParsedPluginOptions,
|
||||
PluginOptions,
|
||||
} from './Options';
|
||||
import {compileFn} from './Pipeline';
|
||||
import {
|
||||
filterSuppressionsThatAffectFunction,
|
||||
@@ -34,7 +38,7 @@ import {GeneratedSource} from '../HIR';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
|
||||
export type CompilerPass = {
|
||||
opts: PluginOptions;
|
||||
opts: ParsedPluginOptions;
|
||||
filename: string | null;
|
||||
comments: Array<t.CommentBlock | t.CommentLine>;
|
||||
code: string | null;
|
||||
@@ -45,7 +49,7 @@ const DYNAMIC_GATING_DIRECTIVE = new RegExp('^use memo if\\(([^\\)]*)\\)$');
|
||||
|
||||
export function tryFindDirectiveEnablingMemoization(
|
||||
directives: Array<t.Directive>,
|
||||
opts: PluginOptions,
|
||||
opts: ParsedPluginOptions,
|
||||
): Result<t.Directive | null, CompilerError> {
|
||||
const optIn = directives.find(directive =>
|
||||
OPT_IN_DIRECTIVES.has(directive.value.value),
|
||||
@@ -81,7 +85,7 @@ export function findDirectiveDisablingMemoization(
|
||||
}
|
||||
function findDirectivesDynamicGating(
|
||||
directives: Array<t.Directive>,
|
||||
opts: PluginOptions,
|
||||
opts: ParsedPluginOptions,
|
||||
): Result<
|
||||
{
|
||||
gating: ExternalFunction;
|
||||
@@ -104,14 +108,14 @@ function findDirectivesDynamicGating(
|
||||
errors.push({
|
||||
reason: `Dynamic gating directive is not a valid JavaScript identifier`,
|
||||
description: `Found '${directive.value.value}'`,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: ErrorCategory.Gating,
|
||||
loc: directive.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.hasErrors()) {
|
||||
if (errors.hasAnyErrors()) {
|
||||
return Err(errors);
|
||||
} else if (result.length > 1) {
|
||||
const error = new CompilerError();
|
||||
@@ -120,7 +124,7 @@ function findDirectivesDynamicGating(
|
||||
description: `Expected a single directive but found [${result
|
||||
.map(r => r.directive.value.value)
|
||||
.join(', ')}]`,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: ErrorCategory.Gating,
|
||||
loc: result[0].directive.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -138,15 +142,13 @@ function findDirectivesDynamicGating(
|
||||
}
|
||||
}
|
||||
|
||||
function isCriticalError(err: unknown): boolean {
|
||||
return !(err instanceof CompilerError) || err.isCritical();
|
||||
function isError(err: unknown): boolean {
|
||||
return !(err instanceof CompilerError) || err.hasErrors();
|
||||
}
|
||||
|
||||
function isConfigError(err: unknown): boolean {
|
||||
if (err instanceof CompilerError) {
|
||||
return err.details.some(
|
||||
detail => detail.severity === ErrorSeverity.InvalidConfig,
|
||||
);
|
||||
return err.details.some(detail => detail.category === ErrorCategory.Config);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -211,8 +213,7 @@ function handleError(
|
||||
logError(err, context, fnLoc);
|
||||
if (
|
||||
context.opts.panicThreshold === 'all_errors' ||
|
||||
(context.opts.panicThreshold === 'critical_errors' &&
|
||||
isCriticalError(err)) ||
|
||||
(context.opts.panicThreshold === 'critical_errors' && isError(err)) ||
|
||||
isConfigError(err) // Always throws regardless of panic threshold
|
||||
) {
|
||||
throw err;
|
||||
@@ -313,7 +314,13 @@ function insertNewOutlinedFunctionNode(
|
||||
CompilerError.invariant(insertedFuncDecl.isFunctionDeclaration(), {
|
||||
reason: 'Expected inserted function declaration',
|
||||
description: `Got: ${insertedFuncDecl}`,
|
||||
loc: insertedFuncDecl.node?.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: insertedFuncDecl.node?.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
return insertedFuncDecl;
|
||||
}
|
||||
@@ -422,7 +429,14 @@ export function compileProgram(
|
||||
for (const outlined of compiled.outlined) {
|
||||
CompilerError.invariant(outlined.fn.outlined.length === 0, {
|
||||
reason: 'Unexpected nested outlined functions',
|
||||
loc: outlined.fn.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: outlined.fn.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
const fn = insertNewOutlinedFunctionNode(
|
||||
program,
|
||||
@@ -455,7 +469,7 @@ export function compileProgram(
|
||||
new CompilerErrorDetail({
|
||||
reason:
|
||||
'Unexpected compiled functions when module scope opt-out is present',
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: ErrorCategory.Invariant,
|
||||
loc: null,
|
||||
}),
|
||||
);
|
||||
@@ -490,7 +504,20 @@ function findFunctionsToCompile(
|
||||
): Array<CompileSource> {
|
||||
const queue: Array<CompileSource> = [];
|
||||
const traverseFunction = (fn: BabelFn, pass: CompilerPass): void => {
|
||||
// In 'all' mode, compile only top level functions
|
||||
if (
|
||||
pass.opts.compilationMode === 'all' &&
|
||||
fn.scope.getProgramParent() !== fn.scope.parent
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fnType = getReactFunctionType(fn, pass);
|
||||
|
||||
if (pass.opts.environment.validateNoDynamicallyCreatedComponentsOrHooks) {
|
||||
validateNoDynamicallyCreatedComponentsOrHooks(fn, pass, programContext);
|
||||
}
|
||||
|
||||
if (fnType === null || programContext.alreadyCompiled.has(fn.node)) {
|
||||
return;
|
||||
}
|
||||
@@ -810,7 +837,7 @@ function shouldSkipCompilation(
|
||||
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,
|
||||
category: ErrorCategory.Config,
|
||||
loc: null,
|
||||
}),
|
||||
);
|
||||
@@ -834,6 +861,72 @@ function shouldSkipCompilation(
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that Components/Hooks are always defined at module level. This prevents scope reference
|
||||
* errors that occur when the compiler attempts to optimize the nested component/hook while its
|
||||
* parent function remains uncompiled.
|
||||
*/
|
||||
function validateNoDynamicallyCreatedComponentsOrHooks(
|
||||
fn: BabelFn,
|
||||
pass: CompilerPass,
|
||||
programContext: ProgramContext,
|
||||
): void {
|
||||
const parentNameExpr = getFunctionName(fn);
|
||||
const parentName =
|
||||
parentNameExpr !== null && parentNameExpr.isIdentifier()
|
||||
? parentNameExpr.node.name
|
||||
: '<anonymous>';
|
||||
|
||||
const validateNestedFunction = (
|
||||
nestedFn: NodePath<
|
||||
t.FunctionDeclaration | t.FunctionExpression | t.ArrowFunctionExpression
|
||||
>,
|
||||
): void => {
|
||||
if (
|
||||
nestedFn.node === fn.node ||
|
||||
programContext.alreadyCompiled.has(nestedFn.node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nestedFn.scope.getProgramParent() !== nestedFn.scope.parent) {
|
||||
const nestedFnType = getReactFunctionType(nestedFn as BabelFn, pass);
|
||||
const nestedFnNameExpr = getFunctionName(nestedFn as BabelFn);
|
||||
const nestedName =
|
||||
nestedFnNameExpr !== null && nestedFnNameExpr.isIdentifier()
|
||||
? nestedFnNameExpr.node.name
|
||||
: '<anonymous>';
|
||||
if (nestedFnType === 'Component' || nestedFnType === 'Hook') {
|
||||
CompilerError.throwDiagnostic({
|
||||
category: ErrorCategory.Factories,
|
||||
reason: `Components and hooks cannot be created dynamically`,
|
||||
description: `The function \`${nestedName}\` appears to be a React ${nestedFnType.toLowerCase()}, but it's defined inside \`${parentName}\`. Components and Hooks should always be declared at module scope`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
message: 'this function dynamically created a component/hook',
|
||||
loc: parentNameExpr?.node.loc ?? fn.node.loc ?? null,
|
||||
},
|
||||
{
|
||||
kind: 'error',
|
||||
message: 'the component is created here',
|
||||
loc: nestedFnNameExpr?.node.loc ?? nestedFn.node.loc ?? null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
nestedFn.skip();
|
||||
};
|
||||
|
||||
fn.traverse({
|
||||
FunctionDeclaration: validateNestedFunction,
|
||||
FunctionExpression: validateNestedFunction,
|
||||
ArrowFunctionExpression: validateNestedFunction,
|
||||
});
|
||||
}
|
||||
|
||||
function getReactFunctionType(
|
||||
fn: BabelFn,
|
||||
pass: CompilerPass,
|
||||
@@ -872,11 +965,6 @@ function getReactFunctionType(
|
||||
return componentSyntaxType;
|
||||
}
|
||||
case 'all': {
|
||||
// Compile only top level functions
|
||||
if (fn.scope.getProgramParent() !== fn.scope.parent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getComponentOrHookLike(fn, hookPattern) ?? 'Other';
|
||||
}
|
||||
default: {
|
||||
@@ -1336,7 +1424,13 @@ export function getReactCompilerRuntimeModule(
|
||||
{
|
||||
reason: 'Expected target to already be validated',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import type * as BabelCore from '@babel/core';
|
||||
import {hasOwnProperty} from '../Utils/utils';
|
||||
import {PluginOptions} from './Options';
|
||||
import {ParsedPluginOptions} from './Options';
|
||||
|
||||
function hasModule(name: string): boolean {
|
||||
if (typeof require === 'undefined') {
|
||||
@@ -52,7 +52,9 @@ export function pipelineUsesReanimatedPlugin(
|
||||
return hasModule('react-native-reanimated');
|
||||
}
|
||||
|
||||
export function injectReanimatedFlag(options: PluginOptions): PluginOptions {
|
||||
export function injectReanimatedFlag(
|
||||
options: ParsedPluginOptions,
|
||||
): ParsedPluginOptions {
|
||||
return {
|
||||
...options,
|
||||
environment: {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
CompilerSuggestionOperation,
|
||||
ErrorSeverity,
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {GeneratedSource} from '../HIR';
|
||||
@@ -86,12 +86,18 @@ export function findProgramSuppressions(
|
||||
let enableComment: t.Comment | null = null;
|
||||
let source: SuppressionSource | null = null;
|
||||
|
||||
const rulePattern = `(${ruleNames.join('|')})`;
|
||||
const disableNextLinePattern = new RegExp(
|
||||
`eslint-disable-next-line ${rulePattern}`,
|
||||
);
|
||||
const disablePattern = new RegExp(`eslint-disable ${rulePattern}`);
|
||||
const enablePattern = new RegExp(`eslint-enable ${rulePattern}`);
|
||||
let disableNextLinePattern: RegExp | null = null;
|
||||
let disablePattern: RegExp | null = null;
|
||||
let enablePattern: RegExp | null = null;
|
||||
if (ruleNames.length !== 0) {
|
||||
const rulePattern = `(${ruleNames.join('|')})`;
|
||||
disableNextLinePattern = new RegExp(
|
||||
`eslint-disable-next-line ${rulePattern}`,
|
||||
);
|
||||
disablePattern = new RegExp(`eslint-disable ${rulePattern}`);
|
||||
enablePattern = new RegExp(`eslint-enable ${rulePattern}`);
|
||||
}
|
||||
|
||||
const flowSuppressionPattern = new RegExp(
|
||||
'\\$(FlowFixMe\\w*|FlowExpectedError|FlowIssue)\\[react\\-rule',
|
||||
);
|
||||
@@ -107,6 +113,7 @@ export function findProgramSuppressions(
|
||||
* CommentLine within the block.
|
||||
*/
|
||||
disableComment == null &&
|
||||
disableNextLinePattern != null &&
|
||||
disableNextLinePattern.test(comment.value)
|
||||
) {
|
||||
disableComment = comment;
|
||||
@@ -124,12 +131,16 @@ export function findProgramSuppressions(
|
||||
source = 'Flow';
|
||||
}
|
||||
|
||||
if (disablePattern.test(comment.value)) {
|
||||
if (disablePattern != null && disablePattern.test(comment.value)) {
|
||||
disableComment = comment;
|
||||
source = 'Eslint';
|
||||
}
|
||||
|
||||
if (enablePattern.test(comment.value) && source === 'Eslint') {
|
||||
if (
|
||||
enablePattern != null &&
|
||||
enablePattern.test(comment.value) &&
|
||||
source === 'Eslint'
|
||||
) {
|
||||
enableComment = comment;
|
||||
}
|
||||
|
||||
@@ -152,7 +163,14 @@ export function suppressionsToCompilerError(
|
||||
): CompilerError {
|
||||
CompilerError.invariant(suppressionRanges.length !== 0, {
|
||||
reason: `Expected at least suppression comment source range`,
|
||||
loc: GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
const error = new CompilerError();
|
||||
for (const suppressionRange of suppressionRanges) {
|
||||
@@ -183,9 +201,9 @@ export function suppressionsToCompilerError(
|
||||
}
|
||||
error.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: reason,
|
||||
reason: 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,
|
||||
category: ErrorCategory.Suppression,
|
||||
suggestions: [
|
||||
{
|
||||
description: suggestion,
|
||||
@@ -196,7 +214,7 @@ export function suppressionsToCompilerError(
|
||||
op: CompilerSuggestionOperation.Remove,
|
||||
},
|
||||
],
|
||||
}).withDetail({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: suppressionRange.disableComment.loc ?? null,
|
||||
message: 'Found React rule suppression',
|
||||
|
||||
@@ -8,27 +8,27 @@
|
||||
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,
|
||||
CompilerDiagnosticOptions,
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
|
||||
function throwInvalidReact(
|
||||
options: Omit<CompilerDiagnosticOptions, 'severity'>,
|
||||
options: CompilerDiagnosticOptions,
|
||||
{logger, filename}: TraversalState,
|
||||
): never {
|
||||
const detail: CompilerDiagnosticOptions = {
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
...options,
|
||||
};
|
||||
logger?.logEvent(filename, {
|
||||
kind: 'CompileError',
|
||||
fnLoc: null,
|
||||
detail: new CompilerDiagnostic(detail),
|
||||
detail: new CompilerDiagnostic(options),
|
||||
});
|
||||
CompilerError.throwDiagnostic(detail);
|
||||
CompilerError.throwDiagnostic(options);
|
||||
}
|
||||
|
||||
function isAutodepsSigil(
|
||||
@@ -92,10 +92,11 @@ function assertValidEffectImportReference(
|
||||
*/
|
||||
throwInvalidReact(
|
||||
{
|
||||
category:
|
||||
category: ErrorCategory.AutomaticEffectDependencies,
|
||||
reason:
|
||||
'Cannot infer dependencies of this effect. This will break your build!',
|
||||
description:
|
||||
'To resolve, either pass a dependency array or fix reported compiler bailout diagnostics.' +
|
||||
'To resolve, either pass a dependency array or fix reported compiler bailout diagnostics' +
|
||||
(maybeErrorDiagnostic ? ` ${maybeErrorDiagnostic}` : ''),
|
||||
details: [
|
||||
{
|
||||
@@ -123,13 +124,11 @@ function assertValidFireImportReference(
|
||||
);
|
||||
throwInvalidReact(
|
||||
{
|
||||
category:
|
||||
'[Fire] Untransformed reference to compiler-required feature.',
|
||||
category: ErrorCategory.Fire,
|
||||
reason: '[Fire] Untransformed reference to compiler-required feature.',
|
||||
description:
|
||||
'Either remove this `fire` call or ensure it is successfully transformed by the compiler' +
|
||||
maybeErrorDiagnostic
|
||||
? ` ${maybeErrorDiagnostic}`
|
||||
: '',
|
||||
(maybeErrorDiagnostic != null ? ` ${maybeErrorDiagnostic}` : ''),
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
@@ -216,7 +215,14 @@ function validateImportSpecifier(
|
||||
const binding = local.scope.getBinding(local.node.name);
|
||||
CompilerError.invariant(binding != null, {
|
||||
reason: 'Expected binding to be found for import specifier',
|
||||
loc: local.node.loc ?? null,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: local.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
checkFn(binding.referencePaths, state);
|
||||
}
|
||||
@@ -236,7 +242,14 @@ function validateNamespacedImport(
|
||||
|
||||
CompilerError.invariant(binding != null, {
|
||||
reason: 'Expected binding to be found for import specifier',
|
||||
loc: local.node.loc ?? null,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: local.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
const filteredReferences = new Map<
|
||||
CheckInvalidReferenceFn,
|
||||
|
||||
@@ -46,7 +46,14 @@ export function raiseUnificationErrors(
|
||||
if (errs.length === 0) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Should not have array of zero errors',
|
||||
loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else if (errs.length === 1) {
|
||||
CompilerError.throwInvalidJS({
|
||||
|
||||
@@ -152,7 +152,13 @@ export function makeLinearId(id: number): LinearId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected LinearId id to be a non-negative integer',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return id as LinearId;
|
||||
@@ -167,7 +173,13 @@ export function makeTypeParameterId(id: number): TypeParameterId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected TypeParameterId to be a non-negative integer',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return id as TypeParameterId;
|
||||
@@ -191,7 +203,13 @@ export function makeVariableId(id: number): VariableId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected VariableId id to be a non-negative integer',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return id as VariableId;
|
||||
@@ -399,7 +417,14 @@ function convertFlowType(flowType: FlowType, loc: string): ResolvedType {
|
||||
} else {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unsupported property kind ${prop.kind}`,
|
||||
loc: GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -468,7 +493,14 @@ function convertFlowType(flowType: FlowType, loc: string): ResolvedType {
|
||||
} else {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unsupported property kind ${prop.kind}`,
|
||||
loc: GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -487,7 +519,14 @@ function convertFlowType(flowType: FlowType, loc: string): ResolvedType {
|
||||
} else {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unsupported property kind ${prop.kind}`,
|
||||
loc: GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -500,7 +539,14 @@ function convertFlowType(flowType: FlowType, loc: string): ResolvedType {
|
||||
}
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unsupported class instance type ${flowType.def.type.kind}`,
|
||||
loc: GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
case 'Fun':
|
||||
@@ -559,7 +605,14 @@ function convertFlowType(flowType: FlowType, loc: string): ResolvedType {
|
||||
} else {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unsupported component props type ${propsType.type.kind}`,
|
||||
loc: GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -712,7 +765,14 @@ export class FlowTypeEnv implements ITypeEnv {
|
||||
// TODO: use flow-js only for web environments (e.g. playground)
|
||||
CompilerError.invariant(env.config.flowTypeProvider != null, {
|
||||
reason: 'Expected flowDumpTypes to be defined in environment config',
|
||||
loc: GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
let stdout: any;
|
||||
if (source === lastFlowSource) {
|
||||
|
||||
@@ -38,7 +38,13 @@ export function assertConsistentIdentifiers(fn: HIRFunction): void {
|
||||
CompilerError.invariant(instr.lvalue.identifier.name === null, {
|
||||
reason: `Expected all lvalues to be temporaries`,
|
||||
description: `Found named lvalue \`${instr.lvalue.identifier.name}\``,
|
||||
loc: instr.lvalue.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instr.lvalue.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
CompilerError.invariant(!assignments.has(instr.lvalue.identifier.id), {
|
||||
@@ -46,7 +52,13 @@ export function assertConsistentIdentifiers(fn: HIRFunction): void {
|
||||
description: `Found duplicate assignment of '${printPlace(
|
||||
instr.lvalue,
|
||||
)}'`,
|
||||
loc: instr.lvalue.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instr.lvalue.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
assignments.add(instr.lvalue.identifier.id);
|
||||
@@ -77,7 +89,13 @@ function validate(
|
||||
CompilerError.invariant(identifier === previous, {
|
||||
reason: `Duplicate identifier object`,
|
||||
description: `Found duplicate identifier object for id ${identifier.id}`,
|
||||
loc: loc ?? GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: loc ?? GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,7 +18,13 @@ export function assertTerminalSuccessorsExist(fn: HIRFunction): void {
|
||||
description: `Block bb${successor} does not exist for terminal '${printTerminal(
|
||||
block.terminal,
|
||||
)}'`,
|
||||
loc: (block.terminal as any).loc ?? GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: (block.terminal as any).loc ?? GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return successor;
|
||||
@@ -33,14 +39,26 @@ export function assertTerminalPredsExist(fn: HIRFunction): void {
|
||||
CompilerError.invariant(predBlock != null, {
|
||||
reason: 'Expected predecessor block to exist',
|
||||
description: `Block ${block.id} references non-existent ${pred}`,
|
||||
loc: GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
CompilerError.invariant(
|
||||
[...eachTerminalSuccessor(predBlock.terminal)].includes(block.id),
|
||||
{
|
||||
reason: 'Terminal successor does not reference correct predecessor',
|
||||
description: `Block bb${block.id} has bb${predBlock.id} as a predecessor, but bb${predBlock.id}'s successors do not include bb${block.id}`,
|
||||
loc: GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -131,7 +131,13 @@ export function recursivelyTraverseItems<T, TContext>(
|
||||
CompilerError.invariant(disjoint || nested, {
|
||||
reason: 'Invalid nesting in program blocks or scopes',
|
||||
description: `Items overlap but are not nested: ${maybeParentRange.start}:${maybeParentRange.end}(${currRange.start}:${currRange.end})`,
|
||||
loc: GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
if (disjoint) {
|
||||
exit(maybeParent, context);
|
||||
|
||||
@@ -57,7 +57,13 @@ function validateMutableRange(
|
||||
{
|
||||
reason: `Invalid mutable range: [${range.start}:${range.end}]`,
|
||||
description: `${printPlace(place)} in ${description}`,
|
||||
loc: place.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: place.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
CompilerSuggestionOperation,
|
||||
ErrorSeverity,
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {assertExhaustive, hasNode} from '../Utils/utils';
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
makePropertyLiteral,
|
||||
makeType,
|
||||
promoteTemporary,
|
||||
validateIdentifierName,
|
||||
} from './HIR';
|
||||
import HIRBuilder, {Bindings, createTemporaryPlace} from './HIRBuilder';
|
||||
import {BuiltInArrayId} from './ObjectShape';
|
||||
@@ -107,10 +108,10 @@ export function lower(
|
||||
if (binding.kind !== 'Identifier') {
|
||||
builder.errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: 'Could not find binding',
|
||||
description: `[BuildHIR] Could not find binding for param \`${param.node.name}\`.`,
|
||||
}).withDetail({
|
||||
category: ErrorCategory.Invariant,
|
||||
reason: 'Could not find binding',
|
||||
description: `[BuildHIR] Could not find binding for param \`${param.node.name}\``,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: param.node.loc ?? null,
|
||||
message: 'Could not find binding',
|
||||
@@ -171,10 +172,10 @@ export function lower(
|
||||
} else {
|
||||
builder.errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: `Handle ${param.node.type} parameters`,
|
||||
description: `[BuildHIR] Add support for ${param.node.type} parameters.`,
|
||||
}).withDetail({
|
||||
category: ErrorCategory.Todo,
|
||||
reason: `Handle ${param.node.type} parameters`,
|
||||
description: `[BuildHIR] Add support for ${param.node.type} parameters`,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: param.node.loc ?? null,
|
||||
message: 'Unsupported parameter type',
|
||||
@@ -202,10 +203,10 @@ export function lower(
|
||||
} else {
|
||||
builder.errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: `Unexpected function body kind`,
|
||||
description: `Expected function body to be an expression or a block statement, got \`${body.type}\`.`,
|
||||
}).withDetail({
|
||||
category: ErrorCategory.Syntax,
|
||||
reason: `Unexpected function body kind`,
|
||||
description: `Expected function body to be an expression or a block statement, got \`${body.type}\``,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: body.node.loc ?? null,
|
||||
message: 'Expected a block statement or expression',
|
||||
@@ -213,7 +214,17 @@ export function lower(
|
||||
);
|
||||
}
|
||||
|
||||
if (builder.errors.hasErrors()) {
|
||||
let validatedId: HIRFunction['id'] = null;
|
||||
if (id != null) {
|
||||
const idResult = validateIdentifierName(id);
|
||||
if (idResult.isErr()) {
|
||||
builder.errors.merge(idResult.unwrapErr());
|
||||
} else {
|
||||
validatedId = idResult.unwrap().value;
|
||||
}
|
||||
}
|
||||
|
||||
if (builder.errors.hasAnyErrors()) {
|
||||
return Err(builder.errors);
|
||||
}
|
||||
|
||||
@@ -234,7 +245,8 @@ export function lower(
|
||||
);
|
||||
|
||||
return Ok({
|
||||
id,
|
||||
id: validatedId,
|
||||
nameHint: null,
|
||||
params,
|
||||
fnType: bindings == null ? env.fnType : 'Other',
|
||||
returnTypeAnnotation: null, // TODO: extract the actual return type node if present
|
||||
@@ -272,7 +284,7 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason:
|
||||
'(BuildHIR::lowerStatement) Support ThrowStatement inside of try/catch',
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -439,7 +451,13 @@ function lowerStatement(
|
||||
reason: 'Expected to find binding for hoisted identifier',
|
||||
description: `Could not find a binding for ${id.node.name}`,
|
||||
suggestions: null,
|
||||
loc: id.node.loc ?? GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: id.node.loc ?? GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
if (builder.environment.isHoistedIdentifier(binding.identifier)) {
|
||||
// Already hoisted
|
||||
@@ -459,7 +477,7 @@ function lowerStatement(
|
||||
kind = InstructionKind.HoistedFunction;
|
||||
} else if (!binding.path.isVariableDeclarator()) {
|
||||
builder.errors.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
reason: 'Unsupported declaration type for hoisting',
|
||||
description: `variable "${binding.identifier.name}" declared with ${binding.path.type}`,
|
||||
suggestions: null,
|
||||
@@ -468,7 +486,7 @@ function lowerStatement(
|
||||
continue;
|
||||
} else {
|
||||
builder.errors.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
reason: 'Handle non-const declarations for hoisting',
|
||||
description: `variable "${binding.identifier.name}" declared with ${binding.kind}`,
|
||||
suggestions: null,
|
||||
@@ -481,7 +499,14 @@ function lowerStatement(
|
||||
CompilerError.invariant(identifier.kind === 'Identifier', {
|
||||
reason:
|
||||
'Expected hoisted binding to be a local identifier, not a global',
|
||||
loc: id.node.loc ?? GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: id.node.loc ?? GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
const place: Place = {
|
||||
effect: Effect.Unknown,
|
||||
@@ -548,7 +573,7 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason:
|
||||
'(BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement',
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -620,7 +645,7 @@ function lowerStatement(
|
||||
if (test.node == null) {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerStatement) Handle empty test in ForStatement`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -771,7 +796,7 @@ function lowerStatement(
|
||||
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,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: case_.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -843,7 +868,7 @@ function lowerStatement(
|
||||
if (nodeKind === 'var') {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerStatement) Handle ${nodeKind} kinds in VariableDeclaration`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -871,7 +896,7 @@ function lowerStatement(
|
||||
if (binding.kind !== 'Identifier') {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Could not find binding for declaration.`,
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: ErrorCategory.Invariant,
|
||||
loc: id.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -888,7 +913,7 @@ function lowerStatement(
|
||||
const declRangeStart = declaration.parentPath.node.start!;
|
||||
builder.errors.push({
|
||||
reason: `Expect \`const\` declaration not to be reassigned`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: id.node.loc ?? null,
|
||||
suggestions: [
|
||||
{
|
||||
@@ -935,7 +960,7 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason: `Expected variable declaration to be an identifier if no initializer was provided`,
|
||||
description: `Got a \`${id.type}\``,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1014,7 +1039,13 @@ function lowerStatement(
|
||||
CompilerError.invariant(stmt.get('id').type === 'Identifier', {
|
||||
reason: 'function declarations must have a name',
|
||||
description: null,
|
||||
loc: stmt.node.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: stmt.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
const id = stmt.get('id') as NodePath<t.Identifier>;
|
||||
@@ -1043,7 +1074,7 @@ function lowerStatement(
|
||||
if (stmt.node.await) {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerStatement) Handle for-await loops`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1114,7 +1145,13 @@ function lowerStatement(
|
||||
CompilerError.invariant(declarations.length === 1, {
|
||||
reason: `Expected only one declaration in the init of a ForOfStatement, got ${declarations.length}`,
|
||||
description: null,
|
||||
loc: left.node.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: left.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
const id = declarations[0].get('id');
|
||||
@@ -1129,8 +1166,15 @@ function lowerStatement(
|
||||
test = lowerValueToTemporary(builder, assign);
|
||||
} else {
|
||||
CompilerError.invariant(left.isLVal(), {
|
||||
loc: leftLoc,
|
||||
reason: 'Expected ForOf init to be a variable declaration or lval',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: leftLoc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
const assign = lowerAssignment(
|
||||
builder,
|
||||
@@ -1207,7 +1251,13 @@ function lowerStatement(
|
||||
CompilerError.invariant(declarations.length === 1, {
|
||||
reason: `Expected only one declaration in the init of a ForInStatement, got ${declarations.length}`,
|
||||
description: null,
|
||||
loc: left.node.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: left.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
const id = declarations[0].get('id');
|
||||
@@ -1222,8 +1272,15 @@ function lowerStatement(
|
||||
test = lowerValueToTemporary(builder, assign);
|
||||
} else {
|
||||
CompilerError.invariant(left.isLVal(), {
|
||||
loc: leftLoc,
|
||||
reason: 'Expected ForIn init to be a variable declaration or lval',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: leftLoc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
const assign = lowerAssignment(
|
||||
builder,
|
||||
@@ -1275,7 +1332,7 @@ function lowerStatement(
|
||||
if (!hasNode(handlerPath)) {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerStatement) Handle TryStatement without a catch clause`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1284,7 +1341,7 @@ function lowerStatement(
|
||||
if (hasNode(stmt.get('finalizer'))) {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerStatement) Handle TryStatement with a finalizer ('finally') clause`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1377,7 +1434,7 @@ function lowerStatement(
|
||||
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,
|
||||
category: ErrorCategory.UnsupportedSyntax,
|
||||
loc: stmtPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1397,7 +1454,7 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason: 'Inline `class` declarations are not supported',
|
||||
description: `Move class declarations outside of components/hooks`,
|
||||
severity: ErrorSeverity.UnsupportedJS,
|
||||
category: ErrorCategory.UnsupportedSyntax,
|
||||
loc: stmtPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1426,7 +1483,7 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason:
|
||||
'JavaScript `import` and `export` statements may only appear at the top level of a module',
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: stmtPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1441,7 +1498,7 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason:
|
||||
'TypeScript `namespace` statements may only appear at the top level of a module',
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: stmtPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1519,7 +1576,7 @@ function lowerObjectPropertyKey(
|
||||
*/
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: key.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1544,7 +1601,7 @@ function lowerObjectPropertyKey(
|
||||
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: key.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1601,7 +1658,7 @@ function lowerExpression(
|
||||
if (!valuePath.isExpression()) {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${valuePath.type} values in ObjectExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: valuePath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1627,7 +1684,7 @@ function lowerExpression(
|
||||
if (propertyPath.node.kind !== 'method') {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${propertyPath.node.kind} functions in ObjectExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: propertyPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1648,7 +1705,7 @@ function lowerExpression(
|
||||
} else {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${propertyPath.type} properties in ObjectExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: propertyPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1681,7 +1738,7 @@ function lowerExpression(
|
||||
} else {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${element.type} elements in ArrayExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: element.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1701,7 +1758,7 @@ function lowerExpression(
|
||||
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,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: calleePath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1727,7 +1784,7 @@ function lowerExpression(
|
||||
if (!calleePath.isExpression()) {
|
||||
builder.errors.push({
|
||||
reason: `Expected Expression, got ${calleePath.type} in CallExpression (v8 intrinsics not supported). This error is likely caused by a bug in React Compiler. Please file an issue`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: calleePath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1761,7 +1818,7 @@ function lowerExpression(
|
||||
if (!leftPath.isExpression()) {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Expected Expression, got ${leftPath.type} lval in BinaryExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: leftPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1773,7 +1830,7 @@ function lowerExpression(
|
||||
if (operator === '|>') {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Pipe operator not supported`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: leftPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1802,7 +1859,7 @@ function lowerExpression(
|
||||
if (last === null) {
|
||||
builder.errors.push({
|
||||
reason: `Expected sequence expression to have at least one expression`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: expr.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2014,7 +2071,7 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Unsupported syntax on the left side of an AssignmentExpression`,
|
||||
description: `Expected an LVal, got: ${left.type}`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: left.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2042,7 +2099,7 @@ function lowerExpression(
|
||||
if (binaryOperator == null) {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${operator} operators in AssignmentExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: expr.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2141,7 +2198,7 @@ function lowerExpression(
|
||||
default: {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Expected Identifier or MemberExpression, got ${expr.type} lval in AssignmentExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: expr.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2180,7 +2237,7 @@ function lowerExpression(
|
||||
if (!attribute.isJSXAttribute()) {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${attribute.type} attributes in JSXElement`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: attribute.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2193,7 +2250,7 @@ function lowerExpression(
|
||||
if (propName.indexOf(':') !== -1) {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Unexpected colon in attribute name \`${propName}\``,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: namePath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2202,7 +2259,13 @@ function lowerExpression(
|
||||
CompilerError.invariant(namePath.isJSXNamespacedName(), {
|
||||
reason: 'Refinement',
|
||||
description: null,
|
||||
loc: namePath.node.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: namePath.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
const namespace = namePath.node.namespace.name;
|
||||
@@ -2223,7 +2286,7 @@ function lowerExpression(
|
||||
if (!valueExpr.isJSXExpressionContainer()) {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${valueExpr.type} attribute values in JSXElement`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: valueExpr.node?.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2233,7 +2296,7 @@ function lowerExpression(
|
||||
if (!expression.isExpression()) {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${expression.type} expressions in JSXExpressionContainer within JSXElement`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: valueExpr.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2256,8 +2319,14 @@ function lowerExpression(
|
||||
// This is already checked in builder.resolveIdentifier
|
||||
CompilerError.invariant(tagIdentifier.kind !== 'Identifier', {
|
||||
reason: `<${tagName}> tags should be module-level imports`,
|
||||
loc: openingIdentifier.node.loc ?? GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: openingIdentifier.node.loc ?? GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
@@ -2290,8 +2359,8 @@ function lowerExpression(
|
||||
for (const [name, locations] of Object.entries(fbtLocations)) {
|
||||
if (locations.length > 1) {
|
||||
CompilerError.throwDiagnostic({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: 'Support duplicate fbt tags',
|
||||
category: ErrorCategory.Todo,
|
||||
reason: 'Support duplicate fbt tags',
|
||||
description: `Support \`<${tagName}>\` tags with multiple \`<${tagName}:${name}>\` values`,
|
||||
details: locations.map(loc => {
|
||||
return {
|
||||
@@ -2351,7 +2420,7 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason:
|
||||
'(BuildHIR::lowerExpression) Handle tagged template with interpolations',
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2361,7 +2430,13 @@ function lowerExpression(
|
||||
reason:
|
||||
"there should be only one quasi as we don't support interpolations yet",
|
||||
description: null,
|
||||
loc: expr.node.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: expr.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
const value = expr.get('quasi').get('quasis').at(0)!.node.value;
|
||||
@@ -2369,7 +2444,7 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason:
|
||||
'(BuildHIR::lowerExpression) Handle tagged template where cooked value is different from raw value',
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2391,7 +2466,7 @@ function lowerExpression(
|
||||
if (subexprs.length !== quasis.length - 1) {
|
||||
builder.errors.push({
|
||||
reason: `Unexpected quasi and subexpression lengths in template literal`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2401,7 +2476,7 @@ function lowerExpression(
|
||||
if (subexprs.some(e => !e.isExpression())) {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Handle TSType in TemplateLiteral.`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2443,7 +2518,7 @@ function lowerExpression(
|
||||
} else {
|
||||
builder.errors.push({
|
||||
reason: `Only object properties can be deleted`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: expr.node.loc ?? null,
|
||||
suggestions: [
|
||||
{
|
||||
@@ -2458,7 +2533,7 @@ function lowerExpression(
|
||||
} else if (expr.node.operator === 'throw') {
|
||||
builder.errors.push({
|
||||
reason: `Throw expressions are not supported`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: expr.node.loc ?? null,
|
||||
suggestions: [
|
||||
{
|
||||
@@ -2579,7 +2654,7 @@ function lowerExpression(
|
||||
if (!argument.isIdentifier()) {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle UpdateExpression with ${argument.type} argument`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2587,7 +2662,7 @@ function lowerExpression(
|
||||
} else if (builder.isContextIdentifier(argument)) {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle UpdateExpression to variables captured within lambdas.`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2604,10 +2679,10 @@ function lowerExpression(
|
||||
* lowerIdentifierForAssignment should have already reported an error if it returned null,
|
||||
* we check here just in case
|
||||
*/
|
||||
if (!builder.errors.hasErrors()) {
|
||||
if (!builder.errors.hasAnyErrors()) {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Found an invalid UpdateExpression without a previously reported error`,
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: ErrorCategory.Invariant,
|
||||
loc: exprLoc,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2616,7 +2691,7 @@ function lowerExpression(
|
||||
} else if (lvalue.kind === 'Global') {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Support UpdateExpression where argument is a global`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprLoc,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2671,7 +2746,7 @@ function lowerExpression(
|
||||
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle MetaProperty expressions other than import.meta`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2680,7 +2755,7 @@ function lowerExpression(
|
||||
default: {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${exprPath.type} expressions`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2759,7 +2834,13 @@ function lowerOptionalMemberExpression(
|
||||
CompilerError.invariant(object !== null, {
|
||||
reason: 'Satisfy type checker',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
@@ -2977,7 +3058,7 @@ function lowerReorderableExpression(
|
||||
if (!isReorderableExpression(builder, expr, true)) {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::node.lowerReorderableExpression) Expression type \`${expr.type}\` cannot be safely reordered`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: expr.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3000,6 +3081,12 @@ function isReorderableExpression(
|
||||
return true;
|
||||
}
|
||||
}
|
||||
case 'TSInstantiationExpression': {
|
||||
const innerExpr = (expr as NodePath<t.TSInstantiationExpression>).get(
|
||||
'expression',
|
||||
) as NodePath<t.Expression>;
|
||||
return isReorderableExpression(builder, innerExpr, allowLocalIdentifiers);
|
||||
}
|
||||
case 'RegExpLiteral':
|
||||
case 'StringLiteral':
|
||||
case 'NumericLiteral':
|
||||
@@ -3173,7 +3260,7 @@ function lowerArguments(
|
||||
} else {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${argPath.type} arguments in CallExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: argPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3208,7 +3295,7 @@ function lowerMemberExpression(
|
||||
} else {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerMemberExpression) Handle ${propertyNode.type} property`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: propertyNode.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3229,7 +3316,7 @@ function lowerMemberExpression(
|
||||
if (!propertyNode.isExpression()) {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerMemberExpression) Expected Expression, got ${propertyNode.type} property`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: propertyNode.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3288,7 +3375,7 @@ function lowerJsxElementName(
|
||||
builder.errors.push({
|
||||
reason: `Expected JSXNamespacedName to have no colons in the namespace or name`,
|
||||
description: `Got \`${namespace}\` : \`${name}\``,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3302,7 +3389,7 @@ function lowerJsxElementName(
|
||||
} else {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerJsxElementName) Handle ${exprPath.type} tags`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3327,7 +3414,13 @@ function lowerJsxMemberExpression(
|
||||
CompilerError.invariant(object.isJSXIdentifier(), {
|
||||
reason: `TypeScript refinement fail: expected 'JsxIdentifier', got \`${object.node.type}\``,
|
||||
description: null,
|
||||
loc: object.node.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: object.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
@@ -3369,7 +3462,13 @@ function lowerJsxElement(
|
||||
CompilerError.invariant(expression.isExpression(), {
|
||||
reason: `(BuildHIR::lowerJsxElement) Expected Expression but found ${expression.type}!`,
|
||||
description: null,
|
||||
loc: expression.node.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: expression.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return lowerExpressionToTemporary(builder, expression);
|
||||
@@ -3400,7 +3499,7 @@ function lowerJsxElement(
|
||||
} else {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerJsxElement) Unhandled JsxElement, got: ${exprPath.type}`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3482,17 +3581,14 @@ function lowerFunctionToValue(
|
||||
): InstructionValue {
|
||||
const exprNode = expr.node;
|
||||
const exprLoc = exprNode.loc ?? GeneratedSource;
|
||||
let name: string | null = null;
|
||||
if (expr.isFunctionExpression()) {
|
||||
name = expr.get('id')?.node?.name ?? null;
|
||||
}
|
||||
const loweredFunc = lowerFunction(builder, expr);
|
||||
if (!loweredFunc) {
|
||||
return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
|
||||
}
|
||||
return {
|
||||
kind: 'FunctionExpression',
|
||||
name,
|
||||
name: loweredFunc.func.id,
|
||||
nameHint: null,
|
||||
type: expr.node.type,
|
||||
loc: exprLoc,
|
||||
loweredFunc,
|
||||
@@ -3587,7 +3683,7 @@ function lowerIdentifier(
|
||||
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,
|
||||
category: ErrorCategory.UnsupportedSyntax,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3643,7 +3739,7 @@ function lowerIdentifierForAssignment(
|
||||
// Else its an internal error bc we couldn't find the binding
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Could not find binding for declaration.`,
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: ErrorCategory.Invariant,
|
||||
loc: path.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3655,7 +3751,7 @@ function lowerIdentifierForAssignment(
|
||||
) {
|
||||
builder.errors.push({
|
||||
reason: `Cannot reassign a \`const\` variable`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: path.node.loc ?? null,
|
||||
description:
|
||||
binding.identifier.name != null
|
||||
@@ -3712,7 +3808,7 @@ function lowerAssignment(
|
||||
if (kind === InstructionKind.Const && !isHoistedIdentifier) {
|
||||
builder.errors.push({
|
||||
reason: `Expected \`const\` declaration not to be reassigned`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: lvalue.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3726,7 +3822,7 @@ function lowerAssignment(
|
||||
) {
|
||||
builder.errors.push({
|
||||
reason: `Unexpected context variable kind`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: lvalue.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3770,7 +3866,13 @@ function lowerAssignment(
|
||||
CompilerError.invariant(kind === InstructionKind.Reassign, {
|
||||
reason: 'MemberExpression may only appear in an assignment expression',
|
||||
description: null,
|
||||
loc: lvaluePath.node.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: lvaluePath.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
const lvalue = lvaluePath as NodePath<t.MemberExpression>;
|
||||
@@ -3797,7 +3899,7 @@ function lowerAssignment(
|
||||
} else {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Handle ${property.type} properties in MemberExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: property.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3809,7 +3911,7 @@ function lowerAssignment(
|
||||
builder.errors.push({
|
||||
reason:
|
||||
'(BuildHIR::lowerAssignment) Expected private name to appear as a non-computed property',
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: property.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3874,7 +3976,7 @@ function lowerAssignment(
|
||||
continue;
|
||||
} else if (identifier.kind === 'Global') {
|
||||
builder.errors.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
reason:
|
||||
'Expected reassignment of globals to enable forceTemporaries',
|
||||
loc: element.node.loc ?? GeneratedSource,
|
||||
@@ -3913,7 +4015,7 @@ function lowerAssignment(
|
||||
continue;
|
||||
} else if (identifier.kind === 'Global') {
|
||||
builder.errors.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
reason:
|
||||
'Expected reassignment of globals to enable forceTemporaries',
|
||||
loc: element.node.loc ?? GeneratedSource,
|
||||
@@ -3986,7 +4088,7 @@ function lowerAssignment(
|
||||
if (!argument.isIdentifier()) {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Handle ${argument.node.type} rest element in ObjectPattern`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: argument.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -4017,7 +4119,7 @@ function lowerAssignment(
|
||||
continue;
|
||||
} else if (identifier.kind === 'Global') {
|
||||
builder.errors.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
reason:
|
||||
'Expected reassignment of globals to enable forceTemporaries',
|
||||
loc: property.node.loc ?? GeneratedSource,
|
||||
@@ -4034,7 +4136,7 @@ function lowerAssignment(
|
||||
if (!property.isObjectProperty()) {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Handle ${property.type} properties in ObjectPattern`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: property.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -4043,7 +4145,7 @@ function lowerAssignment(
|
||||
if (property.node.computed) {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: property.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -4057,7 +4159,7 @@ function lowerAssignment(
|
||||
if (!element.isLVal()) {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Expected object property value to be an LVal, got: ${element.type}`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: element.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -4079,7 +4181,7 @@ function lowerAssignment(
|
||||
continue;
|
||||
} else if (identifier.kind === 'Global') {
|
||||
builder.errors.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
reason:
|
||||
'Expected reassignment of globals to enable forceTemporaries',
|
||||
loc: element.node.loc ?? GeneratedSource,
|
||||
@@ -4228,7 +4330,7 @@ function lowerAssignment(
|
||||
default: {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Handle ${lvaluePath.type} assignments`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: lvaluePath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
@@ -234,7 +234,14 @@ function pushEndScopeTerminal(
|
||||
const fallthroughId = context.fallthroughs.get(scope.id);
|
||||
CompilerError.invariant(fallthroughId != null, {
|
||||
reason: 'Expected scope to exist',
|
||||
loc: GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
context.rewrites.push({
|
||||
kind: 'EndScope',
|
||||
|
||||
@@ -269,7 +269,14 @@ class PropertyPathRegistry {
|
||||
CompilerError.invariant(reactive === rootNode.fullPath.reactive, {
|
||||
reason:
|
||||
'[HoistablePropertyLoads] Found inconsistencies in `reactive` flag when deduping identifier reads within the same scope',
|
||||
loc: identifier.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: identifier.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
return rootNode;
|
||||
@@ -447,6 +454,32 @@ function collectNonNullsInBlocks(
|
||||
assumedNonNullObjects.add(entry);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
fn.env.config.enablePreserveExistingMemoizationGuarantees &&
|
||||
instr.value.kind === 'StartMemoize' &&
|
||||
instr.value.deps != null
|
||||
) {
|
||||
for (const dep of instr.value.deps) {
|
||||
if (dep.root.kind === 'NamedLocal') {
|
||||
if (
|
||||
!isImmutableAtInstr(dep.root.value.identifier, instr.id, context)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
for (let i = 0; i < dep.path.length; i++) {
|
||||
const pathEntry = dep.path[i]!;
|
||||
if (pathEntry.optional) {
|
||||
break;
|
||||
}
|
||||
const depNode = context.registry.getOrCreateProperty({
|
||||
identifier: dep.root.value.identifier,
|
||||
path: dep.path.slice(0, i),
|
||||
reactive: dep.root.value.reactive,
|
||||
});
|
||||
assumedNonNullObjects.add(depNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,7 +531,14 @@ function propagateNonNull(
|
||||
if (node == null) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Bad node ${nodeId}, kind: ${direction}`,
|
||||
loc: GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
const neighbors = Array.from(
|
||||
@@ -570,7 +610,14 @@ function propagateNonNull(
|
||||
CompilerError.invariant(i++ < 100, {
|
||||
reason:
|
||||
'[CollectHoistablePropertyLoads] fixed point iteration did not terminate after 100 loops',
|
||||
loc: GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
changed = false;
|
||||
@@ -602,7 +649,13 @@ export function assertNonNull<T extends NonNullable<U>, U>(
|
||||
CompilerError.invariant(value != null, {
|
||||
reason: 'Unexpected null',
|
||||
description: source != null ? `(from ${source})` : null,
|
||||
loc: GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -186,7 +186,13 @@ function matchOptionalTestBlock(
|
||||
reason:
|
||||
'[OptionalChainDeps] Inconsistent optional chaining property load',
|
||||
description: `Test=${printIdentifier(terminal.test.identifier)} PropertyLoad base=${printIdentifier(propertyLoad.value.object.identifier)}`,
|
||||
loc: propertyLoad.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: propertyLoad.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -194,7 +200,14 @@ function matchOptionalTestBlock(
|
||||
storeLocal.value.identifier.id === propertyLoad.lvalue.identifier.id,
|
||||
{
|
||||
reason: '[OptionalChainDeps] Unexpected storeLocal',
|
||||
loc: propertyLoad.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: propertyLoad.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
if (
|
||||
@@ -211,7 +224,14 @@ function matchOptionalTestBlock(
|
||||
alternate.instructions[1].value.kind === 'StoreLocal',
|
||||
{
|
||||
reason: 'Unexpected alternate structure',
|
||||
loc: terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -247,7 +267,14 @@ function traverseOptionalBlock(
|
||||
if (maybeTest.terminal.kind === 'branch') {
|
||||
CompilerError.invariant(optional.terminal.optional, {
|
||||
reason: '[OptionalChainDeps] Expect base case to be always optional',
|
||||
loc: optional.terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: optional.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
/**
|
||||
* Optional base expressions are currently within value blocks which cannot
|
||||
@@ -285,7 +312,14 @@ function traverseOptionalBlock(
|
||||
maybeTest.instructions.at(-1)!.lvalue.identifier.id,
|
||||
{
|
||||
reason: '[OptionalChainDeps] Unexpected test expression',
|
||||
loc: maybeTest.terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: maybeTest.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
baseObject = {
|
||||
@@ -374,7 +408,14 @@ function traverseOptionalBlock(
|
||||
reason:
|
||||
'[OptionalChainDeps] Unexpected instructions an inner optional block. ' +
|
||||
'This indicates that the compiler may be incorrectly concatenating two unrelated optional chains',
|
||||
loc: optional.terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: optional.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
const matchConsequentResult = matchOptionalTestBlock(test, context.blocks);
|
||||
@@ -387,7 +428,13 @@ function traverseOptionalBlock(
|
||||
{
|
||||
reason: '[OptionalChainDeps] Unexpected optional goto-fallthrough',
|
||||
description: `${matchConsequentResult.consequentGoto} != ${optional.terminal.fallthrough}`,
|
||||
loc: optional.terminal.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: optional.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
const load = {
|
||||
|
||||
@@ -24,7 +24,14 @@ export function computeUnconditionalBlocks(fn: HIRFunction): Set<BlockId> {
|
||||
CompilerError.invariant(!unconditionalBlocks.has(current), {
|
||||
reason:
|
||||
'Internal error: non-terminating loop in ComputeUnconditionalBlocks',
|
||||
loc: null,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
unconditionalBlocks.add(current);
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* 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 {Effect, ValueKind} from '..';
|
||||
import {TypeConfig} from './TypeSchema';
|
||||
|
||||
/**
|
||||
* Libraries developed before we officially documented the [Rules of React](https://react.dev/reference/rules)
|
||||
* implement APIs which cannot be memoized safely, either via manual or automatic memoization.
|
||||
*
|
||||
* Any non-hook API that is designed to be called during render (not events/effects) should be safe to memoize:
|
||||
*
|
||||
* ```js
|
||||
* function Component() {
|
||||
* const {someFunction} = useLibrary();
|
||||
* // it should always be safe to memoize functions like this
|
||||
* const result = useMemo(() => someFunction(), [someFunction]);
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* However, some APIs implement "interior mutability" — mutating values rather than copying into a new value
|
||||
* and setting state with the new value. Such functions (`someFunction()` in the example) could return different
|
||||
* values even though the function itself is the same object. This breaks memoization, since React relies on
|
||||
* the outer object (or function) changing if part of its value has changed.
|
||||
*
|
||||
* Given that we didn't have the Rules of React precisely documented prior to the introduction of React compiler,
|
||||
* it's understandable that some libraries accidentally shipped APIs that break this rule. However, developers
|
||||
* can easily run into pitfalls with these APIs. They may manually memoize them, which can break their app. Or
|
||||
* they may try using React Compiler, and think that the compiler has broken their code.
|
||||
*
|
||||
* To help ensure that developers can successfully use the compiler with existing code, this file teaches the
|
||||
* compiler about specific APIs that are known to be incompatible with memoization. We've tried to be as precise
|
||||
* as possible.
|
||||
*
|
||||
* The React team is open to collaborating with library authors to help develop compatible versions of these APIs,
|
||||
* and we have already reached out to the teams who own any API listed here to ensure they are aware of the issue.
|
||||
*/
|
||||
export function defaultModuleTypeProvider(
|
||||
moduleName: string,
|
||||
): TypeConfig | null {
|
||||
switch (moduleName) {
|
||||
case 'react-hook-form': {
|
||||
return {
|
||||
kind: 'object',
|
||||
properties: {
|
||||
useForm: {
|
||||
kind: 'hook',
|
||||
returnType: {
|
||||
kind: 'object',
|
||||
properties: {
|
||||
// Only the `watch()` function returned by react-hook-form's `useForm()` API is incompatible
|
||||
watch: {
|
||||
kind: 'function',
|
||||
positionalParams: [],
|
||||
restParam: Effect.Read,
|
||||
calleeEffect: Effect.Read,
|
||||
returnType: {kind: 'type', name: 'Any'},
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
knownIncompatible: `React Hook Form's \`useForm()\` API returns a \`watch()\` function which cannot be memoized safely.`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case '@tanstack/react-table': {
|
||||
return {
|
||||
kind: 'object',
|
||||
properties: {
|
||||
/*
|
||||
* Many of the properties of `useReactTable()`'s return value are incompatible, so we mark the entire hook
|
||||
* as incompatible
|
||||
*/
|
||||
useReactTable: {
|
||||
kind: 'hook',
|
||||
positionalParams: [],
|
||||
restParam: Effect.Read,
|
||||
returnType: {kind: 'type', name: 'Any'},
|
||||
knownIncompatible: `TanStack Table's \`useReactTable()\` API returns functions that cannot be memoized safely`,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case '@tanstack/react-virtual': {
|
||||
return {
|
||||
kind: 'object',
|
||||
properties: {
|
||||
/*
|
||||
* Many of the properties of `useVirtualizer()`'s return value are incompatible, so we mark the entire hook
|
||||
* as incompatible
|
||||
*/
|
||||
useVirtualizer: {
|
||||
kind: 'hook',
|
||||
positionalParams: [],
|
||||
restParam: Effect.Read,
|
||||
returnType: {kind: 'type', name: 'Any'},
|
||||
knownIncompatible: `TanStack Virtual's \`useVirtualizer()\` API returns functions that cannot be memoized safely`,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -54,7 +54,14 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
prevAccessType == null || prevAccessType === accessType,
|
||||
{
|
||||
reason: 'Conflicting access types',
|
||||
loc: GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
let nextNode = currNode.properties.get(path[i].property);
|
||||
@@ -90,7 +97,13 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
CompilerError.invariant(reactive === rootNode.reactive, {
|
||||
reason: '[DeriveMinimalDependenciesHIR] Conflicting reactive root flag',
|
||||
description: `Identifier ${printIdentifier(identifier)}`,
|
||||
loc: GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
return rootNode;
|
||||
|
||||
@@ -89,7 +89,13 @@ export class Dominator<T> {
|
||||
CompilerError.invariant(dominator !== undefined, {
|
||||
reason: 'Unknown node',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return dominator === id ? null : dominator;
|
||||
@@ -130,7 +136,13 @@ export class PostDominator<T> {
|
||||
CompilerError.invariant(dominator !== undefined, {
|
||||
reason: 'Unknown node',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return dominator === id ? null : dominator;
|
||||
@@ -175,7 +187,13 @@ function computeImmediateDominators<T>(graph: Graph<T>): Map<T, T> {
|
||||
CompilerError.invariant(newIdom !== null, {
|
||||
reason: `At least one predecessor must have been visited for block ${id}`,
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
*/
|
||||
|
||||
import * as t from '@babel/types';
|
||||
import {ZodError, z} from 'zod';
|
||||
import {fromZodError} from 'zod-validation-error';
|
||||
import {ZodError, z} from 'zod/v4';
|
||||
import {fromZodError} from 'zod-validation-error/v4';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {Logger, ProgramContext} from '../Entrypoint';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
import {Scope as BabelScope, NodePath} from '@babel/traverse';
|
||||
import {TypeSchema} from './TypeSchema';
|
||||
import {FlowTypeEnv} from '../Flood/Types';
|
||||
import {defaultModuleTypeProvider} from './DefaultModuleTypeProvider';
|
||||
|
||||
export const ReactElementSymbolSchema = z.object({
|
||||
elementSymbol: z.union([
|
||||
@@ -82,21 +83,11 @@ export type ExternalFunction = z.infer<typeof ExternalFunctionSchema>;
|
||||
export const USE_FIRE_FUNCTION_NAME = 'useFire';
|
||||
export const EMIT_FREEZE_GLOBAL_GATING = '__DEV__';
|
||||
|
||||
export const MacroMethodSchema = z.union([
|
||||
z.object({type: z.literal('wildcard')}),
|
||||
z.object({type: z.literal('name'), name: z.string()}),
|
||||
]);
|
||||
|
||||
// Would like to change this to drop the string option, but breaks compatibility with existing configs
|
||||
export const MacroSchema = z.union([
|
||||
z.string(),
|
||||
z.tuple([z.string(), z.array(MacroMethodSchema)]),
|
||||
]);
|
||||
export const MacroSchema = z.string();
|
||||
|
||||
export type CompilerMode = 'all_features' | 'no_inferred_memo';
|
||||
|
||||
export type Macro = z.infer<typeof MacroSchema>;
|
||||
export type MacroMethod = z.infer<typeof MacroMethodSchema>;
|
||||
|
||||
const HookSchema = z.object({
|
||||
/*
|
||||
@@ -158,7 +149,7 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* A function that, given the name of a module, can optionally return a description
|
||||
* of that module's type signature.
|
||||
*/
|
||||
moduleTypeProvider: z.nullable(z.function().args(z.string())).default(null),
|
||||
moduleTypeProvider: z.nullable(z.any()).default(null),
|
||||
|
||||
/**
|
||||
* A list of functions which the application compiles as macros, where
|
||||
@@ -209,7 +200,7 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* that if a useEffect or useCallback references a function value, that function value will be
|
||||
* considered frozen, and in turn all of its referenced variables will be considered frozen as well.
|
||||
*/
|
||||
enablePreserveExistingMemoizationGuarantees: z.boolean().default(false),
|
||||
enablePreserveExistingMemoizationGuarantees: z.boolean().default(true),
|
||||
|
||||
/**
|
||||
* Validates that all useMemo/useCallback values are also memoized by Forget. This mode can be
|
||||
@@ -248,12 +239,7 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* 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
|
||||
*/
|
||||
enableNewMutationAliasingModel: z.boolean().default(true),
|
||||
flowTypeProvider: z.nullable(z.any()).default(null),
|
||||
|
||||
/**
|
||||
* Enables inference of optional dependency chains. Without this flag
|
||||
@@ -265,6 +251,8 @@ export const EnvironmentConfigSchema = z.object({
|
||||
|
||||
enableFire: z.boolean().default(false),
|
||||
|
||||
enableNameAnonymousFunctions: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Enables inference and auto-insertion of effect dependencies. Takes in an array of
|
||||
* configurable module and import pairs to allow for user-land experimentation. For example,
|
||||
@@ -623,6 +611,13 @@ export const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(true),
|
||||
|
||||
/**
|
||||
* Treat identifiers as SetState type if both
|
||||
* - they are named with a "set-" prefix
|
||||
* - they are called somewhere
|
||||
*/
|
||||
enableTreatSetIdentifiersAsStateSetters: z.boolean().default(false),
|
||||
|
||||
/*
|
||||
* If specified a value, the compiler lowers any calls to `useContext` to use
|
||||
* this value as the callee.
|
||||
@@ -654,7 +649,21 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* Invalid:
|
||||
* useMemo(() => { ... }, [...]);
|
||||
*/
|
||||
validateNoVoidUseMemo: z.boolean().default(false),
|
||||
validateNoVoidUseMemo: z.boolean().default(true),
|
||||
|
||||
/**
|
||||
* Validates that Components/Hooks are always defined at module level. This prevents scope
|
||||
* reference errors that occur when the compiler attempts to optimize the nested component/hook
|
||||
* while its parent function remains uncompiled.
|
||||
*/
|
||||
validateNoDynamicallyCreatedComponentsOrHooks: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* When enabled, allows setState calls in effects when the value being set is
|
||||
* derived from a ref. This is useful for patterns where initial layout measurements
|
||||
* from refs need to be stored in state during mount.
|
||||
*/
|
||||
enableAllowSetStateFromRefsInEffects: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
|
||||
@@ -747,7 +756,13 @@ export class Environment {
|
||||
CompilerError.invariant(!this.#globals.has(hookName), {
|
||||
reason: `[Globals] Found existing definition in global registry for custom hook ${hookName}`,
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
this.#globals.set(
|
||||
@@ -780,7 +795,14 @@ export class Environment {
|
||||
CompilerError.invariant(code != null, {
|
||||
reason:
|
||||
'Expected Environment to be initialized with source code when a Flow type provider is specified',
|
||||
loc: null,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
this.#flowTypeEnvironment.init(this, code);
|
||||
} else {
|
||||
@@ -791,7 +813,14 @@ export class Environment {
|
||||
get typeContext(): FlowTypeEnv {
|
||||
CompilerError.invariant(this.#flowTypeEnvironment != null, {
|
||||
reason: 'Flow type environment not initialized',
|
||||
loc: null,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
return this.#flowTypeEnvironment;
|
||||
}
|
||||
@@ -858,10 +887,22 @@ export class Environment {
|
||||
#resolveModuleType(moduleName: string, loc: SourceLocation): Global | null {
|
||||
let moduleType = this.#moduleTypes.get(moduleName);
|
||||
if (moduleType === undefined) {
|
||||
if (this.config.moduleTypeProvider == null) {
|
||||
/*
|
||||
* NOTE: Zod doesn't work when specifying a function as a default, so we have to
|
||||
* fallback to the default value here
|
||||
*/
|
||||
const moduleTypeProvider =
|
||||
this.config.moduleTypeProvider ?? defaultModuleTypeProvider;
|
||||
if (moduleTypeProvider == null) {
|
||||
return null;
|
||||
}
|
||||
const unparsedModuleConfig = this.config.moduleTypeProvider(moduleName);
|
||||
if (typeof moduleTypeProvider !== 'function') {
|
||||
CompilerError.throwInvalidConfig({
|
||||
reason: `Expected a function for \`moduleTypeProvider\``,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
const unparsedModuleConfig = moduleTypeProvider(moduleName);
|
||||
if (unparsedModuleConfig != null) {
|
||||
const parsedModuleConfig = TypeSchema.safeParse(unparsedModuleConfig);
|
||||
if (!parsedModuleConfig.success) {
|
||||
@@ -1035,7 +1076,13 @@ export class Environment {
|
||||
CompilerError.invariant(shape !== undefined, {
|
||||
reason: `[HIR] Forget internal error: cannot resolve shape ${shapeId}`,
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return shape.properties.get('*') ?? null;
|
||||
@@ -1060,7 +1107,13 @@ export class Environment {
|
||||
CompilerError.invariant(shape !== undefined, {
|
||||
reason: `[HIR] Forget internal error: cannot resolve shape ${shapeId}`,
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
if (typeof property === 'string') {
|
||||
@@ -1085,7 +1138,13 @@ export class Environment {
|
||||
CompilerError.invariant(shape !== undefined, {
|
||||
reason: `[HIR] Forget internal error: cannot resolve shape ${shapeId}`,
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return shape.functionType;
|
||||
|
||||
@@ -184,7 +184,13 @@ function handleAssignment(
|
||||
CompilerError.invariant(valuePath.isLVal(), {
|
||||
reason: `[FindContextIdentifiers] Expected object property value to be an LVal, got: ${valuePath.type}`,
|
||||
description: null,
|
||||
loc: valuePath.node.loc ?? GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: valuePath.node.loc ?? GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
handleAssignment(currentFn, identifiers, valuePath);
|
||||
@@ -192,7 +198,13 @@ function handleAssignment(
|
||||
CompilerError.invariant(property.isRestElement(), {
|
||||
reason: `[FindContextIdentifiers] Invalid assumptions for babel types.`,
|
||||
description: null,
|
||||
loc: property.node.loc ?? GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: property.node.loc ?? GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
handleAssignment(currentFn, identifiers, property);
|
||||
|
||||
@@ -1001,6 +1001,7 @@ export function installTypeConfig(
|
||||
mutableOnlyIfOperandsAreMutable:
|
||||
typeConfig.mutableOnlyIfOperandsAreMutable === true,
|
||||
aliasing: typeConfig.aliasing,
|
||||
knownIncompatible: typeConfig.knownIncompatible ?? null,
|
||||
});
|
||||
}
|
||||
case 'hook': {
|
||||
@@ -1019,6 +1020,7 @@ export function installTypeConfig(
|
||||
returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen,
|
||||
noAlias: typeConfig.noAlias === true,
|
||||
aliasing: typeConfig.aliasing,
|
||||
knownIncompatible: typeConfig.knownIncompatible ?? null,
|
||||
});
|
||||
}
|
||||
case 'object': {
|
||||
|
||||
@@ -7,14 +7,19 @@
|
||||
|
||||
import {BindingKind} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {Environment, ReactFunctionType} from './Environment';
|
||||
import type {HookKind} from './ObjectShape';
|
||||
import {Type, makeType} from './Types';
|
||||
import {z} from 'zod';
|
||||
import {z} from 'zod/v4';
|
||||
import type {AliasingEffect} from '../Inference/AliasingEffects';
|
||||
import {isReservedWord} from '../Utils/Keyword';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
|
||||
/*
|
||||
* *******************************************************************************************
|
||||
@@ -53,7 +58,8 @@ export type SourceLocation = t.SourceLocation | typeof GeneratedSource;
|
||||
*/
|
||||
export type ReactiveFunction = {
|
||||
loc: SourceLocation;
|
||||
id: string | null;
|
||||
id: ValidIdentifierName | null;
|
||||
nameHint: string | null;
|
||||
params: Array<Place | SpreadPattern>;
|
||||
generator: boolean;
|
||||
async: boolean;
|
||||
@@ -275,37 +281,21 @@ export type ReactiveTryTerminal = {
|
||||
// A function lowered to HIR form, ie where its body is lowered to an HIR control-flow graph
|
||||
export type HIRFunction = {
|
||||
loc: SourceLocation;
|
||||
id: string | null;
|
||||
id: ValidIdentifierName | null;
|
||||
nameHint: string | null;
|
||||
fnType: ReactFunctionType;
|
||||
env: Environment;
|
||||
params: Array<Place | SpreadPattern>;
|
||||
returnTypeAnnotation: t.FlowType | t.TSType | null;
|
||||
returns: Place;
|
||||
context: Array<Place>;
|
||||
effects: Array<FunctionEffect> | null;
|
||||
body: HIR;
|
||||
generator: boolean;
|
||||
async: boolean;
|
||||
directives: Array<string>;
|
||||
aliasingEffects?: Array<AliasingEffect> | null;
|
||||
aliasingEffects: Array<AliasingEffect> | null;
|
||||
};
|
||||
|
||||
export type FunctionEffect =
|
||||
| {
|
||||
kind: 'GlobalMutation';
|
||||
error: CompilerErrorDetailOptions;
|
||||
}
|
||||
| {
|
||||
kind: 'ReactMutation';
|
||||
error: CompilerErrorDetailOptions;
|
||||
}
|
||||
| {
|
||||
kind: 'ContextMutation';
|
||||
places: ReadonlySet<Place>;
|
||||
effect: Effect;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
|
||||
/*
|
||||
* Each reactive scope may have its own control-flow, so the instructions form
|
||||
* a control-flow graph. The graph comprises a set of basic blocks which reference
|
||||
@@ -1140,7 +1130,8 @@ export type JsxAttribute =
|
||||
|
||||
export type FunctionExpression = {
|
||||
kind: 'FunctionExpression';
|
||||
name: string | null;
|
||||
name: ValidIdentifierName | null;
|
||||
nameHint: string | null;
|
||||
loweredFunc: LoweredFunction;
|
||||
type:
|
||||
| 'ArrowFunctionExpression'
|
||||
@@ -1315,31 +1306,52 @@ export function forkTemporaryIdentifier(
|
||||
};
|
||||
}
|
||||
|
||||
export function validateIdentifierName(
|
||||
name: string,
|
||||
): Result<ValidatedIdentifier, CompilerError> {
|
||||
if (isReservedWord(name)) {
|
||||
const error = new CompilerError();
|
||||
error.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Syntax,
|
||||
reason: 'Expected a non-reserved identifier name',
|
||||
description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: 'reserved word',
|
||||
}),
|
||||
);
|
||||
return Err(error);
|
||||
} else if (!t.isValidIdentifier(name)) {
|
||||
const error = new CompilerError();
|
||||
error.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Syntax,
|
||||
reason: `Expected a valid identifier name`,
|
||||
description: `\`${name}\` is not a valid JavaScript identifier`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: 'reserved word',
|
||||
}),
|
||||
);
|
||||
}
|
||||
return Ok({
|
||||
kind: 'named',
|
||||
value: name as ValidIdentifierName,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a valid identifier name. This should *not* be used for synthesizing
|
||||
* identifier names: only call this method for identifier names that appear in the
|
||||
* original source code.
|
||||
*/
|
||||
export function makeIdentifierName(name: string): ValidatedIdentifier {
|
||||
if (isReservedWord(name)) {
|
||||
CompilerError.throwInvalidJS({
|
||||
reason: 'Expected a non-reserved identifier name',
|
||||
loc: GeneratedSource,
|
||||
description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`,
|
||||
suggestions: null,
|
||||
});
|
||||
} else {
|
||||
CompilerError.invariant(t.isValidIdentifier(name), {
|
||||
reason: `Expected a valid identifier name`,
|
||||
loc: GeneratedSource,
|
||||
description: `\`${name}\` is not a valid JavaScript identifier`,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
return {
|
||||
kind: 'named',
|
||||
value: name as ValidIdentifierName,
|
||||
};
|
||||
return validateIdentifierName(name).unwrap();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1351,8 +1363,14 @@ export function makeIdentifierName(name: string): ValidatedIdentifier {
|
||||
export function promoteTemporary(identifier: Identifier): void {
|
||||
CompilerError.invariant(identifier.name === null, {
|
||||
reason: `Expected a temporary (unnamed) identifier`,
|
||||
loc: GeneratedSource,
|
||||
description: `Identifier already has a name, \`${identifier.name}\``,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
identifier.name = {
|
||||
@@ -1375,8 +1393,14 @@ export function isPromotedTemporary(name: string): boolean {
|
||||
export function promoteTemporaryJsxTag(identifier: Identifier): void {
|
||||
CompilerError.invariant(identifier.name === null, {
|
||||
reason: `Expected a temporary (unnamed) identifier`,
|
||||
loc: GeneratedSource,
|
||||
description: `Identifier already has a name, \`${identifier.name}\``,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
identifier.name = {
|
||||
@@ -1544,7 +1568,13 @@ export function isMutableEffect(
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected unknown effect',
|
||||
description: null,
|
||||
loc: location,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: location,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
@@ -1677,7 +1707,13 @@ export function makeBlockId(id: number): BlockId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected block id to be a non-negative integer',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return id as BlockId;
|
||||
@@ -1694,7 +1730,13 @@ export function makeScopeId(id: number): ScopeId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected block id to be a non-negative integer',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return id as ScopeId;
|
||||
@@ -1711,7 +1753,13 @@ export function makeIdentifierId(id: number): IdentifierId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected identifier id to be a non-negative integer',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return id as IdentifierId;
|
||||
@@ -1728,7 +1776,13 @@ export function makeDeclarationId(id: number): DeclarationId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected declaration id to be a non-negative integer',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return id as DeclarationId;
|
||||
@@ -1745,7 +1799,13 @@ export function makeInstructionId(id: number): InstructionId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected instruction id to be a non-negative integer',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return id as InstructionId;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import {Binding, NodePath} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import {CompilerError, ErrorSeverity} from '../CompilerError';
|
||||
import {CompilerError, ErrorCategory} from '../CompilerError';
|
||||
import {Environment} from './Environment';
|
||||
import {
|
||||
BasicBlock,
|
||||
@@ -309,8 +309,8 @@ export default class HIRBuilder {
|
||||
resolveBinding(node: t.Identifier): Identifier {
|
||||
if (node.name === 'fbt') {
|
||||
CompilerError.throwDiagnostic({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: 'Support local variables named `fbt`',
|
||||
category: ErrorCategory.Todo,
|
||||
reason: 'Support local variables named `fbt`',
|
||||
description:
|
||||
'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',
|
||||
details: [
|
||||
@@ -322,6 +322,21 @@ export default class HIRBuilder {
|
||||
],
|
||||
});
|
||||
}
|
||||
if (node.name === 'this') {
|
||||
CompilerError.throwDiagnostic({
|
||||
category: ErrorCategory.UnsupportedSyntax,
|
||||
reason: '`this` is not supported syntax',
|
||||
description:
|
||||
'React Compiler does not support compiling functions that use `this`',
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
message: '`this` was used here',
|
||||
loc: node.loc ?? GeneratedSource,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
const originalName = node.name;
|
||||
let name = originalName;
|
||||
let index = 0;
|
||||
@@ -492,7 +507,13 @@ export default class HIRBuilder {
|
||||
{
|
||||
reason: 'Mismatched label',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
@@ -515,7 +536,13 @@ export default class HIRBuilder {
|
||||
{
|
||||
reason: 'Mismatched label',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
@@ -551,7 +578,13 @@ export default class HIRBuilder {
|
||||
{
|
||||
reason: 'Mismatched loops',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
@@ -576,7 +609,13 @@ export default class HIRBuilder {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Expected a loop or switch to be in scope',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
@@ -597,7 +636,13 @@ export default class HIRBuilder {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Continue may only refer to a labeled loop',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
@@ -605,7 +650,13 @@ export default class HIRBuilder {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Expected a loop to be in scope',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
@@ -628,7 +679,13 @@ function _shrink(func: HIR): void {
|
||||
CompilerError.invariant(block != null, {
|
||||
reason: `expected block ${blockId} to exist`,
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
target = getTargetIfIndirection(block);
|
||||
@@ -760,7 +817,13 @@ function getReversePostorderedBlocks(func: HIR): HIR['blocks'] {
|
||||
CompilerError.invariant(block != null, {
|
||||
reason: '[HIRBuilder] Unexpected null block',
|
||||
description: `expected block ${blockId} to exist`,
|
||||
loc: GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
const successors = [...eachTerminalSuccessor(block.terminal)].reverse();
|
||||
const fallthrough = terminalFallthrough(block.terminal);
|
||||
@@ -816,7 +879,13 @@ export function markInstructionIds(func: HIR): void {
|
||||
CompilerError.invariant(!visited.has(instr), {
|
||||
reason: `${printInstruction(instr)} already visited!`,
|
||||
description: null,
|
||||
loc: instr.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instr.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
visited.add(instr);
|
||||
@@ -839,7 +908,13 @@ export function markPredecessors(func: HIR): void {
|
||||
CompilerError.invariant(block != null, {
|
||||
reason: 'unexpected missing block',
|
||||
description: `block ${blockId}`,
|
||||
loc: GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
if (prevBlock) {
|
||||
block.preds.add(prevBlock.id);
|
||||
|
||||
@@ -61,7 +61,13 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
|
||||
CompilerError.invariant(predecessor !== undefined, {
|
||||
reason: `Expected predecessor ${predecessorId} to exist`,
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
if (predecessor.terminal.kind !== 'goto' || predecessor.kind !== 'block') {
|
||||
@@ -77,7 +83,13 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
|
||||
CompilerError.invariant(phi.operands.size === 1, {
|
||||
reason: `Found a block with a single predecessor but where a phi has multiple (${phi.operands.size}) operands`,
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
const operand = Array.from(phi.operands.values())[0]!;
|
||||
|
||||
@@ -119,7 +119,13 @@ function parseAliasingSignatureConfig(
|
||||
CompilerError.invariant(!lifetimes.has(temp), {
|
||||
reason: `Invalid type configuration for module`,
|
||||
description: `Expected aliasing signature to have unique names for receiver, params, rest, returns, and temporaries in module '${moduleName}'`,
|
||||
loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
const place = signatureArgument(lifetimes.size);
|
||||
lifetimes.set(temp, place);
|
||||
@@ -130,7 +136,13 @@ function parseAliasingSignatureConfig(
|
||||
CompilerError.invariant(place != null, {
|
||||
reason: `Invalid type configuration for module`,
|
||||
description: `Expected aliasing signature effects to reference known names from receiver/params/rest/returns/temporaries, but '${temp}' is not a known name in '${moduleName}'`,
|
||||
loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
return place;
|
||||
}
|
||||
@@ -265,7 +277,13 @@ function addShape(
|
||||
CompilerError.invariant(!registry.has(id), {
|
||||
reason: `[ObjectShape] Could not add shape to registry: name ${id} already exists.`,
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
registry.set(id, shape);
|
||||
@@ -332,6 +350,7 @@ export type FunctionSignature = {
|
||||
mutableOnlyIfOperandsAreMutable?: boolean;
|
||||
|
||||
impure?: boolean;
|
||||
knownIncompatible?: string | null | undefined;
|
||||
|
||||
canonicalName?: string;
|
||||
|
||||
|
||||
@@ -56,6 +56,9 @@ export function printFunction(fn: HIRFunction): string {
|
||||
} else {
|
||||
definition += '<<anonymous>>';
|
||||
}
|
||||
if (fn.nameHint != null) {
|
||||
definition += ` ${fn.nameHint}`;
|
||||
}
|
||||
if (fn.params.length !== 0) {
|
||||
definition +=
|
||||
'(' +
|
||||
@@ -554,23 +557,11 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
|
||||
const context = instrValue.loweredFunc.func.context
|
||||
.map(dep => printPlace(dep))
|
||||
.join(',');
|
||||
const effects =
|
||||
instrValue.loweredFunc.func.effects
|
||||
?.map(effect => {
|
||||
if (effect.kind === 'ContextMutation') {
|
||||
return `ContextMutation places=[${[...effect.places]
|
||||
.map(place => printPlace(place))
|
||||
.join(', ')}] effect=${effect.effect}`;
|
||||
} else {
|
||||
return `GlobalMutation`;
|
||||
}
|
||||
})
|
||||
.join(', ') ?? '';
|
||||
const aliasingEffects =
|
||||
instrValue.loweredFunc.func.aliasingEffects
|
||||
?.map(printAliasingEffect)
|
||||
?.join(', ') ?? '';
|
||||
value = `${kind} ${name} @context[${context}] @effects[${effects}] @aliasingEffects=[${aliasingEffects}]\n${fn}`;
|
||||
value = `${kind} ${name} @context[${context}] @aliasingEffects=[${aliasingEffects}]\n${fn}`;
|
||||
break;
|
||||
}
|
||||
case 'TaggedTemplateExpression': {
|
||||
@@ -608,7 +599,13 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
|
||||
{
|
||||
reason: 'Bad assumption about quasi length.',
|
||||
description: null,
|
||||
loc: instrValue.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instrValue.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
@@ -877,8 +874,15 @@ export function printManualMemoDependency(
|
||||
} else {
|
||||
CompilerError.invariant(val.root.value.identifier.name?.kind === 'named', {
|
||||
reason: 'DepsValidation: expected named local variable in depslist',
|
||||
description: null,
|
||||
suggestions: null,
|
||||
loc: val.root.value.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: val.root.value.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
rootStr = nameOnly
|
||||
? val.root.value.identifier.name.value
|
||||
@@ -892,7 +896,8 @@ export function printType(type: Type): string {
|
||||
if (type.kind === 'Object' && type.shapeId != null) {
|
||||
return `:T${type.kind}<${type.shapeId}>`;
|
||||
} else if (type.kind === 'Function' && type.shapeId != null) {
|
||||
return `:T${type.kind}<${type.shapeId}>`;
|
||||
const returnType = printType(type.return);
|
||||
return `:T${type.kind}<${type.shapeId}>()${returnType !== '' ? `: ${returnType}` : ''}`;
|
||||
} else {
|
||||
return `:T${type.kind}`;
|
||||
}
|
||||
@@ -995,16 +1000,16 @@ export function printAliasingEffect(effect: AliasingEffect): string {
|
||||
case 'MutateConditionally':
|
||||
case 'MutateTransitive':
|
||||
case 'MutateTransitiveConditionally': {
|
||||
return `${effect.kind} ${printPlaceForAliasEffect(effect.value)}`;
|
||||
return `${effect.kind} ${printPlaceForAliasEffect(effect.value)}${effect.kind === 'Mutate' && effect.reason?.kind === 'AssignCurrentProperty' ? ' (assign `.current`)' : ''}`;
|
||||
}
|
||||
case 'MutateFrozen': {
|
||||
return `MutateFrozen ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.category)}`;
|
||||
return `MutateFrozen ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
|
||||
}
|
||||
case 'MutateGlobal': {
|
||||
return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.category)}`;
|
||||
return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
|
||||
}
|
||||
case 'Impure': {
|
||||
return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.category)}`;
|
||||
return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
|
||||
}
|
||||
case 'Render': {
|
||||
return `Render ${printPlaceForAliasEffect(effect.place)}`;
|
||||
|
||||
@@ -86,7 +86,14 @@ export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
|
||||
const hoistables = hoistablePropertyLoads.get(scope.id);
|
||||
CompilerError.invariant(hoistables != null, {
|
||||
reason: '[PropagateScopeDependencies] Scope not found in tracked blocks',
|
||||
loc: GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
/**
|
||||
* Step 2: Calculate hoistable dependencies.
|
||||
@@ -428,7 +435,14 @@ export class DependencyCollectionContext {
|
||||
const scopedDependencies = this.#dependencies.value;
|
||||
CompilerError.invariant(scopedDependencies != null, {
|
||||
reason: '[PropagateScopeDeps]: Unexpected scope mismatch',
|
||||
loc: scope.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: scope.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Restore context of previous scope
|
||||
|
||||
@@ -53,7 +53,14 @@ export function pruneUnusedLabelsHIR(fn: HIRFunction): void {
|
||||
next.phis.size === 0 && fallthrough.phis.size === 0,
|
||||
{
|
||||
reason: 'Unexpected phis when merging label blocks',
|
||||
loc: label.terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: label.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -64,7 +71,14 @@ export function pruneUnusedLabelsHIR(fn: HIRFunction): void {
|
||||
fallthrough.preds.has(nextId),
|
||||
{
|
||||
reason: 'Unexpected block predecessors when merging label blocks',
|
||||
loc: label.terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: label.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -202,8 +202,14 @@ function writeOptionalDependency(
|
||||
CompilerError.invariant(firstOptional !== -1, {
|
||||
reason:
|
||||
'[ScopeDependencyUtils] Internal invariant broken: expected optional path',
|
||||
loc: dep.identifier.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: dep.identifier.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
if (firstOptional === dep.path.length - 1) {
|
||||
@@ -239,7 +245,13 @@ function writeOptionalDependency(
|
||||
CompilerError.invariant(testIdentifier !== null, {
|
||||
reason: 'Satisfy type checker',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import {isValidIdentifier} from '@babel/types';
|
||||
import {z} from 'zod';
|
||||
import {z} from 'zod/v4';
|
||||
import {Effect, ValueKind} from '..';
|
||||
import {
|
||||
EffectSchema,
|
||||
@@ -251,6 +251,7 @@ export type FunctionTypeConfig = {
|
||||
impure?: boolean | null | undefined;
|
||||
canonicalName?: string | null | undefined;
|
||||
aliasing?: AliasingSignatureConfig | null | undefined;
|
||||
knownIncompatible?: string | null | undefined;
|
||||
};
|
||||
export const FunctionTypeSchema: z.ZodType<FunctionTypeConfig> = z.object({
|
||||
kind: z.literal('function'),
|
||||
@@ -264,6 +265,7 @@ export const FunctionTypeSchema: z.ZodType<FunctionTypeConfig> = z.object({
|
||||
impure: z.boolean().nullable().optional(),
|
||||
canonicalName: z.string().nullable().optional(),
|
||||
aliasing: AliasingSignatureSchema.nullable().optional(),
|
||||
knownIncompatible: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export type HookTypeConfig = {
|
||||
@@ -274,6 +276,7 @@ export type HookTypeConfig = {
|
||||
returnValueKind?: ValueKind | null | undefined;
|
||||
noAlias?: boolean | null | undefined;
|
||||
aliasing?: AliasingSignatureConfig | null | undefined;
|
||||
knownIncompatible?: string | null | undefined;
|
||||
};
|
||||
export const HookTypeSchema: z.ZodType<HookTypeConfig> = z.object({
|
||||
kind: z.literal('hook'),
|
||||
@@ -283,6 +286,7 @@ export const HookTypeSchema: z.ZodType<HookTypeConfig> = z.object({
|
||||
returnValueKind: ValueKindSchema.nullable().optional(),
|
||||
noAlias: z.boolean().nullable().optional(),
|
||||
aliasing: AliasingSignatureSchema.nullable().optional(),
|
||||
knownIncompatible: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export type BuiltInTypeConfig =
|
||||
|
||||
@@ -87,7 +87,13 @@ export function makeTypeId(id: number): TypeId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected instruction id to be a non-negative integer',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return id as TypeId;
|
||||
|
||||
@@ -1233,7 +1233,14 @@ export class ScopeBlockTraversal {
|
||||
CompilerError.invariant(blockInfo.scope.id === top, {
|
||||
reason:
|
||||
'Expected traversed block fallthrough to match top-most active scope',
|
||||
loc: block.instructions[0]?.loc ?? block.terminal.id,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: block.instructions[0]?.loc ?? block.terminal.id,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
this.#activeScopes.pop();
|
||||
}
|
||||
@@ -1247,7 +1254,14 @@ export class ScopeBlockTraversal {
|
||||
!this.blockInfos.has(block.terminal.fallthrough),
|
||||
{
|
||||
reason: 'Expected unique scope blocks and fallthroughs',
|
||||
loc: block.terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: block.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
this.blockInfos.set(block.terminal.block, {
|
||||
|
||||
@@ -231,7 +231,7 @@ export function hashEffect(effect: AliasingEffect): string {
|
||||
effect.kind,
|
||||
effect.place.identifier.id,
|
||||
effect.error.severity,
|
||||
effect.error.category,
|
||||
effect.error.reason,
|
||||
effect.error.description,
|
||||
printSourceLocation(effect.error.primaryLocation() ?? GeneratedSource),
|
||||
].join(':');
|
||||
|
||||
@@ -6,20 +6,10 @@
|
||||
*/
|
||||
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
LoweredFunction,
|
||||
isRefOrRefValue,
|
||||
makeInstructionId,
|
||||
} from '../HIR';
|
||||
import {Effect, HIRFunction, IdentifierId, makeInstructionId} from '../HIR';
|
||||
import {deadCodeElimination} from '../Optimization';
|
||||
import {inferReactiveScopeVariables} from '../ReactiveScopes';
|
||||
import {rewriteInstructionKindsBasedOnReassignment} from '../SSA';
|
||||
import {inferMutableRanges} from './InferMutableRanges';
|
||||
import inferReferenceEffects from './InferReferenceEffects';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {inferMutationAliasingEffects} from './InferMutationAliasingEffects';
|
||||
import {inferMutationAliasingRanges} from './InferMutationAliasingRanges';
|
||||
@@ -30,12 +20,7 @@ export default function analyseFunctions(func: HIRFunction): void {
|
||||
switch (instr.value.kind) {
|
||||
case 'ObjectMethod':
|
||||
case 'FunctionExpression': {
|
||||
if (!func.env.config.enableNewMutationAliasingModel) {
|
||||
lower(instr.value.loweredFunc.func);
|
||||
infer(instr.value.loweredFunc);
|
||||
} else {
|
||||
lowerWithMutationAliasing(instr.value.loweredFunc.func);
|
||||
}
|
||||
lowerWithMutationAliasing(instr.value.loweredFunc.func);
|
||||
|
||||
/**
|
||||
* Reset mutable range for outer inferReferenceEffects
|
||||
@@ -93,7 +78,14 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
|
||||
case 'Apply': {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,
|
||||
loc: effect.function.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: effect.function.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
case 'Mutate':
|
||||
@@ -140,58 +132,3 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
|
||||
value: fn,
|
||||
});
|
||||
}
|
||||
|
||||
function lower(func: HIRFunction): void {
|
||||
analyseFunctions(func);
|
||||
inferReferenceEffects(func, {isFunctionExpression: true});
|
||||
deadCodeElimination(func);
|
||||
inferMutableRanges(func);
|
||||
rewriteInstructionKindsBasedOnReassignment(func);
|
||||
inferReactiveScopeVariables(func);
|
||||
func.env.logger?.debugLogIRs?.({
|
||||
kind: 'hir',
|
||||
name: 'AnalyseFunction (inner)',
|
||||
value: func,
|
||||
});
|
||||
}
|
||||
|
||||
function infer(loweredFunc: LoweredFunction): void {
|
||||
for (const operand of loweredFunc.func.context) {
|
||||
const identifier = operand.identifier;
|
||||
CompilerError.invariant(operand.effect === Effect.Unknown, {
|
||||
reason:
|
||||
'[AnalyseFunctions] Expected Function context effects to not have been set',
|
||||
loc: operand.loc,
|
||||
});
|
||||
if (isRefOrRefValue(identifier)) {
|
||||
/*
|
||||
* TODO: this is a hack to ensure we treat functions which reference refs
|
||||
* as having a capture and therefore being considered mutable. this ensures
|
||||
* the function gets a mutable range which accounts for anywhere that it
|
||||
* could be called, and allows us to help ensure it isn't called during
|
||||
* render
|
||||
*/
|
||||
operand.effect = Effect.Capture;
|
||||
} else if (isMutatedOrReassigned(identifier)) {
|
||||
/**
|
||||
* Reflects direct reassignments, PropertyStores, and ConditionallyMutate
|
||||
* (directly or through maybe-aliases)
|
||||
*/
|
||||
operand.effect = Effect.Capture;
|
||||
} else {
|
||||
operand.effect = Effect.Read;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isMutatedOrReassigned(id: Identifier): boolean {
|
||||
/*
|
||||
* This check checks for mutation and reassingnment, so the usual check for
|
||||
* mutation (ie, `mutableRange.end - mutableRange.start > 1`) isn't quite
|
||||
* enough.
|
||||
*
|
||||
* We need to track re-assignments in context refs as we need to reflect the
|
||||
* re-assignment back to the captured refs.
|
||||
*/
|
||||
return id.mutableRange.end > id.mutableRange.start;
|
||||
}
|
||||
|
||||
@@ -5,12 +5,8 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorSeverity,
|
||||
SourceLocation,
|
||||
} from '..';
|
||||
import {CompilerDiagnostic, CompilerError, SourceLocation} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
CallExpression,
|
||||
Effect,
|
||||
@@ -300,11 +296,11 @@ function extractManualMemoizationArgs(
|
||||
if (fnPlace == null) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: `Expected a callback function to be passed to ${kind}`,
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: `Expected a callback function to be passed to ${kind}`,
|
||||
description: `Expected a callback function to be passed to ${kind}`,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: instr.value.loc,
|
||||
message: `Expected a callback function to be passed to ${kind}`,
|
||||
@@ -315,11 +311,11 @@ function extractManualMemoizationArgs(
|
||||
if (fnPlace.kind === 'Spread' || depsListPlace?.kind === 'Spread') {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: `Unexpected spread argument to ${kind}`,
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: `Unexpected spread argument to ${kind}`,
|
||||
description: `Unexpected spread argument to ${kind}`,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: instr.value.loc,
|
||||
message: `Unexpected spread argument to ${kind}`,
|
||||
@@ -335,11 +331,11 @@ function extractManualMemoizationArgs(
|
||||
if (maybeDepsList == null) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
description: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: depsListPlace.loc,
|
||||
message: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
@@ -353,11 +349,11 @@ function extractManualMemoizationArgs(
|
||||
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\`)`,
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
|
||||
description: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: dep.loc,
|
||||
message: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
|
||||
@@ -442,40 +438,6 @@ export function dropManualMemoization(
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bailout on void return useMemos. This is an anti-pattern where code might be using
|
||||
* useMemo like useEffect: running arbirtary side-effects synced to changes in specific
|
||||
* values.
|
||||
*/
|
||||
if (
|
||||
func.env.config.validateNoVoidUseMemo &&
|
||||
manualMemo.kind === 'useMemo'
|
||||
) {
|
||||
const funcToCheck = sidemap.functions.get(
|
||||
fnPlace.identifier.id,
|
||||
)?.value;
|
||||
if (funcToCheck !== undefined && funcToCheck.loweredFunc.func) {
|
||||
if (!hasNonVoidReturn(funcToCheck.loweredFunc.func)) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'useMemo() callbacks must return a value',
|
||||
description: `This ${
|
||||
manualMemo.loadInstr.value.kind === 'PropertyLoad'
|
||||
? 'React.useMemo'
|
||||
: 'useMemo'
|
||||
} callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects.`,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: instr.value.loc,
|
||||
message: 'useMemo() callbacks must return a value',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
instr.value = getManualMemoizationReplacement(
|
||||
fnPlace,
|
||||
instr.value.loc,
|
||||
@@ -498,11 +460,11 @@ 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`,
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: `Expected the first argument to be an inline function expression`,
|
||||
description: `Expected the first argument to be an inline function expression`,
|
||||
suggestions: [],
|
||||
}).withDetail({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: fnPlace.loc,
|
||||
message: `Expected the first argument to be an inline function expression`,
|
||||
@@ -617,7 +579,14 @@ function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
|
||||
default: {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected terminal in optional`,
|
||||
loc: terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: `Unexpected ${terminal.kind} in optional`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -626,17 +595,3 @@ function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
|
||||
}
|
||||
return optionals;
|
||||
}
|
||||
|
||||
function hasNonVoidReturn(func: HIRFunction): boolean {
|
||||
for (const [, block] of func.body.blocks) {
|
||||
if (block.terminal.kind === 'return') {
|
||||
if (
|
||||
block.terminal.returnVariant === 'Explicit' ||
|
||||
block.terminal.returnVariant === 'Implicit'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,134 +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 {
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
isMutableEffect,
|
||||
isRefOrRefLikeMutableType,
|
||||
makeInstructionId,
|
||||
} from '../HIR/HIR';
|
||||
import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
/**
|
||||
* If a function captures a mutable value but never gets called, we don't infer a
|
||||
* mutable range for that function. This means that we also don't alias the function
|
||||
* with its mutable captures.
|
||||
*
|
||||
* This case is tricky, because we don't generally know for sure what is a mutation
|
||||
* and what may just be a normal function call. For example:
|
||||
*
|
||||
* ```
|
||||
* hook useFoo() {
|
||||
* const x = makeObject();
|
||||
* return () => {
|
||||
* return readObject(x); // could be a mutation!
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* If we pessimistically assume that all such cases are mutations, we'd have to group
|
||||
* lots of memo scopes together unnecessarily. However, if there is definitely a mutation:
|
||||
*
|
||||
* ```
|
||||
* hook useFoo(createEntryForKey) {
|
||||
* const cache = new WeakMap();
|
||||
* return (key) => {
|
||||
* let entry = cache.get(key);
|
||||
* if (entry == null) {
|
||||
* entry = createEntryForKey(key);
|
||||
* cache.set(key, entry); // known mutation!
|
||||
* }
|
||||
* return entry;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Then we have to ensure that the function and its mutable captures alias together and
|
||||
* end up in the same scope. However, aliasing together isn't enough if the function
|
||||
* and operands all have empty mutable ranges (end = start + 1).
|
||||
*
|
||||
* This pass finds function expressions and object methods that have an empty mutable range
|
||||
* and known-mutable operands which also don't have a mutable range, and ensures that the
|
||||
* function and those operands are aliased together *and* that their ranges are updated to
|
||||
* end after the function expression. This is sufficient to ensure that a reactive scope is
|
||||
* created for the alias set.
|
||||
*/
|
||||
export function inferAliasForUncalledFunctions(
|
||||
fn: HIRFunction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
instrs: for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
if (
|
||||
value.kind !== 'ObjectMethod' &&
|
||||
value.kind !== 'FunctionExpression'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* If the function is known to be mutated, we will have
|
||||
* already aliased any mutable operands with it
|
||||
*/
|
||||
const range = lvalue.identifier.mutableRange;
|
||||
if (range.end > range.start + 1) {
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* If the function already has operands with an active mutable range,
|
||||
* then we don't need to do anything — the function will have already
|
||||
* been visited and included in some mutable alias set. This case can
|
||||
* also occur due to visiting the same function in an earlier iteration
|
||||
* of the outer fixpoint loop.
|
||||
*/
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
if (isMutable(instr, operand)) {
|
||||
continue instrs;
|
||||
}
|
||||
}
|
||||
const operands: Set<Identifier> = new Set();
|
||||
for (const effect of value.loweredFunc.func.effects ?? []) {
|
||||
if (effect.kind !== 'ContextMutation') {
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* We're looking for known-mutations only, so we look at the effects
|
||||
* rather than function context
|
||||
*/
|
||||
if (effect.effect === Effect.Store || effect.effect === Effect.Mutate) {
|
||||
for (const operand of effect.places) {
|
||||
/*
|
||||
* It's possible that function effect analysis thinks there was a context mutation,
|
||||
* but then InferReferenceEffects figures out some operands are globals and therefore
|
||||
* creates a non-mutable effect for those operands.
|
||||
* We should change InferReferenceEffects to swap the ContextMutation for a global
|
||||
* mutation in that case, but for now we just filter them out here
|
||||
*/
|
||||
if (
|
||||
isMutableEffect(operand.effect, operand.loc) &&
|
||||
!isRefOrRefLikeMutableType(operand.identifier.type)
|
||||
) {
|
||||
operands.add(operand.identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (operands.size !== 0) {
|
||||
operands.add(lvalue.identifier);
|
||||
aliases.union([...operands]);
|
||||
// Update mutable ranges, if the ranges are empty then a reactive scope isn't created
|
||||
for (const operand of operands) {
|
||||
operand.mutableRange.end = makeInstructionId(instr.id + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,68 +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,
|
||||
Identifier,
|
||||
Instruction,
|
||||
isPrimitiveType,
|
||||
Place,
|
||||
} from '../HIR/HIR';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
export type AliasSet = Set<Identifier>;
|
||||
|
||||
export function inferAliases(func: HIRFunction): DisjointSet<Identifier> {
|
||||
const aliases = new DisjointSet<Identifier>();
|
||||
for (const [_, block] of func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
inferInstr(instr, aliases);
|
||||
}
|
||||
}
|
||||
|
||||
return aliases;
|
||||
}
|
||||
|
||||
function inferInstr(
|
||||
instr: Instruction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
const {lvalue, value: instrValue} = instr;
|
||||
let alias: Place | null = null;
|
||||
switch (instrValue.kind) {
|
||||
case 'LoadLocal':
|
||||
case 'LoadContext': {
|
||||
if (isPrimitiveType(instrValue.place.identifier)) {
|
||||
return;
|
||||
}
|
||||
alias = instrValue.place;
|
||||
break;
|
||||
}
|
||||
case 'StoreLocal':
|
||||
case 'StoreContext': {
|
||||
alias = instrValue.value;
|
||||
break;
|
||||
}
|
||||
case 'Destructure': {
|
||||
alias = instrValue.value;
|
||||
break;
|
||||
}
|
||||
case 'ComputedLoad':
|
||||
case 'PropertyLoad': {
|
||||
alias = instrValue.object;
|
||||
break;
|
||||
}
|
||||
case 'TypeCastExpression': {
|
||||
alias = instrValue.value;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
aliases.union([lvalue.identifier, alias.identifier]);
|
||||
}
|
||||
@@ -1,27 +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, Identifier} from '../HIR/HIR';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
export function inferAliasForPhis(
|
||||
func: HIRFunction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
for (const [_, block] of func.body.blocks) {
|
||||
for (const phi of block.phis) {
|
||||
const isPhiMutatedAfterCreation: boolean =
|
||||
phi.place.identifier.mutableRange.end >
|
||||
(block.instructions.at(0)?.id ?? block.terminal.id);
|
||||
if (isPhiMutatedAfterCreation) {
|
||||
for (const [, operand] of phi.operands) {
|
||||
aliases.union([phi.place.identifier, operand.identifier]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,68 +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 {
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
InstructionId,
|
||||
Place,
|
||||
} from '../HIR/HIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionValueOperand,
|
||||
} from '../HIR/visitors';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
export function inferAliasForStores(
|
||||
func: HIRFunction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
for (const [_, block] of func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
const {value, lvalue} = instr;
|
||||
const isStore =
|
||||
lvalue.effect === Effect.Store ||
|
||||
/*
|
||||
* Some typed functions annotate callees or arguments
|
||||
* as Effect.Store.
|
||||
*/
|
||||
![...eachInstructionValueOperand(value)].every(
|
||||
operand => operand.effect !== Effect.Store,
|
||||
);
|
||||
|
||||
if (!isStore) {
|
||||
continue;
|
||||
}
|
||||
for (const operand of eachInstructionLValue(instr)) {
|
||||
maybeAlias(aliases, lvalue, operand, instr.id);
|
||||
}
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
if (
|
||||
operand.effect === Effect.Capture ||
|
||||
operand.effect === Effect.Store
|
||||
) {
|
||||
maybeAlias(aliases, lvalue, operand, instr.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function maybeAlias(
|
||||
aliases: DisjointSet<Identifier>,
|
||||
lvalue: Place,
|
||||
rvalue: Place,
|
||||
id: InstructionId,
|
||||
): void {
|
||||
if (
|
||||
lvalue.identifier.mutableRange.end > id + 1 ||
|
||||
rvalue.identifier.mutableRange.end > id
|
||||
) {
|
||||
aliases.union([lvalue.identifier, rvalue.identifier]);
|
||||
}
|
||||
}
|
||||
@@ -438,7 +438,14 @@ function rewriteSplices(
|
||||
{
|
||||
reason:
|
||||
'[InferEffectDependencies] Internal invariant broken: expected block instructions to be sorted',
|
||||
loc: originalInstrs[cursor].loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: originalInstrs[cursor].loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
currBlock.instructions.push(originalInstrs[cursor]);
|
||||
@@ -447,7 +454,14 @@ function rewriteSplices(
|
||||
CompilerError.invariant(originalInstrs[cursor].id === rewrite.location, {
|
||||
reason:
|
||||
'[InferEffectDependencies] Internal invariant broken: splice location not found',
|
||||
loc: originalInstrs[cursor].loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: originalInstrs[cursor].loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (rewrite.kind === 'instr') {
|
||||
@@ -467,7 +481,14 @@ function rewriteSplices(
|
||||
{
|
||||
reason:
|
||||
'[InferEffectDependencies] Internal invariant broken: expected entry block to have a fallthrough',
|
||||
loc: entryBlock.terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: entryBlock.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
const originalTerminal = currBlock.terminal;
|
||||
@@ -566,7 +587,14 @@ function inferMinimalDependencies(
|
||||
CompilerError.invariant(hoistableToFnEntry != null, {
|
||||
reason:
|
||||
'[InferEffectDependencies] Internal invariant broken: missing entry block',
|
||||
loc: fnInstr.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: fnInstr.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const dependencies = inferDependencies(
|
||||
@@ -622,7 +650,14 @@ function inferDependencies(
|
||||
CompilerError.invariant(resultUnfiltered != null, {
|
||||
reason:
|
||||
'[InferEffectDependencies] Internal invariant broken: missing scope dependencies',
|
||||
loc: fn.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: fn.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const fnContext = new Set(fn.context.map(dep => dep.identifier.id));
|
||||
|
||||
@@ -1,351 +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 {
|
||||
CompilerError,
|
||||
CompilerErrorDetailOptions,
|
||||
ErrorSeverity,
|
||||
ValueKind,
|
||||
} from '..';
|
||||
import {
|
||||
AbstractValue,
|
||||
BasicBlock,
|
||||
Effect,
|
||||
Environment,
|
||||
FunctionEffect,
|
||||
Instruction,
|
||||
InstructionValue,
|
||||
Place,
|
||||
ValueReason,
|
||||
getHookKind,
|
||||
isRefOrRefValue,
|
||||
} from '../HIR';
|
||||
import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
interface State {
|
||||
kind(place: Place): AbstractValue;
|
||||
values(place: Place): Array<InstructionValue>;
|
||||
isDefined(place: Place): boolean;
|
||||
}
|
||||
|
||||
function inferOperandEffect(state: State, place: Place): null | FunctionEffect {
|
||||
const value = state.kind(place);
|
||||
CompilerError.invariant(value != null, {
|
||||
reason: 'Expected operand to have a kind',
|
||||
loc: null,
|
||||
});
|
||||
|
||||
switch (place.effect) {
|
||||
case Effect.Store:
|
||||
case Effect.Mutate: {
|
||||
if (isRefOrRefValue(place.identifier)) {
|
||||
break;
|
||||
} else if (value.kind === ValueKind.Context) {
|
||||
CompilerError.invariant(value.context.size > 0, {
|
||||
reason:
|
||||
"[InferFunctionEffects] Expected Context-kind value's capture list to be non-empty.",
|
||||
loc: place.loc,
|
||||
});
|
||||
return {
|
||||
kind: 'ContextMutation',
|
||||
loc: place.loc,
|
||||
effect: place.effect,
|
||||
places: value.context,
|
||||
};
|
||||
} else if (
|
||||
value.kind !== ValueKind.Mutable &&
|
||||
// We ignore mutations of primitives since this is not a React-specific problem
|
||||
value.kind !== ValueKind.Primitive
|
||||
) {
|
||||
let reason = getWriteErrorReason(value);
|
||||
return {
|
||||
kind:
|
||||
value.reason.size === 1 && value.reason.has(ValueReason.Global)
|
||||
? 'GlobalMutation'
|
||||
: 'ReactMutation',
|
||||
error: {
|
||||
reason,
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function inheritFunctionEffects(
|
||||
state: State,
|
||||
place: Place,
|
||||
): Array<FunctionEffect> {
|
||||
const effects = inferFunctionInstrEffects(state, place);
|
||||
|
||||
return effects
|
||||
.flatMap(effect => {
|
||||
if (effect.kind === 'GlobalMutation' || effect.kind === 'ReactMutation') {
|
||||
return [effect];
|
||||
} else {
|
||||
const effects: Array<FunctionEffect | null> = [];
|
||||
CompilerError.invariant(effect.kind === 'ContextMutation', {
|
||||
reason: 'Expected ContextMutation',
|
||||
loc: null,
|
||||
});
|
||||
/**
|
||||
* Contextual effects need to be replayed against the current inference
|
||||
* state, which may know more about the value to which the effect applied.
|
||||
* The main cases are:
|
||||
* 1. The mutated context value is _still_ a context value in the current scope,
|
||||
* so we have to continue propagating the original context mutation.
|
||||
* 2. The mutated context value is a mutable value in the current scope,
|
||||
* so the context mutation was fine and we can skip propagating the effect.
|
||||
* 3. The mutated context value is an immutable value in the current scope,
|
||||
* resulting in a non-ContextMutation FunctionEffect. We propagate that new,
|
||||
* more detailed effect to the current function context.
|
||||
*/
|
||||
for (const place of effect.places) {
|
||||
if (state.isDefined(place)) {
|
||||
const replayedEffect = inferOperandEffect(state, {
|
||||
...place,
|
||||
loc: effect.loc,
|
||||
effect: effect.effect,
|
||||
});
|
||||
if (replayedEffect != null) {
|
||||
if (replayedEffect.kind === 'ContextMutation') {
|
||||
// Case 1, still a context value so propagate the original effect
|
||||
effects.push(effect);
|
||||
} else {
|
||||
// Case 3, immutable value so propagate the more precise effect
|
||||
effects.push(replayedEffect);
|
||||
}
|
||||
} // else case 2, local mutable value so this effect was fine
|
||||
}
|
||||
}
|
||||
return effects;
|
||||
}
|
||||
})
|
||||
.filter((effect): effect is FunctionEffect => effect != null);
|
||||
}
|
||||
|
||||
function inferFunctionInstrEffects(
|
||||
state: State,
|
||||
place: Place,
|
||||
): Array<FunctionEffect> {
|
||||
const effects: Array<FunctionEffect> = [];
|
||||
const instrs = state.values(place);
|
||||
CompilerError.invariant(instrs != null, {
|
||||
reason: 'Expected operand to have instructions',
|
||||
loc: null,
|
||||
});
|
||||
|
||||
for (const instr of instrs) {
|
||||
if (
|
||||
(instr.kind === 'FunctionExpression' || instr.kind === 'ObjectMethod') &&
|
||||
instr.loweredFunc.func.effects != null
|
||||
) {
|
||||
effects.push(...instr.loweredFunc.func.effects);
|
||||
}
|
||||
}
|
||||
|
||||
return effects;
|
||||
}
|
||||
|
||||
function operandEffects(
|
||||
state: State,
|
||||
place: Place,
|
||||
filterRenderSafe: boolean,
|
||||
): Array<FunctionEffect> {
|
||||
const functionEffects: Array<FunctionEffect> = [];
|
||||
const effect = inferOperandEffect(state, place);
|
||||
effect && functionEffects.push(effect);
|
||||
functionEffects.push(...inheritFunctionEffects(state, place));
|
||||
if (filterRenderSafe) {
|
||||
return functionEffects.filter(effect => !isEffectSafeOutsideRender(effect));
|
||||
} else {
|
||||
return functionEffects;
|
||||
}
|
||||
}
|
||||
|
||||
export function inferInstructionFunctionEffects(
|
||||
env: Environment,
|
||||
state: State,
|
||||
instr: Instruction,
|
||||
): Array<FunctionEffect> {
|
||||
const functionEffects: Array<FunctionEffect> = [];
|
||||
switch (instr.value.kind) {
|
||||
case 'JsxExpression': {
|
||||
if (instr.value.tag.kind === 'Identifier') {
|
||||
functionEffects.push(...operandEffects(state, instr.value.tag, false));
|
||||
}
|
||||
instr.value.children?.forEach(child =>
|
||||
functionEffects.push(...operandEffects(state, child, false)),
|
||||
);
|
||||
for (const attr of instr.value.props) {
|
||||
if (attr.kind === 'JsxSpreadAttribute') {
|
||||
functionEffects.push(...operandEffects(state, attr.argument, false));
|
||||
} else {
|
||||
functionEffects.push(...operandEffects(state, attr.place, true));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ObjectMethod':
|
||||
case 'FunctionExpression': {
|
||||
/**
|
||||
* If this function references other functions, propagate the referenced function's
|
||||
* effects to this function.
|
||||
*
|
||||
* ```
|
||||
* let f = () => global = true;
|
||||
* let g = () => f();
|
||||
* g();
|
||||
* ```
|
||||
*
|
||||
* In this example, because `g` references `f`, we propagate the GlobalMutation from
|
||||
* `f` to `g`. Thus, referencing `g` in `g()` will evaluate the GlobalMutation in the outer
|
||||
* function effect context and report an error. But if instead we do:
|
||||
*
|
||||
* ```
|
||||
* let f = () => global = true;
|
||||
* let g = () => f();
|
||||
* useEffect(() => g(), [g])
|
||||
* ```
|
||||
*
|
||||
* Now `g`'s effects will be discarded since they're in a useEffect.
|
||||
*/
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
instr.value.loweredFunc.func.effects ??= [];
|
||||
instr.value.loweredFunc.func.effects.push(
|
||||
...inferFunctionInstrEffects(state, operand),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'MethodCall':
|
||||
case 'CallExpression': {
|
||||
let callee;
|
||||
if (instr.value.kind === 'MethodCall') {
|
||||
callee = instr.value.property;
|
||||
functionEffects.push(
|
||||
...operandEffects(state, instr.value.receiver, false),
|
||||
);
|
||||
} else {
|
||||
callee = instr.value.callee;
|
||||
}
|
||||
functionEffects.push(...operandEffects(state, callee, false));
|
||||
let isHook = getHookKind(env, callee.identifier) != null;
|
||||
for (const arg of instr.value.args) {
|
||||
const place = arg.kind === 'Identifier' ? arg : arg.place;
|
||||
/*
|
||||
* Join the effects of the argument with the effects of the enclosing function,
|
||||
* unless the we're detecting a global mutation inside a useEffect hook
|
||||
*/
|
||||
functionEffects.push(...operandEffects(state, place, isHook));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'StartMemoize':
|
||||
case 'FinishMemoize':
|
||||
case 'LoadLocal':
|
||||
case 'StoreLocal': {
|
||||
break;
|
||||
}
|
||||
case 'StoreGlobal': {
|
||||
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)',
|
||||
loc: instr.loc,
|
||||
suggestions: null,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
functionEffects.push(...operandEffects(state, operand, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
return functionEffects;
|
||||
}
|
||||
|
||||
export function inferTerminalFunctionEffects(
|
||||
state: State,
|
||||
block: BasicBlock,
|
||||
): Array<FunctionEffect> {
|
||||
const functionEffects: Array<FunctionEffect> = [];
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
functionEffects.push(...operandEffects(state, operand, true));
|
||||
}
|
||||
return functionEffects;
|
||||
}
|
||||
|
||||
export function transformFunctionEffectErrors(
|
||||
functionEffects: Array<FunctionEffect>,
|
||||
): Array<CompilerErrorDetailOptions> {
|
||||
return functionEffects.map(eff => {
|
||||
switch (eff.kind) {
|
||||
case 'ReactMutation':
|
||||
case 'GlobalMutation': {
|
||||
return eff.error;
|
||||
}
|
||||
case 'ContextMutation': {
|
||||
return {
|
||||
severity: ErrorSeverity.Invariant,
|
||||
reason: `Unexpected ContextMutation in top-level function effects`,
|
||||
loc: eff.loc,
|
||||
};
|
||||
}
|
||||
default:
|
||||
assertExhaustive(
|
||||
eff,
|
||||
`Unexpected function effect kind \`${(eff as any).kind}\``,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isEffectSafeOutsideRender(effect: FunctionEffect): boolean {
|
||||
return effect.kind === 'GlobalMutation';
|
||||
}
|
||||
|
||||
export function getWriteErrorReason(abstractValue: AbstractValue): string {
|
||||
if (abstractValue.reason.has(ValueReason.Global)) {
|
||||
return 'Modifying a variable defined outside a component or hook is not allowed. Consider using an effect';
|
||||
} else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
|
||||
return 'Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX';
|
||||
} else if (abstractValue.reason.has(ValueReason.Context)) {
|
||||
return `Modifying a value returned from 'useContext()' is not allowed.`;
|
||||
} else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
|
||||
return 'Modifying a value returned from a function whose return value should not be mutated';
|
||||
} else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
|
||||
return 'Modifying component props or hook arguments is not allowed. Consider using a local variable instead';
|
||||
} 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";
|
||||
} 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";
|
||||
} 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()';
|
||||
} 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';
|
||||
} 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';
|
||||
} else {
|
||||
return 'This modifies a variable that React considers immutable';
|
||||
}
|
||||
}
|
||||
@@ -1,218 +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 {
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
InstructionId,
|
||||
InstructionKind,
|
||||
isArrayType,
|
||||
isMapType,
|
||||
isRefOrRefValue,
|
||||
isSetType,
|
||||
makeInstructionId,
|
||||
Place,
|
||||
} from '../HIR/HIR';
|
||||
import {printPlace} from '../HIR/PrintHIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
/*
|
||||
* For each usage of a value in the given function, determines if the usage
|
||||
* may be succeeded by a mutable usage of that same value and if so updates
|
||||
* the usage to be mutable.
|
||||
*
|
||||
* Stated differently, this inference ensures that inferred capabilities of
|
||||
* each reference are as follows:
|
||||
* - freeze: the value is frozen at this point
|
||||
* - readonly: the value is not modified at this point *or any subsequent
|
||||
* point*
|
||||
* - mutable: the value is modified at this point *or some subsequent point*.
|
||||
*
|
||||
* Note that this refines the capabilities inferered by InferReferenceCapability,
|
||||
* which looks at individual references and not the lifetime of a value's mutability.
|
||||
*
|
||||
* == Algorithm
|
||||
*
|
||||
* TODO:
|
||||
* 1. Forward data-flow analysis to determine aliasing. Unlike InferReferenceCapability
|
||||
* which only tracks aliasing of top-level variables (`y = x`), this analysis needs
|
||||
* to know if a value is aliased anywhere (`y.x = x`). The forward data flow tracks
|
||||
* all possible locations which may have aliased a value. The concrete result is
|
||||
* a mapping of each Place to the set of possibly-mutable values it may alias.
|
||||
*
|
||||
* ```
|
||||
* const x = []; // {x: v0; v0: mutable []}
|
||||
* const y = {}; // {x: v0, y: v1; v0: mutable [], v1: mutable []}
|
||||
* y.x = x; // {x: v0, y: v1; v0: mutable [v1], v1: mutable [v0]}
|
||||
* read(x); // {x: v0, y: v1; v0: mutable [v1], v1: mutable [v0]}
|
||||
* mutate(y); // can infer that y mutates v0 and v1
|
||||
* ```
|
||||
*
|
||||
* DONE:
|
||||
* 2. Forward data-flow analysis to compute mutability liveness. Walk forwards over
|
||||
* the CFG and track which values are mutated in a successor.
|
||||
*
|
||||
* ```
|
||||
* mutate(y); // mutable y => v0, v1 mutated
|
||||
* read(x); // x maps to v0, v1, those are in the mutated-later set, so x is mutable here
|
||||
* ...
|
||||
* ```
|
||||
*/
|
||||
|
||||
function infer(place: Place, instrId: InstructionId): void {
|
||||
if (!isRefOrRefValue(place.identifier)) {
|
||||
place.identifier.mutableRange.end = makeInstructionId(instrId + 1);
|
||||
}
|
||||
}
|
||||
|
||||
function inferPlace(
|
||||
place: Place,
|
||||
instrId: InstructionId,
|
||||
inferMutableRangeForStores: boolean,
|
||||
): void {
|
||||
switch (place.effect) {
|
||||
case Effect.Unknown: {
|
||||
throw new Error(`Found an unknown place ${printPlace(place)}}!`);
|
||||
}
|
||||
case Effect.Capture:
|
||||
case Effect.Read:
|
||||
case Effect.Freeze:
|
||||
return;
|
||||
case Effect.Store:
|
||||
if (inferMutableRangeForStores) {
|
||||
infer(place, instrId);
|
||||
}
|
||||
return;
|
||||
case Effect.ConditionallyMutateIterator: {
|
||||
const identifier = place.identifier;
|
||||
if (
|
||||
!isArrayType(identifier) &&
|
||||
!isSetType(identifier) &&
|
||||
!isMapType(identifier)
|
||||
) {
|
||||
infer(place, instrId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case Effect.ConditionallyMutate:
|
||||
case Effect.Mutate: {
|
||||
infer(place, instrId);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
assertExhaustive(place.effect, `Unexpected ${printPlace(place)} effect`);
|
||||
}
|
||||
}
|
||||
|
||||
export function inferMutableLifetimes(
|
||||
func: HIRFunction,
|
||||
inferMutableRangeForStores: boolean,
|
||||
): void {
|
||||
/*
|
||||
* Context variables only appear to mutate where they are assigned, but we need
|
||||
* to force their range to start at their declaration. Track the declaring instruction
|
||||
* id so that the ranges can be extended if/when they are reassigned
|
||||
*/
|
||||
const contextVariableDeclarationInstructions = new Map<
|
||||
Identifier,
|
||||
InstructionId
|
||||
>();
|
||||
for (const [_, block] of func.body.blocks) {
|
||||
for (const phi of block.phis) {
|
||||
const isPhiMutatedAfterCreation: boolean =
|
||||
phi.place.identifier.mutableRange.end >
|
||||
(block.instructions.at(0)?.id ?? block.terminal.id);
|
||||
if (
|
||||
inferMutableRangeForStores &&
|
||||
isPhiMutatedAfterCreation &&
|
||||
phi.place.identifier.mutableRange.start === 0
|
||||
) {
|
||||
for (const [, operand] of phi.operands) {
|
||||
if (phi.place.identifier.mutableRange.start === 0) {
|
||||
phi.place.identifier.mutableRange.start =
|
||||
operand.identifier.mutableRange.start;
|
||||
} else {
|
||||
phi.place.identifier.mutableRange.start = makeInstructionId(
|
||||
Math.min(
|
||||
phi.place.identifier.mutableRange.start,
|
||||
operand.identifier.mutableRange.start,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
for (const operand of eachInstructionLValue(instr)) {
|
||||
const lvalueId = operand.identifier;
|
||||
|
||||
/*
|
||||
* lvalue start being mutable when they're initially assigned a
|
||||
* value.
|
||||
*/
|
||||
lvalueId.mutableRange.start = instr.id;
|
||||
|
||||
/*
|
||||
* Let's be optimistic and assume this lvalue is not mutable by
|
||||
* default.
|
||||
*/
|
||||
lvalueId.mutableRange.end = makeInstructionId(instr.id + 1);
|
||||
}
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
inferPlace(operand, instr.id, inferMutableRangeForStores);
|
||||
}
|
||||
|
||||
if (
|
||||
instr.value.kind === 'DeclareContext' ||
|
||||
(instr.value.kind === 'StoreContext' &&
|
||||
instr.value.lvalue.kind !== InstructionKind.Reassign &&
|
||||
!contextVariableDeclarationInstructions.has(
|
||||
instr.value.lvalue.place.identifier,
|
||||
))
|
||||
) {
|
||||
/**
|
||||
* Save declarations of context variables if they hasn't already been
|
||||
* declared (due to hoisted declarations).
|
||||
*/
|
||||
contextVariableDeclarationInstructions.set(
|
||||
instr.value.lvalue.place.identifier,
|
||||
instr.id,
|
||||
);
|
||||
} else if (instr.value.kind === 'StoreContext') {
|
||||
/*
|
||||
* Else this is a reassignment, extend the range from the declaration (if present).
|
||||
* Note that declarations may not be present for context variables that are reassigned
|
||||
* within a function expression before (or without) a read of the same variable
|
||||
*/
|
||||
const declaration = contextVariableDeclarationInstructions.get(
|
||||
instr.value.lvalue.place.identifier,
|
||||
);
|
||||
if (
|
||||
declaration != null &&
|
||||
!isRefOrRefValue(instr.value.lvalue.place.identifier)
|
||||
) {
|
||||
const range = instr.value.lvalue.place.identifier.mutableRange;
|
||||
if (range.start === 0) {
|
||||
range.start = declaration;
|
||||
} else {
|
||||
range.start = makeInstructionId(Math.min(range.start, declaration));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
inferPlace(operand, block.terminal.id, inferMutableRangeForStores);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +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, Identifier} from '../HIR/HIR';
|
||||
import {inferAliasForUncalledFunctions} from './InerAliasForUncalledFunctions';
|
||||
import {inferAliases} from './InferAlias';
|
||||
import {inferAliasForPhis} from './InferAliasForPhis';
|
||||
import {inferAliasForStores} from './InferAliasForStores';
|
||||
import {inferMutableLifetimes} from './InferMutableLifetimes';
|
||||
import {inferMutableRangesForAlias} from './InferMutableRangesForAlias';
|
||||
import {inferTryCatchAliases} from './InferTryCatchAliases';
|
||||
|
||||
export function inferMutableRanges(ir: HIRFunction): void {
|
||||
// Infer mutable ranges for non fields
|
||||
inferMutableLifetimes(ir, false);
|
||||
|
||||
// Calculate aliases
|
||||
const aliases = inferAliases(ir);
|
||||
/*
|
||||
* Calculate aliases for try/catch, where any value created
|
||||
* in the try block could be aliased to the catch param
|
||||
*/
|
||||
inferTryCatchAliases(ir, aliases);
|
||||
|
||||
/*
|
||||
* Eagerly canonicalize so that if nothing changes we can bail out
|
||||
* after a single iteration
|
||||
*/
|
||||
let prevAliases: Map<Identifier, Identifier> = aliases.canonicalize();
|
||||
while (true) {
|
||||
// Infer mutable ranges for aliases that are not fields
|
||||
inferMutableRangesForAlias(ir, aliases);
|
||||
|
||||
// Update aliasing information of fields
|
||||
inferAliasForStores(ir, aliases);
|
||||
|
||||
// Update aliasing information of phis
|
||||
inferAliasForPhis(ir, aliases);
|
||||
|
||||
const nextAliases = aliases.canonicalize();
|
||||
if (areEqualMaps(prevAliases, nextAliases)) {
|
||||
break;
|
||||
}
|
||||
prevAliases = nextAliases;
|
||||
}
|
||||
|
||||
// Re-infer mutable ranges for all values
|
||||
inferMutableLifetimes(ir, true);
|
||||
|
||||
/**
|
||||
* The second inferMutableLifetimes() call updates mutable ranges
|
||||
* of values to account for Store effects. Now we need to update
|
||||
* all aliases of such values to extend their ranges as well. Note
|
||||
* that the store only mutates the the directly aliased value and
|
||||
* not any of its inner captured references. For example:
|
||||
*
|
||||
* ```
|
||||
* let y;
|
||||
* if (cond) {
|
||||
* y = [];
|
||||
* } else {
|
||||
* y = [{}];
|
||||
* }
|
||||
* y.push(z);
|
||||
* ```
|
||||
*
|
||||
* The Store effect from the `y.push` modifies the values that `y`
|
||||
* directly aliases - the two arrays from the if/else branches -
|
||||
* but does not modify values that `y` "contains" such as the
|
||||
* object literal or `z`.
|
||||
*/
|
||||
prevAliases = aliases.canonicalize();
|
||||
while (true) {
|
||||
inferMutableRangesForAlias(ir, aliases);
|
||||
inferAliasForPhis(ir, aliases);
|
||||
inferAliasForUncalledFunctions(ir, aliases);
|
||||
const nextAliases = aliases.canonicalize();
|
||||
if (areEqualMaps(prevAliases, nextAliases)) {
|
||||
break;
|
||||
}
|
||||
prevAliases = nextAliases;
|
||||
}
|
||||
}
|
||||
|
||||
function areEqualMaps<T, U>(a: Map<T, U>, b: Map<T, U>): boolean {
|
||||
if (a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
for (const [key, value] of a) {
|
||||
if (!b.has(key)) {
|
||||
return false;
|
||||
}
|
||||
if (b.get(key) !== value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1,54 +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,
|
||||
Identifier,
|
||||
InstructionId,
|
||||
isRefOrRefValue,
|
||||
} from '../HIR/HIR';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
export function inferMutableRangesForAlias(
|
||||
_fn: HIRFunction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
const aliasSets = aliases.buildSets();
|
||||
for (const aliasSet of aliasSets) {
|
||||
/*
|
||||
* Update mutableRange.end only if the identifiers have actually been
|
||||
* mutated.
|
||||
*/
|
||||
const mutatingIdentifiers = [...aliasSet].filter(
|
||||
id =>
|
||||
id.mutableRange.end - id.mutableRange.start > 1 && !isRefOrRefValue(id),
|
||||
);
|
||||
|
||||
if (mutatingIdentifiers.length > 0) {
|
||||
// Find final instruction which mutates this alias set.
|
||||
let lastMutatingInstructionId = 0;
|
||||
for (const id of mutatingIdentifiers) {
|
||||
if (id.mutableRange.end > lastMutatingInstructionId) {
|
||||
lastMutatingInstructionId = id.mutableRange.end;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Update mutableRange.end for all aliases in this set ending before the
|
||||
* last mutation.
|
||||
*/
|
||||
for (const alias of aliasSet) {
|
||||
if (
|
||||
alias.mutableRange.end < lastMutatingInstructionId &&
|
||||
!isRefOrRefValue(alias)
|
||||
) {
|
||||
alias.mutableRange.end = lastMutatingInstructionId as InstructionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
Effect,
|
||||
ErrorSeverity,
|
||||
SourceLocation,
|
||||
ValueKind,
|
||||
} from '..';
|
||||
@@ -19,6 +18,8 @@ import {
|
||||
DeclarationId,
|
||||
Environment,
|
||||
FunctionExpression,
|
||||
GeneratedSource,
|
||||
getHookKind,
|
||||
HIRFunction,
|
||||
Hole,
|
||||
IdentifierId,
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
InstructionKind,
|
||||
InstructionValue,
|
||||
isArrayType,
|
||||
isJsxType,
|
||||
isMapType,
|
||||
isPrimitiveType,
|
||||
isRefOrRefValue,
|
||||
@@ -34,6 +36,7 @@ import {
|
||||
Phi,
|
||||
Place,
|
||||
SpreadPattern,
|
||||
Type,
|
||||
ValueReason,
|
||||
} from '../HIR';
|
||||
import {
|
||||
@@ -43,12 +46,6 @@ import {
|
||||
eachTerminalSuccessor,
|
||||
} from '../HIR/visitors';
|
||||
import {Ok, Result} from '../Utils/Result';
|
||||
import {
|
||||
getArgumentEffect,
|
||||
getFunctionCallSignature,
|
||||
isKnownMutableEffect,
|
||||
mergeValueKinds,
|
||||
} from './InferReferenceEffects';
|
||||
import {
|
||||
assertExhaustive,
|
||||
getOrInsertDefault,
|
||||
@@ -62,10 +59,8 @@ import {
|
||||
printInstruction,
|
||||
printInstructionValue,
|
||||
printPlace,
|
||||
printSourceLocation,
|
||||
} from '../HIR/PrintHIR';
|
||||
import {FunctionSignature} from '../HIR/ObjectShape';
|
||||
import {getWriteErrorReason} from './InferFunctionEffects';
|
||||
import prettyFormat from 'pretty-format';
|
||||
import {createTemporaryPlace} from '../HIR/HIRBuilder';
|
||||
import {
|
||||
@@ -74,6 +69,7 @@ import {
|
||||
hashEffect,
|
||||
MutationReason,
|
||||
} from './AliasingEffects';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
@@ -139,7 +135,13 @@ export function inferMutationAliasingEffects(
|
||||
reason:
|
||||
'Expected React component to have not more than two parameters: one for props and for ref',
|
||||
description: null,
|
||||
loc: fn.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: fn.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
const [props, ref] = fn.params;
|
||||
@@ -197,6 +199,7 @@ export function inferMutationAliasingEffects(
|
||||
isFunctionExpression,
|
||||
fn,
|
||||
hoistedContextDeclarations,
|
||||
findNonMutatedDestructureSpreads(fn),
|
||||
);
|
||||
|
||||
let iterationCount = 0;
|
||||
@@ -206,7 +209,13 @@ export function inferMutationAliasingEffects(
|
||||
CompilerError.invariant(false, {
|
||||
reason: `[InferMutationAliasingEffects] Potential infinite loop`,
|
||||
description: `A value, temporary place, or effect was not cached properly`,
|
||||
loc: fn.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: fn.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
for (const [blockId, block] of fn.body.blocks) {
|
||||
@@ -280,15 +289,18 @@ class Context {
|
||||
isFuctionExpression: boolean;
|
||||
fn: HIRFunction;
|
||||
hoistedContextDeclarations: Map<DeclarationId, Place | null>;
|
||||
nonMutatingSpreads: Set<IdentifierId>;
|
||||
|
||||
constructor(
|
||||
isFunctionExpression: boolean,
|
||||
fn: HIRFunction,
|
||||
hoistedContextDeclarations: Map<DeclarationId, Place | null>,
|
||||
nonMutatingSpreads: Set<IdentifierId>,
|
||||
) {
|
||||
this.isFuctionExpression = isFunctionExpression;
|
||||
this.fn = fn;
|
||||
this.hoistedContextDeclarations = hoistedContextDeclarations;
|
||||
this.nonMutatingSpreads = nonMutatingSpreads;
|
||||
}
|
||||
|
||||
cacheApplySignature(
|
||||
@@ -315,6 +327,161 @@ class Context {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds objects created via ObjectPattern spread destructuring
|
||||
* (`const {x, ...spread} = ...`) where a) the rvalue is known frozen and
|
||||
* b) the spread value cannot possibly be directly mutated. The idea is that
|
||||
* for this set of values, we can treat the spread object as frozen.
|
||||
*
|
||||
* The primary use case for this is props spreading:
|
||||
*
|
||||
* ```
|
||||
* function Component({prop, ...otherProps}) {
|
||||
* const transformedProp = transform(prop, otherProps.foo);
|
||||
* // pass `otherProps` down:
|
||||
* return <Foo {...otherProps} prop={transformedProp} />;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Here we know that since `otherProps` cannot be mutated, we don't have to treat
|
||||
* it as mutable: `otherProps.foo` only reads a value that must be frozen, so it
|
||||
* can be treated as frozen too.
|
||||
*/
|
||||
function findNonMutatedDestructureSpreads(fn: HIRFunction): Set<IdentifierId> {
|
||||
const knownFrozen = new Set<IdentifierId>();
|
||||
if (fn.fnType === 'Component') {
|
||||
const [props] = fn.params;
|
||||
if (props != null && props.kind === 'Identifier') {
|
||||
knownFrozen.add(props.identifier.id);
|
||||
}
|
||||
} else {
|
||||
for (const param of fn.params) {
|
||||
if (param.kind === 'Identifier') {
|
||||
knownFrozen.add(param.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Map of temporaries to identifiers for spread objects
|
||||
const candidateNonMutatingSpreads = new Map<IdentifierId, IdentifierId>();
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
if (candidateNonMutatingSpreads.size !== 0) {
|
||||
for (const phi of block.phis) {
|
||||
for (const operand of phi.operands.values()) {
|
||||
const spread = candidateNonMutatingSpreads.get(operand.identifier.id);
|
||||
if (spread != null) {
|
||||
candidateNonMutatingSpreads.delete(spread);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
switch (value.kind) {
|
||||
case 'Destructure': {
|
||||
if (
|
||||
!knownFrozen.has(value.value.identifier.id) ||
|
||||
!(
|
||||
value.lvalue.kind === InstructionKind.Let ||
|
||||
value.lvalue.kind === InstructionKind.Const
|
||||
) ||
|
||||
value.lvalue.pattern.kind !== 'ObjectPattern'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
for (const item of value.lvalue.pattern.properties) {
|
||||
if (item.kind !== 'Spread') {
|
||||
continue;
|
||||
}
|
||||
candidateNonMutatingSpreads.set(
|
||||
item.place.identifier.id,
|
||||
item.place.identifier.id,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'LoadLocal': {
|
||||
const spread = candidateNonMutatingSpreads.get(
|
||||
value.place.identifier.id,
|
||||
);
|
||||
if (spread != null) {
|
||||
candidateNonMutatingSpreads.set(lvalue.identifier.id, spread);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'StoreLocal': {
|
||||
const spread = candidateNonMutatingSpreads.get(
|
||||
value.value.identifier.id,
|
||||
);
|
||||
if (spread != null) {
|
||||
candidateNonMutatingSpreads.set(lvalue.identifier.id, spread);
|
||||
candidateNonMutatingSpreads.set(
|
||||
value.lvalue.place.identifier.id,
|
||||
spread,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'JsxFragment':
|
||||
case 'JsxExpression': {
|
||||
// Passing objects created with spread to jsx can't mutate them
|
||||
break;
|
||||
}
|
||||
case 'PropertyLoad': {
|
||||
// Properties must be frozen since the original value was frozen
|
||||
break;
|
||||
}
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
if (getHookKind(fn.env, callee.identifier) != null) {
|
||||
// Hook calls have frozen arguments, and non-ref returns are frozen
|
||||
if (!isRefOrRefValue(lvalue.identifier)) {
|
||||
knownFrozen.add(lvalue.identifier.id);
|
||||
}
|
||||
} else {
|
||||
// Non-hook calls check their operands, since they are potentially mutable
|
||||
if (candidateNonMutatingSpreads.size !== 0) {
|
||||
// Otherwise any reference to the spread object itself may mutate
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
const spread = candidateNonMutatingSpreads.get(
|
||||
operand.identifier.id,
|
||||
);
|
||||
if (spread != null) {
|
||||
candidateNonMutatingSpreads.delete(spread);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (candidateNonMutatingSpreads.size !== 0) {
|
||||
// Otherwise any reference to the spread object itself may mutate
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
const spread = candidateNonMutatingSpreads.get(
|
||||
operand.identifier.id,
|
||||
);
|
||||
if (spread != null) {
|
||||
candidateNonMutatingSpreads.delete(spread);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const nonMutatingSpreads = new Set<IdentifierId>();
|
||||
for (const [key, value] of candidateNonMutatingSpreads) {
|
||||
if (key === value) {
|
||||
nonMutatingSpreads.add(key);
|
||||
}
|
||||
}
|
||||
return nonMutatingSpreads;
|
||||
}
|
||||
|
||||
function inferParam(
|
||||
param: Place | SpreadPattern,
|
||||
initialState: InferenceState,
|
||||
@@ -361,7 +528,14 @@ function inferBlock(
|
||||
CompilerError.invariant(state.kind(handlerParam) != null, {
|
||||
reason:
|
||||
'Expected catch binding to be intialized with a DeclareLocal Catch instruction',
|
||||
loc: terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
const effects: Array<AliasingEffect> = [];
|
||||
for (const instr of block.instructions) {
|
||||
@@ -450,7 +624,6 @@ function applySignature(
|
||||
const reason = getWriteErrorReason({
|
||||
kind: value.kind,
|
||||
reason: value.reason,
|
||||
context: new Set(),
|
||||
});
|
||||
const variable =
|
||||
effect.value.identifier.name !== null &&
|
||||
@@ -458,10 +631,10 @@ function applySignature(
|
||||
? `\`${effect.value.identifier.name.value}\``
|
||||
: 'value';
|
||||
const diagnostic = CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'This value cannot be modified',
|
||||
description: `${reason}.`,
|
||||
}).withDetail({
|
||||
category: ErrorCategory.Immutability,
|
||||
reason: 'This value cannot be modified',
|
||||
description: reason,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `${variable} cannot be modified`,
|
||||
@@ -470,9 +643,8 @@ function applySignature(
|
||||
effect.kind === 'Mutate' &&
|
||||
effect.reason?.kind === 'AssignCurrentProperty'
|
||||
) {
|
||||
diagnostic.withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
diagnostic.withDetails({
|
||||
kind: 'hint',
|
||||
message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`,
|
||||
});
|
||||
}
|
||||
@@ -513,7 +685,14 @@ function applySignature(
|
||||
) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Expected instruction lvalue to be initialized`,
|
||||
loc: instruction.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instruction.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
return effects.length !== 0 ? effects : null;
|
||||
@@ -542,7 +721,13 @@ function applyEffect(
|
||||
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
|
||||
reason: `Cannot re-initialize variable within an instruction`,
|
||||
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
|
||||
loc: effect.into.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: effect.into.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
initialized.add(effect.into.identifier.id);
|
||||
|
||||
@@ -581,7 +766,13 @@ function applyEffect(
|
||||
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
|
||||
reason: `Cannot re-initialize variable within an instruction`,
|
||||
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
|
||||
loc: effect.into.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: effect.into.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
initialized.add(effect.into.identifier.id);
|
||||
|
||||
@@ -641,7 +832,13 @@ function applyEffect(
|
||||
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
|
||||
reason: `Cannot re-initialize variable within an instruction`,
|
||||
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
|
||||
loc: effect.into.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: effect.into.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
initialized.add(effect.into.identifier.id);
|
||||
|
||||
@@ -711,11 +908,21 @@ function applyEffect(
|
||||
case 'Alias':
|
||||
case 'Capture': {
|
||||
CompilerError.invariant(
|
||||
effect.kind === 'Capture' || initialized.has(effect.into.identifier.id),
|
||||
effect.kind === 'Capture' ||
|
||||
effect.kind === 'MaybeAlias' ||
|
||||
initialized.has(effect.into.identifier.id),
|
||||
{
|
||||
reason: `Expected destination value to already be initialized within this instruction for Alias effect`,
|
||||
description: `Destination ${printPlace(effect.into)} is not initialized in this instruction`,
|
||||
loc: effect.into.loc,
|
||||
reason: `Expected destination to already be initialized within this instruction`,
|
||||
description:
|
||||
`Destination ${printPlace(effect.into)} is not initialized in this ` +
|
||||
`instruction for effect ${printAliasingEffect(effect)}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: effect.into.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
/*
|
||||
@@ -724,49 +931,67 @@ function applyEffect(
|
||||
* copy-on-write semantics, then we can prune the effect
|
||||
*/
|
||||
const intoKind = state.kind(effect.into).kind;
|
||||
let isMutableDesination: boolean;
|
||||
let destinationType: 'context' | 'mutable' | null = null;
|
||||
switch (intoKind) {
|
||||
case ValueKind.Context:
|
||||
case ValueKind.Mutable:
|
||||
case ValueKind.MaybeFrozen: {
|
||||
isMutableDesination = true;
|
||||
case ValueKind.Context: {
|
||||
destinationType = 'context';
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
isMutableDesination = false;
|
||||
case ValueKind.Mutable:
|
||||
case ValueKind.MaybeFrozen: {
|
||||
destinationType = 'mutable';
|
||||
break;
|
||||
}
|
||||
}
|
||||
const fromKind = state.kind(effect.from).kind;
|
||||
let isMutableReferenceType: boolean;
|
||||
let sourceType: 'context' | 'mutable' | 'frozen' | null = null;
|
||||
switch (fromKind) {
|
||||
case ValueKind.Context: {
|
||||
sourceType = 'context';
|
||||
break;
|
||||
}
|
||||
case ValueKind.Global:
|
||||
case ValueKind.Primitive: {
|
||||
isMutableReferenceType = false;
|
||||
break;
|
||||
}
|
||||
case ValueKind.Frozen: {
|
||||
isMutableReferenceType = false;
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
{
|
||||
kind: 'ImmutableCapture',
|
||||
from: effect.from,
|
||||
into: effect.into,
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
sourceType = 'frozen';
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
isMutableReferenceType = true;
|
||||
sourceType = 'mutable';
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isMutableDesination && isMutableReferenceType) {
|
||||
|
||||
if (sourceType === 'frozen') {
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
{
|
||||
kind: 'ImmutableCapture',
|
||||
from: effect.from,
|
||||
into: effect.into,
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
} else if (
|
||||
(sourceType === 'mutable' && destinationType === 'mutable') ||
|
||||
effect.kind === 'MaybeAlias'
|
||||
) {
|
||||
effects.push(effect);
|
||||
} else if (
|
||||
(sourceType === 'context' && destinationType != null) ||
|
||||
(sourceType === 'mutable' && destinationType === 'context')
|
||||
) {
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
{kind: 'MaybeAlias', from: effect.from, into: effect.into},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -774,7 +999,13 @@ function applyEffect(
|
||||
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
|
||||
reason: `Cannot re-initialize variable within an instruction`,
|
||||
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
|
||||
loc: effect.into.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: effect.into.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
initialized.add(effect.into.identifier.id);
|
||||
|
||||
@@ -1043,18 +1274,18 @@ function applyEffect(
|
||||
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.`,
|
||||
category: ErrorCategory.Immutability,
|
||||
reason: 'Cannot access variable before it is declared',
|
||||
description: `${variable ?? 'This variable'} is accessed before it is declared, which prevents the earlier access from updating when this value changes over time`,
|
||||
});
|
||||
if (hoistedAccess != null && hoistedAccess.loc != effect.value.loc) {
|
||||
diagnostic.withDetail({
|
||||
diagnostic.withDetails({
|
||||
kind: 'error',
|
||||
loc: hoistedAccess.loc,
|
||||
message: `${variable ?? 'variable'} accessed before it is declared`,
|
||||
});
|
||||
}
|
||||
diagnostic.withDetail({
|
||||
diagnostic.withDetails({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `${variable ?? 'variable'} is declared here`,
|
||||
@@ -1075,7 +1306,6 @@ function applyEffect(
|
||||
const reason = getWriteErrorReason({
|
||||
kind: value.kind,
|
||||
reason: value.reason,
|
||||
context: new Set(),
|
||||
});
|
||||
const variable =
|
||||
effect.value.identifier.name !== null &&
|
||||
@@ -1083,10 +1313,10 @@ function applyEffect(
|
||||
? `\`${effect.value.identifier.name.value}\``
|
||||
: 'value';
|
||||
const diagnostic = CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'This value cannot be modified',
|
||||
description: `${reason}.`,
|
||||
}).withDetail({
|
||||
category: ErrorCategory.Immutability,
|
||||
reason: 'This value cannot be modified',
|
||||
description: reason,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `${variable} cannot be modified`,
|
||||
@@ -1095,9 +1325,8 @@ function applyEffect(
|
||||
effect.kind === 'Mutate' &&
|
||||
effect.reason?.kind === 'AssignCurrentProperty'
|
||||
) {
|
||||
diagnostic.withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
diagnostic.withDetails({
|
||||
kind: 'hint',
|
||||
message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`,
|
||||
});
|
||||
}
|
||||
@@ -1177,7 +1406,13 @@ class InferenceState {
|
||||
reason:
|
||||
'[InferMutationAliasingEffects] Expected all top-level identifiers to be defined as variables, not values',
|
||||
description: null,
|
||||
loc: value.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: value.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
this.#values.set(value, kind);
|
||||
@@ -1188,7 +1423,13 @@ class InferenceState {
|
||||
CompilerError.invariant(values != null, {
|
||||
reason: `[InferMutationAliasingEffects] Expected value kind to be initialized`,
|
||||
description: `${printPlace(place)}`,
|
||||
loc: place.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: place.loc,
|
||||
message: 'this is uninitialized',
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return Array.from(values);
|
||||
@@ -1200,7 +1441,13 @@ class InferenceState {
|
||||
CompilerError.invariant(values != null, {
|
||||
reason: `[InferMutationAliasingEffects] Expected value kind to be initialized`,
|
||||
description: `${printPlace(place)}`,
|
||||
loc: place.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: place.loc,
|
||||
message: 'this is uninitialized',
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
let mergedKind: AbstractValue | null = null;
|
||||
@@ -1212,7 +1459,13 @@ class InferenceState {
|
||||
CompilerError.invariant(mergedKind !== null, {
|
||||
reason: `[InferMutationAliasingEffects] Expected at least one value`,
|
||||
description: `No value found at \`${printPlace(place)}\``,
|
||||
loc: place.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: place.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return mergedKind;
|
||||
@@ -1224,7 +1477,13 @@ class InferenceState {
|
||||
CompilerError.invariant(values != null, {
|
||||
reason: `[InferMutationAliasingEffects] Expected value for identifier to be initialized`,
|
||||
description: `${printIdentifier(value.identifier)}`,
|
||||
loc: value.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: value.loc,
|
||||
message: 'Expected value for identifier to be initialized',
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
this.#variables.set(place.identifier.id, new Set(values));
|
||||
@@ -1235,7 +1494,13 @@ class InferenceState {
|
||||
CompilerError.invariant(values != null, {
|
||||
reason: `[InferMutationAliasingEffects] Expected value for identifier to be initialized`,
|
||||
description: `${printIdentifier(value.identifier)}`,
|
||||
loc: value.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: value.loc,
|
||||
message: 'Expected value for identifier to be initialized',
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
const prevValues = this.values(place);
|
||||
@@ -1248,11 +1513,15 @@ class InferenceState {
|
||||
// Defines (initializing or updating) a variable with a specific kind of value.
|
||||
define(place: Place, value: InstructionValue): void {
|
||||
CompilerError.invariant(this.#values.has(value), {
|
||||
reason: `[InferMutationAliasingEffects] Expected value to be initialized at '${printSourceLocation(
|
||||
value.loc,
|
||||
)}'`,
|
||||
reason: `[InferMutationAliasingEffects] Expected value to be initialized`,
|
||||
description: printInstructionValue(value),
|
||||
loc: value.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: value.loc,
|
||||
message: 'Expected value for identifier to be initialized',
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
this.#variables.set(place.identifier.id, new Set([value]));
|
||||
@@ -1707,8 +1976,16 @@ function computeSignatureForInstruction(
|
||||
}
|
||||
case 'PropertyStore':
|
||||
case 'ComputedStore': {
|
||||
/**
|
||||
* Add a hint about naming as "ref"/"-Ref", but only if we weren't able to infer any
|
||||
* type for the object. In some cases the variable may be named like a ref, but is
|
||||
* also used as a ref callback such that we infer the type as a function rather than
|
||||
* a ref.
|
||||
*/
|
||||
const mutationReason: MutationReason | null =
|
||||
value.kind === 'PropertyStore' && value.property === 'current'
|
||||
value.kind === 'PropertyStore' &&
|
||||
value.property === 'current' &&
|
||||
value.object.identifier.type.kind === 'Type'
|
||||
? {kind: 'AssignCurrentProperty'}
|
||||
: null;
|
||||
effects.push({
|
||||
@@ -1876,6 +2153,23 @@ function computeSignatureForInstruction(
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const prop of value.props) {
|
||||
if (
|
||||
prop.kind === 'JsxAttribute' &&
|
||||
prop.place.identifier.type.kind === 'Function' &&
|
||||
(isJsxType(prop.place.identifier.type.return) ||
|
||||
(prop.place.identifier.type.return.kind === 'Phi' &&
|
||||
prop.place.identifier.type.return.operands.some(operand =>
|
||||
isJsxType(operand),
|
||||
)))
|
||||
) {
|
||||
// Any props which return jsx are assumed to be called during render
|
||||
effects.push({
|
||||
kind: 'Render',
|
||||
place: prop.place,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1920,7 +2214,9 @@ function computeSignatureForInstruction(
|
||||
kind: 'Create',
|
||||
into: place,
|
||||
reason: ValueReason.Other,
|
||||
value: ValueKind.Mutable,
|
||||
value: context.nonMutatingSpreads.has(place.identifier.id)
|
||||
? ValueKind.Frozen
|
||||
: ValueKind.Mutable,
|
||||
});
|
||||
effects.push({
|
||||
kind: 'Capture',
|
||||
@@ -2042,11 +2338,11 @@ function computeSignatureForInstruction(
|
||||
kind: 'MutateGlobal',
|
||||
place: value.value,
|
||||
error: CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category:
|
||||
category: ErrorCategory.Globals,
|
||||
reason:
|
||||
'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({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: instr.loc,
|
||||
message: `${variable} cannot be reassigned`,
|
||||
@@ -2075,7 +2371,7 @@ function computeSignatureForInstruction(
|
||||
effects.push({
|
||||
kind: 'Freeze',
|
||||
value: operand,
|
||||
reason: ValueReason.Other,
|
||||
reason: ValueReason.HookCaptured,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2141,20 +2437,40 @@ function computeEffectsForLegacySignature(
|
||||
kind: 'Impure',
|
||||
place: receiver,
|
||||
error: CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot call impure function during render',
|
||||
category: ErrorCategory.Purity,
|
||||
reason: 'Cannot call impure function during render',
|
||||
description:
|
||||
(signature.canonicalName != null
|
||||
? `\`${signature.canonicalName}\` is an impure function. `
|
||||
: '') +
|
||||
'Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
|
||||
}).withDetail({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc,
|
||||
message: 'Cannot call impure function',
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (signature.knownIncompatible != null && state.env.isInferredMemoEnabled) {
|
||||
const errors = new CompilerError();
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.IncompatibleLibrary,
|
||||
reason: 'Use of incompatible library',
|
||||
description: [
|
||||
'This API returns functions which cannot be memoized without leading to stale UI. ' +
|
||||
'To prevent this, by default React Compiler will skip memoizing this component/hook. ' +
|
||||
'However, you may see issues if values from this API are passed to other components/hooks that are ' +
|
||||
'memoized',
|
||||
].join(''),
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: receiver.loc,
|
||||
message: signature.knownIncompatible,
|
||||
}),
|
||||
);
|
||||
throw errors;
|
||||
}
|
||||
const stores: Array<Place> = [];
|
||||
const captures: Array<Place> = [];
|
||||
function visit(place: Place, effect: Effect): void {
|
||||
@@ -2569,3 +2885,208 @@ export type AbstractValue = {
|
||||
kind: ValueKind;
|
||||
reason: ReadonlySet<ValueReason>;
|
||||
};
|
||||
|
||||
export function getWriteErrorReason(abstractValue: AbstractValue): string {
|
||||
if (abstractValue.reason.has(ValueReason.Global)) {
|
||||
return 'Modifying a variable defined outside a component or hook is not allowed. Consider using an effect';
|
||||
} else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
|
||||
return 'Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX';
|
||||
} else if (abstractValue.reason.has(ValueReason.Context)) {
|
||||
return `Modifying a value returned from 'useContext()' is not allowed.`;
|
||||
} else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
|
||||
return 'Modifying a value returned from a function whose return value should not be mutated';
|
||||
} else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
|
||||
return 'Modifying component props or hook arguments is not allowed. Consider using a local variable instead';
|
||||
} 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";
|
||||
} 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";
|
||||
} 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()';
|
||||
} 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';
|
||||
} 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';
|
||||
} else {
|
||||
return 'This modifies a variable that React considers immutable';
|
||||
}
|
||||
}
|
||||
|
||||
function getArgumentEffect(
|
||||
signatureEffect: Effect | null,
|
||||
arg: Place | SpreadPattern,
|
||||
): Effect {
|
||||
if (signatureEffect != null) {
|
||||
if (arg.kind === 'Identifier') {
|
||||
return signatureEffect;
|
||||
} else if (
|
||||
signatureEffect === Effect.Mutate ||
|
||||
signatureEffect === Effect.ConditionallyMutate
|
||||
) {
|
||||
return signatureEffect;
|
||||
} else {
|
||||
// see call-spread-argument-mutable-iterator test fixture
|
||||
if (signatureEffect === Effect.Freeze) {
|
||||
CompilerError.throwTodo({
|
||||
reason: 'Support spread syntax for hook arguments',
|
||||
loc: arg.place.loc,
|
||||
});
|
||||
}
|
||||
// effects[i] is Effect.Capture | Effect.Read | Effect.Store
|
||||
return Effect.ConditionallyMutateIterator;
|
||||
}
|
||||
} else {
|
||||
return Effect.ConditionallyMutate;
|
||||
}
|
||||
}
|
||||
|
||||
export function getFunctionCallSignature(
|
||||
env: Environment,
|
||||
type: Type,
|
||||
): FunctionSignature | null {
|
||||
if (type.kind !== 'Function') {
|
||||
return null;
|
||||
}
|
||||
return env.getFunctionSignature(type);
|
||||
}
|
||||
|
||||
export function isKnownMutableEffect(effect: Effect): boolean {
|
||||
switch (effect) {
|
||||
case Effect.Store:
|
||||
case Effect.ConditionallyMutate:
|
||||
case Effect.ConditionallyMutateIterator:
|
||||
case Effect.Mutate: {
|
||||
return true;
|
||||
}
|
||||
|
||||
case Effect.Unknown: {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected unknown effect',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
case Effect.Read:
|
||||
case Effect.Capture:
|
||||
case Effect.Freeze: {
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(effect, `Unexpected effect \`${effect}\``);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins two values using the following rules:
|
||||
* == Effect Transitions ==
|
||||
*
|
||||
* Freezing an immutable value has not effect:
|
||||
* ┌───────────────┐
|
||||
* │ │
|
||||
* ▼ │ Freeze
|
||||
* ┌──────────────────────────┐ │
|
||||
* │ Immutable │──┘
|
||||
* └──────────────────────────┘
|
||||
*
|
||||
* Freezing a mutable or maybe-frozen value makes it frozen. Freezing a frozen
|
||||
* value has no effect:
|
||||
* ┌───────────────┐
|
||||
* ┌─────────────────────────┐ Freeze │ │
|
||||
* │ MaybeFrozen │────┐ ▼ │ Freeze
|
||||
* └─────────────────────────┘ │ ┌──────────────────────────┐ │
|
||||
* ├────▶│ Frozen │──┘
|
||||
* │ └──────────────────────────┘
|
||||
* ┌─────────────────────────┐ │
|
||||
* │ Mutable │────┘
|
||||
* └─────────────────────────┘
|
||||
*
|
||||
* == Join Lattice ==
|
||||
* - immutable | mutable => mutable
|
||||
* The justification is that immutable and mutable values are different types,
|
||||
* and functions can introspect them to tell the difference (if the argument
|
||||
* is null return early, else if its an object mutate it).
|
||||
* - frozen | mutable => maybe-frozen
|
||||
* Frozen values are indistinguishable from mutable values at runtime, so callers
|
||||
* cannot dynamically avoid mutation of "frozen" values. If a value could be
|
||||
* frozen we have to distinguish it from a mutable value. But it also isn't known
|
||||
* frozen yet, so we distinguish as maybe-frozen.
|
||||
* - immutable | frozen => frozen
|
||||
* This is subtle and falls out of the above rules. If a value could be any of
|
||||
* immutable, mutable, or frozen, then at runtime it could either be a primitive
|
||||
* or a reference type, and callers can't distinguish frozen or not for reference
|
||||
* types. To ensure that any sequence of joins btw those three states yields the
|
||||
* correct maybe-frozen, these two have to produce a frozen value.
|
||||
* - <any> | maybe-frozen => maybe-frozen
|
||||
* - immutable | context => context
|
||||
* - mutable | context => context
|
||||
* - frozen | context => maybe-frozen
|
||||
*
|
||||
* ┌──────────────────────────┐
|
||||
* │ Immutable │───┐
|
||||
* └──────────────────────────┘ │
|
||||
* │ ┌─────────────────────────┐
|
||||
* ├───▶│ Frozen │──┐
|
||||
* ┌──────────────────────────┐ │ └─────────────────────────┘ │
|
||||
* │ Frozen │───┤ │ ┌─────────────────────────┐
|
||||
* └──────────────────────────┘ │ ├─▶│ MaybeFrozen │
|
||||
* │ ┌─────────────────────────┐ │ └─────────────────────────┘
|
||||
* ├───▶│ MaybeFrozen │──┘
|
||||
* ┌──────────────────────────┐ │ └─────────────────────────┘
|
||||
* │ Mutable │───┘
|
||||
* └──────────────────────────┘
|
||||
*/
|
||||
function mergeValueKinds(a: ValueKind, b: ValueKind): ValueKind {
|
||||
if (a === b) {
|
||||
return a;
|
||||
} else if (a === ValueKind.MaybeFrozen || b === ValueKind.MaybeFrozen) {
|
||||
return ValueKind.MaybeFrozen;
|
||||
// after this a and b differ and neither are MaybeFrozen
|
||||
} else if (a === ValueKind.Mutable || b === ValueKind.Mutable) {
|
||||
if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
|
||||
// frozen | mutable
|
||||
return ValueKind.MaybeFrozen;
|
||||
} else if (a === ValueKind.Context || b === ValueKind.Context) {
|
||||
// context | mutable
|
||||
return ValueKind.Context;
|
||||
} else {
|
||||
// mutable | immutable
|
||||
return ValueKind.Mutable;
|
||||
}
|
||||
} else if (a === ValueKind.Context || b === ValueKind.Context) {
|
||||
if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
|
||||
// frozen | context
|
||||
return ValueKind.MaybeFrozen;
|
||||
} else {
|
||||
// context | immutable
|
||||
return ValueKind.Context;
|
||||
}
|
||||
} else if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
|
||||
return ValueKind.Frozen;
|
||||
} else if (a === ValueKind.Global || b === ValueKind.Global) {
|
||||
return ValueKind.Global;
|
||||
} else {
|
||||
CompilerError.invariant(
|
||||
a === ValueKind.Primitive && b == ValueKind.Primitive,
|
||||
{
|
||||
reason: `Unexpected value kind in mergeValues()`,
|
||||
description: `Found kinds ${a} and ${b}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
return ValueKind.Primitive;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
} from '../HIR/visitors';
|
||||
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {AliasingEffect} from './AliasingEffects';
|
||||
import {AliasingEffect, MutationReason} from './AliasingEffects';
|
||||
|
||||
/**
|
||||
* This pass builds an abstract model of the heap and interprets the effects of the
|
||||
@@ -101,6 +101,7 @@ export function inferMutationAliasingRanges(
|
||||
transitive: boolean;
|
||||
kind: MutationKind;
|
||||
place: Place;
|
||||
reason: MutationReason | null;
|
||||
}> = [];
|
||||
const renders: Array<{index: number; place: Place}> = [];
|
||||
|
||||
@@ -176,6 +177,7 @@ export function inferMutationAliasingRanges(
|
||||
effect.kind === 'MutateTransitive'
|
||||
? MutationKind.Definite
|
||||
: MutationKind.Conditional,
|
||||
reason: null,
|
||||
place: effect.value,
|
||||
});
|
||||
} else if (
|
||||
@@ -190,6 +192,7 @@ export function inferMutationAliasingRanges(
|
||||
effect.kind === 'Mutate'
|
||||
? MutationKind.Definite
|
||||
: MutationKind.Conditional,
|
||||
reason: effect.kind === 'Mutate' ? (effect.reason ?? null) : null,
|
||||
place: effect.value,
|
||||
});
|
||||
} else if (
|
||||
@@ -226,7 +229,14 @@ export function inferMutationAliasingRanges(
|
||||
} else {
|
||||
CompilerError.invariant(effect.kind === 'Freeze', {
|
||||
reason: `Unexpected '${effect.kind}' effect for MaybeThrow terminal`,
|
||||
loc: block.terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: block.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -241,6 +251,7 @@ export function inferMutationAliasingRanges(
|
||||
mutation.transitive,
|
||||
mutation.kind,
|
||||
mutation.place.loc,
|
||||
mutation.reason,
|
||||
errors,
|
||||
);
|
||||
}
|
||||
@@ -267,6 +278,7 @@ export function inferMutationAliasingRanges(
|
||||
functionEffects.push({
|
||||
kind: 'Mutate',
|
||||
value: {...place, loc: node.local.loc},
|
||||
reason: node.mutationReason,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -373,7 +385,14 @@ export function inferMutationAliasingRanges(
|
||||
case 'Apply': {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,
|
||||
loc: effect.function.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: effect.function.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
case 'MutateTransitive':
|
||||
@@ -507,6 +526,7 @@ export function inferMutationAliasingRanges(
|
||||
true,
|
||||
MutationKind.Conditional,
|
||||
into.loc,
|
||||
null,
|
||||
ignoredErrors,
|
||||
);
|
||||
for (const from of tracked) {
|
||||
@@ -519,7 +539,14 @@ export function inferMutationAliasingRanges(
|
||||
const fromNode = state.nodes.get(from.identifier);
|
||||
CompilerError.invariant(fromNode != null, {
|
||||
reason: `Expected a node to exist for all parameters and context variables`,
|
||||
loc: into.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: into.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
if (fromNode.lastMutated === mutationIndex) {
|
||||
if (into.identifier.id === fn.returns.identifier.id) {
|
||||
@@ -541,7 +568,7 @@ export function inferMutationAliasingRanges(
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.hasErrors() && !isFunctionExpression) {
|
||||
if (errors.hasAnyErrors() && !isFunctionExpression) {
|
||||
return Err(errors);
|
||||
}
|
||||
return Ok(functionEffects);
|
||||
@@ -580,6 +607,7 @@ type Node = {
|
||||
transitive: {kind: MutationKind; loc: SourceLocation} | null;
|
||||
local: {kind: MutationKind; loc: SourceLocation} | null;
|
||||
lastMutated: number;
|
||||
mutationReason: MutationReason | null;
|
||||
value:
|
||||
| {kind: 'Object'}
|
||||
| {kind: 'Phi'}
|
||||
@@ -599,6 +627,7 @@ class AliasingState {
|
||||
transitive: null,
|
||||
local: null,
|
||||
lastMutated: 0,
|
||||
mutationReason: null,
|
||||
value,
|
||||
});
|
||||
}
|
||||
@@ -697,6 +726,7 @@ class AliasingState {
|
||||
transitive: boolean,
|
||||
startKind: MutationKind,
|
||||
loc: SourceLocation,
|
||||
reason: MutationReason | null,
|
||||
errors: CompilerError,
|
||||
): void {
|
||||
const seen = new Map<Identifier, MutationKind>();
|
||||
@@ -717,6 +747,7 @@ class AliasingState {
|
||||
if (node == null) {
|
||||
continue;
|
||||
}
|
||||
node.mutationReason ??= reason;
|
||||
node.lastMutated = Math.max(node.lastMutated, index);
|
||||
if (end != null) {
|
||||
node.id.mutableRange.end = makeInstructionId(
|
||||
@@ -748,7 +779,13 @@ class AliasingState {
|
||||
if (edge.index >= index) {
|
||||
break;
|
||||
}
|
||||
queue.push({place: edge.node, transitive, direction: 'forwards', kind});
|
||||
queue.push({
|
||||
place: edge.node,
|
||||
transitive,
|
||||
direction: 'forwards',
|
||||
// Traversing a maybeAlias edge always downgrades to conditional mutation
|
||||
kind: edge.kind === 'maybeAlias' ? MutationKind.Conditional : kind,
|
||||
});
|
||||
}
|
||||
for (const [alias, when] of node.createdFrom) {
|
||||
if (when >= index) {
|
||||
@@ -776,7 +813,12 @@ class AliasingState {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push({place: alias, transitive, direction: 'backwards', kind});
|
||||
queue.push({
|
||||
place: alias,
|
||||
transitive,
|
||||
direction: 'backwards',
|
||||
kind,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* MaybeAlias indicates potential data flow from unknown function calls,
|
||||
|
||||
@@ -349,7 +349,13 @@ export function inferReactivePlaces(fn: HIRFunction): void {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected unknown effect',
|
||||
description: null,
|
||||
loc: operand.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: operand.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user