Compare commits
118 Commits
rewrite-va
...
pr34347
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96d9825588 | ||
|
|
0fba073934 | ||
|
|
735e9ac54e | ||
|
|
5d64f74211 | ||
|
|
3302d1f791 | ||
|
|
7697a9f62e | ||
|
|
3168e08f83 | ||
|
|
2805f0ed9e | ||
|
|
ac3e705a18 | ||
|
|
8e60cb7ed5 | ||
|
|
6a58b80020 | ||
|
|
b1b0955f2b | ||
|
|
1549bda33f | ||
|
|
bb6f0c8d2f | ||
|
|
aad7c664ff | ||
|
|
3fe51c9e14 | ||
|
|
4082b0e7d3 | ||
|
|
6b49c449b6 | ||
|
|
872b4fef6d | ||
|
|
c5362a380f | ||
|
|
89a803fcec | ||
|
|
8d7b5e4903 | ||
|
|
3434ff4f4b | ||
|
|
bd5b1b7639 | ||
|
|
0a1f1fcd50 | ||
|
|
b870042915 | ||
|
|
33a1095d72 | ||
|
|
213594860f | ||
|
|
9c2e2b8475 | ||
|
|
4123f6b771 | ||
|
|
cb1e73be04 | ||
|
|
cacc20e37c | ||
|
|
bb7c9c1b8a | ||
|
|
44f8451ede | ||
|
|
ad4ecb6e6e | ||
|
|
26e87b5f15 | ||
|
|
75dc0026d6 | ||
|
|
df10309e2b | ||
|
|
e42f3d30ca | ||
|
|
67e743fba5 | ||
|
|
9eede45646 | ||
|
|
090777d78a | ||
|
|
4049cfeeab | ||
|
|
e67e3bed92 | ||
|
|
06cfa99f37 | ||
|
|
05addfc663 | ||
|
|
d260b0d8b8 | ||
|
|
425ba0ad6d | ||
|
|
6de32a5a07 | ||
|
|
698bb4deb7 | ||
|
|
11d7bcf88c | ||
|
|
a85ec041d6 | ||
|
|
7d29ecbeb2 | ||
|
|
253abc78a1 | ||
|
|
d73b6f1110 | ||
|
|
d5586e2059 | ||
|
|
ec5dd0ab3a | ||
|
|
8120753665 | ||
|
|
3770ff3853 | ||
|
|
873f711299 | ||
|
|
5f06c3d22a | ||
|
|
243a56b9a2 | ||
|
|
83c7379b96 | ||
|
|
c2ac8b4f0e | ||
|
|
03fda05d2c | ||
|
|
0bc71e67ab | ||
|
|
3e20dc8b9c | ||
|
|
ae5c2f82b3 | ||
|
|
0bdb9206b7 | ||
|
|
f508edc83f | ||
|
|
0c89b160f6 | ||
|
|
87a45ae37f | ||
|
|
01ed0e9642 | ||
|
|
b58a8e3c40 | ||
|
|
42b1b33a24 | ||
|
|
7a36dfedc7 | ||
|
|
546bac7281 | ||
|
|
2cb8edbb05 | ||
|
|
431bb0bddb | ||
|
|
5063b3283f | ||
|
|
eaf6adb127 | ||
|
|
6ffcac8558 | ||
|
|
724b324b96 | ||
|
|
45a6532a08 | ||
|
|
8dba9311e5 | ||
|
|
2d98b45d92 | ||
|
|
2ba7b07ce1 | ||
|
|
a96a0f3903 | ||
|
|
02a8811864 | ||
|
|
379a083b9a | ||
|
|
534bed5fa7 | ||
|
|
db06f6b751 | ||
|
|
9433fe357a | ||
|
|
0032b2a3ee | ||
|
|
14c50e344c | ||
|
|
f1222f7652 | ||
|
|
9baecbf02b | ||
|
|
0422a00e3e | ||
|
|
47fd2f5e14 | ||
|
|
1dc3bdead1 | ||
|
|
de06211dbe | ||
|
|
ac7820a99e | ||
|
|
3c67bbe5f9 | ||
|
|
2c9a42dfd7 | ||
|
|
f1e70b5e0a | ||
|
|
d587434c35 | ||
|
|
ca292f7a57 | ||
|
|
62a634b972 | ||
|
|
53d07944df | ||
|
|
34ce3acafd | ||
|
|
6445b3154e | ||
|
|
ab5238d5a4 | ||
|
|
7a934a16b8 | ||
|
|
59ef3c4baf | ||
|
|
72965f3615 | ||
|
|
594fb5e9ab | ||
|
|
98286cf8e3 | ||
|
|
cf6e502ed2 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"packages": ["packages/react", "packages/react-dom", "packages/react-server-dom-webpack", "packages/scheduler"],
|
||||
"buildCommand": "download-build-in-codesandbox-ci",
|
||||
"node": "18",
|
||||
"node": "20",
|
||||
"publishDirectory": {
|
||||
"react": "build/oss-experimental/react",
|
||||
"react-dom": "build/oss-experimental/react-dom",
|
||||
|
||||
@@ -28,3 +28,6 @@ packages/react-devtools-shared/src/hooks/__tests__/__source__/__untransformed__/
|
||||
packages/react-devtools-shell/dist
|
||||
packages/react-devtools-timeline/dist
|
||||
packages/react-devtools-timeline/static
|
||||
|
||||
# Imported third-party Flow types
|
||||
flow-typed/
|
||||
|
||||
12
.eslintrc.js
12
.eslintrc.js
@@ -468,6 +468,7 @@ module.exports = {
|
||||
files: ['packages/react-server-dom-webpack/**/*.js'],
|
||||
globals: {
|
||||
__webpack_chunk_load__: 'readonly',
|
||||
__webpack_get_script_filename__: 'readonly',
|
||||
__webpack_require__: 'readonly',
|
||||
},
|
||||
},
|
||||
@@ -546,13 +547,10 @@ module.exports = {
|
||||
},
|
||||
|
||||
globals: {
|
||||
$Call: 'readonly',
|
||||
$ElementType: 'readonly',
|
||||
$Flow$ModuleRef: 'readonly',
|
||||
$FlowFixMe: 'readonly',
|
||||
$Keys: 'readonly',
|
||||
$NonMaybeType: 'readonly',
|
||||
$PropertyType: 'readonly',
|
||||
$ReadOnly: 'readonly',
|
||||
$ReadOnlyArray: 'readonly',
|
||||
$ArrayBufferView: 'readonly',
|
||||
@@ -567,6 +565,7 @@ module.exports = {
|
||||
BigInt: 'readonly',
|
||||
BigInt64Array: 'readonly',
|
||||
BigUint64Array: 'readonly',
|
||||
CacheType: 'readonly',
|
||||
Class: 'readonly',
|
||||
ClientRect: 'readonly',
|
||||
CopyInspectedElementPath: 'readonly',
|
||||
@@ -581,13 +580,15 @@ module.exports = {
|
||||
IteratorResult: 'readonly',
|
||||
JSONValue: 'readonly',
|
||||
JSResourceReference: 'readonly',
|
||||
mixin$Animatable: 'readonly',
|
||||
MouseEventHandler: 'readonly',
|
||||
NavigateEvent: 'readonly',
|
||||
PerformanceMeasureOptions: 'readonly',
|
||||
PropagationPhases: 'readonly',
|
||||
PropertyDescriptor: 'readonly',
|
||||
React$AbstractComponent: 'readonly',
|
||||
PropertyDescriptorMap: 'readonly',
|
||||
Proxy$traps: 'readonly',
|
||||
React$Component: 'readonly',
|
||||
React$ComponentType: 'readonly',
|
||||
React$Config: 'readonly',
|
||||
React$Context: 'readonly',
|
||||
React$Element: 'readonly',
|
||||
@@ -619,7 +620,6 @@ module.exports = {
|
||||
PropertyIndexedKeyframes: 'readonly',
|
||||
KeyframeAnimationOptions: 'readonly',
|
||||
GetAnimationsOptions: 'readonly',
|
||||
Animatable: 'readonly',
|
||||
ScrollTimeline: 'readonly',
|
||||
EventListenerOptionsOrUseCapture: 'readonly',
|
||||
FocusOptions: 'readonly',
|
||||
|
||||
2
.github/workflows/compiler_playground.yml
vendored
2
.github/workflows/compiler_playground.yml
vendored
@@ -57,8 +57,6 @@ jobs:
|
||||
key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }}
|
||||
- run: npx playwright install --with-deps chromium
|
||||
if: steps.cache_playwright_browsers.outputs.cache-hit != 'true'
|
||||
- run: npx playwright install-deps
|
||||
if: steps.cache_playwright_browsers.outputs.cache-hit == 'true'
|
||||
- run: CI=true yarn test
|
||||
- run: ls -R test-results
|
||||
if: '!cancelled()'
|
||||
|
||||
17
.github/workflows/runtime_build_and_test.yml
vendored
17
.github/workflows/runtime_build_and_test.yml
vendored
@@ -316,7 +316,7 @@ jobs:
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- run: ./scripts/react-compiler/build-compiler.sh && ./scripts/react-compiler/link-compiler.sh
|
||||
- run: yarn workspace eslint-plugin-react-hooks test
|
||||
|
||||
|
||||
# ----- BUILD -----
|
||||
build_and_lint:
|
||||
name: yarn build and lint
|
||||
@@ -811,9 +811,18 @@ jobs:
|
||||
pattern: _build_*
|
||||
path: build
|
||||
merge-multiple: true
|
||||
- run: |
|
||||
npx playwright install
|
||||
sudo npx playwright install-deps
|
||||
- name: Check Playwright version
|
||||
id: playwright_version
|
||||
run: echo "playwright_version=$(npm ls @playwright/test | grep @playwright | sed 's/.*@//' | head -1)" >> "$GITHUB_OUTPUT"
|
||||
- name: Cache Playwright Browsers for version ${{ steps.playwright_version.outputs.playwright_version }}
|
||||
id: cache_playwright_browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }}
|
||||
- name: Playwright install deps
|
||||
if: steps.cache_playwright_browsers.outputs.cache-hit != 'true'
|
||||
run: npx playwright install --with-deps chromium
|
||||
- run: ./scripts/ci/run_devtools_e2e_tests.js
|
||||
env:
|
||||
RELEASE_CHANNEL: experimental
|
||||
|
||||
@@ -18,6 +18,7 @@ jobs:
|
||||
permissions:
|
||||
# Used to create a review and close PRs
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
- name: Close PR
|
||||
uses: actions/github-script@v7
|
||||
|
||||
@@ -8,6 +8,7 @@ module.exports = {
|
||||
'@babel/plugin-syntax-jsx',
|
||||
'@babel/plugin-transform-flow-strip-types',
|
||||
['@babel/plugin-transform-class-properties', {loose: true}],
|
||||
['@babel/plugin-transform-private-methods', {loose: true}],
|
||||
'@babel/plugin-transform-classes',
|
||||
],
|
||||
presets: [
|
||||
|
||||
101
compiler/apps/playground/components/Editor/ConfigEditor.tsx
Normal file
101
compiler/apps/playground/components/Editor/ConfigEditor.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
|
||||
import type {editor} from 'monaco-editor';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import {useState} from 'react';
|
||||
import {Resizable} from 're-resizable';
|
||||
import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
import {
|
||||
generateOverridePragmaFromConfig,
|
||||
updateSourceWithOverridePragma,
|
||||
} from '../../lib/configUtils';
|
||||
|
||||
loader.config({monaco});
|
||||
|
||||
export default function ConfigEditor(): JSX.Element {
|
||||
const [, setMonaco] = useState<Monaco | null>(null);
|
||||
const store = useStore();
|
||||
const dispatchStore = useStoreDispatch();
|
||||
|
||||
const handleChange: (value: string | undefined) => void = async value => {
|
||||
if (value === undefined) return;
|
||||
|
||||
try {
|
||||
const newPragma = await generateOverridePragmaFromConfig(value);
|
||||
const updatedSource = updateSourceWithOverridePragma(
|
||||
store.source,
|
||||
newPragma,
|
||||
);
|
||||
|
||||
// Update the store with both the new config and updated source
|
||||
dispatchStore({
|
||||
type: 'updateFile',
|
||||
payload: {
|
||||
source: updatedSource,
|
||||
config: value,
|
||||
},
|
||||
});
|
||||
} catch (_) {
|
||||
dispatchStore({
|
||||
type: 'updateFile',
|
||||
payload: {
|
||||
source: store.source,
|
||||
config: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleMount: (
|
||||
_: editor.IStandaloneCodeEditor,
|
||||
monaco: Monaco,
|
||||
) => void = (_, monaco) => {
|
||||
setMonaco(monaco);
|
||||
|
||||
const uri = monaco.Uri.parse(`file:///config.js`);
|
||||
const model = monaco.editor.getModel(uri);
|
||||
if (model) {
|
||||
model.updateOptions({tabSize: 2});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col flex-none border-r border-gray-200">
|
||||
<h2 className="p-4 duration-150 ease-in border-b cursor-default border-grey-200 font-light text-secondary">
|
||||
Config Overrides
|
||||
</h2>
|
||||
<Resizable
|
||||
minWidth={300}
|
||||
maxWidth={600}
|
||||
defaultSize={{width: 350, height: 'auto'}}
|
||||
enable={{right: true}}
|
||||
className="!h-[calc(100vh_-_3.5rem_-_4rem)]">
|
||||
<MonacoEditor
|
||||
path={'config.js'}
|
||||
language={'javascript'}
|
||||
value={store.config}
|
||||
onMount={handleMount}
|
||||
onChange={handleChange}
|
||||
options={{
|
||||
...monacoOptions,
|
||||
lineNumbers: 'off',
|
||||
folding: false,
|
||||
renderLineHighlight: 'none',
|
||||
scrollBeyondLastLine: false,
|
||||
hideCursorInOverviewRuler: true,
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
fontSize: 12,
|
||||
}}
|
||||
/>
|
||||
</Resizable>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
type Store,
|
||||
} from '../../lib/stores';
|
||||
import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
import ConfigEditor from './ConfigEditor';
|
||||
import Input from './Input';
|
||||
import {
|
||||
CompilerOutput,
|
||||
@@ -46,6 +47,8 @@ import {
|
||||
} from './Output';
|
||||
import {transformFromAstSync} from '@babel/core';
|
||||
import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint';
|
||||
import {useSearchParams} from 'next/navigation';
|
||||
import {parseAndFormatConfig} from '../../lib/configUtils';
|
||||
|
||||
function parseInput(
|
||||
input: string,
|
||||
@@ -291,7 +294,13 @@ export default function Editor(): JSX.Element {
|
||||
[deferredStore.source],
|
||||
);
|
||||
|
||||
// TODO: Remove this once the config editor is more stable
|
||||
const searchParams = useSearchParams();
|
||||
const search = searchParams.get('showConfig');
|
||||
const shouldShowConfig = search === 'true';
|
||||
|
||||
useMountEffect(() => {
|
||||
// Initialize store
|
||||
let mountStore: Store;
|
||||
try {
|
||||
mountStore = initStoreFromUrlOrLocalStorage();
|
||||
@@ -307,9 +316,17 @@ export default function Editor(): JSX.Element {
|
||||
});
|
||||
mountStore = defaultStore;
|
||||
}
|
||||
dispatchStore({
|
||||
type: 'setStore',
|
||||
payload: {store: mountStore},
|
||||
|
||||
parseAndFormatConfig(mountStore.source).then(config => {
|
||||
dispatchStore({
|
||||
type: 'setStore',
|
||||
payload: {
|
||||
store: {
|
||||
...mountStore,
|
||||
config,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -328,6 +345,7 @@ export default function Editor(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex basis top-14">
|
||||
{shouldShowConfig && <ConfigEditor />}
|
||||
<div className={clsx('relative sm:basis-1/4')}>
|
||||
<Input language={language} errors={errors} />
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,7 @@ import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
// @ts-expect-error TODO: Make TS recognize .d.ts files, in addition to loading them with webpack.
|
||||
import React$Types from '../../node_modules/@types/react/index.d.ts';
|
||||
import {parseAndFormatConfig} from '../../lib/configUtils.ts';
|
||||
|
||||
loader.config({monaco});
|
||||
|
||||
@@ -79,13 +80,17 @@ export default function Input({errors, language}: Props): JSX.Element {
|
||||
});
|
||||
}, [monaco, language]);
|
||||
|
||||
const handleChange: (value: string | undefined) => void = value => {
|
||||
const handleChange: (value: string | undefined) => void = async value => {
|
||||
if (!value) return;
|
||||
|
||||
// Parse and format the config
|
||||
const config = await parseAndFormatConfig(value);
|
||||
|
||||
dispatchStore({
|
||||
type: 'updateFile',
|
||||
payload: {
|
||||
source: value,
|
||||
config,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -56,6 +56,7 @@ type ReducerAction =
|
||||
type: 'updateFile';
|
||||
payload: {
|
||||
source: string;
|
||||
config?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -66,10 +67,11 @@ function storeReducer(store: Store, action: ReducerAction): Store {
|
||||
return newStore;
|
||||
}
|
||||
case 'updateFile': {
|
||||
const {source} = action.payload;
|
||||
const {source, config} = action.payload;
|
||||
const newStore = {
|
||||
...store,
|
||||
source,
|
||||
config,
|
||||
};
|
||||
return newStore;
|
||||
}
|
||||
|
||||
87
compiler/apps/playground/lib/configUtils.ts
Normal file
87
compiler/apps/playground/lib/configUtils.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 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 parserBabel from 'prettier/plugins/babel';
|
||||
import prettierPluginEstree from 'prettier/plugins/estree';
|
||||
import * as prettier from 'prettier/standalone';
|
||||
import {parseConfigPragmaAsString} from '../../../packages/babel-plugin-react-compiler/src/Utils/TestUtils';
|
||||
|
||||
/**
|
||||
* Parse config from pragma and format it with prettier
|
||||
*/
|
||||
export async function parseAndFormatConfig(source: string): Promise<string> {
|
||||
const pragma = source.substring(0, source.indexOf('\n'));
|
||||
let configString = parseConfigPragmaAsString(pragma);
|
||||
if (configString !== '') {
|
||||
configString = `(${configString})`;
|
||||
}
|
||||
|
||||
try {
|
||||
const formatted = await prettier.format(configString, {
|
||||
semi: true,
|
||||
parser: 'babel-ts',
|
||||
plugins: [parserBabel, prettierPluginEstree],
|
||||
});
|
||||
return formatted;
|
||||
} catch (error) {
|
||||
console.error('Error formatting config:', error);
|
||||
return ''; // Return empty string if not valid for now
|
||||
}
|
||||
}
|
||||
|
||||
function extractCurlyBracesContent(input: string): string {
|
||||
const startIndex = input.indexOf('{');
|
||||
const endIndex = input.lastIndexOf('}');
|
||||
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
|
||||
throw new Error('No outer curly braces found in input');
|
||||
}
|
||||
return input.slice(startIndex, endIndex + 1);
|
||||
}
|
||||
|
||||
function cleanContent(content: string): string {
|
||||
return content
|
||||
.replace(/[\r\n]+/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a the override pragma comment from a formatted config object string
|
||||
*/
|
||||
export async function generateOverridePragmaFromConfig(
|
||||
formattedConfigString: string,
|
||||
): Promise<string> {
|
||||
const content = extractCurlyBracesContent(formattedConfigString);
|
||||
const cleanConfig = cleanContent(content);
|
||||
|
||||
// Format the config to ensure it's valid
|
||||
await prettier.format(`(${cleanConfig})`, {
|
||||
semi: false,
|
||||
parser: 'babel-ts',
|
||||
plugins: [parserBabel, prettierPluginEstree],
|
||||
});
|
||||
|
||||
return `// @OVERRIDE:${cleanConfig}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the override pragma comment in source code.
|
||||
*/
|
||||
export function updateSourceWithOverridePragma(
|
||||
source: string,
|
||||
newPragma: string,
|
||||
): string {
|
||||
const firstLineEnd = source.indexOf('\n');
|
||||
const firstLine = source.substring(0, firstLineEnd);
|
||||
|
||||
const pragmaRegex = /^\/\/\s*@/;
|
||||
if (firstLineEnd !== -1 && pragmaRegex.test(firstLine.trim())) {
|
||||
return newPragma + source.substring(firstLineEnd);
|
||||
} else {
|
||||
return newPragma + '\n' + source;
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,10 @@ export default function MyApp() {
|
||||
|
||||
export const defaultStore: Store = {
|
||||
source: index,
|
||||
config: '',
|
||||
};
|
||||
|
||||
export const emptyStore: Store = {
|
||||
source: '',
|
||||
config: '',
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ import {defaultStore} from '../defaultStore';
|
||||
*/
|
||||
export interface Store {
|
||||
source: string;
|
||||
config?: string;
|
||||
}
|
||||
export function encodeStore(store: Store): string {
|
||||
return compressToEncodedURIComponent(JSON.stringify(store));
|
||||
@@ -65,5 +66,14 @@ export function initStoreFromUrlOrLocalStorage(): Store {
|
||||
const raw = decodeStore(encodedSource);
|
||||
|
||||
invariant(isValidStore(raw), 'Invalid Store');
|
||||
|
||||
// Add config property if missing for backwards compatibility
|
||||
if (!('config' in raw)) {
|
||||
return {
|
||||
...raw,
|
||||
config: '',
|
||||
};
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
1
compiler/apps/playground/next-env.d.ts
vendored
1
compiler/apps/playground/next-env.d.ts
vendored
@@ -1,5 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -34,26 +34,30 @@
|
||||
"invariant": "^2.2.4",
|
||||
"lz-string": "^1.5.0",
|
||||
"monaco-editor": "^0.52.0",
|
||||
"next": "^15.2.0-canary.64",
|
||||
"next": "15.5.2",
|
||||
"notistack": "^3.0.0-alpha.7",
|
||||
"prettier": "^3.3.3",
|
||||
"pretty-format": "^29.3.1",
|
||||
"re-resizable": "^6.9.16",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.11.9",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"clsx": "^1.2.1",
|
||||
"concurrently": "^7.4.0",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-next": "^15.0.1",
|
||||
"eslint-config-next": "15.5.2",
|
||||
"monaco-editor-webpack-plugin": "^7.1.0",
|
||||
"postcss": "^8.4.31",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"wait-on": "^7.2.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,8 @@
|
||||
"test": "yarn workspaces run test",
|
||||
"snap": "yarn workspace babel-plugin-react-compiler run snap",
|
||||
"snap:build": "yarn workspace snap run build",
|
||||
"npm:publish": "node scripts/release/publish"
|
||||
"npm:publish": "node scripts/release/publish",
|
||||
"eslint-docs": "yarn workspace babel-plugin-react-compiler build && node scripts/build-eslint-docs.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"fs-extra": "^4.0.2",
|
||||
|
||||
@@ -7,9 +7,10 @@
|
||||
|
||||
import * as t from '@babel/types';
|
||||
import {codeFrameColumns} from '@babel/code-frame';
|
||||
import type {SourceLocation} from './HIR';
|
||||
import {type SourceLocation} from './HIR';
|
||||
import {Err, Ok, Result} from './Utils/Result';
|
||||
import {assertExhaustive} from './Utils/utils';
|
||||
import invariant from 'invariant';
|
||||
|
||||
export enum ErrorSeverity {
|
||||
/**
|
||||
@@ -35,6 +36,14 @@ export enum ErrorSeverity {
|
||||
* memoization.
|
||||
*/
|
||||
CannotPreserveMemoization = 'CannotPreserveMemoization',
|
||||
/**
|
||||
* An API that is known to be incompatible with the compiler. Generally as a result of
|
||||
* the library using "interior mutability", ie having a value whose referential identity
|
||||
* stays the same but which provides access to values that can change. For example a
|
||||
* function that doesn't change but returns different results, or an object that doesn't
|
||||
* change identity but whose properties change.
|
||||
*/
|
||||
IncompatibleLibrary = 'IncompatibleLibrary',
|
||||
/**
|
||||
* Unhandled syntax that we don't support yet.
|
||||
*/
|
||||
@@ -47,8 +56,9 @@ export enum ErrorSeverity {
|
||||
}
|
||||
|
||||
export type CompilerDiagnosticOptions = {
|
||||
category: ErrorCategory;
|
||||
severity: ErrorSeverity;
|
||||
category: string;
|
||||
reason: string;
|
||||
description: string;
|
||||
details: Array<CompilerDiagnosticDetail>;
|
||||
suggestions?: Array<CompilerSuggestion> | null | undefined;
|
||||
@@ -58,11 +68,15 @@ export type CompilerDiagnosticDetail =
|
||||
/**
|
||||
* A/the source of the error
|
||||
*/
|
||||
{
|
||||
kind: 'error';
|
||||
loc: SourceLocation | null;
|
||||
message: string;
|
||||
};
|
||||
| {
|
||||
kind: 'error';
|
||||
loc: SourceLocation | null;
|
||||
message: string;
|
||||
}
|
||||
| {
|
||||
kind: 'hint';
|
||||
message: string;
|
||||
};
|
||||
|
||||
export enum CompilerSuggestionOperation {
|
||||
InsertBefore,
|
||||
@@ -87,9 +101,10 @@ export type CompilerSuggestion =
|
||||
};
|
||||
|
||||
export type CompilerErrorDetailOptions = {
|
||||
category: ErrorCategory;
|
||||
severity: ErrorSeverity;
|
||||
reason: string;
|
||||
description?: string | null | undefined;
|
||||
severity: ErrorSeverity;
|
||||
loc: SourceLocation | null;
|
||||
suggestions?: Array<CompilerSuggestion> | null | undefined;
|
||||
};
|
||||
@@ -115,8 +130,8 @@ export class CompilerDiagnostic {
|
||||
return new CompilerDiagnostic({...options, details: []});
|
||||
}
|
||||
|
||||
get category(): CompilerDiagnosticOptions['category'] {
|
||||
return this.options.category;
|
||||
get reason(): CompilerDiagnosticOptions['reason'] {
|
||||
return this.options.reason;
|
||||
}
|
||||
get description(): CompilerDiagnosticOptions['description'] {
|
||||
return this.options.description;
|
||||
@@ -127,6 +142,9 @@ export class CompilerDiagnostic {
|
||||
get suggestions(): CompilerDiagnosticOptions['suggestions'] {
|
||||
return this.options.suggestions;
|
||||
}
|
||||
get category(): ErrorCategory {
|
||||
return this.options.category;
|
||||
}
|
||||
|
||||
withDetail(detail: CompilerDiagnosticDetail): CompilerDiagnostic {
|
||||
this.options.details.push(detail);
|
||||
@@ -134,12 +152,17 @@ export class CompilerDiagnostic {
|
||||
}
|
||||
|
||||
primaryLocation(): SourceLocation | null {
|
||||
return this.options.details.filter(d => d.kind === 'error')[0]?.loc ?? null;
|
||||
const firstErrorDetail = this.options.details.filter(
|
||||
d => d.kind === 'error',
|
||||
)[0];
|
||||
return firstErrorDetail != null && firstErrorDetail.kind === 'error'
|
||||
? firstErrorDetail.loc
|
||||
: null;
|
||||
}
|
||||
|
||||
printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
|
||||
const buffer = [
|
||||
printErrorSummary(this.severity, this.category),
|
||||
printErrorSummary(this.severity, this.reason),
|
||||
'\n\n',
|
||||
this.description,
|
||||
];
|
||||
@@ -167,9 +190,14 @@ export class CompilerDiagnostic {
|
||||
buffer.push(codeFrame);
|
||||
break;
|
||||
}
|
||||
case 'hint': {
|
||||
buffer.push('\n\n');
|
||||
buffer.push(detail.message);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
detail.kind,
|
||||
detail,
|
||||
`Unexpected detail kind ${(detail as any).kind}`,
|
||||
);
|
||||
}
|
||||
@@ -179,7 +207,7 @@ export class CompilerDiagnostic {
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
const buffer = [printErrorSummary(this.severity, this.category)];
|
||||
const buffer = [printErrorSummary(this.severity, this.reason)];
|
||||
if (this.description != null) {
|
||||
buffer.push(`. ${this.description}.`);
|
||||
}
|
||||
@@ -217,6 +245,9 @@ export class CompilerErrorDetail {
|
||||
get suggestions(): CompilerErrorDetailOptions['suggestions'] {
|
||||
return this.options.suggestions;
|
||||
}
|
||||
get category(): ErrorCategory {
|
||||
return this.options.category;
|
||||
}
|
||||
|
||||
primaryLocation(): SourceLocation | null {
|
||||
return this.loc;
|
||||
@@ -266,13 +297,14 @@ export class CompilerError extends Error {
|
||||
|
||||
static invariant(
|
||||
condition: unknown,
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity'>,
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity' | 'category'>,
|
||||
): asserts condition {
|
||||
if (!condition) {
|
||||
const errors = new CompilerError();
|
||||
errors.pushErrorDetail(
|
||||
new CompilerErrorDetail({
|
||||
...options,
|
||||
category: ErrorCategory.Invariant,
|
||||
severity: ErrorSeverity.Invariant,
|
||||
}),
|
||||
);
|
||||
@@ -287,23 +319,28 @@ export class CompilerError extends Error {
|
||||
}
|
||||
|
||||
static throwTodo(
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity'>,
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity' | 'category'>,
|
||||
): never {
|
||||
const errors = new CompilerError();
|
||||
errors.pushErrorDetail(
|
||||
new CompilerErrorDetail({...options, severity: ErrorSeverity.Todo}),
|
||||
new CompilerErrorDetail({
|
||||
...options,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
}),
|
||||
);
|
||||
throw errors;
|
||||
}
|
||||
|
||||
static throwInvalidJS(
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity'>,
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity' | 'category'>,
|
||||
): never {
|
||||
const errors = new CompilerError();
|
||||
errors.pushErrorDetail(
|
||||
new CompilerErrorDetail({
|
||||
...options,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
}),
|
||||
);
|
||||
throw errors;
|
||||
@@ -323,13 +360,14 @@ export class CompilerError extends Error {
|
||||
}
|
||||
|
||||
static throwInvalidConfig(
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity'>,
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity' | 'category'>,
|
||||
): never {
|
||||
const errors = new CompilerError();
|
||||
errors.pushErrorDetail(
|
||||
new CompilerErrorDetail({
|
||||
...options,
|
||||
severity: ErrorSeverity.InvalidConfig,
|
||||
category: ErrorCategory.Config,
|
||||
}),
|
||||
);
|
||||
throw errors;
|
||||
@@ -393,6 +431,7 @@ export class CompilerError extends Error {
|
||||
|
||||
push(options: CompilerErrorDetailOptions): CompilerErrorDetail {
|
||||
const detail = new CompilerErrorDetail({
|
||||
category: options.category,
|
||||
reason: options.reason,
|
||||
description: options.description ?? null,
|
||||
severity: options.severity,
|
||||
@@ -427,7 +466,8 @@ export class CompilerError extends Error {
|
||||
case ErrorSeverity.InvalidJS:
|
||||
case ErrorSeverity.InvalidReact:
|
||||
case ErrorSeverity.InvalidConfig:
|
||||
case ErrorSeverity.UnsupportedJS: {
|
||||
case ErrorSeverity.UnsupportedJS:
|
||||
case ErrorSeverity.IncompatibleLibrary: {
|
||||
return true;
|
||||
}
|
||||
case ErrorSeverity.CannotPreserveMemoization:
|
||||
@@ -475,8 +515,9 @@ function printErrorSummary(severity: ErrorSeverity, message: string): string {
|
||||
severityCategory = 'Error';
|
||||
break;
|
||||
}
|
||||
case ErrorSeverity.IncompatibleLibrary:
|
||||
case ErrorSeverity.CannotPreserveMemoization: {
|
||||
severityCategory = 'Memoization';
|
||||
severityCategory = 'Compilation Skipped';
|
||||
break;
|
||||
}
|
||||
case ErrorSeverity.Invariant: {
|
||||
@@ -493,3 +534,370 @@ function printErrorSummary(severity: ErrorSeverity, message: string): string {
|
||||
}
|
||||
return `${severityCategory}: ${message}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* See getRuleForCategory() for how these map to ESLint rules
|
||||
*/
|
||||
export enum ErrorCategory {
|
||||
// Checking for valid hooks usage (non conditional, non-first class, non reactive, etc)
|
||||
Hooks = 'Hooks',
|
||||
|
||||
// Checking for no capitalized calls (not definitively an error, hence separating)
|
||||
CapitalizedCalls = 'CapitalizedCalls',
|
||||
|
||||
// Checking for static components
|
||||
StaticComponents = 'StaticComponents',
|
||||
|
||||
// Checking for valid usage of manual memoization
|
||||
UseMemo = 'UseMemo',
|
||||
|
||||
// Checking for higher order functions acting as factories for components/hooks
|
||||
Factories = 'Factories',
|
||||
|
||||
// Checks that manual memoization is preserved
|
||||
PreserveManualMemo = 'PreserveManualMemo',
|
||||
|
||||
// Checks for known incompatible libraries
|
||||
IncompatibleLibrary = 'IncompatibleLibrary',
|
||||
|
||||
// Checking for no mutations of props, hook arguments, hook return values
|
||||
Immutability = 'Immutability',
|
||||
|
||||
// Checking for assignments to globals
|
||||
Globals = 'Globals',
|
||||
|
||||
// Checking for valid usage of refs, ie no access during render
|
||||
Refs = 'Refs',
|
||||
|
||||
// Checks for memoized effect deps
|
||||
EffectDependencies = 'EffectDependencies',
|
||||
|
||||
// Checks for no setState in effect bodies
|
||||
EffectSetState = 'EffectSetState',
|
||||
|
||||
EffectDerivationsOfState = 'EffectDerivationsOfState',
|
||||
|
||||
// Validates against try/catch in place of error boundaries
|
||||
ErrorBoundaries = 'ErrorBoundaries',
|
||||
|
||||
// Checking for pure functions
|
||||
Purity = 'Purity',
|
||||
|
||||
// Validates against setState in render
|
||||
RenderSetState = 'RenderSetState',
|
||||
|
||||
// Internal invariants
|
||||
Invariant = 'Invariant',
|
||||
|
||||
// Todos
|
||||
Todo = 'Todo',
|
||||
|
||||
// Syntax errors
|
||||
Syntax = 'Syntax',
|
||||
|
||||
// Checks for use of unsupported syntax
|
||||
UnsupportedSyntax = 'UnsupportedSyntax',
|
||||
|
||||
// Config errors
|
||||
Config = 'Config',
|
||||
|
||||
// Gating error
|
||||
Gating = 'Gating',
|
||||
|
||||
// Suppressions
|
||||
Suppression = 'Suppression',
|
||||
|
||||
// Issues with auto deps
|
||||
AutomaticEffectDependencies = 'AutomaticEffectDependencies',
|
||||
|
||||
// Issues with `fire`
|
||||
Fire = 'Fire',
|
||||
|
||||
// fbt-specific issues
|
||||
FBT = 'FBT',
|
||||
}
|
||||
|
||||
export type LintRule = {
|
||||
// Stores the category the rule corresponds to, used to filter errors when reporting
|
||||
category: ErrorCategory;
|
||||
|
||||
/**
|
||||
* The "name" of the rule as it will be used by developers to enable/disable, eg
|
||||
* "eslint-disable-nest line <name>"
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* A description of the rule that appears somewhere in ESLint. This does not affect
|
||||
* how error messages are formatted
|
||||
*/
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* If true, this rule will automatically appear in the default, "recommended" ESLint
|
||||
* rule set. Otherwise it will be part of an `allRules` export that developers can
|
||||
* use to opt-in to showing output of all possible rules.
|
||||
*
|
||||
* NOTE: not all validations are enabled by default! Setting this flag only affects
|
||||
* whether a given rule is part of the recommended set. The corresponding validation
|
||||
* also should be enabled by default if you want the error to actually show up!
|
||||
*/
|
||||
recommended: boolean;
|
||||
};
|
||||
|
||||
const RULE_NAME_PATTERN = /^[a-z]+(-[a-z]+)*$/;
|
||||
|
||||
export function getRuleForCategory(category: ErrorCategory): LintRule {
|
||||
const rule = getRuleForCategoryImpl(category);
|
||||
invariant(
|
||||
RULE_NAME_PATTERN.test(rule.name),
|
||||
`Invalid rule name, got '${rule.name}' but rules must match ${RULE_NAME_PATTERN.toString()}`,
|
||||
);
|
||||
return rule;
|
||||
}
|
||||
|
||||
function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
switch (category) {
|
||||
case ErrorCategory.AutomaticEffectDependencies: {
|
||||
return {
|
||||
category,
|
||||
name: 'automatic-effect-dependencies',
|
||||
description:
|
||||
'Verifies that automatic effect dependencies are compiled if opted-in',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.CapitalizedCalls: {
|
||||
return {
|
||||
category,
|
||||
name: 'capitalized-calls',
|
||||
description:
|
||||
'Validates against calling capitalized functions/methods instead of using JSX',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Config: {
|
||||
return {
|
||||
category,
|
||||
name: 'config',
|
||||
description: 'Validates the compiler configuration options',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectDependencies: {
|
||||
return {
|
||||
category,
|
||||
name: 'memoized-effect-dependencies',
|
||||
description: 'Validates that effect dependencies are memoized',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectDerivationsOfState: {
|
||||
return {
|
||||
category,
|
||||
name: 'no-deriving-state-in-effects',
|
||||
description:
|
||||
'Validates against deriving values from state in an effect',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectSetState: {
|
||||
return {
|
||||
category,
|
||||
name: 'set-state-in-effect',
|
||||
description:
|
||||
'Validates against calling setState synchronously in an effect, which can lead to re-renders that degrade performance',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.ErrorBoundaries: {
|
||||
return {
|
||||
category,
|
||||
name: 'error-boundaries',
|
||||
description:
|
||||
'Validates usage of error boundaries instead of try/catch for errors in child components',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Factories: {
|
||||
return {
|
||||
category,
|
||||
name: 'component-hook-factories',
|
||||
description:
|
||||
'Validates against higher order functions defining nested components or hooks. ' +
|
||||
'Components and hooks should be defined at the module level',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.FBT: {
|
||||
return {
|
||||
category,
|
||||
name: 'fbt',
|
||||
description: 'Validates usage of fbt',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Fire: {
|
||||
return {
|
||||
category,
|
||||
name: 'fire',
|
||||
description: 'Validates usage of `fire`',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Gating: {
|
||||
return {
|
||||
category,
|
||||
name: 'gating',
|
||||
description:
|
||||
'Validates configuration of [gating mode](https://react.dev/reference/react-compiler/gating)',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Globals: {
|
||||
return {
|
||||
category,
|
||||
name: 'globals',
|
||||
description:
|
||||
'Validates against assignment/mutation of globals during render, part of ensuring that ' +
|
||||
'[side effects must render outside of render](https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Hooks: {
|
||||
return {
|
||||
category,
|
||||
name: 'hooks',
|
||||
description: 'Validates the rules of hooks',
|
||||
/**
|
||||
* TODO: the "Hooks" rule largely reimplements the "rules-of-hooks" non-compiler rule.
|
||||
* We need to dedeupe these (moving the remaining bits into the compiler) and then enable
|
||||
* this rule.
|
||||
*/
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Immutability: {
|
||||
return {
|
||||
category,
|
||||
name: 'immutability',
|
||||
description:
|
||||
'Validates against mutating props, state, and other values that [are immutable](https://react.dev/reference/rules/components-and-hooks-must-be-pure#props-and-state-are-immutable)',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Invariant: {
|
||||
return {
|
||||
category,
|
||||
name: 'invariant',
|
||||
description: 'Internal invariants',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.PreserveManualMemo: {
|
||||
return {
|
||||
category,
|
||||
name: 'preserve-manual-memoization',
|
||||
description:
|
||||
'Validates that existing manual memoized is preserved by the compiler. ' +
|
||||
'React Compiler will only compile components and hooks if its inference ' +
|
||||
'[matches or exceeds the existing manual memoization](https://react.dev/learn/react-compiler/introduction#what-should-i-do-about-usememo-usecallback-and-reactmemo)',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Purity: {
|
||||
return {
|
||||
category,
|
||||
name: 'purity',
|
||||
description:
|
||||
'Validates that [components/hooks are pure](https://react.dev/reference/rules/components-and-hooks-must-be-pure) by checking that they do not call known-impure functions',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Refs: {
|
||||
return {
|
||||
category,
|
||||
name: 'refs',
|
||||
description:
|
||||
'Validates correct usage of refs, not reading/writing during render. See the "pitfalls" section in [`useRef()` usage](https://react.dev/reference/react/useRef#usage)',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.RenderSetState: {
|
||||
return {
|
||||
category,
|
||||
name: 'set-state-in-render',
|
||||
description:
|
||||
'Validates against setting state during render, which can trigger additional renders and potential infinite render loops',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.StaticComponents: {
|
||||
return {
|
||||
category,
|
||||
name: 'static-components',
|
||||
description:
|
||||
'Validates that components are static, not recreated every render. Components that are recreated dynamically can reset state and trigger excessive re-rendering',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Suppression: {
|
||||
return {
|
||||
category,
|
||||
name: 'rule-suppression',
|
||||
description: 'Validates against suppression of other rules',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Syntax: {
|
||||
return {
|
||||
category,
|
||||
name: 'syntax',
|
||||
description: 'Validates against invalid syntax',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Todo: {
|
||||
return {
|
||||
category,
|
||||
name: 'todo',
|
||||
description: 'Unimplemented features',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.UnsupportedSyntax: {
|
||||
return {
|
||||
category,
|
||||
name: 'unsupported-syntax',
|
||||
description:
|
||||
'Validates against syntax that we do not plan to support in React Compiler',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.UseMemo: {
|
||||
return {
|
||||
category,
|
||||
name: 'use-memo',
|
||||
description:
|
||||
'Validates usage of the useMemo() hook against common mistakes. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.IncompatibleLibrary: {
|
||||
return {
|
||||
category,
|
||||
name: 'incompatible-library',
|
||||
description:
|
||||
'Validates against usage of libraries which are incompatible with memoization (manual or automatic)',
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(category, `Unsupported category ${category}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const LintRules: Array<LintRule> = Object.keys(ErrorCategory).map(
|
||||
category => getRuleForCategory(category as any),
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@ import {NodePath} from '@babel/core';
|
||||
import * as t from '@babel/types';
|
||||
import {Scope as BabelScope} from '@babel/traverse';
|
||||
|
||||
import {CompilerError, ErrorSeverity} from '../CompilerError';
|
||||
import {CompilerError, ErrorCategory, ErrorSeverity} from '../CompilerError';
|
||||
import {
|
||||
EnvironmentConfig,
|
||||
GeneratedSource,
|
||||
@@ -38,6 +38,7 @@ export function validateRestrictedImports(
|
||||
ImportDeclaration(importDeclPath) {
|
||||
if (restrictedImports.has(importDeclPath.node.source.value)) {
|
||||
error.push({
|
||||
category: ErrorCategory.Todo,
|
||||
severity: ErrorSeverity.Todo,
|
||||
reason: 'Bailing out due to blocklisted import',
|
||||
description: `Import from module ${importDeclPath.node.source.value}`,
|
||||
@@ -205,6 +206,7 @@ export class ProgramContext {
|
||||
}
|
||||
const error = new CompilerError();
|
||||
error.push({
|
||||
category: ErrorCategory.Todo,
|
||||
severity: ErrorSeverity.Todo,
|
||||
reason: 'Encountered conflicting global in generated program',
|
||||
description: `Conflict from local binding ${name}`,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import {NodePath} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import prettyFormat from 'pretty-format';
|
||||
import {Logger, ProgramContext} from '.';
|
||||
import {Logger, ProgramContext, SingleLineSuppressionRange} from '.';
|
||||
import {
|
||||
HIRFunction,
|
||||
ReactiveFunction,
|
||||
@@ -33,9 +33,7 @@ import {findContextIdentifiers} from '../HIR/FindContextIdentifiers';
|
||||
import {
|
||||
analyseFunctions,
|
||||
dropManualMemoization,
|
||||
inferMutableRanges,
|
||||
inferReactivePlaces,
|
||||
inferReferenceEffects,
|
||||
inlineImmediatelyInvokedFunctionExpressions,
|
||||
inferEffectDependencies,
|
||||
} from '../Inference';
|
||||
@@ -100,7 +98,6 @@ import {outlineJSX} from '../Optimization/OutlineJsx';
|
||||
import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls';
|
||||
import {transformFire} from '../Transform';
|
||||
import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureFunctionsInRender';
|
||||
import {CompilerError} from '..';
|
||||
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
|
||||
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
|
||||
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
|
||||
@@ -124,6 +121,7 @@ function run(
|
||||
logger: Logger | null,
|
||||
filename: string | null,
|
||||
code: string | null,
|
||||
suppressions: Array<SingleLineSuppressionRange>,
|
||||
): CodegenFunction {
|
||||
const contextIdentifiers = findContextIdentifiers(func);
|
||||
const env = new Environment(
|
||||
@@ -137,6 +135,7 @@ function run(
|
||||
filename,
|
||||
code,
|
||||
programContext,
|
||||
suppressions,
|
||||
);
|
||||
env.logger?.debugLogIRs?.({
|
||||
kind: 'debug',
|
||||
@@ -229,26 +228,12 @@ function runWithEnvironment(
|
||||
analyseFunctions(hir);
|
||||
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
|
||||
|
||||
if (!env.config.enableNewMutationAliasingModel) {
|
||||
const fnEffectErrors = inferReferenceEffects(hir);
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (fnEffectErrors.length > 0) {
|
||||
CompilerError.throw(fnEffectErrors[0]);
|
||||
}
|
||||
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
|
||||
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (mutabilityAliasingErrors.isErr()) {
|
||||
throw mutabilityAliasingErrors.unwrapErr();
|
||||
}
|
||||
log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
|
||||
} else {
|
||||
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
|
||||
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (mutabilityAliasingErrors.isErr()) {
|
||||
throw mutabilityAliasingErrors.unwrapErr();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!env.config.enableNewMutationAliasingModel) {
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
}
|
||||
|
||||
// Note: Has to come after infer reference effects because "dead" code may still affect inference
|
||||
@@ -263,20 +248,15 @@ function runWithEnvironment(
|
||||
pruneMaybeThrows(hir);
|
||||
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
|
||||
|
||||
if (!env.config.enableNewMutationAliasingModel) {
|
||||
inferMutableRanges(hir);
|
||||
log({kind: 'hir', name: 'InferMutableRanges', value: hir});
|
||||
} else {
|
||||
const mutabilityAliasingErrors = inferMutationAliasingRanges(hir, {
|
||||
isFunctionExpression: false,
|
||||
});
|
||||
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (mutabilityAliasingErrors.isErr()) {
|
||||
throw mutabilityAliasingErrors.unwrapErr();
|
||||
}
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
const mutabilityAliasingRangeErrors = inferMutationAliasingRanges(hir, {
|
||||
isFunctionExpression: false,
|
||||
});
|
||||
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (mutabilityAliasingRangeErrors.isErr()) {
|
||||
throw mutabilityAliasingRangeErrors.unwrapErr();
|
||||
}
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
}
|
||||
|
||||
if (env.isInferredMemoEnabled) {
|
||||
@@ -308,12 +288,7 @@ function runWithEnvironment(
|
||||
validateNoImpureFunctionsInRender(hir).unwrap();
|
||||
}
|
||||
|
||||
if (
|
||||
env.config.validateNoFreezingKnownMutableFunctions ||
|
||||
env.config.enableNewMutationAliasingModel
|
||||
) {
|
||||
validateNoFreezingKnownMutableFunctions(hir).unwrap();
|
||||
}
|
||||
validateNoFreezingKnownMutableFunctions(hir).unwrap();
|
||||
}
|
||||
|
||||
inferReactivePlaces(hir);
|
||||
@@ -594,6 +569,7 @@ export function compileFn(
|
||||
logger: Logger | null,
|
||||
filename: string | null,
|
||||
code: string | null,
|
||||
singleLineSuppressions: Array<SingleLineSuppressionRange>,
|
||||
): CodegenFunction {
|
||||
return run(
|
||||
func,
|
||||
@@ -604,5 +580,6 @@ export function compileFn(
|
||||
logger,
|
||||
filename,
|
||||
code,
|
||||
singleLineSuppressions,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import * as t from '@babel/types';
|
||||
import {
|
||||
CompilerError,
|
||||
CompilerErrorDetail,
|
||||
ErrorCategory,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {ExternalFunction, ReactFunctionType} from '../HIR/Environment';
|
||||
@@ -26,8 +27,9 @@ import {
|
||||
import {CompilerReactTarget, PluginOptions} from './Options';
|
||||
import {compileFn} from './Pipeline';
|
||||
import {
|
||||
filterSuppressionsThatAffectFunction,
|
||||
filterSuppressionsThatAffectNode,
|
||||
findProgramSuppressions,
|
||||
SingleLineSuppressionRange,
|
||||
suppressionsToCompilerError,
|
||||
} from './Suppression';
|
||||
import {GeneratedSource} from '../HIR';
|
||||
@@ -105,6 +107,7 @@ function findDirectivesDynamicGating(
|
||||
reason: `Dynamic gating directive is not a valid JavaScript identifier`,
|
||||
description: `Found '${directive.value.value}'`,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: ErrorCategory.Gating,
|
||||
loc: directive.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -121,6 +124,7 @@ function findDirectivesDynamicGating(
|
||||
.map(r => r.directive.value.value)
|
||||
.join(', ')}]`,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: ErrorCategory.Gating,
|
||||
loc: result[0].directive.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -456,6 +460,7 @@ export function compileProgram(
|
||||
reason:
|
||||
'Unexpected compiled functions when module scope opt-out is present',
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: ErrorCategory.Invariant,
|
||||
loc: null,
|
||||
}),
|
||||
);
|
||||
@@ -490,7 +495,20 @@ function findFunctionsToCompile(
|
||||
): Array<CompileSource> {
|
||||
const queue: Array<CompileSource> = [];
|
||||
const traverseFunction = (fn: BabelFn, pass: CompilerPass): void => {
|
||||
// In 'all' mode, compile only top level functions
|
||||
if (
|
||||
pass.opts.compilationMode === 'all' &&
|
||||
fn.scope.getProgramParent() !== fn.scope.parent
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fnType = getReactFunctionType(fn, pass);
|
||||
|
||||
if (pass.opts.environment.validateNoDynamicallyCreatedComponentsOrHooks) {
|
||||
validateNoDynamicallyCreatedComponentsOrHooks(fn, pass, programContext);
|
||||
}
|
||||
|
||||
if (fnType === null || programContext.alreadyCompiled.has(fn.node)) {
|
||||
return;
|
||||
}
|
||||
@@ -674,11 +692,17 @@ function tryCompileFunction(
|
||||
* Program node itself. We need to figure out whether an eslint suppression range
|
||||
* applies to this function first.
|
||||
*/
|
||||
const suppressionsInFunction = filterSuppressionsThatAffectFunction(
|
||||
const suppressionsInFunction = filterSuppressionsThatAffectNode(
|
||||
programContext.suppressions,
|
||||
fn,
|
||||
);
|
||||
if (suppressionsInFunction.length > 0) {
|
||||
const singleLineSuppressions = suppressionsInFunction.filter(
|
||||
s => s.kind === 'single-line',
|
||||
) as Array<SingleLineSuppressionRange>;
|
||||
const multiLineSuppressions = suppressionsInFunction.filter(
|
||||
s => s.kind === 'multi-line',
|
||||
);
|
||||
if (multiLineSuppressions.length > 0) {
|
||||
return {
|
||||
kind: 'error',
|
||||
error: suppressionsToCompilerError(suppressionsInFunction),
|
||||
@@ -697,6 +721,7 @@ function tryCompileFunction(
|
||||
programContext.opts.logger,
|
||||
programContext.filename,
|
||||
programContext.code,
|
||||
singleLineSuppressions,
|
||||
),
|
||||
};
|
||||
} catch (err) {
|
||||
@@ -735,6 +760,7 @@ function retryCompileFunction(
|
||||
programContext.opts.logger,
|
||||
programContext.filename,
|
||||
programContext.code,
|
||||
[], // ignore suppressions in the retry pipeline
|
||||
);
|
||||
|
||||
if (!retryResult.hasFireRewrite && !retryResult.hasInferredEffect) {
|
||||
@@ -811,6 +837,7 @@ function shouldSkipCompilation(
|
||||
description:
|
||||
"When the 'sources' config options is specified, the React compiler will only compile files with a name",
|
||||
severity: ErrorSeverity.InvalidConfig,
|
||||
category: ErrorCategory.Config,
|
||||
loc: null,
|
||||
}),
|
||||
);
|
||||
@@ -834,6 +861,73 @@ function shouldSkipCompilation(
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that Components/Hooks are always defined at module level. This prevents scope reference
|
||||
* errors that occur when the compiler attempts to optimize the nested component/hook while its
|
||||
* parent function remains uncompiled.
|
||||
*/
|
||||
function validateNoDynamicallyCreatedComponentsOrHooks(
|
||||
fn: BabelFn,
|
||||
pass: CompilerPass,
|
||||
programContext: ProgramContext,
|
||||
): void {
|
||||
const parentNameExpr = getFunctionName(fn);
|
||||
const parentName =
|
||||
parentNameExpr !== null && parentNameExpr.isIdentifier()
|
||||
? parentNameExpr.node.name
|
||||
: '<anonymous>';
|
||||
|
||||
const validateNestedFunction = (
|
||||
nestedFn: NodePath<
|
||||
t.FunctionDeclaration | t.FunctionExpression | t.ArrowFunctionExpression
|
||||
>,
|
||||
): void => {
|
||||
if (
|
||||
nestedFn.node === fn.node ||
|
||||
programContext.alreadyCompiled.has(nestedFn.node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nestedFn.scope.getProgramParent() !== nestedFn.scope.parent) {
|
||||
const nestedFnType = getReactFunctionType(nestedFn as BabelFn, pass);
|
||||
const nestedFnNameExpr = getFunctionName(nestedFn as BabelFn);
|
||||
const nestedName =
|
||||
nestedFnNameExpr !== null && nestedFnNameExpr.isIdentifier()
|
||||
? nestedFnNameExpr.node.name
|
||||
: '<anonymous>';
|
||||
if (nestedFnType === 'Component' || nestedFnType === 'Hook') {
|
||||
CompilerError.throwDiagnostic({
|
||||
category: ErrorCategory.Factories,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: `Components and hooks cannot be created dynamically`,
|
||||
description: `The function \`${nestedName}\` appears to be a React ${nestedFnType.toLowerCase()}, but it's defined inside \`${parentName}\`. Components and Hooks should always be declared at module scope`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
message: 'this function dynamically created a component/hook',
|
||||
loc: parentNameExpr?.node.loc ?? fn.node.loc ?? null,
|
||||
},
|
||||
{
|
||||
kind: 'error',
|
||||
message: 'the component is created here',
|
||||
loc: nestedFnNameExpr?.node.loc ?? nestedFn.node.loc ?? null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
nestedFn.skip();
|
||||
};
|
||||
|
||||
fn.traverse({
|
||||
FunctionDeclaration: validateNestedFunction,
|
||||
FunctionExpression: validateNestedFunction,
|
||||
ArrowFunctionExpression: validateNestedFunction,
|
||||
});
|
||||
}
|
||||
|
||||
function getReactFunctionType(
|
||||
fn: BabelFn,
|
||||
pass: CompilerPass,
|
||||
@@ -872,11 +966,6 @@ function getReactFunctionType(
|
||||
return componentSyntaxType;
|
||||
}
|
||||
case 'all': {
|
||||
// Compile only top level functions
|
||||
if (fn.scope.getProgramParent() !== fn.scope.parent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getComponentOrHookLike(fn, hookPattern) ?? 'Other';
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
CompilerSuggestionOperation,
|
||||
ErrorCategory,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
@@ -24,11 +25,23 @@ import {GeneratedSource} from '../HIR';
|
||||
* The enable comment can be missing in the case where only a disable block is present, ie the rest
|
||||
* of the file has potential React violations.
|
||||
*/
|
||||
export type SuppressionRange = {
|
||||
disableComment: t.Comment;
|
||||
enableComment: t.Comment | null;
|
||||
source: SuppressionSource;
|
||||
};
|
||||
export type SuppressionRange =
|
||||
| {
|
||||
kind: 'single-line';
|
||||
source: SuppressionSource;
|
||||
comment: t.Comment;
|
||||
}
|
||||
| {
|
||||
kind: 'multi-line';
|
||||
source: SuppressionSource;
|
||||
disableComment: t.Comment;
|
||||
enableComment: t.Comment | null;
|
||||
};
|
||||
|
||||
export type SingleLineSuppressionRange = Extract<
|
||||
SuppressionRange,
|
||||
{kind: 'single-line'}
|
||||
>;
|
||||
|
||||
type SuppressionSource = 'Eslint' | 'Flow';
|
||||
|
||||
@@ -37,15 +50,23 @@ type SuppressionSource = 'Eslint' | 'Flow';
|
||||
* 1. The suppression is within the function's body; or
|
||||
* 2. The suppression wraps the function
|
||||
*/
|
||||
export function filterSuppressionsThatAffectFunction(
|
||||
suppressionRanges: Array<SuppressionRange>,
|
||||
fn: NodePath<t.Function>,
|
||||
): Array<SuppressionRange> {
|
||||
const suppressionsInScope: Array<SuppressionRange> = [];
|
||||
const fnNode = fn.node;
|
||||
export function filterSuppressionsThatAffectNode<T extends SuppressionRange>(
|
||||
suppressionRanges: Array<T>,
|
||||
node: NodePath,
|
||||
): Array<T> {
|
||||
const suppressionsInScope: Array<T> = [];
|
||||
const fnNode = node.node;
|
||||
for (const suppressionRange of suppressionRanges) {
|
||||
const enableComment =
|
||||
suppressionRange.kind === 'single-line'
|
||||
? suppressionRange.comment
|
||||
: suppressionRange.enableComment;
|
||||
const disableComment =
|
||||
suppressionRange.kind === 'single-line'
|
||||
? suppressionRange.comment
|
||||
: suppressionRange.disableComment;
|
||||
if (
|
||||
suppressionRange.disableComment.start == null ||
|
||||
disableComment.start == null ||
|
||||
fnNode.start == null ||
|
||||
fnNode.end == null
|
||||
) {
|
||||
@@ -53,22 +74,20 @@ export function filterSuppressionsThatAffectFunction(
|
||||
}
|
||||
// The suppression is within the function
|
||||
if (
|
||||
suppressionRange.disableComment.start > fnNode.start &&
|
||||
disableComment.start > fnNode.start &&
|
||||
// If there is no matching enable, the rest of the file has potential violations
|
||||
(suppressionRange.enableComment === null ||
|
||||
(suppressionRange.enableComment.end != null &&
|
||||
suppressionRange.enableComment.end < fnNode.end))
|
||||
(enableComment === null ||
|
||||
(enableComment.end != null && enableComment.end < fnNode.end))
|
||||
) {
|
||||
suppressionsInScope.push(suppressionRange);
|
||||
}
|
||||
|
||||
// The suppression wraps the function
|
||||
if (
|
||||
suppressionRange.disableComment.start < fnNode.start &&
|
||||
disableComment.start < fnNode.start &&
|
||||
// If there is no matching enable, the rest of the file has potential violations
|
||||
(suppressionRange.enableComment === null ||
|
||||
(suppressionRange.enableComment.end != null &&
|
||||
suppressionRange.enableComment.end > fnNode.end))
|
||||
(enableComment === null ||
|
||||
(enableComment.end != null && enableComment.end > fnNode.end))
|
||||
) {
|
||||
suppressionsInScope.push(suppressionRange);
|
||||
}
|
||||
@@ -82,9 +101,7 @@ export function findProgramSuppressions(
|
||||
flowSuppressions: boolean,
|
||||
): Array<SuppressionRange> {
|
||||
const suppressionRanges: Array<SuppressionRange> = [];
|
||||
let disableComment: t.Comment | null = null;
|
||||
let enableComment: t.Comment | null = null;
|
||||
let source: SuppressionSource | null = null;
|
||||
let suppression: SuppressionRange | null = null;
|
||||
|
||||
const rulePattern = `(${ruleNames.join('|')})`;
|
||||
const disableNextLinePattern = new RegExp(
|
||||
@@ -106,42 +123,49 @@ export function findProgramSuppressions(
|
||||
* If we're already within a CommentBlock, we should not restart the range prematurely for a
|
||||
* CommentLine within the block.
|
||||
*/
|
||||
disableComment == null &&
|
||||
suppression == null &&
|
||||
disableNextLinePattern.test(comment.value)
|
||||
) {
|
||||
disableComment = comment;
|
||||
enableComment = comment;
|
||||
source = 'Eslint';
|
||||
suppression = {
|
||||
kind: 'single-line',
|
||||
comment,
|
||||
source: 'Eslint',
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
flowSuppressions &&
|
||||
disableComment == null &&
|
||||
suppression == null &&
|
||||
flowSuppressionPattern.test(comment.value)
|
||||
) {
|
||||
disableComment = comment;
|
||||
enableComment = comment;
|
||||
source = 'Flow';
|
||||
suppression = {
|
||||
kind: 'single-line',
|
||||
comment,
|
||||
source: 'Flow',
|
||||
};
|
||||
}
|
||||
|
||||
if (disablePattern.test(comment.value)) {
|
||||
disableComment = comment;
|
||||
source = 'Eslint';
|
||||
suppression = {
|
||||
kind: 'multi-line',
|
||||
disableComment: comment,
|
||||
enableComment: null,
|
||||
source: 'Eslint',
|
||||
};
|
||||
}
|
||||
|
||||
if (enablePattern.test(comment.value) && source === 'Eslint') {
|
||||
enableComment = comment;
|
||||
if (
|
||||
enablePattern.test(comment.value) &&
|
||||
suppression != null &&
|
||||
suppression.kind === 'multi-line' &&
|
||||
suppression.source === 'Eslint'
|
||||
) {
|
||||
suppression.enableComment = comment;
|
||||
}
|
||||
|
||||
if (disableComment != null && source != null) {
|
||||
suppressionRanges.push({
|
||||
disableComment: disableComment,
|
||||
enableComment: enableComment,
|
||||
source,
|
||||
});
|
||||
disableComment = null;
|
||||
enableComment = null;
|
||||
source = null;
|
||||
if (suppression != null) {
|
||||
suppressionRanges.push(suppression);
|
||||
suppression = null;
|
||||
}
|
||||
}
|
||||
return suppressionRanges;
|
||||
@@ -156,10 +180,11 @@ export function suppressionsToCompilerError(
|
||||
});
|
||||
const error = new CompilerError();
|
||||
for (const suppressionRange of suppressionRanges) {
|
||||
if (
|
||||
suppressionRange.disableComment.start == null ||
|
||||
suppressionRange.disableComment.end == null
|
||||
) {
|
||||
const disableComment =
|
||||
suppressionRange.kind === 'single-line'
|
||||
? suppressionRange.comment
|
||||
: suppressionRange.disableComment;
|
||||
if (disableComment.start == null || disableComment.end == null) {
|
||||
continue;
|
||||
}
|
||||
let reason, suggestion;
|
||||
@@ -183,22 +208,20 @@ export function suppressionsToCompilerError(
|
||||
}
|
||||
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()}\``,
|
||||
reason: reason,
|
||||
description: `React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression \`${disableComment.value.trim()}\``,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: ErrorCategory.Suppression,
|
||||
suggestions: [
|
||||
{
|
||||
description: suggestion,
|
||||
range: [
|
||||
suppressionRange.disableComment.start,
|
||||
suppressionRange.disableComment.end,
|
||||
],
|
||||
range: [disableComment.start, disableComment.end],
|
||||
op: CompilerSuggestionOperation.Remove,
|
||||
},
|
||||
],
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: suppressionRange.disableComment.loc ?? null,
|
||||
loc: disableComment.loc ?? null,
|
||||
message: 'Found React rule suppression',
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -13,7 +13,11 @@ import {getOrInsertWith} from '../Utils/utils';
|
||||
import {Environment, GeneratedSource} from '../HIR';
|
||||
import {DEFAULT_EXPORT} from '../HIR/Environment';
|
||||
import {CompileProgramMetadata} from './Program';
|
||||
import {CompilerDiagnostic, CompilerDiagnosticOptions} from '../CompilerError';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerDiagnosticOptions,
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
|
||||
function throwInvalidReact(
|
||||
options: Omit<CompilerDiagnosticOptions, 'severity'>,
|
||||
@@ -92,7 +96,8 @@ function assertValidEffectImportReference(
|
||||
*/
|
||||
throwInvalidReact(
|
||||
{
|
||||
category:
|
||||
category: ErrorCategory.AutomaticEffectDependencies,
|
||||
reason:
|
||||
'Cannot infer dependencies of this effect. This will break your build!',
|
||||
description:
|
||||
'To resolve, either pass a dependency array or fix reported compiler bailout diagnostics.' +
|
||||
@@ -123,8 +128,8 @@ function assertValidFireImportReference(
|
||||
);
|
||||
throwInvalidReact(
|
||||
{
|
||||
category:
|
||||
'[Fire] Untransformed reference to compiler-required feature.',
|
||||
category: ErrorCategory.Fire,
|
||||
reason: '[Fire] Untransformed reference to compiler-required feature.',
|
||||
description:
|
||||
'Either remove this `fire` call or ensure it is successfully transformed by the compiler' +
|
||||
maybeErrorDiagnostic
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
CompilerSuggestionOperation,
|
||||
ErrorCategory,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
@@ -50,6 +51,11 @@ import {
|
||||
} from './HIR';
|
||||
import HIRBuilder, {Bindings, createTemporaryPlace} from './HIRBuilder';
|
||||
import {BuiltInArrayId} from './ObjectShape';
|
||||
import {
|
||||
filterSuppressionsThatAffectNode,
|
||||
SingleLineSuppressionRange,
|
||||
suppressionsToCompilerError,
|
||||
} from '../Entrypoint';
|
||||
|
||||
/*
|
||||
* *******************************************************************************************
|
||||
@@ -108,7 +114,8 @@ export function lower(
|
||||
builder.errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: 'Could not find binding',
|
||||
category: ErrorCategory.Invariant,
|
||||
reason: 'Could not find binding',
|
||||
description: `[BuildHIR] Could not find binding for param \`${param.node.name}\`.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
@@ -172,7 +179,8 @@ export function lower(
|
||||
builder.errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: `Handle ${param.node.type} parameters`,
|
||||
category: ErrorCategory.Todo,
|
||||
reason: `Handle ${param.node.type} parameters`,
|
||||
description: `[BuildHIR] Add support for ${param.node.type} parameters.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
@@ -203,7 +211,8 @@ export function lower(
|
||||
builder.errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: `Unexpected function body kind`,
|
||||
category: ErrorCategory.Syntax,
|
||||
reason: `Unexpected function body kind`,
|
||||
description: `Expected function body to be an expression or a block statement, got \`${body.type}\`.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
@@ -233,6 +242,42 @@ export function lower(
|
||||
null,
|
||||
);
|
||||
|
||||
if (bindings == null) {
|
||||
/**
|
||||
* Any single-line suppressions which didn't get captured by a call expression
|
||||
* are thrown as errors from the outermost function being compiled. This is to
|
||||
* allow suppressions within function expressions that are passed to useEffect,
|
||||
* eg
|
||||
*
|
||||
* ```
|
||||
* useEffect(() => {
|
||||
* console.log(foo);
|
||||
* // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
* }, []);
|
||||
* ```
|
||||
*
|
||||
* Where we can't throw an error when exiting the function expression, but rather
|
||||
* want that suppression to bubble up to the useEffect() call node.
|
||||
*
|
||||
* Whereas the following should error since it's at the top-level
|
||||
*
|
||||
* ```
|
||||
* function Component() {
|
||||
* const f = () => {
|
||||
* // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
* };
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
const suppressions = filterSuppressionsThatAffectNode(
|
||||
env.suppressions,
|
||||
func,
|
||||
);
|
||||
if (suppressions.length !== 0) {
|
||||
throw suppressionsToCompilerError(suppressions);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok({
|
||||
id,
|
||||
params,
|
||||
@@ -273,6 +318,7 @@ function lowerStatement(
|
||||
reason:
|
||||
'(BuildHIR::lowerStatement) Support ThrowStatement inside of try/catch',
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -460,6 +506,7 @@ function lowerStatement(
|
||||
} else if (!binding.path.isVariableDeclarator()) {
|
||||
builder.errors.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
reason: 'Unsupported declaration type for hoisting',
|
||||
description: `variable "${binding.identifier.name}" declared with ${binding.path.type}`,
|
||||
suggestions: null,
|
||||
@@ -469,6 +516,7 @@ function lowerStatement(
|
||||
} else {
|
||||
builder.errors.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
reason: 'Handle non-const declarations for hoisting',
|
||||
description: `variable "${binding.identifier.name}" declared with ${binding.kind}`,
|
||||
suggestions: null,
|
||||
@@ -549,6 +597,7 @@ function lowerStatement(
|
||||
reason:
|
||||
'(BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement',
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -621,6 +670,7 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerStatement) Handle empty test in ForStatement`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -772,6 +822,7 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason: `Expected at most one \`default\` branch in a switch statement, this code should have failed to parse`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: case_.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -844,6 +895,7 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerStatement) Handle ${nodeKind} kinds in VariableDeclaration`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -872,6 +924,7 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Could not find binding for declaration.`,
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: ErrorCategory.Invariant,
|
||||
loc: id.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -889,6 +942,7 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason: `Expect \`const\` declaration not to be reassigned`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: id.node.loc ?? null,
|
||||
suggestions: [
|
||||
{
|
||||
@@ -936,6 +990,7 @@ function lowerStatement(
|
||||
reason: `Expected variable declaration to be an identifier if no initializer was provided`,
|
||||
description: `Got a \`${id.type}\``,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1044,6 +1099,7 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerStatement) Handle for-await loops`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1276,6 +1332,7 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerStatement) Handle TryStatement without a catch clause`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1285,6 +1342,7 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerStatement) Handle TryStatement with a finalizer ('finally') clause`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1378,6 +1436,7 @@ function lowerStatement(
|
||||
reason: `JavaScript 'with' syntax is not supported`,
|
||||
description: `'with' syntax is considered deprecated and removed from JavaScript standards, consider alternatives`,
|
||||
severity: ErrorSeverity.UnsupportedJS,
|
||||
category: ErrorCategory.UnsupportedSyntax,
|
||||
loc: stmtPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1398,6 +1457,7 @@ function lowerStatement(
|
||||
reason: 'Inline `class` declarations are not supported',
|
||||
description: `Move class declarations outside of components/hooks`,
|
||||
severity: ErrorSeverity.UnsupportedJS,
|
||||
category: ErrorCategory.UnsupportedSyntax,
|
||||
loc: stmtPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1427,6 +1487,7 @@ function lowerStatement(
|
||||
reason:
|
||||
'JavaScript `import` and `export` statements may only appear at the top level of a module',
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: stmtPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1442,6 +1503,7 @@ function lowerStatement(
|
||||
reason:
|
||||
'TypeScript `namespace` statements may only appear at the top level of a module',
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: stmtPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1520,6 +1582,7 @@ function lowerObjectPropertyKey(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: key.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1545,6 +1608,7 @@ function lowerObjectPropertyKey(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: key.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1602,6 +1666,7 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${valuePath.type} values in ObjectExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: valuePath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1628,6 +1693,7 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${propertyPath.node.kind} functions in ObjectExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: propertyPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1649,6 +1715,7 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${propertyPath.type} properties in ObjectExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: propertyPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1682,6 +1749,7 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${element.type} elements in ArrayExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: element.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1702,6 +1770,7 @@ function lowerExpression(
|
||||
reason: `Expected an expression as the \`new\` expression receiver (v8 intrinsics are not supported)`,
|
||||
description: `Got a \`${calleePath.node.type}\``,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: calleePath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1728,6 +1797,7 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `Expected Expression, got ${calleePath.type} in CallExpression (v8 intrinsics not supported). This error is likely caused by a bug in React Compiler. Please file an issue`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: calleePath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1737,21 +1807,27 @@ function lowerExpression(
|
||||
const memberExpr = lowerMemberExpression(builder, calleePath);
|
||||
const propertyPlace = lowerValueToTemporary(builder, memberExpr.value);
|
||||
const args = lowerArguments(builder, expr.get('arguments'));
|
||||
const suppressions = consumeSuppressionOnNode(builder, expr);
|
||||
|
||||
return {
|
||||
kind: 'MethodCall',
|
||||
receiver: memberExpr.object,
|
||||
property: {...propertyPlace},
|
||||
args,
|
||||
loc: exprLoc,
|
||||
suppressions,
|
||||
};
|
||||
} else {
|
||||
const callee = lowerExpressionToTemporary(builder, calleePath);
|
||||
const args = lowerArguments(builder, expr.get('arguments'));
|
||||
const suppressions = consumeSuppressionOnNode(builder, expr);
|
||||
|
||||
return {
|
||||
kind: 'CallExpression',
|
||||
callee,
|
||||
args,
|
||||
loc: exprLoc,
|
||||
suppressions,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1762,6 +1838,7 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Expected Expression, got ${leftPath.type} lval in BinaryExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: leftPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1774,6 +1851,7 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Pipe operator not supported`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: leftPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1803,6 +1881,7 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `Expected sequence expression to have at least one expression`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: expr.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2015,6 +2094,7 @@ function lowerExpression(
|
||||
reason: `(BuildHIR::lowerExpression) Unsupported syntax on the left side of an AssignmentExpression`,
|
||||
description: `Expected an LVal, got: ${left.type}`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: left.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2043,6 +2123,7 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${operator} operators in AssignmentExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: expr.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2142,6 +2223,7 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Expected Identifier or MemberExpression, got ${expr.type} lval in AssignmentExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: expr.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2181,6 +2263,7 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${attribute.type} attributes in JSXElement`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: attribute.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2194,6 +2277,7 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Unexpected colon in attribute name \`${propName}\``,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: namePath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2224,6 +2308,7 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${valueExpr.type} attribute values in JSXElement`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: valueExpr.node?.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2234,6 +2319,7 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${expression.type} expressions in JSXExpressionContainer within JSXElement`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: valueExpr.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2291,7 +2377,8 @@ function lowerExpression(
|
||||
if (locations.length > 1) {
|
||||
CompilerError.throwDiagnostic({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: 'Support duplicate fbt tags',
|
||||
category: ErrorCategory.FBT,
|
||||
reason: 'Support duplicate fbt tags',
|
||||
description: `Support \`<${tagName}>\` tags with multiple \`<${tagName}:${name}>\` values`,
|
||||
details: locations.map(loc => {
|
||||
return {
|
||||
@@ -2352,6 +2439,7 @@ function lowerExpression(
|
||||
reason:
|
||||
'(BuildHIR::lowerExpression) Handle tagged template with interpolations',
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2370,6 +2458,7 @@ function lowerExpression(
|
||||
reason:
|
||||
'(BuildHIR::lowerExpression) Handle tagged template where cooked value is different from raw value',
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2392,6 +2481,7 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `Unexpected quasi and subexpression lengths in template literal`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2402,6 +2492,7 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Handle TSType in TemplateLiteral.`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2444,6 +2535,7 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `Only object properties can be deleted`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: expr.node.loc ?? null,
|
||||
suggestions: [
|
||||
{
|
||||
@@ -2459,6 +2551,7 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `Throw expressions are not supported`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: expr.node.loc ?? null,
|
||||
suggestions: [
|
||||
{
|
||||
@@ -2580,6 +2673,7 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle UpdateExpression with ${argument.type} argument`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2588,6 +2682,7 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle UpdateExpression to variables captured within lambdas.`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2608,6 +2703,7 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Found an invalid UpdateExpression without a previously reported error`,
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: ErrorCategory.Invariant,
|
||||
loc: exprLoc,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2617,6 +2713,7 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Support UpdateExpression where argument is a global`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprLoc,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2672,6 +2769,7 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle MetaProperty expressions other than import.meta`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2681,6 +2779,7 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${exprPath.type} expressions`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2904,6 +3003,7 @@ function lowerOptionalCallExpression(
|
||||
builder.enterReserved(consequent, () => {
|
||||
const args = lowerArguments(builder, expr.get('arguments'));
|
||||
const temp = buildTemporaryPlace(builder, loc);
|
||||
const suppressions = consumeSuppressionOnNode(builder, expr);
|
||||
if (callee.kind === 'CallExpression') {
|
||||
builder.push({
|
||||
id: makeInstructionId(0),
|
||||
@@ -2913,6 +3013,7 @@ function lowerOptionalCallExpression(
|
||||
callee: {...callee.callee},
|
||||
args,
|
||||
loc,
|
||||
suppressions,
|
||||
},
|
||||
effects: null,
|
||||
loc,
|
||||
@@ -2927,6 +3028,7 @@ function lowerOptionalCallExpression(
|
||||
property: {...callee.property},
|
||||
args,
|
||||
loc,
|
||||
suppressions,
|
||||
},
|
||||
effects: null,
|
||||
loc,
|
||||
@@ -2978,6 +3080,7 @@ function lowerReorderableExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::node.lowerReorderableExpression) Expression type \`${expr.type}\` cannot be safely reordered`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: expr.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3174,6 +3277,7 @@ function lowerArguments(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${argPath.type} arguments in CallExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: argPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3209,6 +3313,7 @@ function lowerMemberExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerMemberExpression) Handle ${propertyNode.type} property`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: propertyNode.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3230,6 +3335,7 @@ function lowerMemberExpression(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerMemberExpression) Expected Expression, got ${propertyNode.type} property`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: propertyNode.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3289,6 +3395,7 @@ function lowerJsxElementName(
|
||||
reason: `Expected JSXNamespacedName to have no colons in the namespace or name`,
|
||||
description: `Got \`${namespace}\` : \`${name}\``,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3303,6 +3410,7 @@ function lowerJsxElementName(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerJsxElementName) Handle ${exprPath.type} tags`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3401,6 +3509,7 @@ function lowerJsxElement(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerJsxElement) Unhandled JsxElement, got: ${exprPath.type}`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3588,6 +3697,7 @@ function lowerIdentifier(
|
||||
description:
|
||||
'Eval is an anti-pattern in JavaScript, and the code executed cannot be evaluated by React Compiler',
|
||||
severity: ErrorSeverity.UnsupportedJS,
|
||||
category: ErrorCategory.UnsupportedSyntax,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3644,6 +3754,7 @@ function lowerIdentifierForAssignment(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Could not find binding for declaration.`,
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: ErrorCategory.Invariant,
|
||||
loc: path.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3656,6 +3767,7 @@ function lowerIdentifierForAssignment(
|
||||
builder.errors.push({
|
||||
reason: `Cannot reassign a \`const\` variable`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: path.node.loc ?? null,
|
||||
description:
|
||||
binding.identifier.name != null
|
||||
@@ -3713,6 +3825,7 @@ function lowerAssignment(
|
||||
builder.errors.push({
|
||||
reason: `Expected \`const\` declaration not to be reassigned`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: lvalue.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3727,6 +3840,7 @@ function lowerAssignment(
|
||||
builder.errors.push({
|
||||
reason: `Unexpected context variable kind`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: lvalue.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3798,6 +3912,7 @@ function lowerAssignment(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Handle ${property.type} properties in MemberExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: property.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3810,6 +3925,7 @@ function lowerAssignment(
|
||||
reason:
|
||||
'(BuildHIR::lowerAssignment) Expected private name to appear as a non-computed property',
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: property.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -3875,6 +3991,7 @@ function lowerAssignment(
|
||||
} else if (identifier.kind === 'Global') {
|
||||
builder.errors.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
reason:
|
||||
'Expected reassignment of globals to enable forceTemporaries',
|
||||
loc: element.node.loc ?? GeneratedSource,
|
||||
@@ -3914,6 +4031,7 @@ function lowerAssignment(
|
||||
} else if (identifier.kind === 'Global') {
|
||||
builder.errors.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
reason:
|
||||
'Expected reassignment of globals to enable forceTemporaries',
|
||||
loc: element.node.loc ?? GeneratedSource,
|
||||
@@ -3987,6 +4105,7 @@ function lowerAssignment(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Handle ${argument.node.type} rest element in ObjectPattern`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: argument.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -4018,6 +4137,7 @@ function lowerAssignment(
|
||||
} else if (identifier.kind === 'Global') {
|
||||
builder.errors.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
reason:
|
||||
'Expected reassignment of globals to enable forceTemporaries',
|
||||
loc: property.node.loc ?? GeneratedSource,
|
||||
@@ -4035,6 +4155,7 @@ function lowerAssignment(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Handle ${property.type} properties in ObjectPattern`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: property.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -4044,6 +4165,7 @@ function lowerAssignment(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: property.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -4058,6 +4180,7 @@ function lowerAssignment(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Expected object property value to be an LVal, got: ${element.type}`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: element.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -4080,6 +4203,7 @@ function lowerAssignment(
|
||||
} else if (identifier.kind === 'Global') {
|
||||
builder.errors.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
reason:
|
||||
'Expected reassignment of globals to enable forceTemporaries',
|
||||
loc: element.node.loc ?? GeneratedSource,
|
||||
@@ -4229,6 +4353,7 @@ function lowerAssignment(
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Handle ${lvaluePath.type} assignments`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: lvaluePath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -4402,3 +4527,23 @@ export function lowerType(node: t.FlowType | t.TSType): Type {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the (single-line) suppression comments from the environment that are scoped
|
||||
* to within the given `node`, removing them from the environment's suppressions list.
|
||||
*
|
||||
* By calling this function depth-first, we can associate suppressions with the innermost
|
||||
* call expression that they effect. Unconsumed suppressions are thrown at the parent
|
||||
* function boundary.
|
||||
*/
|
||||
function consumeSuppressionOnNode(
|
||||
builder: HIRBuilder,
|
||||
node: NodePath,
|
||||
): Array<SingleLineSuppressionRange> {
|
||||
const env = builder.environment;
|
||||
const suppressions = filterSuppressionsThatAffectNode(env.suppressions, node);
|
||||
env.suppressions = env.suppressions.filter(
|
||||
s => suppressions.indexOf(s) === -1,
|
||||
);
|
||||
return suppressions;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {Effect, ValueKind} from '..';
|
||||
import {TypeConfig} from './TypeSchema';
|
||||
|
||||
/**
|
||||
* Libraries developed before we officially documented the [Rules of React](https://react.dev/reference/rules)
|
||||
* implement APIs which cannot be memoized safely, either via manual or automatic memoization.
|
||||
*
|
||||
* Any non-hook API that is designed to be called during render (not events/effects) should be safe to memoize:
|
||||
*
|
||||
* ```js
|
||||
* function Component() {
|
||||
* const {someFunction} = useLibrary();
|
||||
* // it should always be safe to memoize functions like this
|
||||
* const result = useMemo(() => someFunction(), [someFunction]);
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* However, some APIs implement "interior mutability" — mutating values rather than copying into a new value
|
||||
* and setting state with the new value. Such functions (`someFunction()` in the example) could return different
|
||||
* values even though the function itself is the same object. This breaks memoization, since React relies on
|
||||
* the outer object (or function) changing if part of its value has changed.
|
||||
*
|
||||
* Given that we didn't have the Rules of React precisely documented prior to the introduction of React compiler,
|
||||
* it's understandable that some libraries accidentally shipped APIs that break this rule. However, developers
|
||||
* can easily run into pitfalls with these APIs. They may manually memoize them, which can break their app. Or
|
||||
* they may try using React Compiler, and think that the compiler has broken their code.
|
||||
*
|
||||
* To help ensure that developers can successfully use the compiler with existing code, this file teaches the
|
||||
* compiler about specific APIs that are known to be incompatible with memoization. We've tried to be as precise
|
||||
* as possible.
|
||||
*
|
||||
* The React team is open to collaborating with library authors to help develop compatible versions of these APIs,
|
||||
* and we have already reached out to the teams who own any API listed here to ensure they are aware of the issue.
|
||||
*/
|
||||
export function defaultModuleTypeProvider(
|
||||
moduleName: string,
|
||||
): TypeConfig | null {
|
||||
switch (moduleName) {
|
||||
case 'react-hook-form': {
|
||||
return {
|
||||
kind: 'object',
|
||||
properties: {
|
||||
useForm: {
|
||||
kind: 'hook',
|
||||
returnType: {
|
||||
kind: 'object',
|
||||
properties: {
|
||||
// Only the `watch()` function returned by react-hook-form's `useForm()` API is incompatible
|
||||
watch: {
|
||||
kind: 'function',
|
||||
positionalParams: [],
|
||||
restParam: Effect.Read,
|
||||
calleeEffect: Effect.Read,
|
||||
returnType: {kind: 'type', name: 'Any'},
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
knownIncompatible: `React Hook Form's \`useForm()\` API returns a \`watch()\` function which cannot be memoized safely.`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case '@tanstack/react-table': {
|
||||
return {
|
||||
kind: 'object',
|
||||
properties: {
|
||||
/*
|
||||
* Many of the properties of `useReactTable()`'s return value are incompatible, so we mark the entire hook
|
||||
* as incompatible
|
||||
*/
|
||||
useReactTable: {
|
||||
kind: 'hook',
|
||||
positionalParams: [],
|
||||
restParam: Effect.Read,
|
||||
returnType: {kind: 'type', name: 'Any'},
|
||||
knownIncompatible: `TanStack Table's \`useReactTable()\` API returns functions that cannot be memoized safely`,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -9,7 +9,11 @@ import * as t from '@babel/types';
|
||||
import {ZodError, z} from 'zod';
|
||||
import {fromZodError} from 'zod-validation-error';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {Logger, ProgramContext} from '../Entrypoint';
|
||||
import {
|
||||
Logger,
|
||||
ProgramContext,
|
||||
SingleLineSuppressionRange,
|
||||
} from '../Entrypoint';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {
|
||||
DEFAULT_GLOBALS,
|
||||
@@ -50,6 +54,7 @@ import {
|
||||
import {Scope as BabelScope, NodePath} from '@babel/traverse';
|
||||
import {TypeSchema} from './TypeSchema';
|
||||
import {FlowTypeEnv} from '../Flood/Types';
|
||||
import {defaultModuleTypeProvider} from './DefaultModuleTypeProvider';
|
||||
|
||||
export const ReactElementSymbolSchema = z.object({
|
||||
elementSymbol: z.union([
|
||||
@@ -250,11 +255,6 @@ export const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
flowTypeProvider: z.nullable(z.function().args(z.string())).default(null),
|
||||
|
||||
/**
|
||||
* Enable a new model for mutability and aliasing inference
|
||||
*/
|
||||
enableNewMutationAliasingModel: z.boolean().default(true),
|
||||
|
||||
/**
|
||||
* Enables inference of optional dependency chains. Without this flag
|
||||
* a property chain such as `props?.items?.foo` will infer as a dep on
|
||||
@@ -655,6 +655,13 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* useMemo(() => { ... }, [...]);
|
||||
*/
|
||||
validateNoVoidUseMemo: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Validates that Components/Hooks are always defined at module level. This prevents scope
|
||||
* reference errors that occur when the compiler attempts to optimize the nested component/hook
|
||||
* while its parent function remains uncompiled.
|
||||
*/
|
||||
validateNoDynamicallyCreatedComponentsOrHooks: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
|
||||
@@ -699,6 +706,7 @@ export class Environment {
|
||||
hasFireRewrite: boolean;
|
||||
hasInferredEffect: boolean;
|
||||
inferredEffectLocations: Set<SourceLocation> = new Set();
|
||||
suppressions: Array<SingleLineSuppressionRange>;
|
||||
|
||||
#contextIdentifiers: Set<t.Identifier>;
|
||||
#hoistedIdentifiers: Set<t.Identifier>;
|
||||
@@ -717,6 +725,7 @@ export class Environment {
|
||||
filename: string | null,
|
||||
code: string | null,
|
||||
programContext: ProgramContext,
|
||||
suppressions: Array<SingleLineSuppressionRange>,
|
||||
) {
|
||||
this.#scope = scope;
|
||||
this.fnType = fnType;
|
||||
@@ -730,6 +739,7 @@ export class Environment {
|
||||
this.#globals = new Map(DEFAULT_GLOBALS);
|
||||
this.hasFireRewrite = false;
|
||||
this.hasInferredEffect = false;
|
||||
this.suppressions = suppressions;
|
||||
|
||||
if (
|
||||
config.disableMemoizationForDebugging &&
|
||||
@@ -858,10 +868,16 @@ export class Environment {
|
||||
#resolveModuleType(moduleName: string, loc: SourceLocation): Global | null {
|
||||
let moduleType = this.#moduleTypes.get(moduleName);
|
||||
if (moduleType === undefined) {
|
||||
if (this.config.moduleTypeProvider == null) {
|
||||
/*
|
||||
* NOTE: Zod doesn't work when specifying a function as a default, so we have to
|
||||
* fallback to the default value here
|
||||
*/
|
||||
const moduleTypeProvider =
|
||||
this.config.moduleTypeProvider ?? defaultModuleTypeProvider;
|
||||
if (moduleTypeProvider == null) {
|
||||
return null;
|
||||
}
|
||||
const unparsedModuleConfig = this.config.moduleTypeProvider(moduleName);
|
||||
const unparsedModuleConfig = moduleTypeProvider(moduleName);
|
||||
if (unparsedModuleConfig != null) {
|
||||
const parsedModuleConfig = TypeSchema.safeParse(unparsedModuleConfig);
|
||||
if (!parsedModuleConfig.success) {
|
||||
|
||||
@@ -1001,6 +1001,7 @@ export function installTypeConfig(
|
||||
mutableOnlyIfOperandsAreMutable:
|
||||
typeConfig.mutableOnlyIfOperandsAreMutable === true,
|
||||
aliasing: typeConfig.aliasing,
|
||||
knownIncompatible: typeConfig.knownIncompatible ?? null,
|
||||
});
|
||||
}
|
||||
case 'hook': {
|
||||
@@ -1019,6 +1020,7 @@ export function installTypeConfig(
|
||||
returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen,
|
||||
noAlias: typeConfig.noAlias === true,
|
||||
aliasing: typeConfig.aliasing,
|
||||
knownIncompatible: typeConfig.knownIncompatible ?? null,
|
||||
});
|
||||
}
|
||||
case 'object': {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import {BindingKind} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {Environment, ReactFunctionType} from './Environment';
|
||||
import type {HookKind} from './ObjectShape';
|
||||
@@ -15,6 +15,7 @@ import {Type, makeType} from './Types';
|
||||
import {z} from 'zod';
|
||||
import type {AliasingEffect} from '../Inference/AliasingEffects';
|
||||
import {isReservedWord} from '../Utils/Keyword';
|
||||
import {SingleLineSuppressionRange} from '../Entrypoint';
|
||||
|
||||
/*
|
||||
* *******************************************************************************************
|
||||
@@ -282,30 +283,13 @@ export type HIRFunction = {
|
||||
returnTypeAnnotation: t.FlowType | t.TSType | null;
|
||||
returns: Place;
|
||||
context: Array<Place>;
|
||||
effects: Array<FunctionEffect> | null;
|
||||
body: HIR;
|
||||
generator: boolean;
|
||||
async: boolean;
|
||||
directives: Array<string>;
|
||||
aliasingEffects?: Array<AliasingEffect> | null;
|
||||
aliasingEffects: Array<AliasingEffect> | null;
|
||||
};
|
||||
|
||||
export type FunctionEffect =
|
||||
| {
|
||||
kind: 'GlobalMutation';
|
||||
error: CompilerErrorDetailOptions;
|
||||
}
|
||||
| {
|
||||
kind: 'ReactMutation';
|
||||
error: CompilerErrorDetailOptions;
|
||||
}
|
||||
| {
|
||||
kind: 'ContextMutation';
|
||||
places: ReadonlySet<Place>;
|
||||
effect: Effect;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
|
||||
/*
|
||||
* Each reactive scope may have its own control-flow, so the instructions form
|
||||
* a control-flow graph. The graph comprises a set of basic blocks which reference
|
||||
@@ -860,6 +844,7 @@ export type MethodCall = {
|
||||
property: Place;
|
||||
args: Array<Place | SpreadPattern>;
|
||||
loc: SourceLocation;
|
||||
suppressions?: Array<SingleLineSuppressionRange>;
|
||||
};
|
||||
|
||||
export type CallExpression = {
|
||||
@@ -868,6 +853,7 @@ export type CallExpression = {
|
||||
args: Array<Place | SpreadPattern>;
|
||||
loc: SourceLocation;
|
||||
typeArguments?: Array<t.FlowType>;
|
||||
suppressions?: Array<SingleLineSuppressionRange>;
|
||||
};
|
||||
|
||||
export type NewExpression = {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import {Binding, NodePath} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import {CompilerError, ErrorSeverity} from '../CompilerError';
|
||||
import {CompilerError, ErrorCategory, ErrorSeverity} from '../CompilerError';
|
||||
import {Environment} from './Environment';
|
||||
import {
|
||||
BasicBlock,
|
||||
@@ -310,7 +310,8 @@ export default class HIRBuilder {
|
||||
if (node.name === 'fbt') {
|
||||
CompilerError.throwDiagnostic({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: 'Support local variables named `fbt`',
|
||||
category: ErrorCategory.FBT,
|
||||
reason: 'Support local variables named `fbt`',
|
||||
description:
|
||||
'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',
|
||||
details: [
|
||||
@@ -322,6 +323,22 @@ export default class HIRBuilder {
|
||||
],
|
||||
});
|
||||
}
|
||||
if (node.name === 'this') {
|
||||
CompilerError.throwDiagnostic({
|
||||
severity: ErrorSeverity.UnsupportedJS,
|
||||
category: ErrorCategory.UnsupportedSyntax,
|
||||
reason: '`this` is not supported syntax',
|
||||
description:
|
||||
'React Compiler does not support compiling functions that use `this`',
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
message: '`this` was used here',
|
||||
loc: node.loc ?? GeneratedSource,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
const originalName = node.name;
|
||||
let name = originalName;
|
||||
let index = 0;
|
||||
|
||||
@@ -332,6 +332,7 @@ export type FunctionSignature = {
|
||||
mutableOnlyIfOperandsAreMutable?: boolean;
|
||||
|
||||
impure?: boolean;
|
||||
knownIncompatible?: string | null | undefined;
|
||||
|
||||
canonicalName?: string;
|
||||
|
||||
|
||||
@@ -554,23 +554,11 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
|
||||
const context = instrValue.loweredFunc.func.context
|
||||
.map(dep => printPlace(dep))
|
||||
.join(',');
|
||||
const effects =
|
||||
instrValue.loweredFunc.func.effects
|
||||
?.map(effect => {
|
||||
if (effect.kind === 'ContextMutation') {
|
||||
return `ContextMutation places=[${[...effect.places]
|
||||
.map(place => printPlace(place))
|
||||
.join(', ')}] effect=${effect.effect}`;
|
||||
} else {
|
||||
return `GlobalMutation`;
|
||||
}
|
||||
})
|
||||
.join(', ') ?? '';
|
||||
const aliasingEffects =
|
||||
instrValue.loweredFunc.func.aliasingEffects
|
||||
?.map(printAliasingEffect)
|
||||
?.join(', ') ?? '';
|
||||
value = `${kind} ${name} @context[${context}] @effects[${effects}] @aliasingEffects=[${aliasingEffects}]\n${fn}`;
|
||||
value = `${kind} ${name} @context[${context}] @aliasingEffects=[${aliasingEffects}]\n${fn}`;
|
||||
break;
|
||||
}
|
||||
case 'TaggedTemplateExpression': {
|
||||
@@ -892,7 +880,8 @@ export function printType(type: Type): string {
|
||||
if (type.kind === 'Object' && type.shapeId != null) {
|
||||
return `:T${type.kind}<${type.shapeId}>`;
|
||||
} else if (type.kind === 'Function' && type.shapeId != null) {
|
||||
return `:T${type.kind}<${type.shapeId}>`;
|
||||
const returnType = printType(type.return);
|
||||
return `:T${type.kind}<${type.shapeId}>()${returnType !== '' ? `: ${returnType}` : ''}`;
|
||||
} else {
|
||||
return `:T${type.kind}`;
|
||||
}
|
||||
@@ -995,16 +984,16 @@ export function printAliasingEffect(effect: AliasingEffect): string {
|
||||
case 'MutateConditionally':
|
||||
case 'MutateTransitive':
|
||||
case 'MutateTransitiveConditionally': {
|
||||
return `${effect.kind} ${printPlaceForAliasEffect(effect.value)}`;
|
||||
return `${effect.kind} ${printPlaceForAliasEffect(effect.value)}${effect.kind === 'Mutate' && effect.reason?.kind === 'AssignCurrentProperty' ? ' (assign `.current`)' : ''}`;
|
||||
}
|
||||
case 'MutateFrozen': {
|
||||
return `MutateFrozen ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.category)}`;
|
||||
return `MutateFrozen ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
|
||||
}
|
||||
case 'MutateGlobal': {
|
||||
return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.category)}`;
|
||||
return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
|
||||
}
|
||||
case 'Impure': {
|
||||
return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.category)}`;
|
||||
return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
|
||||
}
|
||||
case 'Render': {
|
||||
return `Render ${printPlaceForAliasEffect(effect.place)}`;
|
||||
|
||||
@@ -251,6 +251,7 @@ export type FunctionTypeConfig = {
|
||||
impure?: boolean | null | undefined;
|
||||
canonicalName?: string | null | undefined;
|
||||
aliasing?: AliasingSignatureConfig | null | undefined;
|
||||
knownIncompatible?: string | null | undefined;
|
||||
};
|
||||
export const FunctionTypeSchema: z.ZodType<FunctionTypeConfig> = z.object({
|
||||
kind: z.literal('function'),
|
||||
@@ -264,6 +265,7 @@ export const FunctionTypeSchema: z.ZodType<FunctionTypeConfig> = z.object({
|
||||
impure: z.boolean().nullable().optional(),
|
||||
canonicalName: z.string().nullable().optional(),
|
||||
aliasing: AliasingSignatureSchema.nullable().optional(),
|
||||
knownIncompatible: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export type HookTypeConfig = {
|
||||
@@ -274,6 +276,7 @@ export type HookTypeConfig = {
|
||||
returnValueKind?: ValueKind | null | undefined;
|
||||
noAlias?: boolean | null | undefined;
|
||||
aliasing?: AliasingSignatureConfig | null | undefined;
|
||||
knownIncompatible?: string | null | undefined;
|
||||
};
|
||||
export const HookTypeSchema: z.ZodType<HookTypeConfig> = z.object({
|
||||
kind: z.literal('hook'),
|
||||
@@ -283,6 +286,7 @@ export const HookTypeSchema: z.ZodType<HookTypeConfig> = z.object({
|
||||
returnValueKind: ValueKindSchema.nullable().optional(),
|
||||
noAlias: z.boolean().nullable().optional(),
|
||||
aliasing: AliasingSignatureSchema.nullable().optional(),
|
||||
knownIncompatible: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export type BuiltInTypeConfig =
|
||||
|
||||
@@ -50,7 +50,7 @@ export type AliasingEffect =
|
||||
/**
|
||||
* Mutate the value and any direct aliases (not captures). Errors if the value is not mutable.
|
||||
*/
|
||||
| {kind: 'Mutate'; value: Place}
|
||||
| {kind: 'Mutate'; value: Place; reason?: MutationReason | null}
|
||||
/**
|
||||
* Mutate the value and any direct aliases (not captures), but only if the value is known mutable.
|
||||
* This should be rare.
|
||||
@@ -174,6 +174,8 @@ export type AliasingEffect =
|
||||
place: Place;
|
||||
};
|
||||
|
||||
export type MutationReason = {kind: 'AssignCurrentProperty'};
|
||||
|
||||
export function hashEffect(effect: AliasingEffect): string {
|
||||
switch (effect.kind) {
|
||||
case 'Apply': {
|
||||
@@ -229,7 +231,7 @@ export function hashEffect(effect: AliasingEffect): string {
|
||||
effect.kind,
|
||||
effect.place.identifier.id,
|
||||
effect.error.severity,
|
||||
effect.error.category,
|
||||
effect.error.reason,
|
||||
effect.error.description,
|
||||
printSourceLocation(effect.error.primaryLocation() ?? GeneratedSource),
|
||||
].join(':');
|
||||
|
||||
@@ -6,20 +6,10 @@
|
||||
*/
|
||||
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
LoweredFunction,
|
||||
isRefOrRefValue,
|
||||
makeInstructionId,
|
||||
} from '../HIR';
|
||||
import {Effect, HIRFunction, IdentifierId, makeInstructionId} from '../HIR';
|
||||
import {deadCodeElimination} from '../Optimization';
|
||||
import {inferReactiveScopeVariables} from '../ReactiveScopes';
|
||||
import {rewriteInstructionKindsBasedOnReassignment} from '../SSA';
|
||||
import {inferMutableRanges} from './InferMutableRanges';
|
||||
import inferReferenceEffects from './InferReferenceEffects';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {inferMutationAliasingEffects} from './InferMutationAliasingEffects';
|
||||
import {inferMutationAliasingRanges} from './InferMutationAliasingRanges';
|
||||
@@ -30,12 +20,7 @@ export default function analyseFunctions(func: HIRFunction): void {
|
||||
switch (instr.value.kind) {
|
||||
case 'ObjectMethod':
|
||||
case 'FunctionExpression': {
|
||||
if (!func.env.config.enableNewMutationAliasingModel) {
|
||||
lower(instr.value.loweredFunc.func);
|
||||
infer(instr.value.loweredFunc);
|
||||
} else {
|
||||
lowerWithMutationAliasing(instr.value.loweredFunc.func);
|
||||
}
|
||||
lowerWithMutationAliasing(instr.value.loweredFunc.func);
|
||||
|
||||
/**
|
||||
* Reset mutable range for outer inferReferenceEffects
|
||||
@@ -140,58 +125,3 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
|
||||
value: fn,
|
||||
});
|
||||
}
|
||||
|
||||
function lower(func: HIRFunction): void {
|
||||
analyseFunctions(func);
|
||||
inferReferenceEffects(func, {isFunctionExpression: true});
|
||||
deadCodeElimination(func);
|
||||
inferMutableRanges(func);
|
||||
rewriteInstructionKindsBasedOnReassignment(func);
|
||||
inferReactiveScopeVariables(func);
|
||||
func.env.logger?.debugLogIRs?.({
|
||||
kind: 'hir',
|
||||
name: 'AnalyseFunction (inner)',
|
||||
value: func,
|
||||
});
|
||||
}
|
||||
|
||||
function infer(loweredFunc: LoweredFunction): void {
|
||||
for (const operand of loweredFunc.func.context) {
|
||||
const identifier = operand.identifier;
|
||||
CompilerError.invariant(operand.effect === Effect.Unknown, {
|
||||
reason:
|
||||
'[AnalyseFunctions] Expected Function context effects to not have been set',
|
||||
loc: operand.loc,
|
||||
});
|
||||
if (isRefOrRefValue(identifier)) {
|
||||
/*
|
||||
* TODO: this is a hack to ensure we treat functions which reference refs
|
||||
* as having a capture and therefore being considered mutable. this ensures
|
||||
* the function gets a mutable range which accounts for anywhere that it
|
||||
* could be called, and allows us to help ensure it isn't called during
|
||||
* render
|
||||
*/
|
||||
operand.effect = Effect.Capture;
|
||||
} else if (isMutatedOrReassigned(identifier)) {
|
||||
/**
|
||||
* Reflects direct reassignments, PropertyStores, and ConditionallyMutate
|
||||
* (directly or through maybe-aliases)
|
||||
*/
|
||||
operand.effect = Effect.Capture;
|
||||
} else {
|
||||
operand.effect = Effect.Read;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isMutatedOrReassigned(id: Identifier): boolean {
|
||||
/*
|
||||
* This check checks for mutation and reassingnment, so the usual check for
|
||||
* mutation (ie, `mutableRange.end - mutableRange.start > 1`) isn't quite
|
||||
* enough.
|
||||
*
|
||||
* We need to track re-assignments in context refs as we need to reflect the
|
||||
* re-assignment back to the captured refs.
|
||||
*/
|
||||
return id.mutableRange.end > id.mutableRange.start;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ErrorSeverity,
|
||||
SourceLocation,
|
||||
} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
CallExpression,
|
||||
Effect,
|
||||
@@ -254,6 +255,7 @@ function getManualMemoizationReplacement(
|
||||
*/
|
||||
args: [],
|
||||
loc,
|
||||
suppressions: [],
|
||||
};
|
||||
} else {
|
||||
/*
|
||||
@@ -300,8 +302,9 @@ function extractManualMemoizationArgs(
|
||||
if (fnPlace == null) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: `Expected a callback function to be passed to ${kind}`,
|
||||
reason: `Expected a callback function to be passed to ${kind}`,
|
||||
description: `Expected a callback function to be passed to ${kind}`,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
@@ -315,8 +318,9 @@ function extractManualMemoizationArgs(
|
||||
if (fnPlace.kind === 'Spread' || depsListPlace?.kind === 'Spread') {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: `Unexpected spread argument to ${kind}`,
|
||||
reason: `Unexpected spread argument to ${kind}`,
|
||||
description: `Unexpected spread argument to ${kind}`,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
@@ -335,8 +339,9 @@ function extractManualMemoizationArgs(
|
||||
if (maybeDepsList == null) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
reason: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
description: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
@@ -353,8 +358,9 @@ function extractManualMemoizationArgs(
|
||||
if (maybeDep == null) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
|
||||
reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
|
||||
description: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
@@ -459,7 +465,8 @@ export function dropManualMemoization(
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'useMemo() callbacks must return a value',
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: 'useMemo() callbacks must return a value',
|
||||
description: `This ${
|
||||
manualMemo.loadInstr.value.kind === 'PropertyLoad'
|
||||
? 'React.useMemo'
|
||||
@@ -498,8 +505,9 @@ export function dropManualMemoization(
|
||||
if (!sidemap.functions.has(fnPlace.identifier.id)) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: `Expected the first argument to be an inline function expression`,
|
||||
reason: `Expected the first argument to be an inline function expression`,
|
||||
description: `Expected the first argument to be an inline function expression`,
|
||||
suggestions: [],
|
||||
}).withDetail({
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
isMutableEffect,
|
||||
isRefOrRefLikeMutableType,
|
||||
makeInstructionId,
|
||||
} from '../HIR/HIR';
|
||||
import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
/**
|
||||
* If a function captures a mutable value but never gets called, we don't infer a
|
||||
* mutable range for that function. This means that we also don't alias the function
|
||||
* with its mutable captures.
|
||||
*
|
||||
* This case is tricky, because we don't generally know for sure what is a mutation
|
||||
* and what may just be a normal function call. For example:
|
||||
*
|
||||
* ```
|
||||
* hook useFoo() {
|
||||
* const x = makeObject();
|
||||
* return () => {
|
||||
* return readObject(x); // could be a mutation!
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* If we pessimistically assume that all such cases are mutations, we'd have to group
|
||||
* lots of memo scopes together unnecessarily. However, if there is definitely a mutation:
|
||||
*
|
||||
* ```
|
||||
* hook useFoo(createEntryForKey) {
|
||||
* const cache = new WeakMap();
|
||||
* return (key) => {
|
||||
* let entry = cache.get(key);
|
||||
* if (entry == null) {
|
||||
* entry = createEntryForKey(key);
|
||||
* cache.set(key, entry); // known mutation!
|
||||
* }
|
||||
* return entry;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Then we have to ensure that the function and its mutable captures alias together and
|
||||
* end up in the same scope. However, aliasing together isn't enough if the function
|
||||
* and operands all have empty mutable ranges (end = start + 1).
|
||||
*
|
||||
* This pass finds function expressions and object methods that have an empty mutable range
|
||||
* and known-mutable operands which also don't have a mutable range, and ensures that the
|
||||
* function and those operands are aliased together *and* that their ranges are updated to
|
||||
* end after the function expression. This is sufficient to ensure that a reactive scope is
|
||||
* created for the alias set.
|
||||
*/
|
||||
export function inferAliasForUncalledFunctions(
|
||||
fn: HIRFunction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
instrs: for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
if (
|
||||
value.kind !== 'ObjectMethod' &&
|
||||
value.kind !== 'FunctionExpression'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* If the function is known to be mutated, we will have
|
||||
* already aliased any mutable operands with it
|
||||
*/
|
||||
const range = lvalue.identifier.mutableRange;
|
||||
if (range.end > range.start + 1) {
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* If the function already has operands with an active mutable range,
|
||||
* then we don't need to do anything — the function will have already
|
||||
* been visited and included in some mutable alias set. This case can
|
||||
* also occur due to visiting the same function in an earlier iteration
|
||||
* of the outer fixpoint loop.
|
||||
*/
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
if (isMutable(instr, operand)) {
|
||||
continue instrs;
|
||||
}
|
||||
}
|
||||
const operands: Set<Identifier> = new Set();
|
||||
for (const effect of value.loweredFunc.func.effects ?? []) {
|
||||
if (effect.kind !== 'ContextMutation') {
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* We're looking for known-mutations only, so we look at the effects
|
||||
* rather than function context
|
||||
*/
|
||||
if (effect.effect === Effect.Store || effect.effect === Effect.Mutate) {
|
||||
for (const operand of effect.places) {
|
||||
/*
|
||||
* It's possible that function effect analysis thinks there was a context mutation,
|
||||
* but then InferReferenceEffects figures out some operands are globals and therefore
|
||||
* creates a non-mutable effect for those operands.
|
||||
* We should change InferReferenceEffects to swap the ContextMutation for a global
|
||||
* mutation in that case, but for now we just filter them out here
|
||||
*/
|
||||
if (
|
||||
isMutableEffect(operand.effect, operand.loc) &&
|
||||
!isRefOrRefLikeMutableType(operand.identifier.type)
|
||||
) {
|
||||
operands.add(operand.identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (operands.size !== 0) {
|
||||
operands.add(lvalue.identifier);
|
||||
aliases.union([...operands]);
|
||||
// Update mutable ranges, if the ranges are empty then a reactive scope isn't created
|
||||
for (const operand of operands) {
|
||||
operand.mutableRange.end = makeInstructionId(instr.id + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
Instruction,
|
||||
isPrimitiveType,
|
||||
Place,
|
||||
} from '../HIR/HIR';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
export type AliasSet = Set<Identifier>;
|
||||
|
||||
export function inferAliases(func: HIRFunction): DisjointSet<Identifier> {
|
||||
const aliases = new DisjointSet<Identifier>();
|
||||
for (const [_, block] of func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
inferInstr(instr, aliases);
|
||||
}
|
||||
}
|
||||
|
||||
return aliases;
|
||||
}
|
||||
|
||||
function inferInstr(
|
||||
instr: Instruction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
const {lvalue, value: instrValue} = instr;
|
||||
let alias: Place | null = null;
|
||||
switch (instrValue.kind) {
|
||||
case 'LoadLocal':
|
||||
case 'LoadContext': {
|
||||
if (isPrimitiveType(instrValue.place.identifier)) {
|
||||
return;
|
||||
}
|
||||
alias = instrValue.place;
|
||||
break;
|
||||
}
|
||||
case 'StoreLocal':
|
||||
case 'StoreContext': {
|
||||
alias = instrValue.value;
|
||||
break;
|
||||
}
|
||||
case 'Destructure': {
|
||||
alias = instrValue.value;
|
||||
break;
|
||||
}
|
||||
case 'ComputedLoad':
|
||||
case 'PropertyLoad': {
|
||||
alias = instrValue.object;
|
||||
break;
|
||||
}
|
||||
case 'TypeCastExpression': {
|
||||
alias = instrValue.value;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
aliases.union([lvalue.identifier, alias.identifier]);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {HIRFunction, Identifier} from '../HIR/HIR';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
export function inferAliasForPhis(
|
||||
func: HIRFunction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
for (const [_, block] of func.body.blocks) {
|
||||
for (const phi of block.phis) {
|
||||
const isPhiMutatedAfterCreation: boolean =
|
||||
phi.place.identifier.mutableRange.end >
|
||||
(block.instructions.at(0)?.id ?? block.terminal.id);
|
||||
if (isPhiMutatedAfterCreation) {
|
||||
for (const [, operand] of phi.operands) {
|
||||
aliases.union([phi.place.identifier, operand.identifier]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
InstructionId,
|
||||
Place,
|
||||
} from '../HIR/HIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionValueOperand,
|
||||
} from '../HIR/visitors';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
export function inferAliasForStores(
|
||||
func: HIRFunction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
for (const [_, block] of func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
const {value, lvalue} = instr;
|
||||
const isStore =
|
||||
lvalue.effect === Effect.Store ||
|
||||
/*
|
||||
* Some typed functions annotate callees or arguments
|
||||
* as Effect.Store.
|
||||
*/
|
||||
![...eachInstructionValueOperand(value)].every(
|
||||
operand => operand.effect !== Effect.Store,
|
||||
);
|
||||
|
||||
if (!isStore) {
|
||||
continue;
|
||||
}
|
||||
for (const operand of eachInstructionLValue(instr)) {
|
||||
maybeAlias(aliases, lvalue, operand, instr.id);
|
||||
}
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
if (
|
||||
operand.effect === Effect.Capture ||
|
||||
operand.effect === Effect.Store
|
||||
) {
|
||||
maybeAlias(aliases, lvalue, operand, instr.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function maybeAlias(
|
||||
aliases: DisjointSet<Identifier>,
|
||||
lvalue: Place,
|
||||
rvalue: Place,
|
||||
id: InstructionId,
|
||||
): void {
|
||||
if (
|
||||
lvalue.identifier.mutableRange.end > id + 1 ||
|
||||
rvalue.identifier.mutableRange.end > id
|
||||
) {
|
||||
aliases.union([lvalue.identifier, rvalue.identifier]);
|
||||
}
|
||||
}
|
||||
@@ -1,351 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
CompilerError,
|
||||
CompilerErrorDetailOptions,
|
||||
ErrorSeverity,
|
||||
ValueKind,
|
||||
} from '..';
|
||||
import {
|
||||
AbstractValue,
|
||||
BasicBlock,
|
||||
Effect,
|
||||
Environment,
|
||||
FunctionEffect,
|
||||
Instruction,
|
||||
InstructionValue,
|
||||
Place,
|
||||
ValueReason,
|
||||
getHookKind,
|
||||
isRefOrRefValue,
|
||||
} from '../HIR';
|
||||
import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
interface State {
|
||||
kind(place: Place): AbstractValue;
|
||||
values(place: Place): Array<InstructionValue>;
|
||||
isDefined(place: Place): boolean;
|
||||
}
|
||||
|
||||
function inferOperandEffect(state: State, place: Place): null | FunctionEffect {
|
||||
const value = state.kind(place);
|
||||
CompilerError.invariant(value != null, {
|
||||
reason: 'Expected operand to have a kind',
|
||||
loc: null,
|
||||
});
|
||||
|
||||
switch (place.effect) {
|
||||
case Effect.Store:
|
||||
case Effect.Mutate: {
|
||||
if (isRefOrRefValue(place.identifier)) {
|
||||
break;
|
||||
} else if (value.kind === ValueKind.Context) {
|
||||
CompilerError.invariant(value.context.size > 0, {
|
||||
reason:
|
||||
"[InferFunctionEffects] Expected Context-kind value's capture list to be non-empty.",
|
||||
loc: place.loc,
|
||||
});
|
||||
return {
|
||||
kind: 'ContextMutation',
|
||||
loc: place.loc,
|
||||
effect: place.effect,
|
||||
places: value.context,
|
||||
};
|
||||
} else if (
|
||||
value.kind !== ValueKind.Mutable &&
|
||||
// We ignore mutations of primitives since this is not a React-specific problem
|
||||
value.kind !== ValueKind.Primitive
|
||||
) {
|
||||
let reason = getWriteErrorReason(value);
|
||||
return {
|
||||
kind:
|
||||
value.reason.size === 1 && value.reason.has(ValueReason.Global)
|
||||
? 'GlobalMutation'
|
||||
: 'ReactMutation',
|
||||
error: {
|
||||
reason,
|
||||
description:
|
||||
place.identifier.name !== null &&
|
||||
place.identifier.name.kind === 'named'
|
||||
? `Found mutation of \`${place.identifier.name.value}\``
|
||||
: null,
|
||||
loc: place.loc,
|
||||
suggestions: null,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
},
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function inheritFunctionEffects(
|
||||
state: State,
|
||||
place: Place,
|
||||
): Array<FunctionEffect> {
|
||||
const effects = inferFunctionInstrEffects(state, place);
|
||||
|
||||
return effects
|
||||
.flatMap(effect => {
|
||||
if (effect.kind === 'GlobalMutation' || effect.kind === 'ReactMutation') {
|
||||
return [effect];
|
||||
} else {
|
||||
const effects: Array<FunctionEffect | null> = [];
|
||||
CompilerError.invariant(effect.kind === 'ContextMutation', {
|
||||
reason: 'Expected ContextMutation',
|
||||
loc: null,
|
||||
});
|
||||
/**
|
||||
* Contextual effects need to be replayed against the current inference
|
||||
* state, which may know more about the value to which the effect applied.
|
||||
* The main cases are:
|
||||
* 1. The mutated context value is _still_ a context value in the current scope,
|
||||
* so we have to continue propagating the original context mutation.
|
||||
* 2. The mutated context value is a mutable value in the current scope,
|
||||
* so the context mutation was fine and we can skip propagating the effect.
|
||||
* 3. The mutated context value is an immutable value in the current scope,
|
||||
* resulting in a non-ContextMutation FunctionEffect. We propagate that new,
|
||||
* more detailed effect to the current function context.
|
||||
*/
|
||||
for (const place of effect.places) {
|
||||
if (state.isDefined(place)) {
|
||||
const replayedEffect = inferOperandEffect(state, {
|
||||
...place,
|
||||
loc: effect.loc,
|
||||
effect: effect.effect,
|
||||
});
|
||||
if (replayedEffect != null) {
|
||||
if (replayedEffect.kind === 'ContextMutation') {
|
||||
// Case 1, still a context value so propagate the original effect
|
||||
effects.push(effect);
|
||||
} else {
|
||||
// Case 3, immutable value so propagate the more precise effect
|
||||
effects.push(replayedEffect);
|
||||
}
|
||||
} // else case 2, local mutable value so this effect was fine
|
||||
}
|
||||
}
|
||||
return effects;
|
||||
}
|
||||
})
|
||||
.filter((effect): effect is FunctionEffect => effect != null);
|
||||
}
|
||||
|
||||
function inferFunctionInstrEffects(
|
||||
state: State,
|
||||
place: Place,
|
||||
): Array<FunctionEffect> {
|
||||
const effects: Array<FunctionEffect> = [];
|
||||
const instrs = state.values(place);
|
||||
CompilerError.invariant(instrs != null, {
|
||||
reason: 'Expected operand to have instructions',
|
||||
loc: null,
|
||||
});
|
||||
|
||||
for (const instr of instrs) {
|
||||
if (
|
||||
(instr.kind === 'FunctionExpression' || instr.kind === 'ObjectMethod') &&
|
||||
instr.loweredFunc.func.effects != null
|
||||
) {
|
||||
effects.push(...instr.loweredFunc.func.effects);
|
||||
}
|
||||
}
|
||||
|
||||
return effects;
|
||||
}
|
||||
|
||||
function operandEffects(
|
||||
state: State,
|
||||
place: Place,
|
||||
filterRenderSafe: boolean,
|
||||
): Array<FunctionEffect> {
|
||||
const functionEffects: Array<FunctionEffect> = [];
|
||||
const effect = inferOperandEffect(state, place);
|
||||
effect && functionEffects.push(effect);
|
||||
functionEffects.push(...inheritFunctionEffects(state, place));
|
||||
if (filterRenderSafe) {
|
||||
return functionEffects.filter(effect => !isEffectSafeOutsideRender(effect));
|
||||
} else {
|
||||
return functionEffects;
|
||||
}
|
||||
}
|
||||
|
||||
export function inferInstructionFunctionEffects(
|
||||
env: Environment,
|
||||
state: State,
|
||||
instr: Instruction,
|
||||
): Array<FunctionEffect> {
|
||||
const functionEffects: Array<FunctionEffect> = [];
|
||||
switch (instr.value.kind) {
|
||||
case 'JsxExpression': {
|
||||
if (instr.value.tag.kind === 'Identifier') {
|
||||
functionEffects.push(...operandEffects(state, instr.value.tag, false));
|
||||
}
|
||||
instr.value.children?.forEach(child =>
|
||||
functionEffects.push(...operandEffects(state, child, false)),
|
||||
);
|
||||
for (const attr of instr.value.props) {
|
||||
if (attr.kind === 'JsxSpreadAttribute') {
|
||||
functionEffects.push(...operandEffects(state, attr.argument, false));
|
||||
} else {
|
||||
functionEffects.push(...operandEffects(state, attr.place, true));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ObjectMethod':
|
||||
case 'FunctionExpression': {
|
||||
/**
|
||||
* If this function references other functions, propagate the referenced function's
|
||||
* effects to this function.
|
||||
*
|
||||
* ```
|
||||
* let f = () => global = true;
|
||||
* let g = () => f();
|
||||
* g();
|
||||
* ```
|
||||
*
|
||||
* In this example, because `g` references `f`, we propagate the GlobalMutation from
|
||||
* `f` to `g`. Thus, referencing `g` in `g()` will evaluate the GlobalMutation in the outer
|
||||
* function effect context and report an error. But if instead we do:
|
||||
*
|
||||
* ```
|
||||
* let f = () => global = true;
|
||||
* let g = () => f();
|
||||
* useEffect(() => g(), [g])
|
||||
* ```
|
||||
*
|
||||
* Now `g`'s effects will be discarded since they're in a useEffect.
|
||||
*/
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
instr.value.loweredFunc.func.effects ??= [];
|
||||
instr.value.loweredFunc.func.effects.push(
|
||||
...inferFunctionInstrEffects(state, operand),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'MethodCall':
|
||||
case 'CallExpression': {
|
||||
let callee;
|
||||
if (instr.value.kind === 'MethodCall') {
|
||||
callee = instr.value.property;
|
||||
functionEffects.push(
|
||||
...operandEffects(state, instr.value.receiver, false),
|
||||
);
|
||||
} else {
|
||||
callee = instr.value.callee;
|
||||
}
|
||||
functionEffects.push(...operandEffects(state, callee, false));
|
||||
let isHook = getHookKind(env, callee.identifier) != null;
|
||||
for (const arg of instr.value.args) {
|
||||
const place = arg.kind === 'Identifier' ? arg : arg.place;
|
||||
/*
|
||||
* Join the effects of the argument with the effects of the enclosing function,
|
||||
* unless the we're detecting a global mutation inside a useEffect hook
|
||||
*/
|
||||
functionEffects.push(...operandEffects(state, place, isHook));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'StartMemoize':
|
||||
case 'FinishMemoize':
|
||||
case 'LoadLocal':
|
||||
case 'StoreLocal': {
|
||||
break;
|
||||
}
|
||||
case 'StoreGlobal': {
|
||||
functionEffects.push({
|
||||
kind: 'GlobalMutation',
|
||||
error: {
|
||||
reason:
|
||||
'Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)',
|
||||
loc: instr.loc,
|
||||
suggestions: null,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
functionEffects.push(...operandEffects(state, operand, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
return functionEffects;
|
||||
}
|
||||
|
||||
export function inferTerminalFunctionEffects(
|
||||
state: State,
|
||||
block: BasicBlock,
|
||||
): Array<FunctionEffect> {
|
||||
const functionEffects: Array<FunctionEffect> = [];
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
functionEffects.push(...operandEffects(state, operand, true));
|
||||
}
|
||||
return functionEffects;
|
||||
}
|
||||
|
||||
export function transformFunctionEffectErrors(
|
||||
functionEffects: Array<FunctionEffect>,
|
||||
): Array<CompilerErrorDetailOptions> {
|
||||
return functionEffects.map(eff => {
|
||||
switch (eff.kind) {
|
||||
case 'ReactMutation':
|
||||
case 'GlobalMutation': {
|
||||
return eff.error;
|
||||
}
|
||||
case 'ContextMutation': {
|
||||
return {
|
||||
severity: ErrorSeverity.Invariant,
|
||||
reason: `Unexpected ContextMutation in top-level function effects`,
|
||||
loc: eff.loc,
|
||||
};
|
||||
}
|
||||
default:
|
||||
assertExhaustive(
|
||||
eff,
|
||||
`Unexpected function effect kind \`${(eff as any).kind}\``,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isEffectSafeOutsideRender(effect: FunctionEffect): boolean {
|
||||
return effect.kind === 'GlobalMutation';
|
||||
}
|
||||
|
||||
export function getWriteErrorReason(abstractValue: AbstractValue): string {
|
||||
if (abstractValue.reason.has(ValueReason.Global)) {
|
||||
return 'Modifying a variable defined outside a component or hook is not allowed. Consider using an effect';
|
||||
} else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
|
||||
return 'Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX';
|
||||
} else if (abstractValue.reason.has(ValueReason.Context)) {
|
||||
return `Modifying a value returned from 'useContext()' is not allowed.`;
|
||||
} else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
|
||||
return 'Modifying a value returned from a function whose return value should not be mutated';
|
||||
} else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
|
||||
return 'Modifying component props or hook arguments is not allowed. Consider using a local variable instead';
|
||||
} else if (abstractValue.reason.has(ValueReason.State)) {
|
||||
return "Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead";
|
||||
} else if (abstractValue.reason.has(ValueReason.ReducerState)) {
|
||||
return "Modifying a value returned from 'useReducer()', which should not be modified directly. Use the dispatch function to update instead";
|
||||
} else if (abstractValue.reason.has(ValueReason.Effect)) {
|
||||
return 'Modifying a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the modification before calling useEffect()';
|
||||
} else if (abstractValue.reason.has(ValueReason.HookCaptured)) {
|
||||
return 'Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook';
|
||||
} else if (abstractValue.reason.has(ValueReason.HookReturn)) {
|
||||
return 'Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed';
|
||||
} else {
|
||||
return 'This modifies a variable that React considers immutable';
|
||||
}
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
InstructionId,
|
||||
InstructionKind,
|
||||
isArrayType,
|
||||
isMapType,
|
||||
isRefOrRefValue,
|
||||
isSetType,
|
||||
makeInstructionId,
|
||||
Place,
|
||||
} from '../HIR/HIR';
|
||||
import {printPlace} from '../HIR/PrintHIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
/*
|
||||
* For each usage of a value in the given function, determines if the usage
|
||||
* may be succeeded by a mutable usage of that same value and if so updates
|
||||
* the usage to be mutable.
|
||||
*
|
||||
* Stated differently, this inference ensures that inferred capabilities of
|
||||
* each reference are as follows:
|
||||
* - freeze: the value is frozen at this point
|
||||
* - readonly: the value is not modified at this point *or any subsequent
|
||||
* point*
|
||||
* - mutable: the value is modified at this point *or some subsequent point*.
|
||||
*
|
||||
* Note that this refines the capabilities inferered by InferReferenceCapability,
|
||||
* which looks at individual references and not the lifetime of a value's mutability.
|
||||
*
|
||||
* == Algorithm
|
||||
*
|
||||
* TODO:
|
||||
* 1. Forward data-flow analysis to determine aliasing. Unlike InferReferenceCapability
|
||||
* which only tracks aliasing of top-level variables (`y = x`), this analysis needs
|
||||
* to know if a value is aliased anywhere (`y.x = x`). The forward data flow tracks
|
||||
* all possible locations which may have aliased a value. The concrete result is
|
||||
* a mapping of each Place to the set of possibly-mutable values it may alias.
|
||||
*
|
||||
* ```
|
||||
* const x = []; // {x: v0; v0: mutable []}
|
||||
* const y = {}; // {x: v0, y: v1; v0: mutable [], v1: mutable []}
|
||||
* y.x = x; // {x: v0, y: v1; v0: mutable [v1], v1: mutable [v0]}
|
||||
* read(x); // {x: v0, y: v1; v0: mutable [v1], v1: mutable [v0]}
|
||||
* mutate(y); // can infer that y mutates v0 and v1
|
||||
* ```
|
||||
*
|
||||
* DONE:
|
||||
* 2. Forward data-flow analysis to compute mutability liveness. Walk forwards over
|
||||
* the CFG and track which values are mutated in a successor.
|
||||
*
|
||||
* ```
|
||||
* mutate(y); // mutable y => v0, v1 mutated
|
||||
* read(x); // x maps to v0, v1, those are in the mutated-later set, so x is mutable here
|
||||
* ...
|
||||
* ```
|
||||
*/
|
||||
|
||||
function infer(place: Place, instrId: InstructionId): void {
|
||||
if (!isRefOrRefValue(place.identifier)) {
|
||||
place.identifier.mutableRange.end = makeInstructionId(instrId + 1);
|
||||
}
|
||||
}
|
||||
|
||||
function inferPlace(
|
||||
place: Place,
|
||||
instrId: InstructionId,
|
||||
inferMutableRangeForStores: boolean,
|
||||
): void {
|
||||
switch (place.effect) {
|
||||
case Effect.Unknown: {
|
||||
throw new Error(`Found an unknown place ${printPlace(place)}}!`);
|
||||
}
|
||||
case Effect.Capture:
|
||||
case Effect.Read:
|
||||
case Effect.Freeze:
|
||||
return;
|
||||
case Effect.Store:
|
||||
if (inferMutableRangeForStores) {
|
||||
infer(place, instrId);
|
||||
}
|
||||
return;
|
||||
case Effect.ConditionallyMutateIterator: {
|
||||
const identifier = place.identifier;
|
||||
if (
|
||||
!isArrayType(identifier) &&
|
||||
!isSetType(identifier) &&
|
||||
!isMapType(identifier)
|
||||
) {
|
||||
infer(place, instrId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case Effect.ConditionallyMutate:
|
||||
case Effect.Mutate: {
|
||||
infer(place, instrId);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
assertExhaustive(place.effect, `Unexpected ${printPlace(place)} effect`);
|
||||
}
|
||||
}
|
||||
|
||||
export function inferMutableLifetimes(
|
||||
func: HIRFunction,
|
||||
inferMutableRangeForStores: boolean,
|
||||
): void {
|
||||
/*
|
||||
* Context variables only appear to mutate where they are assigned, but we need
|
||||
* to force their range to start at their declaration. Track the declaring instruction
|
||||
* id so that the ranges can be extended if/when they are reassigned
|
||||
*/
|
||||
const contextVariableDeclarationInstructions = new Map<
|
||||
Identifier,
|
||||
InstructionId
|
||||
>();
|
||||
for (const [_, block] of func.body.blocks) {
|
||||
for (const phi of block.phis) {
|
||||
const isPhiMutatedAfterCreation: boolean =
|
||||
phi.place.identifier.mutableRange.end >
|
||||
(block.instructions.at(0)?.id ?? block.terminal.id);
|
||||
if (
|
||||
inferMutableRangeForStores &&
|
||||
isPhiMutatedAfterCreation &&
|
||||
phi.place.identifier.mutableRange.start === 0
|
||||
) {
|
||||
for (const [, operand] of phi.operands) {
|
||||
if (phi.place.identifier.mutableRange.start === 0) {
|
||||
phi.place.identifier.mutableRange.start =
|
||||
operand.identifier.mutableRange.start;
|
||||
} else {
|
||||
phi.place.identifier.mutableRange.start = makeInstructionId(
|
||||
Math.min(
|
||||
phi.place.identifier.mutableRange.start,
|
||||
operand.identifier.mutableRange.start,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
for (const operand of eachInstructionLValue(instr)) {
|
||||
const lvalueId = operand.identifier;
|
||||
|
||||
/*
|
||||
* lvalue start being mutable when they're initially assigned a
|
||||
* value.
|
||||
*/
|
||||
lvalueId.mutableRange.start = instr.id;
|
||||
|
||||
/*
|
||||
* Let's be optimistic and assume this lvalue is not mutable by
|
||||
* default.
|
||||
*/
|
||||
lvalueId.mutableRange.end = makeInstructionId(instr.id + 1);
|
||||
}
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
inferPlace(operand, instr.id, inferMutableRangeForStores);
|
||||
}
|
||||
|
||||
if (
|
||||
instr.value.kind === 'DeclareContext' ||
|
||||
(instr.value.kind === 'StoreContext' &&
|
||||
instr.value.lvalue.kind !== InstructionKind.Reassign &&
|
||||
!contextVariableDeclarationInstructions.has(
|
||||
instr.value.lvalue.place.identifier,
|
||||
))
|
||||
) {
|
||||
/**
|
||||
* Save declarations of context variables if they hasn't already been
|
||||
* declared (due to hoisted declarations).
|
||||
*/
|
||||
contextVariableDeclarationInstructions.set(
|
||||
instr.value.lvalue.place.identifier,
|
||||
instr.id,
|
||||
);
|
||||
} else if (instr.value.kind === 'StoreContext') {
|
||||
/*
|
||||
* Else this is a reassignment, extend the range from the declaration (if present).
|
||||
* Note that declarations may not be present for context variables that are reassigned
|
||||
* within a function expression before (or without) a read of the same variable
|
||||
*/
|
||||
const declaration = contextVariableDeclarationInstructions.get(
|
||||
instr.value.lvalue.place.identifier,
|
||||
);
|
||||
if (
|
||||
declaration != null &&
|
||||
!isRefOrRefValue(instr.value.lvalue.place.identifier)
|
||||
) {
|
||||
const range = instr.value.lvalue.place.identifier.mutableRange;
|
||||
if (range.start === 0) {
|
||||
range.start = declaration;
|
||||
} else {
|
||||
range.start = makeInstructionId(Math.min(range.start, declaration));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
inferPlace(operand, block.terminal.id, inferMutableRangeForStores);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {HIRFunction, Identifier} from '../HIR/HIR';
|
||||
import {inferAliasForUncalledFunctions} from './InerAliasForUncalledFunctions';
|
||||
import {inferAliases} from './InferAlias';
|
||||
import {inferAliasForPhis} from './InferAliasForPhis';
|
||||
import {inferAliasForStores} from './InferAliasForStores';
|
||||
import {inferMutableLifetimes} from './InferMutableLifetimes';
|
||||
import {inferMutableRangesForAlias} from './InferMutableRangesForAlias';
|
||||
import {inferTryCatchAliases} from './InferTryCatchAliases';
|
||||
|
||||
export function inferMutableRanges(ir: HIRFunction): void {
|
||||
// Infer mutable ranges for non fields
|
||||
inferMutableLifetimes(ir, false);
|
||||
|
||||
// Calculate aliases
|
||||
const aliases = inferAliases(ir);
|
||||
/*
|
||||
* Calculate aliases for try/catch, where any value created
|
||||
* in the try block could be aliased to the catch param
|
||||
*/
|
||||
inferTryCatchAliases(ir, aliases);
|
||||
|
||||
/*
|
||||
* Eagerly canonicalize so that if nothing changes we can bail out
|
||||
* after a single iteration
|
||||
*/
|
||||
let prevAliases: Map<Identifier, Identifier> = aliases.canonicalize();
|
||||
while (true) {
|
||||
// Infer mutable ranges for aliases that are not fields
|
||||
inferMutableRangesForAlias(ir, aliases);
|
||||
|
||||
// Update aliasing information of fields
|
||||
inferAliasForStores(ir, aliases);
|
||||
|
||||
// Update aliasing information of phis
|
||||
inferAliasForPhis(ir, aliases);
|
||||
|
||||
const nextAliases = aliases.canonicalize();
|
||||
if (areEqualMaps(prevAliases, nextAliases)) {
|
||||
break;
|
||||
}
|
||||
prevAliases = nextAliases;
|
||||
}
|
||||
|
||||
// Re-infer mutable ranges for all values
|
||||
inferMutableLifetimes(ir, true);
|
||||
|
||||
/**
|
||||
* The second inferMutableLifetimes() call updates mutable ranges
|
||||
* of values to account for Store effects. Now we need to update
|
||||
* all aliases of such values to extend their ranges as well. Note
|
||||
* that the store only mutates the the directly aliased value and
|
||||
* not any of its inner captured references. For example:
|
||||
*
|
||||
* ```
|
||||
* let y;
|
||||
* if (cond) {
|
||||
* y = [];
|
||||
* } else {
|
||||
* y = [{}];
|
||||
* }
|
||||
* y.push(z);
|
||||
* ```
|
||||
*
|
||||
* The Store effect from the `y.push` modifies the values that `y`
|
||||
* directly aliases - the two arrays from the if/else branches -
|
||||
* but does not modify values that `y` "contains" such as the
|
||||
* object literal or `z`.
|
||||
*/
|
||||
prevAliases = aliases.canonicalize();
|
||||
while (true) {
|
||||
inferMutableRangesForAlias(ir, aliases);
|
||||
inferAliasForPhis(ir, aliases);
|
||||
inferAliasForUncalledFunctions(ir, aliases);
|
||||
const nextAliases = aliases.canonicalize();
|
||||
if (areEqualMaps(prevAliases, nextAliases)) {
|
||||
break;
|
||||
}
|
||||
prevAliases = nextAliases;
|
||||
}
|
||||
}
|
||||
|
||||
function areEqualMaps<T, U>(a: Map<T, U>, b: Map<T, U>): boolean {
|
||||
if (a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
for (const [key, value] of a) {
|
||||
if (!b.has(key)) {
|
||||
return false;
|
||||
}
|
||||
if (b.get(key) !== value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
InstructionId,
|
||||
isRefOrRefValue,
|
||||
} from '../HIR/HIR';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
export function inferMutableRangesForAlias(
|
||||
_fn: HIRFunction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
const aliasSets = aliases.buildSets();
|
||||
for (const aliasSet of aliasSets) {
|
||||
/*
|
||||
* Update mutableRange.end only if the identifiers have actually been
|
||||
* mutated.
|
||||
*/
|
||||
const mutatingIdentifiers = [...aliasSet].filter(
|
||||
id =>
|
||||
id.mutableRange.end - id.mutableRange.start > 1 && !isRefOrRefValue(id),
|
||||
);
|
||||
|
||||
if (mutatingIdentifiers.length > 0) {
|
||||
// Find final instruction which mutates this alias set.
|
||||
let lastMutatingInstructionId = 0;
|
||||
for (const id of mutatingIdentifiers) {
|
||||
if (id.mutableRange.end > lastMutatingInstructionId) {
|
||||
lastMutatingInstructionId = id.mutableRange.end;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Update mutableRange.end for all aliases in this set ending before the
|
||||
* last mutation.
|
||||
*/
|
||||
for (const alias of aliasSet) {
|
||||
if (
|
||||
alias.mutableRange.end < lastMutatingInstructionId &&
|
||||
!isRefOrRefValue(alias)
|
||||
) {
|
||||
alias.mutableRange.end = lastMutatingInstructionId as InstructionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
DeclarationId,
|
||||
Environment,
|
||||
FunctionExpression,
|
||||
GeneratedSource,
|
||||
HIRFunction,
|
||||
Hole,
|
||||
IdentifierId,
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
InstructionKind,
|
||||
InstructionValue,
|
||||
isArrayType,
|
||||
isJsxType,
|
||||
isMapType,
|
||||
isPrimitiveType,
|
||||
isRefOrRefValue,
|
||||
@@ -34,6 +36,7 @@ import {
|
||||
Phi,
|
||||
Place,
|
||||
SpreadPattern,
|
||||
Type,
|
||||
ValueReason,
|
||||
} from '../HIR';
|
||||
import {
|
||||
@@ -43,12 +46,6 @@ import {
|
||||
eachTerminalSuccessor,
|
||||
} from '../HIR/visitors';
|
||||
import {Ok, Result} from '../Utils/Result';
|
||||
import {
|
||||
getArgumentEffect,
|
||||
getFunctionCallSignature,
|
||||
isKnownMutableEffect,
|
||||
mergeValueKinds,
|
||||
} from './InferReferenceEffects';
|
||||
import {
|
||||
assertExhaustive,
|
||||
getOrInsertDefault,
|
||||
@@ -65,10 +62,15 @@ import {
|
||||
printSourceLocation,
|
||||
} from '../HIR/PrintHIR';
|
||||
import {FunctionSignature} from '../HIR/ObjectShape';
|
||||
import {getWriteErrorReason} from './InferFunctionEffects';
|
||||
import prettyFormat from 'pretty-format';
|
||||
import {createTemporaryPlace} from '../HIR/HIRBuilder';
|
||||
import {AliasingEffect, AliasingSignature, hashEffect} from './AliasingEffects';
|
||||
import {
|
||||
AliasingEffect,
|
||||
AliasingSignature,
|
||||
hashEffect,
|
||||
MutationReason,
|
||||
} from './AliasingEffects';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
@@ -445,25 +447,35 @@ function applySignature(
|
||||
const reason = getWriteErrorReason({
|
||||
kind: value.kind,
|
||||
reason: value.reason,
|
||||
context: new Set(),
|
||||
});
|
||||
const variable =
|
||||
effect.value.identifier.name !== null &&
|
||||
effect.value.identifier.name.kind === 'named'
|
||||
? `\`${effect.value.identifier.name.value}\``
|
||||
: 'value';
|
||||
const diagnostic = CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Immutability,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'This value cannot be modified',
|
||||
description: `${reason}.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `${variable} cannot be modified`,
|
||||
});
|
||||
if (
|
||||
effect.kind === 'Mutate' &&
|
||||
effect.reason?.kind === 'AssignCurrentProperty'
|
||||
) {
|
||||
diagnostic.withDetail({
|
||||
kind: 'hint',
|
||||
message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`,
|
||||
});
|
||||
}
|
||||
effects.push({
|
||||
kind: 'MutateFrozen',
|
||||
place: effect.value,
|
||||
error: CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'This value cannot be modified',
|
||||
description: `${reason}.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `${variable} cannot be modified`,
|
||||
}),
|
||||
error: diagnostic,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1027,8 +1039,9 @@ function applyEffect(
|
||||
effect.value.identifier.declarationId,
|
||||
);
|
||||
const diagnostic = CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Immutability,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot access variable before it is declared',
|
||||
reason: 'Cannot access variable before it is declared',
|
||||
description: `${variable ?? 'This variable'} is accessed before it is declared, which prevents the earlier access from updating when this value changes over time.`,
|
||||
});
|
||||
if (hoistedAccess != null && hoistedAccess.loc != effect.value.loc) {
|
||||
@@ -1059,13 +1072,31 @@ function applyEffect(
|
||||
const reason = getWriteErrorReason({
|
||||
kind: value.kind,
|
||||
reason: value.reason,
|
||||
context: new Set(),
|
||||
});
|
||||
const variable =
|
||||
effect.value.identifier.name !== null &&
|
||||
effect.value.identifier.name.kind === 'named'
|
||||
? `\`${effect.value.identifier.name.value}\``
|
||||
: 'value';
|
||||
const diagnostic = CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Immutability,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'This value cannot be modified',
|
||||
description: `${reason}.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `${variable} cannot be modified`,
|
||||
});
|
||||
if (
|
||||
effect.kind === 'Mutate' &&
|
||||
effect.reason?.kind === 'AssignCurrentProperty'
|
||||
) {
|
||||
diagnostic.withDetail({
|
||||
kind: 'hint',
|
||||
message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`,
|
||||
});
|
||||
}
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
@@ -1075,15 +1106,7 @@ function applyEffect(
|
||||
? 'MutateFrozen'
|
||||
: 'MutateGlobal',
|
||||
place: effect.value,
|
||||
error: CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'This value cannot be modified',
|
||||
description: `${reason}.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `${variable} cannot be modified`,
|
||||
}),
|
||||
error: diagnostic,
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
@@ -1680,7 +1703,15 @@ function computeSignatureForInstruction(
|
||||
}
|
||||
case 'PropertyStore':
|
||||
case 'ComputedStore': {
|
||||
effects.push({kind: 'Mutate', value: value.object});
|
||||
const mutationReason: MutationReason | null =
|
||||
value.kind === 'PropertyStore' && value.property === 'current'
|
||||
? {kind: 'AssignCurrentProperty'}
|
||||
: null;
|
||||
effects.push({
|
||||
kind: 'Mutate',
|
||||
value: value.object,
|
||||
reason: mutationReason,
|
||||
});
|
||||
effects.push({
|
||||
kind: 'Capture',
|
||||
from: value.value,
|
||||
@@ -1841,6 +1872,23 @@ function computeSignatureForInstruction(
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const prop of value.props) {
|
||||
if (
|
||||
prop.kind === 'JsxAttribute' &&
|
||||
prop.place.identifier.type.kind === 'Function' &&
|
||||
(isJsxType(prop.place.identifier.type.return) ||
|
||||
(prop.place.identifier.type.return.kind === 'Phi' &&
|
||||
prop.place.identifier.type.return.operands.some(operand =>
|
||||
isJsxType(operand),
|
||||
)))
|
||||
) {
|
||||
// Any props which return jsx are assumed to be called during render
|
||||
effects.push({
|
||||
kind: 'Render',
|
||||
place: prop.place,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -2007,8 +2055,9 @@ function computeSignatureForInstruction(
|
||||
kind: 'MutateGlobal',
|
||||
place: value.value,
|
||||
error: CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Globals,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category:
|
||||
reason:
|
||||
'Cannot reassign variables declared outside of the component/hook',
|
||||
description: `Variable ${variable} is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)`,
|
||||
}).withDetail({
|
||||
@@ -2040,7 +2089,7 @@ function computeSignatureForInstruction(
|
||||
effects.push({
|
||||
kind: 'Freeze',
|
||||
value: operand,
|
||||
reason: ValueReason.Other,
|
||||
reason: ValueReason.HookCaptured,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2106,8 +2155,9 @@ function computeEffectsForLegacySignature(
|
||||
kind: 'Impure',
|
||||
place: receiver,
|
||||
error: CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Purity,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot call impure function during render',
|
||||
reason: 'Cannot call impure function during render',
|
||||
description:
|
||||
(signature.canonicalName != null
|
||||
? `\`${signature.canonicalName}\` is an impure function. `
|
||||
@@ -2120,6 +2170,27 @@ function computeEffectsForLegacySignature(
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (signature.knownIncompatible != null && state.env.isInferredMemoEnabled) {
|
||||
const errors = new CompilerError();
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.IncompatibleLibrary,
|
||||
severity: ErrorSeverity.IncompatibleLibrary,
|
||||
reason: 'Use of incompatible library',
|
||||
description: [
|
||||
'This API returns functions which cannot be memoized without leading to stale UI. ' +
|
||||
'To prevent this, by default React Compiler will skip memoizing this component/hook. ' +
|
||||
'However, you may see issues if values from this API are passed to other components/hooks that are ' +
|
||||
'memoized.',
|
||||
].join(''),
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: receiver.loc,
|
||||
message: signature.knownIncompatible,
|
||||
}),
|
||||
);
|
||||
throw errors;
|
||||
}
|
||||
const stores: Array<Place> = [];
|
||||
const captures: Array<Place> = [];
|
||||
function visit(place: Place, effect: Effect): void {
|
||||
@@ -2534,3 +2605,196 @@ export type AbstractValue = {
|
||||
kind: ValueKind;
|
||||
reason: ReadonlySet<ValueReason>;
|
||||
};
|
||||
|
||||
export function getWriteErrorReason(abstractValue: AbstractValue): string {
|
||||
if (abstractValue.reason.has(ValueReason.Global)) {
|
||||
return 'Modifying a variable defined outside a component or hook is not allowed. Consider using an effect';
|
||||
} else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
|
||||
return 'Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX';
|
||||
} else if (abstractValue.reason.has(ValueReason.Context)) {
|
||||
return `Modifying a value returned from 'useContext()' is not allowed.`;
|
||||
} else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
|
||||
return 'Modifying a value returned from a function whose return value should not be mutated';
|
||||
} else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
|
||||
return 'Modifying component props or hook arguments is not allowed. Consider using a local variable instead';
|
||||
} else if (abstractValue.reason.has(ValueReason.State)) {
|
||||
return "Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead";
|
||||
} else if (abstractValue.reason.has(ValueReason.ReducerState)) {
|
||||
return "Modifying a value returned from 'useReducer()', which should not be modified directly. Use the dispatch function to update instead";
|
||||
} else if (abstractValue.reason.has(ValueReason.Effect)) {
|
||||
return 'Modifying a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the modification before calling useEffect()';
|
||||
} else if (abstractValue.reason.has(ValueReason.HookCaptured)) {
|
||||
return 'Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook';
|
||||
} else if (abstractValue.reason.has(ValueReason.HookReturn)) {
|
||||
return 'Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed';
|
||||
} else {
|
||||
return 'This modifies a variable that React considers immutable';
|
||||
}
|
||||
}
|
||||
|
||||
function getArgumentEffect(
|
||||
signatureEffect: Effect | null,
|
||||
arg: Place | SpreadPattern,
|
||||
): Effect {
|
||||
if (signatureEffect != null) {
|
||||
if (arg.kind === 'Identifier') {
|
||||
return signatureEffect;
|
||||
} else if (
|
||||
signatureEffect === Effect.Mutate ||
|
||||
signatureEffect === Effect.ConditionallyMutate
|
||||
) {
|
||||
return signatureEffect;
|
||||
} else {
|
||||
// see call-spread-argument-mutable-iterator test fixture
|
||||
if (signatureEffect === Effect.Freeze) {
|
||||
CompilerError.throwTodo({
|
||||
reason: 'Support spread syntax for hook arguments',
|
||||
loc: arg.place.loc,
|
||||
});
|
||||
}
|
||||
// effects[i] is Effect.Capture | Effect.Read | Effect.Store
|
||||
return Effect.ConditionallyMutateIterator;
|
||||
}
|
||||
} else {
|
||||
return Effect.ConditionallyMutate;
|
||||
}
|
||||
}
|
||||
|
||||
export function getFunctionCallSignature(
|
||||
env: Environment,
|
||||
type: Type,
|
||||
): FunctionSignature | null {
|
||||
if (type.kind !== 'Function') {
|
||||
return null;
|
||||
}
|
||||
return env.getFunctionSignature(type);
|
||||
}
|
||||
|
||||
export function isKnownMutableEffect(effect: Effect): boolean {
|
||||
switch (effect) {
|
||||
case Effect.Store:
|
||||
case Effect.ConditionallyMutate:
|
||||
case Effect.ConditionallyMutateIterator:
|
||||
case Effect.Mutate: {
|
||||
return true;
|
||||
}
|
||||
|
||||
case Effect.Unknown: {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected unknown effect',
|
||||
description: null,
|
||||
loc: GeneratedSource,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
case Effect.Read:
|
||||
case Effect.Capture:
|
||||
case Effect.Freeze: {
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(effect, `Unexpected effect \`${effect}\``);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins two values using the following rules:
|
||||
* == Effect Transitions ==
|
||||
*
|
||||
* Freezing an immutable value has not effect:
|
||||
* ┌───────────────┐
|
||||
* │ │
|
||||
* ▼ │ Freeze
|
||||
* ┌──────────────────────────┐ │
|
||||
* │ Immutable │──┘
|
||||
* └──────────────────────────┘
|
||||
*
|
||||
* Freezing a mutable or maybe-frozen value makes it frozen. Freezing a frozen
|
||||
* value has no effect:
|
||||
* ┌───────────────┐
|
||||
* ┌─────────────────────────┐ Freeze │ │
|
||||
* │ MaybeFrozen │────┐ ▼ │ Freeze
|
||||
* └─────────────────────────┘ │ ┌──────────────────────────┐ │
|
||||
* ├────▶│ Frozen │──┘
|
||||
* │ └──────────────────────────┘
|
||||
* ┌─────────────────────────┐ │
|
||||
* │ Mutable │────┘
|
||||
* └─────────────────────────┘
|
||||
*
|
||||
* == Join Lattice ==
|
||||
* - immutable | mutable => mutable
|
||||
* The justification is that immutable and mutable values are different types,
|
||||
* and functions can introspect them to tell the difference (if the argument
|
||||
* is null return early, else if its an object mutate it).
|
||||
* - frozen | mutable => maybe-frozen
|
||||
* Frozen values are indistinguishable from mutable values at runtime, so callers
|
||||
* cannot dynamically avoid mutation of "frozen" values. If a value could be
|
||||
* frozen we have to distinguish it from a mutable value. But it also isn't known
|
||||
* frozen yet, so we distinguish as maybe-frozen.
|
||||
* - immutable | frozen => frozen
|
||||
* This is subtle and falls out of the above rules. If a value could be any of
|
||||
* immutable, mutable, or frozen, then at runtime it could either be a primitive
|
||||
* or a reference type, and callers can't distinguish frozen or not for reference
|
||||
* types. To ensure that any sequence of joins btw those three states yields the
|
||||
* correct maybe-frozen, these two have to produce a frozen value.
|
||||
* - <any> | maybe-frozen => maybe-frozen
|
||||
* - immutable | context => context
|
||||
* - mutable | context => context
|
||||
* - frozen | context => maybe-frozen
|
||||
*
|
||||
* ┌──────────────────────────┐
|
||||
* │ Immutable │───┐
|
||||
* └──────────────────────────┘ │
|
||||
* │ ┌─────────────────────────┐
|
||||
* ├───▶│ Frozen │──┐
|
||||
* ┌──────────────────────────┐ │ └─────────────────────────┘ │
|
||||
* │ Frozen │───┤ │ ┌─────────────────────────┐
|
||||
* └──────────────────────────┘ │ ├─▶│ MaybeFrozen │
|
||||
* │ ┌─────────────────────────┐ │ └─────────────────────────┘
|
||||
* ├───▶│ MaybeFrozen │──┘
|
||||
* ┌──────────────────────────┐ │ └─────────────────────────┘
|
||||
* │ Mutable │───┘
|
||||
* └──────────────────────────┘
|
||||
*/
|
||||
function mergeValueKinds(a: ValueKind, b: ValueKind): ValueKind {
|
||||
if (a === b) {
|
||||
return a;
|
||||
} else if (a === ValueKind.MaybeFrozen || b === ValueKind.MaybeFrozen) {
|
||||
return ValueKind.MaybeFrozen;
|
||||
// after this a and b differ and neither are MaybeFrozen
|
||||
} else if (a === ValueKind.Mutable || b === ValueKind.Mutable) {
|
||||
if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
|
||||
// frozen | mutable
|
||||
return ValueKind.MaybeFrozen;
|
||||
} else if (a === ValueKind.Context || b === ValueKind.Context) {
|
||||
// context | mutable
|
||||
return ValueKind.Context;
|
||||
} else {
|
||||
// mutable | immutable
|
||||
return ValueKind.Mutable;
|
||||
}
|
||||
} else if (a === ValueKind.Context || b === ValueKind.Context) {
|
||||
if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
|
||||
// frozen | context
|
||||
return ValueKind.MaybeFrozen;
|
||||
} else {
|
||||
// context | immutable
|
||||
return ValueKind.Context;
|
||||
}
|
||||
} else if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
|
||||
return ValueKind.Frozen;
|
||||
} else if (a === ValueKind.Global || b === ValueKind.Global) {
|
||||
return ValueKind.Global;
|
||||
} else {
|
||||
CompilerError.invariant(
|
||||
a === ValueKind.Primitive && b == ValueKind.Primitive,
|
||||
{
|
||||
reason: `Unexpected value kind in mergeValues()`,
|
||||
description: `Found kinds ${a} and ${b}`,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
);
|
||||
return ValueKind.Primitive;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
} from '../HIR/visitors';
|
||||
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {AliasingEffect} from './AliasingEffects';
|
||||
import {AliasingEffect, MutationReason} from './AliasingEffects';
|
||||
|
||||
/**
|
||||
* This pass builds an abstract model of the heap and interprets the effects of the
|
||||
@@ -101,6 +101,7 @@ export function inferMutationAliasingRanges(
|
||||
transitive: boolean;
|
||||
kind: MutationKind;
|
||||
place: Place;
|
||||
reason: MutationReason | null;
|
||||
}> = [];
|
||||
const renders: Array<{index: number; place: Place}> = [];
|
||||
|
||||
@@ -176,6 +177,7 @@ export function inferMutationAliasingRanges(
|
||||
effect.kind === 'MutateTransitive'
|
||||
? MutationKind.Definite
|
||||
: MutationKind.Conditional,
|
||||
reason: null,
|
||||
place: effect.value,
|
||||
});
|
||||
} else if (
|
||||
@@ -190,6 +192,7 @@ export function inferMutationAliasingRanges(
|
||||
effect.kind === 'Mutate'
|
||||
? MutationKind.Definite
|
||||
: MutationKind.Conditional,
|
||||
reason: effect.kind === 'Mutate' ? (effect.reason ?? null) : null,
|
||||
place: effect.value,
|
||||
});
|
||||
} else if (
|
||||
@@ -241,6 +244,7 @@ export function inferMutationAliasingRanges(
|
||||
mutation.transitive,
|
||||
mutation.kind,
|
||||
mutation.place.loc,
|
||||
mutation.reason,
|
||||
errors,
|
||||
);
|
||||
}
|
||||
@@ -267,6 +271,7 @@ export function inferMutationAliasingRanges(
|
||||
functionEffects.push({
|
||||
kind: 'Mutate',
|
||||
value: {...place, loc: node.local.loc},
|
||||
reason: node.mutationReason,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -507,6 +512,7 @@ export function inferMutationAliasingRanges(
|
||||
true,
|
||||
MutationKind.Conditional,
|
||||
into.loc,
|
||||
null,
|
||||
ignoredErrors,
|
||||
);
|
||||
for (const from of tracked) {
|
||||
@@ -580,6 +586,7 @@ type Node = {
|
||||
transitive: {kind: MutationKind; loc: SourceLocation} | null;
|
||||
local: {kind: MutationKind; loc: SourceLocation} | null;
|
||||
lastMutated: number;
|
||||
mutationReason: MutationReason | null;
|
||||
value:
|
||||
| {kind: 'Object'}
|
||||
| {kind: 'Phi'}
|
||||
@@ -599,6 +606,7 @@ class AliasingState {
|
||||
transitive: null,
|
||||
local: null,
|
||||
lastMutated: 0,
|
||||
mutationReason: null,
|
||||
value,
|
||||
});
|
||||
}
|
||||
@@ -697,6 +705,7 @@ class AliasingState {
|
||||
transitive: boolean,
|
||||
startKind: MutationKind,
|
||||
loc: SourceLocation,
|
||||
reason: MutationReason | null,
|
||||
errors: CompilerError,
|
||||
): void {
|
||||
const seen = new Map<Identifier, MutationKind>();
|
||||
@@ -717,6 +726,7 @@ class AliasingState {
|
||||
if (node == null) {
|
||||
continue;
|
||||
}
|
||||
node.mutationReason ??= reason;
|
||||
node.lastMutated = Math.max(node.lastMutated, index);
|
||||
if (end != null) {
|
||||
node.id.mutableRange.end = makeInstructionId(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,49 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {BlockId, HIRFunction, Identifier} from '../HIR';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
/*
|
||||
* Any values created within a try/catch block could be aliased to the try handler.
|
||||
* Our lowering ensures that every instruction within a try block will be lowered into a
|
||||
* basic block ending in a maybe-throw terminal that points to its catch block, so we can
|
||||
* iterate such blocks and alias their instruction lvalues to the handler's param (if present).
|
||||
*/
|
||||
export function inferTryCatchAliases(
|
||||
fn: HIRFunction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
const handlerParams: Map<BlockId, Identifier> = new Map();
|
||||
for (const [_, block] of fn.body.blocks) {
|
||||
if (
|
||||
block.terminal.kind === 'try' &&
|
||||
block.terminal.handlerBinding !== null
|
||||
) {
|
||||
handlerParams.set(
|
||||
block.terminal.handler,
|
||||
block.terminal.handlerBinding.identifier,
|
||||
);
|
||||
} else if (block.terminal.kind === 'maybe-throw') {
|
||||
const handlerParam = handlerParams.get(block.terminal.handler);
|
||||
if (handlerParam === undefined) {
|
||||
/*
|
||||
* There's no catch clause param, nothing to alias to so
|
||||
* skip this block
|
||||
*/
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* Otherwise alias all values created in this block to the
|
||||
* catch clause param
|
||||
*/
|
||||
for (const instr of block.instructions) {
|
||||
aliases.union([handlerParam, instr.lvalue.identifier]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,6 @@
|
||||
|
||||
export {default as analyseFunctions} from './AnalyseFunctions';
|
||||
export {dropManualMemoization} from './DropManualMemoization';
|
||||
export {inferMutableRanges} from './InferMutableRanges';
|
||||
export {inferReactivePlaces} from './InferReactivePlaces';
|
||||
export {default as inferReferenceEffects} from './InferReferenceEffects';
|
||||
export {inlineImmediatelyInvokedFunctionExpressions} from './InlineImmediatelyInvokedFunctionExpressions';
|
||||
export {inferEffectDependencies} from './InferEffectDependencies';
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
mapInstructionValueOperands,
|
||||
mapTerminalOperands,
|
||||
} from '../HIR/visitors';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
|
||||
type InlinedJsxDeclarationMap = Map<
|
||||
DeclarationId,
|
||||
@@ -83,6 +84,7 @@ export function inlineJsxTransform(
|
||||
kind: 'CompileDiagnostic',
|
||||
fnLoc: null,
|
||||
detail: {
|
||||
category: ErrorCategory.Todo,
|
||||
reason: 'JSX Inlining is not supported on value blocks',
|
||||
loc: instr.loc,
|
||||
},
|
||||
|
||||
@@ -255,7 +255,6 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
returnTypeAnnotation: null,
|
||||
returns: createTemporaryPlace(env, GeneratedSource),
|
||||
context: [],
|
||||
effects: null,
|
||||
body: {
|
||||
entry: block.id,
|
||||
blocks: new Map([[block.id, block]]),
|
||||
@@ -263,6 +262,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
generator: false,
|
||||
async: false,
|
||||
directives: [],
|
||||
aliasingEffects: [],
|
||||
};
|
||||
|
||||
reversePostorderBlocks(fn.body);
|
||||
|
||||
@@ -370,7 +370,6 @@ function emitOutlinedFn(
|
||||
returnTypeAnnotation: null,
|
||||
returns: createTemporaryPlace(env, GeneratedSource),
|
||||
context: [],
|
||||
effects: null,
|
||||
body: {
|
||||
entry: block.id,
|
||||
blocks: new Map([[block.id, block]]),
|
||||
@@ -378,6 +377,7 @@ function emitOutlinedFn(
|
||||
generator: false,
|
||||
async: false,
|
||||
directives: [],
|
||||
aliasingEffects: [],
|
||||
};
|
||||
return fn;
|
||||
}
|
||||
|
||||
@@ -175,6 +175,41 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
|
||||
if (node != null) {
|
||||
valueBlockNodes.set(fallthrough, node);
|
||||
}
|
||||
} else if (terminal.kind === 'goto') {
|
||||
/**
|
||||
* If we encounter a goto that is not to the natural fallthrough of the current
|
||||
* block (not the topmost fallthrough on the stack), then this is a goto to a
|
||||
* label. Any scopes that extend beyond the goto must be extended to include
|
||||
* the labeled range, so that the break statement doesn't accidentally jump
|
||||
* out of the scope. We do this by extending the start and end of the scope's
|
||||
* range to the label and its fallthrough respectively.
|
||||
*/
|
||||
const start = activeBlockFallthroughRanges.find(
|
||||
range => range.fallthrough === terminal.block,
|
||||
);
|
||||
if (start != null && start !== activeBlockFallthroughRanges.at(-1)) {
|
||||
const fallthroughBlock = fn.body.blocks.get(start.fallthrough)!;
|
||||
const firstId =
|
||||
fallthroughBlock.instructions[0]?.id ?? fallthroughBlock.terminal.id;
|
||||
for (const scope of activeScopes) {
|
||||
/**
|
||||
* activeScopes is only filtered at block start points, so some of the
|
||||
* scopes may not actually be active anymore, ie we've past their end
|
||||
* instruction. Only extend ranges for scopes that are actually active.
|
||||
*
|
||||
* TODO: consider pruning activeScopes per instruction
|
||||
*/
|
||||
if (scope.range.end <= terminal.id) {
|
||||
continue;
|
||||
}
|
||||
scope.range.start = makeInstructionId(
|
||||
Math.min(start.range.start, scope.range.start),
|
||||
);
|
||||
scope.range.end = makeInstructionId(
|
||||
Math.max(firstId, scope.range.end),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
pruneUnusedLabels,
|
||||
renameVariables,
|
||||
} from '.';
|
||||
import {CompilerError, ErrorSeverity} from '../CompilerError';
|
||||
import {CompilerError, ErrorCategory, ErrorSeverity} from '../CompilerError';
|
||||
import {Environment, ExternalFunction} from '../HIR';
|
||||
import {
|
||||
ArrayPattern,
|
||||
@@ -2185,6 +2185,7 @@ function codegenInstructionValue(
|
||||
(declarator.id as t.Identifier).name
|
||||
}'`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: declarator.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -2193,6 +2194,7 @@ function codegenInstructionValue(
|
||||
cx.errors.push({
|
||||
reason: `(CodegenReactiveFunction::codegenInstructionValue) Handle conversion of ${stmt.type} to expression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
getHookKind,
|
||||
isMutableEffect,
|
||||
} from '../HIR';
|
||||
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
|
||||
import {assertExhaustive, getOrInsertDefault} from '../Utils/utils';
|
||||
import {getPlaceScope, ReactiveScope} from '../HIR/HIR';
|
||||
import {
|
||||
@@ -35,6 +34,7 @@ import {
|
||||
visitReactiveFunction,
|
||||
} from './visitors';
|
||||
import {printPlace} from '../HIR/PrintHIR';
|
||||
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
|
||||
|
||||
/*
|
||||
* This pass prunes reactive scopes that are not necessary to bound downstream computation.
|
||||
@@ -411,7 +411,9 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
|
||||
this.state = state;
|
||||
this.options = {
|
||||
memoizeJsxElements: !this.env.config.enableForest,
|
||||
forceMemoizePrimitives: this.env.config.enableForest,
|
||||
forceMemoizePrimitives:
|
||||
this.env.config.enableForest ||
|
||||
this.env.config.enablePreserveExistingMemoizationGuarantees,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -534,9 +536,23 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
|
||||
case 'JSXText':
|
||||
case 'BinaryExpression':
|
||||
case 'UnaryExpression': {
|
||||
const level = options.forceMemoizePrimitives
|
||||
? MemoizationLevel.Memoized
|
||||
: MemoizationLevel.Never;
|
||||
if (options.forceMemoizePrimitives) {
|
||||
/**
|
||||
* Because these instructions produce primitives we usually don't consider
|
||||
* them as escape points: they are known to copy, not return references.
|
||||
* However if we're forcing memoization of primitives then we mark these
|
||||
* instructions as needing memoization and walk their rvalues to ensure
|
||||
* any scopes transitively reachable from the rvalues are considered for
|
||||
* memoization. Note: we may still prune primitive-producing scopes if
|
||||
* they don't ultimately escape at all.
|
||||
*/
|
||||
const level = MemoizationLevel.Conditional;
|
||||
return {
|
||||
lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
|
||||
rvalues: [...eachReactiveValueOperand(value)],
|
||||
};
|
||||
}
|
||||
const level = MemoizationLevel.Never;
|
||||
return {
|
||||
// All of these instructions return a primitive value and never need to be memoized
|
||||
lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
|
||||
@@ -685,9 +701,7 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
|
||||
}
|
||||
case 'ComputedLoad':
|
||||
case 'PropertyLoad': {
|
||||
const level = options.forceMemoizePrimitives
|
||||
? MemoizationLevel.Memoized
|
||||
: MemoizationLevel.Conditional;
|
||||
const level = MemoizationLevel.Conditional;
|
||||
return {
|
||||
// Indirection for the inner value, memoized if the value is
|
||||
lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
import {eachInstructionOperand} from '../HIR/visitors';
|
||||
import {printSourceLocationLine} from '../HIR/PrintHIR';
|
||||
import {USE_FIRE_FUNCTION_NAME} from '../HIR/Environment';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
|
||||
/*
|
||||
* TODO(jmbrown):
|
||||
@@ -133,6 +134,7 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
|
||||
loc: value.loc,
|
||||
description: null,
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: ErrorCategory.Invariant,
|
||||
reason: '[InsertFire] No LoadGlobal found for useEffect call',
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -179,6 +181,7 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
|
||||
description:
|
||||
'You must use an array literal for an effect dependency array when that effect uses `fire()`',
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: ErrorCategory.Fire,
|
||||
reason: CANNOT_COMPILE_FIRE,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -189,6 +192,7 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
|
||||
description:
|
||||
'You must use an array literal for an effect dependency array when that effect uses `fire()`',
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: ErrorCategory.Fire,
|
||||
reason: CANNOT_COMPILE_FIRE,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -223,6 +227,7 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
|
||||
loc: value.loc,
|
||||
description: null,
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: ErrorCategory.Invariant,
|
||||
reason:
|
||||
'[InsertFire] No loadLocal found for fire call argument',
|
||||
suggestions: null,
|
||||
@@ -246,6 +251,7 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
|
||||
description:
|
||||
'`fire()` can only receive a function call such as `fire(fn(a,b)). Method calls and other expressions are not allowed',
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: ErrorCategory.Fire,
|
||||
reason: CANNOT_COMPILE_FIRE,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -264,6 +270,7 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
|
||||
loc: value.loc,
|
||||
description,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: ErrorCategory.Fire,
|
||||
reason: CANNOT_COMPILE_FIRE,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -395,6 +402,7 @@ function ensureNoRemainingCalleeCaptures(
|
||||
this effect or not used with a fire() call at all. ${calleeName} was used with fire() on line \
|
||||
${printSourceLocationLine(calleeInfo.fireLoc)} in this effect`,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: ErrorCategory.Fire,
|
||||
reason: CANNOT_COMPILE_FIRE,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -411,6 +419,7 @@ function ensureNoMoreFireUses(fn: HIRFunction, context: Context): void {
|
||||
context.pushError({
|
||||
loc: place.identifier.loc,
|
||||
description: 'Cannot use `fire` outside of a useEffect function',
|
||||
category: ErrorCategory.Fire,
|
||||
severity: ErrorSeverity.Invariant,
|
||||
reason: CANNOT_COMPILE_FIRE,
|
||||
suggestions: null,
|
||||
|
||||
@@ -777,6 +777,15 @@ class Unifier {
|
||||
return {kind: 'Phi', operands: type.operands.map(o => this.get(o))};
|
||||
}
|
||||
|
||||
if (type.kind === 'Function') {
|
||||
return {
|
||||
kind: 'Function',
|
||||
isConstructor: type.isConstructor,
|
||||
shapeId: type.shapeId,
|
||||
return: this.get(type.return),
|
||||
};
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,10 +90,13 @@ export function Ok<T>(val: T): OkImpl<T> {
|
||||
}
|
||||
|
||||
class OkImpl<T> implements Result<T, never> {
|
||||
constructor(private val: T) {}
|
||||
#val: T;
|
||||
constructor(val: T) {
|
||||
this.#val = val;
|
||||
}
|
||||
|
||||
map<U>(fn: (val: T) => U): Result<U, never> {
|
||||
return new OkImpl(fn(this.val));
|
||||
return new OkImpl(fn(this.#val));
|
||||
}
|
||||
|
||||
mapErr<F>(_fn: (val: never) => F): Result<T, F> {
|
||||
@@ -101,15 +104,15 @@ class OkImpl<T> implements Result<T, never> {
|
||||
}
|
||||
|
||||
mapOr<U>(_fallback: U, fn: (val: T) => U): U {
|
||||
return fn(this.val);
|
||||
return fn(this.#val);
|
||||
}
|
||||
|
||||
mapOrElse<U>(_fallback: () => U, fn: (val: T) => U): U {
|
||||
return fn(this.val);
|
||||
return fn(this.#val);
|
||||
}
|
||||
|
||||
andThen<U>(fn: (val: T) => Result<U, never>): Result<U, never> {
|
||||
return fn(this.val);
|
||||
return fn(this.#val);
|
||||
}
|
||||
|
||||
and<U>(res: Result<U, never>): Result<U, never> {
|
||||
@@ -133,30 +136,30 @@ class OkImpl<T> implements Result<T, never> {
|
||||
}
|
||||
|
||||
expect(_msg: string): T {
|
||||
return this.val;
|
||||
return this.#val;
|
||||
}
|
||||
|
||||
expectErr(msg: string): never {
|
||||
throw new Error(`${msg}: ${this.val}`);
|
||||
throw new Error(`${msg}: ${this.#val}`);
|
||||
}
|
||||
|
||||
unwrap(): T {
|
||||
return this.val;
|
||||
return this.#val;
|
||||
}
|
||||
|
||||
unwrapOr(_fallback: T): T {
|
||||
return this.val;
|
||||
return this.#val;
|
||||
}
|
||||
|
||||
unwrapOrElse(_fallback: (val: never) => T): T {
|
||||
return this.val;
|
||||
return this.#val;
|
||||
}
|
||||
|
||||
unwrapErr(): never {
|
||||
if (this.val instanceof Error) {
|
||||
throw this.val;
|
||||
if (this.#val instanceof Error) {
|
||||
throw this.#val;
|
||||
}
|
||||
throw new Error(`Can't unwrap \`Ok\` to \`Err\`: ${this.val}`);
|
||||
throw new Error(`Can't unwrap \`Ok\` to \`Err\`: ${this.#val}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,14 +168,17 @@ export function Err<E>(val: E): ErrImpl<E> {
|
||||
}
|
||||
|
||||
class ErrImpl<E> implements Result<never, E> {
|
||||
constructor(private val: E) {}
|
||||
#val: E;
|
||||
constructor(val: E) {
|
||||
this.#val = val;
|
||||
}
|
||||
|
||||
map<U>(_fn: (val: never) => U): Result<U, E> {
|
||||
return this;
|
||||
}
|
||||
|
||||
mapErr<F>(fn: (val: E) => F): Result<never, F> {
|
||||
return new ErrImpl(fn(this.val));
|
||||
return new ErrImpl(fn(this.#val));
|
||||
}
|
||||
|
||||
mapOr<U>(fallback: U, _fn: (val: never) => U): U {
|
||||
@@ -196,7 +202,7 @@ class ErrImpl<E> implements Result<never, E> {
|
||||
}
|
||||
|
||||
orElse<F>(fn: (val: E) => ErrImpl<F>): Result<never, F> {
|
||||
return fn(this.val);
|
||||
return fn(this.#val);
|
||||
}
|
||||
|
||||
isOk(): this is OkImpl<never> {
|
||||
@@ -208,18 +214,18 @@ class ErrImpl<E> implements Result<never, E> {
|
||||
}
|
||||
|
||||
expect(msg: string): never {
|
||||
throw new Error(`${msg}: ${this.val}`);
|
||||
throw new Error(`${msg}: ${this.#val}`);
|
||||
}
|
||||
|
||||
expectErr(_msg: string): E {
|
||||
return this.val;
|
||||
return this.#val;
|
||||
}
|
||||
|
||||
unwrap(): never {
|
||||
if (this.val instanceof Error) {
|
||||
throw this.val;
|
||||
if (this.#val instanceof Error) {
|
||||
throw this.#val;
|
||||
}
|
||||
throw new Error(`Can't unwrap \`Err\` to \`Ok\`: ${this.val}`);
|
||||
throw new Error(`Can't unwrap \`Err\` to \`Ok\`: ${this.#val}`);
|
||||
}
|
||||
|
||||
unwrapOr<T>(fallback: T): T {
|
||||
@@ -227,10 +233,10 @@ class ErrImpl<E> implements Result<never, E> {
|
||||
}
|
||||
|
||||
unwrapOrElse<T>(fallback: (val: E) => T): T {
|
||||
return fallback(this.val);
|
||||
return fallback(this.#val);
|
||||
}
|
||||
|
||||
unwrapErr(): E {
|
||||
return this.val;
|
||||
return this.#val;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,6 +182,11 @@ export function parseConfigPragmaForTests(
|
||||
environment?: PartialEnvironmentConfig;
|
||||
},
|
||||
): PluginOptions {
|
||||
const overridePragma = parseConfigPragmaAsString(pragma);
|
||||
if (overridePragma !== '') {
|
||||
return parseConfigStringAsJS(overridePragma, defaults);
|
||||
}
|
||||
|
||||
const environment = parseConfigPragmaEnvironmentForTest(
|
||||
pragma,
|
||||
defaults.environment ?? {},
|
||||
@@ -217,3 +222,90 @@ export function parseConfigPragmaForTests(
|
||||
}
|
||||
return parsePluginOptions(options);
|
||||
}
|
||||
|
||||
export function parseConfigPragmaAsString(pragma: string): string {
|
||||
// Check if it's in JS override format
|
||||
for (const {key, value: val} of splitPragma(pragma)) {
|
||||
if (key === 'OVERRIDE' && val != null) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function parseConfigStringAsJS(
|
||||
configString: string,
|
||||
defaults: {
|
||||
compilationMode: CompilationMode;
|
||||
environment?: PartialEnvironmentConfig;
|
||||
},
|
||||
): PluginOptions {
|
||||
let parsedConfig: any;
|
||||
try {
|
||||
// Parse the JavaScript object literal
|
||||
parsedConfig = new Function(`return ${configString}`)();
|
||||
} catch (error) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Failed to parse config pragma as JavaScript object',
|
||||
description: `Could not parse: ${configString}. Error: ${error}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
console.log('OVERRIDE:', parsedConfig);
|
||||
|
||||
const environment = parseConfigPragmaEnvironmentForTest(
|
||||
'',
|
||||
defaults.environment ?? {},
|
||||
);
|
||||
|
||||
const options: Record<keyof PluginOptions, unknown> = {
|
||||
...defaultOptions,
|
||||
panicThreshold: 'all_errors',
|
||||
compilationMode: defaults.compilationMode,
|
||||
environment,
|
||||
};
|
||||
|
||||
// Apply parsed config, merging environment if it exists
|
||||
if (parsedConfig.environment) {
|
||||
const mergedEnvironment = {
|
||||
...(options.environment as Record<string, unknown>),
|
||||
...parsedConfig.environment,
|
||||
};
|
||||
|
||||
// Validate environment config
|
||||
const validatedEnvironment =
|
||||
EnvironmentConfigSchema.safeParse(mergedEnvironment);
|
||||
if (!validatedEnvironment.success) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Invalid environment configuration in config pragma',
|
||||
description: `${fromZodError(validatedEnvironment.error)}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
options.environment = validatedEnvironment.data;
|
||||
}
|
||||
|
||||
// Apply other config options
|
||||
for (const [key, value] of Object.entries(parsedConfig)) {
|
||||
if (key === 'environment') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasOwnProperty(defaultOptions, key)) {
|
||||
if (key === 'target' && value === 'donotuse_meta_internal') {
|
||||
options[key] = {
|
||||
kind: value,
|
||||
runtimeModule: 'react',
|
||||
};
|
||||
} else {
|
||||
options[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parsePluginOptions(options);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import * as t from '@babel/types';
|
||||
import {
|
||||
CompilerError,
|
||||
CompilerErrorDetail,
|
||||
ErrorCategory,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
|
||||
@@ -124,6 +125,7 @@ export function validateHooksUsage(
|
||||
recordError(
|
||||
place.loc,
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Hooks,
|
||||
description: null,
|
||||
reason,
|
||||
loc: place.loc,
|
||||
@@ -140,6 +142,7 @@ export function validateHooksUsage(
|
||||
recordError(
|
||||
place.loc,
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Hooks,
|
||||
description: null,
|
||||
reason:
|
||||
'Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values',
|
||||
@@ -157,6 +160,7 @@ export function validateHooksUsage(
|
||||
recordError(
|
||||
place.loc,
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Hooks,
|
||||
description: null,
|
||||
reason:
|
||||
'Hooks must be the same function on every render, but this value may change over time to a different function. See https://react.dev/reference/rules/react-calls-components-and-hooks#dont-dynamically-use-hooks',
|
||||
@@ -424,7 +428,7 @@ export function validateHooksUsage(
|
||||
}
|
||||
|
||||
for (const [, error] of errorsByPlace) {
|
||||
errors.push(error);
|
||||
errors.pushErrorDetail(error);
|
||||
}
|
||||
return errors.asResult();
|
||||
}
|
||||
@@ -448,6 +452,7 @@ function visitFunctionExpression(errors: CompilerError, fn: HIRFunction): void {
|
||||
if (hookKind != null) {
|
||||
errors.pushErrorDetail(
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Hooks,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason:
|
||||
'Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)',
|
||||
|
||||
@@ -6,13 +6,14 @@
|
||||
*/
|
||||
|
||||
import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {HIRFunction, IdentifierId, Place} from '../HIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
|
||||
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
|
||||
|
||||
/**
|
||||
* Validates that local variables cannot be reassigned after render.
|
||||
@@ -36,8 +37,9 @@ export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
|
||||
: 'variable';
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Immutability,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot reassign variable after render completes',
|
||||
reason: 'Cannot reassign variable after render completes',
|
||||
description: `Reassigning ${variable} after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
@@ -91,8 +93,9 @@ function getContextReassignment(
|
||||
: 'variable';
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Immutability,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot reassign variable in async function',
|
||||
reason: 'Cannot reassign variable in async function',
|
||||
description:
|
||||
'Reassigning a variable in an async function can cause inconsistent behavior on subsequent renders. Consider using state instead',
|
||||
}).withDetail({
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import {CompilerError, ErrorSeverity} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
Identifier,
|
||||
Instruction,
|
||||
@@ -108,6 +109,7 @@ class Visitor extends ReactiveFunctionVisitor<CompilerError> {
|
||||
isUnmemoized(deps.identifier, this.scopes))
|
||||
) {
|
||||
state.push({
|
||||
category: ErrorCategory.EffectDependencies,
|
||||
reason:
|
||||
'React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior',
|
||||
description: null,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import {CompilerError, EnvironmentConfig, ErrorSeverity} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {HIRFunction, IdentifierId} from '../HIR';
|
||||
import {DEFAULT_GLOBALS} from '../HIR/Globals';
|
||||
import {Result} from '../Utils/Result';
|
||||
@@ -56,6 +57,7 @@ export function validateNoCapitalizedCalls(
|
||||
const calleeName = capitalLoadGlobals.get(calleeIdentifier);
|
||||
if (calleeName != null) {
|
||||
CompilerError.throwInvalidReact({
|
||||
category: ErrorCategory.CapitalizedCalls,
|
||||
reason,
|
||||
description: `${calleeName} may be a component.`,
|
||||
loc: value.loc,
|
||||
@@ -79,6 +81,7 @@ export function validateNoCapitalizedCalls(
|
||||
const propertyName = capitalizedProperties.get(propertyIdentifier);
|
||||
if (propertyName != null) {
|
||||
errors.push({
|
||||
category: ErrorCategory.CapitalizedCalls,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason,
|
||||
description: `${propertyName} may be a component.`,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import {CompilerError, ErrorSeverity, SourceLocation} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
ArrayExpression,
|
||||
BlockId,
|
||||
@@ -219,6 +220,7 @@ function validateEffect(
|
||||
|
||||
for (const loc of setStateLocations) {
|
||||
errors.push({
|
||||
category: ErrorCategory.EffectDerivationsOfState,
|
||||
reason:
|
||||
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
|
||||
description: null,
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
*/
|
||||
|
||||
import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
FunctionEffect,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
isMutableEffect,
|
||||
isRefOrRefLikeMutableType,
|
||||
Place,
|
||||
} from '../HIR';
|
||||
@@ -18,8 +17,8 @@ import {
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {AliasingEffect} from '../Inference/AliasingEffects';
|
||||
import {Result} from '../Utils/Result';
|
||||
import {Iterable_some} from '../Utils/utils';
|
||||
|
||||
/**
|
||||
* Validates that functions with known mutations (ie due to types) cannot be passed
|
||||
@@ -50,14 +49,14 @@ export function validateNoFreezingKnownMutableFunctions(
|
||||
const errors = new CompilerError();
|
||||
const contextMutationEffects: Map<
|
||||
IdentifierId,
|
||||
Extract<FunctionEffect, {kind: 'ContextMutation'}>
|
||||
Extract<AliasingEffect, {kind: 'Mutate'} | {kind: 'MutateTransitive'}>
|
||||
> = new Map();
|
||||
|
||||
function visitOperand(operand: Place): void {
|
||||
if (operand.effect === Effect.Freeze) {
|
||||
const effect = contextMutationEffects.get(operand.identifier.id);
|
||||
if (effect != null) {
|
||||
const place = [...effect.places][0];
|
||||
const place = effect.value;
|
||||
const variable =
|
||||
place != null &&
|
||||
place.identifier.name != null &&
|
||||
@@ -66,8 +65,9 @@ export function validateNoFreezingKnownMutableFunctions(
|
||||
: 'a local variable';
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Immutability,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot modify local variables after render completes',
|
||||
reason: 'Cannot modify local variables after render completes',
|
||||
description: `This argument is a function which may reassign or mutate ${variable} after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.`,
|
||||
})
|
||||
.withDetail({
|
||||
@@ -77,7 +77,7 @@ export function validateNoFreezingKnownMutableFunctions(
|
||||
})
|
||||
.withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.loc,
|
||||
loc: effect.value.loc,
|
||||
message: `This modifies ${variable}`,
|
||||
}),
|
||||
);
|
||||
@@ -108,27 +108,7 @@ export function validateNoFreezingKnownMutableFunctions(
|
||||
break;
|
||||
}
|
||||
case 'FunctionExpression': {
|
||||
const knownMutation = (value.loweredFunc.func.effects ?? []).find(
|
||||
effect => {
|
||||
return (
|
||||
effect.kind === 'ContextMutation' &&
|
||||
(effect.effect === Effect.Store ||
|
||||
effect.effect === Effect.Mutate) &&
|
||||
Iterable_some(effect.places, place => {
|
||||
return (
|
||||
isMutableEffect(place.effect, place.loc) &&
|
||||
!isRefOrRefLikeMutableType(place.identifier.type)
|
||||
);
|
||||
})
|
||||
);
|
||||
},
|
||||
);
|
||||
if (knownMutation && knownMutation.kind === 'ContextMutation') {
|
||||
contextMutationEffects.set(lvalue.identifier.id, knownMutation);
|
||||
} else if (
|
||||
fn.env.config.enableNewMutationAliasingModel &&
|
||||
value.loweredFunc.func.aliasingEffects != null
|
||||
) {
|
||||
if (value.loweredFunc.func.aliasingEffects != null) {
|
||||
const context = new Set(
|
||||
value.loweredFunc.func.context.map(p => p.identifier.id),
|
||||
);
|
||||
@@ -149,12 +129,7 @@ export function validateNoFreezingKnownMutableFunctions(
|
||||
context.has(effect.value.identifier.id) &&
|
||||
!isRefOrRefLikeMutableType(effect.value.identifier.type)
|
||||
) {
|
||||
contextMutationEffects.set(lvalue.identifier.id, {
|
||||
kind: 'ContextMutation',
|
||||
effect: Effect.Mutate,
|
||||
loc: effect.value.loc,
|
||||
places: new Set([effect.value]),
|
||||
});
|
||||
contextMutationEffects.set(lvalue.identifier.id, effect);
|
||||
break effects;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
*/
|
||||
|
||||
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {HIRFunction} from '../HIR';
|
||||
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
|
||||
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
/**
|
||||
@@ -36,7 +37,8 @@ export function validateNoImpureFunctionsInRender(
|
||||
if (signature != null && signature.impure === true) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: 'Cannot call impure function during render',
|
||||
category: ErrorCategory.Purity,
|
||||
reason: 'Cannot call impure function during render',
|
||||
description:
|
||||
(signature.canonicalName != null
|
||||
? `\`${signature.canonicalName}\` is an impure function. `
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {BlockId, HIRFunction} from '../HIR';
|
||||
import {Result} from '../Utils/Result';
|
||||
import {retainWhere} from '../Utils/utils';
|
||||
@@ -36,8 +37,9 @@ export function validateNoJSXInTryStatement(
|
||||
case 'JsxFragment': {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.ErrorBoundaries,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Avoid constructing JSX within try/catch',
|
||||
reason: 'Avoid constructing JSX within try/catch',
|
||||
description: `React does not immediately render components when JSX is rendered, so any errors from this component will not be caught by the try/catch. To catch errors in rendering a given component, wrap that component in an error boundary. (https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {
|
||||
@@ -468,8 +469,9 @@ function validateNoRefAccessInRenderImpl(
|
||||
didError = true;
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot access refs during render',
|
||||
reason: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
@@ -731,8 +733,9 @@ function guardCheck(errors: CompilerError, operand: Place, env: Env): void {
|
||||
if (env.get(operand.identifier.id)?.kind === 'Guard') {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot access refs during render',
|
||||
reason: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
@@ -755,8 +758,9 @@ function validateNoRefValueAccess(
|
||||
) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot access refs during render',
|
||||
reason: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
@@ -781,8 +785,9 @@ function validateNoRefPassedToFunction(
|
||||
) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot access refs during render',
|
||||
reason: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
@@ -803,8 +808,9 @@ function validateNoRefUpdate(
|
||||
if (type?.kind === 'Ref' || type?.kind === 'RefValue') {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot access refs during render',
|
||||
reason: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
@@ -824,8 +830,9 @@ function validateNoDirectRefValueAccess(
|
||||
if (type?.kind === 'RefValue') {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot access refs during render',
|
||||
reason: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {
|
||||
@@ -96,7 +97,8 @@ export function validateNoSetStateInEffects(
|
||||
if (setState !== undefined) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category:
|
||||
category: ErrorCategory.EffectSetState,
|
||||
reason:
|
||||
'Calling setState synchronously within an effect can trigger cascading renders',
|
||||
description:
|
||||
'Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. ' +
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {HIRFunction, IdentifierId, isSetStateType} from '../HIR';
|
||||
@@ -128,7 +129,8 @@ function validateNoSetStateInRenderImpl(
|
||||
if (activeManualMemoId !== null) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category:
|
||||
category: ErrorCategory.RenderSetState,
|
||||
reason:
|
||||
'Calling setState from useMemo may trigger an infinite loop',
|
||||
description:
|
||||
'Each time the memo callback is evaluated it will change state. This can cause a memoization dependency to change, running the memo function again and causing an infinite loop. Instead of setting state in useMemo(), prefer deriving the value during render. (https://react.dev/reference/react/useState)',
|
||||
@@ -143,7 +145,8 @@ function validateNoSetStateInRenderImpl(
|
||||
} else if (unconditionalBlocks.has(block.id)) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category:
|
||||
category: ErrorCategory.RenderSetState,
|
||||
reason:
|
||||
'Calling setState during render may trigger an infinite loop',
|
||||
description:
|
||||
'Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState)',
|
||||
@@ -152,7 +155,7 @@ function validateNoSetStateInRenderImpl(
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: callee.loc,
|
||||
message: 'Found setState() within useMemo()',
|
||||
message: 'Found setState() in render',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {
|
||||
@@ -281,9 +282,9 @@ function validateInferredDep(
|
||||
}
|
||||
errorState.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.PreserveManualMemo,
|
||||
severity: ErrorSeverity.CannotPreserveMemoization,
|
||||
category:
|
||||
'Compilation skipped because existing memoization could not be preserved',
|
||||
reason: '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. ',
|
||||
@@ -535,9 +536,9 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
) {
|
||||
state.errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.PreserveManualMemo,
|
||||
severity: ErrorSeverity.CannotPreserveMemoization,
|
||||
category:
|
||||
'Compilation skipped because existing memoization could not be preserved',
|
||||
reason: '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.',
|
||||
@@ -583,9 +584,9 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
if (isUnmemoized(identifier, this.scopes)) {
|
||||
state.errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.PreserveManualMemo,
|
||||
severity: ErrorSeverity.CannotPreserveMemoization,
|
||||
category:
|
||||
'Compilation skipped because existing memoization could not be preserved',
|
||||
reason: '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
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {HIRFunction, IdentifierId, SourceLocation} from '../HIR';
|
||||
@@ -65,8 +66,9 @@ export function validateStaticComponents(
|
||||
if (location != null) {
|
||||
error.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.StaticComponents,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot create components during render',
|
||||
reason: 'Cannot create components during render',
|
||||
description: `Components created during render will reset their state each time they are created. Declare components outside of render. `,
|
||||
})
|
||||
.withDetail({
|
||||
|
||||
@@ -8,14 +8,18 @@
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {SuppressionRange, suppressionsToCompilerError} from '../Entrypoint';
|
||||
import {FunctionExpression, HIRFunction, IdentifierId} from '../HIR';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
const errors = new CompilerError();
|
||||
const suppressions: Array<SuppressionRange> = [];
|
||||
const useMemos = new Set<IdentifierId>();
|
||||
const effects = new Set<IdentifierId>();
|
||||
const react = new Set<IdentifierId>();
|
||||
const functions = new Map<IdentifierId, FunctionExpression>();
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
@@ -24,6 +28,12 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
case 'LoadGlobal': {
|
||||
if (value.binding.name === 'useMemo') {
|
||||
useMemos.add(lvalue.identifier.id);
|
||||
} else if (
|
||||
value.binding.name === 'useEffect' ||
|
||||
value.binding.name === 'useLayoutEffect' ||
|
||||
value.binding.name === 'useInsertionEffect'
|
||||
) {
|
||||
effects.add(lvalue.identifier.id);
|
||||
} else if (value.binding.name === 'React') {
|
||||
react.add(lvalue.identifier.id);
|
||||
}
|
||||
@@ -33,6 +43,12 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
if (react.has(value.object.identifier.id)) {
|
||||
if (value.property === 'useMemo') {
|
||||
useMemos.add(lvalue.identifier.id);
|
||||
} else if (
|
||||
value.property === 'useEffect' ||
|
||||
value.property === 'useLayoutEffect' ||
|
||||
value.property === 'useInsertionEffect'
|
||||
) {
|
||||
effects.add(lvalue.identifier.id);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -46,9 +62,18 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
// Is the function being called useMemo, with at least 1 argument?
|
||||
const callee =
|
||||
value.kind === 'CallExpression'
|
||||
? value.callee.identifier.id
|
||||
: value.property.identifier.id;
|
||||
const isUseMemo = useMemos.has(callee);
|
||||
? value.callee.identifier
|
||||
: value.property.identifier;
|
||||
const isEffect = effects.has(callee.id);
|
||||
if (
|
||||
!isEffect &&
|
||||
value.suppressions != null &&
|
||||
value.suppressions.length !== 0
|
||||
) {
|
||||
suppressions.push(...value.suppressions);
|
||||
}
|
||||
|
||||
const isUseMemo = useMemos.has(callee.id);
|
||||
if (!isUseMemo || value.args.length === 0) {
|
||||
continue;
|
||||
}
|
||||
@@ -74,8 +99,9 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
: firstParam.place.loc;
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'useMemo() callbacks may not accept parameters',
|
||||
reason: 'useMemo() callbacks may not accept parameters',
|
||||
description:
|
||||
'useMemo() callbacks are called by React to cache calculations across re-renders. They should not take parameters. Instead, directly reference the props, state, or local variables needed for the computation.',
|
||||
suggestions: null,
|
||||
@@ -90,8 +116,9 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
if (body.loweredFunc.func.async || body.loweredFunc.func.generator) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category:
|
||||
reason:
|
||||
'useMemo() callbacks may not be async or generator functions',
|
||||
description:
|
||||
'useMemo() callbacks are called once and must synchronously return a value.',
|
||||
@@ -109,5 +136,9 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (suppressions.length !== 0) {
|
||||
const suppressionError = suppressionsToCompilerError(suppressions);
|
||||
errors.merge(suppressionError);
|
||||
}
|
||||
return errors.asResult();
|
||||
}
|
||||
|
||||
@@ -46,14 +46,16 @@ function useFoo(t0) {
|
||||
t1 = $[0];
|
||||
}
|
||||
let items = t1;
|
||||
bb0: if ($[1] !== cond) {
|
||||
if (cond) {
|
||||
items = [];
|
||||
} else {
|
||||
break bb0;
|
||||
}
|
||||
if ($[1] !== cond) {
|
||||
bb0: {
|
||||
if (cond) {
|
||||
items = [];
|
||||
} else {
|
||||
break bb0;
|
||||
}
|
||||
|
||||
items.push(2);
|
||||
items.push(2);
|
||||
}
|
||||
$[1] = cond;
|
||||
$[2] = items;
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
function Component() {
|
||||
const foo = () => {
|
||||
someGlobal = true;
|
||||
};
|
||||
// spreading a function is weird, but it doesn't call the function so this is allowed
|
||||
return <div {...foo} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
|
||||
function Component() {
|
||||
const $ = _c(1);
|
||||
const foo = _temp;
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <div {...foo} />;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
function _temp() {
|
||||
someGlobal = true;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -3,5 +3,6 @@ function Component() {
|
||||
const foo = () => {
|
||||
someGlobal = true;
|
||||
};
|
||||
// spreading a function is weird, but it doesn't call the function so this is allowed
|
||||
return <div {...foo} />;
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
|
||||
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* 1. `InferMutableRanges` derives the mutable range of identifiers and their
|
||||
* aliases from `LoadLocal`, `PropertyLoad`, etc
|
||||
* - After this pass, y's mutable range only extends to `arrayPush(x, y)`
|
||||
* - We avoid assigning mutable ranges to loads after y's mutable range, as
|
||||
* these are working with an immutable value. As a result, `LoadLocal y` and
|
||||
* `PropertyLoad y` do not get mutable ranges
|
||||
* 2. `InferReactiveScopeVariables` extends mutable ranges and creates scopes,
|
||||
* as according to the 'co-mutation' of different values
|
||||
* - Here, we infer that
|
||||
* - `arrayPush(y, x)` might alias `x` and `y` to each other
|
||||
* - `setPropertyKey(x, ...)` may mutate both `x` and `y`
|
||||
* - This pass correctly extends the mutable range of `y`
|
||||
* - Since we didn't run `InferMutableRange` logic again, the LoadLocal /
|
||||
* PropertyLoads still don't have a mutable range
|
||||
*
|
||||
* Note that the this bug is an edge case. Compiler output is only invalid for:
|
||||
* - function expressions with
|
||||
* `enableTransitivelyFreezeFunctionExpressions:false`
|
||||
* - functions that throw and get retried without clearing the memocache
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}</div>
|
||||
* Forget:
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
|
||||
*/
|
||||
function useFoo({a, b}: {a: number, b: number}) {
|
||||
const x = [];
|
||||
const y = {value: a};
|
||||
|
||||
arrayPush(x, y); // x and y co-mutate
|
||||
const y_alias = y;
|
||||
const cb = () => y_alias.value;
|
||||
setPropertyByKey(x[0], 'value', b); // might overwrite y.value
|
||||
return <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{a: 2, b: 10}],
|
||||
sequentialRenders: [
|
||||
{a: 2, b: 10},
|
||||
{a: 2, b: 11},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { arrayPush, setPropertyByKey, Stringify } from "shared-runtime";
|
||||
|
||||
function useFoo(t0) {
|
||||
const $ = _c(5);
|
||||
const { a, b } = t0;
|
||||
let t1;
|
||||
if ($[0] !== a || $[1] !== b) {
|
||||
const x = [];
|
||||
const y = { value: a };
|
||||
|
||||
arrayPush(x, y);
|
||||
const y_alias = y;
|
||||
let t2;
|
||||
if ($[3] !== y_alias.value) {
|
||||
t2 = () => y_alias.value;
|
||||
$[3] = y_alias.value;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
const cb = t2;
|
||||
setPropertyByKey(x[0], "value", b);
|
||||
t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
$[0] = a;
|
||||
$[1] = b;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{ a: 2, b: 10 }],
|
||||
sequentialRenders: [
|
||||
{ a: 2, b: 10 },
|
||||
{ a: 2, b: 11 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
|
||||
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* 1. `InferMutableRanges` derives the mutable range of identifiers and their
|
||||
* aliases from `LoadLocal`, `PropertyLoad`, etc
|
||||
* - After this pass, y's mutable range only extends to `arrayPush(x, y)`
|
||||
* - We avoid assigning mutable ranges to loads after y's mutable range, as
|
||||
* these are working with an immutable value. As a result, `LoadLocal y` and
|
||||
* `PropertyLoad y` do not get mutable ranges
|
||||
* 2. `InferReactiveScopeVariables` extends mutable ranges and creates scopes,
|
||||
* as according to the 'co-mutation' of different values
|
||||
* - Here, we infer that
|
||||
* - `arrayPush(y, x)` might alias `x` and `y` to each other
|
||||
* - `setPropertyKey(x, ...)` may mutate both `x` and `y`
|
||||
* - This pass correctly extends the mutable range of `y`
|
||||
* - Since we didn't run `InferMutableRange` logic again, the LoadLocal /
|
||||
* PropertyLoads still don't have a mutable range
|
||||
*
|
||||
* Note that the this bug is an edge case. Compiler output is only invalid for:
|
||||
* - function expressions with
|
||||
* `enableTransitivelyFreezeFunctionExpressions:false`
|
||||
* - functions that throw and get retried without clearing the memocache
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}</div>
|
||||
* Forget:
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
|
||||
*/
|
||||
function useFoo({a, b}: {a: number, b: number}) {
|
||||
const x = [];
|
||||
const y = {value: a};
|
||||
|
||||
arrayPush(x, y); // x and y co-mutate
|
||||
const y_alias = y;
|
||||
const cb = () => y_alias.value;
|
||||
setPropertyByKey(x[0], 'value', b); // might overwrite y.value
|
||||
return <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{a: 2, b: 10}],
|
||||
sequentialRenders: [
|
||||
{a: 2, b: 10},
|
||||
{a: 2, b: 11},
|
||||
],
|
||||
};
|
||||
@@ -1,87 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
|
||||
import {setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Variation of bug in `bug-aliased-capture-aliased-mutate`
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
|
||||
* Forget:
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
|
||||
*/
|
||||
|
||||
function useFoo({a}: {a: number, b: number}) {
|
||||
const arr = [];
|
||||
const obj = {value: a};
|
||||
|
||||
setPropertyByKey(obj, 'arr', arr);
|
||||
const obj_alias = obj;
|
||||
const cb = () => obj_alias.arr.length;
|
||||
for (let i = 0; i < a; i++) {
|
||||
arr.push(i);
|
||||
}
|
||||
return <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{a: 2}],
|
||||
sequentialRenders: [{a: 2}, {a: 3}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { setPropertyByKey, Stringify } from "shared-runtime";
|
||||
|
||||
function useFoo(t0) {
|
||||
const $ = _c(4);
|
||||
const { a } = t0;
|
||||
let t1;
|
||||
if ($[0] !== a) {
|
||||
const arr = [];
|
||||
const obj = { value: a };
|
||||
|
||||
setPropertyByKey(obj, "arr", arr);
|
||||
const obj_alias = obj;
|
||||
let t2;
|
||||
if ($[2] !== obj_alias.arr.length) {
|
||||
t2 = () => obj_alias.arr.length;
|
||||
$[2] = obj_alias.arr.length;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const cb = t2;
|
||||
for (let i = 0; i < a; i++) {
|
||||
arr.push(i);
|
||||
}
|
||||
|
||||
t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
$[0] = a;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{ a: 2 }],
|
||||
sequentialRenders: [{ a: 2 }, { a: 3 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
|
||||
import {setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Variation of bug in `bug-aliased-capture-aliased-mutate`
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
|
||||
* Forget:
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
|
||||
*/
|
||||
|
||||
function useFoo({a}: {a: number, b: number}) {
|
||||
const arr = [];
|
||||
const obj = {value: a};
|
||||
|
||||
setPropertyByKey(obj, 'arr', arr);
|
||||
const obj_alias = obj;
|
||||
const cb = () => obj_alias.arr.length;
|
||||
for (let i = 0; i < a; i++) {
|
||||
arr.push(i);
|
||||
}
|
||||
return <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{a: 2}],
|
||||
sequentialRenders: [{a: 2}, {a: 3}],
|
||||
};
|
||||
@@ -85,19 +85,11 @@ import { makeArray, mutate } from "shared-runtime";
|
||||
* used when we analyze CallExpressions.
|
||||
*/
|
||||
function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const $ = _c(3);
|
||||
const { foo, bar } = t0;
|
||||
let t1;
|
||||
if ($[0] !== foo) {
|
||||
t1 = { foo };
|
||||
$[0] = foo;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const x = t1;
|
||||
let y;
|
||||
if ($[2] !== bar || $[3] !== x) {
|
||||
if ($[0] !== bar || $[1] !== foo) {
|
||||
const x = { foo };
|
||||
y = { bar };
|
||||
const f0 = function () {
|
||||
const a = makeArray(y);
|
||||
@@ -108,11 +100,11 @@ function Component(t0) {
|
||||
|
||||
f0();
|
||||
mutate(y.x);
|
||||
$[2] = bar;
|
||||
$[3] = x;
|
||||
$[4] = y;
|
||||
$[0] = bar;
|
||||
$[1] = foo;
|
||||
$[2] = y;
|
||||
} else {
|
||||
y = $[4];
|
||||
y = $[2];
|
||||
}
|
||||
return y;
|
||||
}
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Fixture showing an edge case for ReactiveScope variable propagation.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
|
||||
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
|
||||
* Forget:
|
||||
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
|
||||
* [[ (exception in render) Error: invariant broken ]]
|
||||
*
|
||||
*/
|
||||
function Component() {
|
||||
const obj = CONST_TRUE ? {inner: {value: 'hello'}} : null;
|
||||
const boxedInner = [obj?.inner];
|
||||
useIdentity(null);
|
||||
mutate(obj);
|
||||
if (boxedInner[0] !== obj?.inner) {
|
||||
throw new Error('invariant broken');
|
||||
}
|
||||
return <Stringify obj={obj} inner={boxedInner} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{arg: 0}],
|
||||
sequentialRenders: [{arg: 0}, {arg: 1}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
|
||||
import { CONST_TRUE, Stringify, mutate, useIdentity } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* Fixture showing an edge case for ReactiveScope variable propagation.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
|
||||
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
|
||||
* Forget:
|
||||
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
|
||||
* [[ (exception in render) Error: invariant broken ]]
|
||||
*
|
||||
*/
|
||||
function Component() {
|
||||
const $ = _c(4);
|
||||
const obj = CONST_TRUE ? { inner: { value: "hello" } } : null;
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = [obj?.inner];
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const boxedInner = t0;
|
||||
useIdentity(null);
|
||||
mutate(obj);
|
||||
if (boxedInner[0] !== obj?.inner) {
|
||||
throw new Error("invariant broken");
|
||||
}
|
||||
let t1;
|
||||
if ($[1] !== boxedInner || $[2] !== obj) {
|
||||
t1 = <Stringify obj={obj} inner={boxedInner} />;
|
||||
$[1] = boxedInner;
|
||||
$[2] = obj;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ arg: 0 }],
|
||||
sequentialRenders: [{ arg: 0 }, { arg: 1 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Fixture showing an edge case for ReactiveScope variable propagation.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
|
||||
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
|
||||
* Forget:
|
||||
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
|
||||
* [[ (exception in render) Error: invariant broken ]]
|
||||
*
|
||||
*/
|
||||
function Component() {
|
||||
const obj = CONST_TRUE ? {inner: {value: 'hello'}} : null;
|
||||
const boxedInner = [obj?.inner];
|
||||
useIdentity(null);
|
||||
mutate(obj);
|
||||
if (boxedInner[0] !== obj?.inner) {
|
||||
throw new Error('invariant broken');
|
||||
}
|
||||
return <Stringify obj={obj} inner={boxedInner} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{arg: 0}],
|
||||
sequentialRenders: [{arg: 0}, {arg: 1}],
|
||||
};
|
||||
@@ -1,110 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {identity, mutate} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
|
||||
* with the mutation hoisted to a named variable instead of being directly
|
||||
* inlined into the Object key.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* Forget:
|
||||
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
|
||||
*/
|
||||
function Component(props) {
|
||||
const key = {};
|
||||
const tmp = (mutate(key), key);
|
||||
const context = {
|
||||
// Here, `tmp` is frozen (as it's inferred to be a primitive/string)
|
||||
[tmp]: identity([props.value]),
|
||||
};
|
||||
mutate(key);
|
||||
return [context, key];
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
sequentialRenders: [{value: 42}, {value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
|
||||
import { identity, mutate } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
|
||||
* with the mutation hoisted to a named variable instead of being directly
|
||||
* inlined into the Object key.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* Forget:
|
||||
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
|
||||
*/
|
||||
function Component(props) {
|
||||
const $ = _c(8);
|
||||
let key;
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
key = {};
|
||||
t0 = (mutate(key), key);
|
||||
$[0] = key;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
key = $[0];
|
||||
t0 = $[1];
|
||||
}
|
||||
const tmp = t0;
|
||||
let t1;
|
||||
if ($[2] !== props.value) {
|
||||
t1 = identity([props.value]);
|
||||
$[2] = props.value;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
let t2;
|
||||
if ($[4] !== t1) {
|
||||
t2 = { [tmp]: t1 };
|
||||
$[4] = t1;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
const context = t2;
|
||||
|
||||
mutate(key);
|
||||
let t3;
|
||||
if ($[6] !== context) {
|
||||
t3 = [context, key];
|
||||
$[6] = context;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: 42 }],
|
||||
sequentialRenders: [{ value: 42 }, { value: 42 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {identity, mutate} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
|
||||
* with the mutation hoisted to a named variable instead of being directly
|
||||
* inlined into the Object key.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* Forget:
|
||||
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
|
||||
*/
|
||||
function Component(props) {
|
||||
const key = {};
|
||||
const tmp = (mutate(key), key);
|
||||
const context = {
|
||||
// Here, `tmp` is frozen (as it's inferred to be a primitive/string)
|
||||
[tmp]: identity([props.value]),
|
||||
};
|
||||
mutate(key);
|
||||
return [context, key];
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
sequentialRenders: [{value: 42}, {value: 42}],
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component(props) {
|
||||
useEffect(
|
||||
() => {
|
||||
console.log(props.value);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
return <div />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function Component(props) {
|
||||
const $ = _c(4);
|
||||
let t0;
|
||||
if ($[0] !== props.value) {
|
||||
t0 = () => {
|
||||
console.log(props.value);
|
||||
};
|
||||
$[0] = props.value;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
let t1;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = [];
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
useEffect(t0, t1);
|
||||
let t2;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <div />;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,10 @@
|
||||
function Component(props) {
|
||||
useEffect(
|
||||
() => {
|
||||
console.log(props.value);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
return <div />;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component(props) {
|
||||
useEffect(() => {
|
||||
console.log(props.value);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
return <div />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function Component(props) {
|
||||
const $ = _c(4);
|
||||
let t0;
|
||||
if ($[0] !== props.value) {
|
||||
t0 = () => {
|
||||
console.log(props.value);
|
||||
};
|
||||
$[0] = props.value;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
let t1;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = [];
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
useEffect(t0, t1);
|
||||
let t2;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <div />;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,7 @@
|
||||
function Component(props) {
|
||||
useEffect(() => {
|
||||
console.log(props.value);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
return <div />;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
|
||||
import {useMemo} from 'react';
|
||||
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const result = useMemo(
|
||||
() => makeObject(props.value).value + 1,
|
||||
[props.value]
|
||||
);
|
||||
console.log(result);
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
function makeObject(value) {
|
||||
console.log(value);
|
||||
return {value};
|
||||
}
|
||||
|
||||
export const TODO_FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
sequentialRenders: [
|
||||
{value: 42},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
{value: 3.14},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
|
||||
import { useMemo } from "react";
|
||||
import { makeObject_Primitives, ValidateMemoization } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const result = makeObject(props.value).value + 1;
|
||||
|
||||
console.log(result);
|
||||
return "ok";
|
||||
}
|
||||
|
||||
function makeObject(value) {
|
||||
console.log(value);
|
||||
return { value };
|
||||
}
|
||||
|
||||
export const TODO_FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: 42 }],
|
||||
sequentialRenders: [
|
||||
{ value: 42 },
|
||||
{ value: 42 },
|
||||
{ value: 3.14 },
|
||||
{ value: 3.14 },
|
||||
{ value: 42 },
|
||||
{ value: 3.14 },
|
||||
{ value: 42 },
|
||||
{ value: 3.14 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,32 @@
|
||||
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
|
||||
import {useMemo} from 'react';
|
||||
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const result = useMemo(
|
||||
() => makeObject(props.value).value + 1,
|
||||
[props.value]
|
||||
);
|
||||
console.log(result);
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
function makeObject(value) {
|
||||
console.log(value);
|
||||
return {value};
|
||||
}
|
||||
|
||||
export const TODO_FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
sequentialRenders: [
|
||||
{value: 42},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
{value: 3.14},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
|
||||
import {useMemo} from 'react';
|
||||
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const result = makeObject(props.value).value + 1;
|
||||
console.log(result);
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
function makeObject(value) {
|
||||
console.log(value);
|
||||
return {value};
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
sequentialRenders: [
|
||||
{value: 42},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
{value: 3.14},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
|
||||
import { useMemo } from "react";
|
||||
import { makeObject_Primitives, ValidateMemoization } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const result = makeObject(props.value).value + 1;
|
||||
console.log(result);
|
||||
return "ok";
|
||||
}
|
||||
|
||||
function makeObject(value) {
|
||||
console.log(value);
|
||||
return { value };
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: 42 }],
|
||||
sequentialRenders: [
|
||||
{ value: 42 },
|
||||
{ value: 42 },
|
||||
{ value: 3.14 },
|
||||
{ value: 3.14 },
|
||||
{ value: 42 },
|
||||
{ value: 3.14 },
|
||||
{ value: 42 },
|
||||
{ value: 3.14 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) "ok"
|
||||
"ok"
|
||||
"ok"
|
||||
"ok"
|
||||
"ok"
|
||||
"ok"
|
||||
"ok"
|
||||
"ok"
|
||||
logs: [42,43,42,43,3.14,4.140000000000001,3.14,4.140000000000001,42,43,3.14,4.140000000000001,42,43,3.14,4.140000000000001]
|
||||
@@ -0,0 +1,29 @@
|
||||
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
|
||||
import {useMemo} from 'react';
|
||||
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const result = makeObject(props.value).value + 1;
|
||||
console.log(result);
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
function makeObject(value) {
|
||||
console.log(value);
|
||||
return {value};
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
sequentialRenders: [
|
||||
{value: 42},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
{value: 3.14},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
],
|
||||
};
|
||||
@@ -24,9 +24,18 @@ function useThing(fn) {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Expected a non-reserved identifier name
|
||||
Error: `this` is not supported syntax
|
||||
|
||||
`this` is a reserved word in JavaScript and cannot be used as an identifier name.
|
||||
React Compiler does not support compiling functions that use `this`
|
||||
|
||||
error.reserved-words.ts:8:28
|
||||
6 |
|
||||
7 | if (ref.current === null) {
|
||||
> 8 | ref.current = function (this: unknown, ...args) {
|
||||
| ^^^^^^^^^^^^^ `this` was used here
|
||||
9 | return fnRef.current.call(this, ...args);
|
||||
10 | };
|
||||
11 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
function Component() {
|
||||
const foo = () => {
|
||||
someGlobal = true;
|
||||
};
|
||||
return <div {...foo} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## 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)
|
||||
5 | };
|
||||
6 | return <div {...foo} />;
|
||||
7 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// Fixture to test that we show a hint to name as `ref` or `-Ref` when attempting
|
||||
// to assign .current inside an effect
|
||||
function Component({foo}) {
|
||||
useEffect(() => {
|
||||
foo.current = true;
|
||||
}, [foo]);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: This value cannot be modified
|
||||
|
||||
Modifying component props or hook arguments is not allowed. Consider using a local variable instead.
|
||||
|
||||
error.assign-ref-in-effect-hint.ts:5:4
|
||||
3 | function Component({foo}) {
|
||||
4 | useEffect(() => {
|
||||
> 5 | foo.current = true;
|
||||
| ^^^ `foo` cannot be modified
|
||||
6 | }, [foo]);
|
||||
7 | }
|
||||
8 |
|
||||
|
||||
Hint: If this value is a Ref (value returned by `useRef()`), rename the variable to end in "Ref".
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// Fixture to test that we show a hint to name as `ref` or `-Ref` when attempting
|
||||
// to assign .current inside an effect
|
||||
function Component({foo}) {
|
||||
useEffect(() => {
|
||||
foo.current = true;
|
||||
}, [foo]);
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel:false
|
||||
|
||||
import {useCallback, useEffect, useRef} from 'react';
|
||||
import {useHook} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const params = useHook();
|
||||
const update = useCallback(
|
||||
partialParams => {
|
||||
const nextParams = {
|
||||
...params,
|
||||
...partialParams,
|
||||
};
|
||||
nextParams.param = 'value';
|
||||
console.log(nextParams);
|
||||
},
|
||||
[params]
|
||||
);
|
||||
const ref = useRef(null);
|
||||
useEffect(() => {
|
||||
if (ref.current === null) {
|
||||
update();
|
||||
}
|
||||
}, [update]);
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## 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(() => {
|
||||
| ^^^^^^^
|
||||
> 21 | if (ref.current === null) {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 22 | update();
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 23 | }
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 24 | }, [update]);
|
||||
| ^^^^ This function may (indirectly) reassign or modify a local variable after render
|
||||
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]
|
||||
```
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
// @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel:false
|
||||
|
||||
import {useCallback, useEffect, useRef} from 'react';
|
||||
import {useHook} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const params = useHook();
|
||||
const update = useCallback(
|
||||
partialParams => {
|
||||
const nextParams = {
|
||||
...params,
|
||||
...partialParams,
|
||||
};
|
||||
nextParams.param = 'value';
|
||||
console.log(nextParams);
|
||||
},
|
||||
[params]
|
||||
);
|
||||
const ref = useRef(null);
|
||||
useEffect(() => {
|
||||
if (ref.current === null) {
|
||||
update();
|
||||
}
|
||||
}, [update]);
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
@@ -26,7 +26,7 @@ function Component(props) {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Memoization: Compilation skipped because existing memoization could not be preserved
|
||||
Compilation Skipped: Existing memoization could not be preserved
|
||||
|
||||
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. The inferred dependency was `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source.
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user