Compare commits

..

3 Commits

Author SHA1 Message Date
Joe Savona
2649d102b8 [compiler][wip] Improve diagnostic infra
Work in progress, i'm experimenting with revamping our diagnostic infra. Starting with a better format for representing errors, with an ability to point ot multiple locations, along with better printing of errors. Of course, Babel still controls the printing in the majority case so this still needs more work.
2025-07-24 14:48:36 -07:00
Joe Savona
1517c63a78 [compiler] Enable additional lints by default
Enable more validations to help catch bad patterns, but only in the linter. These rules are already enabled by default in the compiler _if_ violations could produce unsafe output.
2025-07-24 14:43:03 -07:00
Joe Savona
5fbe88ab40 [compiler] Validate against setState in all effect types 2025-07-24 14:43:03 -07:00
1672 changed files with 23976 additions and 80030 deletions

View File

@@ -1,7 +1,7 @@
{
"packages": ["packages/react", "packages/react-dom", "packages/react-server-dom-webpack", "packages/scheduler"],
"buildCommand": "download-build-in-codesandbox-ci",
"node": "20",
"node": "18",
"publishDirectory": {
"react": "build/oss-experimental/react",
"react-dom": "build/oss-experimental/react-dom",

View File

@@ -28,6 +28,3 @@ 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/

View File

@@ -468,7 +468,6 @@ module.exports = {
files: ['packages/react-server-dom-webpack/**/*.js'],
globals: {
__webpack_chunk_load__: 'readonly',
__webpack_get_script_filename__: 'readonly',
__webpack_require__: 'readonly',
},
},
@@ -517,14 +516,6 @@ module.exports = {
__IS_INTERNAL_VERSION__: 'readonly',
},
},
{
files: ['packages/react-devtools-*/**/*.js'],
excludedFiles: '**/__tests__/**/*.js',
plugins: ['eslint-plugin-react-hooks-published'],
rules: {
'react-hooks-published/rules-of-hooks': ERROR,
},
},
{
files: ['packages/eslint-plugin-react-hooks/src/**/*'],
extends: ['plugin:@typescript-eslint/recommended'],
@@ -555,10 +546,13 @@ module.exports = {
},
globals: {
$Call: 'readonly',
$ElementType: 'readonly',
$Flow$ModuleRef: 'readonly',
$FlowFixMe: 'readonly',
$Keys: 'readonly',
$NonMaybeType: 'readonly',
$PropertyType: 'readonly',
$ReadOnly: 'readonly',
$ReadOnlyArray: 'readonly',
$ArrayBufferView: 'readonly',
@@ -573,7 +567,6 @@ module.exports = {
BigInt: 'readonly',
BigInt64Array: 'readonly',
BigUint64Array: 'readonly',
CacheType: 'readonly',
Class: 'readonly',
ClientRect: 'readonly',
CopyInspectedElementPath: 'readonly',
@@ -585,19 +578,16 @@ 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',
PropertyDescriptorMap: 'readonly',
Proxy$traps: 'readonly',
React$AbstractComponent: 'readonly',
React$Component: 'readonly',
React$ComponentType: 'readonly',
React$Config: 'readonly',
React$Context: 'readonly',
React$Element: 'readonly',
@@ -618,7 +608,6 @@ module.exports = {
symbol: 'readonly',
SyntheticEvent: 'readonly',
SyntheticMouseEvent: 'readonly',
SyntheticPointerEvent: 'readonly',
Thenable: 'readonly',
TimeoutID: 'readonly',
WheelEventHandler: 'readonly',
@@ -629,6 +618,7 @@ module.exports = {
PropertyIndexedKeyframes: 'readonly',
KeyframeAnimationOptions: 'readonly',
GetAnimationsOptions: 'readonly',
Animatable: 'readonly',
ScrollTimeline: 'readonly',
EventListenerOptionsOrUseCapture: 'readonly',
FocusOptions: 'readonly',

View File

@@ -11,7 +11,7 @@ body:
options:
- label: React Compiler core (the JS output is incorrect, or your app works incorrectly after optimization)
- label: babel-plugin-react-compiler (build issue installing or using the Babel plugin)
- label: eslint-plugin-react-hooks (build issue installing or using the eslint plugin)
- label: eslint-plugin-react-compiler (build issue installing or using the eslint plugin)
- label: react-compiler-healthcheck (build issue installing or using the healthcheck script)
- type: input
attributes:

View File

@@ -57,6 +57,8 @@ 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()'

View File

@@ -19,9 +19,6 @@ on:
tag_version:
required: false
type: string
dry_run:
required: false
type: boolean
secrets:
NPM_TOKEN:
required: true
@@ -58,13 +55,7 @@ jobs:
key: compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/yarn.lock') }}
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- if: inputs.dry_run == true
name: Publish packages to npm (dry run)
run: |
cp ./scripts/release/ci-npmrc ~/.npmrc
scripts/release/publish.js --frfr --debug --ci --versionName=${{ inputs.version_name }} --tag=${{ inputs.dist_tag }} ${{ inputs.tag_version && format('--tagVersion={0}', inputs.tag_version) || '' }}
- if: inputs.dry_run != true
name: Publish packages to npm
- name: Publish packages to npm
run: |
cp ./scripts/release/ci-npmrc ~/.npmrc
scripts/release/publish.js --frfr --ci --versionName=${{ inputs.version_name }} --tag=${{ inputs.dist_tag }} ${{ inputs.tag_version && format('--tagVersion={0}', inputs.tag_version) || '' }}

View File

@@ -17,9 +17,6 @@ on:
tag_version:
required: false
type: string
dry_run:
required: false
type: boolean
permissions: {}
@@ -36,6 +33,5 @@ jobs:
dist_tag: ${{ inputs.dist_tag }}
version_name: ${{ inputs.version_name }}
tag_version: ${{ inputs.tag_version }}
dry_run: ${{ inputs.dry_run }}
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -19,6 +19,5 @@ jobs:
release_channel: experimental
dist_tag: experimental
version_name: '0.0.0'
dry_run: false
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -1,49 +0,0 @@
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 }}

View File

@@ -92,7 +92,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: react-devtools
path: build/devtools
path: build/devtools.tgz
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/playwright-artifacts
path: ./tmp/screenshots
if-no-files-found: warn

View File

@@ -194,7 +194,7 @@ jobs:
if: steps.node_modules.outputs.cache-hit != 'true'
- run: |
yarn generate-inline-fizz-runtime
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)
git diff --quiet || (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 --exit-code || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false)
git diff --quiet || (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,11 +766,6 @@ 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
@@ -781,7 +776,7 @@ jobs:
uses: actions/upload-artifact/merge@v4
with:
name: react-devtools
pattern: react-devtools-*
pattern: react-devtools-*-extension
run_devtools_e2e_tests:
name: Run DevTools e2e tests
@@ -816,27 +811,12 @@ jobs:
pattern: _build_*
path: build
merge-multiple: true
- 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: |
npx playwright install
sudo npx playwright install-deps
- 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:

View File

@@ -162,13 +162,10 @@ jobs:
mv build/facebook-react-native/react-is/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-is/
mv build/facebook-react-native/react-test-renderer/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-test-renderer/
# Delete the OSS renderers, these are sync'd to RN separately.
# Delete OSS renderer. OSS renderer is synced through internal script.
RENDERER_FOLDER=$BASE_FOLDER/react-native-github/Libraries/Renderer/implementations/
rm $RENDERER_FOLDER/ReactFabric-{dev,prod,profiling}.js
# Delete the legacy renderer shim, this is not sync'd and will get deleted in the future.
SHIM_FOLDER=$BASE_FOLDER/react-native-github/Libraries/Renderer/shims/
rm $SHIM_FOLDER/ReactNative.js
rm $RENDERER_FOLDER/ReactNativeRenderer-{dev,prod,profiling}.js
# Copy eslint-plugin-react-hooks
# NOTE: This is different from www, here we include the full package

View File

@@ -4,10 +4,8 @@ on:
pull_request_target:
types: [opened, ready_for_review]
paths-ignore:
- packages/react-devtools**
- compiler/**
- .github/workflows/compiler_**.yml
- .github/workflows/devtools**.yml
permissions: {}

View File

@@ -82,6 +82,7 @@ jobs:
run: |
scripts/release/publish.js \
--ci \
--skipTests \
--tags=${{ inputs.dist_tag }} \
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
@@ -90,10 +91,11 @@ jobs:
run: |
scripts/release/publish.js \
--ci \
--skipTests \
--tags=${{ inputs.dist_tag }} \
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
- if: '${{ !inputs.skip_packages && !inputs.only_packages }}'
- if: '${{ !(inputs.skip_packages && inputs.only_packages) }}'
name: 'Publish all packages'
run: |
scripts/release/publish.js \

View File

@@ -18,7 +18,6 @@ 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
View File

@@ -23,7 +23,6 @@ chrome-user-data
.vscode
*.swp
*.swo
/tmp
packages/react-devtools-core/dist
packages/react-devtools-extensions/chrome/build

View File

@@ -1,81 +1,3 @@
## 19.2.0 (October 1st, 2025)
Below is a list of all new features, APIs, and bug fixes.
Read the [React 19.2 release post](https://react.dev/blog/2025/10/01/react-19-2) for more information.
### New React Features
- [`<Activity>`](https://react.dev/reference/react/Activity): A new API to hide and restore the UI and internal state of its children.
- [`useEffectEvent`](https://react.dev/reference/react/useEffectEvent) is a React Hook that lets you extract non-reactive logic into an [Effect Event](https://react.dev/learn/separating-events-from-effects#declaring-an-effect-event).
- [`cacheSignal`](https://react.dev/reference/react/cacheSignal) (for RSCs) lets your know when the `cache()` lifetime is over.
- [React Performance tracks](https://react.dev/reference/dev-tools/react-performance-tracks) appear on the Performance panels timeline in your browser developer tools
### New React DOM Features
- Added resume APIs for partial pre-rendering with Web Streams:
- [`resume`](https://react.dev/reference/react-dom/server/resume): to resume a prerender to a stream.
- [`resumeAndPrerender`](https://react.dev/reference/react-dom/static/resumeAndPrerender): to resume a prerender to HTML.
- Added resume APIs for partial pre-rendering with Node Streams:
- [`resumeToPipeableStream`](https://react.dev/reference/react-dom/server/resumeToPipeableStream): to resume a prerender to a stream.
- [`resumeAndPrerenderToNodeStream`](https://react.dev/reference/react-dom/static/resumeAndPrerenderToNodeStream): to resume a prerender to HTML.
- Updated [`prerender`](https://react.dev/reference/react-dom/static/prerender) APIs to return a `postponed` state that can be passed to the `resume` APIs.
### Notable changes
- React DOM now batches suspense boundary reveals, matching the behavior of client side rendering. This change is especially noticeable when animating the reveal of Suspense boundaries e.g. with the upcoming `<ViewTransition>` Component. React will batch as much reveals as possible before the first paint while trying to hit popular first-contentful paint metrics.
- Add Node Web Streams (`prerender`, `renderToReadableStream`) to server-side-rendering APIs for Node.js
- Use underscore instead of `:` IDs generated by useId
### All Changes
#### React
- `<Activity />` was developed over many years, starting before `ClassComponent.setState` (@acdlite @sebmarkbage and many others)
- Stringify context as "SomeContext" instead of "SomeContext.Provider" (@kassens [#33507](https://github.com/facebook/react/pull/33507))
- Include stack of cause of React instrumentation errors with `%o` placeholder (@eps1lon [#34198](https://github.com/facebook/react/pull/34198))
- Fix infinite `useDeferredValue` loop in popstate event (@acdlite [#32821](https://github.com/facebook/react/pull/32821))
- Fix a bug when an initial value was passed to `useDeferredValue` (@acdlite [#34376](https://github.com/facebook/react/pull/34376))
- Fix a crash when submitting forms with Client Actions (@sebmarkbage [#33055](https://github.com/facebook/react/pull/33055))
- Hide/unhide the content of dehydrated suspense boundaries if they resuspend (@sebmarkbage [#32900](https://github.com/facebook/react/pull/32900))
- Avoid stack overflow on wide trees during Hot Reload (@sophiebits [#34145](https://github.com/facebook/react/pull/34145))
- Improve Owner and Component stacks in various places (@sebmarkbage, @eps1lon: [#33629](https://github.com/facebook/react/pull/33629), [#33724](https://github.com/facebook/react/pull/33724), [#32735](https://github.com/facebook/react/pull/32735), [#33723](https://github.com/facebook/react/pull/33723))
- Add `cacheSignal` (@sebmarkbage [#33557](https://github.com/facebook/react/pull/33557))
#### React DOM
- Block on Suspensey Fonts during reveal of server-side-rendered content (@sebmarkbage [#33342](https://github.com/facebook/react/pull/33342))
- Use underscore instead of `:` for IDs generated by `useId` (@sebmarkbage, @eps1lon: [#32001](https://github.com/facebook/react/pull/32001), [https://github.com/facebook/react/pull/33342](https://github.com/facebook/react/pull/33342)[#33099](https://github.com/facebook/react/pull/33099), [#33422](https://github.com/facebook/react/pull/33422))
- Stop warning when ARIA 1.3 attributes are used (@Abdul-Omira [#34264](https://github.com/facebook/react/pull/34264))
- Allow `nonce` to be used on hoistable styles (@Andarist [#32461](https://github.com/facebook/react/pull/32461))
- Warn for using a React owned node as a Container if it also has text content (@sebmarkbage [#32774](https://github.com/facebook/react/pull/32774))
- s/HTML/text for for error messages if text hydration mismatches (@rickhanlonii [#32763](https://github.com/facebook/react/pull/32763))
- Fix a bug with `React.use` inside `React.lazy`\-ed Component (@hi-ogawa [#33941](https://github.com/facebook/react/pull/33941))
- Enable the `progressiveChunkSize` option for server-side-rendering APIs (@sebmarkbage [#33027](https://github.com/facebook/react/pull/33027))
- Fix a bug with deeply nested Suspense inside Suspense fallback when server-side-rendering (@gnoff [#33467](https://github.com/facebook/react/pull/33467))
- Avoid hanging when suspending after aborting while rendering (@gnoff [#34192](https://github.com/facebook/react/pull/34192))
- Add Node Web Streams to server-side-rendering APIs for Node.js (@sebmarkbage [#33475](https://github.com/facebook/react/pull/33475))
#### React Server Components
- Preload `<img>` and `<link>` using hints before they're rendered (@sebmarkbage [#34604](https://github.com/facebook/react/pull/34604))
- Log error if production elements are rendered during development (@eps1lon [#34189](https://github.com/facebook/react/pull/34189))
- Fix a bug when returning a Temporary reference (e.g. a Client Reference) from Server Functions (@sebmarkbage [#34084](https://github.com/facebook/react/pull/34084), @denk0403 [#33761](https://github.com/facebook/react/pull/33761))
- Pass line/column to `filterStackFrame` (@eps1lon [#33707](https://github.com/facebook/react/pull/33707))
- Support Async Modules in Turbopack Server References (@lubieowoce [#34531](https://github.com/facebook/react/pull/34531))
- Add support for .mjs file extension in Webpack (@jennyscript [#33028](https://github.com/facebook/react/pull/33028))
- Fix a wrong missing key warning (@unstubbable [#34350](https://github.com/facebook/react/pull/34350))
- Make console log resolve in predictable order (@sebmarkbage [#33665](https://github.com/facebook/react/pull/33665))
#### React Reconciler
- [createContainer](https://github.com/facebook/react/blob/v19.2.0/packages/react-reconciler/src/ReactFiberReconciler.js#L255-L261) and [createHydrationContainer](https://github.com/facebook/react/blob/v19.2.0/packages/react-reconciler/src/ReactFiberReconciler.js#L305-L312) had their parameter order adjusted after `on*` handlers to account for upcoming experimental APIs
## 19.1.1 (July 28, 2025)
### React
* Fixed Owner Stacks to work with ES2015 function.name semantics ([#33680](https://github.com/facebook/react/pull/33680) by @hoxyq)
## 19.1.0 (March 28, 2025)
### Owner Stack

View File

@@ -1,6 +1,5 @@
acdlite
eps1lon
EugeneChoi4
gaearon
gnoff
unstubbable

View File

@@ -7,18 +7,18 @@
//
// The @latest channel uses the version as-is, e.g.:
//
// 19.3.0
// 19.1.0
//
// The @canary channel appends additional information, with the scheme
// <version>-<label>-<commit_sha>, e.g.:
//
// 19.3.0-canary-a1c2d3e4
// 19.1.0-canary-a1c2d3e4
//
// The @experimental channel doesn't include a version, only a date and a sha, e.g.:
//
// 0.0.0-experimental-241c4467e-20200129
const ReactVersion = '19.3.0';
const ReactVersion = '19.2.0';
// The label used by the @canary channel. Represents the upcoming release's
// stability. Most of the time, this will be "canary", but we may temporarily
@@ -33,8 +33,8 @@ const canaryChannelLabel = 'canary';
const rcNumber = 0;
const stablePackages = {
'eslint-plugin-react-hooks': '7.1.0',
'jest-react': '0.18.0',
'eslint-plugin-react-hooks': '6.1.0',
'jest-react': '0.17.0',
react: ReactVersion,
'react-art': ReactVersion,
'react-dom': ReactVersion,
@@ -42,12 +42,12 @@ const stablePackages = {
'react-server-dom-turbopack': ReactVersion,
'react-server-dom-parcel': ReactVersion,
'react-is': ReactVersion,
'react-reconciler': '0.34.0',
'react-refresh': '0.19.0',
'react-reconciler': '0.33.0',
'react-refresh': '0.18.0',
'react-test-renderer': ReactVersion,
'use-subscription': '1.13.0',
'use-sync-external-store': '1.7.0',
scheduler: '0.28.0',
'use-subscription': '1.12.0',
'use-sync-external-store': '1.6.0',
scheduler: '0.27.0',
};
// These packages do not exist in the @canary or @latest channel, only

View File

@@ -8,7 +8,6 @@ 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: [

View File

@@ -1,4 +1,5 @@
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;

View File

@@ -1,5 +0,0 @@
import type { PluginOptions } from 
'babel-plugin-react-compiler/dist';
({
  //compilationMode: "all"
} satisfies PluginOptions);

View File

@@ -1,14 +0,0 @@
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;
}

View File

@@ -5,9 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
import {expect, test, type Page} from '@playwright/test';
import {expect, test} from '@playwright/test';
import {encodeStore, type Store} from '../../lib/stores';
import {defaultConfig} from '../../lib/defaultStore';
import {format} from 'prettier';
function isMonacoLoaded(): boolean {
@@ -21,16 +20,6 @@ function formatPrint(data: Array<string>): Promise<string> {
return format(data.join(''), {parser: 'babel'});
}
async function expandConfigs(page: Page): Promise<void> {
const expandButton = page.locator('[title="Expand config editor"]');
await expandButton.click();
await page.waitForSelector('.monaco-editor-config', {state: 'visible'});
}
const TEST_SOURCE = `export default function TestComponent({ x }) {
return <Button>{x}</Button>;
}`;
const TEST_CASE_INPUTS = [
{
name: 'module-scope-use-memo',
@@ -132,9 +121,10 @@ test('editor should open successfully', async ({page}) => {
test('editor should compile from hash successfully', async ({page}) => {
const store: Store = {
source: TEST_SOURCE,
config: defaultConfig,
showInternals: false,
source: `export default function TestComponent({ x }) {
return <Button>{x}</Button>;
}
`,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
@@ -146,7 +136,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-output').allInnerTexts()) ?? [];
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
const output = await formatPrint(text);
expect(output).not.toEqual('');
@@ -155,9 +145,10 @@ test('editor should compile from hash successfully', async ({page}) => {
test('reset button works', async ({page}) => {
const store: Store = {
source: TEST_SOURCE,
config: defaultConfig,
showInternals: false,
source: `export default function TestComponent({ x }) {
return <Button>{x}</Button>;
}
`,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
@@ -166,201 +157,33 @@ 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-output').allInnerTexts()) ?? [];
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
const output = await formatPrint(text);
const configText =
(await page.locator('.monaco-editor-config').allInnerTexts()) ?? [];
const configOutput = configText.join('');
expect(output).not.toEqual('');
expect(output).toMatchSnapshot('02-default-output.txt');
expect(configOutput).not.toEqual('');
expect(configOutput).toMatchSnapshot('default-config.txt');
});
test('defaults load when only source is in Store', async ({page}) => {
// Test for backwards compatibility
const partial = {
source: TEST_SOURCE,
};
const hash = encodeStore(partial as Store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/03-missing-defaults.png',
});
// Config editor has default config
const configText =
(await page.locator('.monaco-editor-config').allInnerTexts()) ?? [];
const configOutput = configText.join('');
expect(configOutput).not.toEqual('');
expect(configOutput).toMatchSnapshot('default-config.txt');
const checkbox = page.locator('label.show-internals');
await expect(checkbox).not.toBeChecked();
const ssaTab = page.locator('text=SSA');
await expect(ssaTab).not.toBeVisible();
});
test('show internals button toggles correctly', async ({page}) => {
await page.goto(`/`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
// show internals should be off
const checkbox = page.locator('label.show-internals');
await checkbox.click();
await page.screenshot({
fullPage: true,
path: 'test-results/04-show-internals-on.png',
});
await expect(checkbox).toBeChecked();
const ssaTab = page.locator('text=SSA');
await expect(ssaTab).toBeVisible();
});
test('error is displayed when config has syntax error', async ({page}) => {
const store: Store = {
source: TEST_SOURCE,
config: `compilationMode: `,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/05-config-syntax-error.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
const output = text.join('');
// Remove hidden chars
expect(output.replace(/\s+/g, ' ')).toContain('Invalid override format');
});
test('error is displayed when config has validation error', async ({page}) => {
const store: Store = {
source: TEST_SOURCE,
config: `import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
({
compilationMode: "123"
} satisfies PluginOptions);`,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/06-config-validation-error.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
const output = text.join('');
expect(output.replace(/\s+/g, ' ')).toContain('Unexpected compilationMode');
});
test('disableMemoizationForDebugging flag works as expected', async ({
page,
}) => {
const store: Store = {
source: TEST_SOURCE,
config: `import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
({
environment: {
disableMemoizationForDebugging: true
}
} satisfies PluginOptions);`,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/07-config-disableMemoizationForDebugging-flag.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
const output = await formatPrint(text);
expect(output).not.toEqual('');
expect(output).toMatchSnapshot('disableMemoizationForDebugging-output.txt');
});
test('error is displayed when source has syntax error', async ({page}) => {
const syntaxErrorSource = `function TestComponent(props) {
const oops = props.
return (
<>{oops}</>
);
}`;
const store: Store = {
source: syntaxErrorSource,
config: defaultConfig,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`);
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/08-source-syntax-error.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
const output = text.join('');
expect(output.replace(/\s+/g, ' ')).toContain(
'Expected identifier to be defined before being used',
);
});
TEST_CASE_INPUTS.forEach((t, idx) =>
test(`playground compiles: ${t.name}`, async ({page}) => {
const store: Store = {
source: t.input,
config: defaultConfig,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await page.screenshot({
fullPage: true,
path: `test-results/08-0${idx}-${t.name}.png`,
path: `test-results/03-0${idx}-${t.name}.png`,
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
let output: string;
if (t.noFormat) {
output = text.join('');

View File

@@ -0,0 +1,56 @@
/**
* 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;

View File

@@ -1,126 +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 {Resizable} from 're-resizable';
import React, {
useId,
unstable_ViewTransition as ViewTransition,
unstable_addTransitionType as addTransitionType,
startTransition,
} from 'react';
import {EXPAND_ACCORDION_TRANSITION} from '../lib/transitionTypes';
type TabsRecord = Map<string, React.ReactNode>;
export default function AccordionWindow(props: {
defaultTab: string | null;
tabs: TabsRecord;
tabsOpen: Set<string>;
setTabsOpen: (newTab: Set<string>) => void;
changedPasses: Set<string>;
}): React.ReactElement {
return (
<div className="flex-1 min-w-[550px] sm:min-w-0">
<div className="flex flex-row h-full">
{Array.from(props.tabs.keys()).map(name => {
return (
<AccordionWindowItem
name={name}
key={name}
tabs={props.tabs}
tabsOpen={props.tabsOpen}
setTabsOpen={props.setTabsOpen}
hasChanged={props.changedPasses.has(name)}
/>
);
})}
</div>
</div>
);
}
function AccordionWindowItem({
name,
tabs,
tabsOpen,
setTabsOpen,
hasChanged,
}: {
name: string;
tabs: TabsRecord;
tabsOpen: Set<string>;
setTabsOpen: (newTab: Set<string>) => void;
hasChanged: boolean;
isFailure: boolean;
}): React.ReactElement {
const id = useId();
const isShow = tabsOpen.has(name);
const transitionName = `accordion-window-item-${id}`;
const toggleTabs = (): void => {
startTransition(() => {
addTransitionType(EXPAND_ACCORDION_TRANSITION);
const nextState = new Set(tabsOpen);
if (nextState.has(name)) {
nextState.delete(name);
} else {
nextState.add(name);
}
setTabsOpen(nextState);
});
};
// Replace spaces with non-breaking spaces
const displayName = name.replace(/ /g, '\u00A0');
return (
<div key={name} className="flex flex-row">
{isShow ? (
<ViewTransition
name={transitionName}
update={{
[EXPAND_ACCORDION_TRANSITION]: 'expand-accordion',
default: 'none',
}}>
<Resizable className="border-r" minWidth={550} enable={{right: true}}>
<h2
title="Minimize tab"
aria-label="Minimize tab"
onClick={toggleTabs}
className={`p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 ${
hasChanged ? 'font-bold' : 'font-light'
} text-secondary hover:text-link`}>
- {displayName}
</h2>
{tabs.get(name) ?? <div>No output for {name}</div>}
</Resizable>
</ViewTransition>
) : (
<ViewTransition
name={transitionName}
update={{
[EXPAND_ACCORDION_TRANSITION]: 'expand-accordion',
default: 'none',
}}>
<div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
<button
title={`Expand compiler tab: ${name}`}
aria-label={`Expand compiler tab: ${name}`}
style={{transform: 'rotate(90deg) translate(-50%)'}}
onClick={toggleTabs}
className={`flex-grow-0 w-5 transition-colors duration-150 ease-in ${
hasChanged ? 'font-bold' : 'font-light'
} text-secondary hover:text-link`}>
{displayName}
</button>
</div>
</ViewTransition>
)}
</div>
);
}

View File

@@ -1,210 +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 MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
import type {editor} from 'monaco-editor';
import * as monaco from 'monaco-editor';
import React, {
useState,
useRef,
unstable_ViewTransition as ViewTransition,
unstable_addTransitionType as addTransitionType,
startTransition,
Activity,
} from 'react';
import {Resizable} from 're-resizable';
import {useStore, useStoreDispatch} from '../StoreContext';
import {monacoConfigOptions} from './monacoOptions';
import {IconChevron} from '../Icons/IconChevron';
import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes';
// @ts-expect-error - webpack asset/source loader handles .d.ts files as strings
import compilerTypeDefs from 'babel-plugin-react-compiler/dist/index.d.ts';
loader.config({monaco});
export default function ConfigEditor({
formattedAppliedConfig,
}: {
formattedAppliedConfig: string;
}): React.ReactElement {
const [isExpanded, setIsExpanded] = useState(false);
return (
<>
<Activity mode={isExpanded ? 'visible' : 'hidden'}>
<ExpandedEditor
onToggle={() => {
startTransition(() => {
addTransitionType(CONFIG_PANEL_TRANSITION);
setIsExpanded(false);
});
}}
formattedAppliedConfig={formattedAppliedConfig}
/>
</Activity>
<Activity mode={isExpanded ? 'hidden' : 'visible'}>
<CollapsedEditor
onToggle={() => {
startTransition(() => {
addTransitionType(CONFIG_PANEL_TRANSITION);
setIsExpanded(true);
});
}}
/>
</Activity>
</>
);
}
function ExpandedEditor({
onToggle,
formattedAppliedConfig,
}: {
onToggle: (expanded: boolean) => void;
formattedAppliedConfig: string;
}): React.ReactElement {
const store = useStore();
const dispatchStore = useStoreDispatch();
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
const handleChange: (value: string | undefined) => void = (
value: string | undefined,
) => {
if (value === undefined) return;
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
debounceTimerRef.current = setTimeout(() => {
dispatchStore({
type: 'updateConfig',
payload: {
config: value,
},
});
}, 500); // 500ms debounce delay
};
const handleMount: (
_: editor.IStandaloneCodeEditor,
monaco: Monaco,
) => void = (_, monaco) => {
// Add the babel-plugin-react-compiler type definitions to Monaco
monaco.languages.typescript.typescriptDefaults.addExtraLib(
//@ts-expect-error - compilerTypeDefs is a string
compilerTypeDefs,
'file:///node_modules/babel-plugin-react-compiler/dist/index.d.ts',
);
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.Latest,
allowNonTsExtensions: true,
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
module: monaco.languages.typescript.ModuleKind.ESNext,
noEmit: true,
strict: false,
esModuleInterop: true,
allowSyntheticDefaultImports: true,
jsx: monaco.languages.typescript.JsxEmit.React,
});
};
return (
<ViewTransition
enter={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}
exit={{[CONFIG_PANEL_TRANSITION]: 'slide-out', default: 'none'}}>
<Resizable
minWidth={300}
maxWidth={600}
defaultSize={{width: 350}}
enable={{right: true, bottom: false}}>
<div className="bg-blue-10 relative h-full flex flex-col !h-[calc(100vh_-_3.5rem)] border border-gray-300">
<div
className="absolute w-8 h-16 bg-blue-10 rounded-r-full flex items-center justify-center z-[2] cursor-pointer border border-l-0 border-gray-300"
title="Minimize config editor"
onClick={onToggle}
style={{
top: '50%',
marginTop: '-32px',
right: '-32px',
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
}}>
<IconChevron displayDirection="left" className="text-blue-50" />
</div>
<div className="flex-1 flex flex-col m-2 mb-2">
<div className="pb-2">
<h2 className="inline-block text-blue-50 py-1.5 px-1.5 xs:px-3 sm:px-4 text-sm">
Config Overrides
</h2>
</div>
<div className="flex-1 border border-gray-300">
<MonacoEditor
path={'config.ts'}
language={'typescript'}
value={store.config}
onMount={handleMount}
onChange={handleChange}
loading={''}
className="monaco-editor-config"
options={monacoConfigOptions}
/>
</div>
</div>
<div className="flex-1 flex flex-col m-2">
<div className="pb-2">
<h2 className="inline-block text-blue-50 py-1.5 px-1.5 xs:px-3 sm:px-4 text-sm">
Applied Configs
</h2>
</div>
<div className="flex-1 border border-gray-300">
<MonacoEditor
path={'applied-config.js'}
language={'javascript'}
value={formattedAppliedConfig}
loading={''}
className="monaco-editor-applied-config"
options={{
...monacoConfigOptions,
readOnly: true,
}}
/>
</div>
</div>
</div>
</Resizable>
</ViewTransition>
);
}
function CollapsedEditor({
onToggle,
}: {
onToggle: () => void;
}): React.ReactElement {
return (
<div
className="w-4 !h-[calc(100vh_-_3.5rem)]"
style={{position: 'relative'}}>
<div
className="absolute w-10 h-16 bg-blue-10 hover:translate-x-2 transition-transform rounded-r-full flex items-center justify-center z-[2] cursor-pointer border border-gray-300"
title="Expand config editor"
onClick={onToggle}
style={{
top: '50%',
marginTop: '-32px',
left: '-8px',
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
}}>
<IconChevron displayDirection="right" className="text-blue-50" />
</div>
</div>
);
}

View File

@@ -5,63 +5,299 @@
* LICENSE file in the root directory of this source tree.
*/
import {
import {parse as babelParse, ParseResult} from '@babel/parser';
import * as HermesParser from 'hermes-parser';
import * as t from '@babel/types';
import BabelPluginReactCompiler, {
CompilerError,
CompilerErrorDetail,
CompilerDiagnostic,
Effect,
ErrorSeverity,
parseConfigPragmaForTests,
ValueKind,
type Hook,
PluginOptions,
CompilerPipelineValue,
parsePluginOptions,
printReactiveFunctionWithOutlined,
printFunctionWithOutlined,
} from 'babel-plugin-react-compiler';
import {useDeferredValue, useMemo, useState} from 'react';
import {useStore} from '../StoreContext';
import ConfigEditor from './ConfigEditor';
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 Input from './Input';
import {CompilerOutput, default as Output} from './Output';
import {compile} from '../../lib/compilation';
import prettyFormat from 'pretty-format';
import {
CompilerOutput,
CompilerTransformOutput,
default as Output,
PrintedCompilerPipelineValue,
} from './Output';
import {transformFromAstSync} from '@babel/core';
import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint';
function parseInput(
input: string,
language: 'flow' | 'typescript',
): ParseResult<t.File> {
// Extract the first line to quickly check for custom test directives
if (language === 'flow') {
return HermesParser.parse(input, {
babel: true,
flow: 'all',
sourceType: 'module',
enableExperimentalComponentSyntax: true,
});
} else {
return babelParse(input, {
plugins: ['typescript', 'jsx'],
sourceType: 'module',
}) as ParseResult<t.File>;
}
}
function invokeCompiler(
source: string,
language: 'flow' | 'typescript',
options: PluginOptions,
): CompilerTransformOutput {
const ast = parseInput(source, language);
let result = transformFromAstSync(ast, source, {
filename: '_playgroundFile.js',
highlightCode: false,
retainLines: true,
plugins: [[BabelPluginReactCompiler, options]],
ast: true,
sourceType: 'module',
configFile: false,
sourceMaps: true,
babelrc: false,
});
if (result?.ast == null || result?.code == null || result?.map == null) {
throw new Error('Expected successful compilation');
}
return {
code: result.code,
sourceMaps: result.map,
language,
};
}
const COMMON_HOOKS: Array<[string, Hook]> = [
[
'useFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'usePaginationFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'useRefetchableFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'useLazyLoadQuery',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'usePreloadedQuery',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
];
function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
const error = new CompilerError();
const otherErrors: Array<CompilerErrorDetail> = [];
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
const entry = results.get(result.name);
if (Array.isArray(entry)) {
entry.push(result);
} else {
results.set(result.name, [result]);
}
};
let language: 'flow' | 'typescript';
if (source.match(/\@flow/)) {
language = 'flow';
} else {
language = 'typescript';
}
let transformOutput;
try {
// Extract the first line to quickly check for custom test directives
const pragma = source.substring(0, source.indexOf('\n'));
const logIR = (result: CompilerPipelineValue): void => {
switch (result.kind) {
case 'ast': {
break;
}
case 'hir': {
upsert({
kind: 'hir',
fnName: result.value.id,
name: result.name,
value: printFunctionWithOutlined(result.value),
});
break;
}
case 'reactive': {
upsert({
kind: 'reactive',
fnName: result.value.id,
name: result.name,
value: printReactiveFunctionWithOutlined(result.value),
});
break;
}
case 'debug': {
upsert({
kind: 'debug',
fnName: null,
name: result.name,
value: result.value,
});
break;
}
default: {
const _: never = result;
throw new Error(`Unhandled result ${result}`);
}
}
};
const parsedOptions = parseConfigPragmaForTests(pragma, {
compilationMode: 'infer',
});
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(new CompilerErrorDetail(event.detail));
}
},
},
});
transformOutput = invokeCompiler(source, language, opts);
} catch (err) {
/**
* error might be an invariant violation or other runtime error
* (i.e. object shape that is not CompilerError)
*/
if (err instanceof CompilerError && err.details.length > 0) {
error.details.push(...err.details);
} else {
/**
* Handle unexpected failures by logging (to get a stack trace)
* and reporting
*/
console.error(err);
error.details.push(
new CompilerErrorDetail({
severity: ErrorSeverity.Invariant,
reason: `Unexpected failure when transforming input! ${err}`,
loc: null,
suggestions: null,
}),
);
}
}
// Only include logger errors if there weren't other errors
if (!error.hasErrors() && otherErrors.length !== 0) {
otherErrors.forEach(e => error.push(e));
}
if (error.hasErrors()) {
return [{kind: 'err', results, error: error}, language];
}
return [{kind: 'ok', results, transformOutput}, language];
}
export default function Editor(): JSX.Element {
const store = useStore();
const deferredStore = useDeferredValue(store);
const [compilerOutput, language, appliedOptions] = useMemo(
() => compile(deferredStore.source, 'compiler', deferredStore.config),
[deferredStore.source, deferredStore.config],
const dispatchStore = useStoreDispatch();
const {enqueueSnackbar} = useSnackbar();
const [compilerOutput, language] = useMemo(
() => compile(deferredStore.source),
[deferredStore.source],
);
const [linterOutput] = useMemo(
() => compile(deferredStore.source, 'linter', deferredStore.config),
[deferredStore.source, deferredStore.config],
);
const [formattedAppliedConfig, setFormattedAppliedConfig] = useState('');
let mergedOutput: CompilerOutput;
let errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
if (compilerOutput.kind === 'ok') {
errors = linterOutput.kind === 'ok' ? [] : linterOutput.error.details;
mergedOutput = {
...compilerOutput,
errors,
};
} else {
mergedOutput = compilerOutput;
errors = compilerOutput.error.details;
}
if (appliedOptions) {
const formatted = prettyFormat(appliedOptions, {
printFunctionName: false,
printBasicPrototype: false,
});
if (formatted !== formattedAppliedConfig) {
setFormattedAppliedConfig(formatted);
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},
});
});
return (
<>
<div className="relative flex top-14">
<div className="flex-shrink-0">
<ConfigEditor formattedAppliedConfig={formattedAppliedConfig} />
<div className="relative flex basis top-14">
<div className={clsx('relative sm:basis-1/4')}>
<Input
language={language}
errors={
compilerOutput.kind === 'err' ? compilerOutput.error.details : []
}
/>
</div>
<div className="flex flex-1 min-w-0">
<Input language={language} errors={errors} />
<Output store={deferredStore} compilerOutput={mergedOutput} />
<div className={clsx('flex sm:flex flex-wrap')}>
<Output store={deferredStore} compilerOutput={compilerOutput} />
</div>
</div>
</>

View File

@@ -6,31 +6,22 @@
*/
import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
import {
CompilerErrorDetail,
CompilerDiagnostic,
} from 'babel-plugin-react-compiler';
import {CompilerErrorDetail} from 'babel-plugin-react-compiler';
import invariant from 'invariant';
import type {editor} from 'monaco-editor';
import * as monaco from 'monaco-editor';
import {
useEffect,
useState,
unstable_ViewTransition as ViewTransition,
} from 'react';
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';
import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes';
// @ts-expect-error TODO: Make TS recognize .d.ts files, in addition to loading them with webpack.
import React$Types from '../../node_modules/@types/react/index.d.ts';
loader.config({monaco});
type Props = {
errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
errors: Array<CompilerErrorDetail>;
language: 'flow' | 'typescript';
};
@@ -45,13 +36,13 @@ export default function Input({errors, language}: Props): JSX.Element {
const uri = monaco.Uri.parse(`file:///index.js`);
const model = monaco.editor.getModel(uri);
invariant(model, 'Model must exist for the selected input file.');
renderReactCompilerMarkers({
monaco,
model,
details: errors,
source: store.source,
});
}, [monaco, errors, store.source]);
renderReactCompilerMarkers({monaco, model, details: errors});
/**
* 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]);
useEffect(() => {
/**
@@ -83,11 +74,11 @@ export default function Input({errors, language}: Props): JSX.Element {
});
}, [monaco, language]);
const handleChange: (value: string | undefined) => void = async value => {
const handleChange: (value: string | undefined) => void = value => {
if (!value) return;
dispatchStore({
type: 'updateSource',
type: 'updateFile',
payload: {
source: value,
},
@@ -139,42 +130,30 @@ 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 (
<ViewTransition
update={{
[CONFIG_PANEL_TRANSITION]: 'container',
default: 'none',
}}>
<div className="flex-1 min-w-[550px] sm:min-w-0">
<div className="flex flex-col h-full !h-[calc(100vh_-_3.5rem)] border-r border-gray-200">
<TabbedWindow
tabs={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
/>
</div>
</div>
</ViewTransition>
<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>
);
}

View File

@@ -11,46 +11,19 @@ import {
InformationCircleIcon,
} from '@heroicons/react/outline';
import MonacoEditor, {DiffEditor} from '@monaco-editor/react';
import {
CompilerErrorDetail,
CompilerDiagnostic,
type CompilerError,
} from 'babel-plugin-react-compiler';
import {type CompilerError} from 'babel-plugin-react-compiler';
import parserBabel from 'prettier/plugins/babel';
import * as prettierPluginEstree from 'prettier/plugins/estree';
import * as prettier from 'prettier/standalone';
import {memo, ReactNode, useEffect, useState} from 'react';
import {type Store} from '../../lib/stores';
import {
memo,
ReactNode,
use,
useState,
Suspense,
unstable_ViewTransition as ViewTransition,
unstable_addTransitionType as addTransitionType,
startTransition,
} from 'react';
import AccordionWindow from '../AccordionWindow';
import TabbedWindow from '../TabbedWindow';
import {monacoOptions} from './monacoOptions';
import {BabelFileResult} from '@babel/core';
import {
CONFIG_PANEL_TRANSITION,
TOGGLE_INTERNALS_TRANSITION,
EXPAND_ACCORDION_TRANSITION,
} from '../../lib/transitionTypes';
import {LRUCache} from 'lru-cache';
const MemoizedOutput = memo(Output);
export default MemoizedOutput;
export const BASIC_OUTPUT_TAB_NAMES = ['Output', 'SourceMap'];
const tabifyCache = new LRUCache<Store, Promise<Map<string, ReactNode>>>({
max: 5,
});
export type PrintedCompilerPipelineValue =
| {
kind: 'hir';
@@ -71,7 +44,6 @@ export type CompilerOutput =
kind: 'ok';
transformOutput: CompilerTransformOutput;
results: Map<string, Array<PrintedCompilerPipelineValue>>;
errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
}
| {
kind: 'err';
@@ -87,16 +59,12 @@ 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': {
@@ -155,36 +123,10 @@ async function tabify(
parser: transformOutput.language === 'flow' ? 'babel-flow' : 'babel-ts',
plugins: [parserBabel, prettierPluginEstree],
});
let output: string;
let language: string;
if (compilerOutput.errors.length === 0) {
output = code;
language = 'javascript';
} else {
language = 'markdown';
output = `
# Summary
React Compiler compiled this function successfully, but there are lint errors that indicate potential issues with the original code.
## ${compilerOutput.errors.length} Lint Errors
${compilerOutput.errors.map(e => e.printErrorMessage(source, {eslint: false})).join('\n\n')}
## Output
\`\`\`js
${code}
\`\`\`
`.trim();
}
reorderedTabs.set(
'Output',
'JS',
<TextTabContent
output={output}
language={language}
output={code}
diff={null}
showInfoPanel={false}></TextTabContent>,
);
@@ -200,18 +142,6 @@ ${code}
</>,
);
}
} else if (compilerOutput.kind === 'err') {
const errors = compilerOutput.error.printErrorMessage(source, {
eslint: false,
});
reorderedTabs.set(
'Output',
<TextTabContent
output={errors}
language="markdown"
diff={null}
showInfoPanel={false}></TextTabContent>,
);
}
tabs.forEach((tab, name) => {
reorderedTabs.set(name, tab);
@@ -219,25 +149,6 @@ ${code}
return reorderedTabs;
}
function tabifyCached(
store: Store,
compilerOutput: CompilerOutput,
): Promise<Map<string, ReactNode>> {
const cached = tabifyCache.get(store);
if (cached) return cached;
const result = tabify(store.source, compilerOutput, store.showInternals);
tabifyCache.set(store, result);
return result;
}
function Fallback(): JSX.Element {
return (
<div className="w-full h-monaco_small sm:h-monaco flex items-center justify-center">
Loading...
</div>
);
}
function utf16ToUTF8(s: string): string {
return unescape(encodeURIComponent(s));
}
@@ -251,40 +162,17 @@ function getSourceMapUrl(code: string, map: string): string | null {
}
function Output({store, compilerOutput}: Props): JSX.Element {
return (
<Suspense fallback={<Fallback />}>
<OutputContent store={store} compilerOutput={compilerOutput} />
</Suspense>
const [tabsOpen, setTabsOpen] = useState<Set<string>>(() => new Set(['JS']));
const [tabs, setTabs] = useState<Map<string, React.ReactNode>>(
() => new Map(),
);
}
useEffect(() => {
tabify(store.source, compilerOutput).then(tabs => {
setTabs(tabs);
});
}, [store.source, compilerOutput]);
function OutputContent({store, compilerOutput}: Props): JSX.Element {
const [tabsOpen, setTabsOpen] = useState<Set<string>>(
() => new Set(['Output']),
);
const [activeTab, setActiveTab] = useState<string>('Output');
/*
* Update the active tab back to the output or errors tab when the compilation state
* changes between success/failure.
*/
const [previousOutputKind, setPreviousOutputKind] = useState(
compilerOutput.kind,
);
const isFailure = compilerOutput.kind !== 'ok';
if (compilerOutput.kind !== previousOutputKind) {
setPreviousOutputKind(compilerOutput.kind);
if (isFailure) {
startTransition(() => {
addTransitionType(EXPAND_ACCORDION_TRANSITION);
setTabsOpen(prev => new Set(prev).add('Output'));
setActiveTab('Output');
});
}
}
const changedPasses: Set<string> = new Set(['Output', 'HIR']); // Initial and final passes should always be bold
const changedPasses: Set<string> = new Set(['JS', 'HIR']); // Initial and final passes should always be bold
let lastResult: string = '';
for (const [passName, results] of compilerOutput.results) {
for (const result of results) {
@@ -298,40 +186,31 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element {
lastResult = currResult;
}
}
const tabs = use(tabifyCached(store, compilerOutput));
if (!store.showInternals) {
return (
<ViewTransition
update={{
[CONFIG_PANEL_TRANSITION]: 'container',
[TOGGLE_INTERNALS_TRANSITION]: '',
default: 'none',
}}>
<TabbedWindow
tabs={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
/>
</ViewTransition>
);
}
return (
<ViewTransition
update={{
[CONFIG_PANEL_TRANSITION]: 'accordion-container',
[TOGGLE_INTERNALS_TRANSITION]: '',
default: 'none',
}}>
<AccordionWindow
defaultTab={store.showInternals ? 'HIR' : 'Output'}
<>
<TabbedWindow
defaultTab="HIR"
setTabsOpen={setTabsOpen}
tabsOpen={tabsOpen}
tabs={tabs}
changedPasses={changedPasses}
/>
</ViewTransition>
{compilerOutput.kind === 'err' ? (
<div
className="flex flex-wrap absolute bottom-0 bg-white grow border-y border-grey-200 transition-all ease-in"
style={{width: 'calc(100vw - 650px)'}}>
<div className="w-full p-4 basis-full border-b">
<h2>COMPILER ERRORS</h2>
</div>
<pre
className="p-4 basis-full text-red-600 overflow-y-scroll whitespace-pre-wrap"
style={{width: 'calc(100vw - 650px)', height: '150px'}}>
<code>{compilerOutput.error.toString()}</code>
</pre>
</div>
) : null}
</>
);
}
@@ -339,12 +218,10 @@ function TextTabContent({
output,
diff,
showInfoPanel,
language,
}: {
output: string;
diff: string | null;
showInfoPanel: boolean;
language: string;
}): JSX.Element {
const [diffMode, setDiffMode] = useState(false);
return (
@@ -383,29 +260,20 @@ function TextTabContent({
<DiffEditor
original={diff}
modified={output}
loading={''}
options={{
...monacoOptions,
scrollbar: {
vertical: 'hidden',
},
dimension: {
width: 0,
height: 0,
},
readOnly: true,
lineNumbers: 'off',
glyphMargin: false,
// Undocumented see https://github.com/Microsoft/vscode/issues/30795#issuecomment-410998882
overviewRulerLanes: 0,
lineDecorationsWidth: 0,
lineNumbersMinChars: 0,
}}
/>
) : (
<MonacoEditor
language={language ?? 'javascript'}
defaultLanguage="javascript"
value={output}
loading={''}
className="monaco-editor-output"
options={{
...monacoOptions,
readOnly: true,

View File

@@ -28,18 +28,5 @@ export const monacoOptions: Partial<EditorProps['options']> = {
automaticLayout: true,
wordWrap: 'on',
wrappingIndent: 'same',
tabSize: 2,
};
export const monacoConfigOptions: Partial<EditorProps['options']> = {
...monacoOptions,
lineNumbers: 'off',
renderLineHighlight: 'none',
overviewRulerBorder: false,
overviewRulerLanes: 0,
fontSize: 12,
scrollBeyondLastLine: false,
glyphMargin: false,
wrappingIndent: 'deepIndent',
};

View File

@@ -10,20 +10,14 @@ import {CheckIcon} from '@heroicons/react/solid';
import clsx from 'clsx';
import Link from 'next/link';
import {useSnackbar} from 'notistack';
import {
useState,
startTransition,
unstable_addTransitionType as addTransitionType,
} from 'react';
import {useState} from 'react';
import {defaultStore} from '../lib/defaultStore';
import {IconGitHub} from './Icons/IconGitHub';
import Logo from './Logo';
import {useStore, useStoreDispatch} from './StoreContext';
import {TOGGLE_INTERNALS_TRANSITION} from '../lib/transitionTypes';
import {useStoreDispatch} from './StoreContext';
export default function Header(): JSX.Element {
const [showCheck, setShowCheck] = useState(false);
const store = useStore();
const dispatchStore = useStoreDispatch();
const {enqueueSnackbar, closeSnackbar} = useSnackbar();
@@ -62,32 +56,6 @@ export default function Header(): JSX.Element {
<p className="hidden select-none sm:block">React Compiler Playground</p>
</div>
<div className="flex items-center text-[15px] gap-4">
<div className="flex items-center gap-2">
<label className="show-internals relative inline-block w-[34px] h-5">
<input
type="checkbox"
checked={store.showInternals}
onChange={() =>
startTransition(() => {
addTransitionType(TOGGLE_INTERNALS_TRANSITION);
dispatchStore({type: 'toggleInternals'});
})
}
className="absolute opacity-0 cursor-pointer h-full w-full m-0"
/>
<span
className={clsx(
'absolute inset-0 rounded-full cursor-pointer transition-all duration-250',
"before:content-[''] before:absolute before:w-4 before:h-4 before:left-0.5 before:bottom-0.5",
'before:bg-white before:rounded-full before:transition-transform before:duration-250',
'focus-within:shadow-[0_0_1px_#2196F3]',
store.showInternals
? 'bg-link before:translate-x-3.5'
: 'bg-gray-300',
)}></span>
</label>
<span className="text-secondary">Show Internals</span>
</div>
<button
title="Reset Playground"
aria-label="Reset Playground"

View File

@@ -1,41 +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 {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>
);
});

View File

@@ -6,14 +6,10 @@
*/
import type {Dispatch, ReactNode} from 'react';
import {useState, useEffect, useReducer} from 'react';
import {useEffect, useReducer} from 'react';
import createContext from '../lib/createContext';
import {emptyStore, defaultStore} from '../lib/defaultStore';
import {
saveStore,
initStoreFromUrlOrLocalStorage,
type Store,
} from '../lib/stores';
import {emptyStore} from '../lib/defaultStore';
import {saveStore, type Store} from '../lib/stores';
const StoreContext = createContext<Store>();
@@ -34,20 +30,6 @@ 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);
@@ -57,7 +39,7 @@ export function StoreProvider({children}: {children: ReactNode}): JSX.Element {
return (
<StoreContext.Provider value={store}>
<StoreDispatchContext.Provider value={dispatch}>
{isPageReady ? children : null}
{children}
</StoreDispatchContext.Provider>
</StoreContext.Provider>
);
@@ -71,19 +53,10 @@ type ReducerAction =
};
}
| {
type: 'updateSource';
type: 'updateFile';
payload: {
source: string;
};
}
| {
type: 'updateConfig';
payload: {
config: string;
};
}
| {
type: 'toggleInternals';
};
function storeReducer(store: Store, action: ReducerAction): Store {
@@ -92,28 +65,13 @@ function storeReducer(store: Store, action: ReducerAction): Store {
const newStore = action.payload.store;
return newStore;
}
case 'updateSource': {
const source = action.payload.source;
case 'updateFile': {
const {source} = action.payload;
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;
}
}
}

View File

@@ -4,78 +4,103 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React, {
startTransition,
useId,
unstable_ViewTransition as ViewTransition,
unstable_addTransitionType as addTransitionType,
} from 'react';
import clsx from 'clsx';
import {TOGGLE_TAB_TRANSITION} from '../lib/transitionTypes';
export default function TabbedWindow({
tabs,
activeTab,
onTabChange,
}: {
tabs: Map<string, React.ReactNode>;
activeTab: string;
onTabChange: (tab: string) => void;
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 {
const id = useId();
const transitionName = `tab-highlight-${id}`;
const handleTabChange = (tab: string): void => {
startTransition(() => {
addTransitionType(TOGGLE_TAB_TRANSITION);
onTabChange(tab);
});
};
return (
<div className="flex-1 min-w-[550px] sm:min-w-0">
<div className="flex flex-col h-full max-w-full">
<div className="flex p-2 flex-shrink-0">
{Array.from(tabs.keys()).map(tab => {
const isActive = activeTab === tab;
return (
<button
key={tab}
onClick={() => handleTabChange(tab)}
className={clsx(
'transition-transform py-1.5 px-1.5 xs:px-3 sm:px-4 rounded-full text-sm relative',
isActive ? 'text-link' : 'hover:bg-primary/5',
)}>
{isActive && (
<ViewTransition
name={transitionName}
enter={{default: 'none'}}
exit={{default: 'none'}}
share={{
[TOGGLE_TAB_TRANSITION]: 'tab-highlight',
default: 'none',
}}
update={{default: 'none'}}>
<div className="absolute inset-0 bg-highlight rounded-full" />
</ViewTransition>
)}
<ViewTransition
enter={{default: 'none'}}
exit={{default: 'none'}}
update={{
[TOGGLE_TAB_TRANSITION]: 'tab-text',
default: 'none',
}}>
<span className="relative z-1">{tab}</span>
</ViewTransition>
</button>
);
})}
</div>
<div className="flex-1 overflow-hidden w-full h-full">
{tabs.get(activeTab)}
</div>
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,
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>
);
}

View File

@@ -1,308 +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 {parse as babelParse, ParseResult} from '@babel/parser';
import * as HermesParser from 'hermes-parser';
import * as t from '@babel/types';
import BabelPluginReactCompiler, {
CompilerError,
CompilerErrorDetail,
CompilerDiagnostic,
Effect,
ErrorCategory,
parseConfigPragmaForTests,
ValueKind,
type Hook,
PluginOptions,
CompilerPipelineValue,
parsePluginOptions,
printReactiveFunctionWithOutlined,
printFunctionWithOutlined,
type LoggerEvent,
} from 'babel-plugin-react-compiler';
import {transformFromAstSync} from '@babel/core';
import type {
CompilerOutput,
CompilerTransformOutput,
PrintedCompilerPipelineValue,
} from '../components/Editor/Output';
function parseInput(
input: string,
language: 'flow' | 'typescript',
): ParseResult<t.File> {
// Extract the first line to quickly check for custom test directives
if (language === 'flow') {
return HermesParser.parse(input, {
babel: true,
flow: 'all',
sourceType: 'module',
enableExperimentalComponentSyntax: true,
});
} else {
return babelParse(input, {
plugins: ['typescript', 'jsx'],
sourceType: 'module',
}) as ParseResult<t.File>;
}
}
function invokeCompiler(
source: string,
language: 'flow' | 'typescript',
options: PluginOptions,
): CompilerTransformOutput {
const ast = parseInput(source, language);
let result = transformFromAstSync(ast, source, {
filename: '_playgroundFile.js',
highlightCode: false,
retainLines: true,
plugins: [[BabelPluginReactCompiler, options]],
ast: true,
sourceType: 'module',
configFile: false,
sourceMaps: true,
babelrc: false,
});
if (result?.ast == null || result?.code == null || result?.map == null) {
throw new Error('Expected successful compilation');
}
return {
code: result.code,
sourceMaps: result.map,
language,
};
}
const COMMON_HOOKS: Array<[string, Hook]> = [
[
'useFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'usePaginationFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'useRefetchableFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'useLazyLoadQuery',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'usePreloadedQuery',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
];
function parseOptions(
source: string,
mode: 'compiler' | 'linter',
configOverrides: string,
): PluginOptions {
// Extract the first line to quickly check for custom test directives
const pragma = source.substring(0, source.indexOf('\n'));
const parsedPragmaOptions = parseConfigPragmaForTests(pragma, {
compilationMode: 'infer',
environment:
mode === 'linter'
? {
// enabled in compiler
validateRefAccessDuringRender: false,
// enabled in linter
validateNoSetStateInRender: true,
validateNoSetStateInEffects: true,
validateNoJSXInTryStatements: true,
validateNoImpureFunctionsInRender: true,
validateStaticComponents: true,
validateNoFreezingKnownMutableFunctions: true,
validateNoVoidUseMemo: true,
}
: {
/* use defaults for compiler mode */
},
});
// Parse config overrides from config editor
let configOverrideOptions: any = {};
const configMatch = configOverrides.match(/^\s*import.*?\n\n\((.*)\)/s);
if (configOverrides.trim()) {
if (configMatch && configMatch[1]) {
const configString = configMatch[1].replace(/satisfies.*$/, '').trim();
configOverrideOptions = new Function(`return (${configString})`)();
} else {
throw new Error('Invalid override format');
}
}
const opts: PluginOptions = parsePluginOptions({
...parsedPragmaOptions,
...configOverrideOptions,
environment: {
...parsedPragmaOptions.environment,
...configOverrideOptions.environment,
customHooks: new Map([...COMMON_HOOKS]),
},
});
return opts;
}
export function compile(
source: string,
mode: 'compiler' | 'linter',
configOverrides: string,
): [CompilerOutput, 'flow' | 'typescript', PluginOptions | null] {
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
const error = new CompilerError();
const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
const entry = results.get(result.name);
if (Array.isArray(entry)) {
entry.push(result);
} else {
results.set(result.name, [result]);
}
};
let language: 'flow' | 'typescript';
if (source.match(/\@flow/)) {
language = 'flow';
} else {
language = 'typescript';
}
let transformOutput;
let baseOpts: PluginOptions | null = null;
try {
baseOpts = parseOptions(source, mode, configOverrides);
} catch (err) {
error.details.push(
new CompilerErrorDetail({
category: ErrorCategory.Config,
reason: `Unexpected failure when transforming configs! \n${err}`,
loc: null,
suggestions: null,
}),
);
}
if (baseOpts) {
try {
const logIR = (result: CompilerPipelineValue): void => {
switch (result.kind) {
case 'ast': {
break;
}
case 'hir': {
upsert({
kind: 'hir',
fnName: result.value.id,
name: result.name,
value: printFunctionWithOutlined(result.value),
});
break;
}
case 'reactive': {
upsert({
kind: 'reactive',
fnName: result.value.id,
name: result.name,
value: printReactiveFunctionWithOutlined(result.value),
});
break;
}
case 'debug': {
upsert({
kind: 'debug',
fnName: null,
name: result.name,
value: result.value,
});
break;
}
default: {
const _: never = result;
throw new Error(`Unhandled result ${result}`);
}
}
};
// Add logger options to the parsed options
const opts = {
...baseOpts,
logger: {
debugLogIRs: logIR,
logEvent: (_filename: string | null, event: LoggerEvent): void => {
if (event.kind === 'CompileError') {
otherErrors.push(event.detail);
}
},
},
};
transformOutput = invokeCompiler(source, language, opts);
} catch (err) {
/**
* error might be an invariant violation or other runtime error
* (i.e. object shape that is not CompilerError)
*/
if (err instanceof CompilerError && err.details.length > 0) {
error.merge(err);
} else {
/**
* Handle unexpected failures by logging (to get a stack trace)
* and reporting
*/
error.details.push(
new CompilerErrorDetail({
category: ErrorCategory.Invariant,
reason: `Unexpected failure when transforming input! \n${err}`,
loc: null,
suggestions: null,
}),
);
}
}
}
// Only include logger errors if there weren't other errors
if (!error.hasErrors() && otherErrors.length !== 0) {
otherErrors.forEach(e => error.details.push(e));
}
if (error.hasErrors() || !transformOutput) {
return [{kind: 'err', results, error}, language, baseOpts];
}
return [
{kind: 'ok', results, transformOutput, errors: error.details},
language,
baseOpts,
];
}

View File

@@ -13,21 +13,10 @@ 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,
};

View File

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

View File

@@ -10,20 +10,18 @@ import {
compressToEncodedURIComponent,
decompressFromEncodedURIComponent,
} from 'lz-string';
import {defaultStore, defaultConfig} from '../defaultStore';
import {defaultStore} 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): any {
export function decodeStore(hash: string): Store {
return JSON.parse(decompressFromEncodedURIComponent(hash));
}
@@ -64,14 +62,8 @@ export function initStoreFromUrlOrLocalStorage(): Store {
*/
if (!encodedSource) return defaultStore;
const raw: any = decodeStore(encodedSource);
const raw = decodeStore(encodedSource);
invariant(isValidStore(raw), 'Invalid Store');
// 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,
};
return raw;
}

View File

@@ -1,11 +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.
*/
export const CONFIG_PANEL_TRANSITION = 'config-panel';
export const TOGGLE_TAB_TRANSITION = 'toggle-tab';
export const TOGGLE_INTERNALS_TRANSITION = 'toggle-internals';
export const EXPAND_ACCORDION_TRANSITION = 'open-accordion';

View File

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

View File

@@ -11,7 +11,6 @@ const path = require('path');
const nextConfig = {
experimental: {
reactCompiler: true,
viewTransition: true,
},
reactStrictMode: true,
webpack: (config, options) => {

View File

@@ -26,39 +26,34 @@
"@babel/traverse": "^7.18.9",
"@babel/types": "7.26.3",
"@heroicons/react": "^1.0.6",
"@monaco-editor/react": "^4.8.0-rc.2",
"@playwright/test": "^1.56.1",
"@monaco-editor/react": "^4.4.6",
"@playwright/test": "^1.51.1",
"@use-gesture/react": "^10.2.22",
"hermes-eslint": "^0.25.0",
"hermes-parser": "^0.25.0",
"invariant": "^2.2.4",
"lru-cache": "^11.2.2",
"lz-string": "^1.5.0",
"monaco-editor": "^0.52.0",
"next": "15.6.0-canary.7",
"next": "^15.2.0-canary.64",
"notistack": "^3.0.0-alpha.7",
"prettier": "^3.3.3",
"pretty-format": "^29.3.1",
"re-resizable": "^6.9.16",
"react": "19.2",
"react-dom": "19.2"
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/node": "18.11.9",
"@types/react": "19.2",
"@types/react-dom": "19.2",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"autoprefixer": "^10.4.13",
"clsx": "^1.2.1",
"concurrently": "^7.4.0",
"eslint": "^8.28.0",
"eslint-config-next": "15.5.2",
"eslint-config-next": "^15.0.1",
"monaco-editor-webpack-plugin": "^7.1.0",
"postcss": "^8.4.31",
"tailwindcss": "^3.2.4",
"wait-on": "^7.2.0"
},
"resolutions": {
"@types/react": "19.2",
"@types/react-dom": "19.2"
}
}

View File

@@ -55,16 +55,12 @@ export default defineConfig({
// contextOptions: {
// ignoreHTTPSErrors: true,
// },
viewport: {width: 1920, height: 1080},
},
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
viewport: {width: 1920, height: 1080},
},
use: {...devices['Desktop Chrome']},
},
// {
// name: 'Desktop Firefox',

View File

@@ -69,75 +69,3 @@
scrollbar-width: none; /* Firefox */
}
}
::view-transition-old(.slide-in) {
animation-name: slideOutLeft;
}
::view-transition-new(.slide-in) {
animation-name: slideInLeft;
}
::view-transition-group(.slide-in) {
z-index: 1;
}
::view-transition-old(.slide-out) {
animation-name: slideOutLeft;
}
::view-transition-new(.slide-out) {
animation-name: slideInLeft;
}
::view-transition-group(.slide-out) {
z-index: 1;
}
@keyframes slideOutLeft {
from {
transform: translateX(0);
}
to {
transform: translateX(-100%);
}
}
@keyframes slideInLeft {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
::view-transition-old(.container),
::view-transition-new(.container) {
height: 100%;
}
::view-transition-old(.accordion-container),
::view-transition-new(.accordion-container) {
height: 100%;
object-fit: none;
object-position: left;
}
::view-transition-old(.tab-highlight),
::view-transition-new(.tab-highlight) {
height: 100%;
}
::view-transition-group(.tab-text) {
z-index: 1;
}
::view-transition-old(.expand-accordion),
::view-transition-new(.expand-accordion) {
width: auto;
}
::view-transition-group(.expand-accordion) {
overflow: clip;
}
/**
* For some reason, the original Monaco editor is still visible to the
* left of the DiffEditor. This is a workaround for better visual clarity.
*/
.monaco-diff-editor .editor.original{
visibility: hidden !important;
}

View File

@@ -6,9 +6,6 @@
"dom.iterable",
"esnext"
],
"types": [
"react/experimental"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -19,7 +16,7 @@
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"jsx": "preserve",
"incremental": true,
"plugins": [
{

File diff suppressed because it is too large Load Diff

View File

@@ -19,8 +19,7 @@
"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",
"eslint-docs": "yarn workspace babel-plugin-react-compiler build && node scripts/build-eslint-docs.js"
"npm:publish": "node scripts/release/publish"
},
"dependencies": {
"fs-extra": "^4.0.2",

View File

@@ -52,8 +52,8 @@
"react-dom": "0.0.0-experimental-4beb1fd8-20241118",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.2",
"zod": "^3.25.0 || ^4.0.0",
"zod-validation-error": "^3.5.0 || ^4.0.0"
"zod": "^3.22.4",
"zod-validation-error": "^2.1.0"
},
"resolutions": {
"./**/@babel/parser": "7.7.4",

View File

@@ -84,7 +84,7 @@ export default function BabelPluginReactCompiler(
}
} catch (e) {
if (e instanceof CompilerError) {
throw e.withPrintedMessage(pass.file.code, {eslint: false});
throw new Error(e.printErrorMessage(pass.file.code));
}
throw e;
}

View File

@@ -17,7 +17,7 @@ export function runBabelPluginReactCompiler(
text: string,
file: string,
language: 'flow' | 'typescript',
options: PluginOptions | null,
options: Partial<PluginOptions> | null,
includeAst: boolean = false,
): BabelCore.BabelFileResult {
const ast = BabelParser.parse(text, {

View File

@@ -51,26 +51,12 @@ function insertAdditionalFunctionDeclaration(
CompilerError.invariant(originalFnName != null && compiled.id != null, {
reason:
'Expected function declarations that are referenced elsewhere to have a named identifier',
description: null,
details: [
{
kind: 'error',
loc: fnPath.node.loc ?? null,
message: null,
},
],
loc: fnPath.node.loc ?? null,
});
CompilerError.invariant(originalFnParams.length === compiledParams.length, {
reason:
'Expected React Compiler optimized function declarations to have the same number of parameters as source',
description: null,
details: [
{
kind: 'error',
loc: fnPath.node.loc ?? null,
message: null,
},
],
loc: fnPath.node.loc ?? null,
});
const gatingCondition = t.identifier(
@@ -154,13 +140,7 @@ export function insertGatedFunctionDeclaration(
CompilerError.invariant(compiled.type === 'FunctionDeclaration', {
reason: 'Expected compiled node type to match input type',
description: `Got ${compiled.type} but expected FunctionDeclaration`,
details: [
{
kind: 'error',
loc: fnPath.node.loc ?? null,
message: null,
},
],
loc: fnPath.node.loc ?? null,
});
insertAdditionalFunctionDeclaration(
fnPath,

View File

@@ -9,7 +9,7 @@ import {NodePath} from '@babel/core';
import * as t from '@babel/types';
import {Scope as BabelScope} from '@babel/traverse';
import {CompilerError, ErrorCategory} from '../CompilerError';
import {CompilerError, ErrorSeverity} 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, ParsedPluginOptions} from './Options';
import {LoggerEvent, PluginOptions} 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({
category: ErrorCategory.Todo,
severity: ErrorSeverity.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.hasAnyErrors()) {
if (error.hasErrors()) {
return error;
} else {
return null;
@@ -56,7 +56,7 @@ export function validateRestrictedImports(
type ProgramContextOptions = {
program: NodePath<t.Program>;
suppressions: Array<SuppressionRange>;
opts: ParsedPluginOptions;
opts: PluginOptions;
filename: string | null;
code: string | null;
hasModuleScopeOptOut: boolean;
@@ -66,7 +66,7 @@ export class ProgramContext {
* Program and environment context
*/
scope: BabelScope;
opts: ParsedPluginOptions;
opts: PluginOptions;
filename: string | null;
code: string | null;
reactRuntimeModule: string;
@@ -205,7 +205,7 @@ export class ProgramContext {
}
const error = new CompilerError();
error.push({
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
reason: 'Encountered conflicting global in generated program',
description: `Conflict from local binding ${name}`,
loc: scope.getBinding(name)?.path.node.loc ?? null,
@@ -240,7 +240,7 @@ export function addImportsToProgram(
programContext: ProgramContext,
): void {
const existingImports = getExistingImports(path);
const stmts: Array<t.ImportDeclaration | t.VariableDeclaration> = [];
const stmts: Array<t.ImportDeclaration> = [];
const sortedModules = [...programContext.imports.entries()].sort(([a], [b]) =>
a.localeCompare(b),
);
@@ -256,14 +256,8 @@ export function addImportsToProgram(
{
reason:
'Encountered conflicting import specifiers in generated program',
description: `Conflict from import ${loweredImport.module}:(${loweredImport.imported} as ${loweredImport.name})`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
description: `Conflict from import ${loweredImport.module}:(${loweredImport.imported} as ${loweredImport.name}).`,
loc: GeneratedSource,
suggestions: null,
},
);
@@ -274,13 +268,7 @@ export function addImportsToProgram(
reason:
'Found inconsistent import specifier. This is an internal bug.',
description: `Expected import ${moduleName}:${specifierName} but found ${loweredImport.module}:${loweredImport.imported}`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
},
);
}
@@ -303,29 +291,9 @@ export function addImportsToProgram(
if (maybeExistingImports != null) {
maybeExistingImports.pushContainer('specifiers', importSpecifiers);
} else {
if (path.node.sourceType === 'module') {
stmts.push(
t.importDeclaration(importSpecifiers, t.stringLiteral(moduleName)),
);
} else {
stmts.push(
t.variableDeclaration('const', [
t.variableDeclarator(
t.objectPattern(
sortedImport.map(specifier => {
return t.objectProperty(
t.identifier(specifier.imported),
t.identifier(specifier.name),
);
}),
),
t.callExpression(t.identifier('require'), [
t.stringLiteral(moduleName),
]),
),
]),
);
}
stmts.push(
t.importDeclaration(importSpecifiers, t.stringLiteral(moduleName)),
);
}
}
path.unshiftContainer('body', stmts);

View File

@@ -6,7 +6,7 @@
*/
import * as t from '@babel/types';
import {z} from 'zod/v4';
import {z} from 'zod';
import {
CompilerDiagnostic,
CompilerError,
@@ -20,7 +20,7 @@ import {
tryParseExternalFunction,
} from '../HIR/Environment';
import {hasOwnProperty} from '../Utils/utils';
import {fromZodError} from 'zod-validation-error/v4';
import {fromZodError} from 'zod-validation-error';
import {CompilerPipelineValue} from './Pipeline';
const PanicThresholdOptionsSchema = z.enum([
@@ -51,8 +51,8 @@ const CustomOptOutDirectiveSchema = z
.default(null);
type CustomOptOutDirective = z.infer<typeof CustomOptOutDirectiveSchema>;
export type PluginOptions = Partial<{
environment: Partial<EnvironmentConfig>;
export type PluginOptions = {
environment: EnvironmentConfig;
logger: Logger | null;
@@ -102,25 +102,14 @@ export type PluginOptions = Partial<{
panicThreshold: PanicThresholdOptions;
/**
* @deprecated
*
/*
* When enabled, Forget will continue statically analyzing and linting code, but skip over codegen
* passes.
*
* NOTE: ignored if `outputMode` is specified
*
* Defaults to false
*/
noEmit: boolean;
/**
* If specified, overrides `noEmit` and controls the output mode of the compiler.
*
* Defaults to null
*/
outputMode: CompilerOutputMode | null;
/*
* Determines the strategy for determining which functions to compile. Note that regardless of
* which mode is enabled, a component can be opted out by adding the string literal
@@ -146,12 +135,7 @@ export type PluginOptions = Partial<{
*/
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.
*/
@@ -177,11 +161,7 @@ export type PluginOptions = Partial<{
* a userspace approximation of runtime APIs.
*/
target: CompilerReactTarget;
}>;
export type ParsedPluginOptions = Required<
Omit<PluginOptions, 'environment'>
> & {environment: EnvironmentConfig};
};
const CompilerReactTargetSchema = z.union([
z.literal('17'),
@@ -223,19 +203,6 @@ const CompilationModeSchema = z.enum([
export type CompilationMode = z.infer<typeof CompilationModeSchema>;
const CompilerOutputModeSchema = z.enum([
// Build optimized for SSR, with client features removed
'ssr',
// Build optimized for the client, with auto memoization
'client',
// Build optimized for the client without auto memo
'client-no-memo',
// Lint mode, the output is unused but validations should run
'lint',
]);
export type CompilerOutputMode = z.infer<typeof CompilerOutputModeSchema>;
/**
* Represents 'events' that may occur during compilation. Events are only
* recorded when a logger is set (through the config).
@@ -310,14 +277,13 @@ export type Logger = {
debugLogIRs?: (value: CompilerPipelineValue) => void;
};
export const defaultOptions: ParsedPluginOptions = {
export const defaultOptions: PluginOptions = {
compilationMode: 'infer',
panicThreshold: 'none',
environment: parseEnvironmentConfig({}).unwrap(),
logger: null,
gating: null,
noEmit: false,
outputMode: null,
dynamicGating: null,
eslintSuppressionRules: null,
flowSuppressions: true,
@@ -328,9 +294,9 @@ export const defaultOptions: ParsedPluginOptions = {
enableReanimatedCheck: true,
customOptOutDirectives: null,
target: '19',
};
} as const;
export function parsePluginOptions(obj: unknown): ParsedPluginOptions {
export function parsePluginOptions(obj: unknown): PluginOptions {
if (obj == null || typeof obj !== 'object') {
return defaultOptions;
}

View File

@@ -8,7 +8,7 @@
import {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import prettyFormat from 'pretty-format';
import {CompilerOutputMode, Logger, ProgramContext} from '.';
import {Logger, ProgramContext} from '.';
import {
HIRFunction,
ReactiveFunction,
@@ -24,6 +24,7 @@ import {
pruneUnusedLabelsHIR,
} from '../HIR';
import {
CompilerMode,
Environment,
EnvironmentConfig,
ReactFunctionType,
@@ -32,7 +33,9 @@ import {findContextIdentifiers} from '../HIR/FindContextIdentifiers';
import {
analyseFunctions,
dropManualMemoization,
inferMutableRanges,
inferReactivePlaces,
inferReferenceEffects,
inlineImmediatelyInvokedFunctionExpressions,
inferEffectDependencies,
} from '../Inference';
@@ -89,6 +92,7 @@ import {
} from '../Validation';
import {validateLocalsNotReassignedAfterRender} from '../Validation/ValidateLocalsNotReassignedAfterRender';
import {outlineFunctions} from '../Optimization/OutlineFunctions';
import {propagatePhiTypes} from '../TypeInference/PropagatePhiTypes';
import {lowerContextAccess} from '../Optimization/LowerContextAccess';
import {validateNoSetStateInEffects} from '../Validation/ValidateNoSetStateInEffects';
import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement';
@@ -97,16 +101,11 @@ import {outlineJSX} from '../Optimization/OutlineJsx';
import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls';
import {transformFire} from '../Transform';
import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureFunctionsInRender';
import {CompilerError} from '..';
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
import {validateNoDerivedComputationsInEffects_exp} from '../Validation/ValidateNoDerivedComputationsInEffects_exp';
import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
import {optimizeForSSR} from '../Optimization/OptimizeForSSR';
import {validateExhaustiveDependencies} from '../Validation/ValidateExhaustiveDependencies';
import {validateSourceLocations} from '../Validation/ValidateSourceLocations';
export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -120,7 +119,7 @@ function run(
>,
config: EnvironmentConfig,
fnType: ReactFunctionType,
mode: CompilerOutputMode,
mode: CompilerMode,
programContext: ProgramContext,
logger: Logger | null,
filename: string | null,
@@ -170,12 +169,12 @@ function runWithEnvironment(
validateUseMemo(hir).unwrap();
if (
env.enableDropManualMemoization &&
env.isInferredMemoEnabled &&
!env.config.enablePreserveExistingManualUseMemo &&
!env.config.disableMemoizationForDebugging &&
!env.config.enableChangeDetectionForDebugging
) {
dropManualMemoization(hir).unwrap();
dropManualMemoization(hir);
log({kind: 'hir', name: 'DropManualMemoization', value: hir});
}
@@ -206,7 +205,7 @@ function runWithEnvironment(
inferTypes(hir);
log({kind: 'hir', name: 'InferTypes', value: hir});
if (env.enableValidations) {
if (env.isInferredMemoEnabled) {
if (env.config.validateHooksUsage) {
validateHooksUsage(hir).unwrap();
}
@@ -230,17 +229,26 @@ function runWithEnvironment(
analyseFunctions(hir);
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
if (env.enableValidations) {
if (mutabilityAliasingErrors.isErr()) {
throw mutabilityAliasingErrors.unwrapErr();
if (!env.config.enableNewMutationAliasingModel) {
const fnEffectErrors = inferReferenceEffects(hir);
if (env.isInferredMemoEnabled) {
if (fnEffectErrors.length > 0) {
CompilerError.throw(fnEffectErrors[0]);
}
}
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.outputMode === 'ssr') {
optimizeForSSR(hir);
log({kind: 'hir', name: 'OptimizeForSSR', value: hir});
if (!env.config.enableNewMutationAliasingModel) {
validateLocalsNotReassignedAfterRender(hir);
}
// Note: Has to come after infer reference effects because "dead" code may still affect inference
@@ -255,18 +263,23 @@ function runWithEnvironment(
pruneMaybeThrows(hir);
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
const mutabilityAliasingRangeErrors = inferMutationAliasingRanges(hir, {
isFunctionExpression: false,
});
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
if (env.enableValidations) {
if (mutabilityAliasingRangeErrors.isErr()) {
throw mutabilityAliasingRangeErrors.unwrapErr();
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);
}
validateLocalsNotReassignedAfterRender(hir);
}
if (env.enableValidations) {
if (env.isInferredMemoEnabled) {
if (env.config.assertValidMutableRanges) {
assertValidMutableRanges(hir);
}
@@ -279,14 +292,8 @@ function runWithEnvironment(
validateNoSetStateInRender(hir).unwrap();
}
if (env.config.validateNoDerivedComputationsInEffects_exp) {
env.logErrors(validateNoDerivedComputationsInEffects_exp(hir));
} else if (env.config.validateNoDerivedComputationsInEffects) {
validateNoDerivedComputationsInEffects(hir);
}
if (env.config.validateNoSetStateInEffects) {
env.logErrors(validateNoSetStateInEffects(hir, env));
env.logErrors(validateNoSetStateInEffects(hir));
}
if (env.config.validateNoJSXInTryStatements) {
@@ -297,17 +304,17 @@ function runWithEnvironment(
validateNoImpureFunctionsInRender(hir).unwrap();
}
validateNoFreezingKnownMutableFunctions(hir).unwrap();
if (
env.config.validateNoFreezingKnownMutableFunctions ||
env.config.enableNewMutationAliasingModel
) {
validateNoFreezingKnownMutableFunctions(hir).unwrap();
}
}
inferReactivePlaces(hir);
log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
if (env.config.validateExhaustiveMemoizationDependencies) {
// NOTE: this relies on reactivity inference running first
validateExhaustiveDependencies(hir).unwrap();
}
rewriteInstructionKindsBasedOnReassignment(hir);
log({
kind: 'hir',
@@ -315,11 +322,18 @@ function runWithEnvironment(
value: hir,
});
if (env.enableValidations && env.config.validateStaticComponents) {
env.logErrors(validateStaticComponents(hir));
}
propagatePhiTypes(hir);
log({
kind: 'hir',
name: 'PropagatePhiTypes',
value: hir,
});
if (env.isInferredMemoEnabled) {
if (env.config.validateStaticComponents) {
env.logErrors(validateStaticComponents(hir));
}
if (env.enableMemoization) {
/**
* Only create reactive scopes (which directly map to generated memo blocks)
* if inferred memoization is enabled. This makes all later passes which
@@ -340,15 +354,6 @@ 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});
@@ -569,10 +574,6 @@ function runWithEnvironment(
log({kind: 'ast', name: 'Codegen (outlined)', value: outlined.fn});
}
if (env.config.validateSourceLocations) {
validateSourceLocations(func, ast).unwrap();
}
/**
* This flag should be only set for unit / fixture tests to check
* that Forget correctly handles unexpected errors (e.g. exceptions
@@ -591,7 +592,7 @@ export function compileFn(
>,
config: EnvironmentConfig,
fnType: ReactFunctionType,
mode: CompilerOutputMode,
mode: CompilerMode,
programContext: ProgramContext,
logger: Logger | null,
filename: string | null,

View File

@@ -10,7 +10,7 @@ import * as t from '@babel/types';
import {
CompilerError,
CompilerErrorDetail,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {ExternalFunction, ReactFunctionType} from '../HIR/Environment';
import {CodegenFunction} from '../ReactiveScopes';
@@ -23,12 +23,7 @@ import {
ProgramContext,
validateRestrictedImports,
} from './Imports';
import {
CompilerOutputMode,
CompilerReactTarget,
ParsedPluginOptions,
PluginOptions,
} from './Options';
import {CompilerReactTarget, PluginOptions} from './Options';
import {compileFn} from './Pipeline';
import {
filterSuppressionsThatAffectFunction,
@@ -39,7 +34,7 @@ import {GeneratedSource} from '../HIR';
import {Err, Ok, Result} from '../Utils/Result';
export type CompilerPass = {
opts: ParsedPluginOptions;
opts: PluginOptions;
filename: string | null;
comments: Array<t.CommentBlock | t.CommentLine>;
code: string | null;
@@ -50,7 +45,7 @@ const DYNAMIC_GATING_DIRECTIVE = new RegExp('^use memo if\\(([^\\)]*)\\)$');
export function tryFindDirectiveEnablingMemoization(
directives: Array<t.Directive>,
opts: ParsedPluginOptions,
opts: PluginOptions,
): Result<t.Directive | null, CompilerError> {
const optIn = directives.find(directive =>
OPT_IN_DIRECTIVES.has(directive.value.value),
@@ -86,7 +81,7 @@ export function findDirectiveDisablingMemoization(
}
function findDirectivesDynamicGating(
directives: Array<t.Directive>,
opts: ParsedPluginOptions,
opts: PluginOptions,
): Result<
{
gating: ExternalFunction;
@@ -109,14 +104,14 @@ function findDirectivesDynamicGating(
errors.push({
reason: `Dynamic gating directive is not a valid JavaScript identifier`,
description: `Found '${directive.value.value}'`,
category: ErrorCategory.Gating,
severity: ErrorSeverity.InvalidReact,
loc: directive.loc ?? null,
suggestions: null,
});
}
}
}
if (errors.hasAnyErrors()) {
if (errors.hasErrors()) {
return Err(errors);
} else if (result.length > 1) {
const error = new CompilerError();
@@ -125,7 +120,7 @@ function findDirectivesDynamicGating(
description: `Expected a single directive but found [${result
.map(r => r.directive.value.value)
.join(', ')}]`,
category: ErrorCategory.Gating,
severity: ErrorSeverity.InvalidReact,
loc: result[0].directive.loc ?? null,
suggestions: null,
});
@@ -143,13 +138,15 @@ function findDirectivesDynamicGating(
}
}
function isError(err: unknown): boolean {
return !(err instanceof CompilerError) || err.hasErrors();
function isCriticalError(err: unknown): boolean {
return !(err instanceof CompilerError) || err.isCritical();
}
function isConfigError(err: unknown): boolean {
if (err instanceof CompilerError) {
return err.details.some(detail => detail.category === ErrorCategory.Config);
return err.details.some(
detail => detail.severity === ErrorSeverity.InvalidConfig,
);
}
return false;
}
@@ -214,7 +211,8 @@ function handleError(
logError(err, context, fnLoc);
if (
context.opts.panicThreshold === 'all_errors' ||
(context.opts.panicThreshold === 'critical_errors' && isError(err)) ||
(context.opts.panicThreshold === 'critical_errors' &&
isCriticalError(err)) ||
isConfigError(err) // Always throws regardless of panic threshold
) {
throw err;
@@ -315,13 +313,7 @@ function insertNewOutlinedFunctionNode(
CompilerError.invariant(insertedFuncDecl.isFunctionDeclaration(), {
reason: 'Expected inserted function declaration',
description: `Got: ${insertedFuncDecl}`,
details: [
{
kind: 'error',
loc: insertedFuncDecl.node?.loc ?? null,
message: null,
},
],
loc: insertedFuncDecl.node?.loc ?? null,
});
return insertedFuncDecl;
}
@@ -400,15 +392,7 @@ export function compileProgram(
*/
const suppressions = findProgramSuppressions(
pass.comments,
/*
* If the compiler is validating hooks rules and exhaustive memo dependencies, we don't need to check
* for React ESLint suppressions
*/
pass.opts.environment.validateExhaustiveMemoizationDependencies &&
pass.opts.environment.validateHooksUsage
? null
: (pass.opts.eslintSuppressionRules ?? DEFAULT_ESLINT_SUPPRESSIONS),
// Always bail on Flow suppressions
pass.opts.eslintSuppressionRules ?? DEFAULT_ESLINT_SUPPRESSIONS,
pass.opts.flowSuppressions,
);
@@ -430,30 +414,15 @@ export function compileProgram(
);
const compiledFns: Array<CompileResult> = [];
// outputMode takes precedence if specified
const outputMode: CompilerOutputMode =
pass.opts.outputMode ?? (pass.opts.noEmit ? 'lint' : 'client');
while (queue.length !== 0) {
const current = queue.shift()!;
const compiled = processFn(
current.fn,
current.fnType,
programContext,
outputMode,
);
const compiled = processFn(current.fn, current.fnType, programContext);
if (compiled != null) {
for (const outlined of compiled.outlined) {
CompilerError.invariant(outlined.fn.outlined.length === 0, {
reason: 'Unexpected nested outlined functions',
description: null,
details: [
{
kind: 'error',
loc: outlined.fn.loc,
message: null,
},
],
loc: outlined.fn.loc,
});
const fn = insertNewOutlinedFunctionNode(
program,
@@ -486,7 +455,7 @@ export function compileProgram(
new CompilerErrorDetail({
reason:
'Unexpected compiled functions when module scope opt-out is present',
category: ErrorCategory.Invariant,
severity: ErrorSeverity.Invariant,
loc: null,
}),
);
@@ -521,20 +490,7 @@ 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;
}
@@ -598,7 +554,6 @@ function processFn(
fn: BabelFn,
fnType: ReactFunctionType,
programContext: ProgramContext,
outputMode: CompilerOutputMode,
): null | CodegenFunction {
let directives: {
optIn: t.Directive | null;
@@ -634,27 +589,18 @@ function processFn(
}
let compiledFn: CodegenFunction;
const compileResult = tryCompileFunction(
fn,
fnType,
programContext,
outputMode,
);
const compileResult = tryCompileFunction(fn, fnType, programContext);
if (compileResult.kind === 'error') {
if (directives.optOut != null) {
logError(compileResult.error, programContext, fn.node.loc ?? null);
} else {
handleError(compileResult.error, programContext, fn.node.loc ?? null);
}
if (outputMode === 'client') {
const retryResult = retryCompileFunction(fn, fnType, programContext);
if (retryResult == null) {
return null;
}
compiledFn = retryResult;
} else {
const retryResult = retryCompileFunction(fn, fnType, programContext);
if (retryResult == null) {
return null;
}
compiledFn = retryResult;
} else {
compiledFn = compileResult.compiledFn;
}
@@ -690,7 +636,7 @@ function processFn(
if (programContext.hasModuleScopeOptOut) {
return null;
} else if (programContext.opts.outputMode === 'lint') {
} else if (programContext.opts.noEmit) {
/**
* inferEffectDependencies + noEmit is currently only used for linting. In
* this mode, add source locations for where the compiler *can* infer effect
@@ -720,7 +666,6 @@ function tryCompileFunction(
fn: BabelFn,
fnType: ReactFunctionType,
programContext: ProgramContext,
outputMode: CompilerOutputMode,
):
| {kind: 'compile'; compiledFn: CodegenFunction}
| {kind: 'error'; error: unknown} {
@@ -747,7 +692,7 @@ function tryCompileFunction(
fn,
programContext.opts.environment,
fnType,
outputMode,
'all_features',
programContext,
programContext.opts.logger,
programContext.filename,
@@ -785,7 +730,7 @@ function retryCompileFunction(
fn,
environment,
fnType,
'client-no-memo',
'no_inferred_memo',
programContext,
programContext.opts.logger,
programContext.filename,
@@ -865,7 +810,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",
category: ErrorCategory.Config,
severity: ErrorSeverity.InvalidConfig,
loc: null,
}),
);
@@ -889,72 +834,6 @@ 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,
@@ -993,6 +872,11 @@ 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: {
@@ -1452,13 +1336,7 @@ export function getReactCompilerRuntimeModule(
{
reason: 'Expected target to already be validated',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
},
);

View File

@@ -7,7 +7,7 @@
import type * as BabelCore from '@babel/core';
import {hasOwnProperty} from '../Utils/utils';
import {ParsedPluginOptions} from './Options';
import {PluginOptions} from './Options';
function hasModule(name: string): boolean {
if (typeof require === 'undefined') {
@@ -52,9 +52,7 @@ export function pipelineUsesReanimatedPlugin(
return hasModule('react-native-reanimated');
}
export function injectReanimatedFlag(
options: ParsedPluginOptions,
): ParsedPluginOptions {
export function injectReanimatedFlag(options: PluginOptions): PluginOptions {
return {
...options,
environment: {

View File

@@ -8,10 +8,10 @@
import {NodePath} from '@babel/core';
import * as t from '@babel/types';
import {
CompilerDiagnostic,
CompilerError,
CompilerErrorDetail,
CompilerSuggestionOperation,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {assertExhaustive} from '../Utils/utils';
import {GeneratedSource} from '../HIR';
@@ -78,7 +78,7 @@ export function filterSuppressionsThatAffectFunction(
export function findProgramSuppressions(
programComments: Array<t.Comment>,
ruleNames: Array<string> | null,
ruleNames: Array<string>,
flowSuppressions: boolean,
): Array<SuppressionRange> {
const suppressionRanges: Array<SuppressionRange> = [];
@@ -86,18 +86,12 @@ export function findProgramSuppressions(
let enableComment: t.Comment | null = null;
let source: SuppressionSource | null = null;
let disableNextLinePattern: RegExp | null = null;
let disablePattern: RegExp | null = null;
let enablePattern: RegExp | null = null;
if (ruleNames != null && 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 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}`);
const flowSuppressionPattern = new RegExp(
'\\$(FlowFixMe\\w*|FlowExpectedError|FlowIssue)\\[react\\-rule',
);
@@ -113,7 +107,6 @@ export function findProgramSuppressions(
* CommentLine within the block.
*/
disableComment == null &&
disableNextLinePattern != null &&
disableNextLinePattern.test(comment.value)
) {
disableComment = comment;
@@ -131,16 +124,12 @@ export function findProgramSuppressions(
source = 'Flow';
}
if (disablePattern != null && disablePattern.test(comment.value)) {
if (disablePattern.test(comment.value)) {
disableComment = comment;
source = 'Eslint';
}
if (
enablePattern != null &&
enablePattern.test(comment.value) &&
source === 'Eslint'
) {
if (enablePattern.test(comment.value) && source === 'Eslint') {
enableComment = comment;
}
@@ -163,14 +152,7 @@ export function suppressionsToCompilerError(
): CompilerError {
CompilerError.invariant(suppressionRanges.length !== 0, {
reason: `Expected at least suppression comment source range`,
description: null,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
const error = new CompilerError();
for (const suppressionRange of suppressionRanges) {
@@ -199,11 +181,12 @@ export function suppressionsToCompilerError(
'Unhandled suppression source',
);
}
error.pushDiagnostic(
CompilerDiagnostic.create({
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()}\``,
category: ErrorCategory.Suppression,
error.pushErrorDetail(
new CompilerErrorDetail({
reason: `${reason}. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior`,
description: suppressionRange.disableComment.value.trim(),
severity: ErrorSeverity.InvalidReact,
loc: suppressionRange.disableComment.loc ?? null,
suggestions: [
{
description: suggestion,
@@ -214,10 +197,6 @@ export function suppressionsToCompilerError(
op: CompilerSuggestionOperation.Remove,
},
],
}).withDetails({
kind: 'error',
loc: suppressionRange.disableComment.loc ?? null,
message: 'Found React rule suppression',
}),
);
}

View File

@@ -8,27 +8,27 @@
import {NodePath} from '@babel/core';
import * as t from '@babel/types';
import {CompilerError, EnvironmentConfig, Logger} from '..';
import {CompilerError, EnvironmentConfig, ErrorSeverity, 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,
ErrorCategory,
} from '../CompilerError';
import {CompilerDiagnostic, CompilerDiagnosticOptions} from '../CompilerError';
function throwInvalidReact(
options: CompilerDiagnosticOptions,
options: Omit<CompilerDiagnosticOptions, 'severity'>,
{logger, filename}: TraversalState,
): never {
const detail: CompilerDiagnosticOptions = {
severity: ErrorSeverity.InvalidReact,
...options,
};
logger?.logEvent(filename, {
kind: 'CompileError',
fnLoc: null,
detail: new CompilerDiagnostic(options),
detail: new CompilerDiagnostic(detail),
});
CompilerError.throwDiagnostic(options);
CompilerError.throwDiagnostic(detail);
}
function isAutodepsSigil(
@@ -92,11 +92,10 @@ function assertValidEffectImportReference(
*/
throwInvalidReact(
{
category: ErrorCategory.AutomaticEffectDependencies,
reason:
category:
'Cannot infer dependencies of this effect. This will break your build!',
description:
'To resolve, either pass a dependency array or fix reported compiler bailout diagnostics' +
'To resolve, either pass a dependency array or fix reported compiler bailout diagnostics.' +
(maybeErrorDiagnostic ? ` ${maybeErrorDiagnostic}` : ''),
details: [
{
@@ -124,11 +123,13 @@ function assertValidFireImportReference(
);
throwInvalidReact(
{
category: ErrorCategory.Fire,
reason: '[Fire] Untransformed reference to compiler-required feature.',
category:
'[Fire] Untransformed reference to compiler-required feature.',
description:
'Either remove this `fire` call or ensure it is successfully transformed by the compiler' +
(maybeErrorDiagnostic != null ? ` ${maybeErrorDiagnostic}` : ''),
maybeErrorDiagnostic
? ` ${maybeErrorDiagnostic}`
: '',
details: [
{
kind: 'error',
@@ -215,14 +216,7 @@ function validateImportSpecifier(
const binding = local.scope.getBinding(local.node.name);
CompilerError.invariant(binding != null, {
reason: 'Expected binding to be found for import specifier',
description: null,
details: [
{
kind: 'error',
loc: local.node.loc ?? null,
message: null,
},
],
loc: local.node.loc ?? null,
});
checkFn(binding.referencePaths, state);
}
@@ -242,14 +236,7 @@ function validateNamespacedImport(
CompilerError.invariant(binding != null, {
reason: 'Expected binding to be found for import specifier',
description: null,
details: [
{
kind: 'error',
loc: local.node.loc ?? null,
message: null,
},
],
loc: local.node.loc ?? null,
});
const filteredReferences = new Map<
CheckInvalidReferenceFn,

View File

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

View File

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

View File

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

View File

@@ -38,13 +38,7 @@ 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}\``,
details: [
{
kind: 'error',
loc: instr.lvalue.loc,
message: null,
},
],
loc: instr.lvalue.loc,
suggestions: null,
});
CompilerError.invariant(!assignments.has(instr.lvalue.identifier.id), {
@@ -52,13 +46,7 @@ export function assertConsistentIdentifiers(fn: HIRFunction): void {
description: `Found duplicate assignment of '${printPlace(
instr.lvalue,
)}'`,
details: [
{
kind: 'error',
loc: instr.lvalue.loc,
message: null,
},
],
loc: instr.lvalue.loc,
suggestions: null,
});
assignments.add(instr.lvalue.identifier.id);
@@ -89,13 +77,7 @@ function validate(
CompilerError.invariant(identifier === previous, {
reason: `Duplicate identifier object`,
description: `Found duplicate identifier object for id ${identifier.id}`,
details: [
{
kind: 'error',
loc: loc ?? GeneratedSource,
message: null,
},
],
loc: loc ?? GeneratedSource,
suggestions: null,
});
}

View File

@@ -18,13 +18,7 @@ export function assertTerminalSuccessorsExist(fn: HIRFunction): void {
description: `Block bb${successor} does not exist for terminal '${printTerminal(
block.terminal,
)}'`,
details: [
{
kind: 'error',
loc: (block.terminal as any).loc ?? GeneratedSource,
message: null,
},
],
loc: (block.terminal as any).loc ?? GeneratedSource,
suggestions: null,
});
return successor;
@@ -39,26 +33,14 @@ export function assertTerminalPredsExist(fn: HIRFunction): void {
CompilerError.invariant(predBlock != null, {
reason: 'Expected predecessor block to exist',
description: `Block ${block.id} references non-existent ${pred}`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
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}`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
},
);
}

View File

@@ -131,13 +131,7 @@ 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})`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
if (disjoint) {
exit(maybeParent, context);

View File

@@ -57,13 +57,7 @@ function validateMutableRange(
{
reason: `Invalid mutable range: [${range.start}:${range.end}]`,
description: `${printPlace(place)} in ${description}`,
details: [
{
kind: 'error',
loc: place.loc,
message: null,
},
],
loc: place.loc,
},
);
}

View File

@@ -9,10 +9,9 @@ import {NodePath, Scope} from '@babel/traverse';
import * as t from '@babel/types';
import invariant from 'invariant';
import {
CompilerDiagnostic,
CompilerError,
CompilerSuggestionOperation,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {Err, Ok, Result} from '../Utils/Result';
import {assertExhaustive, hasNode} from '../Utils/utils';
@@ -47,7 +46,6 @@ import {
makePropertyLiteral,
makeType,
promoteTemporary,
validateIdentifierName,
} from './HIR';
import HIRBuilder, {Bindings, createTemporaryPlace} from './HIRBuilder';
import {BuiltInArrayId} from './ObjectShape';
@@ -106,17 +104,12 @@ export function lower(
if (param.isIdentifier()) {
const binding = builder.resolveIdentifier(param);
if (binding.kind !== 'Identifier') {
builder.errors.pushDiagnostic(
CompilerDiagnostic.create({
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',
}),
);
builder.errors.push({
reason: `(BuildHIR::lower) Could not find binding for param \`${param.node.name}\``,
severity: ErrorSeverity.Invariant,
loc: param.node.loc ?? null,
suggestions: null,
});
return;
}
const place: Place = {
@@ -170,17 +163,12 @@ export function lower(
'Assignment',
);
} else {
builder.errors.pushDiagnostic(
CompilerDiagnostic.create({
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',
}),
);
builder.errors.push({
reason: `(BuildHIR::lower) Handle ${param.node.type} params`,
severity: ErrorSeverity.Todo,
loc: param.node.loc ?? null,
suggestions: null,
});
}
});
@@ -190,7 +178,6 @@ export function lower(
const fallthrough = builder.reserve('block');
const terminal: ReturnTerminal = {
kind: 'return',
returnVariant: 'Implicit',
loc: GeneratedSource,
value: lowerExpressionToTemporary(builder, body),
id: makeInstructionId(0),
@@ -201,37 +188,22 @@ export function lower(
lowerStatement(builder, body);
directives = body.get('directives').map(d => d.node.value.value);
} else {
builder.errors.pushDiagnostic(
CompilerDiagnostic.create({
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',
}),
);
builder.errors.push({
severity: ErrorSeverity.InvalidJS,
reason: `Unexpected function body kind`,
description: `Expected function body to be an expression or a block statement, got \`${body.type}\``,
loc: body.node.loc ?? null,
suggestions: null,
});
}
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()) {
if (builder.errors.hasErrors()) {
return Err(builder.errors);
}
builder.terminate(
{
kind: 'return',
returnVariant: 'Void',
loc: GeneratedSource,
value: lowerValueToTemporary(builder, {
kind: 'Primitive',
@@ -245,8 +217,7 @@ export function lower(
);
return Ok({
id: validatedId,
nameHint: null,
id,
params,
fnType: bindings == null ? env.fnType : 'Other',
returnTypeAnnotation: null, // TODO: extract the actual return type node if present
@@ -284,7 +255,7 @@ function lowerStatement(
builder.errors.push({
reason:
'(BuildHIR::lowerStatement) Support ThrowStatement inside of try/catch',
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -316,7 +287,6 @@ function lowerStatement(
}
const terminal: ReturnTerminal = {
kind: 'return',
returnVariant: 'Explicit',
loc: stmt.node.loc ?? GeneratedSource,
value,
id: makeInstructionId(0),
@@ -451,13 +421,7 @@ function lowerStatement(
reason: 'Expected to find binding for hoisted identifier',
description: `Could not find a binding for ${id.node.name}`,
suggestions: null,
details: [
{
kind: 'error',
loc: id.node.loc ?? GeneratedSource,
message: null,
},
],
loc: id.node.loc ?? GeneratedSource,
});
if (builder.environment.isHoistedIdentifier(binding.identifier)) {
// Already hoisted
@@ -477,7 +441,7 @@ function lowerStatement(
kind = InstructionKind.HoistedFunction;
} else if (!binding.path.isVariableDeclarator()) {
builder.errors.push({
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
reason: 'Unsupported declaration type for hoisting',
description: `variable "${binding.identifier.name}" declared with ${binding.path.type}`,
suggestions: null,
@@ -486,7 +450,7 @@ function lowerStatement(
continue;
} else {
builder.errors.push({
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
reason: 'Handle non-const declarations for hoisting',
description: `variable "${binding.identifier.name}" declared with ${binding.kind}`,
suggestions: null,
@@ -499,14 +463,7 @@ function lowerStatement(
CompilerError.invariant(identifier.kind === 'Identifier', {
reason:
'Expected hoisted binding to be a local identifier, not a global',
description: null,
details: [
{
kind: 'error',
loc: id.node.loc ?? GeneratedSource,
message: null,
},
],
loc: id.node.loc ?? GeneratedSource,
});
const place: Place = {
effect: Effect.Unknown,
@@ -573,7 +530,7 @@ function lowerStatement(
builder.errors.push({
reason:
'(BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement',
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -645,7 +602,7 @@ function lowerStatement(
if (test.node == null) {
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle empty test in ForStatement`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -796,7 +753,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`,
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: case_.node.loc ?? null,
suggestions: null,
});
@@ -868,7 +825,7 @@ function lowerStatement(
if (nodeKind === 'var') {
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle ${nodeKind} kinds in VariableDeclaration`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -896,7 +853,7 @@ function lowerStatement(
if (binding.kind !== 'Identifier') {
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Could not find binding for declaration.`,
category: ErrorCategory.Invariant,
severity: ErrorSeverity.Invariant,
loc: id.node.loc ?? null,
suggestions: null,
});
@@ -913,7 +870,7 @@ function lowerStatement(
const declRangeStart = declaration.parentPath.node.start!;
builder.errors.push({
reason: `Expect \`const\` declaration not to be reassigned`,
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: id.node.loc ?? null,
suggestions: [
{
@@ -960,7 +917,7 @@ function lowerStatement(
builder.errors.push({
reason: `Expected variable declaration to be an identifier if no initializer was provided`,
description: `Got a \`${id.type}\``,
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -1039,13 +996,7 @@ function lowerStatement(
CompilerError.invariant(stmt.get('id').type === 'Identifier', {
reason: 'function declarations must have a name',
description: null,
details: [
{
kind: 'error',
loc: stmt.node.loc ?? null,
message: null,
},
],
loc: stmt.node.loc ?? null,
suggestions: null,
});
const id = stmt.get('id') as NodePath<t.Identifier>;
@@ -1074,7 +1025,7 @@ function lowerStatement(
if (stmt.node.await) {
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle for-await loops`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -1145,13 +1096,7 @@ function lowerStatement(
CompilerError.invariant(declarations.length === 1, {
reason: `Expected only one declaration in the init of a ForOfStatement, got ${declarations.length}`,
description: null,
details: [
{
kind: 'error',
loc: left.node.loc ?? null,
message: null,
},
],
loc: left.node.loc ?? null,
suggestions: null,
});
const id = declarations[0].get('id');
@@ -1166,15 +1111,8 @@ 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,
@@ -1251,13 +1189,7 @@ function lowerStatement(
CompilerError.invariant(declarations.length === 1, {
reason: `Expected only one declaration in the init of a ForInStatement, got ${declarations.length}`,
description: null,
details: [
{
kind: 'error',
loc: left.node.loc ?? null,
message: null,
},
],
loc: left.node.loc ?? null,
suggestions: null,
});
const id = declarations[0].get('id');
@@ -1272,15 +1204,8 @@ 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,
@@ -1332,7 +1257,7 @@ function lowerStatement(
if (!hasNode(handlerPath)) {
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle TryStatement without a catch clause`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -1341,7 +1266,7 @@ function lowerStatement(
if (hasNode(stmt.get('finalizer'))) {
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle TryStatement with a finalizer ('finally') clause`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -1434,7 +1359,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`,
category: ErrorCategory.UnsupportedSyntax,
severity: ErrorSeverity.UnsupportedJS,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
@@ -1454,7 +1379,7 @@ function lowerStatement(
builder.errors.push({
reason: 'Inline `class` declarations are not supported',
description: `Move class declarations outside of components/hooks`,
category: ErrorCategory.UnsupportedSyntax,
severity: ErrorSeverity.UnsupportedJS,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
@@ -1483,7 +1408,7 @@ function lowerStatement(
builder.errors.push({
reason:
'JavaScript `import` and `export` statements may only appear at the top level of a module',
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
@@ -1498,7 +1423,7 @@ function lowerStatement(
builder.errors.push({
reason:
'TypeScript `namespace` statements may only appear at the top level of a module',
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
@@ -1568,6 +1493,20 @@ function lowerObjectPropertyKey(
name: key.node.value,
};
} else if (property.node.computed && key.isExpression()) {
if (!key.isIdentifier() && !key.isMemberExpression()) {
/*
* NOTE: allowing complex key expressions can trigger a bug where a mutation is made conditional
* see fixture
* error.object-expression-computed-key-modified-during-after-construction.js
*/
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`,
severity: ErrorSeverity.Todo,
loc: key.node.loc ?? null,
suggestions: null,
});
return null;
}
const place = lowerExpressionToTemporary(builder, key);
return {
kind: 'computed',
@@ -1587,7 +1526,7 @@ function lowerObjectPropertyKey(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: key.node.loc ?? null,
suggestions: null,
});
@@ -1644,7 +1583,7 @@ function lowerExpression(
if (!valuePath.isExpression()) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${valuePath.type} values in ObjectExpression`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: valuePath.node.loc ?? null,
suggestions: null,
});
@@ -1670,7 +1609,7 @@ function lowerExpression(
if (propertyPath.node.kind !== 'method') {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${propertyPath.node.kind} functions in ObjectExpression`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: propertyPath.node.loc ?? null,
suggestions: null,
});
@@ -1691,7 +1630,7 @@ function lowerExpression(
} else {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${propertyPath.type} properties in ObjectExpression`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: propertyPath.node.loc ?? null,
suggestions: null,
});
@@ -1724,7 +1663,7 @@ function lowerExpression(
} else {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${element.type} elements in ArrayExpression`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: element.node.loc ?? null,
suggestions: null,
});
@@ -1744,7 +1683,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}\``,
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: calleePath.node.loc ?? null,
suggestions: null,
});
@@ -1770,7 +1709,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`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: calleePath.node.loc ?? null,
suggestions: null,
});
@@ -1804,7 +1743,7 @@ function lowerExpression(
if (!leftPath.isExpression()) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Expected Expression, got ${leftPath.type} lval in BinaryExpression`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: leftPath.node.loc ?? null,
suggestions: null,
});
@@ -1816,7 +1755,7 @@ function lowerExpression(
if (operator === '|>') {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Pipe operator not supported`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: leftPath.node.loc ?? null,
suggestions: null,
});
@@ -1845,7 +1784,7 @@ function lowerExpression(
if (last === null) {
builder.errors.push({
reason: `Expected sequence expression to have at least one expression`,
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: expr.node.loc ?? null,
suggestions: null,
});
@@ -2057,7 +1996,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}`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: left.node.loc ?? null,
suggestions: null,
});
@@ -2085,7 +2024,7 @@ function lowerExpression(
if (binaryOperator == null) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${operator} operators in AssignmentExpression`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: expr.node.loc ?? null,
suggestions: null,
});
@@ -2184,7 +2123,7 @@ function lowerExpression(
default: {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Expected Identifier or MemberExpression, got ${expr.type} lval in AssignmentExpression`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: expr.node.loc ?? null,
suggestions: null,
});
@@ -2223,7 +2162,7 @@ function lowerExpression(
if (!attribute.isJSXAttribute()) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${attribute.type} attributes in JSXElement`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: attribute.node.loc ?? null,
suggestions: null,
});
@@ -2236,7 +2175,7 @@ function lowerExpression(
if (propName.indexOf(':') !== -1) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Unexpected colon in attribute name \`${propName}\``,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: namePath.node.loc ?? null,
suggestions: null,
});
@@ -2245,13 +2184,7 @@ function lowerExpression(
CompilerError.invariant(namePath.isJSXNamespacedName(), {
reason: 'Refinement',
description: null,
details: [
{
kind: 'error',
loc: namePath.node.loc ?? null,
message: null,
},
],
loc: namePath.node.loc ?? null,
suggestions: null,
});
const namespace = namePath.node.namespace.name;
@@ -2272,7 +2205,7 @@ function lowerExpression(
if (!valueExpr.isJSXExpressionContainer()) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${valueExpr.type} attribute values in JSXElement`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: valueExpr.node?.loc ?? null,
suggestions: null,
});
@@ -2282,7 +2215,7 @@ function lowerExpression(
if (!expression.isExpression()) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${expression.type} expressions in JSXExpressionContainer within JSXElement`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: valueExpr.node.loc ?? null,
suggestions: null,
});
@@ -2305,14 +2238,8 @@ 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,
});
}
@@ -2345,8 +2272,8 @@ function lowerExpression(
for (const [name, locations] of Object.entries(fbtLocations)) {
if (locations.length > 1) {
CompilerError.throwDiagnostic({
category: ErrorCategory.Todo,
reason: 'Support duplicate fbt tags',
severity: ErrorSeverity.Todo,
category: 'Support duplicate fbt tags',
description: `Support \`<${tagName}>\` tags with multiple \`<${tagName}:${name}>\` values`,
details: locations.map(loc => {
return {
@@ -2406,7 +2333,7 @@ function lowerExpression(
builder.errors.push({
reason:
'(BuildHIR::lowerExpression) Handle tagged template with interpolations',
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2416,13 +2343,7 @@ function lowerExpression(
reason:
"there should be only one quasi as we don't support interpolations yet",
description: null,
details: [
{
kind: 'error',
loc: expr.node.loc ?? null,
message: null,
},
],
loc: expr.node.loc ?? null,
suggestions: null,
});
const value = expr.get('quasi').get('quasis').at(0)!.node.value;
@@ -2430,7 +2351,7 @@ function lowerExpression(
builder.errors.push({
reason:
'(BuildHIR::lowerExpression) Handle tagged template where cooked value is different from raw value',
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2452,7 +2373,7 @@ function lowerExpression(
if (subexprs.length !== quasis.length - 1) {
builder.errors.push({
reason: `Unexpected quasi and subexpression lengths in template literal`,
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2462,7 +2383,7 @@ function lowerExpression(
if (subexprs.some(e => !e.isExpression())) {
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle TSType in TemplateLiteral.`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2504,7 +2425,7 @@ function lowerExpression(
} else {
builder.errors.push({
reason: `Only object properties can be deleted`,
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: expr.node.loc ?? null,
suggestions: [
{
@@ -2519,7 +2440,7 @@ function lowerExpression(
} else if (expr.node.operator === 'throw') {
builder.errors.push({
reason: `Throw expressions are not supported`,
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: expr.node.loc ?? null,
suggestions: [
{
@@ -2640,7 +2561,7 @@ function lowerExpression(
if (!argument.isIdentifier()) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle UpdateExpression with ${argument.type} argument`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2648,7 +2569,7 @@ function lowerExpression(
} else if (builder.isContextIdentifier(argument)) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle UpdateExpression to variables captured within lambdas.`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2665,10 +2586,10 @@ function lowerExpression(
* lowerIdentifierForAssignment should have already reported an error if it returned null,
* we check here just in case
*/
if (!builder.errors.hasAnyErrors()) {
if (!builder.errors.hasErrors()) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Found an invalid UpdateExpression without a previously reported error`,
category: ErrorCategory.Invariant,
severity: ErrorSeverity.Invariant,
loc: exprLoc,
suggestions: null,
});
@@ -2677,7 +2598,7 @@ function lowerExpression(
} else if (lvalue.kind === 'Global') {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Support UpdateExpression where argument is a global`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: exprLoc,
suggestions: null,
});
@@ -2732,7 +2653,7 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle MetaProperty expressions other than import.meta`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2741,7 +2662,7 @@ function lowerExpression(
default: {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${exprPath.type} expressions`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2820,13 +2741,7 @@ function lowerOptionalMemberExpression(
CompilerError.invariant(object !== null, {
reason: 'Satisfy type checker',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
@@ -3044,7 +2959,7 @@ function lowerReorderableExpression(
if (!isReorderableExpression(builder, expr, true)) {
builder.errors.push({
reason: `(BuildHIR::node.lowerReorderableExpression) Expression type \`${expr.type}\` cannot be safely reordered`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: expr.node.loc ?? null,
suggestions: null,
});
@@ -3067,12 +2982,6 @@ 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':
@@ -3246,7 +3155,7 @@ function lowerArguments(
} else {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${argPath.type} arguments in CallExpression`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: argPath.node.loc ?? null,
suggestions: null,
});
@@ -3281,7 +3190,7 @@ function lowerMemberExpression(
} else {
builder.errors.push({
reason: `(BuildHIR::lowerMemberExpression) Handle ${propertyNode.type} property`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: propertyNode.node.loc ?? null,
suggestions: null,
});
@@ -3302,7 +3211,7 @@ function lowerMemberExpression(
if (!propertyNode.isExpression()) {
builder.errors.push({
reason: `(BuildHIR::lowerMemberExpression) Expected Expression, got ${propertyNode.type} property`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: propertyNode.node.loc ?? null,
suggestions: null,
});
@@ -3361,7 +3270,7 @@ function lowerJsxElementName(
builder.errors.push({
reason: `Expected JSXNamespacedName to have no colons in the namespace or name`,
description: `Got \`${namespace}\` : \`${name}\``,
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -3375,7 +3284,7 @@ function lowerJsxElementName(
} else {
builder.errors.push({
reason: `(BuildHIR::lowerJsxElementName) Handle ${exprPath.type} tags`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -3400,13 +3309,7 @@ function lowerJsxMemberExpression(
CompilerError.invariant(object.isJSXIdentifier(), {
reason: `TypeScript refinement fail: expected 'JsxIdentifier', got \`${object.node.type}\``,
description: null,
details: [
{
kind: 'error',
loc: object.node.loc ?? null,
message: null,
},
],
loc: object.node.loc ?? null,
suggestions: null,
});
@@ -3448,13 +3351,7 @@ function lowerJsxElement(
CompilerError.invariant(expression.isExpression(), {
reason: `(BuildHIR::lowerJsxElement) Expected Expression but found ${expression.type}!`,
description: null,
details: [
{
kind: 'error',
loc: expression.node.loc ?? null,
message: null,
},
],
loc: expression.node.loc ?? null,
suggestions: null,
});
return lowerExpressionToTemporary(builder, expression);
@@ -3485,7 +3382,7 @@ function lowerJsxElement(
} else {
builder.errors.push({
reason: `(BuildHIR::lowerJsxElement) Unhandled JsxElement, got: ${exprPath.type}`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -3567,14 +3464,17 @@ 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: loweredFunc.func.id,
nameHint: null,
name,
type: expr.node.type,
loc: exprLoc,
loweredFunc,
@@ -3669,7 +3569,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',
category: ErrorCategory.UnsupportedSyntax,
severity: ErrorSeverity.UnsupportedJS,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -3725,7 +3625,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.`,
category: ErrorCategory.Invariant,
severity: ErrorSeverity.Invariant,
loc: path.node.loc ?? null,
suggestions: null,
});
@@ -3737,7 +3637,7 @@ function lowerIdentifierForAssignment(
) {
builder.errors.push({
reason: `Cannot reassign a \`const\` variable`,
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: path.node.loc ?? null,
description:
binding.identifier.name != null
@@ -3794,7 +3694,7 @@ function lowerAssignment(
if (kind === InstructionKind.Const && !isHoistedIdentifier) {
builder.errors.push({
reason: `Expected \`const\` declaration not to be reassigned`,
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: lvalue.node.loc ?? null,
suggestions: null,
});
@@ -3808,7 +3708,7 @@ function lowerAssignment(
) {
builder.errors.push({
reason: `Unexpected context variable kind`,
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: lvalue.node.loc ?? null,
suggestions: null,
});
@@ -3852,13 +3752,7 @@ function lowerAssignment(
CompilerError.invariant(kind === InstructionKind.Reassign, {
reason: 'MemberExpression may only appear in an assignment expression',
description: null,
details: [
{
kind: 'error',
loc: lvaluePath.node.loc ?? null,
message: null,
},
],
loc: lvaluePath.node.loc ?? null,
suggestions: null,
});
const lvalue = lvaluePath as NodePath<t.MemberExpression>;
@@ -3885,7 +3779,7 @@ function lowerAssignment(
} else {
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle ${property.type} properties in MemberExpression`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: property.node.loc ?? null,
suggestions: null,
});
@@ -3897,7 +3791,7 @@ function lowerAssignment(
builder.errors.push({
reason:
'(BuildHIR::lowerAssignment) Expected private name to appear as a non-computed property',
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: property.node.loc ?? null,
suggestions: null,
});
@@ -3962,7 +3856,7 @@ function lowerAssignment(
continue;
} else if (identifier.kind === 'Global') {
builder.errors.push({
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
reason:
'Expected reassignment of globals to enable forceTemporaries',
loc: element.node.loc ?? GeneratedSource,
@@ -4001,7 +3895,7 @@ function lowerAssignment(
continue;
} else if (identifier.kind === 'Global') {
builder.errors.push({
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
reason:
'Expected reassignment of globals to enable forceTemporaries',
loc: element.node.loc ?? GeneratedSource,
@@ -4074,7 +3968,7 @@ function lowerAssignment(
if (!argument.isIdentifier()) {
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle ${argument.node.type} rest element in ObjectPattern`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: argument.node.loc ?? null,
suggestions: null,
});
@@ -4105,7 +3999,7 @@ function lowerAssignment(
continue;
} else if (identifier.kind === 'Global') {
builder.errors.push({
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
reason:
'Expected reassignment of globals to enable forceTemporaries',
loc: property.node.loc ?? GeneratedSource,
@@ -4122,7 +4016,7 @@ function lowerAssignment(
if (!property.isObjectProperty()) {
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle ${property.type} properties in ObjectPattern`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: property.node.loc ?? null,
suggestions: null,
});
@@ -4131,7 +4025,7 @@ function lowerAssignment(
if (property.node.computed) {
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: property.node.loc ?? null,
suggestions: null,
});
@@ -4145,7 +4039,7 @@ function lowerAssignment(
if (!element.isLVal()) {
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Expected object property value to be an LVal, got: ${element.type}`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: element.node.loc ?? null,
suggestions: null,
});
@@ -4167,7 +4061,7 @@ function lowerAssignment(
continue;
} else if (identifier.kind === 'Global') {
builder.errors.push({
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
reason:
'Expected reassignment of globals to enable forceTemporaries',
loc: element.node.loc ?? GeneratedSource,
@@ -4316,7 +4210,7 @@ function lowerAssignment(
default: {
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle ${lvaluePath.type} assignments`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: lvaluePath.node.loc ?? null,
suggestions: null,
});

View File

@@ -234,14 +234,7 @@ function pushEndScopeTerminal(
const fallthroughId = context.fallthroughs.get(scope.id);
CompilerError.invariant(fallthroughId != null, {
reason: 'Expected scope to exist',
description: null,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
context.rewrites.push({
kind: 'EndScope',

View File

@@ -269,14 +269,7 @@ class PropertyPathRegistry {
CompilerError.invariant(reactive === rootNode.fullPath.reactive, {
reason:
'[HoistablePropertyLoads] Found inconsistencies in `reactive` flag when deduping identifier reads within the same scope',
description: null,
details: [
{
kind: 'error',
loc: identifier.loc,
message: null,
},
],
loc: identifier.loc,
});
}
return rootNode;
@@ -454,32 +447,6 @@ function collectNonNullsInBlocks(
assumedNonNullObjects.add(entry);
}
}
} else if (
fn.env.config.enablePreserveExistingMemoizationGuarantees &&
instr.value.kind === 'StartMemoize' &&
instr.value.deps != null
) {
for (const dep of instr.value.deps) {
if (dep.root.kind === 'NamedLocal') {
if (
!isImmutableAtInstr(dep.root.value.identifier, instr.id, context)
) {
continue;
}
for (let i = 0; i < dep.path.length; i++) {
const pathEntry = dep.path[i]!;
if (pathEntry.optional) {
break;
}
const depNode = context.registry.getOrCreateProperty({
identifier: dep.root.value.identifier,
path: dep.path.slice(0, i),
reactive: dep.root.value.reactive,
});
assumedNonNullObjects.add(depNode);
}
}
}
}
}
@@ -531,14 +498,7 @@ function propagateNonNull(
if (node == null) {
CompilerError.invariant(false, {
reason: `Bad node ${nodeId}, kind: ${direction}`,
description: null,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
}
const neighbors = Array.from(
@@ -610,14 +570,7 @@ function propagateNonNull(
CompilerError.invariant(i++ < 100, {
reason:
'[CollectHoistablePropertyLoads] fixed point iteration did not terminate after 100 loops',
description: null,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
changed = false;
@@ -649,13 +602,7 @@ export function assertNonNull<T extends NonNullable<U>, U>(
CompilerError.invariant(value != null, {
reason: 'Unexpected null',
description: source != null ? `(from ${source})` : null,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
return value;
}

View File

@@ -186,13 +186,7 @@ function matchOptionalTestBlock(
reason:
'[OptionalChainDeps] Inconsistent optional chaining property load',
description: `Test=${printIdentifier(terminal.test.identifier)} PropertyLoad base=${printIdentifier(propertyLoad.value.object.identifier)}`,
details: [
{
kind: 'error',
loc: propertyLoad.loc,
message: null,
},
],
loc: propertyLoad.loc,
},
);
@@ -200,14 +194,7 @@ function matchOptionalTestBlock(
storeLocal.value.identifier.id === propertyLoad.lvalue.identifier.id,
{
reason: '[OptionalChainDeps] Unexpected storeLocal',
description: null,
details: [
{
kind: 'error',
loc: propertyLoad.loc,
message: null,
},
],
loc: propertyLoad.loc,
},
);
if (
@@ -224,14 +211,7 @@ function matchOptionalTestBlock(
alternate.instructions[1].value.kind === 'StoreLocal',
{
reason: 'Unexpected alternate structure',
description: null,
details: [
{
kind: 'error',
loc: terminal.loc,
message: null,
},
],
loc: terminal.loc,
},
);
@@ -267,14 +247,7 @@ function traverseOptionalBlock(
if (maybeTest.terminal.kind === 'branch') {
CompilerError.invariant(optional.terminal.optional, {
reason: '[OptionalChainDeps] Expect base case to be always optional',
description: null,
details: [
{
kind: 'error',
loc: optional.terminal.loc,
message: null,
},
],
loc: optional.terminal.loc,
});
/**
* Optional base expressions are currently within value blocks which cannot
@@ -312,14 +285,7 @@ function traverseOptionalBlock(
maybeTest.instructions.at(-1)!.lvalue.identifier.id,
{
reason: '[OptionalChainDeps] Unexpected test expression',
description: null,
details: [
{
kind: 'error',
loc: maybeTest.terminal.loc,
message: null,
},
],
loc: maybeTest.terminal.loc,
},
);
baseObject = {
@@ -408,14 +374,7 @@ function traverseOptionalBlock(
reason:
'[OptionalChainDeps] Unexpected instructions an inner optional block. ' +
'This indicates that the compiler may be incorrectly concatenating two unrelated optional chains',
description: null,
details: [
{
kind: 'error',
loc: optional.terminal.loc,
message: null,
},
],
loc: optional.terminal.loc,
});
}
const matchConsequentResult = matchOptionalTestBlock(test, context.blocks);
@@ -428,13 +387,7 @@ function traverseOptionalBlock(
{
reason: '[OptionalChainDeps] Unexpected optional goto-fallthrough',
description: `${matchConsequentResult.consequentGoto} != ${optional.terminal.fallthrough}`,
details: [
{
kind: 'error',
loc: optional.terminal.loc,
message: null,
},
],
loc: optional.terminal.loc,
},
);
const load = {

View File

@@ -24,14 +24,7 @@ export function computeUnconditionalBlocks(fn: HIRFunction): Set<BlockId> {
CompilerError.invariant(!unconditionalBlocks.has(current), {
reason:
'Internal error: non-terminating loop in ComputeUnconditionalBlocks',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
unconditionalBlocks.add(current);

View File

@@ -1,109 +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, 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;
}

View File

@@ -54,14 +54,7 @@ export class ReactiveScopeDependencyTreeHIR {
prevAccessType == null || prevAccessType === accessType,
{
reason: 'Conflicting access types',
description: null,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
},
);
let nextNode = currNode.properties.get(path[i].property);
@@ -97,13 +90,7 @@ export class ReactiveScopeDependencyTreeHIR {
CompilerError.invariant(reactive === rootNode.reactive, {
reason: '[DeriveMinimalDependenciesHIR] Conflicting reactive root flag',
description: `Identifier ${printIdentifier(identifier)}`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
}
return rootNode;

View File

@@ -89,13 +89,7 @@ export class Dominator<T> {
CompilerError.invariant(dominator !== undefined, {
reason: 'Unknown node',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return dominator === id ? null : dominator;
@@ -136,13 +130,7 @@ export class PostDominator<T> {
CompilerError.invariant(dominator !== undefined, {
reason: 'Unknown node',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return dominator === id ? null : dominator;
@@ -187,13 +175,7 @@ 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,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});

View File

@@ -6,10 +6,10 @@
*/
import * as t from '@babel/types';
import {ZodError, z} from 'zod/v4';
import {fromZodError} from 'zod-validation-error/v4';
import {ZodError, z} from 'zod';
import {fromZodError} from 'zod-validation-error';
import {CompilerError} from '../CompilerError';
import {CompilerOutputMode, Logger, ProgramContext} from '../Entrypoint';
import {Logger, ProgramContext} from '../Entrypoint';
import {Err, Ok, Result} from '../Utils/Result';
import {
DEFAULT_GLOBALS,
@@ -49,9 +49,6 @@ import {
} from './ObjectShape';
import {Scope as BabelScope, NodePath} from '@babel/traverse';
import {TypeSchema} from './TypeSchema';
import {FlowTypeEnv} from '../Flood/Types';
import {defaultModuleTypeProvider} from './DefaultModuleTypeProvider';
import {assertExhaustive} from '../Utils/utils';
export const ReactElementSymbolSchema = z.object({
elementSymbol: z.union([
@@ -84,11 +81,21 @@ export type ExternalFunction = z.infer<typeof ExternalFunctionSchema>;
export const USE_FIRE_FUNCTION_NAME = 'useFire';
export const EMIT_FREEZE_GLOBAL_GATING = '__DEV__';
export const MacroSchema = z.string();
export const MacroMethodSchema = z.union([
z.object({type: z.literal('wildcard')}),
z.object({type: z.literal('name'), name: z.string()}),
]);
// Would like to change this to drop the string option, but breaks compatibility with existing configs
export const MacroSchema = z.union([
z.string(),
z.tuple([z.string(), z.array(MacroMethodSchema)]),
]);
export type CompilerMode = 'all_features' | 'no_inferred_memo';
export type Macro = z.infer<typeof MacroSchema>;
export type MacroMethod = z.infer<typeof MacroMethodSchema>;
const HookSchema = z.object({
/*
@@ -150,7 +157,7 @@ export const EnvironmentConfigSchema = z.object({
* A function that, given the name of a module, can optionally return a description
* of that module's type signature.
*/
moduleTypeProvider: z.nullable(z.any()).default(null),
moduleTypeProvider: z.nullable(z.function().args(z.string())).default(null),
/**
* A list of functions which the application compiles as macros, where
@@ -201,7 +208,7 @@ export const EnvironmentConfigSchema = z.object({
* that if a useEffect or useCallback references a function value, that function value will be
* considered frozen, and in turn all of its referenced variables will be considered frozen as well.
*/
enablePreserveExistingMemoizationGuarantees: z.boolean().default(true),
enablePreserveExistingMemoizationGuarantees: z.boolean().default(false),
/**
* Validates that all useMemo/useCallback values are also memoized by Forget. This mode can be
@@ -218,11 +225,6 @@ export const EnvironmentConfigSchema = z.object({
*/
validatePreserveExistingMemoizationGuarantees: z.boolean().default(true),
/**
* Validate that dependencies supplied to manual memoization calls are exhaustive.
*/
validateExhaustiveMemoizationDependencies: z.boolean().default(false),
/**
* When this is true, rather than pruning existing manual memoization but ensuring or validating
* that the memoized values remain memoized, the compiler will simply not prune existing calls to
@@ -242,10 +244,9 @@ export const EnvironmentConfigSchema = z.object({
enableUseTypeAnnotations: z.boolean().default(false),
/**
* Allows specifying a function that can populate HIR with type information from
* Flow
* Enable a new model for mutability and aliasing inference
*/
flowTypeProvider: z.nullable(z.any()).default(null),
enableNewMutationAliasingModel: z.boolean().default(true),
/**
* Enables inference of optional dependency chains. Without this flag
@@ -257,8 +258,6 @@ 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,
@@ -324,18 +323,6 @@ export const EnvironmentConfigSchema = z.object({
*/
validateNoSetStateInEffects: z.boolean().default(false),
/**
* Validates that effects are not used to calculate derived data which could instead be computed
* during render.
*/
validateNoDerivedComputationsInEffects: z.boolean().default(false),
/**
* Experimental: Validates that effects are not used to calculate derived data which could instead be computed
* during render. Generates a custom error message for each type of violation.
*/
validateNoDerivedComputationsInEffects_exp: z.boolean().default(false),
/**
* Validates against creating JSX within a try block and recommends using an error boundary
* instead.
@@ -370,13 +357,6 @@ export const EnvironmentConfigSchema = z.object({
validateNoCapitalizedCalls: z.nullable(z.array(z.string())).default(null),
validateBlocklistedImports: z.nullable(z.array(z.string())).default(null),
/**
* Validates that AST nodes generated during codegen have proper source locations.
* This is useful for debugging issues with source maps and Istanbul coverage.
* When enabled, the compiler will error if important source locations are missing in the generated AST.
*/
validateSourceLocations: z.boolean().default(false),
/**
* Validate against impure functions called during render
*/
@@ -628,14 +608,7 @@ export const EnvironmentConfigSchema = z.object({
*
* Here the variables `ref` and `myRef` will be typed as Refs.
*/
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),
enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(false),
/*
* If specified a value, the compiler lowers any calls to `useContext` to use
@@ -658,45 +631,6 @@ export const EnvironmentConfigSchema = z.object({
* ```
*/
lowerContextAccess: ExternalFunctionSchema.nullable().default(null),
/**
* If enabled, will validate useMemos that don't return any values:
*
* Valid:
* useMemo(() => foo, [foo]);
* useMemo(() => { return foo }, [foo]);
* Invalid:
* useMemo(() => { ... }, [...]);
*/
validateNoVoidUseMemo: z.boolean().default(true),
/**
* Validates that Components/Hooks are always defined at module level. This prevents scope
* reference errors that occur when the compiler attempts to optimize the nested component/hook
* while its parent function remains uncompiled.
*/
validateNoDynamicallyCreatedComponentsOrHooks: z.boolean().default(false),
/**
* When enabled, allows setState calls in effects based on valid patterns involving refs:
* - Allow setState where the value being set is derived from a ref. This is useful where
* state needs to take into account layer information, and a layout effect reads layout
* data from a ref and sets state.
* - Allow conditionally calling setState after manually comparing previous/new values
* for changes via a ref. Relying on effect deps is insufficient for non-primitive values,
* so a ref is generally required to manually track previous values and compare prev/next
* for meaningful changes before setting state.
*/
enableAllowSetStateFromRefsInEffects: z.boolean().default(true),
/**
* Enables inference of event handler types for JSX props on built-in DOM elements.
* When enabled, functions passed to event handler props (props starting with "on")
* on primitive JSX tags are inferred to have the BuiltinEventHandlerId type, which
* allows ref access within those functions since DOM event handlers are guaranteed
* by React to only execute in response to events, not during render.
*/
enableInferEventHandlers: z.boolean().default(false),
});
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
@@ -736,7 +670,7 @@ export class Environment {
code: string | null;
config: EnvironmentConfig;
fnType: ReactFunctionType;
outputMode: CompilerOutputMode;
compilerMode: CompilerMode;
programContext: ProgramContext;
hasFireRewrite: boolean;
hasInferredEffect: boolean;
@@ -746,12 +680,10 @@ export class Environment {
#hoistedIdentifiers: Set<t.Identifier>;
parentFunction: NodePath<t.Function>;
#flowTypeEnvironment: FlowTypeEnv | null;
constructor(
scope: BabelScope,
fnType: ReactFunctionType,
outputMode: CompilerOutputMode,
compilerMode: CompilerMode,
config: EnvironmentConfig,
contextIdentifiers: Set<t.Identifier>,
parentFunction: NodePath<t.Function>, // the outermost function being compiled
@@ -762,7 +694,7 @@ export class Environment {
) {
this.#scope = scope;
this.fnType = fnType;
this.outputMode = outputMode;
this.compilerMode = compilerMode;
this.config = config;
this.filename = filename;
this.code = code;
@@ -789,13 +721,7 @@ export class Environment {
CompilerError.invariant(!this.#globals.has(hookName), {
reason: `[Globals] Found existing definition in global registry for custom hook ${hookName}`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
this.#globals.set(
@@ -822,101 +748,10 @@ export class Environment {
this.parentFunction = parentFunction;
this.#contextIdentifiers = contextIdentifiers;
this.#hoistedIdentifiers = new Set();
if (config.flowTypeProvider != null) {
this.#flowTypeEnvironment = new FlowTypeEnv();
CompilerError.invariant(code != null, {
reason:
'Expected Environment to be initialized with source code when a Flow type provider is specified',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
});
this.#flowTypeEnvironment.init(this, code);
} else {
this.#flowTypeEnvironment = null;
}
}
get typeContext(): FlowTypeEnv {
CompilerError.invariant(this.#flowTypeEnvironment != null, {
reason: 'Flow type environment not initialized',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
});
return this.#flowTypeEnvironment;
}
get enableDropManualMemoization(): boolean {
switch (this.outputMode) {
case 'lint': {
// linting drops to be more compatible with compiler analysis
return true;
}
case 'client':
case 'ssr': {
return true;
}
case 'client-no-memo': {
return false;
}
default: {
assertExhaustive(
this.outputMode,
`Unexpected output mode '${this.outputMode}'`,
);
}
}
}
get enableMemoization(): boolean {
switch (this.outputMode) {
case 'client':
case 'lint': {
// linting also enables memoization so that we can check if manual memoization is preserved
return true;
}
case 'ssr':
case 'client-no-memo': {
return false;
}
default: {
assertExhaustive(
this.outputMode,
`Unexpected output mode '${this.outputMode}'`,
);
}
}
}
get enableValidations(): boolean {
switch (this.outputMode) {
case 'client':
case 'lint':
case 'ssr': {
return true;
}
case 'client-no-memo': {
return false;
}
default: {
assertExhaustive(
this.outputMode,
`Unexpected output mode '${this.outputMode}'`,
);
}
}
get isInferredMemoEnabled(): boolean {
return this.compilerMode !== 'no_inferred_memo';
}
get nextIdentifierId(): IdentifierId {
@@ -977,22 +812,10 @@ export class Environment {
#resolveModuleType(moduleName: string, loc: SourceLocation): Global | null {
let moduleType = this.#moduleTypes.get(moduleName);
if (moduleType === undefined) {
/*
* 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) {
if (this.config.moduleTypeProvider == null) {
return null;
}
if (typeof moduleTypeProvider !== 'function') {
CompilerError.throwInvalidConfig({
reason: `Expected a function for \`moduleTypeProvider\``,
loc,
});
}
const unparsedModuleConfig = moduleTypeProvider(moduleName);
const unparsedModuleConfig = this.config.moduleTypeProvider(moduleName);
if (unparsedModuleConfig != null) {
const parsedModuleConfig = TypeSchema.safeParse(unparsedModuleConfig);
if (!parsedModuleConfig.success) {
@@ -1166,13 +989,7 @@ export class Environment {
CompilerError.invariant(shape !== undefined, {
reason: `[HIR] Forget internal error: cannot resolve shape ${shapeId}`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return shape.properties.get('*') ?? null;
@@ -1197,13 +1014,7 @@ export class Environment {
CompilerError.invariant(shape !== undefined, {
reason: `[HIR] Forget internal error: cannot resolve shape ${shapeId}`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
if (typeof property === 'string') {
@@ -1228,13 +1039,7 @@ export class Environment {
CompilerError.invariant(shape !== undefined, {
reason: `[HIR] Forget internal error: cannot resolve shape ${shapeId}`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return shape.functionType;

View File

@@ -184,13 +184,7 @@ function handleAssignment(
CompilerError.invariant(valuePath.isLVal(), {
reason: `[FindContextIdentifiers] Expected object property value to be an LVal, got: ${valuePath.type}`,
description: null,
details: [
{
kind: 'error',
loc: valuePath.node.loc ?? GeneratedSource,
message: null,
},
],
loc: valuePath.node.loc ?? GeneratedSource,
suggestions: null,
});
handleAssignment(currentFn, identifiers, valuePath);
@@ -198,13 +192,7 @@ function handleAssignment(
CompilerError.invariant(property.isRestElement(), {
reason: `[FindContextIdentifiers] Invalid assumptions for babel types.`,
description: null,
details: [
{
kind: 'error',
loc: property.node.loc ?? GeneratedSource,
message: null,
},
],
loc: property.node.loc ?? GeneratedSource,
suggestions: null,
});
handleAssignment(currentFn, identifiers, property);

View File

@@ -23,14 +23,13 @@ import {
BuiltInUseInsertionEffectHookId,
BuiltInUseLayoutEffectHookId,
BuiltInUseOperatorId,
BuiltInUseOptimisticId,
BuiltInUseReducerId,
BuiltInUseRefId,
BuiltInUseStateId,
BuiltInUseTransitionId,
BuiltInWeakMapId,
BuiltInWeakSetId,
BuiltInEffectEventId,
BuiltinEffectEventId,
ReanimatedSharedValueId,
ShapeRegistry,
addFunction,
@@ -115,99 +114,6 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
returnValueKind: ValueKind.Mutable,
}),
],
[
'entries',
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [Effect.Capture],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
aliasing: {
receiver: '@receiver',
params: ['@object'],
rest: null,
returns: '@returns',
temporaries: [],
effects: [
{
kind: 'Create',
into: '@returns',
reason: ValueReason.KnownReturnSignature,
value: ValueKind.Mutable,
},
// Object values are captured into the return
{
kind: 'Capture',
from: '@object',
into: '@returns',
},
],
},
}),
],
[
'keys',
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [Effect.Read],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
aliasing: {
receiver: '@receiver',
params: ['@object'],
rest: null,
returns: '@returns',
temporaries: [],
effects: [
{
kind: 'Create',
into: '@returns',
reason: ValueReason.KnownReturnSignature,
value: ValueKind.Mutable,
},
// Only keys are captured, and keys are immutable
{
kind: 'ImmutableCapture',
from: '@object',
into: '@returns',
},
],
},
}),
],
[
'values',
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [Effect.Capture],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
aliasing: {
receiver: '@receiver',
params: ['@object'],
rest: null,
returns: '@returns',
temporaries: [],
effects: [
{
kind: 'Create',
into: '@returns',
reason: ValueReason.KnownReturnSignature,
value: ValueKind.Mutable,
},
// Object values are captured into the return
{
kind: 'Capture',
from: '@object',
into: '@returns',
},
],
},
}),
],
]),
],
[
@@ -819,18 +725,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
returnValueKind: ValueKind.Frozen,
}),
],
[
'useOptimistic',
addHook(DEFAULT_SHAPES, {
positionalParams: [],
restParam: Effect.Freeze,
returnType: {kind: 'Object', shapeId: BuiltInUseOptimisticId},
calleeEffect: Effect.Read,
hookKind: 'useOptimistic',
returnValueKind: ValueKind.Frozen,
returnValueReason: ValueReason.State,
}),
],
[
'use',
addFunction(
@@ -876,7 +770,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
returnType: {
kind: 'Function',
return: {kind: 'Poly'},
shapeId: BuiltInEffectEventId,
shapeId: BuiltinEffectEventId,
isConstructor: false,
},
calleeEffect: Effect.Read,
@@ -1014,7 +908,6 @@ export function installTypeConfig(
mutableOnlyIfOperandsAreMutable:
typeConfig.mutableOnlyIfOperandsAreMutable === true,
aliasing: typeConfig.aliasing,
knownIncompatible: typeConfig.knownIncompatible ?? null,
});
}
case 'hook': {
@@ -1033,7 +926,6 @@ export function installTypeConfig(
returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen,
noAlias: typeConfig.noAlias === true,
aliasing: typeConfig.aliasing,
knownIncompatible: typeConfig.knownIncompatible ?? null,
});
}
case 'object': {

View File

@@ -7,19 +7,13 @@
import {BindingKind} from '@babel/traverse';
import * as t from '@babel/types';
import {
CompilerDiagnostic,
CompilerError,
ErrorCategory,
} from '../CompilerError';
import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError';
import {assertExhaustive} from '../Utils/utils';
import {Environment, ReactFunctionType} from './Environment';
import type {HookKind} from './ObjectShape';
import {Type, makeType} from './Types';
import {z} from 'zod/v4';
import {z} from 'zod';
import type {AliasingEffect} from '../Inference/AliasingEffects';
import {isReservedWord} from '../Utils/Keyword';
import {Err, Ok, Result} from '../Utils/Result';
/*
* *******************************************************************************************
@@ -58,8 +52,7 @@ export type SourceLocation = t.SourceLocation | typeof GeneratedSource;
*/
export type ReactiveFunction = {
loc: SourceLocation;
id: ValidIdentifierName | null;
nameHint: string | null;
id: string | null;
params: Array<Place | SpreadPattern>;
generator: boolean;
async: boolean;
@@ -281,21 +274,37 @@ 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: ValidIdentifierName | null;
nameHint: string | null;
id: 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
@@ -437,20 +446,8 @@ export type ThrowTerminal = {
};
export type Case = {test: Place | null; block: BlockId};
export type ReturnVariant = 'Void' | 'Implicit' | 'Explicit';
export type ReturnTerminal = {
kind: 'return';
/**
* Void:
* () => { ... }
* function() { ... }
* Implicit (ArrowFunctionExpression only):
* () => foo
* Explicit:
* () => { return ... }
* function () { return ... }
*/
returnVariant: ReturnVariant;
loc: SourceLocation;
value: Place;
id: InstructionId;
@@ -817,11 +814,6 @@ export type StartMemoize = {
* (e.g. useMemo without a second arg)
*/
deps: Array<ManualMemoDependency> | null;
/**
* The source location of the dependencies argument. Used for
* emitting diagnostics with a suggested replacement
*/
depsLoc: SourceLocation | null;
loc: SourceLocation;
};
export type FinishMemoize = {
@@ -1135,8 +1127,7 @@ export type JsxAttribute =
export type FunctionExpression = {
kind: 'FunctionExpression';
name: ValidIdentifierName | null;
nameHint: string | null;
name: string | null;
loweredFunc: LoweredFunction;
type:
| 'ArrowFunctionExpression'
@@ -1311,52 +1302,22 @@ 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 {
return validateIdentifierName(name).unwrap();
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,
};
}
/**
@@ -1368,14 +1329,8 @@ 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 = {
@@ -1398,14 +1353,8 @@ 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 = {
@@ -1573,13 +1522,7 @@ export function isMutableEffect(
CompilerError.invariant(false, {
reason: 'Unexpected unknown effect',
description: null,
details: [
{
kind: 'error',
loc: location,
message: null,
},
],
loc: location,
suggestions: null,
});
}
@@ -1685,28 +1628,6 @@ export function areEqualPaths(a: DependencyPath, b: DependencyPath): boolean {
)
);
}
export function isSubPath(
subpath: DependencyPath,
path: DependencyPath,
): boolean {
return (
subpath.length <= path.length &&
subpath.every(
(item, ix) =>
item.property === path[ix].property &&
item.optional === path[ix].optional,
)
);
}
export function isSubPathIgnoringOptionals(
subpath: DependencyPath,
path: DependencyPath,
): boolean {
return (
subpath.length <= path.length &&
subpath.every((item, ix) => item.property === path[ix].property)
);
}
export function getPlaceScope(
id: InstructionId,
@@ -1734,13 +1655,7 @@ 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,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return id as BlockId;
@@ -1757,13 +1672,7 @@ 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,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return id as ScopeId;
@@ -1780,13 +1689,7 @@ 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,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return id as IdentifierId;
@@ -1803,13 +1706,7 @@ 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,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return id as DeclarationId;
@@ -1826,13 +1723,7 @@ 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,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return id as InstructionId;
@@ -1850,10 +1741,6 @@ export function isPrimitiveType(id: Identifier): boolean {
return id.type.kind === 'Primitive';
}
export function isPlainObjectType(id: Identifier): boolean {
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInObject';
}
export function isArrayType(id: Identifier): boolean {
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInArray';
}
@@ -1918,18 +1805,6 @@ export function isStartTransitionType(id: Identifier): boolean {
);
}
export function isUseOptimisticType(id: Identifier): boolean {
return (
id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseOptimistic'
);
}
export function isSetOptimisticType(id: Identifier): boolean {
return (
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetOptimistic'
);
}
export function isSetActionStateType(id: Identifier): boolean {
return (
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetActionState'
@@ -1963,8 +1838,7 @@ export function isStableType(id: Identifier): boolean {
isSetActionStateType(id) ||
isDispatcherType(id) ||
isUseRefType(id) ||
isStartTransitionType(id) ||
isSetOptimisticType(id)
isStartTransitionType(id)
);
}
@@ -1975,9 +1849,8 @@ export function isStableTypeContainer(id: Identifier): boolean {
}
return (
isUseStateType(id) || // setState
isUseActionStateType(id) || // setActionState
type_.shapeId === 'BuiltInUseActionState' || // setActionState
isUseReducerType(id) || // dispatcher
isUseOptimisticType(id) || // setOptimistic
type_.shapeId === 'BuiltInUseTransition' // startTransition
);
}
@@ -1997,7 +1870,6 @@ export function evaluatesToStableTypeOrContainer(
case 'useActionState':
case 'useRef':
case 'useTransition':
case 'useOptimistic':
return true;
}
}

View File

@@ -7,7 +7,7 @@
import {Binding, NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {CompilerError, ErrorCategory} from '../CompilerError';
import {CompilerError, ErrorSeverity} 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({
category: ErrorCategory.Todo,
reason: 'Support local variables named `fbt`',
severity: ErrorSeverity.Todo,
category: 'Support local variables named `fbt`',
description:
'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',
details: [
@@ -322,21 +322,6 @@ 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;
@@ -507,13 +492,7 @@ export default class HIRBuilder {
{
reason: 'Mismatched label',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
},
);
@@ -536,13 +515,7 @@ export default class HIRBuilder {
{
reason: 'Mismatched label',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
},
);
@@ -578,13 +551,7 @@ export default class HIRBuilder {
{
reason: 'Mismatched loops',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
},
);
@@ -609,13 +576,7 @@ export default class HIRBuilder {
CompilerError.invariant(false, {
reason: 'Expected a loop or switch to be in scope',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
}
@@ -636,13 +597,7 @@ export default class HIRBuilder {
CompilerError.invariant(false, {
reason: 'Continue may only refer to a labeled loop',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
}
@@ -650,13 +605,7 @@ export default class HIRBuilder {
CompilerError.invariant(false, {
reason: 'Expected a loop to be in scope',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
}
@@ -679,13 +628,7 @@ function _shrink(func: HIR): void {
CompilerError.invariant(block != null, {
reason: `expected block ${blockId} to exist`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
target = getTargetIfIndirection(block);
@@ -817,13 +760,7 @@ function getReversePostorderedBlocks(func: HIR): HIR['blocks'] {
CompilerError.invariant(block != null, {
reason: '[HIRBuilder] Unexpected null block',
description: `expected block ${blockId} to exist`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
const successors = [...eachTerminalSuccessor(block.terminal)].reverse();
const fallthrough = terminalFallthrough(block.terminal);
@@ -879,13 +816,7 @@ export function markInstructionIds(func: HIR): void {
CompilerError.invariant(!visited.has(instr), {
reason: `${printInstruction(instr)} already visited!`,
description: null,
details: [
{
kind: 'error',
loc: instr.loc,
message: null,
},
],
loc: instr.loc,
suggestions: null,
});
visited.add(instr);
@@ -908,13 +839,7 @@ export function markPredecessors(func: HIR): void {
CompilerError.invariant(block != null, {
reason: 'unexpected missing block',
description: `block ${blockId}`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
if (prevBlock) {
block.preds.add(prevBlock.id);

View File

@@ -61,13 +61,7 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
CompilerError.invariant(predecessor !== undefined, {
reason: `Expected predecessor ${predecessorId} to exist`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
if (predecessor.terminal.kind !== 'goto' || predecessor.kind !== 'block') {
@@ -83,13 +77,7 @@ 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,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
const operand = Array.from(phi.operands.values())[0]!;

View File

@@ -119,13 +119,7 @@ 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}'`,
details: [
{
kind: 'error',
loc,
message: null,
},
],
loc,
});
const place = signatureArgument(lifetimes.size);
lifetimes.set(temp, place);
@@ -136,13 +130,7 @@ 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}'`,
details: [
{
kind: 'error',
loc,
message: null,
},
],
loc,
});
return place;
}
@@ -154,7 +142,6 @@ function parseAliasingSignatureConfig(
const effects = typeConfig.effects.map(
(effect: AliasingEffectConfig): AliasingEffect => {
switch (effect.kind) {
case 'ImmutableCapture':
case 'CreateFrom':
case 'Capture':
case 'Alias':
@@ -277,13 +264,7 @@ function addShape(
CompilerError.invariant(!registry.has(id), {
reason: `[ObjectShape] Could not add shape to registry: name ${id} already exists.`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
registry.set(id, shape);
@@ -304,7 +285,6 @@ export type HookKind =
| 'useTransition'
| 'useImperativeHandle'
| 'useEffectEvent'
| 'useOptimistic'
| 'Custom';
/*
@@ -351,7 +331,6 @@ export type FunctionSignature = {
mutableOnlyIfOperandsAreMutable?: boolean;
impure?: boolean;
knownIncompatible?: string | null | undefined;
canonicalName?: string;
@@ -400,15 +379,12 @@ export const BuiltInUseReducerId = 'BuiltInUseReducer';
export const BuiltInDispatchId = 'BuiltInDispatch';
export const BuiltInUseContextHookId = 'BuiltInUseContextHook';
export const BuiltInUseTransitionId = 'BuiltInUseTransition';
export const BuiltInUseOptimisticId = 'BuiltInUseOptimistic';
export const BuiltInSetOptimisticId = 'BuiltInSetOptimistic';
export const BuiltInStartTransitionId = 'BuiltInStartTransition';
export const BuiltInFireId = 'BuiltInFire';
export const BuiltInFireFunctionId = 'BuiltInFireFunction';
export const BuiltInUseEffectEventId = 'BuiltInUseEffectEvent';
export const BuiltInEffectEventId = 'BuiltInEffectEventFunction';
export const BuiltinEffectEventId = 'BuiltInEffectEventFunction';
export const BuiltInAutodepsId = 'BuiltInAutoDepsId';
export const BuiltInEventHandlerId = 'BuiltInEventHandlerId';
// See getReanimatedModuleType() in Globals.ts — this is part of supporting Reanimated's ref-like types
export const ReanimatedSharedValueId = 'ReanimatedSharedValueId';
@@ -1189,25 +1165,6 @@ addObject(BUILTIN_SHAPES, BuiltInUseTransitionId, [
],
]);
addObject(BUILTIN_SHAPES, BuiltInUseOptimisticId, [
['0', {kind: 'Poly'}],
[
'1',
addFunction(
BUILTIN_SHAPES,
[],
{
positionalParams: [],
restParam: Effect.Freeze,
returnType: PRIMITIVE_TYPE,
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
},
BuiltInSetOptimisticId,
),
],
]);
addObject(BUILTIN_SHAPES, BuiltInUseActionStateId, [
['0', {kind: 'Poly'}],
[
@@ -1254,8 +1211,6 @@ addObject(BUILTIN_SHAPES, BuiltInRefValueId, [
['*', {kind: 'Object', shapeId: BuiltInRefValueId}],
]);
addObject(BUILTIN_SHAPES, ReanimatedSharedValueId, []);
addFunction(
BUILTIN_SHAPES,
[],
@@ -1266,20 +1221,7 @@ addFunction(
calleeEffect: Effect.ConditionallyMutate,
returnValueKind: ValueKind.Mutable,
},
BuiltInEffectEventId,
);
addFunction(
BUILTIN_SHAPES,
[],
{
positionalParams: [],
restParam: Effect.ConditionallyMutate,
returnType: {kind: 'Poly'},
calleeEffect: Effect.ConditionallyMutate,
returnValueKind: ValueKind.Mutable,
},
BuiltInEventHandlerId,
BuiltinEffectEventId,
);
/**

View File

@@ -56,9 +56,6 @@ export function printFunction(fn: HIRFunction): string {
} else {
definition += '<<anonymous>>';
}
if (fn.nameHint != null) {
definition += ` ${fn.nameHint}`;
}
if (fn.params.length !== 0) {
definition +=
'(' +
@@ -218,7 +215,7 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
break;
}
case 'return': {
value = `[${terminal.id}] Return ${terminal.returnVariant}${
value = `[${terminal.id}] Return${
terminal.value != null ? ' ' + printPlace(terminal.value) : ''
}`;
if (terminal.effects != null) {
@@ -557,11 +554,23 @@ 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}] @aliasingEffects=[${aliasingEffects}]\n${fn}`;
value = `${kind} ${name} @context[${context}] @effects[${effects}] @aliasingEffects=[${aliasingEffects}]\n${fn}`;
break;
}
case 'TaggedTemplateExpression': {
@@ -599,13 +608,7 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
{
reason: 'Bad assumption about quasi length.',
description: null,
details: [
{
kind: 'error',
loc: instrValue.loc,
message: null,
},
],
loc: instrValue.loc,
suggestions: null,
},
);
@@ -874,15 +877,8 @@ 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,
details: [
{
kind: 'error',
loc: val.root.value.loc,
message: null,
},
],
loc: val.root.value.loc,
});
rootStr = nameOnly
? val.root.value.identifier.name.value
@@ -896,8 +892,7 @@ 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) {
const returnType = printType(type.return);
return `:T${type.kind}<${type.shapeId}>()${returnType !== '' ? `: ${returnType}` : ''}`;
return `:T${type.kind}<${type.shapeId}>`;
} else {
return `:T${type.kind}`;
}
@@ -948,10 +943,7 @@ export function printAliasingEffect(effect: AliasingEffect): string {
return `Assign ${printPlaceForAliasEffect(effect.into)} = ${printPlaceForAliasEffect(effect.from)}`;
}
case 'Alias': {
return `Alias ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
}
case 'MaybeAlias': {
return `MaybeAlias ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
return `Alias ${printPlaceForAliasEffect(effect.into)} = ${printPlaceForAliasEffect(effect.from)}`;
}
case 'Capture': {
return `Capture ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
@@ -1000,7 +992,7 @@ export function printAliasingEffect(effect: AliasingEffect): string {
case 'MutateConditionally':
case 'MutateTransitive':
case 'MutateTransitiveConditionally': {
return `${effect.kind} ${printPlaceForAliasEffect(effect.value)}${effect.kind === 'Mutate' && effect.reason?.kind === 'AssignCurrentProperty' ? ' (assign `.current`)' : ''}`;
return `${effect.kind} ${printPlaceForAliasEffect(effect.value)}`;
}
case 'MutateFrozen': {
return `MutateFrozen ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;

View File

@@ -86,14 +86,7 @@ export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
const hoistables = hoistablePropertyLoads.get(scope.id);
CompilerError.invariant(hoistables != null, {
reason: '[PropagateScopeDependencies] Scope not found in tracked blocks',
description: null,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
/**
* Step 2: Calculate hoistable dependencies.
@@ -435,14 +428,7 @@ export class DependencyCollectionContext {
const scopedDependencies = this.#dependencies.value;
CompilerError.invariant(scopedDependencies != null, {
reason: '[PropagateScopeDeps]: Unexpected scope mismatch',
description: null,
details: [
{
kind: 'error',
loc: scope.loc,
message: null,
},
],
loc: scope.loc,
});
// Restore context of previous scope

View File

@@ -53,14 +53,7 @@ export function pruneUnusedLabelsHIR(fn: HIRFunction): void {
next.phis.size === 0 && fallthrough.phis.size === 0,
{
reason: 'Unexpected phis when merging label blocks',
description: null,
details: [
{
kind: 'error',
loc: label.terminal.loc,
message: null,
},
],
loc: label.terminal.loc,
},
);
@@ -71,14 +64,7 @@ export function pruneUnusedLabelsHIR(fn: HIRFunction): void {
fallthrough.preds.has(nextId),
{
reason: 'Unexpected block predecessors when merging label blocks',
description: null,
details: [
{
kind: 'error',
loc: label.terminal.loc,
message: null,
},
],
loc: label.terminal.loc,
},
);

View File

@@ -202,14 +202,8 @@ 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) {
@@ -245,13 +239,7 @@ function writeOptionalDependency(
CompilerError.invariant(testIdentifier !== null, {
reason: 'Satisfy type checker',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});

View File

@@ -6,7 +6,7 @@
*/
import {isValidIdentifier} from '@babel/types';
import {z} from 'zod/v4';
import {z} from 'zod';
import {Effect, ValueKind} from '..';
import {
EffectSchema,
@@ -111,19 +111,6 @@ export const AliasEffectSchema: z.ZodType<AliasEffectConfig> = z.object({
into: LifetimeIdSchema,
});
export type ImmutableCaptureEffectConfig = {
kind: 'ImmutableCapture';
from: string;
into: string;
};
export const ImmutableCaptureEffectSchema: z.ZodType<ImmutableCaptureEffectConfig> =
z.object({
kind: z.literal('ImmutableCapture'),
from: LifetimeIdSchema,
into: LifetimeIdSchema,
});
export type CaptureEffectConfig = {
kind: 'Capture';
from: string;
@@ -200,7 +187,6 @@ export type AliasingEffectConfig =
| AssignEffectConfig
| AliasEffectConfig
| CaptureEffectConfig
| ImmutableCaptureEffectConfig
| ImpureEffectConfig
| MutateEffectConfig
| MutateTransitiveConditionallyConfig
@@ -213,7 +199,6 @@ export const AliasingEffectSchema: z.ZodType<AliasingEffectConfig> = z.union([
AssignEffectSchema,
AliasEffectSchema,
CaptureEffectSchema,
ImmutableCaptureEffectSchema,
ImpureEffectSchema,
MutateEffectSchema,
MutateTransitiveConditionallySchema,
@@ -251,7 +236,6 @@ 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'),
@@ -265,7 +249,6 @@ 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 = {
@@ -276,7 +259,6 @@ 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'),
@@ -286,7 +268,6 @@ 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 =

View File

@@ -87,13 +87,7 @@ 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,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return id as TypeId;

View File

@@ -11,7 +11,6 @@ import {
BasicBlock,
BlockId,
Instruction,
InstructionKind,
InstructionValue,
makeInstructionId,
Pattern,
@@ -33,32 +32,6 @@ export function* eachInstructionLValue(
yield* eachInstructionValueLValue(instr.value);
}
export function* eachInstructionLValueWithKind(
instr: ReactiveInstruction,
): Iterable<[Place, InstructionKind]> {
switch (instr.value.kind) {
case 'DeclareContext':
case 'StoreContext':
case 'DeclareLocal':
case 'StoreLocal': {
yield [instr.value.lvalue.place, instr.value.lvalue.kind];
break;
}
case 'Destructure': {
const kind = instr.value.lvalue.kind;
for (const place of eachPatternOperand(instr.value.lvalue.pattern)) {
yield [place, kind];
}
break;
}
case 'PostfixUpdate':
case 'PrefixUpdate': {
yield [instr.value.lvalue, InstructionKind.Reassign];
break;
}
}
}
export function* eachInstructionValueLValue(
value: ReactiveValue,
): Iterable<Place> {
@@ -372,51 +345,6 @@ export function* eachPatternOperand(pattern: Pattern): Iterable<Place> {
}
}
export function* eachPatternItem(
pattern: Pattern,
): Iterable<Place | SpreadPattern> {
switch (pattern.kind) {
case 'ArrayPattern': {
for (const item of pattern.items) {
if (item.kind === 'Identifier') {
yield item;
} else if (item.kind === 'Spread') {
yield item;
} else if (item.kind === 'Hole') {
continue;
} else {
assertExhaustive(
item,
`Unexpected item kind \`${(item as any).kind}\``,
);
}
}
break;
}
case 'ObjectPattern': {
for (const property of pattern.properties) {
if (property.kind === 'ObjectProperty') {
yield property.place;
} else if (property.kind === 'Spread') {
yield property;
} else {
assertExhaustive(
property,
`Unexpected item kind \`${(property as any).kind}\``,
);
}
}
break;
}
default: {
assertExhaustive(
pattern,
`Unexpected pattern kind \`${(pattern as any).kind}\``,
);
}
}
}
export function mapInstructionLValues(
instr: Instruction,
fn: (place: Place) => Place,
@@ -804,7 +732,6 @@ export function mapTerminalSuccessors(
case 'return': {
return {
kind: 'return',
returnVariant: terminal.returnVariant,
loc: terminal.loc,
value: terminal.value,
id: makeInstructionId(0),
@@ -1260,14 +1187,7 @@ export class ScopeBlockTraversal {
CompilerError.invariant(blockInfo.scope.id === top, {
reason:
'Expected traversed block fallthrough to match top-most active scope',
description: null,
details: [
{
kind: 'error',
loc: block.instructions[0]?.loc ?? block.terminal.id,
message: null,
},
],
loc: block.instructions[0]?.loc ?? block.terminal.id,
});
this.#activeScopes.pop();
}
@@ -1281,14 +1201,7 @@ export class ScopeBlockTraversal {
!this.blockInfos.has(block.terminal.fallthrough),
{
reason: 'Expected unique scope blocks and fallthroughs',
description: null,
details: [
{
kind: 'error',
loc: block.terminal.loc,
message: null,
},
],
loc: block.terminal.loc,
},
);
this.blockInfos.set(block.terminal.block, {

View File

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

View File

@@ -6,10 +6,20 @@
*/
import {CompilerError} from '../CompilerError';
import {Effect, HIRFunction, IdentifierId, makeInstructionId} from '../HIR';
import {
Effect,
HIRFunction,
Identifier,
IdentifierId,
LoweredFunction,
isRefOrRefValue,
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';
@@ -20,7 +30,12 @@ export default function analyseFunctions(func: HIRFunction): void {
switch (instr.value.kind) {
case 'ObjectMethod':
case 'FunctionExpression': {
lowerWithMutationAliasing(instr.value.loweredFunc.func);
if (!func.env.config.enableNewMutationAliasingModel) {
lower(instr.value.loweredFunc.func);
infer(instr.value.loweredFunc);
} else {
lowerWithMutationAliasing(instr.value.loweredFunc.func);
}
/**
* Reset mutable range for outer inferReferenceEffects
@@ -70,22 +85,14 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
case 'Assign':
case 'Alias':
case 'Capture':
case 'CreateFrom':
case 'MaybeAlias': {
case 'CreateFrom': {
capturedOrMutated.add(effect.from.identifier.id);
break;
}
case 'Apply': {
CompilerError.invariant(false, {
reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,
description: null,
details: [
{
kind: 'error',
loc: effect.function.loc,
message: null,
},
],
loc: effect.function.loc,
});
}
case 'Mutate':
@@ -132,3 +139,58 @@ 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;
}

View File

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

View File

@@ -5,8 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerDiagnostic, CompilerError, SourceLocation} from '..';
import {ErrorCategory} from '../CompilerError';
import {CompilerError, SourceLocation} from '..';
import {
CallExpression,
Effect,
@@ -31,7 +30,6 @@ import {
makeInstructionId,
} from '../HIR';
import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
import {Result} from '../Utils/Result';
type ManualMemoCallee = {
kind: 'useMemo' | 'useCallback';
@@ -42,7 +40,7 @@ type IdentifierSidemap = {
functions: Map<IdentifierId, TInstruction<FunctionExpression>>;
manualMemos: Map<IdentifierId, ManualMemoCallee>;
react: Set<IdentifierId>;
maybeDepsLists: Map<IdentifierId, {loc: SourceLocation; deps: Array<Place>}>;
maybeDepsLists: Map<IdentifierId, Array<Place>>;
maybeDeps: Map<IdentifierId, ManualMemoDependency>;
optionals: Set<IdentifierId>;
};
@@ -159,10 +157,10 @@ function collectTemporaries(
}
case 'ArrayExpression': {
if (value.elements.every(e => e.kind === 'Identifier')) {
sidemap.maybeDepsLists.set(instr.lvalue.identifier.id, {
loc: value.loc,
deps: value.elements as Array<Place>,
});
sidemap.maybeDepsLists.set(
instr.lvalue.identifier.id,
value.elements as Array<Place>,
);
}
break;
}
@@ -182,7 +180,6 @@ function makeManualMemoizationMarkers(
fnExpr: Place,
env: Environment,
depsList: Array<ManualMemoDependency> | null,
depsLoc: SourceLocation | null,
memoDecl: Place,
manualMemoId: number,
): [TInstruction<StartMemoize>, TInstruction<FinishMemoize>] {
@@ -198,7 +195,6 @@ function makeManualMemoizationMarkers(
* as dependencies
*/
deps: depsList,
depsLoc,
loc: fnExpr.loc,
},
effects: null,
@@ -287,87 +283,54 @@ function extractManualMemoizationArgs(
instr: TInstruction<CallExpression> | TInstruction<MethodCall>,
kind: 'useCallback' | 'useMemo',
sidemap: IdentifierSidemap,
errors: CompilerError,
): {
fnPlace: Place;
depsList: Array<ManualMemoDependency> | null;
depsLoc: SourceLocation | null;
} | null {
} {
const [fnPlace, depsListPlace] = instr.value.args as Array<
Place | SpreadPattern | undefined
>;
if (fnPlace == null || fnPlace.kind !== 'Identifier') {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected a callback function to be passed to ${kind}`,
description:
kind === 'useCallback'
? 'The first argument to useCallback() must be a function to cache'
: 'The first argument to useMemo() must be a function that calculates a result to cache',
suggestions: null,
}).withDetails({
kind: 'error',
loc: instr.value.loc,
message:
kind === 'useCallback'
? `Expected a callback function`
: `Expected a memoization function`,
}),
if (fnPlace == null) {
CompilerError.throwInvalidReact({
reason: `Expected a callback function to be passed to ${kind}`,
loc: instr.value.loc,
suggestions: null,
});
}
if (fnPlace.kind === 'Spread' || depsListPlace?.kind === 'Spread') {
CompilerError.throwInvalidReact({
reason: `Unexpected spread argument to ${kind}`,
loc: instr.value.loc,
suggestions: null,
});
}
let depsList: Array<ManualMemoDependency> | null = null;
if (depsListPlace != null) {
const maybeDepsList = sidemap.maybeDepsLists.get(
depsListPlace.identifier.id,
);
return null;
}
if (depsListPlace == null) {
return {
fnPlace,
depsList: null,
depsLoc: null,
};
}
const maybeDepsList =
depsListPlace.kind === 'Identifier'
? sidemap.maybeDepsLists.get(depsListPlace.identifier.id)
: null;
if (maybeDepsList == null) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
if (maybeDepsList == null) {
CompilerError.throwInvalidReact({
reason: `Expected the dependency list for ${kind} to be an array literal`,
description: `Expected the dependency list for ${kind} to be an array literal`,
suggestions: null,
}).withDetails({
kind: 'error',
loc:
depsListPlace?.kind === 'Identifier' ? depsListPlace.loc : instr.loc,
message: `Expected the dependency list for ${kind} to be an array literal`,
}),
);
return null;
}
const depsList: Array<ManualMemoDependency> = [];
for (const dep of maybeDepsList.deps) {
const maybeDep = sidemap.maybeDeps.get(dep.identifier.id);
if (maybeDep == null) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
description: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
suggestions: null,
}).withDetails({
kind: 'error',
loc: dep.loc,
message: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
}),
);
} else {
depsList.push(maybeDep);
loc: depsListPlace.loc,
});
}
depsList = maybeDepsList.map(dep => {
const maybeDep = sidemap.maybeDeps.get(dep.identifier.id);
if (maybeDep == null) {
CompilerError.throwInvalidReact({
reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
suggestions: null,
loc: dep.loc,
});
}
return maybeDep;
});
}
return {
fnPlace,
depsList,
depsLoc: maybeDepsList.loc,
};
}
@@ -378,14 +341,8 @@ function extractManualMemoizationArgs(
* rely on type inference to find useMemo/useCallback invocations, and instead does basic tracking
* of globals and property loads to find both direct calls as well as usage via the React namespace,
* eg `React.useMemo()`.
*
* This pass also validates that useMemo callbacks return a value (not void), ensuring that useMemo
* is only used for memoizing values and not for running arbitrary side effects.
*/
export function dropManualMemoization(
func: HIRFunction,
): Result<void, CompilerError> {
const errors = new CompilerError();
export function dropManualMemoization(func: HIRFunction): void {
const isValidationEnabled =
func.env.config.validatePreserveExistingMemoizationGuarantees ||
func.env.config.validateNoSetStateInRender ||
@@ -428,18 +385,11 @@ export function dropManualMemoization(
const manualMemo = sidemap.manualMemos.get(id);
if (manualMemo != null) {
const memoDetails = extractManualMemoizationArgs(
const {fnPlace, depsList} = extractManualMemoizationArgs(
instr as TInstruction<CallExpression> | TInstruction<MethodCall>,
manualMemo.kind,
sidemap,
errors,
);
if (memoDetails == null) {
continue;
}
const {fnPlace, depsList, depsLoc} = memoDetails;
instr.value = getManualMemoizationReplacement(
fnPlace,
instr.value.loc,
@@ -460,19 +410,11 @@ export function dropManualMemoization(
* is rare and likely sketchy.
*/
if (!sidemap.functions.has(fnPlace.identifier.id)) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
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: [],
}).withDetails({
kind: 'error',
loc: fnPlace.loc,
message: `Expected the first argument to be an inline function expression`,
}),
);
continue;
CompilerError.throwInvalidReact({
reason: `Expected the first argument to be an inline function expression`,
suggestions: [],
loc: fnPlace.loc,
});
}
const memoDecl: Place =
manualMemo.kind === 'useMemo'
@@ -489,7 +431,6 @@ export function dropManualMemoization(
fnPlace,
func.env,
depsList,
depsLoc,
memoDecl,
nextManualMemoId++,
);
@@ -545,8 +486,6 @@ export function dropManualMemoization(
markInstructionIds(func.body);
}
}
return errors.asResult();
}
function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
@@ -582,14 +521,7 @@ function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
default: {
CompilerError.invariant(false, {
reason: `Unexpected terminal in optional`,
description: null,
details: [
{
kind: 'error',
loc: terminal.loc,
message: `Unexpected ${terminal.kind} in optional`,
},
],
loc: terminal.loc,
});
}
}

View File

@@ -0,0 +1,134 @@
/**
* 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);
}
}
}
}
}

View File

@@ -0,0 +1,68 @@
/**
* 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]);
}

View File

@@ -0,0 +1,27 @@
/**
* 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]);
}
}
}
}
}

View File

@@ -0,0 +1,68 @@
/**
* 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]);
}
}

View File

@@ -438,14 +438,7 @@ function rewriteSplices(
{
reason:
'[InferEffectDependencies] Internal invariant broken: expected block instructions to be sorted',
description: null,
details: [
{
kind: 'error',
loc: originalInstrs[cursor].loc,
message: null,
},
],
loc: originalInstrs[cursor].loc,
},
);
currBlock.instructions.push(originalInstrs[cursor]);
@@ -454,14 +447,7 @@ function rewriteSplices(
CompilerError.invariant(originalInstrs[cursor].id === rewrite.location, {
reason:
'[InferEffectDependencies] Internal invariant broken: splice location not found',
description: null,
details: [
{
kind: 'error',
loc: originalInstrs[cursor].loc,
message: null,
},
],
loc: originalInstrs[cursor].loc,
});
if (rewrite.kind === 'instr') {
@@ -481,14 +467,7 @@ function rewriteSplices(
{
reason:
'[InferEffectDependencies] Internal invariant broken: expected entry block to have a fallthrough',
description: null,
details: [
{
kind: 'error',
loc: entryBlock.terminal.loc,
message: null,
},
],
loc: entryBlock.terminal.loc,
},
);
const originalTerminal = currBlock.terminal;
@@ -587,14 +566,7 @@ function inferMinimalDependencies(
CompilerError.invariant(hoistableToFnEntry != null, {
reason:
'[InferEffectDependencies] Internal invariant broken: missing entry block',
description: null,
details: [
{
kind: 'error',
loc: fnInstr.loc,
message: null,
},
],
loc: fnInstr.loc,
});
const dependencies = inferDependencies(
@@ -650,14 +622,7 @@ function inferDependencies(
CompilerError.invariant(resultUnfiltered != null, {
reason:
'[InferEffectDependencies] Internal invariant broken: missing scope dependencies',
description: null,
details: [
{
kind: 'error',
loc: fn.loc,
message: null,
},
],
loc: fn.loc,
});
const fnContext = new Set(fn.context.map(dep => dep.identifier.id));

View File

@@ -0,0 +1,351 @@
/**
* 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 'Writing to a variable defined outside a component or hook is not allowed. Consider using an effect';
} else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
return 'Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX';
} else if (abstractValue.reason.has(ValueReason.Context)) {
return `Mutating a value returned from 'useContext()', which should not be mutated`;
} else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
return 'Mutating a value returned from a function whose return value should not be mutated';
} else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
return 'Mutating component props or hook arguments is not allowed. Consider using a local variable instead';
} else if (abstractValue.reason.has(ValueReason.State)) {
return "Mutating a value returned from 'useState()', which should not be mutated. Use the setter function to update instead";
} else if (abstractValue.reason.has(ValueReason.ReducerState)) {
return "Mutating a value returned from 'useReducer()', which should not be mutated. Use the dispatch function to update instead";
} else if (abstractValue.reason.has(ValueReason.Effect)) {
return 'Updating a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the mutation before calling useEffect()';
} else if (abstractValue.reason.has(ValueReason.HookCaptured)) {
return 'Updating a value previously passed as an argument to a hook is not allowed. Consider moving the mutation before calling the hook';
} else if (abstractValue.reason.has(ValueReason.HookReturn)) {
return 'Updating a value returned from a hook is not allowed. Consider moving the mutation into the hook where the value is constructed';
} else {
return 'This mutates a variable that React considers immutable';
}
}

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