Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7defbdf2f3 | ||
|
|
84af9085c1 | ||
|
|
128abcfa01 | ||
|
|
e3c9656d20 | ||
|
|
27b4076ab0 | ||
|
|
81d66927af | ||
|
|
6a4c8f51fa | ||
|
|
16df13b84c | ||
|
|
7899729130 | ||
|
|
a51f925217 | ||
|
|
941cd803a7 | ||
|
|
851bad0c88 | ||
|
|
5e0c951b58 | ||
|
|
348a4e2d44 | ||
|
|
5d49b2b7f4 | ||
|
|
ae22247dce | ||
|
|
e3f191803c | ||
|
|
e12b0bdc3b | ||
|
|
92d7ad5dd9 | ||
|
|
67a44bcd1b | ||
|
|
3fa927b674 | ||
|
|
47664deb8e | ||
|
|
5502d85cc7 | ||
|
|
8a8e9a7edf | ||
|
|
68f00c901c | ||
|
|
93d7aa69b2 | ||
|
|
20e5431747 | ||
|
|
1a27af3607 | ||
|
|
0e10ee906e | ||
|
|
0c813c528d | ||
|
|
a9ad64c852 | ||
|
|
7fc888dde2 | ||
|
|
67415c8c4a | ||
|
|
f3a803617e | ||
|
|
fe84397e81 | ||
|
|
b1c519f3d4 | ||
|
|
8c1501452c | ||
|
|
bd9e6e0bed | ||
|
|
835b00908b | ||
|
|
e2ba45bb39 | ||
|
|
886b3d36d7 | ||
|
|
288d428af1 | ||
|
|
a34c5dff15 | ||
|
|
3bf8ab430e | ||
|
|
acada3035f | ||
|
|
969a9790ad | ||
|
|
665de2ed28 | ||
|
|
eda778b8ae | ||
|
|
1836b46fff | ||
|
|
eec50b17b3 | ||
|
|
a9410fb487 | ||
|
|
6b70072c4f | ||
|
|
b2cff47472 | ||
|
|
8943025358 | ||
|
|
3d9d22cbdb | ||
|
|
d4374b3ae3 | ||
|
|
3f2a42a5de | ||
|
|
294c33f34d | ||
|
|
3fb190f729 | ||
|
|
f5e96b9740 | ||
|
|
78992521a8 | ||
|
|
80d7aa17ad | ||
|
|
474f25842a | ||
|
|
1fef581e1a | ||
|
|
60d9b9740d | ||
|
|
c4e2508dad | ||
|
|
de5a1b203e | ||
|
|
b9a045368b | ||
|
|
e2cc315a1b | ||
|
|
5a31758ed6 | ||
|
|
ba6590dd7c | ||
|
|
2710795a1e |
@@ -577,6 +577,7 @@ module.exports = {
|
||||
$AsyncIterator: 'readonly',
|
||||
Iterator: 'readonly',
|
||||
AsyncIterator: 'readonly',
|
||||
IntervalID: 'readonly',
|
||||
IteratorResult: 'readonly',
|
||||
JSONValue: 'readonly',
|
||||
JSResourceReference: 'readonly',
|
||||
|
||||
49
.github/workflows/devtools_discord_notify.yml
vendored
Normal file
49
.github/workflows/devtools_discord_notify.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: (DevTools) Discord Notify
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, ready_for_review]
|
||||
paths:
|
||||
- packages/react-devtools**
|
||||
- .github/workflows/devtools_**.yml
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
check_access:
|
||||
if: ${{ github.event.pull_request.draft == false }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
|
||||
steps:
|
||||
- run: echo ${{ github.event.pull_request.author_association }}
|
||||
- name: Check is member or collaborator
|
||||
id: check_is_member_or_collaborator
|
||||
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}
|
||||
run: echo "is_member_or_collaborator=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
check_maintainer:
|
||||
if: ${{ needs.check_access.outputs.is_member_or_collaborator == 'true' || needs.check_access.outputs.is_member_or_collaborator == true }}
|
||||
needs: [check_access]
|
||||
uses: facebook/react/.github/workflows/shared_check_maintainer.yml@main
|
||||
permissions:
|
||||
# Used by check_maintainer
|
||||
contents: read
|
||||
with:
|
||||
actor: ${{ github.event.pull_request.user.login }}
|
||||
|
||||
notify:
|
||||
if: ${{ needs.check_maintainer.outputs.is_core_team == 'true' }}
|
||||
needs: check_maintainer
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
|
||||
with:
|
||||
webhook-url: ${{ secrets.DEVTOOLS_DISCORD_WEBHOOK_URL }}
|
||||
embed-author-name: ${{ github.event.pull_request.user.login }}
|
||||
embed-author-url: ${{ github.event.pull_request.user.html_url }}
|
||||
embed-author-icon-url: ${{ github.event.pull_request.user.avatar_url }}
|
||||
embed-title: '#${{ github.event.number }} (+${{github.event.pull_request.additions}} -${{github.event.pull_request.deletions}}): ${{ github.event.pull_request.title }}'
|
||||
embed-description: ${{ github.event.pull_request.body }}
|
||||
embed-url: ${{ github.event.pull_request.html_url }}
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: react-devtools
|
||||
path: build/devtools.tgz
|
||||
path: build/devtools
|
||||
if-no-files-found: error
|
||||
# Simplifies getting the extension for local testing
|
||||
- name: Archive chrome extension
|
||||
@@ -201,5 +201,5 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: screenshots
|
||||
path: ./tmp/screenshots
|
||||
path: ./tmp/playwright-artifacts
|
||||
if-no-files-found: warn
|
||||
|
||||
13
.github/workflows/runtime_build_and_test.yml
vendored
13
.github/workflows/runtime_build_and_test.yml
vendored
@@ -766,6 +766,11 @@ jobs:
|
||||
name: react-devtools-${{ matrix.browser }}-extension
|
||||
path: build/devtools/${{ matrix.browser }}-extension.zip
|
||||
if-no-files-found: error
|
||||
- name: Archive ${{ matrix.browser }} metadata
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: react-devtools-${{ matrix.browser }}-metadata
|
||||
path: build/devtools/webpack-stats.*.json
|
||||
|
||||
merge_devtools_artifacts:
|
||||
name: Merge DevTools artifacts
|
||||
@@ -776,7 +781,7 @@ jobs:
|
||||
uses: actions/upload-artifact/merge@v4
|
||||
with:
|
||||
name: react-devtools
|
||||
pattern: react-devtools-*-extension
|
||||
pattern: react-devtools-*
|
||||
|
||||
run_devtools_e2e_tests:
|
||||
name: Run DevTools e2e tests
|
||||
@@ -826,6 +831,12 @@ jobs:
|
||||
- run: ./scripts/ci/run_devtools_e2e_tests.js
|
||||
env:
|
||||
RELEASE_CHANNEL: experimental
|
||||
- name: Archive Playwright report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: devtools-playwright-artifacts
|
||||
path: tmp/playwright-artifacts
|
||||
if-no-files-found: warn
|
||||
|
||||
# ----- SIZEBOT -----
|
||||
sizebot:
|
||||
|
||||
2
.github/workflows/runtime_discord_notify.yml
vendored
2
.github/workflows/runtime_discord_notify.yml
vendored
@@ -4,8 +4,10 @@ on:
|
||||
pull_request_target:
|
||||
types: [opened, ready_for_review]
|
||||
paths-ignore:
|
||||
- packages/react-devtools**
|
||||
- compiler/**
|
||||
- .github/workflows/compiler_**.yml
|
||||
- .github/workflows/devtools**.yml
|
||||
|
||||
permissions: {}
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,6 +23,7 @@ chrome-user-data
|
||||
.vscode
|
||||
*.swp
|
||||
*.swo
|
||||
/tmp
|
||||
|
||||
packages/react-devtools-core/dist
|
||||
packages/react-devtools-extensions/chrome/build
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { c as _c } from "react/compiler-runtime"; //
|
||||
@compilationMode:"all"
|
||||
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"all"
|
||||
function nonReactFn() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
|
||||
106
compiler/apps/playground/components/AccordionWindow.tsx
Normal file
106
compiler/apps/playground/components/AccordionWindow.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {Resizable} from 're-resizable';
|
||||
import React, {useCallback} from 'react';
|
||||
|
||||
type TabsRecord = Map<string, React.ReactNode>;
|
||||
|
||||
export default function AccordionWindow(props: {
|
||||
defaultTab: string | null;
|
||||
tabs: TabsRecord;
|
||||
tabsOpen: Set<string>;
|
||||
setTabsOpen: (newTab: Set<string>) => void;
|
||||
changedPasses: Set<string>;
|
||||
}): React.ReactElement {
|
||||
if (props.tabs.size === 0) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-center"
|
||||
style={{width: 'calc(100vw - 650px)'}}>
|
||||
No compiler output detected, see errors below
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-row h-full">
|
||||
{Array.from(props.tabs.keys()).map(name => {
|
||||
return (
|
||||
<AccordionWindowItem
|
||||
name={name}
|
||||
key={name}
|
||||
tabs={props.tabs}
|
||||
tabsOpen={props.tabsOpen}
|
||||
setTabsOpen={props.setTabsOpen}
|
||||
hasChanged={props.changedPasses.has(name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AccordionWindowItem({
|
||||
name,
|
||||
tabs,
|
||||
tabsOpen,
|
||||
setTabsOpen,
|
||||
hasChanged,
|
||||
}: {
|
||||
name: string;
|
||||
tabs: TabsRecord;
|
||||
tabsOpen: Set<string>;
|
||||
setTabsOpen: (newTab: Set<string>) => void;
|
||||
hasChanged: boolean;
|
||||
}): React.ReactElement {
|
||||
const isShow = tabsOpen.has(name);
|
||||
|
||||
const toggleTabs = useCallback(() => {
|
||||
const nextState = new Set(tabsOpen);
|
||||
if (nextState.has(name)) {
|
||||
nextState.delete(name);
|
||||
} else {
|
||||
nextState.add(name);
|
||||
}
|
||||
setTabsOpen(nextState);
|
||||
}, [tabsOpen, name, setTabsOpen]);
|
||||
|
||||
// Replace spaces with non-breaking spaces
|
||||
const displayName = name.replace(/ /g, '\u00A0');
|
||||
|
||||
return (
|
||||
<div key={name} className="flex flex-row">
|
||||
{isShow ? (
|
||||
<Resizable className="border-r" minWidth={550} enable={{right: true}}>
|
||||
<h2
|
||||
title="Minimize tab"
|
||||
aria-label="Minimize tab"
|
||||
onClick={toggleTabs}
|
||||
className={`p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 ${
|
||||
hasChanged ? 'font-bold' : 'font-light'
|
||||
} text-secondary hover:text-link`}>
|
||||
- {displayName}
|
||||
</h2>
|
||||
{tabs.get(name) ?? <div>No output for {name}</div>}
|
||||
</Resizable>
|
||||
) : (
|
||||
<div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
|
||||
<button
|
||||
title={`Expand compiler tab: ${name}`}
|
||||
aria-label={`Expand compiler tab: ${name}`}
|
||||
style={{transform: 'rotate(90deg) translate(-50%)'}}
|
||||
onClick={toggleTabs}
|
||||
className={`flex-grow-0 w-5 transition-colors duration-150 ease-in ${
|
||||
hasChanged ? 'font-bold' : 'font-light'
|
||||
} text-secondary hover:text-link`}>
|
||||
{displayName}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,96 +6,192 @@
|
||||
*/
|
||||
|
||||
import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
|
||||
import {PluginOptions} from 'babel-plugin-react-compiler';
|
||||
import type {editor} from 'monaco-editor';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import {useState} from 'react';
|
||||
import React, {useState} from 'react';
|
||||
import {Resizable} from 're-resizable';
|
||||
import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
import {
|
||||
generateOverridePragmaFromConfig,
|
||||
updateSourceWithOverridePragma,
|
||||
} from '../../lib/configUtils';
|
||||
import {IconChevron} from '../Icons/IconChevron';
|
||||
import prettyFormat from 'pretty-format';
|
||||
|
||||
// @ts-expect-error - webpack asset/source loader handles .d.ts files as strings
|
||||
import compilerTypeDefs from 'babel-plugin-react-compiler/dist/index.d.ts';
|
||||
|
||||
loader.config({monaco});
|
||||
|
||||
export default function ConfigEditor(): JSX.Element {
|
||||
const [, setMonaco] = useState<Monaco | null>(null);
|
||||
export default function ConfigEditor({
|
||||
appliedOptions,
|
||||
}: {
|
||||
appliedOptions: PluginOptions | null;
|
||||
}): React.ReactElement {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return isExpanded ? (
|
||||
<ExpandedEditor onToggle={setIsExpanded} appliedOptions={appliedOptions} />
|
||||
) : (
|
||||
<CollapsedEditor onToggle={setIsExpanded} />
|
||||
);
|
||||
}
|
||||
|
||||
function ExpandedEditor({
|
||||
onToggle,
|
||||
appliedOptions,
|
||||
}: {
|
||||
onToggle: (expanded: boolean) => void;
|
||||
appliedOptions: PluginOptions | null;
|
||||
}): React.ReactElement {
|
||||
const store = useStore();
|
||||
const dispatchStore = useStoreDispatch();
|
||||
|
||||
const handleChange: (value: string | undefined) => void = async value => {
|
||||
const handleChange: (value: string | undefined) => void = 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
dispatchStore({
|
||||
type: 'updateConfig',
|
||||
payload: {
|
||||
config: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleMount: (
|
||||
_: editor.IStandaloneCodeEditor,
|
||||
monaco: Monaco,
|
||||
) => void = (_, monaco) => {
|
||||
setMonaco(monaco);
|
||||
// Add the babel-plugin-react-compiler type definitions to Monaco
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
||||
//@ts-expect-error - compilerTypeDefs is a string
|
||||
compilerTypeDefs,
|
||||
'file:///node_modules/babel-plugin-react-compiler/dist/index.d.ts',
|
||||
);
|
||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
||||
target: monaco.languages.typescript.ScriptTarget.Latest,
|
||||
allowNonTsExtensions: true,
|
||||
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
||||
module: monaco.languages.typescript.ModuleKind.ESNext,
|
||||
noEmit: true,
|
||||
strict: false,
|
||||
esModuleInterop: true,
|
||||
allowSyntheticDefaultImports: true,
|
||||
jsx: monaco.languages.typescript.JsxEmit.React,
|
||||
});
|
||||
|
||||
const uri = monaco.Uri.parse(`file:///config.js`);
|
||||
const uri = monaco.Uri.parse(`file:///config.ts`);
|
||||
const model = monaco.editor.getModel(uri);
|
||||
if (model) {
|
||||
model.updateOptions({tabSize: 2});
|
||||
}
|
||||
};
|
||||
|
||||
const formattedAppliedOptions = appliedOptions
|
||||
? prettyFormat(appliedOptions, {
|
||||
printFunctionName: false,
|
||||
printBasicPrototype: false,
|
||||
})
|
||||
: 'Invalid configs';
|
||||
|
||||
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>
|
||||
<Resizable
|
||||
minWidth={300}
|
||||
maxWidth={600}
|
||||
defaultSize={{width: 350}}
|
||||
enable={{right: true, bottom: false}}>
|
||||
<div className="bg-blue-10 relative h-full flex flex-col !h-[calc(100vh_-_3.5rem)] border border-gray-300">
|
||||
<div
|
||||
className="absolute w-8 h-16 bg-blue-10 rounded-r-full flex items-center justify-center z-[2] cursor-pointer border border-l-0 border-gray-300"
|
||||
title="Minimize config editor"
|
||||
onClick={() => onToggle(false)}
|
||||
style={{
|
||||
top: '50%',
|
||||
marginTop: '-32px',
|
||||
right: '-32px',
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
}}>
|
||||
<IconChevron displayDirection="left" className="text-blue-50" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col m-2 mb-2">
|
||||
<div className="pb-2">
|
||||
<h2 className="inline-block text-blue-50 py-1.5 px-1.5 xs:px-3 sm:px-4 text-sm">
|
||||
Config Overrides
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex-1 rounded-lg overflow-hidden border border-gray-300">
|
||||
<MonacoEditor
|
||||
path={'config.ts'}
|
||||
language={'typescript'}
|
||||
value={store.config}
|
||||
onMount={handleMount}
|
||||
onChange={handleChange}
|
||||
options={{
|
||||
...monacoOptions,
|
||||
lineNumbers: 'off',
|
||||
renderLineHighlight: 'none',
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
fontSize: 12,
|
||||
scrollBeyondLastLine: false,
|
||||
glyphMargin: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col m-2">
|
||||
<div className="pb-2">
|
||||
<h2 className="inline-block text-blue-50 py-1.5 px-1.5 xs:px-3 sm:px-4 text-sm">
|
||||
Applied Configs
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex-1 rounded-lg overflow-hidden border border-gray-300">
|
||||
<MonacoEditor
|
||||
path={'applied-config.js'}
|
||||
language={'javascript'}
|
||||
value={formattedAppliedOptions}
|
||||
options={{
|
||||
...monacoOptions,
|
||||
lineNumbers: 'off',
|
||||
renderLineHighlight: 'none',
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
fontSize: 12,
|
||||
scrollBeyondLastLine: false,
|
||||
readOnly: true,
|
||||
glyphMargin: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Resizable>
|
||||
);
|
||||
}
|
||||
|
||||
function CollapsedEditor({
|
||||
onToggle,
|
||||
}: {
|
||||
onToggle: (expanded: boolean) => void;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<div
|
||||
className="w-4 !h-[calc(100vh_-_3.5rem)]"
|
||||
style={{position: 'relative'}}>
|
||||
<div
|
||||
className="absolute w-10 h-16 bg-blue-10 hover:translate-x-2 transition-transform rounded-r-full flex items-center justify-center z-[2] cursor-pointer border border-gray-300"
|
||||
title="Expand config editor"
|
||||
onClick={() => onToggle(true)}
|
||||
style={{
|
||||
top: '50%',
|
||||
marginTop: '-32px',
|
||||
left: '-8px',
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
}}>
|
||||
<IconChevron displayDirection="right" className="text-blue-50" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import BabelPluginReactCompiler, {
|
||||
CompilerErrorDetail,
|
||||
CompilerDiagnostic,
|
||||
Effect,
|
||||
ErrorSeverity,
|
||||
ErrorCategory,
|
||||
parseConfigPragmaForTests,
|
||||
ValueKind,
|
||||
type Hook,
|
||||
@@ -22,8 +22,8 @@ import BabelPluginReactCompiler, {
|
||||
parsePluginOptions,
|
||||
printReactiveFunctionWithOutlined,
|
||||
printFunctionWithOutlined,
|
||||
type LoggerEvent,
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import clsx from 'clsx';
|
||||
import invariant from 'invariant';
|
||||
import {useSnackbar} from 'notistack';
|
||||
import {useDeferredValue, useMemo} from 'react';
|
||||
@@ -46,9 +46,6 @@ import {
|
||||
PrintedCompilerPipelineValue,
|
||||
} 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,
|
||||
@@ -145,10 +142,66 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
|
||||
],
|
||||
];
|
||||
|
||||
function parseOptions(
|
||||
source: string,
|
||||
mode: 'compiler' | 'linter',
|
||||
configOverrides: string,
|
||||
): PluginOptions {
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
const pragma = source.substring(0, source.indexOf('\n'));
|
||||
|
||||
const parsedPragmaOptions = parseConfigPragmaForTests(pragma, {
|
||||
compilationMode: 'infer',
|
||||
environment:
|
||||
mode === 'linter'
|
||||
? {
|
||||
// enabled in compiler
|
||||
validateRefAccessDuringRender: false,
|
||||
// enabled in linter
|
||||
validateNoSetStateInRender: true,
|
||||
validateNoSetStateInEffects: true,
|
||||
validateNoJSXInTryStatements: true,
|
||||
validateNoImpureFunctionsInRender: true,
|
||||
validateStaticComponents: true,
|
||||
validateNoFreezingKnownMutableFunctions: true,
|
||||
validateNoVoidUseMemo: true,
|
||||
}
|
||||
: {
|
||||
/* use defaults for compiler mode */
|
||||
},
|
||||
});
|
||||
|
||||
// Parse config overrides from config editor
|
||||
let configOverrideOptions: any = {};
|
||||
const configMatch = configOverrides.match(/^\s*import.*?\n\n\((.*)\)/s);
|
||||
// TODO: initialize store with URL params, not empty store
|
||||
if (configOverrides.trim()) {
|
||||
if (configMatch && configMatch[1]) {
|
||||
const configString = configMatch[1].replace(/satisfies.*$/, '').trim();
|
||||
configOverrideOptions = new Function(`return (${configString})`)();
|
||||
} else {
|
||||
throw new Error('Invalid override format');
|
||||
}
|
||||
}
|
||||
|
||||
const opts: PluginOptions = parsePluginOptions({
|
||||
...parsedPragmaOptions,
|
||||
...configOverrideOptions,
|
||||
environment: {
|
||||
...parsedPragmaOptions.environment,
|
||||
...configOverrideOptions.environment,
|
||||
customHooks: new Map([...COMMON_HOOKS]),
|
||||
},
|
||||
});
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
function compile(
|
||||
source: string,
|
||||
mode: 'compiler' | 'linter',
|
||||
): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
configOverrides: string,
|
||||
): [CompilerOutput, 'flow' | 'typescript', PluginOptions | null] {
|
||||
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
|
||||
const error = new CompilerError();
|
||||
const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
|
||||
@@ -167,104 +220,94 @@ function compile(
|
||||
language = 'typescript';
|
||||
}
|
||||
let transformOutput;
|
||||
|
||||
let baseOpts: PluginOptions | null = null;
|
||||
try {
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
const pragma = source.substring(0, source.indexOf('\n'));
|
||||
const logIR = (result: CompilerPipelineValue): void => {
|
||||
switch (result.kind) {
|
||||
case 'ast': {
|
||||
break;
|
||||
}
|
||||
case 'hir': {
|
||||
upsert({
|
||||
kind: 'hir',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'reactive': {
|
||||
upsert({
|
||||
kind: 'reactive',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printReactiveFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'debug': {
|
||||
upsert({
|
||||
kind: 'debug',
|
||||
fnName: null,
|
||||
name: result.name,
|
||||
value: result.value,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const _: never = result;
|
||||
throw new Error(`Unhandled result ${result}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
const parsedOptions = parseConfigPragmaForTests(pragma, {
|
||||
compilationMode: 'infer',
|
||||
environment:
|
||||
mode === 'linter'
|
||||
? {
|
||||
// enabled in compiler
|
||||
validateRefAccessDuringRender: false,
|
||||
// enabled in linter
|
||||
validateNoSetStateInRender: true,
|
||||
validateNoSetStateInEffects: true,
|
||||
validateNoJSXInTryStatements: true,
|
||||
validateNoImpureFunctionsInRender: true,
|
||||
validateStaticComponents: true,
|
||||
validateNoFreezingKnownMutableFunctions: true,
|
||||
validateNoVoidUseMemo: true,
|
||||
}
|
||||
: {
|
||||
/* use defaults for compiler mode */
|
||||
},
|
||||
});
|
||||
const opts: PluginOptions = parsePluginOptions({
|
||||
...parsedOptions,
|
||||
environment: {
|
||||
...parsedOptions.environment,
|
||||
customHooks: new Map([...COMMON_HOOKS]),
|
||||
},
|
||||
logger: {
|
||||
debugLogIRs: logIR,
|
||||
logEvent: (_filename: string | null, event: LoggerEvent) => {
|
||||
if (event.kind === 'CompileError') {
|
||||
otherErrors.push(event.detail);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
transformOutput = invokeCompiler(source, language, opts);
|
||||
baseOpts = parseOptions(source, mode, configOverrides);
|
||||
} catch (err) {
|
||||
/**
|
||||
* error might be an invariant violation or other runtime error
|
||||
* (i.e. object shape that is not CompilerError)
|
||||
*/
|
||||
if (err instanceof CompilerError && err.details.length > 0) {
|
||||
error.merge(err);
|
||||
} else {
|
||||
error.details.push(
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Config,
|
||||
reason: `Unexpected failure when transforming configs! \n${err}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (baseOpts) {
|
||||
try {
|
||||
const logIR = (result: CompilerPipelineValue): void => {
|
||||
switch (result.kind) {
|
||||
case 'ast': {
|
||||
break;
|
||||
}
|
||||
case 'hir': {
|
||||
upsert({
|
||||
kind: 'hir',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'reactive': {
|
||||
upsert({
|
||||
kind: 'reactive',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printReactiveFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'debug': {
|
||||
upsert({
|
||||
kind: 'debug',
|
||||
fnName: null,
|
||||
name: result.name,
|
||||
value: result.value,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const _: never = result;
|
||||
throw new Error(`Unhandled result ${result}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
// Add logger options to the parsed options
|
||||
const opts = {
|
||||
...baseOpts,
|
||||
logger: {
|
||||
debugLogIRs: logIR,
|
||||
logEvent: (_filename: string | null, event: LoggerEvent): void => {
|
||||
if (event.kind === 'CompileError') {
|
||||
otherErrors.push(event.detail);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
transformOutput = invokeCompiler(source, language, opts);
|
||||
} catch (err) {
|
||||
/**
|
||||
* Handle unexpected failures by logging (to get a stack trace)
|
||||
* and reporting
|
||||
* error might be an invariant violation or other runtime error
|
||||
* (i.e. object shape that is not CompilerError)
|
||||
*/
|
||||
console.error(err);
|
||||
error.details.push(
|
||||
new CompilerErrorDetail({
|
||||
severity: ErrorSeverity.Invariant,
|
||||
reason: `Unexpected failure when transforming input! ${err}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
if (err instanceof CompilerError && err.details.length > 0) {
|
||||
error.merge(err);
|
||||
} else {
|
||||
/**
|
||||
* Handle unexpected failures by logging (to get a stack trace)
|
||||
* and reporting
|
||||
*/
|
||||
error.details.push(
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Invariant,
|
||||
reason: `Unexpected failure when transforming input! \n${err}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Only include logger errors if there weren't other errors
|
||||
@@ -272,11 +315,12 @@ function compile(
|
||||
otherErrors.forEach(e => error.details.push(e));
|
||||
}
|
||||
if (error.hasErrors()) {
|
||||
return [{kind: 'err', results, error}, language];
|
||||
return [{kind: 'err', results, error}, language, baseOpts];
|
||||
}
|
||||
return [
|
||||
{kind: 'ok', results, transformOutput, errors: error.details},
|
||||
language,
|
||||
baseOpts,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -285,20 +329,15 @@ export default function Editor(): JSX.Element {
|
||||
const deferredStore = useDeferredValue(store);
|
||||
const dispatchStore = useStoreDispatch();
|
||||
const {enqueueSnackbar} = useSnackbar();
|
||||
const [compilerOutput, language] = useMemo(
|
||||
() => compile(deferredStore.source, 'compiler'),
|
||||
[deferredStore.source],
|
||||
const [compilerOutput, language, appliedOptions] = useMemo(
|
||||
() => compile(deferredStore.source, 'compiler', deferredStore.config),
|
||||
[deferredStore.source, deferredStore.config],
|
||||
);
|
||||
const [linterOutput] = useMemo(
|
||||
() => compile(deferredStore.source, 'linter'),
|
||||
[deferredStore.source],
|
||||
() => compile(deferredStore.source, 'linter', deferredStore.config),
|
||||
[deferredStore.source, deferredStore.config],
|
||||
);
|
||||
|
||||
// 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;
|
||||
@@ -317,16 +356,11 @@ export default function Editor(): JSX.Element {
|
||||
mountStore = defaultStore;
|
||||
}
|
||||
|
||||
parseAndFormatConfig(mountStore.source).then(config => {
|
||||
dispatchStore({
|
||||
type: 'setStore',
|
||||
payload: {
|
||||
store: {
|
||||
...mountStore,
|
||||
config,
|
||||
},
|
||||
},
|
||||
});
|
||||
dispatchStore({
|
||||
type: 'setStore',
|
||||
payload: {
|
||||
store: mountStore,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -344,13 +378,17 @@ 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 className="relative flex top-14">
|
||||
<div className="flex-shrink-0">
|
||||
<ConfigEditor appliedOptions={appliedOptions} />
|
||||
</div>
|
||||
<div className={clsx('flex sm:flex flex-wrap')}>
|
||||
<Output store={deferredStore} compilerOutput={mergedOutput} />
|
||||
<div className="flex flex-1 min-w-0">
|
||||
<div className="flex-1 min-w-[550px] sm:min-w-0">
|
||||
<Input language={language} errors={errors} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-[550px] sm:min-w-0">
|
||||
<Output store={deferredStore} compilerOutput={mergedOutput} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
*/
|
||||
|
||||
import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
|
||||
import {CompilerErrorDetail} from 'babel-plugin-react-compiler';
|
||||
import {
|
||||
CompilerErrorDetail,
|
||||
CompilerDiagnostic,
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import invariant from 'invariant';
|
||||
import type {editor} from 'monaco-editor';
|
||||
import * as monaco from 'monaco-editor';
|
||||
@@ -14,15 +17,15 @@ import {Resizable} from 're-resizable';
|
||||
import {useEffect, useState} from 'react';
|
||||
import {renderReactCompilerMarkers} from '../../lib/reactCompilerMonacoDiagnostics';
|
||||
import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
import TabbedWindow from '../TabbedWindow';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
// @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});
|
||||
|
||||
type Props = {
|
||||
errors: Array<CompilerErrorDetail>;
|
||||
errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
|
||||
language: 'flow' | 'typescript';
|
||||
};
|
||||
|
||||
@@ -83,14 +86,10 @@ export default function Input({errors, language}: Props): JSX.Element {
|
||||
const handleChange: (value: string | undefined) => void = async value => {
|
||||
if (!value) return;
|
||||
|
||||
// Parse and format the config
|
||||
const config = await parseAndFormatConfig(value);
|
||||
|
||||
dispatchStore({
|
||||
type: 'updateFile',
|
||||
type: 'updateSource',
|
||||
payload: {
|
||||
source: value,
|
||||
config,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -140,30 +139,51 @@ export default function Input({errors, language}: Props): JSX.Element {
|
||||
});
|
||||
};
|
||||
|
||||
const editorContent = (
|
||||
<MonacoEditor
|
||||
path={'index.js'}
|
||||
/**
|
||||
* .js and .jsx files are specified to be TS so that Monaco can actually
|
||||
* check their syntax using its TS language service. They are still JS files
|
||||
* due to their extensions, so TS language features don't work.
|
||||
*/
|
||||
language={'javascript'}
|
||||
value={store.source}
|
||||
onMount={handleMount}
|
||||
onChange={handleChange}
|
||||
options={monacoOptions}
|
||||
/>
|
||||
);
|
||||
|
||||
const tabs = new Map([['Input', editorContent]]);
|
||||
const [activeTab, setActiveTab] = useState('Input');
|
||||
|
||||
const tabbedContent = (
|
||||
<div className="flex flex-col h-full">
|
||||
<TabbedWindow
|
||||
tabs={tabs}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col flex-none border-r border-gray-200">
|
||||
<Resizable
|
||||
minWidth={650}
|
||||
enable={{right: true}}
|
||||
/**
|
||||
* Restrict MonacoEditor's height, since the config autoLayout:true
|
||||
* will grow the editor to fit within parent element
|
||||
*/
|
||||
className="!h-[calc(100vh_-_3.5rem)]">
|
||||
<MonacoEditor
|
||||
path={'index.js'}
|
||||
{store.showInternals ? (
|
||||
<Resizable
|
||||
minWidth={550}
|
||||
enable={{right: true}}
|
||||
/**
|
||||
* .js and .jsx files are specified to be TS so that Monaco can actually
|
||||
* check their syntax using its TS language service. They are still JS files
|
||||
* due to their extensions, so TS language features don't work.
|
||||
* Restrict MonacoEditor's height, since the config autoLayout:true
|
||||
* will grow the editor to fit within parent element
|
||||
*/
|
||||
language={'javascript'}
|
||||
value={store.source}
|
||||
onMount={handleMount}
|
||||
onChange={handleChange}
|
||||
options={monacoOptions}
|
||||
/>
|
||||
</Resizable>
|
||||
className="!h-[calc(100vh_-_3.5rem)]">
|
||||
{tabbedContent}
|
||||
</Resizable>
|
||||
) : (
|
||||
<div className="!h-[calc(100vh_-_3.5rem)]">{tabbedContent}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,13 +21,17 @@ import * as prettierPluginEstree from 'prettier/plugins/estree';
|
||||
import * as prettier from 'prettier/standalone';
|
||||
import {memo, ReactNode, useEffect, useState} from 'react';
|
||||
import {type Store} from '../../lib/stores';
|
||||
import AccordionWindow from '../AccordionWindow';
|
||||
import TabbedWindow from '../TabbedWindow';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
import {BabelFileResult} from '@babel/core';
|
||||
|
||||
const MemoizedOutput = memo(Output);
|
||||
|
||||
export default MemoizedOutput;
|
||||
|
||||
export const BASIC_OUTPUT_TAB_NAMES = ['Output', 'SourceMap'];
|
||||
|
||||
export type PrintedCompilerPipelineValue =
|
||||
| {
|
||||
kind: 'hir';
|
||||
@@ -64,12 +68,16 @@ type Props = {
|
||||
async function tabify(
|
||||
source: string,
|
||||
compilerOutput: CompilerOutput,
|
||||
showInternals: boolean,
|
||||
): Promise<Map<string, ReactNode>> {
|
||||
const tabs = new Map<string, React.ReactNode>();
|
||||
const reorderedTabs = new Map<string, React.ReactNode>();
|
||||
const concattedResults = new Map<string, string>();
|
||||
// Concat all top level function declaration results into a single tab for each pass
|
||||
for (const [passName, results] of compilerOutput.results) {
|
||||
if (!showInternals && !BASIC_OUTPUT_TAB_NAMES.includes(passName)) {
|
||||
continue;
|
||||
}
|
||||
for (const result of results) {
|
||||
switch (result.kind) {
|
||||
case 'hir': {
|
||||
@@ -211,6 +219,7 @@ function Output({store, compilerOutput}: Props): JSX.Element {
|
||||
const [tabs, setTabs] = useState<Map<string, React.ReactNode>>(
|
||||
() => new Map(),
|
||||
);
|
||||
const [activeTab, setActiveTab] = useState<string>('Output');
|
||||
|
||||
/*
|
||||
* Update the active tab back to the output or errors tab when the compilation state
|
||||
@@ -222,13 +231,14 @@ function Output({store, compilerOutput}: Props): JSX.Element {
|
||||
if (compilerOutput.kind !== previousOutputKind) {
|
||||
setPreviousOutputKind(compilerOutput.kind);
|
||||
setTabsOpen(new Set(['Output']));
|
||||
setActiveTab('Output');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
tabify(store.source, compilerOutput).then(tabs => {
|
||||
tabify(store.source, compilerOutput, store.showInternals).then(tabs => {
|
||||
setTabs(tabs);
|
||||
});
|
||||
}, [store.source, compilerOutput]);
|
||||
}, [store.source, compilerOutput, store.showInternals]);
|
||||
|
||||
const changedPasses: Set<string> = new Set(['Output', 'HIR']); // Initial and final passes should always be bold
|
||||
let lastResult: string = '';
|
||||
@@ -245,16 +255,24 @@ function Output({store, compilerOutput}: Props): JSX.Element {
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
if (!store.showInternals) {
|
||||
return (
|
||||
<TabbedWindow
|
||||
defaultTab="HIR"
|
||||
setTabsOpen={setTabsOpen}
|
||||
tabsOpen={tabsOpen}
|
||||
tabs={tabs}
|
||||
changedPasses={changedPasses}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AccordionWindow
|
||||
defaultTab={store.showInternals ? 'HIR' : 'Output'}
|
||||
setTabsOpen={setTabsOpen}
|
||||
tabsOpen={tabsOpen}
|
||||
tabs={tabs}
|
||||
changedPasses={changedPasses}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,10 +14,11 @@ import {useState} from 'react';
|
||||
import {defaultStore} from '../lib/defaultStore';
|
||||
import {IconGitHub} from './Icons/IconGitHub';
|
||||
import Logo from './Logo';
|
||||
import {useStoreDispatch} from './StoreContext';
|
||||
import {useStore, useStoreDispatch} from './StoreContext';
|
||||
|
||||
export default function Header(): JSX.Element {
|
||||
const [showCheck, setShowCheck] = useState(false);
|
||||
const store = useStore();
|
||||
const dispatchStore = useStoreDispatch();
|
||||
const {enqueueSnackbar, closeSnackbar} = useSnackbar();
|
||||
|
||||
@@ -56,6 +57,27 @@ export default function Header(): JSX.Element {
|
||||
<p className="hidden select-none sm:block">React Compiler Playground</p>
|
||||
</div>
|
||||
<div className="flex items-center text-[15px] gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="relative inline-block w-[34px] h-5">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={store.showInternals}
|
||||
onChange={() => dispatchStore({type: 'toggleInternals'})}
|
||||
className="absolute opacity-0 cursor-pointer h-full w-full m-0"
|
||||
/>
|
||||
<span
|
||||
className={clsx(
|
||||
'absolute inset-0 rounded-full cursor-pointer transition-all duration-250',
|
||||
"before:content-[''] before:absolute before:w-4 before:h-4 before:left-0.5 before:bottom-0.5",
|
||||
'before:bg-white before:rounded-full before:transition-transform before:duration-250',
|
||||
'focus-within:shadow-[0_0_1px_#2196F3]',
|
||||
store.showInternals
|
||||
? 'bg-link before:translate-x-3.5'
|
||||
: 'bg-gray-300',
|
||||
)}></span>
|
||||
</label>
|
||||
<span className="text-secondary">Show Internals</span>
|
||||
</div>
|
||||
<button
|
||||
title="Reset Playground"
|
||||
aria-label="Reset Playground"
|
||||
|
||||
41
compiler/apps/playground/components/Icons/IconChevron.tsx
Normal file
41
compiler/apps/playground/components/Icons/IconChevron.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {memo} from 'react';
|
||||
|
||||
export const IconChevron = memo<
|
||||
JSX.IntrinsicElements['svg'] & {
|
||||
/**
|
||||
* The direction the arrow should point.
|
||||
*/
|
||||
displayDirection: 'right' | 'left';
|
||||
}
|
||||
>(function IconChevron({className, displayDirection, ...props}) {
|
||||
const rotationClass =
|
||||
displayDirection === 'left' ? 'rotate-90' : '-rotate-90';
|
||||
const classes = className ? `${rotationClass} ${className}` : rotationClass;
|
||||
|
||||
return (
|
||||
<svg
|
||||
className={classes}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
{...props}>
|
||||
<g fill="none" fillRule="evenodd" transform="translate(-446 -398)">
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="nonzero"
|
||||
d="M95.8838835,240.366117 C95.3957281,239.877961 94.6042719,239.877961 94.1161165,240.366117 C93.6279612,240.854272 93.6279612,241.645728 94.1161165,242.133883 L98.6161165,246.633883 C99.1042719,247.122039 99.8957281,247.122039 100.383883,246.633883 L104.883883,242.133883 C105.372039,241.645728 105.372039,240.854272 104.883883,240.366117 C104.395728,239.877961 103.604272,239.877961 103.116117,240.366117 L99.5,243.982233 L95.8838835,240.366117 Z"
|
||||
transform="translate(356.5 164.5)"
|
||||
/>
|
||||
<polygon points="446 418 466 418 466 398 446 398" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
});
|
||||
@@ -53,11 +53,19 @@ type ReducerAction =
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'updateFile';
|
||||
type: 'updateSource';
|
||||
payload: {
|
||||
source: string;
|
||||
config?: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'updateConfig';
|
||||
payload: {
|
||||
config: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'toggleInternals';
|
||||
};
|
||||
|
||||
function storeReducer(store: Store, action: ReducerAction): Store {
|
||||
@@ -66,14 +74,28 @@ function storeReducer(store: Store, action: ReducerAction): Store {
|
||||
const newStore = action.payload.store;
|
||||
return newStore;
|
||||
}
|
||||
case 'updateFile': {
|
||||
const {source, config} = action.payload;
|
||||
case 'updateSource': {
|
||||
const source = action.payload.source;
|
||||
const newStore = {
|
||||
...store,
|
||||
source,
|
||||
};
|
||||
return newStore;
|
||||
}
|
||||
case 'updateConfig': {
|
||||
const config = action.payload.config;
|
||||
const newStore = {
|
||||
...store,
|
||||
config,
|
||||
};
|
||||
return newStore;
|
||||
}
|
||||
case 'toggleInternals': {
|
||||
const newStore = {
|
||||
...store,
|
||||
showInternals: !store.showInternals,
|
||||
};
|
||||
return newStore;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,103 +4,47 @@
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import {Resizable} from 're-resizable';
|
||||
import React, {useCallback} from 'react';
|
||||
|
||||
type TabsRecord = Map<string, React.ReactNode>;
|
||||
|
||||
export default function TabbedWindow(props: {
|
||||
defaultTab: string | null;
|
||||
tabs: TabsRecord;
|
||||
tabsOpen: Set<string>;
|
||||
setTabsOpen: (newTab: Set<string>) => void;
|
||||
changedPasses: Set<string>;
|
||||
export default function TabbedWindow({
|
||||
tabs,
|
||||
activeTab,
|
||||
onTabChange,
|
||||
}: {
|
||||
tabs: Map<string, React.ReactNode>;
|
||||
activeTab: string;
|
||||
onTabChange: (tab: string) => void;
|
||||
}): React.ReactElement {
|
||||
if (props.tabs.size === 0) {
|
||||
if (tabs.size === 0) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-center"
|
||||
style={{width: 'calc(100vw - 650px)'}}>
|
||||
<div className="flex items-center justify-center flex-1 max-w-full">
|
||||
No compiler output detected, see errors below
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-row">
|
||||
{Array.from(props.tabs.keys()).map(name => {
|
||||
return (
|
||||
<TabbedWindowItem
|
||||
name={name}
|
||||
key={name}
|
||||
tabs={props.tabs}
|
||||
tabsOpen={props.tabsOpen}
|
||||
setTabsOpen={props.setTabsOpen}
|
||||
hasChanged={props.changedPasses.has(name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TabbedWindowItem({
|
||||
name,
|
||||
tabs,
|
||||
tabsOpen,
|
||||
setTabsOpen,
|
||||
hasChanged,
|
||||
}: {
|
||||
name: string;
|
||||
tabs: TabsRecord;
|
||||
tabsOpen: Set<string>;
|
||||
setTabsOpen: (newTab: Set<string>) => void;
|
||||
hasChanged: boolean;
|
||||
}): React.ReactElement {
|
||||
const isShow = tabsOpen.has(name);
|
||||
|
||||
const toggleTabs = useCallback(() => {
|
||||
const nextState = new Set(tabsOpen);
|
||||
if (nextState.has(name)) {
|
||||
nextState.delete(name);
|
||||
} else {
|
||||
nextState.add(name);
|
||||
}
|
||||
setTabsOpen(nextState);
|
||||
}, [tabsOpen, name, setTabsOpen]);
|
||||
|
||||
// Replace spaces with non-breaking spaces
|
||||
const displayName = name.replace(/ /g, '\u00A0');
|
||||
|
||||
return (
|
||||
<div key={name} className="flex flex-row">
|
||||
{isShow ? (
|
||||
<Resizable className="border-r" minWidth={550} enable={{right: true}}>
|
||||
<h2
|
||||
title="Minimize tab"
|
||||
aria-label="Minimize tab"
|
||||
onClick={toggleTabs}
|
||||
className={`p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 ${
|
||||
hasChanged ? 'font-bold' : 'font-light'
|
||||
} text-secondary hover:text-link`}>
|
||||
- {displayName}
|
||||
</h2>
|
||||
{tabs.get(name) ?? <div>No output for {name}</div>}
|
||||
</Resizable>
|
||||
) : (
|
||||
<div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
|
||||
<button
|
||||
title={`Expand compiler tab: ${name}`}
|
||||
aria-label={`Expand compiler tab: ${name}`}
|
||||
style={{transform: 'rotate(90deg) translate(-50%)'}}
|
||||
onClick={toggleTabs}
|
||||
className={`flex-grow-0 w-5 transition-colors duration-150 ease-in ${
|
||||
hasChanged ? 'font-bold' : 'font-light'
|
||||
} text-secondary hover:text-link`}>
|
||||
{displayName}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col h-full max-w-full">
|
||||
<div className="flex p-2 flex-shrink-0">
|
||||
{Array.from(tabs.keys()).map(tab => {
|
||||
const isActive = activeTab === tab;
|
||||
return (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => onTabChange(tab)}
|
||||
className={clsx(
|
||||
'active:scale-95 transition-transform py-1.5 px-1.5 xs:px-3 sm:px-4 rounded-full text-sm',
|
||||
!isActive && 'hover:bg-primary/5',
|
||||
isActive && 'bg-highlight text-link',
|
||||
)}>
|
||||
{tab}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden w-full h-full">
|
||||
{tabs.get(activeTab)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -13,12 +13,21 @@ export default function MyApp() {
|
||||
}
|
||||
`;
|
||||
|
||||
export const defaultConfig = `\
|
||||
import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
|
||||
|
||||
({
|
||||
//compilationMode: "all"
|
||||
} satisfies Partial<PluginOptions>);`;
|
||||
|
||||
export const defaultStore: Store = {
|
||||
source: index,
|
||||
config: '',
|
||||
config: defaultConfig,
|
||||
showInternals: false,
|
||||
};
|
||||
|
||||
export const emptyStore: Store = {
|
||||
source: '',
|
||||
config: '',
|
||||
showInternals: false,
|
||||
};
|
||||
|
||||
@@ -10,19 +10,20 @@ import {
|
||||
compressToEncodedURIComponent,
|
||||
decompressFromEncodedURIComponent,
|
||||
} from 'lz-string';
|
||||
import {defaultStore} from '../defaultStore';
|
||||
import {defaultStore, defaultConfig} from '../defaultStore';
|
||||
|
||||
/**
|
||||
* Global Store for Playground
|
||||
*/
|
||||
export interface Store {
|
||||
source: string;
|
||||
config?: string;
|
||||
config: string;
|
||||
showInternals: boolean;
|
||||
}
|
||||
export function encodeStore(store: Store): string {
|
||||
return compressToEncodedURIComponent(JSON.stringify(store));
|
||||
}
|
||||
export function decodeStore(hash: string): Store {
|
||||
export function decodeStore(hash: string): any {
|
||||
return JSON.parse(decompressFromEncodedURIComponent(hash));
|
||||
}
|
||||
|
||||
@@ -63,17 +64,14 @@ export function initStoreFromUrlOrLocalStorage(): Store {
|
||||
*/
|
||||
if (!encodedSource) return defaultStore;
|
||||
|
||||
const raw = decodeStore(encodedSource);
|
||||
const raw: any = decodeStore(encodedSource);
|
||||
|
||||
invariant(isValidStore(raw), 'Invalid Store');
|
||||
|
||||
// Add config property if missing for backwards compatibility
|
||||
if (!('config' in raw)) {
|
||||
return {
|
||||
...raw,
|
||||
config: '',
|
||||
};
|
||||
}
|
||||
|
||||
return raw;
|
||||
// Make sure all properties are populated
|
||||
return {
|
||||
source: raw.source,
|
||||
config: 'config' in raw ? raw.config : defaultConfig,
|
||||
showInternals: 'showInternals' in raw ? raw.showInternals : false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -55,12 +55,16 @@ export default defineConfig({
|
||||
// contextOptions: {
|
||||
// ignoreHTTPSErrors: true,
|
||||
// },
|
||||
viewport: {width: 1920, height: 1080},
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {...devices['Desktop Chrome']},
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
viewport: {width: 1920, height: 1080},
|
||||
},
|
||||
},
|
||||
// {
|
||||
// name: 'Desktop Firefox',
|
||||
|
||||
@@ -14,52 +14,30 @@ import invariant from 'invariant';
|
||||
|
||||
export enum ErrorSeverity {
|
||||
/**
|
||||
* Invalid JS syntax, or valid syntax that is semantically invalid which may indicate some
|
||||
* misunderstanding on the user’s part.
|
||||
* An actionable error that the developer can fix. For example, product code errors should be
|
||||
* reported as such.
|
||||
*/
|
||||
InvalidJS = 'InvalidJS',
|
||||
Error = 'Error',
|
||||
/**
|
||||
* JS syntax that is not supported and which we do not plan to support. Developers should
|
||||
* rewrite to use supported forms.
|
||||
* An error that the developer may not necessarily be able to fix. For example, syntax not
|
||||
* supported by the compiler does not indicate any fault in the product code.
|
||||
*/
|
||||
UnsupportedJS = 'UnsupportedJS',
|
||||
Warning = 'Warning',
|
||||
/**
|
||||
* Code that breaks the rules of React.
|
||||
* Not an error. These will not be surfaced in ESLint, but may be surfaced in other ways
|
||||
* (eg Forgive) where informational hints can be shown.
|
||||
*/
|
||||
InvalidReact = 'InvalidReact',
|
||||
Hint = 'Hint',
|
||||
/**
|
||||
* Incorrect configuration of the compiler.
|
||||
* These errors will not be reported anywhere. Useful for work in progress validations.
|
||||
*/
|
||||
InvalidConfig = 'InvalidConfig',
|
||||
/**
|
||||
* Code that can reasonably occur and that doesn't break any rules, but is unsafe to preserve
|
||||
* 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.
|
||||
*/
|
||||
Todo = 'Todo',
|
||||
/**
|
||||
* An unexpected internal error in the compiler that indicates critical issues that can panic
|
||||
* the compiler.
|
||||
*/
|
||||
Invariant = 'Invariant',
|
||||
Off = 'Off',
|
||||
}
|
||||
|
||||
export type CompilerDiagnosticOptions = {
|
||||
category: ErrorCategory;
|
||||
severity: ErrorSeverity;
|
||||
reason: string;
|
||||
description: string;
|
||||
description: string | null;
|
||||
details: Array<CompilerDiagnosticDetail>;
|
||||
suggestions?: Array<CompilerSuggestion> | null | undefined;
|
||||
};
|
||||
@@ -71,7 +49,7 @@ export type CompilerDiagnosticDetail =
|
||||
| {
|
||||
kind: 'error';
|
||||
loc: SourceLocation | null;
|
||||
message: string;
|
||||
message: string | null;
|
||||
}
|
||||
| {
|
||||
kind: 'hint';
|
||||
@@ -100,9 +78,11 @@ export type CompilerSuggestion =
|
||||
description: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated use {@link CompilerDiagnosticOptions} instead
|
||||
*/
|
||||
export type CompilerErrorDetailOptions = {
|
||||
category: ErrorCategory;
|
||||
severity: ErrorSeverity;
|
||||
reason: string;
|
||||
description?: string | null | undefined;
|
||||
loc: SourceLocation | null;
|
||||
@@ -136,8 +116,8 @@ export class CompilerDiagnostic {
|
||||
get description(): CompilerDiagnosticOptions['description'] {
|
||||
return this.options.description;
|
||||
}
|
||||
get severity(): CompilerDiagnosticOptions['severity'] {
|
||||
return this.options.severity;
|
||||
get severity(): ErrorSeverity {
|
||||
return getRuleForCategory(this.category).severity;
|
||||
}
|
||||
get suggestions(): CompilerDiagnosticOptions['suggestions'] {
|
||||
return this.options.suggestions;
|
||||
@@ -146,8 +126,8 @@ export class CompilerDiagnostic {
|
||||
return this.options.category;
|
||||
}
|
||||
|
||||
withDetail(detail: CompilerDiagnosticDetail): CompilerDiagnostic {
|
||||
this.options.details.push(detail);
|
||||
withDetails(...details: Array<CompilerDiagnosticDetail>): CompilerDiagnostic {
|
||||
this.options.details.push(...details);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -161,11 +141,10 @@ export class CompilerDiagnostic {
|
||||
}
|
||||
|
||||
printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
|
||||
const buffer = [
|
||||
printErrorSummary(this.severity, this.reason),
|
||||
'\n\n',
|
||||
this.description,
|
||||
];
|
||||
const buffer = [printErrorSummary(this.category, this.reason)];
|
||||
if (this.description != null) {
|
||||
buffer.push('\n\n', `${this.description}.`);
|
||||
}
|
||||
for (const detail of this.options.details) {
|
||||
switch (detail.kind) {
|
||||
case 'error': {
|
||||
@@ -175,9 +154,9 @@ export class CompilerDiagnostic {
|
||||
}
|
||||
let codeFrame: string;
|
||||
try {
|
||||
codeFrame = printCodeFrame(source, loc, detail.message);
|
||||
codeFrame = printCodeFrame(source, loc, detail.message ?? '');
|
||||
} catch (e) {
|
||||
codeFrame = detail.message;
|
||||
codeFrame = detail.message ?? '';
|
||||
}
|
||||
buffer.push('\n\n');
|
||||
if (loc.filename != null) {
|
||||
@@ -207,7 +186,7 @@ export class CompilerDiagnostic {
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
const buffer = [printErrorSummary(this.severity, this.reason)];
|
||||
const buffer = [printErrorSummary(this.category, this.reason)];
|
||||
if (this.description != null) {
|
||||
buffer.push(`. ${this.description}.`);
|
||||
}
|
||||
@@ -219,9 +198,11 @@ export class CompilerDiagnostic {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* Each bailout or invariant in HIR lowering creates an {@link CompilerErrorDetail}, which is then
|
||||
* aggregated into a single {@link CompilerError} later.
|
||||
*
|
||||
* @deprecated use {@link CompilerDiagnostic} instead
|
||||
*/
|
||||
export class CompilerErrorDetail {
|
||||
options: CompilerErrorDetailOptions;
|
||||
@@ -236,8 +217,8 @@ export class CompilerErrorDetail {
|
||||
get description(): CompilerErrorDetailOptions['description'] {
|
||||
return this.options.description;
|
||||
}
|
||||
get severity(): CompilerErrorDetailOptions['severity'] {
|
||||
return this.options.severity;
|
||||
get severity(): ErrorSeverity {
|
||||
return getRuleForCategory(this.category).severity;
|
||||
}
|
||||
get loc(): CompilerErrorDetailOptions['loc'] {
|
||||
return this.options.loc;
|
||||
@@ -254,7 +235,7 @@ export class CompilerErrorDetail {
|
||||
}
|
||||
|
||||
printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
|
||||
const buffer = [printErrorSummary(this.severity, this.reason)];
|
||||
const buffer = [printErrorSummary(this.category, this.reason)];
|
||||
if (this.description != null) {
|
||||
buffer.push(`\n\n${this.description}.`);
|
||||
}
|
||||
@@ -279,7 +260,7 @@ export class CompilerErrorDetail {
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
const buffer = [printErrorSummary(this.severity, this.reason)];
|
||||
const buffer = [printErrorSummary(this.category, this.reason)];
|
||||
if (this.description != null) {
|
||||
buffer.push(`. ${this.description}.`);
|
||||
}
|
||||
@@ -291,22 +272,28 @@ export class CompilerErrorDetail {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An aggregate of {@link CompilerDiagnostic}. This allows us to aggregate all issues found by the
|
||||
* compiler into a single error before we throw. Where possible, prefer to push diagnostics into
|
||||
* the error aggregate instead of throwing immediately.
|
||||
*/
|
||||
export class CompilerError extends Error {
|
||||
details: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
|
||||
disabledDetails: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
|
||||
printedMessage: string | null = null;
|
||||
|
||||
static invariant(
|
||||
condition: unknown,
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity' | 'category'>,
|
||||
options: Omit<CompilerDiagnosticOptions, 'category'>,
|
||||
): asserts condition {
|
||||
if (!condition) {
|
||||
const errors = new CompilerError();
|
||||
errors.pushErrorDetail(
|
||||
new CompilerErrorDetail({
|
||||
...options,
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
reason: options.reason,
|
||||
description: options.description,
|
||||
category: ErrorCategory.Invariant,
|
||||
severity: ErrorSeverity.Invariant,
|
||||
}),
|
||||
}).withDetails(...options.details),
|
||||
);
|
||||
throw errors;
|
||||
}
|
||||
@@ -319,13 +306,12 @@ export class CompilerError extends Error {
|
||||
}
|
||||
|
||||
static throwTodo(
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity' | 'category'>,
|
||||
options: Omit<CompilerErrorDetailOptions, 'category'>,
|
||||
): never {
|
||||
const errors = new CompilerError();
|
||||
errors.pushErrorDetail(
|
||||
new CompilerErrorDetail({
|
||||
...options,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
}),
|
||||
);
|
||||
@@ -333,40 +319,31 @@ export class CompilerError extends Error {
|
||||
}
|
||||
|
||||
static throwInvalidJS(
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity' | 'category'>,
|
||||
options: Omit<CompilerErrorDetailOptions, 'category'>,
|
||||
): never {
|
||||
const errors = new CompilerError();
|
||||
errors.pushErrorDetail(
|
||||
new CompilerErrorDetail({
|
||||
...options,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
}),
|
||||
);
|
||||
throw errors;
|
||||
}
|
||||
|
||||
static throwInvalidReact(
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity'>,
|
||||
): never {
|
||||
static throwInvalidReact(options: CompilerErrorDetailOptions): never {
|
||||
const errors = new CompilerError();
|
||||
errors.pushErrorDetail(
|
||||
new CompilerErrorDetail({
|
||||
...options,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
}),
|
||||
);
|
||||
errors.pushErrorDetail(new CompilerErrorDetail(options));
|
||||
throw errors;
|
||||
}
|
||||
|
||||
static throwInvalidConfig(
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity' | 'category'>,
|
||||
options: Omit<CompilerErrorDetailOptions, 'category'>,
|
||||
): never {
|
||||
const errors = new CompilerError();
|
||||
errors.pushErrorDetail(
|
||||
new CompilerErrorDetail({
|
||||
...options,
|
||||
severity: ErrorSeverity.InvalidConfig,
|
||||
category: ErrorCategory.Config,
|
||||
}),
|
||||
);
|
||||
@@ -383,6 +360,7 @@ export class CompilerError extends Error {
|
||||
super(...args);
|
||||
this.name = 'ReactCompilerError';
|
||||
this.details = [];
|
||||
this.disabledDetails = [];
|
||||
}
|
||||
|
||||
override get message(): string {
|
||||
@@ -423,62 +401,93 @@ export class CompilerError extends Error {
|
||||
|
||||
merge(other: CompilerError): void {
|
||||
this.details.push(...other.details);
|
||||
this.disabledDetails.push(...other.disabledDetails);
|
||||
}
|
||||
|
||||
pushDiagnostic(diagnostic: CompilerDiagnostic): void {
|
||||
this.details.push(diagnostic);
|
||||
if (diagnostic.severity === ErrorSeverity.Off) {
|
||||
this.disabledDetails.push(diagnostic);
|
||||
} else {
|
||||
this.details.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use {@link pushDiagnostic} instead
|
||||
*/
|
||||
push(options: CompilerErrorDetailOptions): CompilerErrorDetail {
|
||||
const detail = new CompilerErrorDetail({
|
||||
category: options.category,
|
||||
reason: options.reason,
|
||||
description: options.description ?? null,
|
||||
severity: options.severity,
|
||||
suggestions: options.suggestions,
|
||||
loc: typeof options.loc === 'symbol' ? null : options.loc,
|
||||
});
|
||||
return this.pushErrorDetail(detail);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use {@link pushDiagnostic} instead
|
||||
*/
|
||||
pushErrorDetail(detail: CompilerErrorDetail): CompilerErrorDetail {
|
||||
this.details.push(detail);
|
||||
if (detail.severity === ErrorSeverity.Off) {
|
||||
this.disabledDetails.push(detail);
|
||||
} else {
|
||||
this.details.push(detail);
|
||||
}
|
||||
return detail;
|
||||
}
|
||||
|
||||
hasErrors(): boolean {
|
||||
hasAnyErrors(): boolean {
|
||||
return this.details.length > 0;
|
||||
}
|
||||
|
||||
asResult(): Result<void, CompilerError> {
|
||||
return this.hasErrors() ? Err(this) : Ok(undefined);
|
||||
return this.hasAnyErrors() ? Err(this) : Ok(undefined);
|
||||
}
|
||||
|
||||
/*
|
||||
* An error is critical if it means the compiler has entered into a broken state and cannot
|
||||
* continue safely. Other expected errors such as Todos mean that we can skip over that component
|
||||
* but otherwise continue compiling the rest of the app.
|
||||
/**
|
||||
* Returns true if any of the error details are of severity Error.
|
||||
*/
|
||||
isCritical(): boolean {
|
||||
return this.details.some(detail => {
|
||||
switch (detail.severity) {
|
||||
case ErrorSeverity.Invariant:
|
||||
case ErrorSeverity.InvalidJS:
|
||||
case ErrorSeverity.InvalidReact:
|
||||
case ErrorSeverity.InvalidConfig:
|
||||
case ErrorSeverity.UnsupportedJS:
|
||||
case ErrorSeverity.IncompatibleLibrary: {
|
||||
return true;
|
||||
}
|
||||
case ErrorSeverity.CannotPreserveMemoization:
|
||||
case ErrorSeverity.Todo: {
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(detail.severity, 'Unhandled error severity');
|
||||
}
|
||||
hasErrors(): boolean {
|
||||
for (const detail of this.details) {
|
||||
if (detail.severity === ErrorSeverity.Error) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there are no Errors and there is at least one Warning.
|
||||
*/
|
||||
hasWarning(): boolean {
|
||||
let res = false;
|
||||
for (const detail of this.details) {
|
||||
if (detail.severity === ErrorSeverity.Error) {
|
||||
return false;
|
||||
}
|
||||
if (detail.severity === ErrorSeverity.Warning) {
|
||||
res = true;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
hasHints(): boolean {
|
||||
let res = false;
|
||||
for (const detail of this.details) {
|
||||
if (detail.severity === ErrorSeverity.Error) {
|
||||
return false;
|
||||
}
|
||||
if (detail.severity === ErrorSeverity.Warning) {
|
||||
return false;
|
||||
}
|
||||
if (detail.severity === ErrorSeverity.Hint) {
|
||||
res = true;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,115 +514,161 @@ function printCodeFrame(
|
||||
);
|
||||
}
|
||||
|
||||
function printErrorSummary(severity: ErrorSeverity, message: string): string {
|
||||
let severityCategory: string;
|
||||
switch (severity) {
|
||||
case ErrorSeverity.InvalidConfig:
|
||||
case ErrorSeverity.InvalidJS:
|
||||
case ErrorSeverity.InvalidReact:
|
||||
case ErrorSeverity.UnsupportedJS: {
|
||||
severityCategory = 'Error';
|
||||
function printErrorSummary(category: ErrorCategory, message: string): string {
|
||||
let heading: string;
|
||||
switch (category) {
|
||||
case ErrorCategory.AutomaticEffectDependencies:
|
||||
case ErrorCategory.CapitalizedCalls:
|
||||
case ErrorCategory.Config:
|
||||
case ErrorCategory.EffectStateDerivationCalculateInRender:
|
||||
case ErrorCategory.EffectSetState:
|
||||
case ErrorCategory.ErrorBoundaries:
|
||||
case ErrorCategory.Factories:
|
||||
case ErrorCategory.FBT:
|
||||
case ErrorCategory.Fire:
|
||||
case ErrorCategory.Gating:
|
||||
case ErrorCategory.Globals:
|
||||
case ErrorCategory.Hooks:
|
||||
case ErrorCategory.Immutability:
|
||||
case ErrorCategory.Purity:
|
||||
case ErrorCategory.Refs:
|
||||
case ErrorCategory.RenderSetState:
|
||||
case ErrorCategory.StaticComponents:
|
||||
case ErrorCategory.Suppression:
|
||||
case ErrorCategory.Syntax:
|
||||
case ErrorCategory.UseMemo: {
|
||||
heading = 'Error';
|
||||
break;
|
||||
}
|
||||
case ErrorSeverity.IncompatibleLibrary:
|
||||
case ErrorSeverity.CannotPreserveMemoization: {
|
||||
severityCategory = 'Compilation Skipped';
|
||||
case ErrorCategory.EffectDependencies:
|
||||
case ErrorCategory.IncompatibleLibrary:
|
||||
case ErrorCategory.PreserveManualMemo:
|
||||
case ErrorCategory.UnsupportedSyntax: {
|
||||
heading = 'Compilation Skipped';
|
||||
break;
|
||||
}
|
||||
case ErrorSeverity.Invariant: {
|
||||
severityCategory = 'Invariant';
|
||||
case ErrorCategory.Invariant: {
|
||||
heading = 'Invariant';
|
||||
break;
|
||||
}
|
||||
case ErrorSeverity.Todo: {
|
||||
severityCategory = 'Todo';
|
||||
case ErrorCategory.Todo: {
|
||||
heading = 'Todo';
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(severity, `Unexpected severity '${severity}'`);
|
||||
assertExhaustive(category, `Unhandled category '${category}'`);
|
||||
}
|
||||
}
|
||||
return `${severityCategory}: ${message}`;
|
||||
return `${heading}: ${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)
|
||||
/**
|
||||
* 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)
|
||||
/**
|
||||
* Checking for no capitalized calls (not definitively an error, hence separating)
|
||||
*/
|
||||
CapitalizedCalls = 'CapitalizedCalls',
|
||||
|
||||
// Checking for static components
|
||||
/**
|
||||
* Checking for static components
|
||||
*/
|
||||
StaticComponents = 'StaticComponents',
|
||||
|
||||
// Checking for valid usage of manual memoization
|
||||
/**
|
||||
* Checking for valid usage of manual memoization
|
||||
*/
|
||||
UseMemo = 'UseMemo',
|
||||
|
||||
// Checking for higher order functions acting as factories for components/hooks
|
||||
/**
|
||||
* Checking for higher order functions acting as factories for components/hooks
|
||||
*/
|
||||
Factories = 'Factories',
|
||||
|
||||
// Checks that manual memoization is preserved
|
||||
/**
|
||||
* Checks that manual memoization is preserved
|
||||
*/
|
||||
PreserveManualMemo = 'PreserveManualMemo',
|
||||
|
||||
// Checks for known incompatible libraries
|
||||
/**
|
||||
* Checks for known incompatible libraries
|
||||
*/
|
||||
IncompatibleLibrary = 'IncompatibleLibrary',
|
||||
|
||||
// Checking for no mutations of props, hook arguments, hook return values
|
||||
/**
|
||||
* Checking for no mutations of props, hook arguments, hook return values
|
||||
*/
|
||||
Immutability = 'Immutability',
|
||||
|
||||
// Checking for assignments to globals
|
||||
/**
|
||||
* Checking for assignments to globals
|
||||
*/
|
||||
Globals = 'Globals',
|
||||
|
||||
// Checking for valid usage of refs, ie no access during render
|
||||
/**
|
||||
* Checking for valid usage of refs, ie no access during render
|
||||
*/
|
||||
Refs = 'Refs',
|
||||
|
||||
// Checks for memoized effect deps
|
||||
/**
|
||||
* Checks for memoized effect deps
|
||||
*/
|
||||
EffectDependencies = 'EffectDependencies',
|
||||
|
||||
// Checks for no setState in effect bodies
|
||||
/**
|
||||
* Checks for no setState in effect bodies
|
||||
*/
|
||||
EffectSetState = 'EffectSetState',
|
||||
|
||||
EffectDerivationsOfState = 'EffectDerivationsOfState',
|
||||
|
||||
// Validates against try/catch in place of error boundaries
|
||||
/**
|
||||
* Checks for no deriving state in effects, solved by calculate in render
|
||||
*/
|
||||
EffectStateDerivationCalculateInRender = 'EffectStateDerivationCalculateInRender',
|
||||
/**
|
||||
* Validates against try/catch in place of error boundaries
|
||||
*/
|
||||
ErrorBoundaries = 'ErrorBoundaries',
|
||||
|
||||
// Checking for pure functions
|
||||
/**
|
||||
* Checking for pure functions
|
||||
*/
|
||||
Purity = 'Purity',
|
||||
|
||||
// Validates against setState in render
|
||||
/**
|
||||
* Validates against setState in render
|
||||
*/
|
||||
RenderSetState = 'RenderSetState',
|
||||
|
||||
// Internal invariants
|
||||
/**
|
||||
* Internal invariants
|
||||
*/
|
||||
Invariant = 'Invariant',
|
||||
|
||||
// Todos
|
||||
/**
|
||||
* Todos
|
||||
*/
|
||||
Todo = 'Todo',
|
||||
|
||||
// Syntax errors
|
||||
/**
|
||||
* Syntax errors
|
||||
*/
|
||||
Syntax = 'Syntax',
|
||||
|
||||
// Checks for use of unsupported syntax
|
||||
/**
|
||||
* Checks for use of unsupported syntax
|
||||
*/
|
||||
UnsupportedSyntax = 'UnsupportedSyntax',
|
||||
|
||||
// Config errors
|
||||
/**
|
||||
* Config errors
|
||||
*/
|
||||
Config = 'Config',
|
||||
|
||||
// Gating error
|
||||
/**
|
||||
* Gating error
|
||||
*/
|
||||
Gating = 'Gating',
|
||||
|
||||
// Suppressions
|
||||
/**
|
||||
* Suppressions
|
||||
*/
|
||||
Suppression = 'Suppression',
|
||||
|
||||
// Issues with auto deps
|
||||
/**
|
||||
* Issues with auto deps
|
||||
*/
|
||||
AutomaticEffectDependencies = 'AutomaticEffectDependencies',
|
||||
|
||||
// Issues with `fire`
|
||||
/**
|
||||
* Issues with `fire`
|
||||
*/
|
||||
Fire = 'Fire',
|
||||
|
||||
// fbt-specific issues
|
||||
/**
|
||||
* fbt-specific issues
|
||||
*/
|
||||
FBT = 'FBT',
|
||||
}
|
||||
|
||||
@@ -621,6 +676,9 @@ export type LintRule = {
|
||||
// Stores the category the rule corresponds to, used to filter errors when reporting
|
||||
category: ErrorCategory;
|
||||
|
||||
// Stores the severity of the error, which is used to map to lint levels such as error/warning.
|
||||
severity: ErrorSeverity;
|
||||
|
||||
/**
|
||||
* The "name" of the rule as it will be used by developers to enable/disable, eg
|
||||
* "eslint-disable-nest line <name>"
|
||||
@@ -661,6 +719,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
case ErrorCategory.AutomaticEffectDependencies: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'automatic-effect-dependencies',
|
||||
description:
|
||||
'Verifies that automatic effect dependencies are compiled if opted-in',
|
||||
@@ -670,6 +729,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
case ErrorCategory.CapitalizedCalls: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'capitalized-calls',
|
||||
description:
|
||||
'Validates against calling capitalized functions/methods instead of using JSX',
|
||||
@@ -679,6 +739,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
case ErrorCategory.Config: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'config',
|
||||
description: 'Validates the compiler configuration options',
|
||||
recommended: true,
|
||||
@@ -687,15 +748,17 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
case ErrorCategory.EffectDependencies: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'memoized-effect-dependencies',
|
||||
description: 'Validates that effect dependencies are memoized',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectDerivationsOfState: {
|
||||
case ErrorCategory.EffectStateDerivationCalculateInRender: {
|
||||
return {
|
||||
category,
|
||||
name: 'no-deriving-state-in-effects',
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'no-deriving-state-in-effects-calculate-in-render',
|
||||
description:
|
||||
'Validates against deriving values from state in an effect',
|
||||
recommended: false,
|
||||
@@ -704,6 +767,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
case ErrorCategory.EffectSetState: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'set-state-in-effect',
|
||||
description:
|
||||
'Validates against calling setState synchronously in an effect, which can lead to re-renders that degrade performance',
|
||||
@@ -713,6 +777,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
case ErrorCategory.ErrorBoundaries: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'error-boundaries',
|
||||
description:
|
||||
'Validates usage of error boundaries instead of try/catch for errors in child components',
|
||||
@@ -722,6 +787,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
case ErrorCategory.Factories: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'component-hook-factories',
|
||||
description:
|
||||
'Validates against higher order functions defining nested components or hooks. ' +
|
||||
@@ -732,6 +798,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
case ErrorCategory.FBT: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'fbt',
|
||||
description: 'Validates usage of fbt',
|
||||
recommended: false,
|
||||
@@ -740,6 +807,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
case ErrorCategory.Fire: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'fire',
|
||||
description: 'Validates usage of `fire`',
|
||||
recommended: false,
|
||||
@@ -748,6 +816,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
case ErrorCategory.Gating: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'gating',
|
||||
description:
|
||||
'Validates configuration of [gating mode](https://react.dev/reference/react-compiler/gating)',
|
||||
@@ -757,6 +826,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
case ErrorCategory.Globals: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'globals',
|
||||
description:
|
||||
'Validates against assignment/mutation of globals during render, part of ensuring that ' +
|
||||
@@ -767,6 +837,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
case ErrorCategory.Hooks: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'hooks',
|
||||
description: 'Validates the rules of hooks',
|
||||
/**
|
||||
@@ -780,6 +851,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
case ErrorCategory.Immutability: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
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)',
|
||||
@@ -789,6 +861,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
case ErrorCategory.Invariant: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'invariant',
|
||||
description: 'Internal invariants',
|
||||
recommended: false,
|
||||
@@ -797,6 +870,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
case ErrorCategory.PreserveManualMemo: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'preserve-manual-memoization',
|
||||
description:
|
||||
'Validates that existing manual memoized is preserved by the compiler. ' +
|
||||
@@ -808,6 +882,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
case ErrorCategory.Purity: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
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',
|
||||
@@ -817,6 +892,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
case ErrorCategory.Refs: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
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)',
|
||||
@@ -826,6 +902,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
case ErrorCategory.RenderSetState: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'set-state-in-render',
|
||||
description:
|
||||
'Validates against setting state during render, which can trigger additional renders and potential infinite render loops',
|
||||
@@ -835,6 +912,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
case ErrorCategory.StaticComponents: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
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',
|
||||
@@ -844,6 +922,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
case ErrorCategory.Suppression: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'rule-suppression',
|
||||
description: 'Validates against suppression of other rules',
|
||||
recommended: false,
|
||||
@@ -852,6 +931,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
case ErrorCategory.Syntax: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'syntax',
|
||||
description: 'Validates against invalid syntax',
|
||||
recommended: false,
|
||||
@@ -860,6 +940,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
case ErrorCategory.Todo: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Hint,
|
||||
name: 'todo',
|
||||
description: 'Unimplemented features',
|
||||
recommended: false,
|
||||
@@ -868,6 +949,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
case ErrorCategory.UnsupportedSyntax: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Warning,
|
||||
name: 'unsupported-syntax',
|
||||
description:
|
||||
'Validates against syntax that we do not plan to support in React Compiler',
|
||||
@@ -877,6 +959,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
case ErrorCategory.UseMemo: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
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.',
|
||||
@@ -886,6 +969,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
case ErrorCategory.IncompatibleLibrary: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Warning,
|
||||
name: 'incompatible-library',
|
||||
description:
|
||||
'Validates against usage of libraries which are incompatible with memoization (manual or automatic)',
|
||||
|
||||
@@ -51,12 +51,26 @@ function insertAdditionalFunctionDeclaration(
|
||||
CompilerError.invariant(originalFnName != null && compiled.id != null, {
|
||||
reason:
|
||||
'Expected function declarations that are referenced elsewhere to have a named identifier',
|
||||
loc: fnPath.node.loc ?? null,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: fnPath.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
CompilerError.invariant(originalFnParams.length === compiledParams.length, {
|
||||
reason:
|
||||
'Expected React Compiler optimized function declarations to have the same number of parameters as source',
|
||||
loc: fnPath.node.loc ?? null,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: fnPath.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const gatingCondition = t.identifier(
|
||||
@@ -140,7 +154,13 @@ export function insertGatedFunctionDeclaration(
|
||||
CompilerError.invariant(compiled.type === 'FunctionDeclaration', {
|
||||
reason: 'Expected compiled node type to match input type',
|
||||
description: `Got ${compiled.type} but expected FunctionDeclaration`,
|
||||
loc: fnPath.node.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: fnPath.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
insertAdditionalFunctionDeclaration(
|
||||
fnPath,
|
||||
|
||||
@@ -9,7 +9,7 @@ import {NodePath} from '@babel/core';
|
||||
import * as t from '@babel/types';
|
||||
import {Scope as BabelScope} from '@babel/traverse';
|
||||
|
||||
import {CompilerError, ErrorCategory, ErrorSeverity} from '../CompilerError';
|
||||
import {CompilerError, ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
EnvironmentConfig,
|
||||
GeneratedSource,
|
||||
@@ -39,7 +39,6 @@ export function validateRestrictedImports(
|
||||
if (restrictedImports.has(importDeclPath.node.source.value)) {
|
||||
error.push({
|
||||
category: ErrorCategory.Todo,
|
||||
severity: ErrorSeverity.Todo,
|
||||
reason: 'Bailing out due to blocklisted import',
|
||||
description: `Import from module ${importDeclPath.node.source.value}`,
|
||||
loc: importDeclPath.node.loc ?? null,
|
||||
@@ -47,7 +46,7 @@ export function validateRestrictedImports(
|
||||
}
|
||||
},
|
||||
});
|
||||
if (error.hasErrors()) {
|
||||
if (error.hasAnyErrors()) {
|
||||
return error;
|
||||
} else {
|
||||
return null;
|
||||
@@ -207,7 +206,6 @@ export class ProgramContext {
|
||||
const error = new CompilerError();
|
||||
error.push({
|
||||
category: ErrorCategory.Todo,
|
||||
severity: ErrorSeverity.Todo,
|
||||
reason: 'Encountered conflicting global in generated program',
|
||||
description: `Conflict from local binding ${name}`,
|
||||
loc: scope.getBinding(name)?.path.node.loc ?? null,
|
||||
@@ -258,8 +256,14 @@ export function addImportsToProgram(
|
||||
{
|
||||
reason:
|
||||
'Encountered conflicting import specifiers in generated program',
|
||||
description: `Conflict from import ${loweredImport.module}:(${loweredImport.imported} as ${loweredImport.name}).`,
|
||||
loc: GeneratedSource,
|
||||
description: `Conflict from import ${loweredImport.module}:(${loweredImport.imported} as ${loweredImport.name})`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
@@ -270,7 +274,13 @@ export function addImportsToProgram(
|
||||
reason:
|
||||
'Found inconsistent import specifier. This is an internal bug.',
|
||||
description: `Expected import ${moduleName}:${specifierName} but found ${loweredImport.module}:${loweredImport.imported}`,
|
||||
loc: GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -135,7 +135,12 @@ export type PluginOptions = {
|
||||
*/
|
||||
eslintSuppressionRules: Array<string> | null | undefined;
|
||||
|
||||
/**
|
||||
* Whether to report "suppression" errors for Flow suppressions. If false, suppression errors
|
||||
* are only emitted for ESLint suppressions
|
||||
*/
|
||||
flowSuppressions: boolean;
|
||||
|
||||
/*
|
||||
* Ignore 'use no forget' annotations. Helpful during testing but should not be used in production.
|
||||
*/
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import {NodePath} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import prettyFormat from 'pretty-format';
|
||||
import {Logger, ProgramContext, SingleLineSuppressionRange} from '.';
|
||||
import {Logger, ProgramContext} from '.';
|
||||
import {
|
||||
HIRFunction,
|
||||
ReactiveFunction,
|
||||
@@ -103,6 +103,7 @@ import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoF
|
||||
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
|
||||
import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
|
||||
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
|
||||
import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
|
||||
|
||||
export type CompilerPipelineValue =
|
||||
| {kind: 'ast'; name: string; value: CodegenFunction}
|
||||
@@ -121,7 +122,6 @@ function run(
|
||||
logger: Logger | null,
|
||||
filename: string | null,
|
||||
code: string | null,
|
||||
suppressions: Array<SingleLineSuppressionRange>,
|
||||
): CodegenFunction {
|
||||
const contextIdentifiers = findContextIdentifiers(func);
|
||||
const env = new Environment(
|
||||
@@ -135,7 +135,6 @@ function run(
|
||||
filename,
|
||||
code,
|
||||
programContext,
|
||||
suppressions,
|
||||
);
|
||||
env.logger?.debugLogIRs?.({
|
||||
kind: 'debug',
|
||||
@@ -277,7 +276,7 @@ function runWithEnvironment(
|
||||
}
|
||||
|
||||
if (env.config.validateNoSetStateInEffects) {
|
||||
env.logErrors(validateNoSetStateInEffects(hir));
|
||||
env.logErrors(validateNoSetStateInEffects(hir, env));
|
||||
}
|
||||
|
||||
if (env.config.validateNoJSXInTryStatements) {
|
||||
@@ -326,6 +325,15 @@ function runWithEnvironment(
|
||||
outlineJSX(hir);
|
||||
}
|
||||
|
||||
if (env.config.enableNameAnonymousFunctions) {
|
||||
nameAnonymousFunctions(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'NameAnonymousFunctions',
|
||||
value: hir,
|
||||
});
|
||||
}
|
||||
|
||||
if (env.config.enableFunctionOutlining) {
|
||||
outlineFunctions(hir, fbtOperands);
|
||||
log({kind: 'hir', name: 'OutlineFunctions', value: hir});
|
||||
@@ -569,7 +577,6 @@ export function compileFn(
|
||||
logger: Logger | null,
|
||||
filename: string | null,
|
||||
code: string | null,
|
||||
singleLineSuppressions: Array<SingleLineSuppressionRange>,
|
||||
): CodegenFunction {
|
||||
return run(
|
||||
func,
|
||||
@@ -580,6 +587,5 @@ export function compileFn(
|
||||
logger,
|
||||
filename,
|
||||
code,
|
||||
singleLineSuppressions,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
CompilerError,
|
||||
CompilerErrorDetail,
|
||||
ErrorCategory,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {ExternalFunction, ReactFunctionType} from '../HIR/Environment';
|
||||
import {CodegenFunction} from '../ReactiveScopes';
|
||||
@@ -27,9 +26,8 @@ import {
|
||||
import {CompilerReactTarget, PluginOptions} from './Options';
|
||||
import {compileFn} from './Pipeline';
|
||||
import {
|
||||
filterSuppressionsThatAffectNode,
|
||||
filterSuppressionsThatAffectFunction,
|
||||
findProgramSuppressions,
|
||||
SingleLineSuppressionRange,
|
||||
suppressionsToCompilerError,
|
||||
} from './Suppression';
|
||||
import {GeneratedSource} from '../HIR';
|
||||
@@ -106,7 +104,6 @@ function findDirectivesDynamicGating(
|
||||
errors.push({
|
||||
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,
|
||||
@@ -114,7 +111,7 @@ function findDirectivesDynamicGating(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.hasErrors()) {
|
||||
if (errors.hasAnyErrors()) {
|
||||
return Err(errors);
|
||||
} else if (result.length > 1) {
|
||||
const error = new CompilerError();
|
||||
@@ -123,7 +120,6 @@ function findDirectivesDynamicGating(
|
||||
description: `Expected a single directive but found [${result
|
||||
.map(r => r.directive.value.value)
|
||||
.join(', ')}]`,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: ErrorCategory.Gating,
|
||||
loc: result[0].directive.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -142,15 +138,13 @@ function findDirectivesDynamicGating(
|
||||
}
|
||||
}
|
||||
|
||||
function isCriticalError(err: unknown): boolean {
|
||||
return !(err instanceof CompilerError) || err.isCritical();
|
||||
function isError(err: unknown): boolean {
|
||||
return !(err instanceof CompilerError) || err.hasErrors();
|
||||
}
|
||||
|
||||
function isConfigError(err: unknown): boolean {
|
||||
if (err instanceof CompilerError) {
|
||||
return err.details.some(
|
||||
detail => detail.severity === ErrorSeverity.InvalidConfig,
|
||||
);
|
||||
return err.details.some(detail => detail.category === ErrorCategory.Config);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -215,8 +209,7 @@ function handleError(
|
||||
logError(err, context, fnLoc);
|
||||
if (
|
||||
context.opts.panicThreshold === 'all_errors' ||
|
||||
(context.opts.panicThreshold === 'critical_errors' &&
|
||||
isCriticalError(err)) ||
|
||||
(context.opts.panicThreshold === 'critical_errors' && isError(err)) ||
|
||||
isConfigError(err) // Always throws regardless of panic threshold
|
||||
) {
|
||||
throw err;
|
||||
@@ -317,7 +310,13 @@ function insertNewOutlinedFunctionNode(
|
||||
CompilerError.invariant(insertedFuncDecl.isFunctionDeclaration(), {
|
||||
reason: 'Expected inserted function declaration',
|
||||
description: `Got: ${insertedFuncDecl}`,
|
||||
loc: insertedFuncDecl.node?.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: insertedFuncDecl.node?.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
return insertedFuncDecl;
|
||||
}
|
||||
@@ -426,7 +425,14 @@ export function compileProgram(
|
||||
for (const outlined of compiled.outlined) {
|
||||
CompilerError.invariant(outlined.fn.outlined.length === 0, {
|
||||
reason: 'Unexpected nested outlined functions',
|
||||
loc: outlined.fn.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: outlined.fn.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
const fn = insertNewOutlinedFunctionNode(
|
||||
program,
|
||||
@@ -459,7 +465,6 @@ export function compileProgram(
|
||||
new CompilerErrorDetail({
|
||||
reason:
|
||||
'Unexpected compiled functions when module scope opt-out is present',
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: ErrorCategory.Invariant,
|
||||
loc: null,
|
||||
}),
|
||||
@@ -692,17 +697,11 @@ function tryCompileFunction(
|
||||
* Program node itself. We need to figure out whether an eslint suppression range
|
||||
* applies to this function first.
|
||||
*/
|
||||
const suppressionsInFunction = filterSuppressionsThatAffectNode(
|
||||
const suppressionsInFunction = filterSuppressionsThatAffectFunction(
|
||||
programContext.suppressions,
|
||||
fn,
|
||||
);
|
||||
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) {
|
||||
if (suppressionsInFunction.length > 0) {
|
||||
return {
|
||||
kind: 'error',
|
||||
error: suppressionsToCompilerError(suppressionsInFunction),
|
||||
@@ -721,7 +720,6 @@ function tryCompileFunction(
|
||||
programContext.opts.logger,
|
||||
programContext.filename,
|
||||
programContext.code,
|
||||
singleLineSuppressions,
|
||||
),
|
||||
};
|
||||
} catch (err) {
|
||||
@@ -760,7 +758,6 @@ function retryCompileFunction(
|
||||
programContext.opts.logger,
|
||||
programContext.filename,
|
||||
programContext.code,
|
||||
[], // ignore suppressions in the retry pipeline
|
||||
);
|
||||
|
||||
if (!retryResult.hasFireRewrite && !retryResult.hasInferredEffect) {
|
||||
@@ -836,7 +833,6 @@ function shouldSkipCompilation(
|
||||
reason: `Expected a filename but found none.`,
|
||||
description:
|
||||
"When the 'sources' config options is specified, the React compiler will only compile files with a name",
|
||||
severity: ErrorSeverity.InvalidConfig,
|
||||
category: ErrorCategory.Config,
|
||||
loc: null,
|
||||
}),
|
||||
@@ -899,7 +895,6 @@ function validateNoDynamicallyCreatedComponentsOrHooks(
|
||||
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: [
|
||||
@@ -1425,7 +1420,13 @@ export function getReactCompilerRuntimeModule(
|
||||
{
|
||||
reason: 'Expected target to already be validated',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
CompilerError,
|
||||
CompilerSuggestionOperation,
|
||||
ErrorCategory,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {GeneratedSource} from '../HIR';
|
||||
@@ -25,23 +24,11 @@ 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 =
|
||||
| {
|
||||
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'}
|
||||
>;
|
||||
export type SuppressionRange = {
|
||||
disableComment: t.Comment;
|
||||
enableComment: t.Comment | null;
|
||||
source: SuppressionSource;
|
||||
};
|
||||
|
||||
type SuppressionSource = 'Eslint' | 'Flow';
|
||||
|
||||
@@ -50,23 +37,15 @@ type SuppressionSource = 'Eslint' | 'Flow';
|
||||
* 1. The suppression is within the function's body; or
|
||||
* 2. The suppression wraps the function
|
||||
*/
|
||||
export function filterSuppressionsThatAffectNode<T extends SuppressionRange>(
|
||||
suppressionRanges: Array<T>,
|
||||
node: NodePath,
|
||||
): Array<T> {
|
||||
const suppressionsInScope: Array<T> = [];
|
||||
const fnNode = node.node;
|
||||
export function filterSuppressionsThatAffectFunction(
|
||||
suppressionRanges: Array<SuppressionRange>,
|
||||
fn: NodePath<t.Function>,
|
||||
): Array<SuppressionRange> {
|
||||
const suppressionsInScope: Array<SuppressionRange> = [];
|
||||
const fnNode = fn.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 (
|
||||
disableComment.start == null ||
|
||||
suppressionRange.disableComment.start == null ||
|
||||
fnNode.start == null ||
|
||||
fnNode.end == null
|
||||
) {
|
||||
@@ -74,20 +53,22 @@ export function filterSuppressionsThatAffectNode<T extends SuppressionRange>(
|
||||
}
|
||||
// The suppression is within the function
|
||||
if (
|
||||
disableComment.start > fnNode.start &&
|
||||
suppressionRange.disableComment.start > fnNode.start &&
|
||||
// If there is no matching enable, the rest of the file has potential violations
|
||||
(enableComment === null ||
|
||||
(enableComment.end != null && enableComment.end < fnNode.end))
|
||||
(suppressionRange.enableComment === null ||
|
||||
(suppressionRange.enableComment.end != null &&
|
||||
suppressionRange.enableComment.end < fnNode.end))
|
||||
) {
|
||||
suppressionsInScope.push(suppressionRange);
|
||||
}
|
||||
|
||||
// The suppression wraps the function
|
||||
if (
|
||||
disableComment.start < fnNode.start &&
|
||||
suppressionRange.disableComment.start < fnNode.start &&
|
||||
// If there is no matching enable, the rest of the file has potential violations
|
||||
(enableComment === null ||
|
||||
(enableComment.end != null && enableComment.end > fnNode.end))
|
||||
(suppressionRange.enableComment === null ||
|
||||
(suppressionRange.enableComment.end != null &&
|
||||
suppressionRange.enableComment.end > fnNode.end))
|
||||
) {
|
||||
suppressionsInScope.push(suppressionRange);
|
||||
}
|
||||
@@ -101,14 +82,22 @@ export function findProgramSuppressions(
|
||||
flowSuppressions: boolean,
|
||||
): Array<SuppressionRange> {
|
||||
const suppressionRanges: Array<SuppressionRange> = [];
|
||||
let suppression: SuppressionRange | null = null;
|
||||
let disableComment: t.Comment | null = null;
|
||||
let enableComment: t.Comment | null = null;
|
||||
let source: SuppressionSource | null = null;
|
||||
|
||||
let disableNextLinePattern: RegExp | null = null;
|
||||
let disablePattern: RegExp | null = null;
|
||||
let enablePattern: RegExp | null = null;
|
||||
if (ruleNames.length !== 0) {
|
||||
const rulePattern = `(${ruleNames.join('|')})`;
|
||||
disableNextLinePattern = new RegExp(
|
||||
`eslint-disable-next-line ${rulePattern}`,
|
||||
);
|
||||
disablePattern = new RegExp(`eslint-disable ${rulePattern}`);
|
||||
enablePattern = new RegExp(`eslint-enable ${rulePattern}`);
|
||||
}
|
||||
|
||||
const rulePattern = `(${ruleNames.join('|')})`;
|
||||
const disableNextLinePattern = new RegExp(
|
||||
`eslint-disable-next-line ${rulePattern}`,
|
||||
);
|
||||
const disablePattern = new RegExp(`eslint-disable ${rulePattern}`);
|
||||
const enablePattern = new RegExp(`eslint-enable ${rulePattern}`);
|
||||
const flowSuppressionPattern = new RegExp(
|
||||
'\\$(FlowFixMe\\w*|FlowExpectedError|FlowIssue)\\[react\\-rule',
|
||||
);
|
||||
@@ -123,49 +112,47 @@ export function findProgramSuppressions(
|
||||
* If we're already within a CommentBlock, we should not restart the range prematurely for a
|
||||
* CommentLine within the block.
|
||||
*/
|
||||
suppression == null &&
|
||||
disableComment == null &&
|
||||
disableNextLinePattern != null &&
|
||||
disableNextLinePattern.test(comment.value)
|
||||
) {
|
||||
suppression = {
|
||||
kind: 'single-line',
|
||||
comment,
|
||||
source: 'Eslint',
|
||||
};
|
||||
disableComment = comment;
|
||||
enableComment = comment;
|
||||
source = 'Eslint';
|
||||
}
|
||||
|
||||
if (
|
||||
flowSuppressions &&
|
||||
suppression == null &&
|
||||
disableComment == null &&
|
||||
flowSuppressionPattern.test(comment.value)
|
||||
) {
|
||||
suppression = {
|
||||
kind: 'single-line',
|
||||
comment,
|
||||
source: 'Flow',
|
||||
};
|
||||
disableComment = comment;
|
||||
enableComment = comment;
|
||||
source = 'Flow';
|
||||
}
|
||||
|
||||
if (disablePattern.test(comment.value)) {
|
||||
suppression = {
|
||||
kind: 'multi-line',
|
||||
disableComment: comment,
|
||||
enableComment: null,
|
||||
source: 'Eslint',
|
||||
};
|
||||
if (disablePattern != null && disablePattern.test(comment.value)) {
|
||||
disableComment = comment;
|
||||
source = 'Eslint';
|
||||
}
|
||||
|
||||
if (
|
||||
enablePattern != null &&
|
||||
enablePattern.test(comment.value) &&
|
||||
suppression != null &&
|
||||
suppression.kind === 'multi-line' &&
|
||||
suppression.source === 'Eslint'
|
||||
source === 'Eslint'
|
||||
) {
|
||||
suppression.enableComment = comment;
|
||||
enableComment = comment;
|
||||
}
|
||||
|
||||
if (suppression != null) {
|
||||
suppressionRanges.push(suppression);
|
||||
suppression = null;
|
||||
if (disableComment != null && source != null) {
|
||||
suppressionRanges.push({
|
||||
disableComment: disableComment,
|
||||
enableComment: enableComment,
|
||||
source,
|
||||
});
|
||||
disableComment = null;
|
||||
enableComment = null;
|
||||
source = null;
|
||||
}
|
||||
}
|
||||
return suppressionRanges;
|
||||
@@ -176,15 +163,21 @@ export function suppressionsToCompilerError(
|
||||
): CompilerError {
|
||||
CompilerError.invariant(suppressionRanges.length !== 0, {
|
||||
reason: `Expected at least suppression comment source range`,
|
||||
loc: GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
const error = new CompilerError();
|
||||
for (const suppressionRange of suppressionRanges) {
|
||||
const disableComment =
|
||||
suppressionRange.kind === 'single-line'
|
||||
? suppressionRange.comment
|
||||
: suppressionRange.disableComment;
|
||||
if (disableComment.start == null || disableComment.end == null) {
|
||||
if (
|
||||
suppressionRange.disableComment.start == null ||
|
||||
suppressionRange.disableComment.end == null
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let reason, suggestion;
|
||||
@@ -209,19 +202,21 @@ export function suppressionsToCompilerError(
|
||||
error.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
reason: reason,
|
||||
description: `React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression \`${disableComment.value.trim()}\``,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
description: `React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression \`${suppressionRange.disableComment.value.trim()}\``,
|
||||
category: ErrorCategory.Suppression,
|
||||
suggestions: [
|
||||
{
|
||||
description: suggestion,
|
||||
range: [disableComment.start, disableComment.end],
|
||||
range: [
|
||||
suppressionRange.disableComment.start,
|
||||
suppressionRange.disableComment.end,
|
||||
],
|
||||
op: CompilerSuggestionOperation.Remove,
|
||||
},
|
||||
],
|
||||
}).withDetail({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: disableComment.loc ?? null,
|
||||
loc: suppressionRange.disableComment.loc ?? null,
|
||||
message: 'Found React rule suppression',
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import {NodePath} from '@babel/core';
|
||||
import * as t from '@babel/types';
|
||||
|
||||
import {CompilerError, EnvironmentConfig, ErrorSeverity, Logger} from '..';
|
||||
import {CompilerError, EnvironmentConfig, Logger} from '..';
|
||||
import {getOrInsertWith} from '../Utils/utils';
|
||||
import {Environment, GeneratedSource} from '../HIR';
|
||||
import {DEFAULT_EXPORT} from '../HIR/Environment';
|
||||
@@ -20,19 +20,15 @@ import {
|
||||
} from '../CompilerError';
|
||||
|
||||
function throwInvalidReact(
|
||||
options: Omit<CompilerDiagnosticOptions, 'severity'>,
|
||||
options: CompilerDiagnosticOptions,
|
||||
{logger, filename}: TraversalState,
|
||||
): never {
|
||||
const detail: CompilerDiagnosticOptions = {
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
...options,
|
||||
};
|
||||
logger?.logEvent(filename, {
|
||||
kind: 'CompileError',
|
||||
fnLoc: null,
|
||||
detail: new CompilerDiagnostic(detail),
|
||||
detail: new CompilerDiagnostic(options),
|
||||
});
|
||||
CompilerError.throwDiagnostic(detail);
|
||||
CompilerError.throwDiagnostic(options);
|
||||
}
|
||||
|
||||
function isAutodepsSigil(
|
||||
@@ -100,7 +96,7 @@ function assertValidEffectImportReference(
|
||||
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.' +
|
||||
'To resolve, either pass a dependency array or fix reported compiler bailout diagnostics' +
|
||||
(maybeErrorDiagnostic ? ` ${maybeErrorDiagnostic}` : ''),
|
||||
details: [
|
||||
{
|
||||
@@ -132,9 +128,7 @@ function assertValidFireImportReference(
|
||||
reason: '[Fire] Untransformed reference to compiler-required feature.',
|
||||
description:
|
||||
'Either remove this `fire` call or ensure it is successfully transformed by the compiler' +
|
||||
maybeErrorDiagnostic
|
||||
? ` ${maybeErrorDiagnostic}`
|
||||
: '',
|
||||
(maybeErrorDiagnostic != null ? ` ${maybeErrorDiagnostic}` : ''),
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
@@ -221,7 +215,14 @@ function validateImportSpecifier(
|
||||
const binding = local.scope.getBinding(local.node.name);
|
||||
CompilerError.invariant(binding != null, {
|
||||
reason: 'Expected binding to be found for import specifier',
|
||||
loc: local.node.loc ?? null,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: local.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
checkFn(binding.referencePaths, state);
|
||||
}
|
||||
@@ -241,7 +242,14 @@ function validateNamespacedImport(
|
||||
|
||||
CompilerError.invariant(binding != null, {
|
||||
reason: 'Expected binding to be found for import specifier',
|
||||
loc: local.node.loc ?? null,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: local.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
const filteredReferences = new Map<
|
||||
CheckInvalidReferenceFn,
|
||||
|
||||
@@ -46,7 +46,14 @@ export function raiseUnificationErrors(
|
||||
if (errs.length === 0) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Should not have array of zero errors',
|
||||
loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else if (errs.length === 1) {
|
||||
CompilerError.throwInvalidJS({
|
||||
|
||||
@@ -152,7 +152,13 @@ export function makeLinearId(id: number): LinearId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected LinearId id to be a non-negative integer',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return id as LinearId;
|
||||
@@ -167,7 +173,13 @@ export function makeTypeParameterId(id: number): TypeParameterId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected TypeParameterId to be a non-negative integer',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return id as TypeParameterId;
|
||||
@@ -191,7 +203,13 @@ export function makeVariableId(id: number): VariableId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected VariableId id to be a non-negative integer',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return id as VariableId;
|
||||
@@ -399,7 +417,14 @@ function convertFlowType(flowType: FlowType, loc: string): ResolvedType {
|
||||
} else {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unsupported property kind ${prop.kind}`,
|
||||
loc: GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -468,7 +493,14 @@ function convertFlowType(flowType: FlowType, loc: string): ResolvedType {
|
||||
} else {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unsupported property kind ${prop.kind}`,
|
||||
loc: GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -487,7 +519,14 @@ function convertFlowType(flowType: FlowType, loc: string): ResolvedType {
|
||||
} else {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unsupported property kind ${prop.kind}`,
|
||||
loc: GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -500,7 +539,14 @@ function convertFlowType(flowType: FlowType, loc: string): ResolvedType {
|
||||
}
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unsupported class instance type ${flowType.def.type.kind}`,
|
||||
loc: GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
case 'Fun':
|
||||
@@ -559,7 +605,14 @@ function convertFlowType(flowType: FlowType, loc: string): ResolvedType {
|
||||
} else {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unsupported component props type ${propsType.type.kind}`,
|
||||
loc: GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -712,7 +765,14 @@ export class FlowTypeEnv implements ITypeEnv {
|
||||
// TODO: use flow-js only for web environments (e.g. playground)
|
||||
CompilerError.invariant(env.config.flowTypeProvider != null, {
|
||||
reason: 'Expected flowDumpTypes to be defined in environment config',
|
||||
loc: GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
let stdout: any;
|
||||
if (source === lastFlowSource) {
|
||||
|
||||
@@ -38,7 +38,13 @@ export function assertConsistentIdentifiers(fn: HIRFunction): void {
|
||||
CompilerError.invariant(instr.lvalue.identifier.name === null, {
|
||||
reason: `Expected all lvalues to be temporaries`,
|
||||
description: `Found named lvalue \`${instr.lvalue.identifier.name}\``,
|
||||
loc: instr.lvalue.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instr.lvalue.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
CompilerError.invariant(!assignments.has(instr.lvalue.identifier.id), {
|
||||
@@ -46,7 +52,13 @@ export function assertConsistentIdentifiers(fn: HIRFunction): void {
|
||||
description: `Found duplicate assignment of '${printPlace(
|
||||
instr.lvalue,
|
||||
)}'`,
|
||||
loc: instr.lvalue.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instr.lvalue.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
assignments.add(instr.lvalue.identifier.id);
|
||||
@@ -77,7 +89,13 @@ function validate(
|
||||
CompilerError.invariant(identifier === previous, {
|
||||
reason: `Duplicate identifier object`,
|
||||
description: `Found duplicate identifier object for id ${identifier.id}`,
|
||||
loc: loc ?? GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: loc ?? GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,7 +18,13 @@ export function assertTerminalSuccessorsExist(fn: HIRFunction): void {
|
||||
description: `Block bb${successor} does not exist for terminal '${printTerminal(
|
||||
block.terminal,
|
||||
)}'`,
|
||||
loc: (block.terminal as any).loc ?? GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: (block.terminal as any).loc ?? GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return successor;
|
||||
@@ -33,14 +39,26 @@ export function assertTerminalPredsExist(fn: HIRFunction): void {
|
||||
CompilerError.invariant(predBlock != null, {
|
||||
reason: 'Expected predecessor block to exist',
|
||||
description: `Block ${block.id} references non-existent ${pred}`,
|
||||
loc: GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
CompilerError.invariant(
|
||||
[...eachTerminalSuccessor(predBlock.terminal)].includes(block.id),
|
||||
{
|
||||
reason: 'Terminal successor does not reference correct predecessor',
|
||||
description: `Block bb${block.id} has bb${predBlock.id} as a predecessor, but bb${predBlock.id}'s successors do not include bb${block.id}`,
|
||||
loc: GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -131,7 +131,13 @@ export function recursivelyTraverseItems<T, TContext>(
|
||||
CompilerError.invariant(disjoint || nested, {
|
||||
reason: 'Invalid nesting in program blocks or scopes',
|
||||
description: `Items overlap but are not nested: ${maybeParentRange.start}:${maybeParentRange.end}(${currRange.start}:${currRange.end})`,
|
||||
loc: GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
if (disjoint) {
|
||||
exit(maybeParent, context);
|
||||
|
||||
@@ -57,7 +57,13 @@ function validateMutableRange(
|
||||
{
|
||||
reason: `Invalid mutable range: [${range.start}:${range.end}]`,
|
||||
description: `${printPlace(place)} in ${description}`,
|
||||
loc: place.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: place.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
CompilerError,
|
||||
CompilerSuggestionOperation,
|
||||
ErrorCategory,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {assertExhaustive, hasNode} from '../Utils/utils';
|
||||
@@ -48,14 +47,10 @@ import {
|
||||
makePropertyLiteral,
|
||||
makeType,
|
||||
promoteTemporary,
|
||||
validateIdentifierName,
|
||||
} from './HIR';
|
||||
import HIRBuilder, {Bindings, createTemporaryPlace} from './HIRBuilder';
|
||||
import {BuiltInArrayId} from './ObjectShape';
|
||||
import {
|
||||
filterSuppressionsThatAffectNode,
|
||||
SingleLineSuppressionRange,
|
||||
suppressionsToCompilerError,
|
||||
} from '../Entrypoint';
|
||||
|
||||
/*
|
||||
* *******************************************************************************************
|
||||
@@ -113,11 +108,10 @@ export function lower(
|
||||
if (binding.kind !== 'Identifier') {
|
||||
builder.errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: ErrorCategory.Invariant,
|
||||
reason: 'Could not find binding',
|
||||
description: `[BuildHIR] Could not find binding for param \`${param.node.name}\`.`,
|
||||
}).withDetail({
|
||||
description: `[BuildHIR] Could not find binding for param \`${param.node.name}\``,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: param.node.loc ?? null,
|
||||
message: 'Could not find binding',
|
||||
@@ -178,11 +172,10 @@ export function lower(
|
||||
} else {
|
||||
builder.errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
reason: `Handle ${param.node.type} parameters`,
|
||||
description: `[BuildHIR] Add support for ${param.node.type} parameters.`,
|
||||
}).withDetail({
|
||||
description: `[BuildHIR] Add support for ${param.node.type} parameters`,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: param.node.loc ?? null,
|
||||
message: 'Unsupported parameter type',
|
||||
@@ -210,11 +203,10 @@ export function lower(
|
||||
} else {
|
||||
builder.errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
reason: `Unexpected function body kind`,
|
||||
description: `Expected function body to be an expression or a block statement, got \`${body.type}\`.`,
|
||||
}).withDetail({
|
||||
description: `Expected function body to be an expression or a block statement, got \`${body.type}\``,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: body.node.loc ?? null,
|
||||
message: 'Expected a block statement or expression',
|
||||
@@ -222,7 +214,17 @@ export function lower(
|
||||
);
|
||||
}
|
||||
|
||||
if (builder.errors.hasErrors()) {
|
||||
let validatedId: HIRFunction['id'] = null;
|
||||
if (id != null) {
|
||||
const idResult = validateIdentifierName(id);
|
||||
if (idResult.isErr()) {
|
||||
builder.errors.merge(idResult.unwrapErr());
|
||||
} else {
|
||||
validatedId = idResult.unwrap().value;
|
||||
}
|
||||
}
|
||||
|
||||
if (builder.errors.hasAnyErrors()) {
|
||||
return Err(builder.errors);
|
||||
}
|
||||
|
||||
@@ -242,44 +244,9 @@ 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,
|
||||
id: validatedId,
|
||||
nameHint: null,
|
||||
params,
|
||||
fnType: bindings == null ? env.fnType : 'Other',
|
||||
returnTypeAnnotation: null, // TODO: extract the actual return type node if present
|
||||
@@ -317,7 +284,6 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason:
|
||||
'(BuildHIR::lowerStatement) Support ThrowStatement inside of try/catch',
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -485,7 +451,13 @@ function lowerStatement(
|
||||
reason: 'Expected to find binding for hoisted identifier',
|
||||
description: `Could not find a binding for ${id.node.name}`,
|
||||
suggestions: null,
|
||||
loc: id.node.loc ?? GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: id.node.loc ?? GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
if (builder.environment.isHoistedIdentifier(binding.identifier)) {
|
||||
// Already hoisted
|
||||
@@ -505,7 +477,6 @@ function lowerStatement(
|
||||
kind = InstructionKind.HoistedFunction;
|
||||
} 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}`,
|
||||
@@ -515,7 +486,6 @@ function lowerStatement(
|
||||
continue;
|
||||
} 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}`,
|
||||
@@ -529,7 +499,14 @@ function lowerStatement(
|
||||
CompilerError.invariant(identifier.kind === 'Identifier', {
|
||||
reason:
|
||||
'Expected hoisted binding to be a local identifier, not a global',
|
||||
loc: id.node.loc ?? GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: id.node.loc ?? GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
const place: Place = {
|
||||
effect: Effect.Unknown,
|
||||
@@ -596,7 +573,6 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason:
|
||||
'(BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement',
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -669,7 +645,6 @@ function lowerStatement(
|
||||
if (test.node == null) {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerStatement) Handle empty test in ForStatement`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -821,7 +796,6 @@ function lowerStatement(
|
||||
if (hasDefault) {
|
||||
builder.errors.push({
|
||||
reason: `Expected at most one \`default\` branch in a switch statement, this code should have failed to parse`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: case_.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -894,7 +868,6 @@ function lowerStatement(
|
||||
if (nodeKind === 'var') {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerStatement) Handle ${nodeKind} kinds in VariableDeclaration`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -923,7 +896,6 @@ function lowerStatement(
|
||||
if (binding.kind !== 'Identifier') {
|
||||
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,
|
||||
@@ -941,7 +913,6 @@ function lowerStatement(
|
||||
const declRangeStart = declaration.parentPath.node.start!;
|
||||
builder.errors.push({
|
||||
reason: `Expect \`const\` declaration not to be reassigned`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: id.node.loc ?? null,
|
||||
suggestions: [
|
||||
@@ -989,7 +960,6 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
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,
|
||||
@@ -1069,7 +1039,13 @@ function lowerStatement(
|
||||
CompilerError.invariant(stmt.get('id').type === 'Identifier', {
|
||||
reason: 'function declarations must have a name',
|
||||
description: null,
|
||||
loc: stmt.node.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: stmt.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
const id = stmt.get('id') as NodePath<t.Identifier>;
|
||||
@@ -1098,7 +1074,6 @@ function lowerStatement(
|
||||
if (stmt.node.await) {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerStatement) Handle for-await loops`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -1170,7 +1145,13 @@ function lowerStatement(
|
||||
CompilerError.invariant(declarations.length === 1, {
|
||||
reason: `Expected only one declaration in the init of a ForOfStatement, got ${declarations.length}`,
|
||||
description: null,
|
||||
loc: left.node.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: left.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
const id = declarations[0].get('id');
|
||||
@@ -1185,8 +1166,15 @@ function lowerStatement(
|
||||
test = lowerValueToTemporary(builder, assign);
|
||||
} else {
|
||||
CompilerError.invariant(left.isLVal(), {
|
||||
loc: leftLoc,
|
||||
reason: 'Expected ForOf init to be a variable declaration or lval',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: leftLoc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
const assign = lowerAssignment(
|
||||
builder,
|
||||
@@ -1263,7 +1251,13 @@ function lowerStatement(
|
||||
CompilerError.invariant(declarations.length === 1, {
|
||||
reason: `Expected only one declaration in the init of a ForInStatement, got ${declarations.length}`,
|
||||
description: null,
|
||||
loc: left.node.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: left.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
const id = declarations[0].get('id');
|
||||
@@ -1278,8 +1272,15 @@ function lowerStatement(
|
||||
test = lowerValueToTemporary(builder, assign);
|
||||
} else {
|
||||
CompilerError.invariant(left.isLVal(), {
|
||||
loc: leftLoc,
|
||||
reason: 'Expected ForIn init to be a variable declaration or lval',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: leftLoc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
const assign = lowerAssignment(
|
||||
builder,
|
||||
@@ -1331,7 +1332,6 @@ function lowerStatement(
|
||||
if (!hasNode(handlerPath)) {
|
||||
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,
|
||||
@@ -1341,7 +1341,6 @@ function lowerStatement(
|
||||
if (hasNode(stmt.get('finalizer'))) {
|
||||
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,
|
||||
@@ -1435,7 +1434,6 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason: `JavaScript 'with' syntax is not supported`,
|
||||
description: `'with' syntax is considered deprecated and removed from JavaScript standards, consider alternatives`,
|
||||
severity: ErrorSeverity.UnsupportedJS,
|
||||
category: ErrorCategory.UnsupportedSyntax,
|
||||
loc: stmtPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -1456,7 +1454,6 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason: 'Inline `class` declarations are not supported',
|
||||
description: `Move class declarations outside of components/hooks`,
|
||||
severity: ErrorSeverity.UnsupportedJS,
|
||||
category: ErrorCategory.UnsupportedSyntax,
|
||||
loc: stmtPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -1486,7 +1483,6 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason:
|
||||
'JavaScript `import` and `export` statements may only appear at the top level of a module',
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: stmtPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -1502,7 +1498,6 @@ function lowerStatement(
|
||||
builder.errors.push({
|
||||
reason:
|
||||
'TypeScript `namespace` statements may only appear at the top level of a module',
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: stmtPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -1581,7 +1576,6 @@ function lowerObjectPropertyKey(
|
||||
*/
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: key.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -1607,7 +1601,6 @@ function lowerObjectPropertyKey(
|
||||
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: key.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -1665,7 +1658,6 @@ function lowerExpression(
|
||||
if (!valuePath.isExpression()) {
|
||||
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,
|
||||
@@ -1692,7 +1684,6 @@ function lowerExpression(
|
||||
if (propertyPath.node.kind !== 'method') {
|
||||
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,
|
||||
@@ -1714,7 +1705,6 @@ function lowerExpression(
|
||||
} else {
|
||||
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,
|
||||
@@ -1748,7 +1738,6 @@ function lowerExpression(
|
||||
} else {
|
||||
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,
|
||||
@@ -1769,7 +1758,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason: `Expected an expression as the \`new\` expression receiver (v8 intrinsics are not supported)`,
|
||||
description: `Got a \`${calleePath.node.type}\``,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: calleePath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -1796,7 +1784,6 @@ function lowerExpression(
|
||||
if (!calleePath.isExpression()) {
|
||||
builder.errors.push({
|
||||
reason: `Expected Expression, got ${calleePath.type} in CallExpression (v8 intrinsics not supported). This error is likely caused by a bug in React Compiler. Please file an issue`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: calleePath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -1807,27 +1794,21 @@ 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1837,7 +1818,6 @@ function lowerExpression(
|
||||
if (!leftPath.isExpression()) {
|
||||
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,
|
||||
@@ -1850,7 +1830,6 @@ function lowerExpression(
|
||||
if (operator === '|>') {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Pipe operator not supported`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: leftPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -1880,7 +1859,6 @@ function lowerExpression(
|
||||
if (last === null) {
|
||||
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,
|
||||
@@ -2093,7 +2071,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
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,
|
||||
@@ -2122,7 +2099,6 @@ function lowerExpression(
|
||||
if (binaryOperator == null) {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${operator} operators in AssignmentExpression`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: expr.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -2222,7 +2198,6 @@ function lowerExpression(
|
||||
default: {
|
||||
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,
|
||||
@@ -2262,7 +2237,6 @@ function lowerExpression(
|
||||
if (!attribute.isJSXAttribute()) {
|
||||
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,
|
||||
@@ -2276,7 +2250,6 @@ function lowerExpression(
|
||||
if (propName.indexOf(':') !== -1) {
|
||||
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,
|
||||
@@ -2286,7 +2259,13 @@ function lowerExpression(
|
||||
CompilerError.invariant(namePath.isJSXNamespacedName(), {
|
||||
reason: 'Refinement',
|
||||
description: null,
|
||||
loc: namePath.node.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: namePath.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
const namespace = namePath.node.namespace.name;
|
||||
@@ -2307,7 +2286,6 @@ function lowerExpression(
|
||||
if (!valueExpr.isJSXExpressionContainer()) {
|
||||
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,
|
||||
@@ -2318,7 +2296,6 @@ function lowerExpression(
|
||||
if (!expression.isExpression()) {
|
||||
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,
|
||||
@@ -2342,8 +2319,14 @@ function lowerExpression(
|
||||
// This is already checked in builder.resolveIdentifier
|
||||
CompilerError.invariant(tagIdentifier.kind !== 'Identifier', {
|
||||
reason: `<${tagName}> tags should be module-level imports`,
|
||||
loc: openingIdentifier.node.loc ?? GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: openingIdentifier.node.loc ?? GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
@@ -2376,8 +2359,7 @@ function lowerExpression(
|
||||
for (const [name, locations] of Object.entries(fbtLocations)) {
|
||||
if (locations.length > 1) {
|
||||
CompilerError.throwDiagnostic({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.FBT,
|
||||
category: ErrorCategory.Todo,
|
||||
reason: 'Support duplicate fbt tags',
|
||||
description: `Support \`<${tagName}>\` tags with multiple \`<${tagName}:${name}>\` values`,
|
||||
details: locations.map(loc => {
|
||||
@@ -2438,7 +2420,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
reason:
|
||||
'(BuildHIR::lowerExpression) Handle tagged template with interpolations',
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -2449,7 +2430,13 @@ function lowerExpression(
|
||||
reason:
|
||||
"there should be only one quasi as we don't support interpolations yet",
|
||||
description: null,
|
||||
loc: expr.node.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: expr.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
const value = expr.get('quasi').get('quasis').at(0)!.node.value;
|
||||
@@ -2457,7 +2444,6 @@ function lowerExpression(
|
||||
builder.errors.push({
|
||||
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,
|
||||
@@ -2480,7 +2466,6 @@ function lowerExpression(
|
||||
if (subexprs.length !== quasis.length - 1) {
|
||||
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,
|
||||
@@ -2491,7 +2476,6 @@ function lowerExpression(
|
||||
if (subexprs.some(e => !e.isExpression())) {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Handle TSType in TemplateLiteral.`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -2534,7 +2518,6 @@ function lowerExpression(
|
||||
} else {
|
||||
builder.errors.push({
|
||||
reason: `Only object properties can be deleted`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: expr.node.loc ?? null,
|
||||
suggestions: [
|
||||
@@ -2550,7 +2533,6 @@ function lowerExpression(
|
||||
} else if (expr.node.operator === 'throw') {
|
||||
builder.errors.push({
|
||||
reason: `Throw expressions are not supported`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: expr.node.loc ?? null,
|
||||
suggestions: [
|
||||
@@ -2672,7 +2654,6 @@ function lowerExpression(
|
||||
if (!argument.isIdentifier()) {
|
||||
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,
|
||||
@@ -2681,7 +2662,6 @@ function lowerExpression(
|
||||
} else if (builder.isContextIdentifier(argument)) {
|
||||
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,
|
||||
@@ -2699,10 +2679,9 @@ function lowerExpression(
|
||||
* lowerIdentifierForAssignment should have already reported an error if it returned null,
|
||||
* we check here just in case
|
||||
*/
|
||||
if (!builder.errors.hasErrors()) {
|
||||
if (!builder.errors.hasAnyErrors()) {
|
||||
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,
|
||||
@@ -2712,7 +2691,6 @@ function lowerExpression(
|
||||
} else if (lvalue.kind === 'Global') {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Support UpdateExpression where argument is a global`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprLoc,
|
||||
suggestions: null,
|
||||
@@ -2768,7 +2746,6 @@ function lowerExpression(
|
||||
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle MetaProperty expressions other than import.meta`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -2778,7 +2755,6 @@ function lowerExpression(
|
||||
default: {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle ${exprPath.type} expressions`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -2858,7 +2834,13 @@ function lowerOptionalMemberExpression(
|
||||
CompilerError.invariant(object !== null, {
|
||||
reason: 'Satisfy type checker',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
@@ -3003,7 +2985,6 @@ 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),
|
||||
@@ -3013,7 +2994,6 @@ function lowerOptionalCallExpression(
|
||||
callee: {...callee.callee},
|
||||
args,
|
||||
loc,
|
||||
suppressions,
|
||||
},
|
||||
effects: null,
|
||||
loc,
|
||||
@@ -3028,7 +3008,6 @@ function lowerOptionalCallExpression(
|
||||
property: {...callee.property},
|
||||
args,
|
||||
loc,
|
||||
suppressions,
|
||||
},
|
||||
effects: null,
|
||||
loc,
|
||||
@@ -3079,7 +3058,6 @@ function lowerReorderableExpression(
|
||||
if (!isReorderableExpression(builder, expr, true)) {
|
||||
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,
|
||||
@@ -3276,7 +3254,6 @@ function lowerArguments(
|
||||
} else {
|
||||
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,
|
||||
@@ -3312,7 +3289,6 @@ function lowerMemberExpression(
|
||||
} else {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerMemberExpression) Handle ${propertyNode.type} property`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: propertyNode.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -3334,7 +3310,6 @@ function lowerMemberExpression(
|
||||
if (!propertyNode.isExpression()) {
|
||||
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,
|
||||
@@ -3394,7 +3369,6 @@ function lowerJsxElementName(
|
||||
builder.errors.push({
|
||||
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,
|
||||
@@ -3409,7 +3383,6 @@ function lowerJsxElementName(
|
||||
} else {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerJsxElementName) Handle ${exprPath.type} tags`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -3435,7 +3408,13 @@ function lowerJsxMemberExpression(
|
||||
CompilerError.invariant(object.isJSXIdentifier(), {
|
||||
reason: `TypeScript refinement fail: expected 'JsxIdentifier', got \`${object.node.type}\``,
|
||||
description: null,
|
||||
loc: object.node.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: object.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
@@ -3477,7 +3456,13 @@ function lowerJsxElement(
|
||||
CompilerError.invariant(expression.isExpression(), {
|
||||
reason: `(BuildHIR::lowerJsxElement) Expected Expression but found ${expression.type}!`,
|
||||
description: null,
|
||||
loc: expression.node.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: expression.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return lowerExpressionToTemporary(builder, expression);
|
||||
@@ -3508,7 +3493,6 @@ function lowerJsxElement(
|
||||
} else {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerJsxElement) Unhandled JsxElement, got: ${exprPath.type}`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -3591,17 +3575,14 @@ function lowerFunctionToValue(
|
||||
): InstructionValue {
|
||||
const exprNode = expr.node;
|
||||
const exprLoc = exprNode.loc ?? GeneratedSource;
|
||||
let name: string | null = null;
|
||||
if (expr.isFunctionExpression()) {
|
||||
name = expr.get('id')?.node?.name ?? null;
|
||||
}
|
||||
const loweredFunc = lowerFunction(builder, expr);
|
||||
if (!loweredFunc) {
|
||||
return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
|
||||
}
|
||||
return {
|
||||
kind: 'FunctionExpression',
|
||||
name,
|
||||
name: loweredFunc.func.id,
|
||||
nameHint: null,
|
||||
type: expr.node.type,
|
||||
loc: exprLoc,
|
||||
loweredFunc,
|
||||
@@ -3696,7 +3677,6 @@ function lowerIdentifier(
|
||||
reason: `The 'eval' function is not supported`,
|
||||
description:
|
||||
'Eval is an anti-pattern in JavaScript, and the code executed cannot be evaluated by React Compiler',
|
||||
severity: ErrorSeverity.UnsupportedJS,
|
||||
category: ErrorCategory.UnsupportedSyntax,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -3753,7 +3733,6 @@ function lowerIdentifierForAssignment(
|
||||
// Else its an internal error bc we couldn't find the binding
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Could not find binding for declaration.`,
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: ErrorCategory.Invariant,
|
||||
loc: path.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -3766,7 +3745,6 @@ function lowerIdentifierForAssignment(
|
||||
) {
|
||||
builder.errors.push({
|
||||
reason: `Cannot reassign a \`const\` variable`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: path.node.loc ?? null,
|
||||
description:
|
||||
@@ -3824,7 +3802,6 @@ function lowerAssignment(
|
||||
if (kind === InstructionKind.Const && !isHoistedIdentifier) {
|
||||
builder.errors.push({
|
||||
reason: `Expected \`const\` declaration not to be reassigned`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: lvalue.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -3839,7 +3816,6 @@ function lowerAssignment(
|
||||
) {
|
||||
builder.errors.push({
|
||||
reason: `Unexpected context variable kind`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: ErrorCategory.Syntax,
|
||||
loc: lvalue.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -3884,7 +3860,13 @@ function lowerAssignment(
|
||||
CompilerError.invariant(kind === InstructionKind.Reassign, {
|
||||
reason: 'MemberExpression may only appear in an assignment expression',
|
||||
description: null,
|
||||
loc: lvaluePath.node.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: lvaluePath.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
const lvalue = lvaluePath as NodePath<t.MemberExpression>;
|
||||
@@ -3911,7 +3893,6 @@ function lowerAssignment(
|
||||
} else {
|
||||
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,
|
||||
@@ -3924,7 +3905,6 @@ function lowerAssignment(
|
||||
builder.errors.push({
|
||||
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,
|
||||
@@ -3990,7 +3970,6 @@ function lowerAssignment(
|
||||
continue;
|
||||
} else if (identifier.kind === 'Global') {
|
||||
builder.errors.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
reason:
|
||||
'Expected reassignment of globals to enable forceTemporaries',
|
||||
@@ -4030,7 +4009,6 @@ function lowerAssignment(
|
||||
continue;
|
||||
} else if (identifier.kind === 'Global') {
|
||||
builder.errors.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
reason:
|
||||
'Expected reassignment of globals to enable forceTemporaries',
|
||||
@@ -4104,7 +4082,6 @@ function lowerAssignment(
|
||||
if (!argument.isIdentifier()) {
|
||||
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,
|
||||
@@ -4136,7 +4113,6 @@ function lowerAssignment(
|
||||
continue;
|
||||
} else if (identifier.kind === 'Global') {
|
||||
builder.errors.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
reason:
|
||||
'Expected reassignment of globals to enable forceTemporaries',
|
||||
@@ -4154,7 +4130,6 @@ function lowerAssignment(
|
||||
if (!property.isObjectProperty()) {
|
||||
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,
|
||||
@@ -4164,7 +4139,6 @@ function lowerAssignment(
|
||||
if (property.node.computed) {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: property.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -4179,7 +4153,6 @@ function lowerAssignment(
|
||||
if (!element.isLVal()) {
|
||||
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,
|
||||
@@ -4202,7 +4175,6 @@ function lowerAssignment(
|
||||
continue;
|
||||
} else if (identifier.kind === 'Global') {
|
||||
builder.errors.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
reason:
|
||||
'Expected reassignment of globals to enable forceTemporaries',
|
||||
@@ -4352,7 +4324,6 @@ function lowerAssignment(
|
||||
default: {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerAssignment) Handle ${lvaluePath.type} assignments`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: lvaluePath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -4527,23 +4498,3 @@ 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;
|
||||
}
|
||||
|
||||
@@ -234,7 +234,14 @@ function pushEndScopeTerminal(
|
||||
const fallthroughId = context.fallthroughs.get(scope.id);
|
||||
CompilerError.invariant(fallthroughId != null, {
|
||||
reason: 'Expected scope to exist',
|
||||
loc: GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
context.rewrites.push({
|
||||
kind: 'EndScope',
|
||||
|
||||
@@ -269,7 +269,14 @@ class PropertyPathRegistry {
|
||||
CompilerError.invariant(reactive === rootNode.fullPath.reactive, {
|
||||
reason:
|
||||
'[HoistablePropertyLoads] Found inconsistencies in `reactive` flag when deduping identifier reads within the same scope',
|
||||
loc: identifier.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: identifier.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
return rootNode;
|
||||
@@ -498,7 +505,14 @@ function propagateNonNull(
|
||||
if (node == null) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Bad node ${nodeId}, kind: ${direction}`,
|
||||
loc: GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
const neighbors = Array.from(
|
||||
@@ -570,7 +584,14 @@ function propagateNonNull(
|
||||
CompilerError.invariant(i++ < 100, {
|
||||
reason:
|
||||
'[CollectHoistablePropertyLoads] fixed point iteration did not terminate after 100 loops',
|
||||
loc: GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
changed = false;
|
||||
@@ -602,7 +623,13 @@ export function assertNonNull<T extends NonNullable<U>, U>(
|
||||
CompilerError.invariant(value != null, {
|
||||
reason: 'Unexpected null',
|
||||
description: source != null ? `(from ${source})` : null,
|
||||
loc: GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -186,7 +186,13 @@ function matchOptionalTestBlock(
|
||||
reason:
|
||||
'[OptionalChainDeps] Inconsistent optional chaining property load',
|
||||
description: `Test=${printIdentifier(terminal.test.identifier)} PropertyLoad base=${printIdentifier(propertyLoad.value.object.identifier)}`,
|
||||
loc: propertyLoad.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: propertyLoad.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -194,7 +200,14 @@ function matchOptionalTestBlock(
|
||||
storeLocal.value.identifier.id === propertyLoad.lvalue.identifier.id,
|
||||
{
|
||||
reason: '[OptionalChainDeps] Unexpected storeLocal',
|
||||
loc: propertyLoad.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: propertyLoad.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
if (
|
||||
@@ -211,7 +224,14 @@ function matchOptionalTestBlock(
|
||||
alternate.instructions[1].value.kind === 'StoreLocal',
|
||||
{
|
||||
reason: 'Unexpected alternate structure',
|
||||
loc: terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -247,7 +267,14 @@ function traverseOptionalBlock(
|
||||
if (maybeTest.terminal.kind === 'branch') {
|
||||
CompilerError.invariant(optional.terminal.optional, {
|
||||
reason: '[OptionalChainDeps] Expect base case to be always optional',
|
||||
loc: optional.terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: optional.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
/**
|
||||
* Optional base expressions are currently within value blocks which cannot
|
||||
@@ -285,7 +312,14 @@ function traverseOptionalBlock(
|
||||
maybeTest.instructions.at(-1)!.lvalue.identifier.id,
|
||||
{
|
||||
reason: '[OptionalChainDeps] Unexpected test expression',
|
||||
loc: maybeTest.terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: maybeTest.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
baseObject = {
|
||||
@@ -374,7 +408,14 @@ function traverseOptionalBlock(
|
||||
reason:
|
||||
'[OptionalChainDeps] Unexpected instructions an inner optional block. ' +
|
||||
'This indicates that the compiler may be incorrectly concatenating two unrelated optional chains',
|
||||
loc: optional.terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: optional.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
const matchConsequentResult = matchOptionalTestBlock(test, context.blocks);
|
||||
@@ -387,7 +428,13 @@ function traverseOptionalBlock(
|
||||
{
|
||||
reason: '[OptionalChainDeps] Unexpected optional goto-fallthrough',
|
||||
description: `${matchConsequentResult.consequentGoto} != ${optional.terminal.fallthrough}`,
|
||||
loc: optional.terminal.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: optional.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
const load = {
|
||||
|
||||
@@ -24,7 +24,14 @@ export function computeUnconditionalBlocks(fn: HIRFunction): Set<BlockId> {
|
||||
CompilerError.invariant(!unconditionalBlocks.has(current), {
|
||||
reason:
|
||||
'Internal error: non-terminating loop in ComputeUnconditionalBlocks',
|
||||
loc: null,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
unconditionalBlocks.add(current);
|
||||
|
||||
@@ -86,6 +86,24 @@ export function defaultModuleTypeProvider(
|
||||
},
|
||||
};
|
||||
}
|
||||
case '@tanstack/react-virtual': {
|
||||
return {
|
||||
kind: 'object',
|
||||
properties: {
|
||||
/*
|
||||
* Many of the properties of `useVirtualizer()`'s return value are incompatible, so we mark the entire hook
|
||||
* as incompatible
|
||||
*/
|
||||
useVirtualizer: {
|
||||
kind: 'hook',
|
||||
positionalParams: [],
|
||||
restParam: Effect.Read,
|
||||
returnType: {kind: 'type', name: 'Any'},
|
||||
knownIncompatible: `TanStack Virtual's \`useVirtualizer()\` API returns functions that cannot be memoized safely`,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -54,7 +54,14 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
prevAccessType == null || prevAccessType === accessType,
|
||||
{
|
||||
reason: 'Conflicting access types',
|
||||
loc: GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
let nextNode = currNode.properties.get(path[i].property);
|
||||
@@ -90,7 +97,13 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
CompilerError.invariant(reactive === rootNode.reactive, {
|
||||
reason: '[DeriveMinimalDependenciesHIR] Conflicting reactive root flag',
|
||||
description: `Identifier ${printIdentifier(identifier)}`,
|
||||
loc: GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
return rootNode;
|
||||
|
||||
@@ -89,7 +89,13 @@ export class Dominator<T> {
|
||||
CompilerError.invariant(dominator !== undefined, {
|
||||
reason: 'Unknown node',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return dominator === id ? null : dominator;
|
||||
@@ -130,7 +136,13 @@ export class PostDominator<T> {
|
||||
CompilerError.invariant(dominator !== undefined, {
|
||||
reason: 'Unknown node',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return dominator === id ? null : dominator;
|
||||
@@ -175,7 +187,13 @@ function computeImmediateDominators<T>(graph: Graph<T>): Map<T, T> {
|
||||
CompilerError.invariant(newIdom !== null, {
|
||||
reason: `At least one predecessor must have been visited for block ${id}`,
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
|
||||
@@ -9,11 +9,7 @@ import * as t from '@babel/types';
|
||||
import {ZodError, z} from 'zod';
|
||||
import {fromZodError} from 'zod-validation-error';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
Logger,
|
||||
ProgramContext,
|
||||
SingleLineSuppressionRange,
|
||||
} from '../Entrypoint';
|
||||
import {Logger, ProgramContext} from '../Entrypoint';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {
|
||||
DEFAULT_GLOBALS,
|
||||
@@ -265,6 +261,8 @@ export const EnvironmentConfigSchema = z.object({
|
||||
|
||||
enableFire: z.boolean().default(false),
|
||||
|
||||
enableNameAnonymousFunctions: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Enables inference and auto-insertion of effect dependencies. Takes in an array of
|
||||
* configurable module and import pairs to allow for user-land experimentation. For example,
|
||||
@@ -623,6 +621,13 @@ export const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(true),
|
||||
|
||||
/**
|
||||
* Treat identifiers as SetState type if both
|
||||
* - they are named with a "set-" prefix
|
||||
* - they are called somewhere
|
||||
*/
|
||||
enableTreatSetIdentifiersAsStateSetters: z.boolean().default(false),
|
||||
|
||||
/*
|
||||
* If specified a value, the compiler lowers any calls to `useContext` to use
|
||||
* this value as the callee.
|
||||
@@ -662,6 +667,13 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* while its parent function remains uncompiled.
|
||||
*/
|
||||
validateNoDynamicallyCreatedComponentsOrHooks: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* When enabled, allows setState calls in effects when the value being set is
|
||||
* derived from a ref. This is useful for patterns where initial layout measurements
|
||||
* from refs need to be stored in state during mount.
|
||||
*/
|
||||
enableAllowSetStateFromRefsInEffects: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
|
||||
@@ -706,7 +718,6 @@ export class Environment {
|
||||
hasFireRewrite: boolean;
|
||||
hasInferredEffect: boolean;
|
||||
inferredEffectLocations: Set<SourceLocation> = new Set();
|
||||
suppressions: Array<SingleLineSuppressionRange>;
|
||||
|
||||
#contextIdentifiers: Set<t.Identifier>;
|
||||
#hoistedIdentifiers: Set<t.Identifier>;
|
||||
@@ -725,7 +736,6 @@ export class Environment {
|
||||
filename: string | null,
|
||||
code: string | null,
|
||||
programContext: ProgramContext,
|
||||
suppressions: Array<SingleLineSuppressionRange>,
|
||||
) {
|
||||
this.#scope = scope;
|
||||
this.fnType = fnType;
|
||||
@@ -739,7 +749,6 @@ export class Environment {
|
||||
this.#globals = new Map(DEFAULT_GLOBALS);
|
||||
this.hasFireRewrite = false;
|
||||
this.hasInferredEffect = false;
|
||||
this.suppressions = suppressions;
|
||||
|
||||
if (
|
||||
config.disableMemoizationForDebugging &&
|
||||
@@ -757,7 +766,13 @@ export class Environment {
|
||||
CompilerError.invariant(!this.#globals.has(hookName), {
|
||||
reason: `[Globals] Found existing definition in global registry for custom hook ${hookName}`,
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
this.#globals.set(
|
||||
@@ -790,7 +805,14 @@ export class Environment {
|
||||
CompilerError.invariant(code != null, {
|
||||
reason:
|
||||
'Expected Environment to be initialized with source code when a Flow type provider is specified',
|
||||
loc: null,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
this.#flowTypeEnvironment.init(this, code);
|
||||
} else {
|
||||
@@ -801,7 +823,14 @@ export class Environment {
|
||||
get typeContext(): FlowTypeEnv {
|
||||
CompilerError.invariant(this.#flowTypeEnvironment != null, {
|
||||
reason: 'Flow type environment not initialized',
|
||||
loc: null,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
return this.#flowTypeEnvironment;
|
||||
}
|
||||
@@ -1051,7 +1080,13 @@ export class Environment {
|
||||
CompilerError.invariant(shape !== undefined, {
|
||||
reason: `[HIR] Forget internal error: cannot resolve shape ${shapeId}`,
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return shape.properties.get('*') ?? null;
|
||||
@@ -1076,7 +1111,13 @@ export class Environment {
|
||||
CompilerError.invariant(shape !== undefined, {
|
||||
reason: `[HIR] Forget internal error: cannot resolve shape ${shapeId}`,
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
if (typeof property === 'string') {
|
||||
@@ -1101,7 +1142,13 @@ export class Environment {
|
||||
CompilerError.invariant(shape !== undefined, {
|
||||
reason: `[HIR] Forget internal error: cannot resolve shape ${shapeId}`,
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return shape.functionType;
|
||||
|
||||
@@ -184,7 +184,13 @@ function handleAssignment(
|
||||
CompilerError.invariant(valuePath.isLVal(), {
|
||||
reason: `[FindContextIdentifiers] Expected object property value to be an LVal, got: ${valuePath.type}`,
|
||||
description: null,
|
||||
loc: valuePath.node.loc ?? GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: valuePath.node.loc ?? GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
handleAssignment(currentFn, identifiers, valuePath);
|
||||
@@ -192,7 +198,13 @@ function handleAssignment(
|
||||
CompilerError.invariant(property.isRestElement(), {
|
||||
reason: `[FindContextIdentifiers] Invalid assumptions for babel types.`,
|
||||
description: null,
|
||||
loc: property.node.loc ?? GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: property.node.loc ?? GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
handleAssignment(currentFn, identifiers, property);
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
|
||||
import {BindingKind} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {Environment, ReactFunctionType} from './Environment';
|
||||
import type {HookKind} from './ObjectShape';
|
||||
@@ -15,7 +19,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';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
|
||||
/*
|
||||
* *******************************************************************************************
|
||||
@@ -54,7 +58,8 @@ export type SourceLocation = t.SourceLocation | typeof GeneratedSource;
|
||||
*/
|
||||
export type ReactiveFunction = {
|
||||
loc: SourceLocation;
|
||||
id: string | null;
|
||||
id: ValidIdentifierName | null;
|
||||
nameHint: string | null;
|
||||
params: Array<Place | SpreadPattern>;
|
||||
generator: boolean;
|
||||
async: boolean;
|
||||
@@ -276,7 +281,8 @@ export type ReactiveTryTerminal = {
|
||||
// A function lowered to HIR form, ie where its body is lowered to an HIR control-flow graph
|
||||
export type HIRFunction = {
|
||||
loc: SourceLocation;
|
||||
id: string | null;
|
||||
id: ValidIdentifierName | null;
|
||||
nameHint: string | null;
|
||||
fnType: ReactFunctionType;
|
||||
env: Environment;
|
||||
params: Array<Place | SpreadPattern>;
|
||||
@@ -844,7 +850,6 @@ export type MethodCall = {
|
||||
property: Place;
|
||||
args: Array<Place | SpreadPattern>;
|
||||
loc: SourceLocation;
|
||||
suppressions?: Array<SingleLineSuppressionRange>;
|
||||
};
|
||||
|
||||
export type CallExpression = {
|
||||
@@ -853,7 +858,6 @@ export type CallExpression = {
|
||||
args: Array<Place | SpreadPattern>;
|
||||
loc: SourceLocation;
|
||||
typeArguments?: Array<t.FlowType>;
|
||||
suppressions?: Array<SingleLineSuppressionRange>;
|
||||
};
|
||||
|
||||
export type NewExpression = {
|
||||
@@ -1126,7 +1130,8 @@ export type JsxAttribute =
|
||||
|
||||
export type FunctionExpression = {
|
||||
kind: 'FunctionExpression';
|
||||
name: string | null;
|
||||
name: ValidIdentifierName | null;
|
||||
nameHint: string | null;
|
||||
loweredFunc: LoweredFunction;
|
||||
type:
|
||||
| 'ArrowFunctionExpression'
|
||||
@@ -1301,31 +1306,52 @@ export function forkTemporaryIdentifier(
|
||||
};
|
||||
}
|
||||
|
||||
export function validateIdentifierName(
|
||||
name: string,
|
||||
): Result<ValidatedIdentifier, CompilerError> {
|
||||
if (isReservedWord(name)) {
|
||||
const error = new CompilerError();
|
||||
error.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Syntax,
|
||||
reason: 'Expected a non-reserved identifier name',
|
||||
description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: 'reserved word',
|
||||
}),
|
||||
);
|
||||
return Err(error);
|
||||
} else if (!t.isValidIdentifier(name)) {
|
||||
const error = new CompilerError();
|
||||
error.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Syntax,
|
||||
reason: `Expected a valid identifier name`,
|
||||
description: `\`${name}\` is not a valid JavaScript identifier`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: 'reserved word',
|
||||
}),
|
||||
);
|
||||
}
|
||||
return Ok({
|
||||
kind: 'named',
|
||||
value: name as ValidIdentifierName,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a valid identifier name. This should *not* be used for synthesizing
|
||||
* identifier names: only call this method for identifier names that appear in the
|
||||
* original source code.
|
||||
*/
|
||||
export function makeIdentifierName(name: string): ValidatedIdentifier {
|
||||
if (isReservedWord(name)) {
|
||||
CompilerError.throwInvalidJS({
|
||||
reason: 'Expected a non-reserved identifier name',
|
||||
loc: GeneratedSource,
|
||||
description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`,
|
||||
suggestions: null,
|
||||
});
|
||||
} else {
|
||||
CompilerError.invariant(t.isValidIdentifier(name), {
|
||||
reason: `Expected a valid identifier name`,
|
||||
loc: GeneratedSource,
|
||||
description: `\`${name}\` is not a valid JavaScript identifier`,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
return {
|
||||
kind: 'named',
|
||||
value: name as ValidIdentifierName,
|
||||
};
|
||||
return validateIdentifierName(name).unwrap();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1337,8 +1363,14 @@ export function makeIdentifierName(name: string): ValidatedIdentifier {
|
||||
export function promoteTemporary(identifier: Identifier): void {
|
||||
CompilerError.invariant(identifier.name === null, {
|
||||
reason: `Expected a temporary (unnamed) identifier`,
|
||||
loc: GeneratedSource,
|
||||
description: `Identifier already has a name, \`${identifier.name}\``,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
identifier.name = {
|
||||
@@ -1361,8 +1393,14 @@ export function isPromotedTemporary(name: string): boolean {
|
||||
export function promoteTemporaryJsxTag(identifier: Identifier): void {
|
||||
CompilerError.invariant(identifier.name === null, {
|
||||
reason: `Expected a temporary (unnamed) identifier`,
|
||||
loc: GeneratedSource,
|
||||
description: `Identifier already has a name, \`${identifier.name}\``,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
identifier.name = {
|
||||
@@ -1530,7 +1568,13 @@ export function isMutableEffect(
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected unknown effect',
|
||||
description: null,
|
||||
loc: location,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: location,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
@@ -1663,7 +1707,13 @@ export function makeBlockId(id: number): BlockId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected block id to be a non-negative integer',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return id as BlockId;
|
||||
@@ -1680,7 +1730,13 @@ export function makeScopeId(id: number): ScopeId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected block id to be a non-negative integer',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return id as ScopeId;
|
||||
@@ -1697,7 +1753,13 @@ export function makeIdentifierId(id: number): IdentifierId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected identifier id to be a non-negative integer',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return id as IdentifierId;
|
||||
@@ -1714,7 +1776,13 @@ export function makeDeclarationId(id: number): DeclarationId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected declaration id to be a non-negative integer',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return id as DeclarationId;
|
||||
@@ -1731,7 +1799,13 @@ export function makeInstructionId(id: number): InstructionId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected instruction id to be a non-negative integer',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return id as InstructionId;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import {Binding, NodePath} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import {CompilerError, ErrorCategory, ErrorSeverity} from '../CompilerError';
|
||||
import {CompilerError, ErrorCategory} from '../CompilerError';
|
||||
import {Environment} from './Environment';
|
||||
import {
|
||||
BasicBlock,
|
||||
@@ -309,8 +309,7 @@ export default class HIRBuilder {
|
||||
resolveBinding(node: t.Identifier): Identifier {
|
||||
if (node.name === 'fbt') {
|
||||
CompilerError.throwDiagnostic({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.FBT,
|
||||
category: ErrorCategory.Todo,
|
||||
reason: 'Support local variables named `fbt`',
|
||||
description:
|
||||
'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',
|
||||
@@ -325,7 +324,6 @@ export default class HIRBuilder {
|
||||
}
|
||||
if (node.name === 'this') {
|
||||
CompilerError.throwDiagnostic({
|
||||
severity: ErrorSeverity.UnsupportedJS,
|
||||
category: ErrorCategory.UnsupportedSyntax,
|
||||
reason: '`this` is not supported syntax',
|
||||
description:
|
||||
@@ -509,7 +507,13 @@ export default class HIRBuilder {
|
||||
{
|
||||
reason: 'Mismatched label',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
@@ -532,7 +536,13 @@ export default class HIRBuilder {
|
||||
{
|
||||
reason: 'Mismatched label',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
@@ -568,7 +578,13 @@ export default class HIRBuilder {
|
||||
{
|
||||
reason: 'Mismatched loops',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
@@ -593,7 +609,13 @@ export default class HIRBuilder {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Expected a loop or switch to be in scope',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
@@ -614,7 +636,13 @@ export default class HIRBuilder {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Continue may only refer to a labeled loop',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
@@ -622,7 +650,13 @@ export default class HIRBuilder {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Expected a loop to be in scope',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
@@ -645,7 +679,13 @@ function _shrink(func: HIR): void {
|
||||
CompilerError.invariant(block != null, {
|
||||
reason: `expected block ${blockId} to exist`,
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
target = getTargetIfIndirection(block);
|
||||
@@ -777,7 +817,13 @@ function getReversePostorderedBlocks(func: HIR): HIR['blocks'] {
|
||||
CompilerError.invariant(block != null, {
|
||||
reason: '[HIRBuilder] Unexpected null block',
|
||||
description: `expected block ${blockId} to exist`,
|
||||
loc: GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
const successors = [...eachTerminalSuccessor(block.terminal)].reverse();
|
||||
const fallthrough = terminalFallthrough(block.terminal);
|
||||
@@ -833,7 +879,13 @@ export function markInstructionIds(func: HIR): void {
|
||||
CompilerError.invariant(!visited.has(instr), {
|
||||
reason: `${printInstruction(instr)} already visited!`,
|
||||
description: null,
|
||||
loc: instr.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instr.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
visited.add(instr);
|
||||
@@ -856,7 +908,13 @@ export function markPredecessors(func: HIR): void {
|
||||
CompilerError.invariant(block != null, {
|
||||
reason: 'unexpected missing block',
|
||||
description: `block ${blockId}`,
|
||||
loc: GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
if (prevBlock) {
|
||||
block.preds.add(prevBlock.id);
|
||||
|
||||
@@ -61,7 +61,13 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
|
||||
CompilerError.invariant(predecessor !== undefined, {
|
||||
reason: `Expected predecessor ${predecessorId} to exist`,
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
if (predecessor.terminal.kind !== 'goto' || predecessor.kind !== 'block') {
|
||||
@@ -77,7 +83,13 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
|
||||
CompilerError.invariant(phi.operands.size === 1, {
|
||||
reason: `Found a block with a single predecessor but where a phi has multiple (${phi.operands.size}) operands`,
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
const operand = Array.from(phi.operands.values())[0]!;
|
||||
|
||||
@@ -119,7 +119,13 @@ function parseAliasingSignatureConfig(
|
||||
CompilerError.invariant(!lifetimes.has(temp), {
|
||||
reason: `Invalid type configuration for module`,
|
||||
description: `Expected aliasing signature to have unique names for receiver, params, rest, returns, and temporaries in module '${moduleName}'`,
|
||||
loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
const place = signatureArgument(lifetimes.size);
|
||||
lifetimes.set(temp, place);
|
||||
@@ -130,7 +136,13 @@ function parseAliasingSignatureConfig(
|
||||
CompilerError.invariant(place != null, {
|
||||
reason: `Invalid type configuration for module`,
|
||||
description: `Expected aliasing signature effects to reference known names from receiver/params/rest/returns/temporaries, but '${temp}' is not a known name in '${moduleName}'`,
|
||||
loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
return place;
|
||||
}
|
||||
@@ -265,7 +277,13 @@ function addShape(
|
||||
CompilerError.invariant(!registry.has(id), {
|
||||
reason: `[ObjectShape] Could not add shape to registry: name ${id} already exists.`,
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
registry.set(id, shape);
|
||||
|
||||
@@ -56,6 +56,9 @@ export function printFunction(fn: HIRFunction): string {
|
||||
} else {
|
||||
definition += '<<anonymous>>';
|
||||
}
|
||||
if (fn.nameHint != null) {
|
||||
definition += ` ${fn.nameHint}`;
|
||||
}
|
||||
if (fn.params.length !== 0) {
|
||||
definition +=
|
||||
'(' +
|
||||
@@ -596,7 +599,13 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
|
||||
{
|
||||
reason: 'Bad assumption about quasi length.',
|
||||
description: null,
|
||||
loc: instrValue.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instrValue.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
@@ -865,8 +874,15 @@ export function printManualMemoDependency(
|
||||
} else {
|
||||
CompilerError.invariant(val.root.value.identifier.name?.kind === 'named', {
|
||||
reason: 'DepsValidation: expected named local variable in depslist',
|
||||
description: null,
|
||||
suggestions: null,
|
||||
loc: val.root.value.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: val.root.value.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
rootStr = nameOnly
|
||||
? val.root.value.identifier.name.value
|
||||
|
||||
@@ -86,7 +86,14 @@ export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
|
||||
const hoistables = hoistablePropertyLoads.get(scope.id);
|
||||
CompilerError.invariant(hoistables != null, {
|
||||
reason: '[PropagateScopeDependencies] Scope not found in tracked blocks',
|
||||
loc: GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
/**
|
||||
* Step 2: Calculate hoistable dependencies.
|
||||
@@ -428,7 +435,14 @@ export class DependencyCollectionContext {
|
||||
const scopedDependencies = this.#dependencies.value;
|
||||
CompilerError.invariant(scopedDependencies != null, {
|
||||
reason: '[PropagateScopeDeps]: Unexpected scope mismatch',
|
||||
loc: scope.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: scope.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Restore context of previous scope
|
||||
|
||||
@@ -53,7 +53,14 @@ export function pruneUnusedLabelsHIR(fn: HIRFunction): void {
|
||||
next.phis.size === 0 && fallthrough.phis.size === 0,
|
||||
{
|
||||
reason: 'Unexpected phis when merging label blocks',
|
||||
loc: label.terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: label.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -64,7 +71,14 @@ export function pruneUnusedLabelsHIR(fn: HIRFunction): void {
|
||||
fallthrough.preds.has(nextId),
|
||||
{
|
||||
reason: 'Unexpected block predecessors when merging label blocks',
|
||||
loc: label.terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: label.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -202,8 +202,14 @@ function writeOptionalDependency(
|
||||
CompilerError.invariant(firstOptional !== -1, {
|
||||
reason:
|
||||
'[ScopeDependencyUtils] Internal invariant broken: expected optional path',
|
||||
loc: dep.identifier.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: dep.identifier.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
if (firstOptional === dep.path.length - 1) {
|
||||
@@ -239,7 +245,13 @@ function writeOptionalDependency(
|
||||
CompilerError.invariant(testIdentifier !== null, {
|
||||
reason: 'Satisfy type checker',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
|
||||
@@ -87,7 +87,13 @@ export function makeTypeId(id: number): TypeId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected instruction id to be a non-negative integer',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return id as TypeId;
|
||||
|
||||
@@ -1233,7 +1233,14 @@ export class ScopeBlockTraversal {
|
||||
CompilerError.invariant(blockInfo.scope.id === top, {
|
||||
reason:
|
||||
'Expected traversed block fallthrough to match top-most active scope',
|
||||
loc: block.instructions[0]?.loc ?? block.terminal.id,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: block.instructions[0]?.loc ?? block.terminal.id,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
this.#activeScopes.pop();
|
||||
}
|
||||
@@ -1247,7 +1254,14 @@ export class ScopeBlockTraversal {
|
||||
!this.blockInfos.has(block.terminal.fallthrough),
|
||||
{
|
||||
reason: 'Expected unique scope blocks and fallthroughs',
|
||||
loc: block.terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: block.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
this.blockInfos.set(block.terminal.block, {
|
||||
|
||||
@@ -78,7 +78,14 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
|
||||
case 'Apply': {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,
|
||||
loc: effect.function.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: effect.function.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
case 'Mutate':
|
||||
|
||||
@@ -5,12 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorSeverity,
|
||||
SourceLocation,
|
||||
} from '..';
|
||||
import {CompilerDiagnostic, CompilerError, SourceLocation} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
CallExpression,
|
||||
@@ -255,7 +250,6 @@ function getManualMemoizationReplacement(
|
||||
*/
|
||||
args: [],
|
||||
loc,
|
||||
suppressions: [],
|
||||
};
|
||||
} else {
|
||||
/*
|
||||
@@ -303,11 +297,10 @@ function extractManualMemoizationArgs(
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: `Expected a callback function to be passed to ${kind}`,
|
||||
description: `Expected a callback function to be passed to ${kind}`,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: instr.value.loc,
|
||||
message: `Expected a callback function to be passed to ${kind}`,
|
||||
@@ -319,11 +312,10 @@ function extractManualMemoizationArgs(
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: `Unexpected spread argument to ${kind}`,
|
||||
description: `Unexpected spread argument to ${kind}`,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: instr.value.loc,
|
||||
message: `Unexpected spread argument to ${kind}`,
|
||||
@@ -340,11 +332,10 @@ function extractManualMemoizationArgs(
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
description: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: depsListPlace.loc,
|
||||
message: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
@@ -359,11 +350,10 @@ function extractManualMemoizationArgs(
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
|
||||
description: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: dep.loc,
|
||||
message: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
|
||||
@@ -464,16 +454,15 @@ export function dropManualMemoization(
|
||||
if (!hasNonVoidReturn(funcToCheck.loweredFunc.func)) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: 'useMemo() callbacks must return a value',
|
||||
description: `This ${
|
||||
manualMemo.loadInstr.value.kind === 'PropertyLoad'
|
||||
? 'React.useMemo'
|
||||
: 'useMemo'
|
||||
} callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects.`,
|
||||
} callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects`,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: instr.value.loc,
|
||||
message: 'useMemo() callbacks must return a value',
|
||||
@@ -506,11 +495,10 @@ export function dropManualMemoization(
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: `Expected the first argument to be an inline function expression`,
|
||||
description: `Expected the first argument to be an inline function expression`,
|
||||
suggestions: [],
|
||||
}).withDetail({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: fnPlace.loc,
|
||||
message: `Expected the first argument to be an inline function expression`,
|
||||
@@ -625,7 +613,14 @@ function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
|
||||
default: {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected terminal in optional`,
|
||||
loc: terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: `Unexpected ${terminal.kind} in optional`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,7 +438,14 @@ function rewriteSplices(
|
||||
{
|
||||
reason:
|
||||
'[InferEffectDependencies] Internal invariant broken: expected block instructions to be sorted',
|
||||
loc: originalInstrs[cursor].loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: originalInstrs[cursor].loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
currBlock.instructions.push(originalInstrs[cursor]);
|
||||
@@ -447,7 +454,14 @@ function rewriteSplices(
|
||||
CompilerError.invariant(originalInstrs[cursor].id === rewrite.location, {
|
||||
reason:
|
||||
'[InferEffectDependencies] Internal invariant broken: splice location not found',
|
||||
loc: originalInstrs[cursor].loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: originalInstrs[cursor].loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (rewrite.kind === 'instr') {
|
||||
@@ -467,7 +481,14 @@ function rewriteSplices(
|
||||
{
|
||||
reason:
|
||||
'[InferEffectDependencies] Internal invariant broken: expected entry block to have a fallthrough',
|
||||
loc: entryBlock.terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: entryBlock.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
const originalTerminal = currBlock.terminal;
|
||||
@@ -566,7 +587,14 @@ function inferMinimalDependencies(
|
||||
CompilerError.invariant(hoistableToFnEntry != null, {
|
||||
reason:
|
||||
'[InferEffectDependencies] Internal invariant broken: missing entry block',
|
||||
loc: fnInstr.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: fnInstr.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const dependencies = inferDependencies(
|
||||
@@ -622,7 +650,14 @@ function inferDependencies(
|
||||
CompilerError.invariant(resultUnfiltered != null, {
|
||||
reason:
|
||||
'[InferEffectDependencies] Internal invariant broken: missing scope dependencies',
|
||||
loc: fn.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: fn.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const fnContext = new Set(fn.context.map(dep => dep.identifier.id));
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
Effect,
|
||||
ErrorSeverity,
|
||||
SourceLocation,
|
||||
ValueKind,
|
||||
} from '..';
|
||||
@@ -59,7 +58,6 @@ import {
|
||||
printInstruction,
|
||||
printInstructionValue,
|
||||
printPlace,
|
||||
printSourceLocation,
|
||||
} from '../HIR/PrintHIR';
|
||||
import {FunctionSignature} from '../HIR/ObjectShape';
|
||||
import prettyFormat from 'pretty-format';
|
||||
@@ -136,7 +134,13 @@ export function inferMutationAliasingEffects(
|
||||
reason:
|
||||
'Expected React component to have not more than two parameters: one for props and for ref',
|
||||
description: null,
|
||||
loc: fn.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: fn.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
const [props, ref] = fn.params;
|
||||
@@ -203,7 +207,13 @@ export function inferMutationAliasingEffects(
|
||||
CompilerError.invariant(false, {
|
||||
reason: `[InferMutationAliasingEffects] Potential infinite loop`,
|
||||
description: `A value, temporary place, or effect was not cached properly`,
|
||||
loc: fn.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: fn.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
for (const [blockId, block] of fn.body.blocks) {
|
||||
@@ -358,7 +368,14 @@ function inferBlock(
|
||||
CompilerError.invariant(state.kind(handlerParam) != null, {
|
||||
reason:
|
||||
'Expected catch binding to be intialized with a DeclareLocal Catch instruction',
|
||||
loc: terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
const effects: Array<AliasingEffect> = [];
|
||||
for (const instr of block.instructions) {
|
||||
@@ -455,10 +472,9 @@ function applySignature(
|
||||
: 'value';
|
||||
const diagnostic = CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Immutability,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'This value cannot be modified',
|
||||
description: `${reason}.`,
|
||||
}).withDetail({
|
||||
description: reason,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `${variable} cannot be modified`,
|
||||
@@ -467,7 +483,7 @@ function applySignature(
|
||||
effect.kind === 'Mutate' &&
|
||||
effect.reason?.kind === 'AssignCurrentProperty'
|
||||
) {
|
||||
diagnostic.withDetail({
|
||||
diagnostic.withDetails({
|
||||
kind: 'hint',
|
||||
message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`,
|
||||
});
|
||||
@@ -509,7 +525,14 @@ function applySignature(
|
||||
) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Expected instruction lvalue to be initialized`,
|
||||
loc: instruction.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instruction.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
return effects.length !== 0 ? effects : null;
|
||||
@@ -538,7 +561,13 @@ function applyEffect(
|
||||
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
|
||||
reason: `Cannot re-initialize variable within an instruction`,
|
||||
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
|
||||
loc: effect.into.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: effect.into.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
initialized.add(effect.into.identifier.id);
|
||||
|
||||
@@ -577,7 +606,13 @@ function applyEffect(
|
||||
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
|
||||
reason: `Cannot re-initialize variable within an instruction`,
|
||||
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
|
||||
loc: effect.into.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: effect.into.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
initialized.add(effect.into.identifier.id);
|
||||
|
||||
@@ -637,7 +672,13 @@ function applyEffect(
|
||||
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
|
||||
reason: `Cannot re-initialize variable within an instruction`,
|
||||
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
|
||||
loc: effect.into.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: effect.into.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
initialized.add(effect.into.identifier.id);
|
||||
|
||||
@@ -707,11 +748,21 @@ function applyEffect(
|
||||
case 'Alias':
|
||||
case 'Capture': {
|
||||
CompilerError.invariant(
|
||||
effect.kind === 'Capture' || initialized.has(effect.into.identifier.id),
|
||||
effect.kind === 'Capture' ||
|
||||
effect.kind === 'MaybeAlias' ||
|
||||
initialized.has(effect.into.identifier.id),
|
||||
{
|
||||
reason: `Expected destination value to already be initialized within this instruction for Alias effect`,
|
||||
description: `Destination ${printPlace(effect.into)} is not initialized in this instruction`,
|
||||
loc: effect.into.loc,
|
||||
reason: `Expected destination to already be initialized within this instruction`,
|
||||
description:
|
||||
`Destination ${printPlace(effect.into)} is not initialized in this ` +
|
||||
`instruction for effect ${printAliasingEffect(effect)}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: effect.into.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
/*
|
||||
@@ -720,49 +771,67 @@ function applyEffect(
|
||||
* copy-on-write semantics, then we can prune the effect
|
||||
*/
|
||||
const intoKind = state.kind(effect.into).kind;
|
||||
let isMutableDesination: boolean;
|
||||
let destinationType: 'context' | 'mutable' | null = null;
|
||||
switch (intoKind) {
|
||||
case ValueKind.Context:
|
||||
case ValueKind.Mutable:
|
||||
case ValueKind.MaybeFrozen: {
|
||||
isMutableDesination = true;
|
||||
case ValueKind.Context: {
|
||||
destinationType = 'context';
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
isMutableDesination = false;
|
||||
case ValueKind.Mutable:
|
||||
case ValueKind.MaybeFrozen: {
|
||||
destinationType = 'mutable';
|
||||
break;
|
||||
}
|
||||
}
|
||||
const fromKind = state.kind(effect.from).kind;
|
||||
let isMutableReferenceType: boolean;
|
||||
let sourceType: 'context' | 'mutable' | 'frozen' | null = null;
|
||||
switch (fromKind) {
|
||||
case ValueKind.Context: {
|
||||
sourceType = 'context';
|
||||
break;
|
||||
}
|
||||
case ValueKind.Global:
|
||||
case ValueKind.Primitive: {
|
||||
isMutableReferenceType = false;
|
||||
break;
|
||||
}
|
||||
case ValueKind.Frozen: {
|
||||
isMutableReferenceType = false;
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
{
|
||||
kind: 'ImmutableCapture',
|
||||
from: effect.from,
|
||||
into: effect.into,
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
sourceType = 'frozen';
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
isMutableReferenceType = true;
|
||||
sourceType = 'mutable';
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isMutableDesination && isMutableReferenceType) {
|
||||
|
||||
if (sourceType === 'frozen') {
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
{
|
||||
kind: 'ImmutableCapture',
|
||||
from: effect.from,
|
||||
into: effect.into,
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
} else if (
|
||||
(sourceType === 'mutable' && destinationType === 'mutable') ||
|
||||
effect.kind === 'MaybeAlias'
|
||||
) {
|
||||
effects.push(effect);
|
||||
} else if (
|
||||
(sourceType === 'context' && destinationType != null) ||
|
||||
(sourceType === 'mutable' && destinationType === 'context')
|
||||
) {
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
{kind: 'MaybeAlias', from: effect.from, into: effect.into},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -770,7 +839,13 @@ function applyEffect(
|
||||
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
|
||||
reason: `Cannot re-initialize variable within an instruction`,
|
||||
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
|
||||
loc: effect.into.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: effect.into.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
initialized.add(effect.into.identifier.id);
|
||||
|
||||
@@ -1040,18 +1115,17 @@ function applyEffect(
|
||||
);
|
||||
const diagnostic = CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Immutability,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'Cannot access variable before it is declared',
|
||||
description: `${variable ?? 'This variable'} is accessed before it is declared, which prevents the earlier access from updating when this value changes over time.`,
|
||||
description: `${variable ?? 'This variable'} is accessed before it is declared, which prevents the earlier access from updating when this value changes over time`,
|
||||
});
|
||||
if (hoistedAccess != null && hoistedAccess.loc != effect.value.loc) {
|
||||
diagnostic.withDetail({
|
||||
diagnostic.withDetails({
|
||||
kind: 'error',
|
||||
loc: hoistedAccess.loc,
|
||||
message: `${variable ?? 'variable'} accessed before it is declared`,
|
||||
});
|
||||
}
|
||||
diagnostic.withDetail({
|
||||
diagnostic.withDetails({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `${variable ?? 'variable'} is declared here`,
|
||||
@@ -1080,10 +1154,9 @@ function applyEffect(
|
||||
: 'value';
|
||||
const diagnostic = CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Immutability,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'This value cannot be modified',
|
||||
description: `${reason}.`,
|
||||
}).withDetail({
|
||||
description: reason,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `${variable} cannot be modified`,
|
||||
@@ -1092,7 +1165,7 @@ function applyEffect(
|
||||
effect.kind === 'Mutate' &&
|
||||
effect.reason?.kind === 'AssignCurrentProperty'
|
||||
) {
|
||||
diagnostic.withDetail({
|
||||
diagnostic.withDetails({
|
||||
kind: 'hint',
|
||||
message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`,
|
||||
});
|
||||
@@ -1173,7 +1246,13 @@ class InferenceState {
|
||||
reason:
|
||||
'[InferMutationAliasingEffects] Expected all top-level identifiers to be defined as variables, not values',
|
||||
description: null,
|
||||
loc: value.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: value.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
this.#values.set(value, kind);
|
||||
@@ -1184,7 +1263,13 @@ class InferenceState {
|
||||
CompilerError.invariant(values != null, {
|
||||
reason: `[InferMutationAliasingEffects] Expected value kind to be initialized`,
|
||||
description: `${printPlace(place)}`,
|
||||
loc: place.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: place.loc,
|
||||
message: 'this is uninitialized',
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return Array.from(values);
|
||||
@@ -1196,7 +1281,13 @@ class InferenceState {
|
||||
CompilerError.invariant(values != null, {
|
||||
reason: `[InferMutationAliasingEffects] Expected value kind to be initialized`,
|
||||
description: `${printPlace(place)}`,
|
||||
loc: place.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: place.loc,
|
||||
message: 'this is uninitialized',
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
let mergedKind: AbstractValue | null = null;
|
||||
@@ -1208,7 +1299,13 @@ class InferenceState {
|
||||
CompilerError.invariant(mergedKind !== null, {
|
||||
reason: `[InferMutationAliasingEffects] Expected at least one value`,
|
||||
description: `No value found at \`${printPlace(place)}\``,
|
||||
loc: place.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: place.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return mergedKind;
|
||||
@@ -1220,7 +1317,13 @@ class InferenceState {
|
||||
CompilerError.invariant(values != null, {
|
||||
reason: `[InferMutationAliasingEffects] Expected value for identifier to be initialized`,
|
||||
description: `${printIdentifier(value.identifier)}`,
|
||||
loc: value.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: value.loc,
|
||||
message: 'Expected value for identifier to be initialized',
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
this.#variables.set(place.identifier.id, new Set(values));
|
||||
@@ -1231,7 +1334,13 @@ class InferenceState {
|
||||
CompilerError.invariant(values != null, {
|
||||
reason: `[InferMutationAliasingEffects] Expected value for identifier to be initialized`,
|
||||
description: `${printIdentifier(value.identifier)}`,
|
||||
loc: value.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: value.loc,
|
||||
message: 'Expected value for identifier to be initialized',
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
const prevValues = this.values(place);
|
||||
@@ -1244,11 +1353,15 @@ class InferenceState {
|
||||
// Defines (initializing or updating) a variable with a specific kind of value.
|
||||
define(place: Place, value: InstructionValue): void {
|
||||
CompilerError.invariant(this.#values.has(value), {
|
||||
reason: `[InferMutationAliasingEffects] Expected value to be initialized at '${printSourceLocation(
|
||||
value.loc,
|
||||
)}'`,
|
||||
reason: `[InferMutationAliasingEffects] Expected value to be initialized`,
|
||||
description: printInstructionValue(value),
|
||||
loc: value.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: value.loc,
|
||||
message: 'Expected value for identifier to be initialized',
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
this.#variables.set(place.identifier.id, new Set([value]));
|
||||
@@ -2056,11 +2169,10 @@ function computeSignatureForInstruction(
|
||||
place: value.value,
|
||||
error: CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Globals,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
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({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: instr.loc,
|
||||
message: `${variable} cannot be reassigned`,
|
||||
@@ -2156,14 +2268,13 @@ function computeEffectsForLegacySignature(
|
||||
place: receiver,
|
||||
error: CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Purity,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'Cannot call impure function during render',
|
||||
description:
|
||||
(signature.canonicalName != null
|
||||
? `\`${signature.canonicalName}\` is an impure function. `
|
||||
: '') +
|
||||
'Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
|
||||
}).withDetail({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc,
|
||||
message: 'Cannot call impure function',
|
||||
@@ -2175,15 +2286,14 @@ function computeEffectsForLegacySignature(
|
||||
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.',
|
||||
'memoized',
|
||||
].join(''),
|
||||
}).withDetail({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: receiver.loc,
|
||||
message: signature.knownIncompatible,
|
||||
@@ -2683,7 +2793,13 @@ export function isKnownMutableEffect(effect: Effect): boolean {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected unknown effect',
|
||||
description: null,
|
||||
loc: GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
@@ -2792,7 +2908,13 @@ function mergeValueKinds(a: ValueKind, b: ValueKind): ValueKind {
|
||||
{
|
||||
reason: `Unexpected value kind in mergeValues()`,
|
||||
description: `Found kinds ${a} and ${b}`,
|
||||
loc: GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
return ValueKind.Primitive;
|
||||
|
||||
@@ -229,7 +229,14 @@ export function inferMutationAliasingRanges(
|
||||
} else {
|
||||
CompilerError.invariant(effect.kind === 'Freeze', {
|
||||
reason: `Unexpected '${effect.kind}' effect for MaybeThrow terminal`,
|
||||
loc: block.terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: block.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -378,7 +385,14 @@ export function inferMutationAliasingRanges(
|
||||
case 'Apply': {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,
|
||||
loc: effect.function.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: effect.function.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
case 'MutateTransitive':
|
||||
@@ -525,7 +539,14 @@ export function inferMutationAliasingRanges(
|
||||
const fromNode = state.nodes.get(from.identifier);
|
||||
CompilerError.invariant(fromNode != null, {
|
||||
reason: `Expected a node to exist for all parameters and context variables`,
|
||||
loc: into.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: into.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
if (fromNode.lastMutated === mutationIndex) {
|
||||
if (into.identifier.id === fn.returns.identifier.id) {
|
||||
@@ -547,7 +568,7 @@ export function inferMutationAliasingRanges(
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.hasErrors() && !isFunctionExpression) {
|
||||
if (errors.hasAnyErrors() && !isFunctionExpression) {
|
||||
return Err(errors);
|
||||
}
|
||||
return Ok(functionEffects);
|
||||
@@ -758,7 +779,13 @@ class AliasingState {
|
||||
if (edge.index >= index) {
|
||||
break;
|
||||
}
|
||||
queue.push({place: edge.node, transitive, direction: 'forwards', kind});
|
||||
queue.push({
|
||||
place: edge.node,
|
||||
transitive,
|
||||
direction: 'forwards',
|
||||
// Traversing a maybeAlias edge always downgrades to conditional mutation
|
||||
kind: edge.kind === 'maybeAlias' ? MutationKind.Conditional : kind,
|
||||
});
|
||||
}
|
||||
for (const [alias, when] of node.createdFrom) {
|
||||
if (when >= index) {
|
||||
@@ -786,7 +813,12 @@ class AliasingState {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push({place: alias, transitive, direction: 'backwards', kind});
|
||||
queue.push({
|
||||
place: alias,
|
||||
transitive,
|
||||
direction: 'backwards',
|
||||
kind,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* MaybeAlias indicates potential data flow from unknown function calls,
|
||||
|
||||
@@ -349,7 +349,13 @@ export function inferReactivePlaces(fn: HIRFunction): void {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected unknown effect',
|
||||
description: null,
|
||||
loc: operand.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: operand.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -191,7 +191,14 @@ function evaluatePhi(phi: Phi, constants: Constants): Constant | null {
|
||||
case 'Primitive': {
|
||||
CompilerError.invariant(value.kind === 'Primitive', {
|
||||
reason: 'value kind expected to be Primitive',
|
||||
loc: null,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
@@ -204,7 +211,14 @@ function evaluatePhi(phi: Phi, constants: Constants): Constant | null {
|
||||
case 'LoadGlobal': {
|
||||
CompilerError.invariant(value.kind === 'LoadGlobal', {
|
||||
reason: 'value kind expected to be LoadGlobal',
|
||||
loc: null,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
|
||||
@@ -709,7 +709,14 @@ function createPropsProperties(
|
||||
const spreadProp = jsxSpreadAttributes[0];
|
||||
CompilerError.invariant(spreadProp.kind === 'JsxSpreadAttribute', {
|
||||
reason: 'Spread prop attribute must be of kind JSXSpreadAttribute',
|
||||
loc: instr.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instr.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
propsProperty = {
|
||||
kind: 'ObjectProperty',
|
||||
|
||||
@@ -78,10 +78,17 @@ export function instructionReordering(fn: HIRFunction): void {
|
||||
}
|
||||
CompilerError.invariant(shared.size === 0, {
|
||||
reason: `InstructionReordering: expected all reorderable nodes to have been emitted`,
|
||||
loc:
|
||||
[...shared.values()]
|
||||
.map(node => node.instruction?.loc)
|
||||
.filter(loc => loc != null)[0] ?? GeneratedSource,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc:
|
||||
[...shared.values()]
|
||||
.map(node => node.instruction?.loc)
|
||||
.filter(loc => loc != null)[0] ?? GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
markInstructionIds(fn.body);
|
||||
}
|
||||
@@ -302,7 +309,13 @@ function reorderBlock(
|
||||
node.reorderability === Reorderability.Reorderable,
|
||||
{
|
||||
reason: `Expected all remaining instructions to be reorderable`,
|
||||
loc: node.instruction?.loc ?? block.terminal.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: node.instruction?.loc ?? block.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
description:
|
||||
node.instruction != null
|
||||
? `Instruction [${node.instruction.id}] was not emitted yet but is not reorderable`
|
||||
|
||||
@@ -249,6 +249,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
const fn: HIRFunction = {
|
||||
loc: GeneratedSource,
|
||||
id: null,
|
||||
nameHint: null,
|
||||
fnType: 'Other',
|
||||
env,
|
||||
params: [obj],
|
||||
@@ -275,6 +276,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
value: {
|
||||
kind: 'FunctionExpression',
|
||||
name: null,
|
||||
nameHint: null,
|
||||
loweredFunc: {
|
||||
func: fn,
|
||||
},
|
||||
|
||||
@@ -31,7 +31,9 @@ export function outlineFunctions(
|
||||
) {
|
||||
const loweredFunc = value.loweredFunc.func;
|
||||
|
||||
const id = fn.env.generateGloballyUniqueIdentifierName(loweredFunc.id);
|
||||
const id = fn.env.generateGloballyUniqueIdentifierName(
|
||||
loweredFunc.id ?? loweredFunc.nameHint,
|
||||
);
|
||||
loweredFunc.id = id.value;
|
||||
|
||||
fn.env.outlineFunction(loweredFunc, null);
|
||||
|
||||
@@ -364,6 +364,7 @@ function emitOutlinedFn(
|
||||
const fn: HIRFunction = {
|
||||
loc: GeneratedSource,
|
||||
id: null,
|
||||
nameHint: null,
|
||||
fnType: 'Other',
|
||||
env,
|
||||
params: [propsObj],
|
||||
|
||||
@@ -52,7 +52,13 @@ export function pruneMaybeThrows(fn: HIRFunction): void {
|
||||
const mappedTerminal = terminalMapping.get(predecessor);
|
||||
CompilerError.invariant(mappedTerminal != null, {
|
||||
reason: `Expected non-existing phi operand's predecessor to have been mapped to a new terminal`,
|
||||
loc: GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
description: `Could not find mapping for predecessor bb${predecessor} in block bb${
|
||||
block.id
|
||||
} for phi ${printPlace(phi.place)}`,
|
||||
|
||||
@@ -41,8 +41,15 @@ function findScopesToMerge(fn: HIRFunction): DisjointSet<ReactiveScope> {
|
||||
{
|
||||
reason:
|
||||
'Internal error: Expected all ObjectExpressions and ObjectMethods to have non-null scope.',
|
||||
description: null,
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
mergeScopesBuilder.union([operandScope, lvalueScope]);
|
||||
|
||||
@@ -170,7 +170,14 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
|
||||
|
||||
CompilerError.invariant(!valueBlockNodes.has(fallthrough), {
|
||||
reason: 'Expect hir blocks to have unique fallthroughs',
|
||||
loc: terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
if (node != null) {
|
||||
valueBlockNodes.set(fallthrough, node);
|
||||
@@ -252,7 +259,14 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
|
||||
// Transition from block->value block, derive the outer block range
|
||||
CompilerError.invariant(fallthrough !== null, {
|
||||
reason: `Expected a fallthrough for value block`,
|
||||
loc: terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
const fallthroughBlock = fn.body.blocks.get(fallthrough)!;
|
||||
const nextId =
|
||||
|
||||
@@ -81,10 +81,16 @@ class CheckInstructionsAgainstScopesVisitor extends ReactiveFunctionVisitor<
|
||||
!this.activeScopes.has(scope.id)
|
||||
) {
|
||||
CompilerError.invariant(false, {
|
||||
description: `Instruction [${id}] is part of scope @${scope.id}, but that scope has already completed.`,
|
||||
loc: place.loc,
|
||||
reason:
|
||||
'Encountered an instruction that should be part of a scope, but where that scope has already completed',
|
||||
description: `Instruction [${id}] is part of scope @${scope.id}, but that scope has already completed`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: place.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -28,7 +28,14 @@ class Visitor extends ReactiveFunctionVisitor<Set<BlockId>> {
|
||||
if (terminal.kind === 'break' || terminal.kind === 'continue') {
|
||||
CompilerError.invariant(seenLabels.has(terminal.target), {
|
||||
reason: 'Unexpected break to invalid label',
|
||||
loc: stmt.terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: stmt.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ export function buildReactiveFunction(fn: HIRFunction): ReactiveFunction {
|
||||
return {
|
||||
loc: fn.loc,
|
||||
id: fn.id,
|
||||
nameHint: fn.nameHint,
|
||||
params: fn.params,
|
||||
generator: fn.generator,
|
||||
async: fn.async,
|
||||
@@ -70,7 +71,13 @@ class Driver {
|
||||
CompilerError.invariant(!this.cx.emitted.has(block.id), {
|
||||
reason: `Cannot emit the same block twice: bb${block.id}`,
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
this.cx.emitted.add(block.id);
|
||||
@@ -130,7 +137,14 @@ class Driver {
|
||||
if (this.cx.isScheduled(terminal.consequent)) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected 'if' where the consequent is already scheduled`,
|
||||
loc: terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
consequent = this.traverseBlock(
|
||||
@@ -143,7 +157,14 @@ class Driver {
|
||||
if (this.cx.isScheduled(alternateId)) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected 'if' where the alternate is already scheduled`,
|
||||
loc: terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
alternate = this.traverseBlock(this.cx.ir.blocks.get(alternateId)!);
|
||||
@@ -196,7 +217,14 @@ class Driver {
|
||||
if (this.cx.isScheduled(case_.block)) {
|
||||
CompilerError.invariant(case_.block === terminal.fallthrough, {
|
||||
reason: `Unexpected 'switch' where a case is already scheduled and block is not the fallthrough`,
|
||||
loc: terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
@@ -255,7 +283,14 @@ class Driver {
|
||||
} else {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected 'do-while' where the loop is already scheduled`,
|
||||
loc: terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -316,7 +351,14 @@ class Driver {
|
||||
} else {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected 'while' where the loop is already scheduled`,
|
||||
loc: terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -402,7 +444,14 @@ class Driver {
|
||||
} else {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected 'for' where the loop is already scheduled`,
|
||||
loc: terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -500,7 +549,14 @@ class Driver {
|
||||
} else {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected 'for-of' where the loop is already scheduled`,
|
||||
loc: terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -572,7 +628,14 @@ class Driver {
|
||||
} else {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected 'for-in' where the loop is already scheduled`,
|
||||
loc: terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -615,7 +678,14 @@ class Driver {
|
||||
if (this.cx.isScheduled(terminal.alternate)) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected 'branch' where the alternate is already scheduled`,
|
||||
loc: terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
alternate = this.traverseBlock(
|
||||
@@ -653,7 +723,14 @@ class Driver {
|
||||
if (this.cx.isScheduled(terminal.block)) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected 'label' where the block is already scheduled`,
|
||||
loc: terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
block = this.traverseBlock(this.cx.ir.blocks.get(terminal.block)!);
|
||||
@@ -811,7 +888,14 @@ class Driver {
|
||||
if (this.cx.isScheduled(terminal.block)) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected 'scope' where the block is already scheduled`,
|
||||
loc: terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
block = this.traverseBlock(this.cx.ir.blocks.get(terminal.block)!);
|
||||
@@ -837,7 +921,13 @@ class Driver {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected unsupported terminal',
|
||||
description: null,
|
||||
loc: terminal.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
@@ -874,7 +964,13 @@ class Driver {
|
||||
reason:
|
||||
'Expected branch block to end in an instruction that sets the test value',
|
||||
description: null,
|
||||
loc: instr.lvalue.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instr.lvalue.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
@@ -906,7 +1002,13 @@ class Driver {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Expected goto value block to have at least one instruction',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
} else if (defaultBlock.instructions.length === 1) {
|
||||
@@ -1191,14 +1293,27 @@ class Driver {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Expected a break target',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
if (this.cx.scopeFallthroughs.has(target.block)) {
|
||||
CompilerError.invariant(target.type === 'implicit', {
|
||||
reason: 'Expected reactive scope to implicitly break to fallthrough',
|
||||
loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
return null;
|
||||
}
|
||||
@@ -1224,7 +1339,13 @@ class Driver {
|
||||
CompilerError.invariant(target !== null, {
|
||||
reason: `Expected continue target to be scheduled for bb${block}`,
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
@@ -1299,7 +1420,13 @@ class Context {
|
||||
CompilerError.invariant(!this.#scheduled.has(block), {
|
||||
reason: `Break block is already scheduled: bb${block}`,
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
this.#scheduled.add(block);
|
||||
@@ -1318,7 +1445,13 @@ class Context {
|
||||
CompilerError.invariant(!this.#scheduled.has(continueBlock), {
|
||||
reason: `Continue block is already scheduled: bb${continueBlock}`,
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
this.#scheduled.add(continueBlock);
|
||||
@@ -1346,7 +1479,13 @@ class Context {
|
||||
CompilerError.invariant(last !== undefined && last.id === scheduleId, {
|
||||
reason: 'Can only unschedule the last target',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
if (last.type !== 'loop' || last.ownsBlock !== null) {
|
||||
@@ -1421,7 +1560,13 @@ class Context {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Expected a break target',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
pruneUnusedLabels,
|
||||
renameVariables,
|
||||
} from '.';
|
||||
import {CompilerError, ErrorCategory, ErrorSeverity} from '../CompilerError';
|
||||
import {CompilerError, ErrorCategory} from '../CompilerError';
|
||||
import {Environment, ExternalFunction} from '../HIR';
|
||||
import {
|
||||
ArrayPattern,
|
||||
@@ -61,6 +61,7 @@ export const EARLY_RETURN_SENTINEL = 'react.early_return_sentinel';
|
||||
export type CodegenFunction = {
|
||||
type: 'CodegenFunction';
|
||||
id: t.Identifier | null;
|
||||
nameHint: string | null;
|
||||
params: t.FunctionDeclaration['params'];
|
||||
body: t.BlockStatement;
|
||||
generator: boolean;
|
||||
@@ -296,7 +297,14 @@ export function codegenFunction(
|
||||
CompilerError.invariant(globalGating != null, {
|
||||
reason:
|
||||
'Bad config not caught! Expected at least one of gating or globalGating',
|
||||
loc: null,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
ifTest = globalGating;
|
||||
@@ -365,7 +373,7 @@ function codegenReactiveFunction(
|
||||
}
|
||||
}
|
||||
|
||||
if (cx.errors.hasErrors()) {
|
||||
if (cx.errors.hasAnyErrors()) {
|
||||
return Err(cx.errors);
|
||||
}
|
||||
|
||||
@@ -376,6 +384,7 @@ function codegenReactiveFunction(
|
||||
type: 'CodegenFunction',
|
||||
loc: fn.loc,
|
||||
id: fn.id !== null ? t.identifier(fn.id) : null,
|
||||
nameHint: fn.nameHint,
|
||||
params,
|
||||
body,
|
||||
generator: fn.generator,
|
||||
@@ -499,10 +508,16 @@ function codegenBlock(cx: Context, block: ReactiveBlock): t.BlockStatement {
|
||||
continue;
|
||||
}
|
||||
CompilerError.invariant(temp.get(key)! === value, {
|
||||
loc: null,
|
||||
reason: 'Expected temporary value to be unchanged',
|
||||
description: null,
|
||||
suggestions: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
cx.temp = temp;
|
||||
@@ -670,7 +685,13 @@ function codegenReactiveScope(
|
||||
description: `Declaration \`${printIdentifier(
|
||||
identifier,
|
||||
)}\` is unnamed in scope @${scope.id}`,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
@@ -707,7 +728,13 @@ function codegenReactiveScope(
|
||||
CompilerError.invariant(firstOutputIndex !== null, {
|
||||
reason: `Expected scope to have at least one declaration`,
|
||||
description: `Scope '@${scope.id}' has no declarations`,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
testCondition = t.binaryExpression(
|
||||
@@ -730,7 +757,13 @@ function codegenReactiveScope(
|
||||
{
|
||||
reason: `Expected to not have both change detection enabled and memoization disabled`,
|
||||
description: `Incompatible config options`,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
testCondition = t.logicalExpression(
|
||||
@@ -914,8 +947,14 @@ function codegenReactiveScope(
|
||||
earlyReturnValue.value.name.kind === 'named',
|
||||
{
|
||||
reason: `Expected early return value to be promoted to a named variable`,
|
||||
loc: earlyReturnValue.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: earlyReturnValue.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
@@ -945,7 +984,8 @@ function codegenTerminal(
|
||||
if (terminal.targetKind === 'implicit') {
|
||||
return null;
|
||||
}
|
||||
return t.breakStatement(
|
||||
return createBreakStatement(
|
||||
terminal.loc,
|
||||
terminal.targetKind === 'labeled'
|
||||
? t.identifier(codegenLabel(terminal.target))
|
||||
: null,
|
||||
@@ -955,14 +995,16 @@ function codegenTerminal(
|
||||
if (terminal.targetKind === 'implicit') {
|
||||
return null;
|
||||
}
|
||||
return t.continueStatement(
|
||||
return createContinueStatement(
|
||||
terminal.loc,
|
||||
terminal.targetKind === 'labeled'
|
||||
? t.identifier(codegenLabel(terminal.target))
|
||||
: null,
|
||||
);
|
||||
}
|
||||
case 'for': {
|
||||
return t.forStatement(
|
||||
return createForStatement(
|
||||
terminal.loc,
|
||||
codegenForInit(cx, terminal.init),
|
||||
codegenInstructionValueToExpression(cx, terminal.test),
|
||||
terminal.update !== null
|
||||
@@ -975,7 +1017,13 @@ function codegenTerminal(
|
||||
CompilerError.invariant(terminal.init.kind === 'SequenceExpression', {
|
||||
reason: `Expected a sequence expression init for for..in`,
|
||||
description: `Got \`${terminal.init.kind}\` expression instead`,
|
||||
loc: terminal.init.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.init.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
if (terminal.init.instructions.length !== 2) {
|
||||
@@ -1010,7 +1058,13 @@ function codegenTerminal(
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Expected a StoreLocal or Destructure to be assigned to the collection`,
|
||||
description: `Found ${iterableItem.value.kind}`,
|
||||
loc: iterableItem.value.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: iterableItem.value.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
@@ -1027,7 +1081,13 @@ function codegenTerminal(
|
||||
reason:
|
||||
'Destructure should never be Reassign as it would be an Object/ArrayPattern',
|
||||
description: null,
|
||||
loc: iterableItem.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: iterableItem.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
case InstructionKind.Catch:
|
||||
@@ -1038,7 +1098,13 @@ function codegenTerminal(
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected ${iterableItem.value.lvalue.kind} variable in for..in collection`,
|
||||
description: null,
|
||||
loc: iterableItem.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: iterableItem.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
default:
|
||||
@@ -1047,7 +1113,8 @@ function codegenTerminal(
|
||||
`Unhandled lvalue kind: ${iterableItem.value.lvalue.kind}`,
|
||||
);
|
||||
}
|
||||
return t.forInStatement(
|
||||
return createForInStatement(
|
||||
terminal.loc,
|
||||
/*
|
||||
* Special handling here since we only want the VariableDeclarators without any inits
|
||||
* This needs to be updated when we handle non-trivial ForOf inits
|
||||
@@ -1067,7 +1134,13 @@ function codegenTerminal(
|
||||
{
|
||||
reason: `Expected a single-expression sequence expression init for for..of`,
|
||||
description: `Got \`${terminal.init.kind}\` expression instead`,
|
||||
loc: terminal.init.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.init.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
@@ -1076,7 +1149,13 @@ function codegenTerminal(
|
||||
CompilerError.invariant(terminal.test.kind === 'SequenceExpression', {
|
||||
reason: `Expected a sequence expression test for for..of`,
|
||||
description: `Got \`${terminal.init.kind}\` expression instead`,
|
||||
loc: terminal.test.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.test.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
if (terminal.test.instructions.length !== 2) {
|
||||
@@ -1110,7 +1189,13 @@ function codegenTerminal(
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Expected a StoreLocal or Destructure to be assigned to the collection`,
|
||||
description: `Found ${iterableItem.value.kind}`,
|
||||
loc: iterableItem.value.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: iterableItem.value.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
@@ -1131,7 +1216,13 @@ function codegenTerminal(
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected ${iterableItem.value.lvalue.kind} variable in for..of collection`,
|
||||
description: null,
|
||||
loc: iterableItem.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: iterableItem.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
default:
|
||||
@@ -1140,7 +1231,8 @@ function codegenTerminal(
|
||||
`Unhandled lvalue kind: ${iterableItem.value.lvalue.kind}`,
|
||||
);
|
||||
}
|
||||
return t.forOfStatement(
|
||||
return createForOfStatement(
|
||||
terminal.loc,
|
||||
/*
|
||||
* Special handling here since we only want the VariableDeclarators without any inits
|
||||
* This needs to be updated when we handle non-trivial ForOf inits
|
||||
@@ -1162,7 +1254,7 @@ function codegenTerminal(
|
||||
alternate = block;
|
||||
}
|
||||
}
|
||||
return t.ifStatement(test, consequent, alternate);
|
||||
return createIfStatement(terminal.loc, test, consequent, alternate);
|
||||
}
|
||||
case 'return': {
|
||||
const value = codegenPlaceToExpression(cx, terminal.value);
|
||||
@@ -1173,7 +1265,8 @@ function codegenTerminal(
|
||||
return t.returnStatement(value);
|
||||
}
|
||||
case 'switch': {
|
||||
return t.switchStatement(
|
||||
return createSwitchStatement(
|
||||
terminal.loc,
|
||||
codegenPlaceToExpression(cx, terminal.test),
|
||||
terminal.cases.map(case_ => {
|
||||
const test =
|
||||
@@ -1186,15 +1279,26 @@ function codegenTerminal(
|
||||
);
|
||||
}
|
||||
case 'throw': {
|
||||
return t.throwStatement(codegenPlaceToExpression(cx, terminal.value));
|
||||
return createThrowStatement(
|
||||
terminal.loc,
|
||||
codegenPlaceToExpression(cx, terminal.value),
|
||||
);
|
||||
}
|
||||
case 'do-while': {
|
||||
const test = codegenInstructionValueToExpression(cx, terminal.test);
|
||||
return t.doWhileStatement(test, codegenBlock(cx, terminal.loop));
|
||||
return createDoWhileStatement(
|
||||
terminal.loc,
|
||||
test,
|
||||
codegenBlock(cx, terminal.loop),
|
||||
);
|
||||
}
|
||||
case 'while': {
|
||||
const test = codegenInstructionValueToExpression(cx, terminal.test);
|
||||
return t.whileStatement(test, codegenBlock(cx, terminal.loop));
|
||||
return createWhileStatement(
|
||||
terminal.loc,
|
||||
test,
|
||||
codegenBlock(cx, terminal.loop),
|
||||
);
|
||||
}
|
||||
case 'label': {
|
||||
return codegenBlock(cx, terminal.block);
|
||||
@@ -1205,7 +1309,8 @@ function codegenTerminal(
|
||||
catchParam = convertIdentifier(terminal.handlerBinding.identifier);
|
||||
cx.temp.set(terminal.handlerBinding.identifier.declarationId, null);
|
||||
}
|
||||
return t.tryStatement(
|
||||
return createTryStatement(
|
||||
terminal.loc,
|
||||
codegenBlock(cx, terminal.block),
|
||||
t.catchClause(catchParam, codegenBlock(cx, terminal.handler)),
|
||||
);
|
||||
@@ -1272,7 +1377,13 @@ function codegenInstructionNullable(
|
||||
reason:
|
||||
'Encountered a destructuring operation where some identifiers are already declared (reassignments) but others are not (declarations)',
|
||||
description: null,
|
||||
loc: instr.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instr.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
} else if (hasReassign) {
|
||||
@@ -1285,7 +1396,13 @@ function codegenInstructionNullable(
|
||||
CompilerError.invariant(instr.lvalue === null, {
|
||||
reason: `Const declaration cannot be referenced as an expression`,
|
||||
description: null,
|
||||
loc: instr.value.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instr.value.loc,
|
||||
message: `this is ${kind}`,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return createVariableDeclaration(instr.loc, 'const', [
|
||||
@@ -1296,20 +1413,38 @@ function codegenInstructionNullable(
|
||||
CompilerError.invariant(instr.lvalue === null, {
|
||||
reason: `Function declaration cannot be referenced as an expression`,
|
||||
description: null,
|
||||
loc: instr.value.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instr.value.loc,
|
||||
message: `this is ${kind}`,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
const genLvalue = codegenLValue(cx, lvalue);
|
||||
CompilerError.invariant(genLvalue.type === 'Identifier', {
|
||||
reason: 'Expected an identifier as a function declaration lvalue',
|
||||
description: null,
|
||||
loc: instr.value.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instr.value.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
CompilerError.invariant(value?.type === 'FunctionExpression', {
|
||||
reason: 'Expected a function as a function declaration value',
|
||||
description: `Got ${value == null ? String(value) : value.type} at ${printInstruction(instr)}`,
|
||||
loc: instr.value.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instr.value.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return createFunctionDeclaration(
|
||||
@@ -1325,7 +1460,13 @@ function codegenInstructionNullable(
|
||||
CompilerError.invariant(instr.lvalue === null, {
|
||||
reason: `Const declaration cannot be referenced as an expression`,
|
||||
description: null,
|
||||
loc: instr.value.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instr.value.loc,
|
||||
message: 'this is const',
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return createVariableDeclaration(instr.loc, 'let', [
|
||||
@@ -1336,7 +1477,13 @@ function codegenInstructionNullable(
|
||||
CompilerError.invariant(value !== null, {
|
||||
reason: 'Expected a value for reassignment',
|
||||
description: null,
|
||||
loc: instr.value.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instr.value.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
const expr = t.assignmentExpression(
|
||||
@@ -1369,7 +1516,13 @@ function codegenInstructionNullable(
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Expected ${kind} to have been pruned in PruneHoistedContexts`,
|
||||
description: null,
|
||||
loc: instr.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instr.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
@@ -1387,7 +1540,14 @@ function codegenInstructionNullable(
|
||||
} else if (instr.value.kind === 'ObjectMethod') {
|
||||
CompilerError.invariant(instr.lvalue, {
|
||||
reason: 'Expected object methods to have a temp lvalue',
|
||||
loc: null,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
cx.objectMethods.set(instr.lvalue.identifier.id, instr.value);
|
||||
@@ -1434,7 +1594,13 @@ function codegenForInit(
|
||||
(instr.kind === 'let' || instr.kind === 'const'),
|
||||
{
|
||||
reason: 'Expected a variable declaration',
|
||||
loc: init.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: init.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
description: `Got ${instr.type}`,
|
||||
suggestions: null,
|
||||
},
|
||||
@@ -1447,7 +1613,13 @@ function codegenForInit(
|
||||
});
|
||||
CompilerError.invariant(declarators.length > 0, {
|
||||
reason: 'Expected a variable declaration',
|
||||
loc: init.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: init.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
description: null,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1543,7 +1715,13 @@ const createExpressionStatement = withLoc(t.expressionStatement);
|
||||
const _createLabelledStatement = withLoc(t.labeledStatement);
|
||||
const createVariableDeclaration = withLoc(t.variableDeclaration);
|
||||
const createFunctionDeclaration = withLoc(t.functionDeclaration);
|
||||
const _createWhileStatement = withLoc(t.whileStatement);
|
||||
const createWhileStatement = withLoc(t.whileStatement);
|
||||
const createDoWhileStatement = withLoc(t.doWhileStatement);
|
||||
const createSwitchStatement = withLoc(t.switchStatement);
|
||||
const createIfStatement = withLoc(t.ifStatement);
|
||||
const createForStatement = withLoc(t.forStatement);
|
||||
const createForOfStatement = withLoc(t.forOfStatement);
|
||||
const createForInStatement = withLoc(t.forInStatement);
|
||||
const createTaggedTemplateExpression = withLoc(t.taggedTemplateExpression);
|
||||
const createLogicalExpression = withLoc(t.logicalExpression);
|
||||
const createSequenceExpression = withLoc(t.sequenceExpression);
|
||||
@@ -1558,6 +1736,10 @@ const createJsxText = withLoc(t.jsxText);
|
||||
const createJsxClosingElement = withLoc(t.jsxClosingElement);
|
||||
const createJsxOpeningElement = withLoc(t.jsxOpeningElement);
|
||||
const createStringLiteral = withLoc(t.stringLiteral);
|
||||
const createThrowStatement = withLoc(t.throwStatement);
|
||||
const createTryStatement = withLoc(t.tryStatement);
|
||||
const createBreakStatement = withLoc(t.breakStatement);
|
||||
const createContinueStatement = withLoc(t.continueStatement);
|
||||
|
||||
function createHookGuard(
|
||||
guard: ExternalFunction,
|
||||
@@ -1768,7 +1950,13 @@ function codegenInstructionValue(
|
||||
CompilerError.invariant(t.isExpression(optionalValue.callee), {
|
||||
reason: 'v8 intrinsics are validated during lowering',
|
||||
description: null,
|
||||
loc: optionalValue.callee.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: optionalValue.callee.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
value = t.optionalCallExpression(
|
||||
@@ -1784,7 +1972,13 @@ function codegenInstructionValue(
|
||||
CompilerError.invariant(t.isExpression(property), {
|
||||
reason: 'Private names are validated during lowering',
|
||||
description: null,
|
||||
loc: property.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: property.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
value = t.optionalMemberExpression(
|
||||
@@ -1800,7 +1994,13 @@ function codegenInstructionValue(
|
||||
reason:
|
||||
'Expected an optional value to resolve to a call expression or member expression',
|
||||
description: `Got a \`${optionalValue.type}\``,
|
||||
loc: instrValue.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instrValue.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
@@ -1816,10 +2016,15 @@ function codegenInstructionValue(
|
||||
t.isOptionalMemberExpression(memberExpr),
|
||||
{
|
||||
reason:
|
||||
'[Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. ' +
|
||||
`Got a \`${memberExpr.type}\``,
|
||||
'[Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression',
|
||||
description: null,
|
||||
loc: memberExpr.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: memberExpr.loc ?? null,
|
||||
message: `Got: '${memberExpr.type}'`,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
@@ -1833,7 +2038,13 @@ function codegenInstructionValue(
|
||||
'[Codegen] Internal error: Forget should always generate MethodCall::property ' +
|
||||
'as a MemberExpression of MethodCall::receiver',
|
||||
description: null,
|
||||
loc: memberExpr.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: memberExpr.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
@@ -1878,7 +2089,14 @@ function codegenInstructionValue(
|
||||
const method = cx.objectMethods.get(property.place.identifier.id);
|
||||
CompilerError.invariant(method, {
|
||||
reason: 'Expected ObjectMethod instruction',
|
||||
loc: null,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
const loweredFunc = method.loweredFunc;
|
||||
@@ -1949,7 +2167,13 @@ function codegenInstructionValue(
|
||||
CompilerError.invariant(tagValue.type === 'StringLiteral', {
|
||||
reason: `Expected JSX tag to be an identifier or string, got \`${tagValue.type}\``,
|
||||
description: null,
|
||||
loc: tagValue.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: tagValue.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
if (tagValue.value.indexOf(':') >= 0) {
|
||||
@@ -1969,7 +2193,13 @@ function codegenInstructionValue(
|
||||
SINGLE_CHILD_FBT_TAGS.has(tagValue.value)
|
||||
) {
|
||||
CompilerError.invariant(instrValue.children != null, {
|
||||
loc: instrValue.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instrValue.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
reason: 'Expected fbt element to have children',
|
||||
suggestions: null,
|
||||
description: null,
|
||||
@@ -2098,6 +2328,7 @@ function codegenInstructionValue(
|
||||
),
|
||||
reactiveFunction,
|
||||
).unwrap();
|
||||
|
||||
if (instrValue.type === 'ArrowFunctionExpression') {
|
||||
let body: t.BlockStatement | t.Expression = fn.body;
|
||||
if (body.body.length === 1 && loweredFunc.directives.length == 0) {
|
||||
@@ -2109,14 +2340,26 @@ function codegenInstructionValue(
|
||||
value = t.arrowFunctionExpression(fn.params, body, fn.async);
|
||||
} else {
|
||||
value = t.functionExpression(
|
||||
fn.id ??
|
||||
(instrValue.name != null ? t.identifier(instrValue.name) : null),
|
||||
instrValue.name != null ? t.identifier(instrValue.name) : null,
|
||||
fn.params,
|
||||
fn.body,
|
||||
fn.generator,
|
||||
fn.async,
|
||||
);
|
||||
}
|
||||
if (
|
||||
cx.env.config.enableNameAnonymousFunctions &&
|
||||
instrValue.name == null &&
|
||||
instrValue.nameHint != null
|
||||
) {
|
||||
const name = instrValue.nameHint;
|
||||
value = t.memberExpression(
|
||||
t.objectExpression([t.objectProperty(t.stringLiteral(name), value)]),
|
||||
t.stringLiteral(name),
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'TaggedTemplateExpression': {
|
||||
@@ -2184,7 +2427,6 @@ function codegenInstructionValue(
|
||||
reason: `(CodegenReactiveFunction::codegenInstructionValue) Cannot declare variables in a value block, tried to declare '${
|
||||
(declarator.id as t.Identifier).name
|
||||
}'`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: declarator.loc ?? null,
|
||||
suggestions: null,
|
||||
@@ -2193,7 +2435,6 @@ function codegenInstructionValue(
|
||||
} else {
|
||||
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,
|
||||
@@ -2273,7 +2514,13 @@ function codegenInstructionValue(
|
||||
{
|
||||
reason: `Unexpected StoreLocal in codegenInstructionValue`,
|
||||
description: null,
|
||||
loc: instrValue.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instrValue.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
@@ -2303,7 +2550,13 @@ function codegenInstructionValue(
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected ${instrValue.kind} in codegenInstructionValue`,
|
||||
description: null,
|
||||
loc: instrValue.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instrValue.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
@@ -2314,6 +2567,9 @@ function codegenInstructionValue(
|
||||
);
|
||||
}
|
||||
}
|
||||
if (instrValue.loc != null && instrValue.loc != GeneratedSource) {
|
||||
value.loc = instrValue.loc;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -2449,7 +2705,13 @@ function convertMemberExpressionToJsx(
|
||||
CompilerError.invariant(expr.property.type === 'Identifier', {
|
||||
reason: 'Expected JSX member expression property to be a string',
|
||||
description: null,
|
||||
loc: expr.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: expr.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
const property = t.jsxIdentifier(expr.property.name);
|
||||
@@ -2460,7 +2722,13 @@ function convertMemberExpressionToJsx(
|
||||
reason:
|
||||
'Expected JSX member expression to be an identifier or nested member expression',
|
||||
description: null,
|
||||
loc: expr.object.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: expr.object.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
const object = convertMemberExpressionToJsx(expr.object);
|
||||
@@ -2484,7 +2752,13 @@ function codegenObjectPropertyKey(
|
||||
CompilerError.invariant(t.isExpression(expr), {
|
||||
reason: 'Expected object property key to be an expression',
|
||||
description: null,
|
||||
loc: key.name.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: key.name.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return expr;
|
||||
@@ -2631,7 +2905,13 @@ function codegenPlace(cx: Context, place: Place): t.Expression | t.JSXText {
|
||||
description: `Value for '${printPlace(
|
||||
place,
|
||||
)}' was not set in the codegen context`,
|
||||
loc: place.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: place.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
const identifier = convertIdentifier(place.identifier);
|
||||
@@ -2644,7 +2924,13 @@ function convertIdentifier(identifier: Identifier): t.Identifier {
|
||||
identifier.name !== null && identifier.name.kind === 'named',
|
||||
{
|
||||
reason: `Expected temporaries to be promoted to named identifiers in an earlier pass`,
|
||||
loc: GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
description: `identifier ${identifier.id} is unnamed`,
|
||||
suggestions: null,
|
||||
},
|
||||
@@ -2660,7 +2946,14 @@ function compareScopeDependency(
|
||||
a.identifier.name?.kind === 'named' && b.identifier.name?.kind === 'named',
|
||||
{
|
||||
reason: '[Codegen] Expected named identifier for dependency',
|
||||
loc: a.identifier.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: a.identifier.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
const aName = [
|
||||
@@ -2684,7 +2977,14 @@ function compareScopeDeclaration(
|
||||
a.identifier.name?.kind === 'named' && b.identifier.name?.kind === 'named',
|
||||
{
|
||||
reason: '[Codegen] Expected named identifier for declaration',
|
||||
loc: a.identifier.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: a.identifier.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
const aName = a.identifier.name.value;
|
||||
|
||||
@@ -75,7 +75,13 @@ export function flattenScopesWithHooksOrUseHIR(fn: HIRFunction): void {
|
||||
CompilerError.invariant(terminal.kind === 'scope', {
|
||||
reason: `Expected block to have a scope terminal`,
|
||||
description: `Expected block bb${block.id} to end in a scope terminal`,
|
||||
loc: terminal.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
const body = fn.body.blocks.get(terminal.block)!;
|
||||
if (
|
||||
|
||||
@@ -162,7 +162,13 @@ export function inferReactiveScopeVariables(fn: HIRFunction): void {
|
||||
});
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Invalid mutable range for scope`,
|
||||
loc: GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
description: `Scope @${scope.id} has range [${scope.range.start}:${
|
||||
scope.range.end
|
||||
}] but the valid range is [1:${maxInstruction + 1}]`,
|
||||
|
||||
@@ -159,11 +159,17 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
|
||||
const merged: Array<MergedScope> = [];
|
||||
function reset(): void {
|
||||
CompilerError.invariant(current !== null, {
|
||||
loc: null,
|
||||
reason:
|
||||
'MergeConsecutiveScopes: expected current scope to be non-null if reset()',
|
||||
suggestions: null,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
if (current.to > current.from + 1) {
|
||||
merged.push(current);
|
||||
@@ -375,10 +381,16 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
|
||||
}
|
||||
const mergedScope = block[entry.from]!;
|
||||
CompilerError.invariant(mergedScope.kind === 'scope', {
|
||||
loc: null,
|
||||
reason:
|
||||
'MergeConsecutiveScopes: Expected scope starting index to be a scope',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
nextInstructions.push(mergedScope);
|
||||
|
||||
@@ -323,7 +323,13 @@ function writeTerminal(writer: Writer, terminal: ReactiveTerminal): void {
|
||||
CompilerError.invariant(block != null, {
|
||||
reason: 'Expected case to have a block',
|
||||
description: null,
|
||||
loc: case_.test?.loc ?? null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: case_.test?.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
writeReactiveInstructions(writer, block);
|
||||
|
||||
@@ -290,7 +290,14 @@ class PromoteInterposedTemporaries extends ReactiveFunctionVisitor<InterState> {
|
||||
CompilerError.invariant(lval.identifier.name != null, {
|
||||
reason:
|
||||
'PromoteInterposedTemporaries: Assignment targets not expected to be temporaries',
|
||||
loc: instruction.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instruction.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -454,7 +461,13 @@ function promoteIdentifier(identifier: Identifier, state: State): void {
|
||||
reason:
|
||||
'promoteTemporary: Expected to be called only for temporary variables',
|
||||
description: null,
|
||||
loc: GeneratedSource,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
if (state.tags.has(identifier.declarationId)) {
|
||||
|
||||
@@ -145,7 +145,14 @@ class Visitor extends ReactiveFunctionTransform<VisitorState> {
|
||||
if (maybeHoistedFn != null) {
|
||||
CompilerError.invariant(maybeHoistedFn.kind === 'func', {
|
||||
reason: '[PruneHoistedContexts] Unexpected hoisted function',
|
||||
loc: instruction.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instruction.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
maybeHoistedFn.definition = instruction.value.lvalue.place;
|
||||
/**
|
||||
|
||||
@@ -196,7 +196,14 @@ class Visitor extends ReactiveFunctionVisitor<CreateUpdate> {
|
||||
): void {
|
||||
CompilerError.invariant(state !== 'Create', {
|
||||
reason: "Visiting a terminal statement with state 'Create'",
|
||||
loc: stmt.terminal.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: stmt.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
super.visitTerminal(stmt, state);
|
||||
}
|
||||
|
||||
@@ -264,7 +264,13 @@ class State {
|
||||
CompilerError.invariant(identifierNode !== undefined, {
|
||||
reason: 'Expected identifier to be initialized',
|
||||
description: `[${id}] operand=${printPlace(place)} for identifier declaration ${identifier}`,
|
||||
loc: place.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: place.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
identifierNode.scopes.add(scope.id);
|
||||
@@ -286,7 +292,13 @@ function computeMemoizedIdentifiers(state: State): Set<DeclarationId> {
|
||||
CompilerError.invariant(node !== undefined, {
|
||||
reason: `Expected a node for all identifiers, none found for \`${id}\``,
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
if (node.seen) {
|
||||
@@ -328,7 +340,13 @@ function computeMemoizedIdentifiers(state: State): Set<DeclarationId> {
|
||||
CompilerError.invariant(node !== undefined, {
|
||||
reason: 'Expected a node for all scopes',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
if (node.seen) {
|
||||
@@ -977,7 +995,13 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
|
||||
CompilerError.invariant(identifierNode !== undefined, {
|
||||
reason: 'Expected identifier to be initialized',
|
||||
description: null,
|
||||
loc: stmt.terminal.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: stmt.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
for (const scope of scopes) {
|
||||
@@ -1002,7 +1026,13 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
|
||||
CompilerError.invariant(identifierNode !== undefined, {
|
||||
reason: 'Expected identifier to be initialized',
|
||||
description: null,
|
||||
loc: reassignment.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: reassignment.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
for (const scope of scopes) {
|
||||
|
||||
@@ -186,7 +186,13 @@ class Scopes {
|
||||
CompilerError.invariant(last === next, {
|
||||
reason: 'Mismatch push/pop calls',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -97,7 +97,13 @@ export function eliminateRedundantPhi(
|
||||
CompilerError.invariant(same !== null, {
|
||||
reason: 'Expected phis to be non-empty',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
rewrites.set(phi.place.identifier, same);
|
||||
@@ -149,12 +155,26 @@ export function eliminateRedundantPhi(
|
||||
for (const phi of block.phis) {
|
||||
CompilerError.invariant(!rewrites.has(phi.place.identifier), {
|
||||
reason: '[EliminateRedundantPhis]: rewrite not complete',
|
||||
loc: phi.place.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: phi.place.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
for (const [, operand] of phi.operands) {
|
||||
CompilerError.invariant(!rewrites.has(operand.identifier), {
|
||||
reason: '[EliminateRedundantPhis]: rewrite not complete',
|
||||
loc: phi.place.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: phi.place.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,13 @@ class SSABuilder {
|
||||
CompilerError.invariant(this.#current !== null, {
|
||||
reason: 'we need to be in a block to access state!',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
return this.#states.get(this.#current)!;
|
||||
@@ -253,7 +259,13 @@ function enterSSAImpl(
|
||||
CompilerError.invariant(!visitedBlocks.has(block), {
|
||||
reason: `found a cycle! visiting bb${block.id} again`,
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
@@ -266,7 +278,13 @@ function enterSSAImpl(
|
||||
CompilerError.invariant(func.context.length === 0, {
|
||||
reason: `Expected function context to be empty for outer function declarations`,
|
||||
description: null,
|
||||
loc: func.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: func.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
func.params = func.params.map(param => {
|
||||
@@ -295,7 +313,13 @@ function enterSSAImpl(
|
||||
reason:
|
||||
'Expected function expression entry block to have zero predecessors',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
entry.preds.add(blockId);
|
||||
|
||||
@@ -59,7 +59,13 @@ export function rewriteInstructionKindsBasedOnReassignment(
|
||||
{
|
||||
reason: `Expected variable not to be defined prior to declaration`,
|
||||
description: `${printPlace(lvalue.place)} was already defined`,
|
||||
loc: lvalue.place.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: lvalue.place.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
declarations.set(lvalue.place.identifier.declarationId, lvalue);
|
||||
@@ -77,7 +83,13 @@ export function rewriteInstructionKindsBasedOnReassignment(
|
||||
{
|
||||
reason: `Expected variable not to be defined prior to declaration`,
|
||||
description: `${printPlace(lvalue.place)} was already defined`,
|
||||
loc: lvalue.place.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: lvalue.place.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
declarations.set(lvalue.place.identifier.declarationId, lvalue);
|
||||
@@ -101,7 +113,13 @@ export function rewriteInstructionKindsBasedOnReassignment(
|
||||
description: `other places were \`${kind}\` but '${printPlace(
|
||||
place,
|
||||
)}' is const`,
|
||||
loc: place.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: place.loc,
|
||||
message: 'Expected consistent kind for destructuring',
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
@@ -114,7 +132,13 @@ export function rewriteInstructionKindsBasedOnReassignment(
|
||||
CompilerError.invariant(block.kind !== 'value', {
|
||||
reason: `TODO: Handle reassignment in a value block where the original declaration was removed by dead code elimination (DCE)`,
|
||||
description: null,
|
||||
loc: place.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: place.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
declarations.set(place.identifier.declarationId, lvalue);
|
||||
@@ -125,7 +149,13 @@ export function rewriteInstructionKindsBasedOnReassignment(
|
||||
description: `Other places were \`${kind}\` but '${printPlace(
|
||||
place,
|
||||
)}' is const`,
|
||||
loc: place.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: place.loc,
|
||||
message: 'Expected consistent kind for destructuring',
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
@@ -138,7 +168,13 @@ export function rewriteInstructionKindsBasedOnReassignment(
|
||||
description: `Other places were \`${kind}\` but '${printPlace(
|
||||
place,
|
||||
)}' is reassigned`,
|
||||
loc: place.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: place.loc,
|
||||
message: 'Expected consistent kind for destructuring',
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
@@ -150,7 +186,13 @@ export function rewriteInstructionKindsBasedOnReassignment(
|
||||
CompilerError.invariant(kind !== null, {
|
||||
reason: 'Expected at least one operand',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
lvalue.kind = kind;
|
||||
@@ -163,7 +205,13 @@ export function rewriteInstructionKindsBasedOnReassignment(
|
||||
CompilerError.invariant(declaration !== undefined, {
|
||||
reason: `Expected variable to have been defined`,
|
||||
description: `No declaration for ${printPlace(lvalue)}`,
|
||||
loc: lvalue.loc,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: lvalue.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
declaration.kind = InstructionKind.Let;
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* 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 {
|
||||
FunctionExpression,
|
||||
getHookKind,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
} from '../HIR';
|
||||
|
||||
export function nameAnonymousFunctions(fn: HIRFunction): void {
|
||||
if (fn.id == null) {
|
||||
return;
|
||||
}
|
||||
const parentName = fn.id;
|
||||
const functions = nameAnonymousFunctionsImpl(fn);
|
||||
function visit(node: Node, prefix: string): void {
|
||||
if (node.generatedName != null) {
|
||||
/**
|
||||
* Note that we don't generate a name for functions that already had one,
|
||||
* so we'll only add the prefix to anonymous functions regardless of
|
||||
* nesting depth.
|
||||
*/
|
||||
const name = `${prefix}${node.generatedName}]`;
|
||||
node.fn.nameHint = name;
|
||||
node.fn.loweredFunc.func.nameHint = name;
|
||||
}
|
||||
/**
|
||||
* Whether or not we generated a name for the function at this node,
|
||||
* traverse into its nested functions to assign them names
|
||||
*/
|
||||
const nextPrefix = `${prefix}${node.generatedName ?? node.fn.name ?? '<anonymous>'} > `;
|
||||
for (const inner of node.inner) {
|
||||
visit(inner, nextPrefix);
|
||||
}
|
||||
}
|
||||
for (const node of functions) {
|
||||
visit(node, `${parentName}[`);
|
||||
}
|
||||
}
|
||||
|
||||
type Node = {
|
||||
fn: FunctionExpression;
|
||||
generatedName: string | null;
|
||||
inner: Array<Node>;
|
||||
};
|
||||
|
||||
function nameAnonymousFunctionsImpl(fn: HIRFunction): Array<Node> {
|
||||
// Functions that we track to generate names for
|
||||
const functions: Map<IdentifierId, Node> = new Map();
|
||||
// Tracks temporaries that read from variables/globals/properties
|
||||
const names: Map<IdentifierId, string> = new Map();
|
||||
// Tracks all function nodes to bubble up for later renaming
|
||||
const nodes: Array<Node> = [];
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
switch (value.kind) {
|
||||
case 'LoadGlobal': {
|
||||
names.set(lvalue.identifier.id, value.binding.name);
|
||||
break;
|
||||
}
|
||||
case 'LoadContext':
|
||||
case 'LoadLocal': {
|
||||
const name = value.place.identifier.name;
|
||||
if (name != null && name.kind === 'named') {
|
||||
names.set(lvalue.identifier.id, name.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'PropertyLoad': {
|
||||
const objectName = names.get(value.object.identifier.id);
|
||||
if (objectName != null) {
|
||||
names.set(
|
||||
lvalue.identifier.id,
|
||||
`${objectName}.${String(value.property)}`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'FunctionExpression': {
|
||||
const inner = nameAnonymousFunctionsImpl(value.loweredFunc.func);
|
||||
const node: Node = {
|
||||
fn: value,
|
||||
generatedName: null,
|
||||
inner,
|
||||
};
|
||||
/**
|
||||
* Bubble-up all functions, even if they're named, so that we can
|
||||
* later generate names for any inner anonymous functions
|
||||
*/
|
||||
nodes.push(node);
|
||||
if (value.name == null) {
|
||||
// but only generate names for anonymous functions
|
||||
functions.set(lvalue.identifier.id, node);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'StoreContext':
|
||||
case 'StoreLocal': {
|
||||
const node = functions.get(value.value.identifier.id);
|
||||
const variableName = value.lvalue.place.identifier.name;
|
||||
if (
|
||||
node != null &&
|
||||
variableName != null &&
|
||||
variableName.kind === 'named'
|
||||
) {
|
||||
node.generatedName = variableName.value;
|
||||
functions.delete(value.value.identifier.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
const callee =
|
||||
value.kind === 'MethodCall' ? value.property : value.callee;
|
||||
const hookKind = getHookKind(fn.env, callee.identifier);
|
||||
let calleeName: string | null = null;
|
||||
if (hookKind != null && hookKind !== 'Custom') {
|
||||
calleeName = hookKind;
|
||||
} else {
|
||||
calleeName = names.get(callee.identifier.id) ?? '(anonymous)';
|
||||
}
|
||||
let fnArgCount = 0;
|
||||
for (const arg of value.args) {
|
||||
if (arg.kind === 'Identifier' && functions.has(arg.identifier.id)) {
|
||||
fnArgCount++;
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < value.args.length; i++) {
|
||||
const arg = value.args[i]!;
|
||||
if (arg.kind === 'Spread') {
|
||||
continue;
|
||||
}
|
||||
const node = functions.get(arg.identifier.id);
|
||||
if (node != null) {
|
||||
const generatedName =
|
||||
fnArgCount > 1 ? `${calleeName}(arg${i})` : `${calleeName}()`;
|
||||
node.generatedName = generatedName;
|
||||
functions.delete(arg.identifier.id);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'JsxExpression': {
|
||||
for (const attr of value.props) {
|
||||
if (attr.kind === 'JsxSpreadAttribute') {
|
||||
continue;
|
||||
}
|
||||
const node = functions.get(attr.place.identifier.id);
|
||||
if (node != null) {
|
||||
const elementName =
|
||||
value.tag.kind === 'BuiltinTag'
|
||||
? value.tag.name
|
||||
: (names.get(value.tag.identifier.id) ?? null);
|
||||
const propName =
|
||||
elementName == null
|
||||
? attr.name
|
||||
: `<${elementName}>.${attr.name}`;
|
||||
node.generatedName = `${propName}`;
|
||||
functions.delete(attr.place.identifier.id);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
@@ -5,12 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
CompilerError,
|
||||
CompilerErrorDetailOptions,
|
||||
ErrorSeverity,
|
||||
SourceLocation,
|
||||
} from '..';
|
||||
import {CompilerError, CompilerErrorDetailOptions, SourceLocation} from '..';
|
||||
import {
|
||||
ArrayExpression,
|
||||
CallExpression,
|
||||
@@ -133,7 +128,6 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
|
||||
context.pushError({
|
||||
loc: value.loc,
|
||||
description: null,
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: ErrorCategory.Invariant,
|
||||
reason: '[InsertFire] No LoadGlobal found for useEffect call',
|
||||
suggestions: null,
|
||||
@@ -180,7 +174,6 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
|
||||
loc: value.args[1].loc,
|
||||
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,
|
||||
@@ -191,7 +184,6 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
|
||||
loc: value.args[1].place.loc,
|
||||
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,
|
||||
@@ -226,7 +218,6 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
|
||||
context.pushError({
|
||||
loc: value.loc,
|
||||
description: null,
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: ErrorCategory.Invariant,
|
||||
reason:
|
||||
'[InsertFire] No loadLocal found for fire call argument',
|
||||
@@ -250,7 +241,6 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
|
||||
loc: value.loc,
|
||||
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,
|
||||
@@ -269,7 +259,6 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
|
||||
context.pushError({
|
||||
loc: value.loc,
|
||||
description,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: ErrorCategory.Fire,
|
||||
reason: CANNOT_COMPILE_FIRE,
|
||||
suggestions: null,
|
||||
@@ -401,7 +390,6 @@ function ensureNoRemainingCalleeCaptures(
|
||||
description: `All uses of ${calleeName} must be either used with a fire() call in \
|
||||
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,
|
||||
@@ -420,7 +408,6 @@ function ensureNoMoreFireUses(fn: HIRFunction, context: Context): void {
|
||||
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,
|
||||
});
|
||||
@@ -711,7 +698,7 @@ class Context {
|
||||
}
|
||||
|
||||
hasErrors(): boolean {
|
||||
return this.#errors.hasErrors();
|
||||
return this.#errors.hasAnyErrors();
|
||||
}
|
||||
|
||||
throwIfErrorsFound(): void {
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
BuiltInObjectId,
|
||||
BuiltInPropsId,
|
||||
BuiltInRefValueId,
|
||||
BuiltInSetStateId,
|
||||
BuiltInUseRefId,
|
||||
} from '../HIR/ObjectShape';
|
||||
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
|
||||
@@ -276,9 +277,16 @@ function* generateInstructionTypes(
|
||||
* We should change Hook to a subtype of Function or change unifier logic.
|
||||
* (see https://github.com/facebook/react-forget/pull/1427)
|
||||
*/
|
||||
let shapeId: string | null = null;
|
||||
if (env.config.enableTreatSetIdentifiersAsStateSetters) {
|
||||
const name = getName(names, value.callee.identifier.id);
|
||||
if (name.startsWith('set')) {
|
||||
shapeId = BuiltInSetStateId;
|
||||
}
|
||||
}
|
||||
yield equation(value.callee.identifier.type, {
|
||||
kind: 'Function',
|
||||
shapeId: null,
|
||||
shapeId,
|
||||
return: returnType,
|
||||
isConstructor: false,
|
||||
});
|
||||
@@ -616,7 +624,13 @@ class Unifier {
|
||||
CompilerError.invariant(type.operands.length > 0, {
|
||||
reason: 'there should be at least one operand',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
|
||||
@@ -21,7 +21,13 @@ export default class DisjointSet<T> {
|
||||
CompilerError.invariant(first != null, {
|
||||
reason: 'Expected set to be non-empty',
|
||||
description: null,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
/*
|
||||
|
||||
@@ -164,7 +164,13 @@ function parseConfigPragmaEnvironmentForTest(
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Internal error, could not parse config from pragma string',
|
||||
description: `${fromZodError(config.error)}`,
|
||||
loc: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
@@ -182,11 +188,6 @@ export function parseConfigPragmaForTests(
|
||||
environment?: PartialEnvironmentConfig;
|
||||
},
|
||||
): PluginOptions {
|
||||
const overridePragma = parseConfigPragmaAsString(pragma);
|
||||
if (overridePragma !== '') {
|
||||
return parseConfigStringAsJS(overridePragma, defaults);
|
||||
}
|
||||
|
||||
const environment = parseConfigPragmaEnvironmentForTest(
|
||||
pragma,
|
||||
defaults.environment ?? {},
|
||||
@@ -222,90 +223,3 @@ export function parseConfigPragmaForTests(
|
||||
}
|
||||
return parsePluginOptions(options);
|
||||
}
|
||||
|
||||
export function parseConfigPragmaAsString(pragma: string): string {
|
||||
// Check if it's in JS override format
|
||||
for (const {key, value: val} of splitPragma(pragma)) {
|
||||
if (key === 'OVERRIDE' && val != null) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function parseConfigStringAsJS(
|
||||
configString: string,
|
||||
defaults: {
|
||||
compilationMode: CompilationMode;
|
||||
environment?: PartialEnvironmentConfig;
|
||||
},
|
||||
): PluginOptions {
|
||||
let parsedConfig: any;
|
||||
try {
|
||||
// Parse the JavaScript object literal
|
||||
parsedConfig = new Function(`return ${configString}`)();
|
||||
} catch (error) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Failed to parse config pragma as JavaScript object',
|
||||
description: `Could not parse: ${configString}. Error: ${error}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
console.log('OVERRIDE:', parsedConfig);
|
||||
|
||||
const 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);
|
||||
}
|
||||
|
||||
@@ -106,12 +106,19 @@ function visit(
|
||||
}
|
||||
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Expected all references to a variable to be consistently local or context references`,
|
||||
loc: place.loc,
|
||||
reason:
|
||||
'Expected all references to a variable to be consistently local or context references',
|
||||
description: `Identifier ${printPlace(
|
||||
place,
|
||||
)} is referenced as a ${kind} variable, but was previously referenced as a ${prev} variable`,
|
||||
)} is referenced as a ${kind} variable, but was previously referenced as a ${prev.kind} variable`,
|
||||
suggestions: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: place.loc,
|
||||
message: `this is ${prev.kind}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
CompilerError,
|
||||
CompilerErrorDetail,
|
||||
ErrorCategory,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
|
||||
import {isHookName} from '../HIR/Environment';
|
||||
@@ -129,7 +128,6 @@ export function validateHooksUsage(
|
||||
description: null,
|
||||
reason,
|
||||
loc: place.loc,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
@@ -147,7 +145,6 @@ export function validateHooksUsage(
|
||||
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',
|
||||
loc: place.loc,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
@@ -165,7 +162,6 @@ export function validateHooksUsage(
|
||||
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',
|
||||
loc: place.loc,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
@@ -453,7 +449,6 @@ function visitFunctionExpression(errors: CompilerError, fn: HIRFunction): void {
|
||||
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)',
|
||||
loc: callee.loc,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..';
|
||||
import {CompilerDiagnostic, CompilerError, Effect} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {HIRFunction, IdentifierId, Place} from '../HIR';
|
||||
import {
|
||||
@@ -38,10 +38,9 @@ export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Immutability,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'Cannot reassign variable after render completes',
|
||||
description: `Reassigning ${variable} after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.`,
|
||||
}).withDetail({
|
||||
description: `Reassigning ${variable} after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead`,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: reassignment.loc,
|
||||
message: `Cannot reassign ${variable} after render completes`,
|
||||
@@ -94,11 +93,10 @@ function getContextReassignment(
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Immutability,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'Cannot reassign variable in async function',
|
||||
description:
|
||||
'Reassigning a variable in an async function can cause inconsistent behavior on subsequent renders. Consider using state instead',
|
||||
}).withDetail({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: reassignment.loc,
|
||||
message: `Cannot reassign ${variable}`,
|
||||
@@ -193,7 +191,14 @@ function getContextReassignment(
|
||||
for (const operand of operands) {
|
||||
CompilerError.invariant(operand.effect !== Effect.Unknown, {
|
||||
reason: `Expected effects to be inferred prior to ValidateLocalsNotReassignedAfterRender`,
|
||||
loc: operand.loc,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: operand.loc,
|
||||
message: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
const reassignment = reassigningFunctions.get(
|
||||
operand.identifier.id,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, ErrorSeverity} from '..';
|
||||
import {CompilerError} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
Identifier,
|
||||
@@ -113,7 +113,6 @@ class Visitor extends ReactiveFunctionVisitor<CompilerError> {
|
||||
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,
|
||||
severity: ErrorSeverity.CannotPreserveMemoization,
|
||||
loc: typeof instruction.loc !== 'symbol' ? instruction.loc : null,
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, EnvironmentConfig, ErrorSeverity} from '..';
|
||||
import {CompilerError, EnvironmentConfig} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {HIRFunction, IdentifierId} from '../HIR';
|
||||
import {DEFAULT_GLOBALS} from '../HIR/Globals';
|
||||
@@ -59,7 +59,7 @@ export function validateNoCapitalizedCalls(
|
||||
CompilerError.throwInvalidReact({
|
||||
category: ErrorCategory.CapitalizedCalls,
|
||||
reason,
|
||||
description: `${calleeName} may be a component.`,
|
||||
description: `${calleeName} may be a component`,
|
||||
loc: value.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -82,9 +82,8 @@ export function validateNoCapitalizedCalls(
|
||||
if (propertyName != null) {
|
||||
errors.push({
|
||||
category: ErrorCategory.CapitalizedCalls,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason,
|
||||
description: `${propertyName} may be a component.`,
|
||||
description: `${propertyName} may be a component`,
|
||||
loc: value.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
@@ -5,135 +5,296 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, ErrorSeverity, SourceLocation} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
ArrayExpression,
|
||||
BlockId,
|
||||
FunctionExpression,
|
||||
BasicBlock,
|
||||
GeneratedSource,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
Instruction,
|
||||
isSetStateType,
|
||||
Place,
|
||||
isUseStateType,
|
||||
Effect,
|
||||
isUseEffectHookType,
|
||||
FunctionExpression,
|
||||
BlockId,
|
||||
SourceLocation,
|
||||
CallExpression,
|
||||
} from '../HIR';
|
||||
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
import {
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
type TypeOfValue = 'ignored' | 'fromProps' | 'fromState' | 'fromPropsAndState';
|
||||
|
||||
type DerivationMetadata = {
|
||||
typeOfValue: TypeOfValue;
|
||||
place: Place;
|
||||
sourcesIds: Set<IdentifierId>;
|
||||
};
|
||||
|
||||
type DerivationCache = Map<IdentifierId, DerivationMetadata>;
|
||||
|
||||
type SetStateCallCache = Map<string | undefined | null, Array<Place>>;
|
||||
|
||||
type FunctionExpressionsCache = Map<IdentifierId, FunctionExpression>;
|
||||
|
||||
type DerivedSetStateCall = {
|
||||
value: CallExpression;
|
||||
sourceIds: Set<IdentifierId>;
|
||||
};
|
||||
|
||||
type ErrorMetadata = {
|
||||
derivedComputationDetails: string;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
|
||||
const DERIVE_IN_RENDER_REASON =
|
||||
'You might net need an effect. Derive values in render, not effects.';
|
||||
|
||||
const DERIVE_IN_RENDER_DETAIL_MESSAGE =
|
||||
'This should be computed during render, not in an effect';
|
||||
|
||||
const DERIVE_IN_RENDER_DESCRIPTION =
|
||||
'State derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user';
|
||||
|
||||
/**
|
||||
* Validates that useEffect is not used for derived computations which could/should
|
||||
* be performed in render.
|
||||
*
|
||||
* See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* // 🔴 Avoid: redundant state and unnecessary Effect
|
||||
* const [fullName, setFullName] = useState('');
|
||||
* useEffect(() => {
|
||||
* setFullName(firstName + ' ' + lastName);
|
||||
* }, [firstName, lastName]);
|
||||
* ```
|
||||
*
|
||||
* Instead use:
|
||||
*
|
||||
* ```
|
||||
* // ✅ Good: calculated during rendering
|
||||
* const fullName = firstName + ' ' + lastName;
|
||||
* ```
|
||||
*/
|
||||
export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
|
||||
const functions: Map<IdentifierId, FunctionExpression> = new Map();
|
||||
const locals: Map<IdentifierId, IdentifierId> = new Map();
|
||||
const derivationCache: DerivationCache = new Map();
|
||||
const setStateCallCache: SetStateCallCache = new Map();
|
||||
const effectSetStateCache: SetStateCallCache = new Map();
|
||||
const functionExpressionsCache: FunctionExpressionsCache = new Map();
|
||||
|
||||
const errors = new CompilerError();
|
||||
const stateDerivationErrors: Array<ErrorMetadata> = [];
|
||||
|
||||
parseFNParameters(fn, derivationCache);
|
||||
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
parseBlockPhi(block, derivationCache);
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
if (value.kind === 'LoadLocal') {
|
||||
locals.set(lvalue.identifier.id, value.place.identifier.id);
|
||||
} else if (value.kind === 'ArrayExpression') {
|
||||
candidateDependencies.set(lvalue.identifier.id, value);
|
||||
} else if (value.kind === 'FunctionExpression') {
|
||||
functions.set(lvalue.identifier.id, value);
|
||||
} else if (
|
||||
value.kind === 'CallExpression' ||
|
||||
value.kind === 'MethodCall'
|
||||
parseInstr(
|
||||
instr,
|
||||
derivationCache,
|
||||
setStateCallCache,
|
||||
effectSetStateCache,
|
||||
functionExpressionsCache,
|
||||
stateDerivationErrors,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const compilerError = generateCompilerErrors(stateDerivationErrors);
|
||||
|
||||
if (compilerError.hasErrors()) {
|
||||
throw compilerError;
|
||||
}
|
||||
}
|
||||
|
||||
function parseFNParameters(fn: HIRFunction, derivationCache: DerivationCache) {
|
||||
if (fn.fnType === 'Hook') {
|
||||
for (const param of fn.params) {
|
||||
if (param.kind === 'Identifier') {
|
||||
derivationCache.set(param.identifier.id, {
|
||||
place: param,
|
||||
sourcesIds: new Set([param.identifier.id]),
|
||||
typeOfValue: 'fromProps',
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (fn.fnType === 'Component') {
|
||||
const props = fn.params[0];
|
||||
if (props != null && props.kind === 'Identifier') {
|
||||
derivationCache.set(props.identifier.id, {
|
||||
place: props,
|
||||
sourcesIds: new Set([props.identifier.id]),
|
||||
typeOfValue: 'fromProps',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseBlockPhi(
|
||||
block: BasicBlock,
|
||||
derivationCache: DerivationCache,
|
||||
): void {
|
||||
for (const phi of block.phis) {
|
||||
let typeOfValue: TypeOfValue = 'ignored';
|
||||
let sourcesIds: Set<IdentifierId> = new Set();
|
||||
for (const operand of phi.operands.values()) {
|
||||
const operandMetadata = derivationCache.get(operand.identifier.id);
|
||||
|
||||
if (operandMetadata === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
|
||||
sourcesIds.add(operand.identifier.id);
|
||||
}
|
||||
|
||||
if (typeOfValue !== 'ignored') {
|
||||
addDerivationEntry(phi.place, sourcesIds, typeOfValue, derivationCache);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function joinValue(
|
||||
lvalueType: TypeOfValue,
|
||||
valueType: TypeOfValue,
|
||||
): TypeOfValue {
|
||||
if (lvalueType === 'ignored') return valueType;
|
||||
if (valueType === 'ignored') return lvalueType;
|
||||
if (lvalueType === valueType) return lvalueType;
|
||||
return 'fromPropsAndState';
|
||||
}
|
||||
|
||||
function addDerivationEntry(
|
||||
derivedVar: Place,
|
||||
sourcesIds: Set<IdentifierId>,
|
||||
typeOfValue: TypeOfValue,
|
||||
derivationCache: DerivationCache,
|
||||
): void {
|
||||
let newValue: DerivationMetadata = {
|
||||
place: derivedVar,
|
||||
sourcesIds: new Set(),
|
||||
typeOfValue: typeOfValue ?? 'ignored',
|
||||
};
|
||||
|
||||
if (sourcesIds !== undefined) {
|
||||
for (const id of sourcesIds) {
|
||||
const sourcePlace = derivationCache.get(id)?.place;
|
||||
|
||||
if (sourcePlace === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* If the identifier of the source is a promoted identifier, then
|
||||
* we should set the target as the source.
|
||||
*/
|
||||
if (
|
||||
sourcePlace.identifier.name === null ||
|
||||
sourcePlace.identifier.name?.kind === 'promoted'
|
||||
) {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
if (
|
||||
isUseEffectHookType(callee.identifier) &&
|
||||
value.args.length === 2 &&
|
||||
value.args[0].kind === 'Identifier' &&
|
||||
value.args[1].kind === 'Identifier'
|
||||
) {
|
||||
const effectFunction = functions.get(value.args[0].identifier.id);
|
||||
const deps = candidateDependencies.get(value.args[1].identifier.id);
|
||||
if (
|
||||
effectFunction != null &&
|
||||
deps != null &&
|
||||
deps.elements.length !== 0 &&
|
||||
deps.elements.every(element => element.kind === 'Identifier')
|
||||
) {
|
||||
const dependencies: Array<IdentifierId> = deps.elements.map(dep => {
|
||||
CompilerError.invariant(dep.kind === 'Identifier', {
|
||||
reason: `Dependency is checked as a place above`,
|
||||
loc: value.loc,
|
||||
});
|
||||
return locals.get(dep.identifier.id) ?? dep.identifier.id;
|
||||
});
|
||||
validateEffect(
|
||||
effectFunction.loweredFunc.func,
|
||||
dependencies,
|
||||
errors,
|
||||
);
|
||||
}
|
||||
}
|
||||
newValue.sourcesIds.add(derivedVar.identifier.id);
|
||||
} else {
|
||||
newValue.sourcesIds.add(sourcePlace.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.hasErrors()) {
|
||||
throw errors;
|
||||
|
||||
derivationCache.set(derivedVar.identifier.id, newValue);
|
||||
}
|
||||
|
||||
function parseInstr(
|
||||
instr: Instruction,
|
||||
derivationCache: DerivationCache,
|
||||
setStateCallCache: SetStateCallCache,
|
||||
effectSetStateCache: SetStateCallCache,
|
||||
functionExpressionsCache: FunctionExpressionsCache,
|
||||
stateDerivationErrors: Array<ErrorMetadata>,
|
||||
): void {
|
||||
const {value, lvalue} = instr;
|
||||
|
||||
let typeOfValue: TypeOfValue = 'ignored';
|
||||
const sources: Set<IdentifierId> = new Set();
|
||||
|
||||
// Recursively parse function expressions
|
||||
if (value.kind === 'FunctionExpression') {
|
||||
for (const [, block] of value.loweredFunc.func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
functionExpressionsCache.set(lvalue.identifier.id, value);
|
||||
|
||||
parseInstr(
|
||||
instr,
|
||||
derivationCache,
|
||||
setStateCallCache,
|
||||
effectSetStateCache,
|
||||
functionExpressionsCache,
|
||||
stateDerivationErrors,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Record setState calls
|
||||
else if (
|
||||
value.kind === 'CallExpression' &&
|
||||
isSetStateType(value.callee.identifier)
|
||||
) {
|
||||
addSetStateCallEntry(value.callee, setStateCallCache);
|
||||
} else if (value.kind === 'CallExpression' || value.kind === 'MethodCall') {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
|
||||
// Handle values derived from useState calls
|
||||
if (isUseStateType(lvalue.identifier)) {
|
||||
const stateValueSource = value.args[0];
|
||||
if (stateValueSource.kind === 'Identifier') {
|
||||
sources.add(stateValueSource.identifier.id);
|
||||
}
|
||||
typeOfValue = joinValue(typeOfValue, 'fromState');
|
||||
}
|
||||
// Validate useEffect calls
|
||||
else if (
|
||||
isUseEffectHookType(callee.identifier) &&
|
||||
value.args.length === 2 &&
|
||||
value.args[0].kind === 'Identifier' &&
|
||||
value.args[1].kind === 'Identifier'
|
||||
) {
|
||||
const effectFunction = functionExpressionsCache.get(
|
||||
value.args[0].identifier.id,
|
||||
);
|
||||
|
||||
validateEffect(
|
||||
effectFunction?.loweredFunc.func,
|
||||
effectSetStateCache,
|
||||
derivationCache,
|
||||
stateDerivationErrors,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
parseOperands(instr, derivationCache, typeOfValue, sources);
|
||||
}
|
||||
|
||||
function addSetStateCallEntry(
|
||||
callee: Place,
|
||||
setStateCallCache: SetStateCallCache,
|
||||
) {
|
||||
if (callee.loc === GeneratedSource) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (setStateCallCache.has(callee.loc.identifierName)) {
|
||||
setStateCallCache.get(callee.loc.identifierName)!.push(callee);
|
||||
} else {
|
||||
setStateCallCache.set(callee.loc.identifierName, [callee]);
|
||||
}
|
||||
}
|
||||
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction,
|
||||
effectDeps: Array<IdentifierId>,
|
||||
errors: CompilerError,
|
||||
effectFunction: HIRFunction | undefined,
|
||||
effectSetStateCache: SetStateCallCache,
|
||||
derivationCache: DerivationCache,
|
||||
stateDerivationErrors: Array<ErrorMetadata>,
|
||||
): void {
|
||||
for (const operand of effectFunction.context) {
|
||||
if (isSetStateType(operand.identifier)) {
|
||||
continue;
|
||||
} else if (effectDeps.find(dep => dep === operand.identifier.id) != null) {
|
||||
continue;
|
||||
} else {
|
||||
// Captured something other than the effect dep or setState
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const dep of effectDeps) {
|
||||
if (
|
||||
effectFunction.context.find(operand => operand.identifier.id === dep) ==
|
||||
null
|
||||
) {
|
||||
// effect dep wasn't actually used in the function
|
||||
return;
|
||||
}
|
||||
if (effectFunction === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const seenBlocks: Set<BlockId> = new Set();
|
||||
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
|
||||
for (const dep of effectDeps) {
|
||||
values.set(dep, [dep]);
|
||||
}
|
||||
const effectDerivedSetStateCalls: Array<DerivedSetStateCall> = [];
|
||||
|
||||
const setStateLocations: Array<SourceLocation> = [];
|
||||
for (const block of effectFunction.body.blocks.values()) {
|
||||
for (const pred of block.preds) {
|
||||
if (!seenBlocks.has(pred)) {
|
||||
@@ -141,92 +302,183 @@ function validateEffect(
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const phi of block.phis) {
|
||||
const aggregateDeps: Set<IdentifierId> = new Set();
|
||||
for (const operand of phi.operands.values()) {
|
||||
const deps = values.get(operand.identifier.id);
|
||||
if (deps != null) {
|
||||
for (const dep of deps) {
|
||||
aggregateDeps.add(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (aggregateDeps.size !== 0) {
|
||||
values.set(phi.place.identifier.id, Array.from(aggregateDeps));
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
switch (instr.value.kind) {
|
||||
case 'Primitive':
|
||||
case 'JSXText':
|
||||
case 'LoadGlobal': {
|
||||
break;
|
||||
}
|
||||
case 'LoadLocal': {
|
||||
const deps = values.get(instr.value.place.identifier.id);
|
||||
if (deps != null) {
|
||||
values.set(instr.lvalue.identifier.id, deps);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ComputedLoad':
|
||||
case 'PropertyLoad':
|
||||
case 'BinaryExpression':
|
||||
case 'TemplateLiteral':
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
const aggregateDeps: Set<IdentifierId> = new Set();
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
const deps = values.get(operand.identifier.id);
|
||||
if (deps != null) {
|
||||
for (const dep of deps) {
|
||||
aggregateDeps.add(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (aggregateDeps.size !== 0) {
|
||||
values.set(instr.lvalue.identifier.id, Array.from(aggregateDeps));
|
||||
}
|
||||
const {value} = instr;
|
||||
if (
|
||||
value.kind === 'CallExpression' &&
|
||||
isSetStateType(value.callee.identifier) &&
|
||||
value.args.length === 1 &&
|
||||
value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
addSetStateCallEntry(value.callee, effectSetStateCache);
|
||||
const argMetadata = derivationCache.get(value.args[0].identifier.id);
|
||||
|
||||
if (
|
||||
instr.value.kind === 'CallExpression' &&
|
||||
isSetStateType(instr.value.callee.identifier) &&
|
||||
instr.value.args.length === 1 &&
|
||||
instr.value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
const deps = values.get(instr.value.args[0].identifier.id);
|
||||
if (deps != null && new Set(deps).size === effectDeps.length) {
|
||||
setStateLocations.push(instr.value.callee.loc);
|
||||
} else {
|
||||
// doesn't depend on any deps
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return;
|
||||
if (argMetadata !== undefined) {
|
||||
effectDerivedSetStateCalls.push({
|
||||
value: value,
|
||||
sourceIds: argMetadata.sourcesIds,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
if (values.has(operand.identifier.id)) {
|
||||
//
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
seenBlocks.add(block.id);
|
||||
}
|
||||
|
||||
for (const loc of setStateLocations) {
|
||||
errors.push({
|
||||
category: ErrorCategory.EffectDerivationsOfState,
|
||||
reason:
|
||||
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
|
||||
description: null,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc,
|
||||
suggestions: null,
|
||||
});
|
||||
generateDerivedComputationDetails(
|
||||
effectDerivedSetStateCalls,
|
||||
derivationCache,
|
||||
stateDerivationErrors,
|
||||
);
|
||||
}
|
||||
|
||||
function generateDerivedComputationDetails(
|
||||
effectDerivedSetStateCalls: Array<DerivedSetStateCall>,
|
||||
derivationCache: DerivationCache,
|
||||
stateDerivationErrors: Array<ErrorMetadata>,
|
||||
) {
|
||||
console.log(derivationCache);
|
||||
for (const derivedCall of effectDerivedSetStateCalls) {
|
||||
const arg = derivedCall.value.args[0];
|
||||
if (arg.kind === 'Identifier') {
|
||||
const argMetadata = derivationCache.get(arg.identifier.id);
|
||||
if (argMetadata !== undefined) {
|
||||
const derivationSources: Array<string> = [];
|
||||
|
||||
for (const sourceId of argMetadata.sourcesIds) {
|
||||
const sourceMetadata = derivationCache.get(sourceId);
|
||||
if (sourceMetadata !== undefined) {
|
||||
const sourceName =
|
||||
sourceMetadata.place.identifier.name?.value ||
|
||||
`identifier_${sourceId}`;
|
||||
derivationSources.push(sourceName);
|
||||
}
|
||||
}
|
||||
|
||||
let derivationType: string;
|
||||
switch (argMetadata.typeOfValue) {
|
||||
case 'fromProps':
|
||||
derivationType = 'props';
|
||||
break;
|
||||
case 'fromState':
|
||||
derivationType = 'local state';
|
||||
break;
|
||||
case 'fromPropsAndState':
|
||||
derivationType = 'local state and props';
|
||||
break;
|
||||
default:
|
||||
derivationType = 'unknown source';
|
||||
break;
|
||||
}
|
||||
|
||||
const sourcesList =
|
||||
derivationSources.length > 0
|
||||
? ` [${derivationSources.join(', ')}]`
|
||||
: '';
|
||||
|
||||
const formattedDetails = `State is being derived from ${derivationType}${sourcesList}`;
|
||||
|
||||
stateDerivationErrors.push({
|
||||
derivedComputationDetails: formattedDetails,
|
||||
loc: derivedCall.value.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseOperands(
|
||||
instr: Instruction,
|
||||
derivationCache: DerivationCache,
|
||||
typeOfValue: TypeOfValue,
|
||||
sourceIds: Set<IdentifierId>,
|
||||
) {
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
const operandMetadata = derivationCache.get(operand.identifier.id);
|
||||
|
||||
if (operandMetadata === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
|
||||
for (const id of operandMetadata.sourcesIds) {
|
||||
sourceIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeOfValue === 'ignored') {
|
||||
return;
|
||||
}
|
||||
|
||||
propagateTypeOfValue(instr, sourceIds, typeOfValue, derivationCache);
|
||||
}
|
||||
|
||||
function propagateTypeOfValue(
|
||||
instr: Instruction,
|
||||
sourceIds: Set<IdentifierId>,
|
||||
typeOfValue: TypeOfValue,
|
||||
derivationCache: DerivationCache,
|
||||
): void {
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
addDerivationEntry(lvalue, sourceIds, typeOfValue, derivationCache);
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
switch (operand.effect) {
|
||||
case Effect.Capture:
|
||||
case Effect.Store:
|
||||
case Effect.ConditionallyMutate:
|
||||
case Effect.ConditionallyMutateIterator:
|
||||
case Effect.Mutate: {
|
||||
if (isMutable(instr, operand)) {
|
||||
addDerivationEntry(operand, sourceIds, typeOfValue, derivationCache);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Effect.Freeze:
|
||||
case Effect.Read: {
|
||||
// no-op
|
||||
break;
|
||||
}
|
||||
case Effect.Unknown: {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected unknown effect',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: operand.loc,
|
||||
message: 'Unexpected unknown effect',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
operand.effect,
|
||||
`Unexpected effect kind \`${operand.effect}\``,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generateCompilerErrors(stateDerivationErrors: Array<ErrorMetadata>) {
|
||||
const throwableErrors = new CompilerError();
|
||||
for (const e of stateDerivationErrors) {
|
||||
throwableErrors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
description:
|
||||
DERIVE_IN_RENDER_DESCRIPTION + `\n\n${e.derivedComputationDetails}`,
|
||||
category: ErrorCategory.EffectStateDerivationCalculateInRender,
|
||||
reason: DERIVE_IN_RENDER_REASON,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: e.loc,
|
||||
message: DERIVE_IN_RENDER_DETAIL_MESSAGE,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return throwableErrors;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..';
|
||||
import {CompilerDiagnostic, CompilerError, Effect} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
HIRFunction,
|
||||
@@ -66,16 +66,15 @@ export function validateNoFreezingKnownMutableFunctions(
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Immutability,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'Cannot modify local variables after render completes',
|
||||
description: `This argument is a function which may reassign or mutate ${variable} after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.`,
|
||||
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({
|
||||
.withDetails({
|
||||
kind: 'error',
|
||||
loc: operand.loc,
|
||||
message: `This function may (indirectly) reassign or modify ${variable} after render`,
|
||||
})
|
||||
.withDetail({
|
||||
.withDetails({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `This modifies ${variable}`,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
|
||||
import {CompilerDiagnostic, CompilerError} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {HIRFunction} from '../HIR';
|
||||
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
|
||||
@@ -44,9 +44,8 @@ export function validateNoImpureFunctionsInRender(
|
||||
? `\`${signature.canonicalName}\` is an impure function. `
|
||||
: '') +
|
||||
'Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: callee.loc,
|
||||
message: 'Cannot call impure function',
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
|
||||
import {CompilerDiagnostic, CompilerError} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {BlockId, HIRFunction} from '../HIR';
|
||||
import {Result} from '../Utils/Result';
|
||||
@@ -38,10 +38,9 @@ export function validateNoJSXInTryStatement(
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.ErrorBoundaries,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'Avoid constructing JSX within try/catch',
|
||||
description: `React does not immediately render components when JSX is rendered, so any errors from this component will not be caught by the try/catch. To catch errors in rendering a given component, wrap that component in an error boundary. (https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)`,
|
||||
}).withDetail({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: value.loc,
|
||||
message: 'Avoid constructing JSX within try/catch',
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {
|
||||
BlockId,
|
||||
@@ -58,8 +57,14 @@ function makeRefId(id: number): RefId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected identifier id to be a non-negative integer',
|
||||
description: null,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
return id as RefId;
|
||||
}
|
||||
@@ -192,19 +197,40 @@ function tyEqual(a: RefAccessType, b: RefAccessType): boolean {
|
||||
case 'Guard':
|
||||
CompilerError.invariant(b.kind === 'Guard', {
|
||||
reason: 'Expected ref value',
|
||||
loc: null,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
return a.refId === b.refId;
|
||||
case 'RefValue':
|
||||
CompilerError.invariant(b.kind === 'RefValue', {
|
||||
reason: 'Expected ref value',
|
||||
loc: null,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
return a.loc == b.loc;
|
||||
case 'Structure': {
|
||||
CompilerError.invariant(b.kind === 'Structure', {
|
||||
reason: 'Expected structure',
|
||||
loc: null,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
const fnTypesEqual =
|
||||
(a.fn === null && b.fn === null) ||
|
||||
@@ -243,7 +269,14 @@ function joinRefAccessTypes(...types: Array<RefAccessType>): RefAccessType {
|
||||
a.kind === 'Structure' && b.kind === 'Structure',
|
||||
{
|
||||
reason: 'Expected structure',
|
||||
loc: null,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
const fn =
|
||||
@@ -470,10 +503,9 @@ function validateNoRefAccessInRenderImpl(
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetail({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: callee.loc,
|
||||
message: `This function accesses a ref value`,
|
||||
@@ -607,12 +639,55 @@ function validateNoRefAccessInRenderImpl(
|
||||
case 'StartMemoize':
|
||||
case 'FinishMemoize':
|
||||
break;
|
||||
case 'LoadGlobal': {
|
||||
if (instr.value.binding.name === 'undefined') {
|
||||
env.set(instr.lvalue.identifier.id, {kind: 'Nullable'});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'Primitive': {
|
||||
if (instr.value.value == null) {
|
||||
env.set(instr.lvalue.identifier.id, {kind: 'Nullable'});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'UnaryExpression': {
|
||||
if (instr.value.operator === '!') {
|
||||
const value = env.get(instr.value.value.identifier.id);
|
||||
const refId =
|
||||
value?.kind === 'RefValue' && value.refId != null
|
||||
? value.refId
|
||||
: null;
|
||||
if (refId !== null) {
|
||||
/*
|
||||
* Record an error suggesting the `if (ref.current == null)` pattern,
|
||||
* but also record the lvalue as a guard so that we don't emit a second
|
||||
* error for the write to the ref
|
||||
*/
|
||||
env.set(instr.lvalue.identifier.id, {kind: 'Guard', refId});
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
reason: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
})
|
||||
.withDetails({
|
||||
kind: 'error',
|
||||
loc: instr.value.value.loc,
|
||||
message: `Cannot access ref value during render`,
|
||||
})
|
||||
.withDetails({
|
||||
kind: 'hint',
|
||||
message:
|
||||
'To initialize a ref only once, check that the ref is null with the pattern `if (ref.current == null) { ref.current = ... }`',
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
validateNoRefValueAccess(errors, env, instr.value.value);
|
||||
break;
|
||||
}
|
||||
case 'BinaryExpression': {
|
||||
const left = env.get(instr.value.left.identifier.id);
|
||||
const right = env.get(instr.value.right.identifier.id);
|
||||
@@ -703,14 +778,21 @@ function validateNoRefAccessInRenderImpl(
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.hasErrors()) {
|
||||
if (errors.hasAnyErrors()) {
|
||||
return Err(errors);
|
||||
}
|
||||
}
|
||||
|
||||
CompilerError.invariant(!env.hasChanged(), {
|
||||
reason: 'Ref type environment did not converge',
|
||||
loc: null,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return Ok(
|
||||
@@ -734,10 +816,9 @@ function guardCheck(errors: CompilerError, operand: Place, env: Env): void {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetail({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: operand.loc,
|
||||
message: `Cannot access ref value during render`,
|
||||
@@ -759,10 +840,9 @@ function validateNoRefValueAccess(
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetail({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: (type.kind === 'RefValue' && type.loc) || operand.loc,
|
||||
message: `Cannot access ref value during render`,
|
||||
@@ -786,10 +866,9 @@ function validateNoRefPassedToFunction(
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetail({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: (type.kind === 'RefValue' && type.loc) || loc,
|
||||
message: `Passing a ref to a function may read its value during render`,
|
||||
@@ -809,10 +888,9 @@ function validateNoRefUpdate(
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetail({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: (type.kind === 'RefValue' && type.loc) || loc,
|
||||
message: `Cannot update ref during render`,
|
||||
@@ -831,10 +909,9 @@ function validateNoDirectRefValueAccess(
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetail({
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: type.loc ?? operand.loc,
|
||||
message: `Cannot access ref value during render`,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user