diff --git a/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/compilationMode-all-output.txt b/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/compilationMode-all-output.txt index 0f35215e86d71..0084911eec1b7 100644 --- a/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/compilationMode-all-output.txt +++ b/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/compilationMode-all-output.txt @@ -1,5 +1,5 @@ import { c as _c } from "react/compiler-runtime"; //  -        @compilationMode:"all" +@compilationMode:"all" function nonReactFn() {   const $ = _c(1);   let t0; diff --git a/compiler/apps/playground/components/Editor/EditorImpl.tsx b/compiler/apps/playground/components/Editor/EditorImpl.tsx index 02813a8d2fd01..0ced1e54ed76d 100644 --- a/compiler/apps/playground/components/Editor/EditorImpl.tsx +++ b/compiler/apps/playground/components/Editor/EditorImpl.tsx @@ -11,6 +11,7 @@ import * as t from '@babel/types'; import BabelPluginReactCompiler, { CompilerError, CompilerErrorDetail, + CompilerDiagnostic, Effect, ErrorSeverity, parseConfigPragmaForTests, @@ -141,10 +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>(); const error = new CompilerError(); - const otherErrors: Array = []; + const otherErrors: Array = []; const upsert: (result: PrintedCompilerPipelineValue) => void = result => { const entry = results.get(result.name); if (Array.isArray(entry)) { @@ -203,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, @@ -214,7 +234,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] { debugLogIRs: logIR, logEvent: (_filename: string | null, event: LoggerEvent) => { if (event.kind === 'CompileError') { - otherErrors.push(new CompilerErrorDetail(event.detail)); + otherErrors.push(event.detail); } }, }, @@ -226,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) @@ -245,12 +265,15 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] { } // Only include logger errors if there weren't other errors if (!error.hasErrors() && otherErrors.length !== 0) { - otherErrors.forEach(e => error.push(e)); + otherErrors.forEach(e => error.details.push(e)); } if (error.hasErrors()) { - return [{kind: 'err', results, error: error}, language]; + return [{kind: 'err', results, error}, language]; } - return [{kind: 'ok', results, transformOutput}, language]; + return [ + {kind: 'ok', results, transformOutput, errors: error.details}, + language, + ]; } export default function Editor(): JSX.Element { @@ -259,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], ); @@ -285,19 +312,26 @@ export default function Editor(): JSX.Element { }); }); + let mergedOutput: CompilerOutput; + let errors: Array; + if (compilerOutput.kind === 'ok') { + errors = linterOutput.kind === 'ok' ? [] : linterOutput.error.details; + mergedOutput = { + ...compilerOutput, + errors, + }; + } else { + mergedOutput = compilerOutput; + errors = compilerOutput.error.details; + } return ( <>
- +
- +
diff --git a/compiler/apps/playground/components/Editor/Input.tsx b/compiler/apps/playground/components/Editor/Input.tsx index 0992591183c15..5cfa56d77f743 100644 --- a/compiler/apps/playground/components/Editor/Input.tsx +++ b/compiler/apps/playground/components/Editor/Input.tsx @@ -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(() => { /** diff --git a/compiler/apps/playground/components/Editor/Output.tsx b/compiler/apps/playground/components/Editor/Output.tsx index 7886f11e62370..bf7bd3eb65078 100644 --- a/compiler/apps/playground/components/Editor/Output.tsx +++ b/compiler/apps/playground/components/Editor/Output.tsx @@ -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>; + errors: Array; } | { 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', , ); @@ -142,6 +173,18 @@ async function tabify( , ); } + } else if (compilerOutput.kind === 'err') { + const errors = compilerOutput.error.printErrorMessage(source, { + eslint: false, + }); + reorderedTabs.set( + 'Output', + , + ); } 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>(() => new Set(['JS'])); + const [tabsOpen, setTabsOpen] = useState>( + () => new Set(['Output']), + ); const [tabs, setTabs] = useState>( () => 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 = new Set(['JS', 'HIR']); // Initial and final passes should always be bold + const changedPasses: Set = 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' ? ( -
-
-

COMPILER ERRORS

-
-
-            {compilerOutput.error.toString()}
-          
-
- ) : 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({ /> ) : ( = { automaticLayout: true, wordWrap: 'on', - wrappingIndent: 'deepIndent', + wrappingIndent: 'same', }; diff --git a/compiler/apps/playground/lib/reactCompilerMonacoDiagnostics.ts b/compiler/apps/playground/lib/reactCompilerMonacoDiagnostics.ts index a800e25773295..cece2fa7c6dd3 100644 --- a/compiler/apps/playground/lib/reactCompilerMonacoDiagnostics.ts +++ b/compiler/apps/playground/lib/reactCompilerMonacoDiagnostics.ts @@ -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; + details: Array; + source: string; }; let decorations: Array = []; export function renderReactCompilerMarkers({ monaco, model, details, + source, }: ReactCompilerMarkerConfig): void { const markers: Array = []; for (const detail of details) { - const marker = mapReactCompilerDiagnosticToMonacoMarker(detail, monaco); + const marker = mapReactCompilerDiagnosticToMonacoMarker( + detail, + monaco, + source, + ); if (marker == null) { continue; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Babel/BabelPlugin.ts b/compiler/packages/babel-plugin-react-compiler/src/Babel/BabelPlugin.ts index 58167194249e5..ed74f4664953b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Babel/BabelPlugin.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Babel/BabelPlugin.ts @@ -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 { diff --git a/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts b/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts index 75e01abaefa8f..ee0d0f74c5f3c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts @@ -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'; @@ -44,6 +46,24 @@ export enum ErrorSeverity { Invariant = 'Invariant', } +export type CompilerDiagnosticOptions = { + severity: ErrorSeverity; + category: string; + description: string; + details: Array; + suggestions?: Array | null | undefined; +}; + +export type CompilerDiagnosticDetail = + /** + * A/the source of the error + */ + { + kind: 'error'; + loc: SourceLocation | null; + message: string; + }; + export enum CompilerSuggestionOperation { InsertBefore, InsertAfter, @@ -74,6 +94,103 @@ export type CompilerErrorDetailOptions = { suggestions?: Array | 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, + ): 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. @@ -101,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 = []; + details: Array = []; + printedMessage: string | null = null; static invariant( condition: unknown, @@ -136,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, ): never { @@ -198,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, @@ -260,3 +441,55 @@ export class CompilerError extends Error { }); } } + +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}`; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts index 0c23ceb345296..c13940ed10a21 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts @@ -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'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts index c5ca3434b1b54..648ff01ba713c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -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) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts index de8d16fb12a6e..79bbee37a565c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts @@ -181,7 +181,7 @@ function logError( context.opts.logger.logEvent(context.filename, { kind: 'CompileError', fnLoc, - detail: detail.options, + detail, }); } } else { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Suppression.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Suppression.ts index 4d0369f5210ca..ee341b111f1ae 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Suppression.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Suppression.ts @@ -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', }), ); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/ValidateNoUntransformedReferences.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/ValidateNoUntransformedReferences.ts index d363e11831d3d..16d7c3713c6dd 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/ValidateNoUntransformedReferences.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/ValidateNoUntransformedReferences.ts @@ -8,32 +8,27 @@ 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, + options: Omit, {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( @@ -97,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, ); @@ -124,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, ); diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts index cd7d6a7a35b35..aa940c99e6128 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts @@ -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()) { @@ -2271,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, + }; + }), }); } } @@ -3503,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(); diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index a552803171ad5..5b5b78fc5202c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -318,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 diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts index c3a6c18d3aaff..81959ea361564 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts @@ -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; diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts index 23471a00b097a..cecbb49e44360 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts @@ -995,13 +995,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)}`; diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts index 52bbefc732856..88786bc5dd2bb 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts @@ -345,6 +345,51 @@ export function* eachPatternOperand(pattern: Pattern): Iterable { } } +export function* eachPatternItem( + pattern: Pattern, +): Iterable { + 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, diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/AliasingEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/AliasingEffects.ts index f844129e26bde..95f4e0f5bb1a0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/AliasingEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/AliasingEffects.ts @@ -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, @@ -133,19 +133,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 @@ -211,9 +211,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': diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferFunctionEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferFunctionEffects.ts index 9b347ebb6c4c9..a01ca188a0afa 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferFunctionEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferFunctionEffects.ts @@ -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'; } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts index b91b606d507e6..ca6c6ebae96f6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts @@ -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`, + }), }); } } @@ -1013,33 +1017,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 +1049,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 +1060,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 +1074,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, @@ -1864,19 +1862,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 +2001,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 +2104,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 = []; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts index 79f8cf8c0e85b..cd344342225ce 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts @@ -195,7 +195,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}); @@ -549,7 +549,7 @@ function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void { case 'Impure': case 'MutateFrozen': case 'MutateGlobal': { - errors.push(effect.error); + errors.pushDiagnostic(effect.error); break; } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts index 859c871c263ad..6a764d674012f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts @@ -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; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts b/compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts index badca01dde236..7c1df1ba413b5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts @@ -113,8 +113,13 @@ function* splitPragma( */ function parseConfigPragmaEnvironmentForTest( pragma: string, + defaultConfig: PartialEnvironmentConfig, ): EnvironmentConfig { - const maybeConfig: Partial> = {}; + // throw early if the defaults are invalid + EnvironmentConfigSchema.parse(defaultConfig); + + const maybeConfig: Partial> = + 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 = { ...defaultOptions, panicThreshold: 'all_errors', diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts index 9c41ebcae19f6..31bbf8c94d354 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts @@ -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); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoFreezingKnownMutableFunctions.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoFreezingKnownMutableFunctions.ts index 573db2f6b7d00..7a79c74780e2a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoFreezingKnownMutableFunctions.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoFreezingKnownMutableFunctions.ts @@ -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}`, + }), + ); } } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureFunctionsInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureFunctionsInRender.ts index 6e88773ecf0bd..85adb79ceb859 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureFunctionsInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureFunctionsInRender.ts @@ -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', + }), + ); } } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoJSXInTryStatement.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoJSXInTryStatement.ts index 505302f7d12c2..eea6c0a08e951 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoJSXInTryStatement.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoJSXInTryStatement.ts @@ -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; } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInPassiveEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInEffects.ts similarity index 76% rename from compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInPassiveEffects.ts rename to compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInEffects.ts index a36c347faa001..3c810025a7e5c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInPassiveEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInEffects.ts @@ -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 { const setStateFunctions: Map = 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', + }), + ); } } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInRender.ts index fc101581b30b0..81209d61c6853 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInRender.ts @@ -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; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts index c150c1fbd7486..6bb59247dab7c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts @@ -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 { @@ -519,14 +533,21 @@ class Visitor extends ReactiveFunctionVisitor { !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 { 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', + }), + ); } } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateStaticComponents.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateStaticComponents.ts index f7adef6ca7128..7f5fb408b4596 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateStaticComponents.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateStaticComponents.ts @@ -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', + }), + ); } } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateUseMemo.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateUseMemo.ts index fd4d781ef3c6f..69ab401c89f8a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateUseMemo.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateUseMemo.ts @@ -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 { } 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; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/Logger-test.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/Logger-test.ts index 4b41068f4eb71..096b723554f01 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/Logger-test.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/Logger-test.ts @@ -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'); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/envConfig-test.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/envConfig-test.ts index 500edab8f20f5..f8a6330977fe8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/envConfig-test.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/envConfig-test.ts @@ -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"."`, ); }); @@ -38,7 +38,7 @@ describe('parseConfigPragma()', () => { ], } as any); }).toThrowErrorMatchingInlineSnapshot( - `"InvalidConfig: Could not validate environment config. Update React Compiler config to fix the error. Validation error: autodepsIndex must be > 0 at "inferEffectDependencies[0].autodepsIndex""`, + `"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: autodepsIndex must be > 0 at "inferEffectDependencies[0].autodepsIndex"."`, ); }); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-pattern-spread-creates-array.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-pattern-spread-creates-array.expect.md new file mode 100644 index 0000000000000..9994a6536f419 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-pattern-spread-creates-array.expect.md @@ -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 ( + <> + + + + ); +} + +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 = ; + $[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} + + + ); + $[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)
{"inputs":[],"output":{"a":0,"b":"value1","c":true}}
{"inputs":[[0,1,2]],"output":[1,2,{"a":0,"b":"value1","c":true}]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-pattern-spread-creates-array.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-pattern-spread-creates-array.js new file mode 100644 index 0000000000000..888bdbcb8b935 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-pattern-spread-creates-array.js @@ -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 ( + <> + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{array: [0, 1, 2]}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error._todo.computed-lval-in-destructure.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error._todo.computed-lval-in-destructure.expect.md index f44ae83b2cee0..2d633a3d0fdd9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error._todo.computed-lval-in-destructure.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error._todo.computed-lval-in-destructure.expect.md @@ -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 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-component-tag-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-component-tag-function.expect.md index 5553f235a0847..ce42e651259c7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-component-tag-function.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-component-tag-function.expect.md @@ -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 ; 6 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-children.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-children.expect.md index d380137836cc1..ee57ea6eb0cd9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-children.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-children.expect.md @@ -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 diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-spread-attribute.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-spread-attribute.expect.md index 3f0b5530ee2d2..8476885de7739 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-spread-attribute.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-spread-attribute.expect.md @@ -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
; 7 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bailout-on-flow-suppression.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bailout-on-flow-suppression.expect.md index 1d5b4abdf7d42..6e522e16669dc 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bailout-on-flow-suppression.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bailout-on-flow-suppression.expect.md @@ -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 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bailout-on-suppression-of-custom-rule.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bailout-on-suppression-of-custom-rule.expect.md index d74ebd119c345..3221f97731d9a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bailout-on-suppression-of-custom-rule.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bailout-on-suppression-of-custom-rule.expect.md @@ -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
{x}
; + 9 | } + 10 | /* eslint-enable my-app/react-rule */ ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-old-inference-false-positive-ref-validation-in-use-effect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-old-inference-false-positive-ref-validation-in-use-effect.expect.md index e1cebb00df56f..cc0ad9de117d2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-old-inference-false-positive-ref-validation-in-use-effect.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-old-inference-false-positive-ref-validation-in-use-effect.expect.md @@ -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] ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.call-args-destructuring-asignment-complex.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.call-args-destructuring-asignment-complex.expect.md index cb2ce1a20df0a..624bc8b0b571c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.call-args-destructuring-asignment-complex.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.call-args-destructuring-asignment-complex.expect.md @@ -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 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capitalized-function-call-aliased.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capitalized-function-call-aliased.expect.md index 94b3ae1035ea2..499f2dd873972 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capitalized-function-call-aliased.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capitalized-function-call-aliased.expect.md @@ -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 | ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capitalized-function-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capitalized-function-call.expect.md index d8b0f8facfed0..a89efa7c451d8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capitalized-function-call.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capitalized-function-call.expect.md @@ -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 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capitalized-method-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capitalized-method-call.expect.md index 39dc43e4a563c..c957e5bf7a555 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capitalized-method-call.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capitalized-method-call.expect.md @@ -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 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-mutation.expect.md index cff34e3449376..36aba1765a445 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-mutation.expect.md @@ -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) + | ^^^^^^^^^^^^^^^^^ 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')(), -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) +Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) -InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (15:15) +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 | } ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.conditional-hook-unknown-hook-react-namespace.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.conditional-hook-unknown-hook-react-namespace.expect.md index 7ea8ae9809370..fbf5ca665b4f7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.conditional-hook-unknown-hook-react-namespace.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.conditional-hook-unknown-hook-react-namespace.expect.md @@ -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 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.conditional-hooks-as-method-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.conditional-hooks-as-method-call.expect.md index c2ad547414aa9..2f8806787d2ee 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.conditional-hooks-as-method-call.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.conditional-hooks-as-method-call.expect.md @@ -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 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.context-variable-only-chained-assign.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.context-variable-only-chained-assign.expect.md index 0318fa9525fda..6e9887c5ac521 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.context-variable-only-chained-assign.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.context-variable-only-chained-assign.expect.md @@ -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); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.declare-reassign-variable-in-function-declaration.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.declare-reassign-variable-in-function-declaration.expect.md index 2a6dce11f242e..e5c28e6e362f4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.declare-reassign-variable-in-function-declaration.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.declare-reassign-variable-in-function-declaration.expect.md @@ -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 ; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.default-param-accesses-local.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.default-param-accesses-local.expect.md index dbf084466d80d..4bf9a06b6e979 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.default-param-accesses-local.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.default-param-accesses-local.expect.md @@ -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 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.dont-hoist-inline-reference.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.dont-hoist-inline-reference.expect.md index b08d151be64c4..00a68405f8a1f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.dont-hoist-inline-reference.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.dont-hoist-inline-reference.expect.md @@ -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 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.emit-freeze-conflicting-global.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.emit-freeze-conflicting-global.expect.md index a54cc98708f1e..d8436fa2c046e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.emit-freeze-conflicting-global.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.emit-freeze-conflicting-global.expect.md @@ -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 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.function-expression-references-variable-its-assigned-to.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.function-expression-references-variable-its-assigned-to.expect.md index 76ac6d77a2774..a8a83f6b116da 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.function-expression-references-variable-its-assigned-to.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.function-expression-references-variable-its-assigned-to.expect.md @@ -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
; 6 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional-optional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional-optional.expect.md index 048fee7ee1d58..39d9aa83bd4c9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional-optional.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional-optional.expect.md @@ -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 | 14 | ); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional.expect.md index ca3ee2ae1388e..5950ff64a62c7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional.expect.md @@ -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 | 14 | ); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisting-simple-function-declaration.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisting-simple-function-declaration.expect.md index 1ba0d59e17265..d323477c175dc 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisting-simple-function-declaration.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisting-simple-function-declaration.expect.md @@ -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 = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-identifier.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-identifier.expect.md index 5e0a9886272bc..2b60b2ec7cf51 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-identifier.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-identifier.expect.md @@ -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 ; 15 | } 16 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-memberexpr.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-memberexpr.expect.md index c5af59d642426..c57d55e29a3d9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-memberexpr.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-memberexpr.expect.md @@ -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 ; 15 | } 16 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-property-load-local-hook.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-property-load-local-hook.expect.md index 0949fb3072fcb..3f8e6403aff01 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-property-load-local-hook.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-property-load-local-hook.expect.md @@ -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 = { ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-ref-value.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-ref-value.expect.md index d92d918fe9f3c..63c70cb9f90c9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-ref-value.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-ref-value.expect.md @@ -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 = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ReactUseMemo-async-callback.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ReactUseMemo-async-callback.expect.md index db616600e80dc..4aac70a933274 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ReactUseMemo-async-callback.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ReactUseMemo-async-callback.expect.md @@ -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 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-during-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-during-render.expect.md index 02748366456fb..123428f602497 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-during-render.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-during-render.expect.md @@ -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 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-aliased-ref-in-callback-invoked-during-render-.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-aliased-ref-in-callback-invoked-during-render-.expect.md index e2ce2cceae3b9..1da271e5618e6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-aliased-ref-in-callback-invoked-during-render-.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-aliased-ref-in-callback-invoked-during-render-.expect.md @@ -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 ; 8 | }; > 9 | return {props.items.map(item => renderItem(item))}; - | ^^^^^^^^^^^^^^^^^^^^^^^^ 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 | ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-array-push-frozen.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-array-push-frozen.expect.md index 0440117adbfc7..a401df523c623 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-array-push-frozen.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-array-push-frozen.expect.md @@ -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 |
{x}
; > 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 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assign-hook-to-local.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assign-hook-to-local.expect.md index a4327cf961bfc..e07aa2e32c1b4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assign-hook-to-local.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assign-hook-to-local.expect.md @@ -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 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-computed-store-to-frozen-value.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-computed-store-to-frozen-value.expect.md index 2318d38feb80f..d0e4864a76187 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-computed-store-to-frozen-value.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-computed-store-to-frozen-value.expect.md @@ -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 |
{x}
; > 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 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-aliased-hook-import.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-aliased-hook-import.expect.md index 14bf83054607e..a89b7dc0f0902 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-aliased-hook-import.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-aliased-hook-import.expect.md @@ -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 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-aliased-react-hook.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-aliased-react-hook.expect.md index 6c81f3d2be5b0..b5c2a7eb59793 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-aliased-react-hook.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-aliased-react-hook.expect.md @@ -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 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-non-hook-imported-as-hook.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-non-hook-imported-as-hook.expect.md index d0fb92e751c86..c904e866ff63c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-non-hook-imported-as-hook.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-non-hook-imported-as-hook.expect.md @@ -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 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-setState-in-useMemo.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-setState-in-useMemo.expect.md index f1666cc4013f7..c99dfc1e195df 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-setState-in-useMemo.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-setState-in-useMemo.expect.md @@ -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 | ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-delete-computed-property-of-frozen-value.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-delete-computed-property-of-frozen-value.expect.md index 7116e4d197154..1518035ae04da 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-delete-computed-property-of-frozen-value.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-delete-computed-property-of-frozen-value.expect.md @@ -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 |
{x}
; > 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 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-delete-property-of-frozen-value.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-delete-property-of-frozen-value.expect.md index c6176d1afc5d9..47f10323ca4d8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-delete-property-of-frozen-value.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-delete-property-of-frozen-value.expect.md @@ -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 |
{x}
; > 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 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-destructure-assignment-to-global.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-destructure-assignment-to-global.expect.md index b3471873eb079..4b49c5f65301c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-destructure-assignment-to-global.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-destructure-assignment-to-global.expect.md @@ -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 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-destructure-to-local-global-variables.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-destructure-to-local-global-variables.expect.md index b3303fa189a33..6da3b558bda7f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-destructure-to-local-global-variables.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-destructure-to-local-global-variables.expect.md @@ -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 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-ref-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-ref-in-render.expect.md index b5547a1328629..556d9a26371b3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-ref-in-render.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-ref-in-render.expect.md @@ -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 ; 11 | }); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.invalid-rules-of-hooks-8566f9a360e2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.invalid-rules-of-hooks-8566f9a360e2.expect.md index fabbf9b089a22..520a8e4097d14 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.invalid-rules-of-hooks-8566f9a360e2.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.invalid-rules-of-hooks-8566f9a360e2.expect.md @@ -20,10 +20,15 @@ const MemoizedButton = memo(function (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) + +todo.error.invalid-rules-of-hooks-8566f9a360e2.ts:8:4 6 | const MemoizedButton = memo(function (props) { 7 | if (props.fancy) { > 8 | useCustomHook(); - | ^^^^^^^^^^^^^ 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) (8:8) + | ^^^^^^^^^^^^^ 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) 9 | } 10 | return ; 11 | }); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.invalid-rules-of-hooks-a0058f0b446d.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.invalid-rules-of-hooks-a0058f0b446d.expect.md index b6e240e26c37d..acd4ff9395fc6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.invalid-rules-of-hooks-a0058f0b446d.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.invalid-rules-of-hooks-a0058f0b446d.expect.md @@ -19,10 +19,15 @@ function ComponentWithConditionalHook() { ## 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) + +todo.error.invalid-rules-of-hooks-a0058f0b446d.ts:8:4 6 | function ComponentWithConditionalHook() { 7 | if (cond) { > 8 | Namespace.useConditionalHook(); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 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) (8:8) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 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) 9 | } 10 | } 11 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-27c18dc8dad2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-27c18dc8dad2.expect.md index 83e94b7616692..8f2783f96909e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-27c18dc8dad2.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-27c18dc8dad2.expect.md @@ -20,10 +20,15 @@ const FancyButton = React.forwardRef((props, ref) => { ## 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) + +todo.error.rules-of-hooks-27c18dc8dad2.ts:8:4 6 | const FancyButton = React.forwardRef((props, ref) => { 7 | if (props.fancy) { > 8 | useCustomHook(); - | ^^^^^^^^^^^^^ 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) (8:8) + | ^^^^^^^^^^^^^ 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) 9 | } 10 | return ; 11 | }); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-d0935abedc42.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-d0935abedc42.expect.md index a96e8e0878bc3..343c51787e259 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-d0935abedc42.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-d0935abedc42.expect.md @@ -19,10 +19,15 @@ React.unknownFunction((foo, bar) => { ## 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) + +todo.error.rules-of-hooks-d0935abedc42.ts:8:4 6 | React.unknownFunction((foo, bar) => { 7 | if (foo) { > 8 | useNotAHook(bar); - | ^^^^^^^^^^^ 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) (8:8) + | ^^^^^^^^^^^ 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) 9 | } 10 | }); 11 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-e29c874aa913.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-e29c874aa913.expect.md index 6ce7fc2c8bcdf..a9960ad44dd68 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-e29c874aa913.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-e29c874aa913.expect.md @@ -20,10 +20,15 @@ function useHook() { ## 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) + +todo.error.rules-of-hooks-e29c874aa913.ts:9:4 7 | try { 8 | f(); > 9 | useState(); - | ^^^^^^^^ 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) (9:9) + | ^^^^^^^^ 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) 10 | } catch {} 11 | } 12 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-conditionally-assigned-dynamically-constructed-component-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-conditionally-assigned-dynamically-constructed-component-in-render.expect.md index af8103b7ae515..1224a5b9cf9ca 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-conditionally-assigned-dynamically-constructed-component-in-render.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-conditionally-assigned-dynamically-constructed-component-in-render.expect.md @@ -50,8 +50,7 @@ function Example(props) { ## Logs ``` -{"kind":"CompileError","detail":{"options":{"reason":"Components created during render will reset their state each time they are created. Declare components outside of render. ","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":9,"column":10,"index":202},"end":{"line":9,"column":19,"index":211},"filename":"invalid-conditionally-assigned-dynamically-constructed-component-in-render.ts"}}},"fnLoc":null} -{"kind":"CompileError","detail":{"options":{"reason":"The component may be created during render","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":5,"column":16,"index":124},"end":{"line":5,"column":33,"index":141},"filename":"invalid-conditionally-assigned-dynamically-constructed-component-in-render.ts"}}},"fnLoc":null} +{"kind":"CompileError","detail":{"options":{"severity":"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. ","details":[{"kind":"error","loc":{"start":{"line":9,"column":10,"index":202},"end":{"line":9,"column":19,"index":211},"filename":"invalid-conditionally-assigned-dynamically-constructed-component-in-render.ts"},"message":"This component is created during render"},{"kind":"error","loc":{"start":{"line":5,"column":16,"index":124},"end":{"line":5,"column":33,"index":141},"filename":"invalid-conditionally-assigned-dynamically-constructed-component-in-render.ts"},"message":"The component is created during render here"}]}},"fnLoc":null} {"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":45},"end":{"line":10,"column":1,"index":217},"filename":"invalid-conditionally-assigned-dynamically-constructed-component-in-render.ts"},"fnName":"Example","memoSlots":3,"memoBlocks":2,"memoValues":2,"prunedMemoBlocks":0,"prunedMemoValues":0} ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-construct-component-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-construct-component-in-render.expect.md index 7720863da34c2..7bec7f73b551a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-construct-component-in-render.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-construct-component-in-render.expect.md @@ -32,8 +32,7 @@ function Example(props) { ## Logs ``` -{"kind":"CompileError","detail":{"options":{"reason":"Components created during render will reset their state each time they are created. Declare components outside of render. ","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":4,"column":10,"index":120},"end":{"line":4,"column":19,"index":129},"filename":"invalid-dynamically-construct-component-in-render.ts"}}},"fnLoc":null} -{"kind":"CompileError","detail":{"options":{"reason":"The component may be created during render","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":3,"column":20,"index":91},"end":{"line":3,"column":37,"index":108},"filename":"invalid-dynamically-construct-component-in-render.ts"}}},"fnLoc":null} +{"kind":"CompileError","detail":{"options":{"severity":"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. ","details":[{"kind":"error","loc":{"start":{"line":4,"column":10,"index":120},"end":{"line":4,"column":19,"index":129},"filename":"invalid-dynamically-construct-component-in-render.ts"},"message":"This component is created during render"},{"kind":"error","loc":{"start":{"line":3,"column":20,"index":91},"end":{"line":3,"column":37,"index":108},"filename":"invalid-dynamically-construct-component-in-render.ts"},"message":"The component is created during render here"}]}},"fnLoc":null} {"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":45},"end":{"line":5,"column":1,"index":135},"filename":"invalid-dynamically-construct-component-in-render.ts"},"fnName":"Example","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0} ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-constructed-component-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-constructed-component-function.expect.md index 8d218bf24b0de..e7f9ad7f30fd6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-constructed-component-function.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-constructed-component-function.expect.md @@ -37,8 +37,7 @@ function Example(props) { ## Logs ``` -{"kind":"CompileError","detail":{"options":{"reason":"Components created during render will reset their state each time they are created. Declare components outside of render. ","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":6,"column":10,"index":130},"end":{"line":6,"column":19,"index":139},"filename":"invalid-dynamically-constructed-component-function.ts"}}},"fnLoc":null} -{"kind":"CompileError","detail":{"options":{"reason":"The component may be created during render","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":3,"column":2,"index":73},"end":{"line":5,"column":3,"index":119},"filename":"invalid-dynamically-constructed-component-function.ts"}}},"fnLoc":null} +{"kind":"CompileError","detail":{"options":{"severity":"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. ","details":[{"kind":"error","loc":{"start":{"line":6,"column":10,"index":130},"end":{"line":6,"column":19,"index":139},"filename":"invalid-dynamically-constructed-component-function.ts"},"message":"This component is created during render"},{"kind":"error","loc":{"start":{"line":3,"column":2,"index":73},"end":{"line":5,"column":3,"index":119},"filename":"invalid-dynamically-constructed-component-function.ts"},"message":"The component is created during render here"}]}},"fnLoc":null} {"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":45},"end":{"line":7,"column":1,"index":145},"filename":"invalid-dynamically-constructed-component-function.ts"},"fnName":"Example","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0} ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-constructed-component-method-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-constructed-component-method-call.expect.md index e3bc7a5eb5796..f6220645ef87b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-constructed-component-method-call.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-constructed-component-method-call.expect.md @@ -41,8 +41,7 @@ function Example(props) { ## Logs ``` -{"kind":"CompileError","detail":{"options":{"reason":"Components created during render will reset their state each time they are created. Declare components outside of render. ","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":4,"column":10,"index":118},"end":{"line":4,"column":19,"index":127},"filename":"invalid-dynamically-constructed-component-method-call.ts"}}},"fnLoc":null} -{"kind":"CompileError","detail":{"options":{"reason":"The component may be created during render","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":3,"column":20,"index":91},"end":{"line":3,"column":35,"index":106},"filename":"invalid-dynamically-constructed-component-method-call.ts"}}},"fnLoc":null} +{"kind":"CompileError","detail":{"options":{"severity":"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. ","details":[{"kind":"error","loc":{"start":{"line":4,"column":10,"index":118},"end":{"line":4,"column":19,"index":127},"filename":"invalid-dynamically-constructed-component-method-call.ts"},"message":"This component is created during render"},{"kind":"error","loc":{"start":{"line":3,"column":20,"index":91},"end":{"line":3,"column":35,"index":106},"filename":"invalid-dynamically-constructed-component-method-call.ts"},"message":"The component is created during render here"}]}},"fnLoc":null} {"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":45},"end":{"line":5,"column":1,"index":133},"filename":"invalid-dynamically-constructed-component-method-call.ts"},"fnName":"Example","memoSlots":4,"memoBlocks":2,"memoValues":2,"prunedMemoBlocks":0,"prunedMemoValues":0} ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-constructed-component-new.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-constructed-component-new.expect.md index 02e9f4f4a4b8d..4a72cdd855516 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-constructed-component-new.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-constructed-component-new.expect.md @@ -32,8 +32,7 @@ function Example(props) { ## Logs ``` -{"kind":"CompileError","detail":{"options":{"reason":"Components created during render will reset their state each time they are created. Declare components outside of render. ","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":4,"column":10,"index":125},"end":{"line":4,"column":19,"index":134},"filename":"invalid-dynamically-constructed-component-new.ts"}}},"fnLoc":null} -{"kind":"CompileError","detail":{"options":{"reason":"The component may be created during render","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":3,"column":20,"index":91},"end":{"line":3,"column":42,"index":113},"filename":"invalid-dynamically-constructed-component-new.ts"}}},"fnLoc":null} +{"kind":"CompileError","detail":{"options":{"severity":"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. ","details":[{"kind":"error","loc":{"start":{"line":4,"column":10,"index":125},"end":{"line":4,"column":19,"index":134},"filename":"invalid-dynamically-constructed-component-new.ts"},"message":"This component is created during render"},{"kind":"error","loc":{"start":{"line":3,"column":20,"index":91},"end":{"line":3,"column":42,"index":113},"filename":"invalid-dynamically-constructed-component-new.ts"},"message":"The component is created during render here"}]}},"fnLoc":null} {"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":45},"end":{"line":5,"column":1,"index":140},"filename":"invalid-dynamically-constructed-component-new.ts"},"fnName":"Example","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0} ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.error.object-pattern-computed-key.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.error.object-pattern-computed-key.expect.md index 1856784ce0eb7..7bc1e49069b6a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.error.object-pattern-computed-key.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.error.object-pattern-computed-key.expect.md @@ -21,10 +21,15 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` +Found 1 error: + +Todo: (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern + +todo.error.object-pattern-computed-key.ts:5:9 3 | const SCALE = 2; 4 | function Component(props) { > 5 | const {[props.name]: value} = props; - | ^^^^^^^^^^^^^^^^^^^ Todo: (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern (5:5) + | ^^^^^^^^^^^^^^^^^^^ (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern 6 | return value; 7 | } 8 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.todo-syntax.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.todo-syntax.expect.md index aa3d989296a77..006d2a49c0203 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.todo-syntax.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.todo-syntax.expect.md @@ -29,10 +29,17 @@ function Component({prop1}) { ## Error ``` +Found 1 error: + +Error: [Fire] Untransformed reference to compiler-required feature. + + Todo: (BuildHIR::lowerStatement) Handle TryStatement without a catch clause (11:4) + +error.todo-syntax.ts:18:4 16 | }; 17 | useEffect(() => { > 18 | fire(foo()); - | ^^^^ InvalidReact: [Fire] Untransformed reference to compiler-required feature. Either remove this `fire` call or ensure it is successfully transformed by the compiler. (Bailout reason: Todo: (BuildHIR::lowerStatement) Handle TryStatement without a catch clause (11:15)) (18:18) + | ^^^^ Untransformed `fire` call 19 | }); 20 | } 21 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.untransformed-fire-reference.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.untransformed-fire-reference.expect.md index 0141ffb8adb9a..8481ed2c576b0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.untransformed-fire-reference.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.untransformed-fire-reference.expect.md @@ -13,10 +13,17 @@ console.log(fire == null); ## Error ``` +Found 1 error: + +Error: [Fire] Untransformed reference to compiler-required feature. + + null + +error.untransformed-fire-reference.ts:4:12 2 | import {fire} from 'react'; 3 | > 4 | console.log(fire == null); - | ^^^^ InvalidReact: [Fire] Untransformed reference to compiler-required feature. Either remove this `fire` call or ensure it is successfully transformed by the compiler (4:4) + | ^^^^ Untransformed `fire` call 5 | ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.use-no-memo.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.use-no-memo.expect.md index 275012351c25b..f84686bc36bd2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.use-no-memo.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.use-no-memo.expect.md @@ -30,10 +30,17 @@ function Component({props, bar}) { ## Error ``` +Found 1 error: + +Error: [Fire] Untransformed reference to compiler-required feature. + + null + +error.use-no-memo.ts:15:4 13 | }; 14 | useEffect(() => { > 15 | fire(foo(props)); - | ^^^^ InvalidReact: [Fire] Untransformed reference to compiler-required feature. Either remove this `fire` call or ensure it is successfully transformed by the compiler (15:15) + | ^^^^ Untransformed `fire` call 16 | fire(foo()); 17 | fire(bar()); 18 | }); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-mix-fire-and-no-fire.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-mix-fire-and-no-fire.expect.md index e73451a896ee4..1eb6bf66e9642 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-mix-fire-and-no-fire.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-mix-fire-and-no-fire.expect.md @@ -27,10 +27,17 @@ function Component(props) { ## Error ``` +Found 1 error: + +Error: Cannot compile `fire` + +All uses of foo must be either used with a fire() call in this effect or not used with a fire() call at all. foo was used with fire() on line 10:10 in this effect. + +error.invalid-mix-fire-and-no-fire.ts:11:6 9 | function nested() { 10 | fire(foo(props)); > 11 | foo(props); - | ^^^ InvalidReact: Cannot compile `fire`. All uses of foo must be either used with a fire() call in this effect or not used with a fire() call at all. foo was used with fire() on line 10:10 in this effect (11:11) + | ^^^ Cannot compile `fire` 12 | } 13 | 14 | nested(); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-multiple-args.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-multiple-args.expect.md index 8329717cb3939..c519d43fb41c0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-multiple-args.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-multiple-args.expect.md @@ -22,10 +22,17 @@ function Component({bar, baz}) { ## Error ``` +Found 1 error: + +Error: Cannot compile `fire` + +fire() can only take in a single call expression as an argument but received multiple arguments. + +error.invalid-multiple-args.ts:9:4 7 | }; 8 | useEffect(() => { > 9 | fire(foo(bar), baz); - | ^^^^^^^^^^^^^^^^^^^ InvalidReact: Cannot compile `fire`. fire() can only take in a single call expression as an argument but received multiple arguments (9:9) + | ^^^^^^^^^^^^^^^^^^^ Cannot compile `fire` 10 | }); 11 | 12 | return null; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-nested-use-effect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-nested-use-effect.expect.md index 1e1ff49b372cb..2e767c3c71e61 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-nested-use-effect.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-nested-use-effect.expect.md @@ -28,10 +28,17 @@ function Component(props) { ## Error ``` +Found 1 error: + +Error: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) + +Cannot call useEffect within a function expression. + +error.invalid-nested-use-effect.ts:9:4 7 | }; 8 | useEffect(() => { > 9 | useEffect(() => { - | ^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call useEffect within a function expression (9:9) + | ^^^^^^^^^ Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) 10 | function nested() { 11 | fire(foo(props)); 12 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-not-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-not-call.expect.md index 855c7b7d706cb..40c4bc5394dce 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-not-call.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-not-call.expect.md @@ -22,10 +22,17 @@ function Component(props) { ## Error ``` +Found 1 error: + +Error: Cannot compile `fire` + +`fire()` can only receive a function call such as `fire(fn(a,b)). Method calls and other expressions are not allowed. + +error.invalid-not-call.ts:9:4 7 | }; 8 | useEffect(() => { > 9 | fire(props); - | ^^^^^^^^^^^ InvalidReact: Cannot compile `fire`. `fire()` can only receive a function call such as `fire(fn(a,b)). Method calls and other expressions are not allowed (9:9) + | ^^^^^^^^^^^ Cannot compile `fire` 10 | }); 11 | 12 | return null; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-outside-effect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-outside-effect.expect.md index 687a21f98cdb4..81c36a362cff0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-outside-effect.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-outside-effect.expect.md @@ -24,15 +24,33 @@ function Component({props, bar}) { ## Error ``` +Found 2 errors: + +Invariant: Cannot compile `fire` + +Cannot use `fire` outside of a useEffect function. + +error.invalid-outside-effect.ts:8:2 6 | console.log(props); 7 | }; > 8 | fire(foo(props)); - | ^^^^ Invariant: Cannot compile `fire`. Cannot use `fire` outside of a useEffect function (8:8) - -Invariant: Cannot compile `fire`. Cannot use `fire` outside of a useEffect function (11:11) + | ^^^^ Cannot compile `fire` 9 | 10 | useCallback(() => { 11 | fire(foo(props)); + +Invariant: Cannot compile `fire` + +Cannot use `fire` outside of a useEffect function. + +error.invalid-outside-effect.ts:11:4 + 9 | + 10 | useCallback(() => { +> 11 | fire(foo(props)); + | ^^^^ Cannot compile `fire` + 12 | }, [foo, props]); + 13 | + 14 | return null; ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-no-array-literal.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-no-array-literal.expect.md index dcd9312bb2e53..96cea9c08f0f2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-no-array-literal.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-no-array-literal.expect.md @@ -25,10 +25,17 @@ function Component(props) { ## Error ``` +Found 1 error: + +Invariant: Cannot compile `fire` + +You must use an array literal for an effect dependency array when that effect uses `fire()`. + +error.invalid-rewrite-deps-no-array-literal.ts:13:5 11 | useEffect(() => { 12 | fire(foo(props)); > 13 | }, deps); - | ^^^^ Invariant: Cannot compile `fire`. You must use an array literal for an effect dependency array when that effect uses `fire()` (13:13) + | ^^^^ Cannot compile `fire` 14 | 15 | return null; 16 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.expect.md index 91c5523564cdd..4dc5336ebe011 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.expect.md @@ -28,10 +28,17 @@ function Component(props) { ## Error ``` +Found 1 error: + +Invariant: Cannot compile `fire` + +You must use an array literal for an effect dependency array when that effect uses `fire()`. + +error.invalid-rewrite-deps-spread.ts:15:7 13 | fire(foo(props)); 14 | }, > 15 | ...deps - | ^^^^ Invariant: Cannot compile `fire`. You must use an array literal for an effect dependency array when that effect uses `fire()` (15:15) + | ^^^^ Cannot compile `fire` 16 | ); 17 | 18 | return null; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-spread.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-spread.expect.md index c0b797fc14471..dcd302bbe104c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-spread.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-spread.expect.md @@ -22,10 +22,17 @@ function Component(props) { ## Error ``` +Found 1 error: + +Error: Cannot compile `fire` + +fire() can only take in a single call expression as an argument but received a spread argument. + +error.invalid-spread.ts:9:4 7 | }; 8 | useEffect(() => { > 9 | fire(...foo); - | ^^^^^^^^^^^^ InvalidReact: Cannot compile `fire`. fire() can only take in a single call expression as an argument but received a spread argument (9:9) + | ^^^^^^^^^^^^ Cannot compile `fire` 10 | }); 11 | 12 | return null; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.todo-method.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.todo-method.expect.md index 3f237cfc6f364..67410297f3032 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.todo-method.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.todo-method.expect.md @@ -22,10 +22,17 @@ function Component(props) { ## Error ``` +Found 1 error: + +Error: Cannot compile `fire` + +`fire()` can only receive a function call such as `fire(fn(a,b)). Method calls and other expressions are not allowed. + +error.todo-method.ts:9:4 7 | }; 8 | useEffect(() => { > 9 | fire(props.foo()); - | ^^^^^^^^^^^^^^^^^ InvalidReact: Cannot compile `fire`. `fire()` can only receive a function call such as `fire(fn(a,b)). Method calls and other expressions are not allowed (9:9) + | ^^^^^^^^^^^^^^^^^ Cannot compile `fire` 10 | }); 11 | 12 | return null; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener-transitive.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener-transitive.expect.md index dd48adcda7158..ac55bd0469935 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener-transitive.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener-transitive.expect.md @@ -2,7 +2,7 @@ ## Input ```javascript -// @validateNoSetStateInPassiveEffects +// @validateNoSetStateInEffects import {useEffect, useState} from 'react'; function Component() { @@ -26,7 +26,7 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInPassiveEffects +import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects import { useEffect, useState } from "react"; function Component() { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener-transitive.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener-transitive.js index 8b1e159071ef3..525f3e97d1911 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener-transitive.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener-transitive.js @@ -1,4 +1,4 @@ -// @validateNoSetStateInPassiveEffects +// @validateNoSetStateInEffects import {useEffect, useState} from 'react'; function Component() { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener.expect.md index 7fdd01fd0a76e..a7deed9afb09e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener.expect.md @@ -2,7 +2,7 @@ ## Input ```javascript -// @validateNoSetStateInPassiveEffects +// @validateNoSetStateInEffects import {useEffect, useState} from 'react'; function Component() { @@ -23,7 +23,7 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInPassiveEffects +import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects import { useEffect, useState } from "react"; function Component() { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener.js index ba9720cba9b70..723e4841f6d3a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener.js @@ -1,4 +1,4 @@ -// @validateNoSetStateInPassiveEffects +// @validateNoSetStateInEffects import {useEffect, useState} from 'react'; function Component() { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/parseConfigPragma-test.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/parseConfigPragma-test.ts index 903afe4c20b9f..0ee50a0e761f0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/parseConfigPragma-test.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/parseConfigPragma-test.ts @@ -15,11 +15,11 @@ describe('parseConfigPragmaForTests()', () => { // Validate defaults first to make sure that the parser is getting the value from the pragma, // and not just missing it and getting the default value expect(defaultConfig.enableUseTypeAnnotations).toBe(false); - expect(defaultConfig.validateNoSetStateInPassiveEffects).toBe(false); + expect(defaultConfig.validateNoSetStateInEffects).toBe(false); expect(defaultConfig.validateNoSetStateInRender).toBe(true); const config = parseConfigPragmaForTests( - '@enableUseTypeAnnotations @validateNoSetStateInPassiveEffects:true @validateNoSetStateInRender:false', + '@enableUseTypeAnnotations @validateNoSetStateInEffects:true @validateNoSetStateInRender:false', {compilationMode: defaultOptions.compilationMode}, ); expect(config).toEqual({ @@ -28,7 +28,7 @@ describe('parseConfigPragmaForTests()', () => { environment: { ...defaultOptions.environment, enableUseTypeAnnotations: true, - validateNoSetStateInPassiveEffects: true, + validateNoSetStateInEffects: true, validateNoSetStateInRender: false, enableResetCacheOnSourceFileChanges: false, }, diff --git a/compiler/packages/babel-plugin-react-compiler/src/index.ts b/compiler/packages/babel-plugin-react-compiler/src/index.ts index bbd814b2b60e3..34244892756b4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/index.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/index.ts @@ -9,9 +9,12 @@ export {runBabelPluginReactCompiler} from './Babel/RunReactCompilerBabelPlugin'; export { CompilerError, CompilerErrorDetail, + CompilerDiagnostic, CompilerSuggestionOperation, ErrorSeverity, type CompilerErrorDetailOptions, + type CompilerDiagnosticOptions, + type CompilerDiagnosticDetail, } from './CompilerError'; export { compileFn as compile, diff --git a/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRule-test.ts b/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRule-test.ts index 39c5dc2ea8e8c..bff40e9649d96 100644 --- a/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRule-test.ts +++ b/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRule-test.ts @@ -104,8 +104,7 @@ const tests: CompilerTestCases = { }`, errors: [ { - message: - '(BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration', + message: /Handle var kinds in VariableDeclaration/, }, ], }, @@ -119,8 +118,7 @@ const tests: CompilerTestCases = { }`, errors: [ { - message: - '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', + message: /React Compiler has skipped optimizing this component/, suggestions: [ { output: normalizeIndent` @@ -158,12 +156,10 @@ const tests: CompilerTestCases = { }`, errors: [ { - message: - '(BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration', + message: /Handle var kinds in VariableDeclaration/, }, { - message: - 'Mutating component props or hook arguments is not allowed. Consider using a local variable instead', + message: /Modifying component props or hook arguments is not allowed/, }, ], }, @@ -182,8 +178,7 @@ const tests: CompilerTestCases = { }`, errors: [ { - message: - '[ReactCompilerBailout] (BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration (@:3:2)', + message: /Handle var kinds in VariableDeclaration/, }, ], }, @@ -200,7 +195,7 @@ const tests: CompilerTestCases = { errors: [ { message: - '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)', + /Cannot reassign variables declared outside of the component\/hook/, }, ], }, @@ -274,8 +269,7 @@ const tests: CompilerTestCases = { ], errors: [ { - message: - '[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.', + message: /Cannot infer dependencies of this effect/, }, ], }, diff --git a/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRuleTypescript-test.ts b/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRuleTypescript-test.ts index f67ff673cbbbb..5a2bea68526fb 100644 --- a/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRuleTypescript-test.ts +++ b/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRuleTypescript-test.ts @@ -61,8 +61,7 @@ const tests: CompilerTestCases = { `, errors: [ { - message: - "Mutating a value returned from 'useState()', which should not be mutated. Use the setter function to update instead", + message: /Modifying a value returned from 'useState\(\)'/, line: 7, }, ], diff --git a/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts b/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts index e9eee26bdabc6..1d326012864b9 100644 --- a/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts +++ b/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts @@ -10,6 +10,9 @@ import {transformFromAstSync} from '@babel/core'; import PluginProposalPrivateMethods from '@babel/plugin-proposal-private-methods'; import type {SourceLocation as BabelSourceLocation} from '@babel/types'; import BabelPluginReactCompiler, { + CompilerDiagnostic, + CompilerDiagnosticOptions, + CompilerErrorDetail, CompilerErrorDetailOptions, CompilerSuggestionOperation, ErrorSeverity, @@ -18,15 +21,11 @@ import BabelPluginReactCompiler, { OPT_OUT_DIRECTIVES, type PluginOptions, } from 'babel-plugin-react-compiler/src'; -import {Logger} from 'babel-plugin-react-compiler/src/Entrypoint'; +import {Logger, LoggerEvent} from 'babel-plugin-react-compiler/src/Entrypoint'; import type {Rule} from 'eslint'; import {Statement} from 'estree'; import * as HermesParser from 'hermes-parser'; -type CompilerErrorDetailWithLoc = Omit & { - loc: BabelSourceLocation; -}; - function assertExhaustive(_: never, errorMsg: string): never { throw new Error(errorMsg); } @@ -38,19 +37,15 @@ const DEFAULT_REPORTABLE_LEVELS = new Set([ let reportableLevels = DEFAULT_REPORTABLE_LEVELS; function isReportableDiagnostic( - detail: CompilerErrorDetailOptions, -): detail is CompilerErrorDetailWithLoc { - return ( - reportableLevels.has(detail.severity) && - detail.loc != null && - typeof detail.loc !== 'symbol' - ); + detail: CompilerErrorDetail | CompilerDiagnostic, +): boolean { + return reportableLevels.has(detail.severity); } function makeSuggestions( - detail: CompilerErrorDetailOptions, + detail: CompilerErrorDetailOptions | CompilerDiagnosticOptions, ): Array { - let suggest: Array = []; + const suggest: Array = []; if (Array.isArray(detail.suggestions)) { for (const suggestion of detail.suggestions) { switch (suggestion.op) { @@ -107,6 +102,12 @@ const COMPILER_OPTIONS: Partial = { flowSuppressions: false, environment: validateEnvironmentConfig({ validateRefAccessDuringRender: false, + validateNoSetStateInRender: true, + validateNoSetStateInEffects: true, + validateNoJSXInTryStatements: true, + validateNoImpureFunctionsInRender: true, + validateStaticComponents: true, + validateNoFreezingKnownMutableFunctions: true, }), }; @@ -128,10 +129,10 @@ const rule: Rule.RuleModule = { const filename = context.filename ?? context.getFilename(); const userOpts = context.options[0] ?? {}; if ( - userOpts['reportableLevels'] != null && - userOpts['reportableLevels'] instanceof Set + userOpts.reportableLevels != null && + userOpts.reportableLevels instanceof Set ) { - reportableLevels = userOpts['reportableLevels']; + reportableLevels = userOpts.reportableLevels; } else { reportableLevels = DEFAULT_REPORTABLE_LEVELS; } @@ -144,11 +145,11 @@ const rule: Rule.RuleModule = { */ let __unstable_donotuse_reportAllBailouts: boolean = false; if ( - userOpts['__unstable_donotuse_reportAllBailouts'] != null && - typeof userOpts['__unstable_donotuse_reportAllBailouts'] === 'boolean' + userOpts.__unstable_donotuse_reportAllBailouts != null && + typeof userOpts.__unstable_donotuse_reportAllBailouts === 'boolean' ) { __unstable_donotuse_reportAllBailouts = - userOpts['__unstable_donotuse_reportAllBailouts']; + userOpts.__unstable_donotuse_reportAllBailouts; } let shouldReportUnusedOptOutDirective = true; @@ -162,16 +163,17 @@ const rule: Rule.RuleModule = { }); const userLogger: Logger | null = options.logger; options.logger = { - logEvent: (filename, event): void => { - userLogger?.logEvent(filename, event); + logEvent: (eventFilename, event): void => { + userLogger?.logEvent(eventFilename, event); if (event.kind === 'CompileError') { shouldReportUnusedOptOutDirective = false; const detail = event.detail; - const suggest = makeSuggestions(detail); + const suggest = makeSuggestions(detail.options); if (__unstable_donotuse_reportAllBailouts && event.fnLoc != null) { + const loc = detail.primaryLocation(); const locStr = - detail.loc != null && typeof detail.loc !== 'symbol' - ? ` (@:${detail.loc.start.line}:${detail.loc.start.column})` + loc != null && typeof loc !== 'symbol' + ? ` (@:${loc.start.line}:${loc.start.column})` : ''; /** * Report bailouts with a smaller span (just the first line). @@ -187,10 +189,10 @@ const rule: Rule.RuleModule = { endLoc = { line: event.fnLoc.start.line, // Babel loc line numbers are 1-indexed - column: sourceCode.text.split( - /\r?\n|\r|\n/g, - event.fnLoc.start.line, - )[event.fnLoc.start.line - 1].length, + column: + sourceCode.text.split(/\r?\n|\r|\n/g)[ + event.fnLoc.start.line - 1 + ]?.length ?? 0, }; } const firstLineLoc = { @@ -198,29 +200,32 @@ const rule: Rule.RuleModule = { end: endLoc, }; context.report({ - message: `[ReactCompilerBailout] ${detail.reason}${locStr}`, + message: `${detail.printErrorMessage(sourceCode.text, {eslint: true})} ${locStr}`, loc: firstLineLoc, suggest, }); } - if (!isReportableDiagnostic(detail)) { + const loc = detail.primaryLocation(); + if ( + !isReportableDiagnostic(detail) || + loc == null || + typeof loc === 'symbol' + ) { return; } if ( - hasFlowSuppression(detail.loc, 'react-rule-hook') || - hasFlowSuppression(detail.loc, 'react-rule-unsafe-ref') + hasFlowSuppression(loc, 'react-rule-hook') || + hasFlowSuppression(loc, 'react-rule-unsafe-ref') ) { // If Flow already caught this error, we don't need to report it again. return; } - const loc = - detail.loc == null || typeof detail.loc == 'symbol' - ? event.fnLoc - : detail.loc; if (loc != null) { context.report({ - message: detail.reason, + message: detail.printErrorMessage(sourceCode.text, { + eslint: true, + }), loc, suggest, }); @@ -233,8 +238,8 @@ const rule: Rule.RuleModule = { options.environment = validateEnvironmentConfig( options.environment ?? {}, ); - } catch (err) { - options.logger?.logEvent('', err); + } catch (err: unknown) { + options.logger?.logEvent('', err as LoggerEvent); } function hasFlowSuppression( diff --git a/compiler/packages/snap/src/runner-worker.ts b/compiler/packages/snap/src/runner-worker.ts index fd4763b20322e..554348534e305 100644 --- a/compiler/packages/snap/src/runner-worker.ts +++ b/compiler/packages/snap/src/runner-worker.ts @@ -145,29 +145,6 @@ async function compile( console.error(e.stack); } error = e.message.replace(/\u001b[^m]*m/g, ''); - const loc = e.details?.[0]?.loc; - if (loc != null) { - try { - error = codeFrameColumns( - input, - { - start: { - line: loc.start.line, - column: loc.start.column + 1, - }, - end: { - line: loc.end.line, - column: loc.end.column + 1, - }, - }, - { - message: e.message, - }, - ); - } catch { - // In case the location data isn't valid, skip printing a code frame. - } - } } // Promote console errors so they can be recorded in fixture output diff --git a/packages/eslint-plugin-react-hooks/__tests__/ReactCompilerRule-test.ts b/packages/eslint-plugin-react-hooks/__tests__/ReactCompilerRule-test.ts index 30762e5819535..a636c59375425 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ReactCompilerRule-test.ts +++ b/packages/eslint-plugin-react-hooks/__tests__/ReactCompilerRule-test.ts @@ -106,8 +106,7 @@ const tests: CompilerTestCases = { }`, errors: [ { - message: - '(BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration', + message: /Handle var kinds in VariableDeclaration/, }, ], }, @@ -121,8 +120,7 @@ const tests: CompilerTestCases = { }`, errors: [ { - message: - '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', + message: /React Compiler has skipped optimizing this component/, suggestions: [ { output: normalizeIndent` @@ -160,12 +158,10 @@ const tests: CompilerTestCases = { }`, errors: [ { - message: - '(BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration', + message: /Handle var kinds in VariableDeclaration/, }, { - message: - 'Mutating component props or hook arguments is not allowed. Consider using a local variable instead', + message: /Modifying component props or hook arguments is not allowed/, }, ], }, @@ -184,8 +180,7 @@ const tests: CompilerTestCases = { }`, errors: [ { - message: - '[ReactCompilerBailout] (BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration (@:3:2)', + message: /Handle var kinds in VariableDeclaration/, }, ], }, @@ -202,7 +197,7 @@ const tests: CompilerTestCases = { errors: [ { message: - '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)', + /Cannot reassign variables declared outside of the component\/hook/, }, ], }, @@ -248,6 +243,37 @@ const tests: CompilerTestCases = { }, ], }, + { + name: 'Pipeline errors are reported', + code: normalizeIndent` + import useMyEffect from 'useMyEffect'; + function Component({a}) { + 'use no memo'; + useMyEffect(() => console.log(a.b)); + return
Hello world
; + } + `, + options: [ + { + environment: { + inferEffectDependencies: [ + { + function: { + source: 'useMyEffect', + importSpecifierName: 'default', + }, + numRequiredArgs: 1, + }, + ], + }, + }, + ], + errors: [ + { + message: /Cannot infer dependencies of this effect/, + }, + ], + }, ], }; @@ -259,4 +285,4 @@ const eslintTester = new ESLintTesterV8({ enableExperimentalComponentSyntax: true, }, }); -eslintTester.run('react-compiler - eslint: v8', ReactCompilerRule, tests); +eslintTester.run('react-compiler', ReactCompilerRule, tests); diff --git a/packages/eslint-plugin-react-hooks/__tests__/ReactCompilerRuleTypescript-test.ts b/packages/eslint-plugin-react-hooks/__tests__/ReactCompilerRuleTypescript-test.ts index 2efe6c7a38419..9d05cd1871fe9 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ReactCompilerRuleTypescript-test.ts +++ b/packages/eslint-plugin-react-hooks/__tests__/ReactCompilerRuleTypescript-test.ts @@ -63,8 +63,7 @@ const tests: CompilerTestCases = { `, errors: [ { - message: - "Mutating a value returned from 'useState()', which should not be mutated. Use the setter function to update instead", + message: /Modifying a value returned from 'useState\(\)'/, line: 7, }, ], @@ -75,4 +74,4 @@ const tests: CompilerTestCases = { const eslintTester = new ESLintTesterV8({ parser: require.resolve('@typescript-eslint/parser-v5'), }); -eslintTester.run('react-compiler - eslint: v8', ReactCompilerRule, tests); +eslintTester.run('react-compiler', ReactCompilerRule, tests); diff --git a/packages/eslint-plugin-react-hooks/src/rules/ReactCompiler.ts b/packages/eslint-plugin-react-hooks/src/rules/ReactCompiler.ts index 67d5745a1c7ea..0492c2c30c309 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/ReactCompiler.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/ReactCompiler.ts @@ -11,7 +11,10 @@ import {transformFromAstSync} from '@babel/core'; import PluginProposalPrivateMethods from '@babel/plugin-transform-private-methods'; import type {SourceLocation as BabelSourceLocation} from '@babel/types'; import BabelPluginReactCompiler, { + type CompilerErrorDetail, type CompilerErrorDetailOptions, + type CompilerDiagnostic, + type CompilerDiagnosticOptions, CompilerSuggestionOperation, ErrorSeverity, parsePluginOptions, @@ -25,10 +28,6 @@ import type {Rule} from 'eslint'; import {Statement} from 'estree'; import * as HermesParser from 'hermes-parser'; -type CompilerErrorDetailWithLoc = Omit & { - loc: BabelSourceLocation; -}; - function assertExhaustive(_: never, errorMsg: string): never { throw new Error(errorMsg); } @@ -40,17 +39,13 @@ const DEFAULT_REPORTABLE_LEVELS = new Set([ let reportableLevels = DEFAULT_REPORTABLE_LEVELS; function isReportableDiagnostic( - detail: CompilerErrorDetailOptions, -): detail is CompilerErrorDetailWithLoc { - return ( - reportableLevels.has(detail.severity) && - detail.loc != null && - typeof detail.loc !== 'symbol' - ); + detail: CompilerErrorDetail | CompilerDiagnostic, +): boolean { + return reportableLevels.has(detail.severity); } function makeSuggestions( - detail: CompilerErrorDetailOptions, + detail: CompilerErrorDetailOptions | CompilerDiagnosticOptions, ): Array { const suggest: Array = []; if (Array.isArray(detail.suggestions)) { @@ -109,6 +104,12 @@ const COMPILER_OPTIONS: Partial = { flowSuppressions: false, environment: validateEnvironmentConfig({ validateRefAccessDuringRender: false, + validateNoSetStateInRender: true, + validateNoSetStateInEffects: true, + validateNoJSXInTryStatements: true, + validateNoImpureFunctionsInRender: true, + validateStaticComponents: true, + validateNoFreezingKnownMutableFunctions: true, }), }; @@ -169,11 +170,12 @@ const rule: Rule.RuleModule = { if (event.kind === 'CompileError') { shouldReportUnusedOptOutDirective = false; const detail = event.detail; - const suggest = makeSuggestions(detail); + const suggest = makeSuggestions(detail.options); if (__unstable_donotuse_reportAllBailouts && event.fnLoc != null) { + const loc = detail.primaryLocation(); const locStr = - detail.loc != null && typeof detail.loc !== 'symbol' - ? ` (@:${detail.loc.start.line}:${detail.loc.start.column})` + loc != null && typeof loc !== 'symbol' + ? ` (@:${loc.start.line}:${loc.start.column})` : ''; /** * Report bailouts with a smaller span (just the first line). @@ -200,29 +202,32 @@ const rule: Rule.RuleModule = { end: endLoc, }; context.report({ - message: `[ReactCompilerBailout] ${detail.reason}${locStr}`, + message: `${detail.printErrorMessage(sourceCode.text, {eslint: true})} ${locStr}`, loc: firstLineLoc, suggest, }); } - if (!isReportableDiagnostic(detail)) { + const loc = detail.primaryLocation(); + if ( + !isReportableDiagnostic(detail) || + loc == null || + typeof loc === 'symbol' + ) { return; } if ( - hasFlowSuppression(detail.loc, 'react-rule-hook') || - hasFlowSuppression(detail.loc, 'react-rule-unsafe-ref') + hasFlowSuppression(loc, 'react-rule-hook') || + hasFlowSuppression(loc, 'react-rule-unsafe-ref') ) { // If Flow already caught this error, we don't need to report it again. return; } - const loc = - detail.loc == null || typeof detail.loc === 'symbol' - ? event.fnLoc - : detail.loc; if (loc != null) { context.report({ - message: detail.reason, + message: detail.printErrorMessage(sourceCode.text, { + eslint: true, + }), loc, suggest, }); diff --git a/packages/react-devtools-extensions/src/main/index.js b/packages/react-devtools-extensions/src/main/index.js index a82df1e226338..d7756ca991bef 100644 --- a/packages/react-devtools-extensions/src/main/index.js +++ b/packages/react-devtools-extensions/src/main/index.js @@ -111,7 +111,7 @@ function createBridge() { chrome.devtools.panels.elements.onSelectionChanged.removeListener( onBrowserElementSelectionChanged, ); - if (sourcesPanel) { + if (sourcesPanel && sourcesPanel.onSelectionChanged) { currentSelectedSource = null; sourcesPanel.onSelectionChanged.removeListener( onBrowserSourceSelectionChanged, @@ -124,7 +124,7 @@ function createBridge() { chrome.devtools.panels.elements.onSelectionChanged.addListener( onBrowserElementSelectionChanged, ); - if (sourcesPanel) { + if (sourcesPanel && sourcesPanel.onSelectionChanged) { sourcesPanel.onSelectionChanged.addListener( onBrowserSourceSelectionChanged, ); @@ -317,7 +317,7 @@ function createSourcesEditorPanel() { } const sourcesPanel = chrome.devtools.panels.sources; - if (!sourcesPanel) { + if (!sourcesPanel || !sourcesPanel.createSidebarPane) { // Firefox doesn't currently support extending the source panel. return; }