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 e5005d02c4e..746da01b331 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -8,7 +8,7 @@ import {NodePath} from '@babel/traverse'; import * as t from '@babel/types'; import prettyFormat from 'pretty-format'; -import {Logger, ProgramContext} from '.'; +import {Logger, ProgramContext, SingleLineSuppressionRange} from '.'; import { HIRFunction, ReactiveFunction, @@ -121,6 +121,7 @@ function run( logger: Logger | null, filename: string | null, code: string | null, + suppressions: Array, ): CodegenFunction { const contextIdentifiers = findContextIdentifiers(func); const env = new Environment( @@ -134,6 +135,7 @@ function run( filename, code, programContext, + suppressions, ); env.logger?.debugLogIRs?.({ kind: 'debug', @@ -567,6 +569,7 @@ export function compileFn( logger: Logger | null, filename: string | null, code: string | null, + singleLineSuppressions: Array, ): CodegenFunction { return run( func, @@ -577,5 +580,6 @@ export function compileFn( logger, filename, code, + singleLineSuppressions, ); } 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 5a9ef9495fa..a19b277fdc5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts @@ -27,8 +27,9 @@ import { import {CompilerReactTarget, PluginOptions} from './Options'; import {compileFn} from './Pipeline'; import { - filterSuppressionsThatAffectFunction, + filterSuppressionsThatAffectNode, findProgramSuppressions, + SingleLineSuppressionRange, suppressionsToCompilerError, } from './Suppression'; import {GeneratedSource} from '../HIR'; @@ -691,11 +692,17 @@ function tryCompileFunction( * Program node itself. We need to figure out whether an eslint suppression range * applies to this function first. */ - const suppressionsInFunction = filterSuppressionsThatAffectFunction( + const suppressionsInFunction = filterSuppressionsThatAffectNode( programContext.suppressions, fn, ); - if (suppressionsInFunction.length > 0) { + const singleLineSuppressions = suppressionsInFunction.filter( + s => s.kind === 'single-line', + ) as Array; + const multiLineSuppressions = suppressionsInFunction.filter( + s => s.kind === 'multi-line', + ); + if (multiLineSuppressions.length > 0) { return { kind: 'error', error: suppressionsToCompilerError(suppressionsInFunction), @@ -714,6 +721,7 @@ function tryCompileFunction( programContext.opts.logger, programContext.filename, programContext.code, + singleLineSuppressions, ), }; } catch (err) { @@ -752,6 +760,7 @@ function retryCompileFunction( programContext.opts.logger, programContext.filename, programContext.code, + [], // ignore suppressions in the retry pipeline ); if (!retryResult.hasFireRewrite && !retryResult.hasInferredEffect) { 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 a0d06f96f0e..968dc6db8ad 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Suppression.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Suppression.ts @@ -25,11 +25,23 @@ import {GeneratedSource} from '../HIR'; * The enable comment can be missing in the case where only a disable block is present, ie the rest * of the file has potential React violations. */ -export type SuppressionRange = { - disableComment: t.Comment; - enableComment: t.Comment | null; - source: SuppressionSource; -}; +export type SuppressionRange = + | { + kind: 'single-line'; + source: SuppressionSource; + comment: t.Comment; + } + | { + kind: 'multi-line'; + source: SuppressionSource; + disableComment: t.Comment; + enableComment: t.Comment | null; + }; + +export type SingleLineSuppressionRange = Extract< + SuppressionRange, + {kind: 'single-line'} +>; type SuppressionSource = 'Eslint' | 'Flow'; @@ -38,15 +50,23 @@ type SuppressionSource = 'Eslint' | 'Flow'; * 1. The suppression is within the function's body; or * 2. The suppression wraps the function */ -export function filterSuppressionsThatAffectFunction( - suppressionRanges: Array, - fn: NodePath, -): Array { - const suppressionsInScope: Array = []; - const fnNode = fn.node; +export function filterSuppressionsThatAffectNode( + suppressionRanges: Array, + node: NodePath, +): Array { + const suppressionsInScope: Array = []; + const fnNode = node.node; for (const suppressionRange of suppressionRanges) { + const enableComment = + suppressionRange.kind === 'single-line' + ? suppressionRange.comment + : suppressionRange.enableComment; + const disableComment = + suppressionRange.kind === 'single-line' + ? suppressionRange.comment + : suppressionRange.disableComment; if ( - suppressionRange.disableComment.start == null || + disableComment.start == null || fnNode.start == null || fnNode.end == null ) { @@ -54,22 +74,20 @@ export function filterSuppressionsThatAffectFunction( } // The suppression is within the function if ( - suppressionRange.disableComment.start > fnNode.start && + disableComment.start > fnNode.start && // If there is no matching enable, the rest of the file has potential violations - (suppressionRange.enableComment === null || - (suppressionRange.enableComment.end != null && - suppressionRange.enableComment.end < fnNode.end)) + (enableComment === null || + (enableComment.end != null && enableComment.end < fnNode.end)) ) { suppressionsInScope.push(suppressionRange); } // The suppression wraps the function if ( - suppressionRange.disableComment.start < fnNode.start && + disableComment.start < fnNode.start && // If there is no matching enable, the rest of the file has potential violations - (suppressionRange.enableComment === null || - (suppressionRange.enableComment.end != null && - suppressionRange.enableComment.end > fnNode.end)) + (enableComment === null || + (enableComment.end != null && enableComment.end > fnNode.end)) ) { suppressionsInScope.push(suppressionRange); } @@ -83,9 +101,7 @@ export function findProgramSuppressions( flowSuppressions: boolean, ): Array { const suppressionRanges: Array = []; - let disableComment: t.Comment | null = null; - let enableComment: t.Comment | null = null; - let source: SuppressionSource | null = null; + let suppression: SuppressionRange | null = null; const rulePattern = `(${ruleNames.join('|')})`; const disableNextLinePattern = new RegExp( @@ -107,42 +123,49 @@ export function findProgramSuppressions( * If we're already within a CommentBlock, we should not restart the range prematurely for a * CommentLine within the block. */ - disableComment == null && + suppression == null && disableNextLinePattern.test(comment.value) ) { - disableComment = comment; - enableComment = comment; - source = 'Eslint'; + suppression = { + kind: 'single-line', + comment, + source: 'Eslint', + }; } if ( flowSuppressions && - disableComment == null && + suppression == null && flowSuppressionPattern.test(comment.value) ) { - disableComment = comment; - enableComment = comment; - source = 'Flow'; + suppression = { + kind: 'single-line', + comment, + source: 'Flow', + }; } if (disablePattern.test(comment.value)) { - disableComment = comment; - source = 'Eslint'; + suppression = { + kind: 'multi-line', + disableComment: comment, + enableComment: null, + source: 'Eslint', + }; } - if (enablePattern.test(comment.value) && source === 'Eslint') { - enableComment = comment; + if ( + enablePattern.test(comment.value) && + suppression != null && + suppression.kind === 'multi-line' && + suppression.source === 'Eslint' + ) { + suppression.enableComment = comment; } - if (disableComment != null && source != null) { - suppressionRanges.push({ - disableComment: disableComment, - enableComment: enableComment, - source, - }); - disableComment = null; - enableComment = null; - source = null; + if (suppression != null) { + suppressionRanges.push(suppression); + suppression = null; } } return suppressionRanges; @@ -157,10 +180,11 @@ export function suppressionsToCompilerError( }); const error = new CompilerError(); for (const suppressionRange of suppressionRanges) { - if ( - suppressionRange.disableComment.start == null || - suppressionRange.disableComment.end == null - ) { + const disableComment = + suppressionRange.kind === 'single-line' + ? suppressionRange.comment + : suppressionRange.disableComment; + if (disableComment.start == null || disableComment.end == null) { continue; } let reason, suggestion; @@ -185,22 +209,19 @@ export function suppressionsToCompilerError( error.pushDiagnostic( CompilerDiagnostic.create({ reason: reason, - description: `React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression \`${suppressionRange.disableComment.value.trim()}\``, + description: `React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression \`${disableComment.value.trim()}\``, severity: ErrorSeverity.InvalidReact, category: ErrorCategory.Suppression, suggestions: [ { description: suggestion, - range: [ - suppressionRange.disableComment.start, - suppressionRange.disableComment.end, - ], + range: [disableComment.start, disableComment.end], op: CompilerSuggestionOperation.Remove, }, ], }).withDetail({ kind: 'error', - loc: suppressionRange.disableComment.loc ?? null, + loc: disableComment.loc ?? null, message: 'Found React rule suppression', }), ); 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 77f2a04e7cf..de01d7139e6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts @@ -51,6 +51,11 @@ import { } from './HIR'; import HIRBuilder, {Bindings, createTemporaryPlace} from './HIRBuilder'; import {BuiltInArrayId} from './ObjectShape'; +import { + filterSuppressionsThatAffectNode, + SingleLineSuppressionRange, + suppressionsToCompilerError, +} from '../Entrypoint'; /* * ******************************************************************************************* @@ -237,6 +242,42 @@ export function lower( null, ); + if (bindings == null) { + /** + * Any single-line suppressions which didn't get captured by a call expression + * are thrown as errors from the outermost function being compiled. This is to + * allow suppressions within function expressions that are passed to useEffect, + * eg + * + * ``` + * useEffect(() => { + * console.log(foo); + * // eslint-disable-next-line react-hooks/exhaustive-deps + * }, []); + * ``` + * + * Where we can't throw an error when exiting the function expression, but rather + * want that suppression to bubble up to the useEffect() call node. + * + * Whereas the following should error since it's at the top-level + * + * ``` + * function Component() { + * const f = () => { + * // eslint-disable-next-line react-hooks/exhaustive-deps + * }; + * } + * ``` + */ + const suppressions = filterSuppressionsThatAffectNode( + env.suppressions, + func, + ); + if (suppressions.length !== 0) { + throw suppressionsToCompilerError(suppressions); + } + } + return Ok({ id, params, @@ -1766,21 +1807,27 @@ function lowerExpression( const memberExpr = lowerMemberExpression(builder, calleePath); const propertyPlace = lowerValueToTemporary(builder, memberExpr.value); const args = lowerArguments(builder, expr.get('arguments')); + const suppressions = consumeSuppressionOnNode(builder, expr); + return { kind: 'MethodCall', receiver: memberExpr.object, property: {...propertyPlace}, args, loc: exprLoc, + suppressions, }; } else { const callee = lowerExpressionToTemporary(builder, calleePath); const args = lowerArguments(builder, expr.get('arguments')); + const suppressions = consumeSuppressionOnNode(builder, expr); + return { kind: 'CallExpression', callee, args, loc: exprLoc, + suppressions, }; } } @@ -2956,6 +3003,7 @@ function lowerOptionalCallExpression( builder.enterReserved(consequent, () => { const args = lowerArguments(builder, expr.get('arguments')); const temp = buildTemporaryPlace(builder, loc); + const suppressions = consumeSuppressionOnNode(builder, expr); if (callee.kind === 'CallExpression') { builder.push({ id: makeInstructionId(0), @@ -2965,6 +3013,7 @@ function lowerOptionalCallExpression( callee: {...callee.callee}, args, loc, + suppressions, }, effects: null, loc, @@ -2979,6 +3028,7 @@ function lowerOptionalCallExpression( property: {...callee.property}, args, loc, + suppressions, }, effects: null, loc, @@ -4477,3 +4527,23 @@ export function lowerType(node: t.FlowType | t.TSType): Type { } } } + +/** + * Extracts the (single-line) suppression comments from the environment that are scoped + * to within the given `node`, removing them from the environment's suppressions list. + * + * By calling this function depth-first, we can associate suppressions with the innermost + * call expression that they effect. Unconsumed suppressions are thrown at the parent + * function boundary. + */ +function consumeSuppressionOnNode( + builder: HIRBuilder, + node: NodePath, +): Array { + const env = builder.environment; + const suppressions = filterSuppressionsThatAffectNode(env.suppressions, node); + env.suppressions = env.suppressions.filter( + s => suppressions.indexOf(s) === -1, + ); + return suppressions; +} 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 421b204e655..f7b2f5823b1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -9,7 +9,11 @@ import * as t from '@babel/types'; import {ZodError, z} from 'zod'; import {fromZodError} from 'zod-validation-error'; import {CompilerError} from '../CompilerError'; -import {Logger, ProgramContext} from '../Entrypoint'; +import { + Logger, + ProgramContext, + SingleLineSuppressionRange, +} from '../Entrypoint'; import {Err, Ok, Result} from '../Utils/Result'; import { DEFAULT_GLOBALS, @@ -702,6 +706,7 @@ export class Environment { hasFireRewrite: boolean; hasInferredEffect: boolean; inferredEffectLocations: Set = new Set(); + suppressions: Array; #contextIdentifiers: Set; #hoistedIdentifiers: Set; @@ -720,6 +725,7 @@ export class Environment { filename: string | null, code: string | null, programContext: ProgramContext, + suppressions: Array, ) { this.#scope = scope; this.fnType = fnType; @@ -733,6 +739,7 @@ export class Environment { this.#globals = new Map(DEFAULT_GLOBALS); this.hasFireRewrite = false; this.hasInferredEffect = false; + this.suppressions = suppressions; if ( config.disableMemoizationForDebugging && diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts index 6b3ba6f94c8..7c69c0aab5d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -15,6 +15,7 @@ import {Type, makeType} from './Types'; import {z} from 'zod'; import type {AliasingEffect} from '../Inference/AliasingEffects'; import {isReservedWord} from '../Utils/Keyword'; +import {SingleLineSuppressionRange} from '../Entrypoint'; /* * ******************************************************************************************* @@ -843,6 +844,7 @@ export type MethodCall = { property: Place; args: Array; loc: SourceLocation; + suppressions?: Array; }; export type CallExpression = { @@ -851,6 +853,7 @@ export type CallExpression = { args: Array; loc: SourceLocation; typeArguments?: Array; + suppressions?: Array; }; export type NewExpression = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts index 412efcfe7ae..32405a05309 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts @@ -255,6 +255,7 @@ function getManualMemoizationReplacement( */ args: [], loc, + suppressions: [], }; } else { /* 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 8ef78aa1964..a0e95932688 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts @@ -2089,7 +2089,7 @@ function computeSignatureForInstruction( effects.push({ kind: 'Freeze', value: operand, - reason: ValueReason.Other, + reason: ValueReason.HookCaptured, }); } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts index 1dcaf0b798e..5735f7e8011 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts @@ -546,7 +546,7 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor< * memoization. Note: we may still prune primitive-producing scopes if * they don't ultimately escape at all. */ - const level = MemoizationLevel.Memoized; + const level = MemoizationLevel.Conditional; return { lvalues: lvalue !== null ? [{place: lvalue, level}] : [], rvalues: [...eachReactiveValueOperand(value)], @@ -701,9 +701,7 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor< } case 'ComputedLoad': case 'PropertyLoad': { - const level = options.forceMemoizePrimitives - ? MemoizationLevel.Memoized - : MemoizationLevel.Conditional; + const level = MemoizationLevel.Conditional; return { // Indirection for the inner value, memoized if the value is lvalues: lvalue !== null ? [{place: lvalue, level}] : [], 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 3146bbea38a..a4b50cd39fe 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateUseMemo.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateUseMemo.ts @@ -11,12 +11,15 @@ import { ErrorCategory, ErrorSeverity, } from '../CompilerError'; +import {SuppressionRange, suppressionsToCompilerError} from '../Entrypoint'; import {FunctionExpression, HIRFunction, IdentifierId} from '../HIR'; import {Result} from '../Utils/Result'; export function validateUseMemo(fn: HIRFunction): Result { const errors = new CompilerError(); + const suppressions: Array = []; const useMemos = new Set(); + const effects = new Set(); const react = new Set(); const functions = new Map(); for (const [, block] of fn.body.blocks) { @@ -25,6 +28,12 @@ export function validateUseMemo(fn: HIRFunction): Result { case 'LoadGlobal': { if (value.binding.name === 'useMemo') { useMemos.add(lvalue.identifier.id); + } else if ( + value.binding.name === 'useEffect' || + value.binding.name === 'useLayoutEffect' || + value.binding.name === 'useInsertionEffect' + ) { + effects.add(lvalue.identifier.id); } else if (value.binding.name === 'React') { react.add(lvalue.identifier.id); } @@ -34,6 +43,12 @@ export function validateUseMemo(fn: HIRFunction): Result { if (react.has(value.object.identifier.id)) { if (value.property === 'useMemo') { useMemos.add(lvalue.identifier.id); + } else if ( + value.property === 'useEffect' || + value.property === 'useLayoutEffect' || + value.property === 'useInsertionEffect' + ) { + effects.add(lvalue.identifier.id); } } break; @@ -47,9 +62,18 @@ export function validateUseMemo(fn: HIRFunction): Result { // Is the function being called useMemo, with at least 1 argument? const callee = value.kind === 'CallExpression' - ? value.callee.identifier.id - : value.property.identifier.id; - const isUseMemo = useMemos.has(callee); + ? value.callee.identifier + : value.property.identifier; + const isEffect = effects.has(callee.id); + if ( + !isEffect && + value.suppressions != null && + value.suppressions.length !== 0 + ) { + suppressions.push(...value.suppressions); + } + + const isUseMemo = useMemos.has(callee.id); if (!isUseMemo || value.args.length === 0) { continue; } @@ -112,5 +136,9 @@ export function validateUseMemo(fn: HIRFunction): Result { } } } + if (suppressions.length !== 0) { + const suppressionError = suppressionsToCompilerError(suppressions); + errors.merge(suppressionError); + } return errors.asResult(); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/compile-exhaustive-deps-disable-in-useEffect-call-as-parent.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/compile-exhaustive-deps-disable-in-useEffect-call-as-parent.expect.md new file mode 100644 index 00000000000..a1678e43a1b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/compile-exhaustive-deps-disable-in-useEffect-call-as-parent.expect.md @@ -0,0 +1,55 @@ + +## Input + +```javascript +function Component(props) { + useEffect( + () => { + console.log(props.value); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + return
; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Component(props) { + const $ = _c(4); + let t0; + if ($[0] !== props.value) { + t0 = () => { + console.log(props.value); + }; + $[0] = props.value; + $[1] = t0; + } else { + t0 = $[1]; + } + let t1; + if ($[2] === Symbol.for("react.memo_cache_sentinel")) { + t1 = []; + $[2] = t1; + } else { + t1 = $[2]; + } + useEffect(t0, t1); + let t2; + if ($[3] === Symbol.for("react.memo_cache_sentinel")) { + t2 =
; + $[3] = t2; + } else { + t2 = $[3]; + } + return t2; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/compile-exhaustive-deps-disable-in-useEffect-call-as-parent.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/compile-exhaustive-deps-disable-in-useEffect-call-as-parent.js new file mode 100644 index 00000000000..11c482d0656 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/compile-exhaustive-deps-disable-in-useEffect-call-as-parent.js @@ -0,0 +1,10 @@ +function Component(props) { + useEffect( + () => { + console.log(props.value); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + return
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/compile-exhaustive-deps-disable-in-useEffect-lambda-as-parent.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/compile-exhaustive-deps-disable-in-useEffect-lambda-as-parent.expect.md new file mode 100644 index 00000000000..cc728a4e741 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/compile-exhaustive-deps-disable-in-useEffect-lambda-as-parent.expect.md @@ -0,0 +1,52 @@ + +## Input + +```javascript +function Component(props) { + useEffect(() => { + console.log(props.value); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return
; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Component(props) { + const $ = _c(4); + let t0; + if ($[0] !== props.value) { + t0 = () => { + console.log(props.value); + }; + $[0] = props.value; + $[1] = t0; + } else { + t0 = $[1]; + } + let t1; + if ($[2] === Symbol.for("react.memo_cache_sentinel")) { + t1 = []; + $[2] = t1; + } else { + t1 = $[2]; + } + useEffect(t0, t1); + let t2; + if ($[3] === Symbol.for("react.memo_cache_sentinel")) { + t2 =
; + $[3] = t2; + } else { + t2 = $[3]; + } + return t2; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/compile-exhaustive-deps-disable-in-useEffect-lambda-as-parent.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/compile-exhaustive-deps-disable-in-useEffect-lambda-as-parent.js new file mode 100644 index 00000000000..678169f8ab3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/compile-exhaustive-deps-disable-in-useEffect-lambda-as-parent.js @@ -0,0 +1,7 @@ +function Component(props) { + useEffect(() => { + console.log(props.value); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/meta-isms/repro-cx-namespace-nesting.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/meta-isms/repro-cx-namespace-nesting.expect.md index 7f1fb96617e..90de08f3335 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/meta-isms/repro-cx-namespace-nesting.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/meta-isms/repro-cx-namespace-nesting.expect.md @@ -2,6 +2,7 @@ ## Input ```javascript +// @compilationMode:"infer" import {makeArray} from 'shared-runtime'; function Component() { @@ -30,7 +31,7 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; +import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer" import { makeArray } from "shared-runtime"; function Component() { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/meta-isms/repro-cx-namespace-nesting.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/meta-isms/repro-cx-namespace-nesting.js index 352e2e5c19b..41aebae7e33 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/meta-isms/repro-cx-namespace-nesting.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/meta-isms/repro-cx-namespace-nesting.js @@ -1,3 +1,4 @@ +// @compilationMode:"infer" import {makeArray} from 'shared-runtime'; function Component() { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-existing-memoization-guarantees/lambda-with-fbt-preserve-memoization.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-existing-memoization-guarantees/lambda-with-fbt-preserve-memoization.expect.md new file mode 100644 index 00000000000..bafbb5c5ef3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-existing-memoization-guarantees/lambda-with-fbt-preserve-memoization.expect.md @@ -0,0 +1,86 @@ + +## Input + +```javascript +// @enablePreserveExistingMemoizationGuarantees +import {fbt} from 'fbt'; + +function Component() { + const buttonLabel = () => { + if (!someCondition) { + return {'Purchase as a gift'}; + } else if ( + !iconOnly && + showPrice && + item?.current_gift_offer?.price?.formatted != null + ) { + return ( + + {'Gift | '} + + {item?.current_gift_offer?.price?.formatted} + + + ); + } else if (!iconOnly && !showPrice) { + return {'Gift'}; + } + }; + + return ( + +