Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
294fa2c422 | ||
|
|
129aa85e16 | ||
|
|
bcea86945c | ||
|
|
2ae8b3dacf | ||
|
|
7f510554ad | ||
|
|
a39da6c61f | ||
|
|
48bc166428 | ||
|
|
72848027a5 | ||
|
|
707e321f8f | ||
|
|
0d39496eab | ||
|
|
6f4294af9b | ||
|
|
448f781a52 | ||
|
|
5020d48d28 | ||
|
|
3082604bdc | ||
|
|
4f34cc4a2e | ||
|
|
3d14fcf03f | ||
|
|
edac0dded9 | ||
|
|
3586a7f9e8 | ||
|
|
f6fb1a07a5 | ||
|
|
7513996f20 | ||
|
|
bb4418d647 | ||
|
|
074e92777c | ||
|
|
ac7da9d46d | ||
|
|
0dca9c2471 | ||
|
|
b9af1404ea | ||
|
|
e9638c33d7 | ||
|
|
28d4bc496b | ||
|
|
dffacc7b80 | ||
|
|
da7487b681 | ||
|
|
9fec565a9b | ||
|
|
996d0eb055 | ||
|
|
d85ec5f5bd | ||
|
|
fe813143e2 | ||
|
|
2f0e7e570d | ||
|
|
56d0ddae18 | ||
|
|
345ca24f13 | ||
|
|
97cdd5d3c3 | ||
|
|
eb7f8b42c9 | ||
|
|
eed2560762 | ||
|
|
463b808176 | ||
|
|
96c61b7f1f | ||
|
|
0bfa404bac | ||
|
|
81e1ee7476 | ||
|
|
4a3ff8eed6 | ||
|
|
ec4374c387 | ||
|
|
60b5271a9a | ||
|
|
033edca721 | ||
|
|
e6dc25daea | ||
|
|
150f022444 | ||
|
|
49ded1d12a | ||
|
|
3a43e72d66 | ||
|
|
8ba3501cd9 |
@@ -474,7 +474,7 @@ module.exports = {
|
||||
{
|
||||
files: ['packages/react-server-dom-turbopack/**/*.js'],
|
||||
globals: {
|
||||
__turbopack_load__: 'readonly',
|
||||
__turbopack_load_by_url__: 'readonly',
|
||||
__turbopack_require__: 'readonly',
|
||||
},
|
||||
},
|
||||
|
||||
44
.github/workflows/runtime_build_and_test.yml
vendored
44
.github/workflows/runtime_build_and_test.yml
vendored
@@ -6,6 +6,12 @@ on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- compiler/**
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
commit_sha:
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
permissions: {}
|
||||
|
||||
@@ -28,7 +34,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Check cache hit
|
||||
uses: actions/cache/restore@v4
|
||||
id: node_modules
|
||||
@@ -69,7 +75,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Check cache hit
|
||||
uses: actions/cache/restore@v4
|
||||
id: node_modules
|
||||
@@ -117,7 +123,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/github-script@v7
|
||||
id: set-matrix
|
||||
with:
|
||||
@@ -136,7 +142,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -166,7 +172,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -198,7 +204,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -254,7 +260,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -325,7 +331,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -420,7 +426,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -465,7 +471,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -493,7 +499,7 @@ jobs:
|
||||
merge-multiple: true
|
||||
- name: Display structure of build
|
||||
run: ls -R build
|
||||
- run: echo ${{ github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
|
||||
- run: echo ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
|
||||
- name: Scrape warning messages
|
||||
run: |
|
||||
mkdir -p ./build/__test_utils__
|
||||
@@ -530,7 +536,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -570,7 +576,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -607,7 +613,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -648,7 +654,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -722,7 +728,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -779,7 +785,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -824,7 +830,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -873,7 +879,7 @@ jobs:
|
||||
node ./scripts/print-warnings/print-warnings.js > build/__test_utils__/ReactAllWarnings.js
|
||||
- name: Display structure of build for PR
|
||||
run: ls -R build
|
||||
- run: echo ${{ github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
|
||||
- run: echo ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
|
||||
- run: node ./scripts/tasks/danger
|
||||
- name: Archive sizebot results
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { c as _c } from "react/compiler-runtime"; //
|
||||
@compilationMode:"all"
|
||||
@compilationMode:"all"
|
||||
function nonReactFn() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as t from '@babel/types';
|
||||
import BabelPluginReactCompiler, {
|
||||
CompilerError,
|
||||
CompilerErrorDetail,
|
||||
CompilerDiagnostic,
|
||||
Effect,
|
||||
ErrorSeverity,
|
||||
parseConfigPragmaForTests,
|
||||
@@ -44,6 +45,7 @@ import {
|
||||
PrintedCompilerPipelineValue,
|
||||
} from './Output';
|
||||
import {transformFromAstSync} from '@babel/core';
|
||||
import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint';
|
||||
|
||||
function parseInput(
|
||||
input: string,
|
||||
@@ -140,9 +142,13 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
|
||||
],
|
||||
];
|
||||
|
||||
function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
function compile(
|
||||
source: string,
|
||||
mode: 'compiler' | 'linter',
|
||||
): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
|
||||
const error = new CompilerError();
|
||||
const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
|
||||
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
|
||||
const entry = results.get(result.name);
|
||||
if (Array.isArray(entry)) {
|
||||
@@ -201,6 +207,22 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
};
|
||||
const parsedOptions = parseConfigPragmaForTests(pragma, {
|
||||
compilationMode: 'infer',
|
||||
environment:
|
||||
mode === 'linter'
|
||||
? {
|
||||
// enabled in compiler
|
||||
validateRefAccessDuringRender: false,
|
||||
// enabled in linter
|
||||
validateNoSetStateInRender: true,
|
||||
validateNoSetStateInEffects: true,
|
||||
validateNoJSXInTryStatements: true,
|
||||
validateNoImpureFunctionsInRender: true,
|
||||
validateStaticComponents: true,
|
||||
validateNoFreezingKnownMutableFunctions: true,
|
||||
}
|
||||
: {
|
||||
/* use defaults for compiler mode */
|
||||
},
|
||||
});
|
||||
const opts: PluginOptions = parsePluginOptions({
|
||||
...parsedOptions,
|
||||
@@ -210,7 +232,11 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
},
|
||||
logger: {
|
||||
debugLogIRs: logIR,
|
||||
logEvent: () => {},
|
||||
logEvent: (_filename: string | null, event: LoggerEvent) => {
|
||||
if (event.kind === 'CompileError') {
|
||||
otherErrors.push(event.detail);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
transformOutput = invokeCompiler(source, language, opts);
|
||||
@@ -220,7 +246,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
* (i.e. object shape that is not CompilerError)
|
||||
*/
|
||||
if (err instanceof CompilerError && err.details.length > 0) {
|
||||
error.details.push(...err.details);
|
||||
error.merge(err);
|
||||
} else {
|
||||
/**
|
||||
* Handle unexpected failures by logging (to get a stack trace)
|
||||
@@ -237,10 +263,17 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (error.hasErrors()) {
|
||||
return [{kind: 'err', results, error: error}, language];
|
||||
// Only include logger errors if there weren't other errors
|
||||
if (!error.hasErrors() && otherErrors.length !== 0) {
|
||||
otherErrors.forEach(e => error.details.push(e));
|
||||
}
|
||||
return [{kind: 'ok', results, transformOutput}, language];
|
||||
if (error.hasErrors()) {
|
||||
return [{kind: 'err', results, error}, language];
|
||||
}
|
||||
return [
|
||||
{kind: 'ok', results, transformOutput, errors: error.details},
|
||||
language,
|
||||
];
|
||||
}
|
||||
|
||||
export default function Editor(): JSX.Element {
|
||||
@@ -249,7 +282,11 @@ export default function Editor(): JSX.Element {
|
||||
const dispatchStore = useStoreDispatch();
|
||||
const {enqueueSnackbar} = useSnackbar();
|
||||
const [compilerOutput, language] = useMemo(
|
||||
() => compile(deferredStore.source),
|
||||
() => compile(deferredStore.source, 'compiler'),
|
||||
[deferredStore.source],
|
||||
);
|
||||
const [linterOutput] = useMemo(
|
||||
() => compile(deferredStore.source, 'linter'),
|
||||
[deferredStore.source],
|
||||
);
|
||||
|
||||
@@ -275,19 +312,26 @@ export default function Editor(): JSX.Element {
|
||||
});
|
||||
});
|
||||
|
||||
let mergedOutput: CompilerOutput;
|
||||
let errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
|
||||
if (compilerOutput.kind === 'ok') {
|
||||
errors = linterOutput.kind === 'ok' ? [] : linterOutput.error.details;
|
||||
mergedOutput = {
|
||||
...compilerOutput,
|
||||
errors,
|
||||
};
|
||||
} else {
|
||||
mergedOutput = compilerOutput;
|
||||
errors = compilerOutput.error.details;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex basis top-14">
|
||||
<div className={clsx('relative sm:basis-1/4')}>
|
||||
<Input
|
||||
language={language}
|
||||
errors={
|
||||
compilerOutput.kind === 'err' ? compilerOutput.error.details : []
|
||||
}
|
||||
/>
|
||||
<Input language={language} errors={errors} />
|
||||
</div>
|
||||
<div className={clsx('flex sm:flex flex-wrap')}>
|
||||
<Output store={deferredStore} compilerOutput={compilerOutput} />
|
||||
<Output store={deferredStore} compilerOutput={mergedOutput} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -36,13 +36,18 @@ export default function Input({errors, language}: Props): JSX.Element {
|
||||
const uri = monaco.Uri.parse(`file:///index.js`);
|
||||
const model = monaco.editor.getModel(uri);
|
||||
invariant(model, 'Model must exist for the selected input file.');
|
||||
renderReactCompilerMarkers({monaco, model, details: errors});
|
||||
renderReactCompilerMarkers({
|
||||
monaco,
|
||||
model,
|
||||
details: errors,
|
||||
source: store.source,
|
||||
});
|
||||
/**
|
||||
* N.B. that `tabSize` is a model property, not an editor property.
|
||||
* So, the tab size has to be set per model.
|
||||
*/
|
||||
model.updateOptions({tabSize: 2});
|
||||
}, [monaco, errors]);
|
||||
}, [monaco, errors, store.source]);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
|
||||
@@ -11,7 +11,11 @@ import {
|
||||
InformationCircleIcon,
|
||||
} from '@heroicons/react/outline';
|
||||
import MonacoEditor, {DiffEditor} from '@monaco-editor/react';
|
||||
import {type CompilerError} from 'babel-plugin-react-compiler';
|
||||
import {
|
||||
CompilerErrorDetail,
|
||||
CompilerDiagnostic,
|
||||
type CompilerError,
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import parserBabel from 'prettier/plugins/babel';
|
||||
import * as prettierPluginEstree from 'prettier/plugins/estree';
|
||||
import * as prettier from 'prettier/standalone';
|
||||
@@ -44,6 +48,7 @@ export type CompilerOutput =
|
||||
kind: 'ok';
|
||||
transformOutput: CompilerTransformOutput;
|
||||
results: Map<string, Array<PrintedCompilerPipelineValue>>;
|
||||
errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
|
||||
}
|
||||
| {
|
||||
kind: 'err';
|
||||
@@ -123,10 +128,36 @@ async function tabify(
|
||||
parser: transformOutput.language === 'flow' ? 'babel-flow' : 'babel-ts',
|
||||
plugins: [parserBabel, prettierPluginEstree],
|
||||
});
|
||||
|
||||
let output: string;
|
||||
let language: string;
|
||||
if (compilerOutput.errors.length === 0) {
|
||||
output = code;
|
||||
language = 'javascript';
|
||||
} else {
|
||||
language = 'markdown';
|
||||
output = `
|
||||
# Summary
|
||||
|
||||
React Compiler compiled this function successfully, but there are lint errors that indicate potential issues with the original code.
|
||||
|
||||
## ${compilerOutput.errors.length} Lint Errors
|
||||
|
||||
${compilerOutput.errors.map(e => e.printErrorMessage(source, {eslint: false})).join('\n\n')}
|
||||
|
||||
## Output
|
||||
|
||||
\`\`\`js
|
||||
${code}
|
||||
\`\`\`
|
||||
`.trim();
|
||||
}
|
||||
|
||||
reorderedTabs.set(
|
||||
'JS',
|
||||
'Output',
|
||||
<TextTabContent
|
||||
output={code}
|
||||
output={output}
|
||||
language={language}
|
||||
diff={null}
|
||||
showInfoPanel={false}></TextTabContent>,
|
||||
);
|
||||
@@ -142,6 +173,18 @@ async function tabify(
|
||||
</>,
|
||||
);
|
||||
}
|
||||
} else if (compilerOutput.kind === 'err') {
|
||||
const errors = compilerOutput.error.printErrorMessage(source, {
|
||||
eslint: false,
|
||||
});
|
||||
reorderedTabs.set(
|
||||
'Output',
|
||||
<TextTabContent
|
||||
output={errors}
|
||||
language="markdown"
|
||||
diff={null}
|
||||
showInfoPanel={false}></TextTabContent>,
|
||||
);
|
||||
}
|
||||
tabs.forEach((tab, name) => {
|
||||
reorderedTabs.set(name, tab);
|
||||
@@ -162,17 +205,32 @@ function getSourceMapUrl(code: string, map: string): string | null {
|
||||
}
|
||||
|
||||
function Output({store, compilerOutput}: Props): JSX.Element {
|
||||
const [tabsOpen, setTabsOpen] = useState<Set<string>>(() => new Set(['JS']));
|
||||
const [tabsOpen, setTabsOpen] = useState<Set<string>>(
|
||||
() => new Set(['Output']),
|
||||
);
|
||||
const [tabs, setTabs] = useState<Map<string, React.ReactNode>>(
|
||||
() => new Map(),
|
||||
);
|
||||
|
||||
/*
|
||||
* Update the active tab back to the output or errors tab when the compilation state
|
||||
* changes between success/failure.
|
||||
*/
|
||||
const [previousOutputKind, setPreviousOutputKind] = useState(
|
||||
compilerOutput.kind,
|
||||
);
|
||||
if (compilerOutput.kind !== previousOutputKind) {
|
||||
setPreviousOutputKind(compilerOutput.kind);
|
||||
setTabsOpen(new Set(['Output']));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
tabify(store.source, compilerOutput).then(tabs => {
|
||||
setTabs(tabs);
|
||||
});
|
||||
}, [store.source, compilerOutput]);
|
||||
|
||||
const changedPasses: Set<string> = new Set(['JS', 'HIR']); // Initial and final passes should always be bold
|
||||
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) {
|
||||
for (const result of results) {
|
||||
@@ -196,20 +254,6 @@ function Output({store, compilerOutput}: Props): JSX.Element {
|
||||
tabs={tabs}
|
||||
changedPasses={changedPasses}
|
||||
/>
|
||||
{compilerOutput.kind === 'err' ? (
|
||||
<div
|
||||
className="flex flex-wrap absolute bottom-0 bg-white grow border-y border-grey-200 transition-all ease-in"
|
||||
style={{width: 'calc(100vw - 650px)'}}>
|
||||
<div className="w-full p-4 basis-full border-b">
|
||||
<h2>COMPILER ERRORS</h2>
|
||||
</div>
|
||||
<pre
|
||||
className="p-4 basis-full text-red-600 overflow-y-scroll whitespace-pre-wrap"
|
||||
style={{width: 'calc(100vw - 650px)', height: '150px'}}>
|
||||
<code>{compilerOutput.error.toString()}</code>
|
||||
</pre>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -218,10 +262,12 @@ function TextTabContent({
|
||||
output,
|
||||
diff,
|
||||
showInfoPanel,
|
||||
language,
|
||||
}: {
|
||||
output: string;
|
||||
diff: string | null;
|
||||
showInfoPanel: boolean;
|
||||
language: string;
|
||||
}): JSX.Element {
|
||||
const [diffMode, setDiffMode] = useState(false);
|
||||
return (
|
||||
@@ -272,7 +318,7 @@ function TextTabContent({
|
||||
/>
|
||||
) : (
|
||||
<MonacoEditor
|
||||
defaultLanguage="javascript"
|
||||
language={language ?? 'javascript'}
|
||||
value={output}
|
||||
options={{
|
||||
...monacoOptions,
|
||||
|
||||
@@ -28,5 +28,5 @@ export const monacoOptions: Partial<EditorProps['options']> = {
|
||||
|
||||
automaticLayout: true,
|
||||
wordWrap: 'on',
|
||||
wrappingIndent: 'deepIndent',
|
||||
wrappingIndent: 'same',
|
||||
};
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
*/
|
||||
|
||||
import {Monaco} from '@monaco-editor/react';
|
||||
import {CompilerErrorDetail, ErrorSeverity} from 'babel-plugin-react-compiler';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerErrorDetail,
|
||||
ErrorSeverity,
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import {MarkerSeverity, type editor} from 'monaco-editor';
|
||||
|
||||
function mapReactCompilerSeverityToMonaco(
|
||||
@@ -22,38 +26,46 @@ function mapReactCompilerSeverityToMonaco(
|
||||
}
|
||||
|
||||
function mapReactCompilerDiagnosticToMonacoMarker(
|
||||
detail: CompilerErrorDetail,
|
||||
detail: CompilerErrorDetail | CompilerDiagnostic,
|
||||
monaco: Monaco,
|
||||
source: string,
|
||||
): editor.IMarkerData | null {
|
||||
if (detail.loc == null || typeof detail.loc === 'symbol') {
|
||||
const loc = detail.primaryLocation();
|
||||
if (loc == null || typeof loc === 'symbol') {
|
||||
return null;
|
||||
}
|
||||
const severity = mapReactCompilerSeverityToMonaco(detail.severity, monaco);
|
||||
let message = detail.printErrorMessage();
|
||||
let message = detail.printErrorMessage(source, {eslint: true});
|
||||
return {
|
||||
severity,
|
||||
message,
|
||||
startLineNumber: detail.loc.start.line,
|
||||
startColumn: detail.loc.start.column + 1,
|
||||
endLineNumber: detail.loc.end.line,
|
||||
endColumn: detail.loc.end.column + 1,
|
||||
startLineNumber: loc.start.line,
|
||||
startColumn: loc.start.column + 1,
|
||||
endLineNumber: loc.end.line,
|
||||
endColumn: loc.end.column + 1,
|
||||
};
|
||||
}
|
||||
|
||||
type ReactCompilerMarkerConfig = {
|
||||
monaco: Monaco;
|
||||
model: editor.ITextModel;
|
||||
details: Array<CompilerErrorDetail>;
|
||||
details: Array<CompilerErrorDetail | CompilerDiagnostic>;
|
||||
source: string;
|
||||
};
|
||||
let decorations: Array<string> = [];
|
||||
export function renderReactCompilerMarkers({
|
||||
monaco,
|
||||
model,
|
||||
details,
|
||||
source,
|
||||
}: ReactCompilerMarkerConfig): void {
|
||||
const markers: Array<editor.IMarkerData> = [];
|
||||
for (const detail of details) {
|
||||
const marker = mapReactCompilerDiagnosticToMonacoMarker(detail, monaco);
|
||||
const marker = mapReactCompilerDiagnosticToMonacoMarker(
|
||||
detail,
|
||||
monaco,
|
||||
source,
|
||||
);
|
||||
if (marker == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
pipelineUsesReanimatedPlugin,
|
||||
} from '../Entrypoint/Reanimated';
|
||||
import validateNoUntransformedReferences from '../Entrypoint/ValidateNoUntransformedReferences';
|
||||
import {CompilerError} from '..';
|
||||
|
||||
const ENABLE_REACT_COMPILER_TIMINGS =
|
||||
process.env['ENABLE_REACT_COMPILER_TIMINGS'] === '1';
|
||||
@@ -34,51 +35,58 @@ export default function BabelPluginReactCompiler(
|
||||
*/
|
||||
Program: {
|
||||
enter(prog, pass): void {
|
||||
const filename = pass.filename ?? 'unknown';
|
||||
if (ENABLE_REACT_COMPILER_TIMINGS === true) {
|
||||
performance.mark(`${filename}:start`, {
|
||||
detail: 'BabelPlugin:Program:start',
|
||||
});
|
||||
}
|
||||
let opts = parsePluginOptions(pass.opts);
|
||||
const isDev =
|
||||
(typeof __DEV__ !== 'undefined' && __DEV__ === true) ||
|
||||
process.env['NODE_ENV'] === 'development';
|
||||
if (
|
||||
opts.enableReanimatedCheck === true &&
|
||||
pipelineUsesReanimatedPlugin(pass.file.opts.plugins)
|
||||
) {
|
||||
opts = injectReanimatedFlag(opts);
|
||||
}
|
||||
if (
|
||||
opts.environment.enableResetCacheOnSourceFileChanges !== false &&
|
||||
isDev
|
||||
) {
|
||||
opts = {
|
||||
...opts,
|
||||
environment: {
|
||||
...opts.environment,
|
||||
enableResetCacheOnSourceFileChanges: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
const result = compileProgram(prog, {
|
||||
opts,
|
||||
filename: pass.filename ?? null,
|
||||
comments: pass.file.ast.comments ?? [],
|
||||
code: pass.file.code,
|
||||
});
|
||||
validateNoUntransformedReferences(
|
||||
prog,
|
||||
pass.filename ?? null,
|
||||
opts.logger,
|
||||
opts.environment,
|
||||
result,
|
||||
);
|
||||
if (ENABLE_REACT_COMPILER_TIMINGS === true) {
|
||||
performance.mark(`${filename}:end`, {
|
||||
detail: 'BabelPlugin:Program:end',
|
||||
try {
|
||||
const filename = pass.filename ?? 'unknown';
|
||||
if (ENABLE_REACT_COMPILER_TIMINGS === true) {
|
||||
performance.mark(`${filename}:start`, {
|
||||
detail: 'BabelPlugin:Program:start',
|
||||
});
|
||||
}
|
||||
let opts = parsePluginOptions(pass.opts);
|
||||
const isDev =
|
||||
(typeof __DEV__ !== 'undefined' && __DEV__ === true) ||
|
||||
process.env['NODE_ENV'] === 'development';
|
||||
if (
|
||||
opts.enableReanimatedCheck === true &&
|
||||
pipelineUsesReanimatedPlugin(pass.file.opts.plugins)
|
||||
) {
|
||||
opts = injectReanimatedFlag(opts);
|
||||
}
|
||||
if (
|
||||
opts.environment.enableResetCacheOnSourceFileChanges !== false &&
|
||||
isDev
|
||||
) {
|
||||
opts = {
|
||||
...opts,
|
||||
environment: {
|
||||
...opts.environment,
|
||||
enableResetCacheOnSourceFileChanges: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
const result = compileProgram(prog, {
|
||||
opts,
|
||||
filename: pass.filename ?? null,
|
||||
comments: pass.file.ast.comments ?? [],
|
||||
code: pass.file.code,
|
||||
});
|
||||
validateNoUntransformedReferences(
|
||||
prog,
|
||||
pass.filename ?? null,
|
||||
opts.logger,
|
||||
opts.environment,
|
||||
result,
|
||||
);
|
||||
if (ENABLE_REACT_COMPILER_TIMINGS === true) {
|
||||
performance.mark(`${filename}:end`, {
|
||||
detail: 'BabelPlugin:Program:end',
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof CompilerError) {
|
||||
throw e.withPrintedMessage(pass.file.code, {eslint: false});
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
exit(_, pass): void {
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import * as t from '@babel/types';
|
||||
import {codeFrameColumns} from '@babel/code-frame';
|
||||
import type {SourceLocation} from './HIR';
|
||||
import {Err, Ok, Result} from './Utils/Result';
|
||||
import {assertExhaustive} from './Utils/utils';
|
||||
@@ -15,6 +17,11 @@ export enum ErrorSeverity {
|
||||
* misunderstanding on the user’s part.
|
||||
*/
|
||||
InvalidJS = 'InvalidJS',
|
||||
/**
|
||||
* JS syntax that is not supported and which we do not plan to support. Developers should
|
||||
* rewrite to use supported forms.
|
||||
*/
|
||||
UnsupportedJS = 'UnsupportedJS',
|
||||
/**
|
||||
* Code that breaks the rules of React.
|
||||
*/
|
||||
@@ -39,6 +46,24 @@ export enum ErrorSeverity {
|
||||
Invariant = 'Invariant',
|
||||
}
|
||||
|
||||
export type CompilerDiagnosticOptions = {
|
||||
severity: ErrorSeverity;
|
||||
category: string;
|
||||
description: string;
|
||||
details: Array<CompilerDiagnosticDetail>;
|
||||
suggestions?: Array<CompilerSuggestion> | null | undefined;
|
||||
};
|
||||
|
||||
export type CompilerDiagnosticDetail =
|
||||
/**
|
||||
* A/the source of the error
|
||||
*/
|
||||
{
|
||||
kind: 'error';
|
||||
loc: SourceLocation | null;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export enum CompilerSuggestionOperation {
|
||||
InsertBefore,
|
||||
InsertAfter,
|
||||
@@ -69,6 +94,103 @@ export type CompilerErrorDetailOptions = {
|
||||
suggestions?: Array<CompilerSuggestion> | null | undefined;
|
||||
};
|
||||
|
||||
export type PrintErrorMessageOptions = {
|
||||
/**
|
||||
* ESLint uses 1-indexed columns and prints one error at a time
|
||||
* So it doesn't require the "Found # error(s)" text
|
||||
*/
|
||||
eslint: boolean;
|
||||
};
|
||||
|
||||
export class CompilerDiagnostic {
|
||||
options: CompilerDiagnosticOptions;
|
||||
|
||||
constructor(options: CompilerDiagnosticOptions) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
static create(
|
||||
options: Omit<CompilerDiagnosticOptions, 'details'>,
|
||||
): CompilerDiagnostic {
|
||||
return new CompilerDiagnostic({...options, details: []});
|
||||
}
|
||||
|
||||
get category(): CompilerDiagnosticOptions['category'] {
|
||||
return this.options.category;
|
||||
}
|
||||
get description(): CompilerDiagnosticOptions['description'] {
|
||||
return this.options.description;
|
||||
}
|
||||
get severity(): CompilerDiagnosticOptions['severity'] {
|
||||
return this.options.severity;
|
||||
}
|
||||
get suggestions(): CompilerDiagnosticOptions['suggestions'] {
|
||||
return this.options.suggestions;
|
||||
}
|
||||
|
||||
withDetail(detail: CompilerDiagnosticDetail): CompilerDiagnostic {
|
||||
this.options.details.push(detail);
|
||||
return this;
|
||||
}
|
||||
|
||||
primaryLocation(): SourceLocation | null {
|
||||
return this.options.details.filter(d => d.kind === 'error')[0]?.loc ?? null;
|
||||
}
|
||||
|
||||
printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
|
||||
const buffer = [
|
||||
printErrorSummary(this.severity, this.category),
|
||||
'\n\n',
|
||||
this.description,
|
||||
];
|
||||
for (const detail of this.options.details) {
|
||||
switch (detail.kind) {
|
||||
case 'error': {
|
||||
const loc = detail.loc;
|
||||
if (loc == null || typeof loc === 'symbol') {
|
||||
continue;
|
||||
}
|
||||
let codeFrame: string;
|
||||
try {
|
||||
codeFrame = printCodeFrame(source, loc, detail.message);
|
||||
} catch (e) {
|
||||
codeFrame = detail.message;
|
||||
}
|
||||
buffer.push('\n\n');
|
||||
if (loc.filename != null) {
|
||||
const line = loc.start.line;
|
||||
const column = options.eslint
|
||||
? loc.start.column + 1
|
||||
: loc.start.column;
|
||||
buffer.push(`${loc.filename}:${line}:${column}\n`);
|
||||
}
|
||||
buffer.push(codeFrame);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
detail.kind,
|
||||
`Unexpected detail kind ${(detail as any).kind}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return buffer.join('');
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
const buffer = [printErrorSummary(this.severity, this.category)];
|
||||
if (this.description != null) {
|
||||
buffer.push(`. ${this.description}.`);
|
||||
}
|
||||
const loc = this.primaryLocation();
|
||||
if (loc != null && typeof loc !== 'symbol') {
|
||||
buffer.push(` (${loc.start.line}:${loc.start.column})`);
|
||||
}
|
||||
return buffer.join('');
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Each bailout or invariant in HIR lowering creates an {@link CompilerErrorDetail}, which is then
|
||||
* aggregated into a single {@link CompilerError} later.
|
||||
@@ -96,24 +218,51 @@ export class CompilerErrorDetail {
|
||||
return this.options.suggestions;
|
||||
}
|
||||
|
||||
printErrorMessage(): string {
|
||||
const buffer = [`${this.severity}: ${this.reason}`];
|
||||
primaryLocation(): SourceLocation | null {
|
||||
return this.loc;
|
||||
}
|
||||
|
||||
printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
|
||||
const buffer = [printErrorSummary(this.severity, this.reason)];
|
||||
if (this.description != null) {
|
||||
buffer.push(`. ${this.description}`);
|
||||
buffer.push(`\n\n${this.description}.`);
|
||||
}
|
||||
if (this.loc != null && typeof this.loc !== 'symbol') {
|
||||
buffer.push(` (${this.loc.start.line}:${this.loc.end.line})`);
|
||||
const loc = this.loc;
|
||||
if (loc != null && typeof loc !== 'symbol') {
|
||||
let codeFrame: string;
|
||||
try {
|
||||
codeFrame = printCodeFrame(source, loc, this.reason);
|
||||
} catch (e) {
|
||||
codeFrame = '';
|
||||
}
|
||||
buffer.push(`\n\n`);
|
||||
if (loc.filename != null) {
|
||||
const line = loc.start.line;
|
||||
const column = options.eslint ? loc.start.column + 1 : loc.start.column;
|
||||
buffer.push(`${loc.filename}:${line}:${column}\n`);
|
||||
}
|
||||
buffer.push(codeFrame);
|
||||
buffer.push('\n\n');
|
||||
}
|
||||
return buffer.join('');
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.printErrorMessage();
|
||||
const buffer = [printErrorSummary(this.severity, this.reason)];
|
||||
if (this.description != null) {
|
||||
buffer.push(`. ${this.description}.`);
|
||||
}
|
||||
const loc = this.loc;
|
||||
if (loc != null && typeof loc !== 'symbol') {
|
||||
buffer.push(` (${loc.start.line}:${loc.start.column})`);
|
||||
}
|
||||
return buffer.join('');
|
||||
}
|
||||
}
|
||||
|
||||
export class CompilerError extends Error {
|
||||
details: Array<CompilerErrorDetail> = [];
|
||||
details: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
|
||||
printedMessage: string | null = null;
|
||||
|
||||
static invariant(
|
||||
condition: unknown,
|
||||
@@ -131,6 +280,12 @@ export class CompilerError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
static throwDiagnostic(options: CompilerDiagnosticOptions): never {
|
||||
const errors = new CompilerError();
|
||||
errors.pushDiagnostic(new CompilerDiagnostic(options));
|
||||
throw errors;
|
||||
}
|
||||
|
||||
static throwTodo(
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity'>,
|
||||
): never {
|
||||
@@ -193,18 +348,49 @@ export class CompilerError extends Error {
|
||||
}
|
||||
|
||||
override get message(): string {
|
||||
return this.toString();
|
||||
return this.printedMessage ?? this.toString();
|
||||
}
|
||||
|
||||
override set message(_message: string) {}
|
||||
|
||||
override toString(): string {
|
||||
if (this.printedMessage) {
|
||||
return this.printedMessage;
|
||||
}
|
||||
if (Array.isArray(this.details)) {
|
||||
return this.details.map(detail => detail.toString()).join('\n\n');
|
||||
}
|
||||
return this.name;
|
||||
}
|
||||
|
||||
withPrintedMessage(
|
||||
source: string,
|
||||
options: PrintErrorMessageOptions,
|
||||
): CompilerError {
|
||||
this.printedMessage = this.printErrorMessage(source, options);
|
||||
return this;
|
||||
}
|
||||
|
||||
printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
|
||||
if (options.eslint && this.details.length === 1) {
|
||||
return this.details[0].printErrorMessage(source, options);
|
||||
}
|
||||
return (
|
||||
`Found ${this.details.length} error${this.details.length === 1 ? '' : 's'}:\n\n` +
|
||||
this.details
|
||||
.map(detail => detail.printErrorMessage(source, options).trim())
|
||||
.join('\n\n')
|
||||
);
|
||||
}
|
||||
|
||||
merge(other: CompilerError): void {
|
||||
this.details.push(...other.details);
|
||||
}
|
||||
|
||||
pushDiagnostic(diagnostic: CompilerDiagnostic): void {
|
||||
this.details.push(diagnostic);
|
||||
}
|
||||
|
||||
push(options: CompilerErrorDetailOptions): CompilerErrorDetail {
|
||||
const detail = new CompilerErrorDetail({
|
||||
reason: options.reason,
|
||||
@@ -241,13 +427,69 @@ export class CompilerError extends Error {
|
||||
case ErrorSeverity.InvalidJS:
|
||||
case ErrorSeverity.InvalidReact:
|
||||
case ErrorSeverity.InvalidConfig:
|
||||
case ErrorSeverity.UnsupportedJS: {
|
||||
return true;
|
||||
}
|
||||
case ErrorSeverity.CannotPreserveMemoization:
|
||||
case ErrorSeverity.Todo:
|
||||
case ErrorSeverity.Todo: {
|
||||
return false;
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(detail.severity, 'Unhandled error severity');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function printCodeFrame(
|
||||
source: string,
|
||||
loc: t.SourceLocation,
|
||||
message: string,
|
||||
): string {
|
||||
return codeFrameColumns(
|
||||
source,
|
||||
{
|
||||
start: {
|
||||
line: loc.start.line,
|
||||
column: loc.start.column + 1,
|
||||
},
|
||||
end: {
|
||||
line: loc.end.line,
|
||||
column: loc.end.column + 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
message,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function printErrorSummary(severity: ErrorSeverity, message: string): string {
|
||||
let severityCategory: string;
|
||||
switch (severity) {
|
||||
case ErrorSeverity.InvalidConfig:
|
||||
case ErrorSeverity.InvalidJS:
|
||||
case ErrorSeverity.InvalidReact:
|
||||
case ErrorSeverity.UnsupportedJS: {
|
||||
severityCategory = 'Error';
|
||||
break;
|
||||
}
|
||||
case ErrorSeverity.CannotPreserveMemoization: {
|
||||
severityCategory = 'Memoization';
|
||||
break;
|
||||
}
|
||||
case ErrorSeverity.Invariant: {
|
||||
severityCategory = 'Invariant';
|
||||
break;
|
||||
}
|
||||
case ErrorSeverity.Todo: {
|
||||
severityCategory = 'Todo';
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(severity, `Unexpected severity '${severity}'`);
|
||||
}
|
||||
}
|
||||
return `${severityCategory}: ${message}`;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,12 @@
|
||||
|
||||
import * as t from '@babel/types';
|
||||
import {z} from 'zod';
|
||||
import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
CompilerErrorDetail,
|
||||
CompilerErrorDetailOptions,
|
||||
} from '../CompilerError';
|
||||
import {
|
||||
EnvironmentConfig,
|
||||
ExternalFunction,
|
||||
@@ -224,7 +229,7 @@ export type LoggerEvent =
|
||||
export type CompileErrorEvent = {
|
||||
kind: 'CompileError';
|
||||
fnLoc: t.SourceLocation | null;
|
||||
detail: CompilerErrorDetailOptions;
|
||||
detail: CompilerErrorDetail | CompilerDiagnostic;
|
||||
};
|
||||
export type CompileDiagnosticEvent = {
|
||||
kind: 'CompileDiagnostic';
|
||||
|
||||
@@ -94,7 +94,7 @@ import {validateLocalsNotReassignedAfterRender} from '../Validation/ValidateLoca
|
||||
import {outlineFunctions} from '../Optimization/OutlineFunctions';
|
||||
import {propagatePhiTypes} from '../TypeInference/PropagatePhiTypes';
|
||||
import {lowerContextAccess} from '../Optimization/LowerContextAccess';
|
||||
import {validateNoSetStateInPassiveEffects} from '../Validation/ValidateNoSetStateInPassiveEffects';
|
||||
import {validateNoSetStateInEffects} from '../Validation/ValidateNoSetStateInEffects';
|
||||
import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement';
|
||||
import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHIR';
|
||||
import {outlineJSX} from '../Optimization/OutlineJsx';
|
||||
@@ -292,8 +292,8 @@ function runWithEnvironment(
|
||||
validateNoSetStateInRender(hir).unwrap();
|
||||
}
|
||||
|
||||
if (env.config.validateNoSetStateInPassiveEffects) {
|
||||
env.logErrors(validateNoSetStateInPassiveEffects(hir));
|
||||
if (env.config.validateNoSetStateInEffects) {
|
||||
env.logErrors(validateNoSetStateInEffects(hir));
|
||||
}
|
||||
|
||||
if (env.config.validateNoJSXInTryStatements) {
|
||||
|
||||
@@ -181,7 +181,7 @@ function logError(
|
||||
context.opts.logger.logEvent(context.filename, {
|
||||
kind: 'CompileError',
|
||||
fnLoc,
|
||||
detail: detail.options,
|
||||
detail,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
import {NodePath} from '@babel/core';
|
||||
import * as t from '@babel/types';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
CompilerErrorDetail,
|
||||
CompilerSuggestionOperation,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
@@ -181,12 +181,11 @@ export function suppressionsToCompilerError(
|
||||
'Unhandled suppression source',
|
||||
);
|
||||
}
|
||||
error.pushErrorDetail(
|
||||
new CompilerErrorDetail({
|
||||
reason: `${reason}. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior`,
|
||||
description: suppressionRange.disableComment.value.trim(),
|
||||
error.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: reason,
|
||||
description: `React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression \`${suppressionRange.disableComment.value.trim()}\``,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc: suppressionRange.disableComment.loc ?? null,
|
||||
suggestions: [
|
||||
{
|
||||
description: suggestion,
|
||||
@@ -197,6 +196,10 @@ export function suppressionsToCompilerError(
|
||||
op: CompilerSuggestionOperation.Remove,
|
||||
},
|
||||
],
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: suppressionRange.disableComment.loc ?? null,
|
||||
message: 'Found React rule suppression',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,35 +8,63 @@
|
||||
import {NodePath} from '@babel/core';
|
||||
import * as t from '@babel/types';
|
||||
|
||||
import {
|
||||
CompilerError,
|
||||
CompilerErrorDetailOptions,
|
||||
EnvironmentConfig,
|
||||
ErrorSeverity,
|
||||
Logger,
|
||||
} from '..';
|
||||
import {CompilerError, EnvironmentConfig, ErrorSeverity, Logger} from '..';
|
||||
import {getOrInsertWith} from '../Utils/utils';
|
||||
import {Environment} from '../HIR';
|
||||
import {Environment, GeneratedSource} from '../HIR';
|
||||
import {DEFAULT_EXPORT} from '../HIR/Environment';
|
||||
import {CompileProgramMetadata} from './Program';
|
||||
import {CompilerDiagnostic, CompilerDiagnosticOptions} from '../CompilerError';
|
||||
|
||||
function throwInvalidReact(
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity'>,
|
||||
options: Omit<CompilerDiagnosticOptions, 'severity'>,
|
||||
{logger, filename}: TraversalState,
|
||||
): never {
|
||||
const detail: CompilerErrorDetailOptions = {
|
||||
...options,
|
||||
const detail: CompilerDiagnosticOptions = {
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
...options,
|
||||
};
|
||||
logger?.logEvent(filename, {
|
||||
kind: 'CompileError',
|
||||
fnLoc: null,
|
||||
detail,
|
||||
detail: new CompilerDiagnostic(detail),
|
||||
});
|
||||
CompilerError.throw(detail);
|
||||
CompilerError.throwDiagnostic(detail);
|
||||
}
|
||||
|
||||
function isAutodepsSigil(
|
||||
arg: NodePath<t.ArgumentPlaceholder | t.SpreadElement | t.Expression>,
|
||||
): boolean {
|
||||
// Check for AUTODEPS identifier imported from React
|
||||
if (arg.isIdentifier() && arg.node.name === 'AUTODEPS') {
|
||||
const binding = arg.scope.getBinding(arg.node.name);
|
||||
if (binding && binding.path.isImportSpecifier()) {
|
||||
const importSpecifier = binding.path.node as t.ImportSpecifier;
|
||||
if (importSpecifier.imported.type === 'Identifier') {
|
||||
return (importSpecifier.imported as t.Identifier).name === 'AUTODEPS';
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for React.AUTODEPS member expression
|
||||
if (arg.isMemberExpression() && !arg.node.computed) {
|
||||
const object = arg.get('object');
|
||||
const property = arg.get('property');
|
||||
|
||||
if (
|
||||
object.isIdentifier() &&
|
||||
object.node.name === 'React' &&
|
||||
property.isIdentifier() &&
|
||||
property.node.name === 'AUTODEPS'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
function assertValidEffectImportReference(
|
||||
numArgs: number,
|
||||
autodepsIndex: number,
|
||||
paths: Array<NodePath<t.Node>>,
|
||||
context: TraversalState,
|
||||
): void {
|
||||
@@ -49,11 +77,10 @@ function assertValidEffectImportReference(
|
||||
maybeCalleeLoc != null &&
|
||||
context.inferredEffectLocations.has(maybeCalleeLoc);
|
||||
/**
|
||||
* Only error on untransformed references of the form `useMyEffect(...)`
|
||||
* or `moduleNamespace.useMyEffect(...)`, with matching argument counts.
|
||||
* TODO: do we also want a mode to also hard error on non-call references?
|
||||
* Error on effect calls that still have AUTODEPS in their args
|
||||
*/
|
||||
if (args.length === numArgs && !hasInferredEffect) {
|
||||
const hasAutodepsArg = args.some(isAutodepsSigil);
|
||||
if (hasAutodepsArg && !hasInferredEffect) {
|
||||
const maybeErrorDiagnostic = matchCompilerDiagnostic(
|
||||
path,
|
||||
context.transformErrors,
|
||||
@@ -65,14 +92,18 @@ function assertValidEffectImportReference(
|
||||
*/
|
||||
throwInvalidReact(
|
||||
{
|
||||
reason:
|
||||
'[InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. ' +
|
||||
'This will break your build! ' +
|
||||
'To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics.',
|
||||
description: maybeErrorDiagnostic
|
||||
? `(Bailout reason: ${maybeErrorDiagnostic})`
|
||||
: null,
|
||||
loc: parent.node.loc ?? null,
|
||||
category:
|
||||
'Cannot infer dependencies of this effect. This will break your build!',
|
||||
description:
|
||||
'To resolve, either pass a dependency array or fix reported compiler bailout diagnostics.' +
|
||||
(maybeErrorDiagnostic ? ` ${maybeErrorDiagnostic}` : ''),
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
message: 'Cannot infer dependencies',
|
||||
loc: parent.node.loc ?? GeneratedSource,
|
||||
},
|
||||
],
|
||||
},
|
||||
context,
|
||||
);
|
||||
@@ -92,13 +123,20 @@ function assertValidFireImportReference(
|
||||
);
|
||||
throwInvalidReact(
|
||||
{
|
||||
reason:
|
||||
'[Fire] Untransformed reference to compiler-required feature. ' +
|
||||
'Either remove this `fire` call or ensure it is successfully transformed by the compiler',
|
||||
description: maybeErrorDiagnostic
|
||||
? `(Bailout reason: ${maybeErrorDiagnostic})`
|
||||
: null,
|
||||
loc: paths[0].node.loc ?? null,
|
||||
category:
|
||||
'[Fire] Untransformed reference to compiler-required feature.',
|
||||
description:
|
||||
'Either remove this `fire` call or ensure it is successfully transformed by the compiler' +
|
||||
maybeErrorDiagnostic
|
||||
? ` ${maybeErrorDiagnostic}`
|
||||
: '',
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
message: 'Untransformed `fire` call',
|
||||
loc: paths[0].node.loc ?? GeneratedSource,
|
||||
},
|
||||
],
|
||||
},
|
||||
context,
|
||||
);
|
||||
@@ -128,12 +166,12 @@ export default function validateNoUntransformedReferences(
|
||||
if (env.inferEffectDependencies) {
|
||||
for (const {
|
||||
function: {source, importSpecifierName},
|
||||
numRequiredArgs,
|
||||
autodepsIndex,
|
||||
} of env.inferEffectDependencies) {
|
||||
const module = getOrInsertWith(moduleLoadChecks, source, () => new Map());
|
||||
module.set(
|
||||
importSpecifierName,
|
||||
assertValidEffectImportReference.bind(null, numRequiredArgs),
|
||||
assertValidEffectImportReference.bind(null, autodepsIndex),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {NodePath, Scope} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import invariant from 'invariant';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
CompilerSuggestionOperation,
|
||||
ErrorSeverity,
|
||||
@@ -104,12 +105,17 @@ export function lower(
|
||||
if (param.isIdentifier()) {
|
||||
const binding = builder.resolveIdentifier(param);
|
||||
if (binding.kind !== 'Identifier') {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lower) Could not find binding for param \`${param.node.name}\``,
|
||||
severity: ErrorSeverity.Invariant,
|
||||
loc: param.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
builder.errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.Invariant,
|
||||
category: 'Could not find binding',
|
||||
description: `[BuildHIR] Could not find binding for param \`${param.node.name}\`.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: param.node.loc ?? null,
|
||||
message: 'Could not find binding',
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const place: Place = {
|
||||
@@ -163,12 +169,17 @@ export function lower(
|
||||
'Assignment',
|
||||
);
|
||||
} else {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lower) Handle ${param.node.type} params`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
loc: param.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
builder.errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: `Handle ${param.node.type} parameters`,
|
||||
description: `[BuildHIR] Add support for ${param.node.type} parameters.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: param.node.loc ?? null,
|
||||
message: 'Unsupported parameter type',
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -188,13 +199,17 @@ export function lower(
|
||||
lowerStatement(builder, body);
|
||||
directives = body.get('directives').map(d => d.node.value.value);
|
||||
} else {
|
||||
builder.errors.push({
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
reason: `Unexpected function body kind`,
|
||||
description: `Expected function body to be an expression or a block statement, got \`${body.type}\``,
|
||||
loc: body.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
builder.errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
category: `Unexpected function body kind`,
|
||||
description: `Expected function body to be an expression or a block statement, got \`${body.type}\`.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: body.node.loc ?? null,
|
||||
message: 'Expected a block statement or expression',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (builder.errors.hasErrors()) {
|
||||
@@ -1355,13 +1370,85 @@ function lowerStatement(
|
||||
|
||||
return;
|
||||
}
|
||||
case 'TypeAlias':
|
||||
case 'TSInterfaceDeclaration':
|
||||
case 'TSTypeAliasDeclaration': {
|
||||
// We do not preserve type annotations/syntax through transformation
|
||||
case 'WithStatement': {
|
||||
builder.errors.push({
|
||||
reason: `JavaScript 'with' syntax is not supported`,
|
||||
description: `'with' syntax is considered deprecated and removed from JavaScript standards, consider alternatives`,
|
||||
severity: ErrorSeverity.UnsupportedJS,
|
||||
loc: stmtPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'UnsupportedNode',
|
||||
loc: stmtPath.node.loc ?? GeneratedSource,
|
||||
node: stmtPath.node,
|
||||
});
|
||||
return;
|
||||
}
|
||||
case 'ClassDeclaration': {
|
||||
/**
|
||||
* In theory we could support inline class declarations, but this is rare enough in practice
|
||||
* and complex enough to support that we don't anticipate supporting anytime soon. Developers
|
||||
* are encouraged to lift classes out of component/hook declarations.
|
||||
*/
|
||||
builder.errors.push({
|
||||
reason: 'Inline `class` declarations are not supported',
|
||||
description: `Move class declarations outside of components/hooks`,
|
||||
severity: ErrorSeverity.UnsupportedJS,
|
||||
loc: stmtPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'UnsupportedNode',
|
||||
loc: stmtPath.node.loc ?? GeneratedSource,
|
||||
node: stmtPath.node,
|
||||
});
|
||||
return;
|
||||
}
|
||||
case 'EnumDeclaration':
|
||||
case 'TSEnumDeclaration': {
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'UnsupportedNode',
|
||||
loc: stmtPath.node.loc ?? GeneratedSource,
|
||||
node: stmtPath.node,
|
||||
});
|
||||
return;
|
||||
}
|
||||
case 'ExportAllDeclaration':
|
||||
case 'ExportDefaultDeclaration':
|
||||
case 'ExportNamedDeclaration':
|
||||
case 'ImportDeclaration':
|
||||
case 'TSExportAssignment':
|
||||
case 'TSImportEqualsDeclaration': {
|
||||
builder.errors.push({
|
||||
reason:
|
||||
'JavaScript `import` and `export` statements may only appear at the top level of a module',
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
loc: stmtPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'UnsupportedNode',
|
||||
loc: stmtPath.node.loc ?? GeneratedSource,
|
||||
node: stmtPath.node,
|
||||
});
|
||||
return;
|
||||
}
|
||||
case 'TSNamespaceExportDeclaration': {
|
||||
builder.errors.push({
|
||||
reason:
|
||||
'TypeScript `namespace` statements may only appear at the top level of a module',
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
loc: stmtPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'UnsupportedNode',
|
||||
loc: stmtPath.node.loc ?? GeneratedSource,
|
||||
node: stmtPath.node,
|
||||
});
|
||||
return;
|
||||
}
|
||||
case 'ClassDeclaration':
|
||||
case 'DeclareClass':
|
||||
case 'DeclareExportAllDeclaration':
|
||||
case 'DeclareExportDeclaration':
|
||||
@@ -1372,31 +1459,14 @@ function lowerStatement(
|
||||
case 'DeclareOpaqueType':
|
||||
case 'DeclareTypeAlias':
|
||||
case 'DeclareVariable':
|
||||
case 'EnumDeclaration':
|
||||
case 'ExportAllDeclaration':
|
||||
case 'ExportDefaultDeclaration':
|
||||
case 'ExportNamedDeclaration':
|
||||
case 'ImportDeclaration':
|
||||
case 'InterfaceDeclaration':
|
||||
case 'OpaqueType':
|
||||
case 'TSDeclareFunction':
|
||||
case 'TSEnumDeclaration':
|
||||
case 'TSExportAssignment':
|
||||
case 'TSImportEqualsDeclaration':
|
||||
case 'TSInterfaceDeclaration':
|
||||
case 'TSModuleDeclaration':
|
||||
case 'TSNamespaceExportDeclaration':
|
||||
case 'WithStatement': {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerStatement) Handle ${stmtPath.type} statements`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
loc: stmtPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'UnsupportedNode',
|
||||
loc: stmtPath.node.loc ?? GeneratedSource,
|
||||
node: stmtPath.node,
|
||||
});
|
||||
case 'TSTypeAliasDeclaration':
|
||||
case 'TypeAlias': {
|
||||
// We do not preserve type annotations/syntax through transformation
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
@@ -2216,11 +2286,17 @@ function lowerExpression(
|
||||
});
|
||||
for (const [name, locations] of Object.entries(fbtLocations)) {
|
||||
if (locations.length > 1) {
|
||||
CompilerError.throwTodo({
|
||||
reason: `Support <${tagName}> tags with multiple <${tagName}:${name}> values`,
|
||||
loc: locations.at(-1) ?? GeneratedSource,
|
||||
description: null,
|
||||
suggestions: null,
|
||||
CompilerError.throwDiagnostic({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: 'Support duplicate fbt tags',
|
||||
description: `Support \`<${tagName}>\` tags with multiple \`<${tagName}:${name}>\` values`,
|
||||
details: locations.map(loc => {
|
||||
return {
|
||||
kind: 'error',
|
||||
message: `Multiple \`<${tagName}:${name}>\` tags found`,
|
||||
loc,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2946,6 +3022,8 @@ function isReorderableExpression(
|
||||
}
|
||||
}
|
||||
}
|
||||
case 'TSAsExpression':
|
||||
case 'TSNonNullExpression':
|
||||
case 'TypeCastExpression': {
|
||||
return isReorderableExpression(
|
||||
builder,
|
||||
@@ -3446,9 +3524,8 @@ function lowerFunction(
|
||||
);
|
||||
let loweredFunc: HIRFunction;
|
||||
if (lowering.isErr()) {
|
||||
lowering
|
||||
.unwrapErr()
|
||||
.details.forEach(detail => builder.errors.pushErrorDetail(detail));
|
||||
const functionErrors = lowering.unwrapErr();
|
||||
builder.errors.merge(functionErrors);
|
||||
return null;
|
||||
}
|
||||
loweredFunc = lowering.unwrap();
|
||||
@@ -3502,6 +3579,16 @@ function lowerIdentifier(
|
||||
return place;
|
||||
}
|
||||
default: {
|
||||
if (binding.kind === 'Global' && binding.name === 'eval') {
|
||||
builder.errors.push({
|
||||
reason: `The 'eval' function is not supported`,
|
||||
description:
|
||||
'Eval is an anti-pattern in JavaScript, and the code executed cannot be evaluated by React Compiler',
|
||||
severity: ErrorSeverity.UnsupportedJS,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
return lowerValueToTemporary(builder, {
|
||||
kind: 'LoadGlobal',
|
||||
binding,
|
||||
|
||||
@@ -265,21 +265,19 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* {
|
||||
* module: 'react',
|
||||
* imported: 'useEffect',
|
||||
* numRequiredArgs: 1,
|
||||
* autodepsIndex: 1,
|
||||
* },{
|
||||
* module: 'MyExperimentalEffectHooks',
|
||||
* imported: 'useExperimentalEffect',
|
||||
* numRequiredArgs: 2,
|
||||
* autodepsIndex: 2,
|
||||
* },
|
||||
* ]
|
||||
* would insert dependencies for calls of `useEffect` imported from `react` and calls of
|
||||
* useExperimentalEffect` from `MyExperimentalEffectHooks`.
|
||||
*
|
||||
* `numRequiredArgs` tells the compiler the amount of arguments required to append a dependency
|
||||
* array to the end of the call. With the configuration above, we'd insert dependencies for
|
||||
* `useEffect` if it is only given a single argument and it would be appended to the argument list.
|
||||
*
|
||||
* numRequiredArgs must always be greater than 0, otherwise there is no function to analyze for dependencies
|
||||
* `autodepsIndex` tells the compiler which index we expect the AUTODEPS to appear in.
|
||||
* With the configuration above, we'd insert dependencies for `useEffect` if it has two
|
||||
* arguments, and the second is AUTODEPS.
|
||||
*
|
||||
* Still experimental.
|
||||
*/
|
||||
@@ -288,7 +286,7 @@ export const EnvironmentConfigSchema = z.object({
|
||||
z.array(
|
||||
z.object({
|
||||
function: ExternalFunctionSchema,
|
||||
numRequiredArgs: z.number().min(1, 'numRequiredArgs must be > 0'),
|
||||
autodepsIndex: z.number().min(1, 'autodepsIndex must be > 0'),
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -320,10 +318,10 @@ export const EnvironmentConfigSchema = z.object({
|
||||
validateNoSetStateInRender: z.boolean().default(true),
|
||||
|
||||
/**
|
||||
* Validates that setState is not called directly within a passive effect (useEffect).
|
||||
* Validates that setState is not called synchronously within an effect (useEffect and friends).
|
||||
* Scheduling a setState (with an event listener, subscription, etc) is valid.
|
||||
*/
|
||||
validateNoSetStateInPassiveEffects: z.boolean().default(false),
|
||||
validateNoSetStateInEffects: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Validates against creating JSX within a try block and recommends using an error boundary
|
||||
|
||||
@@ -9,6 +9,7 @@ import {Effect, ValueKind, ValueReason} from './HIR';
|
||||
import {
|
||||
BUILTIN_SHAPES,
|
||||
BuiltInArrayId,
|
||||
BuiltInAutodepsId,
|
||||
BuiltInFireFunctionId,
|
||||
BuiltInFireId,
|
||||
BuiltInMapId,
|
||||
@@ -780,6 +781,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
BuiltInUseEffectEventId,
|
||||
),
|
||||
],
|
||||
['AUTODEPS', addObject(DEFAULT_SHAPES, BuiltInAutodepsId, [])],
|
||||
];
|
||||
|
||||
TYPED_GLOBALS.push(
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import {Binding, NodePath} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {CompilerError, ErrorSeverity} from '../CompilerError';
|
||||
import {Environment} from './Environment';
|
||||
import {
|
||||
BasicBlock,
|
||||
@@ -308,9 +308,18 @@ export default class HIRBuilder {
|
||||
|
||||
resolveBinding(node: t.Identifier): Identifier {
|
||||
if (node.name === 'fbt') {
|
||||
CompilerError.throwTodo({
|
||||
reason: 'Support local variables named "fbt"',
|
||||
loc: node.loc ?? null,
|
||||
CompilerError.throwDiagnostic({
|
||||
severity: ErrorSeverity.Todo,
|
||||
category: 'Support local variables named `fbt`',
|
||||
description:
|
||||
'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
message: 'Rename to avoid conflict with fbt plugin',
|
||||
loc: node.loc ?? GeneratedSource,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
const originalName = node.name;
|
||||
|
||||
@@ -384,6 +384,7 @@ export const BuiltInFireId = 'BuiltInFire';
|
||||
export const BuiltInFireFunctionId = 'BuiltInFireFunction';
|
||||
export const BuiltInUseEffectEventId = 'BuiltInUseEffectEvent';
|
||||
export const BuiltinEffectEventId = 'BuiltInEffectEventFunction';
|
||||
export const BuiltInAutodepsId = 'BuiltInAutoDepsId';
|
||||
|
||||
// See getReanimatedModuleType() in Globals.ts — this is part of supporting Reanimated's ref-like types
|
||||
export const ReanimatedSharedValueId = 'ReanimatedSharedValueId';
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import generate from '@babel/generator';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {printReactiveScopeSummary} from '../ReactiveScopes/PrintReactiveFunction';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
@@ -466,7 +465,7 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
|
||||
break;
|
||||
}
|
||||
case 'UnsupportedNode': {
|
||||
value = `UnsupportedNode(${generate(instrValue.node).code})`;
|
||||
value = `UnsupportedNode ${instrValue.node.type}`;
|
||||
break;
|
||||
}
|
||||
case 'LoadLocal': {
|
||||
@@ -944,7 +943,10 @@ export function printAliasingEffect(effect: AliasingEffect): string {
|
||||
return `Assign ${printPlaceForAliasEffect(effect.into)} = ${printPlaceForAliasEffect(effect.from)}`;
|
||||
}
|
||||
case 'Alias': {
|
||||
return `Alias ${printPlaceForAliasEffect(effect.into)} = ${printPlaceForAliasEffect(effect.from)}`;
|
||||
return `Alias ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
|
||||
}
|
||||
case 'MaybeAlias': {
|
||||
return `MaybeAlias ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
|
||||
}
|
||||
case 'Capture': {
|
||||
return `Capture ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
|
||||
@@ -996,13 +998,13 @@ export function printAliasingEffect(effect: AliasingEffect): string {
|
||||
return `${effect.kind} ${printPlaceForAliasEffect(effect.value)}`;
|
||||
}
|
||||
case 'MutateFrozen': {
|
||||
return `MutateFrozen ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
|
||||
return `MutateFrozen ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.category)}`;
|
||||
}
|
||||
case 'MutateGlobal': {
|
||||
return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
|
||||
return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.category)}`;
|
||||
}
|
||||
case 'Impure': {
|
||||
return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
|
||||
return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.category)}`;
|
||||
}
|
||||
case 'Render': {
|
||||
return `Render ${printPlaceForAliasEffect(effect.place)}`;
|
||||
|
||||
@@ -345,6 +345,51 @@ export function* eachPatternOperand(pattern: Pattern): Iterable<Place> {
|
||||
}
|
||||
}
|
||||
|
||||
export function* eachPatternItem(
|
||||
pattern: Pattern,
|
||||
): Iterable<Place | SpreadPattern> {
|
||||
switch (pattern.kind) {
|
||||
case 'ArrayPattern': {
|
||||
for (const item of pattern.items) {
|
||||
if (item.kind === 'Identifier') {
|
||||
yield item;
|
||||
} else if (item.kind === 'Spread') {
|
||||
yield item;
|
||||
} else if (item.kind === 'Hole') {
|
||||
continue;
|
||||
} else {
|
||||
assertExhaustive(
|
||||
item,
|
||||
`Unexpected item kind \`${(item as any).kind}\``,
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ObjectPattern': {
|
||||
for (const property of pattern.properties) {
|
||||
if (property.kind === 'ObjectProperty') {
|
||||
yield property.place;
|
||||
} else if (property.kind === 'Spread') {
|
||||
yield property;
|
||||
} else {
|
||||
assertExhaustive(
|
||||
property,
|
||||
`Unexpected item kind \`${(property as any).kind}\``,
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
pattern,
|
||||
`Unexpected pattern kind \`${(pattern as any).kind}\``,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function mapInstructionLValues(
|
||||
instr: Instruction,
|
||||
fn: (place: Place) => Place,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerErrorDetailOptions} from '../CompilerError';
|
||||
import {CompilerDiagnostic} from '../CompilerError';
|
||||
import {
|
||||
FunctionExpression,
|
||||
GeneratedSource,
|
||||
@@ -90,6 +90,23 @@ export type AliasingEffect =
|
||||
* c could be mutating a.
|
||||
*/
|
||||
| {kind: 'Alias'; from: Place; into: Place}
|
||||
|
||||
/**
|
||||
* Indicates the potential for information flow from `from` to `into`. This is used for a specific
|
||||
* case: functions with unknown signatures. If the compiler sees a call such as `foo(x)`, it has to
|
||||
* consider several possibilities (which may depend on the arguments):
|
||||
* - foo(x) returns a new mutable value that does not capture any information from x.
|
||||
* - foo(x) returns a new mutable value that *does* capture information from x.
|
||||
* - foo(x) returns x itself, ie foo is the identity function
|
||||
*
|
||||
* The same is true of functions that take multiple arguments: `cond(a, b, c)` could conditionally
|
||||
* return b or c depending on the value of a.
|
||||
*
|
||||
* To represent this case, MaybeAlias represents the fact that an aliasing relationship could exist.
|
||||
* Any mutations that flow through this relationship automatically become conditional.
|
||||
*/
|
||||
| {kind: 'MaybeAlias'; from: Place; into: Place}
|
||||
|
||||
/**
|
||||
* Records direct assignment: `into = from`.
|
||||
*/
|
||||
@@ -133,19 +150,19 @@ export type AliasingEffect =
|
||||
/**
|
||||
* Mutation of a value known to be immutable
|
||||
*/
|
||||
| {kind: 'MutateFrozen'; place: Place; error: CompilerErrorDetailOptions}
|
||||
| {kind: 'MutateFrozen'; place: Place; error: CompilerDiagnostic}
|
||||
/**
|
||||
* Mutation of a global
|
||||
*/
|
||||
| {
|
||||
kind: 'MutateGlobal';
|
||||
place: Place;
|
||||
error: CompilerErrorDetailOptions;
|
||||
error: CompilerDiagnostic;
|
||||
}
|
||||
/**
|
||||
* Indicates a side-effect that is not safe during render
|
||||
*/
|
||||
| {kind: 'Impure'; place: Place; error: CompilerErrorDetailOptions}
|
||||
| {kind: 'Impure'; place: Place; error: CompilerDiagnostic}
|
||||
/**
|
||||
* Indicates that a given place is accessed during render. Used to distingush
|
||||
* hook arguments that are known to be called immediately vs those used for
|
||||
@@ -183,7 +200,8 @@ export function hashEffect(effect: AliasingEffect): string {
|
||||
case 'ImmutableCapture':
|
||||
case 'Assign':
|
||||
case 'Alias':
|
||||
case 'Capture': {
|
||||
case 'Capture':
|
||||
case 'MaybeAlias': {
|
||||
return [
|
||||
effect.kind,
|
||||
effect.from.identifier.id,
|
||||
@@ -211,9 +229,9 @@ export function hashEffect(effect: AliasingEffect): string {
|
||||
effect.kind,
|
||||
effect.place.identifier.id,
|
||||
effect.error.severity,
|
||||
effect.error.reason,
|
||||
effect.error.category,
|
||||
effect.error.description,
|
||||
printSourceLocation(effect.error.loc ?? GeneratedSource),
|
||||
printSourceLocation(effect.error.primaryLocation() ?? GeneratedSource),
|
||||
].join(':');
|
||||
}
|
||||
case 'Mutate':
|
||||
|
||||
@@ -85,7 +85,8 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
|
||||
case 'Assign':
|
||||
case 'Alias':
|
||||
case 'Capture':
|
||||
case 'CreateFrom': {
|
||||
case 'CreateFrom':
|
||||
case 'MaybeAlias': {
|
||||
capturedOrMutated.add(effect.from.identifier.id);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@ import {
|
||||
} from '../HIR/visitors';
|
||||
import {empty} from '../Utils/Stack';
|
||||
import {getOrInsertWith} from '../Utils/utils';
|
||||
import {deadCodeElimination} from '../Optimization';
|
||||
import {BuiltInAutodepsId} from '../HIR/ObjectShape';
|
||||
|
||||
/**
|
||||
* Infers reactive dependencies captured by useEffect lambdas and adds them as
|
||||
@@ -77,7 +79,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
);
|
||||
moduleTargets.set(
|
||||
effectTarget.function.importSpecifierName,
|
||||
effectTarget.numRequiredArgs,
|
||||
effectTarget.autodepsIndex,
|
||||
);
|
||||
}
|
||||
const autodepFnLoads = new Map<IdentifierId, number>();
|
||||
@@ -135,7 +137,6 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
}
|
||||
} else if (value.kind === 'LoadGlobal') {
|
||||
loadGlobals.add(lvalue.identifier.id);
|
||||
|
||||
/*
|
||||
* TODO: Handle properties on default exports, like
|
||||
* import React from 'react';
|
||||
@@ -169,8 +170,22 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
) {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
|
||||
const autodepsArgIndex = value.args.findIndex(
|
||||
arg =>
|
||||
arg.kind === 'Identifier' &&
|
||||
arg.identifier.type.kind === 'Object' &&
|
||||
arg.identifier.type.shapeId === BuiltInAutodepsId,
|
||||
);
|
||||
const autodepsArgExpectedIndex = autodepFnLoads.get(
|
||||
callee.identifier.id,
|
||||
);
|
||||
|
||||
if (
|
||||
value.args.length === autodepFnLoads.get(callee.identifier.id) &&
|
||||
value.args.length > 0 &&
|
||||
autodepsArgExpectedIndex != null &&
|
||||
autodepsArgIndex === autodepsArgExpectedIndex &&
|
||||
autodepFnLoads.has(callee.identifier.id) &&
|
||||
value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
// We have a useEffect call with no deps array, so we need to infer the deps
|
||||
@@ -260,7 +275,10 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
effects: null,
|
||||
},
|
||||
});
|
||||
value.args.push({...depsPlace, effect: Effect.Freeze});
|
||||
value.args[autodepsArgIndex] = {
|
||||
...depsPlace,
|
||||
effect: Effect.Freeze,
|
||||
};
|
||||
fn.env.inferredEffectLocations.add(callee.loc);
|
||||
} else if (loadGlobals.has(value.args[0].identifier.id)) {
|
||||
// Global functions have no reactive dependencies, so we can insert an empty array
|
||||
@@ -275,7 +293,10 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
effects: null,
|
||||
},
|
||||
});
|
||||
value.args.push({...depsPlace, effect: Effect.Freeze});
|
||||
value.args[autodepsArgIndex] = {
|
||||
...depsPlace,
|
||||
effect: Effect.Freeze,
|
||||
};
|
||||
fn.env.inferredEffectLocations.add(callee.loc);
|
||||
}
|
||||
} else if (
|
||||
@@ -323,6 +344,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
// Renumber instructions and fix scope ranges
|
||||
markInstructionIds(fn.body);
|
||||
fixScopeAndIdentifierRanges(fn.body);
|
||||
deadCodeElimination(fn);
|
||||
|
||||
fn.env.hasInferredEffect = true;
|
||||
}
|
||||
@@ -408,6 +430,7 @@ function rewriteSplices(
|
||||
rewriteBlocks.push(currBlock);
|
||||
|
||||
let cursor = 0;
|
||||
|
||||
for (const rewrite of splices) {
|
||||
while (originalInstrs[cursor].id < rewrite.location) {
|
||||
CompilerError.invariant(
|
||||
@@ -429,7 +452,7 @@ function rewriteSplices(
|
||||
|
||||
if (rewrite.kind === 'instr') {
|
||||
currBlock.instructions.push(rewrite.value);
|
||||
} else {
|
||||
} else if (rewrite.kind === 'block') {
|
||||
const {entry, blocks} = rewrite.value;
|
||||
const entryBlock = blocks.get(entry)!;
|
||||
// splice in all instructions from the entry block
|
||||
|
||||
@@ -326,26 +326,26 @@ function isEffectSafeOutsideRender(effect: FunctionEffect): boolean {
|
||||
|
||||
export function getWriteErrorReason(abstractValue: AbstractValue): string {
|
||||
if (abstractValue.reason.has(ValueReason.Global)) {
|
||||
return 'Writing to a variable defined outside a component or hook is not allowed. Consider using an effect';
|
||||
return 'Modifying a variable defined outside a component or hook is not allowed. Consider using an effect';
|
||||
} else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
|
||||
return 'Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX';
|
||||
return 'Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX';
|
||||
} else if (abstractValue.reason.has(ValueReason.Context)) {
|
||||
return `Mutating a value returned from 'useContext()', which should not be mutated`;
|
||||
return `Modifying a value returned from 'useContext()' is not allowed.`;
|
||||
} else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
|
||||
return 'Mutating a value returned from a function whose return value should not be mutated';
|
||||
return 'Modifying a value returned from a function whose return value should not be mutated';
|
||||
} else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
|
||||
return 'Mutating component props or hook arguments is not allowed. Consider using a local variable instead';
|
||||
return 'Modifying component props or hook arguments is not allowed. Consider using a local variable instead';
|
||||
} else if (abstractValue.reason.has(ValueReason.State)) {
|
||||
return "Mutating a value returned from 'useState()', which should not be mutated. Use the setter function to update instead";
|
||||
return "Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead";
|
||||
} else if (abstractValue.reason.has(ValueReason.ReducerState)) {
|
||||
return "Mutating a value returned from 'useReducer()', which should not be mutated. Use the dispatch function to update instead";
|
||||
return "Modifying a value returned from 'useReducer()', which should not be modified directly. Use the dispatch function to update instead";
|
||||
} else if (abstractValue.reason.has(ValueReason.Effect)) {
|
||||
return 'Updating a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the mutation before calling useEffect()';
|
||||
return 'Modifying a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the modification before calling useEffect()';
|
||||
} else if (abstractValue.reason.has(ValueReason.HookCaptured)) {
|
||||
return 'Updating a value previously passed as an argument to a hook is not allowed. Consider moving the mutation before calling the hook';
|
||||
return 'Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook';
|
||||
} else if (abstractValue.reason.has(ValueReason.HookReturn)) {
|
||||
return 'Updating a value returned from a hook is not allowed. Consider moving the mutation into the hook where the value is constructed';
|
||||
return 'Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed';
|
||||
} else {
|
||||
return 'This mutates a variable that React considers immutable';
|
||||
return 'This modifies a variable that React considers immutable';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
Effect,
|
||||
ErrorSeverity,
|
||||
@@ -36,8 +37,8 @@ import {
|
||||
ValueReason,
|
||||
} from '../HIR';
|
||||
import {
|
||||
eachInstructionValueLValue,
|
||||
eachInstructionValueOperand,
|
||||
eachPatternItem,
|
||||
eachTerminalOperand,
|
||||
eachTerminalSuccessor,
|
||||
} from '../HIR/visitors';
|
||||
@@ -446,20 +447,23 @@ function applySignature(
|
||||
reason: value.reason,
|
||||
context: new Set(),
|
||||
});
|
||||
const variable =
|
||||
effect.value.identifier.name !== null &&
|
||||
effect.value.identifier.name.kind === 'named'
|
||||
? `\`${effect.value.identifier.name.value}\``
|
||||
: 'value';
|
||||
effects.push({
|
||||
kind: 'MutateFrozen',
|
||||
place: effect.value,
|
||||
error: {
|
||||
error: CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason,
|
||||
description:
|
||||
effect.value.identifier.name !== null &&
|
||||
effect.value.identifier.name.kind === 'named'
|
||||
? `Found mutation of \`${effect.value.identifier.name.value}\``
|
||||
: null,
|
||||
category: 'This value cannot be modified',
|
||||
description: `${reason}.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
suggestions: null,
|
||||
},
|
||||
message: `${variable} cannot be modified`,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -687,6 +691,7 @@ function applyEffect(
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'MaybeAlias':
|
||||
case 'Alias':
|
||||
case 'Capture': {
|
||||
CompilerError.invariant(
|
||||
@@ -951,7 +956,7 @@ function applyEffect(
|
||||
context,
|
||||
state,
|
||||
// OK: recording information flow
|
||||
{kind: 'Alias', from: operand, into: effect.into},
|
||||
{kind: 'MaybeAlias', from: operand, into: effect.into},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
@@ -1013,33 +1018,31 @@ function applyEffect(
|
||||
effect.value.identifier.declarationId,
|
||||
)
|
||||
) {
|
||||
const description =
|
||||
const variable =
|
||||
effect.value.identifier.name !== null &&
|
||||
effect.value.identifier.name.kind === 'named'
|
||||
? `Variable \`${effect.value.identifier.name.value}\` is accessed before it is declared`
|
||||
? `\`${effect.value.identifier.name.value}\``
|
||||
: null;
|
||||
const hoistedAccess = context.hoistedContextDeclarations.get(
|
||||
effect.value.identifier.declarationId,
|
||||
);
|
||||
const diagnostic = CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot access variable before it is declared',
|
||||
description: `${variable ?? 'This variable'} is accessed before it is declared, which prevents the earlier access from updating when this value changes over time.`,
|
||||
});
|
||||
if (hoistedAccess != null && hoistedAccess.loc != effect.value.loc) {
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
{
|
||||
kind: 'MutateFrozen',
|
||||
place: effect.value,
|
||||
error: {
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: `This variable is accessed before it is declared, which may prevent it from updating as the assigned value changes over time`,
|
||||
description,
|
||||
loc: hoistedAccess.loc,
|
||||
suggestions: null,
|
||||
},
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
diagnostic.withDetail({
|
||||
kind: 'error',
|
||||
loc: hoistedAccess.loc,
|
||||
message: `${variable ?? 'variable'} accessed before it is declared`,
|
||||
});
|
||||
}
|
||||
diagnostic.withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `${variable ?? 'variable'} is declared here`,
|
||||
});
|
||||
|
||||
applyEffect(
|
||||
context,
|
||||
@@ -1047,13 +1050,7 @@ function applyEffect(
|
||||
{
|
||||
kind: 'MutateFrozen',
|
||||
place: effect.value,
|
||||
error: {
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: `This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time`,
|
||||
description,
|
||||
loc: effect.value.loc,
|
||||
suggestions: null,
|
||||
},
|
||||
error: diagnostic,
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
@@ -1064,11 +1061,11 @@ function applyEffect(
|
||||
reason: value.reason,
|
||||
context: new Set(),
|
||||
});
|
||||
const description =
|
||||
const variable =
|
||||
effect.value.identifier.name !== null &&
|
||||
effect.value.identifier.name.kind === 'named'
|
||||
? `Found mutation of \`${effect.value.identifier.name.value}\``
|
||||
: null;
|
||||
? `\`${effect.value.identifier.name.value}\``
|
||||
: 'value';
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
@@ -1078,13 +1075,15 @@ function applyEffect(
|
||||
? 'MutateFrozen'
|
||||
: 'MutateGlobal',
|
||||
place: effect.value,
|
||||
error: {
|
||||
error: CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason,
|
||||
description,
|
||||
category: 'This value cannot be modified',
|
||||
description: `${reason}.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
suggestions: null,
|
||||
},
|
||||
message: `${variable} cannot be modified`,
|
||||
}),
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
@@ -1325,7 +1324,7 @@ class InferenceState {
|
||||
return 'mutate-global';
|
||||
}
|
||||
case ValueKind.MaybeFrozen: {
|
||||
return 'none';
|
||||
return 'mutate-frozen';
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(kind, `Unexpected kind ${kind}`);
|
||||
@@ -1864,19 +1863,34 @@ function computeSignatureForInstruction(
|
||||
break;
|
||||
}
|
||||
case 'Destructure': {
|
||||
for (const patternLValue of eachInstructionValueLValue(value)) {
|
||||
if (isPrimitiveType(patternLValue.identifier)) {
|
||||
for (const patternItem of eachPatternItem(value.lvalue.pattern)) {
|
||||
const place =
|
||||
patternItem.kind === 'Identifier' ? patternItem : patternItem.place;
|
||||
if (isPrimitiveType(place.identifier)) {
|
||||
effects.push({
|
||||
kind: 'Create',
|
||||
into: patternLValue,
|
||||
into: place,
|
||||
value: ValueKind.Primitive,
|
||||
reason: ValueReason.Other,
|
||||
});
|
||||
} else {
|
||||
} else if (patternItem.kind === 'Identifier') {
|
||||
effects.push({
|
||||
kind: 'CreateFrom',
|
||||
from: value.value,
|
||||
into: patternLValue,
|
||||
into: place,
|
||||
});
|
||||
} else {
|
||||
// Spread creates a new object/array that captures from the RValue
|
||||
effects.push({
|
||||
kind: 'Create',
|
||||
into: place,
|
||||
reason: ValueReason.Other,
|
||||
value: ValueKind.Mutable,
|
||||
});
|
||||
effects.push({
|
||||
kind: 'Capture',
|
||||
from: value.value,
|
||||
into: place,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1988,16 +2002,20 @@ function computeSignatureForInstruction(
|
||||
break;
|
||||
}
|
||||
case 'StoreGlobal': {
|
||||
const variable = `\`${value.name}\``;
|
||||
effects.push({
|
||||
kind: 'MutateGlobal',
|
||||
place: value.value,
|
||||
error: {
|
||||
reason:
|
||||
'Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)',
|
||||
loc: instr.loc,
|
||||
suggestions: null,
|
||||
error: CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
},
|
||||
category:
|
||||
'Cannot reassign variables declared outside of the component/hook',
|
||||
description: `Variable ${variable} is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: instr.loc,
|
||||
message: `${variable} cannot be reassigned`,
|
||||
}),
|
||||
});
|
||||
effects.push({kind: 'Assign', from: value.value, into: lvalue});
|
||||
break;
|
||||
@@ -2087,17 +2105,19 @@ function computeEffectsForLegacySignature(
|
||||
effects.push({
|
||||
kind: 'Impure',
|
||||
place: receiver,
|
||||
error: {
|
||||
reason:
|
||||
'Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
|
||||
description:
|
||||
signature.canonicalName != null
|
||||
? `\`${signature.canonicalName}\` is an impure function whose results may change on every call`
|
||||
: null,
|
||||
error: CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot call impure function during render',
|
||||
description:
|
||||
(signature.canonicalName != null
|
||||
? `\`${signature.canonicalName}\` is an impure function. `
|
||||
: '') +
|
||||
'Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc,
|
||||
suggestions: null,
|
||||
},
|
||||
message: 'Cannot call impure function',
|
||||
}),
|
||||
});
|
||||
}
|
||||
const stores: Array<Place> = [];
|
||||
@@ -2357,6 +2377,7 @@ function computeEffectsForSignature(
|
||||
// Apply substitutions
|
||||
for (const effect of signature.effects) {
|
||||
switch (effect.kind) {
|
||||
case 'MaybeAlias':
|
||||
case 'Assign':
|
||||
case 'ImmutableCapture':
|
||||
case 'Alias':
|
||||
|
||||
@@ -160,6 +160,8 @@ export function inferMutationAliasingRanges(
|
||||
state.assign(index++, effect.from, effect.into);
|
||||
} else if (effect.kind === 'Alias') {
|
||||
state.assign(index++, effect.from, effect.into);
|
||||
} else if (effect.kind === 'MaybeAlias') {
|
||||
state.maybeAlias(index++, effect.from, effect.into);
|
||||
} else if (effect.kind === 'Capture') {
|
||||
state.capture(index++, effect.from, effect.into);
|
||||
} else if (
|
||||
@@ -195,7 +197,7 @@ export function inferMutationAliasingRanges(
|
||||
effect.kind === 'MutateGlobal' ||
|
||||
effect.kind === 'Impure'
|
||||
) {
|
||||
errors.push(effect.error);
|
||||
errors.pushDiagnostic(effect.error);
|
||||
functionEffects.push(effect);
|
||||
} else if (effect.kind === 'Render') {
|
||||
renders.push({index: index++, place: effect.place});
|
||||
@@ -346,7 +348,8 @@ export function inferMutationAliasingRanges(
|
||||
case 'Assign':
|
||||
case 'Alias':
|
||||
case 'Capture':
|
||||
case 'CreateFrom': {
|
||||
case 'CreateFrom':
|
||||
case 'MaybeAlias': {
|
||||
const isMutatedOrReassigned =
|
||||
effect.into.identifier.mutableRange.end > instr.id;
|
||||
if (isMutatedOrReassigned) {
|
||||
@@ -549,7 +552,7 @@ function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
|
||||
case 'Impure':
|
||||
case 'MutateFrozen':
|
||||
case 'MutateGlobal': {
|
||||
errors.push(effect.error);
|
||||
errors.pushDiagnostic(effect.error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -567,7 +570,12 @@ type Node = {
|
||||
createdFrom: Map<Identifier, number>;
|
||||
captures: Map<Identifier, number>;
|
||||
aliases: Map<Identifier, number>;
|
||||
edges: Array<{index: number; node: Identifier; kind: 'capture' | 'alias'}>;
|
||||
maybeAliases: Map<Identifier, number>;
|
||||
edges: Array<{
|
||||
index: number;
|
||||
node: Identifier;
|
||||
kind: 'capture' | 'alias' | 'maybeAlias';
|
||||
}>;
|
||||
transitive: {kind: MutationKind; loc: SourceLocation} | null;
|
||||
local: {kind: MutationKind; loc: SourceLocation} | null;
|
||||
lastMutated: number;
|
||||
@@ -585,6 +593,7 @@ class AliasingState {
|
||||
createdFrom: new Map(),
|
||||
captures: new Map(),
|
||||
aliases: new Map(),
|
||||
maybeAliases: new Map(),
|
||||
edges: [],
|
||||
transitive: null,
|
||||
local: null,
|
||||
@@ -630,6 +639,18 @@ class AliasingState {
|
||||
}
|
||||
}
|
||||
|
||||
maybeAlias(index: number, from: Place, into: Place): void {
|
||||
const fromNode = this.nodes.get(from.identifier);
|
||||
const toNode = this.nodes.get(into.identifier);
|
||||
if (fromNode == null || toNode == null) {
|
||||
return;
|
||||
}
|
||||
fromNode.edges.push({index, node: into.identifier, kind: 'maybeAlias'});
|
||||
if (!toNode.maybeAliases.has(from.identifier)) {
|
||||
toNode.maybeAliases.set(from.identifier, index);
|
||||
}
|
||||
}
|
||||
|
||||
render(index: number, start: Identifier, errors: CompilerError): void {
|
||||
const seen = new Set<Identifier>();
|
||||
const queue: Array<Identifier> = [start];
|
||||
@@ -673,22 +694,24 @@ class AliasingState {
|
||||
// Null is used for simulated mutations
|
||||
end: InstructionId | null,
|
||||
transitive: boolean,
|
||||
kind: MutationKind,
|
||||
startKind: MutationKind,
|
||||
loc: SourceLocation,
|
||||
errors: CompilerError,
|
||||
): void {
|
||||
const seen = new Set<Identifier>();
|
||||
const seen = new Map<Identifier, MutationKind>();
|
||||
const queue: Array<{
|
||||
place: Identifier;
|
||||
transitive: boolean;
|
||||
direction: 'backwards' | 'forwards';
|
||||
}> = [{place: start, transitive, direction: 'backwards'}];
|
||||
kind: MutationKind;
|
||||
}> = [{place: start, transitive, direction: 'backwards', kind: startKind}];
|
||||
while (queue.length !== 0) {
|
||||
const {place: current, transitive, direction} = queue.pop()!;
|
||||
if (seen.has(current)) {
|
||||
const {place: current, transitive, direction, kind} = queue.pop()!;
|
||||
const previousKind = seen.get(current);
|
||||
if (previousKind != null && previousKind >= kind) {
|
||||
continue;
|
||||
}
|
||||
seen.add(current);
|
||||
seen.set(current, kind);
|
||||
const node = this.nodes.get(current);
|
||||
if (node == null) {
|
||||
continue;
|
||||
@@ -724,13 +747,18 @@ class AliasingState {
|
||||
if (edge.index >= index) {
|
||||
break;
|
||||
}
|
||||
queue.push({place: edge.node, transitive, direction: 'forwards'});
|
||||
queue.push({place: edge.node, transitive, direction: 'forwards', kind});
|
||||
}
|
||||
for (const [alias, when] of node.createdFrom) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push({place: alias, transitive: true, direction: 'backwards'});
|
||||
queue.push({
|
||||
place: alias,
|
||||
transitive: true,
|
||||
direction: 'backwards',
|
||||
kind,
|
||||
});
|
||||
}
|
||||
if (direction === 'backwards' || node.value.kind !== 'Phi') {
|
||||
/**
|
||||
@@ -747,7 +775,25 @@ class AliasingState {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push({place: alias, transitive, direction: 'backwards'});
|
||||
queue.push({place: alias, transitive, direction: 'backwards', kind});
|
||||
}
|
||||
/**
|
||||
* MaybeAlias indicates potential data flow from unknown function calls,
|
||||
* so we downgrade mutations through these aliases to consider them
|
||||
* conditional. This means we'll consider them for mutation *range*
|
||||
* purposes but not report validation errors for mutations, since
|
||||
* we aren't sure that the `from` value could actually be aliased.
|
||||
*/
|
||||
for (const [alias, when] of node.maybeAliases) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push({
|
||||
place: alias,
|
||||
transitive,
|
||||
direction: 'backwards',
|
||||
kind: MutationKind.Conditional,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
@@ -758,7 +804,12 @@ class AliasingState {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push({place: capture, transitive, direction: 'backwards'});
|
||||
queue.push({
|
||||
place: capture,
|
||||
transitive,
|
||||
direction: 'backwards',
|
||||
kind,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +153,10 @@ This is somewhat the inverse of `Capture`. The `CreateFrom` effect describes tha
|
||||
|
||||
Describes immutable data flow from one value to another. This is not currently used for anything, but is intended to eventually power a more sophisticated escape analysis.
|
||||
|
||||
### MaybeAlias
|
||||
|
||||
Describes potential data flow that the compiler knows may occur behind a function call, but cannot be sure about. For example, `foo(x)` _may_ be the identity function and return `x`, or `cond(a, b, c)` may conditionally return `b` or `c` depending on the value of `a`, but those functions could just as easily return new mutable values and not capture any information from their arguments. MaybeAlias represents that we have to consider the potential for data flow when deciding mutable ranges, but should be conservative about reporting errors. For example, `foo(someFrozenValue).property = true` should not error since we don't know for certain that foo returns its input.
|
||||
|
||||
### State-Changing Effects
|
||||
|
||||
The following effects describe state changes to specific values, not data flow. In many cases, JavaScript semantics will involve a combination of both data-flow effects *and* state-change effects. For example, `object.property = value` has data flow (`Capture object <- value`) and mutation (`Mutate object`).
|
||||
@@ -347,6 +351,17 @@ a.b = b; // capture
|
||||
mutate(a); // can transitively mutate b
|
||||
```
|
||||
|
||||
### MaybeAlias makes mutation conditional
|
||||
|
||||
Because we don't know for certain that the aliasing occurs, we consider the mutation conditional against the source.
|
||||
|
||||
```
|
||||
MaybeAlias a <- b
|
||||
Mutate a
|
||||
=>
|
||||
MutateConditional b
|
||||
```
|
||||
|
||||
### Freeze Does Not Freeze the Value
|
||||
|
||||
Freeze does not freeze the value itself:
|
||||
|
||||
@@ -829,12 +829,14 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
|
||||
};
|
||||
}
|
||||
case 'UnsupportedNode': {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected unsupported node`,
|
||||
description: null,
|
||||
loc: value.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
const lvalues = [];
|
||||
if (lvalue !== null) {
|
||||
lvalues.push({place: lvalue, level: MemoizationLevel.Never});
|
||||
}
|
||||
return {
|
||||
lvalues,
|
||||
rvalues: [],
|
||||
};
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
|
||||
@@ -360,6 +360,12 @@ function* generateInstructionTypes(
|
||||
value: makePropertyLiteral(propertyName),
|
||||
},
|
||||
});
|
||||
} else if (item.kind === 'Spread') {
|
||||
// Array pattern spread always creates an array
|
||||
yield equation(item.place.identifier.type, {
|
||||
kind: 'Object',
|
||||
shapeId: BuiltInArrayId,
|
||||
});
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -75,21 +75,21 @@ const testComplexConfigDefaults: PartialEnvironmentConfig = {
|
||||
source: 'react',
|
||||
importSpecifierName: 'useEffect',
|
||||
},
|
||||
numRequiredArgs: 1,
|
||||
autodepsIndex: 1,
|
||||
},
|
||||
{
|
||||
function: {
|
||||
source: 'shared-runtime',
|
||||
importSpecifierName: 'useSpecialEffect',
|
||||
},
|
||||
numRequiredArgs: 2,
|
||||
autodepsIndex: 2,
|
||||
},
|
||||
{
|
||||
function: {
|
||||
source: 'useEffectWrapper',
|
||||
importSpecifierName: 'default',
|
||||
},
|
||||
numRequiredArgs: 1,
|
||||
autodepsIndex: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -113,8 +113,13 @@ function* splitPragma(
|
||||
*/
|
||||
function parseConfigPragmaEnvironmentForTest(
|
||||
pragma: string,
|
||||
defaultConfig: PartialEnvironmentConfig,
|
||||
): EnvironmentConfig {
|
||||
const maybeConfig: Partial<Record<keyof EnvironmentConfig, unknown>> = {};
|
||||
// throw early if the defaults are invalid
|
||||
EnvironmentConfigSchema.parse(defaultConfig);
|
||||
|
||||
const maybeConfig: Partial<Record<keyof EnvironmentConfig, unknown>> =
|
||||
defaultConfig;
|
||||
|
||||
for (const {key, value: val} of splitPragma(pragma)) {
|
||||
if (!hasOwnProperty(EnvironmentConfigSchema.shape, key)) {
|
||||
@@ -174,9 +179,13 @@ export function parseConfigPragmaForTests(
|
||||
pragma: string,
|
||||
defaults: {
|
||||
compilationMode: CompilationMode;
|
||||
environment?: PartialEnvironmentConfig;
|
||||
},
|
||||
): PluginOptions {
|
||||
const environment = parseConfigPragmaEnvironmentForTest(pragma);
|
||||
const environment = parseConfigPragmaEnvironmentForTest(
|
||||
pragma,
|
||||
defaults.environment ?? {},
|
||||
);
|
||||
const options: Record<keyof PluginOptions, unknown> = {
|
||||
...defaultOptions,
|
||||
panicThreshold: 'all_errors',
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, Effect} from '..';
|
||||
import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..';
|
||||
import {HIRFunction, IdentifierId, Place} from '../HIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
@@ -28,16 +28,24 @@ export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
|
||||
false,
|
||||
);
|
||||
if (reassignment !== null) {
|
||||
CompilerError.throwInvalidReact({
|
||||
reason:
|
||||
'Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead',
|
||||
description:
|
||||
reassignment.identifier.name !== null &&
|
||||
reassignment.identifier.name.kind === 'named'
|
||||
? `Variable \`${reassignment.identifier.name.value}\` cannot be reassigned after render`
|
||||
: '',
|
||||
loc: reassignment.loc,
|
||||
});
|
||||
const errors = new CompilerError();
|
||||
const variable =
|
||||
reassignment.identifier.name != null &&
|
||||
reassignment.identifier.name.kind === 'named'
|
||||
? `\`${reassignment.identifier.name.value}\``
|
||||
: 'variable';
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot reassign variable after render completes',
|
||||
description: `Reassigning ${variable} after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: reassignment.loc,
|
||||
message: `Cannot reassign ${variable} after render completes`,
|
||||
}),
|
||||
);
|
||||
throw errors;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,16 +83,25 @@ function getContextReassignment(
|
||||
// if the function or its depends reassign, propagate that fact on the lvalue
|
||||
if (reassignment !== null) {
|
||||
if (isAsync || value.loweredFunc.func.async) {
|
||||
CompilerError.throwInvalidReact({
|
||||
reason:
|
||||
'Reassigning a variable in an async function can cause inconsistent behavior on subsequent renders. Consider using state instead',
|
||||
description:
|
||||
reassignment.identifier.name !== null &&
|
||||
reassignment.identifier.name.kind === 'named'
|
||||
? `Variable \`${reassignment.identifier.name.value}\` cannot be reassigned after render`
|
||||
: '',
|
||||
loc: reassignment.loc,
|
||||
});
|
||||
const errors = new CompilerError();
|
||||
const variable =
|
||||
reassignment.identifier.name !== null &&
|
||||
reassignment.identifier.name.kind === 'named'
|
||||
? `\`${reassignment.identifier.name.value}\``
|
||||
: 'variable';
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot reassign variable in async function',
|
||||
description:
|
||||
'Reassigning a variable in an async function can cause inconsistent behavior on subsequent renders. Consider using state instead',
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: reassignment.loc,
|
||||
message: `Cannot reassign ${variable}`,
|
||||
}),
|
||||
);
|
||||
throw errors;
|
||||
}
|
||||
reassigningFunctions.set(lvalue.identifier.id, reassignment);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, Effect, ErrorSeverity} from '..';
|
||||
import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..';
|
||||
import {
|
||||
FunctionEffect,
|
||||
HIRFunction,
|
||||
@@ -57,16 +57,30 @@ export function validateNoFreezingKnownMutableFunctions(
|
||||
if (operand.effect === Effect.Freeze) {
|
||||
const effect = contextMutationEffects.get(operand.identifier.id);
|
||||
if (effect != null) {
|
||||
errors.push({
|
||||
reason: `This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead`,
|
||||
loc: operand.loc,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
});
|
||||
errors.push({
|
||||
reason: `The function modifies a local variable here`,
|
||||
loc: effect.loc,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
});
|
||||
const place = [...effect.places][0];
|
||||
const variable =
|
||||
place != null &&
|
||||
place.identifier.name != null &&
|
||||
place.identifier.name.kind === 'named'
|
||||
? `\`${place.identifier.name.value}\``
|
||||
: 'a local variable';
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot modify local variables after render completes',
|
||||
description: `This argument is a function which may reassign or mutate ${variable} after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.`,
|
||||
})
|
||||
.withDetail({
|
||||
kind: 'error',
|
||||
loc: operand.loc,
|
||||
message: `This function may (indirectly) reassign or modify ${variable} after render`,
|
||||
})
|
||||
.withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.loc,
|
||||
message: `This modifies ${variable}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, ErrorSeverity} from '..';
|
||||
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
|
||||
import {HIRFunction} from '../HIR';
|
||||
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
|
||||
import {Result} from '../Utils/Result';
|
||||
@@ -34,17 +34,22 @@ export function validateNoImpureFunctionsInRender(
|
||||
callee.identifier.type,
|
||||
);
|
||||
if (signature != null && signature.impure === true) {
|
||||
errors.push({
|
||||
reason:
|
||||
'Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
|
||||
description:
|
||||
signature.canonicalName != null
|
||||
? `\`${signature.canonicalName}\` is an impure function whose results may change on every call`
|
||||
: null,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc: callee.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: 'Cannot call impure function during render',
|
||||
description:
|
||||
(signature.canonicalName != null
|
||||
? `\`${signature.canonicalName}\` is an impure function. `
|
||||
: '') +
|
||||
'Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: callee.loc,
|
||||
message: 'Cannot call impure function',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, ErrorSeverity} from '..';
|
||||
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
|
||||
import {BlockId, HIRFunction} from '../HIR';
|
||||
import {Result} from '../Utils/Result';
|
||||
import {retainWhere} from '../Utils/utils';
|
||||
@@ -34,11 +34,17 @@ export function validateNoJSXInTryStatement(
|
||||
switch (value.kind) {
|
||||
case 'JsxExpression':
|
||||
case 'JsxFragment': {
|
||||
errors.push({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: `Unexpected JSX element within a try statement. To catch errors in rendering a given component, wrap that component in an error boundary. (https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)`,
|
||||
loc: value.loc,
|
||||
});
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Avoid constructing JSX within try/catch',
|
||||
description: `React does not immediately render components when JSX is rendered, so any errors from this component will not be caught by the try/catch. To catch errors in rendering a given component, wrap that component in an error boundary. (https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: value.loc,
|
||||
message: 'Avoid constructing JSX within try/catch',
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,26 +5,32 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, ErrorSeverity} from '../CompilerError';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
isSetStateType,
|
||||
isUseEffectHookType,
|
||||
isUseInsertionEffectHookType,
|
||||
isUseLayoutEffectHookType,
|
||||
Place,
|
||||
} from '../HIR';
|
||||
import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
/**
|
||||
* Validates against calling setState in the body of a *passive* effect (useEffect),
|
||||
* Validates against calling setState in the body of an effect (useEffect and friends),
|
||||
* while allowing calling setState in callbacks scheduled by the effect.
|
||||
*
|
||||
* Calling setState during execution of a useEffect triggers a re-render, which is
|
||||
* often bad for performance and frequently has more efficient and straightforward
|
||||
* alternatives. See https://react.dev/learn/you-might-not-need-an-effect for examples.
|
||||
*/
|
||||
export function validateNoSetStateInPassiveEffects(
|
||||
export function validateNoSetStateInEffects(
|
||||
fn: HIRFunction,
|
||||
): Result<void, CompilerError> {
|
||||
const setStateFunctions: Map<IdentifierId, Place> = new Map();
|
||||
@@ -79,19 +85,30 @@ export function validateNoSetStateInPassiveEffects(
|
||||
instr.value.kind === 'MethodCall'
|
||||
? instr.value.receiver
|
||||
: instr.value.callee;
|
||||
if (isUseEffectHookType(callee.identifier)) {
|
||||
if (
|
||||
isUseEffectHookType(callee.identifier) ||
|
||||
isUseLayoutEffectHookType(callee.identifier) ||
|
||||
isUseInsertionEffectHookType(callee.identifier)
|
||||
) {
|
||||
const arg = instr.value.args[0];
|
||||
if (arg !== undefined && arg.kind === 'Identifier') {
|
||||
const setState = setStateFunctions.get(arg.identifier.id);
|
||||
if (setState !== undefined) {
|
||||
errors.push({
|
||||
reason:
|
||||
'Calling setState directly within a useEffect causes cascading renders and is not recommended. Consider alternatives to useEffect. (https://react.dev/learn/you-might-not-need-an-effect)',
|
||||
description: null,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc: setState.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category:
|
||||
'Calling setState within an effect can trigger cascading renders',
|
||||
description:
|
||||
'Calling setState directly within a useEffect causes cascading renders that can hurt performance, and is not recommended. Consider alternatives to useEffect. (https://react.dev/learn/you-might-not-need-an-effect)',
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: setState.loc,
|
||||
message:
|
||||
'Avoid calling setState() in the top-level of an effect',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,11 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, ErrorSeverity} from '../CompilerError';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {HIRFunction, IdentifierId, isSetStateType} from '../HIR';
|
||||
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
|
||||
import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
@@ -122,23 +126,35 @@ function validateNoSetStateInRenderImpl(
|
||||
unconditionalSetStateFunctions.has(callee.identifier.id)
|
||||
) {
|
||||
if (activeManualMemoId !== null) {
|
||||
errors.push({
|
||||
reason:
|
||||
'Calling setState from useMemo may trigger an infinite loop. (https://react.dev/reference/react/useState)',
|
||||
description: null,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc: callee.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category:
|
||||
'Calling setState from useMemo may trigger an infinite loop',
|
||||
description:
|
||||
'Each time the memo callback is evaluated it will change state. This can cause a memoization dependency to change, running the memo function again and causing an infinite loop. Instead of setting state in useMemo(), prefer deriving the value during render. (https://react.dev/reference/react/useState)',
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: callee.loc,
|
||||
message: 'Found setState() within useMemo()',
|
||||
}),
|
||||
);
|
||||
} else if (unconditionalBlocks.has(block.id)) {
|
||||
errors.push({
|
||||
reason:
|
||||
'This is an unconditional set state during render, which will trigger an infinite loop. (https://react.dev/reference/react/useState)',
|
||||
description: null,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc: callee.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category:
|
||||
'Calling setState during render may trigger an infinite loop',
|
||||
description:
|
||||
'Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState)',
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: callee.loc,
|
||||
message: 'Found setState() within useMemo()',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, ErrorSeverity} from '../CompilerError';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {
|
||||
DeclarationId,
|
||||
Effect,
|
||||
@@ -275,27 +279,37 @@ function validateInferredDep(
|
||||
errorDiagnostic = merge(errorDiagnostic ?? compareResult, compareResult);
|
||||
}
|
||||
}
|
||||
errorState.push({
|
||||
severity: ErrorSeverity.CannotPreserveMemoization,
|
||||
reason:
|
||||
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected',
|
||||
description:
|
||||
DEBUG ||
|
||||
// If the dependency is a named variable then we can report it. Otherwise only print in debug mode
|
||||
(dep.identifier.name != null && dep.identifier.name.kind === 'named')
|
||||
? `The inferred dependency was \`${prettyPrintScopeDependency(
|
||||
dep,
|
||||
)}\`, but the source dependencies were [${validDepsInMemoBlock
|
||||
.map(dep => printManualMemoDependency(dep, true))
|
||||
.join(', ')}]. ${
|
||||
errorDiagnostic
|
||||
? getCompareDependencyResultDescription(errorDiagnostic)
|
||||
: 'Inferred dependency not present in source'
|
||||
}`
|
||||
: null,
|
||||
loc: memoLocation,
|
||||
suggestions: null,
|
||||
});
|
||||
errorState.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.CannotPreserveMemoization,
|
||||
category:
|
||||
'Compilation skipped because existing memoization could not be preserved',
|
||||
description: [
|
||||
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. ',
|
||||
'The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. ',
|
||||
DEBUG ||
|
||||
// If the dependency is a named variable then we can report it. Otherwise only print in debug mode
|
||||
(dep.identifier.name != null && dep.identifier.name.kind === 'named')
|
||||
? `The inferred dependency was \`${prettyPrintScopeDependency(
|
||||
dep,
|
||||
)}\`, but the source dependencies were [${validDepsInMemoBlock
|
||||
.map(dep => printManualMemoDependency(dep, true))
|
||||
.join(', ')}]. ${
|
||||
errorDiagnostic
|
||||
? getCompareDependencyResultDescription(errorDiagnostic)
|
||||
: 'Inferred dependency not present in source'
|
||||
}.`
|
||||
: '',
|
||||
]
|
||||
.join('')
|
||||
.trim(),
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: memoLocation,
|
||||
message: 'Could not preserve existing manual memoization',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
@@ -519,14 +533,21 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
!this.scopes.has(identifier.scope.id) &&
|
||||
!this.prunedScopes.has(identifier.scope.id)
|
||||
) {
|
||||
state.errors.push({
|
||||
reason:
|
||||
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly',
|
||||
description: null,
|
||||
severity: ErrorSeverity.CannotPreserveMemoization,
|
||||
loc,
|
||||
suggestions: null,
|
||||
});
|
||||
state.errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.CannotPreserveMemoization,
|
||||
category:
|
||||
'Compilation skipped because existing memoization could not be preserved',
|
||||
description: [
|
||||
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. ',
|
||||
'This dependency may be mutated later, which could cause the value to change unexpectedly.',
|
||||
].join(''),
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc,
|
||||
message: 'This dependency may be modified later',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -560,16 +581,25 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
|
||||
for (const identifier of decls) {
|
||||
if (isUnmemoized(identifier, this.scopes)) {
|
||||
state.errors.push({
|
||||
reason:
|
||||
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.',
|
||||
description: DEBUG
|
||||
? `${printIdentifier(identifier)} was not memoized`
|
||||
: null,
|
||||
severity: ErrorSeverity.CannotPreserveMemoization,
|
||||
loc,
|
||||
suggestions: null,
|
||||
});
|
||||
state.errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.CannotPreserveMemoization,
|
||||
category:
|
||||
'Compilation skipped because existing memoization could not be preserved',
|
||||
description: [
|
||||
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. ',
|
||||
DEBUG
|
||||
? `${printIdentifier(identifier)} was not memoized.`
|
||||
: '',
|
||||
]
|
||||
.join('')
|
||||
.trim(),
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc,
|
||||
message: 'Could not preserve existing memoization',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, ErrorSeverity} from '../CompilerError';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {HIRFunction, IdentifierId, SourceLocation} from '../HIR';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
@@ -59,20 +63,23 @@ export function validateStaticComponents(
|
||||
value.tag.identifier.id,
|
||||
);
|
||||
if (location != null) {
|
||||
error.push({
|
||||
reason: `Components created during render will reset their state each time they are created. Declare components outside of render. `,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc: value.tag.loc,
|
||||
description: null,
|
||||
suggestions: null,
|
||||
});
|
||||
error.push({
|
||||
reason: `The component may be created during render`,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc: location,
|
||||
description: null,
|
||||
suggestions: null,
|
||||
});
|
||||
error.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'Cannot create components during render',
|
||||
description: `Components created during render will reset their state each time they are created. Declare components outside of render. `,
|
||||
})
|
||||
.withDetail({
|
||||
kind: 'error',
|
||||
loc: value.tag.loc,
|
||||
message: 'This component is created during render',
|
||||
})
|
||||
.withDetail({
|
||||
kind: 'error',
|
||||
loc: location,
|
||||
message: 'The component is created during render here',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, ErrorSeverity} from '..';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {FunctionExpression, HIRFunction, IdentifierId} from '../HIR';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
@@ -63,24 +67,41 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
}
|
||||
|
||||
if (body.loweredFunc.func.params.length > 0) {
|
||||
errors.push({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'useMemo callbacks may not accept any arguments',
|
||||
description: null,
|
||||
loc: body.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
const firstParam = body.loweredFunc.func.params[0];
|
||||
const loc =
|
||||
firstParam.kind === 'Identifier'
|
||||
? firstParam.loc
|
||||
: firstParam.place.loc;
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'useMemo() callbacks may not accept parameters',
|
||||
description:
|
||||
'useMemo() callbacks are called by React to cache calculations across re-renders. They should not take parameters. Instead, directly reference the props, state, or local variables needed for the computation.',
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc,
|
||||
message: 'Callbacks with parameters are not supported',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (body.loweredFunc.func.async || body.loweredFunc.func.generator) {
|
||||
errors.push({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason:
|
||||
'useMemo callbacks may not be async or generator functions',
|
||||
description: null,
|
||||
loc: body.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category:
|
||||
'useMemo() callbacks may not be async or generator functions',
|
||||
description:
|
||||
'useMemo() callbacks are called once and must synchronously return a value.',
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: body.loc,
|
||||
message: 'Async and generator functions are not supported',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
@@ -58,7 +58,8 @@ it('logs failed compilation', () => {
|
||||
|
||||
expect(event.detail.severity).toEqual('InvalidReact');
|
||||
//@ts-ignore
|
||||
const {start, end, identifierName} = event.detail.loc as t.SourceLocation;
|
||||
const {start, end, identifierName} =
|
||||
event.detail.primaryLocation() as t.SourceLocation;
|
||||
expect(start).toEqual({column: 28, index: 28, line: 1});
|
||||
expect(end).toEqual({column: 33, index: 33, line: 1});
|
||||
expect(identifierName).toEqual('props');
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('parseConfigPragma()', () => {
|
||||
validateHooksUsage: 1,
|
||||
} as any);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"InvalidConfig: 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: Expected boolean, received number at "validateHooksUsage"."`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -33,12 +33,12 @@ describe('parseConfigPragma()', () => {
|
||||
source: 'react',
|
||||
importSpecifierName: 'useEffect',
|
||||
},
|
||||
numRequiredArgs: 0,
|
||||
autodepsIndex: 0,
|
||||
},
|
||||
],
|
||||
} as any);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"InvalidConfig: Could not validate environment config. Update React Compiler config to fix the error. Validation error: numRequiredArgs must be > 0 at "inferEffectDependencies[0].numRequiredArgs""`,
|
||||
`"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,104 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
import {useMemo} from 'react';
|
||||
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
// Should memoize independently
|
||||
const x = useMemo(() => makeObject_Primitives(), []);
|
||||
|
||||
const rest = useMemo(() => {
|
||||
const [_, ...rest] = props.array;
|
||||
|
||||
// Should be inferred as Array.proto.push which doesn't mutate input
|
||||
rest.push(x);
|
||||
return rest;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<ValidateMemoization inputs={[]} output={x} />
|
||||
<ValidateMemoization inputs={[props.array]} output={rest} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{array: [0, 1, 2]}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
|
||||
import { useMemo } from "react";
|
||||
import { makeObject_Primitives, ValidateMemoization } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(9);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = makeObject_Primitives();
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const x = t0;
|
||||
let rest;
|
||||
if ($[1] !== props.array) {
|
||||
[, ...rest] = props.array;
|
||||
|
||||
rest.push(x);
|
||||
$[1] = props.array;
|
||||
$[2] = rest;
|
||||
} else {
|
||||
rest = $[2];
|
||||
}
|
||||
const rest_0 = rest;
|
||||
let t1;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <ValidateMemoization inputs={[]} output={x} />;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
let t2;
|
||||
if ($[4] !== props.array) {
|
||||
t2 = [props.array];
|
||||
$[4] = props.array;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
let t3;
|
||||
if ($[6] !== rest_0 || $[7] !== t2) {
|
||||
t3 = (
|
||||
<>
|
||||
{t1}
|
||||
<ValidateMemoization inputs={t2} output={rest_0} />
|
||||
</>
|
||||
);
|
||||
$[6] = rest_0;
|
||||
$[7] = t2;
|
||||
$[8] = t3;
|
||||
} else {
|
||||
t3 = $[8];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ array: [0, 1, 2] }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"inputs":[],"output":{"a":0,"b":"value1","c":true}}</div><div>{"inputs":[[0,1,2]],"output":[1,2,{"a":0,"b":"value1","c":true}]}</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
import {useMemo} from 'react';
|
||||
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
// Should memoize independently
|
||||
const x = useMemo(() => makeObject_Primitives(), []);
|
||||
|
||||
const rest = useMemo(() => {
|
||||
const [_, ...rest] = props.array;
|
||||
|
||||
// Should be inferred as Array.proto.push which doesn't mutate input
|
||||
rest.push(x);
|
||||
return rest;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<ValidateMemoization inputs={[]} output={x} />
|
||||
<ValidateMemoization inputs={[props.array]} output={rest} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{array: [0, 1, 2]}],
|
||||
};
|
||||
@@ -15,10 +15,15 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Todo: (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern
|
||||
|
||||
error._todo.computed-lval-in-destructure.ts:3:9
|
||||
1 | function Component(props) {
|
||||
2 | const computedKey = props.key;
|
||||
> 3 | const {[computedKey]: x} = props.val;
|
||||
| ^^^^^^^^^^^^^^^^ Todo: (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern (3:3)
|
||||
| ^^^^^^^^^^^^^^^^ (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern
|
||||
4 |
|
||||
5 | return x;
|
||||
6 | }
|
||||
|
||||
@@ -15,10 +15,17 @@ function Component() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot reassign variables declared outside of the component/hook
|
||||
|
||||
Variable `someGlobal` is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)
|
||||
|
||||
error.assign-global-in-component-tag-function.ts:3:4
|
||||
1 | function Component() {
|
||||
2 | const Foo = () => {
|
||||
> 3 | someGlobal = true;
|
||||
| ^^^^^^^^^^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (3:3)
|
||||
| ^^^^^^^^^^ `someGlobal` cannot be reassigned
|
||||
4 | };
|
||||
5 | return <Foo />;
|
||||
6 | }
|
||||
|
||||
@@ -18,10 +18,17 @@ function Component() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot reassign variables declared outside of the component/hook
|
||||
|
||||
Variable `someGlobal` is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)
|
||||
|
||||
error.assign-global-in-jsx-children.ts:3:4
|
||||
1 | function Component() {
|
||||
2 | const foo = () => {
|
||||
> 3 | someGlobal = true;
|
||||
| ^^^^^^^^^^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (3:3)
|
||||
| ^^^^^^^^^^ `someGlobal` cannot be reassigned
|
||||
4 | };
|
||||
5 | // Children are generally access/called during render, so
|
||||
6 | // modifying a global in a children function is almost
|
||||
|
||||
@@ -16,10 +16,15 @@ function Component() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)
|
||||
|
||||
error.assign-global-in-jsx-spread-attribute.ts:4:4
|
||||
2 | function Component() {
|
||||
3 | const foo = () => {
|
||||
> 4 | someGlobal = true;
|
||||
| ^^^^^^^^^^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (4:4)
|
||||
| ^^^^^^^^^^ Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)
|
||||
5 | };
|
||||
6 | return <div {...foo} />;
|
||||
7 | }
|
||||
|
||||
@@ -16,10 +16,17 @@ function Foo(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: React Compiler has skipped optimizing this component because one or more React rule violations were reported by Flow
|
||||
|
||||
React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression `$FlowFixMe[react-rule-hook]`
|
||||
|
||||
error.bailout-on-flow-suppression.ts:4:2
|
||||
2 |
|
||||
3 | function Foo(props) {
|
||||
> 4 | // $FlowFixMe[react-rule-hook]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: React Compiler has skipped optimizing this component because one or more React rule violations were reported by Flow. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. $FlowFixMe[react-rule-hook] (4:4)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Found React rule suppression
|
||||
5 | useX();
|
||||
6 | return null;
|
||||
7 | }
|
||||
|
||||
@@ -19,15 +19,33 @@ function lowercasecomponent() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
|
||||
Error: React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled
|
||||
|
||||
React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression `eslint-disable my-app/react-rule`
|
||||
|
||||
error.bailout-on-suppression-of-custom-rule.ts:3:0
|
||||
1 | // @eslintSuppressionRules:["my-app","react-rule"]
|
||||
2 |
|
||||
> 3 | /* eslint-disable my-app/react-rule */
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. eslint-disable my-app/react-rule (3:3)
|
||||
|
||||
InvalidReact: React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. eslint-disable-next-line my-app/react-rule (7:7)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Found React rule suppression
|
||||
4 | function lowercasecomponent() {
|
||||
5 | 'use forget';
|
||||
6 | const x = [];
|
||||
|
||||
Error: React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled
|
||||
|
||||
React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression `eslint-disable-next-line my-app/react-rule`
|
||||
|
||||
error.bailout-on-suppression-of-custom-rule.ts:7:2
|
||||
5 | 'use forget';
|
||||
6 | const x = [];
|
||||
> 7 | // eslint-disable-next-line my-app/react-rule
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Found React rule suppression
|
||||
8 | return <div>{x}</div>;
|
||||
9 | }
|
||||
10 | /* eslint-enable my-app/react-rule */
|
||||
```
|
||||
|
||||
|
||||
@@ -36,6 +36,13 @@ function Component() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot modify local variables after render completes
|
||||
|
||||
This argument is a function which may reassign or mutate a local variable after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
|
||||
|
||||
error.bug-old-inference-false-positive-ref-validation-in-use-effect.ts:20:12
|
||||
18 | );
|
||||
19 | const ref = useRef(null);
|
||||
> 20 | useEffect(() => {
|
||||
@@ -47,12 +54,19 @@ function Component() {
|
||||
> 23 | }
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 24 | }, [update]);
|
||||
| ^^^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (20:24)
|
||||
|
||||
InvalidReact: The function modifies a local variable here (14:14)
|
||||
| ^^^^ This function may (indirectly) reassign or modify a local variable after render
|
||||
25 |
|
||||
26 | return 'ok';
|
||||
27 | }
|
||||
|
||||
error.bug-old-inference-false-positive-ref-validation-in-use-effect.ts:14:6
|
||||
12 | ...partialParams,
|
||||
13 | };
|
||||
> 14 | nextParams.param = 'value';
|
||||
| ^^^^^^^^^^ This modifies a local variable
|
||||
15 | console.log(nextParams);
|
||||
16 | },
|
||||
17 | [params]
|
||||
```
|
||||
|
||||
|
||||
@@ -14,10 +14,15 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Invariant: Const declaration cannot be referenced as an expression
|
||||
|
||||
error.call-args-destructuring-asignment-complex.ts:3:9
|
||||
1 | function Component(props) {
|
||||
2 | let x = makeObject();
|
||||
> 3 | x.foo(([[x]] = makeObject()));
|
||||
| ^^^^^ Invariant: Const declaration cannot be referenced as an expression (3:3)
|
||||
| ^^^^^ Const declaration cannot be referenced as an expression
|
||||
4 | return x;
|
||||
5 | }
|
||||
6 |
|
||||
|
||||
@@ -14,10 +14,17 @@ function Foo() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config
|
||||
|
||||
Bar may be a component..
|
||||
|
||||
error.capitalized-function-call-aliased.ts:4:2
|
||||
2 | function Foo() {
|
||||
3 | let x = Bar;
|
||||
> 4 | x(); // ERROR
|
||||
| ^^^ InvalidReact: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config. Bar may be a component. (4:4)
|
||||
| ^^^ Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config
|
||||
5 | }
|
||||
6 |
|
||||
```
|
||||
|
||||
@@ -15,10 +15,17 @@ function Component() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config
|
||||
|
||||
SomeFunc may be a component..
|
||||
|
||||
error.capitalized-function-call.ts:3:12
|
||||
1 | // @validateNoCapitalizedCalls
|
||||
2 | function Component() {
|
||||
> 3 | const x = SomeFunc();
|
||||
| ^^^^^^^^^^ InvalidReact: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config. SomeFunc may be a component. (3:3)
|
||||
| ^^^^^^^^^^ Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config
|
||||
4 |
|
||||
5 | return x;
|
||||
6 | }
|
||||
|
||||
@@ -15,10 +15,17 @@ function Component() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config
|
||||
|
||||
SomeFunc may be a component..
|
||||
|
||||
error.capitalized-method-call.ts:3:12
|
||||
1 | // @validateNoCapitalizedCalls
|
||||
2 | function Component() {
|
||||
> 3 | const x = someGlobal.SomeFunc();
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config. SomeFunc may be a component. (3:3)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config
|
||||
4 |
|
||||
5 | return x;
|
||||
6 | }
|
||||
|
||||
@@ -32,19 +32,51 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 4 errors:
|
||||
|
||||
Error: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)
|
||||
|
||||
error.capture-ref-for-mutation.ts:12:13
|
||||
10 | };
|
||||
11 | const moveLeft = {
|
||||
> 12 | handler: handleKey('left')(),
|
||||
| ^^^^^^^^^^^^^^^^^ InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) (12:12)
|
||||
|
||||
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (12:12)
|
||||
|
||||
InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) (15:15)
|
||||
|
||||
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (15:15)
|
||||
| ^^^^^^^^^^^^^^^^^ This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)
|
||||
13 | };
|
||||
14 | const moveRight = {
|
||||
15 | handler: handleKey('right')(),
|
||||
|
||||
Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
|
||||
|
||||
error.capture-ref-for-mutation.ts:12:13
|
||||
10 | };
|
||||
11 | const moveLeft = {
|
||||
> 12 | handler: handleKey('left')(),
|
||||
| ^^^^^^^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
|
||||
13 | };
|
||||
14 | const moveRight = {
|
||||
15 | handler: handleKey('right')(),
|
||||
|
||||
Error: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)
|
||||
|
||||
error.capture-ref-for-mutation.ts:15:13
|
||||
13 | };
|
||||
14 | const moveRight = {
|
||||
> 15 | handler: handleKey('right')(),
|
||||
| ^^^^^^^^^^^^^^^^^^ This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)
|
||||
16 | };
|
||||
17 | return [moveLeft, moveRight];
|
||||
18 | }
|
||||
|
||||
Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
|
||||
|
||||
error.capture-ref-for-mutation.ts:15:13
|
||||
13 | };
|
||||
14 | const moveRight = {
|
||||
> 15 | handler: handleKey('right')(),
|
||||
| ^^^^^^^^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
|
||||
16 | };
|
||||
17 | return [moveLeft, moveRight];
|
||||
18 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -16,10 +16,15 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
|
||||
|
||||
error.conditional-hook-unknown-hook-react-namespace.ts:4:8
|
||||
2 | let x = null;
|
||||
3 | if (props.cond) {
|
||||
> 4 | x = React.useNonexistentHook();
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (4:4)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
|
||||
5 | }
|
||||
6 | return x;
|
||||
7 | }
|
||||
|
||||
@@ -16,10 +16,15 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
|
||||
|
||||
error.conditional-hooks-as-method-call.ts:4:8
|
||||
2 | let x = null;
|
||||
3 | if (props.cond) {
|
||||
> 4 | x = Foo.useFoo();
|
||||
| ^^^^^^^^^^ InvalidReact: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (4:4)
|
||||
| ^^^^^^^^^^ Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
|
||||
5 | }
|
||||
6 | return x;
|
||||
7 | }
|
||||
|
||||
@@ -28,10 +28,17 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot reassign variable after render completes
|
||||
|
||||
Reassigning `x` after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.
|
||||
|
||||
error.context-variable-only-chained-assign.ts:10:19
|
||||
8 | };
|
||||
9 | const fn2 = () => {
|
||||
> 10 | const copy2 = (x = 4);
|
||||
| ^ InvalidReact: Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `x` cannot be reassigned after render (10:10)
|
||||
| ^ Cannot reassign `x` after render completes
|
||||
11 | return [invoke(fn1), copy2, identity(copy2)];
|
||||
12 | };
|
||||
13 | return invoke(fn2);
|
||||
|
||||
@@ -17,10 +17,17 @@ function Component() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot reassign variable after render completes
|
||||
|
||||
Reassigning `x` after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.
|
||||
|
||||
error.declare-reassign-variable-in-function-declaration.ts:4:4
|
||||
2 | let x = null;
|
||||
3 | function foo() {
|
||||
> 4 | x = 9;
|
||||
| ^ InvalidReact: Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `x` cannot be reassigned after render (4:4)
|
||||
| ^ Cannot reassign `x` after render completes
|
||||
5 | }
|
||||
6 | const y = bar(foo);
|
||||
7 | return <Child y={y} />;
|
||||
|
||||
@@ -22,6 +22,11 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Todo: (BuildHIR::node.lowerReorderableExpression) Expression type `ArrowFunctionExpression` cannot be safely reordered
|
||||
|
||||
error.default-param-accesses-local.ts:3:6
|
||||
1 | function Component(
|
||||
2 | x,
|
||||
> 3 | y = () => {
|
||||
@@ -29,7 +34,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
> 4 | return x;
|
||||
| ^^^^^^^^^^^^^
|
||||
> 5 | }
|
||||
| ^^^^ Todo: (BuildHIR::node.lowerReorderableExpression) Expression type `ArrowFunctionExpression` cannot be safely reordered (3:5)
|
||||
| ^^^^ (BuildHIR::node.lowerReorderableExpression) Expression type `ArrowFunctionExpression` cannot be safely reordered
|
||||
6 | ) {
|
||||
7 | return y();
|
||||
8 | }
|
||||
|
||||
@@ -19,10 +19,17 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Todo: [hoisting] EnterSSA: Expected identifier to be defined before being used
|
||||
|
||||
Identifier x$1 is undefined.
|
||||
|
||||
error.dont-hoist-inline-reference.ts:3:2
|
||||
1 | import {identity} from 'shared-runtime';
|
||||
2 | function useInvalid() {
|
||||
> 3 | const x = identity(x);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ Todo: [hoisting] EnterSSA: Expected identifier to be defined before being used. Identifier x$1 is undefined (3:3)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ [hoisting] EnterSSA: Expected identifier to be defined before being used
|
||||
4 | return x;
|
||||
5 | }
|
||||
6 |
|
||||
|
||||
@@ -15,10 +15,17 @@ function useFoo(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Todo: Encountered conflicting global in generated program
|
||||
|
||||
Conflict from local binding __DEV__.
|
||||
|
||||
error.emit-freeze-conflicting-global.ts:3:8
|
||||
1 | // @enableEmitFreeze @instrumentForget
|
||||
2 | function useFoo(props) {
|
||||
> 3 | const __DEV__ = 'conflicting global';
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Todo: Encountered conflicting global in generated program. Conflict from local binding __DEV__ (3:3)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Encountered conflicting global in generated program
|
||||
4 | console.log(__DEV__);
|
||||
5 | return foo(props.x);
|
||||
6 | }
|
||||
|
||||
@@ -15,10 +15,17 @@ function Component() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot reassign variable after render completes
|
||||
|
||||
Reassigning `callback` after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.
|
||||
|
||||
error.function-expression-references-variable-its-assigned-to.ts:3:4
|
||||
1 | function Component() {
|
||||
2 | let callback = () => {
|
||||
> 3 | callback = null;
|
||||
| ^^^^^^^^ InvalidReact: Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `callback` cannot be reassigned after render (3:3)
|
||||
| ^^^^^^^^ Cannot reassign `callback` after render completes
|
||||
4 | };
|
||||
5 | return <div onClick={callback} />;
|
||||
6 | }
|
||||
|
||||
@@ -24,6 +24,13 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Memoization: Compilation skipped because existing memoization could not be preserved
|
||||
|
||||
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source.
|
||||
|
||||
error.hoist-optional-member-expression-with-conditional-optional.ts:4:23
|
||||
2 | import {ValidateMemoization} from 'shared-runtime';
|
||||
3 | function Component(props) {
|
||||
> 4 | const data = useMemo(() => {
|
||||
@@ -41,7 +48,7 @@ function Component(props) {
|
||||
> 10 | return x;
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
> 11 | }, [props?.items, props.cond]);
|
||||
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source (4:11)
|
||||
| ^^^^ Could not preserve existing manual memoization
|
||||
12 | return (
|
||||
13 | <ValidateMemoization inputs={[props?.items, props.cond]} output={data} />
|
||||
14 | );
|
||||
|
||||
@@ -24,6 +24,13 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Memoization: Compilation skipped because existing memoization could not be preserved
|
||||
|
||||
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source.
|
||||
|
||||
error.hoist-optional-member-expression-with-conditional.ts:4:23
|
||||
2 | import {ValidateMemoization} from 'shared-runtime';
|
||||
3 | function Component(props) {
|
||||
> 4 | const data = useMemo(() => {
|
||||
@@ -41,7 +48,7 @@ function Component(props) {
|
||||
> 10 | return x;
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
> 11 | }, [props?.items, props.cond]);
|
||||
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source (4:11)
|
||||
| ^^^^ Could not preserve existing manual memoization
|
||||
12 | return (
|
||||
13 | <ValidateMemoization inputs={[props?.items, props.cond]} output={data} />
|
||||
14 | );
|
||||
|
||||
@@ -24,6 +24,11 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Todo: Support functions with unreachable code that may contain hoisted declarations
|
||||
|
||||
error.hoisting-simple-function-declaration.ts:6:2
|
||||
4 | }
|
||||
5 | return baz(); // OK: FuncDecls are HoistableDeclarations that have both declaration and value hoisting
|
||||
> 6 | function baz() {
|
||||
@@ -31,7 +36,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
> 7 | return bar();
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
> 8 | }
|
||||
| ^^^^ Todo: Support functions with unreachable code that may contain hoisted declarations (6:8)
|
||||
| ^^^^ Support functions with unreachable code that may contain hoisted declarations
|
||||
9 | }
|
||||
10 |
|
||||
11 | export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -29,10 +29,17 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: This value cannot be modified
|
||||
|
||||
Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook.
|
||||
|
||||
error.hook-call-freezes-captured-identifier.ts:13:2
|
||||
11 | });
|
||||
12 |
|
||||
> 13 | x.value += count;
|
||||
| ^ InvalidReact: Updating a value previously passed as an argument to a hook is not allowed. Consider moving the mutation before calling the hook (13:13)
|
||||
| ^ value cannot be modified
|
||||
14 | return <Stringify x={x} cb={cb} />;
|
||||
15 | }
|
||||
16 |
|
||||
|
||||
@@ -29,10 +29,17 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: This value cannot be modified
|
||||
|
||||
Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook.
|
||||
|
||||
error.hook-call-freezes-captured-memberexpr.ts:13:2
|
||||
11 | });
|
||||
12 |
|
||||
> 13 | x.value += count;
|
||||
| ^ InvalidReact: Updating a value previously passed as an argument to a hook is not allowed. Consider moving the mutation before calling the hook (13:13)
|
||||
| ^ value cannot be modified
|
||||
14 | return <Stringify x={x} cb={cb} />;
|
||||
15 | }
|
||||
16 |
|
||||
|
||||
@@ -23,15 +23,29 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
|
||||
Error: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values
|
||||
|
||||
error.hook-property-load-local-hook.ts:7:12
|
||||
5 |
|
||||
6 | function Foo() {
|
||||
> 7 | let bar = useFoo.useBar;
|
||||
| ^^^^^^^^^^^^^ InvalidReact: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values (7:7)
|
||||
|
||||
InvalidReact: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values (8:8)
|
||||
| ^^^^^^^^^^^^^ Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values
|
||||
8 | return bar();
|
||||
9 | }
|
||||
10 |
|
||||
|
||||
Error: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values
|
||||
|
||||
error.hook-property-load-local-hook.ts:8:9
|
||||
6 | function Foo() {
|
||||
7 | let bar = useFoo.useBar;
|
||||
> 8 | return bar();
|
||||
| ^^^ Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values
|
||||
9 | }
|
||||
10 |
|
||||
11 | export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
|
||||
|
||||
@@ -20,12 +20,26 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
|
||||
Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
|
||||
|
||||
error.hook-ref-value.ts:5:23
|
||||
3 | function Component(props) {
|
||||
4 | const ref = useRef();
|
||||
> 5 | useEffect(() => {}, [ref.current]);
|
||||
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5)
|
||||
| ^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
|
||||
6 | }
|
||||
7 |
|
||||
8 | export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5)
|
||||
Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
|
||||
|
||||
error.hook-ref-value.ts:5:23
|
||||
3 | function Component(props) {
|
||||
4 | const ref = useRef();
|
||||
> 5 | useEffect(() => {}, [ref.current]);
|
||||
| ^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
|
||||
6 | }
|
||||
7 |
|
||||
8 | export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -15,13 +15,20 @@ function component(a, b) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: useMemo() callbacks may not be async or generator functions
|
||||
|
||||
useMemo() callbacks are called once and must synchronously return a value.
|
||||
|
||||
error.invalid-ReactUseMemo-async-callback.ts:2:24
|
||||
1 | function component(a, b) {
|
||||
> 2 | let x = React.useMemo(async () => {
|
||||
| ^^^^^^^^^^^^^
|
||||
> 3 | await a;
|
||||
| ^^^^^^^^^^^^
|
||||
> 4 | }, []);
|
||||
| ^^^^ InvalidReact: useMemo callbacks may not be async or generator functions (2:4)
|
||||
| ^^^^ Async and generator functions are not supported
|
||||
5 | return x;
|
||||
6 | }
|
||||
7 |
|
||||
|
||||
@@ -15,10 +15,15 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
|
||||
|
||||
error.invalid-access-ref-during-render.ts:4:16
|
||||
2 | function Component(props) {
|
||||
3 | const ref = useRef(null);
|
||||
> 4 | const value = ref.current;
|
||||
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4)
|
||||
| ^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
|
||||
5 | return value;
|
||||
6 | }
|
||||
7 |
|
||||
|
||||
@@ -19,10 +19,15 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
|
||||
|
||||
error.invalid-aliased-ref-in-callback-invoked-during-render-.ts:9:33
|
||||
7 | return <Foo item={item} current={current} />;
|
||||
8 | };
|
||||
> 9 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (9:9)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
|
||||
10 | }
|
||||
11 |
|
||||
```
|
||||
|
||||
@@ -15,10 +15,17 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: This value cannot be modified
|
||||
|
||||
Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX.
|
||||
|
||||
error.invalid-array-push-frozen.ts:4:2
|
||||
2 | const x = [];
|
||||
3 | <div>{x}</div>;
|
||||
> 4 | x.push(props.value);
|
||||
| ^ InvalidReact: Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX (4:4)
|
||||
| ^ value cannot be modified
|
||||
5 | return x;
|
||||
6 | }
|
||||
7 |
|
||||
|
||||
@@ -14,9 +14,14 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values
|
||||
|
||||
error.invalid-assign-hook-to-local.ts:2:12
|
||||
1 | function Component(props) {
|
||||
> 2 | const x = useState;
|
||||
| ^^^^^^^^ InvalidReact: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values (2:2)
|
||||
| ^^^^^^^^ Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values
|
||||
3 | const state = x(null);
|
||||
4 | return state[0];
|
||||
5 | }
|
||||
|
||||
@@ -16,10 +16,17 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: This value cannot be modified
|
||||
|
||||
Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX.
|
||||
|
||||
error.invalid-computed-store-to-frozen-value.ts:5:2
|
||||
3 | // freeze
|
||||
4 | <div>{x}</div>;
|
||||
> 5 | x[0] = true;
|
||||
| ^ InvalidReact: Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX (5:5)
|
||||
| ^ value cannot be modified
|
||||
6 | return x;
|
||||
7 | }
|
||||
8 |
|
||||
|
||||
@@ -18,10 +18,15 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
|
||||
|
||||
error.invalid-conditional-call-aliased-hook-import.ts:6:11
|
||||
4 | let data;
|
||||
5 | if (props.cond) {
|
||||
> 6 | data = readFragment();
|
||||
| ^^^^^^^^^^^^ InvalidReact: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (6:6)
|
||||
| ^^^^^^^^^^^^ Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
|
||||
7 | }
|
||||
8 | return data;
|
||||
9 | }
|
||||
|
||||
@@ -18,10 +18,15 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
|
||||
|
||||
error.invalid-conditional-call-aliased-react-hook.ts:6:10
|
||||
4 | let s;
|
||||
5 | if (props.cond) {
|
||||
> 6 | [s] = state();
|
||||
| ^^^^^ InvalidReact: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (6:6)
|
||||
| ^^^^^ Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
|
||||
7 | }
|
||||
8 | return s;
|
||||
9 | }
|
||||
|
||||
@@ -18,10 +18,15 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
|
||||
|
||||
error.invalid-conditional-call-non-hook-imported-as-hook.ts:6:11
|
||||
4 | let data;
|
||||
5 | if (props.cond) {
|
||||
> 6 | data = useArray();
|
||||
| ^^^^^^^^ InvalidReact: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (6:6)
|
||||
| ^^^^^^^^ Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
|
||||
7 | }
|
||||
8 | return data;
|
||||
9 | }
|
||||
|
||||
@@ -22,15 +22,33 @@ function Component({item, cond}) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
|
||||
Error: Calling setState from useMemo may trigger an infinite loop
|
||||
|
||||
Each time the memo callback is evaluated it will change state. This can cause a memoization dependency to change, running the memo function again and causing an infinite loop. Instead of setting state in useMemo(), prefer deriving the value during render. (https://react.dev/reference/react/useState)
|
||||
|
||||
error.invalid-conditional-setState-in-useMemo.ts:7:6
|
||||
5 | useMemo(() => {
|
||||
6 | if (cond) {
|
||||
> 7 | setPrevItem(item);
|
||||
| ^^^^^^^^^^^ InvalidReact: Calling setState from useMemo may trigger an infinite loop. (https://react.dev/reference/react/useState) (7:7)
|
||||
|
||||
InvalidReact: Calling setState from useMemo may trigger an infinite loop. (https://react.dev/reference/react/useState) (8:8)
|
||||
| ^^^^^^^^^^^ Found setState() within useMemo()
|
||||
8 | setState(0);
|
||||
9 | }
|
||||
10 | }, [cond, key, init]);
|
||||
|
||||
Error: Calling setState from useMemo may trigger an infinite loop
|
||||
|
||||
Each time the memo callback is evaluated it will change state. This can cause a memoization dependency to change, running the memo function again and causing an infinite loop. Instead of setting state in useMemo(), prefer deriving the value during render. (https://react.dev/reference/react/useState)
|
||||
|
||||
error.invalid-conditional-setState-in-useMemo.ts:8:6
|
||||
6 | if (cond) {
|
||||
7 | setPrevItem(item);
|
||||
> 8 | setState(0);
|
||||
| ^^^^^^^^ Found setState() within useMemo()
|
||||
9 | }
|
||||
10 | }, [cond, key, init]);
|
||||
11 |
|
||||
```
|
||||
|
||||
|
||||
@@ -16,10 +16,17 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: This value cannot be modified
|
||||
|
||||
Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX.
|
||||
|
||||
error.invalid-delete-computed-property-of-frozen-value.ts:5:9
|
||||
3 | // freeze
|
||||
4 | <div>{x}</div>;
|
||||
> 5 | delete x[y];
|
||||
| ^ InvalidReact: Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX (5:5)
|
||||
| ^ value cannot be modified
|
||||
6 | return x;
|
||||
7 | }
|
||||
8 |
|
||||
|
||||
@@ -16,10 +16,17 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: This value cannot be modified
|
||||
|
||||
Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX.
|
||||
|
||||
error.invalid-delete-property-of-frozen-value.ts:5:9
|
||||
3 | // freeze
|
||||
4 | <div>{x}</div>;
|
||||
> 5 | delete x.y;
|
||||
| ^ InvalidReact: Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX (5:5)
|
||||
| ^ value cannot be modified
|
||||
6 | return x;
|
||||
7 | }
|
||||
8 |
|
||||
|
||||
@@ -13,9 +13,16 @@ function useFoo(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot reassign variables declared outside of the component/hook
|
||||
|
||||
Variable `x` is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)
|
||||
|
||||
error.invalid-destructure-assignment-to-global.ts:2:3
|
||||
1 | function useFoo(props) {
|
||||
> 2 | [x] = props;
|
||||
| ^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (2:2)
|
||||
| ^ `x` cannot be reassigned
|
||||
3 | return {x};
|
||||
4 | }
|
||||
5 |
|
||||
|
||||
@@ -15,10 +15,17 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot reassign variables declared outside of the component/hook
|
||||
|
||||
Variable `b` is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)
|
||||
|
||||
error.invalid-destructure-to-local-global-variables.ts:3:6
|
||||
1 | function Component(props) {
|
||||
2 | let a;
|
||||
> 3 | [a, b] = props.value;
|
||||
| ^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (3:3)
|
||||
| ^ `b` cannot be reassigned
|
||||
4 |
|
||||
5 | return [a, b];
|
||||
6 | }
|
||||
|
||||
@@ -16,10 +16,15 @@ function Component() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
|
||||
|
||||
error.invalid-disallow-mutating-ref-in-render.ts:4:2
|
||||
2 | function Component() {
|
||||
3 | const ref = useRef(null);
|
||||
> 4 | ref.current = false;
|
||||
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4)
|
||||
| ^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
|
||||
5 |
|
||||
6 | return <button ref={ref} />;
|
||||
7 | }
|
||||
|
||||
@@ -21,12 +21,26 @@ function Component() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
|
||||
Error: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)
|
||||
|
||||
error.invalid-disallow-mutating-refs-in-render-transitive.ts:9:2
|
||||
7 | };
|
||||
8 | const changeRef = setRef;
|
||||
> 9 | changeRef();
|
||||
| ^^^^^^^^^ InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) (9:9)
|
||||
| ^^^^^^^^^ This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)
|
||||
10 |
|
||||
11 | return <button ref={ref} />;
|
||||
12 | }
|
||||
|
||||
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (9:9)
|
||||
Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
|
||||
|
||||
error.invalid-disallow-mutating-refs-in-render-transitive.ts:9:2
|
||||
7 | };
|
||||
8 | const changeRef = setRef;
|
||||
> 9 | changeRef();
|
||||
| ^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
|
||||
10 |
|
||||
11 | return <button ref={ref} />;
|
||||
12 | }
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component(props) {
|
||||
eval('props.x = true');
|
||||
return <div />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: The 'eval' function is not supported
|
||||
|
||||
Eval is an anti-pattern in JavaScript, and the code executed cannot be evaluated by React Compiler.
|
||||
|
||||
error.invalid-eval-unsupported.ts:2:2
|
||||
1 | function Component(props) {
|
||||
> 2 | eval('props.x = true');
|
||||
| ^^^^ The 'eval' function is not supported
|
||||
3 | return <div />;
|
||||
4 | }
|
||||
5 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
function Component(props) {
|
||||
eval('props.x = true');
|
||||
return <div />;
|
||||
}
|
||||
@@ -18,10 +18,17 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: This value cannot be modified
|
||||
|
||||
Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead.
|
||||
|
||||
error.invalid-function-expression-mutates-immutable-value.ts:5:4
|
||||
3 | const onChange = e => {
|
||||
4 | // INVALID! should use copy-on-write and pass the new value
|
||||
> 5 | x.value = e.target.value;
|
||||
| ^ InvalidReact: Mutating a value returned from 'useState()', which should not be mutated. Use the setter function to update instead. Found mutation of `x` (5:5)
|
||||
| ^ `x` cannot be modified
|
||||
6 | setX(x);
|
||||
7 | };
|
||||
8 | return <input value={x.value} onChange={onChange} />;
|
||||
|
||||
@@ -35,10 +35,17 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot reassign variables declared outside of the component/hook
|
||||
|
||||
Variable `someGlobal` is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)
|
||||
|
||||
error.invalid-global-reassignment-indirect.ts:9:4
|
||||
7 |
|
||||
8 | const setGlobal = () => {
|
||||
> 9 | someGlobal = true;
|
||||
| ^^^^^^^^^^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (9:9)
|
||||
| ^^^^^^^^^^ `someGlobal` cannot be reassigned
|
||||
10 | };
|
||||
11 | const indirectSetGlobal = () => {
|
||||
12 | setGlobal();
|
||||
|
||||
@@ -38,15 +38,29 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access variable before it is declared
|
||||
|
||||
`setState` is accessed before it is declared, which prevents the earlier access from updating when this value changes over time.
|
||||
|
||||
error.invalid-hoisting-setstate.ts:19:18
|
||||
17 | * $2 = Function context=setState
|
||||
18 | */
|
||||
> 19 | useEffect(() => setState(2), []);
|
||||
| ^^^^^^^^ InvalidReact: This variable is accessed before it is declared, which may prevent it from updating as the assigned value changes over time. Variable `setState` is accessed before it is declared (19:19)
|
||||
|
||||
InvalidReact: This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time. Variable `setState` is accessed before it is declared (21:21)
|
||||
| ^^^^^^^^ `setState` accessed before it is declared
|
||||
20 |
|
||||
21 | const [state, setState] = useState(0);
|
||||
22 | return <Stringify state={state} />;
|
||||
|
||||
error.invalid-hoisting-setstate.ts:21:16
|
||||
19 | useEffect(() => setState(2), []);
|
||||
20 |
|
||||
> 21 | const [state, setState] = useState(0);
|
||||
| ^^^^^^^^ `setState` is declared here
|
||||
22 | return <Stringify state={state} />;
|
||||
23 | }
|
||||
24 |
|
||||
```
|
||||
|
||||
|
||||
@@ -17,6 +17,13 @@ function useFoo() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot modify local variables after render completes
|
||||
|
||||
This argument is a function which may reassign or mutate `cache` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
|
||||
|
||||
error.invalid-hook-function-argument-mutates-local-variable.ts:5:10
|
||||
3 | function useFoo() {
|
||||
4 | const cache = new Map();
|
||||
> 5 | useHook(() => {
|
||||
@@ -24,9 +31,16 @@ function useFoo() {
|
||||
> 6 | cache.set('key', 'value');
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 7 | });
|
||||
| ^^^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (5:7)
|
||||
| ^^^^ This function may (indirectly) reassign or modify `cache` after render
|
||||
8 | }
|
||||
9 |
|
||||
|
||||
InvalidReact: The function modifies a local variable here (6:6)
|
||||
error.invalid-hook-function-argument-mutates-local-variable.ts:6:4
|
||||
4 | const cache = new Map();
|
||||
5 | useHook(() => {
|
||||
> 6 | cache.set('key', 'value');
|
||||
| ^^^^^ This modifies `cache`
|
||||
7 | });
|
||||
8 | }
|
||||
9 |
|
||||
```
|
||||
|
||||
@@ -17,17 +17,46 @@ function Component() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 3 errors:
|
||||
|
||||
Error: Cannot call impure function during render
|
||||
|
||||
`Date.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)
|
||||
|
||||
error.invalid-impure-functions-in-render.ts:4:15
|
||||
2 |
|
||||
3 | function Component() {
|
||||
> 4 | const date = Date.now();
|
||||
| ^^^^^^^^^^ InvalidReact: Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). `Date.now` is an impure function whose results may change on every call (4:4)
|
||||
|
||||
InvalidReact: Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). `performance.now` is an impure function whose results may change on every call (5:5)
|
||||
|
||||
InvalidReact: Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). `Math.random` is an impure function whose results may change on every call (6:6)
|
||||
| ^^^^^^^^^^ Cannot call impure function
|
||||
5 | const now = performance.now();
|
||||
6 | const rand = Math.random();
|
||||
7 | return <Foo date={date} now={now} rand={rand} />;
|
||||
|
||||
Error: Cannot call impure function during render
|
||||
|
||||
`performance.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)
|
||||
|
||||
error.invalid-impure-functions-in-render.ts:5:14
|
||||
3 | function Component() {
|
||||
4 | const date = Date.now();
|
||||
> 5 | const now = performance.now();
|
||||
| ^^^^^^^^^^^^^^^^^ Cannot call impure function
|
||||
6 | const rand = Math.random();
|
||||
7 | return <Foo date={date} now={now} rand={rand} />;
|
||||
8 | }
|
||||
|
||||
Error: Cannot call impure function during render
|
||||
|
||||
`Math.random` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)
|
||||
|
||||
error.invalid-impure-functions-in-render.ts:6:15
|
||||
4 | const date = Date.now();
|
||||
5 | const now = performance.now();
|
||||
> 6 | const rand = Math.random();
|
||||
| ^^^^^^^^^^^^^ Cannot call impure function
|
||||
7 | return <Foo date={date} now={now} rand={rand} />;
|
||||
8 | }
|
||||
9 |
|
||||
```
|
||||
|
||||
|
||||
@@ -50,10 +50,17 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: This value cannot be modified
|
||||
|
||||
Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX.
|
||||
|
||||
error.invalid-jsx-captures-context-variable.ts:22:2
|
||||
20 | />
|
||||
21 | );
|
||||
> 22 | i = i + 1;
|
||||
| ^ InvalidReact: Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX. Found mutation of `i` (22:22)
|
||||
| ^ `i` cannot be modified
|
||||
23 | items.push(
|
||||
24 | <Stringify
|
||||
25 | key={i}
|
||||
|
||||
@@ -25,10 +25,17 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: This value cannot be modified
|
||||
|
||||
Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX.
|
||||
|
||||
error.invalid-mutate-after-aliased-freeze.ts:13:2
|
||||
11 | // y is MaybeFrozen at this point, since it may alias to x
|
||||
12 | // (which is the above line freezes)
|
||||
> 13 | y.push(props.p2);
|
||||
| ^ InvalidReact: Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX (13:13)
|
||||
| ^ value cannot be modified
|
||||
14 |
|
||||
15 | return <Component x={x} y={y} />;
|
||||
16 | }
|
||||
|
||||
@@ -19,10 +19,17 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: This value cannot be modified
|
||||
|
||||
Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX.
|
||||
|
||||
error.invalid-mutate-after-freeze.ts:7:2
|
||||
5 |
|
||||
6 | // x is Frozen at this point
|
||||
> 7 | x.push(props.p2);
|
||||
| ^ InvalidReact: Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX (7:7)
|
||||
| ^ value cannot be modified
|
||||
8 |
|
||||
9 | return <div>{_}</div>;
|
||||
10 | }
|
||||
|
||||
@@ -24,10 +24,17 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: This value cannot be modified
|
||||
|
||||
Modifying a value returned from 'useContext()' is not allowed..
|
||||
|
||||
error.invalid-mutate-context-in-callback.ts:12:4
|
||||
10 | // independently
|
||||
11 | const onClick = () => {
|
||||
> 12 | FooContext.current = true;
|
||||
| ^^^^^^^^^^ InvalidReact: Mutating a value returned from 'useContext()', which should not be mutated. Found mutation of `FooContext` (12:12)
|
||||
| ^^^^^^^^^^ `FooContext` cannot be modified
|
||||
13 | };
|
||||
14 | return <div onClick={onClick} />;
|
||||
15 | }
|
||||
|
||||
@@ -14,10 +14,17 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: This value cannot be modified
|
||||
|
||||
Modifying a value returned from 'useContext()' is not allowed..
|
||||
|
||||
error.invalid-mutate-context.ts:3:2
|
||||
1 | function Component(props) {
|
||||
2 | const context = useContext(FooContext);
|
||||
> 3 | context.value = props.value;
|
||||
| ^^^^^^^ InvalidReact: Mutating a value returned from 'useContext()', which should not be mutated (3:3)
|
||||
| ^^^^^^^ value cannot be modified
|
||||
4 | return context.value;
|
||||
5 | }
|
||||
6 |
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user