Compare commits
126 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1313e3a73b | ||
|
|
6160773f30 | ||
|
|
eb2f784e75 | ||
|
|
723b25c644 | ||
|
|
bbb7a1fdf7 | ||
|
|
6b344c7c53 | ||
|
|
71b3a03cc9 | ||
|
|
39c6545cef | ||
|
|
613cf80f26 | ||
|
|
ea0c17b095 | ||
|
|
031595d720 | ||
|
|
3cde211b0c | ||
|
|
1d3664665b | ||
|
|
2bcbf254f1 | ||
|
|
aaad0ea055 | ||
|
|
02c80f0d87 | ||
|
|
21272a680f | ||
|
|
1440f4f42d | ||
|
|
f6a4882859 | ||
|
|
b485f7cf64 | ||
|
|
2cfb221937 | ||
|
|
58bdc0bb96 | ||
|
|
bf11d2fb2f | ||
|
|
ec7d9a7249 | ||
|
|
40c7a7f6ca | ||
|
|
3a669170e9 | ||
|
|
a083344699 | ||
|
|
423c44b886 | ||
|
|
f970d5ff32 | ||
|
|
724e7bfb40 | ||
|
|
ef88c588d5 | ||
|
|
dc485c7303 | ||
|
|
c35f6a3041 | ||
|
|
adbc32de32 | ||
|
|
1324e1bb1f | ||
|
|
7f5ea1bf67 | ||
|
|
0e32da71c7 | ||
|
|
2381ecc290 | ||
|
|
5418d8bdc1 | ||
|
|
ed1351c4fb | ||
|
|
93f8593289 | ||
|
|
dc1becd893 | ||
|
|
d8aa94b0f4 | ||
|
|
03ba0c76e1 | ||
|
|
4e00747378 | ||
|
|
7bd8716acd | ||
|
|
7385d1f61a | ||
|
|
85f415e33b | ||
|
|
903366b8b1 | ||
|
|
0fbb9b3683 | ||
|
|
e096403c59 | ||
|
|
1873ad7960 | ||
|
|
77b2f909f6 | ||
|
|
6773248311 | ||
|
|
5747cadf44 | ||
|
|
751edd6e2c | ||
|
|
6cfc9c1ff3 | ||
|
|
e7984651e4 | ||
|
|
5f2b571878 | ||
|
|
56e846921d | ||
|
|
19b71673b1 | ||
|
|
73507ec457 | ||
|
|
03a62b20fd | ||
|
|
b9ec735de2 | ||
|
|
47905a7950 | ||
|
|
7b971c0a55 | ||
|
|
83ea655a0b | ||
|
|
026abeaa5f | ||
|
|
d7215b4970 | ||
|
|
e2ce64acb9 | ||
|
|
34b1567427 | ||
|
|
b467c6e949 | ||
|
|
93d4458fdc | ||
|
|
1d68bce19c | ||
|
|
ead92181bd | ||
|
|
d44659744f | ||
|
|
8454a32f3c | ||
|
|
06fcc8f380 | ||
|
|
91e5c3daf1 | ||
|
|
4b3e662e4c | ||
|
|
3e1b34dc51 | ||
|
|
7568e71854 | ||
|
|
9724e3e66e | ||
|
|
848e0e3a4f | ||
|
|
5c15c1cd34 | ||
|
|
69b4cb8df4 | ||
|
|
a664f5f2ee | ||
|
|
1384ea8230 | ||
|
|
3025aa3964 | ||
|
|
a4eb2dfa6f | ||
|
|
6a8c7fb6f1 | ||
|
|
b65e6fc58b | ||
|
|
c786258422 | ||
|
|
1be3ce9996 | ||
|
|
3b2a398106 | ||
|
|
62ff1e61fc | ||
|
|
0e79784702 | ||
|
|
a2329c10ff | ||
|
|
d3f84a433a | ||
|
|
bc2356176b | ||
|
|
4fdf7cf249 | ||
|
|
614a945d9d | ||
|
|
d6eb735938 | ||
|
|
71753ac90a | ||
|
|
f24d3bbc70 | ||
|
|
85c427d822 | ||
|
|
02bd4458f7 | ||
|
|
0eebd37041 | ||
|
|
74dee8ef64 | ||
|
|
e866b1d1e9 | ||
|
|
19f65ff179 | ||
|
|
26b177bc5e | ||
|
|
056a586928 | ||
|
|
5cc3d49f72 | ||
|
|
289f070d64 | ||
|
|
6a8a8ef326 | ||
|
|
f89ed71ddf | ||
|
|
7d9f876cbc | ||
|
|
df3562dc7f | ||
|
|
b56907db51 | ||
|
|
c825f03067 | ||
|
|
2e68dc76a4 | ||
|
|
ced705d756 | ||
|
|
70b52beca6 | ||
|
|
4a28227960 | ||
|
|
e4a27db283 |
11
.github/workflows/compiler_prereleases.yml
vendored
11
.github/workflows/compiler_prereleases.yml
vendored
@@ -19,6 +19,9 @@ on:
|
||||
tag_version:
|
||||
required: false
|
||||
type: string
|
||||
dry_run:
|
||||
required: false
|
||||
type: boolean
|
||||
secrets:
|
||||
NPM_TOKEN:
|
||||
required: true
|
||||
@@ -55,7 +58,13 @@ jobs:
|
||||
key: compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/yarn.lock') }}
|
||||
- run: yarn install --frozen-lockfile
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- name: Publish packages to npm
|
||||
- if: inputs.dry_run == true
|
||||
name: Publish packages to npm (dry run)
|
||||
run: |
|
||||
cp ./scripts/release/ci-npmrc ~/.npmrc
|
||||
scripts/release/publish.js --frfr --debug --ci --versionName=${{ inputs.version_name }} --tag=${{ inputs.dist_tag }} ${{ inputs.tag_version && format('--tagVersion={0}', inputs.tag_version) || '' }}
|
||||
- if: inputs.dry_run != true
|
||||
name: Publish packages to npm
|
||||
run: |
|
||||
cp ./scripts/release/ci-npmrc ~/.npmrc
|
||||
scripts/release/publish.js --frfr --ci --versionName=${{ inputs.version_name }} --tag=${{ inputs.dist_tag }} ${{ inputs.tag_version && format('--tagVersion={0}', inputs.tag_version) || '' }}
|
||||
|
||||
@@ -17,6 +17,9 @@ on:
|
||||
tag_version:
|
||||
required: false
|
||||
type: string
|
||||
dry_run:
|
||||
required: false
|
||||
type: boolean
|
||||
|
||||
permissions: {}
|
||||
|
||||
@@ -33,5 +36,6 @@ jobs:
|
||||
dist_tag: ${{ inputs.dist_tag }}
|
||||
version_name: ${{ inputs.version_name }}
|
||||
tag_version: ${{ inputs.tag_version }}
|
||||
dry_run: ${{ inputs.dry_run }}
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
@@ -19,5 +19,6 @@ jobs:
|
||||
release_channel: experimental
|
||||
dist_tag: experimental
|
||||
version_name: '0.0.0'
|
||||
dry_run: false
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
4
.github/workflows/runtime_prereleases.yml
vendored
4
.github/workflows/runtime_prereleases.yml
vendored
@@ -82,7 +82,6 @@ jobs:
|
||||
run: |
|
||||
scripts/release/publish.js \
|
||||
--ci \
|
||||
--skipTests \
|
||||
--tags=${{ inputs.dist_tag }} \
|
||||
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry' || '' }}
|
||||
@@ -91,11 +90,10 @@ jobs:
|
||||
run: |
|
||||
scripts/release/publish.js \
|
||||
--ci \
|
||||
--skipTests \
|
||||
--tags=${{ inputs.dist_tag }} \
|
||||
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry' || '' }}
|
||||
- if: '${{ !(inputs.skip_packages && inputs.only_packages) }}'
|
||||
- if: '${{ !inputs.skip_packages && !inputs.only_packages }}'
|
||||
name: 'Publish all packages'
|
||||
run: |
|
||||
scripts/release/publish.js \
|
||||
|
||||
@@ -9,7 +9,7 @@ Read the [React 19.2 release post](https://react.dev/blog/2025/10/01/react-19-2)
|
||||
- [`<Activity>`](https://react.dev/reference/react/Activity): A new API to hide and restore the UI and internal state of its children.
|
||||
- [`useEffectEvent`](https://react.dev/reference/react/useEffectEvent) is a React Hook that lets you extract non-reactive logic into an [Effect Event](https://react.dev/learn/separating-events-from-effects#declaring-an-effect-event).
|
||||
- [`cacheSignal`](https://react.dev/reference/react/cacheSignal) (for RSCs) lets your know when the `cache()` lifetime is over.
|
||||
- [React Performance tracks](https://react.dev/reference/developer-tooling/react-performance-tracks) appear on the Performance panel’s timeline in your browser developer tools
|
||||
- [React Performance tracks](https://react.dev/reference/dev-tools/react-performance-tracks) appear on the Performance panel’s timeline in your browser developer tools
|
||||
|
||||
### New React DOM Features
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ const canaryChannelLabel = 'canary';
|
||||
const rcNumber = 0;
|
||||
|
||||
const stablePackages = {
|
||||
'eslint-plugin-react-hooks': '6.2.0',
|
||||
'eslint-plugin-react-hooks': '7.0.0',
|
||||
'jest-react': '0.18.0',
|
||||
react: ReactVersion,
|
||||
'react-art': ReactVersion,
|
||||
|
||||
@@ -314,6 +314,36 @@ test('disableMemoizationForDebugging flag works as expected', async ({
|
||||
expect(output).toMatchSnapshot('disableMemoizationForDebugging-output.txt');
|
||||
});
|
||||
|
||||
test('error is displayed when source has syntax error', async ({page}) => {
|
||||
const syntaxErrorSource = `function TestComponent(props) {
|
||||
const oops = props.
|
||||
return (
|
||||
<>{oops}</>
|
||||
);
|
||||
}`;
|
||||
const store: Store = {
|
||||
source: syntaxErrorSource,
|
||||
config: defaultConfig,
|
||||
showInternals: false,
|
||||
};
|
||||
const hash = encodeStore(store);
|
||||
await page.goto(`/#${hash}`);
|
||||
await page.waitForFunction(isMonacoLoaded);
|
||||
await expandConfigs(page);
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: 'test-results/08-source-syntax-error.png',
|
||||
});
|
||||
|
||||
const text =
|
||||
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
|
||||
const output = text.join('');
|
||||
|
||||
expect(output.replace(/\s+/g, ' ')).toContain(
|
||||
'Expected identifier to be defined before being used',
|
||||
);
|
||||
});
|
||||
|
||||
TEST_CASE_INPUTS.forEach((t, idx) =>
|
||||
test(`playground compiles: ${t.name}`, async ({page}) => {
|
||||
const store: Store = {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
import {Resizable} from 're-resizable';
|
||||
import React, {
|
||||
useCallback,
|
||||
useId,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
@@ -23,7 +22,6 @@ export default function AccordionWindow(props: {
|
||||
tabsOpen: Set<string>;
|
||||
setTabsOpen: (newTab: Set<string>) => void;
|
||||
changedPasses: Set<string>;
|
||||
isFailure: boolean;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<div className="flex-1 min-w-[550px] sm:min-w-0">
|
||||
@@ -37,7 +35,6 @@ export default function AccordionWindow(props: {
|
||||
tabsOpen={props.tabsOpen}
|
||||
setTabsOpen={props.setTabsOpen}
|
||||
hasChanged={props.changedPasses.has(name)}
|
||||
isFailure={props.isFailure}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -52,7 +49,6 @@ function AccordionWindowItem({
|
||||
tabsOpen,
|
||||
setTabsOpen,
|
||||
hasChanged,
|
||||
isFailure,
|
||||
}: {
|
||||
name: string;
|
||||
tabs: TabsRecord;
|
||||
@@ -62,11 +58,11 @@ function AccordionWindowItem({
|
||||
isFailure: boolean;
|
||||
}): React.ReactElement {
|
||||
const id = useId();
|
||||
const isShow = isFailure ? name === 'Output' : tabsOpen.has(name);
|
||||
const isShow = tabsOpen.has(name);
|
||||
|
||||
const transitionName = `accordion-window-item-${id}`;
|
||||
|
||||
const toggleTabs = () => {
|
||||
const toggleTabs = (): void => {
|
||||
startTransition(() => {
|
||||
addTransitionType(EXPAND_ACCORDION_TRANSITION);
|
||||
const nextState = new Set(tabsOpen);
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
*/
|
||||
|
||||
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 React, {
|
||||
@@ -15,12 +14,12 @@ import React, {
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
startTransition,
|
||||
Activity,
|
||||
} from 'react';
|
||||
import {Resizable} from 're-resizable';
|
||||
import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
import {monacoConfigOptions} from './monacoOptions';
|
||||
import {IconChevron} from '../Icons/IconChevron';
|
||||
import prettyFormat from 'pretty-format';
|
||||
import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes';
|
||||
|
||||
// @ts-expect-error - webpack asset/source loader handles .d.ts files as strings
|
||||
@@ -29,19 +28,15 @@ import compilerTypeDefs from 'babel-plugin-react-compiler/dist/index.d.ts';
|
||||
loader.config({monaco});
|
||||
|
||||
export default function ConfigEditor({
|
||||
appliedOptions,
|
||||
formattedAppliedConfig,
|
||||
}: {
|
||||
appliedOptions: PluginOptions | null;
|
||||
formattedAppliedConfig: string;
|
||||
}): React.ReactElement {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
// TODO: Use <Activity> when it is compatible with Monaco: https://github.com/suren-atoyan/monaco-react/issues/753
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: isExpanded ? 'block' : 'none',
|
||||
}}>
|
||||
<Activity mode={isExpanded ? 'visible' : 'hidden'}>
|
||||
<ExpandedEditor
|
||||
onToggle={() => {
|
||||
startTransition(() => {
|
||||
@@ -49,13 +44,10 @@ export default function ConfigEditor({
|
||||
setIsExpanded(false);
|
||||
});
|
||||
}}
|
||||
appliedOptions={appliedOptions}
|
||||
formattedAppliedConfig={formattedAppliedConfig}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: !isExpanded ? 'block' : 'none',
|
||||
}}>
|
||||
</Activity>
|
||||
<Activity mode={isExpanded ? 'hidden' : 'visible'}>
|
||||
<CollapsedEditor
|
||||
onToggle={() => {
|
||||
startTransition(() => {
|
||||
@@ -64,17 +56,17 @@ export default function ConfigEditor({
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Activity>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ExpandedEditor({
|
||||
onToggle,
|
||||
appliedOptions,
|
||||
formattedAppliedConfig,
|
||||
}: {
|
||||
onToggle: () => void;
|
||||
appliedOptions: PluginOptions | null;
|
||||
onToggle: (expanded: boolean) => void;
|
||||
formattedAppliedConfig: string;
|
||||
}): React.ReactElement {
|
||||
const store = useStore();
|
||||
const dispatchStore = useStoreDispatch();
|
||||
@@ -122,16 +114,10 @@ function ExpandedEditor({
|
||||
});
|
||||
};
|
||||
|
||||
const formattedAppliedOptions = appliedOptions
|
||||
? prettyFormat(appliedOptions, {
|
||||
printFunctionName: false,
|
||||
printBasicPrototype: false,
|
||||
})
|
||||
: 'Invalid configs';
|
||||
|
||||
return (
|
||||
<ViewTransition
|
||||
update={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}>
|
||||
enter={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}
|
||||
exit={{[CONFIG_PANEL_TRANSITION]: 'slide-out', default: 'none'}}>
|
||||
<Resizable
|
||||
minWidth={300}
|
||||
maxWidth={600}
|
||||
@@ -158,7 +144,7 @@ function ExpandedEditor({
|
||||
Config Overrides
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex-1 rounded-lg overflow-hidden border border-gray-300">
|
||||
<div className="flex-1 border border-gray-300">
|
||||
<MonacoEditor
|
||||
path={'config.ts'}
|
||||
language={'typescript'}
|
||||
@@ -167,16 +153,7 @@ function ExpandedEditor({
|
||||
onChange={handleChange}
|
||||
loading={''}
|
||||
className="monaco-editor-config"
|
||||
options={{
|
||||
...monacoOptions,
|
||||
lineNumbers: 'off',
|
||||
renderLineHighlight: 'none',
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
fontSize: 12,
|
||||
scrollBeyondLastLine: false,
|
||||
glyphMargin: false,
|
||||
}}
|
||||
options={monacoConfigOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -186,23 +163,16 @@ function ExpandedEditor({
|
||||
Applied Configs
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex-1 rounded-lg overflow-hidden border border-gray-300">
|
||||
<div className="flex-1 border border-gray-300">
|
||||
<MonacoEditor
|
||||
path={'applied-config.js'}
|
||||
language={'javascript'}
|
||||
value={formattedAppliedOptions}
|
||||
value={formattedAppliedConfig}
|
||||
loading={''}
|
||||
className="monaco-editor-applied-config"
|
||||
options={{
|
||||
...monacoOptions,
|
||||
lineNumbers: 'off',
|
||||
renderLineHighlight: 'none',
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
fontSize: 12,
|
||||
scrollBeyondLastLine: false,
|
||||
...monacoConfigOptions,
|
||||
readOnly: true,
|
||||
glyphMargin: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -5,316 +5,17 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {parse as babelParse, ParseResult} from '@babel/parser';
|
||||
import * as HermesParser from 'hermes-parser';
|
||||
import * as t from '@babel/types';
|
||||
import BabelPluginReactCompiler, {
|
||||
CompilerError,
|
||||
import {
|
||||
CompilerErrorDetail,
|
||||
CompilerDiagnostic,
|
||||
Effect,
|
||||
ErrorCategory,
|
||||
parseConfigPragmaForTests,
|
||||
ValueKind,
|
||||
type Hook,
|
||||
PluginOptions,
|
||||
CompilerPipelineValue,
|
||||
parsePluginOptions,
|
||||
printReactiveFunctionWithOutlined,
|
||||
printFunctionWithOutlined,
|
||||
type LoggerEvent,
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import {
|
||||
useDeferredValue,
|
||||
useMemo,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
} from 'react';
|
||||
import {useDeferredValue, useMemo, useState} from 'react';
|
||||
import {useStore} from '../StoreContext';
|
||||
import ConfigEditor from './ConfigEditor';
|
||||
import Input from './Input';
|
||||
import {
|
||||
CompilerOutput,
|
||||
CompilerTransformOutput,
|
||||
default as Output,
|
||||
PrintedCompilerPipelineValue,
|
||||
} from './Output';
|
||||
import {transformFromAstSync} from '@babel/core';
|
||||
|
||||
function parseInput(
|
||||
input: string,
|
||||
language: 'flow' | 'typescript',
|
||||
): ParseResult<t.File> {
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
if (language === 'flow') {
|
||||
return HermesParser.parse(input, {
|
||||
babel: true,
|
||||
flow: 'all',
|
||||
sourceType: 'module',
|
||||
enableExperimentalComponentSyntax: true,
|
||||
});
|
||||
} else {
|
||||
return babelParse(input, {
|
||||
plugins: ['typescript', 'jsx'],
|
||||
sourceType: 'module',
|
||||
}) as ParseResult<t.File>;
|
||||
}
|
||||
}
|
||||
|
||||
function invokeCompiler(
|
||||
source: string,
|
||||
language: 'flow' | 'typescript',
|
||||
options: PluginOptions,
|
||||
): CompilerTransformOutput {
|
||||
const ast = parseInput(source, language);
|
||||
let result = transformFromAstSync(ast, source, {
|
||||
filename: '_playgroundFile.js',
|
||||
highlightCode: false,
|
||||
retainLines: true,
|
||||
plugins: [[BabelPluginReactCompiler, options]],
|
||||
ast: true,
|
||||
sourceType: 'module',
|
||||
configFile: false,
|
||||
sourceMaps: true,
|
||||
babelrc: false,
|
||||
});
|
||||
if (result?.ast == null || result?.code == null || result?.map == null) {
|
||||
throw new Error('Expected successful compilation');
|
||||
}
|
||||
return {
|
||||
code: result.code,
|
||||
sourceMaps: result.map,
|
||||
language,
|
||||
};
|
||||
}
|
||||
|
||||
const COMMON_HOOKS: Array<[string, Hook]> = [
|
||||
[
|
||||
'useFragment',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'usePaginationFragment',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'useRefetchableFragment',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'useLazyLoadQuery',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'usePreloadedQuery',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
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);
|
||||
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',
|
||||
configOverrides: string,
|
||||
): [CompilerOutput, 'flow' | 'typescript', PluginOptions | null] {
|
||||
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
|
||||
const error = new CompilerError();
|
||||
const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
|
||||
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
|
||||
const entry = results.get(result.name);
|
||||
if (Array.isArray(entry)) {
|
||||
entry.push(result);
|
||||
} else {
|
||||
results.set(result.name, [result]);
|
||||
}
|
||||
};
|
||||
let language: 'flow' | 'typescript';
|
||||
if (source.match(/\@flow/)) {
|
||||
language = 'flow';
|
||||
} else {
|
||||
language = 'typescript';
|
||||
}
|
||||
let transformOutput;
|
||||
|
||||
let baseOpts: PluginOptions | null = null;
|
||||
try {
|
||||
baseOpts = parseOptions(source, mode, configOverrides);
|
||||
} catch (err) {
|
||||
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) {
|
||||
/**
|
||||
* 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 {
|
||||
/**
|
||||
* 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
|
||||
if (!error.hasErrors() && otherErrors.length !== 0) {
|
||||
otherErrors.forEach(e => error.details.push(e));
|
||||
}
|
||||
if (error.hasErrors()) {
|
||||
return [{kind: 'err', results, error}, language, baseOpts];
|
||||
}
|
||||
return [
|
||||
{kind: 'ok', results, transformOutput, errors: error.details},
|
||||
language,
|
||||
baseOpts,
|
||||
];
|
||||
}
|
||||
import {CompilerOutput, default as Output} from './Output';
|
||||
import {compile} from '../../lib/compilation';
|
||||
import prettyFormat from 'pretty-format';
|
||||
|
||||
export default function Editor(): JSX.Element {
|
||||
const store = useStore();
|
||||
@@ -327,6 +28,7 @@ export default function Editor(): JSX.Element {
|
||||
() => compile(deferredStore.source, 'linter', deferredStore.config),
|
||||
[deferredStore.source, deferredStore.config],
|
||||
);
|
||||
const [formattedAppliedConfig, setFormattedAppliedConfig] = useState('');
|
||||
|
||||
let mergedOutput: CompilerOutput;
|
||||
let errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
|
||||
@@ -340,11 +42,22 @@ export default function Editor(): JSX.Element {
|
||||
mergedOutput = compilerOutput;
|
||||
errors = compilerOutput.error.details;
|
||||
}
|
||||
|
||||
if (appliedOptions) {
|
||||
const formatted = prettyFormat(appliedOptions, {
|
||||
printFunctionName: false,
|
||||
printBasicPrototype: false,
|
||||
});
|
||||
if (formatted !== formattedAppliedConfig) {
|
||||
setFormattedAppliedConfig(formatted);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex top-14">
|
||||
<div className="flex-shrink-0">
|
||||
<ConfigEditor appliedOptions={appliedOptions} />
|
||||
<ConfigEditor formattedAppliedConfig={formattedAppliedConfig} />
|
||||
</div>
|
||||
<div className="flex flex-1 min-w-0">
|
||||
<Input language={language} errors={errors} />
|
||||
|
||||
@@ -27,6 +27,8 @@ import {
|
||||
useState,
|
||||
Suspense,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
startTransition,
|
||||
} from 'react';
|
||||
import AccordionWindow from '../AccordionWindow';
|
||||
import TabbedWindow from '../TabbedWindow';
|
||||
@@ -35,6 +37,7 @@ import {BabelFileResult} from '@babel/core';
|
||||
import {
|
||||
CONFIG_PANEL_TRANSITION,
|
||||
TOGGLE_INTERNALS_TRANSITION,
|
||||
EXPAND_ACCORDION_TRANSITION,
|
||||
} from '../../lib/transitionTypes';
|
||||
import {LRUCache} from 'lru-cache';
|
||||
|
||||
@@ -265,8 +268,22 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element {
|
||||
* Update the active tab back to the output or errors tab when the compilation state
|
||||
* changes between success/failure.
|
||||
*/
|
||||
|
||||
const [previousOutputKind, setPreviousOutputKind] = useState(
|
||||
compilerOutput.kind,
|
||||
);
|
||||
const isFailure = compilerOutput.kind !== 'ok';
|
||||
|
||||
if (compilerOutput.kind !== previousOutputKind) {
|
||||
setPreviousOutputKind(compilerOutput.kind);
|
||||
if (isFailure) {
|
||||
startTransition(() => {
|
||||
addTransitionType(EXPAND_ACCORDION_TRANSITION);
|
||||
setTabsOpen(prev => new Set(prev).add('Output'));
|
||||
setActiveTab('Output');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const changedPasses: Set<string> = new Set(['Output', 'HIR']); // Initial and final passes should always be bold
|
||||
let lastResult: string = '';
|
||||
for (const [passName, results] of compilerOutput.results) {
|
||||
@@ -295,8 +312,6 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element {
|
||||
tabs={tabs}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
// Display the Output tab on compilation failure
|
||||
activeTabOverride={isFailure ? 'Output' : undefined}
|
||||
/>
|
||||
</ViewTransition>
|
||||
);
|
||||
@@ -315,7 +330,6 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element {
|
||||
tabsOpen={tabsOpen}
|
||||
tabs={tabs}
|
||||
changedPasses={changedPasses}
|
||||
isFailure={isFailure}
|
||||
/>
|
||||
</ViewTransition>
|
||||
);
|
||||
@@ -372,12 +386,18 @@ function TextTabContent({
|
||||
loading={''}
|
||||
options={{
|
||||
...monacoOptions,
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
},
|
||||
dimension: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
readOnly: true,
|
||||
lineNumbers: 'off',
|
||||
glyphMargin: false,
|
||||
// Undocumented see https://github.com/Microsoft/vscode/issues/30795#issuecomment-410998882
|
||||
lineDecorationsWidth: 0,
|
||||
lineNumbersMinChars: 0,
|
||||
overviewRulerLanes: 0,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -32,3 +32,14 @@ export const monacoOptions: Partial<EditorProps['options']> = {
|
||||
|
||||
tabSize: 2,
|
||||
};
|
||||
|
||||
export const monacoConfigOptions: Partial<EditorProps['options']> = {
|
||||
...monacoOptions,
|
||||
lineNumbers: 'off',
|
||||
renderLineHighlight: 'none',
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
fontSize: 12,
|
||||
scrollBeyondLastLine: false,
|
||||
glyphMargin: false,
|
||||
};
|
||||
|
||||
@@ -17,15 +17,11 @@ export default function TabbedWindow({
|
||||
tabs,
|
||||
activeTab,
|
||||
onTabChange,
|
||||
activeTabOverride,
|
||||
}: {
|
||||
tabs: Map<string, React.ReactNode>;
|
||||
activeTab: string;
|
||||
onTabChange: (tab: string) => void;
|
||||
activeTabOverride?: string;
|
||||
}): React.ReactElement {
|
||||
const currentActiveTab = activeTabOverride ? activeTabOverride : activeTab;
|
||||
|
||||
const id = useId();
|
||||
const transitionName = `tab-highlight-${id}`;
|
||||
|
||||
@@ -41,7 +37,7 @@ export default function TabbedWindow({
|
||||
<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 = currentActiveTab === tab;
|
||||
const isActive = activeTab === tab;
|
||||
return (
|
||||
<button
|
||||
key={tab}
|
||||
@@ -77,7 +73,7 @@ export default function TabbedWindow({
|
||||
})}
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden w-full h-full">
|
||||
{tabs.get(currentActiveTab)}
|
||||
{tabs.get(activeTab)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
308
compiler/apps/playground/lib/compilation.ts
Normal file
308
compiler/apps/playground/lib/compilation.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
/**
|
||||
* 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 {parse as babelParse, ParseResult} from '@babel/parser';
|
||||
import * as HermesParser from 'hermes-parser';
|
||||
import * as t from '@babel/types';
|
||||
import BabelPluginReactCompiler, {
|
||||
CompilerError,
|
||||
CompilerErrorDetail,
|
||||
CompilerDiagnostic,
|
||||
Effect,
|
||||
ErrorCategory,
|
||||
parseConfigPragmaForTests,
|
||||
ValueKind,
|
||||
type Hook,
|
||||
PluginOptions,
|
||||
CompilerPipelineValue,
|
||||
parsePluginOptions,
|
||||
printReactiveFunctionWithOutlined,
|
||||
printFunctionWithOutlined,
|
||||
type LoggerEvent,
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import {transformFromAstSync} from '@babel/core';
|
||||
import type {
|
||||
CompilerOutput,
|
||||
CompilerTransformOutput,
|
||||
PrintedCompilerPipelineValue,
|
||||
} from '../components/Editor/Output';
|
||||
|
||||
function parseInput(
|
||||
input: string,
|
||||
language: 'flow' | 'typescript',
|
||||
): ParseResult<t.File> {
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
if (language === 'flow') {
|
||||
return HermesParser.parse(input, {
|
||||
babel: true,
|
||||
flow: 'all',
|
||||
sourceType: 'module',
|
||||
enableExperimentalComponentSyntax: true,
|
||||
});
|
||||
} else {
|
||||
return babelParse(input, {
|
||||
plugins: ['typescript', 'jsx'],
|
||||
sourceType: 'module',
|
||||
}) as ParseResult<t.File>;
|
||||
}
|
||||
}
|
||||
|
||||
function invokeCompiler(
|
||||
source: string,
|
||||
language: 'flow' | 'typescript',
|
||||
options: PluginOptions,
|
||||
): CompilerTransformOutput {
|
||||
const ast = parseInput(source, language);
|
||||
let result = transformFromAstSync(ast, source, {
|
||||
filename: '_playgroundFile.js',
|
||||
highlightCode: false,
|
||||
retainLines: true,
|
||||
plugins: [[BabelPluginReactCompiler, options]],
|
||||
ast: true,
|
||||
sourceType: 'module',
|
||||
configFile: false,
|
||||
sourceMaps: true,
|
||||
babelrc: false,
|
||||
});
|
||||
if (result?.ast == null || result?.code == null || result?.map == null) {
|
||||
throw new Error('Expected successful compilation');
|
||||
}
|
||||
return {
|
||||
code: result.code,
|
||||
sourceMaps: result.map,
|
||||
language,
|
||||
};
|
||||
}
|
||||
|
||||
const COMMON_HOOKS: Array<[string, Hook]> = [
|
||||
[
|
||||
'useFragment',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'usePaginationFragment',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'useRefetchableFragment',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'useLazyLoadQuery',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'usePreloadedQuery',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
export function compile(
|
||||
source: string,
|
||||
mode: 'compiler' | 'linter',
|
||||
configOverrides: string,
|
||||
): [CompilerOutput, 'flow' | 'typescript', PluginOptions | null] {
|
||||
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
|
||||
const error = new CompilerError();
|
||||
const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
|
||||
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
|
||||
const entry = results.get(result.name);
|
||||
if (Array.isArray(entry)) {
|
||||
entry.push(result);
|
||||
} else {
|
||||
results.set(result.name, [result]);
|
||||
}
|
||||
};
|
||||
let language: 'flow' | 'typescript';
|
||||
if (source.match(/\@flow/)) {
|
||||
language = 'flow';
|
||||
} else {
|
||||
language = 'typescript';
|
||||
}
|
||||
let transformOutput;
|
||||
|
||||
let baseOpts: PluginOptions | null = null;
|
||||
try {
|
||||
baseOpts = parseOptions(source, mode, configOverrides);
|
||||
} catch (err) {
|
||||
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) {
|
||||
/**
|
||||
* 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 {
|
||||
/**
|
||||
* 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
|
||||
if (!error.hasErrors() && otherErrors.length !== 0) {
|
||||
otherErrors.forEach(e => error.details.push(e));
|
||||
}
|
||||
if (error.hasErrors() || !transformOutput) {
|
||||
return [{kind: 'err', results, error}, language, baseOpts];
|
||||
}
|
||||
return [
|
||||
{kind: 'ok', results, transformOutput, errors: error.details},
|
||||
language,
|
||||
baseOpts,
|
||||
];
|
||||
}
|
||||
@@ -26,7 +26,7 @@
|
||||
"@babel/traverse": "^7.18.9",
|
||||
"@babel/types": "7.26.3",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"@monaco-editor/react": "^4.4.6",
|
||||
"@monaco-editor/react": "^4.8.0-rc.2",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@use-gesture/react": "^10.2.22",
|
||||
"hermes-eslint": "^0.25.0",
|
||||
@@ -40,13 +40,13 @@
|
||||
"prettier": "^3.3.3",
|
||||
"pretty-format": "^29.3.1",
|
||||
"re-resizable": "^6.9.16",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1"
|
||||
"react": "19.2",
|
||||
"react-dom": "19.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.11.9",
|
||||
"@types/react": "19.1.13",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"@types/react": "19.2",
|
||||
"@types/react-dom": "19.2",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"clsx": "^1.2.1",
|
||||
"concurrently": "^7.4.0",
|
||||
@@ -58,7 +58,7 @@
|
||||
"wait-on": "^7.2.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9"
|
||||
"@types/react": "19.2",
|
||||
"@types/react-dom": "19.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,15 @@
|
||||
::view-transition-group(.slide-in) {
|
||||
z-index: 1;
|
||||
}
|
||||
::view-transition-old(.slide-out) {
|
||||
animation-name: slideOutLeft;
|
||||
}
|
||||
::view-transition-new(.slide-out) {
|
||||
animation-name: slideInLeft;
|
||||
}
|
||||
::view-transition-group(.slide-out) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@keyframes slideOutLeft {
|
||||
from {
|
||||
@@ -124,3 +133,11 @@
|
||||
::view-transition-group(.expand-accordion) {
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
/**
|
||||
* For some reason, the original Monaco editor is still visible to the
|
||||
* left of the DiffEditor. This is a workaround for better visual clarity.
|
||||
*/
|
||||
.monaco-diff-editor .editor.original{
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
@@ -701,19 +701,19 @@
|
||||
"@jridgewell/resolve-uri" "^3.1.0"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||
|
||||
"@monaco-editor/loader@^1.4.0":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.4.0.tgz#f08227057331ec890fa1e903912a5b711a2ad558"
|
||||
integrity sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==
|
||||
"@monaco-editor/loader@^1.6.1":
|
||||
version "1.6.1"
|
||||
resolved "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.6.1.tgz#c99177d87765abf10de31a0086084e714acfbc0f"
|
||||
integrity sha512-w3tEnj9HYEC73wtjdpR089AqkUPskFRcdkxsiSFt3SoUc3OHpmu+leP94CXBm4mHfefmhsdfI0ZQu6qJ0wgtPg==
|
||||
dependencies:
|
||||
state-local "^1.0.6"
|
||||
|
||||
"@monaco-editor/react@^4.4.6":
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.6.0.tgz#bcc68671e358a21c3814566b865a54b191e24119"
|
||||
integrity sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==
|
||||
"@monaco-editor/react@^4.8.0-rc.2":
|
||||
version "4.8.0-rc.2"
|
||||
resolved "https://registry.npmjs.org/@monaco-editor/react/-/react-4.8.0-rc.2.tgz#e9acf652e23e9f640671a69875f496dde7f098aa"
|
||||
integrity sha512-RzFHKBCnRA4RnozaG/EPhKsbkhX5wcApSa5MElR/AD2ojxhMY+QP+G8aJpxALCnIwKs6L0dec5MJ0nAjMUEqnA==
|
||||
dependencies:
|
||||
"@monaco-editor/loader" "^1.4.0"
|
||||
"@monaco-editor/loader" "^1.6.1"
|
||||
|
||||
"@next/env@15.6.0-canary.7":
|
||||
version "15.6.0-canary.7"
|
||||
@@ -859,6 +859,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.1.9.tgz#5ab695fce1e804184767932365ae6569c11b4b4b"
|
||||
integrity sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==
|
||||
|
||||
"@types/react-dom@19.2":
|
||||
version "19.2.2"
|
||||
resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz#a4cc874797b7ddc9cb180ef0d5dc23f596fc2332"
|
||||
integrity sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==
|
||||
|
||||
"@types/react@19.1.12":
|
||||
version "19.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.12.tgz#7bfaa76aabbb0b4fe0493c21a3a7a93d33e8937b"
|
||||
@@ -866,10 +871,10 @@
|
||||
dependencies:
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/react@19.1.13":
|
||||
version "19.1.13"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.13.tgz#fc650ffa680d739a25a530f5d7ebe00cdd771883"
|
||||
integrity sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==
|
||||
"@types/react@19.2":
|
||||
version "19.2.2"
|
||||
resolved "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz#ba123a75d4c2a51158697160a4ea2ff70aa6bf36"
|
||||
integrity sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==
|
||||
dependencies:
|
||||
csstype "^3.0.2"
|
||||
|
||||
@@ -3589,12 +3594,12 @@ re-resizable@^6.9.16:
|
||||
resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.10.0.tgz#d684a096ab438f1a93f59ad3a580a206b0ce31ee"
|
||||
integrity sha512-hysSK0xmA5nz24HBVztlk4yCqCLCvS32E6ZpWxVKop9x3tqCa4yAj1++facrmkOf62JsJHjmjABdKxXofYioCw==
|
||||
|
||||
react-dom@19.1.1:
|
||||
version "19.1.1"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.1.1.tgz#2daa9ff7f3ae384aeb30e76d5ee38c046dc89893"
|
||||
integrity sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==
|
||||
react-dom@19.2:
|
||||
version "19.2.0"
|
||||
resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz#00ed1e959c365e9a9d48f8918377465466ec3af8"
|
||||
integrity sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==
|
||||
dependencies:
|
||||
scheduler "^0.26.0"
|
||||
scheduler "^0.27.0"
|
||||
|
||||
react-is@^16.13.1:
|
||||
version "16.13.1"
|
||||
@@ -3606,10 +3611,10 @@ react-is@^18.0.0:
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
|
||||
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
|
||||
|
||||
react@19.1.1:
|
||||
version "19.1.1"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-19.1.1.tgz#06d9149ec5e083a67f9a1e39ce97b06a03b644af"
|
||||
integrity sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==
|
||||
react@19.2:
|
||||
version "19.2.0"
|
||||
resolved "https://registry.npmjs.org/react/-/react-19.2.0.tgz#d33dd1721698f4376ae57a54098cb47fc75d93a5"
|
||||
integrity sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==
|
||||
|
||||
read-cache@^1.0.0:
|
||||
version "1.0.0"
|
||||
@@ -3785,10 +3790,10 @@ safe-regex-test@^1.1.0:
|
||||
es-errors "^1.3.0"
|
||||
is-regex "^1.2.1"
|
||||
|
||||
scheduler@^0.26.0:
|
||||
version "0.26.0"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.26.0.tgz#4ce8a8c2a2095f13ea11bf9a445be50c555d6337"
|
||||
integrity sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==
|
||||
scheduler@^0.27.0:
|
||||
version "0.27.0"
|
||||
resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd"
|
||||
integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==
|
||||
|
||||
semver@^6.3.1:
|
||||
version "6.3.1"
|
||||
|
||||
@@ -52,8 +52,8 @@
|
||||
"react-dom": "0.0.0-experimental-4beb1fd8-20241118",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"zod": "^3.22.4",
|
||||
"zod-validation-error": "^2.1.0"
|
||||
"zod": "^3.25.0 || ^4.0.0",
|
||||
"zod-validation-error": "^3.5.0 || ^4.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"./**/@babel/parser": "7.7.4",
|
||||
|
||||
@@ -12,6 +12,28 @@ import {Err, Ok, Result} from './Utils/Result';
|
||||
import {assertExhaustive} from './Utils/utils';
|
||||
import invariant from 'invariant';
|
||||
|
||||
// Number of context lines to display above the source of an error
|
||||
const CODEFRAME_LINES_ABOVE = 2;
|
||||
// Number of context lines to display below the source of an error
|
||||
const CODEFRAME_LINES_BELOW = 3;
|
||||
/*
|
||||
* Max number of lines for the _source_ of an error, before we abbreviate
|
||||
* the display of the source portion
|
||||
*/
|
||||
const CODEFRAME_MAX_LINES = 10;
|
||||
/*
|
||||
* When the error source exceeds the above threshold, how many lines of
|
||||
* the source should be displayed? We show:
|
||||
* - CODEFRAME_LINES_ABOVE context lines
|
||||
* - CODEFRAME_ABBREVIATED_SOURCE_LINES of the error
|
||||
* - '...' ellipsis
|
||||
* - CODEFRAME_ABBREVIATED_SOURCE_LINES of the error
|
||||
* - CODEFRAME_LINES_BELOW context lines
|
||||
*
|
||||
* This value must be at least 2 or else we'll cut off important parts of the error message
|
||||
*/
|
||||
const CODEFRAME_ABBREVIATED_SOURCE_LINES = 5;
|
||||
|
||||
export enum ErrorSeverity {
|
||||
/**
|
||||
* An actionable error that the developer can fix. For example, product code errors should be
|
||||
@@ -496,7 +518,7 @@ function printCodeFrame(
|
||||
loc: t.SourceLocation,
|
||||
message: string,
|
||||
): string {
|
||||
return codeFrameColumns(
|
||||
const printed = codeFrameColumns(
|
||||
source,
|
||||
{
|
||||
start: {
|
||||
@@ -510,8 +532,25 @@ function printCodeFrame(
|
||||
},
|
||||
{
|
||||
message,
|
||||
linesAbove: CODEFRAME_LINES_ABOVE,
|
||||
linesBelow: CODEFRAME_LINES_BELOW,
|
||||
},
|
||||
);
|
||||
const lines = printed.split(/\r?\n/);
|
||||
if (loc.end.line - loc.start.line < CODEFRAME_MAX_LINES) {
|
||||
return printed;
|
||||
}
|
||||
const pipeIndex = lines[0].indexOf('|');
|
||||
return [
|
||||
...lines.slice(
|
||||
0,
|
||||
CODEFRAME_LINES_ABOVE + CODEFRAME_ABBREVIATED_SOURCE_LINES,
|
||||
),
|
||||
' '.repeat(pipeIndex) + '…',
|
||||
...lines.slice(
|
||||
-(CODEFRAME_LINES_BELOW + CODEFRAME_ABBREVIATED_SOURCE_LINES),
|
||||
),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function printErrorSummary(category: ErrorCategory, message: string): string {
|
||||
@@ -536,7 +575,8 @@ function printErrorSummary(category: ErrorCategory, message: string): string {
|
||||
case ErrorCategory.StaticComponents:
|
||||
case ErrorCategory.Suppression:
|
||||
case ErrorCategory.Syntax:
|
||||
case ErrorCategory.UseMemo: {
|
||||
case ErrorCategory.UseMemo:
|
||||
case ErrorCategory.VoidUseMemo: {
|
||||
heading = 'Error';
|
||||
break;
|
||||
}
|
||||
@@ -582,6 +622,10 @@ export enum ErrorCategory {
|
||||
* Checking for valid usage of manual memoization
|
||||
*/
|
||||
UseMemo = 'UseMemo',
|
||||
/**
|
||||
* Checking that useMemos always return a value
|
||||
*/
|
||||
VoidUseMemo = 'VoidUseMemo',
|
||||
/**
|
||||
* Checking for higher order functions acting as factories for components/hooks
|
||||
*/
|
||||
@@ -669,6 +713,21 @@ export enum ErrorCategory {
|
||||
FBT = 'FBT',
|
||||
}
|
||||
|
||||
export enum LintRulePreset {
|
||||
/**
|
||||
* Rules that are stable and included in the `recommended` preset.
|
||||
*/
|
||||
Recommended = 'recommended',
|
||||
/**
|
||||
* Rules that are more experimental and only included in the `recommended-latest` preset.
|
||||
*/
|
||||
RecommendedLatest = 'recommended-latest',
|
||||
/**
|
||||
* Rules that are disabled.
|
||||
*/
|
||||
Off = 'off',
|
||||
}
|
||||
|
||||
export type LintRule = {
|
||||
// Stores the category the rule corresponds to, used to filter errors when reporting
|
||||
category: ErrorCategory;
|
||||
@@ -689,15 +748,14 @@ export type LintRule = {
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* If true, this rule will automatically appear in the default, "recommended" ESLint
|
||||
* rule set. Otherwise it will be part of an `allRules` export that developers can
|
||||
* use to opt-in to showing output of all possible rules.
|
||||
* Configures the preset in which the rule is enabled. If 'off', the rule will not be included in
|
||||
* any preset.
|
||||
*
|
||||
* NOTE: not all validations are enabled by default! Setting this flag only affects
|
||||
* whether a given rule is part of the recommended set. The corresponding validation
|
||||
* also should be enabled by default if you want the error to actually show up!
|
||||
*/
|
||||
recommended: boolean;
|
||||
preset: LintRulePreset;
|
||||
};
|
||||
|
||||
const RULE_NAME_PATTERN = /^[a-z]+(-[a-z]+)*$/;
|
||||
@@ -720,7 +778,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'automatic-effect-dependencies',
|
||||
description:
|
||||
'Verifies that automatic effect dependencies are compiled if opted-in',
|
||||
recommended: false,
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.CapitalizedCalls: {
|
||||
@@ -730,7 +788,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'capitalized-calls',
|
||||
description:
|
||||
'Validates against calling capitalized functions/methods instead of using JSX',
|
||||
recommended: false,
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Config: {
|
||||
@@ -739,7 +797,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'config',
|
||||
description: 'Validates the compiler configuration options',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectDependencies: {
|
||||
@@ -748,7 +806,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'memoized-effect-dependencies',
|
||||
description: 'Validates that effect dependencies are memoized',
|
||||
recommended: false,
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectDerivationsOfState: {
|
||||
@@ -758,7 +816,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'no-deriving-state-in-effects',
|
||||
description:
|
||||
'Validates against deriving values from state in an effect',
|
||||
recommended: false,
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectSetState: {
|
||||
@@ -768,7 +826,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'set-state-in-effect',
|
||||
description:
|
||||
'Validates against calling setState synchronously in an effect, which can lead to re-renders that degrade performance',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.ErrorBoundaries: {
|
||||
@@ -778,7 +836,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'error-boundaries',
|
||||
description:
|
||||
'Validates usage of error boundaries instead of try/catch for errors in child components',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Factories: {
|
||||
@@ -789,7 +847,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
description:
|
||||
'Validates against higher order functions defining nested components or hooks. ' +
|
||||
'Components and hooks should be defined at the module level',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.FBT: {
|
||||
@@ -798,7 +856,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'fbt',
|
||||
description: 'Validates usage of fbt',
|
||||
recommended: false,
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Fire: {
|
||||
@@ -807,7 +865,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'fire',
|
||||
description: 'Validates usage of `fire`',
|
||||
recommended: false,
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Gating: {
|
||||
@@ -817,7 +875,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'gating',
|
||||
description:
|
||||
'Validates configuration of [gating mode](https://react.dev/reference/react-compiler/gating)',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Globals: {
|
||||
@@ -828,7 +886,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
description:
|
||||
'Validates against assignment/mutation of globals during render, part of ensuring that ' +
|
||||
'[side effects must render outside of render](https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Hooks: {
|
||||
@@ -842,7 +900,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
* We need to dedeupe these (moving the remaining bits into the compiler) and then enable
|
||||
* this rule.
|
||||
*/
|
||||
recommended: false,
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Immutability: {
|
||||
@@ -852,7 +910,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'immutability',
|
||||
description:
|
||||
'Validates against mutating props, state, and other values that [are immutable](https://react.dev/reference/rules/components-and-hooks-must-be-pure#props-and-state-are-immutable)',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Invariant: {
|
||||
@@ -861,7 +919,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'invariant',
|
||||
description: 'Internal invariants',
|
||||
recommended: false,
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.PreserveManualMemo: {
|
||||
@@ -873,7 +931,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
'Validates that existing manual memoized is preserved by the compiler. ' +
|
||||
'React Compiler will only compile components and hooks if its inference ' +
|
||||
'[matches or exceeds the existing manual memoization](https://react.dev/learn/react-compiler/introduction#what-should-i-do-about-usememo-usecallback-and-reactmemo)',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Purity: {
|
||||
@@ -883,7 +941,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'purity',
|
||||
description:
|
||||
'Validates that [components/hooks are pure](https://react.dev/reference/rules/components-and-hooks-must-be-pure) by checking that they do not call known-impure functions',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Refs: {
|
||||
@@ -893,7 +951,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'refs',
|
||||
description:
|
||||
'Validates correct usage of refs, not reading/writing during render. See the "pitfalls" section in [`useRef()` usage](https://react.dev/reference/react/useRef#usage)',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.RenderSetState: {
|
||||
@@ -903,7 +961,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'set-state-in-render',
|
||||
description:
|
||||
'Validates against setting state during render, which can trigger additional renders and potential infinite render loops',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.StaticComponents: {
|
||||
@@ -913,7 +971,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'static-components',
|
||||
description:
|
||||
'Validates that components are static, not recreated every render. Components that are recreated dynamically can reset state and trigger excessive re-rendering',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Suppression: {
|
||||
@@ -922,7 +980,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'rule-suppression',
|
||||
description: 'Validates against suppression of other rules',
|
||||
recommended: false,
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Syntax: {
|
||||
@@ -931,7 +989,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'syntax',
|
||||
description: 'Validates against invalid syntax',
|
||||
recommended: false,
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Todo: {
|
||||
@@ -940,7 +998,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Hint,
|
||||
name: 'todo',
|
||||
description: 'Unimplemented features',
|
||||
recommended: false,
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.UnsupportedSyntax: {
|
||||
@@ -950,7 +1008,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'unsupported-syntax',
|
||||
description:
|
||||
'Validates against syntax that we do not plan to support in React Compiler',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.UseMemo: {
|
||||
@@ -960,7 +1018,17 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'use-memo',
|
||||
description:
|
||||
'Validates usage of the useMemo() hook against common mistakes. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.VoidUseMemo: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'void-use-memo',
|
||||
description:
|
||||
'Validates that useMemos always return a value and that the result of the useMemo is used by the component/hook. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
|
||||
preset: LintRulePreset.RecommendedLatest,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.IncompatibleLibrary: {
|
||||
@@ -970,7 +1038,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'incompatible-library',
|
||||
description:
|
||||
'Validates against usage of libraries which are incompatible with memoization (manual or automatic)',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import * as t from '@babel/types';
|
||||
import {z} from 'zod';
|
||||
import {z} from 'zod/v4';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
tryParseExternalFunction,
|
||||
} from '../HIR/Environment';
|
||||
import {hasOwnProperty} from '../Utils/utils';
|
||||
import {fromZodError} from 'zod-validation-error';
|
||||
import {fromZodError} from 'zod-validation-error/v4';
|
||||
import {CompilerPipelineValue} from './Pipeline';
|
||||
|
||||
const PanicThresholdOptionsSchema = z.enum([
|
||||
|
||||
@@ -1568,20 +1568,6 @@ function lowerObjectPropertyKey(
|
||||
name: key.node.value,
|
||||
};
|
||||
} else if (property.node.computed && key.isExpression()) {
|
||||
if (!key.isIdentifier() && !key.isMemberExpression()) {
|
||||
/*
|
||||
* NOTE: allowing complex key expressions can trigger a bug where a mutation is made conditional
|
||||
* see fixture
|
||||
* error.object-expression-computed-key-modified-during-after-construction.js
|
||||
*/
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: key.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
const place = lowerExpressionToTemporary(builder, key);
|
||||
return {
|
||||
kind: 'computed',
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
*/
|
||||
|
||||
import * as t from '@babel/types';
|
||||
import {ZodError, z} from 'zod';
|
||||
import {fromZodError} from 'zod-validation-error';
|
||||
import {ZodError, z} from 'zod/v4';
|
||||
import {fromZodError} from 'zod-validation-error/v4';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {Logger, ProgramContext} from '../Entrypoint';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
@@ -83,21 +83,11 @@ export type ExternalFunction = z.infer<typeof ExternalFunctionSchema>;
|
||||
export const USE_FIRE_FUNCTION_NAME = 'useFire';
|
||||
export const EMIT_FREEZE_GLOBAL_GATING = '__DEV__';
|
||||
|
||||
export const MacroMethodSchema = z.union([
|
||||
z.object({type: z.literal('wildcard')}),
|
||||
z.object({type: z.literal('name'), name: z.string()}),
|
||||
]);
|
||||
|
||||
// Would like to change this to drop the string option, but breaks compatibility with existing configs
|
||||
export const MacroSchema = z.union([
|
||||
z.string(),
|
||||
z.tuple([z.string(), z.array(MacroMethodSchema)]),
|
||||
]);
|
||||
export const MacroSchema = z.string();
|
||||
|
||||
export type CompilerMode = 'all_features' | 'no_inferred_memo';
|
||||
|
||||
export type Macro = z.infer<typeof MacroSchema>;
|
||||
export type MacroMethod = z.infer<typeof MacroMethodSchema>;
|
||||
|
||||
const HookSchema = z.object({
|
||||
/*
|
||||
@@ -159,7 +149,7 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* A function that, given the name of a module, can optionally return a description
|
||||
* of that module's type signature.
|
||||
*/
|
||||
moduleTypeProvider: z.nullable(z.function().args(z.string())).default(null),
|
||||
moduleTypeProvider: z.nullable(z.any()).default(null),
|
||||
|
||||
/**
|
||||
* A list of functions which the application compiles as macros, where
|
||||
@@ -249,7 +239,7 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* Allows specifying a function that can populate HIR with type information from
|
||||
* Flow
|
||||
*/
|
||||
flowTypeProvider: z.nullable(z.function().args(z.string())).default(null),
|
||||
flowTypeProvider: z.nullable(z.any()).default(null),
|
||||
|
||||
/**
|
||||
* Enables inference of optional dependency chains. Without this flag
|
||||
@@ -659,7 +649,7 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* Invalid:
|
||||
* useMemo(() => { ... }, [...]);
|
||||
*/
|
||||
validateNoVoidUseMemo: z.boolean().default(false),
|
||||
validateNoVoidUseMemo: z.boolean().default(true),
|
||||
|
||||
/**
|
||||
* Validates that Components/Hooks are always defined at module level. This prevents scope
|
||||
@@ -906,6 +896,12 @@ export class Environment {
|
||||
if (moduleTypeProvider == null) {
|
||||
return null;
|
||||
}
|
||||
if (typeof moduleTypeProvider !== 'function') {
|
||||
CompilerError.throwInvalidConfig({
|
||||
reason: `Expected a function for \`moduleTypeProvider\``,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
const unparsedModuleConfig = moduleTypeProvider(moduleName);
|
||||
if (unparsedModuleConfig != null) {
|
||||
const parsedModuleConfig = TypeSchema.safeParse(unparsedModuleConfig);
|
||||
|
||||
@@ -16,7 +16,7 @@ import {assertExhaustive} from '../Utils/utils';
|
||||
import {Environment, ReactFunctionType} from './Environment';
|
||||
import type {HookKind} from './ObjectShape';
|
||||
import {Type, makeType} from './Types';
|
||||
import {z} from 'zod';
|
||||
import {z} from 'zod/v4';
|
||||
import type {AliasingEffect} from '../Inference/AliasingEffects';
|
||||
import {isReservedWord} from '../Utils/Keyword';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import {isValidIdentifier} from '@babel/types';
|
||||
import {z} from 'zod';
|
||||
import {z} from 'zod/v4';
|
||||
import {Effect, ValueKind} from '..';
|
||||
import {
|
||||
EffectSchema,
|
||||
|
||||
@@ -438,40 +438,6 @@ export function dropManualMemoization(
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bailout on void return useMemos. This is an anti-pattern where code might be using
|
||||
* useMemo like useEffect: running arbirtary side-effects synced to changes in specific
|
||||
* values.
|
||||
*/
|
||||
if (
|
||||
func.env.config.validateNoVoidUseMemo &&
|
||||
manualMemo.kind === 'useMemo'
|
||||
) {
|
||||
const funcToCheck = sidemap.functions.get(
|
||||
fnPlace.identifier.id,
|
||||
)?.value;
|
||||
if (funcToCheck !== undefined && funcToCheck.loweredFunc.func) {
|
||||
if (!hasNonVoidReturn(funcToCheck.loweredFunc.func)) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: 'useMemo() callbacks must return a value',
|
||||
description: `This ${
|
||||
manualMemo.loadInstr.value.kind === 'PropertyLoad'
|
||||
? 'React.useMemo'
|
||||
: 'useMemo'
|
||||
} callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: instr.value.loc,
|
||||
message: 'useMemo() callbacks must return a value',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
instr.value = getManualMemoizationReplacement(
|
||||
fnPlace,
|
||||
instr.value.loc,
|
||||
@@ -629,17 +595,3 @@ function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
|
||||
}
|
||||
return optionals;
|
||||
}
|
||||
|
||||
function hasNonVoidReturn(func: HIRFunction): boolean {
|
||||
for (const [, block] of func.body.blocks) {
|
||||
if (block.terminal.kind === 'return') {
|
||||
if (
|
||||
block.terminal.returnVariant === 'Explicit' ||
|
||||
block.terminal.returnVariant === 'Implicit'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
Environment,
|
||||
FunctionExpression,
|
||||
GeneratedSource,
|
||||
getHookKind,
|
||||
HIRFunction,
|
||||
Hole,
|
||||
IdentifierId,
|
||||
@@ -198,6 +199,7 @@ export function inferMutationAliasingEffects(
|
||||
isFunctionExpression,
|
||||
fn,
|
||||
hoistedContextDeclarations,
|
||||
findNonMutatedDestructureSpreads(fn),
|
||||
);
|
||||
|
||||
let iterationCount = 0;
|
||||
@@ -287,15 +289,18 @@ class Context {
|
||||
isFuctionExpression: boolean;
|
||||
fn: HIRFunction;
|
||||
hoistedContextDeclarations: Map<DeclarationId, Place | null>;
|
||||
nonMutatingSpreads: Set<IdentifierId>;
|
||||
|
||||
constructor(
|
||||
isFunctionExpression: boolean,
|
||||
fn: HIRFunction,
|
||||
hoistedContextDeclarations: Map<DeclarationId, Place | null>,
|
||||
nonMutatingSpreads: Set<IdentifierId>,
|
||||
) {
|
||||
this.isFuctionExpression = isFunctionExpression;
|
||||
this.fn = fn;
|
||||
this.hoistedContextDeclarations = hoistedContextDeclarations;
|
||||
this.nonMutatingSpreads = nonMutatingSpreads;
|
||||
}
|
||||
|
||||
cacheApplySignature(
|
||||
@@ -322,6 +327,161 @@ class Context {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds objects created via ObjectPattern spread destructuring
|
||||
* (`const {x, ...spread} = ...`) where a) the rvalue is known frozen and
|
||||
* b) the spread value cannot possibly be directly mutated. The idea is that
|
||||
* for this set of values, we can treat the spread object as frozen.
|
||||
*
|
||||
* The primary use case for this is props spreading:
|
||||
*
|
||||
* ```
|
||||
* function Component({prop, ...otherProps}) {
|
||||
* const transformedProp = transform(prop, otherProps.foo);
|
||||
* // pass `otherProps` down:
|
||||
* return <Foo {...otherProps} prop={transformedProp} />;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Here we know that since `otherProps` cannot be mutated, we don't have to treat
|
||||
* it as mutable: `otherProps.foo` only reads a value that must be frozen, so it
|
||||
* can be treated as frozen too.
|
||||
*/
|
||||
function findNonMutatedDestructureSpreads(fn: HIRFunction): Set<IdentifierId> {
|
||||
const knownFrozen = new Set<IdentifierId>();
|
||||
if (fn.fnType === 'Component') {
|
||||
const [props] = fn.params;
|
||||
if (props != null && props.kind === 'Identifier') {
|
||||
knownFrozen.add(props.identifier.id);
|
||||
}
|
||||
} else {
|
||||
for (const param of fn.params) {
|
||||
if (param.kind === 'Identifier') {
|
||||
knownFrozen.add(param.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Map of temporaries to identifiers for spread objects
|
||||
const candidateNonMutatingSpreads = new Map<IdentifierId, IdentifierId>();
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
if (candidateNonMutatingSpreads.size !== 0) {
|
||||
for (const phi of block.phis) {
|
||||
for (const operand of phi.operands.values()) {
|
||||
const spread = candidateNonMutatingSpreads.get(operand.identifier.id);
|
||||
if (spread != null) {
|
||||
candidateNonMutatingSpreads.delete(spread);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
switch (value.kind) {
|
||||
case 'Destructure': {
|
||||
if (
|
||||
!knownFrozen.has(value.value.identifier.id) ||
|
||||
!(
|
||||
value.lvalue.kind === InstructionKind.Let ||
|
||||
value.lvalue.kind === InstructionKind.Const
|
||||
) ||
|
||||
value.lvalue.pattern.kind !== 'ObjectPattern'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
for (const item of value.lvalue.pattern.properties) {
|
||||
if (item.kind !== 'Spread') {
|
||||
continue;
|
||||
}
|
||||
candidateNonMutatingSpreads.set(
|
||||
item.place.identifier.id,
|
||||
item.place.identifier.id,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'LoadLocal': {
|
||||
const spread = candidateNonMutatingSpreads.get(
|
||||
value.place.identifier.id,
|
||||
);
|
||||
if (spread != null) {
|
||||
candidateNonMutatingSpreads.set(lvalue.identifier.id, spread);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'StoreLocal': {
|
||||
const spread = candidateNonMutatingSpreads.get(
|
||||
value.value.identifier.id,
|
||||
);
|
||||
if (spread != null) {
|
||||
candidateNonMutatingSpreads.set(lvalue.identifier.id, spread);
|
||||
candidateNonMutatingSpreads.set(
|
||||
value.lvalue.place.identifier.id,
|
||||
spread,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'JsxFragment':
|
||||
case 'JsxExpression': {
|
||||
// Passing objects created with spread to jsx can't mutate them
|
||||
break;
|
||||
}
|
||||
case 'PropertyLoad': {
|
||||
// Properties must be frozen since the original value was frozen
|
||||
break;
|
||||
}
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
if (getHookKind(fn.env, callee.identifier) != null) {
|
||||
// Hook calls have frozen arguments, and non-ref returns are frozen
|
||||
if (!isRefOrRefValue(lvalue.identifier)) {
|
||||
knownFrozen.add(lvalue.identifier.id);
|
||||
}
|
||||
} else {
|
||||
// Non-hook calls check their operands, since they are potentially mutable
|
||||
if (candidateNonMutatingSpreads.size !== 0) {
|
||||
// Otherwise any reference to the spread object itself may mutate
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
const spread = candidateNonMutatingSpreads.get(
|
||||
operand.identifier.id,
|
||||
);
|
||||
if (spread != null) {
|
||||
candidateNonMutatingSpreads.delete(spread);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (candidateNonMutatingSpreads.size !== 0) {
|
||||
// Otherwise any reference to the spread object itself may mutate
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
const spread = candidateNonMutatingSpreads.get(
|
||||
operand.identifier.id,
|
||||
);
|
||||
if (spread != null) {
|
||||
candidateNonMutatingSpreads.delete(spread);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const nonMutatingSpreads = new Set<IdentifierId>();
|
||||
for (const [key, value] of candidateNonMutatingSpreads) {
|
||||
if (key === value) {
|
||||
nonMutatingSpreads.add(key);
|
||||
}
|
||||
}
|
||||
return nonMutatingSpreads;
|
||||
}
|
||||
|
||||
function inferParam(
|
||||
param: Place | SpreadPattern,
|
||||
initialState: InferenceState,
|
||||
@@ -2054,7 +2214,9 @@ function computeSignatureForInstruction(
|
||||
kind: 'Create',
|
||||
into: place,
|
||||
reason: ValueReason.Other,
|
||||
value: ValueKind.Mutable,
|
||||
value: context.nonMutatingSpreads.has(place.identifier.id)
|
||||
? ValueKind.Frozen
|
||||
: ValueKind.Mutable,
|
||||
});
|
||||
effects.push({
|
||||
kind: 'Capture',
|
||||
|
||||
@@ -8,13 +8,41 @@
|
||||
import {
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
InstructionValue,
|
||||
makeInstructionId,
|
||||
MutableRange,
|
||||
Place,
|
||||
ReactiveValue,
|
||||
ReactiveScope,
|
||||
} from '../HIR';
|
||||
import {Macro, MacroMethod} from '../HIR/Environment';
|
||||
import {eachReactiveValueOperand} from './visitors';
|
||||
import {Macro} from '../HIR/Environment';
|
||||
import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
|
||||
/**
|
||||
* Whether a macro requires its arguments to be transitively inlined (eg fbt)
|
||||
* or just avoid having the top-level values be converted to variables (eg fbt.param)
|
||||
*/
|
||||
enum InlineLevel {
|
||||
Transitive = 'Transitive',
|
||||
Shallow = 'Shallow',
|
||||
}
|
||||
type MacroDefinition = {
|
||||
level: InlineLevel;
|
||||
properties: Map<string, MacroDefinition> | null;
|
||||
};
|
||||
|
||||
const SHALLOW_MACRO: MacroDefinition = {
|
||||
level: InlineLevel.Shallow,
|
||||
properties: null,
|
||||
};
|
||||
const TRANSITIVE_MACRO: MacroDefinition = {
|
||||
level: InlineLevel.Transitive,
|
||||
properties: null,
|
||||
};
|
||||
const FBT_MACRO: MacroDefinition = {
|
||||
level: InlineLevel.Transitive,
|
||||
properties: new Map([['*', SHALLOW_MACRO]]),
|
||||
};
|
||||
FBT_MACRO.properties!.set('enum', FBT_MACRO);
|
||||
|
||||
/**
|
||||
* This pass supports the `fbt` translation system (https://facebook.github.io/fbt/)
|
||||
@@ -39,230 +67,210 @@ import {eachReactiveValueOperand} from './visitors';
|
||||
* ## User-defined macro-like function
|
||||
*
|
||||
* Users can also specify their own functions to be treated similarly to fbt via the
|
||||
* `customMacros` environment configuration.
|
||||
* `customMacros` environment configuration. By default, user-supplied custom macros
|
||||
* have their arguments transitively inlined.
|
||||
*/
|
||||
export function memoizeFbtAndMacroOperandsInSameScope(
|
||||
fn: HIRFunction,
|
||||
): Set<IdentifierId> {
|
||||
const fbtMacroTags = new Set<Macro>([
|
||||
...Array.from(FBT_TAGS).map((tag): Macro => [tag, []]),
|
||||
...(fn.env.config.customMacros ?? []),
|
||||
const macroKinds = new Map<Macro, MacroDefinition>([
|
||||
...Array.from(FBT_TAGS.entries()),
|
||||
...(fn.env.config.customMacros ?? []).map(
|
||||
name => [name, TRANSITIVE_MACRO] as [Macro, MacroDefinition],
|
||||
),
|
||||
]);
|
||||
const fbtValues: Set<IdentifierId> = new Set();
|
||||
const macroMethods = new Map<IdentifierId, Array<Array<MacroMethod>>>();
|
||||
while (true) {
|
||||
let vsize = fbtValues.size;
|
||||
let msize = macroMethods.size;
|
||||
visit(fn, fbtMacroTags, fbtValues, macroMethods);
|
||||
if (vsize === fbtValues.size && msize === macroMethods.size) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return fbtValues;
|
||||
/**
|
||||
* Forward data-flow analysis to identify all macro tags, including
|
||||
* things like `fbt.foo.bar(...)`
|
||||
*/
|
||||
const macroTags = populateMacroTags(fn, macroKinds);
|
||||
|
||||
/**
|
||||
* Reverse data-flow analysis to merge arguments to macro *invocations*
|
||||
* based on the kind of the macro
|
||||
*/
|
||||
const macroValues = mergeMacroArguments(fn, macroTags, macroKinds);
|
||||
|
||||
return macroValues;
|
||||
}
|
||||
|
||||
export const FBT_TAGS: Set<string> = new Set([
|
||||
'fbt',
|
||||
'fbt:param',
|
||||
'fbs',
|
||||
'fbs:param',
|
||||
const FBT_TAGS: Map<string, MacroDefinition> = new Map([
|
||||
['fbt', FBT_MACRO],
|
||||
['fbt:param', SHALLOW_MACRO],
|
||||
['fbt:enum', FBT_MACRO],
|
||||
['fbt:plural', SHALLOW_MACRO],
|
||||
['fbs', FBT_MACRO],
|
||||
['fbs:param', SHALLOW_MACRO],
|
||||
['fbs:enum', FBT_MACRO],
|
||||
['fbs:plural', SHALLOW_MACRO],
|
||||
]);
|
||||
export const SINGLE_CHILD_FBT_TAGS: Set<string> = new Set([
|
||||
'fbt:param',
|
||||
'fbs:param',
|
||||
]);
|
||||
|
||||
function visit(
|
||||
function populateMacroTags(
|
||||
fn: HIRFunction,
|
||||
fbtMacroTags: Set<Macro>,
|
||||
fbtValues: Set<IdentifierId>,
|
||||
macroMethods: Map<IdentifierId, Array<Array<MacroMethod>>>,
|
||||
): void {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const instruction of block.instructions) {
|
||||
const {lvalue, value} = instruction;
|
||||
if (lvalue === null) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
value.kind === 'Primitive' &&
|
||||
typeof value.value === 'string' &&
|
||||
matchesExactTag(value.value, fbtMacroTags)
|
||||
) {
|
||||
/*
|
||||
* We don't distinguish between tag names and strings, so record
|
||||
* all `fbt` string literals in case they are used as a jsx tag.
|
||||
*/
|
||||
fbtValues.add(lvalue.identifier.id);
|
||||
} else if (
|
||||
value.kind === 'LoadGlobal' &&
|
||||
matchesExactTag(value.binding.name, fbtMacroTags)
|
||||
) {
|
||||
// Record references to `fbt` as a global
|
||||
fbtValues.add(lvalue.identifier.id);
|
||||
} else if (
|
||||
value.kind === 'LoadGlobal' &&
|
||||
matchTagRoot(value.binding.name, fbtMacroTags) !== null
|
||||
) {
|
||||
const methods = matchTagRoot(value.binding.name, fbtMacroTags)!;
|
||||
macroMethods.set(lvalue.identifier.id, methods);
|
||||
} else if (
|
||||
value.kind === 'PropertyLoad' &&
|
||||
macroMethods.has(value.object.identifier.id)
|
||||
) {
|
||||
const methods = macroMethods.get(value.object.identifier.id)!;
|
||||
const newMethods = [];
|
||||
for (const method of methods) {
|
||||
if (
|
||||
method.length > 0 &&
|
||||
(method[0].type === 'wildcard' ||
|
||||
(method[0].type === 'name' && method[0].name === value.property))
|
||||
) {
|
||||
if (method.length > 1) {
|
||||
newMethods.push(method.slice(1));
|
||||
} else {
|
||||
fbtValues.add(lvalue.identifier.id);
|
||||
macroKinds: Map<Macro, MacroDefinition>,
|
||||
): Map<IdentifierId, MacroDefinition> {
|
||||
const macroTags = new Map<IdentifierId, MacroDefinition>();
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
switch (value.kind) {
|
||||
case 'Primitive': {
|
||||
if (typeof value.value === 'string') {
|
||||
const macroDefinition = macroKinds.get(value.value);
|
||||
if (macroDefinition != null) {
|
||||
/*
|
||||
* We don't distinguish between tag names and strings, so record
|
||||
* all `fbt` string literals in case they are used as a jsx tag.
|
||||
*/
|
||||
macroTags.set(lvalue.identifier.id, macroDefinition);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (newMethods.length > 0) {
|
||||
macroMethods.set(lvalue.identifier.id, newMethods);
|
||||
}
|
||||
} else if (isFbtCallExpression(fbtValues, value)) {
|
||||
const fbtScope = lvalue.identifier.scope;
|
||||
if (fbtScope === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* if the JSX element's tag was `fbt`, mark all its operands
|
||||
* to ensure that they end up in the same scope as the jsx element
|
||||
* itself.
|
||||
*/
|
||||
for (const operand of eachReactiveValueOperand(value)) {
|
||||
operand.identifier.scope = fbtScope;
|
||||
|
||||
// Expand the jsx element's range to account for its operands
|
||||
expandFbtScopeRange(fbtScope.range, operand.identifier.mutableRange);
|
||||
fbtValues.add(operand.identifier.id);
|
||||
}
|
||||
} else if (
|
||||
isFbtJsxExpression(fbtMacroTags, fbtValues, value) ||
|
||||
isFbtJsxChild(fbtValues, lvalue, value)
|
||||
) {
|
||||
const fbtScope = lvalue.identifier.scope;
|
||||
if (fbtScope === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* if the JSX element's tag was `fbt`, mark all its operands
|
||||
* to ensure that they end up in the same scope as the jsx element
|
||||
* itself.
|
||||
*/
|
||||
for (const operand of eachReactiveValueOperand(value)) {
|
||||
operand.identifier.scope = fbtScope;
|
||||
|
||||
// Expand the jsx element's range to account for its operands
|
||||
expandFbtScopeRange(fbtScope.range, operand.identifier.mutableRange);
|
||||
|
||||
/*
|
||||
* NOTE: we add the operands as fbt values so that they are also
|
||||
* grouped with this expression
|
||||
*/
|
||||
fbtValues.add(operand.identifier.id);
|
||||
}
|
||||
} else if (fbtValues.has(lvalue.identifier.id)) {
|
||||
const fbtScope = lvalue.identifier.scope;
|
||||
if (fbtScope === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const operand of eachReactiveValueOperand(value)) {
|
||||
if (
|
||||
operand.identifier.name !== null &&
|
||||
operand.identifier.name.kind === 'named'
|
||||
) {
|
||||
/*
|
||||
* named identifiers were already locals, we only have to force temporaries
|
||||
* into the same scope
|
||||
*/
|
||||
continue;
|
||||
case 'LoadGlobal': {
|
||||
let macroDefinition = macroKinds.get(value.binding.name);
|
||||
if (macroDefinition != null) {
|
||||
macroTags.set(lvalue.identifier.id, macroDefinition);
|
||||
}
|
||||
operand.identifier.scope = fbtScope;
|
||||
|
||||
// Expand the jsx element's range to account for its operands
|
||||
expandFbtScopeRange(fbtScope.range, operand.identifier.mutableRange);
|
||||
break;
|
||||
}
|
||||
case 'PropertyLoad': {
|
||||
if (typeof value.property === 'string') {
|
||||
const macroDefinition = macroTags.get(value.object.identifier.id);
|
||||
if (macroDefinition != null) {
|
||||
const propertyDefinition =
|
||||
macroDefinition.properties != null
|
||||
? (macroDefinition.properties.get(value.property) ??
|
||||
macroDefinition.properties.get('*'))
|
||||
: null;
|
||||
const propertyMacro = propertyDefinition ?? macroDefinition;
|
||||
macroTags.set(lvalue.identifier.id, propertyMacro);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return macroTags;
|
||||
}
|
||||
|
||||
function matchesExactTag(s: string, tags: Set<Macro>): boolean {
|
||||
return Array.from(tags).some(macro =>
|
||||
typeof macro === 'string'
|
||||
? s === macro
|
||||
: macro[1].length === 0 && macro[0] === s,
|
||||
);
|
||||
}
|
||||
|
||||
function matchTagRoot(
|
||||
s: string,
|
||||
tags: Set<Macro>,
|
||||
): Array<Array<MacroMethod>> | null {
|
||||
const methods: Array<Array<MacroMethod>> = [];
|
||||
for (const macro of tags) {
|
||||
if (typeof macro === 'string') {
|
||||
continue;
|
||||
function mergeMacroArguments(
|
||||
fn: HIRFunction,
|
||||
macroTags: Map<IdentifierId, MacroDefinition>,
|
||||
macroKinds: Map<Macro, MacroDefinition>,
|
||||
): Set<IdentifierId> {
|
||||
const macroValues = new Set<IdentifierId>(macroTags.keys());
|
||||
for (const block of Array.from(fn.body.blocks.values()).reverse()) {
|
||||
for (let i = block.instructions.length - 1; i >= 0; i--) {
|
||||
const instr = block.instructions[i]!;
|
||||
const {lvalue, value} = instr;
|
||||
switch (value.kind) {
|
||||
case 'DeclareContext':
|
||||
case 'DeclareLocal':
|
||||
case 'Destructure':
|
||||
case 'LoadContext':
|
||||
case 'LoadLocal':
|
||||
case 'PostfixUpdate':
|
||||
case 'PrefixUpdate':
|
||||
case 'StoreContext':
|
||||
case 'StoreLocal': {
|
||||
// Instructions that never need to be merged
|
||||
break;
|
||||
}
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
const scope = lvalue.identifier.scope;
|
||||
if (scope == null) {
|
||||
continue;
|
||||
}
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
const macroDefinition =
|
||||
macroTags.get(callee.identifier.id) ??
|
||||
macroTags.get(lvalue.identifier.id);
|
||||
if (macroDefinition != null) {
|
||||
visitOperands(
|
||||
macroDefinition,
|
||||
scope,
|
||||
lvalue,
|
||||
value,
|
||||
macroValues,
|
||||
macroTags,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'JsxExpression': {
|
||||
const scope = lvalue.identifier.scope;
|
||||
if (scope == null) {
|
||||
continue;
|
||||
}
|
||||
let macroDefinition;
|
||||
if (value.tag.kind === 'Identifier') {
|
||||
macroDefinition = macroTags.get(value.tag.identifier.id);
|
||||
} else {
|
||||
macroDefinition = macroKinds.get(value.tag.name);
|
||||
}
|
||||
macroDefinition ??= macroTags.get(lvalue.identifier.id);
|
||||
if (macroDefinition != null) {
|
||||
visitOperands(
|
||||
macroDefinition,
|
||||
scope,
|
||||
lvalue,
|
||||
value,
|
||||
macroValues,
|
||||
macroTags,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const scope = lvalue.identifier.scope;
|
||||
if (scope == null) {
|
||||
continue;
|
||||
}
|
||||
const macroDefinition = macroTags.get(lvalue.identifier.id);
|
||||
if (macroDefinition != null) {
|
||||
visitOperands(
|
||||
macroDefinition,
|
||||
scope,
|
||||
lvalue,
|
||||
value,
|
||||
macroValues,
|
||||
macroTags,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const [tag, rest] = macro;
|
||||
if (tag === s && rest.length > 0) {
|
||||
methods.push(rest);
|
||||
for (const phi of block.phis) {
|
||||
const scope = phi.place.identifier.scope;
|
||||
if (scope == null) {
|
||||
continue;
|
||||
}
|
||||
const macroDefinition = macroTags.get(phi.place.identifier.id);
|
||||
if (
|
||||
macroDefinition == null ||
|
||||
macroDefinition.level === InlineLevel.Shallow
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
macroValues.add(phi.place.identifier.id);
|
||||
for (const operand of phi.operands.values()) {
|
||||
operand.identifier.scope = scope;
|
||||
expandFbtScopeRange(scope.range, operand.identifier.mutableRange);
|
||||
macroTags.set(operand.identifier.id, macroDefinition);
|
||||
macroValues.add(operand.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (methods.length > 0) {
|
||||
return methods;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isFbtCallExpression(
|
||||
fbtValues: Set<IdentifierId>,
|
||||
value: ReactiveValue,
|
||||
): boolean {
|
||||
return (
|
||||
(value.kind === 'CallExpression' &&
|
||||
fbtValues.has(value.callee.identifier.id)) ||
|
||||
(value.kind === 'MethodCall' && fbtValues.has(value.property.identifier.id))
|
||||
);
|
||||
}
|
||||
|
||||
function isFbtJsxExpression(
|
||||
fbtMacroTags: Set<Macro>,
|
||||
fbtValues: Set<IdentifierId>,
|
||||
value: ReactiveValue,
|
||||
): boolean {
|
||||
return (
|
||||
value.kind === 'JsxExpression' &&
|
||||
((value.tag.kind === 'Identifier' &&
|
||||
fbtValues.has(value.tag.identifier.id)) ||
|
||||
(value.tag.kind === 'BuiltinTag' &&
|
||||
matchesExactTag(value.tag.name, fbtMacroTags)))
|
||||
);
|
||||
}
|
||||
|
||||
function isFbtJsxChild(
|
||||
fbtValues: Set<IdentifierId>,
|
||||
lvalue: Place | null,
|
||||
value: ReactiveValue,
|
||||
): boolean {
|
||||
return (
|
||||
(value.kind === 'JsxExpression' || value.kind === 'JsxFragment') &&
|
||||
lvalue !== null &&
|
||||
fbtValues.has(lvalue.identifier.id)
|
||||
);
|
||||
return macroValues;
|
||||
}
|
||||
|
||||
function expandFbtScopeRange(
|
||||
@@ -275,3 +283,22 @@ function expandFbtScopeRange(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function visitOperands(
|
||||
macroDefinition: MacroDefinition,
|
||||
scope: ReactiveScope,
|
||||
lvalue: Place,
|
||||
value: InstructionValue,
|
||||
macroValues: Set<IdentifierId>,
|
||||
macroTags: Map<IdentifierId, MacroDefinition>,
|
||||
): void {
|
||||
macroValues.add(lvalue.identifier.id);
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
if (macroDefinition.level === InlineLevel.Transitive) {
|
||||
operand.identifier.scope = scope;
|
||||
expandFbtScopeRange(scope.range, operand.identifier.mutableRange);
|
||||
macroTags.set(operand.identifier.id, macroDefinition);
|
||||
}
|
||||
macroValues.add(operand.identifier.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,7 +393,7 @@ function* generateInstructionTypes(
|
||||
shapeId: BuiltInArrayId,
|
||||
});
|
||||
} else {
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {fromZodError} from 'zod-validation-error';
|
||||
import {fromZodError} from 'zod-validation-error/v4';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
CompilationMode,
|
||||
@@ -135,16 +135,7 @@ function parseConfigPragmaEnvironmentForTest(
|
||||
} else if (val) {
|
||||
const parsedVal = tryParseTestPragmaValue(val).unwrap();
|
||||
if (key === 'customMacros' && typeof parsedVal === 'string') {
|
||||
const valSplit = parsedVal.split('.');
|
||||
const props = [];
|
||||
for (const elt of valSplit.slice(1)) {
|
||||
if (elt === '*') {
|
||||
props.push({type: 'wildcard'});
|
||||
} else if (elt.length > 0) {
|
||||
props.push({type: 'name', name: elt});
|
||||
}
|
||||
}
|
||||
maybeConfig[key] = [[valSplit[0], props]];
|
||||
maybeConfig[key] = [parsedVal.split('.')[0]];
|
||||
continue;
|
||||
}
|
||||
maybeConfig[key] = parsedVal;
|
||||
|
||||
@@ -10,16 +10,37 @@ import {
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
import {FunctionExpression, HIRFunction, IdentifierId} from '../HIR';
|
||||
import {
|
||||
FunctionExpression,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
SourceLocation,
|
||||
} from '../HIR';
|
||||
import {
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
const errors = new CompilerError();
|
||||
const voidMemoErrors = new CompilerError();
|
||||
const useMemos = new Set<IdentifierId>();
|
||||
const react = new Set<IdentifierId>();
|
||||
const functions = new Map<IdentifierId, FunctionExpression>();
|
||||
const unusedUseMemos = new Map<IdentifierId, SourceLocation>();
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const {lvalue, value} of block.instructions) {
|
||||
if (unusedUseMemos.size !== 0) {
|
||||
/**
|
||||
* Most of the time useMemo results are referenced immediately. Don't bother
|
||||
* scanning instruction operands for useMemos unless there is an as-yet-unused
|
||||
* useMemo.
|
||||
*/
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
unusedUseMemos.delete(operand.identifier.id);
|
||||
}
|
||||
}
|
||||
switch (value.kind) {
|
||||
case 'LoadGlobal': {
|
||||
if (value.binding.name === 'useMemo') {
|
||||
@@ -45,10 +66,8 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
case 'CallExpression': {
|
||||
// Is the function being called useMemo, with at least 1 argument?
|
||||
const callee =
|
||||
value.kind === 'CallExpression'
|
||||
? value.callee.identifier.id
|
||||
: value.property.identifier.id;
|
||||
const isUseMemo = useMemos.has(callee);
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
const isUseMemo = useMemos.has(callee.identifier.id);
|
||||
if (!isUseMemo || value.args.length === 0) {
|
||||
continue;
|
||||
}
|
||||
@@ -104,10 +123,106 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
);
|
||||
}
|
||||
|
||||
validateNoContextVariableAssignment(body.loweredFunc.func, errors);
|
||||
|
||||
if (fn.env.config.validateNoVoidUseMemo) {
|
||||
if (!hasNonVoidReturn(body.loweredFunc.func)) {
|
||||
voidMemoErrors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.VoidUseMemo,
|
||||
reason: 'useMemo() callbacks must return a value',
|
||||
description: `This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: body.loc,
|
||||
message: 'useMemo() callbacks must return a value',
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
unusedUseMemos.set(lvalue.identifier.id, callee.loc);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (unusedUseMemos.size !== 0) {
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
unusedUseMemos.delete(operand.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (unusedUseMemos.size !== 0) {
|
||||
/**
|
||||
* Basic check for unused memos, where the result of the call is never referenced. This runs
|
||||
* before DCE so it's more of an AST-level check that something, _anything_, cares about the value.
|
||||
*
|
||||
* This is easy to defeat with e.g. `const _ = useMemo(...)` but it at least gives us something to teach.
|
||||
* Even a DCE-based version could be bypassed with `noop(useMemo(...))`.
|
||||
*/
|
||||
for (const loc of unusedUseMemos.values()) {
|
||||
voidMemoErrors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.VoidUseMemo,
|
||||
reason: 'useMemo() result is unused',
|
||||
description: `This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc,
|
||||
message: 'useMemo() result is unused',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
fn.env.logErrors(voidMemoErrors.asResult());
|
||||
return errors.asResult();
|
||||
}
|
||||
|
||||
function validateNoContextVariableAssignment(
|
||||
fn: HIRFunction,
|
||||
errors: CompilerError,
|
||||
): void {
|
||||
const context = new Set(fn.context.map(place => place.identifier.id));
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const instr of block.instructions) {
|
||||
const value = instr.value;
|
||||
switch (value.kind) {
|
||||
case 'StoreContext': {
|
||||
if (context.has(value.lvalue.place.identifier.id)) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason:
|
||||
'useMemo() callbacks may not reassign variables declared outside of the callback',
|
||||
description:
|
||||
'useMemo() callbacks must be pure functions and cannot reassign variables defined outside of the callback function',
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: value.lvalue.place.loc,
|
||||
message: 'Cannot reassign variable',
|
||||
}),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors.asResult();
|
||||
}
|
||||
|
||||
function hasNonVoidReturn(func: HIRFunction): boolean {
|
||||
for (const [, block] of func.body.blocks) {
|
||||
if (block.terminal.kind === 'return') {
|
||||
if (
|
||||
block.terminal.returnVariant === 'Explicit' ||
|
||||
block.terminal.returnVariant === 'Implicit'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('parseConfigPragma()', () => {
|
||||
validateHooksUsage: 1,
|
||||
} as any);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: Expected boolean, received number at "validateHooksUsage"."`,
|
||||
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: Invalid input: expected boolean, received number at "validateHooksUsage"."`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('parseConfigPragma()', () => {
|
||||
],
|
||||
} as any);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: autodepsIndex must be > 0 at "inferEffectDependencies[0].autodepsIndex"."`,
|
||||
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: AutodepsIndex must be > 0 at "inferEffectDependencies[0].autodepsIndex"."`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component() {
|
||||
let x;
|
||||
const y = useMemo(() => {
|
||||
let z;
|
||||
x = [];
|
||||
z = true;
|
||||
return z;
|
||||
}, []);
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: useMemo() callbacks may not reassign variables declared outside of the callback
|
||||
|
||||
useMemo() callbacks must be pure functions and cannot reassign variables defined outside of the callback function.
|
||||
|
||||
error.invalid-reassign-variable-in-usememo.ts:5:4
|
||||
3 | const y = useMemo(() => {
|
||||
4 | let z;
|
||||
> 5 | x = [];
|
||||
| ^ Cannot reassign variable
|
||||
6 | z = true;
|
||||
7 | return z;
|
||||
8 | }, []);
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
function Component() {
|
||||
let x;
|
||||
const y = useMemo(() => {
|
||||
let z;
|
||||
x = [];
|
||||
z = true;
|
||||
return z;
|
||||
}, []);
|
||||
return [x, y];
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component(props) {
|
||||
// Intentionally don't bind state, this repros a bug where we didn't
|
||||
// infer the type of destructured properties after a hole in the array
|
||||
let [, setState] = useState();
|
||||
setState(1);
|
||||
return props.foo;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: ['TodoAdd'],
|
||||
isComponent: 'TodoAdd',
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Calling setState during render may trigger an infinite loop
|
||||
|
||||
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
|
||||
|
||||
error.invalid-setState-in-render-unbound-state.ts:5:2
|
||||
3 | // infer the type of destructured properties after a hole in the array
|
||||
4 | let [, setState] = useState();
|
||||
> 5 | setState(1);
|
||||
| ^^^^^^^^ Found setState() in render
|
||||
6 | return props.foo;
|
||||
7 | }
|
||||
8 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
function Component(props) {
|
||||
// Intentionally don't bind state, this repros a bug where we didn't
|
||||
// infer the type of destructured properties after a hole in the array
|
||||
let [, setState] = useState();
|
||||
setState(1);
|
||||
return props.foo;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: ['TodoAdd'],
|
||||
isComponent: 'TodoAdd',
|
||||
};
|
||||
@@ -60,29 +60,7 @@ This argument is a function which may reassign or mutate `cache` after render, w
|
||||
> 22 | // The original issue is that `cache` was not memoized together with the returned
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 23 | // function. This was because neither appears to ever be mutated — the function
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 24 | // is known to mutate `cache` but the function isn't called.
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 25 | //
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 26 | // The fix is to detect cases like this — functions that are mutable but not called -
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 27 | // and ensure that their mutable captures are aliased together into the same scope.
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 28 | const cache = new WeakMap<TInput, TOutput>();
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 29 | return input => {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 30 | let output = cache.get(input);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 31 | if (output == null) {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 32 | output = map(input);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 33 | cache.set(input, output);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 34 | }
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
…
|
||||
> 35 | return output;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 36 | };
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {identity, mutate, mutateAndReturn} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const key = {};
|
||||
const context = {
|
||||
[(mutate(key), key)]: identity([props.value]),
|
||||
};
|
||||
mutate(key);
|
||||
return context;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Todo: (BuildHIR::lowerExpression) Expected Identifier, got SequenceExpression key in ObjectExpression
|
||||
|
||||
error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr.ts:6:6
|
||||
4 | const key = {};
|
||||
5 | const context = {
|
||||
> 6 | [(mutate(key), key)]: identity([props.value]),
|
||||
| ^^^^^^^^^^^^^^^^ (BuildHIR::lowerExpression) Expected Identifier, got SequenceExpression key in ObjectExpression
|
||||
7 | };
|
||||
8 | mutate(key);
|
||||
9 | return context;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {identity, mutate, mutateAndReturn} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const key = {};
|
||||
const context = {
|
||||
[mutateAndReturn(key)]: identity([props.value]),
|
||||
};
|
||||
mutate(key);
|
||||
return context;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Todo: (BuildHIR::lowerExpression) Expected Identifier, got CallExpression key in ObjectExpression
|
||||
|
||||
error.todo-object-expression-computed-key-modified-during-after-construction.ts:6:5
|
||||
4 | const key = {};
|
||||
5 | const context = {
|
||||
> 6 | [mutateAndReturn(key)]: identity([props.value]),
|
||||
| ^^^^^^^^^^^^^^^^^^^^ (BuildHIR::lowerExpression) Expected Identifier, got CallExpression key in ObjectExpression
|
||||
7 | };
|
||||
8 | mutate(key);
|
||||
9 | return context;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {identity, mutate, mutateAndReturn} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const key = {};
|
||||
const context = {
|
||||
[mutateAndReturn(key)]: identity([props.value]),
|
||||
};
|
||||
return context;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Todo: (BuildHIR::lowerExpression) Expected Identifier, got CallExpression key in ObjectExpression
|
||||
|
||||
error.todo-object-expression-computed-key-mutate-key-while-constructing-object.ts:6:5
|
||||
4 | const key = {};
|
||||
5 | const context = {
|
||||
> 6 | [mutateAndReturn(key)]: identity([props.value]),
|
||||
| ^^^^^^^^^^^^^^^^^^^^ (BuildHIR::lowerExpression) Expected Identifier, got CallExpression key in ObjectExpression
|
||||
7 | };
|
||||
8 | return context;
|
||||
9 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {identity, mutate, mutateAndReturn} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const obj = {mutateAndReturn};
|
||||
const key = {};
|
||||
const context = {
|
||||
[obj.mutateAndReturn(key)]: identity([props.value]),
|
||||
};
|
||||
mutate(key);
|
||||
return context;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Todo: (BuildHIR::lowerExpression) Expected Identifier, got CallExpression key in ObjectExpression
|
||||
|
||||
error.todo-object-expression-member-expr-call.ts:7:5
|
||||
5 | const key = {};
|
||||
6 | const context = {
|
||||
> 7 | [obj.mutateAndReturn(key)]: identity([props.value]),
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ (BuildHIR::lowerExpression) Expected Identifier, got CallExpression key in ObjectExpression
|
||||
8 | };
|
||||
9 | mutate(key);
|
||||
10 | return context;
|
||||
```
|
||||
|
||||
|
||||
@@ -64,20 +64,7 @@ error.todo-preserve-memo-deps-mixed-optional-nonoptional-property-chain.ts:7:25
|
||||
> 8 | return identity({
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 9 | callback: () => {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 10 | // This is a bug in our dependency inference: we stop capturing dependencies
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 11 | // after x.a.b?.c. But what this dependency is telling us is that if `x.a.b`
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 12 | // was non-nullish, then we can access `.c.d?.e`. Thus we should take the
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 13 | // full property chain, exactly as-is with optionals/non-optionals, as a
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 14 | // dependency
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 15 | return identity(x.a.b?.c.d?.e);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 16 | },
|
||||
…
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 17 | });
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoVoidUseMemo
|
||||
function Component() {
|
||||
const value = useMemo(() => {
|
||||
console.log('computing');
|
||||
}, []);
|
||||
const value2 = React.useMemo(() => {
|
||||
console.log('computing');
|
||||
}, []);
|
||||
return (
|
||||
<div>
|
||||
{value}
|
||||
{value2}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
|
||||
Error: useMemo() callbacks must return a value
|
||||
|
||||
This useMemo callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects.
|
||||
|
||||
error.useMemo-no-return-value.ts:3:16
|
||||
1 | // @validateNoVoidUseMemo
|
||||
2 | function Component() {
|
||||
> 3 | const value = useMemo(() => {
|
||||
| ^^^^^^^^^^^^^^^
|
||||
> 4 | console.log('computing');
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 5 | }, []);
|
||||
| ^^^^^^^^^ useMemo() callbacks must return a value
|
||||
6 | const value2 = React.useMemo(() => {
|
||||
7 | console.log('computing');
|
||||
8 | }, []);
|
||||
|
||||
Error: useMemo() callbacks must return a value
|
||||
|
||||
This React.useMemo callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects.
|
||||
|
||||
error.useMemo-no-return-value.ts:6:17
|
||||
4 | console.log('computing');
|
||||
5 | }, []);
|
||||
> 6 | const value2 = React.useMemo(() => {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 7 | console.log('computing');
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 8 | }, []);
|
||||
| ^^^^^^^^^ useMemo() callbacks must return a value
|
||||
9 | return (
|
||||
10 | <div>
|
||||
11 | {value}
|
||||
```
|
||||
|
||||
|
||||
@@ -44,15 +44,23 @@ import fbt from "fbt";
|
||||
import { identity } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(3);
|
||||
const $ = _c(5);
|
||||
let t0;
|
||||
if ($[0] !== props.count || $[1] !== props.option) {
|
||||
let t1;
|
||||
if ($[3] !== props.count) {
|
||||
t1 = identity(props.count);
|
||||
$[3] = props.count;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
t0 = (
|
||||
<span>
|
||||
{fbt._(
|
||||
{ "*": "{count} votes for {option}", _1: "1 vote for {option}" },
|
||||
[
|
||||
fbt._plural(identity(props.count), "count"),
|
||||
fbt._plural(t1, "count"),
|
||||
fbt._param(
|
||||
"option",
|
||||
|
||||
|
||||
@@ -44,15 +44,23 @@ import fbt from "fbt";
|
||||
import { identity } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(3);
|
||||
const $ = _c(5);
|
||||
let t0;
|
||||
if ($[0] !== props.count || $[1] !== props.option) {
|
||||
let t1;
|
||||
if ($[3] !== props.count) {
|
||||
t1 = identity(props.count);
|
||||
$[3] = props.count;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
t0 = (
|
||||
<span>
|
||||
{fbt._(
|
||||
{ "*": "{count} votes for {option}", _1: "1 vote for {option}" },
|
||||
[
|
||||
fbt._plural(identity(props.count), "count"),
|
||||
fbt._plural(t1, "count"),
|
||||
fbt._param(
|
||||
"option",
|
||||
|
||||
|
||||
@@ -37,28 +37,47 @@ import { c as _c } from "react/compiler-runtime";
|
||||
import fbt from "fbt";
|
||||
|
||||
function Foo(t0) {
|
||||
const $ = _c(3);
|
||||
const $ = _c(13);
|
||||
const { name1, name2 } = t0;
|
||||
let t1;
|
||||
if ($[0] !== name1 || $[1] !== name2) {
|
||||
let t2;
|
||||
if ($[3] !== name1) {
|
||||
t2 = <b>{name1}</b>;
|
||||
$[3] = name1;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
let t3;
|
||||
if ($[5] !== name1 || $[6] !== t2) {
|
||||
t3 = <span key={name1}>{t2}</span>;
|
||||
$[5] = name1;
|
||||
$[6] = t2;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
let t4;
|
||||
if ($[8] !== name2) {
|
||||
t4 = <b>{name2}</b>;
|
||||
$[8] = name2;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
let t5;
|
||||
if ($[10] !== name2 || $[11] !== t4) {
|
||||
t5 = <span key={name2}>{t4}</span>;
|
||||
$[10] = name2;
|
||||
$[11] = t4;
|
||||
$[12] = t5;
|
||||
} else {
|
||||
t5 = $[12];
|
||||
}
|
||||
t1 = fbt._(
|
||||
"{user1} and {user2} accepted your PR!",
|
||||
[
|
||||
fbt._param(
|
||||
"user1",
|
||||
|
||||
<span key={name1}>
|
||||
<b>{name1}</b>
|
||||
</span>,
|
||||
),
|
||||
fbt._param(
|
||||
"user2",
|
||||
|
||||
<span key={name2}>
|
||||
<b>{name2}</b>
|
||||
</span>,
|
||||
),
|
||||
],
|
||||
[fbt._param("user1", t3), fbt._param("user2", t5)],
|
||||
{ hk: "2PxMie" },
|
||||
);
|
||||
$[0] = name1;
|
||||
|
||||
@@ -29,20 +29,24 @@ import { c as _c } from "react/compiler-runtime";
|
||||
import fbt from "fbt";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(4);
|
||||
const $ = _c(6);
|
||||
const { name, data, icon } = t0;
|
||||
let t1;
|
||||
if ($[0] !== data || $[1] !== icon || $[2] !== name) {
|
||||
let t2;
|
||||
if ($[4] !== name) {
|
||||
t2 = <Text type="h4">{name}</Text>;
|
||||
$[4] = name;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
t1 = (
|
||||
<Text type="body4">
|
||||
{fbt._(
|
||||
"{item author}{icon}{=m2}",
|
||||
[
|
||||
fbt._param(
|
||||
"item author",
|
||||
|
||||
<Text type="h4">{name}</Text>,
|
||||
),
|
||||
fbt._param("item author", t2),
|
||||
fbt._param(
|
||||
"icon",
|
||||
|
||||
|
||||
@@ -27,16 +27,21 @@ import fbt from "fbt";
|
||||
import { identity } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(2);
|
||||
const $ = _c(4);
|
||||
let t0;
|
||||
if ($[0] !== props.text) {
|
||||
const t1 = identity(props.text);
|
||||
let t2;
|
||||
if ($[2] !== t1) {
|
||||
t2 = <>{t1}</>;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
t0 = (
|
||||
<Foo
|
||||
value={fbt._(
|
||||
"{value}%",
|
||||
[fbt._param("value", <>{identity(props.text)}</>)],
|
||||
{ hk: "10F5Cc" },
|
||||
)}
|
||||
value={fbt._("{value}%", [fbt._param("value", t2)], { hk: "10F5Cc" })}
|
||||
/>
|
||||
);
|
||||
$[0] = props.text;
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow
|
||||
import {fbt} from 'fbt';
|
||||
|
||||
function Example({x}) {
|
||||
// "Inner Text" needs to be visible to fbt: the <Bar> element cannot
|
||||
// be memoized separately
|
||||
return (
|
||||
<fbt desc="Description">
|
||||
Outer Text
|
||||
<Foo key="b" x={x}>
|
||||
<Bar key="a">Inner Text</Bar>
|
||||
</Foo>
|
||||
</fbt>
|
||||
);
|
||||
}
|
||||
|
||||
function Foo({x, children}) {
|
||||
'use no memo';
|
||||
return (
|
||||
<>
|
||||
<div>{x}</div>
|
||||
<span>{children}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Bar({children}) {
|
||||
'use no memo';
|
||||
return children;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Example,
|
||||
params: [{x: 'Hello'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { fbt } from "fbt";
|
||||
|
||||
function Example(t0) {
|
||||
const $ = _c(2);
|
||||
const { x } = t0;
|
||||
let t1;
|
||||
if ($[0] !== x) {
|
||||
t1 = fbt._(
|
||||
"Outer Text {=m1}",
|
||||
[
|
||||
fbt._implicitParam(
|
||||
"=m1",
|
||||
|
||||
<Foo key="b" x={x}>
|
||||
{fbt._(
|
||||
"{=m1}",
|
||||
[
|
||||
fbt._implicitParam(
|
||||
"=m1",
|
||||
<Bar key="a">
|
||||
{fbt._("Inner Text", null, { hk: "32YB0l" })}
|
||||
</Bar>,
|
||||
),
|
||||
],
|
||||
{ hk: "23dJsI" },
|
||||
)}
|
||||
</Foo>,
|
||||
),
|
||||
],
|
||||
{ hk: "2RVA7V" },
|
||||
);
|
||||
$[0] = x;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
function Foo({ x, children }) {
|
||||
"use no memo";
|
||||
return (
|
||||
<>
|
||||
<div>{x}</div>
|
||||
<span>{children}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Bar({ children }) {
|
||||
"use no memo";
|
||||
return children;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Example,
|
||||
params: [{ x: "Hello" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) Outer Text <div>Hello</div><span>Inner Text</span>
|
||||
@@ -0,0 +1,35 @@
|
||||
// @flow
|
||||
import {fbt} from 'fbt';
|
||||
|
||||
function Example({x}) {
|
||||
// "Inner Text" needs to be visible to fbt: the <Bar> element cannot
|
||||
// be memoized separately
|
||||
return (
|
||||
<fbt desc="Description">
|
||||
Outer Text
|
||||
<Foo key="b" x={x}>
|
||||
<Bar key="a">Inner Text</Bar>
|
||||
</Foo>
|
||||
</fbt>
|
||||
);
|
||||
}
|
||||
|
||||
function Foo({x, children}) {
|
||||
'use no memo';
|
||||
return (
|
||||
<>
|
||||
<div>{x}</div>
|
||||
<span>{children}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Bar({children}) {
|
||||
'use no memo';
|
||||
return children;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Example,
|
||||
params: [{x: 'Hello'}],
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import fbt from 'fbt';
|
||||
import {Stringify, identity} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
|
||||
* Expected fixture `fbt-param-call-arguments` to succeed but it failed with error:
|
||||
* /fbt-param-call-arguments.ts: Line 19 Column 11: fbt: unsupported babel node: Identifier
|
||||
* ---
|
||||
* t3
|
||||
* ---
|
||||
*/
|
||||
function Component({firstname, lastname}) {
|
||||
'use memo';
|
||||
return (
|
||||
<div>
|
||||
{fbt(
|
||||
[
|
||||
'Name: ',
|
||||
fbt.param('firstname', <Stringify key={0} name={firstname} />),
|
||||
', ',
|
||||
fbt.param(
|
||||
'lastname',
|
||||
identity(
|
||||
fbt(
|
||||
'(inner)' +
|
||||
fbt.param('lastname', <Stringify key={1} name={lastname} />),
|
||||
'Inner fbt value'
|
||||
)
|
||||
)
|
||||
),
|
||||
],
|
||||
'Name'
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstname: 'first', lastname: 'last'}],
|
||||
sequentialRenders: [{firstname: 'first', lastname: 'last'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import fbt from "fbt";
|
||||
import { Stringify, identity } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
|
||||
* Expected fixture `fbt-param-call-arguments` to succeed but it failed with error:
|
||||
* /fbt-param-call-arguments.ts: Line 19 Column 11: fbt: unsupported babel node: Identifier
|
||||
* ---
|
||||
* t3
|
||||
* ---
|
||||
*/
|
||||
function Component(t0) {
|
||||
"use memo";
|
||||
const $ = _c(9);
|
||||
const { firstname, lastname } = t0;
|
||||
let t1;
|
||||
if ($[0] !== firstname || $[1] !== lastname) {
|
||||
let t2;
|
||||
if ($[3] !== firstname) {
|
||||
t2 = <Stringify key={0} name={firstname} />;
|
||||
$[3] = firstname;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
let t3;
|
||||
if ($[5] !== lastname) {
|
||||
t3 = <Stringify key={1} name={lastname} />;
|
||||
$[5] = lastname;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
t1 = fbt._(
|
||||
"Name: {firstname}, {lastname}",
|
||||
[
|
||||
fbt._param("firstname", t2),
|
||||
fbt._param(
|
||||
"lastname",
|
||||
identity(
|
||||
fbt._("(inner){lastname}", [fbt._param("lastname", t3)], {
|
||||
hk: "1Kdxyo",
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
{ hk: "3AiIf8" },
|
||||
);
|
||||
$[0] = firstname;
|
||||
$[1] = lastname;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[7] !== t1) {
|
||||
t2 = <div>{t1}</div>;
|
||||
$[7] = t1;
|
||||
$[8] = t2;
|
||||
} else {
|
||||
t2 = $[8];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ firstname: "first", lastname: "last" }],
|
||||
sequentialRenders: [{ firstname: "first", lastname: "last" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>Name: <div>{"name":"first"}</div>, (inner)<div>{"name":"last"}</div></div>
|
||||
@@ -1,5 +1,5 @@
|
||||
import fbt from 'fbt';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
import {Stringify, identity} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
|
||||
@@ -12,7 +12,7 @@ import {Stringify} from 'shared-runtime';
|
||||
function Component({firstname, lastname}) {
|
||||
'use memo';
|
||||
return (
|
||||
<Stringify>
|
||||
<div>
|
||||
{fbt(
|
||||
[
|
||||
'Name: ',
|
||||
@@ -20,14 +20,18 @@ function Component({firstname, lastname}) {
|
||||
', ',
|
||||
fbt.param(
|
||||
'lastname',
|
||||
<Stringify key={0} name={lastname}>
|
||||
{fbt('(inner fbt)', 'Inner fbt value')}
|
||||
</Stringify>
|
||||
identity(
|
||||
fbt(
|
||||
'(inner)' +
|
||||
fbt.param('lastname', <Stringify key={1} name={lastname} />),
|
||||
'Inner fbt value'
|
||||
)
|
||||
)
|
||||
),
|
||||
],
|
||||
'Name'
|
||||
)}
|
||||
</Stringify>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import fbt from 'fbt';
|
||||
import {identity} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
|
||||
* Expected fixture `fbt-param-call-arguments` to succeed but it failed with error:
|
||||
* /fbt-param-call-arguments.ts: Line 19 Column 11: fbt: unsupported babel node: Identifier
|
||||
* ---
|
||||
* t3
|
||||
* ---
|
||||
*/
|
||||
function Component({firstname, lastname}) {
|
||||
'use memo';
|
||||
return (
|
||||
<div>
|
||||
{fbt(
|
||||
[
|
||||
'Name: ',
|
||||
fbt.param('firstname', identity(firstname)),
|
||||
', ',
|
||||
fbt.param(
|
||||
'lastname',
|
||||
identity(
|
||||
fbt(
|
||||
'(inner)' + fbt.param('lastname', identity(lastname)),
|
||||
'Inner fbt value'
|
||||
)
|
||||
)
|
||||
),
|
||||
],
|
||||
'Name'
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstname: 'first', lastname: 'last'}],
|
||||
sequentialRenders: [{firstname: 'first', lastname: 'last'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import fbt from "fbt";
|
||||
import { identity } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
|
||||
* Expected fixture `fbt-param-call-arguments` to succeed but it failed with error:
|
||||
* /fbt-param-call-arguments.ts: Line 19 Column 11: fbt: unsupported babel node: Identifier
|
||||
* ---
|
||||
* t3
|
||||
* ---
|
||||
*/
|
||||
function Component(t0) {
|
||||
"use memo";
|
||||
const $ = _c(5);
|
||||
const { firstname, lastname } = t0;
|
||||
let t1;
|
||||
if ($[0] !== firstname || $[1] !== lastname) {
|
||||
t1 = fbt._(
|
||||
"Name: {firstname}, {lastname}",
|
||||
[
|
||||
fbt._param(
|
||||
"firstname",
|
||||
|
||||
identity(firstname),
|
||||
),
|
||||
fbt._param(
|
||||
"lastname",
|
||||
|
||||
identity(
|
||||
fbt._(
|
||||
"(inner){lastname}",
|
||||
[
|
||||
fbt._param(
|
||||
"lastname",
|
||||
|
||||
identity(lastname),
|
||||
),
|
||||
],
|
||||
{ hk: "1Kdxyo" },
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
{ hk: "3AiIf8" },
|
||||
);
|
||||
$[0] = firstname;
|
||||
$[1] = lastname;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== t1) {
|
||||
t2 = <div>{t1}</div>;
|
||||
$[3] = t1;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ firstname: "first", lastname: "last" }],
|
||||
sequentialRenders: [{ firstname: "first", lastname: "last" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>Name: first, (inner)last</div>
|
||||
@@ -1,9 +1,5 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import fbt from 'fbt';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
import {identity} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
|
||||
@@ -16,22 +12,25 @@ import {Stringify} from 'shared-runtime';
|
||||
function Component({firstname, lastname}) {
|
||||
'use memo';
|
||||
return (
|
||||
<Stringify>
|
||||
<div>
|
||||
{fbt(
|
||||
[
|
||||
'Name: ',
|
||||
fbt.param('firstname', <Stringify key={0} name={firstname} />),
|
||||
fbt.param('firstname', identity(firstname)),
|
||||
', ',
|
||||
fbt.param(
|
||||
'lastname',
|
||||
<Stringify key={0} name={lastname}>
|
||||
{fbt('(inner fbt)', 'Inner fbt value')}
|
||||
</Stringify>
|
||||
identity(
|
||||
fbt(
|
||||
'(inner)' + fbt.param('lastname', identity(lastname)),
|
||||
'Inner fbt value'
|
||||
)
|
||||
)
|
||||
),
|
||||
],
|
||||
'Name'
|
||||
)}
|
||||
</Stringify>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,17 +39,3 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
params: [{firstname: 'first', lastname: 'last'}],
|
||||
sequentialRenders: [{firstname: 'first', lastname: 'last'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Line 19 Column 11: fbt: unsupported babel node: Identifier
|
||||
---
|
||||
t3
|
||||
---
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {fbt} from 'fbt';
|
||||
import {useState} from 'react';
|
||||
|
||||
const MIN = 10;
|
||||
|
||||
function Component() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
return fbt(
|
||||
'Expected at least ' +
|
||||
fbt.param('min', MIN, {number: true}) +
|
||||
' items, but got ' +
|
||||
fbt.param('count', count, {number: true}) +
|
||||
' items.',
|
||||
'Error description'
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { fbt } from "fbt";
|
||||
import { useState } from "react";
|
||||
|
||||
const MIN = 10;
|
||||
|
||||
function Component() {
|
||||
const $ = _c(2);
|
||||
const [count] = useState(0);
|
||||
let t0;
|
||||
if ($[0] !== count) {
|
||||
t0 = fbt._(
|
||||
{ "*": { "*": "Expected at least {min} items, but got {count} items." } },
|
||||
[
|
||||
fbt._param(
|
||||
"min",
|
||||
|
||||
MIN,
|
||||
[0],
|
||||
),
|
||||
fbt._param(
|
||||
"count",
|
||||
|
||||
count,
|
||||
[0],
|
||||
),
|
||||
],
|
||||
{ hk: "36gbz8" },
|
||||
);
|
||||
$[0] = count;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) Expected at least 10 items, but got 0 items.
|
||||
@@ -0,0 +1,22 @@
|
||||
import {fbt} from 'fbt';
|
||||
import {useState} from 'react';
|
||||
|
||||
const MIN = 10;
|
||||
|
||||
function Component() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
return fbt(
|
||||
'Expected at least ' +
|
||||
fbt.param('min', MIN, {number: true}) +
|
||||
' items, but got ' +
|
||||
fbt.param('count', count, {number: true}) +
|
||||
' items.',
|
||||
'Error description'
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function t(props) {
|
||||
let [, setstate] = useState();
|
||||
setstate(1);
|
||||
return props.foo;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: t,
|
||||
params: ['TodoAdd'],
|
||||
isComponent: 'TodoAdd',
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
function t(props) {
|
||||
const [, setstate] = useState();
|
||||
setstate(1);
|
||||
return props.foo;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: t,
|
||||
params: ["TodoAdd"],
|
||||
isComponent: "TodoAdd",
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
function t(props) {
|
||||
let [, setstate] = useState();
|
||||
setstate(1);
|
||||
return props.foo;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: t,
|
||||
params: ['TodoAdd'],
|
||||
isComponent: 'TodoAdd',
|
||||
};
|
||||
@@ -37,7 +37,7 @@ function Component(props) {
|
||||
const $ = _c(16);
|
||||
let t0;
|
||||
if ($[0] !== props) {
|
||||
t0 = idx(props, _temp);
|
||||
t0 = idx(props, (_) => _.group.label);
|
||||
$[0] = props;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
@@ -46,7 +46,7 @@ function Component(props) {
|
||||
const groupName1 = t0;
|
||||
let t1;
|
||||
if ($[2] !== props) {
|
||||
t1 = idx.a(props, _temp2);
|
||||
t1 = idx.a(props, (__0) => __0.group.label);
|
||||
$[2] = props;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
@@ -73,7 +73,7 @@ function Component(props) {
|
||||
const groupName4 = t3;
|
||||
let t4;
|
||||
if ($[8] !== props) {
|
||||
t4 = idx.hello_world.b.c(props, _temp3);
|
||||
t4 = idx.hello_world.b.c(props, (__3) => __3.group.label);
|
||||
$[8] = props;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
@@ -108,15 +108,6 @@ function Component(props) {
|
||||
}
|
||||
return t5;
|
||||
}
|
||||
function _temp3(__3) {
|
||||
return __3.group.label;
|
||||
}
|
||||
function _temp2(__0) {
|
||||
return __0.group.label;
|
||||
}
|
||||
function _temp(_) {
|
||||
return _.group.label;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -31,7 +31,7 @@ function Component(props) {
|
||||
const $ = _c(10);
|
||||
let t0;
|
||||
if ($[0] !== props) {
|
||||
t0 = idx(props, _temp);
|
||||
t0 = idx(props, (_) => _.group.label);
|
||||
$[0] = props;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
@@ -49,7 +49,7 @@ function Component(props) {
|
||||
const groupName2 = t1;
|
||||
let t2;
|
||||
if ($[4] !== props) {
|
||||
t2 = idx.a.b(props, _temp2);
|
||||
t2 = idx.a.b(props, (__1) => __1.group.label);
|
||||
$[4] = props;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
@@ -74,12 +74,6 @@ function Component(props) {
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
function _temp2(__1) {
|
||||
return __1.group.label;
|
||||
}
|
||||
function _temp(_) {
|
||||
return _.group.label;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -46,18 +46,7 @@ error.todo-syntax.ts:11:2
|
||||
> 12 | () => {
|
||||
| ^^^^^^^^^^^
|
||||
> 13 | try {
|
||||
| ^^^^^^^^^^^
|
||||
> 14 | console.log(prop1);
|
||||
| ^^^^^^^^^^^
|
||||
> 15 | } finally {
|
||||
| ^^^^^^^^^^^
|
||||
> 16 | console.log('exiting');
|
||||
| ^^^^^^^^^^^
|
||||
> 17 | }
|
||||
| ^^^^^^^^^^^
|
||||
> 18 | },
|
||||
| ^^^^^^^^^^^
|
||||
> 19 | [prop1],
|
||||
…
|
||||
| ^^^^^^^^^^^
|
||||
> 20 | AUTODEPS
|
||||
| ^^^^^^^^^^^
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoVoidUseMemo @loggerTestOnly
|
||||
function Component() {
|
||||
useMemo(() => {
|
||||
return [];
|
||||
}, []);
|
||||
return <div />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo @loggerTestOnly
|
||||
function Component() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <div />;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() result is unused","description":"This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":3,"column":2,"index":67},"end":{"line":3,"column":9,"index":74},"filename":"invalid-unused-usememo.ts","identifierName":"useMemo"},"message":"useMemo() result is unused"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":42},"end":{"line":7,"column":1,"index":127},"filename":"invalid-unused-usememo.ts"},"fnName":"Component","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,7 @@
|
||||
// @validateNoVoidUseMemo @loggerTestOnly
|
||||
function Component() {
|
||||
useMemo(() => {
|
||||
return [];
|
||||
}, []);
|
||||
return <div />;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoVoidUseMemo @loggerTestOnly
|
||||
function Component() {
|
||||
const value = useMemo(() => {
|
||||
console.log('computing');
|
||||
}, []);
|
||||
const value2 = React.useMemo(() => {
|
||||
console.log('computing');
|
||||
}, []);
|
||||
return (
|
||||
<div>
|
||||
{value}
|
||||
{value2}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo @loggerTestOnly
|
||||
function Component() {
|
||||
const $ = _c(1);
|
||||
|
||||
console.log("computing");
|
||||
|
||||
console.log("computing");
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = (
|
||||
<div>
|
||||
{undefined}
|
||||
{undefined}
|
||||
</div>
|
||||
);
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() callbacks must return a value","description":"This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":3,"column":24,"index":89},"end":{"line":5,"column":3,"index":130},"filename":"invalid-useMemo-no-return-value.ts"},"message":"useMemo() callbacks must return a value"}]}},"fnLoc":null}
|
||||
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() callbacks must return a value","description":"This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":6,"column":31,"index":168},"end":{"line":8,"column":3,"index":209},"filename":"invalid-useMemo-no-return-value.ts"},"message":"useMemo() callbacks must return a value"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":42},"end":{"line":15,"column":1,"index":283},"filename":"invalid-useMemo-no-return-value.ts"},"fnName":"Component","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoVoidUseMemo
|
||||
// @validateNoVoidUseMemo @loggerTestOnly
|
||||
function Component() {
|
||||
const value = useMemo(() => {
|
||||
console.log('computing');
|
||||
@@ -0,0 +1,33 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @loggerTestOnly
|
||||
function component(a) {
|
||||
let x = useMemo(() => {
|
||||
mutate(a);
|
||||
}, []);
|
||||
return x;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @loggerTestOnly
|
||||
function component(a) {
|
||||
mutate(a);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() callbacks must return a value","description":"This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":3,"column":18,"index":61},"end":{"line":5,"column":3,"index":87},"filename":"invalid-useMemo-return-empty.ts"},"message":"useMemo() callbacks must return a value"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":19},"end":{"line":7,"column":1,"index":107},"filename":"invalid-useMemo-return-empty.ts"},"fnName":"component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":1,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -1,3 +1,4 @@
|
||||
// @loggerTestOnly
|
||||
function component(a) {
|
||||
let x = useMemo(() => {
|
||||
mutate(a);
|
||||
@@ -0,0 +1,63 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {identity, Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const {x, ...rest} = useIdentity(props);
|
||||
const z = rest.z;
|
||||
identity(z);
|
||||
return <Stringify x={x} z={z} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: 'Hello', z: 'World'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { identity, Stringify, useIdentity } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(6);
|
||||
const t0 = useIdentity(props);
|
||||
let rest;
|
||||
let x;
|
||||
if ($[0] !== t0) {
|
||||
({ x, ...rest } = t0);
|
||||
$[0] = t0;
|
||||
$[1] = rest;
|
||||
$[2] = x;
|
||||
} else {
|
||||
rest = $[1];
|
||||
x = $[2];
|
||||
}
|
||||
const z = rest.z;
|
||||
identity(z);
|
||||
let t1;
|
||||
if ($[3] !== x || $[4] !== z) {
|
||||
t1 = <Stringify x={x} z={z} />;
|
||||
$[3] = x;
|
||||
$[4] = z;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ x: "Hello", z: "World" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"x":"Hello","z":"World"}</div>
|
||||
@@ -0,0 +1,13 @@
|
||||
import {identity, Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const {x, ...rest} = useIdentity(props);
|
||||
const z = rest.z;
|
||||
identity(z);
|
||||
return <Stringify x={x} z={z} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: 'Hello', z: 'World'}],
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {identity, Stringify} from 'shared-runtime';
|
||||
|
||||
function Component({x, ...rest}) {
|
||||
return <Stringify {...rest} x={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: 'Hello', z: 'World'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { identity, Stringify } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(6);
|
||||
let rest;
|
||||
let x;
|
||||
if ($[0] !== t0) {
|
||||
({ x, ...rest } = t0);
|
||||
$[0] = t0;
|
||||
$[1] = rest;
|
||||
$[2] = x;
|
||||
} else {
|
||||
rest = $[1];
|
||||
x = $[2];
|
||||
}
|
||||
let t1;
|
||||
if ($[3] !== rest || $[4] !== x) {
|
||||
t1 = <Stringify {...rest} x={x} />;
|
||||
$[3] = rest;
|
||||
$[4] = x;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ x: "Hello", z: "World" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"z":"World","x":"Hello"}</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
import {identity, Stringify} from 'shared-runtime';
|
||||
|
||||
function Component({x, ...rest}) {
|
||||
return <Stringify {...rest} x={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: 'Hello', z: 'World'}],
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {identity, Stringify} from 'shared-runtime';
|
||||
|
||||
function Component({x, ...rest}) {
|
||||
const restAlias = rest;
|
||||
const z = restAlias.z;
|
||||
identity(z);
|
||||
return <Stringify x={x} z={z} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: 'Hello', z: 'World'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { identity, Stringify } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(6);
|
||||
let rest;
|
||||
let x;
|
||||
if ($[0] !== t0) {
|
||||
({ x, ...rest } = t0);
|
||||
$[0] = t0;
|
||||
$[1] = rest;
|
||||
$[2] = x;
|
||||
} else {
|
||||
rest = $[1];
|
||||
x = $[2];
|
||||
}
|
||||
const restAlias = rest;
|
||||
const z = restAlias.z;
|
||||
identity(z);
|
||||
let t1;
|
||||
if ($[3] !== x || $[4] !== z) {
|
||||
t1 = <Stringify x={x} z={z} />;
|
||||
$[3] = x;
|
||||
$[4] = z;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ x: "Hello", z: "World" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"x":"Hello","z":"World"}</div>
|
||||
@@ -0,0 +1,13 @@
|
||||
import {identity, Stringify} from 'shared-runtime';
|
||||
|
||||
function Component({x, ...rest}) {
|
||||
const restAlias = rest;
|
||||
const z = restAlias.z;
|
||||
identity(z);
|
||||
return <Stringify x={x} z={z} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: 'Hello', z: 'World'}],
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {identity, Stringify} from 'shared-runtime';
|
||||
|
||||
function Component({x, ...rest}) {
|
||||
const z = rest.z;
|
||||
identity(z);
|
||||
return <Stringify x={x} z={z} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: 'Hello', z: 'World'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { identity, Stringify } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(6);
|
||||
let rest;
|
||||
let x;
|
||||
if ($[0] !== t0) {
|
||||
({ x, ...rest } = t0);
|
||||
$[0] = t0;
|
||||
$[1] = rest;
|
||||
$[2] = x;
|
||||
} else {
|
||||
rest = $[1];
|
||||
x = $[2];
|
||||
}
|
||||
const z = rest.z;
|
||||
identity(z);
|
||||
let t1;
|
||||
if ($[3] !== x || $[4] !== z) {
|
||||
t1 = <Stringify x={x} z={z} />;
|
||||
$[3] = x;
|
||||
$[4] = z;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ x: "Hello", z: "World" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"x":"Hello","z":"World"}</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
import {identity, Stringify} from 'shared-runtime';
|
||||
|
||||
function Component({x, ...rest}) {
|
||||
const z = rest.z;
|
||||
identity(z);
|
||||
return <Stringify x={x} z={z} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: 'Hello', z: 'World'}],
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {identity, mutate, mutateAndReturn} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const key = {};
|
||||
const context = {
|
||||
[(mutate(key), key)]: identity([props.value]),
|
||||
};
|
||||
mutate(key);
|
||||
return [context, key];
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
sequentialRenders: [{value: 42}, {value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { identity, mutate, mutateAndReturn } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== props.value) {
|
||||
const key = {};
|
||||
const context = { [(mutate(key), key)]: identity([props.value]) };
|
||||
mutate(key);
|
||||
t0 = [context, key];
|
||||
$[0] = props.value;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: 42 }],
|
||||
sequentialRenders: [{ value: 42 }, { value: 42 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
[{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
@@ -6,10 +6,11 @@ function Component(props) {
|
||||
[(mutate(key), key)]: identity([props.value]),
|
||||
};
|
||||
mutate(key);
|
||||
return context;
|
||||
return [context, key];
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
sequentialRenders: [{value: 42}, {value: 42}],
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {identity, mutate, mutateAndReturn} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const key = {};
|
||||
const context = {
|
||||
[mutateAndReturn(key)]: identity([props.value]),
|
||||
};
|
||||
mutate(key);
|
||||
return context;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
sequentialRenders: [{value: 42}, {value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { identity, mutate, mutateAndReturn } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(2);
|
||||
let context;
|
||||
if ($[0] !== props.value) {
|
||||
const key = {};
|
||||
context = { [mutateAndReturn(key)]: identity([props.value]) };
|
||||
mutate(key);
|
||||
$[0] = props.value;
|
||||
$[1] = context;
|
||||
} else {
|
||||
context = $[1];
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: 42 }],
|
||||
sequentialRenders: [{ value: 42 }, { value: 42 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) {"[object Object]":[42]}
|
||||
{"[object Object]":[42]}
|
||||
@@ -12,4 +12,5 @@ function Component(props) {
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
sequentialRenders: [{value: 42}, {value: 42}],
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {identity, mutate, mutateAndReturn} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const key = {};
|
||||
const context = {
|
||||
[mutateAndReturn(key)]: identity([props.value]),
|
||||
};
|
||||
return context;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { identity, mutate, mutateAndReturn } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(5);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
const key = {};
|
||||
|
||||
t0 = mutateAndReturn(key);
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
let t1;
|
||||
if ($[1] !== props.value) {
|
||||
t1 = identity([props.value]);
|
||||
$[1] = props.value;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== t1) {
|
||||
t2 = { [t0]: t1 };
|
||||
$[3] = t1;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
const context = t2;
|
||||
return context;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: 42 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) {"[object Object]":[42]}
|
||||
@@ -0,0 +1,54 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {identity, mutate, mutateAndReturn} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const obj = {mutateAndReturn};
|
||||
const key = {};
|
||||
const context = {
|
||||
[obj.mutateAndReturn(key)]: identity([props.value]),
|
||||
};
|
||||
mutate(key);
|
||||
return context;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { identity, mutate, mutateAndReturn } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(2);
|
||||
let context;
|
||||
if ($[0] !== props.value) {
|
||||
const obj = { mutateAndReturn };
|
||||
const key = {};
|
||||
context = { [obj.mutateAndReturn(key)]: identity([props.value]) };
|
||||
mutate(key);
|
||||
$[0] = props.value;
|
||||
$[1] = context;
|
||||
} else {
|
||||
context = $[1];
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: 42 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) {"[object Object]":[42]}
|
||||
@@ -0,0 +1,52 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
import {useCallback, useTransition} from 'react';
|
||||
|
||||
function useFoo() {
|
||||
const [, /* isPending intentionally not captured */ start] = useTransition();
|
||||
|
||||
return useCallback(() => {
|
||||
start();
|
||||
}, []);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
|
||||
import { useCallback, useTransition } from "react";
|
||||
|
||||
function useFoo() {
|
||||
const $ = _c(1);
|
||||
const [, start] = useTransition();
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {
|
||||
start();
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) "[[ function params=0 ]]"
|
||||
@@ -0,0 +1,15 @@
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
import {useCallback, useTransition} from 'react';
|
||||
|
||||
function useFoo() {
|
||||
const [, /* isPending intentionally not captured */ start] = useTransition();
|
||||
|
||||
return useCallback(() => {
|
||||
start();
|
||||
}, []);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
import {useCallback, useTransition} from 'react';
|
||||
|
||||
function useFoo() {
|
||||
const [, /* state value intentionally not captured */ setState] = useState();
|
||||
|
||||
return useCallback(() => {
|
||||
setState(x => x + 1);
|
||||
}, []);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
|
||||
import { useCallback, useTransition } from "react";
|
||||
|
||||
function useFoo() {
|
||||
const $ = _c(1);
|
||||
const [, setState] = useState();
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {
|
||||
setState(_temp);
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
function _temp(x) {
|
||||
return x + 1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) useState is not defined
|
||||
@@ -0,0 +1,15 @@
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
import {useCallback, useTransition} from 'react';
|
||||
|
||||
function useFoo() {
|
||||
const [, /* state value intentionally not captured */ setState] = useState();
|
||||
|
||||
return useCallback(() => {
|
||||
setState(x => x + 1);
|
||||
}, []);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow
|
||||
export hook useItemLanguage(items) {
|
||||
return useMemo(() => {
|
||||
let language: ?string = null;
|
||||
items.forEach(item => {
|
||||
if (item.language != null) {
|
||||
language = item.language;
|
||||
}
|
||||
});
|
||||
return language;
|
||||
}, [items]);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
export function useItemLanguage(items) {
|
||||
const $ = _c(2);
|
||||
let language;
|
||||
if ($[0] !== items) {
|
||||
language = null;
|
||||
items.forEach((item) => {
|
||||
if (item.language != null) {
|
||||
language = item.language;
|
||||
}
|
||||
});
|
||||
$[0] = items;
|
||||
$[1] = language;
|
||||
} else {
|
||||
language = $[1];
|
||||
}
|
||||
return language;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
export hook useItemLanguage(items) {
|
||||
return useMemo(() => {
|
||||
let language: ?string = null;
|
||||
items.forEach(item => {
|
||||
if (item.language != null) {
|
||||
language = item.language;
|
||||
}
|
||||
});
|
||||
return language;
|
||||
}, [items]);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoVoidUseMemo:false
|
||||
function Component(props) {
|
||||
const item = props.item;
|
||||
const thumbnails = [];
|
||||
@@ -22,7 +23,7 @@ function Component(props) {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo:false
|
||||
function Component(props) {
|
||||
const $ = _c(6);
|
||||
const item = props.item;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @validateNoVoidUseMemo:false
|
||||
function Component(props) {
|
||||
const item = props.item;
|
||||
const thumbnails = [];
|
||||
|
||||
@@ -6,6 +6,7 @@ function Component(props) {
|
||||
const x = useMemo(() => {
|
||||
if (props.cond) {
|
||||
if (props.cond) {
|
||||
return props.value;
|
||||
}
|
||||
}
|
||||
}, [props.cond]);
|
||||
@@ -24,10 +25,18 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
```javascript
|
||||
function Component(props) {
|
||||
if (props.cond) {
|
||||
let t0;
|
||||
bb0: {
|
||||
if (props.cond) {
|
||||
if (props.cond) {
|
||||
t0 = props.value;
|
||||
break bb0;
|
||||
}
|
||||
}
|
||||
t0 = undefined;
|
||||
}
|
||||
const x = t0;
|
||||
return x;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -2,6 +2,7 @@ function Component(props) {
|
||||
const x = useMemo(() => {
|
||||
if (props.cond) {
|
||||
if (props.cond) {
|
||||
return props.value;
|
||||
}
|
||||
}
|
||||
}, [props.cond]);
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function component(a) {
|
||||
let x = useMemo(() => {
|
||||
mutate(a);
|
||||
}, []);
|
||||
return x;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
function component(a) {
|
||||
mutate(a);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -14,6 +14,7 @@ export {
|
||||
ErrorSeverity,
|
||||
ErrorCategory,
|
||||
LintRules,
|
||||
LintRulePreset,
|
||||
type CompilerErrorDetailOptions,
|
||||
type CompilerDiagnosticOptions,
|
||||
type CompilerDiagnosticDetail,
|
||||
@@ -28,10 +29,13 @@ export {
|
||||
ProgramContext,
|
||||
tryFindDirectiveEnablingMemoization as findDirectiveEnablingMemoization,
|
||||
findDirectiveDisablingMemoization,
|
||||
defaultOptions,
|
||||
type CompilerPipelineValue,
|
||||
type Logger,
|
||||
type LoggerEvent,
|
||||
type PluginOptions,
|
||||
type AutoDepsDecorationsEvent,
|
||||
type CompileSuccessEvent,
|
||||
} from './Entrypoint';
|
||||
export {
|
||||
Effect,
|
||||
|
||||
@@ -1,58 +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 {NoUnusedDirectivesRule} from '../src/rules/ReactCompilerRule';
|
||||
import {normalizeIndent, testRule} from './shared-utils';
|
||||
|
||||
testRule('no unused directives rule', NoUnusedDirectivesRule, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
name: "Unused 'use no forget' directive is reported when no errors are present on components",
|
||||
code: normalizeIndent`
|
||||
function Component() {
|
||||
'use no forget';
|
||||
return <div>Hello world</div>
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
message: "Unused 'use no forget' directive",
|
||||
suggestions: [
|
||||
{
|
||||
output:
|
||||
// yuck
|
||||
'\nfunction Component() {\n \n return <div>Hello world</div>\n}\n',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: "Unused 'use no forget' directive is reported when no errors are present on non-components or hooks",
|
||||
code: normalizeIndent`
|
||||
function notacomponent() {
|
||||
'use no forget';
|
||||
return 1 + 1;
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
message: "Unused 'use no forget' directive",
|
||||
suggestions: [
|
||||
{
|
||||
output:
|
||||
// yuck
|
||||
'\nfunction notacomponent() {\n \n return 1 + 1;\n}\n',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -120,7 +120,15 @@ testRule('plugin-recommended', TestRecommendedRules, {
|
||||
|
||||
return <Child x={state} />;
|
||||
}`,
|
||||
errors: [makeTestCaseError('useMemo() callbacks must return a value')],
|
||||
errors: [
|
||||
makeTestCaseError('useMemo() callbacks must return a value'),
|
||||
makeTestCaseError(
|
||||
'Calling setState from useMemo may trigger an infinite loop',
|
||||
),
|
||||
makeTestCaseError(
|
||||
'Calling setState from useMemo may trigger an infinite loop',
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Pipeline errors are reported',
|
||||
|
||||
@@ -10,6 +10,5 @@ module.exports = {
|
||||
plugins: [
|
||||
['@babel/plugin-transform-private-property-in-object', {loose: true}],
|
||||
['@babel/plugin-transform-class-properties', {loose: true}],
|
||||
['@babel/plugin-transform-private-methods', {loose: true}],
|
||||
],
|
||||
};
|
||||
|
||||
@@ -14,10 +14,9 @@
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.24.4",
|
||||
"@babel/parser": "^7.24.4",
|
||||
"@babel/plugin-proposal-private-methods": "^7.18.6",
|
||||
"hermes-parser": "^0.25.1",
|
||||
"zod": "^3.22.4",
|
||||
"zod-validation-error": "^3.0.3"
|
||||
"zod": "^3.25.0 || ^4.0.0",
|
||||
"zod-validation-error": "^3.5.0 || ^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.22.4",
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {Linter, Rule} from 'eslint';
|
||||
import runReactCompiler, {RunCacheEntry} from '../shared/RunReactCompiler';
|
||||
import {
|
||||
ErrorSeverity,
|
||||
LintRulePreset,
|
||||
LintRules,
|
||||
type LintRule,
|
||||
} from 'babel-plugin-react-compiler/src/CompilerError';
|
||||
@@ -150,7 +151,7 @@ function makeRule(rule: LintRule): Rule.RuleModule {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: rule.description,
|
||||
recommended: rule.recommended,
|
||||
recommended: rule.preset === LintRulePreset.Recommended,
|
||||
},
|
||||
fixable: 'code',
|
||||
hasSuggestions: true,
|
||||
@@ -161,69 +162,30 @@ function makeRule(rule: LintRule): Rule.RuleModule {
|
||||
};
|
||||
}
|
||||
|
||||
export const NoUnusedDirectivesRule: Rule.RuleModule = {
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
recommended: true,
|
||||
},
|
||||
fixable: 'code',
|
||||
hasSuggestions: true,
|
||||
// validation is done at runtime with zod
|
||||
schema: [{type: 'object', additionalProperties: true}],
|
||||
},
|
||||
create(context: Rule.RuleContext): Rule.RuleListener {
|
||||
const results = getReactCompilerResult(context);
|
||||
|
||||
for (const directive of results.unusedOptOutDirectives) {
|
||||
context.report({
|
||||
message: `Unused '${directive.directive}' directive`,
|
||||
loc: directive.loc,
|
||||
suggest: [
|
||||
{
|
||||
desc: 'Remove the directive',
|
||||
fix(fixer): Rule.Fix {
|
||||
return fixer.removeRange(directive.range);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
return {};
|
||||
},
|
||||
};
|
||||
|
||||
type RulesConfig = {
|
||||
[name: string]: {rule: Rule.RuleModule; severity: ErrorSeverity};
|
||||
};
|
||||
|
||||
export const allRules: RulesConfig = LintRules.reduce(
|
||||
(acc, rule) => {
|
||||
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
'no-unused-directives': {
|
||||
rule: NoUnusedDirectivesRule,
|
||||
severity: ErrorSeverity.Error,
|
||||
},
|
||||
} as RulesConfig,
|
||||
);
|
||||
export const allRules: RulesConfig = LintRules.reduce((acc, rule) => {
|
||||
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
|
||||
return acc;
|
||||
}, {} as RulesConfig);
|
||||
|
||||
export const recommendedRules: RulesConfig = LintRules.filter(
|
||||
rule => rule.recommended,
|
||||
).reduce(
|
||||
(acc, rule) => {
|
||||
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
'no-unused-directives': {
|
||||
rule: NoUnusedDirectivesRule,
|
||||
severity: ErrorSeverity.Error,
|
||||
},
|
||||
} as RulesConfig,
|
||||
);
|
||||
rule => rule.preset === LintRulePreset.Recommended,
|
||||
).reduce((acc, rule) => {
|
||||
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
|
||||
return acc;
|
||||
}, {} as RulesConfig);
|
||||
|
||||
export const recommendedLatestRules: RulesConfig = LintRules.filter(
|
||||
rule =>
|
||||
rule.preset === LintRulePreset.Recommended ||
|
||||
rule.preset === LintRulePreset.RecommendedLatest,
|
||||
).reduce((acc, rule) => {
|
||||
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
|
||||
return acc;
|
||||
}, {} as RulesConfig);
|
||||
|
||||
export function mapErrorSeverityToESlint(
|
||||
severity: ErrorSeverity,
|
||||
|
||||
@@ -5,20 +5,16 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {transformFromAstSync, traverse} from '@babel/core';
|
||||
import {transformFromAstSync} from '@babel/core';
|
||||
import {parse as babelParse} from '@babel/parser';
|
||||
import {Directive, File} from '@babel/types';
|
||||
// @ts-expect-error: no types available
|
||||
import PluginProposalPrivateMethods from '@babel/plugin-proposal-private-methods';
|
||||
import {File} from '@babel/types';
|
||||
import BabelPluginReactCompiler, {
|
||||
parsePluginOptions,
|
||||
validateEnvironmentConfig,
|
||||
OPT_OUT_DIRECTIVES,
|
||||
type PluginOptions,
|
||||
} from 'babel-plugin-react-compiler/src';
|
||||
import {Logger, LoggerEvent} from 'babel-plugin-react-compiler/src/Entrypoint';
|
||||
import type {SourceCode} from 'eslint';
|
||||
import {SourceLocation} from 'estree';
|
||||
// @ts-expect-error: no types available
|
||||
import * as HermesParser from 'hermes-parser';
|
||||
import {isDeepStrictEqual} from 'util';
|
||||
@@ -45,17 +41,11 @@ const COMPILER_OPTIONS: PluginOptions = {
|
||||
}),
|
||||
};
|
||||
|
||||
export type UnusedOptOutDirective = {
|
||||
loc: SourceLocation;
|
||||
range: [number, number];
|
||||
directive: string;
|
||||
};
|
||||
export type RunCacheEntry = {
|
||||
sourceCode: string;
|
||||
filename: string;
|
||||
userOpts: PluginOptions;
|
||||
flowSuppressions: Array<{line: number; code: string}>;
|
||||
unusedOptOutDirectives: Array<UnusedOptOutDirective>;
|
||||
events: Array<LoggerEvent>;
|
||||
};
|
||||
|
||||
@@ -87,25 +77,6 @@ function getFlowSuppressions(
|
||||
return results;
|
||||
}
|
||||
|
||||
function filterUnusedOptOutDirectives(
|
||||
directives: ReadonlyArray<Directive>,
|
||||
): Array<UnusedOptOutDirective> {
|
||||
const results: Array<UnusedOptOutDirective> = [];
|
||||
for (const directive of directives) {
|
||||
if (
|
||||
OPT_OUT_DIRECTIVES.has(directive.value.value) &&
|
||||
directive.loc != null
|
||||
) {
|
||||
results.push({
|
||||
loc: directive.loc,
|
||||
directive: directive.value.value,
|
||||
range: [directive.start!, directive.end!],
|
||||
});
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function runReactCompilerImpl({
|
||||
sourceCode,
|
||||
filename,
|
||||
@@ -125,7 +96,6 @@ function runReactCompilerImpl({
|
||||
filename,
|
||||
userOpts,
|
||||
flowSuppressions: [],
|
||||
unusedOptOutDirectives: [],
|
||||
events: [],
|
||||
};
|
||||
const userLogger: Logger | null = options.logger;
|
||||
@@ -173,37 +143,11 @@ function runReactCompilerImpl({
|
||||
filename,
|
||||
highlightCode: false,
|
||||
retainLines: true,
|
||||
plugins: [
|
||||
[PluginProposalPrivateMethods, {loose: true}],
|
||||
[BabelPluginReactCompiler, options],
|
||||
],
|
||||
plugins: [[BabelPluginReactCompiler, options]],
|
||||
sourceType: 'module',
|
||||
configFile: false,
|
||||
babelrc: false,
|
||||
});
|
||||
|
||||
if (results.events.filter(e => e.kind === 'CompileError').length === 0) {
|
||||
traverse(babelAST, {
|
||||
FunctionDeclaration(path) {
|
||||
path.node;
|
||||
results.unusedOptOutDirectives.push(
|
||||
...filterUnusedOptOutDirectives(path.node.body.directives),
|
||||
);
|
||||
},
|
||||
ArrowFunctionExpression(path) {
|
||||
if (path.node.body.type === 'BlockStatement') {
|
||||
results.unusedOptOutDirectives.push(
|
||||
...filterUnusedOptOutDirectives(path.node.body.directives),
|
||||
);
|
||||
}
|
||||
},
|
||||
FunctionExpression(path) {
|
||||
results.unusedOptOutDirectives.push(
|
||||
...filterUnusedOptOutDirectives(path.node.body.directives),
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
/* errors handled by injected logger */
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user