Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33b1e80c79 |
@@ -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/
|
||||
|
||||
15
.eslintrc.js
15
.eslintrc.js
@@ -468,14 +468,13 @@ module.exports = {
|
||||
files: ['packages/react-server-dom-webpack/**/*.js'],
|
||||
globals: {
|
||||
__webpack_chunk_load__: 'readonly',
|
||||
__webpack_get_script_filename__: 'readonly',
|
||||
__webpack_require__: 'readonly',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/react-server-dom-turbopack/**/*.js'],
|
||||
globals: {
|
||||
__turbopack_load_by_url__: 'readonly',
|
||||
__turbopack_load__: 'readonly',
|
||||
__turbopack_require__: 'readonly',
|
||||
},
|
||||
},
|
||||
@@ -547,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',
|
||||
@@ -565,7 +567,6 @@ module.exports = {
|
||||
BigInt: 'readonly',
|
||||
BigInt64Array: 'readonly',
|
||||
BigUint64Array: 'readonly',
|
||||
CacheType: 'readonly',
|
||||
Class: 'readonly',
|
||||
ClientRect: 'readonly',
|
||||
CopyInspectedElementPath: 'readonly',
|
||||
@@ -580,15 +581,13 @@ module.exports = {
|
||||
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',
|
||||
@@ -609,7 +608,6 @@ module.exports = {
|
||||
symbol: 'readonly',
|
||||
SyntheticEvent: 'readonly',
|
||||
SyntheticMouseEvent: 'readonly',
|
||||
SyntheticPointerEvent: 'readonly',
|
||||
Thenable: 'readonly',
|
||||
TimeoutID: 'readonly',
|
||||
WheelEventHandler: 'readonly',
|
||||
@@ -620,6 +618,7 @@ module.exports = {
|
||||
PropertyIndexedKeyframes: 'readonly',
|
||||
KeyframeAnimationOptions: 'readonly',
|
||||
GetAnimationsOptions: 'readonly',
|
||||
Animatable: 'readonly',
|
||||
ScrollTimeline: 'readonly',
|
||||
EventListenerOptionsOrUseCapture: 'readonly',
|
||||
FocusOptions: 'readonly',
|
||||
|
||||
2
.github/workflows/compiler_playground.yml
vendored
2
.github/workflows/compiler_playground.yml
vendored
@@ -57,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()'
|
||||
|
||||
61
.github/workflows/runtime_build_and_test.yml
vendored
61
.github/workflows/runtime_build_and_test.yml
vendored
@@ -6,12 +6,6 @@ on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- compiler/**
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
commit_sha:
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
permissions: {}
|
||||
|
||||
@@ -34,7 +28,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Check cache hit
|
||||
uses: actions/cache/restore@v4
|
||||
id: node_modules
|
||||
@@ -75,7 +69,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Check cache hit
|
||||
uses: actions/cache/restore@v4
|
||||
id: node_modules
|
||||
@@ -123,7 +117,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/github-script@v7
|
||||
id: set-matrix
|
||||
with:
|
||||
@@ -142,7 +136,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -172,7 +166,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -204,7 +198,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -260,7 +254,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -316,7 +310,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
|
||||
@@ -331,7 +325,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -426,7 +420,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -471,7 +465,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -499,7 +493,7 @@ jobs:
|
||||
merge-multiple: true
|
||||
- name: Display structure of build
|
||||
run: ls -R build
|
||||
- run: echo ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
|
||||
- run: echo ${{ github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
|
||||
- name: Scrape warning messages
|
||||
run: |
|
||||
mkdir -p ./build/__test_utils__
|
||||
@@ -536,7 +530,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -576,7 +570,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -613,7 +607,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -654,7 +648,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -728,7 +722,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -785,7 +779,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -811,18 +805,9 @@ 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
|
||||
@@ -839,7 +824,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -888,7 +873,7 @@ jobs:
|
||||
node ./scripts/print-warnings/print-warnings.js > build/__test_utils__/ReactAllWarnings.js
|
||||
- name: Display structure of build for PR
|
||||
run: ls -R build
|
||||
- run: echo ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
|
||||
- run: echo ${{ github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
|
||||
- run: node ./scripts/tasks/danger
|
||||
- name: Archive sizebot results
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -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,8 +1,3 @@
|
||||
## 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
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { c as _c } from "react/compiler-runtime"; //
|
||||
@compilationMode:"all"
|
||||
@compilationMode:"all"
|
||||
function nonReactFn() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
|
||||
@@ -1,103 +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 {parseConfigPragmaAsString} from 'babel-plugin-react-compiler';
|
||||
import type {editor} from 'monaco-editor';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import parserBabel from 'prettier/plugins/babel';
|
||||
import * as prettierPluginEstree from 'prettier/plugins/estree';
|
||||
import * as prettier from 'prettier/standalone';
|
||||
import {useState, useEffect} from 'react';
|
||||
import {Resizable} from 're-resizable';
|
||||
import {useStore} from '../StoreContext';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
|
||||
loader.config({monaco});
|
||||
|
||||
export default function ConfigEditor(): JSX.Element {
|
||||
const [, setMonaco] = useState<Monaco | null>(null);
|
||||
const store = useStore();
|
||||
|
||||
// Parse string-based override config from pragma comment and format it
|
||||
const [configJavaScript, setConfigJavaScript] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const pragma = store.source.substring(0, store.source.indexOf('\n'));
|
||||
const configString = `(${parseConfigPragmaAsString(pragma)})`;
|
||||
|
||||
prettier
|
||||
.format(configString, {
|
||||
semi: true,
|
||||
parser: 'babel-ts',
|
||||
plugins: [parserBabel, prettierPluginEstree],
|
||||
})
|
||||
.then(formatted => {
|
||||
setConfigJavaScript(formatted);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error formatting config:', error);
|
||||
setConfigJavaScript('({})'); // Return empty object if not valid for now
|
||||
//TODO: Add validation and error handling for config
|
||||
});
|
||||
console.log('Config:', configString);
|
||||
}, [store.source]);
|
||||
|
||||
const handleChange: (value: string | undefined) => void = value => {
|
||||
if (!value) return;
|
||||
|
||||
// TODO: Implement sync logic to update pragma comments in the source
|
||||
console.log('Config changed:', value);
|
||||
};
|
||||
|
||||
const handleMount: (
|
||||
_: editor.IStandaloneCodeEditor,
|
||||
monaco: Monaco,
|
||||
) => void = (_, monaco) => {
|
||||
setMonaco(monaco);
|
||||
|
||||
const uri = monaco.Uri.parse(`file:///config.js`);
|
||||
const model = monaco.editor.getModel(uri);
|
||||
if (model) {
|
||||
model.updateOptions({tabSize: 2});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col flex-none border-r border-gray-200">
|
||||
<h2 className="p-4 duration-150 ease-in border-b cursor-default border-grey-200 font-light text-secondary">
|
||||
Config Overrides
|
||||
</h2>
|
||||
<Resizable
|
||||
minWidth={300}
|
||||
maxWidth={600}
|
||||
defaultSize={{width: 350, height: 'auto'}}
|
||||
enable={{right: true}}
|
||||
className="!h-[calc(100vh_-_3.5rem_-_4rem)]">
|
||||
<MonacoEditor
|
||||
path={'config.js'}
|
||||
language={'javascript'}
|
||||
value={configJavaScript}
|
||||
onMount={handleMount}
|
||||
onChange={handleChange}
|
||||
options={{
|
||||
...monacoOptions,
|
||||
readOnly: true,
|
||||
lineNumbers: 'off',
|
||||
folding: false,
|
||||
renderLineHighlight: 'none',
|
||||
scrollBeyondLastLine: false,
|
||||
hideCursorInOverviewRuler: true,
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
fontSize: 12,
|
||||
}}
|
||||
/>
|
||||
</Resizable>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import * as t from '@babel/types';
|
||||
import BabelPluginReactCompiler, {
|
||||
CompilerError,
|
||||
CompilerErrorDetail,
|
||||
CompilerDiagnostic,
|
||||
Effect,
|
||||
ErrorSeverity,
|
||||
parseConfigPragmaForTests,
|
||||
@@ -37,7 +36,6 @@ import {
|
||||
type Store,
|
||||
} from '../../lib/stores';
|
||||
import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
import ConfigEditor from './ConfigEditor';
|
||||
import Input from './Input';
|
||||
import {
|
||||
CompilerOutput,
|
||||
@@ -47,7 +45,6 @@ import {
|
||||
} from './Output';
|
||||
import {transformFromAstSync} from '@babel/core';
|
||||
import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint';
|
||||
import {useSearchParams} from 'next/navigation';
|
||||
|
||||
function parseInput(
|
||||
input: string,
|
||||
@@ -144,13 +141,10 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
|
||||
],
|
||||
];
|
||||
|
||||
function compile(
|
||||
source: string,
|
||||
mode: 'compiler' | 'linter',
|
||||
): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
|
||||
const error = new CompilerError();
|
||||
const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
|
||||
const otherErrors: Array<CompilerErrorDetail> = [];
|
||||
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
|
||||
const entry = results.get(result.name);
|
||||
if (Array.isArray(entry)) {
|
||||
@@ -209,23 +203,6 @@ function compile(
|
||||
};
|
||||
const parsedOptions = parseConfigPragmaForTests(pragma, {
|
||||
compilationMode: 'infer',
|
||||
environment:
|
||||
mode === 'linter'
|
||||
? {
|
||||
// enabled in compiler
|
||||
validateRefAccessDuringRender: false,
|
||||
// enabled in linter
|
||||
validateNoSetStateInRender: true,
|
||||
validateNoSetStateInEffects: true,
|
||||
validateNoJSXInTryStatements: true,
|
||||
validateNoImpureFunctionsInRender: true,
|
||||
validateStaticComponents: true,
|
||||
validateNoFreezingKnownMutableFunctions: true,
|
||||
validateNoVoidUseMemo: true,
|
||||
}
|
||||
: {
|
||||
/* use defaults for compiler mode */
|
||||
},
|
||||
});
|
||||
const opts: PluginOptions = parsePluginOptions({
|
||||
...parsedOptions,
|
||||
@@ -237,7 +214,7 @@ function compile(
|
||||
debugLogIRs: logIR,
|
||||
logEvent: (_filename: string | null, event: LoggerEvent) => {
|
||||
if (event.kind === 'CompileError') {
|
||||
otherErrors.push(event.detail);
|
||||
otherErrors.push(new CompilerErrorDetail(event.detail));
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -249,7 +226,7 @@ function compile(
|
||||
* (i.e. object shape that is not CompilerError)
|
||||
*/
|
||||
if (err instanceof CompilerError && err.details.length > 0) {
|
||||
error.merge(err);
|
||||
error.details.push(...err.details);
|
||||
} else {
|
||||
/**
|
||||
* Handle unexpected failures by logging (to get a stack trace)
|
||||
@@ -268,15 +245,12 @@ function compile(
|
||||
}
|
||||
// Only include logger errors if there weren't other errors
|
||||
if (!error.hasErrors() && otherErrors.length !== 0) {
|
||||
otherErrors.forEach(e => error.details.push(e));
|
||||
otherErrors.forEach(e => error.push(e));
|
||||
}
|
||||
if (error.hasErrors()) {
|
||||
return [{kind: 'err', results, error}, language];
|
||||
return [{kind: 'err', results, error: error}, language];
|
||||
}
|
||||
return [
|
||||
{kind: 'ok', results, transformOutput, errors: error.details},
|
||||
language,
|
||||
];
|
||||
return [{kind: 'ok', results, transformOutput}, language];
|
||||
}
|
||||
|
||||
export default function Editor(): JSX.Element {
|
||||
@@ -285,21 +259,11 @@ export default function Editor(): JSX.Element {
|
||||
const dispatchStore = useStoreDispatch();
|
||||
const {enqueueSnackbar} = useSnackbar();
|
||||
const [compilerOutput, language] = useMemo(
|
||||
() => compile(deferredStore.source, 'compiler'),
|
||||
() => compile(deferredStore.source),
|
||||
[deferredStore.source],
|
||||
);
|
||||
const [linterOutput] = useMemo(
|
||||
() => compile(deferredStore.source, 'linter'),
|
||||
[deferredStore.source],
|
||||
);
|
||||
|
||||
// TODO: Remove this once the config editor is more stable
|
||||
const searchParams = useSearchParams();
|
||||
const search = searchParams.get('showConfig');
|
||||
const shouldShowConfig = search === 'true';
|
||||
|
||||
useMountEffect(() => {
|
||||
// Initialize store
|
||||
let mountStore: Store;
|
||||
try {
|
||||
mountStore = initStoreFromUrlOrLocalStorage();
|
||||
@@ -321,27 +285,19 @@ export default function Editor(): JSX.Element {
|
||||
});
|
||||
});
|
||||
|
||||
let mergedOutput: CompilerOutput;
|
||||
let errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
|
||||
if (compilerOutput.kind === 'ok') {
|
||||
errors = linterOutput.kind === 'ok' ? [] : linterOutput.error.details;
|
||||
mergedOutput = {
|
||||
...compilerOutput,
|
||||
errors,
|
||||
};
|
||||
} else {
|
||||
mergedOutput = compilerOutput;
|
||||
errors = compilerOutput.error.details;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex basis top-14">
|
||||
{shouldShowConfig && <ConfigEditor />}
|
||||
<div className={clsx('relative sm:basis-1/4')}>
|
||||
<Input language={language} errors={errors} />
|
||||
<Input
|
||||
language={language}
|
||||
errors={
|
||||
compilerOutput.kind === 'err' ? compilerOutput.error.details : []
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className={clsx('flex sm:flex flex-wrap')}>
|
||||
<Output store={deferredStore} compilerOutput={mergedOutput} />
|
||||
<Output store={deferredStore} compilerOutput={compilerOutput} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -36,18 +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,
|
||||
});
|
||||
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, store.source]);
|
||||
}, [monaco, errors]);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
|
||||
@@ -11,11 +11,7 @@ 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';
|
||||
@@ -48,7 +44,6 @@ export type CompilerOutput =
|
||||
kind: 'ok';
|
||||
transformOutput: CompilerTransformOutput;
|
||||
results: Map<string, Array<PrintedCompilerPipelineValue>>;
|
||||
errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
|
||||
}
|
||||
| {
|
||||
kind: 'err';
|
||||
@@ -128,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>,
|
||||
);
|
||||
@@ -173,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);
|
||||
@@ -205,32 +162,17 @@ function getSourceMapUrl(code: string, map: string): string | null {
|
||||
}
|
||||
|
||||
function Output({store, compilerOutput}: Props): JSX.Element {
|
||||
const [tabsOpen, setTabsOpen] = useState<Set<string>>(
|
||||
() => new Set(['Output']),
|
||||
);
|
||||
const [tabsOpen, setTabsOpen] = useState<Set<string>>(() => new Set(['JS']));
|
||||
const [tabs, setTabs] = useState<Map<string, React.ReactNode>>(
|
||||
() => new Map(),
|
||||
);
|
||||
|
||||
/*
|
||||
* Update the active tab back to the output or errors tab when the compilation state
|
||||
* changes between success/failure.
|
||||
*/
|
||||
const [previousOutputKind, setPreviousOutputKind] = useState(
|
||||
compilerOutput.kind,
|
||||
);
|
||||
if (compilerOutput.kind !== previousOutputKind) {
|
||||
setPreviousOutputKind(compilerOutput.kind);
|
||||
setTabsOpen(new Set(['Output']));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
tabify(store.source, compilerOutput).then(tabs => {
|
||||
setTabs(tabs);
|
||||
});
|
||||
}, [store.source, compilerOutput]);
|
||||
|
||||
const changedPasses: Set<string> = new Set(['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) {
|
||||
@@ -254,6 +196,20 @@ function Output({store, compilerOutput}: Props): JSX.Element {
|
||||
tabs={tabs}
|
||||
changedPasses={changedPasses}
|
||||
/>
|
||||
{compilerOutput.kind === 'err' ? (
|
||||
<div
|
||||
className="flex flex-wrap absolute bottom-0 bg-white grow border-y border-grey-200 transition-all ease-in"
|
||||
style={{width: 'calc(100vw - 650px)'}}>
|
||||
<div className="w-full p-4 basis-full border-b">
|
||||
<h2>COMPILER ERRORS</h2>
|
||||
</div>
|
||||
<pre
|
||||
className="p-4 basis-full text-red-600 overflow-y-scroll whitespace-pre-wrap"
|
||||
style={{width: 'calc(100vw - 650px)', height: '150px'}}>
|
||||
<code>{compilerOutput.error.toString()}</code>
|
||||
</pre>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -262,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 (
|
||||
@@ -318,7 +272,7 @@ function TextTabContent({
|
||||
/>
|
||||
) : (
|
||||
<MonacoEditor
|
||||
language={language ?? 'javascript'}
|
||||
defaultLanguage="javascript"
|
||||
value={output}
|
||||
options={{
|
||||
...monacoOptions,
|
||||
|
||||
@@ -28,5 +28,5 @@ export const monacoOptions: Partial<EditorProps['options']> = {
|
||||
|
||||
automaticLayout: true,
|
||||
wordWrap: 'on',
|
||||
wrappingIndent: 'same',
|
||||
wrappingIndent: 'deepIndent',
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
pipelineUsesReanimatedPlugin,
|
||||
} from '../Entrypoint/Reanimated';
|
||||
import validateNoUntransformedReferences from '../Entrypoint/ValidateNoUntransformedReferences';
|
||||
import {CompilerError} from '..';
|
||||
|
||||
const ENABLE_REACT_COMPILER_TIMINGS =
|
||||
process.env['ENABLE_REACT_COMPILER_TIMINGS'] === '1';
|
||||
@@ -35,58 +34,51 @@ export default function BabelPluginReactCompiler(
|
||||
*/
|
||||
Program: {
|
||||
enter(prog, pass): void {
|
||||
try {
|
||||
const filename = pass.filename ?? 'unknown';
|
||||
if (ENABLE_REACT_COMPILER_TIMINGS === true) {
|
||||
performance.mark(`${filename}:start`, {
|
||||
detail: 'BabelPlugin:Program:start',
|
||||
});
|
||||
}
|
||||
let opts = parsePluginOptions(pass.opts);
|
||||
const isDev =
|
||||
(typeof __DEV__ !== 'undefined' && __DEV__ === true) ||
|
||||
process.env['NODE_ENV'] === 'development';
|
||||
if (
|
||||
opts.enableReanimatedCheck === true &&
|
||||
pipelineUsesReanimatedPlugin(pass.file.opts.plugins)
|
||||
) {
|
||||
opts = injectReanimatedFlag(opts);
|
||||
}
|
||||
if (
|
||||
opts.environment.enableResetCacheOnSourceFileChanges !== false &&
|
||||
isDev
|
||||
) {
|
||||
opts = {
|
||||
...opts,
|
||||
environment: {
|
||||
...opts.environment,
|
||||
enableResetCacheOnSourceFileChanges: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
const result = compileProgram(prog, {
|
||||
opts,
|
||||
filename: pass.filename ?? null,
|
||||
comments: pass.file.ast.comments ?? [],
|
||||
code: pass.file.code,
|
||||
const filename = pass.filename ?? 'unknown';
|
||||
if (ENABLE_REACT_COMPILER_TIMINGS === true) {
|
||||
performance.mark(`${filename}:start`, {
|
||||
detail: 'BabelPlugin:Program:start',
|
||||
});
|
||||
}
|
||||
let opts = parsePluginOptions(pass.opts);
|
||||
const isDev =
|
||||
(typeof __DEV__ !== 'undefined' && __DEV__ === true) ||
|
||||
process.env['NODE_ENV'] === 'development';
|
||||
if (
|
||||
opts.enableReanimatedCheck === true &&
|
||||
pipelineUsesReanimatedPlugin(pass.file.opts.plugins)
|
||||
) {
|
||||
opts = injectReanimatedFlag(opts);
|
||||
}
|
||||
if (
|
||||
opts.environment.enableResetCacheOnSourceFileChanges !== false &&
|
||||
isDev
|
||||
) {
|
||||
opts = {
|
||||
...opts,
|
||||
environment: {
|
||||
...opts.environment,
|
||||
enableResetCacheOnSourceFileChanges: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
const result = compileProgram(prog, {
|
||||
opts,
|
||||
filename: pass.filename ?? null,
|
||||
comments: pass.file.ast.comments ?? [],
|
||||
code: pass.file.code,
|
||||
});
|
||||
validateNoUntransformedReferences(
|
||||
prog,
|
||||
pass.filename ?? null,
|
||||
opts.logger,
|
||||
opts.environment,
|
||||
result,
|
||||
);
|
||||
if (ENABLE_REACT_COMPILER_TIMINGS === true) {
|
||||
performance.mark(`${filename}:end`, {
|
||||
detail: 'BabelPlugin:Program:end',
|
||||
});
|
||||
validateNoUntransformedReferences(
|
||||
prog,
|
||||
pass.filename ?? null,
|
||||
opts.logger,
|
||||
opts.environment,
|
||||
result,
|
||||
);
|
||||
if (ENABLE_REACT_COMPILER_TIMINGS === true) {
|
||||
performance.mark(`${filename}:end`, {
|
||||
detail: 'BabelPlugin:Program:end',
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof CompilerError) {
|
||||
throw e.withPrintedMessage(pass.file.code, {eslint: false});
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
exit(_, pass): void {
|
||||
|
||||
@@ -5,12 +5,9 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import * as t from '@babel/types';
|
||||
import {codeFrameColumns} from '@babel/code-frame';
|
||||
import {type SourceLocation} from './HIR';
|
||||
import type {SourceLocation} from './HIR';
|
||||
import {Err, Ok, Result} from './Utils/Result';
|
||||
import {assertExhaustive} from './Utils/utils';
|
||||
import invariant from 'invariant';
|
||||
|
||||
export enum ErrorSeverity {
|
||||
/**
|
||||
@@ -18,11 +15,6 @@ export enum ErrorSeverity {
|
||||
* misunderstanding on the user’s part.
|
||||
*/
|
||||
InvalidJS = 'InvalidJS',
|
||||
/**
|
||||
* JS syntax that is not supported and which we do not plan to support. Developers should
|
||||
* rewrite to use supported forms.
|
||||
*/
|
||||
UnsupportedJS = 'UnsupportedJS',
|
||||
/**
|
||||
* Code that breaks the rules of React.
|
||||
*/
|
||||
@@ -47,29 +39,6 @@ export enum ErrorSeverity {
|
||||
Invariant = 'Invariant',
|
||||
}
|
||||
|
||||
export type CompilerDiagnosticOptions = {
|
||||
category: ErrorCategory;
|
||||
severity: ErrorSeverity;
|
||||
reason: string;
|
||||
description: string;
|
||||
details: Array<CompilerDiagnosticDetail>;
|
||||
suggestions?: Array<CompilerSuggestion> | null | undefined;
|
||||
};
|
||||
|
||||
export type CompilerDiagnosticDetail =
|
||||
/**
|
||||
* A/the source of the error
|
||||
*/
|
||||
| {
|
||||
kind: 'error';
|
||||
loc: SourceLocation | null;
|
||||
message: string;
|
||||
}
|
||||
| {
|
||||
kind: 'hint';
|
||||
message: string;
|
||||
};
|
||||
|
||||
export enum CompilerSuggestionOperation {
|
||||
InsertBefore,
|
||||
InsertAfter,
|
||||
@@ -93,124 +62,13 @@ export type CompilerSuggestion =
|
||||
};
|
||||
|
||||
export type CompilerErrorDetailOptions = {
|
||||
category: ErrorCategory;
|
||||
severity: ErrorSeverity;
|
||||
reason: string;
|
||||
description?: string | null | undefined;
|
||||
severity: ErrorSeverity;
|
||||
loc: SourceLocation | null;
|
||||
suggestions?: Array<CompilerSuggestion> | null | undefined;
|
||||
};
|
||||
|
||||
export type PrintErrorMessageOptions = {
|
||||
/**
|
||||
* ESLint uses 1-indexed columns and prints one error at a time
|
||||
* So it doesn't require the "Found # error(s)" text
|
||||
*/
|
||||
eslint: boolean;
|
||||
};
|
||||
|
||||
export class CompilerDiagnostic {
|
||||
options: CompilerDiagnosticOptions;
|
||||
|
||||
constructor(options: CompilerDiagnosticOptions) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
static create(
|
||||
options: Omit<CompilerDiagnosticOptions, 'details'>,
|
||||
): CompilerDiagnostic {
|
||||
return new CompilerDiagnostic({...options, details: []});
|
||||
}
|
||||
|
||||
get reason(): CompilerDiagnosticOptions['reason'] {
|
||||
return this.options.reason;
|
||||
}
|
||||
get description(): CompilerDiagnosticOptions['description'] {
|
||||
return this.options.description;
|
||||
}
|
||||
get severity(): CompilerDiagnosticOptions['severity'] {
|
||||
return this.options.severity;
|
||||
}
|
||||
get suggestions(): CompilerDiagnosticOptions['suggestions'] {
|
||||
return this.options.suggestions;
|
||||
}
|
||||
get category(): ErrorCategory {
|
||||
return this.options.category;
|
||||
}
|
||||
|
||||
withDetail(detail: CompilerDiagnosticDetail): CompilerDiagnostic {
|
||||
this.options.details.push(detail);
|
||||
return this;
|
||||
}
|
||||
|
||||
primaryLocation(): SourceLocation | null {
|
||||
const firstErrorDetail = this.options.details.filter(
|
||||
d => d.kind === 'error',
|
||||
)[0];
|
||||
return firstErrorDetail != null && firstErrorDetail.kind === 'error'
|
||||
? firstErrorDetail.loc
|
||||
: null;
|
||||
}
|
||||
|
||||
printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
|
||||
const buffer = [
|
||||
printErrorSummary(this.severity, this.reason),
|
||||
'\n\n',
|
||||
this.description,
|
||||
];
|
||||
for (const detail of this.options.details) {
|
||||
switch (detail.kind) {
|
||||
case 'error': {
|
||||
const loc = detail.loc;
|
||||
if (loc == null || typeof loc === 'symbol') {
|
||||
continue;
|
||||
}
|
||||
let codeFrame: string;
|
||||
try {
|
||||
codeFrame = printCodeFrame(source, loc, detail.message);
|
||||
} catch (e) {
|
||||
codeFrame = detail.message;
|
||||
}
|
||||
buffer.push('\n\n');
|
||||
if (loc.filename != null) {
|
||||
const line = loc.start.line;
|
||||
const column = options.eslint
|
||||
? loc.start.column + 1
|
||||
: loc.start.column;
|
||||
buffer.push(`${loc.filename}:${line}:${column}\n`);
|
||||
}
|
||||
buffer.push(codeFrame);
|
||||
break;
|
||||
}
|
||||
case 'hint': {
|
||||
buffer.push('\n\n');
|
||||
buffer.push(detail.message);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
detail,
|
||||
`Unexpected detail kind ${(detail as any).kind}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return buffer.join('');
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
const buffer = [printErrorSummary(this.severity, this.reason)];
|
||||
if (this.description != null) {
|
||||
buffer.push(`. ${this.description}.`);
|
||||
}
|
||||
const loc = this.primaryLocation();
|
||||
if (loc != null && typeof loc !== 'symbol') {
|
||||
buffer.push(` (${loc.start.line}:${loc.start.column})`);
|
||||
}
|
||||
return buffer.join('');
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Each bailout or invariant in HIR lowering creates an {@link CompilerErrorDetail}, which is then
|
||||
* aggregated into a single {@link CompilerError} later.
|
||||
@@ -237,66 +95,35 @@ export class CompilerErrorDetail {
|
||||
get suggestions(): CompilerErrorDetailOptions['suggestions'] {
|
||||
return this.options.suggestions;
|
||||
}
|
||||
get category(): ErrorCategory {
|
||||
return this.options.category;
|
||||
}
|
||||
|
||||
primaryLocation(): SourceLocation | null {
|
||||
return this.loc;
|
||||
}
|
||||
|
||||
printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
|
||||
const buffer = [printErrorSummary(this.severity, this.reason)];
|
||||
printErrorMessage(): string {
|
||||
const buffer = [`${this.severity}: ${this.reason}`];
|
||||
if (this.description != null) {
|
||||
buffer.push(`\n\n${this.description}.`);
|
||||
buffer.push(`. ${this.description}`);
|
||||
}
|
||||
const loc = this.loc;
|
||||
if (loc != null && typeof loc !== 'symbol') {
|
||||
let codeFrame: string;
|
||||
try {
|
||||
codeFrame = printCodeFrame(source, loc, this.reason);
|
||||
} catch (e) {
|
||||
codeFrame = '';
|
||||
}
|
||||
buffer.push(`\n\n`);
|
||||
if (loc.filename != null) {
|
||||
const line = loc.start.line;
|
||||
const column = options.eslint ? loc.start.column + 1 : loc.start.column;
|
||||
buffer.push(`${loc.filename}:${line}:${column}\n`);
|
||||
}
|
||||
buffer.push(codeFrame);
|
||||
buffer.push('\n\n');
|
||||
if (this.loc != null && typeof this.loc !== 'symbol') {
|
||||
buffer.push(` (${this.loc.start.line}:${this.loc.end.line})`);
|
||||
}
|
||||
return buffer.join('');
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
const buffer = [printErrorSummary(this.severity, this.reason)];
|
||||
if (this.description != null) {
|
||||
buffer.push(`. ${this.description}.`);
|
||||
}
|
||||
const loc = this.loc;
|
||||
if (loc != null && typeof loc !== 'symbol') {
|
||||
buffer.push(` (${loc.start.line}:${loc.start.column})`);
|
||||
}
|
||||
return buffer.join('');
|
||||
return this.printErrorMessage();
|
||||
}
|
||||
}
|
||||
|
||||
export class CompilerError extends Error {
|
||||
details: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
|
||||
printedMessage: string | null = null;
|
||||
details: Array<CompilerErrorDetail> = [];
|
||||
|
||||
static invariant(
|
||||
condition: unknown,
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity' | 'category'>,
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity'>,
|
||||
): asserts condition {
|
||||
if (!condition) {
|
||||
const errors = new CompilerError();
|
||||
errors.pushErrorDetail(
|
||||
new CompilerErrorDetail({
|
||||
...options,
|
||||
category: ErrorCategory.Invariant,
|
||||
severity: ErrorSeverity.Invariant,
|
||||
}),
|
||||
);
|
||||
@@ -304,35 +131,24 @@ export class CompilerError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
static throwDiagnostic(options: CompilerDiagnosticOptions): never {
|
||||
const errors = new CompilerError();
|
||||
errors.pushDiagnostic(new CompilerDiagnostic(options));
|
||||
throw errors;
|
||||
}
|
||||
|
||||
static throwTodo(
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity' | 'category'>,
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity'>,
|
||||
): never {
|
||||
const errors = new CompilerError();
|
||||
errors.pushErrorDetail(
|
||||
new CompilerErrorDetail({
|
||||
...options,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
}),
|
||||
new CompilerErrorDetail({...options, severity: ErrorSeverity.Todo}),
|
||||
);
|
||||
throw errors;
|
||||
}
|
||||
|
||||
static throwInvalidJS(
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity' | 'category'>,
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity'>,
|
||||
): never {
|
||||
const errors = new CompilerError();
|
||||
errors.pushErrorDetail(
|
||||
new CompilerErrorDetail({
|
||||
...options,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
}),
|
||||
);
|
||||
throw errors;
|
||||
@@ -352,14 +168,13 @@ export class CompilerError extends Error {
|
||||
}
|
||||
|
||||
static throwInvalidConfig(
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity' | 'category'>,
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity'>,
|
||||
): never {
|
||||
const errors = new CompilerError();
|
||||
errors.pushErrorDetail(
|
||||
new CompilerErrorDetail({
|
||||
...options,
|
||||
severity: ErrorSeverity.InvalidConfig,
|
||||
category: ErrorCategory.Config,
|
||||
}),
|
||||
);
|
||||
throw errors;
|
||||
@@ -378,52 +193,20 @@ export class CompilerError extends Error {
|
||||
}
|
||||
|
||||
override get message(): string {
|
||||
return this.printedMessage ?? this.toString();
|
||||
return this.toString();
|
||||
}
|
||||
|
||||
override set message(_message: string) {}
|
||||
|
||||
override toString(): string {
|
||||
if (this.printedMessage) {
|
||||
return this.printedMessage;
|
||||
}
|
||||
if (Array.isArray(this.details)) {
|
||||
return this.details.map(detail => detail.toString()).join('\n\n');
|
||||
}
|
||||
return this.name;
|
||||
}
|
||||
|
||||
withPrintedMessage(
|
||||
source: string,
|
||||
options: PrintErrorMessageOptions,
|
||||
): CompilerError {
|
||||
this.printedMessage = this.printErrorMessage(source, options);
|
||||
return this;
|
||||
}
|
||||
|
||||
printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
|
||||
if (options.eslint && this.details.length === 1) {
|
||||
return this.details[0].printErrorMessage(source, options);
|
||||
}
|
||||
return (
|
||||
`Found ${this.details.length} error${this.details.length === 1 ? '' : 's'}:\n\n` +
|
||||
this.details
|
||||
.map(detail => detail.printErrorMessage(source, options).trim())
|
||||
.join('\n\n')
|
||||
);
|
||||
}
|
||||
|
||||
merge(other: CompilerError): void {
|
||||
this.details.push(...other.details);
|
||||
}
|
||||
|
||||
pushDiagnostic(diagnostic: CompilerDiagnostic): void {
|
||||
this.details.push(diagnostic);
|
||||
}
|
||||
|
||||
push(options: CompilerErrorDetailOptions): CompilerErrorDetail {
|
||||
const detail = new CompilerErrorDetail({
|
||||
category: options.category,
|
||||
reason: options.reason,
|
||||
description: options.description ?? null,
|
||||
severity: options.severity,
|
||||
@@ -458,424 +241,13 @@ export class CompilerError extends Error {
|
||||
case ErrorSeverity.InvalidJS:
|
||||
case ErrorSeverity.InvalidReact:
|
||||
case ErrorSeverity.InvalidConfig:
|
||||
case ErrorSeverity.UnsupportedJS: {
|
||||
return true;
|
||||
}
|
||||
case ErrorSeverity.CannotPreserveMemoization:
|
||||
case ErrorSeverity.Todo: {
|
||||
case ErrorSeverity.Todo:
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
default:
|
||||
assertExhaustive(detail.severity, 'Unhandled error severity');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function printCodeFrame(
|
||||
source: string,
|
||||
loc: t.SourceLocation,
|
||||
message: string,
|
||||
): string {
|
||||
return codeFrameColumns(
|
||||
source,
|
||||
{
|
||||
start: {
|
||||
line: loc.start.line,
|
||||
column: loc.start.column + 1,
|
||||
},
|
||||
end: {
|
||||
line: loc.end.line,
|
||||
column: loc.end.column + 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
message,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function printErrorSummary(severity: ErrorSeverity, message: string): string {
|
||||
let severityCategory: string;
|
||||
switch (severity) {
|
||||
case ErrorSeverity.InvalidConfig:
|
||||
case ErrorSeverity.InvalidJS:
|
||||
case ErrorSeverity.InvalidReact:
|
||||
case ErrorSeverity.UnsupportedJS: {
|
||||
severityCategory = 'Error';
|
||||
break;
|
||||
}
|
||||
case ErrorSeverity.CannotPreserveMemoization: {
|
||||
severityCategory = 'Memoization';
|
||||
break;
|
||||
}
|
||||
case ErrorSeverity.Invariant: {
|
||||
severityCategory = 'Invariant';
|
||||
break;
|
||||
}
|
||||
case ErrorSeverity.Todo: {
|
||||
severityCategory = 'Todo';
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(severity, `Unexpected severity '${severity}'`);
|
||||
}
|
||||
}
|
||||
return `${severityCategory}: ${message}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* See getRuleForCategory() for how these map to ESLint rules
|
||||
*/
|
||||
export enum ErrorCategory {
|
||||
// Checking for valid hooks usage (non conditional, non-first class, non reactive, etc)
|
||||
Hooks = 'Hooks',
|
||||
|
||||
// Checking for no capitalized calls (not definitively an error, hence separating)
|
||||
CapitalizedCalls = 'CapitalizedCalls',
|
||||
|
||||
// Checking for static components
|
||||
StaticComponents = 'StaticComponents',
|
||||
|
||||
// Checking for valid usage of manual memoization
|
||||
UseMemo = 'UseMemo',
|
||||
|
||||
// Checking for higher order functions acting as factories for components/hooks
|
||||
Factories = 'Factories',
|
||||
|
||||
// Checks that manual memoization is preserved
|
||||
PreserveManualMemo = 'PreserveManualMemo',
|
||||
|
||||
// Checking for no mutations of props, hook arguments, hook return values
|
||||
Immutability = 'Immutability',
|
||||
|
||||
// Checking for assignments to globals
|
||||
Globals = 'Globals',
|
||||
|
||||
// Checking for valid usage of refs, ie no access during render
|
||||
Refs = 'Refs',
|
||||
|
||||
// Checks for memoized effect deps
|
||||
EffectDependencies = 'EffectDependencies',
|
||||
|
||||
// Checks for no setState in effect bodies
|
||||
EffectSetState = 'EffectSetState',
|
||||
|
||||
EffectDerivationsOfState = 'EffectDerivationsOfState',
|
||||
|
||||
// Validates against try/catch in place of error boundaries
|
||||
ErrorBoundaries = 'ErrorBoundaries',
|
||||
|
||||
// Checking for pure functions
|
||||
Purity = 'Purity',
|
||||
|
||||
// Validates against setState in render
|
||||
RenderSetState = 'RenderSetState',
|
||||
|
||||
// Internal invariants
|
||||
Invariant = 'Invariant',
|
||||
|
||||
// Todos
|
||||
Todo = 'Todo',
|
||||
|
||||
// Syntax errors
|
||||
Syntax = 'Syntax',
|
||||
|
||||
// Checks for use of unsupported syntax
|
||||
UnsupportedSyntax = 'UnsupportedSyntax',
|
||||
|
||||
// Config errors
|
||||
Config = 'Config',
|
||||
|
||||
// Gating error
|
||||
Gating = 'Gating',
|
||||
|
||||
// Suppressions
|
||||
Suppression = 'Suppression',
|
||||
|
||||
// Issues with auto deps
|
||||
AutomaticEffectDependencies = 'AutomaticEffectDependencies',
|
||||
|
||||
// Issues with `fire`
|
||||
Fire = 'Fire',
|
||||
|
||||
// fbt-specific issues
|
||||
FBT = 'FBT',
|
||||
}
|
||||
|
||||
export type LintRule = {
|
||||
// Stores the category the rule corresponds to, used to filter errors when reporting
|
||||
category: ErrorCategory;
|
||||
|
||||
/**
|
||||
* The "name" of the rule as it will be used by developers to enable/disable, eg
|
||||
* "eslint-disable-nest line <name>"
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* A description of the rule that appears somewhere in ESLint. This does not affect
|
||||
* how error messages are formatted
|
||||
*/
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* If true, this rule will automatically appear in the default, "recommended" ESLint
|
||||
* rule set. Otherwise it will be part of an `allRules` export that developers can
|
||||
* use to opt-in to showing output of all possible rules.
|
||||
*
|
||||
* NOTE: not all validations are enabled by default! Setting this flag only affects
|
||||
* whether a given rule is part of the recommended set. The corresponding validation
|
||||
* also should be enabled by default if you want the error to actually show up!
|
||||
*/
|
||||
recommended: boolean;
|
||||
};
|
||||
|
||||
const RULE_NAME_PATTERN = /^[a-z]+(-[a-z]+)*$/;
|
||||
|
||||
export function getRuleForCategory(category: ErrorCategory): LintRule {
|
||||
const rule = getRuleForCategoryImpl(category);
|
||||
invariant(
|
||||
RULE_NAME_PATTERN.test(rule.name),
|
||||
`Invalid rule name, got '${rule.name}' but rules must match ${RULE_NAME_PATTERN.toString()}`,
|
||||
);
|
||||
return rule;
|
||||
}
|
||||
|
||||
function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
switch (category) {
|
||||
case ErrorCategory.AutomaticEffectDependencies: {
|
||||
return {
|
||||
category,
|
||||
name: 'automatic-effect-dependencies',
|
||||
description:
|
||||
'Verifies that automatic effect dependencies are compiled if opted-in',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.CapitalizedCalls: {
|
||||
return {
|
||||
category,
|
||||
name: 'capitalized-calls',
|
||||
description:
|
||||
'Validates against calling capitalized functions/methods instead of using JSX',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Config: {
|
||||
return {
|
||||
category,
|
||||
name: 'config',
|
||||
description: 'Validates the compiler configuration options',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectDependencies: {
|
||||
return {
|
||||
category,
|
||||
name: 'memoized-effect-dependencies',
|
||||
description: 'Validates that effect dependencies are memoized',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectDerivationsOfState: {
|
||||
return {
|
||||
category,
|
||||
name: 'no-deriving-state-in-effects',
|
||||
description:
|
||||
'Validates against deriving values from state in an effect',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectSetState: {
|
||||
return {
|
||||
category,
|
||||
name: 'set-state-in-effect',
|
||||
description:
|
||||
'Validates against calling setState synchronously in an effect, which can lead to re-renders that degrade performance',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.ErrorBoundaries: {
|
||||
return {
|
||||
category,
|
||||
name: 'error-boundaries',
|
||||
description:
|
||||
'Validates usage of error boundaries instead of try/catch for errors in child components',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Factories: {
|
||||
return {
|
||||
category,
|
||||
name: 'component-hook-factories',
|
||||
description:
|
||||
'Validates against higher order functions defining nested components or hooks. ' +
|
||||
'Components and hooks should be defined at the module level',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.FBT: {
|
||||
return {
|
||||
category,
|
||||
name: 'fbt',
|
||||
description: 'Validates usage of fbt',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Fire: {
|
||||
return {
|
||||
category,
|
||||
name: 'fire',
|
||||
description: 'Validates usage of `fire`',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Gating: {
|
||||
return {
|
||||
category,
|
||||
name: 'gating',
|
||||
description:
|
||||
'Validates configuration of [gating mode](https://react.dev/reference/react-compiler/gating)',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Globals: {
|
||||
return {
|
||||
category,
|
||||
name: 'globals',
|
||||
description:
|
||||
'Validates against assignment/mutation of globals during render, part of ensuring that ' +
|
||||
'[side effects must render outside of render](https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Hooks: {
|
||||
return {
|
||||
category,
|
||||
name: 'hooks',
|
||||
description: 'Validates the rules of hooks',
|
||||
/**
|
||||
* TODO: the "Hooks" rule largely reimplements the "rules-of-hooks" non-compiler rule.
|
||||
* We need to dedeupe these (moving the remaining bits into the compiler) and then enable
|
||||
* this rule.
|
||||
*/
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Immutability: {
|
||||
return {
|
||||
category,
|
||||
name: 'immutability',
|
||||
description:
|
||||
'Validates against mutating props, state, and other values that [are immutable](https://react.dev/reference/rules/components-and-hooks-must-be-pure#props-and-state-are-immutable)',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Invariant: {
|
||||
return {
|
||||
category,
|
||||
name: 'invariant',
|
||||
description: 'Internal invariants',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.PreserveManualMemo: {
|
||||
return {
|
||||
category,
|
||||
name: 'preserve-manual-memoization',
|
||||
description:
|
||||
'Validates that existing manual memoized is preserved by the compiler. ' +
|
||||
'React Compiler will only compile components and hooks if its inference ' +
|
||||
'[matches or exceeds the existing manual memoization](https://react.dev/learn/react-compiler/introduction#what-should-i-do-about-usememo-usecallback-and-reactmemo)',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Purity: {
|
||||
return {
|
||||
category,
|
||||
name: 'purity',
|
||||
description:
|
||||
'Validates that [components/hooks are pure](https://react.dev/reference/rules/components-and-hooks-must-be-pure) by checking that they do not call known-impure functions',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Refs: {
|
||||
return {
|
||||
category,
|
||||
name: 'refs',
|
||||
description:
|
||||
'Validates correct usage of refs, not reading/writing during render. See the "pitfalls" section in [`useRef()` usage](https://react.dev/reference/react/useRef#usage)',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.RenderSetState: {
|
||||
return {
|
||||
category,
|
||||
name: 'set-state-in-render',
|
||||
description:
|
||||
'Validates against setting state during render, which can trigger additional renders and potential infinite render loops',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.StaticComponents: {
|
||||
return {
|
||||
category,
|
||||
name: 'static-components',
|
||||
description:
|
||||
'Validates that components are static, not recreated every render. Components that are recreated dynamically can reset state and trigger excessive re-rendering',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Suppression: {
|
||||
return {
|
||||
category,
|
||||
name: 'rule-suppression',
|
||||
description: 'Validates against suppression of other rules',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Syntax: {
|
||||
return {
|
||||
category,
|
||||
name: 'syntax',
|
||||
description: 'Validates against invalid syntax',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Todo: {
|
||||
return {
|
||||
category,
|
||||
name: 'todo',
|
||||
description: 'Unimplemented features',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.UnsupportedSyntax: {
|
||||
return {
|
||||
category,
|
||||
name: 'unsupported-syntax',
|
||||
description:
|
||||
'Validates against syntax that we do not plan to support in React Compiler',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.UseMemo: {
|
||||
return {
|
||||
category,
|
||||
name: 'use-memo',
|
||||
description:
|
||||
'Validates usage of the useMemo() hook against common mistakes. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(category, `Unsupported category ${category}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const LintRules: Array<LintRule> = Object.keys(ErrorCategory).map(
|
||||
category => getRuleForCategory(category as any),
|
||||
);
|
||||
|
||||
@@ -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, ErrorSeverity} from '../CompilerError';
|
||||
import {CompilerError, ErrorSeverity} from '../CompilerError';
|
||||
import {
|
||||
EnvironmentConfig,
|
||||
GeneratedSource,
|
||||
@@ -38,7 +38,6 @@ 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}`,
|
||||
@@ -206,7 +205,6 @@ 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}`,
|
||||
|
||||
@@ -7,12 +7,7 @@
|
||||
|
||||
import * as t from '@babel/types';
|
||||
import {z} from 'zod';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
CompilerErrorDetail,
|
||||
CompilerErrorDetailOptions,
|
||||
} from '../CompilerError';
|
||||
import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError';
|
||||
import {
|
||||
EnvironmentConfig,
|
||||
ExternalFunction,
|
||||
@@ -229,7 +224,7 @@ export type LoggerEvent =
|
||||
export type CompileErrorEvent = {
|
||||
kind: 'CompileError';
|
||||
fnLoc: t.SourceLocation | null;
|
||||
detail: CompilerErrorDetail | CompilerDiagnostic;
|
||||
detail: CompilerErrorDetailOptions;
|
||||
};
|
||||
export type CompileDiagnosticEvent = {
|
||||
kind: 'CompileDiagnostic';
|
||||
|
||||
@@ -33,7 +33,9 @@ import {findContextIdentifiers} from '../HIR/FindContextIdentifiers';
|
||||
import {
|
||||
analyseFunctions,
|
||||
dropManualMemoization,
|
||||
inferMutableRanges,
|
||||
inferReactivePlaces,
|
||||
inferReferenceEffects,
|
||||
inlineImmediatelyInvokedFunctionExpressions,
|
||||
inferEffectDependencies,
|
||||
} from '../Inference';
|
||||
@@ -90,19 +92,20 @@ 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 {validateNoSetStateInPassiveEffects} from '../Validation/ValidateNoSetStateInPassiveEffects';
|
||||
import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement';
|
||||
import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHIR';
|
||||
import {outlineJSX} from '../Optimization/OutlineJsx';
|
||||
import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls';
|
||||
import {transformFire} from '../Transform';
|
||||
import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureFunctionsInRender';
|
||||
import {CompilerError} from '..';
|
||||
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
|
||||
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
|
||||
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
|
||||
import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
|
||||
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
|
||||
|
||||
export type CompilerPipelineValue =
|
||||
| {kind: 'ast'; name: string; value: CodegenFunction}
|
||||
@@ -171,7 +174,7 @@ function runWithEnvironment(
|
||||
!env.config.disableMemoizationForDebugging &&
|
||||
!env.config.enableChangeDetectionForDebugging
|
||||
) {
|
||||
dropManualMemoization(hir).unwrap();
|
||||
dropManualMemoization(hir);
|
||||
log({kind: 'hir', name: 'DropManualMemoization', value: hir});
|
||||
}
|
||||
|
||||
@@ -226,12 +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.isInferredMemoEnabled) {
|
||||
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.config.enableNewMutationAliasingModel) {
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
}
|
||||
|
||||
// Note: Has to come after infer reference effects because "dead" code may still affect inference
|
||||
@@ -246,15 +263,20 @@ 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.isInferredMemoEnabled) {
|
||||
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.isInferredMemoEnabled) {
|
||||
@@ -270,12 +292,8 @@ function runWithEnvironment(
|
||||
validateNoSetStateInRender(hir).unwrap();
|
||||
}
|
||||
|
||||
if (env.config.validateNoDerivedComputationsInEffects) {
|
||||
validateNoDerivedComputationsInEffects(hir);
|
||||
}
|
||||
|
||||
if (env.config.validateNoSetStateInEffects) {
|
||||
env.logErrors(validateNoSetStateInEffects(hir));
|
||||
if (env.config.validateNoSetStateInPassiveEffects) {
|
||||
env.logErrors(validateNoSetStateInPassiveEffects(hir));
|
||||
}
|
||||
|
||||
if (env.config.validateNoJSXInTryStatements) {
|
||||
@@ -286,7 +304,12 @@ function runWithEnvironment(
|
||||
validateNoImpureFunctionsInRender(hir).unwrap();
|
||||
}
|
||||
|
||||
validateNoFreezingKnownMutableFunctions(hir).unwrap();
|
||||
if (
|
||||
env.config.validateNoFreezingKnownMutableFunctions ||
|
||||
env.config.enableNewMutationAliasingModel
|
||||
) {
|
||||
validateNoFreezingKnownMutableFunctions(hir).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
inferReactivePlaces(hir);
|
||||
@@ -299,6 +322,13 @@ function runWithEnvironment(
|
||||
value: hir,
|
||||
});
|
||||
|
||||
propagatePhiTypes(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'PropagatePhiTypes',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (env.config.validateStaticComponents) {
|
||||
env.logErrors(validateStaticComponents(hir));
|
||||
|
||||
@@ -10,7 +10,6 @@ import * as t from '@babel/types';
|
||||
import {
|
||||
CompilerError,
|
||||
CompilerErrorDetail,
|
||||
ErrorCategory,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {ExternalFunction, ReactFunctionType} from '../HIR/Environment';
|
||||
@@ -106,7 +105,6 @@ function findDirectivesDynamicGating(
|
||||
reason: `Dynamic gating directive is not a valid JavaScript identifier`,
|
||||
description: `Found '${directive.value.value}'`,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: ErrorCategory.Gating,
|
||||
loc: directive.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -123,7 +121,6 @@ function findDirectivesDynamicGating(
|
||||
.map(r => r.directive.value.value)
|
||||
.join(', ')}]`,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: ErrorCategory.Gating,
|
||||
loc: result[0].directive.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -184,7 +181,7 @@ function logError(
|
||||
context.opts.logger.logEvent(context.filename, {
|
||||
kind: 'CompileError',
|
||||
fnLoc,
|
||||
detail,
|
||||
detail: detail.options,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -459,7 +456,6 @@ export function compileProgram(
|
||||
reason:
|
||||
'Unexpected compiled functions when module scope opt-out is present',
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: ErrorCategory.Invariant,
|
||||
loc: null,
|
||||
}),
|
||||
);
|
||||
@@ -494,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;
|
||||
}
|
||||
@@ -828,7 +811,6 @@ function shouldSkipCompilation(
|
||||
description:
|
||||
"When the 'sources' config options is specified, the React compiler will only compile files with a name",
|
||||
severity: ErrorSeverity.InvalidConfig,
|
||||
category: ErrorCategory.Config,
|
||||
loc: null,
|
||||
}),
|
||||
);
|
||||
@@ -852,73 +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,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
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,
|
||||
@@ -957,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: {
|
||||
|
||||
@@ -8,10 +8,9 @@
|
||||
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';
|
||||
@@ -182,12 +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()}\``,
|
||||
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,
|
||||
category: ErrorCategory.Suppression,
|
||||
loc: suppressionRange.disableComment.loc ?? null,
|
||||
suggestions: [
|
||||
{
|
||||
description: suggestion,
|
||||
@@ -198,10 +197,6 @@ export function suppressionsToCompilerError(
|
||||
op: CompilerSuggestionOperation.Remove,
|
||||
},
|
||||
],
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: suppressionRange.disableComment.loc ?? null,
|
||||
message: 'Found React rule suppression',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,67 +8,35 @@
|
||||
import {NodePath} from '@babel/core';
|
||||
import * as t from '@babel/types';
|
||||
|
||||
import {CompilerError, EnvironmentConfig, ErrorSeverity, Logger} from '..';
|
||||
import {
|
||||
CompilerError,
|
||||
CompilerErrorDetailOptions,
|
||||
EnvironmentConfig,
|
||||
ErrorSeverity,
|
||||
Logger,
|
||||
} from '..';
|
||||
import {getOrInsertWith} from '../Utils/utils';
|
||||
import {Environment, GeneratedSource} from '../HIR';
|
||||
import {Environment} from '../HIR';
|
||||
import {DEFAULT_EXPORT} from '../HIR/Environment';
|
||||
import {CompileProgramMetadata} from './Program';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerDiagnosticOptions,
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
|
||||
function throwInvalidReact(
|
||||
options: Omit<CompilerDiagnosticOptions, 'severity'>,
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity'>,
|
||||
{logger, filename}: TraversalState,
|
||||
): never {
|
||||
const detail: CompilerDiagnosticOptions = {
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
const detail: CompilerErrorDetailOptions = {
|
||||
...options,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
};
|
||||
logger?.logEvent(filename, {
|
||||
kind: 'CompileError',
|
||||
fnLoc: null,
|
||||
detail: new CompilerDiagnostic(detail),
|
||||
detail,
|
||||
});
|
||||
CompilerError.throwDiagnostic(detail);
|
||||
}
|
||||
|
||||
function isAutodepsSigil(
|
||||
arg: NodePath<t.ArgumentPlaceholder | t.SpreadElement | t.Expression>,
|
||||
): boolean {
|
||||
// Check for AUTODEPS identifier imported from React
|
||||
if (arg.isIdentifier() && arg.node.name === 'AUTODEPS') {
|
||||
const binding = arg.scope.getBinding(arg.node.name);
|
||||
if (binding && binding.path.isImportSpecifier()) {
|
||||
const importSpecifier = binding.path.node as t.ImportSpecifier;
|
||||
if (importSpecifier.imported.type === 'Identifier') {
|
||||
return (importSpecifier.imported as t.Identifier).name === 'AUTODEPS';
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for React.AUTODEPS member expression
|
||||
if (arg.isMemberExpression() && !arg.node.computed) {
|
||||
const object = arg.get('object');
|
||||
const property = arg.get('property');
|
||||
|
||||
if (
|
||||
object.isIdentifier() &&
|
||||
object.node.name === 'React' &&
|
||||
property.isIdentifier() &&
|
||||
property.node.name === 'AUTODEPS'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
CompilerError.throw(detail);
|
||||
}
|
||||
function assertValidEffectImportReference(
|
||||
autodepsIndex: number,
|
||||
numArgs: number,
|
||||
paths: Array<NodePath<t.Node>>,
|
||||
context: TraversalState,
|
||||
): void {
|
||||
@@ -81,10 +49,11 @@ function assertValidEffectImportReference(
|
||||
maybeCalleeLoc != null &&
|
||||
context.inferredEffectLocations.has(maybeCalleeLoc);
|
||||
/**
|
||||
* Error on effect calls that still have AUTODEPS in their args
|
||||
* Only error on untransformed references of the form `useMyEffect(...)`
|
||||
* or `moduleNamespace.useMyEffect(...)`, with matching argument counts.
|
||||
* TODO: do we also want a mode to also hard error on non-call references?
|
||||
*/
|
||||
const hasAutodepsArg = args.some(isAutodepsSigil);
|
||||
if (hasAutodepsArg && !hasInferredEffect) {
|
||||
if (args.length === numArgs && !hasInferredEffect) {
|
||||
const maybeErrorDiagnostic = matchCompilerDiagnostic(
|
||||
path,
|
||||
context.transformErrors,
|
||||
@@ -96,19 +65,14 @@ function assertValidEffectImportReference(
|
||||
*/
|
||||
throwInvalidReact(
|
||||
{
|
||||
category: ErrorCategory.AutomaticEffectDependencies,
|
||||
reason:
|
||||
'Cannot infer dependencies of this effect. This will break your build!',
|
||||
description:
|
||||
'To resolve, either pass a dependency array or fix reported compiler bailout diagnostics.' +
|
||||
(maybeErrorDiagnostic ? ` ${maybeErrorDiagnostic}` : ''),
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
message: 'Cannot infer dependencies',
|
||||
loc: parent.node.loc ?? GeneratedSource,
|
||||
},
|
||||
],
|
||||
'[InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. ' +
|
||||
'This will break your build! ' +
|
||||
'To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics.',
|
||||
description: maybeErrorDiagnostic
|
||||
? `(Bailout reason: ${maybeErrorDiagnostic})`
|
||||
: null,
|
||||
loc: parent.node.loc ?? null,
|
||||
},
|
||||
context,
|
||||
);
|
||||
@@ -128,20 +92,13 @@ function assertValidFireImportReference(
|
||||
);
|
||||
throwInvalidReact(
|
||||
{
|
||||
category: ErrorCategory.Fire,
|
||||
reason: '[Fire] Untransformed reference to compiler-required feature.',
|
||||
description:
|
||||
'Either remove this `fire` call or ensure it is successfully transformed by the compiler' +
|
||||
maybeErrorDiagnostic
|
||||
? ` ${maybeErrorDiagnostic}`
|
||||
: '',
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
message: 'Untransformed `fire` call',
|
||||
loc: paths[0].node.loc ?? GeneratedSource,
|
||||
},
|
||||
],
|
||||
reason:
|
||||
'[Fire] Untransformed reference to compiler-required feature. ' +
|
||||
'Either remove this `fire` call or ensure it is successfully transformed by the compiler',
|
||||
description: maybeErrorDiagnostic
|
||||
? `(Bailout reason: ${maybeErrorDiagnostic})`
|
||||
: null,
|
||||
loc: paths[0].node.loc ?? null,
|
||||
},
|
||||
context,
|
||||
);
|
||||
@@ -171,12 +128,12 @@ export default function validateNoUntransformedReferences(
|
||||
if (env.inferEffectDependencies) {
|
||||
for (const {
|
||||
function: {source, importSpecifierName},
|
||||
autodepsIndex,
|
||||
numRequiredArgs,
|
||||
} of env.inferEffectDependencies) {
|
||||
const module = getOrInsertWith(moduleLoadChecks, source, () => new Map());
|
||||
module.set(
|
||||
importSpecifierName,
|
||||
assertValidEffectImportReference.bind(null, autodepsIndex),
|
||||
assertValidEffectImportReference.bind(null, numRequiredArgs),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -1,131 +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',
|
||||
loc,
|
||||
});
|
||||
} 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,
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,10 +9,8 @@ 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';
|
||||
@@ -106,18 +104,12 @@ export function lower(
|
||||
if (param.isIdentifier()) {
|
||||
const binding = builder.resolveIdentifier(param);
|
||||
if (binding.kind !== 'Identifier') {
|
||||
builder.errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: ErrorCategory.Invariant,
|
||||
reason: 'Could not find binding',
|
||||
description: `[BuildHIR] Could not find binding for param \`${param.node.name}\`.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: param.node.loc ?? null,
|
||||
message: 'Could not find binding',
|
||||
}),
|
||||
);
|
||||
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 = {
|
||||
@@ -171,18 +163,12 @@ export function lower(
|
||||
'Assignment',
|
||||
);
|
||||
} else {
|
||||
builder.errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
reason: `Handle ${param.node.type} parameters`,
|
||||
description: `[BuildHIR] Add support for ${param.node.type} parameters.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: param.node.loc ?? null,
|
||||
message: 'Unsupported parameter type',
|
||||
}),
|
||||
);
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lower) Handle ${param.node.type} params`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
loc: param.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -192,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),
|
||||
@@ -203,18 +188,13 @@ export function lower(
|
||||
lowerStatement(builder, body);
|
||||
directives = body.get('directives').map(d => d.node.value.value);
|
||||
} else {
|
||||
builder.errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
reason: `Unexpected function body kind`,
|
||||
description: `Expected function body to be an expression or a block statement, got \`${body.type}\`.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: body.node.loc ?? null,
|
||||
message: 'Expected a block statement or expression',
|
||||
}),
|
||||
);
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
if (builder.errors.hasErrors()) {
|
||||
@@ -224,7 +204,6 @@ export function lower(
|
||||
builder.terminate(
|
||||
{
|
||||
kind: 'return',
|
||||
returnVariant: 'Void',
|
||||
loc: GeneratedSource,
|
||||
value: lowerValueToTemporary(builder, {
|
||||
kind: 'Primitive',
|
||||
@@ -277,7 +256,6 @@ function lowerStatement(
|
||||
reason:
|
||||
'(BuildHIR::lowerStatement) Support ThrowStatement inside of try/catch',
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -309,7 +287,6 @@ function lowerStatement(
|
||||
}
|
||||
const terminal: ReturnTerminal = {
|
||||
kind: 'return',
|
||||
returnVariant: 'Explicit',
|
||||
loc: stmt.node.loc ?? GeneratedSource,
|
||||
value,
|
||||
id: makeInstructionId(0),
|
||||
@@ -465,7 +442,6 @@ function lowerStatement(
|
||||
} else if (!binding.path.isVariableDeclarator()) {
|
||||
builder.errors.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
reason: 'Unsupported declaration type for hoisting',
|
||||
description: `variable "${binding.identifier.name}" declared with ${binding.path.type}`,
|
||||
suggestions: null,
|
||||
@@ -475,7 +451,6 @@ function lowerStatement(
|
||||
} else {
|
||||
builder.errors.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
reason: 'Handle non-const declarations for hoisting',
|
||||
description: `variable "${binding.identifier.name}" declared with ${binding.kind}`,
|
||||
suggestions: null,
|
||||
@@ -556,7 +531,6 @@ function lowerStatement(
|
||||
reason:
|
||||
'(BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement',
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -629,7 +603,6 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerStatement) Handle empty test in ForStatement`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -781,7 +754,6 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason: `Expected at most one \`default\` branch in a switch statement, this code should have failed to parse`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: case_.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -854,7 +826,6 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerStatement) Handle ${nodeKind} kinds in VariableDeclaration`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -883,7 +854,6 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Could not find binding for declaration.`,
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: ErrorCategory.Invariant,
|
||||
loc: id.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -901,7 +871,6 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason: `Expect \`const\` declaration not to be reassigned`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: id.node.loc ?? null,
|
||||
suggestions: [
|
||||
{
|
||||
@@ -949,7 +918,6 @@ function lowerStatement(
|
||||
reason: `Expected variable declaration to be an identifier if no initializer was provided`,
|
||||
description: `Got a \`${id.type}\``,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1058,7 +1026,6 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerStatement) Handle for-await loops`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1291,7 +1258,6 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerStatement) Handle TryStatement without a catch clause`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1301,7 +1267,6 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerStatement) Handle TryStatement with a finalizer ('finally') clause`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1390,89 +1355,13 @@ function lowerStatement(
|
||||
|
||||
return;
|
||||
}
|
||||
case 'WithStatement': {
|
||||
builder.errors.push({
|
||||
reason: `JavaScript 'with' syntax is not supported`,
|
||||
description: `'with' syntax is considered deprecated and removed from JavaScript standards, consider alternatives`,
|
||||
severity: ErrorSeverity.UnsupportedJS,
|
||||
category: ErrorCategory.UnsupportedSyntax,
|
||||
loc: stmtPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'UnsupportedNode',
|
||||
loc: stmtPath.node.loc ?? GeneratedSource,
|
||||
node: stmtPath.node,
|
||||
});
|
||||
return;
|
||||
}
|
||||
case 'ClassDeclaration': {
|
||||
/**
|
||||
* In theory we could support inline class declarations, but this is rare enough in practice
|
||||
* and complex enough to support that we don't anticipate supporting anytime soon. Developers
|
||||
* are encouraged to lift classes out of component/hook declarations.
|
||||
*/
|
||||
builder.errors.push({
|
||||
reason: 'Inline `class` declarations are not supported',
|
||||
description: `Move class declarations outside of components/hooks`,
|
||||
severity: ErrorSeverity.UnsupportedJS,
|
||||
category: ErrorCategory.UnsupportedSyntax,
|
||||
loc: stmtPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'UnsupportedNode',
|
||||
loc: stmtPath.node.loc ?? GeneratedSource,
|
||||
node: stmtPath.node,
|
||||
});
|
||||
return;
|
||||
}
|
||||
case 'EnumDeclaration':
|
||||
case 'TSEnumDeclaration': {
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'UnsupportedNode',
|
||||
loc: stmtPath.node.loc ?? GeneratedSource,
|
||||
node: stmtPath.node,
|
||||
});
|
||||
return;
|
||||
}
|
||||
case 'ExportAllDeclaration':
|
||||
case 'ExportDefaultDeclaration':
|
||||
case 'ExportNamedDeclaration':
|
||||
case 'ImportDeclaration':
|
||||
case 'TSExportAssignment':
|
||||
case 'TSImportEqualsDeclaration': {
|
||||
builder.errors.push({
|
||||
reason:
|
||||
'JavaScript `import` and `export` statements may only appear at the top level of a module',
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: stmtPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'UnsupportedNode',
|
||||
loc: stmtPath.node.loc ?? GeneratedSource,
|
||||
node: stmtPath.node,
|
||||
});
|
||||
return;
|
||||
}
|
||||
case 'TSNamespaceExportDeclaration': {
|
||||
builder.errors.push({
|
||||
reason:
|
||||
'TypeScript `namespace` statements may only appear at the top level of a module',
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: stmtPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'UnsupportedNode',
|
||||
loc: stmtPath.node.loc ?? GeneratedSource,
|
||||
node: stmtPath.node,
|
||||
});
|
||||
case 'TypeAlias':
|
||||
case 'TSInterfaceDeclaration':
|
||||
case 'TSTypeAliasDeclaration': {
|
||||
// We do not preserve type annotations/syntax through transformation
|
||||
return;
|
||||
}
|
||||
case 'ClassDeclaration':
|
||||
case 'DeclareClass':
|
||||
case 'DeclareExportAllDeclaration':
|
||||
case 'DeclareExportDeclaration':
|
||||
@@ -1483,14 +1372,31 @@ function lowerStatement(
|
||||
case 'DeclareOpaqueType':
|
||||
case 'DeclareTypeAlias':
|
||||
case 'DeclareVariable':
|
||||
case 'EnumDeclaration':
|
||||
case 'ExportAllDeclaration':
|
||||
case 'ExportDefaultDeclaration':
|
||||
case 'ExportNamedDeclaration':
|
||||
case 'ImportDeclaration':
|
||||
case 'InterfaceDeclaration':
|
||||
case 'OpaqueType':
|
||||
case 'TSDeclareFunction':
|
||||
case 'TSInterfaceDeclaration':
|
||||
case 'TSEnumDeclaration':
|
||||
case 'TSExportAssignment':
|
||||
case 'TSImportEqualsDeclaration':
|
||||
case 'TSModuleDeclaration':
|
||||
case 'TSTypeAliasDeclaration':
|
||||
case 'TypeAlias': {
|
||||
// We do not preserve type annotations/syntax through transformation
|
||||
case 'TSNamespaceExportDeclaration':
|
||||
case 'WithStatement': {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerStatement) Handle ${stmtPath.type} statements`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
loc: stmtPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'UnsupportedNode',
|
||||
loc: stmtPath.node.loc ?? GeneratedSource,
|
||||
node: stmtPath.node,
|
||||
});
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
@@ -1541,7 +1447,6 @@ function lowerObjectPropertyKey(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: key.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1567,7 +1472,6 @@ function lowerObjectPropertyKey(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: key.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1625,7 +1529,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${valuePath.type} values in ObjectExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: valuePath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1652,7 +1555,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${propertyPath.node.kind} functions in ObjectExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: propertyPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1674,7 +1576,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${propertyPath.type} properties in ObjectExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: propertyPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1708,7 +1609,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${element.type} elements in ArrayExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: element.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1729,7 +1629,6 @@ function lowerExpression(
|
||||
reason: `Expected an expression as the \`new\` expression receiver (v8 intrinsics are not supported)`,
|
||||
description: `Got a \`${calleePath.node.type}\``,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: calleePath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1756,7 +1655,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `Expected Expression, got ${calleePath.type} in CallExpression (v8 intrinsics not supported). This error is likely caused by a bug in React Compiler. Please file an issue`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: calleePath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1791,7 +1689,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Expected Expression, got ${leftPath.type} lval in BinaryExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: leftPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1804,7 +1701,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Pipe operator not supported`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: leftPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1834,7 +1730,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `Expected sequence expression to have at least one expression`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: expr.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2047,7 +1942,6 @@ function lowerExpression(
|
||||
reason: `(BuildHIR::lowerExpression) Unsupported syntax on the left side of an AssignmentExpression`,
|
||||
description: `Expected an LVal, got: ${left.type}`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: left.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2076,7 +1970,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${operator} operators in AssignmentExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: expr.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2176,7 +2069,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Expected Identifier or MemberExpression, got ${expr.type} lval in AssignmentExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: expr.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2216,7 +2108,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${attribute.type} attributes in JSXElement`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: attribute.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2230,7 +2121,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Unexpected colon in attribute name \`${propName}\``,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: namePath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2261,7 +2151,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${valueExpr.type} attribute values in JSXElement`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: valueExpr.node?.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2272,7 +2161,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${expression.type} expressions in JSXExpressionContainer within JSXElement`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: valueExpr.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2328,18 +2216,11 @@ function lowerExpression(
|
||||
});
|
||||
for (const [name, locations] of Object.entries(fbtLocations)) {
|
||||
if (locations.length > 1) {
|
||||
CompilerError.throwDiagnostic({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.FBT,
|
||||
reason: 'Support duplicate fbt tags',
|
||||
description: `Support \`<${tagName}>\` tags with multiple \`<${tagName}:${name}>\` values`,
|
||||
details: locations.map(loc => {
|
||||
return {
|
||||
kind: 'error',
|
||||
message: `Multiple \`<${tagName}:${name}>\` tags found`,
|
||||
loc,
|
||||
};
|
||||
}),
|
||||
CompilerError.throwTodo({
|
||||
reason: `Support <${tagName}> tags with multiple <${tagName}:${name}> values`,
|
||||
loc: locations.at(-1) ?? GeneratedSource,
|
||||
description: null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2392,7 +2273,6 @@ function lowerExpression(
|
||||
reason:
|
||||
'(BuildHIR::lowerExpression) Handle tagged template with interpolations',
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2411,7 +2291,6 @@ function lowerExpression(
|
||||
reason:
|
||||
'(BuildHIR::lowerExpression) Handle tagged template where cooked value is different from raw value',
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2434,7 +2313,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `Unexpected quasi and subexpression lengths in template literal`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2445,7 +2323,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Handle TSType in TemplateLiteral.`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2488,7 +2365,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `Only object properties can be deleted`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: expr.node.loc ?? null,
|
||||
suggestions: [
|
||||
{
|
||||
@@ -2504,7 +2380,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `Throw expressions are not supported`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: expr.node.loc ?? null,
|
||||
suggestions: [
|
||||
{
|
||||
@@ -2626,7 +2501,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle UpdateExpression with ${argument.type} argument`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2635,7 +2509,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle UpdateExpression to variables captured within lambdas.`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2656,7 +2529,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Found an invalid UpdateExpression without a previously reported error`,
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: ErrorCategory.Invariant,
|
||||
loc: exprLoc,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2666,7 +2538,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Support UpdateExpression where argument is a global`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprLoc,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2722,7 +2593,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle MetaProperty expressions other than import.meta`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2732,7 +2602,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${exprPath.type} expressions`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3030,7 +2899,6 @@ function lowerReorderableExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::node.lowerReorderableExpression) Expression type \`${expr.type}\` cannot be safely reordered`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: expr.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3078,8 +2946,6 @@ function isReorderableExpression(
|
||||
}
|
||||
}
|
||||
}
|
||||
case 'TSAsExpression':
|
||||
case 'TSNonNullExpression':
|
||||
case 'TypeCastExpression': {
|
||||
return isReorderableExpression(
|
||||
builder,
|
||||
@@ -3227,7 +3093,6 @@ function lowerArguments(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${argPath.type} arguments in CallExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: argPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3263,7 +3128,6 @@ function lowerMemberExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerMemberExpression) Handle ${propertyNode.type} property`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: propertyNode.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3285,7 +3149,6 @@ function lowerMemberExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerMemberExpression) Expected Expression, got ${propertyNode.type} property`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: propertyNode.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3345,7 +3208,6 @@ function lowerJsxElementName(
|
||||
reason: `Expected JSXNamespacedName to have no colons in the namespace or name`,
|
||||
description: `Got \`${namespace}\` : \`${name}\``,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3360,7 +3222,6 @@ function lowerJsxElementName(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerJsxElementName) Handle ${exprPath.type} tags`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3459,7 +3320,6 @@ function lowerJsxElement(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerJsxElement) Unhandled JsxElement, got: ${exprPath.type}`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3586,8 +3446,9 @@ function lowerFunction(
|
||||
);
|
||||
let loweredFunc: HIRFunction;
|
||||
if (lowering.isErr()) {
|
||||
const functionErrors = lowering.unwrapErr();
|
||||
builder.errors.merge(functionErrors);
|
||||
lowering
|
||||
.unwrapErr()
|
||||
.details.forEach(detail => builder.errors.pushErrorDetail(detail));
|
||||
return null;
|
||||
}
|
||||
loweredFunc = lowering.unwrap();
|
||||
@@ -3641,17 +3502,6 @@ function lowerIdentifier(
|
||||
return place;
|
||||
}
|
||||
default: {
|
||||
if (binding.kind === 'Global' && binding.name === 'eval') {
|
||||
builder.errors.push({
|
||||
reason: `The 'eval' function is not supported`,
|
||||
description:
|
||||
'Eval is an anti-pattern in JavaScript, and the code executed cannot be evaluated by React Compiler',
|
||||
severity: ErrorSeverity.UnsupportedJS,
|
||||
category: ErrorCategory.UnsupportedSyntax,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
return lowerValueToTemporary(builder, {
|
||||
kind: 'LoadGlobal',
|
||||
binding,
|
||||
@@ -3704,7 +3554,6 @@ function lowerIdentifierForAssignment(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Could not find binding for declaration.`,
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: ErrorCategory.Invariant,
|
||||
loc: path.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3717,7 +3566,6 @@ function lowerIdentifierForAssignment(
|
||||
builder.errors.push({
|
||||
reason: `Cannot reassign a \`const\` variable`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: path.node.loc ?? null,
|
||||
description:
|
||||
binding.identifier.name != null
|
||||
@@ -3775,7 +3623,6 @@ function lowerAssignment(
|
||||
builder.errors.push({
|
||||
reason: `Expected \`const\` declaration not to be reassigned`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: lvalue.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3790,7 +3637,6 @@ function lowerAssignment(
|
||||
builder.errors.push({
|
||||
reason: `Unexpected context variable kind`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: lvalue.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3862,7 +3708,6 @@ function lowerAssignment(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Handle ${property.type} properties in MemberExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: property.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3875,7 +3720,6 @@ function lowerAssignment(
|
||||
reason:
|
||||
'(BuildHIR::lowerAssignment) Expected private name to appear as a non-computed property',
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: property.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3941,7 +3785,6 @@ function lowerAssignment(
|
||||
} else if (identifier.kind === 'Global') {
|
||||
builder.errors.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
reason:
|
||||
'Expected reassignment of globals to enable forceTemporaries',
|
||||
loc: element.node.loc ?? GeneratedSource,
|
||||
@@ -3981,7 +3824,6 @@ function lowerAssignment(
|
||||
} else if (identifier.kind === 'Global') {
|
||||
builder.errors.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
reason:
|
||||
'Expected reassignment of globals to enable forceTemporaries',
|
||||
loc: element.node.loc ?? GeneratedSource,
|
||||
@@ -4055,7 +3897,6 @@ function lowerAssignment(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Handle ${argument.node.type} rest element in ObjectPattern`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: argument.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -4087,7 +3928,6 @@ function lowerAssignment(
|
||||
} else if (identifier.kind === 'Global') {
|
||||
builder.errors.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
reason:
|
||||
'Expected reassignment of globals to enable forceTemporaries',
|
||||
loc: property.node.loc ?? GeneratedSource,
|
||||
@@ -4105,7 +3945,6 @@ function lowerAssignment(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Handle ${property.type} properties in ObjectPattern`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: property.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -4115,7 +3954,6 @@ function lowerAssignment(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: property.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -4130,7 +3968,6 @@ function lowerAssignment(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Expected object property value to be an LVal, got: ${element.type}`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: element.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -4153,7 +3990,6 @@ function lowerAssignment(
|
||||
} else if (identifier.kind === 'Global') {
|
||||
builder.errors.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
reason:
|
||||
'Expected reassignment of globals to enable forceTemporaries',
|
||||
loc: element.node.loc ?? GeneratedSource,
|
||||
@@ -4303,7 +4139,6 @@ function lowerAssignment(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Handle ${lvaluePath.type} assignments`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: lvaluePath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
@@ -49,7 +49,6 @@ import {
|
||||
} from './ObjectShape';
|
||||
import {Scope as BabelScope, NodePath} from '@babel/traverse';
|
||||
import {TypeSchema} from './TypeSchema';
|
||||
import {FlowTypeEnv} from '../Flood/Types';
|
||||
|
||||
export const ReactElementSymbolSchema = z.object({
|
||||
elementSymbol: z.union([
|
||||
@@ -245,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.function().args(z.string())).default(null),
|
||||
enableNewMutationAliasingModel: z.boolean().default(true),
|
||||
|
||||
/**
|
||||
* Enables inference of optional dependency chains. Without this flag
|
||||
@@ -267,19 +265,21 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* {
|
||||
* module: 'react',
|
||||
* imported: 'useEffect',
|
||||
* autodepsIndex: 1,
|
||||
* numRequiredArgs: 1,
|
||||
* },{
|
||||
* module: 'MyExperimentalEffectHooks',
|
||||
* imported: 'useExperimentalEffect',
|
||||
* autodepsIndex: 2,
|
||||
* numRequiredArgs: 2,
|
||||
* },
|
||||
* ]
|
||||
* would insert dependencies for calls of `useEffect` imported from `react` and calls of
|
||||
* useExperimentalEffect` from `MyExperimentalEffectHooks`.
|
||||
*
|
||||
* `autodepsIndex` tells the compiler which index we expect the AUTODEPS to appear in.
|
||||
* With the configuration above, we'd insert dependencies for `useEffect` if it has two
|
||||
* arguments, and the second is AUTODEPS.
|
||||
* `numRequiredArgs` tells the compiler the amount of arguments required to append a dependency
|
||||
* array to the end of the call. With the configuration above, we'd insert dependencies for
|
||||
* `useEffect` if it is only given a single argument and it would be appended to the argument list.
|
||||
*
|
||||
* numRequiredArgs must always be greater than 0, otherwise there is no function to analyze for dependencies
|
||||
*
|
||||
* Still experimental.
|
||||
*/
|
||||
@@ -288,7 +288,7 @@ export const EnvironmentConfigSchema = z.object({
|
||||
z.array(
|
||||
z.object({
|
||||
function: ExternalFunctionSchema,
|
||||
autodepsIndex: z.number().min(1, 'autodepsIndex must be > 0'),
|
||||
numRequiredArgs: z.number().min(1, 'numRequiredArgs must be > 0'),
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -320,16 +320,10 @@ export const EnvironmentConfigSchema = z.object({
|
||||
validateNoSetStateInRender: z.boolean().default(true),
|
||||
|
||||
/**
|
||||
* Validates that setState is not called synchronously within an effect (useEffect and friends).
|
||||
* Validates that setState is not called directly within a passive effect (useEffect).
|
||||
* Scheduling a setState (with an event listener, subscription, etc) is valid.
|
||||
*/
|
||||
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),
|
||||
validateNoSetStateInPassiveEffects: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Validates against creating JSX within a try block and recommends using an error boundary
|
||||
@@ -616,7 +610,7 @@ export const EnvironmentConfigSchema = z.object({
|
||||
*
|
||||
* Here the variables `ref` and `myRef` will be typed as Refs.
|
||||
*/
|
||||
enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(true),
|
||||
enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(false),
|
||||
|
||||
/*
|
||||
* If specified a value, the compiler lowers any calls to `useContext` to use
|
||||
@@ -639,24 +633,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(false),
|
||||
|
||||
/**
|
||||
* Validates that Components/Hooks are always defined at module level. This prevents scope
|
||||
* reference errors that occur when the compiler attempts to optimize the nested component/hook
|
||||
* while its parent function remains uncompiled.
|
||||
*/
|
||||
validateNoDynamicallyCreatedComponentsOrHooks: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
|
||||
@@ -706,8 +682,6 @@ export class Environment {
|
||||
#hoistedIdentifiers: Set<t.Identifier>;
|
||||
parentFunction: NodePath<t.Function>;
|
||||
|
||||
#flowTypeEnvironment: FlowTypeEnv | null;
|
||||
|
||||
constructor(
|
||||
scope: BabelScope,
|
||||
fnType: ReactFunctionType,
|
||||
@@ -776,26 +750,6 @@ export class Environment {
|
||||
this.parentFunction = parentFunction;
|
||||
this.#contextIdentifiers = contextIdentifiers;
|
||||
this.#hoistedIdentifiers = new Set();
|
||||
|
||||
if (config.flowTypeProvider != null) {
|
||||
this.#flowTypeEnvironment = new FlowTypeEnv();
|
||||
CompilerError.invariant(code != null, {
|
||||
reason:
|
||||
'Expected Environment to be initialized with source code when a Flow type provider is specified',
|
||||
loc: null,
|
||||
});
|
||||
this.#flowTypeEnvironment.init(this, code);
|
||||
} else {
|
||||
this.#flowTypeEnvironment = null;
|
||||
}
|
||||
}
|
||||
|
||||
get typeContext(): FlowTypeEnv {
|
||||
CompilerError.invariant(this.#flowTypeEnvironment != null, {
|
||||
reason: 'Flow type environment not initialized',
|
||||
loc: null,
|
||||
});
|
||||
return this.#flowTypeEnvironment;
|
||||
}
|
||||
|
||||
get isInferredMemoEnabled(): boolean {
|
||||
|
||||
@@ -9,7 +9,6 @@ import {Effect, ValueKind, ValueReason} from './HIR';
|
||||
import {
|
||||
BUILTIN_SHAPES,
|
||||
BuiltInArrayId,
|
||||
BuiltInAutodepsId,
|
||||
BuiltInFireFunctionId,
|
||||
BuiltInFireId,
|
||||
BuiltInMapId,
|
||||
@@ -114,99 +113,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',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
]),
|
||||
],
|
||||
[
|
||||
@@ -874,7 +780,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
BuiltInUseEffectEventId,
|
||||
),
|
||||
],
|
||||
['AUTODEPS', addObject(DEFAULT_SHAPES, BuiltInAutodepsId, [])],
|
||||
];
|
||||
|
||||
TYPED_GLOBALS.push(
|
||||
|
||||
@@ -7,14 +7,13 @@
|
||||
|
||||
import {BindingKind} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import {CompilerError} 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';
|
||||
import type {AliasingEffect} from '../Inference/AliasingEffects';
|
||||
import {isReservedWord} from '../Utils/Keyword';
|
||||
|
||||
/*
|
||||
* *******************************************************************************************
|
||||
@@ -282,13 +281,30 @@ export type HIRFunction = {
|
||||
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
|
||||
@@ -430,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;
|
||||
@@ -1304,21 +1308,12 @@ export function forkTemporaryIdentifier(
|
||||
* original source code.
|
||||
*/
|
||||
export function makeIdentifierName(name: string): ValidatedIdentifier {
|
||||
if (isReservedWord(name)) {
|
||||
CompilerError.throwInvalidJS({
|
||||
reason: 'Expected a non-reserved identifier name',
|
||||
loc: GeneratedSource,
|
||||
description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`,
|
||||
suggestions: null,
|
||||
});
|
||||
} else {
|
||||
CompilerError.invariant(t.isValidIdentifier(name), {
|
||||
reason: `Expected a valid identifier name`,
|
||||
loc: GeneratedSource,
|
||||
description: `\`${name}\` is not a valid JavaScript identifier`,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
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,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import {Binding, NodePath} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import {CompilerError, ErrorCategory, ErrorSeverity} from '../CompilerError';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {Environment} from './Environment';
|
||||
import {
|
||||
BasicBlock,
|
||||
@@ -308,35 +308,9 @@ export default class HIRBuilder {
|
||||
|
||||
resolveBinding(node: t.Identifier): Identifier {
|
||||
if (node.name === 'fbt') {
|
||||
CompilerError.throwDiagnostic({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.FBT,
|
||||
reason: 'Support local variables named `fbt`',
|
||||
description:
|
||||
'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
message: 'Rename to avoid conflict with fbt plugin',
|
||||
loc: node.loc ?? GeneratedSource,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (node.name === 'this') {
|
||||
CompilerError.throwDiagnostic({
|
||||
severity: ErrorSeverity.UnsupportedJS,
|
||||
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,
|
||||
},
|
||||
],
|
||||
CompilerError.throwTodo({
|
||||
reason: 'Support local variables named "fbt"',
|
||||
loc: node.loc ?? null,
|
||||
});
|
||||
}
|
||||
const originalName = node.name;
|
||||
|
||||
@@ -142,7 +142,6 @@ function parseAliasingSignatureConfig(
|
||||
const effects = typeConfig.effects.map(
|
||||
(effect: AliasingEffectConfig): AliasingEffect => {
|
||||
switch (effect.kind) {
|
||||
case 'ImmutableCapture':
|
||||
case 'CreateFrom':
|
||||
case 'Capture':
|
||||
case 'Alias':
|
||||
@@ -385,7 +384,6 @@ export const BuiltInFireId = 'BuiltInFire';
|
||||
export const BuiltInFireFunctionId = 'BuiltInFireFunction';
|
||||
export const BuiltInUseEffectEventId = 'BuiltInUseEffectEvent';
|
||||
export const BuiltinEffectEventId = 'BuiltInEffectEventFunction';
|
||||
export const BuiltInAutodepsId = 'BuiltInAutoDepsId';
|
||||
|
||||
// See getReanimatedModuleType() in Globals.ts — this is part of supporting Reanimated's ref-like types
|
||||
export const ReanimatedSharedValueId = 'ReanimatedSharedValueId';
|
||||
@@ -1212,8 +1210,6 @@ addObject(BUILTIN_SHAPES, BuiltInRefValueId, [
|
||||
['*', {kind: 'Object', shapeId: BuiltInRefValueId}],
|
||||
]);
|
||||
|
||||
addObject(BUILTIN_SHAPES, ReanimatedSharedValueId, []);
|
||||
|
||||
addFunction(
|
||||
BUILTIN_SHAPES,
|
||||
[],
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import generate from '@babel/generator';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {printReactiveScopeSummary} from '../ReactiveScopes/PrintReactiveFunction';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
@@ -215,7 +216,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) {
|
||||
@@ -465,7 +466,7 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
|
||||
break;
|
||||
}
|
||||
case 'UnsupportedNode': {
|
||||
value = `UnsupportedNode ${instrValue.node.type}`;
|
||||
value = `UnsupportedNode(${generate(instrValue.node).code})`;
|
||||
break;
|
||||
}
|
||||
case 'LoadLocal': {
|
||||
@@ -554,11 +555,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': {
|
||||
@@ -880,8 +893,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}`;
|
||||
}
|
||||
@@ -932,10 +944,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)}`;
|
||||
@@ -984,7 +993,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)}`;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -345,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,
|
||||
@@ -777,7 +732,6 @@ export function mapTerminalSuccessors(
|
||||
case 'return': {
|
||||
return {
|
||||
kind: 'return',
|
||||
returnVariant: terminal.returnVariant,
|
||||
loc: terminal.loc,
|
||||
value: terminal.value,
|
||||
id: makeInstructionId(0),
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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,8 +85,7 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
|
||||
case 'Assign':
|
||||
case 'Alias':
|
||||
case 'Capture':
|
||||
case 'CreateFrom':
|
||||
case 'MaybeAlias': {
|
||||
case 'CreateFrom': {
|
||||
capturedOrMutated.add(effect.from.identifier.id);
|
||||
break;
|
||||
}
|
||||
@@ -125,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;
|
||||
}
|
||||
|
||||
@@ -5,13 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorSeverity,
|
||||
SourceLocation,
|
||||
} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
import {
|
||||
CallExpression,
|
||||
Effect,
|
||||
@@ -36,7 +30,6 @@ import {
|
||||
makeInstructionId,
|
||||
} from '../HIR';
|
||||
import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
type ManualMemoCallee = {
|
||||
kind: 'useMemo' | 'useCallback';
|
||||
@@ -290,45 +283,26 @@ function extractManualMemoizationArgs(
|
||||
instr: TInstruction<CallExpression> | TInstruction<MethodCall>,
|
||||
kind: 'useCallback' | 'useMemo',
|
||||
sidemap: IdentifierSidemap,
|
||||
errors: CompilerError,
|
||||
): {
|
||||
fnPlace: Place | null;
|
||||
fnPlace: Place;
|
||||
depsList: Array<ManualMemoDependency> | null;
|
||||
} {
|
||||
const [fnPlace, depsListPlace] = instr.value.args as Array<
|
||||
Place | SpreadPattern | undefined
|
||||
>;
|
||||
if (fnPlace == null) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: `Expected a callback function to be passed to ${kind}`,
|
||||
description: `Expected a callback function to be passed to ${kind}`,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: instr.value.loc,
|
||||
message: `Expected a callback function to be passed to ${kind}`,
|
||||
}),
|
||||
);
|
||||
return {fnPlace: null, depsList: null};
|
||||
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') {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: `Unexpected spread argument to ${kind}`,
|
||||
description: `Unexpected spread argument to ${kind}`,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: instr.value.loc,
|
||||
message: `Unexpected spread argument to ${kind}`,
|
||||
}),
|
||||
);
|
||||
return {fnPlace: null, depsList: null};
|
||||
CompilerError.throwInvalidReact({
|
||||
reason: `Unexpected spread argument to ${kind}`,
|
||||
loc: instr.value.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
let depsList: Array<ManualMemoDependency> | null = null;
|
||||
if (depsListPlace != null) {
|
||||
@@ -336,42 +310,23 @@ function extractManualMemoizationArgs(
|
||||
depsListPlace.identifier.id,
|
||||
);
|
||||
if (maybeDepsList == null) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
description: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: depsListPlace.loc,
|
||||
message: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
}),
|
||||
);
|
||||
return {fnPlace, depsList: null};
|
||||
CompilerError.throwInvalidReact({
|
||||
reason: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
suggestions: null,
|
||||
loc: depsListPlace.loc,
|
||||
});
|
||||
}
|
||||
depsList = [];
|
||||
for (const dep of maybeDepsList) {
|
||||
depsList = maybeDepsList.map(dep => {
|
||||
const maybeDep = sidemap.maybeDeps.get(dep.identifier.id);
|
||||
if (maybeDep == null) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
|
||||
description: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
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);
|
||||
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,
|
||||
@@ -386,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 ||
|
||||
@@ -440,48 +389,7 @@ export function dropManualMemoization(
|
||||
instr as TInstruction<CallExpression> | TInstruction<MethodCall>,
|
||||
manualMemo.kind,
|
||||
sidemap,
|
||||
errors,
|
||||
);
|
||||
|
||||
if (fnPlace == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bailout on void return useMemos. This is an anti-pattern where code might be using
|
||||
* useMemo like useEffect: running arbirtary side-effects synced to changes in specific
|
||||
* values.
|
||||
*/
|
||||
if (
|
||||
func.env.config.validateNoVoidUseMemo &&
|
||||
manualMemo.kind === 'useMemo'
|
||||
) {
|
||||
const funcToCheck = sidemap.functions.get(
|
||||
fnPlace.identifier.id,
|
||||
)?.value;
|
||||
if (funcToCheck !== undefined && funcToCheck.loweredFunc.func) {
|
||||
if (!hasNonVoidReturn(funcToCheck.loweredFunc.func)) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: 'useMemo() callbacks must return a value',
|
||||
description: `This ${
|
||||
manualMemo.loadInstr.value.kind === 'PropertyLoad'
|
||||
? 'React.useMemo'
|
||||
: 'useMemo'
|
||||
} callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects.`,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: instr.value.loc,
|
||||
message: 'useMemo() callbacks must return a value',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
instr.value = getManualMemoizationReplacement(
|
||||
fnPlace,
|
||||
instr.value.loc,
|
||||
@@ -502,20 +410,11 @@ export function dropManualMemoization(
|
||||
* is rare and likely sketchy.
|
||||
*/
|
||||
if (!sidemap.functions.has(fnPlace.identifier.id)) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: `Expected the first argument to be an inline function expression`,
|
||||
description: `Expected the first argument to be an inline function expression`,
|
||||
suggestions: [],
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: fnPlace.loc,
|
||||
message: `Expected the first argument to be an inline function expression`,
|
||||
}),
|
||||
);
|
||||
continue;
|
||||
CompilerError.throwInvalidReact({
|
||||
reason: `Expected the first argument to be an inline function expression`,
|
||||
suggestions: [],
|
||||
loc: fnPlace.loc,
|
||||
});
|
||||
}
|
||||
const memoDecl: Place =
|
||||
manualMemo.kind === 'useMemo'
|
||||
@@ -587,8 +486,6 @@ export function dropManualMemoization(
|
||||
markInstructionIds(func.body);
|
||||
}
|
||||
}
|
||||
|
||||
return errors.asResult();
|
||||
}
|
||||
|
||||
function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
|
||||
@@ -633,17 +530,3 @@ function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
|
||||
}
|
||||
return optionals;
|
||||
}
|
||||
|
||||
function hasNonVoidReturn(func: HIRFunction): boolean {
|
||||
for (const [, block] of func.body.blocks) {
|
||||
if (block.terminal.kind === 'return') {
|
||||
if (
|
||||
block.terminal.returnVariant === 'Explicit' ||
|
||||
block.terminal.returnVariant === 'Implicit'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -57,8 +57,6 @@ import {
|
||||
} from '../HIR/visitors';
|
||||
import {empty} from '../Utils/Stack';
|
||||
import {getOrInsertWith} from '../Utils/utils';
|
||||
import {deadCodeElimination} from '../Optimization';
|
||||
import {BuiltInAutodepsId} from '../HIR/ObjectShape';
|
||||
|
||||
/**
|
||||
* Infers reactive dependencies captured by useEffect lambdas and adds them as
|
||||
@@ -79,7 +77,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
);
|
||||
moduleTargets.set(
|
||||
effectTarget.function.importSpecifierName,
|
||||
effectTarget.autodepsIndex,
|
||||
effectTarget.numRequiredArgs,
|
||||
);
|
||||
}
|
||||
const autodepFnLoads = new Map<IdentifierId, number>();
|
||||
@@ -137,6 +135,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
}
|
||||
} else if (value.kind === 'LoadGlobal') {
|
||||
loadGlobals.add(lvalue.identifier.id);
|
||||
|
||||
/*
|
||||
* TODO: Handle properties on default exports, like
|
||||
* import React from 'react';
|
||||
@@ -170,22 +169,8 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
) {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
|
||||
const autodepsArgIndex = value.args.findIndex(
|
||||
arg =>
|
||||
arg.kind === 'Identifier' &&
|
||||
arg.identifier.type.kind === 'Object' &&
|
||||
arg.identifier.type.shapeId === BuiltInAutodepsId,
|
||||
);
|
||||
const autodepsArgExpectedIndex = autodepFnLoads.get(
|
||||
callee.identifier.id,
|
||||
);
|
||||
|
||||
if (
|
||||
value.args.length > 0 &&
|
||||
autodepsArgExpectedIndex != null &&
|
||||
autodepsArgIndex === autodepsArgExpectedIndex &&
|
||||
autodepFnLoads.has(callee.identifier.id) &&
|
||||
value.args.length === autodepFnLoads.get(callee.identifier.id) &&
|
||||
value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
// We have a useEffect call with no deps array, so we need to infer the deps
|
||||
@@ -275,10 +260,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
effects: null,
|
||||
},
|
||||
});
|
||||
value.args[autodepsArgIndex] = {
|
||||
...depsPlace,
|
||||
effect: Effect.Freeze,
|
||||
};
|
||||
value.args.push({...depsPlace, effect: Effect.Freeze});
|
||||
fn.env.inferredEffectLocations.add(callee.loc);
|
||||
} else if (loadGlobals.has(value.args[0].identifier.id)) {
|
||||
// Global functions have no reactive dependencies, so we can insert an empty array
|
||||
@@ -293,10 +275,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
effects: null,
|
||||
},
|
||||
});
|
||||
value.args[autodepsArgIndex] = {
|
||||
...depsPlace,
|
||||
effect: Effect.Freeze,
|
||||
};
|
||||
value.args.push({...depsPlace, effect: Effect.Freeze});
|
||||
fn.env.inferredEffectLocations.add(callee.loc);
|
||||
}
|
||||
} else if (
|
||||
@@ -344,7 +323,6 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
// Renumber instructions and fix scope ranges
|
||||
markInstructionIds(fn.body);
|
||||
fixScopeAndIdentifierRanges(fn.body);
|
||||
deadCodeElimination(fn);
|
||||
|
||||
fn.env.hasInferredEffect = true;
|
||||
}
|
||||
@@ -430,7 +408,6 @@ function rewriteSplices(
|
||||
rewriteBlocks.push(currBlock);
|
||||
|
||||
let cursor = 0;
|
||||
|
||||
for (const rewrite of splices) {
|
||||
while (originalInstrs[cursor].id < rewrite.location) {
|
||||
CompilerError.invariant(
|
||||
@@ -452,7 +429,7 @@ function rewriteSplices(
|
||||
|
||||
if (rewrite.kind === 'instr') {
|
||||
currBlock.instructions.push(rewrite.value);
|
||||
} else if (rewrite.kind === 'block') {
|
||||
} else {
|
||||
const {entry, blocks} = rewrite.value;
|
||||
const entryBlock = blocks.get(entry)!;
|
||||
// splice in all instructions from the entry block
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
InstructionId,
|
||||
InstructionKind,
|
||||
isArrayType,
|
||||
isMapType,
|
||||
isRefOrRefValue,
|
||||
isSetType,
|
||||
makeInstructionId,
|
||||
Place,
|
||||
} from '../HIR/HIR';
|
||||
import {printPlace} from '../HIR/PrintHIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
/*
|
||||
* For each usage of a value in the given function, determines if the usage
|
||||
* may be succeeded by a mutable usage of that same value and if so updates
|
||||
* the usage to be mutable.
|
||||
*
|
||||
* Stated differently, this inference ensures that inferred capabilities of
|
||||
* each reference are as follows:
|
||||
* - freeze: the value is frozen at this point
|
||||
* - readonly: the value is not modified at this point *or any subsequent
|
||||
* point*
|
||||
* - mutable: the value is modified at this point *or some subsequent point*.
|
||||
*
|
||||
* Note that this refines the capabilities inferered by InferReferenceCapability,
|
||||
* which looks at individual references and not the lifetime of a value's mutability.
|
||||
*
|
||||
* == Algorithm
|
||||
*
|
||||
* TODO:
|
||||
* 1. Forward data-flow analysis to determine aliasing. Unlike InferReferenceCapability
|
||||
* which only tracks aliasing of top-level variables (`y = x`), this analysis needs
|
||||
* to know if a value is aliased anywhere (`y.x = x`). The forward data flow tracks
|
||||
* all possible locations which may have aliased a value. The concrete result is
|
||||
* a mapping of each Place to the set of possibly-mutable values it may alias.
|
||||
*
|
||||
* ```
|
||||
* const x = []; // {x: v0; v0: mutable []}
|
||||
* const y = {}; // {x: v0, y: v1; v0: mutable [], v1: mutable []}
|
||||
* y.x = x; // {x: v0, y: v1; v0: mutable [v1], v1: mutable [v0]}
|
||||
* read(x); // {x: v0, y: v1; v0: mutable [v1], v1: mutable [v0]}
|
||||
* mutate(y); // can infer that y mutates v0 and v1
|
||||
* ```
|
||||
*
|
||||
* DONE:
|
||||
* 2. Forward data-flow analysis to compute mutability liveness. Walk forwards over
|
||||
* the CFG and track which values are mutated in a successor.
|
||||
*
|
||||
* ```
|
||||
* mutate(y); // mutable y => v0, v1 mutated
|
||||
* read(x); // x maps to v0, v1, those are in the mutated-later set, so x is mutable here
|
||||
* ...
|
||||
* ```
|
||||
*/
|
||||
|
||||
function infer(place: Place, instrId: InstructionId): void {
|
||||
if (!isRefOrRefValue(place.identifier)) {
|
||||
place.identifier.mutableRange.end = makeInstructionId(instrId + 1);
|
||||
}
|
||||
}
|
||||
|
||||
function inferPlace(
|
||||
place: Place,
|
||||
instrId: InstructionId,
|
||||
inferMutableRangeForStores: boolean,
|
||||
): void {
|
||||
switch (place.effect) {
|
||||
case Effect.Unknown: {
|
||||
throw new Error(`Found an unknown place ${printPlace(place)}}!`);
|
||||
}
|
||||
case Effect.Capture:
|
||||
case Effect.Read:
|
||||
case Effect.Freeze:
|
||||
return;
|
||||
case Effect.Store:
|
||||
if (inferMutableRangeForStores) {
|
||||
infer(place, instrId);
|
||||
}
|
||||
return;
|
||||
case Effect.ConditionallyMutateIterator: {
|
||||
const identifier = place.identifier;
|
||||
if (
|
||||
!isArrayType(identifier) &&
|
||||
!isSetType(identifier) &&
|
||||
!isMapType(identifier)
|
||||
) {
|
||||
infer(place, instrId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case Effect.ConditionallyMutate:
|
||||
case Effect.Mutate: {
|
||||
infer(place, instrId);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
assertExhaustive(place.effect, `Unexpected ${printPlace(place)} effect`);
|
||||
}
|
||||
}
|
||||
|
||||
export function inferMutableLifetimes(
|
||||
func: HIRFunction,
|
||||
inferMutableRangeForStores: boolean,
|
||||
): void {
|
||||
/*
|
||||
* Context variables only appear to mutate where they are assigned, but we need
|
||||
* to force their range to start at their declaration. Track the declaring instruction
|
||||
* id so that the ranges can be extended if/when they are reassigned
|
||||
*/
|
||||
const contextVariableDeclarationInstructions = new Map<
|
||||
Identifier,
|
||||
InstructionId
|
||||
>();
|
||||
for (const [_, block] of func.body.blocks) {
|
||||
for (const phi of block.phis) {
|
||||
const isPhiMutatedAfterCreation: boolean =
|
||||
phi.place.identifier.mutableRange.end >
|
||||
(block.instructions.at(0)?.id ?? block.terminal.id);
|
||||
if (
|
||||
inferMutableRangeForStores &&
|
||||
isPhiMutatedAfterCreation &&
|
||||
phi.place.identifier.mutableRange.start === 0
|
||||
) {
|
||||
for (const [, operand] of phi.operands) {
|
||||
if (phi.place.identifier.mutableRange.start === 0) {
|
||||
phi.place.identifier.mutableRange.start =
|
||||
operand.identifier.mutableRange.start;
|
||||
} else {
|
||||
phi.place.identifier.mutableRange.start = makeInstructionId(
|
||||
Math.min(
|
||||
phi.place.identifier.mutableRange.start,
|
||||
operand.identifier.mutableRange.start,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
for (const operand of eachInstructionLValue(instr)) {
|
||||
const lvalueId = operand.identifier;
|
||||
|
||||
/*
|
||||
* lvalue start being mutable when they're initially assigned a
|
||||
* value.
|
||||
*/
|
||||
lvalueId.mutableRange.start = instr.id;
|
||||
|
||||
/*
|
||||
* Let's be optimistic and assume this lvalue is not mutable by
|
||||
* default.
|
||||
*/
|
||||
lvalueId.mutableRange.end = makeInstructionId(instr.id + 1);
|
||||
}
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
inferPlace(operand, instr.id, inferMutableRangeForStores);
|
||||
}
|
||||
|
||||
if (
|
||||
instr.value.kind === 'DeclareContext' ||
|
||||
(instr.value.kind === 'StoreContext' &&
|
||||
instr.value.lvalue.kind !== InstructionKind.Reassign &&
|
||||
!contextVariableDeclarationInstructions.has(
|
||||
instr.value.lvalue.place.identifier,
|
||||
))
|
||||
) {
|
||||
/**
|
||||
* Save declarations of context variables if they hasn't already been
|
||||
* declared (due to hoisted declarations).
|
||||
*/
|
||||
contextVariableDeclarationInstructions.set(
|
||||
instr.value.lvalue.place.identifier,
|
||||
instr.id,
|
||||
);
|
||||
} else if (instr.value.kind === 'StoreContext') {
|
||||
/*
|
||||
* Else this is a reassignment, extend the range from the declaration (if present).
|
||||
* Note that declarations may not be present for context variables that are reassigned
|
||||
* within a function expression before (or without) a read of the same variable
|
||||
*/
|
||||
const declaration = contextVariableDeclarationInstructions.get(
|
||||
instr.value.lvalue.place.identifier,
|
||||
);
|
||||
if (
|
||||
declaration != null &&
|
||||
!isRefOrRefValue(instr.value.lvalue.place.identifier)
|
||||
) {
|
||||
const range = instr.value.lvalue.place.identifier.mutableRange;
|
||||
if (range.start === 0) {
|
||||
range.start = declaration;
|
||||
} else {
|
||||
range.start = makeInstructionId(Math.min(range.start, declaration));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
inferPlace(operand, block.terminal.id, inferMutableRangeForStores);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {HIRFunction, Identifier} from '../HIR/HIR';
|
||||
import {inferAliasForUncalledFunctions} from './InerAliasForUncalledFunctions';
|
||||
import {inferAliases} from './InferAlias';
|
||||
import {inferAliasForPhis} from './InferAliasForPhis';
|
||||
import {inferAliasForStores} from './InferAliasForStores';
|
||||
import {inferMutableLifetimes} from './InferMutableLifetimes';
|
||||
import {inferMutableRangesForAlias} from './InferMutableRangesForAlias';
|
||||
import {inferTryCatchAliases} from './InferTryCatchAliases';
|
||||
|
||||
export function inferMutableRanges(ir: HIRFunction): void {
|
||||
// Infer mutable ranges for non fields
|
||||
inferMutableLifetimes(ir, false);
|
||||
|
||||
// Calculate aliases
|
||||
const aliases = inferAliases(ir);
|
||||
/*
|
||||
* Calculate aliases for try/catch, where any value created
|
||||
* in the try block could be aliased to the catch param
|
||||
*/
|
||||
inferTryCatchAliases(ir, aliases);
|
||||
|
||||
/*
|
||||
* Eagerly canonicalize so that if nothing changes we can bail out
|
||||
* after a single iteration
|
||||
*/
|
||||
let prevAliases: Map<Identifier, Identifier> = aliases.canonicalize();
|
||||
while (true) {
|
||||
// Infer mutable ranges for aliases that are not fields
|
||||
inferMutableRangesForAlias(ir, aliases);
|
||||
|
||||
// Update aliasing information of fields
|
||||
inferAliasForStores(ir, aliases);
|
||||
|
||||
// Update aliasing information of phis
|
||||
inferAliasForPhis(ir, aliases);
|
||||
|
||||
const nextAliases = aliases.canonicalize();
|
||||
if (areEqualMaps(prevAliases, nextAliases)) {
|
||||
break;
|
||||
}
|
||||
prevAliases = nextAliases;
|
||||
}
|
||||
|
||||
// Re-infer mutable ranges for all values
|
||||
inferMutableLifetimes(ir, true);
|
||||
|
||||
/**
|
||||
* The second inferMutableLifetimes() call updates mutable ranges
|
||||
* of values to account for Store effects. Now we need to update
|
||||
* all aliases of such values to extend their ranges as well. Note
|
||||
* that the store only mutates the the directly aliased value and
|
||||
* not any of its inner captured references. For example:
|
||||
*
|
||||
* ```
|
||||
* let y;
|
||||
* if (cond) {
|
||||
* y = [];
|
||||
* } else {
|
||||
* y = [{}];
|
||||
* }
|
||||
* y.push(z);
|
||||
* ```
|
||||
*
|
||||
* The Store effect from the `y.push` modifies the values that `y`
|
||||
* directly aliases - the two arrays from the if/else branches -
|
||||
* but does not modify values that `y` "contains" such as the
|
||||
* object literal or `z`.
|
||||
*/
|
||||
prevAliases = aliases.canonicalize();
|
||||
while (true) {
|
||||
inferMutableRangesForAlias(ir, aliases);
|
||||
inferAliasForPhis(ir, aliases);
|
||||
inferAliasForUncalledFunctions(ir, aliases);
|
||||
const nextAliases = aliases.canonicalize();
|
||||
if (areEqualMaps(prevAliases, nextAliases)) {
|
||||
break;
|
||||
}
|
||||
prevAliases = nextAliases;
|
||||
}
|
||||
}
|
||||
|
||||
function areEqualMaps<T, U>(a: Map<T, U>, b: Map<T, U>): boolean {
|
||||
if (a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
for (const [key, value] of a) {
|
||||
if (!b.has(key)) {
|
||||
return false;
|
||||
}
|
||||
if (b.get(key) !== value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
InstructionId,
|
||||
isRefOrRefValue,
|
||||
} from '../HIR/HIR';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
export function inferMutableRangesForAlias(
|
||||
_fn: HIRFunction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
const aliasSets = aliases.buildSets();
|
||||
for (const aliasSet of aliasSets) {
|
||||
/*
|
||||
* Update mutableRange.end only if the identifiers have actually been
|
||||
* mutated.
|
||||
*/
|
||||
const mutatingIdentifiers = [...aliasSet].filter(
|
||||
id =>
|
||||
id.mutableRange.end - id.mutableRange.start > 1 && !isRefOrRefValue(id),
|
||||
);
|
||||
|
||||
if (mutatingIdentifiers.length > 0) {
|
||||
// Find final instruction which mutates this alias set.
|
||||
let lastMutatingInstructionId = 0;
|
||||
for (const id of mutatingIdentifiers) {
|
||||
if (id.mutableRange.end > lastMutatingInstructionId) {
|
||||
lastMutatingInstructionId = id.mutableRange.end;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Update mutableRange.end for all aliases in this set ending before the
|
||||
* last mutation.
|
||||
*/
|
||||
for (const alias of aliasSet) {
|
||||
if (
|
||||
alias.mutableRange.end < lastMutatingInstructionId &&
|
||||
!isRefOrRefValue(alias)
|
||||
) {
|
||||
alias.mutableRange.end = lastMutatingInstructionId as InstructionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
Effect,
|
||||
ErrorSeverity,
|
||||
@@ -19,14 +18,13 @@ import {
|
||||
DeclarationId,
|
||||
Environment,
|
||||
FunctionExpression,
|
||||
GeneratedSource,
|
||||
HIRFunction,
|
||||
Hole,
|
||||
IdentifierId,
|
||||
Instruction,
|
||||
InstructionKind,
|
||||
InstructionValue,
|
||||
isArrayType,
|
||||
isJsxType,
|
||||
isMapType,
|
||||
isPrimitiveType,
|
||||
isRefOrRefValue,
|
||||
@@ -35,16 +33,21 @@ import {
|
||||
Phi,
|
||||
Place,
|
||||
SpreadPattern,
|
||||
Type,
|
||||
ValueReason,
|
||||
} from '../HIR';
|
||||
import {
|
||||
eachInstructionValueLValue,
|
||||
eachInstructionValueOperand,
|
||||
eachPatternItem,
|
||||
eachTerminalOperand,
|
||||
eachTerminalSuccessor,
|
||||
} from '../HIR/visitors';
|
||||
import {Ok, Result} from '../Utils/Result';
|
||||
import {
|
||||
getArgumentEffect,
|
||||
getFunctionCallSignature,
|
||||
isKnownMutableEffect,
|
||||
mergeValueKinds,
|
||||
} from './InferReferenceEffects';
|
||||
import {
|
||||
assertExhaustive,
|
||||
getOrInsertDefault,
|
||||
@@ -56,19 +59,15 @@ import {
|
||||
printAliasingSignature,
|
||||
printIdentifier,
|
||||
printInstruction,
|
||||
printInstructionValue,
|
||||
printPlace,
|
||||
printSourceLocation,
|
||||
} from '../HIR/PrintHIR';
|
||||
import {FunctionSignature} from '../HIR/ObjectShape';
|
||||
import {getWriteErrorReason} from './InferFunctionEffects';
|
||||
import prettyFormat from 'pretty-format';
|
||||
import {createTemporaryPlace} from '../HIR/HIRBuilder';
|
||||
import {
|
||||
AliasingEffect,
|
||||
AliasingSignature,
|
||||
hashEffect,
|
||||
MutationReason,
|
||||
} from './AliasingEffects';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {AliasingEffect, AliasingSignature, hashEffect} from './AliasingEffects';
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
@@ -106,11 +105,11 @@ export function inferMutationAliasingEffects(
|
||||
const statesByBlock: Map<BlockId, InferenceState> = new Map();
|
||||
|
||||
for (const ref of fn.context) {
|
||||
const value: AliasingEffect = {
|
||||
kind: 'Create',
|
||||
into: ref,
|
||||
value: ValueKind.Context,
|
||||
reason: ValueReason.Other,
|
||||
// TODO: using InstructionValue as a bit of a hack, but it's pragmatic
|
||||
const value: InstructionValue = {
|
||||
kind: 'ObjectExpression',
|
||||
properties: [],
|
||||
loc: ref.loc,
|
||||
};
|
||||
initialState.initialize(value, {
|
||||
kind: ValueKind.Context,
|
||||
@@ -143,11 +142,10 @@ export function inferMutationAliasingEffects(
|
||||
}
|
||||
if (ref != null) {
|
||||
const place = ref.kind === 'Identifier' ? ref : ref.place;
|
||||
const value: AliasingEffect = {
|
||||
kind: 'Create',
|
||||
into: place,
|
||||
value: ValueKind.Mutable,
|
||||
reason: ValueReason.Other,
|
||||
const value: InstructionValue = {
|
||||
kind: 'ObjectExpression',
|
||||
properties: [],
|
||||
loc: place.loc,
|
||||
};
|
||||
initialState.initialize(value, {
|
||||
kind: ValueKind.Mutable,
|
||||
@@ -264,6 +262,8 @@ function findHoistedContextDeclarations(
|
||||
class Context {
|
||||
internedEffects: Map<string, AliasingEffect> = new Map();
|
||||
instructionSignatureCache: Map<Instruction, InstructionSignature> = new Map();
|
||||
effectInstructionValueCache: Map<AliasingEffect, InstructionValue> =
|
||||
new Map();
|
||||
applySignatureCache: Map<
|
||||
AliasingSignature,
|
||||
Map<AliasingEffect, Array<AliasingEffect> | null>
|
||||
@@ -315,11 +315,10 @@ function inferParam(
|
||||
paramKind: AbstractValue,
|
||||
): void {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
const value: AliasingEffect = {
|
||||
kind: 'Create',
|
||||
into: place,
|
||||
value: paramKind.kind,
|
||||
reason: ValueReason.Other,
|
||||
const value: InstructionValue = {
|
||||
kind: 'Primitive',
|
||||
loc: place.loc,
|
||||
value: undefined,
|
||||
};
|
||||
initialState.initialize(value, paramKind);
|
||||
initialState.define(place, value);
|
||||
@@ -445,35 +444,22 @@ function applySignature(
|
||||
const reason = getWriteErrorReason({
|
||||
kind: value.kind,
|
||||
reason: value.reason,
|
||||
context: new Set(),
|
||||
});
|
||||
const variable =
|
||||
effect.value.identifier.name !== null &&
|
||||
effect.value.identifier.name.kind === 'named'
|
||||
? `\`${effect.value.identifier.name.value}\``
|
||||
: 'value';
|
||||
const diagnostic = CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Immutability,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'This value cannot be modified',
|
||||
description: `${reason}.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `${variable} cannot be modified`,
|
||||
});
|
||||
if (
|
||||
effect.kind === 'Mutate' &&
|
||||
effect.reason?.kind === 'AssignCurrentProperty'
|
||||
) {
|
||||
diagnostic.withDetail({
|
||||
kind: 'hint',
|
||||
message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`,
|
||||
});
|
||||
}
|
||||
effects.push({
|
||||
kind: 'MutateFrozen',
|
||||
place: effect.value,
|
||||
error: diagnostic,
|
||||
error: {
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason,
|
||||
description:
|
||||
effect.value.identifier.name !== null &&
|
||||
effect.value.identifier.name.kind === 'named'
|
||||
? `Found mutation of \`${effect.value.identifier.name.value}\``
|
||||
: null,
|
||||
loc: effect.value.loc,
|
||||
suggestions: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -540,11 +526,20 @@ function applyEffect(
|
||||
});
|
||||
initialized.add(effect.into.identifier.id);
|
||||
|
||||
state.initialize(effect, {
|
||||
let value = context.effectInstructionValueCache.get(effect);
|
||||
if (value == null) {
|
||||
value = {
|
||||
kind: 'ObjectExpression',
|
||||
properties: [],
|
||||
loc: effect.into.loc,
|
||||
};
|
||||
context.effectInstructionValueCache.set(effect, value);
|
||||
}
|
||||
state.initialize(value, {
|
||||
kind: effect.value,
|
||||
reason: new Set([effect.reason]),
|
||||
});
|
||||
state.define(effect.into, effect);
|
||||
state.define(effect.into, value);
|
||||
effects.push(effect);
|
||||
break;
|
||||
}
|
||||
@@ -571,11 +566,20 @@ function applyEffect(
|
||||
initialized.add(effect.into.identifier.id);
|
||||
|
||||
const fromValue = state.kind(effect.from);
|
||||
state.initialize(effect, {
|
||||
let value = context.effectInstructionValueCache.get(effect);
|
||||
if (value == null) {
|
||||
value = {
|
||||
kind: 'ObjectExpression',
|
||||
properties: [],
|
||||
loc: effect.into.loc,
|
||||
};
|
||||
context.effectInstructionValueCache.set(effect, value);
|
||||
}
|
||||
state.initialize(value, {
|
||||
kind: fromValue.kind,
|
||||
reason: new Set(fromValue.reason),
|
||||
});
|
||||
state.define(effect.into, effect);
|
||||
state.define(effect.into, value);
|
||||
switch (fromValue.kind) {
|
||||
case ValueKind.Primitive:
|
||||
case ValueKind.Global: {
|
||||
@@ -663,11 +667,11 @@ function applyEffect(
|
||||
operand.effect = Effect.Read;
|
||||
}
|
||||
}
|
||||
state.initialize(effect, {
|
||||
state.initialize(effect.function, {
|
||||
kind: isMutable ? ValueKind.Mutable : ValueKind.Frozen,
|
||||
reason: new Set([]),
|
||||
});
|
||||
state.define(effect.into, effect);
|
||||
state.define(effect.into, effect.function);
|
||||
for (const capture of effect.captures) {
|
||||
applyEffect(
|
||||
context,
|
||||
@@ -683,7 +687,6 @@ function applyEffect(
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'MaybeAlias':
|
||||
case 'Alias':
|
||||
case 'Capture': {
|
||||
CompilerError.invariant(
|
||||
@@ -773,20 +776,38 @@ function applyEffect(
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
state.initialize(effect, {
|
||||
let value = context.effectInstructionValueCache.get(effect);
|
||||
if (value == null) {
|
||||
value = {
|
||||
kind: 'Primitive',
|
||||
value: undefined,
|
||||
loc: effect.from.loc,
|
||||
};
|
||||
context.effectInstructionValueCache.set(effect, value);
|
||||
}
|
||||
state.initialize(value, {
|
||||
kind: fromKind,
|
||||
reason: new Set(fromValue.reason),
|
||||
});
|
||||
state.define(effect.into, effect);
|
||||
state.define(effect.into, value);
|
||||
break;
|
||||
}
|
||||
case ValueKind.Global:
|
||||
case ValueKind.Primitive: {
|
||||
state.initialize(effect, {
|
||||
let value = context.effectInstructionValueCache.get(effect);
|
||||
if (value == null) {
|
||||
value = {
|
||||
kind: 'Primitive',
|
||||
value: undefined,
|
||||
loc: effect.from.loc,
|
||||
};
|
||||
context.effectInstructionValueCache.set(effect, value);
|
||||
}
|
||||
state.initialize(value, {
|
||||
kind: fromKind,
|
||||
reason: new Set(fromValue.reason),
|
||||
});
|
||||
state.define(effect.into, effect);
|
||||
state.define(effect.into, value);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
@@ -801,15 +822,14 @@ function applyEffect(
|
||||
const functionValues = state.values(effect.function);
|
||||
if (
|
||||
functionValues.length === 1 &&
|
||||
functionValues[0].kind === 'CreateFunction' &&
|
||||
functionValues[0].function.kind === 'FunctionExpression' &&
|
||||
functionValues[0].function.loweredFunc.func.aliasingEffects != null
|
||||
functionValues[0].kind === 'FunctionExpression' &&
|
||||
functionValues[0].loweredFunc.func.aliasingEffects != null
|
||||
) {
|
||||
/*
|
||||
* We're calling a locally declared function, we already know it's effects!
|
||||
* We just have to substitute in the args for the params
|
||||
*/
|
||||
const functionExpr = functionValues[0].function;
|
||||
const functionExpr = functionValues[0];
|
||||
let signature = context.functionSignatureCache.get(functionExpr);
|
||||
if (signature == null) {
|
||||
signature = buildSignatureFromFunctionExpression(
|
||||
@@ -931,7 +951,7 @@ function applyEffect(
|
||||
context,
|
||||
state,
|
||||
// OK: recording information flow
|
||||
{kind: 'MaybeAlias', from: operand, into: effect.into},
|
||||
{kind: 'Alias', from: operand, into: effect.into},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
@@ -993,32 +1013,33 @@ function applyEffect(
|
||||
effect.value.identifier.declarationId,
|
||||
)
|
||||
) {
|
||||
const variable =
|
||||
const description =
|
||||
effect.value.identifier.name !== null &&
|
||||
effect.value.identifier.name.kind === 'named'
|
||||
? `\`${effect.value.identifier.name.value}\``
|
||||
? `Variable \`${effect.value.identifier.name.value}\` is accessed before it is declared`
|
||||
: null;
|
||||
const hoistedAccess = context.hoistedContextDeclarations.get(
|
||||
effect.value.identifier.declarationId,
|
||||
);
|
||||
const diagnostic = CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Immutability,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'Cannot access variable before it is declared',
|
||||
description: `${variable ?? 'This variable'} is accessed before it is declared, which prevents the earlier access from updating when this value changes over time.`,
|
||||
});
|
||||
if (hoistedAccess != null && hoistedAccess.loc != effect.value.loc) {
|
||||
diagnostic.withDetail({
|
||||
kind: 'error',
|
||||
loc: hoistedAccess.loc,
|
||||
message: `${variable ?? 'variable'} accessed before it is declared`,
|
||||
});
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
{
|
||||
kind: 'MutateFrozen',
|
||||
place: effect.value,
|
||||
error: {
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: `This variable is accessed before it is declared, which may prevent it from updating as the assigned value changes over time`,
|
||||
description,
|
||||
loc: hoistedAccess.loc,
|
||||
suggestions: null,
|
||||
},
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
}
|
||||
diagnostic.withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `${variable ?? 'variable'} is declared here`,
|
||||
});
|
||||
|
||||
applyEffect(
|
||||
context,
|
||||
@@ -1026,7 +1047,13 @@ function applyEffect(
|
||||
{
|
||||
kind: 'MutateFrozen',
|
||||
place: effect.value,
|
||||
error: diagnostic,
|
||||
error: {
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: `This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time`,
|
||||
description,
|
||||
loc: effect.value.loc,
|
||||
suggestions: null,
|
||||
},
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
@@ -1035,31 +1062,13 @@ function applyEffect(
|
||||
const reason = getWriteErrorReason({
|
||||
kind: value.kind,
|
||||
reason: value.reason,
|
||||
context: new Set(),
|
||||
});
|
||||
const variable =
|
||||
const description =
|
||||
effect.value.identifier.name !== null &&
|
||||
effect.value.identifier.name.kind === 'named'
|
||||
? `\`${effect.value.identifier.name.value}\``
|
||||
: 'value';
|
||||
const diagnostic = CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Immutability,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'This value cannot be modified',
|
||||
description: `${reason}.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `${variable} cannot be modified`,
|
||||
});
|
||||
if (
|
||||
effect.kind === 'Mutate' &&
|
||||
effect.reason?.kind === 'AssignCurrentProperty'
|
||||
) {
|
||||
diagnostic.withDetail({
|
||||
kind: 'hint',
|
||||
message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`,
|
||||
});
|
||||
}
|
||||
? `Found mutation of \`${effect.value.identifier.name.value}\``
|
||||
: null;
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
@@ -1069,7 +1078,13 @@ function applyEffect(
|
||||
? 'MutateFrozen'
|
||||
: 'MutateGlobal',
|
||||
place: effect.value,
|
||||
error: diagnostic,
|
||||
error: {
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason,
|
||||
description,
|
||||
loc: effect.value.loc,
|
||||
suggestions: null,
|
||||
},
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
@@ -1099,19 +1114,19 @@ class InferenceState {
|
||||
#isFunctionExpression: boolean;
|
||||
|
||||
// The kind of each value, based on its allocation site
|
||||
#values: Map<AliasingEffect, AbstractValue>;
|
||||
#values: Map<InstructionValue, AbstractValue>;
|
||||
/*
|
||||
* The set of values pointed to by each identifier. This is a set
|
||||
* to accomodate phi points (where a variable may have different
|
||||
* values from different control flow paths).
|
||||
*/
|
||||
#variables: Map<IdentifierId, Set<AliasingEffect>>;
|
||||
#variables: Map<IdentifierId, Set<InstructionValue>>;
|
||||
|
||||
constructor(
|
||||
env: Environment,
|
||||
isFunctionExpression: boolean,
|
||||
values: Map<AliasingEffect, AbstractValue>,
|
||||
variables: Map<IdentifierId, Set<AliasingEffect>>,
|
||||
values: Map<InstructionValue, AbstractValue>,
|
||||
variables: Map<IdentifierId, Set<InstructionValue>>,
|
||||
) {
|
||||
this.env = env;
|
||||
this.#isFunctionExpression = isFunctionExpression;
|
||||
@@ -1131,11 +1146,18 @@ class InferenceState {
|
||||
}
|
||||
|
||||
// (Re)initializes a @param value with its default @param kind.
|
||||
initialize(value: AliasingEffect, kind: AbstractValue): void {
|
||||
initialize(value: InstructionValue, kind: AbstractValue): void {
|
||||
CompilerError.invariant(value.kind !== 'LoadLocal', {
|
||||
reason:
|
||||
'[InferMutationAliasingEffects] Expected all top-level identifiers to be defined as variables, not values',
|
||||
description: null,
|
||||
loc: value.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
this.#values.set(value, kind);
|
||||
}
|
||||
|
||||
values(place: Place): Array<AliasingEffect> {
|
||||
values(place: Place): Array<InstructionValue> {
|
||||
const values = this.#variables.get(place.identifier.id);
|
||||
CompilerError.invariant(values != null, {
|
||||
reason: `[InferMutationAliasingEffects] Expected value kind to be initialized`,
|
||||
@@ -1198,13 +1220,13 @@ class InferenceState {
|
||||
}
|
||||
|
||||
// Defines (initializing or updating) a variable with a specific kind of value.
|
||||
define(place: Place, value: AliasingEffect): void {
|
||||
define(place: Place, value: InstructionValue): void {
|
||||
CompilerError.invariant(this.#values.has(value), {
|
||||
reason: `[InferMutationAliasingEffects] Expected value to be initialized at '${printSourceLocation(
|
||||
place.loc,
|
||||
value.loc,
|
||||
)}'`,
|
||||
description: printAliasingEffect(value),
|
||||
loc: place.loc,
|
||||
description: printInstructionValue(value),
|
||||
loc: value.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
this.#variables.set(place.identifier.id, new Set([value]));
|
||||
@@ -1244,17 +1266,17 @@ class InferenceState {
|
||||
}
|
||||
}
|
||||
|
||||
freezeValue(value: AliasingEffect, reason: ValueReason): void {
|
||||
freezeValue(value: InstructionValue, reason: ValueReason): void {
|
||||
this.#values.set(value, {
|
||||
kind: ValueKind.Frozen,
|
||||
reason: new Set([reason]),
|
||||
});
|
||||
if (
|
||||
value.kind === 'CreateFunction' &&
|
||||
value.kind === 'FunctionExpression' &&
|
||||
(this.env.config.enablePreserveExistingMemoizationGuarantees ||
|
||||
this.env.config.enableTransitivelyFreezeFunctionExpressions)
|
||||
) {
|
||||
for (const place of value.function.loweredFunc.func.context) {
|
||||
for (const place of value.loweredFunc.func.context) {
|
||||
this.freeze(place, reason);
|
||||
}
|
||||
}
|
||||
@@ -1303,7 +1325,7 @@ class InferenceState {
|
||||
return 'mutate-global';
|
||||
}
|
||||
case ValueKind.MaybeFrozen: {
|
||||
return 'mutate-frozen';
|
||||
return 'none';
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(kind, `Unexpected kind ${kind}`);
|
||||
@@ -1329,8 +1351,8 @@ class InferenceState {
|
||||
* termination.
|
||||
*/
|
||||
merge(other: InferenceState): InferenceState | null {
|
||||
let nextValues: Map<AliasingEffect, AbstractValue> | null = null;
|
||||
let nextVariables: Map<IdentifierId, Set<AliasingEffect>> | null = null;
|
||||
let nextValues: Map<InstructionValue, AbstractValue> | null = null;
|
||||
let nextVariables: Map<IdentifierId, Set<InstructionValue>> | null = null;
|
||||
|
||||
for (const [id, thisValue] of this.#values) {
|
||||
const otherValue = other.#values.get(id);
|
||||
@@ -1354,7 +1376,7 @@ class InferenceState {
|
||||
for (const [id, thisValues] of this.#variables) {
|
||||
const otherValues = other.#variables.get(id);
|
||||
if (otherValues !== undefined) {
|
||||
let mergedValues: Set<AliasingEffect> | null = null;
|
||||
let mergedValues: Set<InstructionValue> | null = null;
|
||||
for (const otherValue of otherValues) {
|
||||
if (!thisValues.has(otherValue)) {
|
||||
mergedValues = mergedValues ?? new Set(thisValues);
|
||||
@@ -1407,8 +1429,8 @@ class InferenceState {
|
||||
*/
|
||||
debug(): any {
|
||||
const result: any = {values: {}, variables: {}};
|
||||
const objects: Map<AliasingEffect, number> = new Map();
|
||||
function identify(value: AliasingEffect): number {
|
||||
const objects: Map<InstructionValue, number> = new Map();
|
||||
function identify(value: InstructionValue): number {
|
||||
let id = objects.get(value);
|
||||
if (id == null) {
|
||||
id = objects.size;
|
||||
@@ -1420,7 +1442,7 @@ class InferenceState {
|
||||
const id = identify(value);
|
||||
result.values[id] = {
|
||||
abstract: this.debugAbstractValue(kind),
|
||||
value: printAliasingEffect(value),
|
||||
value: printInstructionValue(value),
|
||||
};
|
||||
}
|
||||
for (const [variable, values] of this.#variables) {
|
||||
@@ -1437,7 +1459,7 @@ class InferenceState {
|
||||
}
|
||||
|
||||
inferPhi(phi: Phi): void {
|
||||
const values: Set<AliasingEffect> = new Set();
|
||||
const values: Set<InstructionValue> = new Set();
|
||||
for (const [_, operand] of phi.operands) {
|
||||
const operandValues = this.#variables.get(operand.identifier.id);
|
||||
// This is a backedge that will be handled later by State.merge
|
||||
@@ -1659,15 +1681,7 @@ function computeSignatureForInstruction(
|
||||
}
|
||||
case 'PropertyStore':
|
||||
case 'ComputedStore': {
|
||||
const mutationReason: MutationReason | null =
|
||||
value.kind === 'PropertyStore' && value.property === 'current'
|
||||
? {kind: 'AssignCurrentProperty'}
|
||||
: null;
|
||||
effects.push({
|
||||
kind: 'Mutate',
|
||||
value: value.object,
|
||||
reason: mutationReason,
|
||||
});
|
||||
effects.push({kind: 'Mutate', value: value.object});
|
||||
effects.push({
|
||||
kind: 'Capture',
|
||||
from: value.value,
|
||||
@@ -1828,23 +1842,6 @@ function computeSignatureForInstruction(
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const prop of value.props) {
|
||||
if (
|
||||
prop.kind === 'JsxAttribute' &&
|
||||
prop.place.identifier.type.kind === 'Function' &&
|
||||
(isJsxType(prop.place.identifier.type.return) ||
|
||||
(prop.place.identifier.type.return.kind === 'Phi' &&
|
||||
prop.place.identifier.type.return.operands.some(operand =>
|
||||
isJsxType(operand),
|
||||
)))
|
||||
) {
|
||||
// Any props which return jsx are assumed to be called during render
|
||||
effects.push({
|
||||
kind: 'Render',
|
||||
place: prop.place,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1867,34 +1864,19 @@ function computeSignatureForInstruction(
|
||||
break;
|
||||
}
|
||||
case 'Destructure': {
|
||||
for (const patternItem of eachPatternItem(value.lvalue.pattern)) {
|
||||
const place =
|
||||
patternItem.kind === 'Identifier' ? patternItem : patternItem.place;
|
||||
if (isPrimitiveType(place.identifier)) {
|
||||
for (const patternLValue of eachInstructionValueLValue(value)) {
|
||||
if (isPrimitiveType(patternLValue.identifier)) {
|
||||
effects.push({
|
||||
kind: 'Create',
|
||||
into: place,
|
||||
into: patternLValue,
|
||||
value: ValueKind.Primitive,
|
||||
reason: ValueReason.Other,
|
||||
});
|
||||
} else if (patternItem.kind === 'Identifier') {
|
||||
} else {
|
||||
effects.push({
|
||||
kind: 'CreateFrom',
|
||||
from: value.value,
|
||||
into: place,
|
||||
});
|
||||
} else {
|
||||
// Spread creates a new object/array that captures from the RValue
|
||||
effects.push({
|
||||
kind: 'Create',
|
||||
into: place,
|
||||
reason: ValueReason.Other,
|
||||
value: ValueKind.Mutable,
|
||||
});
|
||||
effects.push({
|
||||
kind: 'Capture',
|
||||
from: value.value,
|
||||
into: place,
|
||||
into: patternLValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2006,21 +1988,16 @@ function computeSignatureForInstruction(
|
||||
break;
|
||||
}
|
||||
case 'StoreGlobal': {
|
||||
const variable = `\`${value.name}\``;
|
||||
effects.push({
|
||||
kind: 'MutateGlobal',
|
||||
place: value.value,
|
||||
error: CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Globals,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
error: {
|
||||
reason:
|
||||
'Cannot reassign variables declared outside of the component/hook',
|
||||
description: `Variable ${variable} is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
'Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)',
|
||||
loc: instr.loc,
|
||||
message: `${variable} cannot be reassigned`,
|
||||
}),
|
||||
suggestions: null,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
},
|
||||
});
|
||||
effects.push({kind: 'Assign', from: value.value, into: lvalue});
|
||||
break;
|
||||
@@ -2110,20 +2087,17 @@ function computeEffectsForLegacySignature(
|
||||
effects.push({
|
||||
kind: 'Impure',
|
||||
place: receiver,
|
||||
error: CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Purity,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'Cannot call impure function during render',
|
||||
error: {
|
||||
reason:
|
||||
'Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
|
||||
description:
|
||||
(signature.canonicalName != null
|
||||
? `\`${signature.canonicalName}\` is an impure function. `
|
||||
: '') +
|
||||
'Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
signature.canonicalName != null
|
||||
? `\`${signature.canonicalName}\` is an impure function whose results may change on every call`
|
||||
: null,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc,
|
||||
message: 'Cannot call impure function',
|
||||
}),
|
||||
suggestions: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
const stores: Array<Place> = [];
|
||||
@@ -2303,9 +2277,8 @@ function areArgumentsImmutableAndNonMutating(
|
||||
const values = state.values(place);
|
||||
for (const value of values) {
|
||||
if (
|
||||
value.kind === 'CreateFunction' &&
|
||||
value.function.kind === 'FunctionExpression' &&
|
||||
value.function.loweredFunc.func.params.some(param => {
|
||||
value.kind === 'FunctionExpression' &&
|
||||
value.loweredFunc.func.params.some(param => {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
const range = place.identifier.mutableRange;
|
||||
return range.end > range.start + 1;
|
||||
@@ -2384,7 +2357,6 @@ function computeEffectsForSignature(
|
||||
// Apply substitutions
|
||||
for (const effect of signature.effects) {
|
||||
switch (effect.kind) {
|
||||
case 'MaybeAlias':
|
||||
case 'Assign':
|
||||
case 'ImmutableCapture':
|
||||
case 'Alias':
|
||||
@@ -2498,47 +2470,10 @@ function computeEffectsForSignature(
|
||||
break;
|
||||
}
|
||||
case 'CreateFunction': {
|
||||
const applyInto = substitutions.get(effect.into.identifier.id);
|
||||
if (applyInto == null || applyInto.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
const captures: Array<Place> = [];
|
||||
for (let i = 0; i < effect.captures.length; i++) {
|
||||
const substitution = substitutions.get(
|
||||
effect.captures[i].identifier.id,
|
||||
);
|
||||
if (substitution == null || substitution.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
captures.push(substitution[0]);
|
||||
}
|
||||
const context: Array<Place> = [];
|
||||
const originalContext = effect.function.loweredFunc.func.context;
|
||||
for (let i = 0; i < originalContext.length; i++) {
|
||||
const substitution = substitutions.get(
|
||||
originalContext[i].identifier.id,
|
||||
);
|
||||
if (substitution == null || substitution.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
context.push(substitution[0]);
|
||||
}
|
||||
effects.push({
|
||||
kind: 'CreateFunction',
|
||||
into: applyInto[0],
|
||||
function: {
|
||||
...effect.function,
|
||||
loweredFunc: {
|
||||
...effect.function.loweredFunc,
|
||||
func: {
|
||||
...effect.function.loweredFunc.func,
|
||||
context,
|
||||
},
|
||||
},
|
||||
},
|
||||
captures,
|
||||
CompilerError.throwTodo({
|
||||
reason: `Support CreateFrom effects in signatures`,
|
||||
loc: receiver.loc,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
@@ -2578,196 +2513,3 @@ export type AbstractValue = {
|
||||
kind: ValueKind;
|
||||
reason: ReadonlySet<ValueReason>;
|
||||
};
|
||||
|
||||
export function getWriteErrorReason(abstractValue: AbstractValue): string {
|
||||
if (abstractValue.reason.has(ValueReason.Global)) {
|
||||
return 'Modifying a variable defined outside a component or hook is not allowed. Consider using an effect';
|
||||
} else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
|
||||
return 'Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX';
|
||||
} else if (abstractValue.reason.has(ValueReason.Context)) {
|
||||
return `Modifying a value returned from 'useContext()' is not allowed.`;
|
||||
} else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
|
||||
return 'Modifying a value returned from a function whose return value should not be mutated';
|
||||
} else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
|
||||
return 'Modifying component props or hook arguments is not allowed. Consider using a local variable instead';
|
||||
} else if (abstractValue.reason.has(ValueReason.State)) {
|
||||
return "Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead";
|
||||
} else if (abstractValue.reason.has(ValueReason.ReducerState)) {
|
||||
return "Modifying a value returned from 'useReducer()', which should not be modified directly. Use the dispatch function to update instead";
|
||||
} else if (abstractValue.reason.has(ValueReason.Effect)) {
|
||||
return 'Modifying a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the modification before calling useEffect()';
|
||||
} else if (abstractValue.reason.has(ValueReason.HookCaptured)) {
|
||||
return 'Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook';
|
||||
} else if (abstractValue.reason.has(ValueReason.HookReturn)) {
|
||||
return 'Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed';
|
||||
} else {
|
||||
return 'This modifies a variable that React considers immutable';
|
||||
}
|
||||
}
|
||||
|
||||
function getArgumentEffect(
|
||||
signatureEffect: Effect | null,
|
||||
arg: Place | SpreadPattern,
|
||||
): Effect {
|
||||
if (signatureEffect != null) {
|
||||
if (arg.kind === 'Identifier') {
|
||||
return signatureEffect;
|
||||
} else if (
|
||||
signatureEffect === Effect.Mutate ||
|
||||
signatureEffect === Effect.ConditionallyMutate
|
||||
) {
|
||||
return signatureEffect;
|
||||
} else {
|
||||
// see call-spread-argument-mutable-iterator test fixture
|
||||
if (signatureEffect === Effect.Freeze) {
|
||||
CompilerError.throwTodo({
|
||||
reason: 'Support spread syntax for hook arguments',
|
||||
loc: arg.place.loc,
|
||||
});
|
||||
}
|
||||
// effects[i] is Effect.Capture | Effect.Read | Effect.Store
|
||||
return Effect.ConditionallyMutateIterator;
|
||||
}
|
||||
} else {
|
||||
return Effect.ConditionallyMutate;
|
||||
}
|
||||
}
|
||||
|
||||
export function getFunctionCallSignature(
|
||||
env: Environment,
|
||||
type: Type,
|
||||
): FunctionSignature | null {
|
||||
if (type.kind !== 'Function') {
|
||||
return null;
|
||||
}
|
||||
return env.getFunctionSignature(type);
|
||||
}
|
||||
|
||||
export function isKnownMutableEffect(effect: Effect): boolean {
|
||||
switch (effect) {
|
||||
case Effect.Store:
|
||||
case Effect.ConditionallyMutate:
|
||||
case Effect.ConditionallyMutateIterator:
|
||||
case Effect.Mutate: {
|
||||
return true;
|
||||
}
|
||||
|
||||
case Effect.Unknown: {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected unknown effect',
|
||||
description: null,
|
||||
loc: GeneratedSource,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
case Effect.Read:
|
||||
case Effect.Capture:
|
||||
case Effect.Freeze: {
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(effect, `Unexpected effect \`${effect}\``);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins two values using the following rules:
|
||||
* == Effect Transitions ==
|
||||
*
|
||||
* Freezing an immutable value has not effect:
|
||||
* ┌───────────────┐
|
||||
* │ │
|
||||
* ▼ │ Freeze
|
||||
* ┌──────────────────────────┐ │
|
||||
* │ Immutable │──┘
|
||||
* └──────────────────────────┘
|
||||
*
|
||||
* Freezing a mutable or maybe-frozen value makes it frozen. Freezing a frozen
|
||||
* value has no effect:
|
||||
* ┌───────────────┐
|
||||
* ┌─────────────────────────┐ Freeze │ │
|
||||
* │ MaybeFrozen │────┐ ▼ │ Freeze
|
||||
* └─────────────────────────┘ │ ┌──────────────────────────┐ │
|
||||
* ├────▶│ Frozen │──┘
|
||||
* │ └──────────────────────────┘
|
||||
* ┌─────────────────────────┐ │
|
||||
* │ Mutable │────┘
|
||||
* └─────────────────────────┘
|
||||
*
|
||||
* == Join Lattice ==
|
||||
* - immutable | mutable => mutable
|
||||
* The justification is that immutable and mutable values are different types,
|
||||
* and functions can introspect them to tell the difference (if the argument
|
||||
* is null return early, else if its an object mutate it).
|
||||
* - frozen | mutable => maybe-frozen
|
||||
* Frozen values are indistinguishable from mutable values at runtime, so callers
|
||||
* cannot dynamically avoid mutation of "frozen" values. If a value could be
|
||||
* frozen we have to distinguish it from a mutable value. But it also isn't known
|
||||
* frozen yet, so we distinguish as maybe-frozen.
|
||||
* - immutable | frozen => frozen
|
||||
* This is subtle and falls out of the above rules. If a value could be any of
|
||||
* immutable, mutable, or frozen, then at runtime it could either be a primitive
|
||||
* or a reference type, and callers can't distinguish frozen or not for reference
|
||||
* types. To ensure that any sequence of joins btw those three states yields the
|
||||
* correct maybe-frozen, these two have to produce a frozen value.
|
||||
* - <any> | maybe-frozen => maybe-frozen
|
||||
* - immutable | context => context
|
||||
* - mutable | context => context
|
||||
* - frozen | context => maybe-frozen
|
||||
*
|
||||
* ┌──────────────────────────┐
|
||||
* │ Immutable │───┐
|
||||
* └──────────────────────────┘ │
|
||||
* │ ┌─────────────────────────┐
|
||||
* ├───▶│ Frozen │──┐
|
||||
* ┌──────────────────────────┐ │ └─────────────────────────┘ │
|
||||
* │ Frozen │───┤ │ ┌─────────────────────────┐
|
||||
* └──────────────────────────┘ │ ├─▶│ MaybeFrozen │
|
||||
* │ ┌─────────────────────────┐ │ └─────────────────────────┘
|
||||
* ├───▶│ MaybeFrozen │──┘
|
||||
* ┌──────────────────────────┐ │ └─────────────────────────┘
|
||||
* │ Mutable │───┘
|
||||
* └──────────────────────────┘
|
||||
*/
|
||||
function mergeValueKinds(a: ValueKind, b: ValueKind): ValueKind {
|
||||
if (a === b) {
|
||||
return a;
|
||||
} else if (a === ValueKind.MaybeFrozen || b === ValueKind.MaybeFrozen) {
|
||||
return ValueKind.MaybeFrozen;
|
||||
// after this a and b differ and neither are MaybeFrozen
|
||||
} else if (a === ValueKind.Mutable || b === ValueKind.Mutable) {
|
||||
if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
|
||||
// frozen | mutable
|
||||
return ValueKind.MaybeFrozen;
|
||||
} else if (a === ValueKind.Context || b === ValueKind.Context) {
|
||||
// context | mutable
|
||||
return ValueKind.Context;
|
||||
} else {
|
||||
// mutable | immutable
|
||||
return ValueKind.Mutable;
|
||||
}
|
||||
} else if (a === ValueKind.Context || b === ValueKind.Context) {
|
||||
if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
|
||||
// frozen | context
|
||||
return ValueKind.MaybeFrozen;
|
||||
} else {
|
||||
// context | immutable
|
||||
return ValueKind.Context;
|
||||
}
|
||||
} else if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
|
||||
return ValueKind.Frozen;
|
||||
} else if (a === ValueKind.Global || b === ValueKind.Global) {
|
||||
return ValueKind.Global;
|
||||
} else {
|
||||
CompilerError.invariant(
|
||||
a === ValueKind.Primitive && b == ValueKind.Primitive,
|
||||
{
|
||||
reason: `Unexpected value kind in mergeValues()`,
|
||||
description: `Found kinds ${a} and ${b}`,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
);
|
||||
return ValueKind.Primitive;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
} from '../HIR/visitors';
|
||||
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {AliasingEffect, MutationReason} from './AliasingEffects';
|
||||
import {AliasingEffect} from './AliasingEffects';
|
||||
|
||||
/**
|
||||
* This pass builds an abstract model of the heap and interprets the effects of the
|
||||
@@ -101,7 +101,6 @@ export function inferMutationAliasingRanges(
|
||||
transitive: boolean;
|
||||
kind: MutationKind;
|
||||
place: Place;
|
||||
reason: MutationReason | null;
|
||||
}> = [];
|
||||
const renders: Array<{index: number; place: Place}> = [];
|
||||
|
||||
@@ -141,7 +140,7 @@ export function inferMutationAliasingRanges(
|
||||
} else if (effect.kind === 'CreateFunction') {
|
||||
state.create(effect.into, {
|
||||
kind: 'Function',
|
||||
effect,
|
||||
function: effect.function.loweredFunc.func,
|
||||
});
|
||||
} else if (effect.kind === 'CreateFrom') {
|
||||
state.createFrom(index++, effect.from, effect.into);
|
||||
@@ -156,13 +155,11 @@ export function inferMutationAliasingRanges(
|
||||
* invariant here.
|
||||
*/
|
||||
if (!state.nodes.has(effect.into.identifier)) {
|
||||
state.create(effect.into, {kind: 'Assign'});
|
||||
state.create(effect.into, {kind: 'Object'});
|
||||
}
|
||||
state.assign(index++, effect.from, effect.into);
|
||||
} else if (effect.kind === 'Alias') {
|
||||
state.assign(index++, effect.from, effect.into);
|
||||
} else if (effect.kind === 'MaybeAlias') {
|
||||
state.maybeAlias(index++, effect.from, effect.into);
|
||||
} else if (effect.kind === 'Capture') {
|
||||
state.capture(index++, effect.from, effect.into);
|
||||
} else if (
|
||||
@@ -177,7 +174,6 @@ export function inferMutationAliasingRanges(
|
||||
effect.kind === 'MutateTransitive'
|
||||
? MutationKind.Definite
|
||||
: MutationKind.Conditional,
|
||||
reason: null,
|
||||
place: effect.value,
|
||||
});
|
||||
} else if (
|
||||
@@ -192,7 +188,6 @@ export function inferMutationAliasingRanges(
|
||||
effect.kind === 'Mutate'
|
||||
? MutationKind.Definite
|
||||
: MutationKind.Conditional,
|
||||
reason: effect.kind === 'Mutate' ? (effect.reason ?? null) : null,
|
||||
place: effect.value,
|
||||
});
|
||||
} else if (
|
||||
@@ -200,7 +195,7 @@ export function inferMutationAliasingRanges(
|
||||
effect.kind === 'MutateGlobal' ||
|
||||
effect.kind === 'Impure'
|
||||
) {
|
||||
errors.pushDiagnostic(effect.error);
|
||||
errors.push(effect.error);
|
||||
functionEffects.push(effect);
|
||||
} else if (effect.kind === 'Render') {
|
||||
renders.push({index: index++, place: effect.place});
|
||||
@@ -244,7 +239,6 @@ export function inferMutationAliasingRanges(
|
||||
mutation.transitive,
|
||||
mutation.kind,
|
||||
mutation.place.loc,
|
||||
mutation.reason,
|
||||
errors,
|
||||
);
|
||||
}
|
||||
@@ -253,7 +247,6 @@ export function inferMutationAliasingRanges(
|
||||
}
|
||||
for (const param of [...fn.context, ...fn.params]) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
|
||||
const node = state.nodes.get(place.identifier);
|
||||
if (node == null) {
|
||||
continue;
|
||||
@@ -271,7 +264,6 @@ export function inferMutationAliasingRanges(
|
||||
functionEffects.push({
|
||||
kind: 'Mutate',
|
||||
value: {...place, loc: node.local.loc},
|
||||
reason: node.mutationReason,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -354,8 +346,7 @@ export function inferMutationAliasingRanges(
|
||||
case 'Assign':
|
||||
case 'Alias':
|
||||
case 'Capture':
|
||||
case 'CreateFrom':
|
||||
case 'MaybeAlias': {
|
||||
case 'CreateFrom': {
|
||||
const isMutatedOrReassigned =
|
||||
effect.into.identifier.mutableRange.end > instr.id;
|
||||
if (isMutatedOrReassigned) {
|
||||
@@ -474,99 +465,6 @@ export function inferMutationAliasingRanges(
|
||||
}
|
||||
}
|
||||
|
||||
const tracked: Array<Place> = [];
|
||||
for (const param of [...fn.params, ...fn.context, fn.returns]) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
tracked.push(place);
|
||||
}
|
||||
|
||||
const returned: Set<Node> = new Set();
|
||||
const queue: Array<Node> = [state.nodes.get(fn.returns.identifier)!];
|
||||
const seen: Set<Node> = new Set();
|
||||
while (queue.length !== 0) {
|
||||
const node = queue.pop()!;
|
||||
if (seen.has(node)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(node);
|
||||
for (const id of node.aliases.keys()) {
|
||||
queue.push(state.nodes.get(id)!);
|
||||
}
|
||||
for (const id of node.createdFrom.keys()) {
|
||||
queue.push(state.nodes.get(id)!);
|
||||
}
|
||||
if (node.id.id === fn.returns.identifier.id) {
|
||||
continue;
|
||||
}
|
||||
switch (node.value.kind) {
|
||||
case 'Assign':
|
||||
case 'CreateFrom': {
|
||||
break;
|
||||
}
|
||||
case 'Phi':
|
||||
case 'Object':
|
||||
case 'Function': {
|
||||
returned.add(node);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
node.value,
|
||||
`Unexpected node value kind '${(node.value as any).kind}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
const returnedValues = [...returned];
|
||||
if (
|
||||
returnedValues.length === 1 &&
|
||||
returnedValues[0].value.kind === 'Object' &&
|
||||
tracked.some(place => place.identifier.id === returnedValues[0].id.id)
|
||||
) {
|
||||
const from = tracked.find(
|
||||
place => place.identifier.id === returnedValues[0].id.id,
|
||||
)!;
|
||||
functionEffects.push({
|
||||
kind: 'Assign',
|
||||
from,
|
||||
into: fn.returns,
|
||||
});
|
||||
} else if (
|
||||
returnedValues.length === 1 &&
|
||||
returnedValues[0].value.kind === 'Function'
|
||||
) {
|
||||
const outerContext = new Set(fn.context.map(p => p.identifier.id));
|
||||
const effect = returnedValues[0].value.effect;
|
||||
functionEffects.push({
|
||||
kind: 'CreateFunction',
|
||||
function: {
|
||||
...effect.function,
|
||||
loweredFunc: {
|
||||
func: {
|
||||
...effect.function.loweredFunc.func,
|
||||
context: effect.function.loweredFunc.func.context.filter(p =>
|
||||
outerContext.has(p.identifier.id),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
captures: effect.captures.filter(p => outerContext.has(p.identifier.id)),
|
||||
into: fn.returns,
|
||||
});
|
||||
} else {
|
||||
const returns = fn.returns.identifier;
|
||||
functionEffects.push({
|
||||
kind: 'Create',
|
||||
into: fn.returns,
|
||||
value: isPrimitiveType(returns)
|
||||
? ValueKind.Primitive
|
||||
: isJsxType(returns.type)
|
||||
? ValueKind.Frozen
|
||||
: ValueKind.Mutable,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Part 3
|
||||
* Finish populating the externally visible effects. Above we bubble-up the side effects
|
||||
@@ -574,12 +472,28 @@ export function inferMutationAliasingRanges(
|
||||
* Here we populate an effect to create the return value as well as populating alias/capture
|
||||
* effects for how data flows between the params, context vars, and return.
|
||||
*/
|
||||
const returns = fn.returns.identifier;
|
||||
functionEffects.push({
|
||||
kind: 'Create',
|
||||
into: fn.returns,
|
||||
value: isPrimitiveType(returns)
|
||||
? ValueKind.Primitive
|
||||
: isJsxType(returns.type)
|
||||
? ValueKind.Frozen
|
||||
: ValueKind.Mutable,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
});
|
||||
/**
|
||||
* Determine precise data-flow effects by simulating transitive mutations of the params/
|
||||
* captures and seeing what other params/context variables are affected. Anything that
|
||||
* would be transitively mutated needs a capture relationship.
|
||||
*/
|
||||
const tracked: Array<Place> = [];
|
||||
const ignoredErrors = new CompilerError();
|
||||
for (const param of [...fn.params, ...fn.context, fn.returns]) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
tracked.push(place);
|
||||
}
|
||||
for (const into of tracked) {
|
||||
const mutationIndex = index++;
|
||||
state.mutate(
|
||||
@@ -589,7 +503,6 @@ export function inferMutationAliasingRanges(
|
||||
true,
|
||||
MutationKind.Conditional,
|
||||
into.loc,
|
||||
null,
|
||||
ignoredErrors,
|
||||
);
|
||||
for (const from of tracked) {
|
||||
@@ -636,7 +549,7 @@ function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
|
||||
case 'Impure':
|
||||
case 'MutateFrozen':
|
||||
case 'MutateGlobal': {
|
||||
errors.pushDiagnostic(effect.error);
|
||||
errors.push(effect.error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -654,25 +567,14 @@ type Node = {
|
||||
createdFrom: Map<Identifier, number>;
|
||||
captures: Map<Identifier, number>;
|
||||
aliases: Map<Identifier, number>;
|
||||
maybeAliases: Map<Identifier, number>;
|
||||
edges: Array<{
|
||||
index: number;
|
||||
node: Identifier;
|
||||
kind: 'capture' | 'alias' | 'maybeAlias';
|
||||
}>;
|
||||
edges: Array<{index: number; node: Identifier; kind: 'capture' | 'alias'}>;
|
||||
transitive: {kind: MutationKind; loc: SourceLocation} | null;
|
||||
local: {kind: MutationKind; loc: SourceLocation} | null;
|
||||
lastMutated: number;
|
||||
mutationReason: MutationReason | null;
|
||||
value:
|
||||
| {kind: 'Assign'}
|
||||
| {kind: 'CreateFrom'}
|
||||
| {kind: 'Object'}
|
||||
| {kind: 'Phi'}
|
||||
| {
|
||||
kind: 'Function';
|
||||
effect: Extract<AliasingEffect, {kind: 'CreateFunction'}>;
|
||||
};
|
||||
| {kind: 'Function'; function: HIRFunction};
|
||||
};
|
||||
class AliasingState {
|
||||
nodes: Map<Identifier, Node> = new Map();
|
||||
@@ -683,18 +585,16 @@ class AliasingState {
|
||||
createdFrom: new Map(),
|
||||
captures: new Map(),
|
||||
aliases: new Map(),
|
||||
maybeAliases: new Map(),
|
||||
edges: [],
|
||||
transitive: null,
|
||||
local: null,
|
||||
lastMutated: 0,
|
||||
mutationReason: null,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
createFrom(index: number, from: Place, into: Place): void {
|
||||
this.create(into, {kind: 'CreateFrom'});
|
||||
this.create(into, {kind: 'Object'});
|
||||
const fromNode = this.nodes.get(from.identifier);
|
||||
const toNode = this.nodes.get(into.identifier);
|
||||
if (fromNode == null || toNode == null) {
|
||||
@@ -730,18 +630,6 @@ class AliasingState {
|
||||
}
|
||||
}
|
||||
|
||||
maybeAlias(index: number, from: Place, into: Place): void {
|
||||
const fromNode = this.nodes.get(from.identifier);
|
||||
const toNode = this.nodes.get(into.identifier);
|
||||
if (fromNode == null || toNode == null) {
|
||||
return;
|
||||
}
|
||||
fromNode.edges.push({index, node: into.identifier, kind: 'maybeAlias'});
|
||||
if (!toNode.maybeAliases.has(from.identifier)) {
|
||||
toNode.maybeAliases.set(from.identifier, index);
|
||||
}
|
||||
}
|
||||
|
||||
render(index: number, start: Identifier, errors: CompilerError): void {
|
||||
const seen = new Set<Identifier>();
|
||||
const queue: Array<Identifier> = [start];
|
||||
@@ -756,10 +644,7 @@ class AliasingState {
|
||||
continue;
|
||||
}
|
||||
if (node.value.kind === 'Function') {
|
||||
appendFunctionErrors(
|
||||
errors,
|
||||
node.value.effect.function.loweredFunc.func,
|
||||
);
|
||||
appendFunctionErrors(errors, node.value.function);
|
||||
}
|
||||
for (const [alias, when] of node.createdFrom) {
|
||||
if (when >= index) {
|
||||
@@ -788,30 +673,26 @@ class AliasingState {
|
||||
// Null is used for simulated mutations
|
||||
end: InstructionId | null,
|
||||
transitive: boolean,
|
||||
startKind: MutationKind,
|
||||
kind: MutationKind,
|
||||
loc: SourceLocation,
|
||||
reason: MutationReason | null,
|
||||
errors: CompilerError,
|
||||
): void {
|
||||
const seen = new Map<Identifier, MutationKind>();
|
||||
const seen = new Set<Identifier>();
|
||||
const queue: Array<{
|
||||
place: Identifier;
|
||||
transitive: boolean;
|
||||
direction: 'backwards' | 'forwards';
|
||||
kind: MutationKind;
|
||||
}> = [{place: start, transitive, direction: 'backwards', kind: startKind}];
|
||||
}> = [{place: start, transitive, direction: 'backwards'}];
|
||||
while (queue.length !== 0) {
|
||||
const {place: current, transitive, direction, kind} = queue.pop()!;
|
||||
const previousKind = seen.get(current);
|
||||
if (previousKind != null && previousKind >= kind) {
|
||||
const {place: current, transitive, direction} = queue.pop()!;
|
||||
if (seen.has(current)) {
|
||||
continue;
|
||||
}
|
||||
seen.set(current, kind);
|
||||
seen.add(current);
|
||||
const node = this.nodes.get(current);
|
||||
if (node == null) {
|
||||
continue;
|
||||
}
|
||||
node.mutationReason ??= reason;
|
||||
node.lastMutated = Math.max(node.lastMutated, index);
|
||||
if (end != null) {
|
||||
node.id.mutableRange.end = makeInstructionId(
|
||||
@@ -823,10 +704,7 @@ class AliasingState {
|
||||
node.transitive == null &&
|
||||
node.local == null
|
||||
) {
|
||||
appendFunctionErrors(
|
||||
errors,
|
||||
node.value.effect.function.loweredFunc.func,
|
||||
);
|
||||
appendFunctionErrors(errors, node.value.function);
|
||||
}
|
||||
if (transitive) {
|
||||
if (node.transitive == null || node.transitive.kind < kind) {
|
||||
@@ -846,18 +724,13 @@ class AliasingState {
|
||||
if (edge.index >= index) {
|
||||
break;
|
||||
}
|
||||
queue.push({place: edge.node, transitive, direction: 'forwards', kind});
|
||||
queue.push({place: edge.node, transitive, direction: 'forwards'});
|
||||
}
|
||||
for (const [alias, when] of node.createdFrom) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push({
|
||||
place: alias,
|
||||
transitive: true,
|
||||
direction: 'backwards',
|
||||
kind,
|
||||
});
|
||||
queue.push({place: alias, transitive: true, direction: 'backwards'});
|
||||
}
|
||||
if (direction === 'backwards' || node.value.kind !== 'Phi') {
|
||||
/**
|
||||
@@ -874,25 +747,7 @@ class AliasingState {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push({place: alias, transitive, direction: 'backwards', kind});
|
||||
}
|
||||
/**
|
||||
* MaybeAlias indicates potential data flow from unknown function calls,
|
||||
* so we downgrade mutations through these aliases to consider them
|
||||
* conditional. This means we'll consider them for mutation *range*
|
||||
* purposes but not report validation errors for mutations, since
|
||||
* we aren't sure that the `from` value could actually be aliased.
|
||||
*/
|
||||
for (const [alias, when] of node.maybeAliases) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push({
|
||||
place: alias,
|
||||
transitive,
|
||||
direction: 'backwards',
|
||||
kind: MutationKind.Conditional,
|
||||
});
|
||||
queue.push({place: alias, transitive, direction: 'backwards'});
|
||||
}
|
||||
}
|
||||
/**
|
||||
@@ -903,12 +758,7 @@ class AliasingState {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push({
|
||||
place: capture,
|
||||
transitive,
|
||||
direction: 'backwards',
|
||||
kind,
|
||||
});
|
||||
queue.push({place: capture, transitive, direction: 'backwards'});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
isStableType,
|
||||
isStableTypeContainer,
|
||||
isUseOperator,
|
||||
isUseRefType,
|
||||
} from '../HIR';
|
||||
import {PostDominator} from '../HIR/Dominator';
|
||||
import {
|
||||
@@ -69,6 +70,13 @@ class StableSidemap {
|
||||
isStable: false,
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
this.env.config.enableTreatRefLikeIdentifiersAsRefs &&
|
||||
isUseRefType(lvalue.identifier)
|
||||
) {
|
||||
this.map.set(lvalue.identifier.id, {
|
||||
isStable: true,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {BlockId, HIRFunction, Identifier} from '../HIR';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
/*
|
||||
* Any values created within a try/catch block could be aliased to the try handler.
|
||||
* Our lowering ensures that every instruction within a try block will be lowered into a
|
||||
* basic block ending in a maybe-throw terminal that points to its catch block, so we can
|
||||
* iterate such blocks and alias their instruction lvalues to the handler's param (if present).
|
||||
*/
|
||||
export function inferTryCatchAliases(
|
||||
fn: HIRFunction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
const handlerParams: Map<BlockId, Identifier> = new Map();
|
||||
for (const [_, block] of fn.body.blocks) {
|
||||
if (
|
||||
block.terminal.kind === 'try' &&
|
||||
block.terminal.handlerBinding !== null
|
||||
) {
|
||||
handlerParams.set(
|
||||
block.terminal.handler,
|
||||
block.terminal.handlerBinding.identifier,
|
||||
);
|
||||
} else if (block.terminal.kind === 'maybe-throw') {
|
||||
const handlerParam = handlerParams.get(block.terminal.handler);
|
||||
if (handlerParam === undefined) {
|
||||
/*
|
||||
* There's no catch clause param, nothing to alias to so
|
||||
* skip this block
|
||||
*/
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* Otherwise alias all values created in this block to the
|
||||
* catch clause param
|
||||
*/
|
||||
for (const instr of block.instructions) {
|
||||
aliases.union([handlerParam, instr.lvalue.identifier]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,10 +153,6 @@ This is somewhat the inverse of `Capture`. The `CreateFrom` effect describes tha
|
||||
|
||||
Describes immutable data flow from one value to another. This is not currently used for anything, but is intended to eventually power a more sophisticated escape analysis.
|
||||
|
||||
### MaybeAlias
|
||||
|
||||
Describes potential data flow that the compiler knows may occur behind a function call, but cannot be sure about. For example, `foo(x)` _may_ be the identity function and return `x`, or `cond(a, b, c)` may conditionally return `b` or `c` depending on the value of `a`, but those functions could just as easily return new mutable values and not capture any information from their arguments. MaybeAlias represents that we have to consider the potential for data flow when deciding mutable ranges, but should be conservative about reporting errors. For example, `foo(someFrozenValue).property = true` should not error since we don't know for certain that foo returns its input.
|
||||
|
||||
### State-Changing Effects
|
||||
|
||||
The following effects describe state changes to specific values, not data flow. In many cases, JavaScript semantics will involve a combination of both data-flow effects *and* state-change effects. For example, `object.property = value` has data flow (`Capture object <- value`) and mutation (`Mutate object`).
|
||||
@@ -351,17 +347,6 @@ a.b = b; // capture
|
||||
mutate(a); // can transitively mutate b
|
||||
```
|
||||
|
||||
### MaybeAlias makes mutation conditional
|
||||
|
||||
Because we don't know for certain that the aliasing occurs, we consider the mutation conditional against the source.
|
||||
|
||||
```
|
||||
MaybeAlias a <- b
|
||||
Mutate a
|
||||
=>
|
||||
MutateConditional b
|
||||
```
|
||||
|
||||
### Freeze Does Not Freeze the Value
|
||||
|
||||
Freeze does not freeze the value itself:
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
export {default as analyseFunctions} from './AnalyseFunctions';
|
||||
export {dropManualMemoization} from './DropManualMemoization';
|
||||
export {inferMutableRanges} from './InferMutableRanges';
|
||||
export {inferReactivePlaces} from './InferReactivePlaces';
|
||||
export {default as inferReferenceEffects} from './InferReferenceEffects';
|
||||
export {inlineImmediatelyInvokedFunctionExpressions} from './InlineImmediatelyInvokedFunctionExpressions';
|
||||
export {inferEffectDependencies} from './InferEffectDependencies';
|
||||
|
||||
@@ -42,7 +42,6 @@ import {
|
||||
mapInstructionValueOperands,
|
||||
mapTerminalOperands,
|
||||
} from '../HIR/visitors';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
|
||||
type InlinedJsxDeclarationMap = Map<
|
||||
DeclarationId,
|
||||
@@ -84,7 +83,6 @@ export function inlineJsxTransform(
|
||||
kind: 'CompileDiagnostic',
|
||||
fnLoc: null,
|
||||
detail: {
|
||||
category: ErrorCategory.Todo,
|
||||
reason: 'JSX Inlining is not supported on value blocks',
|
||||
loc: instr.loc,
|
||||
},
|
||||
|
||||
@@ -237,7 +237,6 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
terminal: {
|
||||
id: makeInstructionId(0),
|
||||
kind: 'return',
|
||||
returnVariant: 'Explicit',
|
||||
loc: GeneratedSource,
|
||||
value: arrayInstr.lvalue,
|
||||
effects: null,
|
||||
@@ -255,6 +254,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
returnTypeAnnotation: null,
|
||||
returns: createTemporaryPlace(env, GeneratedSource),
|
||||
context: [],
|
||||
effects: null,
|
||||
body: {
|
||||
entry: block.id,
|
||||
blocks: new Map([[block.id, block]]),
|
||||
@@ -262,7 +262,6 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
generator: false,
|
||||
async: false,
|
||||
directives: [],
|
||||
aliasingEffects: [],
|
||||
};
|
||||
|
||||
reversePostorderBlocks(fn.body);
|
||||
|
||||
@@ -352,7 +352,6 @@ function emitOutlinedFn(
|
||||
terminal: {
|
||||
id: makeInstructionId(0),
|
||||
kind: 'return',
|
||||
returnVariant: 'Explicit',
|
||||
loc: GeneratedSource,
|
||||
value: instructions.at(-1)!.lvalue,
|
||||
effects: null,
|
||||
@@ -370,6 +369,7 @@ function emitOutlinedFn(
|
||||
returnTypeAnnotation: null,
|
||||
returns: createTemporaryPlace(env, GeneratedSource),
|
||||
context: [],
|
||||
effects: null,
|
||||
body: {
|
||||
entry: block.id,
|
||||
blocks: new Map([[block.id, block]]),
|
||||
@@ -377,7 +377,6 @@ function emitOutlinedFn(
|
||||
generator: false,
|
||||
async: false,
|
||||
directives: [],
|
||||
aliasingEffects: [],
|
||||
};
|
||||
return fn;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
pruneUnusedLabels,
|
||||
renameVariables,
|
||||
} from '.';
|
||||
import {CompilerError, ErrorCategory, ErrorSeverity} from '../CompilerError';
|
||||
import {CompilerError, ErrorSeverity} from '../CompilerError';
|
||||
import {Environment, ExternalFunction} from '../HIR';
|
||||
import {
|
||||
ArrayPattern,
|
||||
@@ -2185,7 +2185,6 @@ function codegenInstructionValue(
|
||||
(declarator.id as t.Identifier).name
|
||||
}'`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: declarator.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2194,7 +2193,6 @@ function codegenInstructionValue(
|
||||
cx.errors.push({
|
||||
reason: `(CodegenReactiveFunction::codegenInstructionValue) Handle conversion of ${stmt.type} to expression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
@@ -119,7 +119,6 @@ class FindLastUsageVisitor extends ReactiveFunctionVisitor<void> {
|
||||
|
||||
class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | null> {
|
||||
lastUsage: Map<DeclarationId, InstructionId>;
|
||||
temporaries: Map<DeclarationId, DeclarationId> = new Map();
|
||||
|
||||
constructor(lastUsage: Map<DeclarationId, InstructionId>) {
|
||||
super();
|
||||
@@ -216,12 +215,6 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
|
||||
current.lvalues.add(
|
||||
instr.instruction.lvalue.identifier.declarationId,
|
||||
);
|
||||
if (instr.instruction.value.kind === 'LoadLocal') {
|
||||
this.temporaries.set(
|
||||
instr.instruction.lvalue.identifier.declarationId,
|
||||
instr.instruction.value.place.identifier.declarationId,
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -243,13 +236,6 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
|
||||
)) {
|
||||
current.lvalues.add(lvalue.identifier.declarationId);
|
||||
}
|
||||
this.temporaries.set(
|
||||
instr.instruction.value.lvalue.place.identifier
|
||||
.declarationId,
|
||||
this.temporaries.get(
|
||||
instr.instruction.value.value.identifier.declarationId,
|
||||
) ?? instr.instruction.value.value.identifier.declarationId,
|
||||
);
|
||||
} else {
|
||||
log(
|
||||
`Reset scope @${current.block.scope.id} from StoreLocal in [${instr.instruction.id}]`,
|
||||
@@ -274,7 +260,7 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
|
||||
case 'scope': {
|
||||
if (
|
||||
current !== null &&
|
||||
canMergeScopes(current.block, instr, this.temporaries) &&
|
||||
canMergeScopes(current.block, instr) &&
|
||||
areLValuesLastUsedByScope(
|
||||
instr.scope,
|
||||
current.lvalues,
|
||||
@@ -440,7 +426,6 @@ function areLValuesLastUsedByScope(
|
||||
function canMergeScopes(
|
||||
current: ReactiveScopeBlock,
|
||||
next: ReactiveScopeBlock,
|
||||
temporaries: Map<DeclarationId, DeclarationId>,
|
||||
): boolean {
|
||||
// Don't merge scopes with reassignments
|
||||
if (
|
||||
@@ -480,14 +465,11 @@ function canMergeScopes(
|
||||
(next.scope.dependencies.size !== 0 &&
|
||||
[...next.scope.dependencies].every(
|
||||
dep =>
|
||||
dep.path.length === 0 &&
|
||||
isAlwaysInvalidatingType(dep.identifier.type) &&
|
||||
Iterable_some(
|
||||
current.scope.declarations.values(),
|
||||
decl =>
|
||||
decl.identifier.declarationId === dep.identifier.declarationId ||
|
||||
decl.identifier.declarationId ===
|
||||
temporaries.get(dep.identifier.declarationId),
|
||||
decl.identifier.declarationId === dep.identifier.declarationId,
|
||||
),
|
||||
))
|
||||
) {
|
||||
@@ -495,16 +477,12 @@ function canMergeScopes(
|
||||
return true;
|
||||
}
|
||||
log(` cannot merge scopes:`);
|
||||
log(
|
||||
` ${printReactiveScopeSummary(current.scope)} ${[...current.scope.declarations.values()].map(decl => decl.identifier.declarationId)}`,
|
||||
);
|
||||
log(
|
||||
` ${printReactiveScopeSummary(next.scope)} ${[...next.scope.dependencies].map(dep => `${dep.identifier.declarationId} ${temporaries.get(dep.identifier.declarationId) ?? dep.identifier.declarationId}`)}`,
|
||||
);
|
||||
log(` ${printReactiveScopeSummary(current.scope)}`);
|
||||
log(` ${printReactiveScopeSummary(next.scope)}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isAlwaysInvalidatingType(type: Type): boolean {
|
||||
function isAlwaysInvalidatingType(type: Type): boolean {
|
||||
switch (type.kind) {
|
||||
case 'Object': {
|
||||
switch (type.shapeId) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
getHookKind,
|
||||
isMutableEffect,
|
||||
} from '../HIR';
|
||||
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
|
||||
import {assertExhaustive, getOrInsertDefault} from '../Utils/utils';
|
||||
import {getPlaceScope, ReactiveScope} from '../HIR/HIR';
|
||||
import {
|
||||
@@ -34,7 +35,6 @@ import {
|
||||
visitReactiveFunction,
|
||||
} from './visitors';
|
||||
import {printPlace} from '../HIR/PrintHIR';
|
||||
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
|
||||
|
||||
/*
|
||||
* This pass prunes reactive scopes that are not necessary to bound downstream computation.
|
||||
@@ -829,14 +829,12 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
|
||||
};
|
||||
}
|
||||
case 'UnsupportedNode': {
|
||||
const lvalues = [];
|
||||
if (lvalue !== null) {
|
||||
lvalues.push({place: lvalue, level: MemoizationLevel.Never});
|
||||
}
|
||||
return {
|
||||
lvalues,
|
||||
rvalues: [],
|
||||
};
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected unsupported node`,
|
||||
description: null,
|
||||
loc: value.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
|
||||
@@ -42,7 +42,6 @@ import {
|
||||
import {eachInstructionOperand} from '../HIR/visitors';
|
||||
import {printSourceLocationLine} from '../HIR/PrintHIR';
|
||||
import {USE_FIRE_FUNCTION_NAME} from '../HIR/Environment';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
|
||||
/*
|
||||
* TODO(jmbrown):
|
||||
@@ -134,7 +133,6 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
|
||||
loc: value.loc,
|
||||
description: null,
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: ErrorCategory.Invariant,
|
||||
reason: '[InsertFire] No LoadGlobal found for useEffect call',
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -181,7 +179,6 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
|
||||
description:
|
||||
'You must use an array literal for an effect dependency array when that effect uses `fire()`',
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: ErrorCategory.Fire,
|
||||
reason: CANNOT_COMPILE_FIRE,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -192,7 +189,6 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
|
||||
description:
|
||||
'You must use an array literal for an effect dependency array when that effect uses `fire()`',
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: ErrorCategory.Fire,
|
||||
reason: CANNOT_COMPILE_FIRE,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -227,7 +223,6 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
|
||||
loc: value.loc,
|
||||
description: null,
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: ErrorCategory.Invariant,
|
||||
reason:
|
||||
'[InsertFire] No loadLocal found for fire call argument',
|
||||
suggestions: null,
|
||||
@@ -251,7 +246,6 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
|
||||
description:
|
||||
'`fire()` can only receive a function call such as `fire(fn(a,b)). Method calls and other expressions are not allowed',
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: ErrorCategory.Fire,
|
||||
reason: CANNOT_COMPILE_FIRE,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -270,7 +264,6 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
|
||||
loc: value.loc,
|
||||
description,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: ErrorCategory.Fire,
|
||||
reason: CANNOT_COMPILE_FIRE,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -402,7 +395,6 @@ function ensureNoRemainingCalleeCaptures(
|
||||
this effect or not used with a fire() call at all. ${calleeName} was used with fire() on line \
|
||||
${printSourceLocationLine(calleeInfo.fireLoc)} in this effect`,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: ErrorCategory.Fire,
|
||||
reason: CANNOT_COMPILE_FIRE,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -419,7 +411,6 @@ function ensureNoMoreFireUses(fn: HIRFunction, context: Context): void {
|
||||
context.pushError({
|
||||
loc: place.identifier.loc,
|
||||
description: 'Cannot use `fire` outside of a useEffect function',
|
||||
category: ErrorCategory.Fire,
|
||||
severity: ErrorSeverity.Invariant,
|
||||
reason: CANNOT_COMPILE_FIRE,
|
||||
suggestions: null,
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
Instruction,
|
||||
InstructionKind,
|
||||
makePropertyLiteral,
|
||||
makeType,
|
||||
PropType,
|
||||
@@ -195,29 +194,12 @@ function* generateInstructionTypes(
|
||||
break;
|
||||
}
|
||||
|
||||
// We intentionally do not infer types for most context variables
|
||||
// We intentionally do not infer types for context variables
|
||||
case 'DeclareContext':
|
||||
case 'StoreContext':
|
||||
case 'LoadContext': {
|
||||
break;
|
||||
}
|
||||
case 'StoreContext': {
|
||||
/**
|
||||
* The caveat is StoreContext const, where we know the value is
|
||||
* assigned once such that everywhere the value is accessed, it
|
||||
* must have the same type from the rvalue.
|
||||
*
|
||||
* A concrete example where this is useful is `const ref = useRef()`
|
||||
* where the ref is referenced before its declaration in a function
|
||||
* expression, causing it to be converted to a const context variable.
|
||||
*/
|
||||
if (value.lvalue.kind === InstructionKind.Const) {
|
||||
yield equation(
|
||||
value.lvalue.place.identifier.type,
|
||||
value.value.identifier.type,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'StoreLocal': {
|
||||
if (env.config.enableUseTypeAnnotations) {
|
||||
@@ -378,12 +360,6 @@ function* generateInstructionTypes(
|
||||
value: makePropertyLiteral(propertyName),
|
||||
},
|
||||
});
|
||||
} else if (item.kind === 'Spread') {
|
||||
// Array pattern spread always creates an array
|
||||
yield equation(item.place.identifier.type, {
|
||||
kind: 'Object',
|
||||
shapeId: BuiltInArrayId,
|
||||
});
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@@ -451,18 +427,6 @@ function* generateInstructionTypes(
|
||||
|
||||
case 'JsxExpression':
|
||||
case 'JsxFragment': {
|
||||
if (env.config.enableTreatRefLikeIdentifiersAsRefs) {
|
||||
if (value.kind === 'JsxExpression') {
|
||||
for (const prop of value.props) {
|
||||
if (prop.kind === 'JsxAttribute' && prop.name === 'ref') {
|
||||
yield equation(prop.place.identifier.type, {
|
||||
kind: 'Object',
|
||||
shapeId: BuiltInUseRefId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
yield equation(left, {kind: 'Object', shapeId: BuiltInJsxId});
|
||||
break;
|
||||
}
|
||||
@@ -478,36 +442,7 @@ function* generateInstructionTypes(
|
||||
yield equation(left, returnType);
|
||||
break;
|
||||
}
|
||||
case 'PropertyStore': {
|
||||
/**
|
||||
* Infer types based on assignments to known object properties
|
||||
* This is important for refs, where assignment to `<maybeRef>.current`
|
||||
* can help us infer that an object itself is a ref
|
||||
*/
|
||||
yield equation(
|
||||
/**
|
||||
* Our property type declarations are best-effort and we haven't tested
|
||||
* using them to drive inference of rvalues from lvalues. We want to emit
|
||||
* a Property type in order to infer refs from `.current` accesses, but
|
||||
* stay conservative by not otherwise inferring anything about rvalues.
|
||||
* So we use a dummy type here.
|
||||
*
|
||||
* TODO: consider using the rvalue type here
|
||||
*/
|
||||
makeType(),
|
||||
// unify() only handles properties in the second position
|
||||
{
|
||||
kind: 'Property',
|
||||
objectType: value.object.identifier.type,
|
||||
objectName: getName(names, value.object.identifier.id),
|
||||
propertyName: {
|
||||
kind: 'literal',
|
||||
value: value.property,
|
||||
},
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'PropertyStore':
|
||||
case 'DeclareLocal':
|
||||
case 'RegExpLiteral':
|
||||
case 'MetaProperty':
|
||||
@@ -777,15 +712,6 @@ class Unifier {
|
||||
return {kind: 'Phi', operands: type.operands.map(o => this.get(o))};
|
||||
}
|
||||
|
||||
if (type.kind === 'Function') {
|
||||
return {
|
||||
kind: 'Function',
|
||||
isConstructor: type.isConstructor,
|
||||
shapeId: type.shapeId,
|
||||
return: this.get(type.return),
|
||||
};
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {HIRFunction, IdentifierId, Type, typeEquals} from '../HIR';
|
||||
|
||||
/**
|
||||
* Temporary workaround for InferTypes not propagating the types of phis.
|
||||
* Previously, LeaveSSA would replace all the identifiers for each phi (operands and
|
||||
* the phi itself) with a single "canonical" identifier, generally chosen as the first
|
||||
* operand to flow into the phi. In case of a phi whose operand was a phi, this could
|
||||
* sometimes be an operand from the earlier phi.
|
||||
*
|
||||
* As a result, even though InferTypes did not propagate types for phis, LeaveSSA
|
||||
* could end up replacing the phi Identifier with another identifer from an operand,
|
||||
* which _did_ have a type inferred.
|
||||
*
|
||||
* This didn't affect the initial construction of mutable ranges because InferMutableRanges
|
||||
* runs before LeaveSSA - thus, the types propagated by LeaveSSA only affected later optimizations,
|
||||
* notably MergeScopesThatInvalidateTogether which uses type to determine if a scope's output
|
||||
* will always invalidate with its input.
|
||||
*
|
||||
* The long-term correct approach is to update InferTypes to infer the types of phis,
|
||||
* but this is complicated because InferMutableRanges inadvertently depends on phis
|
||||
* never having a known type, such that a Store effect cannot occur on a phi value.
|
||||
* Once we fix InferTypes to infer phi types, then we'll also have to update InferMutableRanges
|
||||
* to handle this case.
|
||||
*
|
||||
* As a temporary workaround, this pass propagates the type of phis and can be called
|
||||
* safely *after* InferMutableRanges. Unlike LeaveSSA, this pass only propagates the
|
||||
* type if all operands have the same type, it's its more correct.
|
||||
*/
|
||||
export function propagatePhiTypes(fn: HIRFunction): void {
|
||||
/**
|
||||
* We track which SSA ids have had their types propagated to handle nested ternaries,
|
||||
* see the StoreLocal handling below
|
||||
*/
|
||||
const propagated = new Set<IdentifierId>();
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const phi of block.phis) {
|
||||
/*
|
||||
* We replicate the previous LeaveSSA behavior and only propagate types for
|
||||
* unnamed variables. LeaveSSA would have chosen one of the operands as the
|
||||
* canonical id and taken its type as the type of all identifiers. We're
|
||||
* more conservative and only propagate if the types are the same and the
|
||||
* phi didn't have a type inferred.
|
||||
*
|
||||
* Note that this can change output slightly in cases such as
|
||||
* `cond ? <div /> : null`.
|
||||
*
|
||||
* Previously the first operand's type (BuiltInJsx) would have been propagated,
|
||||
* and this expression may have been merged with subsequent reactive scopes
|
||||
* since it appears (based on that type) to always invalidate.
|
||||
*
|
||||
* But the correct type is `BuiltInJsx | null`, which we can't express and
|
||||
* so leave as a generic `Type`, which does not always invalidate and therefore
|
||||
* does not merge with subsequent scopes.
|
||||
*
|
||||
* We also don't propagate scopes for named variables, to preserve compatibility
|
||||
* with previous LeaveSSA behavior.
|
||||
*/
|
||||
if (
|
||||
phi.place.identifier.type.kind !== 'Type' ||
|
||||
phi.place.identifier.name !== null
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let type: Type | null = null;
|
||||
for (const [, operand] of phi.operands) {
|
||||
if (type === null) {
|
||||
type = operand.identifier.type;
|
||||
} else if (!typeEquals(type, operand.identifier.type)) {
|
||||
type = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (type !== null) {
|
||||
phi.place.identifier.type = type;
|
||||
propagated.add(phi.place.identifier.id);
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
const {value} = instr;
|
||||
switch (value.kind) {
|
||||
case 'StoreLocal': {
|
||||
/**
|
||||
* Nested ternaries can lower to a form with an intermediate StoreLocal where
|
||||
* the value.lvalue is the temporary of the outer ternary, and the value.value
|
||||
* is the result of the inner ternary.
|
||||
*
|
||||
* This is a common pattern in practice and easy enough to support. Again, the
|
||||
* long-term approach is to update InferTypes and InferMutableRanges.
|
||||
*/
|
||||
const lvalue = value.lvalue.place;
|
||||
if (
|
||||
propagated.has(value.value.identifier.id) &&
|
||||
lvalue.identifier.type.kind === 'Type' &&
|
||||
lvalue.identifier.name === null
|
||||
) {
|
||||
lvalue.identifier.type = value.value.identifier.type;
|
||||
propagated.add(lvalue.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,10 +78,6 @@ export default class DisjointSet<T> {
|
||||
return root;
|
||||
}
|
||||
|
||||
has(item: T): boolean {
|
||||
return this.#entries.has(item);
|
||||
}
|
||||
|
||||
/*
|
||||
* Forces the set into canonical form, ie with all items pointing directly to
|
||||
* their root, and returns a Map representing the mapping of items to their roots.
|
||||
|
||||
@@ -1,87 +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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* https://tc39.es/ecma262/multipage/ecmascript-language-lexical-grammar.html#sec-keywords-and-reserved-words
|
||||
*/
|
||||
|
||||
/**
|
||||
* Note: `await` and `yield` are contextually allowed as identifiers.
|
||||
* await: reserved inside async functions and modules
|
||||
* yield: reserved inside generator functions
|
||||
*
|
||||
* Note: `async` is not reserved.
|
||||
*/
|
||||
const RESERVED_WORDS = new Set([
|
||||
'break',
|
||||
'case',
|
||||
'catch',
|
||||
'class',
|
||||
'const',
|
||||
'continue',
|
||||
'debugger',
|
||||
'default',
|
||||
'delete',
|
||||
'do',
|
||||
'else',
|
||||
'enum',
|
||||
'export',
|
||||
'extends',
|
||||
'false',
|
||||
'finally',
|
||||
'for',
|
||||
'function',
|
||||
'if',
|
||||
'import',
|
||||
'in',
|
||||
'instanceof',
|
||||
'new',
|
||||
'null',
|
||||
'return',
|
||||
'super',
|
||||
'switch',
|
||||
'this',
|
||||
'throw',
|
||||
'true',
|
||||
'try',
|
||||
'typeof',
|
||||
'var',
|
||||
'void',
|
||||
'while',
|
||||
'with',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Reserved when a module has a 'use strict' directive.
|
||||
*/
|
||||
const STRICT_MODE_RESERVED_WORDS = new Set([
|
||||
'let',
|
||||
'static',
|
||||
'implements',
|
||||
'interface',
|
||||
'package',
|
||||
'private',
|
||||
'protected',
|
||||
'public',
|
||||
]);
|
||||
/**
|
||||
* The names arguments and eval are not keywords, but they are subject to some restrictions in
|
||||
* strict mode code.
|
||||
*/
|
||||
const STRICT_MODE_RESTRICTED_WORDS = new Set(['eval', 'arguments']);
|
||||
|
||||
/**
|
||||
* Conservative check for whether an identifer name is reserved or not. We assume that code is
|
||||
* written with strict mode.
|
||||
*/
|
||||
export function isReservedWord(identifierName: string): boolean {
|
||||
return (
|
||||
RESERVED_WORDS.has(identifierName) ||
|
||||
STRICT_MODE_RESERVED_WORDS.has(identifierName) ||
|
||||
STRICT_MODE_RESTRICTED_WORDS.has(identifierName)
|
||||
);
|
||||
}
|
||||
@@ -90,13 +90,10 @@ export function Ok<T>(val: T): OkImpl<T> {
|
||||
}
|
||||
|
||||
class OkImpl<T> implements Result<T, never> {
|
||||
#val: T;
|
||||
constructor(val: T) {
|
||||
this.#val = val;
|
||||
}
|
||||
constructor(private val: T) {}
|
||||
|
||||
map<U>(fn: (val: T) => U): Result<U, never> {
|
||||
return new OkImpl(fn(this.#val));
|
||||
return new OkImpl(fn(this.val));
|
||||
}
|
||||
|
||||
mapErr<F>(_fn: (val: never) => F): Result<T, F> {
|
||||
@@ -104,15 +101,15 @@ class OkImpl<T> implements Result<T, never> {
|
||||
}
|
||||
|
||||
mapOr<U>(_fallback: U, fn: (val: T) => U): U {
|
||||
return fn(this.#val);
|
||||
return fn(this.val);
|
||||
}
|
||||
|
||||
mapOrElse<U>(_fallback: () => U, fn: (val: T) => U): U {
|
||||
return fn(this.#val);
|
||||
return fn(this.val);
|
||||
}
|
||||
|
||||
andThen<U>(fn: (val: T) => Result<U, never>): Result<U, never> {
|
||||
return fn(this.#val);
|
||||
return fn(this.val);
|
||||
}
|
||||
|
||||
and<U>(res: Result<U, never>): Result<U, never> {
|
||||
@@ -136,30 +133,30 @@ class OkImpl<T> implements Result<T, never> {
|
||||
}
|
||||
|
||||
expect(_msg: string): T {
|
||||
return this.#val;
|
||||
return this.val;
|
||||
}
|
||||
|
||||
expectErr(msg: string): never {
|
||||
throw new Error(`${msg}: ${this.#val}`);
|
||||
throw new Error(`${msg}: ${this.val}`);
|
||||
}
|
||||
|
||||
unwrap(): T {
|
||||
return this.#val;
|
||||
return this.val;
|
||||
}
|
||||
|
||||
unwrapOr(_fallback: T): T {
|
||||
return this.#val;
|
||||
return this.val;
|
||||
}
|
||||
|
||||
unwrapOrElse(_fallback: (val: never) => T): T {
|
||||
return this.#val;
|
||||
return this.val;
|
||||
}
|
||||
|
||||
unwrapErr(): never {
|
||||
if (this.#val instanceof Error) {
|
||||
throw this.#val;
|
||||
if (this.val instanceof Error) {
|
||||
throw this.val;
|
||||
}
|
||||
throw new Error(`Can't unwrap \`Ok\` to \`Err\`: ${this.#val}`);
|
||||
throw new Error(`Can't unwrap \`Ok\` to \`Err\`: ${this.val}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,17 +165,14 @@ export function Err<E>(val: E): ErrImpl<E> {
|
||||
}
|
||||
|
||||
class ErrImpl<E> implements Result<never, E> {
|
||||
#val: E;
|
||||
constructor(val: E) {
|
||||
this.#val = val;
|
||||
}
|
||||
constructor(private val: E) {}
|
||||
|
||||
map<U>(_fn: (val: never) => U): Result<U, E> {
|
||||
return this;
|
||||
}
|
||||
|
||||
mapErr<F>(fn: (val: E) => F): Result<never, F> {
|
||||
return new ErrImpl(fn(this.#val));
|
||||
return new ErrImpl(fn(this.val));
|
||||
}
|
||||
|
||||
mapOr<U>(fallback: U, _fn: (val: never) => U): U {
|
||||
@@ -202,7 +196,7 @@ class ErrImpl<E> implements Result<never, E> {
|
||||
}
|
||||
|
||||
orElse<F>(fn: (val: E) => ErrImpl<F>): Result<never, F> {
|
||||
return fn(this.#val);
|
||||
return fn(this.val);
|
||||
}
|
||||
|
||||
isOk(): this is OkImpl<never> {
|
||||
@@ -214,18 +208,18 @@ class ErrImpl<E> implements Result<never, E> {
|
||||
}
|
||||
|
||||
expect(msg: string): never {
|
||||
throw new Error(`${msg}: ${this.#val}`);
|
||||
throw new Error(`${msg}: ${this.val}`);
|
||||
}
|
||||
|
||||
expectErr(_msg: string): E {
|
||||
return this.#val;
|
||||
return this.val;
|
||||
}
|
||||
|
||||
unwrap(): never {
|
||||
if (this.#val instanceof Error) {
|
||||
throw this.#val;
|
||||
if (this.val instanceof Error) {
|
||||
throw this.val;
|
||||
}
|
||||
throw new Error(`Can't unwrap \`Err\` to \`Ok\`: ${this.#val}`);
|
||||
throw new Error(`Can't unwrap \`Err\` to \`Ok\`: ${this.val}`);
|
||||
}
|
||||
|
||||
unwrapOr<T>(fallback: T): T {
|
||||
@@ -233,10 +227,10 @@ class ErrImpl<E> implements Result<never, E> {
|
||||
}
|
||||
|
||||
unwrapOrElse<T>(fallback: (val: E) => T): T {
|
||||
return fallback(this.#val);
|
||||
return fallback(this.val);
|
||||
}
|
||||
|
||||
unwrapErr(): E {
|
||||
return this.#val;
|
||||
return this.val;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,21 +75,21 @@ const testComplexConfigDefaults: PartialEnvironmentConfig = {
|
||||
source: 'react',
|
||||
importSpecifierName: 'useEffect',
|
||||
},
|
||||
autodepsIndex: 1,
|
||||
numRequiredArgs: 1,
|
||||
},
|
||||
{
|
||||
function: {
|
||||
source: 'shared-runtime',
|
||||
importSpecifierName: 'useSpecialEffect',
|
||||
},
|
||||
autodepsIndex: 2,
|
||||
numRequiredArgs: 2,
|
||||
},
|
||||
{
|
||||
function: {
|
||||
source: 'useEffectWrapper',
|
||||
importSpecifierName: 'default',
|
||||
},
|
||||
autodepsIndex: 1,
|
||||
numRequiredArgs: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -113,13 +113,8 @@ function* splitPragma(
|
||||
*/
|
||||
function parseConfigPragmaEnvironmentForTest(
|
||||
pragma: string,
|
||||
defaultConfig: PartialEnvironmentConfig,
|
||||
): EnvironmentConfig {
|
||||
// throw early if the defaults are invalid
|
||||
EnvironmentConfigSchema.parse(defaultConfig);
|
||||
|
||||
const maybeConfig: Partial<Record<keyof EnvironmentConfig, unknown>> =
|
||||
defaultConfig;
|
||||
const maybeConfig: Partial<Record<keyof EnvironmentConfig, unknown>> = {};
|
||||
|
||||
for (const {key, value: val} of splitPragma(pragma)) {
|
||||
if (!hasOwnProperty(EnvironmentConfigSchema.shape, key)) {
|
||||
@@ -179,18 +174,9 @@ export function parseConfigPragmaForTests(
|
||||
pragma: string,
|
||||
defaults: {
|
||||
compilationMode: CompilationMode;
|
||||
environment?: PartialEnvironmentConfig;
|
||||
},
|
||||
): PluginOptions {
|
||||
const overridePragma = parseConfigPragmaAsString(pragma);
|
||||
if (overridePragma !== '') {
|
||||
return parseConfigStringAsJS(overridePragma, defaults);
|
||||
}
|
||||
|
||||
const environment = parseConfigPragmaEnvironmentForTest(
|
||||
pragma,
|
||||
defaults.environment ?? {},
|
||||
);
|
||||
const environment = parseConfigPragmaEnvironmentForTest(pragma);
|
||||
const options: Record<keyof PluginOptions, unknown> = {
|
||||
...defaultOptions,
|
||||
panicThreshold: 'all_errors',
|
||||
@@ -222,104 +208,3 @@ export function parseConfigPragmaForTests(
|
||||
}
|
||||
return parsePluginOptions(options);
|
||||
}
|
||||
|
||||
export function parseConfigPragmaAsString(pragma: string): string {
|
||||
// Check if it's in JS override format
|
||||
for (const {key, value: val} of splitPragma(pragma)) {
|
||||
if (key === 'OVERRIDE' && val != null) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function parseConfigStringAsJS(
|
||||
configString: string,
|
||||
defaults: {
|
||||
compilationMode: CompilationMode;
|
||||
environment?: PartialEnvironmentConfig;
|
||||
},
|
||||
): PluginOptions {
|
||||
let parsedConfig: any;
|
||||
try {
|
||||
// Parse the JavaScript object literal
|
||||
parsedConfig = new Function(`return ${configString}`)();
|
||||
} catch (error) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Failed to parse config pragma as JavaScript object',
|
||||
description: `Could not parse: ${configString}. Error: ${error}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
console.log('OVERRIDE:', parsedConfig);
|
||||
|
||||
const options: Record<keyof PluginOptions, unknown> = {
|
||||
...defaultOptions,
|
||||
panicThreshold: 'all_errors',
|
||||
compilationMode: defaults.compilationMode,
|
||||
environment: defaults.environment ?? defaultOptions.environment,
|
||||
};
|
||||
|
||||
// Apply parsed config, merging environment if it exists
|
||||
if (parsedConfig.environment) {
|
||||
const mergedEnvironment = {
|
||||
...(options.environment as Record<string, unknown>),
|
||||
...parsedConfig.environment,
|
||||
};
|
||||
|
||||
// Apply complex defaults for environment flags that are set to true
|
||||
const environmentConfig: Partial<Record<keyof EnvironmentConfig, unknown>> =
|
||||
{};
|
||||
for (const [key, value] of Object.entries(mergedEnvironment)) {
|
||||
if (hasOwnProperty(EnvironmentConfigSchema.shape, key)) {
|
||||
if (value === true && key in testComplexConfigDefaults) {
|
||||
environmentConfig[key] = testComplexConfigDefaults[key];
|
||||
} else {
|
||||
environmentConfig[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate environment config
|
||||
const validatedEnvironment =
|
||||
EnvironmentConfigSchema.safeParse(environmentConfig);
|
||||
if (!validatedEnvironment.success) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Invalid environment configuration in config pragma',
|
||||
description: `${fromZodError(validatedEnvironment.error)}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
if (validatedEnvironment.data.enableResetCacheOnSourceFileChanges == null) {
|
||||
validatedEnvironment.data.enableResetCacheOnSourceFileChanges = false;
|
||||
}
|
||||
|
||||
options.environment = validatedEnvironment.data;
|
||||
}
|
||||
|
||||
// Apply other config options
|
||||
for (const [key, value] of Object.entries(parsedConfig)) {
|
||||
if (key === 'environment') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasOwnProperty(defaultOptions, key)) {
|
||||
if (value === true && key in testComplexPluginOptionDefaults) {
|
||||
options[key] = testComplexPluginOptionDefaults[key];
|
||||
} else if (key === 'target' && value === 'donotuse_meta_internal') {
|
||||
options[key] = {
|
||||
kind: value,
|
||||
runtimeModule: 'react',
|
||||
};
|
||||
} else {
|
||||
options[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parsePluginOptions(options);
|
||||
}
|
||||
|
||||
@@ -33,12 +33,12 @@ export function assertExhaustive(_: never, errorMsg: string): never {
|
||||
// Modifies @param array in place, retaining only the items where the predicate returns true.
|
||||
export function retainWhere<T>(
|
||||
array: Array<T>,
|
||||
predicate: (item: T, index: number) => boolean,
|
||||
predicate: (item: T) => boolean,
|
||||
): void {
|
||||
let writeIndex = 0;
|
||||
for (let readIndex = 0; readIndex < array.length; readIndex++) {
|
||||
const item = array[readIndex];
|
||||
if (predicate(item, readIndex) === true) {
|
||||
if (predicate(item) === true) {
|
||||
array[writeIndex++] = item;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import * as t from '@babel/types';
|
||||
import {
|
||||
CompilerError,
|
||||
CompilerErrorDetail,
|
||||
ErrorCategory,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
|
||||
@@ -125,7 +124,6 @@ export function validateHooksUsage(
|
||||
recordError(
|
||||
place.loc,
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Hooks,
|
||||
description: null,
|
||||
reason,
|
||||
loc: place.loc,
|
||||
@@ -142,7 +140,6 @@ export function validateHooksUsage(
|
||||
recordError(
|
||||
place.loc,
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Hooks,
|
||||
description: null,
|
||||
reason:
|
||||
'Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values',
|
||||
@@ -160,7 +157,6 @@ export function validateHooksUsage(
|
||||
recordError(
|
||||
place.loc,
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Hooks,
|
||||
description: null,
|
||||
reason:
|
||||
'Hooks must be the same function on every render, but this value may change over time to a different function. See https://react.dev/reference/rules/react-calls-components-and-hooks#dont-dynamically-use-hooks',
|
||||
@@ -428,7 +424,7 @@ export function validateHooksUsage(
|
||||
}
|
||||
|
||||
for (const [, error] of errorsByPlace) {
|
||||
errors.pushErrorDetail(error);
|
||||
errors.push(error);
|
||||
}
|
||||
return errors.asResult();
|
||||
}
|
||||
@@ -452,7 +448,6 @@ function visitFunctionExpression(errors: CompilerError, fn: HIRFunction): void {
|
||||
if (hookKind != null) {
|
||||
errors.pushErrorDetail(
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Hooks,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason:
|
||||
'Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)',
|
||||
|
||||
@@ -5,15 +5,14 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {CompilerError, Effect} from '..';
|
||||
import {HIRFunction, IdentifierId, Place} from '../HIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
|
||||
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
|
||||
|
||||
/**
|
||||
* Validates that local variables cannot be reassigned after render.
|
||||
@@ -29,25 +28,16 @@ export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
|
||||
false,
|
||||
);
|
||||
if (reassignment !== null) {
|
||||
const errors = new CompilerError();
|
||||
const variable =
|
||||
reassignment.identifier.name != null &&
|
||||
reassignment.identifier.name.kind === 'named'
|
||||
? `\`${reassignment.identifier.name.value}\``
|
||||
: 'variable';
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Immutability,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'Cannot reassign variable after render completes',
|
||||
description: `Reassigning ${variable} after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: reassignment.loc,
|
||||
message: `Cannot reassign ${variable} after render completes`,
|
||||
}),
|
||||
);
|
||||
throw errors;
|
||||
CompilerError.throwInvalidReact({
|
||||
reason:
|
||||
'Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead',
|
||||
description:
|
||||
reassignment.identifier.name !== null &&
|
||||
reassignment.identifier.name.kind === 'named'
|
||||
? `Variable \`${reassignment.identifier.name.value}\` cannot be reassigned after render`
|
||||
: '',
|
||||
loc: reassignment.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,26 +75,16 @@ function getContextReassignment(
|
||||
// if the function or its depends reassign, propagate that fact on the lvalue
|
||||
if (reassignment !== null) {
|
||||
if (isAsync || value.loweredFunc.func.async) {
|
||||
const errors = new CompilerError();
|
||||
const variable =
|
||||
reassignment.identifier.name !== null &&
|
||||
reassignment.identifier.name.kind === 'named'
|
||||
? `\`${reassignment.identifier.name.value}\``
|
||||
: 'variable';
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Immutability,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'Cannot reassign variable in async function',
|
||||
description:
|
||||
'Reassigning a variable in an async function can cause inconsistent behavior on subsequent renders. Consider using state instead',
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: reassignment.loc,
|
||||
message: `Cannot reassign ${variable}`,
|
||||
}),
|
||||
);
|
||||
throw errors;
|
||||
CompilerError.throwInvalidReact({
|
||||
reason:
|
||||
'Reassigning a variable in an async function can cause inconsistent behavior on subsequent renders. Consider using state instead',
|
||||
description:
|
||||
reassignment.identifier.name !== null &&
|
||||
reassignment.identifier.name.kind === 'named'
|
||||
? `Variable \`${reassignment.identifier.name.value}\` cannot be reassigned after render`
|
||||
: '',
|
||||
loc: reassignment.loc,
|
||||
});
|
||||
}
|
||||
reassigningFunctions.set(lvalue.identifier.id, reassignment);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
*/
|
||||
|
||||
import {CompilerError, ErrorSeverity} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
Identifier,
|
||||
Instruction,
|
||||
@@ -109,7 +108,6 @@ class Visitor extends ReactiveFunctionVisitor<CompilerError> {
|
||||
isUnmemoized(deps.identifier, this.scopes))
|
||||
) {
|
||||
state.push({
|
||||
category: ErrorCategory.EffectDependencies,
|
||||
reason:
|
||||
'React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior',
|
||||
description: null,
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
*/
|
||||
|
||||
import {CompilerError, EnvironmentConfig, ErrorSeverity} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {HIRFunction, IdentifierId} from '../HIR';
|
||||
import {DEFAULT_GLOBALS} from '../HIR/Globals';
|
||||
import {Result} from '../Utils/Result';
|
||||
@@ -57,7 +56,6 @@ export function validateNoCapitalizedCalls(
|
||||
const calleeName = capitalLoadGlobals.get(calleeIdentifier);
|
||||
if (calleeName != null) {
|
||||
CompilerError.throwInvalidReact({
|
||||
category: ErrorCategory.CapitalizedCalls,
|
||||
reason,
|
||||
description: `${calleeName} may be a component.`,
|
||||
loc: value.loc,
|
||||
@@ -81,7 +79,6 @@ export function validateNoCapitalizedCalls(
|
||||
const propertyName = capitalizedProperties.get(propertyIdentifier);
|
||||
if (propertyName != null) {
|
||||
errors.push({
|
||||
category: ErrorCategory.CapitalizedCalls,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason,
|
||||
description: `${propertyName} may be a component.`,
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, ErrorSeverity, SourceLocation} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
ArrayExpression,
|
||||
BlockId,
|
||||
FunctionExpression,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
isSetStateType,
|
||||
isUseEffectHookType,
|
||||
} from '../HIR';
|
||||
import {
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
|
||||
/**
|
||||
* Validates that useEffect is not used for derived computations which could/should
|
||||
* be performed in render.
|
||||
*
|
||||
* See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* // 🔴 Avoid: redundant state and unnecessary Effect
|
||||
* const [fullName, setFullName] = useState('');
|
||||
* useEffect(() => {
|
||||
* setFullName(firstName + ' ' + lastName);
|
||||
* }, [firstName, lastName]);
|
||||
* ```
|
||||
*
|
||||
* Instead use:
|
||||
*
|
||||
* ```
|
||||
* // ✅ Good: calculated during rendering
|
||||
* const fullName = firstName + ' ' + lastName;
|
||||
* ```
|
||||
*/
|
||||
export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
|
||||
const functions: Map<IdentifierId, FunctionExpression> = new Map();
|
||||
const locals: Map<IdentifierId, IdentifierId> = new Map();
|
||||
|
||||
const errors = new CompilerError();
|
||||
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
if (value.kind === 'LoadLocal') {
|
||||
locals.set(lvalue.identifier.id, value.place.identifier.id);
|
||||
} else if (value.kind === 'ArrayExpression') {
|
||||
candidateDependencies.set(lvalue.identifier.id, value);
|
||||
} else if (value.kind === 'FunctionExpression') {
|
||||
functions.set(lvalue.identifier.id, value);
|
||||
} else if (
|
||||
value.kind === 'CallExpression' ||
|
||||
value.kind === 'MethodCall'
|
||||
) {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
if (
|
||||
isUseEffectHookType(callee.identifier) &&
|
||||
value.args.length === 2 &&
|
||||
value.args[0].kind === 'Identifier' &&
|
||||
value.args[1].kind === 'Identifier'
|
||||
) {
|
||||
const effectFunction = functions.get(value.args[0].identifier.id);
|
||||
const deps = candidateDependencies.get(value.args[1].identifier.id);
|
||||
if (
|
||||
effectFunction != null &&
|
||||
deps != null &&
|
||||
deps.elements.length !== 0 &&
|
||||
deps.elements.every(element => element.kind === 'Identifier')
|
||||
) {
|
||||
const dependencies: Array<IdentifierId> = deps.elements.map(dep => {
|
||||
CompilerError.invariant(dep.kind === 'Identifier', {
|
||||
reason: `Dependency is checked as a place above`,
|
||||
loc: value.loc,
|
||||
});
|
||||
return locals.get(dep.identifier.id) ?? dep.identifier.id;
|
||||
});
|
||||
validateEffect(
|
||||
effectFunction.loweredFunc.func,
|
||||
dependencies,
|
||||
errors,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.hasErrors()) {
|
||||
throw errors;
|
||||
}
|
||||
}
|
||||
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction,
|
||||
effectDeps: Array<IdentifierId>,
|
||||
errors: CompilerError,
|
||||
): void {
|
||||
for (const operand of effectFunction.context) {
|
||||
if (isSetStateType(operand.identifier)) {
|
||||
continue;
|
||||
} else if (effectDeps.find(dep => dep === operand.identifier.id) != null) {
|
||||
continue;
|
||||
} else {
|
||||
// Captured something other than the effect dep or setState
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const dep of effectDeps) {
|
||||
if (
|
||||
effectFunction.context.find(operand => operand.identifier.id === dep) ==
|
||||
null
|
||||
) {
|
||||
// effect dep wasn't actually used in the function
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const seenBlocks: Set<BlockId> = new Set();
|
||||
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
|
||||
for (const dep of effectDeps) {
|
||||
values.set(dep, [dep]);
|
||||
}
|
||||
|
||||
const setStateLocations: Array<SourceLocation> = [];
|
||||
for (const block of effectFunction.body.blocks.values()) {
|
||||
for (const pred of block.preds) {
|
||||
if (!seenBlocks.has(pred)) {
|
||||
// skip if block has a back edge
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const phi of block.phis) {
|
||||
const aggregateDeps: Set<IdentifierId> = new Set();
|
||||
for (const operand of phi.operands.values()) {
|
||||
const deps = values.get(operand.identifier.id);
|
||||
if (deps != null) {
|
||||
for (const dep of deps) {
|
||||
aggregateDeps.add(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (aggregateDeps.size !== 0) {
|
||||
values.set(phi.place.identifier.id, Array.from(aggregateDeps));
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
switch (instr.value.kind) {
|
||||
case 'Primitive':
|
||||
case 'JSXText':
|
||||
case 'LoadGlobal': {
|
||||
break;
|
||||
}
|
||||
case 'LoadLocal': {
|
||||
const deps = values.get(instr.value.place.identifier.id);
|
||||
if (deps != null) {
|
||||
values.set(instr.lvalue.identifier.id, deps);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ComputedLoad':
|
||||
case 'PropertyLoad':
|
||||
case 'BinaryExpression':
|
||||
case 'TemplateLiteral':
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
const aggregateDeps: Set<IdentifierId> = new Set();
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
const deps = values.get(operand.identifier.id);
|
||||
if (deps != null) {
|
||||
for (const dep of deps) {
|
||||
aggregateDeps.add(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (aggregateDeps.size !== 0) {
|
||||
values.set(instr.lvalue.identifier.id, Array.from(aggregateDeps));
|
||||
}
|
||||
|
||||
if (
|
||||
instr.value.kind === 'CallExpression' &&
|
||||
isSetStateType(instr.value.callee.identifier) &&
|
||||
instr.value.args.length === 1 &&
|
||||
instr.value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
const deps = values.get(instr.value.args[0].identifier.id);
|
||||
if (deps != null && new Set(deps).size === effectDeps.length) {
|
||||
setStateLocations.push(instr.value.callee.loc);
|
||||
} else {
|
||||
// doesn't depend on any deps
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
if (values.has(operand.identifier.id)) {
|
||||
//
|
||||
return;
|
||||
}
|
||||
}
|
||||
seenBlocks.add(block.id);
|
||||
}
|
||||
|
||||
for (const loc of setStateLocations) {
|
||||
errors.push({
|
||||
category: ErrorCategory.EffectDerivationsOfState,
|
||||
reason:
|
||||
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
|
||||
description: null,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,12 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {CompilerError, Effect, ErrorSeverity} from '..';
|
||||
import {
|
||||
FunctionEffect,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
isMutableEffect,
|
||||
isRefOrRefLikeMutableType,
|
||||
Place,
|
||||
} from '../HIR';
|
||||
@@ -17,8 +18,8 @@ import {
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {AliasingEffect} from '../Inference/AliasingEffects';
|
||||
import {Result} from '../Utils/Result';
|
||||
import {Iterable_some} from '../Utils/utils';
|
||||
|
||||
/**
|
||||
* Validates that functions with known mutations (ie due to types) cannot be passed
|
||||
@@ -49,38 +50,23 @@ export function validateNoFreezingKnownMutableFunctions(
|
||||
const errors = new CompilerError();
|
||||
const contextMutationEffects: Map<
|
||||
IdentifierId,
|
||||
Extract<AliasingEffect, {kind: 'Mutate'} | {kind: 'MutateTransitive'}>
|
||||
Extract<FunctionEffect, {kind: 'ContextMutation'}>
|
||||
> = new Map();
|
||||
|
||||
function visitOperand(operand: Place): void {
|
||||
if (operand.effect === Effect.Freeze) {
|
||||
const effect = contextMutationEffects.get(operand.identifier.id);
|
||||
if (effect != null) {
|
||||
const place = effect.value;
|
||||
const variable =
|
||||
place != null &&
|
||||
place.identifier.name != null &&
|
||||
place.identifier.name.kind === 'named'
|
||||
? `\`${place.identifier.name.value}\``
|
||||
: 'a local variable';
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Immutability,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'Cannot modify local variables after render completes',
|
||||
description: `This argument is a function which may reassign or mutate ${variable} after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.`,
|
||||
})
|
||||
.withDetail({
|
||||
kind: 'error',
|
||||
loc: operand.loc,
|
||||
message: `This function may (indirectly) reassign or modify ${variable} after render`,
|
||||
})
|
||||
.withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `This modifies ${variable}`,
|
||||
}),
|
||||
);
|
||||
errors.push({
|
||||
reason: `This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead`,
|
||||
loc: operand.loc,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
});
|
||||
errors.push({
|
||||
reason: `The function modifies a local variable here`,
|
||||
loc: effect.loc,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,7 +94,27 @@ export function validateNoFreezingKnownMutableFunctions(
|
||||
break;
|
||||
}
|
||||
case 'FunctionExpression': {
|
||||
if (value.loweredFunc.func.aliasingEffects != null) {
|
||||
const knownMutation = (value.loweredFunc.func.effects ?? []).find(
|
||||
effect => {
|
||||
return (
|
||||
effect.kind === 'ContextMutation' &&
|
||||
(effect.effect === Effect.Store ||
|
||||
effect.effect === Effect.Mutate) &&
|
||||
Iterable_some(effect.places, place => {
|
||||
return (
|
||||
isMutableEffect(place.effect, place.loc) &&
|
||||
!isRefOrRefLikeMutableType(place.identifier.type)
|
||||
);
|
||||
})
|
||||
);
|
||||
},
|
||||
);
|
||||
if (knownMutation && knownMutation.kind === 'ContextMutation') {
|
||||
contextMutationEffects.set(lvalue.identifier.id, knownMutation);
|
||||
} else if (
|
||||
fn.env.config.enableNewMutationAliasingModel &&
|
||||
value.loweredFunc.func.aliasingEffects != null
|
||||
) {
|
||||
const context = new Set(
|
||||
value.loweredFunc.func.context.map(p => p.identifier.id),
|
||||
);
|
||||
@@ -129,7 +135,12 @@ export function validateNoFreezingKnownMutableFunctions(
|
||||
context.has(effect.value.identifier.id) &&
|
||||
!isRefOrRefLikeMutableType(effect.value.identifier.type)
|
||||
) {
|
||||
contextMutationEffects.set(lvalue.identifier.id, effect);
|
||||
contextMutationEffects.set(lvalue.identifier.id, {
|
||||
kind: 'ContextMutation',
|
||||
effect: Effect.Mutate,
|
||||
loc: effect.value.loc,
|
||||
places: new Set([effect.value]),
|
||||
});
|
||||
break effects;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -5,10 +5,9 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {CompilerError, ErrorSeverity} from '..';
|
||||
import {HIRFunction} from '../HIR';
|
||||
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
|
||||
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
/**
|
||||
@@ -35,23 +34,17 @@ export function validateNoImpureFunctionsInRender(
|
||||
callee.identifier.type,
|
||||
);
|
||||
if (signature != null && signature.impure === true) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Purity,
|
||||
reason: 'Cannot call impure function during render',
|
||||
description:
|
||||
(signature.canonicalName != null
|
||||
? `\`${signature.canonicalName}\` is an impure function. `
|
||||
: '') +
|
||||
'Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: callee.loc,
|
||||
message: 'Cannot call impure function',
|
||||
}),
|
||||
);
|
||||
errors.push({
|
||||
reason:
|
||||
'Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
|
||||
description:
|
||||
signature.canonicalName != null
|
||||
? `\`${signature.canonicalName}\` is an impure function whose results may change on every call`
|
||||
: null,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc: callee.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {CompilerError, ErrorSeverity} from '..';
|
||||
import {BlockId, HIRFunction} from '../HIR';
|
||||
import {Result} from '../Utils/Result';
|
||||
import {retainWhere} from '../Utils/utils';
|
||||
@@ -35,18 +34,11 @@ export function validateNoJSXInTryStatement(
|
||||
switch (value.kind) {
|
||||
case 'JsxExpression':
|
||||
case 'JsxFragment': {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.ErrorBoundaries,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'Avoid constructing JSX within try/catch',
|
||||
description: `React does not immediately render components when JSX is rendered, so any errors from this component will not be caught by the try/catch. To catch errors in rendering a given component, wrap that component in an error boundary. (https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: value.loc,
|
||||
message: 'Avoid constructing JSX within try/catch',
|
||||
}),
|
||||
);
|
||||
errors.push({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: `Unexpected JSX element within a try statement. To catch errors in rendering a given component, wrap that component in an error boundary. (https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)`,
|
||||
loc: value.loc,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {CompilerError, ErrorSeverity} from '../CompilerError';
|
||||
import {
|
||||
BlockId,
|
||||
HIRFunction,
|
||||
@@ -28,7 +23,6 @@ import {
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {retainWhere} from '../Utils/utils';
|
||||
|
||||
/**
|
||||
* Validates that a function does not access a ref value during render. This includes a partial check
|
||||
@@ -81,18 +75,8 @@ type RefAccessRefType =
|
||||
|
||||
type RefFnType = {readRefEffect: boolean; returnType: RefAccessType};
|
||||
|
||||
class Env {
|
||||
class Env extends Map<IdentifierId, RefAccessType> {
|
||||
#changed = false;
|
||||
#data: Map<IdentifierId, RefAccessType> = new Map();
|
||||
#temporaries: Map<IdentifierId, Place> = new Map();
|
||||
|
||||
lookup(place: Place): Place {
|
||||
return this.#temporaries.get(place.identifier.id) ?? place;
|
||||
}
|
||||
|
||||
define(place: Place, value: Place): void {
|
||||
this.#temporaries.set(place.identifier.id, value);
|
||||
}
|
||||
|
||||
resetChanged(): void {
|
||||
this.#changed = false;
|
||||
@@ -102,14 +86,8 @@ class Env {
|
||||
return this.#changed;
|
||||
}
|
||||
|
||||
get(key: IdentifierId): RefAccessType | undefined {
|
||||
const operandId = this.#temporaries.get(key)?.identifier.id ?? key;
|
||||
return this.#data.get(operandId);
|
||||
}
|
||||
|
||||
set(key: IdentifierId, value: RefAccessType): this {
|
||||
const operandId = this.#temporaries.get(key)?.identifier.id ?? key;
|
||||
const cur = this.#data.get(operandId);
|
||||
override set(key: IdentifierId, value: RefAccessType): this {
|
||||
const cur = this.get(key);
|
||||
const widenedValue = joinRefAccessTypes(value, cur ?? {kind: 'None'});
|
||||
if (
|
||||
!(cur == null && widenedValue.kind === 'None') &&
|
||||
@@ -117,8 +95,7 @@ class Env {
|
||||
) {
|
||||
this.#changed = true;
|
||||
}
|
||||
this.#data.set(operandId, widenedValue);
|
||||
return this;
|
||||
return super.set(key, widenedValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,48 +103,9 @@ export function validateNoRefAccessInRender(
|
||||
fn: HIRFunction,
|
||||
): Result<void, CompilerError> {
|
||||
const env = new Env();
|
||||
collectTemporariesSidemap(fn, env);
|
||||
return validateNoRefAccessInRenderImpl(fn, env).map(_ => undefined);
|
||||
}
|
||||
|
||||
function collectTemporariesSidemap(fn: HIRFunction, env: Env): void {
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
switch (value.kind) {
|
||||
case 'LoadLocal': {
|
||||
const temp = env.lookup(value.place);
|
||||
if (temp != null) {
|
||||
env.define(lvalue, temp);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'StoreLocal': {
|
||||
const temp = env.lookup(value.value);
|
||||
if (temp != null) {
|
||||
env.define(lvalue, temp);
|
||||
env.define(value.lvalue.place, temp);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'PropertyLoad': {
|
||||
if (
|
||||
isUseRefType(value.object.identifier) &&
|
||||
value.property === 'current'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const temp = env.lookup(value.object);
|
||||
if (temp != null) {
|
||||
env.define(lvalue, temp);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function refTypeOfType(place: Place): RefAccessType {
|
||||
if (isRefValueType(place.identifier)) {
|
||||
return {kind: 'RefValue'};
|
||||
@@ -320,27 +258,12 @@ function validateNoRefAccessInRenderImpl(
|
||||
env.set(place.identifier.id, type);
|
||||
}
|
||||
|
||||
const interpolatedAsJsx = new Set<IdentifierId>();
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const instr of block.instructions) {
|
||||
const {value} = instr;
|
||||
if (value.kind === 'JsxExpression' || value.kind === 'JsxFragment') {
|
||||
if (value.children != null) {
|
||||
for (const child of value.children) {
|
||||
interpolatedAsJsx.add(child.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; (i == 0 || env.hasChanged()) && i < 10; i++) {
|
||||
env.resetChanged();
|
||||
returnValues = [];
|
||||
const safeBlocks: Array<{block: BlockId; ref: RefId}> = [];
|
||||
const safeBlocks = new Map<BlockId, RefId>();
|
||||
const errors = new CompilerError();
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
retainWhere(safeBlocks, entry => entry.block !== block.id);
|
||||
for (const phi of block.phis) {
|
||||
env.set(
|
||||
phi.place.identifier.id,
|
||||
@@ -462,80 +385,28 @@ function validateNoRefAccessInRenderImpl(
|
||||
const hookKind = getHookKindForType(fn.env, callee.identifier.type);
|
||||
let returnType: RefAccessType = {kind: 'None'};
|
||||
const fnType = env.get(callee.identifier.id);
|
||||
let didError = false;
|
||||
if (fnType?.kind === 'Structure' && fnType.fn !== null) {
|
||||
returnType = fnType.fn.returnType;
|
||||
if (fnType.fn.readRefEffect) {
|
||||
didError = true;
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: callee.loc,
|
||||
message: `This function accesses a ref value`,
|
||||
}),
|
||||
);
|
||||
errors.push({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason:
|
||||
'This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)',
|
||||
loc: callee.loc,
|
||||
description:
|
||||
callee.identifier.name !== null &&
|
||||
callee.identifier.name.kind === 'named'
|
||||
? `Function \`${callee.identifier.name.value}\` accesses a ref`
|
||||
: null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
/*
|
||||
* If we already reported an error on this instruction, don't report
|
||||
* duplicate errors
|
||||
*/
|
||||
if (!didError) {
|
||||
const isRefLValue = isUseRefType(instr.lvalue.identifier);
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
/**
|
||||
* By default we check that function call operands are not refs,
|
||||
* ref values, or functions that can access refs.
|
||||
*/
|
||||
if (
|
||||
isRefLValue ||
|
||||
(hookKind != null &&
|
||||
hookKind !== 'useState' &&
|
||||
hookKind !== 'useReducer')
|
||||
) {
|
||||
/**
|
||||
* Special cases:
|
||||
*
|
||||
* 1. the lvalue is a ref
|
||||
* In general passing a ref to a function may access that ref
|
||||
* value during render, so we disallow it.
|
||||
*
|
||||
* The main exception is the "mergeRefs" pattern, ie a function
|
||||
* that accepts multiple refs as arguments (or an array of refs)
|
||||
* and returns a new, aggregated ref. If the lvalue is a ref,
|
||||
* we assume that the user is doing this pattern and allow passing
|
||||
* refs.
|
||||
*
|
||||
* Eg `const mergedRef = mergeRefs(ref1, ref2)`
|
||||
*
|
||||
* 2. calling hooks
|
||||
*
|
||||
* Hooks are independently checked to ensure they don't access refs
|
||||
* during render.
|
||||
*/
|
||||
validateNoDirectRefValueAccess(errors, operand, env);
|
||||
} else if (interpolatedAsJsx.has(instr.lvalue.identifier.id)) {
|
||||
/**
|
||||
* Special case: the lvalue is passed as a jsx child
|
||||
*
|
||||
* For example `<Foo>{renderHelper(ref)}</Foo>`. Here we have more
|
||||
* context and infer that the ref is being passed to a component-like
|
||||
* render function which attempts to obey the rules.
|
||||
*/
|
||||
validateNoRefValueAccess(errors, env, operand);
|
||||
} else {
|
||||
validateNoRefPassedToFunction(
|
||||
errors,
|
||||
env,
|
||||
operand,
|
||||
operand.loc,
|
||||
);
|
||||
}
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
if (hookKind != null) {
|
||||
validateNoDirectRefValueAccess(errors, operand, env);
|
||||
} else {
|
||||
validateNoRefAccess(errors, env, operand, operand.loc);
|
||||
}
|
||||
}
|
||||
env.set(instr.lvalue.identifier.id, returnType);
|
||||
@@ -568,39 +439,23 @@ function validateNoRefAccessInRenderImpl(
|
||||
case 'PropertyStore':
|
||||
case 'ComputedDelete':
|
||||
case 'ComputedStore': {
|
||||
const safe = safeBlocks.get(block.id);
|
||||
const target = env.get(instr.value.object.identifier.id);
|
||||
let safe: (typeof safeBlocks)['0'] | null | undefined = null;
|
||||
if (
|
||||
instr.value.kind === 'PropertyStore' &&
|
||||
target != null &&
|
||||
target.kind === 'Ref'
|
||||
safe != null &&
|
||||
target?.kind === 'Ref' &&
|
||||
target.refId === safe
|
||||
) {
|
||||
safe = safeBlocks.find(entry => entry.ref === target.refId);
|
||||
}
|
||||
if (safe != null) {
|
||||
retainWhere(safeBlocks, entry => entry !== safe);
|
||||
safeBlocks.delete(block.id);
|
||||
} else {
|
||||
validateNoRefUpdate(errors, env, instr.value.object, instr.loc);
|
||||
validateNoRefAccess(errors, env, instr.value.object, instr.loc);
|
||||
}
|
||||
if (
|
||||
instr.value.kind === 'ComputedDelete' ||
|
||||
instr.value.kind === 'ComputedStore'
|
||||
) {
|
||||
validateNoRefValueAccess(errors, env, instr.value.property);
|
||||
}
|
||||
if (
|
||||
instr.value.kind === 'ComputedStore' ||
|
||||
instr.value.kind === 'PropertyStore'
|
||||
) {
|
||||
validateNoDirectRefValueAccess(errors, instr.value.value, env);
|
||||
const type = env.get(instr.value.value.identifier.id);
|
||||
if (type != null && type.kind === 'Structure') {
|
||||
let objectType: RefAccessType = type;
|
||||
if (target != null) {
|
||||
objectType = joinRefAccessTypes(objectType, target);
|
||||
}
|
||||
env.set(instr.value.object.identifier.id, objectType);
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
if (operand === instr.value.object) {
|
||||
continue;
|
||||
}
|
||||
validateNoRefValueAccess(errors, env, operand);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -680,11 +535,8 @@ function validateNoRefAccessInRenderImpl(
|
||||
|
||||
if (block.terminal.kind === 'if') {
|
||||
const test = env.get(block.terminal.test.identifier.id);
|
||||
if (
|
||||
test?.kind === 'Guard' &&
|
||||
safeBlocks.find(entry => entry.ref === test.refId) == null
|
||||
) {
|
||||
safeBlocks.push({block: block.terminal.fallthrough, ref: test.refId});
|
||||
if (test?.kind === 'Guard') {
|
||||
safeBlocks.set(block.terminal.consequent, test.refId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -731,18 +583,18 @@ function destructure(
|
||||
|
||||
function guardCheck(errors: CompilerError, operand: Place, env: Env): void {
|
||||
if (env.get(operand.identifier.id)?.kind === 'Guard') {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: operand.loc,
|
||||
message: `Cannot access ref value during render`,
|
||||
}),
|
||||
);
|
||||
errors.push({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason:
|
||||
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
|
||||
loc: operand.loc,
|
||||
description:
|
||||
operand.identifier.name !== null &&
|
||||
operand.identifier.name.kind === 'named'
|
||||
? `Cannot access ref value \`${operand.identifier.name.value}\``
|
||||
: null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -756,22 +608,22 @@ function validateNoRefValueAccess(
|
||||
type?.kind === 'RefValue' ||
|
||||
(type?.kind === 'Structure' && type.fn?.readRefEffect)
|
||||
) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: (type.kind === 'RefValue' && type.loc) || operand.loc,
|
||||
message: `Cannot access ref value during render`,
|
||||
}),
|
||||
);
|
||||
errors.push({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason:
|
||||
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
|
||||
loc: (type.kind === 'RefValue' && type.loc) || operand.loc,
|
||||
description:
|
||||
operand.identifier.name !== null &&
|
||||
operand.identifier.name.kind === 'named'
|
||||
? `Cannot access ref value \`${operand.identifier.name.value}\``
|
||||
: null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function validateNoRefPassedToFunction(
|
||||
function validateNoRefAccess(
|
||||
errors: CompilerError,
|
||||
env: Env,
|
||||
operand: Place,
|
||||
@@ -783,41 +635,18 @@ function validateNoRefPassedToFunction(
|
||||
type?.kind === 'RefValue' ||
|
||||
(type?.kind === 'Structure' && type.fn?.readRefEffect)
|
||||
) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: (type.kind === 'RefValue' && type.loc) || loc,
|
||||
message: `Passing a ref to a function may read its value during render`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateNoRefUpdate(
|
||||
errors: CompilerError,
|
||||
env: Env,
|
||||
operand: Place,
|
||||
loc: SourceLocation,
|
||||
): void {
|
||||
const type = destructure(env.get(operand.identifier.id));
|
||||
if (type?.kind === 'Ref' || type?.kind === 'RefValue') {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: (type.kind === 'RefValue' && type.loc) || loc,
|
||||
message: `Cannot update ref during render`,
|
||||
}),
|
||||
);
|
||||
errors.push({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason:
|
||||
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
|
||||
loc: (type.kind === 'RefValue' && type.loc) || loc,
|
||||
description:
|
||||
operand.identifier.name !== null &&
|
||||
operand.identifier.name.kind === 'named'
|
||||
? `Cannot access ref value \`${operand.identifier.name.value}\``
|
||||
: null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -828,23 +657,17 @@ function validateNoDirectRefValueAccess(
|
||||
): void {
|
||||
const type = destructure(env.get(operand.identifier.id));
|
||||
if (type?.kind === 'RefValue') {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: type.loc ?? operand.loc,
|
||||
message: `Cannot access ref value during render`,
|
||||
}),
|
||||
);
|
||||
errors.push({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason:
|
||||
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
|
||||
loc: type.loc ?? operand.loc,
|
||||
description:
|
||||
operand.identifier.name !== null &&
|
||||
operand.identifier.name.kind === 'named'
|
||||
? `Cannot access ref value \`${operand.identifier.name.value}\``
|
||||
: null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const ERROR_DESCRIPTION =
|
||||
'React refs are values that are not needed for rendering. Refs should only be accessed ' +
|
||||
'outside of render, such as in event handlers or effects. ' +
|
||||
'Accessing a ref value (the `current` property) during render can cause your component ' +
|
||||
'not to update as expected (https://react.dev/reference/react/useRef)';
|
||||
|
||||
@@ -5,33 +5,26 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {CompilerError, ErrorSeverity} from '../CompilerError';
|
||||
import {
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
isSetStateType,
|
||||
isUseEffectHookType,
|
||||
isUseInsertionEffectHookType,
|
||||
isUseLayoutEffectHookType,
|
||||
Place,
|
||||
} from '../HIR';
|
||||
import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
/**
|
||||
* Validates against calling setState in the body of an effect (useEffect and friends),
|
||||
* Validates against calling setState in the body of a *passive* effect (useEffect),
|
||||
* while allowing calling setState in callbacks scheduled by the effect.
|
||||
*
|
||||
* Calling setState during execution of a useEffect triggers a re-render, which is
|
||||
* often bad for performance and frequently has more efficient and straightforward
|
||||
* alternatives. See https://react.dev/learn/you-might-not-need-an-effect for examples.
|
||||
*/
|
||||
export function validateNoSetStateInEffects(
|
||||
export function validateNoSetStateInPassiveEffects(
|
||||
fn: HIRFunction,
|
||||
): Result<void, CompilerError> {
|
||||
const setStateFunctions: Map<IdentifierId, Place> = new Map();
|
||||
@@ -86,36 +79,19 @@ export function validateNoSetStateInEffects(
|
||||
instr.value.kind === 'MethodCall'
|
||||
? instr.value.receiver
|
||||
: instr.value.callee;
|
||||
if (
|
||||
isUseEffectHookType(callee.identifier) ||
|
||||
isUseLayoutEffectHookType(callee.identifier) ||
|
||||
isUseInsertionEffectHookType(callee.identifier)
|
||||
) {
|
||||
if (isUseEffectHookType(callee.identifier)) {
|
||||
const arg = instr.value.args[0];
|
||||
if (arg !== undefined && arg.kind === 'Identifier') {
|
||||
const setState = setStateFunctions.get(arg.identifier.id);
|
||||
if (setState !== undefined) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.EffectSetState,
|
||||
reason:
|
||||
'Calling setState synchronously within an effect can trigger cascading renders',
|
||||
description:
|
||||
'Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. ' +
|
||||
'In general, the body of an effect should do one or both of the following:\n' +
|
||||
'* Update external systems with the latest state from React.\n' +
|
||||
'* Subscribe for updates from some external system, calling setState in a callback function when external state changes.\n\n' +
|
||||
'Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. ' +
|
||||
'(https://react.dev/learn/you-might-not-need-an-effect)',
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: setState.loc,
|
||||
message:
|
||||
'Avoid calling setState() directly within an effect',
|
||||
}),
|
||||
);
|
||||
errors.push({
|
||||
reason:
|
||||
'Calling setState directly within a useEffect causes cascading renders and is not recommended. Consider alternatives to useEffect. (https://react.dev/learn/you-might-not-need-an-effect)',
|
||||
description: null,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc: setState.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {CompilerError, ErrorSeverity} from '../CompilerError';
|
||||
import {HIRFunction, IdentifierId, isSetStateType} from '../HIR';
|
||||
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
|
||||
import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
@@ -127,37 +122,23 @@ function validateNoSetStateInRenderImpl(
|
||||
unconditionalSetStateFunctions.has(callee.identifier.id)
|
||||
) {
|
||||
if (activeManualMemoId !== null) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.RenderSetState,
|
||||
reason:
|
||||
'Calling setState from useMemo may trigger an infinite loop',
|
||||
description:
|
||||
'Each time the memo callback is evaluated it will change state. This can cause a memoization dependency to change, running the memo function again and causing an infinite loop. Instead of setting state in useMemo(), prefer deriving the value during render. (https://react.dev/reference/react/useState)',
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: callee.loc,
|
||||
message: 'Found setState() within useMemo()',
|
||||
}),
|
||||
);
|
||||
errors.push({
|
||||
reason:
|
||||
'Calling setState from useMemo may trigger an infinite loop. (https://react.dev/reference/react/useState)',
|
||||
description: null,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc: callee.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
} else if (unconditionalBlocks.has(block.id)) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.RenderSetState,
|
||||
reason:
|
||||
'Calling setState during render may trigger an infinite loop',
|
||||
description:
|
||||
'Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState)',
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: callee.loc,
|
||||
message: 'Found setState() in render',
|
||||
}),
|
||||
);
|
||||
errors.push({
|
||||
reason:
|
||||
'This is an unconditional set state during render, which will trigger an infinite loop. (https://react.dev/reference/react/useState)',
|
||||
description: null,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc: callee.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -5,12 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {CompilerError, ErrorSeverity} from '../CompilerError';
|
||||
import {
|
||||
DeclarationId,
|
||||
Effect,
|
||||
@@ -280,38 +275,27 @@ function validateInferredDep(
|
||||
errorDiagnostic = merge(errorDiagnostic ?? compareResult, compareResult);
|
||||
}
|
||||
}
|
||||
errorState.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.PreserveManualMemo,
|
||||
severity: ErrorSeverity.CannotPreserveMemoization,
|
||||
reason:
|
||||
'Compilation skipped because existing memoization could not be preserved',
|
||||
description: [
|
||||
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. ',
|
||||
'The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. ',
|
||||
DEBUG ||
|
||||
// If the dependency is a named variable then we can report it. Otherwise only print in debug mode
|
||||
(dep.identifier.name != null && dep.identifier.name.kind === 'named')
|
||||
? `The inferred dependency was \`${prettyPrintScopeDependency(
|
||||
dep,
|
||||
)}\`, but the source dependencies were [${validDepsInMemoBlock
|
||||
.map(dep => printManualMemoDependency(dep, true))
|
||||
.join(', ')}]. ${
|
||||
errorDiagnostic
|
||||
? getCompareDependencyResultDescription(errorDiagnostic)
|
||||
: 'Inferred dependency not present in source'
|
||||
}.`
|
||||
: '',
|
||||
]
|
||||
.join('')
|
||||
.trim(),
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: memoLocation,
|
||||
message: 'Could not preserve existing manual memoization',
|
||||
}),
|
||||
);
|
||||
errorState.push({
|
||||
severity: ErrorSeverity.CannotPreserveMemoization,
|
||||
reason:
|
||||
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected',
|
||||
description:
|
||||
DEBUG ||
|
||||
// If the dependency is a named variable then we can report it. Otherwise only print in debug mode
|
||||
(dep.identifier.name != null && dep.identifier.name.kind === 'named')
|
||||
? `The inferred dependency was \`${prettyPrintScopeDependency(
|
||||
dep,
|
||||
)}\`, but the source dependencies were [${validDepsInMemoBlock
|
||||
.map(dep => printManualMemoDependency(dep, true))
|
||||
.join(', ')}]. ${
|
||||
errorDiagnostic
|
||||
? getCompareDependencyResultDescription(errorDiagnostic)
|
||||
: 'Inferred dependency not present in source'
|
||||
}`
|
||||
: null,
|
||||
loc: memoLocation,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
@@ -535,22 +519,14 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
!this.scopes.has(identifier.scope.id) &&
|
||||
!this.prunedScopes.has(identifier.scope.id)
|
||||
) {
|
||||
state.errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.PreserveManualMemo,
|
||||
severity: ErrorSeverity.CannotPreserveMemoization,
|
||||
reason:
|
||||
'Compilation skipped because existing memoization could not be preserved',
|
||||
description: [
|
||||
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. ',
|
||||
'This dependency may be mutated later, which could cause the value to change unexpectedly.',
|
||||
].join(''),
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc,
|
||||
message: 'This dependency may be modified later',
|
||||
}),
|
||||
);
|
||||
state.errors.push({
|
||||
reason:
|
||||
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly',
|
||||
description: null,
|
||||
severity: ErrorSeverity.CannotPreserveMemoization,
|
||||
loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -584,26 +560,16 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
|
||||
for (const identifier of decls) {
|
||||
if (isUnmemoized(identifier, this.scopes)) {
|
||||
state.errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.PreserveManualMemo,
|
||||
severity: ErrorSeverity.CannotPreserveMemoization,
|
||||
reason:
|
||||
'Compilation skipped because existing memoization could not be preserved',
|
||||
description: [
|
||||
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. ',
|
||||
DEBUG
|
||||
? `${printIdentifier(identifier)} was not memoized.`
|
||||
: '',
|
||||
]
|
||||
.join('')
|
||||
.trim(),
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc,
|
||||
message: 'Could not preserve existing memoization',
|
||||
}),
|
||||
);
|
||||
state.errors.push({
|
||||
reason:
|
||||
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.',
|
||||
description: DEBUG
|
||||
? `${printIdentifier(identifier)} was not memoized`
|
||||
: null,
|
||||
severity: ErrorSeverity.CannotPreserveMemoization,
|
||||
loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {CompilerError, ErrorSeverity} from '../CompilerError';
|
||||
import {HIRFunction, IdentifierId, SourceLocation} from '../HIR';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
@@ -64,24 +59,20 @@ export function validateStaticComponents(
|
||||
value.tag.identifier.id,
|
||||
);
|
||||
if (location != null) {
|
||||
error.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.StaticComponents,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'Cannot create components during render',
|
||||
description: `Components created during render will reset their state each time they are created. Declare components outside of render. `,
|
||||
})
|
||||
.withDetail({
|
||||
kind: 'error',
|
||||
loc: value.tag.loc,
|
||||
message: 'This component is created during render',
|
||||
})
|
||||
.withDetail({
|
||||
kind: 'error',
|
||||
loc: location,
|
||||
message: 'The component is created during render here',
|
||||
}),
|
||||
);
|
||||
error.push({
|
||||
reason: `Components created during render will reset their state each time they are created. Declare components outside of render. `,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc: value.tag.loc,
|
||||
description: null,
|
||||
suggestions: null,
|
||||
});
|
||||
error.push({
|
||||
reason: `The component may be created during render`,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc: location,
|
||||
description: null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {CompilerError, ErrorSeverity} from '..';
|
||||
import {FunctionExpression, HIRFunction, IdentifierId} from '../HIR';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
@@ -68,43 +63,24 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
}
|
||||
|
||||
if (body.loweredFunc.func.params.length > 0) {
|
||||
const firstParam = body.loweredFunc.func.params[0];
|
||||
const loc =
|
||||
firstParam.kind === 'Identifier'
|
||||
? firstParam.loc
|
||||
: firstParam.place.loc;
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'useMemo() callbacks may not accept parameters',
|
||||
description:
|
||||
'useMemo() callbacks are called by React to cache calculations across re-renders. They should not take parameters. Instead, directly reference the props, state, or local variables needed for the computation.',
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc,
|
||||
message: 'Callbacks with parameters are not supported',
|
||||
}),
|
||||
);
|
||||
errors.push({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'useMemo callbacks may not accept any arguments',
|
||||
description: null,
|
||||
loc: body.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
if (body.loweredFunc.func.async || body.loweredFunc.func.generator) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason:
|
||||
'useMemo() callbacks may not be async or generator functions',
|
||||
description:
|
||||
'useMemo() callbacks are called once and must synchronously return a value.',
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: body.loc,
|
||||
message: 'Async and generator functions are not supported',
|
||||
}),
|
||||
);
|
||||
errors.push({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason:
|
||||
'useMemo callbacks may not be async or generator functions',
|
||||
description: null,
|
||||
loc: body.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
@@ -58,8 +58,7 @@ it('logs failed compilation', () => {
|
||||
|
||||
expect(event.detail.severity).toEqual('InvalidReact');
|
||||
//@ts-ignore
|
||||
const {start, end, identifierName} =
|
||||
event.detail.primaryLocation() as t.SourceLocation;
|
||||
const {start, end, identifierName} = event.detail.loc as t.SourceLocation;
|
||||
expect(start).toEqual({column: 28, index: 28, line: 1});
|
||||
expect(end).toEqual({column: 33, index: 33, line: 1});
|
||||
expect(identifierName).toEqual('props');
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('parseConfigPragma()', () => {
|
||||
validateHooksUsage: 1,
|
||||
} as any);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: Expected boolean, received number at "validateHooksUsage"."`,
|
||||
`"InvalidConfig: Could not validate environment config. Update React Compiler config to fix the error. Validation error: Expected boolean, received number at "validateHooksUsage""`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -33,12 +33,12 @@ describe('parseConfigPragma()', () => {
|
||||
source: 'react',
|
||||
importSpecifierName: 'useEffect',
|
||||
},
|
||||
autodepsIndex: 0,
|
||||
numRequiredArgs: 0,
|
||||
},
|
||||
],
|
||||
} as any);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: autodepsIndex must be > 0 at "inferEffectDependencies[0].autodepsIndex"."`,
|
||||
`"InvalidConfig: Could not validate environment config. Update React Compiler config to fix the error. Validation error: numRequiredArgs must be > 0 at "inferEffectDependencies[0].numRequiredArgs""`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useRef} from 'react';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const ref = useRef(props.value);
|
||||
const object = {};
|
||||
object.foo = () => ref.current;
|
||||
return <Stringify object={object} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useRef } from "react";
|
||||
import { Stringify } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(1);
|
||||
const ref = useRef(props.value);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
const object = {};
|
||||
object.foo = () => ref.current;
|
||||
t0 = <Stringify object={object} shouldInvokeFns={true} />;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: 42 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"object":{"foo":{"kind":"Function","result":42}},"shouldInvokeFns":true}</div>
|
||||
@@ -1,14 +0,0 @@
|
||||
import {useRef} from 'react';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const ref = useRef(props.value);
|
||||
const object = {};
|
||||
object.foo = () => ref.current;
|
||||
return <Stringify object={object} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
function Component() {
|
||||
const foo = () => {
|
||||
someGlobal = true;
|
||||
};
|
||||
// spreading a function is weird, but it doesn't call the function so this is allowed
|
||||
return <div {...foo} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
|
||||
function Component() {
|
||||
const $ = _c(1);
|
||||
const foo = _temp;
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <div {...foo} />;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
function _temp() {
|
||||
someGlobal = true;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -1,44 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
|
||||
|
||||
import {useRef} from 'react';
|
||||
|
||||
function Component() {
|
||||
const ref = useRef(null);
|
||||
const ref2 = useRef(null);
|
||||
const mergedRef = mergeRefs([ref], ref2);
|
||||
|
||||
return <Stringify ref={mergedRef} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
|
||||
|
||||
import { useRef } from "react";
|
||||
|
||||
function Component() {
|
||||
const $ = _c(1);
|
||||
const ref = useRef(null);
|
||||
const ref2 = useRef(null);
|
||||
const mergedRef = mergeRefs([ref], ref2);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <Stringify ref={mergedRef} />;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -1,11 +0,0 @@
|
||||
// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
|
||||
|
||||
import {useRef} from 'react';
|
||||
|
||||
function Component() {
|
||||
const ref = useRef(null);
|
||||
const ref2 = useRef(null);
|
||||
const mergedRef = mergeRefs([ref], ref2);
|
||||
|
||||
return <Stringify ref={mergedRef} />;
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
|
||||
|
||||
import {useRef} from 'react';
|
||||
|
||||
function Component(props) {
|
||||
const ref = useRef(null);
|
||||
|
||||
return <Foo>{props.render({ref})}</Foo>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
|
||||
|
||||
import { useRef } from "react";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(3);
|
||||
const ref = useRef(null);
|
||||
|
||||
const T0 = Foo;
|
||||
const t0 = props.render({ ref });
|
||||
let t1;
|
||||
if ($[0] !== T0 || $[1] !== t0) {
|
||||
t1 = <T0>{t0}</T0>;
|
||||
$[0] = T0;
|
||||
$[1] = t0;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -1,9 +0,0 @@
|
||||
// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
|
||||
|
||||
import {useRef} from 'react';
|
||||
|
||||
function Component(props) {
|
||||
const ref = useRef(null);
|
||||
|
||||
return <Foo>{props.render({ref})}</Foo>;
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
|
||||
|
||||
import {useRef} from 'react';
|
||||
|
||||
function Component(props) {
|
||||
const ref = useRef(null);
|
||||
|
||||
return <Foo>{props.render(ref)}</Foo>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
|
||||
|
||||
import { useRef } from "react";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(4);
|
||||
const ref = useRef(null);
|
||||
let t0;
|
||||
if ($[0] !== props.render) {
|
||||
t0 = props.render(ref);
|
||||
$[0] = props.render;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
let t1;
|
||||
if ($[2] !== t0) {
|
||||
t1 = <Foo>{t0}</Foo>;
|
||||
$[2] = t0;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -1,9 +0,0 @@
|
||||
// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
|
||||
|
||||
import {useRef} from 'react';
|
||||
|
||||
function Component(props) {
|
||||
const ref = useRef(null);
|
||||
|
||||
return <Foo>{props.render(ref)}</Foo>;
|
||||
}
|
||||
@@ -27,7 +27,6 @@ function Component() {
|
||||
}
|
||||
|
||||
function Child({ref}) {
|
||||
'use no memo';
|
||||
// This violates the rules of React, so we access the ref in a child
|
||||
// component
|
||||
return ref.current;
|
||||
@@ -101,10 +100,8 @@ function Component() {
|
||||
return t6;
|
||||
}
|
||||
|
||||
function Child({ ref }) {
|
||||
"use no memo";
|
||||
// This violates the rules of React, so we access the ref in a child
|
||||
// component
|
||||
function Child(t0) {
|
||||
const { ref } = t0;
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ function Component() {
|
||||
}
|
||||
|
||||
function Child({ref}) {
|
||||
'use no memo';
|
||||
// This violates the rules of React, so we access the ref in a child
|
||||
// component
|
||||
return ref.current;
|
||||
|
||||
@@ -23,7 +23,6 @@ function Component() {
|
||||
}
|
||||
|
||||
function Child({ref}) {
|
||||
'use no memo';
|
||||
// This violates the rules of React, so we access the ref in a child
|
||||
// component
|
||||
return ref.current;
|
||||
@@ -87,10 +86,8 @@ function Component() {
|
||||
return t5;
|
||||
}
|
||||
|
||||
function Child({ ref }) {
|
||||
"use no memo";
|
||||
// This violates the rules of React, so we access the ref in a child
|
||||
// component
|
||||
function Child(t0) {
|
||||
const { ref } = t0;
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ function Component() {
|
||||
}
|
||||
|
||||
function Child({ref}) {
|
||||
'use no memo';
|
||||
// This violates the rules of React, so we access the ref in a child
|
||||
// component
|
||||
return ref.current;
|
||||
|
||||
@@ -25,7 +25,6 @@ function Component() {
|
||||
}
|
||||
|
||||
function Child({ref}) {
|
||||
'use no memo';
|
||||
// This violates the rules of React, so we access the ref in a child
|
||||
// component
|
||||
return ref.current;
|
||||
@@ -84,10 +83,8 @@ function Component() {
|
||||
}
|
||||
function _temp() {}
|
||||
|
||||
function Child({ ref }) {
|
||||
"use no memo";
|
||||
// This violates the rules of React, so we access the ref in a child
|
||||
// component
|
||||
function Child(t0) {
|
||||
const { ref } = t0;
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ function Component() {
|
||||
}
|
||||
|
||||
function Child({ref}) {
|
||||
'use no memo';
|
||||
// This violates the rules of React, so we access the ref in a child
|
||||
// component
|
||||
return ref.current;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user