Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
720aea5811 | ||
|
|
27b2c46a53 | ||
|
|
6dc956a396 |
@@ -474,7 +474,7 @@ module.exports = {
|
||||
{
|
||||
files: ['packages/react-server-dom-turbopack/**/*.js'],
|
||||
globals: {
|
||||
__turbopack_load_by_url__: 'readonly',
|
||||
__turbopack_load__: 'readonly',
|
||||
__turbopack_require__: 'readonly',
|
||||
},
|
||||
},
|
||||
@@ -608,7 +608,6 @@ module.exports = {
|
||||
symbol: 'readonly',
|
||||
SyntheticEvent: 'readonly',
|
||||
SyntheticMouseEvent: 'readonly',
|
||||
SyntheticPointerEvent: 'readonly',
|
||||
Thenable: 'readonly',
|
||||
TimeoutID: 'readonly',
|
||||
WheelEventHandler: 'readonly',
|
||||
@@ -623,7 +622,6 @@ module.exports = {
|
||||
ScrollTimeline: 'readonly',
|
||||
EventListenerOptionsOrUseCapture: 'readonly',
|
||||
FocusOptions: 'readonly',
|
||||
OptionalEffectTiming: 'readonly',
|
||||
|
||||
spyOnDev: 'readonly',
|
||||
spyOnDevAndProd: 'readonly',
|
||||
|
||||
54
.github/workflows/runtime_build_and_test.yml
vendored
54
.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'
|
||||
@@ -307,12 +301,10 @@ jobs:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
|
||||
- name: Install runtime dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- name: Install compiler dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
working-directory: compiler
|
||||
restore-keys: |
|
||||
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-and-compiler-node_modules-v6-
|
||||
- run: yarn install --frozen-lockfile
|
||||
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
|
||||
@@ -331,7 +323,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 +418,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 +463,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 +491,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 +528,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 +568,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 +605,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 +646,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 +720,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 +777,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'
|
||||
@@ -830,7 +822,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'
|
||||
@@ -879,7 +871,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
|
||||
|
||||
8
.github/workflows/runtime_prereleases.yml
vendored
8
.github/workflows/runtime_prereleases.yml
vendored
@@ -85,7 +85,7 @@ jobs:
|
||||
--skipTests \
|
||||
--tags=${{ inputs.dist_tag }} \
|
||||
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry' || '' }}
|
||||
${{ inputs.dry && '--dry'}}
|
||||
- if: '${{ inputs.skip_packages }}'
|
||||
name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}'
|
||||
run: |
|
||||
@@ -94,19 +94,19 @@ jobs:
|
||||
--skipTests \
|
||||
--tags=${{ inputs.dist_tag }} \
|
||||
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry' || '' }}
|
||||
${{ inputs.dry && '--dry'}}
|
||||
- if: '${{ !(inputs.skip_packages && inputs.only_packages) }}'
|
||||
name: 'Publish all packages'
|
||||
run: |
|
||||
scripts/release/publish.js \
|
||||
--ci \
|
||||
--tags=${{ inputs.dist_tag }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry' || '' }}
|
||||
${{ inputs.dry && '--dry'}}
|
||||
- name: Notify Discord on failure
|
||||
if: failure() && inputs.enableFailureNotification == true
|
||||
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
embed-author-name: "GitHub Actions"
|
||||
embed-title: '[Runtime] Publish of ${{ inputs.release_channel }}@${{ inputs.dist_tag}} release failed'
|
||||
embed-title: 'Publish of $${{ inputs.release_channel }} release failed'
|
||||
embed-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}
|
||||
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
--tags=${{ inputs.tags }} \
|
||||
--publishVersion=${{ inputs.version_to_publish }} \
|
||||
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry' || '' }}
|
||||
${{ inputs.dry && '--dry'}}
|
||||
- if: '${{ inputs.skip_packages }}'
|
||||
name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}'
|
||||
run: |
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
--tags=${{ inputs.tags }} \
|
||||
--publishVersion=${{ inputs.version_to_publish }} \
|
||||
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry' || '' }}
|
||||
${{ inputs.dry && '--dry'}}
|
||||
- name: Archive released package for debugging
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
@@ -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
|
||||
@@ -24,11 +19,11 @@ An Owner Stack is a string representing the components that are directly respons
|
||||
* Updated `useId` to use valid CSS selectors, changing format from `:r123:` to `«r123»`. [#32001](https://github.com/facebook/react/pull/32001)
|
||||
* Added a dev-only warning for null/undefined created in useEffect, useInsertionEffect, and useLayoutEffect. [#32355](https://github.com/facebook/react/pull/32355)
|
||||
* Fixed a bug where dev-only methods were exported in production builds. React.act is no longer available in production builds. [#32200](https://github.com/facebook/react/pull/32200)
|
||||
* Improved consistency across prod and dev to improve compatibility with Google Closure Compiler and bindings [#31808](https://github.com/facebook/react/pull/31808)
|
||||
* Improved consistency across prod and dev to improve compatibility with Google Closure Complier and bindings [#31808](https://github.com/facebook/react/pull/31808)
|
||||
* Improve passive effect scheduling for consistent task yielding. [#31785](https://github.com/facebook/react/pull/31785)
|
||||
* Fixed asserts in React Native when passChildrenWhenCloningPersistedNodes is enabled for OffscreenComponent rendering. [#32528](https://github.com/facebook/react/pull/32528)
|
||||
* Fixed component name resolution for Portal [#32640](https://github.com/facebook/react/pull/32640)
|
||||
* Added support for beforetoggle and toggle events on the dialog element. [#32479](https://github.com/facebook/react/pull/32479)
|
||||
* Added support for beforetoggle and toggle events on the dialog element. #32479 [#32479](https://github.com/facebook/react/pull/32479)
|
||||
|
||||
### React DOM
|
||||
* Fixed double warning when the `href` attribute is an empty string [#31783](https://github.com/facebook/react/pull/31783)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { c as _c } from "react/compiler-runtime"; //
|
||||
@compilationMode:"all"
|
||||
@compilationMode:"all"
|
||||
function nonReactFn() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
|
||||
@@ -11,7 +11,6 @@ import * as t from '@babel/types';
|
||||
import BabelPluginReactCompiler, {
|
||||
CompilerError,
|
||||
CompilerErrorDetail,
|
||||
CompilerDiagnostic,
|
||||
Effect,
|
||||
ErrorSeverity,
|
||||
parseConfigPragmaForTests,
|
||||
@@ -45,7 +44,6 @@ import {
|
||||
PrintedCompilerPipelineValue,
|
||||
} from './Output';
|
||||
import {transformFromAstSync} from '@babel/core';
|
||||
import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint';
|
||||
|
||||
function parseInput(
|
||||
input: string,
|
||||
@@ -142,13 +140,9 @@ 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 upsert: (result: PrintedCompilerPipelineValue) => void = result => {
|
||||
const entry = results.get(result.name);
|
||||
if (Array.isArray(entry)) {
|
||||
@@ -207,23 +201,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,
|
||||
@@ -233,11 +210,7 @@ function compile(
|
||||
},
|
||||
logger: {
|
||||
debugLogIRs: logIR,
|
||||
logEvent: (_filename: string | null, event: LoggerEvent) => {
|
||||
if (event.kind === 'CompileError') {
|
||||
otherErrors.push(event.detail);
|
||||
}
|
||||
},
|
||||
logEvent: () => {},
|
||||
},
|
||||
});
|
||||
transformOutput = invokeCompiler(source, language, opts);
|
||||
@@ -247,7 +220,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)
|
||||
@@ -264,17 +237,10 @@ 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));
|
||||
}
|
||||
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 {
|
||||
@@ -283,11 +249,7 @@ export default function Editor(): JSX.Element {
|
||||
const dispatchStore = useStoreDispatch();
|
||||
const {enqueueSnackbar} = useSnackbar();
|
||||
const [compilerOutput, language] = useMemo(
|
||||
() => compile(deferredStore.source, 'compiler'),
|
||||
[deferredStore.source],
|
||||
);
|
||||
const [linterOutput] = useMemo(
|
||||
() => compile(deferredStore.source, 'linter'),
|
||||
() => compile(deferredStore.source),
|
||||
[deferredStore.source],
|
||||
);
|
||||
|
||||
@@ -313,26 +275,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">
|
||||
<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;
|
||||
}
|
||||
|
||||
@@ -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,8 +5,6 @@
|
||||
* 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 {Err, Ok, Result} from './Utils/Result';
|
||||
import {assertExhaustive} from './Utils/utils';
|
||||
@@ -17,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.
|
||||
*/
|
||||
@@ -46,24 +39,6 @@ export enum ErrorSeverity {
|
||||
Invariant = 'Invariant',
|
||||
}
|
||||
|
||||
export type CompilerDiagnosticOptions = {
|
||||
severity: ErrorSeverity;
|
||||
category: 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;
|
||||
};
|
||||
|
||||
export enum CompilerSuggestionOperation {
|
||||
InsertBefore,
|
||||
InsertAfter,
|
||||
@@ -94,103 +69,6 @@ export type CompilerErrorDetailOptions = {
|
||||
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 category(): CompilerDiagnosticOptions['category'] {
|
||||
return this.options.category;
|
||||
}
|
||||
get description(): CompilerDiagnosticOptions['description'] {
|
||||
return this.options.description;
|
||||
}
|
||||
get severity(): CompilerDiagnosticOptions['severity'] {
|
||||
return this.options.severity;
|
||||
}
|
||||
get suggestions(): CompilerDiagnosticOptions['suggestions'] {
|
||||
return this.options.suggestions;
|
||||
}
|
||||
|
||||
withDetail(detail: CompilerDiagnosticDetail): CompilerDiagnostic {
|
||||
this.options.details.push(detail);
|
||||
return this;
|
||||
}
|
||||
|
||||
primaryLocation(): SourceLocation | null {
|
||||
return this.options.details.filter(d => d.kind === 'error')[0]?.loc ?? null;
|
||||
}
|
||||
|
||||
printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
|
||||
const buffer = [
|
||||
printErrorSummary(this.severity, this.category),
|
||||
'\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;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
detail.kind,
|
||||
`Unexpected detail kind ${(detail as any).kind}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return buffer.join('');
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
const buffer = [printErrorSummary(this.severity, this.category)];
|
||||
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.
|
||||
@@ -218,51 +96,24 @@ export class CompilerErrorDetail {
|
||||
return this.options.suggestions;
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -280,12 +131,6 @@ 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'>,
|
||||
): never {
|
||||
@@ -348,49 +193,18 @@ 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({
|
||||
reason: options.reason,
|
||||
@@ -427,69 +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}`;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -94,7 +94,7 @@ import {validateLocalsNotReassignedAfterRender} from '../Validation/ValidateLoca
|
||||
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';
|
||||
@@ -174,7 +174,7 @@ function runWithEnvironment(
|
||||
!env.config.disableMemoizationForDebugging &&
|
||||
!env.config.enableChangeDetectionForDebugging
|
||||
) {
|
||||
dropManualMemoization(hir).unwrap();
|
||||
dropManualMemoization(hir);
|
||||
log({kind: 'hir', name: 'DropManualMemoization', value: hir});
|
||||
}
|
||||
|
||||
@@ -292,8 +292,8 @@ function runWithEnvironment(
|
||||
validateNoSetStateInRender(hir).unwrap();
|
||||
}
|
||||
|
||||
if (env.config.validateNoSetStateInEffects) {
|
||||
env.logErrors(validateNoSetStateInEffects(hir));
|
||||
if (env.config.validateNoSetStateInPassiveEffects) {
|
||||
env.logErrors(validateNoSetStateInPassiveEffects(hir));
|
||||
}
|
||||
|
||||
if (env.config.validateNoJSXInTryStatements) {
|
||||
|
||||
@@ -181,7 +181,7 @@ function logError(
|
||||
context.opts.logger.logEvent(context.filename, {
|
||||
kind: 'CompileError',
|
||||
fnLoc,
|
||||
detail,
|
||||
detail: detail.options,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
import {NodePath} from '@babel/core';
|
||||
import * as t from '@babel/types';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
CompilerErrorDetail,
|
||||
CompilerSuggestionOperation,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
@@ -181,11 +181,12 @@ export function suppressionsToCompilerError(
|
||||
'Unhandled suppression source',
|
||||
);
|
||||
}
|
||||
error.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: reason,
|
||||
description: `React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression \`${suppressionRange.disableComment.value.trim()}\``,
|
||||
error.pushErrorDetail(
|
||||
new CompilerErrorDetail({
|
||||
reason: `${reason}. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior`,
|
||||
description: suppressionRange.disableComment.value.trim(),
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc: suppressionRange.disableComment.loc ?? null,
|
||||
suggestions: [
|
||||
{
|
||||
description: suggestion,
|
||||
@@ -196,10 +197,6 @@ export function suppressionsToCompilerError(
|
||||
op: CompilerSuggestionOperation.Remove,
|
||||
},
|
||||
],
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: suppressionRange.disableComment.loc ?? null,
|
||||
message: 'Found React rule suppression',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,63 +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} 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 {
|
||||
@@ -77,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,
|
||||
@@ -92,18 +65,14 @@ function assertValidEffectImportReference(
|
||||
*/
|
||||
throwInvalidReact(
|
||||
{
|
||||
category:
|
||||
'Cannot infer dependencies of this effect. This will break your build!',
|
||||
description:
|
||||
'To resolve, either pass a dependency array or fix reported compiler bailout diagnostics.' +
|
||||
(maybeErrorDiagnostic ? ` ${maybeErrorDiagnostic}` : ''),
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
message: 'Cannot infer dependencies',
|
||||
loc: parent.node.loc ?? GeneratedSource,
|
||||
},
|
||||
],
|
||||
reason:
|
||||
'[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,
|
||||
);
|
||||
@@ -123,20 +92,13 @@ function assertValidFireImportReference(
|
||||
);
|
||||
throwInvalidReact(
|
||||
{
|
||||
category:
|
||||
'[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,
|
||||
);
|
||||
@@ -166,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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import {NodePath, Scope} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import invariant from 'invariant';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
CompilerSuggestionOperation,
|
||||
ErrorSeverity,
|
||||
@@ -105,17 +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: '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 = {
|
||||
@@ -169,17 +163,12 @@ export function lower(
|
||||
'Assignment',
|
||||
);
|
||||
} else {
|
||||
builder.errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: `Handle ${param.node.type} parameters`,
|
||||
description: `[BuildHIR] Add support for ${param.node.type} parameters.`,
|
||||
}).withDetail({
|
||||
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,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -189,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),
|
||||
@@ -200,17 +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: `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()) {
|
||||
@@ -220,7 +204,6 @@ export function lower(
|
||||
builder.terminate(
|
||||
{
|
||||
kind: 'return',
|
||||
returnVariant: 'Void',
|
||||
loc: GeneratedSource,
|
||||
value: lowerValueToTemporary(builder, {
|
||||
kind: 'Primitive',
|
||||
@@ -238,6 +221,7 @@ export function lower(
|
||||
params,
|
||||
fnType: bindings == null ? env.fnType : 'Other',
|
||||
returnTypeAnnotation: null, // TODO: extract the actual return type node if present
|
||||
returnType: makeType(),
|
||||
returns: createTemporaryPlace(env, func.node.loc ?? GeneratedSource),
|
||||
body: builder.build(),
|
||||
context,
|
||||
@@ -304,7 +288,6 @@ function lowerStatement(
|
||||
}
|
||||
const terminal: ReturnTerminal = {
|
||||
kind: 'return',
|
||||
returnVariant: 'Explicit',
|
||||
loc: stmt.node.loc ?? GeneratedSource,
|
||||
value,
|
||||
id: makeInstructionId(0),
|
||||
@@ -1373,85 +1356,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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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':
|
||||
@@ -1462,14 +1373,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: {
|
||||
@@ -2289,17 +2217,11 @@ function lowerExpression(
|
||||
});
|
||||
for (const [name, locations] of Object.entries(fbtLocations)) {
|
||||
if (locations.length > 1) {
|
||||
CompilerError.throwDiagnostic({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: 'Support duplicate fbt tags',
|
||||
description: `Support \`<${tagName}>\` tags with multiple \`<${tagName}:${name}>\` values`,
|
||||
details: locations.map(loc => {
|
||||
return {
|
||||
kind: 'error',
|
||||
message: `Multiple \`<${tagName}:${name}>\` tags found`,
|
||||
loc,
|
||||
};
|
||||
}),
|
||||
CompilerError.throwTodo({
|
||||
reason: `Support <${tagName}> tags with multiple <${tagName}:${name}> values`,
|
||||
loc: locations.at(-1) ?? GeneratedSource,
|
||||
description: null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3025,8 +2947,6 @@ function isReorderableExpression(
|
||||
}
|
||||
}
|
||||
}
|
||||
case 'TSAsExpression':
|
||||
case 'TSNonNullExpression':
|
||||
case 'TypeCastExpression': {
|
||||
return isReorderableExpression(
|
||||
builder,
|
||||
@@ -3527,8 +3447,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();
|
||||
@@ -3582,16 +3503,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,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
return lowerValueToTemporary(builder, {
|
||||
kind: 'LoadGlobal',
|
||||
binding,
|
||||
|
||||
@@ -265,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.
|
||||
*/
|
||||
@@ -286,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'),
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -318,10 +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),
|
||||
validateNoSetStateInPassiveEffects: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Validates against creating JSX within a try block and recommends using an error boundary
|
||||
@@ -631,17 +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),
|
||||
});
|
||||
|
||||
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
|
||||
|
||||
@@ -5,11 +5,17 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {Effect, ValueKind, ValueReason} from './HIR';
|
||||
import {
|
||||
Effect,
|
||||
GeneratedSource,
|
||||
makeIdentifierId,
|
||||
Place,
|
||||
ValueKind,
|
||||
ValueReason,
|
||||
} from './HIR';
|
||||
import {
|
||||
BUILTIN_SHAPES,
|
||||
BuiltInArrayId,
|
||||
BuiltInAutodepsId,
|
||||
BuiltInFireFunctionId,
|
||||
BuiltInFireId,
|
||||
BuiltInMapId,
|
||||
@@ -35,12 +41,18 @@ import {
|
||||
addFunction,
|
||||
addHook,
|
||||
addObject,
|
||||
signatureArgument,
|
||||
} from './ObjectShape';
|
||||
import {BuiltInType, ObjectType, PolyType} from './Types';
|
||||
import {TypeConfig} from './TypeSchema';
|
||||
import {
|
||||
AliasingEffectConfig,
|
||||
AliasingSignatureConfig,
|
||||
TypeConfig,
|
||||
} from './TypeSchema';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {isHookName} from './Environment';
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects';
|
||||
|
||||
/*
|
||||
* This file exports types and defaults for JavaScript global objects.
|
||||
@@ -646,35 +658,35 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
hookKind: 'useEffect',
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
aliasing: {
|
||||
receiver: '@receiver',
|
||||
receiver: makeIdentifierId(0),
|
||||
params: [],
|
||||
rest: '@rest',
|
||||
returns: '@returns',
|
||||
temporaries: ['@effect'],
|
||||
rest: makeIdentifierId(1),
|
||||
returns: makeIdentifierId(2),
|
||||
temporaries: [signatureArgument(3)],
|
||||
effects: [
|
||||
// Freezes the function and deps
|
||||
{
|
||||
kind: 'Freeze',
|
||||
value: '@rest',
|
||||
value: signatureArgument(1),
|
||||
reason: ValueReason.Effect,
|
||||
},
|
||||
// Internally creates an effect object that captures the function and deps
|
||||
{
|
||||
kind: 'Create',
|
||||
into: '@effect',
|
||||
into: signatureArgument(3),
|
||||
value: ValueKind.Frozen,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
// The effect stores the function and dependencies
|
||||
{
|
||||
kind: 'Capture',
|
||||
from: '@rest',
|
||||
into: '@effect',
|
||||
from: signatureArgument(1),
|
||||
into: signatureArgument(3),
|
||||
},
|
||||
// Returns undefined
|
||||
{
|
||||
kind: 'Create',
|
||||
into: '@returns',
|
||||
into: signatureArgument(2),
|
||||
value: ValueKind.Primitive,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
@@ -781,7 +793,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
BuiltInUseEffectEventId,
|
||||
),
|
||||
],
|
||||
['AUTODEPS', addObject(DEFAULT_SHAPES, BuiltInAutodepsId, [])],
|
||||
];
|
||||
|
||||
TYPED_GLOBALS.push(
|
||||
@@ -892,6 +903,10 @@ export function installTypeConfig(
|
||||
}
|
||||
}
|
||||
case 'function': {
|
||||
const aliasing =
|
||||
typeConfig.aliasing != null
|
||||
? parseAliasingSignatureConfig(typeConfig.aliasing, moduleName, loc)
|
||||
: null;
|
||||
return addFunction(shapes, [], {
|
||||
positionalParams: typeConfig.positionalParams,
|
||||
restParam: typeConfig.restParam,
|
||||
@@ -907,10 +922,14 @@ export function installTypeConfig(
|
||||
noAlias: typeConfig.noAlias === true,
|
||||
mutableOnlyIfOperandsAreMutable:
|
||||
typeConfig.mutableOnlyIfOperandsAreMutable === true,
|
||||
aliasing: typeConfig.aliasing,
|
||||
aliasing,
|
||||
});
|
||||
}
|
||||
case 'hook': {
|
||||
const aliasing =
|
||||
typeConfig.aliasing != null
|
||||
? parseAliasingSignatureConfig(typeConfig.aliasing, moduleName, loc)
|
||||
: null;
|
||||
return addHook(shapes, {
|
||||
hookKind: 'Custom',
|
||||
positionalParams: typeConfig.positionalParams ?? [],
|
||||
@@ -925,7 +944,7 @@ export function installTypeConfig(
|
||||
),
|
||||
returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen,
|
||||
noAlias: typeConfig.noAlias === true,
|
||||
aliasing: typeConfig.aliasing,
|
||||
aliasing,
|
||||
});
|
||||
}
|
||||
case 'object': {
|
||||
@@ -968,6 +987,90 @@ export function installTypeConfig(
|
||||
}
|
||||
}
|
||||
|
||||
function parseAliasingSignatureConfig(
|
||||
typeConfig: AliasingSignatureConfig,
|
||||
moduleName: string,
|
||||
loc: SourceLocation,
|
||||
): AliasingSignature {
|
||||
const lifetimes = new Map<string, Place>();
|
||||
function define(temp: string): Place {
|
||||
CompilerError.invariant(!lifetimes.has(temp), {
|
||||
reason: `Invalid type configuration for module`,
|
||||
description: `Expected aliasing signature to have unique names for receiver, params, rest, returns, and temporaries in module '${moduleName}'`,
|
||||
loc,
|
||||
});
|
||||
const place = signatureArgument(lifetimes.size);
|
||||
lifetimes.set(temp, place);
|
||||
return place;
|
||||
}
|
||||
function lookup(temp: string): Place {
|
||||
const place = lifetimes.get(temp);
|
||||
CompilerError.invariant(place != null, {
|
||||
reason: `Invalid type configuration for module`,
|
||||
description: `Expected aliasing signature effects to reference known names from receiver/params/rest/returns/temporaries, but '${temp}' is not a known name in '${moduleName}'`,
|
||||
loc,
|
||||
});
|
||||
return place;
|
||||
}
|
||||
const receiver = define(typeConfig.receiver);
|
||||
const params = typeConfig.params.map(define);
|
||||
const rest = typeConfig.rest != null ? define(typeConfig.rest) : null;
|
||||
const returns = define(typeConfig.returns);
|
||||
const temporaries = typeConfig.temporaries.map(define);
|
||||
const effects = typeConfig.effects.map(
|
||||
(effect: AliasingEffectConfig): AliasingEffect => {
|
||||
switch (effect.kind) {
|
||||
case 'Assign': {
|
||||
return {
|
||||
kind: 'Assign',
|
||||
from: lookup(effect.from),
|
||||
into: lookup(effect.into),
|
||||
};
|
||||
}
|
||||
case 'Create': {
|
||||
return {
|
||||
kind: 'Create',
|
||||
into: lookup(effect.into),
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
value: effect.value,
|
||||
};
|
||||
}
|
||||
case 'Freeze': {
|
||||
return {
|
||||
kind: 'Freeze',
|
||||
value: lookup(effect.value),
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
};
|
||||
}
|
||||
case 'Impure': {
|
||||
return {
|
||||
kind: 'Impure',
|
||||
place: lookup(effect.place),
|
||||
error: CompilerError.throwTodo({
|
||||
reason: 'Support impure effect declarations',
|
||||
loc: GeneratedSource,
|
||||
}),
|
||||
};
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
effect,
|
||||
`Unexpected effect kind '${(effect as any).kind}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
return {
|
||||
receiver: receiver.identifier.id,
|
||||
params: params.map(p => p.identifier.id),
|
||||
rest: rest != null ? rest.identifier.id : null,
|
||||
returns: returns.identifier.id,
|
||||
temporaries,
|
||||
effects,
|
||||
};
|
||||
}
|
||||
|
||||
export function getReanimatedModuleType(registry: ShapeRegistry): ObjectType {
|
||||
// hooks that freeze args and return frozen value
|
||||
const frozenHooks = [
|
||||
|
||||
@@ -279,6 +279,7 @@ export type HIRFunction = {
|
||||
env: Environment;
|
||||
params: Array<Place | SpreadPattern>;
|
||||
returnTypeAnnotation: t.FlowType | t.TSType | null;
|
||||
returnType: Type;
|
||||
returns: Place;
|
||||
context: Array<Place>;
|
||||
effects: Array<FunctionEffect> | null;
|
||||
@@ -446,20 +447,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;
|
||||
@@ -1464,20 +1453,6 @@ export const ValueKindSchema = z.enum([
|
||||
ValueKind.Context,
|
||||
]);
|
||||
|
||||
export const ValueReasonSchema = z.enum([
|
||||
ValueReason.Context,
|
||||
ValueReason.Effect,
|
||||
ValueReason.Global,
|
||||
ValueReason.HookCaptured,
|
||||
ValueReason.HookReturn,
|
||||
ValueReason.JsxCaptured,
|
||||
ValueReason.KnownReturnSignature,
|
||||
ValueReason.Other,
|
||||
ValueReason.ReactiveFunctionArgument,
|
||||
ValueReason.ReducerState,
|
||||
ValueReason.State,
|
||||
]);
|
||||
|
||||
// The effect with which a value is modified.
|
||||
export enum Effect {
|
||||
// Default value: not allowed after lifetime inference
|
||||
@@ -1781,10 +1756,6 @@ export function isUseStateType(id: Identifier): boolean {
|
||||
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseState';
|
||||
}
|
||||
|
||||
export function isJsxType(type: Type): boolean {
|
||||
return type.kind === 'Object' && type.shapeId === 'BuiltInJsx';
|
||||
}
|
||||
|
||||
export function isRefOrRefValue(id: Identifier): boolean {
|
||||
return isUseRefType(id) || isRefValueType(id);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import {Binding, NodePath} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import {CompilerError, ErrorSeverity} from '../CompilerError';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {Environment} from './Environment';
|
||||
import {
|
||||
BasicBlock,
|
||||
@@ -308,18 +308,9 @@ export default class HIRBuilder {
|
||||
|
||||
resolveBinding(node: t.Identifier): Identifier {
|
||||
if (node.name === 'fbt') {
|
||||
CompilerError.throwDiagnostic({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: 'Support local variables named `fbt`',
|
||||
description:
|
||||
'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
message: 'Rename to avoid conflict with fbt plugin',
|
||||
loc: node.loc ?? GeneratedSource,
|
||||
},
|
||||
],
|
||||
CompilerError.throwTodo({
|
||||
reason: 'Support local variables named "fbt"',
|
||||
loc: node.loc ?? null,
|
||||
});
|
||||
}
|
||||
const originalName = node.name;
|
||||
|
||||
@@ -107,17 +107,6 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
|
||||
merged.merge(block.id, predecessorId);
|
||||
fn.body.blocks.delete(block.id);
|
||||
}
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const phi of block.phis) {
|
||||
for (const [predecessorId, operand] of phi.operands) {
|
||||
const mapped = merged.get(predecessorId);
|
||||
if (mapped !== predecessorId) {
|
||||
phi.operands.delete(predecessorId);
|
||||
phi.operands.set(mapped, operand);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
markPredecessors(fn.body);
|
||||
for (const [, {terminal}] of fn.body.blocks) {
|
||||
if (terminalHasFallthrough(terminal)) {
|
||||
|
||||
@@ -6,18 +6,14 @@
|
||||
*/
|
||||
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {AliasingSignature} from '../Inference/AliasingEffects';
|
||||
import {
|
||||
Effect,
|
||||
GeneratedSource,
|
||||
Hole,
|
||||
makeDeclarationId,
|
||||
makeIdentifierId,
|
||||
makeInstructionId,
|
||||
Place,
|
||||
SourceLocation,
|
||||
SpreadPattern,
|
||||
ValueKind,
|
||||
ValueReason,
|
||||
} from './HIR';
|
||||
@@ -29,7 +25,6 @@ import {
|
||||
PolyType,
|
||||
PrimitiveType,
|
||||
} from './Types';
|
||||
import {AliasingEffectConfig, AliasingSignatureConfig} from './TypeSchema';
|
||||
|
||||
/*
|
||||
* This file exports types and defaults for JavaScript object shapes. These are
|
||||
@@ -58,20 +53,13 @@ function createAnonId(): string {
|
||||
export function addFunction(
|
||||
registry: ShapeRegistry,
|
||||
properties: Iterable<[string, BuiltInType | PolyType]>,
|
||||
fn: Omit<FunctionSignature, 'hookKind' | 'aliasing'> & {
|
||||
aliasing?: AliasingSignatureConfig | null | undefined;
|
||||
},
|
||||
fn: Omit<FunctionSignature, 'hookKind'>,
|
||||
id: string | null = null,
|
||||
isConstructor: boolean = false,
|
||||
): FunctionType {
|
||||
const shapeId = id ?? createAnonId();
|
||||
const aliasing =
|
||||
fn.aliasing != null
|
||||
? parseAliasingSignatureConfig(fn.aliasing, '<builtin>', GeneratedSource)
|
||||
: null;
|
||||
addShape(registry, shapeId, properties, {
|
||||
...fn,
|
||||
aliasing,
|
||||
hookKind: null,
|
||||
});
|
||||
return {
|
||||
@@ -89,18 +77,11 @@ export function addFunction(
|
||||
*/
|
||||
export function addHook(
|
||||
registry: ShapeRegistry,
|
||||
fn: Omit<FunctionSignature, 'aliasing'> & {
|
||||
hookKind: HookKind;
|
||||
aliasing?: AliasingSignatureConfig | null | undefined;
|
||||
},
|
||||
fn: FunctionSignature & {hookKind: HookKind},
|
||||
id: string | null = null,
|
||||
): FunctionType {
|
||||
const shapeId = id ?? createAnonId();
|
||||
const aliasing =
|
||||
fn.aliasing != null
|
||||
? parseAliasingSignatureConfig(fn.aliasing, '<builtin>', GeneratedSource)
|
||||
: null;
|
||||
addShape(registry, shapeId, [], {...fn, aliasing});
|
||||
addShape(registry, shapeId, [], fn);
|
||||
return {
|
||||
kind: 'Function',
|
||||
return: fn.returnType,
|
||||
@@ -109,129 +90,6 @@ export function addHook(
|
||||
};
|
||||
}
|
||||
|
||||
function parseAliasingSignatureConfig(
|
||||
typeConfig: AliasingSignatureConfig,
|
||||
moduleName: string,
|
||||
loc: SourceLocation,
|
||||
): AliasingSignature {
|
||||
const lifetimes = new Map<string, Place>();
|
||||
function define(temp: string): Place {
|
||||
CompilerError.invariant(!lifetimes.has(temp), {
|
||||
reason: `Invalid type configuration for module`,
|
||||
description: `Expected aliasing signature to have unique names for receiver, params, rest, returns, and temporaries in module '${moduleName}'`,
|
||||
loc,
|
||||
});
|
||||
const place = signatureArgument(lifetimes.size);
|
||||
lifetimes.set(temp, place);
|
||||
return place;
|
||||
}
|
||||
function lookup(temp: string): Place {
|
||||
const place = lifetimes.get(temp);
|
||||
CompilerError.invariant(place != null, {
|
||||
reason: `Invalid type configuration for module`,
|
||||
description: `Expected aliasing signature effects to reference known names from receiver/params/rest/returns/temporaries, but '${temp}' is not a known name in '${moduleName}'`,
|
||||
loc,
|
||||
});
|
||||
return place;
|
||||
}
|
||||
const receiver = define(typeConfig.receiver);
|
||||
const params = typeConfig.params.map(define);
|
||||
const rest = typeConfig.rest != null ? define(typeConfig.rest) : null;
|
||||
const returns = define(typeConfig.returns);
|
||||
const temporaries = typeConfig.temporaries.map(define);
|
||||
const effects = typeConfig.effects.map(
|
||||
(effect: AliasingEffectConfig): AliasingEffect => {
|
||||
switch (effect.kind) {
|
||||
case 'CreateFrom':
|
||||
case 'Capture':
|
||||
case 'Alias':
|
||||
case 'Assign': {
|
||||
const from = lookup(effect.from);
|
||||
const into = lookup(effect.into);
|
||||
return {
|
||||
kind: effect.kind,
|
||||
from,
|
||||
into,
|
||||
};
|
||||
}
|
||||
case 'Mutate':
|
||||
case 'MutateTransitiveConditionally': {
|
||||
const value = lookup(effect.value);
|
||||
return {kind: effect.kind, value};
|
||||
}
|
||||
case 'Create': {
|
||||
const into = lookup(effect.into);
|
||||
return {
|
||||
kind: 'Create',
|
||||
into,
|
||||
reason: effect.reason,
|
||||
value: effect.value,
|
||||
};
|
||||
}
|
||||
case 'Freeze': {
|
||||
const value = lookup(effect.value);
|
||||
return {
|
||||
kind: 'Freeze',
|
||||
value,
|
||||
reason: effect.reason,
|
||||
};
|
||||
}
|
||||
case 'Impure': {
|
||||
const place = lookup(effect.place);
|
||||
return {
|
||||
kind: 'Impure',
|
||||
place,
|
||||
error: CompilerError.throwTodo({
|
||||
reason: 'Support impure effect declarations',
|
||||
loc: GeneratedSource,
|
||||
}),
|
||||
};
|
||||
}
|
||||
case 'Apply': {
|
||||
const receiver = lookup(effect.receiver);
|
||||
const fn = lookup(effect.function);
|
||||
const args: Array<Place | SpreadPattern | Hole> = effect.args.map(
|
||||
arg => {
|
||||
if (typeof arg === 'string') {
|
||||
return lookup(arg);
|
||||
} else if (arg.kind === 'Spread') {
|
||||
return {kind: 'Spread', place: lookup(arg.place)};
|
||||
} else {
|
||||
return arg;
|
||||
}
|
||||
},
|
||||
);
|
||||
const into = lookup(effect.into);
|
||||
return {
|
||||
kind: 'Apply',
|
||||
receiver,
|
||||
function: fn,
|
||||
mutatesFunction: effect.mutatesFunction,
|
||||
args,
|
||||
into,
|
||||
loc,
|
||||
signature: null,
|
||||
};
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
effect,
|
||||
`Unexpected effect kind '${(effect as any).kind}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
return {
|
||||
receiver: receiver.identifier.id,
|
||||
params: params.map(p => p.identifier.id),
|
||||
rest: rest != null ? rest.identifier.id : null,
|
||||
returns: returns.identifier.id,
|
||||
temporaries,
|
||||
effects,
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Add an object to an existing ShapeRegistry.
|
||||
*
|
||||
@@ -334,7 +192,8 @@ export type FunctionSignature = {
|
||||
|
||||
canonicalName?: string;
|
||||
|
||||
aliasing?: AliasingSignature | null | undefined;
|
||||
aliasing?: AliasingSignature | null;
|
||||
todo_aliasing?: AliasingSignature | null;
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -384,7 +243,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';
|
||||
@@ -462,24 +320,24 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
|
||||
calleeEffect: Effect.Store,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
aliasing: {
|
||||
receiver: '@receiver',
|
||||
receiver: makeIdentifierId(0),
|
||||
params: [],
|
||||
rest: '@rest',
|
||||
returns: '@returns',
|
||||
rest: makeIdentifierId(1),
|
||||
returns: makeIdentifierId(2),
|
||||
temporaries: [],
|
||||
effects: [
|
||||
// Push directly mutates the array itself
|
||||
{kind: 'Mutate', value: '@receiver'},
|
||||
{kind: 'Mutate', value: signatureArgument(0)},
|
||||
// The arguments are captured into the array
|
||||
{
|
||||
kind: 'Capture',
|
||||
from: '@rest',
|
||||
into: '@receiver',
|
||||
from: signatureArgument(1),
|
||||
into: signatureArgument(0),
|
||||
},
|
||||
// Returns the new length, a primitive
|
||||
{
|
||||
kind: 'Create',
|
||||
into: '@returns',
|
||||
into: signatureArgument(2),
|
||||
value: ValueKind.Primitive,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
@@ -516,56 +374,58 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
|
||||
noAlias: true,
|
||||
mutableOnlyIfOperandsAreMutable: true,
|
||||
aliasing: {
|
||||
receiver: '@receiver',
|
||||
params: ['@callback'],
|
||||
receiver: makeIdentifierId(0),
|
||||
params: [makeIdentifierId(1)],
|
||||
rest: null,
|
||||
returns: '@returns',
|
||||
returns: makeIdentifierId(2),
|
||||
temporaries: [
|
||||
// Temporary representing captured items of the receiver
|
||||
'@item',
|
||||
signatureArgument(3),
|
||||
// Temporary representing the result of the callback
|
||||
'@callbackReturn',
|
||||
signatureArgument(4),
|
||||
/*
|
||||
* Undefined `this` arg to the callback. Note the signature does not
|
||||
* support passing an explicit thisArg second param
|
||||
*/
|
||||
'@thisArg',
|
||||
signatureArgument(5),
|
||||
],
|
||||
effects: [
|
||||
// Map creates a new mutable array
|
||||
{
|
||||
kind: 'Create',
|
||||
into: '@returns',
|
||||
into: signatureArgument(2),
|
||||
value: ValueKind.Mutable,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
// The first arg to the callback is an item extracted from the receiver array
|
||||
{
|
||||
kind: 'CreateFrom',
|
||||
from: '@receiver',
|
||||
into: '@item',
|
||||
from: signatureArgument(0),
|
||||
into: signatureArgument(3),
|
||||
},
|
||||
// The undefined this for the callback
|
||||
{
|
||||
kind: 'Create',
|
||||
into: '@thisArg',
|
||||
into: signatureArgument(5),
|
||||
value: ValueKind.Primitive,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
// calls the callback, returning the result into a temporary
|
||||
{
|
||||
kind: 'Apply',
|
||||
receiver: '@thisArg',
|
||||
args: ['@item', {kind: 'Hole'}, '@receiver'],
|
||||
function: '@callback',
|
||||
into: '@callbackReturn',
|
||||
receiver: signatureArgument(5),
|
||||
args: [signatureArgument(3), {kind: 'Hole'}, signatureArgument(0)],
|
||||
function: signatureArgument(1),
|
||||
into: signatureArgument(4),
|
||||
signature: null,
|
||||
mutatesFunction: false,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
// captures the result of the callback into the return array
|
||||
{
|
||||
kind: 'Capture',
|
||||
from: '@callbackReturn',
|
||||
into: '@returns',
|
||||
from: signatureArgument(4),
|
||||
into: signatureArgument(2),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -717,28 +577,28 @@ addObject(BUILTIN_SHAPES, BuiltInSetId, [
|
||||
// returnValueKind is technically dependent on the ValueKind of the set itself
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
aliasing: {
|
||||
receiver: '@receiver',
|
||||
receiver: makeIdentifierId(0),
|
||||
params: [],
|
||||
rest: '@rest',
|
||||
returns: '@returns',
|
||||
rest: makeIdentifierId(1),
|
||||
returns: makeIdentifierId(2),
|
||||
temporaries: [],
|
||||
effects: [
|
||||
// Set.add returns the receiver Set
|
||||
{
|
||||
kind: 'Assign',
|
||||
from: '@receiver',
|
||||
into: '@returns',
|
||||
from: signatureArgument(0),
|
||||
into: signatureArgument(2),
|
||||
},
|
||||
// Set.add mutates the set itself
|
||||
{
|
||||
kind: 'Mutate',
|
||||
value: '@receiver',
|
||||
value: signatureArgument(0),
|
||||
},
|
||||
// Captures the rest params into the set
|
||||
{
|
||||
kind: 'Capture',
|
||||
from: '@rest',
|
||||
into: '@receiver',
|
||||
from: signatureArgument(1),
|
||||
into: signatureArgument(0),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1211,8 +1071,6 @@ addObject(BUILTIN_SHAPES, BuiltInRefValueId, [
|
||||
['*', {kind: 'Object', shapeId: BuiltInRefValueId}],
|
||||
]);
|
||||
|
||||
addObject(BUILTIN_SHAPES, ReanimatedSharedValueId, []);
|
||||
|
||||
addFunction(
|
||||
BUILTIN_SHAPES,
|
||||
[],
|
||||
@@ -1445,30 +1303,30 @@ export const DefaultNonmutatingHook = addHook(
|
||||
hookKind: 'Custom',
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
aliasing: {
|
||||
receiver: '@receiver',
|
||||
receiver: makeIdentifierId(0),
|
||||
params: [],
|
||||
rest: '@rest',
|
||||
returns: '@returns',
|
||||
rest: makeIdentifierId(1),
|
||||
returns: makeIdentifierId(2),
|
||||
temporaries: [],
|
||||
effects: [
|
||||
// Freeze the arguments
|
||||
{
|
||||
kind: 'Freeze',
|
||||
value: '@rest',
|
||||
value: signatureArgument(1),
|
||||
reason: ValueReason.HookCaptured,
|
||||
},
|
||||
// Returns a frozen value
|
||||
{
|
||||
kind: 'Create',
|
||||
into: '@returns',
|
||||
into: signatureArgument(2),
|
||||
value: ValueKind.Frozen,
|
||||
reason: ValueReason.HookReturn,
|
||||
},
|
||||
// May alias any arguments into the return
|
||||
{
|
||||
kind: 'Alias',
|
||||
from: '@rest',
|
||||
into: '@returns',
|
||||
from: signatureArgument(1),
|
||||
into: signatureArgument(2),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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';
|
||||
@@ -53,8 +54,6 @@ export function printFunction(fn: HIRFunction): string {
|
||||
let definition = '';
|
||||
if (fn.id !== null) {
|
||||
definition += fn.id;
|
||||
} else {
|
||||
definition += '<<anonymous>>';
|
||||
}
|
||||
if (fn.params.length !== 0) {
|
||||
definition +=
|
||||
@@ -72,8 +71,10 @@ export function printFunction(fn: HIRFunction): string {
|
||||
} else {
|
||||
definition += '()';
|
||||
}
|
||||
definition += `: ${printPlace(fn.returns)}`;
|
||||
output.push(definition);
|
||||
if (definition.length !== 0) {
|
||||
output.push(definition);
|
||||
}
|
||||
output.push(`: ${printType(fn.returnType)} @ ${printPlace(fn.returns)}`);
|
||||
output.push(...fn.directives);
|
||||
output.push(printHIR(fn.body));
|
||||
return output.join('\n');
|
||||
@@ -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': {
|
||||
@@ -714,7 +715,7 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
|
||||
break;
|
||||
}
|
||||
case 'FinishMemoize': {
|
||||
value = `FinishMemoize decl=${printPlace(instrValue.decl)}${instrValue.pruned ? ' pruned' : ''}`;
|
||||
value = `FinishMemoize decl=${printPlace(instrValue.decl)}`;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
@@ -943,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)}`;
|
||||
@@ -998,13 +996,13 @@ export function printAliasingEffect(effect: AliasingEffect): string {
|
||||
return `${effect.kind} ${printPlaceForAliasEffect(effect.value)}`;
|
||||
}
|
||||
case 'MutateFrozen': {
|
||||
return `MutateFrozen ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.category)}`;
|
||||
return `MutateFrozen ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
|
||||
}
|
||||
case 'MutateGlobal': {
|
||||
return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.category)}`;
|
||||
return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
|
||||
}
|
||||
case 'Impure': {
|
||||
return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.category)}`;
|
||||
return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
|
||||
}
|
||||
case 'Render': {
|
||||
return `Render ${printPlaceForAliasEffect(effect.place)}`;
|
||||
|
||||
@@ -8,12 +8,7 @@
|
||||
import {isValidIdentifier} from '@babel/types';
|
||||
import {z} from 'zod';
|
||||
import {Effect, ValueKind} from '..';
|
||||
import {
|
||||
EffectSchema,
|
||||
ValueKindSchema,
|
||||
ValueReason,
|
||||
ValueReasonSchema,
|
||||
} from './HIR';
|
||||
import {EffectSchema, ValueKindSchema} from './HIR';
|
||||
|
||||
export type ObjectPropertiesConfig = {[key: string]: TypeConfig};
|
||||
export const ObjectPropertiesSchema: z.ZodType<ObjectPropertiesConfig> = z
|
||||
@@ -43,48 +38,23 @@ export const LifetimeIdSchema = z.string().refine(id => id.startsWith('@'), {
|
||||
export type FreezeEffectConfig = {
|
||||
kind: 'Freeze';
|
||||
value: string;
|
||||
reason: ValueReason;
|
||||
};
|
||||
|
||||
export const FreezeEffectSchema: z.ZodType<FreezeEffectConfig> = z.object({
|
||||
kind: z.literal('Freeze'),
|
||||
value: LifetimeIdSchema,
|
||||
reason: ValueReasonSchema,
|
||||
});
|
||||
|
||||
export type MutateEffectConfig = {
|
||||
kind: 'Mutate';
|
||||
value: string;
|
||||
};
|
||||
|
||||
export const MutateEffectSchema: z.ZodType<MutateEffectConfig> = z.object({
|
||||
kind: z.literal('Mutate'),
|
||||
value: LifetimeIdSchema,
|
||||
});
|
||||
|
||||
export type MutateTransitiveConditionallyConfig = {
|
||||
kind: 'MutateTransitiveConditionally';
|
||||
value: string;
|
||||
};
|
||||
|
||||
export const MutateTransitiveConditionallySchema: z.ZodType<MutateTransitiveConditionallyConfig> =
|
||||
z.object({
|
||||
kind: z.literal('MutateTransitiveConditionally'),
|
||||
value: LifetimeIdSchema,
|
||||
});
|
||||
|
||||
export type CreateEffectConfig = {
|
||||
kind: 'Create';
|
||||
into: string;
|
||||
value: ValueKind;
|
||||
reason: ValueReason;
|
||||
};
|
||||
|
||||
export const CreateEffectSchema: z.ZodType<CreateEffectConfig> = z.object({
|
||||
kind: z.literal('Create'),
|
||||
into: LifetimeIdSchema,
|
||||
value: ValueKindSchema,
|
||||
reason: ValueReasonSchema,
|
||||
});
|
||||
|
||||
export type AssignEffectConfig = {
|
||||
@@ -99,77 +69,6 @@ export const AssignEffectSchema: z.ZodType<AssignEffectConfig> = z.object({
|
||||
into: LifetimeIdSchema,
|
||||
});
|
||||
|
||||
export type AliasEffectConfig = {
|
||||
kind: 'Alias';
|
||||
from: string;
|
||||
into: string;
|
||||
};
|
||||
|
||||
export const AliasEffectSchema: z.ZodType<AliasEffectConfig> = z.object({
|
||||
kind: z.literal('Alias'),
|
||||
from: LifetimeIdSchema,
|
||||
into: LifetimeIdSchema,
|
||||
});
|
||||
|
||||
export type CaptureEffectConfig = {
|
||||
kind: 'Capture';
|
||||
from: string;
|
||||
into: string;
|
||||
};
|
||||
|
||||
export const CaptureEffectSchema: z.ZodType<CaptureEffectConfig> = z.object({
|
||||
kind: z.literal('Capture'),
|
||||
from: LifetimeIdSchema,
|
||||
into: LifetimeIdSchema,
|
||||
});
|
||||
|
||||
export type CreateFromEffectConfig = {
|
||||
kind: 'CreateFrom';
|
||||
from: string;
|
||||
into: string;
|
||||
};
|
||||
|
||||
export const CreateFromEffectSchema: z.ZodType<CreateFromEffectConfig> =
|
||||
z.object({
|
||||
kind: z.literal('CreateFrom'),
|
||||
from: LifetimeIdSchema,
|
||||
into: LifetimeIdSchema,
|
||||
});
|
||||
|
||||
export type ApplyArgConfig =
|
||||
| string
|
||||
| {kind: 'Spread'; place: string}
|
||||
| {kind: 'Hole'};
|
||||
|
||||
export const ApplyArgSchema: z.ZodType<ApplyArgConfig> = z.union([
|
||||
LifetimeIdSchema,
|
||||
z.object({
|
||||
kind: z.literal('Spread'),
|
||||
place: LifetimeIdSchema,
|
||||
}),
|
||||
z.object({
|
||||
kind: z.literal('Hole'),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type ApplyEffectConfig = {
|
||||
kind: 'Apply';
|
||||
receiver: string;
|
||||
function: string;
|
||||
mutatesFunction: boolean;
|
||||
args: Array<ApplyArgConfig>;
|
||||
into: string;
|
||||
};
|
||||
|
||||
export const ApplyEffectSchema: z.ZodType<ApplyEffectConfig> = z.object({
|
||||
kind: z.literal('Apply'),
|
||||
receiver: LifetimeIdSchema,
|
||||
function: LifetimeIdSchema,
|
||||
mutatesFunction: z.boolean(),
|
||||
args: z.array(ApplyArgSchema),
|
||||
into: LifetimeIdSchema,
|
||||
});
|
||||
|
||||
export type ImpureEffectConfig = {
|
||||
kind: 'Impure';
|
||||
place: string;
|
||||
@@ -183,26 +82,14 @@ export const ImpureEffectSchema: z.ZodType<ImpureEffectConfig> = z.object({
|
||||
export type AliasingEffectConfig =
|
||||
| FreezeEffectConfig
|
||||
| CreateEffectConfig
|
||||
| CreateFromEffectConfig
|
||||
| AssignEffectConfig
|
||||
| AliasEffectConfig
|
||||
| CaptureEffectConfig
|
||||
| ImpureEffectConfig
|
||||
| MutateEffectConfig
|
||||
| MutateTransitiveConditionallyConfig
|
||||
| ApplyEffectConfig;
|
||||
| ImpureEffectConfig;
|
||||
|
||||
export const AliasingEffectSchema: z.ZodType<AliasingEffectConfig> = z.union([
|
||||
FreezeEffectSchema,
|
||||
CreateEffectSchema,
|
||||
CreateFromEffectSchema,
|
||||
AssignEffectSchema,
|
||||
AliasEffectSchema,
|
||||
CaptureEffectSchema,
|
||||
ImpureEffectSchema,
|
||||
MutateEffectSchema,
|
||||
MutateTransitiveConditionallySchema,
|
||||
ApplyEffectSchema,
|
||||
]);
|
||||
|
||||
export type AliasingSignatureConfig = {
|
||||
|
||||
@@ -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,
|
||||
@@ -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
|
||||
@@ -200,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,
|
||||
@@ -229,9 +211,9 @@ export function hashEffect(effect: AliasingEffect): string {
|
||||
effect.kind,
|
||||
effect.place.identifier.id,
|
||||
effect.error.severity,
|
||||
effect.error.category,
|
||||
effect.error.reason,
|
||||
effect.error.description,
|
||||
printSourceLocation(effect.error.primaryLocation() ?? GeneratedSource),
|
||||
printSourceLocation(effect.error.loc ?? GeneratedSource),
|
||||
].join(':');
|
||||
}
|
||||
case 'Mutate':
|
||||
|
||||
@@ -22,6 +22,7 @@ import {inferMutableRanges} from './InferMutableRanges';
|
||||
import inferReferenceEffects from './InferReferenceEffects';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {inferMutationAliasingEffects} from './InferMutationAliasingEffects';
|
||||
import {inferMutationAliasingFunctionEffects} from './InferMutationAliasingFunctionEffects';
|
||||
import {inferMutationAliasingRanges} from './InferMutationAliasingRanges';
|
||||
|
||||
export default function analyseFunctions(func: HIRFunction): void {
|
||||
@@ -67,12 +68,19 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
|
||||
analyseFunctions(fn);
|
||||
inferMutationAliasingEffects(fn, {isFunctionExpression: true});
|
||||
deadCodeElimination(fn);
|
||||
const functionEffects = inferMutationAliasingRanges(fn, {
|
||||
isFunctionExpression: true,
|
||||
}).unwrap();
|
||||
inferMutationAliasingRanges(fn, {isFunctionExpression: true});
|
||||
rewriteInstructionKindsBasedOnReassignment(fn);
|
||||
inferReactiveScopeVariables(fn);
|
||||
fn.aliasingEffects = functionEffects;
|
||||
const effects = inferMutationAliasingFunctionEffects(fn);
|
||||
fn.env.logger?.debugLogIRs?.({
|
||||
kind: 'hir',
|
||||
name: 'AnalyseFunction (inner)',
|
||||
value: fn,
|
||||
});
|
||||
if (effects != null) {
|
||||
fn.aliasingEffects ??= [];
|
||||
fn.aliasingEffects?.push(...effects);
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 2: populate the Effect of each context variable to use in inferring
|
||||
@@ -80,13 +88,12 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
|
||||
* effects to decide if the function may be mutable or not.
|
||||
*/
|
||||
const capturedOrMutated = new Set<IdentifierId>();
|
||||
for (const effect of functionEffects) {
|
||||
for (const effect of effects ?? []) {
|
||||
switch (effect.kind) {
|
||||
case 'Assign':
|
||||
case 'Alias':
|
||||
case 'Capture':
|
||||
case 'CreateFrom':
|
||||
case 'MaybeAlias': {
|
||||
case 'CreateFrom': {
|
||||
capturedOrMutated.add(effect.from.identifier.id);
|
||||
break;
|
||||
}
|
||||
@@ -133,12 +140,6 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
|
||||
operand.effect = Effect.Read;
|
||||
}
|
||||
}
|
||||
|
||||
fn.env.logger?.debugLogIRs?.({
|
||||
kind: 'hir',
|
||||
name: 'AnalyseFunction (inner)',
|
||||
value: fn,
|
||||
});
|
||||
}
|
||||
|
||||
function lower(func: HIRFunction): void {
|
||||
|
||||
@@ -5,12 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorSeverity,
|
||||
SourceLocation,
|
||||
} from '..';
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
import {
|
||||
CallExpression,
|
||||
Effect,
|
||||
@@ -35,7 +30,6 @@ import {
|
||||
makeInstructionId,
|
||||
} from '../HIR';
|
||||
import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
type ManualMemoCallee = {
|
||||
kind: 'useMemo' | 'useCallback';
|
||||
@@ -289,43 +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({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: `Expected a callback function to be passed to ${kind}`,
|
||||
description: `Expected a callback function to be passed to ${kind}`,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
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({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: `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) {
|
||||
@@ -333,40 +310,23 @@ function extractManualMemoizationArgs(
|
||||
depsListPlace.identifier.id,
|
||||
);
|
||||
if (maybeDepsList == null) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
description: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
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({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
|
||||
description: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
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,
|
||||
@@ -381,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 ||
|
||||
@@ -435,47 +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: '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,
|
||||
@@ -496,19 +410,11 @@ export function dropManualMemoization(
|
||||
* is rare and likely sketchy.
|
||||
*/
|
||||
if (!sidemap.functions.has(fnPlace.identifier.id)) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: `Expected the first argument to be an inline function expression`,
|
||||
description: `Expected the first argument to be an inline function expression`,
|
||||
suggestions: [],
|
||||
}).withDetail({
|
||||
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'
|
||||
@@ -580,8 +486,6 @@ export function dropManualMemoization(
|
||||
markInstructionIds(func.body);
|
||||
}
|
||||
}
|
||||
|
||||
return errors.asResult();
|
||||
}
|
||||
|
||||
function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
|
||||
@@ -626,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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -326,26 +326,26 @@ function isEffectSafeOutsideRender(effect: FunctionEffect): boolean {
|
||||
|
||||
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';
|
||||
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 'Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX';
|
||||
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 `Modifying a value returned from 'useContext()' is not allowed.`;
|
||||
return `Mutating a value returned from 'useContext()', which should not be mutated`;
|
||||
} else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
|
||||
return 'Modifying a value returned from a function whose return value should not be mutated';
|
||||
return 'Mutating 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';
|
||||
return 'Mutating 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";
|
||||
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 "Modifying a value returned from 'useReducer()', which should not be modified directly. Use the dispatch function to update instead";
|
||||
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 'Modifying a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the modification before calling useEffect()';
|
||||
return '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 'Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook';
|
||||
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 'Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed';
|
||||
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 modifies a variable that React considers immutable';
|
||||
return 'This mutates a variable that React considers immutable';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
Effect,
|
||||
ErrorSeverity,
|
||||
@@ -37,8 +36,8 @@ import {
|
||||
ValueReason,
|
||||
} from '../HIR';
|
||||
import {
|
||||
eachInstructionValueLValue,
|
||||
eachInstructionValueOperand,
|
||||
eachPatternItem,
|
||||
eachTerminalOperand,
|
||||
eachTerminalSuccessor,
|
||||
} from '../HIR/visitors';
|
||||
@@ -58,6 +57,7 @@ import {
|
||||
import {
|
||||
printAliasingEffect,
|
||||
printAliasingSignature,
|
||||
printFunction,
|
||||
printIdentifier,
|
||||
printInstruction,
|
||||
printInstructionValue,
|
||||
@@ -194,15 +194,19 @@ export function inferMutationAliasingEffects(
|
||||
hoistedContextDeclarations,
|
||||
);
|
||||
|
||||
let iterationCount = 0;
|
||||
let count = 0;
|
||||
while (queuedStates.size !== 0) {
|
||||
iterationCount++;
|
||||
if (iterationCount > 100) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `[InferMutationAliasingEffects] Potential infinite loop`,
|
||||
description: `A value, temporary place, or effect was not cached properly`,
|
||||
loc: fn.loc,
|
||||
});
|
||||
count++;
|
||||
if (count > 100) {
|
||||
console.log(
|
||||
'oops infinite loop',
|
||||
fn.id,
|
||||
typeof fn.loc !== 'symbol' ? fn.loc?.filename : null,
|
||||
);
|
||||
if (DEBUG) {
|
||||
console.log(printFunction(fn));
|
||||
}
|
||||
throw new Error('infinite loop');
|
||||
}
|
||||
for (const [blockId, block] of fn.body.blocks) {
|
||||
const incomingState = queuedStates.get(blockId);
|
||||
@@ -213,6 +217,11 @@ export function inferMutationAliasingEffects(
|
||||
|
||||
statesByBlock.set(blockId, incomingState);
|
||||
const state = incomingState.clone();
|
||||
if (DEBUG) {
|
||||
console.log('*************');
|
||||
console.log(`bb${block.id}`);
|
||||
console.log('*************');
|
||||
}
|
||||
inferBlock(context, state, block);
|
||||
|
||||
for (const nextBlockId of eachTerminalSuccessor(block.terminal)) {
|
||||
@@ -353,11 +362,6 @@ function inferBlock(
|
||||
} else if (terminal.kind === 'maybe-throw') {
|
||||
const handlerParam = context.catchHandlers.get(terminal.handler);
|
||||
if (handlerParam != null) {
|
||||
CompilerError.invariant(state.kind(handlerParam) != null, {
|
||||
reason:
|
||||
'Expected catch binding to be intialized with a DeclareLocal Catch instruction',
|
||||
loc: terminal.loc,
|
||||
});
|
||||
const effects: Array<AliasingEffect> = [];
|
||||
for (const instr of block.instructions) {
|
||||
if (
|
||||
@@ -447,23 +451,20 @@ function applySignature(
|
||||
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';
|
||||
effects.push({
|
||||
kind: 'MutateFrozen',
|
||||
place: effect.value,
|
||||
error: CompilerDiagnostic.create({
|
||||
error: {
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'This value cannot be modified',
|
||||
description: `${reason}.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
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,
|
||||
message: `${variable} cannot be modified`,
|
||||
}),
|
||||
suggestions: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -475,14 +476,14 @@ function applySignature(
|
||||
* Track which values we've already aliased once, so that we can switch to
|
||||
* appendAlias() for subsequent aliases into the same value
|
||||
*/
|
||||
const initialized = new Set<IdentifierId>();
|
||||
const aliased = new Set<IdentifierId>();
|
||||
|
||||
if (DEBUG) {
|
||||
console.log(printInstruction(instruction));
|
||||
}
|
||||
|
||||
for (const effect of signature.effects) {
|
||||
applyEffect(context, state, effect, initialized, effects);
|
||||
applyEffect(context, state, effect, aliased, effects);
|
||||
}
|
||||
if (DEBUG) {
|
||||
console.log(
|
||||
@@ -507,7 +508,7 @@ function applyEffect(
|
||||
context: Context,
|
||||
state: InferenceState,
|
||||
_effect: AliasingEffect,
|
||||
initialized: Set<IdentifierId>,
|
||||
aliased: Set<IdentifierId>,
|
||||
effects: Array<AliasingEffect>,
|
||||
): void {
|
||||
const effect = context.internEffect(_effect);
|
||||
@@ -523,13 +524,6 @@ function applyEffect(
|
||||
break;
|
||||
}
|
||||
case 'Create': {
|
||||
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
|
||||
reason: `Cannot re-initialize variable within an instruction`,
|
||||
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
|
||||
loc: effect.into.loc,
|
||||
});
|
||||
initialized.add(effect.into.identifier.id);
|
||||
|
||||
let value = context.effectInstructionValueCache.get(effect);
|
||||
if (value == null) {
|
||||
value = {
|
||||
@@ -544,7 +538,6 @@ function applyEffect(
|
||||
reason: new Set([effect.reason]),
|
||||
});
|
||||
state.define(effect.into, value);
|
||||
effects.push(effect);
|
||||
break;
|
||||
}
|
||||
case 'ImmutableCapture': {
|
||||
@@ -562,13 +555,6 @@ function applyEffect(
|
||||
break;
|
||||
}
|
||||
case 'CreateFrom': {
|
||||
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
|
||||
reason: `Cannot re-initialize variable within an instruction`,
|
||||
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
|
||||
loc: effect.into.loc,
|
||||
});
|
||||
initialized.add(effect.into.identifier.id);
|
||||
|
||||
const fromValue = state.kind(effect.from);
|
||||
let value = context.effectInstructionValueCache.get(effect);
|
||||
if (value == null) {
|
||||
@@ -587,21 +573,10 @@ function applyEffect(
|
||||
switch (fromValue.kind) {
|
||||
case ValueKind.Primitive:
|
||||
case ValueKind.Global: {
|
||||
effects.push({
|
||||
kind: 'Create',
|
||||
value: fromValue.kind,
|
||||
into: effect.into,
|
||||
reason: [...fromValue.reason][0] ?? ValueReason.Other,
|
||||
});
|
||||
// no need to track this data flow
|
||||
break;
|
||||
}
|
||||
case ValueKind.Frozen: {
|
||||
effects.push({
|
||||
kind: 'Create',
|
||||
value: fromValue.kind,
|
||||
into: effect.into,
|
||||
reason: [...fromValue.reason][0] ?? ValueReason.Other,
|
||||
});
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
@@ -610,7 +585,7 @@ function applyEffect(
|
||||
from: effect.from,
|
||||
into: effect.into,
|
||||
},
|
||||
initialized,
|
||||
aliased,
|
||||
effects,
|
||||
);
|
||||
break;
|
||||
@@ -622,13 +597,6 @@ function applyEffect(
|
||||
break;
|
||||
}
|
||||
case 'CreateFunction': {
|
||||
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
|
||||
reason: `Cannot re-initialize variable within an instruction`,
|
||||
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
|
||||
loc: effect.into.loc,
|
||||
});
|
||||
initialized.add(effect.into.identifier.id);
|
||||
|
||||
effects.push(effect);
|
||||
/**
|
||||
* We consider the function mutable if it has any mutable context variables or
|
||||
@@ -685,23 +653,14 @@ function applyEffect(
|
||||
from: capture,
|
||||
into: effect.into,
|
||||
},
|
||||
initialized,
|
||||
aliased,
|
||||
effects,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'MaybeAlias':
|
||||
case 'Alias':
|
||||
case 'Capture': {
|
||||
CompilerError.invariant(
|
||||
effect.kind === 'Capture' || initialized.has(effect.into.identifier.id),
|
||||
{
|
||||
reason: `Expected destination value to already be initialized within this instruction for Alias effect`,
|
||||
description: `Destination ${printPlace(effect.into)} is not initialized in this instruction`,
|
||||
loc: effect.into.loc,
|
||||
},
|
||||
);
|
||||
/*
|
||||
* Capture describes potential information flow: storing a pointer to one value
|
||||
* within another. If the destination is not mutable, or the source value has
|
||||
@@ -739,7 +698,7 @@ function applyEffect(
|
||||
from: effect.from,
|
||||
into: effect.into,
|
||||
},
|
||||
initialized,
|
||||
aliased,
|
||||
effects,
|
||||
);
|
||||
break;
|
||||
@@ -755,13 +714,6 @@ function applyEffect(
|
||||
break;
|
||||
}
|
||||
case 'Assign': {
|
||||
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
|
||||
reason: `Cannot re-initialize variable within an instruction`,
|
||||
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
|
||||
loc: effect.into.loc,
|
||||
});
|
||||
initialized.add(effect.into.identifier.id);
|
||||
|
||||
/*
|
||||
* Alias represents potential pointer aliasing. If the type is a global,
|
||||
* a primitive (copy-on-write semantics) then we can prune the effect
|
||||
@@ -778,7 +730,7 @@ function applyEffect(
|
||||
from: effect.from,
|
||||
into: effect.into,
|
||||
},
|
||||
initialized,
|
||||
aliased,
|
||||
effects,
|
||||
);
|
||||
let value = context.effectInstructionValueCache.get(effect);
|
||||
@@ -816,7 +768,12 @@ function applyEffect(
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
state.assign(effect.into, effect.from);
|
||||
if (aliased.has(effect.into.identifier.id)) {
|
||||
state.appendAlias(effect.into, effect.from);
|
||||
} else {
|
||||
aliased.add(effect.into.identifier.id);
|
||||
state.alias(effect.into, effect.from);
|
||||
}
|
||||
effects.push(effect);
|
||||
break;
|
||||
}
|
||||
@@ -827,8 +784,7 @@ function applyEffect(
|
||||
const functionValues = state.values(effect.function);
|
||||
if (
|
||||
functionValues.length === 1 &&
|
||||
functionValues[0].kind === 'FunctionExpression' &&
|
||||
functionValues[0].loweredFunc.func.aliasingEffects != null
|
||||
functionValues[0].kind === 'FunctionExpression'
|
||||
) {
|
||||
/*
|
||||
* We're calling a locally declared function, we already know it's effects!
|
||||
@@ -863,15 +819,18 @@ function applyEffect(
|
||||
),
|
||||
);
|
||||
if (signatureEffects != null) {
|
||||
if (DEBUG) {
|
||||
console.log('apply function expression effects');
|
||||
}
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
{kind: 'MutateTransitiveConditionally', value: effect.function},
|
||||
initialized,
|
||||
aliased,
|
||||
effects,
|
||||
);
|
||||
for (const signatureEffect of signatureEffects) {
|
||||
applyEffect(context, state, signatureEffect, initialized, effects);
|
||||
applyEffect(context, state, signatureEffect, aliased, effects);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -895,10 +854,16 @@ function applyEffect(
|
||||
);
|
||||
}
|
||||
if (signatureEffects != null) {
|
||||
if (DEBUG) {
|
||||
console.log('apply aliasing signature effects');
|
||||
}
|
||||
for (const signatureEffect of signatureEffects) {
|
||||
applyEffect(context, state, signatureEffect, initialized, effects);
|
||||
applyEffect(context, state, signatureEffect, aliased, effects);
|
||||
}
|
||||
} else if (effect.signature != null) {
|
||||
if (DEBUG) {
|
||||
console.log('apply legacy signature effects');
|
||||
}
|
||||
const legacyEffects = computeEffectsForLegacySignature(
|
||||
state,
|
||||
effect.signature,
|
||||
@@ -908,9 +873,12 @@ function applyEffect(
|
||||
effect.loc,
|
||||
);
|
||||
for (const legacyEffect of legacyEffects) {
|
||||
applyEffect(context, state, legacyEffect, initialized, effects);
|
||||
applyEffect(context, state, legacyEffect, aliased, effects);
|
||||
}
|
||||
} else {
|
||||
if (DEBUG) {
|
||||
console.log('default effects');
|
||||
}
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
@@ -920,7 +888,7 @@ function applyEffect(
|
||||
value: ValueKind.Mutable,
|
||||
reason: ValueReason.Other,
|
||||
},
|
||||
initialized,
|
||||
aliased,
|
||||
effects,
|
||||
);
|
||||
/*
|
||||
@@ -943,21 +911,21 @@ function applyEffect(
|
||||
kind: 'MutateTransitiveConditionally',
|
||||
value: operand,
|
||||
},
|
||||
initialized,
|
||||
aliased,
|
||||
effects,
|
||||
);
|
||||
}
|
||||
const mutateIterator =
|
||||
arg.kind === 'Spread' ? conditionallyMutateIterator(operand) : null;
|
||||
if (mutateIterator) {
|
||||
applyEffect(context, state, mutateIterator, initialized, effects);
|
||||
applyEffect(context, state, mutateIterator, aliased, effects);
|
||||
}
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
// OK: recording information flow
|
||||
{kind: 'MaybeAlias', from: operand, into: effect.into},
|
||||
initialized,
|
||||
{kind: 'Alias', from: operand, into: effect.into},
|
||||
aliased,
|
||||
effects,
|
||||
);
|
||||
for (const otherArg of [
|
||||
@@ -985,7 +953,7 @@ function applyEffect(
|
||||
from: operand,
|
||||
into: other,
|
||||
},
|
||||
initialized,
|
||||
aliased,
|
||||
effects,
|
||||
);
|
||||
}
|
||||
@@ -1018,31 +986,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({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot access variable before it is declared',
|
||||
description: `${variable ?? 'This variable'} is accessed before it is declared, which prevents the earlier access from updating when this value changes over time.`,
|
||||
});
|
||||
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,
|
||||
},
|
||||
},
|
||||
aliased,
|
||||
effects,
|
||||
);
|
||||
}
|
||||
diagnostic.withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `${variable ?? 'variable'} is declared here`,
|
||||
});
|
||||
|
||||
applyEffect(
|
||||
context,
|
||||
@@ -1050,9 +1020,15 @@ 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,
|
||||
aliased,
|
||||
effects,
|
||||
);
|
||||
} else {
|
||||
@@ -1061,11 +1037,11 @@ function applyEffect(
|
||||
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';
|
||||
? `Found mutation of \`${effect.value.identifier.name.value}\``
|
||||
: null;
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
@@ -1075,17 +1051,15 @@ function applyEffect(
|
||||
? 'MutateFrozen'
|
||||
: 'MutateGlobal',
|
||||
place: effect.value,
|
||||
error: CompilerDiagnostic.create({
|
||||
error: {
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'This value cannot be modified',
|
||||
description: `${reason}.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
reason,
|
||||
description,
|
||||
loc: effect.value.loc,
|
||||
message: `${variable} cannot be modified`,
|
||||
}),
|
||||
suggestions: null,
|
||||
},
|
||||
},
|
||||
initialized,
|
||||
aliased,
|
||||
effects,
|
||||
);
|
||||
}
|
||||
@@ -1192,7 +1166,7 @@ class InferenceState {
|
||||
}
|
||||
|
||||
// Updates the value at @param place to point to the same value as @param value.
|
||||
assign(place: Place, value: Place): void {
|
||||
alias(place: Place, value: Place): void {
|
||||
const values = this.#variables.get(value.identifier.id);
|
||||
CompilerError.invariant(values != null, {
|
||||
reason: `[InferMutationAliasingEffects] Expected value for identifier to be initialized`,
|
||||
@@ -1270,6 +1244,9 @@ class InferenceState {
|
||||
kind: ValueKind.Frozen,
|
||||
reason: new Set([reason]),
|
||||
});
|
||||
if (DEBUG) {
|
||||
console.log(`freeze value: ${printInstructionValue(value)} ${reason}`);
|
||||
}
|
||||
if (
|
||||
value.kind === 'FunctionExpression' &&
|
||||
(this.env.config.enablePreserveExistingMemoizationGuarantees ||
|
||||
@@ -1324,7 +1301,7 @@ class InferenceState {
|
||||
return 'mutate-global';
|
||||
}
|
||||
case ValueKind.MaybeFrozen: {
|
||||
return 'mutate-frozen';
|
||||
return 'none';
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(kind, `Unexpected kind ${kind}`);
|
||||
@@ -1863,34 +1840,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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2002,20 +1964,16 @@ function computeSignatureForInstruction(
|
||||
break;
|
||||
}
|
||||
case 'StoreGlobal': {
|
||||
const variable = `\`${value.name}\``;
|
||||
effects.push({
|
||||
kind: 'MutateGlobal',
|
||||
place: value.value,
|
||||
error: CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category:
|
||||
'Cannot reassign variables declared outside of the component/hook',
|
||||
description: `Variable ${variable} is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
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,
|
||||
message: `${variable} cannot be reassigned`,
|
||||
}),
|
||||
suggestions: null,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
},
|
||||
});
|
||||
effects.push({kind: 'Assign', from: value.value, into: lvalue});
|
||||
break;
|
||||
@@ -2105,19 +2063,17 @@ function computeEffectsForLegacySignature(
|
||||
effects.push({
|
||||
kind: 'Impure',
|
||||
place: receiver,
|
||||
error: CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: '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> = [];
|
||||
@@ -2147,6 +2103,8 @@ function computeEffectsForLegacySignature(
|
||||
const mutateIterator = conditionallyMutateIterator(place);
|
||||
if (mutateIterator != null) {
|
||||
effects.push(mutateIterator);
|
||||
// TODO: should we always push to captures?
|
||||
captures.push(place);
|
||||
}
|
||||
effects.push({
|
||||
kind: 'Capture',
|
||||
@@ -2328,6 +2286,17 @@ function computeEffectsForSignature(
|
||||
// Too many args and there is no rest param to hold them
|
||||
(args.length > signature.params.length && signature.rest == null)
|
||||
) {
|
||||
if (DEBUG) {
|
||||
if (signature.params.length > args.length) {
|
||||
console.log(
|
||||
`not enough args: ${args.length} args for ${signature.params.length} params`,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`too many args: ${args.length} args for ${signature.params.length} params, with no rest param`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// Build substitutions
|
||||
@@ -2342,6 +2311,9 @@ function computeEffectsForSignature(
|
||||
continue;
|
||||
} else if (params == null || i >= params.length || arg.kind === 'Spread') {
|
||||
if (signature.rest == null) {
|
||||
if (DEBUG) {
|
||||
console.log(`no rest value to hold param`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const place = arg.kind === 'Identifier' ? arg : arg.place;
|
||||
@@ -2377,7 +2349,6 @@ function computeEffectsForSignature(
|
||||
// Apply substitutions
|
||||
for (const effect of signature.effects) {
|
||||
switch (effect.kind) {
|
||||
case 'MaybeAlias':
|
||||
case 'Assign':
|
||||
case 'ImmutableCapture':
|
||||
case 'Alias':
|
||||
@@ -2450,14 +2421,23 @@ function computeEffectsForSignature(
|
||||
case 'Apply': {
|
||||
const applyReceiver = substitutions.get(effect.receiver.identifier.id);
|
||||
if (applyReceiver == null || applyReceiver.length !== 1) {
|
||||
if (DEBUG) {
|
||||
console.log(`too many substitutions for receiver`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const applyFunction = substitutions.get(effect.function.identifier.id);
|
||||
if (applyFunction == null || applyFunction.length !== 1) {
|
||||
if (DEBUG) {
|
||||
console.log(`too many substitutions for function`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const applyInto = substitutions.get(effect.into.identifier.id);
|
||||
if (applyInto == null || applyInto.length !== 1) {
|
||||
if (DEBUG) {
|
||||
console.log(`too many substitutions for into`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const applyArgs: Array<Place | SpreadPattern | Hole> = [];
|
||||
@@ -2467,12 +2447,18 @@ function computeEffectsForSignature(
|
||||
} else if (arg.kind === 'Identifier') {
|
||||
const applyArg = substitutions.get(arg.identifier.id);
|
||||
if (applyArg == null || applyArg.length !== 1) {
|
||||
if (DEBUG) {
|
||||
console.log(`too many substitutions for arg`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
applyArgs.push(applyArg[0]);
|
||||
} else {
|
||||
const applyArg = substitutions.get(arg.place.identifier.id);
|
||||
if (applyArg == null || applyArg.length !== 1) {
|
||||
if (DEBUG) {
|
||||
console.log(`too many substitutions for arg`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
applyArgs.push({kind: 'Spread', place: applyArg[0]});
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* 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, Place, ValueKind, ValueReason} from '../HIR';
|
||||
import {getOrInsertDefault} from '../Utils/utils';
|
||||
import {AliasingEffect} from './AliasingEffects';
|
||||
|
||||
/**
|
||||
* This function tracks data flow within an inner function expression in order to
|
||||
* compute a set of data-flow aliasing effects describing data flow between the function's
|
||||
* params, context variables, and return value.
|
||||
*
|
||||
* For example, consider the following function expression:
|
||||
*
|
||||
* ```
|
||||
* (x) => { return [x, y] }
|
||||
* ```
|
||||
*
|
||||
* This function captures both param `x` and context variable `y` into the return value.
|
||||
* Unlike our previous inference which counted this as a mutation of x and y, we want to
|
||||
* build a signature for the function that describes the data flow. We would infer
|
||||
* `Capture x -> return, Capture y -> return` effects for this function.
|
||||
*
|
||||
* This function *also* propagates more ambient-style effects (MutateFrozen, MutateGlobal, Impure, Render)
|
||||
* from instructions within the function up to the function itself.
|
||||
*/
|
||||
export function inferMutationAliasingFunctionEffects(
|
||||
fn: HIRFunction,
|
||||
): Array<AliasingEffect> | null {
|
||||
const effects: Array<AliasingEffect> = [];
|
||||
|
||||
/**
|
||||
* Map used to identify tracked variables: params, context vars, return value
|
||||
* This is used to detect mutation/capturing/aliasing of params/context vars
|
||||
*/
|
||||
const tracked = new Map<IdentifierId, Place>();
|
||||
tracked.set(fn.returns.identifier.id, fn.returns);
|
||||
for (const operand of [...fn.context, ...fn.params]) {
|
||||
const place = operand.kind === 'Identifier' ? operand : operand.place;
|
||||
tracked.set(place.identifier.id, place);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track capturing/aliasing of context vars and params into each other and into the return.
|
||||
* We don't need to track locals and intermediate values, since we're only concerned with effects
|
||||
* as they relate to arguments visible outside the function.
|
||||
*
|
||||
* For each aliased identifier we track capture/alias/createfrom and then merge this with how
|
||||
* the value is used. Eg capturing an alias => capture. See joinEffects() helper.
|
||||
*/
|
||||
type AliasedIdentifier = {
|
||||
kind: AliasingKind;
|
||||
place: Place;
|
||||
};
|
||||
const dataFlow = new Map<IdentifierId, Array<AliasedIdentifier>>();
|
||||
|
||||
/*
|
||||
* Check for aliasing of tracked values. Also joins the effects of how the value is
|
||||
* used (@param kind) with the aliasing type of each value
|
||||
*/
|
||||
function lookup(
|
||||
place: Place,
|
||||
kind: AliasedIdentifier['kind'],
|
||||
): Array<AliasedIdentifier> | null {
|
||||
if (tracked.has(place.identifier.id)) {
|
||||
return [{kind, place}];
|
||||
}
|
||||
return (
|
||||
dataFlow.get(place.identifier.id)?.map(aliased => ({
|
||||
kind: joinEffects(aliased.kind, kind),
|
||||
place: aliased.place,
|
||||
})) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
// todo: fixpoint
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const phi of block.phis) {
|
||||
const operands: Array<AliasedIdentifier> = [];
|
||||
for (const operand of phi.operands.values()) {
|
||||
const inputs = lookup(operand, 'Alias');
|
||||
if (inputs != null) {
|
||||
operands.push(...inputs);
|
||||
}
|
||||
}
|
||||
if (operands.length !== 0) {
|
||||
dataFlow.set(phi.place.identifier.id, operands);
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
if (instr.effects == null) continue;
|
||||
for (const effect of instr.effects) {
|
||||
if (
|
||||
effect.kind === 'Assign' ||
|
||||
effect.kind === 'Capture' ||
|
||||
effect.kind === 'Alias' ||
|
||||
effect.kind === 'CreateFrom'
|
||||
) {
|
||||
const from = lookup(effect.from, effect.kind);
|
||||
if (from == null) {
|
||||
continue;
|
||||
}
|
||||
const into = lookup(effect.into, 'Alias');
|
||||
if (into == null) {
|
||||
getOrInsertDefault(dataFlow, effect.into.identifier.id, []).push(
|
||||
...from,
|
||||
);
|
||||
} else {
|
||||
for (const aliased of into) {
|
||||
getOrInsertDefault(
|
||||
dataFlow,
|
||||
aliased.place.identifier.id,
|
||||
[],
|
||||
).push(...from);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
effect.kind === 'Create' ||
|
||||
effect.kind === 'CreateFunction'
|
||||
) {
|
||||
getOrInsertDefault(dataFlow, effect.into.identifier.id, [
|
||||
{kind: 'Alias', place: effect.into},
|
||||
]);
|
||||
} else if (
|
||||
effect.kind === 'MutateFrozen' ||
|
||||
effect.kind === 'MutateGlobal' ||
|
||||
effect.kind === 'Impure' ||
|
||||
effect.kind === 'Render'
|
||||
) {
|
||||
effects.push(effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (block.terminal.kind === 'return') {
|
||||
const from = lookup(block.terminal.value, 'Alias');
|
||||
if (from != null) {
|
||||
getOrInsertDefault(dataFlow, fn.returns.identifier.id, []).push(
|
||||
...from,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create aliasing effects based on observed data flow
|
||||
let hasReturn = false;
|
||||
for (const [into, from] of dataFlow) {
|
||||
const input = tracked.get(into);
|
||||
if (input == null) {
|
||||
continue;
|
||||
}
|
||||
for (const aliased of from) {
|
||||
if (
|
||||
aliased.place.identifier.id === input.identifier.id ||
|
||||
!tracked.has(aliased.place.identifier.id)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const effect = {kind: aliased.kind, from: aliased.place, into: input};
|
||||
effects.push(effect);
|
||||
if (
|
||||
into === fn.returns.identifier.id &&
|
||||
(aliased.kind === 'Assign' || aliased.kind === 'CreateFrom')
|
||||
) {
|
||||
hasReturn = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: more precise return effect inference
|
||||
if (!hasReturn) {
|
||||
effects.unshift({
|
||||
kind: 'Create',
|
||||
into: fn.returns,
|
||||
value:
|
||||
fn.returnType.kind === 'Primitive'
|
||||
? ValueKind.Primitive
|
||||
: ValueKind.Mutable,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
});
|
||||
}
|
||||
|
||||
return effects;
|
||||
}
|
||||
|
||||
export enum MutationKind {
|
||||
None = 0,
|
||||
Conditional = 1,
|
||||
Definite = 2,
|
||||
}
|
||||
|
||||
type AliasingKind = 'Alias' | 'Capture' | 'CreateFrom' | 'Assign';
|
||||
function joinEffects(
|
||||
effect1: AliasingKind,
|
||||
effect2: AliasingKind,
|
||||
): AliasingKind {
|
||||
if (effect1 === 'Capture' || effect2 === 'Capture') {
|
||||
return 'Capture';
|
||||
} else if (effect1 === 'Assign' || effect2 === 'Assign') {
|
||||
return 'Assign';
|
||||
} else {
|
||||
return 'Alias';
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import prettyFormat from 'pretty-format';
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
import {
|
||||
BlockId,
|
||||
@@ -13,12 +14,8 @@ import {
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
InstructionId,
|
||||
isJsxType,
|
||||
makeInstructionId,
|
||||
ValueKind,
|
||||
ValueReason,
|
||||
Place,
|
||||
isPrimitiveType,
|
||||
} from '../HIR/HIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
@@ -26,58 +23,43 @@ import {
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {AliasingEffect} from './AliasingEffects';
|
||||
import {printFunction} from '../HIR';
|
||||
import {printIdentifier, printPlace} from '../HIR/PrintHIR';
|
||||
import {MutationKind} from './InferMutationAliasingFunctionEffects';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
const DEBUG = false;
|
||||
const VERBOSE = false;
|
||||
|
||||
/**
|
||||
* This pass builds an abstract model of the heap and interprets the effects of the
|
||||
* given function in order to determine the following:
|
||||
* - The mutable ranges of all identifiers in the function
|
||||
* - The externally-visible effects of the function, such as mutations of params and
|
||||
* context-vars, aliasing between params/context-vars/return-value, and impure side
|
||||
* effects.
|
||||
* - The legacy `Effect` to store on each Place.
|
||||
*
|
||||
* This pass builds a data flow graph using the effects, tracking an abstract notion
|
||||
* of "when" each effect occurs relative to the others. It then walks each mutation
|
||||
* effect against the graph, updating the range of each node that would be reachable
|
||||
* at the "time" that the effect occurred.
|
||||
* Infers mutable ranges for all values in the program, using previously inferred
|
||||
* mutation/aliasing effects. This pass builds a data flow graph using the effects,
|
||||
* tracking an abstract notion of "when" each effect occurs relative to the others.
|
||||
* It then walks each mutation effect against the graph, updating the range of each
|
||||
* node that would be reachable at the "time" that the effect occurred.
|
||||
*
|
||||
* This pass also validates against invalid effects: any function that is reachable
|
||||
* by being called, or via a Render effect, is validated against mutating globals
|
||||
* or calling impure code.
|
||||
*
|
||||
* Note that this function also populates the outer function's aliasing effects with
|
||||
* any mutations that apply to its params or context variables.
|
||||
*
|
||||
* ## Example
|
||||
* A function expression such as the following:
|
||||
* any mutations that apply to its params or context variables. For example, a
|
||||
* function expression such as the following:
|
||||
*
|
||||
* ```
|
||||
* (x) => { x.y = true }
|
||||
* ```
|
||||
*
|
||||
* Would populate a `Mutate x` aliasing effect on the outer function.
|
||||
*
|
||||
* ## Returned Function Effects
|
||||
*
|
||||
* The function returns (if successful) a list of externally-visible effects.
|
||||
* This is determined by simulating a conditional, transitive mutation against
|
||||
* each param, context variable, and return value in turn, and seeing which other
|
||||
* such values are affected. If they're affected, they must be captured, so we
|
||||
* record a Capture.
|
||||
*
|
||||
* The only tricky bit is the return value, which could _alias_ (or even assign)
|
||||
* one or more of the params/context-vars rather than just capturing. So we have
|
||||
* to do a bit more tracking for returns.
|
||||
*/
|
||||
export function inferMutationAliasingRanges(
|
||||
fn: HIRFunction,
|
||||
{isFunctionExpression}: {isFunctionExpression: boolean},
|
||||
): Result<Array<AliasingEffect>, CompilerError> {
|
||||
// The set of externally-visible effects
|
||||
const functionEffects: Array<AliasingEffect> = [];
|
||||
|
||||
): Result<void, CompilerError> {
|
||||
if (VERBOSE) {
|
||||
console.log();
|
||||
console.log(printFunction(fn));
|
||||
}
|
||||
/**
|
||||
* Part 1: Infer mutable ranges for values. We build an abstract model of
|
||||
* values, the alias/capture edges between them, and the set of mutations.
|
||||
@@ -133,6 +115,20 @@ export function inferMutationAliasingRanges(
|
||||
seenBlocks.add(block.id);
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
if (
|
||||
instr.value.kind === 'FunctionExpression' ||
|
||||
instr.value.kind === 'ObjectMethod'
|
||||
) {
|
||||
state.create(instr.lvalue, {
|
||||
kind: 'Function',
|
||||
function: instr.value.loweredFunc.func,
|
||||
});
|
||||
} else {
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
state.create(lvalue, {kind: 'Object'});
|
||||
}
|
||||
}
|
||||
|
||||
if (instr.effects == null) continue;
|
||||
for (const effect of instr.effects) {
|
||||
if (effect.kind === 'Create') {
|
||||
@@ -145,23 +141,12 @@ export function inferMutationAliasingRanges(
|
||||
} else if (effect.kind === 'CreateFrom') {
|
||||
state.createFrom(index++, effect.from, effect.into);
|
||||
} else if (effect.kind === 'Assign') {
|
||||
/**
|
||||
* TODO: Invariant that the node is not initialized yet
|
||||
*
|
||||
* InferFunctionExpressionAliasingEffectSignatures currently infers
|
||||
* Assign effects in some places that should be Alias, leading to
|
||||
* Assign effects that reinitialize a value. The end result appears to
|
||||
* be fine, but we should fix that inference pass so that we add the
|
||||
* invariant here.
|
||||
*/
|
||||
if (!state.nodes.has(effect.into.identifier)) {
|
||||
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 (
|
||||
@@ -197,11 +182,9 @@ export function inferMutationAliasingRanges(
|
||||
effect.kind === 'MutateGlobal' ||
|
||||
effect.kind === 'Impure'
|
||||
) {
|
||||
errors.pushDiagnostic(effect.error);
|
||||
functionEffects.push(effect);
|
||||
errors.push(effect.error);
|
||||
} else if (effect.kind === 'Render') {
|
||||
renders.push({index: index++, place: effect.place});
|
||||
functionEffects.push(effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,6 +216,10 @@ export function inferMutationAliasingRanges(
|
||||
}
|
||||
}
|
||||
|
||||
if (VERBOSE) {
|
||||
console.log(state.debug());
|
||||
console.log(pretty(mutations));
|
||||
}
|
||||
for (const mutation of mutations) {
|
||||
state.mutate(
|
||||
mutation.index,
|
||||
@@ -247,9 +234,12 @@ export function inferMutationAliasingRanges(
|
||||
for (const render of renders) {
|
||||
state.render(render.index, render.place.identifier, errors);
|
||||
}
|
||||
if (DEBUG) {
|
||||
console.log(pretty([...state.nodes.keys()]));
|
||||
}
|
||||
fn.aliasingEffects ??= [];
|
||||
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;
|
||||
@@ -258,13 +248,13 @@ export function inferMutationAliasingRanges(
|
||||
if (node.local != null) {
|
||||
if (node.local.kind === MutationKind.Conditional) {
|
||||
mutated = true;
|
||||
functionEffects.push({
|
||||
fn.aliasingEffects.push({
|
||||
kind: 'MutateConditionally',
|
||||
value: {...place, loc: node.local.loc},
|
||||
});
|
||||
} else if (node.local.kind === MutationKind.Definite) {
|
||||
mutated = true;
|
||||
functionEffects.push({
|
||||
fn.aliasingEffects.push({
|
||||
kind: 'Mutate',
|
||||
value: {...place, loc: node.local.loc},
|
||||
});
|
||||
@@ -273,13 +263,13 @@ export function inferMutationAliasingRanges(
|
||||
if (node.transitive != null) {
|
||||
if (node.transitive.kind === MutationKind.Conditional) {
|
||||
mutated = true;
|
||||
functionEffects.push({
|
||||
fn.aliasingEffects.push({
|
||||
kind: 'MutateTransitiveConditionally',
|
||||
value: {...place, loc: node.transitive.loc},
|
||||
});
|
||||
} else if (node.transitive.kind === MutationKind.Definite) {
|
||||
mutated = true;
|
||||
functionEffects.push({
|
||||
fn.aliasingEffects.push({
|
||||
kind: 'MutateTransitive',
|
||||
value: {...place, loc: node.transitive.loc},
|
||||
});
|
||||
@@ -349,8 +339,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) {
|
||||
@@ -469,82 +458,10 @@ export function inferMutationAliasingRanges(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Part 3
|
||||
* Finish populating the externally visible effects. Above we bubble-up the side effects
|
||||
* (MutateFrozen/MutableGlobal/Impure/Render) as well as mutations of context variables.
|
||||
* 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);
|
||||
if (VERBOSE) {
|
||||
console.log(printFunction(fn));
|
||||
}
|
||||
for (const into of tracked) {
|
||||
const mutationIndex = index++;
|
||||
state.mutate(
|
||||
mutationIndex,
|
||||
into.identifier,
|
||||
null,
|
||||
true,
|
||||
MutationKind.Conditional,
|
||||
into.loc,
|
||||
ignoredErrors,
|
||||
);
|
||||
for (const from of tracked) {
|
||||
if (
|
||||
from.identifier.id === into.identifier.id ||
|
||||
from.identifier.id === fn.returns.identifier.id
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const fromNode = state.nodes.get(from.identifier);
|
||||
CompilerError.invariant(fromNode != null, {
|
||||
reason: `Expected a node to exist for all parameters and context variables`,
|
||||
loc: into.loc,
|
||||
});
|
||||
if (fromNode.lastMutated === mutationIndex) {
|
||||
if (into.identifier.id === fn.returns.identifier.id) {
|
||||
// The return value could be any of the params/context variables
|
||||
functionEffects.push({
|
||||
kind: 'Alias',
|
||||
from,
|
||||
into,
|
||||
});
|
||||
} else {
|
||||
// Otherwise params/context-vars can only capture each other
|
||||
functionEffects.push({
|
||||
kind: 'Capture',
|
||||
from,
|
||||
into,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.hasErrors() && !isFunctionExpression) {
|
||||
return Err(errors);
|
||||
}
|
||||
return Ok(functionEffects);
|
||||
return errors.asResult();
|
||||
}
|
||||
|
||||
function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
|
||||
@@ -553,33 +470,21 @@ function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
|
||||
case 'Impure':
|
||||
case 'MutateFrozen':
|
||||
case 'MutateGlobal': {
|
||||
errors.pushDiagnostic(effect.error);
|
||||
errors.push(effect.error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export enum MutationKind {
|
||||
None = 0,
|
||||
Conditional = 1,
|
||||
Definite = 2,
|
||||
}
|
||||
|
||||
type Node = {
|
||||
id: Identifier;
|
||||
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;
|
||||
value:
|
||||
| {kind: 'Object'}
|
||||
| {kind: 'Phi'}
|
||||
@@ -594,11 +499,9 @@ class AliasingState {
|
||||
createdFrom: new Map(),
|
||||
captures: new Map(),
|
||||
aliases: new Map(),
|
||||
maybeAliases: new Map(),
|
||||
edges: [],
|
||||
transitive: null,
|
||||
local: null,
|
||||
lastMutated: 0,
|
||||
value,
|
||||
});
|
||||
}
|
||||
@@ -608,6 +511,11 @@ class AliasingState {
|
||||
const fromNode = this.nodes.get(from.identifier);
|
||||
const toNode = this.nodes.get(into.identifier);
|
||||
if (fromNode == null || toNode == null) {
|
||||
if (VERBOSE) {
|
||||
console.log(
|
||||
`skip: createFrom ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
fromNode.edges.push({index, node: into.identifier, kind: 'alias'});
|
||||
@@ -620,6 +528,11 @@ class AliasingState {
|
||||
const fromNode = this.nodes.get(from.identifier);
|
||||
const toNode = this.nodes.get(into.identifier);
|
||||
if (fromNode == null || toNode == null) {
|
||||
if (VERBOSE) {
|
||||
console.log(
|
||||
`skip: capture ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
fromNode.edges.push({index, node: into.identifier, kind: 'capture'});
|
||||
@@ -632,6 +545,11 @@ class AliasingState {
|
||||
const fromNode = this.nodes.get(from.identifier);
|
||||
const toNode = this.nodes.get(into.identifier);
|
||||
if (fromNode == null || toNode == null) {
|
||||
if (VERBOSE) {
|
||||
console.log(
|
||||
`skip: assign ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
fromNode.edges.push({index, node: into.identifier, kind: 'alias'});
|
||||
@@ -640,18 +558,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];
|
||||
@@ -692,37 +598,46 @@ class AliasingState {
|
||||
mutate(
|
||||
index: number,
|
||||
start: Identifier,
|
||||
// Null is used for simulated mutations
|
||||
end: InstructionId | null,
|
||||
end: InstructionId,
|
||||
transitive: boolean,
|
||||
startKind: MutationKind,
|
||||
kind: MutationKind,
|
||||
loc: SourceLocation,
|
||||
errors: CompilerError,
|
||||
): void {
|
||||
const seen = new Map<Identifier, MutationKind>();
|
||||
if (DEBUG) {
|
||||
console.log(
|
||||
`mutate ix=${index} start=$${start.id} end=[${end}]${transitive ? ' transitive' : ''} kind=${kind}`,
|
||||
);
|
||||
}
|
||||
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) {
|
||||
if (DEBUG) {
|
||||
console.log(
|
||||
`no node! ${printIdentifier(start)} for identifier ${printIdentifier(current)}`,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
node.lastMutated = Math.max(node.lastMutated, index);
|
||||
if (end != null) {
|
||||
node.id.mutableRange.end = makeInstructionId(
|
||||
Math.max(node.id.mutableRange.end, end),
|
||||
if (DEBUG) {
|
||||
console.log(
|
||||
` mutate $${node.id.id} transitive=${transitive} direction=${direction}`,
|
||||
);
|
||||
}
|
||||
node.id.mutableRange.end = makeInstructionId(
|
||||
Math.max(node.id.mutableRange.end, end),
|
||||
);
|
||||
if (
|
||||
node.value.kind === 'Function' &&
|
||||
node.transitive == null &&
|
||||
@@ -748,18 +663,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') {
|
||||
/**
|
||||
@@ -776,25 +686,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'});
|
||||
}
|
||||
}
|
||||
/**
|
||||
@@ -805,14 +697,41 @@ class AliasingState {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push({
|
||||
place: capture,
|
||||
transitive,
|
||||
direction: 'backwards',
|
||||
kind,
|
||||
});
|
||||
queue.push({place: capture, transitive, direction: 'backwards'});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (DEBUG) {
|
||||
const nodes = new Map();
|
||||
for (const id of seen) {
|
||||
const node = this.nodes.get(id);
|
||||
nodes.set(id.id, node);
|
||||
}
|
||||
console.log(pretty(nodes));
|
||||
}
|
||||
}
|
||||
|
||||
debug(): string {
|
||||
return pretty(this.nodes);
|
||||
}
|
||||
}
|
||||
|
||||
export function pretty(v: any): string {
|
||||
return prettyFormat(v, {
|
||||
plugins: [
|
||||
{
|
||||
test: v =>
|
||||
v !== null && typeof v === 'object' && v.kind === 'Identifier',
|
||||
serialize: v => printPlace(v),
|
||||
},
|
||||
{
|
||||
test: v =>
|
||||
v !== null &&
|
||||
typeof v === 'object' &&
|
||||
typeof v.declarationId === 'number',
|
||||
serialize: v =>
|
||||
`${printIdentifier(v)}:${v.mutableRange.start}:${v.mutableRange.end}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
Environment,
|
||||
FunctionExpression,
|
||||
GeneratedSource,
|
||||
GotoTerminal,
|
||||
GotoVariant,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
@@ -20,7 +19,6 @@ import {
|
||||
Place,
|
||||
isStatementBlockKind,
|
||||
makeInstructionId,
|
||||
mergeConsecutiveBlocks,
|
||||
promoteTemporary,
|
||||
reversePostorderBlocks,
|
||||
} from '../HIR';
|
||||
@@ -75,10 +73,6 @@ import {retainWhere} from '../Utils/utils';
|
||||
* - All return statements in the original function expression are replaced with a
|
||||
* StoreLocal to the temporary we allocated before plus a Goto to the fallthrough
|
||||
* block (code following the CallExpression).
|
||||
*
|
||||
* Note that if the inliined function has only one return, we avoid the labeled block
|
||||
* and fully inline the code. The original return is replaced with an assignmen to the
|
||||
* IIFE's call expression lvalue.
|
||||
*/
|
||||
export function inlineImmediatelyInvokedFunctionExpressions(
|
||||
fn: HIRFunction,
|
||||
@@ -152,75 +146,37 @@ export function inlineImmediatelyInvokedFunctionExpressions(
|
||||
*/
|
||||
block.instructions.length = ii;
|
||||
|
||||
if (hasSingleExitReturnTerminal(body.loweredFunc.func)) {
|
||||
block.terminal = {
|
||||
kind: 'goto',
|
||||
block: body.loweredFunc.func.body.entry,
|
||||
id: block.terminal.id,
|
||||
loc: block.terminal.loc,
|
||||
variant: GotoVariant.Break,
|
||||
} as GotoTerminal;
|
||||
for (const block of body.loweredFunc.func.body.blocks.values()) {
|
||||
if (block.terminal.kind === 'return') {
|
||||
block.instructions.push({
|
||||
id: makeInstructionId(0),
|
||||
loc: block.terminal.loc,
|
||||
lvalue: instr.lvalue,
|
||||
value: {
|
||||
kind: 'LoadLocal',
|
||||
loc: block.terminal.loc,
|
||||
place: block.terminal.value,
|
||||
},
|
||||
effects: null,
|
||||
});
|
||||
block.terminal = {
|
||||
kind: 'goto',
|
||||
block: continuationBlockId,
|
||||
id: block.terminal.id,
|
||||
loc: block.terminal.loc,
|
||||
variant: GotoVariant.Break,
|
||||
} as GotoTerminal;
|
||||
}
|
||||
}
|
||||
for (const [id, block] of body.loweredFunc.func.body.blocks) {
|
||||
block.preds.clear();
|
||||
fn.body.blocks.set(id, block);
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
* To account for multiple returns within the lambda, we treat the lambda
|
||||
* as if it were a single labeled statement, and replace all returns with gotos
|
||||
* to the label fallthrough.
|
||||
*/
|
||||
const newTerminal: LabelTerminal = {
|
||||
block: body.loweredFunc.func.body.entry,
|
||||
id: makeInstructionId(0),
|
||||
kind: 'label',
|
||||
fallthrough: continuationBlockId,
|
||||
loc: block.terminal.loc,
|
||||
};
|
||||
block.terminal = newTerminal;
|
||||
/*
|
||||
* To account for complex control flow within the lambda, we treat the lambda
|
||||
* as if it were a single labeled statement, and replace all returns with gotos
|
||||
* to the label fallthrough.
|
||||
*/
|
||||
const newTerminal: LabelTerminal = {
|
||||
block: body.loweredFunc.func.body.entry,
|
||||
id: makeInstructionId(0),
|
||||
kind: 'label',
|
||||
fallthrough: continuationBlockId,
|
||||
loc: block.terminal.loc,
|
||||
};
|
||||
block.terminal = newTerminal;
|
||||
|
||||
// We store the result in the IIFE temporary
|
||||
const result = instr.lvalue;
|
||||
// We store the result in the IIFE temporary
|
||||
const result = instr.lvalue;
|
||||
|
||||
// Declare the IIFE temporary
|
||||
declareTemporary(fn.env, block, result);
|
||||
// Declare the IIFE temporary
|
||||
declareTemporary(fn.env, block, result);
|
||||
|
||||
// Promote the temporary with a name as we require this to persist
|
||||
if (result.identifier.name == null) {
|
||||
promoteTemporary(result.identifier);
|
||||
}
|
||||
// Promote the temporary with a name as we require this to persist
|
||||
promoteTemporary(result.identifier);
|
||||
|
||||
/*
|
||||
* Rewrite blocks from the lambda to replace any `return` with a
|
||||
* store to the result and `goto` the continuation block
|
||||
*/
|
||||
for (const [id, block] of body.loweredFunc.func.body.blocks) {
|
||||
block.preds.clear();
|
||||
rewriteBlock(fn.env, block, continuationBlockId, result);
|
||||
fn.body.blocks.set(id, block);
|
||||
}
|
||||
/*
|
||||
* Rewrite blocks from the lambda to replace any `return` with a
|
||||
* store to the result and `goto` the continuation block
|
||||
*/
|
||||
for (const [id, block] of body.loweredFunc.func.body.blocks) {
|
||||
block.preds.clear();
|
||||
rewriteBlock(fn.env, block, continuationBlockId, result);
|
||||
fn.body.blocks.set(id, block);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -243,7 +199,7 @@ export function inlineImmediatelyInvokedFunctionExpressions(
|
||||
|
||||
if (inlinedFunctions.size !== 0) {
|
||||
// Remove instructions that define lambdas which we inlined
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
retainWhere(
|
||||
block.instructions,
|
||||
instr => !inlinedFunctions.has(instr.lvalue.identifier.id),
|
||||
@@ -257,25 +213,9 @@ export function inlineImmediatelyInvokedFunctionExpressions(
|
||||
reversePostorderBlocks(fn.body);
|
||||
markInstructionIds(fn.body);
|
||||
markPredecessors(fn.body);
|
||||
mergeConsecutiveBlocks(fn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the function has a single exit terminal (throw/return) which is a return
|
||||
*/
|
||||
function hasSingleExitReturnTerminal(fn: HIRFunction): boolean {
|
||||
let hasReturn = false;
|
||||
let exitCount = 0;
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
if (block.terminal.kind === 'return' || block.terminal.kind === 'throw') {
|
||||
hasReturn ||= block.terminal.kind === 'return';
|
||||
exitCount++;
|
||||
}
|
||||
}
|
||||
return exitCount === 1 && hasReturn;
|
||||
}
|
||||
|
||||
/*
|
||||
* Rewrites the block so that all `return` terminals are replaced:
|
||||
* * Add a StoreLocal <returnValue> = <terminal.value>
|
||||
|
||||
@@ -1,559 +0,0 @@
|
||||
# The Mutability & Aliasing Model
|
||||
|
||||
This document describes the new (as of June 2025) mutability and aliasing model powering React Compiler. The mutability and aliasing system is a conceptual subcomponent whose primary role is to determine minimal sets of values that mutate together, and the range of instructions over which those mutations occur. These minimal sets of values that mutate together, and the corresponding instructions doing those mutations, are ultimately grouped into reactive scopes, which then translate into memoization blocks in the output (after substantial additional processing described in the comments of those passes).
|
||||
|
||||
To build an intuition, consider the following example:
|
||||
|
||||
```js
|
||||
function Component() {
|
||||
// a is created and mutated over the course of these two instructions:
|
||||
const a = {};
|
||||
mutate(a);
|
||||
|
||||
// b and c are created and mutated together — mutate might modify b via c
|
||||
const b = {};
|
||||
const c = {b};
|
||||
mutate(c);
|
||||
|
||||
// does not modify a/b/c
|
||||
return <Foo a={a} c={c} />
|
||||
}
|
||||
```
|
||||
|
||||
The goal of mutability and aliasing inference is to understand the set of instructions that create/modify a, b, and c.
|
||||
|
||||
In code, the mutability and aliasing model is compromised of the following phases:
|
||||
|
||||
* `InferMutationAliasingEffects`. Infers a set of mutation and aliasing effects for each instruction. The approach is to generate a set of candidate effects based purely on the semantics of each instruction and the types of the operands, then use abstract interpretation to determine the actual effects (or errros) that would apply. For example, an instruction that by default has a Capture effect might downgrade to an ImmutableCapture effect if the value is known to be frozen.
|
||||
* `InferMutationAliasingRanges`. Infers a mutable range (start:end instruction ids) for each value in the program, and annotates each Place with its effect type for usage in later passes. This builds a graph of data flow through the program over time in order to understand which mutations effect which values.
|
||||
* `InferReactiveScopeVariables`. Given the per-Place effects, determines disjoint sets of values that mutate together and assigns all identifiers in each set to a unique scope, and updates the range to include the ranges of all constituent values.
|
||||
|
||||
Finally, `AnalyzeFunctions` needs to understand the mutation and aliasing semantics of nested FunctionExpression and ObjectMethod values. `AnalyzeFunctions` calls `InferFunctionExpressionAliasingEffectsSignature` to determine the publicly observable set of mutation/aliasing effects for nested functions.
|
||||
|
||||
## Mutation and Aliasing Effects
|
||||
|
||||
The inference model is based on a set of "effects" that describe subtle aspects of mutation, aliasing, and other changes to the state of values over time
|
||||
|
||||
### Creation Effects
|
||||
|
||||
#### Create
|
||||
|
||||
```js
|
||||
{
|
||||
kind: 'Create';
|
||||
into: Place;
|
||||
value: ValueKind;
|
||||
reason: ValueReason;
|
||||
}
|
||||
```
|
||||
|
||||
Describes the creation of a new value with the given kind, and reason for having that kind. For example, `x = 10` might have an effect like `Create x = ValueKind.Primitive [ValueReason.Other]`.
|
||||
|
||||
#### CreateFunction
|
||||
|
||||
```js
|
||||
{
|
||||
kind: 'CreateFunction';
|
||||
captures: Array<Place>;
|
||||
function: FunctionExpression | ObjectMethod;
|
||||
into: Place;
|
||||
}
|
||||
```
|
||||
|
||||
Describes the creation of new function value, capturing the given set of mutable values. CreateFunction is used to specifically track function types so that we can precisely model calls to those functions with `Apply`.
|
||||
|
||||
#### Apply
|
||||
|
||||
```js
|
||||
{
|
||||
kind: 'Apply';
|
||||
receiver: Place;
|
||||
function: Place; // same as receiver for function calls
|
||||
mutatesFunction: boolean; // indicates if this is a type that we consdier to mutate the function itself by default
|
||||
args: Array<Place | SpreadPattern | Hole>;
|
||||
into: Place; // where result is stored
|
||||
signature: FunctionSignature | null;
|
||||
}
|
||||
```
|
||||
|
||||
Describes the potential creation of a value by calling a function. This models `new`, function calls, and method calls. The inference algorithm uses the most precise signature it can determine:
|
||||
|
||||
* If the function is a locally created function expression, we use a signature inferred from the behavior of that function to interpret the effects of calling it with the given arguments.
|
||||
* Else if the function has a known aliasing signature (new style precise effects signature), we apply the arguments to that signature to get a precise set of effects.
|
||||
* Else if the function has a legacy style signature (with per-param effects) we convert the legacy per-Place effects into aliasing effects (described in this doc) and apply those.
|
||||
* Else fall back to inferring a generic set of effects.
|
||||
|
||||
The generic fallback is to assume:
|
||||
- The return value may alias any of the arguments (Alias param -> return)
|
||||
- Any arguments *may* be transitively mutated (MutateTransitiveConditionally param)
|
||||
- Any argument may be captured into any other argument (Capture paramN -> paramM for all N,M where N != M)
|
||||
|
||||
### Aliasing Effects
|
||||
|
||||
These effects describe data-flow only, separately from mutation or other state-changing semantics.
|
||||
|
||||
#### Assign
|
||||
|
||||
```js
|
||||
{
|
||||
kind: 'Assign';
|
||||
from: Place;
|
||||
into: Place;
|
||||
}
|
||||
```
|
||||
|
||||
Describes an `x = y` assignment, where the receiving (into) value is overwritten with a new (from) value. After this effect, any previous assignments/aliases to the receiving value are dropped. Note that `Alias` initializes the receiving value.
|
||||
|
||||
> TODO: InferMutationAliasingRanges may not fully reset aliases on encountering this effect
|
||||
|
||||
#### Alias
|
||||
|
||||
```js
|
||||
{
|
||||
kind: 'Alias';
|
||||
from: Place;
|
||||
into: Place;
|
||||
}
|
||||
```
|
||||
|
||||
Describes that an assignment _may_ occur, but that the possible assignment is non-exclusive. The canonical use-case for `Alias` is a function that may return more than one of its arguments, such as `(x, y, z) => x ? y : z`. Here, the result of this function may be `y` or `z`, but neither one overwrites the other. Note that `Alias` does _not_ initialize the receiving value: it should always be paired with an effect to create the receiving value.
|
||||
|
||||
#### Capture
|
||||
|
||||
```js
|
||||
{
|
||||
kind: 'Capture';
|
||||
from: Place;
|
||||
into: Place;
|
||||
}
|
||||
```
|
||||
|
||||
Describes that a reference to one variable (from) is stored within another value (into). Examples include:
|
||||
- An array expression captures the items of the array (`array = [capturedValue]`)
|
||||
- Array.prototype.push captures the pushed values into the array (`array.push(capturedValue)`)
|
||||
- Property assignment captures the value onto the object (`object.property = capturedValue`)
|
||||
|
||||
#### CreateFrom
|
||||
|
||||
```js
|
||||
{
|
||||
kind: 'CreateFrom';
|
||||
from: Place;
|
||||
into: Place;
|
||||
}
|
||||
```
|
||||
|
||||
This is somewhat the inverse of `Capture`. The `CreateFrom` effect describes that a variable is initialized by extracting _part_ of another value, without taking a direct alias to the full other value. Examples include:
|
||||
|
||||
- Indexing into an array (`createdFrom = array[0]`)
|
||||
- Reading an object property (`createdFrom = object.property`)
|
||||
- Getting a Map key (`createdFrom = map.get(key)`)
|
||||
|
||||
#### ImmutableCapture
|
||||
|
||||
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`).
|
||||
|
||||
#### Freeze
|
||||
|
||||
```js
|
||||
{
|
||||
kind: 'Freeze',
|
||||
// The reference being frozen
|
||||
value: Place;
|
||||
// The reason the value is frozen (passed to a hook, passed to jsx, etc)
|
||||
reason: ValueReason;
|
||||
}
|
||||
```
|
||||
|
||||
Once a reference to a value has been passed to React, that value is generally not safe to mutate further. This is not a strictly required property of React, but is a natural consequence of making components and hooks composable without leaking implementation details. Concretely, once a value has been passed as a JSX prop, passed as argument to a hook, or returned from a hook, it must be assumed that the other "side" — receiver of the prop/argument/return value — will use that value as an input to an effect or memoization unit. Mutating that value (instead of creating a new value) will fail to cause the consuming computation to update:
|
||||
|
||||
```js
|
||||
// INVALID DO NOT DO THIS
|
||||
function Component(props) {
|
||||
const array = useArray(props.value);
|
||||
// OOPS! this value is memoized, the array won't get re-created
|
||||
// when `props.value` changes, so we might just keep pushing new
|
||||
// values to the same array on every render!
|
||||
array.push(props.otherValue);
|
||||
}
|
||||
|
||||
function useArray(a) {
|
||||
return useMemo(() => [a], [a]);
|
||||
}
|
||||
```
|
||||
|
||||
The **Freeze** effect accepts a variable reference and a reason that the value is being frozen. Note: _freeze only applies to the reference, not the underlying value_. Our inference is conservative, and assumes that there may still be other references to the same underlying value which are mutated later. For example:
|
||||
|
||||
```js
|
||||
const x = {};
|
||||
const y = [];
|
||||
x.y = y;
|
||||
freeze(y); // y _reference_ is frozen
|
||||
x.y.push(props.value); // but y is still considered mutable bc of this
|
||||
```
|
||||
|
||||
#### Mutate (and MutateConditionally)
|
||||
|
||||
```js
|
||||
{
|
||||
kind: 'Mutate';
|
||||
value: Place;
|
||||
}
|
||||
```
|
||||
|
||||
Mutate indicates that a value is mutated, without modifying any of the values that it may transitively have captured. Canonical examples include:
|
||||
|
||||
- Pushing an item onto an array modifies the array, but does not modify any items stored _within_ the array (unless the array has a reference to itself!)
|
||||
- Assigning a value to an object property modifies the object, but not any values stored in the object's other properties.
|
||||
|
||||
This helps explain the distinction between Assign/Alias and Capture: Mutate only affects assign/alias but not captures.
|
||||
|
||||
`MutateConditionally` is an alternative in which the mutation _may_ happen depending on the type of the value. The conditional variant is not generally used and included for completeness.
|
||||
|
||||
|
||||
|
||||
#### MutateTransitiveConditionally (and MutateTransitive)
|
||||
|
||||
`MutateTransitiveConditionally` represents an operation that may mutate _any_ aspect of a value, including reaching arbitrarily deep into nested values to mutate them. This is the default semantic for unknown functions — we have no idea what they do, so we assume that they are idempotent but may mutate any aspect of the mutable values that are passed to them.
|
||||
|
||||
There is also `MutateTransitive` for completeness, but this is not generally used.
|
||||
|
||||
### Side Effects
|
||||
|
||||
Finally, there are a few effects that describe error, or potential error, conditions:
|
||||
|
||||
- `MutateFrozen` is always an error, because it indicates known mutation of a value that should not be mutated.
|
||||
- `MutateGlobal` indicates known mutation of a global value, which is not safe during render. This effect is an error if reachable during render, but allowed if only reachable via an event handler or useEffect.
|
||||
- `Impure` indicates calling some other logic that is impure/side-effecting. This is an error if reachable during render, but allowed if only reachable via an event handler or useEffect.
|
||||
- TODO: we could probably merge this and MutateGlobal
|
||||
- `Render` indicates a value that is not mutated, but is known to be called during render. It's used for a few particular places like JSX tags and JSX children, which we assume are accessed during render (while other props may be event handlers etc). This helps to detect more MutateGlobal/Impure effects and reject more invalid programs.
|
||||
|
||||
|
||||
## Rules
|
||||
|
||||
### Mutation of Alias Mutates the Source Value
|
||||
|
||||
```
|
||||
Alias a <- b
|
||||
Mutate a
|
||||
=>
|
||||
Mutate b
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
const a = maybeIdentity(b); // Alias a <- b
|
||||
a.property = value; // a could be b, so this mutates b
|
||||
```
|
||||
|
||||
### Mutation of Assignment Mutates the Source Value
|
||||
|
||||
```
|
||||
Assign a <- b
|
||||
Mutate a
|
||||
=>
|
||||
Mutate b
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
const a = b;
|
||||
a.property = value // a _is_ b, this mutates b
|
||||
```
|
||||
|
||||
### Mutation of CreateFrom Mutates the Source Value
|
||||
|
||||
```
|
||||
CreateFrom a <- b
|
||||
Mutate a
|
||||
=>
|
||||
Mutate b
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
const a = b[index];
|
||||
a.property = value // the contents of b are transitively mutated
|
||||
```
|
||||
|
||||
|
||||
### Mutation of Capture Does *Not* Mutate the Source Value
|
||||
|
||||
```
|
||||
Capture a <- b
|
||||
Mutate a
|
||||
!=>
|
||||
~Mutate b~
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
const a = {};
|
||||
a.b = b;
|
||||
a.property = value; // mutates a, not b
|
||||
```
|
||||
|
||||
### Mutation of Source Affects Alias, Assignment, CreateFrom, and Capture
|
||||
|
||||
```
|
||||
Alias a <- b OR Assign a <- b OR CreateFrom a <- b OR Capture a <- b
|
||||
Mutate b
|
||||
=>
|
||||
Mutate a
|
||||
```
|
||||
|
||||
A derived value changes when it's source value is mutated.
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
const x = {};
|
||||
const y = [x];
|
||||
x.y = true; // this changes the value within `y` ie mutates y
|
||||
```
|
||||
|
||||
|
||||
### TransitiveMutation of Alias, Assignment, CreateFrom, or Capture Mutates the Source
|
||||
|
||||
```
|
||||
Alias a <- b OR Assign a <- b OR CreateFrom a <- b OR Capture a <- b
|
||||
MutateTransitive a
|
||||
=>
|
||||
MutateTransitive b
|
||||
```
|
||||
|
||||
Remember, the intuition for a transitive mutation is that it's something that could traverse arbitrarily deep into an object and mutate whatever it finds. Imagine something that recurses into every nested object/array and sets `.field = value`. Given a function `mutate()` that does this, then:
|
||||
|
||||
```js
|
||||
const a = b; // assign
|
||||
mutate(a); // clearly can transitively mutate b
|
||||
|
||||
const a = maybeIdentity(b); // alias
|
||||
mutate(a); // clearly can transitively mutate b
|
||||
|
||||
const a = b[index]; // createfrom
|
||||
mutate(a); // clearly can transitively mutate b
|
||||
|
||||
const a = {};
|
||||
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:
|
||||
|
||||
```
|
||||
Create x
|
||||
Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
|
||||
Freeze y
|
||||
!=>
|
||||
~Freeze x~
|
||||
```
|
||||
|
||||
This means that subsequent mutations of the original value are valid:
|
||||
|
||||
```
|
||||
Create x
|
||||
Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
|
||||
Freeze y
|
||||
Mutate x
|
||||
=>
|
||||
Mutate x (mutation is ok)
|
||||
```
|
||||
|
||||
As well as mutations through other assignments/aliases/captures/createfroms of the original value:
|
||||
|
||||
```
|
||||
Create x
|
||||
Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
|
||||
Freeze y
|
||||
Alias z <- x OR Capture z <- x OR CreateFrom z <- x OR Assign z <- x
|
||||
Mutate z
|
||||
=>
|
||||
Mutate x (mutation is ok)
|
||||
```
|
||||
|
||||
### Freeze Freezes The Reference
|
||||
|
||||
Although freeze doesn't freeze the value, it does affect the reference. The reference cannot be used to mutate.
|
||||
|
||||
Conditional mutations of the reference are no-ops:
|
||||
|
||||
```
|
||||
Create x
|
||||
Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
|
||||
Freeze y
|
||||
MutateConditional y
|
||||
=>
|
||||
(no mutation)
|
||||
```
|
||||
|
||||
And known mutations of the reference are errors:
|
||||
|
||||
```
|
||||
Create x
|
||||
Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
|
||||
Freeze y
|
||||
MutateConditional y
|
||||
=>
|
||||
MutateFrozen y error=...
|
||||
```
|
||||
|
||||
### Corollary: Transitivity of Assign/Alias/CreateFrom/Capture
|
||||
|
||||
A key part of the inference model is inferring a signature for function expressions. The signature is a minimal set of effects that describes the publicly observable behavior of the function. This can include "global" effects like side effects (MutateGlobal/Impure) as well as mutations/aliasing of parameters and free variables.
|
||||
|
||||
In order to determine the aliasing of params and free variables into each other and/or the return value, we may encounter chains of assign, alias, createfrom, and capture effects. For example:
|
||||
|
||||
```js
|
||||
const f = (x) => {
|
||||
const y = [x]; // capture y <- x
|
||||
const z = y[0]; // createfrom z <- y
|
||||
return z; // assign return <- z
|
||||
}
|
||||
// <Effect> return <- x
|
||||
```
|
||||
|
||||
In this example we can see that there should be some effect on `f` that tracks the flow of data from `x` into the return value. The key constraint is preserving the semantics around how local/transitive mutations of the destination would affect the source.
|
||||
|
||||
#### Each of the effects is transitive with itself
|
||||
|
||||
```
|
||||
Assign b <- a
|
||||
Assign c <- b
|
||||
=>
|
||||
Assign c <- a
|
||||
```
|
||||
|
||||
```
|
||||
Alias b <- a
|
||||
Alias c <- b
|
||||
=>
|
||||
Alias c <- a
|
||||
```
|
||||
|
||||
```
|
||||
CreateFrom b <- a
|
||||
CreateFrom c <- b
|
||||
=>
|
||||
CreateFrom c <- a
|
||||
```
|
||||
|
||||
```
|
||||
Capture b <- a
|
||||
Capture c <- b
|
||||
=>
|
||||
Capture c <- a
|
||||
```
|
||||
|
||||
#### Alias > Assign
|
||||
|
||||
```
|
||||
Assign b <- a
|
||||
Alias c <- b
|
||||
=>
|
||||
Alias c <- a
|
||||
```
|
||||
|
||||
```
|
||||
Alias b <- a
|
||||
Assign c <- b
|
||||
=>
|
||||
Alias c <- a
|
||||
```
|
||||
|
||||
### CreateFrom > Assign/Alias
|
||||
|
||||
Intuition:
|
||||
|
||||
```
|
||||
CreateFrom b <- a
|
||||
Alias c <- b OR Assign c <- b
|
||||
=>
|
||||
CreateFrom c <- a
|
||||
```
|
||||
|
||||
```
|
||||
Alias b <- a OR Assign b <- a
|
||||
CreateFrom c <- b
|
||||
=>
|
||||
CreateFrom c <- a
|
||||
```
|
||||
|
||||
### Capture > Assign/Alias
|
||||
|
||||
Intuition: capturing means that a local mutation of the destination will not affect the source, so we preserve the capture.
|
||||
|
||||
```
|
||||
Capture b <- a
|
||||
Alias c <- b OR Assign c <- b
|
||||
=>
|
||||
Capture c <- a
|
||||
```
|
||||
|
||||
```
|
||||
Alias b <- a OR Assign b <- a
|
||||
Capture c <- b
|
||||
=>
|
||||
Capture c <- a
|
||||
```
|
||||
|
||||
### Capture And CreateFrom
|
||||
|
||||
Intuition: these effects are inverses of each other (capturing into an object, extracting from an object). The result is based on the order of operations:
|
||||
|
||||
Capture then CreatFrom is equivalent to Alias: we have to assume that the result _is_ the original value and that a local mutation of the result could mutate the original.
|
||||
|
||||
```js
|
||||
const b = [a]; // capture
|
||||
const c = b[0]; // createfrom
|
||||
mutate(c); // this clearly can mutate a, so the result must be one of Assign/Alias/CreateFrom
|
||||
```
|
||||
|
||||
We use Alias as the return type because the mutability kind of the result is not derived from the source value (there's a fresh object in between due to the capture), so the full set of effects in practice would be a Create+Alias.
|
||||
|
||||
```
|
||||
Capture b <- a
|
||||
CreateFrom c <- b
|
||||
=>
|
||||
Alias c <- a
|
||||
```
|
||||
|
||||
Meanwhile the opposite direction preserves the capture, because the result is not the same as the source:
|
||||
|
||||
```js
|
||||
const b = a[0]; // createfrom
|
||||
const c = [b]; // capture
|
||||
mutate(c); // does not mutate a, so the result must be Capture
|
||||
```
|
||||
|
||||
```
|
||||
CreateFrom b <- a
|
||||
Capture c <- b
|
||||
=>
|
||||
Capture c <- a
|
||||
```
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
makeBlockId,
|
||||
makeInstructionId,
|
||||
makePropertyLiteral,
|
||||
makeType,
|
||||
markInstructionIds,
|
||||
promoteTemporary,
|
||||
reversePostorderBlocks,
|
||||
@@ -237,7 +238,6 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
terminal: {
|
||||
id: makeInstructionId(0),
|
||||
kind: 'return',
|
||||
returnVariant: 'Explicit',
|
||||
loc: GeneratedSource,
|
||||
value: arrayInstr.lvalue,
|
||||
effects: null,
|
||||
@@ -253,6 +253,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
env,
|
||||
params: [obj],
|
||||
returnTypeAnnotation: null,
|
||||
returnType: makeType(),
|
||||
returns: createTemporaryPlace(env, GeneratedSource),
|
||||
context: [],
|
||||
effects: null,
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
makeBlockId,
|
||||
makeIdentifierName,
|
||||
makeInstructionId,
|
||||
makeType,
|
||||
ObjectProperty,
|
||||
Place,
|
||||
promoteTemporary,
|
||||
@@ -352,7 +353,6 @@ function emitOutlinedFn(
|
||||
terminal: {
|
||||
id: makeInstructionId(0),
|
||||
kind: 'return',
|
||||
returnVariant: 'Explicit',
|
||||
loc: GeneratedSource,
|
||||
value: instructions.at(-1)!.lvalue,
|
||||
effects: null,
|
||||
@@ -368,6 +368,7 @@ function emitOutlinedFn(
|
||||
env,
|
||||
params: [propsObj],
|
||||
returnTypeAnnotation: null,
|
||||
returnType: makeType(),
|
||||
returns: createTemporaryPlace(env, GeneratedSource),
|
||||
context: [],
|
||||
effects: null,
|
||||
|
||||
@@ -349,9 +349,11 @@ function codegenReactiveFunction(
|
||||
fn: ReactiveFunction,
|
||||
): Result<CodegenFunction, CompilerError> {
|
||||
for (const param of fn.params) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
cx.temp.set(place.identifier.declarationId, null);
|
||||
cx.declare(place.identifier);
|
||||
if (param.kind === 'Identifier') {
|
||||
cx.temp.set(param.identifier.declarationId, null);
|
||||
} else {
|
||||
cx.temp.set(param.place.identifier.declarationId, null);
|
||||
}
|
||||
}
|
||||
|
||||
const params = fn.params.map(param => convertParameter(param));
|
||||
@@ -1181,7 +1183,7 @@ function codegenTerminal(
|
||||
? codegenPlaceToExpression(cx, case_.test)
|
||||
: null;
|
||||
const block = codegenBlock(cx, case_.block!);
|
||||
return t.switchCase(test, block.body.length === 0 ? [] : [block]);
|
||||
return t.switchCase(test, [block]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,10 +79,6 @@ export function extractScopeDeclarationsFromDestructuring(
|
||||
fn: ReactiveFunction,
|
||||
): void {
|
||||
const state = new State(fn.env);
|
||||
for (const param of fn.params) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
state.declared.add(place.identifier.declarationId);
|
||||
}
|
||||
visitReactiveFunction(fn, new Visitor(), state);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
@@ -1066,29 +1064,12 @@ class PruneScopesTransform extends ReactiveFunctionTransform<
|
||||
|
||||
const value = instruction.value;
|
||||
if (value.kind === 'StoreLocal' && value.lvalue.kind === 'Reassign') {
|
||||
// Complex cases of useMemo inlining result in a temporary that is reassigned
|
||||
const ids = getOrInsertDefault(
|
||||
this.reassignments,
|
||||
value.lvalue.place.identifier.declarationId,
|
||||
new Set(),
|
||||
);
|
||||
ids.add(value.value.identifier);
|
||||
} else if (
|
||||
value.kind === 'LoadLocal' &&
|
||||
value.place.identifier.scope != null &&
|
||||
instruction.lvalue != null &&
|
||||
instruction.lvalue.identifier.scope == null
|
||||
) {
|
||||
/*
|
||||
* Simpler cases result in a direct assignment to the original lvalue, with a
|
||||
* LoadLocal
|
||||
*/
|
||||
const ids = getOrInsertDefault(
|
||||
this.reassignments,
|
||||
instruction.lvalue.identifier.declarationId,
|
||||
new Set(),
|
||||
);
|
||||
ids.add(value.place.identifier);
|
||||
} else if (value.kind === 'FinishMemoize') {
|
||||
let decls;
|
||||
if (value.decl.identifier.scope == null) {
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
Instruction,
|
||||
InstructionKind,
|
||||
makePropertyLiteral,
|
||||
makeType,
|
||||
PropType,
|
||||
@@ -91,8 +90,7 @@ function apply(func: HIRFunction, unifier: Unifier): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
const returns = func.returns.identifier;
|
||||
returns.type = unifier.get(returns.type);
|
||||
func.returnType = unifier.get(func.returnType);
|
||||
}
|
||||
|
||||
type TypeEquation = {
|
||||
@@ -145,12 +143,12 @@ function* generate(
|
||||
}
|
||||
}
|
||||
if (returnTypes.length > 1) {
|
||||
yield equation(func.returns.identifier.type, {
|
||||
yield equation(func.returnType, {
|
||||
kind: 'Phi',
|
||||
operands: returnTypes,
|
||||
});
|
||||
} else if (returnTypes.length === 1) {
|
||||
yield equation(func.returns.identifier.type, returnTypes[0]!);
|
||||
yield equation(func.returnType, returnTypes[0]!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,29 +193,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 +359,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;
|
||||
}
|
||||
@@ -432,7 +407,7 @@ function* generateInstructionTypes(
|
||||
yield equation(left, {
|
||||
kind: 'Function',
|
||||
shapeId: BuiltInFunctionId,
|
||||
return: value.loweredFunc.func.returns.identifier.type,
|
||||
return: value.loweredFunc.func.returnType,
|
||||
isConstructor: false,
|
||||
});
|
||||
break;
|
||||
@@ -451,18 +426,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 +441,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':
|
||||
|
||||
@@ -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,13 +174,9 @@ export function parseConfigPragmaForTests(
|
||||
pragma: string,
|
||||
defaults: {
|
||||
compilationMode: CompilationMode;
|
||||
environment?: PartialEnvironmentConfig;
|
||||
},
|
||||
): PluginOptions {
|
||||
const environment = parseConfigPragmaEnvironmentForTest(
|
||||
pragma,
|
||||
defaults.environment ?? {},
|
||||
);
|
||||
const environment = parseConfigPragmaEnvironmentForTest(pragma);
|
||||
const options: Record<keyof PluginOptions, unknown> = {
|
||||
...defaultOptions,
|
||||
panicThreshold: 'all_errors',
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..';
|
||||
import {CompilerError, Effect} from '..';
|
||||
import {HIRFunction, IdentifierId, Place} from '../HIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
@@ -28,24 +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({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot reassign variable after render completes',
|
||||
description: `Reassigning ${variable} after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.`,
|
||||
}).withDetail({
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,25 +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({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot reassign variable in async function',
|
||||
description:
|
||||
'Reassigning a variable in an async function can cause inconsistent behavior on subsequent renders. Consider using state instead',
|
||||
}).withDetail({
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..';
|
||||
import {CompilerError, Effect, ErrorSeverity} from '..';
|
||||
import {
|
||||
FunctionEffect,
|
||||
HIRFunction,
|
||||
@@ -57,30 +57,16 @@ export function validateNoFreezingKnownMutableFunctions(
|
||||
if (operand.effect === Effect.Freeze) {
|
||||
const effect = contextMutationEffects.get(operand.identifier.id);
|
||||
if (effect != null) {
|
||||
const place = [...effect.places][0];
|
||||
const variable =
|
||||
place != null &&
|
||||
place.identifier.name != null &&
|
||||
place.identifier.name.kind === 'named'
|
||||
? `\`${place.identifier.name.value}\``
|
||||
: 'a local variable';
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot modify local variables after render completes',
|
||||
description: `This argument is a function which may reassign or mutate ${variable} after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.`,
|
||||
})
|
||||
.withDetail({
|
||||
kind: 'error',
|
||||
loc: operand.loc,
|
||||
message: `This function may (indirectly) reassign or modify ${variable} after render`,
|
||||
})
|
||||
.withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
|
||||
import {CompilerError, ErrorSeverity} from '..';
|
||||
import {HIRFunction} from '../HIR';
|
||||
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
|
||||
import {Result} from '../Utils/Result';
|
||||
@@ -34,22 +34,17 @@ export function validateNoImpureFunctionsInRender(
|
||||
callee.identifier.type,
|
||||
);
|
||||
if (signature != null && signature.impure === true) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: 'Cannot call impure function during render',
|
||||
description:
|
||||
(signature.canonicalName != null
|
||||
? `\`${signature.canonicalName}\` is an impure function. `
|
||||
: '') +
|
||||
'Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
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,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
|
||||
import {CompilerError, ErrorSeverity} from '..';
|
||||
import {BlockId, HIRFunction} from '../HIR';
|
||||
import {Result} from '../Utils/Result';
|
||||
import {retainWhere} from '../Utils/utils';
|
||||
@@ -34,17 +34,11 @@ export function validateNoJSXInTryStatement(
|
||||
switch (value.kind) {
|
||||
case 'JsxExpression':
|
||||
case 'JsxFragment': {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Avoid constructing JSX within try/catch',
|
||||
description: `React does not immediately render components when JSX is rendered, so any errors from this component will not be caught by the try/catch. To catch errors in rendering a given component, wrap that component in an error boundary. (https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)`,
|
||||
}).withDetail({
|
||||
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,11 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {CompilerError, ErrorSeverity} from '../CompilerError';
|
||||
import {
|
||||
BlockId,
|
||||
HIRFunction,
|
||||
@@ -262,20 +258,6 @@ 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 = [];
|
||||
@@ -403,75 +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({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: '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 ||
|
||||
interpolatedAsJsx.has(instr.lvalue.identifier.id) ||
|
||||
hookKind != null
|
||||
) {
|
||||
/**
|
||||
* 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) 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.
|
||||
*
|
||||
* 3) hooks
|
||||
*
|
||||
* Hooks are independently checked to ensure they don't access refs
|
||||
* during render.
|
||||
*/
|
||||
validateNoDirectRefValueAccess(errors, operand, env);
|
||||
} 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);
|
||||
@@ -514,7 +449,7 @@ function validateNoRefAccessInRenderImpl(
|
||||
) {
|
||||
safeBlocks.delete(block.id);
|
||||
} else {
|
||||
validateNoRefUpdate(errors, env, instr.value.object, instr.loc);
|
||||
validateNoRefAccess(errors, env, instr.value.object, instr.loc);
|
||||
}
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
if (operand === instr.value.object) {
|
||||
@@ -648,17 +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({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: operand.loc,
|
||||
message: `Cannot access ref value during render`,
|
||||
}),
|
||||
);
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -672,21 +608,22 @@ function validateNoRefValueAccess(
|
||||
type?.kind === 'RefValue' ||
|
||||
(type?.kind === 'Structure' && type.fn?.readRefEffect)
|
||||
) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: (type.kind === 'RefValue' && type.loc) || operand.loc,
|
||||
message: `Cannot access ref value during render`,
|
||||
}),
|
||||
);
|
||||
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,
|
||||
@@ -698,43 +635,18 @@ function validateNoRefPassedToFunction(
|
||||
type?.kind === 'RefValue' ||
|
||||
(type?.kind === 'Structure' && type.fn?.readRefEffect)
|
||||
) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: (type.kind === 'RefValue' && type.loc) || loc,
|
||||
message: `Passing a ref to a function may read its value during render`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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' ||
|
||||
(type?.kind === 'Structure' && type.fn?.readRefEffect)
|
||||
) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: (type.kind === 'RefValue' && type.loc) || loc,
|
||||
message: `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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -745,22 +657,17 @@ function validateNoDirectRefValueAccess(
|
||||
): void {
|
||||
const type = destructure(env.get(operand.identifier.id));
|
||||
if (type?.kind === 'RefValue') {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: type.loc ?? operand.loc,
|
||||
message: `Cannot access ref value during render`,
|
||||
}),
|
||||
);
|
||||
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,32 +5,26 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
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();
|
||||
@@ -85,35 +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:
|
||||
'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,11 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {CompilerError, ErrorSeverity} from '../CompilerError';
|
||||
import {HIRFunction, IdentifierId, isSetStateType} from '../HIR';
|
||||
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
|
||||
import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
@@ -126,35 +122,23 @@ function validateNoSetStateInRenderImpl(
|
||||
unconditionalSetStateFunctions.has(callee.identifier.id)
|
||||
) {
|
||||
if (activeManualMemoId !== null) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category:
|
||||
'Calling setState from useMemo may trigger an infinite loop',
|
||||
description:
|
||||
'Each time the memo callback is evaluated it will change state. This can cause a memoization dependency to change, running the memo function again and causing an infinite loop. Instead of setting state in useMemo(), prefer deriving the value during render. (https://react.dev/reference/react/useState)',
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
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:
|
||||
'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() within useMemo()',
|
||||
}),
|
||||
);
|
||||
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,11 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {CompilerError, ErrorSeverity} from '../CompilerError';
|
||||
import {
|
||||
DeclarationId,
|
||||
Effect,
|
||||
@@ -279,37 +275,27 @@ function validateInferredDep(
|
||||
errorDiagnostic = merge(errorDiagnostic ?? compareResult, compareResult);
|
||||
}
|
||||
}
|
||||
errorState.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.CannotPreserveMemoization,
|
||||
category:
|
||||
'Compilation skipped because existing memoization could not be preserved',
|
||||
description: [
|
||||
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. ',
|
||||
'The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. ',
|
||||
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> {
|
||||
@@ -459,13 +445,11 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
*/
|
||||
this.recordTemporaries(instruction, state);
|
||||
const value = instruction.value;
|
||||
// Track reassignments from inlining of manual memo
|
||||
if (
|
||||
value.kind === 'StoreLocal' &&
|
||||
value.lvalue.kind === 'Reassign' &&
|
||||
state.manualMemoState != null
|
||||
) {
|
||||
// Complex cases of inlining end up with a temporary that is reassigned
|
||||
const ids = getOrInsertDefault(
|
||||
state.manualMemoState.reassignments,
|
||||
value.lvalue.place.identifier.declarationId,
|
||||
@@ -473,21 +457,6 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
);
|
||||
ids.add(value.value.identifier);
|
||||
}
|
||||
if (
|
||||
value.kind === 'LoadLocal' &&
|
||||
value.place.identifier.scope != null &&
|
||||
instruction.lvalue != null &&
|
||||
instruction.lvalue.identifier.scope == null &&
|
||||
state.manualMemoState != null
|
||||
) {
|
||||
// Simpler cases of inlining assign to the original IIFE lvalue
|
||||
const ids = getOrInsertDefault(
|
||||
state.manualMemoState.reassignments,
|
||||
instruction.lvalue.identifier.declarationId,
|
||||
new Set(),
|
||||
);
|
||||
ids.add(value.place.identifier);
|
||||
}
|
||||
if (value.kind === 'StartMemoize') {
|
||||
let depsFromSource: Array<ManualMemoDependency> | null = null;
|
||||
if (value.deps != null) {
|
||||
@@ -533,21 +502,14 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
!this.scopes.has(identifier.scope.id) &&
|
||||
!this.prunedScopes.has(identifier.scope.id)
|
||||
) {
|
||||
state.errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.CannotPreserveMemoization,
|
||||
category:
|
||||
'Compilation skipped because existing memoization could not be preserved',
|
||||
description: [
|
||||
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. ',
|
||||
'This dependency may be mutated later, which could cause the value to change unexpectedly.',
|
||||
].join(''),
|
||||
}).withDetail({
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -581,25 +543,16 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
|
||||
for (const identifier of decls) {
|
||||
if (isUnmemoized(identifier, this.scopes)) {
|
||||
state.errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.CannotPreserveMemoization,
|
||||
category:
|
||||
'Compilation skipped because existing memoization could not be preserved',
|
||||
description: [
|
||||
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. ',
|
||||
DEBUG
|
||||
? `${printIdentifier(identifier)} was not memoized.`
|
||||
: '',
|
||||
]
|
||||
.join('')
|
||||
.trim(),
|
||||
}).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,11 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {CompilerError, ErrorSeverity} from '../CompilerError';
|
||||
import {HIRFunction, IdentifierId, SourceLocation} from '../HIR';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
@@ -63,23 +59,20 @@ export function validateStaticComponents(
|
||||
value.tag.identifier.id,
|
||||
);
|
||||
if (location != null) {
|
||||
error.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: '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,11 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {CompilerError, ErrorSeverity} from '..';
|
||||
import {FunctionExpression, HIRFunction, IdentifierId} from '../HIR';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
@@ -67,41 +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({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'useMemo() callbacks may not accept parameters',
|
||||
description:
|
||||
'useMemo() callbacks are called by React to cache calculations across re-renders. They should not take parameters. Instead, directly reference the props, state, or local variables needed for the computation.',
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
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({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category:
|
||||
'useMemo() callbacks may not be async or generator functions',
|
||||
description:
|
||||
'useMemo() callbacks are called once and must synchronously return a value.',
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
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""`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -26,16 +26,20 @@ import { c as _c } from "react/compiler-runtime";
|
||||
import { getNull } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(2);
|
||||
const $ = _c(3);
|
||||
let t0;
|
||||
let items;
|
||||
if ($[0] !== props.a) {
|
||||
items = getNull() ?? [];
|
||||
t0 = getNull() ?? [];
|
||||
items = t0;
|
||||
|
||||
items.push(props.a);
|
||||
$[0] = props.a;
|
||||
$[1] = items;
|
||||
$[2] = t0;
|
||||
} else {
|
||||
items = $[1];
|
||||
t0 = $[2];
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -52,13 +52,15 @@ function Component(t0) {
|
||||
}
|
||||
const onClick = t1;
|
||||
let t2;
|
||||
let t3;
|
||||
if ($[2] !== onClick) {
|
||||
t2 = <div onClick={onClick}>{someGlobal.value}</div>;
|
||||
t3 = <div onClick={onClick}>{someGlobal.value}</div>;
|
||||
$[2] = onClick;
|
||||
$[3] = t2;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
t3 = $[3];
|
||||
}
|
||||
t2 = t3;
|
||||
return t2;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
import {useMemo} from 'react';
|
||||
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
// Should memoize independently
|
||||
const x = useMemo(() => makeObject_Primitives(), []);
|
||||
|
||||
const rest = useMemo(() => {
|
||||
const [_, ...rest] = props.array;
|
||||
|
||||
// Should be inferred as Array.proto.push which doesn't mutate input
|
||||
rest.push(x);
|
||||
return rest;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<ValidateMemoization inputs={[]} output={x} />
|
||||
<ValidateMemoization inputs={[props.array]} output={rest} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{array: [0, 1, 2]}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
|
||||
import { useMemo } from "react";
|
||||
import { makeObject_Primitives, ValidateMemoization } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(9);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = makeObject_Primitives();
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const x = t0;
|
||||
let rest;
|
||||
if ($[1] !== props.array) {
|
||||
[, ...rest] = props.array;
|
||||
|
||||
rest.push(x);
|
||||
$[1] = props.array;
|
||||
$[2] = rest;
|
||||
} else {
|
||||
rest = $[2];
|
||||
}
|
||||
const rest_0 = rest;
|
||||
let t1;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <ValidateMemoization inputs={[]} output={x} />;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
let t2;
|
||||
if ($[4] !== props.array) {
|
||||
t2 = [props.array];
|
||||
$[4] = props.array;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
let t3;
|
||||
if ($[6] !== rest_0 || $[7] !== t2) {
|
||||
t3 = (
|
||||
<>
|
||||
{t1}
|
||||
<ValidateMemoization inputs={t2} output={rest_0} />
|
||||
</>
|
||||
);
|
||||
$[6] = rest_0;
|
||||
$[7] = t2;
|
||||
$[8] = t3;
|
||||
} else {
|
||||
t3 = $[8];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ array: [0, 1, 2] }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"inputs":[],"output":{"a":0,"b":"value1","c":true}}</div><div>{"inputs":[[0,1,2]],"output":[1,2,{"a":0,"b":"value1","c":true}]}</div>
|
||||
@@ -1,28 +0,0 @@
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
import {useMemo} from 'react';
|
||||
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
// Should memoize independently
|
||||
const x = useMemo(() => makeObject_Primitives(), []);
|
||||
|
||||
const rest = useMemo(() => {
|
||||
const [_, ...rest] = props.array;
|
||||
|
||||
// Should be inferred as Array.proto.push which doesn't mutate input
|
||||
rest.push(x);
|
||||
return rest;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<ValidateMemoization inputs={[]} output={x} />
|
||||
<ValidateMemoization inputs={[props.array]} output={rest} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{array: [0, 1, 2]}],
|
||||
};
|
||||
@@ -30,46 +30,50 @@ function Component(props) {
|
||||
const $ = _c(4);
|
||||
const [x] = useState(0);
|
||||
let t0;
|
||||
if ($[0] !== x) {
|
||||
t0 = calculateExpensiveNumber(x);
|
||||
$[0] = x;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const expensiveNumber = t0;
|
||||
let t1;
|
||||
if ($[2] !== expensiveNumber) {
|
||||
t1 = <div>{expensiveNumber}</div>;
|
||||
$[2] = expensiveNumber;
|
||||
$[3] = t1;
|
||||
if ($[0] !== x) {
|
||||
t1 = calculateExpensiveNumber(x);
|
||||
$[0] = x;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
t0 = t1;
|
||||
const expensiveNumber = t0;
|
||||
let t2;
|
||||
if ($[2] !== expensiveNumber) {
|
||||
t2 = <div>{expensiveNumber}</div>;
|
||||
$[2] = expensiveNumber;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
function Component2(props) {
|
||||
const $ = _c(4);
|
||||
const [x] = useState(0);
|
||||
let t0;
|
||||
if ($[0] !== x) {
|
||||
t0 = calculateExpensiveNumber(x);
|
||||
$[0] = x;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const expensiveNumber = t0;
|
||||
let t1;
|
||||
if ($[2] !== expensiveNumber) {
|
||||
t1 = <div>{expensiveNumber}</div>;
|
||||
$[2] = expensiveNumber;
|
||||
$[3] = t1;
|
||||
if ($[0] !== x) {
|
||||
t1 = calculateExpensiveNumber(x);
|
||||
$[0] = x;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
t0 = t1;
|
||||
const expensiveNumber = t0;
|
||||
let t2;
|
||||
if ($[2] !== expensiveNumber) {
|
||||
t2 = <div>{expensiveNumber}</div>;
|
||||
$[2] = expensiveNumber;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -32,46 +32,50 @@ function Component(props) {
|
||||
const $ = _c(4);
|
||||
const [x] = useState(0);
|
||||
let t0;
|
||||
if ($[0] !== x) {
|
||||
t0 = calculateExpensiveNumber(x);
|
||||
$[0] = x;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const expensiveNumber = t0;
|
||||
let t1;
|
||||
if ($[2] !== expensiveNumber) {
|
||||
t1 = <div>{expensiveNumber}</div>;
|
||||
$[2] = expensiveNumber;
|
||||
$[3] = t1;
|
||||
if ($[0] !== x) {
|
||||
t1 = calculateExpensiveNumber(x);
|
||||
$[0] = x;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
t0 = t1;
|
||||
const expensiveNumber = t0;
|
||||
let t2;
|
||||
if ($[2] !== expensiveNumber) {
|
||||
t2 = <div>{expensiveNumber}</div>;
|
||||
$[2] = expensiveNumber;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
function Component2(props) {
|
||||
const $ = _c(4);
|
||||
const [x] = useState(0);
|
||||
let t0;
|
||||
if ($[0] !== x) {
|
||||
t0 = calculateExpensiveNumber(x);
|
||||
$[0] = x;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const expensiveNumber = t0;
|
||||
let t1;
|
||||
if ($[2] !== expensiveNumber) {
|
||||
t1 = <div>{expensiveNumber}</div>;
|
||||
$[2] = expensiveNumber;
|
||||
$[3] = t1;
|
||||
if ($[0] !== x) {
|
||||
t1 = calculateExpensiveNumber(x);
|
||||
$[0] = x;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
t0 = t1;
|
||||
const expensiveNumber = t0;
|
||||
let t2;
|
||||
if ($[2] !== expensiveNumber) {
|
||||
t2 = <div>{expensiveNumber}</div>;
|
||||
$[2] = expensiveNumber;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -30,23 +30,25 @@ function Component(props) {
|
||||
const $ = _c(4);
|
||||
const [x] = React.useState(0);
|
||||
let t0;
|
||||
if ($[0] !== x) {
|
||||
t0 = calculateExpensiveNumber(x);
|
||||
$[0] = x;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const expensiveNumber = t0;
|
||||
let t1;
|
||||
if ($[2] !== expensiveNumber) {
|
||||
t1 = <div>{expensiveNumber}</div>;
|
||||
$[2] = expensiveNumber;
|
||||
$[3] = t1;
|
||||
if ($[0] !== x) {
|
||||
t1 = calculateExpensiveNumber(x);
|
||||
$[0] = x;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
t0 = t1;
|
||||
const expensiveNumber = t0;
|
||||
let t2;
|
||||
if ($[2] !== expensiveNumber) {
|
||||
t2 = <div>{expensiveNumber}</div>;
|
||||
$[2] = expensiveNumber;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -36,28 +36,30 @@ function Component(props) {
|
||||
const $ = _c(4);
|
||||
const [x] = React.useState(0);
|
||||
let t0;
|
||||
if ($[0] !== x) {
|
||||
t0 = calculateExpensiveNumber(x);
|
||||
$[0] = x;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const expensiveNumber = t0;
|
||||
let t1;
|
||||
if ($[0] !== x) {
|
||||
t1 = calculateExpensiveNumber(x);
|
||||
$[0] = x;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
t0 = t1;
|
||||
const expensiveNumber = t0;
|
||||
let t2;
|
||||
if ($[2] !== expensiveNumber) {
|
||||
t1 = (
|
||||
t2 = (
|
||||
<div>
|
||||
{expensiveNumber}
|
||||
{`${someImport}`}
|
||||
</div>
|
||||
);
|
||||
$[2] = expensiveNumber;
|
||||
$[3] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
t2 = $[3];
|
||||
}
|
||||
return t1;
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -36,22 +36,26 @@ import { useMemo } from "react";
|
||||
function Component(props) {
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] !== props.value) {
|
||||
t0 = { value: props.value };
|
||||
t1 = { value: props.value };
|
||||
$[0] = props.value;
|
||||
$[1] = t0;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
t1 = $[1];
|
||||
}
|
||||
const handlers = t0;
|
||||
const handlers = t1;
|
||||
bb0: switch (props.test) {
|
||||
case true: {
|
||||
console.log(handlers.value);
|
||||
break bb0;
|
||||
}
|
||||
default:
|
||||
default: {
|
||||
}
|
||||
}
|
||||
const outerHandlers = handlers;
|
||||
|
||||
t0 = handlers;
|
||||
const outerHandlers = t0;
|
||||
return outerHandlers;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,9 +37,11 @@ function useTest() {
|
||||
|
||||
const t1 = (w = 42);
|
||||
const t2 = w;
|
||||
let t3;
|
||||
|
||||
w = 999;
|
||||
t0 = makeArray(t1, t2, 2);
|
||||
t3 = 2;
|
||||
t0 = makeArray(t1, t2, t3);
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
|
||||
@@ -37,9 +37,11 @@ function useTest() {
|
||||
|
||||
const t1 = (w.x = 42);
|
||||
const t2 = w.x;
|
||||
let t3;
|
||||
|
||||
w.x = 999;
|
||||
t0 = makeArray(t1, t2, 2);
|
||||
t3 = 2;
|
||||
t0 = makeArray(t1, t2, t3);
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
|
||||
@@ -32,9 +32,11 @@ function useTest() {
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
const t1 = print(1);
|
||||
let t2;
|
||||
|
||||
print(2);
|
||||
t0 = makeArray(t1, 2);
|
||||
t2 = 2;
|
||||
t0 = makeArray(t1, t2);
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
|
||||
@@ -29,33 +29,37 @@ function useHook(t0) {
|
||||
const $ = _c(7);
|
||||
const { a, b } = t0;
|
||||
let t1;
|
||||
if ($[0] !== a) {
|
||||
t1 = identity({ a });
|
||||
$[0] = a;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const valA = t1;
|
||||
let t2;
|
||||
if ($[2] !== b) {
|
||||
t2 = identity([b]);
|
||||
$[2] = b;
|
||||
$[3] = t2;
|
||||
if ($[0] !== a) {
|
||||
t2 = identity({ a });
|
||||
$[0] = a;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
t2 = $[1];
|
||||
}
|
||||
const valB = t2;
|
||||
t1 = t2;
|
||||
const valA = t1;
|
||||
let t3;
|
||||
let t4;
|
||||
if ($[2] !== b) {
|
||||
t4 = identity([b]);
|
||||
$[2] = b;
|
||||
$[3] = t4;
|
||||
} else {
|
||||
t4 = $[3];
|
||||
}
|
||||
t3 = t4;
|
||||
const valB = t3;
|
||||
let t5;
|
||||
if ($[4] !== valA || $[5] !== valB) {
|
||||
t3 = [valA, valB];
|
||||
t5 = [valA, valB];
|
||||
$[4] = valA;
|
||||
$[5] = valB;
|
||||
$[6] = t3;
|
||||
$[6] = t5;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
t5 = $[6];
|
||||
}
|
||||
return t3;
|
||||
return t5;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -34,8 +34,10 @@ function Component(props) {
|
||||
let Component;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
Component = Stringify;
|
||||
let t0;
|
||||
|
||||
Component = Component;
|
||||
t0 = Component;
|
||||
Component = t0;
|
||||
$[0] = Component;
|
||||
} else {
|
||||
Component = $[0];
|
||||
|
||||
@@ -28,18 +28,20 @@ import { c as _c } from "react/compiler-runtime";
|
||||
function Foo() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = function a(t1) {
|
||||
const x_0 = t1 === undefined ? _temp : t1;
|
||||
return (function b(t2) {
|
||||
const y_0 = t2 === undefined ? [] : t2;
|
||||
t1 = function a(t2) {
|
||||
const x_0 = t2 === undefined ? _temp : t2;
|
||||
return (function b(t3) {
|
||||
const y_0 = t3 === undefined ? [] : t3;
|
||||
return [x_0, y_0];
|
||||
})();
|
||||
};
|
||||
$[0] = t0;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
t1 = $[0];
|
||||
}
|
||||
t0 = t1;
|
||||
return t0;
|
||||
}
|
||||
function _temp() {}
|
||||
|
||||
@@ -67,7 +67,8 @@ function Component(props) {
|
||||
case "b": {
|
||||
break bb1;
|
||||
}
|
||||
case "c":
|
||||
case "c": {
|
||||
}
|
||||
default: {
|
||||
x = 6;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import * as React from "react";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
let x;
|
||||
if ($[0] !== props.value) {
|
||||
x = [];
|
||||
@@ -37,7 +38,8 @@ function Component(props) {
|
||||
} else {
|
||||
x = $[1];
|
||||
}
|
||||
const x_0 = x;
|
||||
t0 = x;
|
||||
const x_0 = t0;
|
||||
return x_0;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,15 +15,10 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Todo: (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern
|
||||
|
||||
error._todo.computed-lval-in-destructure.ts:3:9
|
||||
1 | function Component(props) {
|
||||
2 | const computedKey = props.key;
|
||||
> 3 | const {[computedKey]: x} = props.val;
|
||||
| ^^^^^^^^^^^^^^^^ (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern
|
||||
| ^^^^^^^^^^^^^^^^ Todo: (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern (3:3)
|
||||
4 |
|
||||
5 | return x;
|
||||
6 | }
|
||||
|
||||
@@ -15,17 +15,10 @@ function Component() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot reassign variables declared outside of the component/hook
|
||||
|
||||
Variable `someGlobal` is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)
|
||||
|
||||
error.assign-global-in-component-tag-function.ts:3:4
|
||||
1 | function Component() {
|
||||
2 | const Foo = () => {
|
||||
> 3 | someGlobal = true;
|
||||
| ^^^^^^^^^^ `someGlobal` cannot be reassigned
|
||||
| ^^^^^^^^^^ InvalidReact: 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) (3:3)
|
||||
4 | };
|
||||
5 | return <Foo />;
|
||||
6 | }
|
||||
|
||||
@@ -18,17 +18,10 @@ function Component() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot reassign variables declared outside of the component/hook
|
||||
|
||||
Variable `someGlobal` is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)
|
||||
|
||||
error.assign-global-in-jsx-children.ts:3:4
|
||||
1 | function Component() {
|
||||
2 | const foo = () => {
|
||||
> 3 | someGlobal = true;
|
||||
| ^^^^^^^^^^ `someGlobal` cannot be reassigned
|
||||
| ^^^^^^^^^^ InvalidReact: 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) (3:3)
|
||||
4 | };
|
||||
5 | // Children are generally access/called during render, so
|
||||
6 | // modifying a global in a children function is almost
|
||||
|
||||
@@ -16,15 +16,10 @@ function Component() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)
|
||||
|
||||
error.assign-global-in-jsx-spread-attribute.ts:4:4
|
||||
2 | function Component() {
|
||||
3 | const foo = () => {
|
||||
> 4 | someGlobal = true;
|
||||
| ^^^^^^^^^^ Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)
|
||||
| ^^^^^^^^^^ InvalidReact: 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) (4:4)
|
||||
5 | };
|
||||
6 | return <div {...foo} />;
|
||||
7 | }
|
||||
|
||||
@@ -16,17 +16,10 @@ function Foo(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: React Compiler has skipped optimizing this component because one or more React rule violations were reported by Flow
|
||||
|
||||
React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression `$FlowFixMe[react-rule-hook]`
|
||||
|
||||
error.bailout-on-flow-suppression.ts:4:2
|
||||
2 |
|
||||
3 | function Foo(props) {
|
||||
> 4 | // $FlowFixMe[react-rule-hook]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Found React rule suppression
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: React Compiler has skipped optimizing this component because one or more React rule violations were reported by Flow. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. $FlowFixMe[react-rule-hook] (4:4)
|
||||
5 | useX();
|
||||
6 | return null;
|
||||
7 | }
|
||||
|
||||
@@ -19,33 +19,15 @@ function lowercasecomponent() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
|
||||
Error: React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled
|
||||
|
||||
React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression `eslint-disable my-app/react-rule`
|
||||
|
||||
error.bailout-on-suppression-of-custom-rule.ts:3:0
|
||||
1 | // @eslintSuppressionRules:["my-app","react-rule"]
|
||||
2 |
|
||||
> 3 | /* eslint-disable my-app/react-rule */
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Found React rule suppression
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. eslint-disable my-app/react-rule (3:3)
|
||||
|
||||
InvalidReact: React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. eslint-disable-next-line my-app/react-rule (7:7)
|
||||
4 | function lowercasecomponent() {
|
||||
5 | 'use forget';
|
||||
6 | const x = [];
|
||||
|
||||
Error: React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled
|
||||
|
||||
React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression `eslint-disable-next-line my-app/react-rule`
|
||||
|
||||
error.bailout-on-suppression-of-custom-rule.ts:7:2
|
||||
5 | 'use forget';
|
||||
6 | const x = [];
|
||||
> 7 | // eslint-disable-next-line my-app/react-rule
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Found React rule suppression
|
||||
8 | return <div>{x}</div>;
|
||||
9 | }
|
||||
10 | /* eslint-enable my-app/react-rule */
|
||||
```
|
||||
|
||||
|
||||
@@ -36,13 +36,6 @@ function Component() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot modify local variables after render completes
|
||||
|
||||
This argument is a function which may reassign or mutate a local variable after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
|
||||
|
||||
error.bug-old-inference-false-positive-ref-validation-in-use-effect.ts:20:12
|
||||
18 | );
|
||||
19 | const ref = useRef(null);
|
||||
> 20 | useEffect(() => {
|
||||
@@ -54,19 +47,12 @@ error.bug-old-inference-false-positive-ref-validation-in-use-effect.ts:20:12
|
||||
> 23 | }
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 24 | }, [update]);
|
||||
| ^^^^ This function may (indirectly) reassign or modify a local variable after render
|
||||
| ^^^^ InvalidReact: 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 (20:24)
|
||||
|
||||
InvalidReact: The function modifies a local variable here (14:14)
|
||||
25 |
|
||||
26 | return 'ok';
|
||||
27 | }
|
||||
|
||||
error.bug-old-inference-false-positive-ref-validation-in-use-effect.ts:14:6
|
||||
12 | ...partialParams,
|
||||
13 | };
|
||||
> 14 | nextParams.param = 'value';
|
||||
| ^^^^^^^^^^ This modifies a local variable
|
||||
15 | console.log(nextParams);
|
||||
16 | },
|
||||
17 | [params]
|
||||
```
|
||||
|
||||
|
||||
@@ -14,15 +14,10 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Invariant: Const declaration cannot be referenced as an expression
|
||||
|
||||
error.call-args-destructuring-asignment-complex.ts:3:9
|
||||
1 | function Component(props) {
|
||||
2 | let x = makeObject();
|
||||
> 3 | x.foo(([[x]] = makeObject()));
|
||||
| ^^^^^ Const declaration cannot be referenced as an expression
|
||||
| ^^^^^ Invariant: Const declaration cannot be referenced as an expression (3:3)
|
||||
4 | return x;
|
||||
5 | }
|
||||
6 |
|
||||
|
||||
@@ -14,17 +14,10 @@ function Foo() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config
|
||||
|
||||
Bar may be a component..
|
||||
|
||||
error.capitalized-function-call-aliased.ts:4:2
|
||||
2 | function Foo() {
|
||||
3 | let x = Bar;
|
||||
> 4 | x(); // ERROR
|
||||
| ^^^ Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config
|
||||
| ^^^ InvalidReact: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config. Bar may be a component. (4:4)
|
||||
5 | }
|
||||
6 |
|
||||
```
|
||||
|
||||
@@ -15,17 +15,10 @@ function Component() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config
|
||||
|
||||
SomeFunc may be a component..
|
||||
|
||||
error.capitalized-function-call.ts:3:12
|
||||
1 | // @validateNoCapitalizedCalls
|
||||
2 | function Component() {
|
||||
> 3 | const x = SomeFunc();
|
||||
| ^^^^^^^^^^ Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config
|
||||
| ^^^^^^^^^^ InvalidReact: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config. SomeFunc may be a component. (3:3)
|
||||
4 |
|
||||
5 | return x;
|
||||
6 | }
|
||||
|
||||
@@ -15,17 +15,10 @@ function Component() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config
|
||||
|
||||
SomeFunc may be a component..
|
||||
|
||||
error.capitalized-method-call.ts:3:12
|
||||
1 | // @validateNoCapitalizedCalls
|
||||
2 | function Component() {
|
||||
> 3 | const x = someGlobal.SomeFunc();
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config. SomeFunc may be a component. (3:3)
|
||||
4 |
|
||||
5 | return x;
|
||||
6 | }
|
||||
|
||||
@@ -32,33 +32,19 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
|
||||
Error: Cannot access refs during render
|
||||
|
||||
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)
|
||||
|
||||
error.capture-ref-for-mutation.ts:12:13
|
||||
10 | };
|
||||
11 | const moveLeft = {
|
||||
> 12 | handler: handleKey('left')(),
|
||||
| ^^^^^^^^^^^^^^^^^ This function accesses a ref value
|
||||
| ^^^^^^^^^^^^^^^^^ InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) (12:12)
|
||||
|
||||
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (12:12)
|
||||
|
||||
InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) (15:15)
|
||||
|
||||
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (15:15)
|
||||
13 | };
|
||||
14 | const moveRight = {
|
||||
15 | handler: handleKey('right')(),
|
||||
|
||||
Error: Cannot access refs during render
|
||||
|
||||
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)
|
||||
|
||||
error.capture-ref-for-mutation.ts:15:13
|
||||
13 | };
|
||||
14 | const moveRight = {
|
||||
> 15 | handler: handleKey('right')(),
|
||||
| ^^^^^^^^^^^^^^^^^^ This function accesses a ref value
|
||||
16 | };
|
||||
17 | return [moveLeft, moveRight];
|
||||
18 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -16,15 +16,10 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
|
||||
|
||||
error.conditional-hook-unknown-hook-react-namespace.ts:4:8
|
||||
2 | let x = null;
|
||||
3 | if (props.cond) {
|
||||
> 4 | x = React.useNonexistentHook();
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (4:4)
|
||||
5 | }
|
||||
6 | return x;
|
||||
7 | }
|
||||
|
||||
@@ -16,15 +16,10 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
|
||||
|
||||
error.conditional-hooks-as-method-call.ts:4:8
|
||||
2 | let x = null;
|
||||
3 | if (props.cond) {
|
||||
> 4 | x = Foo.useFoo();
|
||||
| ^^^^^^^^^^ Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
|
||||
| ^^^^^^^^^^ InvalidReact: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (4:4)
|
||||
5 | }
|
||||
6 | return x;
|
||||
7 | }
|
||||
|
||||
@@ -28,17 +28,10 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot reassign variable after render completes
|
||||
|
||||
Reassigning `x` after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.
|
||||
|
||||
error.context-variable-only-chained-assign.ts:10:19
|
||||
8 | };
|
||||
9 | const fn2 = () => {
|
||||
> 10 | const copy2 = (x = 4);
|
||||
| ^ Cannot reassign `x` after render completes
|
||||
| ^ InvalidReact: Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `x` cannot be reassigned after render (10:10)
|
||||
11 | return [invoke(fn1), copy2, identity(copy2)];
|
||||
12 | };
|
||||
13 | return invoke(fn2);
|
||||
|
||||
@@ -17,17 +17,10 @@ function Component() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot reassign variable after render completes
|
||||
|
||||
Reassigning `x` after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.
|
||||
|
||||
error.declare-reassign-variable-in-function-declaration.ts:4:4
|
||||
2 | let x = null;
|
||||
3 | function foo() {
|
||||
> 4 | x = 9;
|
||||
| ^ Cannot reassign `x` after render completes
|
||||
| ^ InvalidReact: Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `x` cannot be reassigned after render (4:4)
|
||||
5 | }
|
||||
6 | const y = bar(foo);
|
||||
7 | return <Child y={y} />;
|
||||
|
||||
@@ -22,11 +22,6 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Todo: (BuildHIR::node.lowerReorderableExpression) Expression type `ArrowFunctionExpression` cannot be safely reordered
|
||||
|
||||
error.default-param-accesses-local.ts:3:6
|
||||
1 | function Component(
|
||||
2 | x,
|
||||
> 3 | y = () => {
|
||||
@@ -34,7 +29,7 @@ error.default-param-accesses-local.ts:3:6
|
||||
> 4 | return x;
|
||||
| ^^^^^^^^^^^^^
|
||||
> 5 | }
|
||||
| ^^^^ (BuildHIR::node.lowerReorderableExpression) Expression type `ArrowFunctionExpression` cannot be safely reordered
|
||||
| ^^^^ Todo: (BuildHIR::node.lowerReorderableExpression) Expression type `ArrowFunctionExpression` cannot be safely reordered (3:5)
|
||||
6 | ) {
|
||||
7 | return y();
|
||||
8 | }
|
||||
|
||||
@@ -19,17 +19,10 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Todo: [hoisting] EnterSSA: Expected identifier to be defined before being used
|
||||
|
||||
Identifier x$1 is undefined.
|
||||
|
||||
error.dont-hoist-inline-reference.ts:3:2
|
||||
1 | import {identity} from 'shared-runtime';
|
||||
2 | function useInvalid() {
|
||||
> 3 | const x = identity(x);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ [hoisting] EnterSSA: Expected identifier to be defined before being used
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ Todo: [hoisting] EnterSSA: Expected identifier to be defined before being used. Identifier x$1 is undefined (3:3)
|
||||
4 | return x;
|
||||
5 | }
|
||||
6 |
|
||||
|
||||
@@ -15,17 +15,10 @@ function useFoo(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Todo: Encountered conflicting global in generated program
|
||||
|
||||
Conflict from local binding __DEV__.
|
||||
|
||||
error.emit-freeze-conflicting-global.ts:3:8
|
||||
1 | // @enableEmitFreeze @instrumentForget
|
||||
2 | function useFoo(props) {
|
||||
> 3 | const __DEV__ = 'conflicting global';
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Encountered conflicting global in generated program
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Todo: Encountered conflicting global in generated program. Conflict from local binding __DEV__ (3:3)
|
||||
4 | console.log(__DEV__);
|
||||
5 | return foo(props.x);
|
||||
6 | }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user