Compare commits
82 Commits
| 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 |
@@ -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,
|
||||
@@ -121,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(
|
||||
@@ -134,6 +135,7 @@ function run(
|
||||
filename,
|
||||
code,
|
||||
programContext,
|
||||
suppressions,
|
||||
);
|
||||
env.logger?.debugLogIRs?.({
|
||||
kind: 'debug',
|
||||
@@ -567,6 +569,7 @@ export function compileFn(
|
||||
logger: Logger | null,
|
||||
filename: string | null,
|
||||
code: string | null,
|
||||
singleLineSuppressions: Array<SingleLineSuppressionRange>,
|
||||
): CodegenFunction {
|
||||
return run(
|
||||
func,
|
||||
@@ -577,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([
|
||||
@@ -650,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>;
|
||||
@@ -694,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>;
|
||||
@@ -712,6 +725,7 @@ export class Environment {
|
||||
filename: string | null,
|
||||
code: string | null,
|
||||
programContext: ProgramContext,
|
||||
suppressions: Array<SingleLineSuppressionRange>,
|
||||
) {
|
||||
this.#scope = scope;
|
||||
this.fnType = fnType;
|
||||
@@ -725,6 +739,7 @@ export class Environment {
|
||||
this.#globals = new Map(DEFAULT_GLOBALS);
|
||||
this.hasFireRewrite = false;
|
||||
this.hasInferredEffect = false;
|
||||
this.suppressions = suppressions;
|
||||
|
||||
if (
|
||||
config.disableMemoizationForDebugging &&
|
||||
@@ -853,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 =
|
||||
|
||||
@@ -231,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(':');
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
InstructionKind,
|
||||
InstructionValue,
|
||||
isArrayType,
|
||||
isJsxType,
|
||||
isMapType,
|
||||
isPrimitiveType,
|
||||
isRefOrRefValue,
|
||||
@@ -69,6 +70,7 @@ import {
|
||||
hashEffect,
|
||||
MutationReason,
|
||||
} from './AliasingEffects';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
@@ -452,8 +454,9 @@ function applySignature(
|
||||
? `\`${effect.value.identifier.name.value}\``
|
||||
: 'value';
|
||||
const diagnostic = CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Immutability,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'This value cannot be modified',
|
||||
reason: 'This value cannot be modified',
|
||||
description: `${reason}.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
@@ -465,8 +468,7 @@ function applySignature(
|
||||
effect.reason?.kind === 'AssignCurrentProperty'
|
||||
) {
|
||||
diagnostic.withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
kind: 'hint',
|
||||
message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`,
|
||||
});
|
||||
}
|
||||
@@ -1037,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) {
|
||||
@@ -1076,8 +1079,9 @@ function applyEffect(
|
||||
? `\`${effect.value.identifier.name.value}\``
|
||||
: 'value';
|
||||
const diagnostic = CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Immutability,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'This value cannot be modified',
|
||||
reason: 'This value cannot be modified',
|
||||
description: `${reason}.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
@@ -1089,8 +1093,7 @@ function applyEffect(
|
||||
effect.reason?.kind === 'AssignCurrentProperty'
|
||||
) {
|
||||
diagnostic.withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
kind: 'hint',
|
||||
message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`,
|
||||
});
|
||||
}
|
||||
@@ -1869,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;
|
||||
}
|
||||
@@ -2035,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({
|
||||
@@ -2068,7 +2089,7 @@ function computeSignatureForInstruction(
|
||||
effects.push({
|
||||
kind: 'Freeze',
|
||||
value: operand,
|
||||
reason: ValueReason.Other,
|
||||
reason: ValueReason.HookCaptured,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2134,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. `
|
||||
@@ -2148,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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {HIRFunction, IdentifierId, Place} from '../HIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
@@ -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,24 +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 (value.loweredFunc.func.aliasingEffects != null) {
|
||||
if (value.loweredFunc.func.aliasingEffects != null) {
|
||||
const context = new Set(
|
||||
value.loweredFunc.func.context.map(p => p.identifier.id),
|
||||
);
|
||||
@@ -146,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,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {HIRFunction} from '../HIR';
|
||||
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,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 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -30,13 +30,7 @@ Modifying a value returned from a hook is not allowed. Consider moving the modif
|
||||
7 | }
|
||||
8 |
|
||||
|
||||
3 | component Foo() {
|
||||
4 | const foo = useFoo();
|
||||
> 5 | foo.current = true;
|
||||
| ^^^ Hint: If this value is a Ref (value returned by `useRef()`), rename the variable to end in "Ref".
|
||||
6 | return <div />;
|
||||
7 | }
|
||||
8 |
|
||||
Hint: If this value is a Ref (value returned by `useRef()`), rename the variable to end in "Ref".
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {knownIncompatible} from 'ReactCompilerKnownIncompatibleTest';
|
||||
|
||||
function Component() {
|
||||
const data = knownIncompatible();
|
||||
return <div>Error</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Compilation Skipped: Use of incompatible library
|
||||
|
||||
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.
|
||||
|
||||
error.invalid-known-incompatible-function.ts:4:15
|
||||
2 |
|
||||
3 | function Component() {
|
||||
> 4 | const data = knownIncompatible();
|
||||
| ^^^^^^^^^^^^^^^^^ useKnownIncompatible is known to be incompatible
|
||||
5 | return <div>Error</div>;
|
||||
6 | }
|
||||
7 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import {knownIncompatible} from 'ReactCompilerKnownIncompatibleTest';
|
||||
|
||||
function Component() {
|
||||
const data = knownIncompatible();
|
||||
return <div>Error</div>;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useKnownIncompatibleIndirect} from 'ReactCompilerKnownIncompatibleTest';
|
||||
|
||||
function Component() {
|
||||
const {incompatible} = useKnownIncompatibleIndirect();
|
||||
return <div>{incompatible()}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Compilation Skipped: Use of incompatible library
|
||||
|
||||
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.
|
||||
|
||||
error.invalid-known-incompatible-hook-return-property.ts:5:15
|
||||
3 | function Component() {
|
||||
4 | const {incompatible} = useKnownIncompatibleIndirect();
|
||||
> 5 | return <div>{incompatible()}</div>;
|
||||
| ^^^^^^^^^^^^ useKnownIncompatibleIndirect returns an incompatible() function that is known incompatible
|
||||
6 | }
|
||||
7 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import {useKnownIncompatibleIndirect} from 'ReactCompilerKnownIncompatibleTest';
|
||||
|
||||
function Component() {
|
||||
const {incompatible} = useKnownIncompatibleIndirect();
|
||||
return <div>{incompatible()}</div>;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useKnownIncompatible} from 'ReactCompilerKnownIncompatibleTest';
|
||||
|
||||
function Component() {
|
||||
const data = useKnownIncompatible();
|
||||
return <div>Error</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Compilation Skipped: Use of incompatible library
|
||||
|
||||
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.
|
||||
|
||||
error.invalid-known-incompatible-hook.ts:4:15
|
||||
2 |
|
||||
3 | function Component() {
|
||||
> 4 | const data = useKnownIncompatible();
|
||||
| ^^^^^^^^^^^^^^^^^^^^ useKnownIncompatible is known to be incompatible
|
||||
5 | return <div>Error</div>;
|
||||
6 | }
|
||||
7 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import {useKnownIncompatible} from 'ReactCompilerKnownIncompatibleTest';
|
||||
|
||||
function Component() {
|
||||
const data = useKnownIncompatible();
|
||||
return <div>Error</div>;
|
||||
}
|
||||
@@ -38,6 +38,8 @@ error.invalid-mutate-context-in-callback.ts:12:4
|
||||
13 | };
|
||||
14 | return <div onClick={onClick} />;
|
||||
15 | }
|
||||
|
||||
Hint: If this value is a Ref (value returned by `useRef()`), rename the variable to end in "Ref".
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component() {
|
||||
const renderItem = item => {
|
||||
// Multiple returns so that the return type is a Phi (union)
|
||||
if (item == null) {
|
||||
return null;
|
||||
}
|
||||
// Normally we assume that it's safe to mutate globals in a function passed
|
||||
// as a prop, because the prop could be used as an event handler or effect.
|
||||
// But if the function returns JSX we can assume it's a render helper, ie
|
||||
// called during render, and thus it's unsafe to mutate globals or call
|
||||
// other impure code.
|
||||
global.property = true;
|
||||
return <Item item={item} value={rand} />;
|
||||
};
|
||||
return <ItemList renderItem={renderItem} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: This value cannot be modified
|
||||
|
||||
Modifying a variable defined outside a component or hook is not allowed. Consider using an effect.
|
||||
|
||||
error.invalid-mutate-global-in-render-helper-phi-return-prop.ts:12:4
|
||||
10 | // called during render, and thus it's unsafe to mutate globals or call
|
||||
11 | // other impure code.
|
||||
> 12 | global.property = true;
|
||||
| ^^^^^^ value cannot be modified
|
||||
13 | return <Item item={item} value={rand} />;
|
||||
14 | };
|
||||
15 | return <ItemList renderItem={renderItem} />;
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
function Component() {
|
||||
const renderItem = item => {
|
||||
// Multiple returns so that the return type is a Phi (union)
|
||||
if (item == null) {
|
||||
return null;
|
||||
}
|
||||
// Normally we assume that it's safe to mutate globals in a function passed
|
||||
// as a prop, because the prop could be used as an event handler or effect.
|
||||
// But if the function returns JSX we can assume it's a render helper, ie
|
||||
// called during render, and thus it's unsafe to mutate globals or call
|
||||
// other impure code.
|
||||
global.property = true;
|
||||
return <Item item={item} value={rand} />;
|
||||
};
|
||||
return <ItemList renderItem={renderItem} />;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component() {
|
||||
const renderItem = item => {
|
||||
// Normally we assume that it's safe to mutate globals in a function passed
|
||||
// as a prop, because the prop could be used as an event handler or effect.
|
||||
// But if the function returns JSX we can assume it's a render helper, ie
|
||||
// called during render, and thus it's unsafe to mutate globals or call
|
||||
// other impure code.
|
||||
global.property = true;
|
||||
return <Item item={item} value={rand} />;
|
||||
};
|
||||
return <ItemList renderItem={renderItem} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: This value cannot be modified
|
||||
|
||||
Modifying a variable defined outside a component or hook is not allowed. Consider using an effect.
|
||||
|
||||
error.invalid-mutate-global-in-render-helper-prop.ts:8:4
|
||||
6 | // called during render, and thus it's unsafe to mutate globals or call
|
||||
7 | // other impure code.
|
||||
> 8 | global.property = true;
|
||||
| ^^^^^^ value cannot be modified
|
||||
9 | return <Item item={item} value={rand} />;
|
||||
10 | };
|
||||
11 | return <ItemList renderItem={renderItem} />;
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
function Component() {
|
||||
const renderItem = item => {
|
||||
// Normally we assume that it's safe to mutate globals in a function passed
|
||||
// as a prop, because the prop could be used as an event handler or effect.
|
||||
// But if the function returns JSX we can assume it's a render helper, ie
|
||||
// called during render, and thus it's unsafe to mutate globals or call
|
||||
// other impure code.
|
||||
global.property = true;
|
||||
return <Item item={item} value={rand} />;
|
||||
};
|
||||
return <ItemList renderItem={renderItem} />;
|
||||
}
|
||||
@@ -20,7 +20,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.edges.nodes`, but the source dependencies were [props.items?.edges?.nodes]. Inferred different dependency than source.
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ error.invalid-unconditional-set-state-in-render.ts:6:2
|
||||
4 | const aliased = setX;
|
||||
5 |
|
||||
> 6 | setX(1);
|
||||
| ^^^^ Found setState() within useMemo()
|
||||
| ^^^^ Found setState() in render
|
||||
7 | aliased(2);
|
||||
8 |
|
||||
9 | return x;
|
||||
@@ -42,7 +42,7 @@ error.invalid-unconditional-set-state-in-render.ts:7:2
|
||||
5 |
|
||||
6 | setX(1);
|
||||
> 7 | aliased(2);
|
||||
| ^^^^^^^ Found setState() within useMemo()
|
||||
| ^^^^^^^ Found setState() in render
|
||||
8 |
|
||||
9 | return x;
|
||||
10 | }
|
||||
|
||||
@@ -25,7 +25,7 @@ function Component(props) {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Memoization: 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
|
||||
Compilation Skipped: 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
|
||||
|
||||
error.invalid-useEffect-dep-not-memoized-bc-range-overlaps-hook.ts:9:2
|
||||
7 |
|
||||
|
||||
@@ -22,7 +22,7 @@ function Component(props) {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Memoization: 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
|
||||
Compilation Skipped: 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
|
||||
|
||||
error.invalid-useEffect-dep-not-memoized.ts:6:2
|
||||
4 | function Component(props) {
|
||||
|
||||
@@ -22,7 +22,7 @@ function Component(props) {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Memoization: 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
|
||||
Compilation Skipped: 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
|
||||
|
||||
error.invalid-useInsertionEffect-dep-not-memoized.ts:6:2
|
||||
4 | function Component(props) {
|
||||
|
||||
@@ -22,7 +22,7 @@ function Component(props) {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Memoization: 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
|
||||
Compilation Skipped: 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
|
||||
|
||||
error.invalid-useLayoutEffect-dep-not-memoized.ts:6:2
|
||||
4 | function Component(props) {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDynamicallyCreatedComponentsOrHooks
|
||||
export function getInput(a) {
|
||||
const Wrapper = () => {
|
||||
const handleChange = () => {
|
||||
a.onChange();
|
||||
};
|
||||
|
||||
return <input onChange={handleChange} />;
|
||||
};
|
||||
|
||||
return Wrapper;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: getInput,
|
||||
isComponent: false,
|
||||
params: [{onChange() {}}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Components and hooks cannot be created dynamically
|
||||
|
||||
The function `Wrapper` appears to be a React component, but it's defined inside `getInput`. Components and Hooks should always be declared at module scope
|
||||
|
||||
error.nested-component-in-normal-function.ts:2:16
|
||||
1 | // @validateNoDynamicallyCreatedComponentsOrHooks
|
||||
> 2 | export function getInput(a) {
|
||||
| ^^^^^^^^ this function dynamically created a component/hook
|
||||
3 | const Wrapper = () => {
|
||||
4 | const handleChange = () => {
|
||||
5 | a.onChange();
|
||||
|
||||
error.nested-component-in-normal-function.ts:3:8
|
||||
1 | // @validateNoDynamicallyCreatedComponentsOrHooks
|
||||
2 | export function getInput(a) {
|
||||
> 3 | const Wrapper = () => {
|
||||
| ^^^^^^^ the component is created here
|
||||
4 | const handleChange = () => {
|
||||
5 | a.onChange();
|
||||
6 | };
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// @validateNoDynamicallyCreatedComponentsOrHooks
|
||||
export function getInput(a) {
|
||||
const Wrapper = () => {
|
||||
const handleChange = () => {
|
||||
a.onChange();
|
||||
};
|
||||
|
||||
return <input onChange={handleChange} />;
|
||||
};
|
||||
|
||||
return Wrapper;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: getInput,
|
||||
isComponent: false,
|
||||
params: [{onChange() {}}],
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDynamicallyCreatedComponentsOrHooks
|
||||
import {useState} from 'react';
|
||||
|
||||
function createCustomHook(config) {
|
||||
function useConfiguredState() {
|
||||
const [state, setState] = useState(0);
|
||||
|
||||
const increment = () => {
|
||||
setState(state + config.step);
|
||||
};
|
||||
|
||||
return [state, increment];
|
||||
}
|
||||
|
||||
return useConfiguredState;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: createCustomHook,
|
||||
isComponent: false,
|
||||
params: [{step: 1}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Components and hooks cannot be created dynamically
|
||||
|
||||
The function `useConfiguredState` appears to be a React hook, but it's defined inside `createCustomHook`. Components and Hooks should always be declared at module scope
|
||||
|
||||
error.nested-hook-in-normal-function.ts:4:9
|
||||
2 | import {useState} from 'react';
|
||||
3 |
|
||||
> 4 | function createCustomHook(config) {
|
||||
| ^^^^^^^^^^^^^^^^ this function dynamically created a component/hook
|
||||
5 | function useConfiguredState() {
|
||||
6 | const [state, setState] = useState(0);
|
||||
7 |
|
||||
|
||||
error.nested-hook-in-normal-function.ts:5:11
|
||||
3 |
|
||||
4 | function createCustomHook(config) {
|
||||
> 5 | function useConfiguredState() {
|
||||
| ^^^^^^^^^^^^^^^^^^ the component is created here
|
||||
6 | const [state, setState] = useState(0);
|
||||
7 |
|
||||
8 | const increment = () => {
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// @validateNoDynamicallyCreatedComponentsOrHooks
|
||||
import {useState} from 'react';
|
||||
|
||||
function createCustomHook(config) {
|
||||
function useConfiguredState() {
|
||||
const [state, setState] = useState(0);
|
||||
|
||||
const increment = () => {
|
||||
setState(state + config.step);
|
||||
};
|
||||
|
||||
return [state, increment];
|
||||
}
|
||||
|
||||
return useConfiguredState;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: createCustomHook,
|
||||
isComponent: false,
|
||||
params: [{step: 1}],
|
||||
};
|
||||
@@ -33,7 +33,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
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 `Ref.current`, but the source dependencies were []. Inferred dependency not present in source.
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
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 `notaref.current`, but the source dependencies were []. Inferred dependency not present in source.
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ component Component() {
|
||||
```
|
||||
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. This value was memoized in source but not in compilation output.
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user