Compare commits
214 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78aa14f401 | ||
|
|
6ee7b65c71 | ||
|
|
d966edf822 | ||
|
|
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 | ||
|
|
724b324b96 | ||
|
|
45a6532a08 | ||
|
|
8dba9311e5 | ||
|
|
2d98b45d92 | ||
|
|
2ba7b07ce1 | ||
|
|
a96a0f3903 | ||
|
|
02a8811864 | ||
|
|
379a083b9a | ||
|
|
534bed5fa7 | ||
|
|
db06f6b751 | ||
|
|
9433fe357a | ||
|
|
0032b2a3ee | ||
|
|
14c50e344c | ||
|
|
f1222f7652 | ||
|
|
9baecbf02b | ||
|
|
0422a00e3e | ||
|
|
47fd2f5e14 | ||
|
|
1dc3bdead1 | ||
|
|
de06211dbe | ||
|
|
ac7820a99e | ||
|
|
3c67bbe5f9 | ||
|
|
2c9a42dfd7 | ||
|
|
f1e70b5e0a | ||
|
|
d587434c35 | ||
|
|
ca292f7a57 | ||
|
|
62a634b972 | ||
|
|
53d07944df | ||
|
|
34ce3acafd | ||
|
|
6445b3154e | ||
|
|
ab5238d5a4 | ||
|
|
7a934a16b8 | ||
|
|
59ef3c4baf | ||
|
|
72965f3615 | ||
|
|
594fb5e9ab | ||
|
|
98286cf8e3 | ||
|
|
cf6e502ed2 | ||
|
|
3958d5d84b | ||
|
|
738aebdbac | ||
|
|
4c9c109cea | ||
|
|
552a5dadcf | ||
|
|
f468d37739 |
@@ -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/
|
||||
|
||||
13
.eslintrc.js
13
.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',
|
||||
},
|
||||
},
|
||||
@@ -546,13 +547,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 +565,7 @@ module.exports = {
|
||||
BigInt: 'readonly',
|
||||
BigInt64Array: 'readonly',
|
||||
BigUint64Array: 'readonly',
|
||||
CacheType: 'readonly',
|
||||
Class: 'readonly',
|
||||
ClientRect: 'readonly',
|
||||
CopyInspectedElementPath: 'readonly',
|
||||
@@ -578,16 +577,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 +621,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()'
|
||||
|
||||
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: {}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
acdlite
|
||||
eps1lon
|
||||
EugeneChoi4
|
||||
gaearon
|
||||
gnoff
|
||||
unstubbable
|
||||
|
||||
@@ -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 Partial<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,15 @@ 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"]');
|
||||
expandButton.click();
|
||||
}
|
||||
|
||||
const TEST_SOURCE = `export default function TestComponent({ x }) {
|
||||
return <Button>{x}</Button>;
|
||||
}`;
|
||||
|
||||
const TEST_CASE_INPUTS = [
|
||||
{
|
||||
name: 'module-scope-use-memo',
|
||||
@@ -121,10 +131,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 +145,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 +154,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 +165,171 @@ 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 Partial<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 Partial<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_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;
|
||||
97
compiler/apps/playground/components/AccordionWindow.tsx
Normal file
97
compiler/apps/playground/components/AccordionWindow.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 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, {useCallback} from 'react';
|
||||
|
||||
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 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>
|
||||
);
|
||||
}
|
||||
|
||||
function AccordionWindowItem({
|
||||
name,
|
||||
tabs,
|
||||
tabsOpen,
|
||||
setTabsOpen,
|
||||
hasChanged,
|
||||
}: {
|
||||
name: string;
|
||||
tabs: TabsRecord;
|
||||
tabsOpen: Set<string>;
|
||||
setTabsOpen: (newTab: Set<string>) => void;
|
||||
hasChanged: boolean;
|
||||
}): React.ReactElement {
|
||||
const isShow = tabsOpen.has(name);
|
||||
|
||||
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');
|
||||
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
218
compiler/apps/playground/components/Editor/ConfigEditor.tsx
Normal file
218
compiler/apps/playground/components/Editor/ConfigEditor.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* 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 {PluginOptions} from 'babel-plugin-react-compiler';
|
||||
import type {editor} from 'monaco-editor';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import React, {useState, useRef} from 'react';
|
||||
import {Resizable} from 're-resizable';
|
||||
import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
import {IconChevron} from '../Icons/IconChevron';
|
||||
import prettyFormat from 'pretty-format';
|
||||
|
||||
// @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({
|
||||
appliedOptions,
|
||||
}: {
|
||||
appliedOptions: PluginOptions | null;
|
||||
}): 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={setIsExpanded}
|
||||
appliedOptions={appliedOptions}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: !isExpanded ? 'block' : 'none',
|
||||
}}>
|
||||
<CollapsedEditor onToggle={setIsExpanded} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ExpandedEditor({
|
||||
onToggle,
|
||||
appliedOptions,
|
||||
}: {
|
||||
onToggle: (expanded: boolean) => void;
|
||||
appliedOptions: PluginOptions | null;
|
||||
}): 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,
|
||||
});
|
||||
};
|
||||
|
||||
const formattedAppliedOptions = appliedOptions
|
||||
? prettyFormat(appliedOptions, {
|
||||
printFunctionName: false,
|
||||
printBasicPrototype: false,
|
||||
})
|
||||
: 'Invalid configs';
|
||||
|
||||
return (
|
||||
<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(false)}
|
||||
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 rounded-lg overflow-hidden border border-gray-300">
|
||||
<MonacoEditor
|
||||
path={'config.ts'}
|
||||
language={'typescript'}
|
||||
value={store.config}
|
||||
onMount={handleMount}
|
||||
onChange={handleChange}
|
||||
loading={''}
|
||||
className="monaco-editor-config"
|
||||
options={{
|
||||
...monacoOptions,
|
||||
lineNumbers: 'off',
|
||||
renderLineHighlight: 'none',
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
fontSize: 12,
|
||||
scrollBeyondLastLine: false,
|
||||
glyphMargin: false,
|
||||
}}
|
||||
/>
|
||||
</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 rounded-lg overflow-hidden border border-gray-300">
|
||||
<MonacoEditor
|
||||
path={'applied-config.js'}
|
||||
language={'javascript'}
|
||||
value={formattedAppliedOptions}
|
||||
loading={''}
|
||||
className="monaco-editor-applied-config"
|
||||
options={{
|
||||
...monacoOptions,
|
||||
lineNumbers: 'off',
|
||||
renderLineHighlight: 'none',
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
fontSize: 12,
|
||||
scrollBeyondLastLine: false,
|
||||
readOnly: true,
|
||||
glyphMargin: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Resizable>
|
||||
);
|
||||
}
|
||||
|
||||
function CollapsedEditor({
|
||||
onToggle,
|
||||
}: {
|
||||
onToggle: (expanded: boolean) => 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(true)}
|
||||
style={{
|
||||
top: '50%',
|
||||
marginTop: '-32px',
|
||||
left: '-8px',
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
}}>
|
||||
<IconChevron displayDirection="right" className="text-blue-50" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import BabelPluginReactCompiler, {
|
||||
CompilerErrorDetail,
|
||||
CompilerDiagnostic,
|
||||
Effect,
|
||||
ErrorSeverity,
|
||||
ErrorCategory,
|
||||
parseConfigPragmaForTests,
|
||||
ValueKind,
|
||||
type Hook,
|
||||
@@ -22,21 +22,11 @@ import BabelPluginReactCompiler, {
|
||||
parsePluginOptions,
|
||||
printReactiveFunctionWithOutlined,
|
||||
printFunctionWithOutlined,
|
||||
type LoggerEvent,
|
||||
} 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 {useStore} from '../StoreContext';
|
||||
import ConfigEditor from './ConfigEditor';
|
||||
import Input from './Input';
|
||||
import {
|
||||
CompilerOutput,
|
||||
@@ -45,7 +35,6 @@ import {
|
||||
PrintedCompilerPipelineValue,
|
||||
} from './Output';
|
||||
import {transformFromAstSync} from '@babel/core';
|
||||
import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint';
|
||||
|
||||
function parseInput(
|
||||
input: string,
|
||||
@@ -142,10 +131,65 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
|
||||
],
|
||||
];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function compile(
|
||||
source: string,
|
||||
mode: 'compiler' | 'linter',
|
||||
): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
configOverrides: string,
|
||||
): [CompilerOutput, 'flow' | 'typescript', PluginOptions | null] {
|
||||
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
|
||||
const error = new CompilerError();
|
||||
const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
|
||||
@@ -164,104 +208,94 @@ function compile(
|
||||
language = 'typescript';
|
||||
}
|
||||
let transformOutput;
|
||||
|
||||
let baseOpts: PluginOptions | null = null;
|
||||
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);
|
||||
baseOpts = parseOptions(source, mode, configOverrides);
|
||||
} 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 {
|
||||
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) {
|
||||
/**
|
||||
* Handle unexpected failures by logging (to get a stack trace)
|
||||
* and reporting
|
||||
* error might be an invariant violation or other runtime error
|
||||
* (i.e. object shape that is not CompilerError)
|
||||
*/
|
||||
console.error(err);
|
||||
error.details.push(
|
||||
new CompilerErrorDetail({
|
||||
severity: ErrorSeverity.Invariant,
|
||||
reason: `Unexpected failure when transforming input! ${err}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
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
|
||||
@@ -269,50 +303,27 @@ function compile(
|
||||
otherErrors.forEach(e => error.details.push(e));
|
||||
}
|
||||
if (error.hasErrors()) {
|
||||
return [{kind: 'err', results, error}, language];
|
||||
return [{kind: 'err', results, error}, language, baseOpts];
|
||||
}
|
||||
return [
|
||||
{kind: 'ok', results, transformOutput, errors: error.details},
|
||||
language,
|
||||
baseOpts,
|
||||
];
|
||||
}
|
||||
|
||||
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},
|
||||
});
|
||||
});
|
||||
|
||||
let mergedOutput: CompilerOutput;
|
||||
let errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
|
||||
if (compilerOutput.kind === 'ok') {
|
||||
@@ -327,12 +338,17 @@ export default function Editor(): JSX.Element {
|
||||
}
|
||||
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 appliedOptions={appliedOptions} />
|
||||
</div>
|
||||
<div className={clsx('flex sm:flex flex-wrap')}>
|
||||
<Output store={deferredStore} compilerOutput={mergedOutput} />
|
||||
<div className="flex flex-1 min-w-0">
|
||||
<div className="flex-1 min-w-[550px] sm:min-w-0">
|
||||
<Input language={language} errors={errors} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-[550px] sm:min-w-0">
|
||||
<Output store={deferredStore} compilerOutput={mergedOutput} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -6,14 +6,17 @@
|
||||
*/
|
||||
|
||||
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 {renderReactCompilerMarkers} from '../../lib/reactCompilerMonacoDiagnostics';
|
||||
import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
import TabbedWindow from '../TabbedWindow';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
// @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';
|
||||
@@ -21,7 +24,7 @@ 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 +45,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 +77,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 +133,38 @@ 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 className="!h-[calc(100vh_-_3.5rem)]">
|
||||
<div className="flex flex-col h-full">
|
||||
<TabbedWindow
|
||||
tabs={tabs}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,13 +21,17 @@ 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 AccordionWindow from '../AccordionWindow';
|
||||
import TabbedWindow from '../TabbedWindow';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
import {BabelFileResult} from '@babel/core';
|
||||
|
||||
const MemoizedOutput = memo(Output);
|
||||
|
||||
export default MemoizedOutput;
|
||||
|
||||
export const BASIC_OUTPUT_TAB_NAMES = ['Output', 'SourceMap'];
|
||||
|
||||
export type PrintedCompilerPipelineValue =
|
||||
| {
|
||||
kind: 'hir';
|
||||
@@ -64,12 +68,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': {
|
||||
@@ -211,6 +219,7 @@ function Output({store, compilerOutput}: Props): JSX.Element {
|
||||
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
|
||||
@@ -222,13 +231,14 @@ function Output({store, compilerOutput}: Props): JSX.Element {
|
||||
if (compilerOutput.kind !== previousOutputKind) {
|
||||
setPreviousOutputKind(compilerOutput.kind);
|
||||
setTabsOpen(new Set(['Output']));
|
||||
setActiveTab('Output');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
tabify(store.source, compilerOutput).then(tabs => {
|
||||
tabify(store.source, compilerOutput, store.showInternals).then(tabs => {
|
||||
setTabs(tabs);
|
||||
});
|
||||
}, [store.source, compilerOutput]);
|
||||
}, [store.source, compilerOutput, store.showInternals]);
|
||||
|
||||
const changedPasses: Set<string> = new Set(['Output', 'HIR']); // Initial and final passes should always be bold
|
||||
let lastResult: string = '';
|
||||
@@ -245,16 +255,24 @@ function Output({store, compilerOutput}: Props): JSX.Element {
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
if (!store.showInternals) {
|
||||
return (
|
||||
<TabbedWindow
|
||||
defaultTab="HIR"
|
||||
setTabsOpen={setTabsOpen}
|
||||
tabsOpen={tabsOpen}
|
||||
tabs={tabs}
|
||||
changedPasses={changedPasses}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AccordionWindow
|
||||
defaultTab={store.showInternals ? 'HIR' : 'Output'}
|
||||
setTabsOpen={setTabsOpen}
|
||||
tabsOpen={tabsOpen}
|
||||
tabs={tabs}
|
||||
changedPasses={changedPasses}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -306,6 +324,7 @@ function TextTabContent({
|
||||
<DiffEditor
|
||||
original={diff}
|
||||
modified={output}
|
||||
loading={''}
|
||||
options={{
|
||||
...monacoOptions,
|
||||
readOnly: true,
|
||||
@@ -320,6 +339,8 @@ function TextTabContent({
|
||||
<MonacoEditor
|
||||
language={language ?? 'javascript'}
|
||||
value={output}
|
||||
loading={''}
|
||||
className="monaco-editor-output"
|
||||
options={{
|
||||
...monacoOptions,
|
||||
readOnly: true,
|
||||
|
||||
@@ -29,4 +29,6 @@ export const monacoOptions: Partial<EditorProps['options']> = {
|
||||
automaticLayout: true,
|
||||
wordWrap: 'on',
|
||||
wrappingIndent: 'same',
|
||||
|
||||
tabSize: 2,
|
||||
};
|
||||
|
||||
@@ -14,10 +14,11 @@ import {useState} 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';
|
||||
|
||||
export default function Header(): JSX.Element {
|
||||
const [showCheck, setShowCheck] = useState(false);
|
||||
const store = useStore();
|
||||
const dispatchStore = useStoreDispatch();
|
||||
const {enqueueSnackbar, closeSnackbar} = useSnackbar();
|
||||
|
||||
@@ -56,6 +57,27 @@ 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={() => 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,40 @@
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
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 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');
|
||||
|
||||
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>
|
||||
)}
|
||||
<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={() => onTabChange(tab)}
|
||||
className={clsx(
|
||||
'active:scale-95 transition-transform py-1.5 px-1.5 xs:px-3 sm:px-4 rounded-full text-sm',
|
||||
!isActive && 'hover:bg-primary/5',
|
||||
isActive && 'bg-highlight text-link',
|
||||
)}>
|
||||
{tab}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden w-full h-full">
|
||||
{tabs.get(activeTab)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
@@ -34,26 +34,30 @@
|
||||
"invariant": "^2.2.4",
|
||||
"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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,471 @@ 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: {
|
||||
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 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 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;
|
||||
|
||||
/**
|
||||
* If true, this rule will automatically appear in the default, "recommended" ESLint
|
||||
* rule set. Otherwise it will be part of an `allRules` export that developers can
|
||||
* use to opt-in to showing output of all possible rules.
|
||||
*
|
||||
* 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!
|
||||
*/
|
||||
recommended: boolean;
|
||||
};
|
||||
|
||||
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',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.CapitalizedCalls: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'capitalized-calls',
|
||||
description:
|
||||
'Validates against calling capitalized functions/methods instead of using JSX',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Config: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'config',
|
||||
description: 'Validates the compiler configuration options',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectDependencies: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'memoized-effect-dependencies',
|
||||
description: 'Validates that effect dependencies are memoized',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectDerivationsOfState: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'no-deriving-state-in-effects',
|
||||
description:
|
||||
'Validates against deriving values from state in an effect',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
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',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
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',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
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',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.FBT: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'fbt',
|
||||
description: 'Validates usage of fbt',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Fire: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'fire',
|
||||
description: 'Validates usage of `fire`',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Gating: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'gating',
|
||||
description:
|
||||
'Validates configuration of [gating mode](https://react.dev/reference/react-compiler/gating)',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
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)',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
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.
|
||||
*/
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
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)',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Invariant: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'invariant',
|
||||
description: 'Internal invariants',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
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)',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
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',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
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)',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
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',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
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',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Suppression: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'rule-suppression',
|
||||
description: 'Validates against suppression of other rules',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Syntax: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'syntax',
|
||||
description: 'Validates against invalid syntax',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Todo: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Hint,
|
||||
name: 'todo',
|
||||
description: 'Unimplemented features',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
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',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
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.',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
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)',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
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,
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import * as t from '@babel/types';
|
||||
import * as TypeErrors from './TypeErrors';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {FlowType} from './FlowTypes';
|
||||
|
||||
export const DEBUG = false;
|
||||
|
||||
@@ -151,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;
|
||||
@@ -166,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;
|
||||
@@ -190,14 +203,18 @@ 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;
|
||||
}
|
||||
|
||||
import {inspect} from 'util';
|
||||
import {FlowType} from './FlowTypes';
|
||||
export function printConcrete<T>(
|
||||
type: ConcreteType<T>,
|
||||
printType: (_: T) => string,
|
||||
@@ -241,7 +258,7 @@ export function printConcrete<T>(
|
||||
case 'Generic':
|
||||
return `T${type.id}`;
|
||||
case 'Object': {
|
||||
const name = `Object ${inspect([...type.members.keys()])}`;
|
||||
const name = `Object [${[...type.members.keys()].map(key => JSON.stringify(key)).join(', ')}]`;
|
||||
return `${name}`;
|
||||
}
|
||||
case 'Tuple': {
|
||||
@@ -400,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,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -469,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,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -488,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,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -501,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':
|
||||
@@ -560,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,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -713,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;
|
||||
@@ -498,7 +505,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 +584,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 +623,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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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([
|
||||
@@ -250,11 +251,6 @@ export const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
flowTypeProvider: z.nullable(z.function().args(z.string())).default(null),
|
||||
|
||||
/**
|
||||
* Enable a new model for mutability and aliasing inference
|
||||
*/
|
||||
enableNewMutationAliasingModel: z.boolean().default(true),
|
||||
|
||||
/**
|
||||
* Enables inference of optional dependency chains. Without this flag
|
||||
* a property chain such as `props?.items?.foo` will infer as a dep on
|
||||
@@ -265,6 +261,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 +621,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.
|
||||
@@ -655,6 +660,20 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* useMemo(() => { ... }, [...]);
|
||||
*/
|
||||
validateNoVoidUseMemo: z.boolean().default(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.
|
||||
*/
|
||||
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 +766,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 +805,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 +823,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 +897,16 @@ 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);
|
||||
const unparsedModuleConfig = moduleTypeProvider(moduleName);
|
||||
if (unparsedModuleConfig != null) {
|
||||
const parsedModuleConfig = TypeSchema.safeParse(unparsedModuleConfig);
|
||||
if (!parsedModuleConfig.success) {
|
||||
@@ -1035,7 +1080,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 +1111,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 +1142,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,7 +7,11 @@
|
||||
|
||||
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';
|
||||
@@ -15,6 +19,7 @@ import {Type, makeType} from './Types';
|
||||
import {z} from 'zod';
|
||||
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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -50,7 +50,7 @@ export type AliasingEffect =
|
||||
/**
|
||||
* Mutate the value and any direct aliases (not captures). Errors if the value is not mutable.
|
||||
*/
|
||||
| {kind: 'Mutate'; value: Place}
|
||||
| {kind: 'Mutate'; value: Place; reason?: MutationReason | null}
|
||||
/**
|
||||
* Mutate the value and any direct aliases (not captures), but only if the value is known mutable.
|
||||
* This should be rare.
|
||||
@@ -174,6 +174,8 @@ export type AliasingEffect =
|
||||
place: Place;
|
||||
};
|
||||
|
||||
export type MutationReason = {kind: 'AssignCurrentProperty'};
|
||||
|
||||
export function hashEffect(effect: AliasingEffect): string {
|
||||
switch (effect.kind) {
|
||||
case 'Apply': {
|
||||
@@ -229,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\`)`,
|
||||
@@ -458,15 +454,15 @@ export function dropManualMemoization(
|
||||
if (!hasNonVoidReturn(funcToCheck.loweredFunc.func)) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'useMemo() callbacks must return a value',
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: '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.`,
|
||||
} callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects`,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: instr.value.loc,
|
||||
message: 'useMemo() callbacks must return a value',
|
||||
@@ -498,11 +494,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 +613,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`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,7 @@ import {
|
||||
DeclarationId,
|
||||
Environment,
|
||||
FunctionExpression,
|
||||
GeneratedSource,
|
||||
HIRFunction,
|
||||
Hole,
|
||||
IdentifierId,
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
InstructionKind,
|
||||
InstructionValue,
|
||||
isArrayType,
|
||||
isJsxType,
|
||||
isMapType,
|
||||
isPrimitiveType,
|
||||
isRefOrRefValue,
|
||||
@@ -34,6 +35,7 @@ import {
|
||||
Phi,
|
||||
Place,
|
||||
SpreadPattern,
|
||||
Type,
|
||||
ValueReason,
|
||||
} from '../HIR';
|
||||
import {
|
||||
@@ -43,12 +45,6 @@ import {
|
||||
eachTerminalSuccessor,
|
||||
} from '../HIR/visitors';
|
||||
import {Ok, Result} from '../Utils/Result';
|
||||
import {
|
||||
getArgumentEffect,
|
||||
getFunctionCallSignature,
|
||||
isKnownMutableEffect,
|
||||
mergeValueKinds,
|
||||
} from './InferReferenceEffects';
|
||||
import {
|
||||
assertExhaustive,
|
||||
getOrInsertDefault,
|
||||
@@ -62,13 +58,17 @@ 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 {AliasingEffect, AliasingSignature, hashEffect} from './AliasingEffects';
|
||||
import {
|
||||
AliasingEffect,
|
||||
AliasingSignature,
|
||||
hashEffect,
|
||||
MutationReason,
|
||||
} from './AliasingEffects';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
@@ -134,7 +134,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;
|
||||
@@ -201,7 +207,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) {
|
||||
@@ -356,7 +368,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) {
|
||||
@@ -445,25 +464,34 @@ function applySignature(
|
||||
const reason = getWriteErrorReason({
|
||||
kind: value.kind,
|
||||
reason: value.reason,
|
||||
context: new Set(),
|
||||
});
|
||||
const variable =
|
||||
effect.value.identifier.name !== null &&
|
||||
effect.value.identifier.name.kind === 'named'
|
||||
? `\`${effect.value.identifier.name.value}\``
|
||||
: 'value';
|
||||
const diagnostic = CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Immutability,
|
||||
reason: 'This value cannot be modified',
|
||||
description: reason,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `${variable} cannot be modified`,
|
||||
});
|
||||
if (
|
||||
effect.kind === 'Mutate' &&
|
||||
effect.reason?.kind === 'AssignCurrentProperty'
|
||||
) {
|
||||
diagnostic.withDetails({
|
||||
kind: 'hint',
|
||||
message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`,
|
||||
});
|
||||
}
|
||||
effects.push({
|
||||
kind: 'MutateFrozen',
|
||||
place: effect.value,
|
||||
error: CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'This value cannot be modified',
|
||||
description: `${reason}.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `${variable} cannot be modified`,
|
||||
}),
|
||||
error: diagnostic,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -497,7 +525,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;
|
||||
@@ -526,7 +561,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);
|
||||
|
||||
@@ -565,7 +606,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);
|
||||
|
||||
@@ -625,7 +672,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);
|
||||
|
||||
@@ -695,11 +748,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,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
/*
|
||||
@@ -708,49 +771,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;
|
||||
}
|
||||
@@ -758,7 +839,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);
|
||||
|
||||
@@ -1027,18 +1114,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`,
|
||||
@@ -1059,13 +1146,30 @@ function applyEffect(
|
||||
const reason = getWriteErrorReason({
|
||||
kind: value.kind,
|
||||
reason: value.reason,
|
||||
context: new Set(),
|
||||
});
|
||||
const variable =
|
||||
effect.value.identifier.name !== null &&
|
||||
effect.value.identifier.name.kind === 'named'
|
||||
? `\`${effect.value.identifier.name.value}\``
|
||||
: 'value';
|
||||
const diagnostic = CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Immutability,
|
||||
reason: 'This value cannot be modified',
|
||||
description: reason,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `${variable} cannot be modified`,
|
||||
});
|
||||
if (
|
||||
effect.kind === 'Mutate' &&
|
||||
effect.reason?.kind === 'AssignCurrentProperty'
|
||||
) {
|
||||
diagnostic.withDetails({
|
||||
kind: 'hint',
|
||||
message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`,
|
||||
});
|
||||
}
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
@@ -1075,15 +1179,7 @@ function applyEffect(
|
||||
? 'MutateFrozen'
|
||||
: 'MutateGlobal',
|
||||
place: effect.value,
|
||||
error: CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'This value cannot be modified',
|
||||
description: `${reason}.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `${variable} cannot be modified`,
|
||||
}),
|
||||
error: diagnostic,
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
@@ -1150,7 +1246,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);
|
||||
@@ -1161,7 +1263,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);
|
||||
@@ -1173,7 +1281,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;
|
||||
@@ -1185,7 +1299,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;
|
||||
@@ -1197,7 +1317,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));
|
||||
@@ -1208,7 +1334,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);
|
||||
@@ -1221,11 +1353,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]));
|
||||
@@ -1680,7 +1816,23 @@ function computeSignatureForInstruction(
|
||||
}
|
||||
case 'PropertyStore':
|
||||
case 'ComputedStore': {
|
||||
effects.push({kind: 'Mutate', value: value.object});
|
||||
/**
|
||||
* 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.object.identifier.type.kind === 'Type'
|
||||
? {kind: 'AssignCurrentProperty'}
|
||||
: null;
|
||||
effects.push({
|
||||
kind: 'Mutate',
|
||||
value: value.object,
|
||||
reason: mutationReason,
|
||||
});
|
||||
effects.push({
|
||||
kind: 'Capture',
|
||||
from: value.value,
|
||||
@@ -1841,6 +1993,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;
|
||||
}
|
||||
@@ -2007,11 +2176,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`,
|
||||
@@ -2040,7 +2209,7 @@ function computeSignatureForInstruction(
|
||||
effects.push({
|
||||
kind: 'Freeze',
|
||||
value: operand,
|
||||
reason: ValueReason.Other,
|
||||
reason: ValueReason.HookCaptured,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2106,20 +2275,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 {
|
||||
@@ -2534,3 +2723,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
@@ -1,49 +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 {BlockId, HIRFunction, Identifier} from '../HIR';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
/*
|
||||
* Any values created within a try/catch block could be aliased to the try handler.
|
||||
* Our lowering ensures that every instruction within a try block will be lowered into a
|
||||
* basic block ending in a maybe-throw terminal that points to its catch block, so we can
|
||||
* iterate such blocks and alias their instruction lvalues to the handler's param (if present).
|
||||
*/
|
||||
export function inferTryCatchAliases(
|
||||
fn: HIRFunction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
const handlerParams: Map<BlockId, Identifier> = new Map();
|
||||
for (const [_, block] of fn.body.blocks) {
|
||||
if (
|
||||
block.terminal.kind === 'try' &&
|
||||
block.terminal.handlerBinding !== null
|
||||
) {
|
||||
handlerParams.set(
|
||||
block.terminal.handler,
|
||||
block.terminal.handlerBinding.identifier,
|
||||
);
|
||||
} else if (block.terminal.kind === 'maybe-throw') {
|
||||
const handlerParam = handlerParams.get(block.terminal.handler);
|
||||
if (handlerParam === undefined) {
|
||||
/*
|
||||
* There's no catch clause param, nothing to alias to so
|
||||
* skip this block
|
||||
*/
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* Otherwise alias all values created in this block to the
|
||||
* catch clause param
|
||||
*/
|
||||
for (const instr of block.instructions) {
|
||||
aliases.union([handlerParam, instr.lvalue.identifier]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,6 @@
|
||||
|
||||
export {default as analyseFunctions} from './AnalyseFunctions';
|
||||
export {dropManualMemoization} from './DropManualMemoization';
|
||||
export {inferMutableRanges} from './InferMutableRanges';
|
||||
export {inferReactivePlaces} from './InferReactivePlaces';
|
||||
export {default as inferReferenceEffects} from './InferReferenceEffects';
|
||||
export {inlineImmediatelyInvokedFunctionExpressions} from './InlineImmediatelyInvokedFunctionExpressions';
|
||||
export {inferEffectDependencies} from './InferEffectDependencies';
|
||||
|
||||
@@ -191,7 +191,14 @@ function evaluatePhi(phi: Phi, constants: Constants): Constant | null {
|
||||
case 'Primitive': {
|
||||
CompilerError.invariant(value.kind === 'Primitive', {
|
||||
reason: 'value kind expected to be Primitive',
|
||||
loc: null,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
@@ -204,7 +211,14 @@ function evaluatePhi(phi: Phi, constants: Constants): Constant | null {
|
||||
case 'LoadGlobal': {
|
||||
CompilerError.invariant(value.kind === 'LoadGlobal', {
|
||||
reason: 'value kind expected to be LoadGlobal',
|
||||
loc: null,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
mapInstructionValueOperands,
|
||||
mapTerminalOperands,
|
||||
} from '../HIR/visitors';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
|
||||
type InlinedJsxDeclarationMap = Map<
|
||||
DeclarationId,
|
||||
@@ -83,6 +84,7 @@ export function inlineJsxTransform(
|
||||
kind: 'CompileDiagnostic',
|
||||
fnLoc: null,
|
||||
detail: {
|
||||
category: ErrorCategory.Todo,
|
||||
reason: 'JSX Inlining is not supported on value blocks',
|
||||
loc: instr.loc,
|
||||
},
|
||||
@@ -707,7 +709,14 @@ function createPropsProperties(
|
||||
const spreadProp = jsxSpreadAttributes[0];
|
||||
CompilerError.invariant(spreadProp.kind === 'JsxSpreadAttribute', {
|
||||
reason: 'Spread prop attribute must be of kind JSXSpreadAttribute',
|
||||
loc: instr.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instr.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
propsProperty = {
|
||||
kind: 'ObjectProperty',
|
||||
|
||||
@@ -78,10 +78,17 @@ export function instructionReordering(fn: HIRFunction): void {
|
||||
}
|
||||
CompilerError.invariant(shared.size === 0, {
|
||||
reason: `InstructionReordering: expected all reorderable nodes to have been emitted`,
|
||||
loc:
|
||||
[...shared.values()]
|
||||
.map(node => node.instruction?.loc)
|
||||
.filter(loc => loc != null)[0] ?? GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc:
|
||||
[...shared.values()]
|
||||
.map(node => node.instruction?.loc)
|
||||
.filter(loc => loc != null)[0] ?? GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
markInstructionIds(fn.body);
|
||||
}
|
||||
@@ -302,7 +309,13 @@ function reorderBlock(
|
||||
node.reorderability === Reorderability.Reorderable,
|
||||
{
|
||||
reason: `Expected all remaining instructions to be reorderable`,
|
||||
loc: node.instruction?.loc ?? block.terminal.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: node.instruction?.loc ?? block.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
description:
|
||||
node.instruction != null
|
||||
? `Instruction [${node.instruction.id}] was not emitted yet but is not reorderable`
|
||||
|
||||
@@ -249,13 +249,13 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
const fn: HIRFunction = {
|
||||
loc: GeneratedSource,
|
||||
id: null,
|
||||
nameHint: null,
|
||||
fnType: 'Other',
|
||||
env,
|
||||
params: [obj],
|
||||
returnTypeAnnotation: null,
|
||||
returns: createTemporaryPlace(env, GeneratedSource),
|
||||
context: [],
|
||||
effects: null,
|
||||
body: {
|
||||
entry: block.id,
|
||||
blocks: new Map([[block.id, block]]),
|
||||
@@ -263,6 +263,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
generator: false,
|
||||
async: false,
|
||||
directives: [],
|
||||
aliasingEffects: [],
|
||||
};
|
||||
|
||||
reversePostorderBlocks(fn.body);
|
||||
@@ -275,6 +276,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
value: {
|
||||
kind: 'FunctionExpression',
|
||||
name: null,
|
||||
nameHint: null,
|
||||
loweredFunc: {
|
||||
func: fn,
|
||||
},
|
||||
|
||||
@@ -31,7 +31,9 @@ export function outlineFunctions(
|
||||
) {
|
||||
const loweredFunc = value.loweredFunc.func;
|
||||
|
||||
const id = fn.env.generateGloballyUniqueIdentifierName(loweredFunc.id);
|
||||
const id = fn.env.generateGloballyUniqueIdentifierName(
|
||||
loweredFunc.id ?? loweredFunc.nameHint,
|
||||
);
|
||||
loweredFunc.id = id.value;
|
||||
|
||||
fn.env.outlineFunction(loweredFunc, null);
|
||||
|
||||
@@ -364,13 +364,13 @@ function emitOutlinedFn(
|
||||
const fn: HIRFunction = {
|
||||
loc: GeneratedSource,
|
||||
id: null,
|
||||
nameHint: null,
|
||||
fnType: 'Other',
|
||||
env,
|
||||
params: [propsObj],
|
||||
returnTypeAnnotation: null,
|
||||
returns: createTemporaryPlace(env, GeneratedSource),
|
||||
context: [],
|
||||
effects: null,
|
||||
body: {
|
||||
entry: block.id,
|
||||
blocks: new Map([[block.id, block]]),
|
||||
@@ -378,6 +378,7 @@ function emitOutlinedFn(
|
||||
generator: false,
|
||||
async: false,
|
||||
directives: [],
|
||||
aliasingEffects: [],
|
||||
};
|
||||
return fn;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,13 @@ export function pruneMaybeThrows(fn: HIRFunction): void {
|
||||
const mappedTerminal = terminalMapping.get(predecessor);
|
||||
CompilerError.invariant(mappedTerminal != null, {
|
||||
reason: `Expected non-existing phi operand's predecessor to have been mapped to a new terminal`,
|
||||
loc: GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
description: `Could not find mapping for predecessor bb${predecessor} in block bb${
|
||||
block.id
|
||||
} for phi ${printPlace(phi.place)}`,
|
||||
|
||||
@@ -41,8 +41,15 @@ function findScopesToMerge(fn: HIRFunction): DisjointSet<ReactiveScope> {
|
||||
{
|
||||
reason:
|
||||
'Internal error: Expected all ObjectExpressions and ObjectMethods to have non-null scope.',
|
||||
description: null,
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
mergeScopesBuilder.union([operandScope, lvalueScope]);
|
||||
|
||||
@@ -170,11 +170,53 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
|
||||
|
||||
CompilerError.invariant(!valueBlockNodes.has(fallthrough), {
|
||||
reason: 'Expect hir blocks to have unique fallthroughs',
|
||||
loc: terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
if (node != null) {
|
||||
valueBlockNodes.set(fallthrough, node);
|
||||
}
|
||||
} else if (terminal.kind === 'goto') {
|
||||
/**
|
||||
* If we encounter a goto that is not to the natural fallthrough of the current
|
||||
* block (not the topmost fallthrough on the stack), then this is a goto to a
|
||||
* label. Any scopes that extend beyond the goto must be extended to include
|
||||
* the labeled range, so that the break statement doesn't accidentally jump
|
||||
* out of the scope. We do this by extending the start and end of the scope's
|
||||
* range to the label and its fallthrough respectively.
|
||||
*/
|
||||
const start = activeBlockFallthroughRanges.find(
|
||||
range => range.fallthrough === terminal.block,
|
||||
);
|
||||
if (start != null && start !== activeBlockFallthroughRanges.at(-1)) {
|
||||
const fallthroughBlock = fn.body.blocks.get(start.fallthrough)!;
|
||||
const firstId =
|
||||
fallthroughBlock.instructions[0]?.id ?? fallthroughBlock.terminal.id;
|
||||
for (const scope of activeScopes) {
|
||||
/**
|
||||
* activeScopes is only filtered at block start points, so some of the
|
||||
* scopes may not actually be active anymore, ie we've past their end
|
||||
* instruction. Only extend ranges for scopes that are actually active.
|
||||
*
|
||||
* TODO: consider pruning activeScopes per instruction
|
||||
*/
|
||||
if (scope.range.end <= terminal.id) {
|
||||
continue;
|
||||
}
|
||||
scope.range.start = makeInstructionId(
|
||||
Math.min(start.range.start, scope.range.start),
|
||||
);
|
||||
scope.range.end = makeInstructionId(
|
||||
Math.max(firstId, scope.range.end),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -217,7 +259,14 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
|
||||
// Transition from block->value block, derive the outer block range
|
||||
CompilerError.invariant(fallthrough !== null, {
|
||||
reason: `Expected a fallthrough for value block`,
|
||||
loc: terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
const fallthroughBlock = fn.body.blocks.get(fallthrough)!;
|
||||
const nextId =
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user