From 8428a7ec5fe6bf8d1763d3ca417c383655f89ba0 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Mon, 12 Jan 2026 16:38:41 -0800 Subject: [PATCH 1/8] [compiler] Improve impurity/ref tracking note: This implements the idea discussed in https://github.com/reactwg/react/discussions/389#discussioncomment-14252280 Currently we create `Impure` effects for impure functions like `Date.now()` or `Math.random()`, and then throw if the effect is reachable during render. However, impurity is a property of the resulting value: if the value isn't accessed during render then it's okay: maybe you're console-logging the time while debugging (fine), or storing the impure value into a ref and only accessing it in an effect or event handler (totally ok). This PR updates to validate that impure values are not transitively consumed during render, building on the `Render` effect. The validation currently uses an algorithm very similar to that of InferReactivePlaces - building a set of known impure values, starting with `Impure` effects as the sources and then flowing outward via data flow and mutations. If a transitively impure value reaches a `Render` effect, it's an error. We record both the source of the impure value and where it gets rendered to make it easier to understand and fix errors: ``` Error: Cannot access impure value during render Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). error.invalid-impure-functions-in-render-via-render-helper.ts:10:25 8 | const array = makeArray(now); 9 | const hasDate = identity(array); > 10 | return ; | ^^^^^^^ Cannot access impure value during render 11 | }; 12 | return ; 13 | } error.invalid-impure-functions-in-render-via-render-helper.ts:6:14 4 | 5 | function Component() { > 6 | const now = Date.now(); | ^^^^^^^^^^ `Date.now` is an impure function. 7 | const renderItem = () => { 8 | const array = makeArray(now); 9 | const hasDate = identity(array); ``` Impure values are allowed to flow into refs, meaning that we now allow `useRef(Date.now())` or `useRef(localFunctionThatReturnsMathDotRandom())` which would have errored previously. The next PR reuses this improved impurity tracking to validate ref access in render as well. --- .../src/CompilerError.ts | 6 + .../src/Entrypoint/Pipeline.ts | 4 +- .../src/HIR/Globals.ts | 121 +++++--- .../src/HIR/ObjectShape.ts | 20 +- .../src/HIR/PrintHIR.ts | 2 +- .../src/HIR/TypeSchema.ts | 19 +- .../src/Inference/AliasingEffects.ts | 8 +- .../src/Inference/ControlDominators.ts | 12 +- .../Inference/InferMutationAliasingEffects.ts | 83 +++-- .../Inference/InferMutationAliasingRanges.ts | 140 ++++++++- .../ValidateNoImpureFunctionsInRender.ts | 59 ---- .../ValidateNoImpureValuesInRender.ts | 283 ++++++++++++++++++ .../Validation/ValidateNoRefAccessInRender.ts | 16 +- .../Validation/ValidateNoSetStateInEffects.ts | 8 +- ...global-in-component-tag-function.expect.md | 7 +- ...assign-global-in-component-tag-function.js | 1 + ...or.assign-global-in-jsx-children.expect.md | 7 +- .../error.assign-global-in-jsx-children.js | 1 + ...-in-render-indirect-via-mutation.expect.md | 38 +++ ...nctions-in-render-indirect-via-mutation.js | 11 + ...ure-functions-in-render-indirect.expect.md | 37 +++ ...lid-impure-functions-in-render-indirect.js | 10 + ...ns-in-render-via-function-call-2.expect.md | 52 ++++ ...functions-in-render-via-function-call-2.js | 16 + ...ions-in-render-via-function-call.expect.md | 50 ++++ ...e-functions-in-render-via-function-call.js | 14 + ...ions-in-render-via-render-helper.expect.md | 71 +++++ ...e-functions-in-render-via-render-helper.js | 13 + ...valid-impure-functions-in-render.expect.md | 42 ++- ...valid-impure-functions-in-render.expect.md | 42 ++- .../valid-use-impure-value-in-ref.expect.md | 49 +++ .../compiler/valid-use-impure-value-in-ref.js | 8 + .../__tests__/ImpureFunctionCallsRule-test.ts | 6 +- 33 files changed, 1057 insertions(+), 199 deletions(-) delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureFunctionsInRender.ts create mode 100644 compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureValuesInRender.ts create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect-via-mutation.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect-via-mutation.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call-2.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call-2.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-render-helper.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-render-helper.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-use-impure-value-in-ref.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-use-impure-value-in-ref.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts b/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts index a61967ef4a0..a98667c6954 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts @@ -132,6 +132,12 @@ export class CompilerDiagnostic { return new CompilerDiagnostic({...options, details: []}); } + clone(): CompilerDiagnostic { + const cloned = CompilerDiagnostic.create({...this.options}); + cloned.options.details = [...this.options.details]; + return cloned; + } + get reason(): CompilerDiagnosticOptions['reason'] { return this.options.reason; } 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 30d66522715..ac7ce4003c7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -96,7 +96,6 @@ import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHI import {outlineJSX} from '../Optimization/OutlineJsx'; import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls'; import {transformFire} from '../Transform'; -import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureFunctionsInRender'; import {validateStaticComponents} from '../Validation/ValidateStaticComponents'; import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions'; import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects'; @@ -107,6 +106,7 @@ import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions'; import {optimizeForSSR} from '../Optimization/OptimizeForSSR'; import {validateExhaustiveDependencies} from '../Validation/ValidateExhaustiveDependencies'; import {validateSourceLocations} from '../Validation/ValidateSourceLocations'; +import {validateNoImpureValuesInRender} from '../Validation/ValidateNoImpureValuesInRender'; export type CompilerPipelineValue = | {kind: 'ast'; name: string; value: CodegenFunction} @@ -297,7 +297,7 @@ function runWithEnvironment( } if (env.config.validateNoImpureFunctionsInRender) { - validateNoImpureFunctionsInRender(hir).unwrap(); + validateNoImpureValuesInRender(hir).unwrap(); } validateNoFreezingKnownMutableFunctions(hir).unwrap(); diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts index 441b5d5452a..5a4d9cf8c92 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts @@ -38,7 +38,7 @@ import { addObject, } from './ObjectShape'; import {BuiltInType, ObjectType, PolyType} from './Types'; -import {TypeConfig} from './TypeSchema'; +import {AliasingSignatureConfig, TypeConfig} from './TypeSchema'; import {assertExhaustive} from '../Utils/utils'; import {isHookName} from './Environment'; import {CompilerError, SourceLocation} from '..'; @@ -626,6 +626,78 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [ // TODO: rest of Global objects ]; +const RenderHookAliasing: ( + reason: ValueReason, +) => AliasingSignatureConfig = reason => ({ + receiver: '@receiver', + params: [], + rest: '@rest', + returns: '@returns', + temporaries: [], + effects: [ + // Freeze the arguments + { + kind: 'Freeze', + value: '@rest', + reason: ValueReason.HookCaptured, + }, + // Render the arguments + { + kind: 'Render', + place: '@rest', + }, + // Returns a frozen value + { + kind: 'Create', + into: '@returns', + value: ValueKind.Frozen, + reason, + }, + // May alias any arguments into the return + { + kind: 'Alias', + from: '@rest', + into: '@returns', + }, + ], +}); + +const EffectHookAliasing: AliasingSignatureConfig = { + receiver: '@receiver', + params: [], + rest: '@rest', + returns: '@returns', + temporaries: ['@effect'], + effects: [ + // Freezes the function and deps + { + kind: 'Freeze', + value: '@rest', + reason: ValueReason.Effect, + }, + // Internally creates an effect object that captures the function and deps + { + kind: 'Create', + into: '@effect', + value: ValueKind.Frozen, + reason: ValueReason.KnownReturnSignature, + }, + // The effect stores the function and dependencies + { + kind: 'Capture', + from: '@rest', + into: '@effect', + }, + // Returns undefined + { + kind: 'Create', + into: '@returns', + value: ValueKind.Primitive, + reason: ValueReason.KnownReturnSignature, + }, + ], +}; + /* * TODO(mofeiZ): We currently only store rest param effects for hooks. * now that FeatureFlag `enableTreatHooksAsFunctions` is removed we can @@ -644,6 +716,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ hookKind: 'useContext', returnValueKind: ValueKind.Frozen, returnValueReason: ValueReason.Context, + aliasing: RenderHookAliasing(ValueReason.Context), }, BuiltInUseContextHookId, ), @@ -658,6 +731,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ hookKind: 'useState', returnValueKind: ValueKind.Frozen, returnValueReason: ValueReason.State, + aliasing: RenderHookAliasing(ValueReason.State), }), ], [ @@ -670,6 +744,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ hookKind: 'useActionState', returnValueKind: ValueKind.Frozen, returnValueReason: ValueReason.State, + aliasing: RenderHookAliasing(ValueReason.HookCaptured), }), ], [ @@ -682,6 +757,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ hookKind: 'useReducer', returnValueKind: ValueKind.Frozen, returnValueReason: ValueReason.ReducerState, + aliasing: RenderHookAliasing(ValueReason.ReducerState), }), ], [ @@ -715,6 +791,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ calleeEffect: Effect.Read, hookKind: 'useMemo', returnValueKind: ValueKind.Frozen, + aliasing: RenderHookAliasing(ValueReason.HookCaptured), }), ], [ @@ -726,6 +803,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ calleeEffect: Effect.Read, hookKind: 'useCallback', returnValueKind: ValueKind.Frozen, + aliasing: RenderHookAliasing(ValueReason.HookCaptured), }), ], [ @@ -739,41 +817,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ calleeEffect: Effect.Read, hookKind: 'useEffect', returnValueKind: ValueKind.Frozen, - aliasing: { - receiver: '@receiver', - params: [], - rest: '@rest', - returns: '@returns', - temporaries: ['@effect'], - effects: [ - // Freezes the function and deps - { - kind: 'Freeze', - value: '@rest', - reason: ValueReason.Effect, - }, - // Internally creates an effect object that captures the function and deps - { - kind: 'Create', - into: '@effect', - value: ValueKind.Frozen, - reason: ValueReason.KnownReturnSignature, - }, - // The effect stores the function and dependencies - { - kind: 'Capture', - from: '@rest', - into: '@effect', - }, - // Returns undefined - { - kind: 'Create', - into: '@returns', - value: ValueKind.Primitive, - reason: ValueReason.KnownReturnSignature, - }, - ], - }, + aliasing: EffectHookAliasing, }, BuiltInUseEffectHookId, ), @@ -789,6 +833,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ calleeEffect: Effect.Read, hookKind: 'useLayoutEffect', returnValueKind: ValueKind.Frozen, + aliasing: EffectHookAliasing, }, BuiltInUseLayoutEffectHookId, ), @@ -804,6 +849,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ calleeEffect: Effect.Read, hookKind: 'useInsertionEffect', returnValueKind: ValueKind.Frozen, + aliasing: EffectHookAliasing, }, BuiltInUseInsertionEffectHookId, ), @@ -817,6 +863,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ calleeEffect: Effect.Read, hookKind: 'useTransition', returnValueKind: ValueKind.Frozen, + aliasing: RenderHookAliasing(ValueReason.HookCaptured), }), ], [ @@ -829,6 +876,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ hookKind: 'useOptimistic', returnValueKind: ValueKind.Frozen, returnValueReason: ValueReason.State, + aliasing: RenderHookAliasing(ValueReason.HookCaptured), }), ], [ @@ -842,6 +890,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ returnType: {kind: 'Poly'}, calleeEffect: Effect.Read, returnValueKind: ValueKind.Frozen, + aliasing: RenderHookAliasing(ValueReason.HookCaptured), }, BuiltInUseOperatorId, ), diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts index c92f9e55623..43fe918eb6c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts @@ -190,14 +190,19 @@ function parseAliasingSignatureConfig( }; } case 'Impure': { - const place = lookup(effect.place); + const into = lookup(effect.into); return { kind: 'Impure', + into, + description: effect.description, + reason: effect.reason, + }; + } + case 'Render': { + const place = lookup(effect.place); + return { + kind: 'Render', place, - error: CompilerError.throwTodo({ - reason: 'Support impure effect declarations', - loc: GeneratedSource, - }), }; } case 'Apply': { @@ -1513,6 +1518,11 @@ export const DefaultNonmutatingHook = addHook( value: '@rest', reason: ValueReason.HookCaptured, }, + // Render the arguments + { + kind: 'Render', + place: '@rest', + }, // Returns a frozen value { kind: 'Create', 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 71fb4c43b33..2d33f7c724a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts @@ -1009,7 +1009,7 @@ export function printAliasingEffect(effect: AliasingEffect): string { return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`; } case 'Impure': { - return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`; + return `Impure ${printPlaceForAliasEffect(effect.into)} reason=${effect.reason} description=${effect.description}`; } case 'Render': { return `Render ${printPlaceForAliasEffect(effect.place)}`; diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts index eeaaebf7a39..3d26b7d71af 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts @@ -185,11 +185,25 @@ export const ApplyEffectSchema: z.ZodType = z.object({ export type ImpureEffectConfig = { kind: 'Impure'; - place: string; + into: string; + reason: string; + description: string; }; export const ImpureEffectSchema: z.ZodType = z.object({ kind: z.literal('Impure'), + into: LifetimeIdSchema, + reason: z.string(), + description: z.string(), +}); + +export type RenderEffectConfig = { + kind: 'Render'; + place: string; +}; + +export const RenderEffectSchema: z.ZodType = z.object({ + kind: z.literal('Render'), place: LifetimeIdSchema, }); @@ -204,7 +218,8 @@ export type AliasingEffectConfig = | ImpureEffectConfig | MutateEffectConfig | MutateTransitiveConditionallyConfig - | ApplyEffectConfig; + | ApplyEffectConfig + | RenderEffectConfig; export const AliasingEffectSchema: z.ZodType = z.union([ FreezeEffectSchema, 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 7f30e25a5c0..e86ea34a46e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/AliasingEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/AliasingEffects.ts @@ -162,7 +162,7 @@ export type AliasingEffect = /** * Indicates a side-effect that is not safe during render */ - | {kind: 'Impure'; place: Place; error: CompilerDiagnostic} + | {kind: 'Impure'; into: Place; reason: string; description: string} /** * 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 @@ -222,6 +222,12 @@ export function hashEffect(effect: AliasingEffect): string { return [effect.kind, effect.value.identifier.id, effect.reason].join(':'); } case 'Impure': + return [ + effect.kind, + effect.into.identifier.id, + effect.reason, + effect.description, + ].join(':'); case 'Render': { return [effect.kind, effect.place.identifier.id].join(':'); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/ControlDominators.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/ControlDominators.ts index 1fab651947a..b6fd85cd054 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/ControlDominators.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/ControlDominators.ts @@ -8,7 +8,7 @@ import {BlockId, computePostDominatorTree, HIRFunction, Place} from '../HIR'; import {PostDominator} from '../HIR/Dominator'; -export type ControlDominators = (id: BlockId) => boolean; +export type ControlDominators = (id: BlockId) => Place | null; /** * Returns an object that lazily calculates whether particular blocks are controlled @@ -23,7 +23,7 @@ export function createControlDominators( }); const postDominatorFrontierCache = new Map>(); - function isControlledBlock(id: BlockId): boolean { + function isControlledBlock(id: BlockId): Place | null { let controlBlocks = postDominatorFrontierCache.get(id); if (controlBlocks === undefined) { controlBlocks = postDominatorFrontier(fn, postDominators, id); @@ -35,24 +35,24 @@ export function createControlDominators( case 'if': case 'branch': { if (isControlVariable(controlBlock.terminal.test)) { - return true; + return controlBlock.terminal.test; } break; } case 'switch': { if (isControlVariable(controlBlock.terminal.test)) { - return true; + return controlBlock.terminal.test; } for (const case_ of controlBlock.terminal.cases) { if (case_.test !== null && isControlVariable(case_.test)) { - return true; + return case_.test; } } break; } } } - return false; + return null; } return isControlledBlock; 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 4a027b87b6a..c0b16cc1dee 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts @@ -27,11 +27,11 @@ import { InstructionKind, InstructionValue, isArrayType, - isJsxType, isMapType, isPrimitiveType, isRefOrRefValue, isSetType, + isUseRefType, makeIdentifierId, Phi, Place, @@ -70,6 +70,7 @@ import { MutationReason, } from './AliasingEffects'; import {ErrorCategory} from '../CompilerError'; +import {REF_ERROR_DESCRIPTION} from '../Validation/ValidateNoRefAccessInRender'; const DEBUG = false; @@ -569,14 +570,21 @@ function inferBlock( terminal.effects = effects.length !== 0 ? effects : null; } } else if (terminal.kind === 'return') { + terminal.effects = [ + context.internEffect({ + kind: 'Alias', + from: terminal.value, + into: context.fn.returns, + }), + ]; if (!context.isFuctionExpression) { - terminal.effects = [ + terminal.effects.push( context.internEffect({ kind: 'Freeze', value: terminal.value, reason: ValueReason.JsxCaptured, }), - ]; + ); } } } @@ -1973,6 +1981,17 @@ function computeSignatureForInstruction( into: lvalue, }); } + if ( + env.config.validateRefAccessDuringRender && + isUseRefType(value.object.identifier) + ) { + effects.push({ + kind: 'Impure', + into: lvalue, + reason: `Cannot access ref value during render`, + description: REF_ERROR_DESCRIPTION, + }); + } break; } case 'PropertyStore': @@ -2155,21 +2174,13 @@ function computeSignatureForInstruction( } } for (const prop of value.props) { - if ( - prop.kind === 'JsxAttribute' && - prop.place.identifier.type.kind === 'Function' && - (isJsxType(prop.place.identifier.type.return) || - (prop.place.identifier.type.return.kind === 'Phi' && - prop.place.identifier.type.return.operands.some(operand => - isJsxType(operand), - ))) - ) { - // Any props which return jsx are assumed to be called during render - effects.push({ - kind: 'Render', - place: prop.place, - }); + if (prop.kind === 'JsxAttribute' && /^on[A-Z]/.test(prop.name)) { + continue; } + effects.push({ + kind: 'Render', + place: prop.kind === 'JsxAttribute' ? prop.place : prop.argument, + }); } } break; @@ -2423,7 +2434,7 @@ function computeEffectsForLegacySignature( lvalue: Place, receiver: Place, args: Array, - loc: SourceLocation, + _loc: SourceLocation, ): Array { const returnValueReason = signature.returnValueReason ?? ValueReason.Other; const effects: Array = []; @@ -2436,20 +2447,15 @@ function computeEffectsForLegacySignature( if (signature.impure && state.env.config.validateNoImpureFunctionsInRender) { effects.push({ kind: 'Impure', - place: receiver, - error: CompilerDiagnostic.create({ - category: ErrorCategory.Purity, - reason: 'Cannot call impure function during render', - description: - (signature.canonicalName != null - ? `\`${signature.canonicalName}\` is an impure function. ` - : '') + - 'Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)', - }).withDetails({ - kind: 'error', - loc, - message: 'Cannot call impure function', - }), + into: lvalue, + reason: + signature.canonicalName != null + ? `\`${signature.canonicalName}\` is an impure function.` + : 'This function is impure', + description: + '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)', }); } if (signature.knownIncompatible != null && state.env.enableValidations) { @@ -2748,7 +2754,18 @@ function computeEffectsForSignature( } break; } - case 'Impure': + case 'Impure': { + const values = substitutions.get(effect.into.identifier.id) ?? []; + for (const value of values) { + effects.push({ + kind: effect.kind, + into: value, + reason: effect.reason, + description: effect.description, + }); + } + break; + } case 'MutateFrozen': case 'MutateGlobal': { const values = substitutions.get(effect.place.identifier.id) ?? []; 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 43148dc4c67..ae1e07c5a73 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts @@ -19,6 +19,7 @@ import { ValueReason, Place, isPrimitiveType, + isUseRefType, } from '../HIR/HIR'; import { eachInstructionLValue, @@ -28,6 +29,9 @@ import { import {assertExhaustive, getOrInsertWith} from '../Utils/utils'; import {Err, Ok, Result} from '../Utils/Result'; import {AliasingEffect, MutationReason} from './AliasingEffects'; +import {printIdentifier, printType} from '../HIR/PrintHIR'; + +const DEBUG = false; /** * This pass builds an abstract model of the heap and interprets the effects of the @@ -104,7 +108,6 @@ export function inferMutationAliasingRanges( reason: MutationReason | null; }> = []; const renders: Array<{index: number; place: Place}> = []; - let index = 0; const errors = new CompilerError(); @@ -197,14 +200,12 @@ export function inferMutationAliasingRanges( }); } else if ( effect.kind === 'MutateFrozen' || - effect.kind === 'MutateGlobal' || - effect.kind === 'Impure' + effect.kind === 'MutateGlobal' ) { errors.pushDiagnostic(effect.error); functionEffects.push(effect); } else if (effect.kind === 'Render') { renders.push({index: index++, place: effect.place}); - functionEffects.push(effect); } } } @@ -214,10 +215,6 @@ export function inferMutationAliasingRanges( state.assign(index, from, into); } } - if (block.terminal.kind === 'return') { - state.assign(index++, block.terminal.value, fn.returns); - } - if ( (block.terminal.kind === 'maybe-throw' || block.terminal.kind === 'return') && @@ -244,6 +241,11 @@ export function inferMutationAliasingRanges( } for (const mutation of mutations) { + if (DEBUG) { + console.log( + `[${mutation.index}] mutate ${printIdentifier(mutation.place.identifier)}`, + ); + } state.mutate( mutation.index, mutation.place.identifier, @@ -255,8 +257,16 @@ export function inferMutationAliasingRanges( errors, ); } + if (DEBUG) { + console.log(state.debug()); + } for (const render of renders) { - state.render(render.index, render.place.identifier, errors); + if (DEBUG) { + console.log( + `[${render.index}] render ${printIdentifier(render.place.identifier)}`, + ); + } + state.render(render.index, render.place, errors); } for (const param of [...fn.context, ...fn.params]) { const place = param.kind === 'Identifier' ? param : param.place; @@ -515,6 +525,13 @@ export function inferMutationAliasingRanges( const ignoredErrors = new CompilerError(); for (const param of [...fn.params, ...fn.context, fn.returns]) { const place = param.kind === 'Identifier' ? param : param.place; + const node = state.nodes.get(place.identifier); + if (node != null && node.render != null) { + functionEffects.push({ + kind: 'Render', + place: place, + }); + } tracked.push(place); } for (const into of tracked) { @@ -577,7 +594,6 @@ export function inferMutationAliasingRanges( function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void { for (const effect of fn.aliasingEffects ?? []) { switch (effect.kind) { - case 'Impure': case 'MutateFrozen': case 'MutateGlobal': { errors.pushDiagnostic(effect.error); @@ -612,10 +628,74 @@ type Node = { | {kind: 'Object'} | {kind: 'Phi'} | {kind: 'Function'; function: HIRFunction}; + render: Place | null; }; + +function _printNode(node: Node): string { + const out: Array = []; + debugNode(out, node); + return out.join('\n'); +} +function debugNode(out: Array, node: Node): void { + out.push( + printIdentifier(node.id) + + printType(node.id.type) + + ` lastMutated=[${node.lastMutated}]`, + ); + if (node.transitive != null) { + out.push(` transitive=${node.transitive.kind}`); + } + if (node.local != null) { + out.push(` local=${node.local.kind}`); + } + if (node.mutationReason != null) { + out.push(` mutationReason=${node.mutationReason?.kind}`); + } + const edges: Array<{ + index: number; + direction: '<=' | '=>'; + kind: string; + id: Identifier; + }> = []; + for (const [alias, index] of node.createdFrom) { + edges.push({index, direction: '<=', kind: 'createFrom', id: alias}); + } + for (const [alias, index] of node.aliases) { + edges.push({index, direction: '<=', kind: 'alias', id: alias}); + } + for (const [alias, index] of node.maybeAliases) { + edges.push({index, direction: '<=', kind: 'alias?', id: alias}); + } + for (const [alias, index] of node.captures) { + edges.push({index, direction: '<=', kind: 'capture', id: alias}); + } + for (const edge of node.edges) { + edges.push({ + index: edge.index, + direction: '=>', + kind: edge.kind, + id: edge.node, + }); + } + edges.sort((a, b) => a.index - b.index); + for (const edge of edges) { + out.push( + ` [${edge.index}] ${edge.direction} ${edge.kind} ${printIdentifier(edge.id)}`, + ); + } +} + class AliasingState { nodes: Map = new Map(); + debug(): string { + const items: Array = []; + for (const [_id, node] of this.nodes) { + debugNode(items, node); + } + return items.join('\n'); + } + create(place: Place, value: Node['value']): void { this.nodes.set(place.identifier, { id: place.identifier, @@ -629,6 +709,7 @@ class AliasingState { lastMutated: 0, mutationReason: null, value, + render: null, }); } @@ -681,9 +762,9 @@ class AliasingState { } } - render(index: number, start: Identifier, errors: CompilerError): void { + render(index: number, start: Place, errors: CompilerError): void { const seen = new Set(); - const queue: Array = [start]; + const queue: Array = [start.identifier]; while (queue.length !== 0) { const current = queue.pop()!; if (seen.has(current)) { @@ -691,11 +772,34 @@ class AliasingState { } seen.add(current); const node = this.nodes.get(current); - if (node == null || node.transitive != null || node.local != null) { + if (node == null || isUseRefType(node.id)) { + if (DEBUG) { + console.log(` render ${printIdentifier(current)}: skip mutated/ref`); + } continue; } - if (node.value.kind === 'Function') { - appendFunctionErrors(errors, node.value.function); + if ( + node.local == null && + node.transitive == null && + node.value.kind === 'Function' + ) { + const returns = node.value.function.returns; + if ( + isJsxType(returns.identifier.type) || + (returns.identifier.type.kind === 'Phi' && + returns.identifier.type.operands.some(operand => + isJsxType(operand), + )) + ) { + appendFunctionErrors(errors, node.value.function); + } + if (DEBUG) { + console.log(` render ${printIdentifier(current)}: skip function`); + } + continue; + } + if (node.render == null) { + node.render = start; } for (const [alias, when] of node.createdFrom) { if (when >= index) { @@ -709,6 +813,12 @@ class AliasingState { } queue.push(alias); } + for (const [alias, when] of node.maybeAliases) { + if (when >= index) { + continue; + } + queue.push(alias); + } for (const [capture, when] of node.captures) { if (when >= index) { continue; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureFunctionsInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureFunctionsInRender.ts deleted file mode 100644 index ca0612d80ce..00000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureFunctionsInRender.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import {CompilerDiagnostic, CompilerError} from '..'; -import {ErrorCategory} from '../CompilerError'; -import {HIRFunction} from '../HIR'; -import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects'; -import {Result} from '../Utils/Result'; - -/** - * Checks that known-impure functions are not called during render. Examples of invalid functions to - * call during render are `Math.random()` and `Date.now()`. Users may extend this set of - * impure functions via a module type provider and specifying functions with `impure: true`. - * - * TODO: add best-effort analysis of functions which are called during render. We have variations of - * this in several of our validation passes and should unify those analyses into a reusable helper - * and use it here. - */ -export function validateNoImpureFunctionsInRender( - fn: HIRFunction, -): Result { - const errors = new CompilerError(); - for (const [, block] of fn.body.blocks) { - for (const instr of block.instructions) { - const value = instr.value; - if (value.kind === 'MethodCall' || value.kind == 'CallExpression') { - const callee = - value.kind === 'MethodCall' ? value.property : value.callee; - const signature = getFunctionCallSignature( - fn.env, - callee.identifier.type, - ); - if (signature != null && signature.impure === true) { - errors.pushDiagnostic( - CompilerDiagnostic.create({ - category: ErrorCategory.Purity, - reason: 'Cannot call impure function during render', - description: - (signature.canonicalName != null - ? `\`${signature.canonicalName}\` is an impure function. ` - : '') + - 'Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)', - suggestions: null, - }).withDetails({ - kind: 'error', - loc: callee.loc, - message: 'Cannot call impure function', - }), - ); - } - } - } - } - return errors.asResult(); -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureValuesInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureValuesInRender.ts new file mode 100644 index 00000000000..78674d87114 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureValuesInRender.ts @@ -0,0 +1,283 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {CompilerDiagnostic, CompilerError, Effect, ErrorCategory} from '..'; +import {HIRFunction, IdentifierId, isUseRefType} from '../HIR'; +import { + eachInstructionLValue, + eachInstructionValueOperand, +} from '../HIR/visitors'; +import {AliasingEffect} from '../Inference/AliasingEffects'; +import {createControlDominators} from '../Inference/ControlDominators'; +import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables'; +import {Err, Ok, Result} from '../Utils/Result'; +import {assertExhaustive, getOrInsertWith} from '../Utils/utils'; + +type ImpureEffect = Extract; +type FunctionCache = Map>; +type ImpuritySignature = {effects: Array; error: CompilerError}; + +export function validateNoImpureValuesInRender( + fn: HIRFunction, +): Result { + const impure = new Map(); + const result = inferImpureValues(fn, impure, new Map()); + + if (result.error.hasAnyErrors()) { + return Err(result.error); + } + return Ok(undefined); +} + +function inferFunctionExpressionMemo( + fn: HIRFunction, + impure: Map, + cache: FunctionCache, +): ImpuritySignature { + const key = fn.context + .map(place => `${place.identifier.id}:${impure.has(place.identifier.id)}`) + .join(','); + return getOrInsertWith( + getOrInsertWith(cache, fn, () => new Map()), + key, + () => { + return inferImpureValues(fn, impure, cache); + }, + ); +} + +function inferImpureValues( + fn: HIRFunction, + impure: Map, + cache: FunctionCache, +): ImpuritySignature { + const getBlockControl = createControlDominators(fn, place => { + return impure.has(place.identifier.id); + }); + + let hasChanges = false; + do { + hasChanges = false; + + for (const block of fn.body.blocks.values()) { + const controlPlace = getBlockControl(block.id); + const controlImpureEffect = + controlPlace != null ? impure.get(controlPlace.identifier.id) : null; + + for (const phi of block.phis) { + if (impure.has(phi.place.identifier.id)) { + // Already marked impure on a previous pass + continue; + } + let impureEffect = null; + for (const [, operand] of phi.operands) { + const operandEffect = impure.get(operand.identifier.id); + if (operandEffect != null) { + impureEffect = operandEffect; + break; + } + } + if (impureEffect != null) { + impure.set(phi.place.identifier.id, impureEffect); + hasChanges = true; + } else { + for (const [pred] of phi.operands) { + const predControl = getBlockControl(pred); + if (predControl != null) { + const predEffect = impure.get(predControl.identifier.id); + if (predEffect != null) { + impure.set(phi.place.identifier.id, predEffect); + hasChanges = true; + break; + } + } + } + } + } + + for (const instr of block.instructions) { + let impureEffect: ImpureEffect | undefined = instr.effects?.find( + (effect: AliasingEffect): effect is ImpureEffect => + effect.kind === 'Impure', + ); + + if ( + impureEffect == null && + (instr.value.kind === 'FunctionExpression' || + instr.value.kind === 'ObjectMethod') + ) { + impureEffect = instr.value.loweredFunc.func.aliasingEffects?.find( + (effect: AliasingEffect): effect is ImpureEffect => + effect.kind === 'Impure', + ); + if (impureEffect == null) { + const result = inferFunctionExpressionMemo( + instr.value.loweredFunc.func, + impure, + cache, + ); + if (!result.error.hasAnyErrors()) { + impureEffect = result.effects[0]; + } + } + } + + if (impureEffect == null) { + for (const operand of eachInstructionValueOperand(instr.value)) { + const operandEffect = impure.get(operand.identifier.id); + if (operandEffect != null) { + impureEffect = operandEffect; + break; + } + } + } + + if (impureEffect != null) { + for (const lvalue of eachInstructionLValue(instr)) { + if (isUseRefType(lvalue.identifier)) { + continue; + } + if (!impure.has(lvalue.identifier.id)) { + impure.set(lvalue.identifier.id, impureEffect); + hasChanges = true; + } + } + } + if (impureEffect != null || controlImpureEffect != null) { + for (const operand of eachInstructionValueOperand(instr.value)) { + switch (operand.effect) { + case Effect.Capture: + case Effect.Store: + case Effect.ConditionallyMutate: + case Effect.ConditionallyMutateIterator: + case Effect.Mutate: { + if ( + !impure.has(operand.identifier.id) && + isMutable(instr, operand) + ) { + impure.set( + operand.identifier.id, + (impureEffect ?? controlImpureEffect)!, + ); + hasChanges = true; + } + break; + } + case Effect.Freeze: + case Effect.Read: { + // no-op + break; + } + case Effect.Unknown: { + CompilerError.invariant(false, { + reason: 'Unexpected unknown effect', + description: null, + details: [ + { + kind: 'error', + loc: operand.loc, + message: null, + }, + ], + suggestions: null, + }); + } + default: { + assertExhaustive( + operand.effect, + `Unexpected effect kind \`${operand.effect}\``, + ); + } + } + } + } + if (impureEffect == null) { + const lvalueEffect = impure.get(instr.lvalue.identifier.id)!; + if (lvalueEffect != null) { + for (const operand of eachInstructionValueOperand(instr.value)) { + if ( + isMutable(instr, operand) && + !impure.has(operand.identifier.id) + ) { + impure.set(operand.identifier.id, lvalueEffect); + hasChanges = true; + } + } + } + } + } + + if (block.terminal.kind === 'return') { + const terminalEffect = impure.get(block.terminal.value.identifier.id); + if (terminalEffect != null && !impure.has(fn.returns.identifier.id)) { + impure.set(fn.returns.identifier.id, terminalEffect); + hasChanges = true; + } + } + } + } while (hasChanges); + + const error = new CompilerError(); + for (const block of fn.body.blocks.values()) { + for (const instr of block.instructions) { + const value = instr.value; + if ( + value.kind === 'FunctionExpression' || + value.kind === 'ObjectMethod' + ) { + const result = inferFunctionExpressionMemo( + value.loweredFunc.func, + impure, + cache, + ); + if (result.error.hasAnyErrors()) { + error.merge(result.error); + } + } + for (const effect of instr.effects ?? []) { + if ( + effect.kind !== 'Render' || + !impure.has(effect.place.identifier.id) + ) { + continue; + } + const impureEffect = impure.get(effect.place.identifier.id)!; + error.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.Purity, + reason: 'Cannot access impure value during render', + description: impureEffect.description, + }) + .withDetails({ + kind: 'error', + loc: effect.place.loc, + message: 'Cannot access impure value during render', + }) + .withDetails({ + kind: 'error', + loc: impureEffect.into.loc, + message: impureEffect.reason, + }), + ); + } + } + } + const impureEffects: Array = []; + for (const param of [...fn.context, ...fn.params, fn.returns]) { + const place = param.kind === 'Identifier' ? param : param.place; + const impureEffect = impure.get(place.identifier.id); + if (impureEffect != null) { + impureEffects.push({ + kind: 'Impure', + into: {...place}, + reason: impureEffect.reason, + description: impureEffect.description, + }); + } + } + return {effects: impureEffects, error}; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts index 232e9f55bbc..179f1db7022 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts @@ -511,7 +511,7 @@ function validateNoRefAccessInRenderImpl( CompilerDiagnostic.create({ category: ErrorCategory.Refs, reason: 'Cannot access refs during render', - description: ERROR_DESCRIPTION, + description: REF_ERROR_DESCRIPTION, }).withDetails({ kind: 'error', loc: callee.loc, @@ -666,7 +666,7 @@ function validateNoRefAccessInRenderImpl( CompilerDiagnostic.create({ category: ErrorCategory.Refs, reason: 'Cannot access refs during render', - description: ERROR_DESCRIPTION, + description: REF_ERROR_DESCRIPTION, }) .withDetails({ kind: 'error', @@ -814,7 +814,7 @@ function guardCheck(errors: CompilerError, operand: Place, env: Env): void { CompilerDiagnostic.create({ category: ErrorCategory.Refs, reason: 'Cannot access refs during render', - description: ERROR_DESCRIPTION, + description: REF_ERROR_DESCRIPTION, }).withDetails({ kind: 'error', loc: operand.loc, @@ -838,7 +838,7 @@ function validateNoRefValueAccess( CompilerDiagnostic.create({ category: ErrorCategory.Refs, reason: 'Cannot access refs during render', - description: ERROR_DESCRIPTION, + description: REF_ERROR_DESCRIPTION, }).withDetails({ kind: 'error', loc: (type.kind === 'RefValue' && type.loc) || operand.loc, @@ -864,7 +864,7 @@ function validateNoRefPassedToFunction( CompilerDiagnostic.create({ category: ErrorCategory.Refs, reason: 'Cannot access refs during render', - description: ERROR_DESCRIPTION, + description: REF_ERROR_DESCRIPTION, }).withDetails({ kind: 'error', loc: (type.kind === 'RefValue' && type.loc) || loc, @@ -886,7 +886,7 @@ function validateNoRefUpdate( CompilerDiagnostic.create({ category: ErrorCategory.Refs, reason: 'Cannot access refs during render', - description: ERROR_DESCRIPTION, + description: REF_ERROR_DESCRIPTION, }).withDetails({ kind: 'error', loc: (type.kind === 'RefValue' && type.loc) || loc, @@ -907,7 +907,7 @@ function validateNoDirectRefValueAccess( CompilerDiagnostic.create({ category: ErrorCategory.Refs, reason: 'Cannot access refs during render', - description: ERROR_DESCRIPTION, + description: REF_ERROR_DESCRIPTION, }).withDetails({ kind: 'error', loc: type.loc ?? operand.loc, @@ -917,7 +917,7 @@ function validateNoDirectRefValueAccess( } } -const ERROR_DESCRIPTION = +export const REF_ERROR_DESCRIPTION = 'React refs are values that are not needed for rendering. Refs should only be accessed ' + 'outside of render, such as in event handlers or effects. ' + 'Accessing a ref value (the `current` property) during render can cause your component ' + diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInEffects.ts index 91de8f20671..06a5c6b9b29 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInEffects.ts @@ -202,10 +202,10 @@ function getSetStateCall( ); }; - const isRefControlledBlock: (id: BlockId) => boolean = + const isRefControlledBlock: (id: BlockId) => Place | null = enableAllowSetStateFromRefsInEffects ? createControlDominators(fn, place => isDerivedFromRef(place)) - : (): boolean => false; + : (): Place | null => null; for (const [, block] of fn.body.blocks) { if (enableAllowSetStateFromRefsInEffects) { @@ -224,7 +224,7 @@ function getSetStateCall( refDerivedValues.add(phi.place.identifier.id); } else { for (const [pred] of phi.operands) { - if (isRefControlledBlock(pred)) { + if (isRefControlledBlock(pred) != null) { refDerivedValues.add(phi.place.identifier.id); break; } @@ -337,7 +337,7 @@ function getSetStateCall( * be needed when initial layout measurements from refs need to be stored in state. */ return null; - } else if (isRefControlledBlock(block.id)) { + } else if (isRefControlledBlock(block.id) != null) { continue; } } 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 b7b707b3e74..f0472f7bca6 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 @@ -5,6 +5,7 @@ function Component() { const Foo = () => { someGlobal = true; + return
; }; return ; } @@ -26,9 +27,9 @@ error.assign-global-in-component-tag-function.ts:3:4 2 | const Foo = () => { > 3 | someGlobal = true; | ^^^^^^^^^^ `someGlobal` cannot be reassigned - 4 | }; - 5 | return ; - 6 | } + 4 | return
; + 5 | }; + 6 | return ; ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-component-tag-function.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-component-tag-function.js index 2982fdf7085..eaf1eceac2a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-component-tag-function.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-component-tag-function.js @@ -1,6 +1,7 @@ function Component() { const Foo = () => { someGlobal = true; + return
; }; return ; } 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 1f5ac0c83df..d57997b8fa6 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 @@ -5,6 +5,7 @@ function Component() { const foo = () => { someGlobal = true; + return
; }; // Children are generally access/called during render, so // modifying a global in a children function is almost @@ -29,9 +30,9 @@ error.assign-global-in-jsx-children.ts:3:4 2 | const foo = () => { > 3 | someGlobal = true; | ^^^^^^^^^^ `someGlobal` cannot be reassigned - 4 | }; - 5 | // Children are generally access/called during render, so - 6 | // modifying a global in a children function is almost + 4 | return
; + 5 | }; + 6 | // Children are generally access/called during render, so ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-children.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-children.js index 82554e8ac43..1def89dd7d5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-children.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-children.js @@ -1,6 +1,7 @@ function Component() { const foo = () => { someGlobal = true; + return
; }; // Children are generally access/called during render, so // modifying a global in a children function is almost diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect-via-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect-via-mutation.expect.md new file mode 100644 index 00000000000..0d4a5b06aff --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect-via-mutation.expect.md @@ -0,0 +1,38 @@ + +## Input + +```javascript +// @validateNoImpureFunctionsInRender + +import {arrayPush, identity, makeArray} from 'shared-runtime'; + +function Component() { + const getDate = () => Date.now(); + const now = getDate(); + const array = []; + arrayPush(array, now); + return ; +} + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot access impure value during render + +Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). + +error.invalid-impure-functions-in-render-indirect-via-mutation.ts:10:23 + 8 | const array = []; + 9 | arrayPush(array, now); +> 10 | return ; + | ^^^^^ Cannot access impure value during render + 11 | } + 12 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect-via-mutation.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect-via-mutation.js new file mode 100644 index 00000000000..18222d860e6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect-via-mutation.js @@ -0,0 +1,11 @@ +// @validateNoImpureFunctionsInRender + +import {arrayPush, identity, makeArray} from 'shared-runtime'; + +function Component() { + const getDate = () => Date.now(); + const now = getDate(); + const array = []; + arrayPush(array, now); + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect.expect.md new file mode 100644 index 00000000000..b5572d81cb8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect.expect.md @@ -0,0 +1,37 @@ + +## Input + +```javascript +// @validateNoImpureFunctionsInRender + +import {identity, makeArray} from 'shared-runtime'; + +function Component() { + const getDate = () => Date.now(); + const array = makeArray(getDate()); + const hasDate = identity(array); + return ; +} + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot access impure value during render + +Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). + +error.invalid-impure-functions-in-render-indirect.ts:9:23 + 7 | const array = makeArray(getDate()); + 8 | const hasDate = identity(array); +> 9 | return ; + | ^^^^^^^ Cannot access impure value during render + 10 | } + 11 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect.js new file mode 100644 index 00000000000..4cf0e46d9d8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect.js @@ -0,0 +1,10 @@ +// @validateNoImpureFunctionsInRender + +import {identity, makeArray} from 'shared-runtime'; + +function Component() { + const getDate = () => Date.now(); + const array = makeArray(getDate()); + const hasDate = identity(array); + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call-2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call-2.expect.md new file mode 100644 index 00000000000..895e34ed432 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call-2.expect.md @@ -0,0 +1,52 @@ + +## Input + +```javascript +// @validateNoImpureFunctionsInRender + +import {identity, makeArray} from 'shared-runtime'; + +function Component() { + const now = () => Date.now(); + const f = () => { + // this should error but we currently lose track of the impurity bc + // the impure value comes from behind a call + const array = makeArray(now()); + const hasDate = identity(array); + return hasDate; + }; + const hasDate = f(); + return ; +} + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot access impure value during render + +Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). + +error.invalid-impure-functions-in-render-via-function-call-2.ts:15:23 + 13 | }; + 14 | const hasDate = f(); +> 15 | return ; + | ^^^^^^^ Cannot access impure value during render + 16 | } + 17 | + +error.invalid-impure-functions-in-render-via-function-call-2.ts:10:28 + 8 | // this should error but we currently lose track of the impurity bc + 9 | // the impure value comes from behind a call +> 10 | const array = makeArray(now()); + | ^^^ `Date.now` is an impure function. + 11 | const hasDate = identity(array); + 12 | return hasDate; + 13 | }; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call-2.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call-2.js new file mode 100644 index 00000000000..9abc485e957 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call-2.js @@ -0,0 +1,16 @@ +// @validateNoImpureFunctionsInRender + +import {identity, makeArray} from 'shared-runtime'; + +function Component() { + const now = () => Date.now(); + const f = () => { + // this should error but we currently lose track of the impurity bc + // the impure value comes from behind a call + const array = makeArray(now()); + const hasDate = identity(array); + return hasDate; + }; + const hasDate = f(); + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call.expect.md new file mode 100644 index 00000000000..76c2cd9098b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call.expect.md @@ -0,0 +1,50 @@ + +## Input + +```javascript +// @validateNoImpureFunctionsInRender + +import {identity, makeArray} from 'shared-runtime'; + +function Component() { + const now = Date.now(); + const f = () => { + const array = makeArray(now); + const hasDate = identity(array); + return hasDate; + }; + const hasDate = f(); + return ; +} + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot access impure value during render + +Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). + +error.invalid-impure-functions-in-render-via-function-call.ts:13:23 + 11 | }; + 12 | const hasDate = f(); +> 13 | return ; + | ^^^^^^^ Cannot access impure value during render + 14 | } + 15 | + +error.invalid-impure-functions-in-render-via-function-call.ts:8:28 + 6 | const now = Date.now(); + 7 | const f = () => { +> 8 | const array = makeArray(now); + | ^^^ `Date.now` is an impure function. + 9 | const hasDate = identity(array); + 10 | return hasDate; + 11 | }; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call.js new file mode 100644 index 00000000000..0ec57a4de35 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call.js @@ -0,0 +1,14 @@ +// @validateNoImpureFunctionsInRender + +import {identity, makeArray} from 'shared-runtime'; + +function Component() { + const now = Date.now(); + const f = () => { + const array = makeArray(now); + const hasDate = identity(array); + return hasDate; + }; + const hasDate = f(); + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-render-helper.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-render-helper.expect.md new file mode 100644 index 00000000000..ffed6129747 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-render-helper.expect.md @@ -0,0 +1,71 @@ + +## Input + +```javascript +// @validateNoImpureFunctionsInRender + +import {identity, makeArray} from 'shared-runtime'; + +function Component() { + const now = Date.now(); + const renderItem = () => { + const array = makeArray(now); + const hasDate = identity(array); + return ; + }; + return ; +} + +``` + + +## Error + +``` +Found 2 errors: + +Error: Cannot access impure value during render + +Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). + +error.invalid-impure-functions-in-render-via-render-helper.ts:10:25 + 8 | const array = makeArray(now); + 9 | const hasDate = identity(array); +> 10 | return ; + | ^^^^^^^ Cannot access impure value during render + 11 | }; + 12 | return ; + 13 | } + +error.invalid-impure-functions-in-render-via-render-helper.ts:6:14 + 4 | + 5 | function Component() { +> 6 | const now = Date.now(); + | ^^^^^^^^^^ `Date.now` is an impure function. + 7 | const renderItem = () => { + 8 | const array = makeArray(now); + 9 | const hasDate = identity(array); + +Error: Cannot access impure value during render + +Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). + +error.invalid-impure-functions-in-render-via-render-helper.ts:12:26 + 10 | return ; + 11 | }; +> 12 | return ; + | ^^^^^^^^^^ Cannot access impure value during render + 13 | } + 14 | + +error.invalid-impure-functions-in-render-via-render-helper.ts:6:14 + 4 | + 5 | function Component() { +> 6 | const now = Date.now(); + | ^^^^^^^^^^ `Date.now` is an impure function. + 7 | const renderItem = () => { + 8 | const array = makeArray(now); + 9 | const hasDate = identity(array); +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-render-helper.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-render-helper.js new file mode 100644 index 00000000000..d1d6fe7a035 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-render-helper.js @@ -0,0 +1,13 @@ +// @validateNoImpureFunctionsInRender + +import {identity, makeArray} from 'shared-runtime'; + +function Component() { + const now = Date.now(); + const renderItem = () => { + const array = makeArray(now); + const hasDate = identity(array); + return ; + }; + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render.expect.md index 255da7389b3..c413d9acd33 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render.expect.md @@ -19,41 +19,65 @@ function Component() { ``` Found 3 errors: -Error: Cannot call impure function during render +Error: Cannot access impure value during render -`Date.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). +Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). + +error.invalid-impure-functions-in-render.ts:7:20 + 5 | const now = performance.now(); + 6 | const rand = Math.random(); +> 7 | return ; + | ^^^^ Cannot access impure value during render + 8 | } + 9 | error.invalid-impure-functions-in-render.ts:4:15 2 | 3 | function Component() { > 4 | const date = Date.now(); - | ^^^^^^^^^^ Cannot call impure function + | ^^^^^^^^^^ `Date.now` is an impure function. 5 | const now = performance.now(); 6 | const rand = Math.random(); 7 | return ; -Error: Cannot call impure function during render +Error: Cannot access impure value during render -`performance.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). +Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). + +error.invalid-impure-functions-in-render.ts:7:31 + 5 | const now = performance.now(); + 6 | const rand = Math.random(); +> 7 | return ; + | ^^^ Cannot access impure value during render + 8 | } + 9 | error.invalid-impure-functions-in-render.ts:5:14 3 | function Component() { 4 | const date = Date.now(); > 5 | const now = performance.now(); - | ^^^^^^^^^^^^^^^^^ Cannot call impure function + | ^^^^^^^^^^^^^^^^^ `performance.now` is an impure function. 6 | const rand = Math.random(); 7 | return ; 8 | } -Error: Cannot call impure function during render +Error: Cannot access impure value during render -`Math.random` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). +Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). + +error.invalid-impure-functions-in-render.ts:7:42 + 5 | const now = performance.now(); + 6 | const rand = Math.random(); +> 7 | return ; + | ^^^^ Cannot access impure value during render + 8 | } + 9 | error.invalid-impure-functions-in-render.ts:6:15 4 | const date = Date.now(); 5 | const now = performance.now(); > 6 | const rand = Math.random(); - | ^^^^^^^^^^^^^ Cannot call impure function + | ^^^^^^^^^^^^^ `Math.random` is an impure function. 7 | return ; 8 | } 9 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-impure-functions-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-impure-functions-in-render.expect.md index 1241971d827..aabc8d2baea 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-impure-functions-in-render.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-impure-functions-in-render.expect.md @@ -19,41 +19,65 @@ function Component() { ``` Found 3 errors: -Error: Cannot call impure function during render +Error: Cannot access impure value during render -`Date.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). +Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). + +error.invalid-impure-functions-in-render.ts:7:20 + 5 | const now = performance.now(); + 6 | const rand = Math.random(); +> 7 | return ; + | ^^^^ Cannot access impure value during render + 8 | } + 9 | error.invalid-impure-functions-in-render.ts:4:15 2 | 3 | function Component() { > 4 | const date = Date.now(); - | ^^^^^^^^^^ Cannot call impure function + | ^^^^^^^^^^ `Date.now` is an impure function. 5 | const now = performance.now(); 6 | const rand = Math.random(); 7 | return ; -Error: Cannot call impure function during render +Error: Cannot access impure value during render -`performance.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). +Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). + +error.invalid-impure-functions-in-render.ts:7:31 + 5 | const now = performance.now(); + 6 | const rand = Math.random(); +> 7 | return ; + | ^^^ Cannot access impure value during render + 8 | } + 9 | error.invalid-impure-functions-in-render.ts:5:14 3 | function Component() { 4 | const date = Date.now(); > 5 | const now = performance.now(); - | ^^^^^^^^^^^^^^^^^ Cannot call impure function + | ^^^^^^^^^^^^^^^^^ `performance.now` is an impure function. 6 | const rand = Math.random(); 7 | return ; 8 | } -Error: Cannot call impure function during render +Error: Cannot access impure value during render -`Math.random` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). +Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). + +error.invalid-impure-functions-in-render.ts:7:42 + 5 | const now = performance.now(); + 6 | const rand = Math.random(); +> 7 | return ; + | ^^^^ Cannot access impure value during render + 8 | } + 9 | error.invalid-impure-functions-in-render.ts:6:15 4 | const date = Date.now(); 5 | const now = performance.now(); > 6 | const rand = Math.random(); - | ^^^^^^^^^^^^^ Cannot call impure function + | ^^^^^^^^^^^^^ `Math.random` is an impure function. 7 | return ; 8 | } 9 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-use-impure-value-in-ref.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-use-impure-value-in-ref.expect.md new file mode 100644 index 00000000000..6e0bf7d018f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-use-impure-value-in-ref.expect.md @@ -0,0 +1,49 @@ + +## Input + +```javascript +// @validateNoImpureFunctionsInRender +import {useIdentity} from 'shared-runtime'; + +function Component() { + const f = () => Math.random(); + const ref = useRef(f()); + return
; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateNoImpureFunctionsInRender +import { useIdentity } from "shared-runtime"; + +function Component() { + const $ = _c(2); + const f = _temp; + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = f(); + $[0] = t0; + } else { + t0 = $[0]; + } + const ref = useRef(t0); + let t1; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t1 =
; + $[1] = t1; + } else { + t1 = $[1]; + } + return t1; +} +function _temp() { + return Math.random(); +} + +``` + +### 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/valid-use-impure-value-in-ref.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-use-impure-value-in-ref.js new file mode 100644 index 00000000000..4002865548a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-use-impure-value-in-ref.js @@ -0,0 +1,8 @@ +// @validateNoImpureFunctionsInRender +import {useIdentity} from 'shared-runtime'; + +function Component() { + const f = () => Math.random(); + const ref = useRef(f()); + return
; +} diff --git a/compiler/packages/eslint-plugin-react-compiler/__tests__/ImpureFunctionCallsRule-test.ts b/compiler/packages/eslint-plugin-react-compiler/__tests__/ImpureFunctionCallsRule-test.ts index f89b049d100..aa27b28822b 100644 --- a/compiler/packages/eslint-plugin-react-compiler/__tests__/ImpureFunctionCallsRule-test.ts +++ b/compiler/packages/eslint-plugin-react-compiler/__tests__/ImpureFunctionCallsRule-test.ts @@ -29,9 +29,9 @@ testRule( } `, errors: [ - makeTestCaseError('Cannot call impure function during render'), - makeTestCaseError('Cannot call impure function during render'), - makeTestCaseError('Cannot call impure function during render'), + makeTestCaseError('Cannot access impure value during render'), + makeTestCaseError('Cannot access impure value during render'), + makeTestCaseError('Cannot access impure value during render'), ], }, ], From 2c4a3b9587709643df7ee7144b822fba6692b291 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Mon, 12 Jan 2026 16:38:41 -0800 Subject: [PATCH 2/8] [compiler] Validate ref reads in render via improved impure value validation Updates to guard against *reading* refs during render via the improved validateNoImpureValuesInRender() pass. InferMutationAliasingEffects generates `Impure` effects for ref reads, and then this pass validates that those accesses don't flow into `Render` effects. We now call the impure value validation first so that it takes priority over validateNoRefAccessInRender - the latter still has all the existing logic for now, but we can dramatically simplify it in a follow-up PR so that it only has to validate against ref writes. --- .../src/Entrypoint/Pipeline.ts | 13 ++-- .../src/HIR/HIR.ts | 26 +++++++ .../src/HIR/HIRBuilder.ts | 2 +- .../src/HIR/ObjectShape.ts | 5 +- .../src/HIR/TypeSchema.ts | 4 ++ .../src/Inference/AliasingEffects.ts | 14 +++- .../Inference/InferMutationAliasingEffects.ts | 70 ++++++++++++------- .../ValidateNoImpureValuesInRender.ts | 46 +++++++----- ...o-object-property-if-not-mutated.expect.md | 52 -------------- ...valid-access-ref-in-reducer-init.expect.md | 28 +++++++- ...or.invalid-access-ref-in-reducer.expect.md | 28 +++++++- ...-mutate-object-with-ref-function.expect.md | 20 ++++-- ...-access-ref-in-state-initializer.expect.md | 28 +++++++- ...-callback-invoked-during-render-.expect.md | 22 ++++-- ...ning-function-in-rendered-object.expect.md | 51 ++++++++++++++ ...-returning-function-in-rendered-object.js} | 0 ...-in-render-indirect-via-mutation.expect.md | 9 +++ ...ure-functions-in-render-indirect.expect.md | 9 +++ ...id-impure-value-in-render-helper.expect.md | 66 +++++++++++++++++ ...r.invalid-impure-value-in-render-helper.js | 8 +++ ...d-ref-prop-in-render-destructure.expect.md | 12 +++- ...ref-prop-in-render-property-load.expect.md | 12 +++- ....invalid-ref-access-render-unary.expect.md | 50 +++---------- ...n-callback-invoked-during-render.expect.md | 26 ++++--- ...error.invalid-ref-value-as-props.expect.md | 4 +- ...f-value-in-event-handler-wrapper.expect.md | 13 +++- .../error.invalid-exhaustive-deps.expect.md | 2 +- .../error.invalid-exhaustive-deps.js | 2 +- 28 files changed, 438 insertions(+), 184 deletions(-) delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-capturing-ref-returning-function-in-rendered-object.expect.md rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/{allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.js => error.invalid-capturing-ref-returning-function-in-rendered-object.js} (100%) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-value-in-render-helper.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-value-in-render-helper.js 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 ac7ce4003c7..d579968e178 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -271,10 +271,6 @@ function runWithEnvironment( assertValidMutableRanges(hir); } - if (env.config.validateRefAccessDuringRender) { - validateNoRefAccessInRender(hir).unwrap(); - } - if (env.config.validateNoSetStateInRender) { validateNoSetStateInRender(hir).unwrap(); } @@ -296,10 +292,17 @@ function runWithEnvironment( env.logErrors(validateNoJSXInTryStatement(hir)); } - if (env.config.validateNoImpureFunctionsInRender) { + if ( + env.config.validateNoImpureFunctionsInRender || + env.config.validateRefAccessDuringRender + ) { validateNoImpureValuesInRender(hir).unwrap(); } + if (env.config.validateRefAccessDuringRender) { + validateNoRefAccessInRender(hir).unwrap(); + } + validateNoFreezingKnownMutableFunctions(hir).unwrap(); } 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 c396f6b0881..14317cac09d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -1890,6 +1890,13 @@ export function isJsxType(type: Type): boolean { return type.kind === 'Object' && type.shapeId === 'BuiltInJsx'; } +export function isJsxOrJsxUnionType(type: Type): boolean { + return ( + (type.kind === 'Object' && type.shapeId === 'BuiltInJsx') || + (type.kind === 'Phi' && type.operands.some(op => isJsxOrJsxUnionType(op))) + ); +} + export function isRefOrRefValue(id: Identifier): boolean { return isUseRefType(id) || isRefValueType(id); } @@ -2058,4 +2065,23 @@ export function getHookKindForType( return null; } +export function areEqualSourceLocations( + loc1: SourceLocation, + loc2: SourceLocation, +): boolean { + if (typeof loc1 === 'symbol' || typeof loc2 === 'symbol') { + return false; + } + return ( + loc1.filename === loc2.filename && + loc1.identifierName === loc2.identifierName && + loc1.start.line === loc2.start.line && + loc1.start.column === loc2.start.column && + loc1.start.index === loc2.start.index && + loc1.end.line === loc2.end.line && + loc1.end.column === loc2.end.column && + loc1.end.index === loc2.end.index + ); +} + export * from './Types'; 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 d3ecb2abdcd..1a2a56a7f79 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts @@ -988,7 +988,7 @@ export function createTemporaryPlace( identifier: makeTemporaryIdentifier(env.nextIdentifierId, loc), reactive: false, effect: Effect.Unknown, - loc: GeneratedSource, + loc, }; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts index 43fe918eb6c..1ea88272d8c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import {CompilerError} from '../CompilerError'; +import {CompilerError, ErrorCategory} from '../CompilerError'; import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects'; import {assertExhaustive} from '../Utils/utils'; import { @@ -194,8 +194,11 @@ function parseAliasingSignatureConfig( return { kind: 'Impure', into, + category: ErrorCategory.Purity, description: effect.description, reason: effect.reason, + sourceMessage: effect.sourceMessage, + usageMessage: effect.usageMessage, }; } case 'Render': { diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts index 3d26b7d71af..a88d70aa7e4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts @@ -188,6 +188,8 @@ export type ImpureEffectConfig = { into: string; reason: string; description: string; + sourceMessage: string; + usageMessage: string; }; export const ImpureEffectSchema: z.ZodType = z.object({ @@ -195,6 +197,8 @@ export const ImpureEffectSchema: z.ZodType = z.object({ into: LifetimeIdSchema, reason: z.string(), description: z.string(), + sourceMessage: z.string(), + usageMessage: z.string(), }); export type RenderEffectConfig = { 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 e86ea34a46e..ce13cf0c3e0 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 {CompilerDiagnostic} from '../CompilerError'; +import {CompilerDiagnostic, ErrorCategory} from '../CompilerError'; import { FunctionExpression, GeneratedSource, @@ -162,7 +162,15 @@ export type AliasingEffect = /** * Indicates a side-effect that is not safe during render */ - | {kind: 'Impure'; into: Place; reason: string; description: string} + | { + kind: 'Impure'; + into: Place; + category: ErrorCategory; + reason: string; + description: string; + usageMessage: string; + sourceMessage: string; + } /** * 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 @@ -227,6 +235,8 @@ export function hashEffect(effect: AliasingEffect): string { effect.into.identifier.id, effect.reason, effect.description, + effect.usageMessage, + effect.sourceMessage, ].join(':'); case 'Render': { return [effect.kind, effect.place.identifier.id].join(':'); 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 c0b16cc1dee..908db0eb1f2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts @@ -27,6 +27,7 @@ import { InstructionKind, InstructionValue, isArrayType, + isJsxOrJsxUnionType, isMapType, isPrimitiveType, isRefOrRefValue, @@ -1988,8 +1989,11 @@ function computeSignatureForInstruction( effects.push({ kind: 'Impure', into: lvalue, + category: ErrorCategory.Refs, reason: `Cannot access ref value during render`, description: REF_ERROR_DESCRIPTION, + sourceMessage: `Ref is initially accessed`, + usageMessage: `Ref value is used during render`, }); } break; @@ -2156,6 +2160,15 @@ function computeSignatureForInstruction( into: lvalue, }); } + if (value.children != null) { + // Children are typically called during render, not used as an event/effect callback + for (const child of value.children) { + effects.push({ + kind: 'Render', + place: child, + }); + } + } if (value.kind === 'JsxExpression') { if (value.tag.kind === 'Identifier') { // Tags are render function, by definition they're called during render @@ -2164,24 +2177,23 @@ function computeSignatureForInstruction( place: value.tag, }); } - if (value.children != null) { - // Children are typically called during render, not used as an event/effect callback - for (const child of value.children) { + for (const prop of value.props) { + const place = + prop.kind === 'JsxAttribute' ? prop.place : prop.argument; + if (place.identifier.type.kind === 'Function') { + if (isJsxOrJsxUnionType(place.identifier.type.return)) { + effects.push({ + kind: 'Render', + place, + }); + } + } else { effects.push({ kind: 'Render', - place: child, + place, }); } } - for (const prop of value.props) { - if (prop.kind === 'JsxAttribute' && /^on[A-Z]/.test(prop.name)) { - continue; - } - effects.push({ - kind: 'Render', - place: prop.kind === 'JsxAttribute' ? prop.place : prop.argument, - }); - } } break; } @@ -2448,14 +2460,17 @@ function computeEffectsForLegacySignature( effects.push({ kind: 'Impure', into: lvalue, - reason: - signature.canonicalName != null - ? `\`${signature.canonicalName}\` is an impure function.` - : 'This function is impure', + category: ErrorCategory.Purity, + reason: 'Cannot access impure value during render', description: '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)', + sourceMessage: + signature.canonicalName != null + ? `\`${signature.canonicalName}\` is an impure function.` + : 'This function is impure', + usageMessage: 'Cannot access impure value during render', }); } if (signature.knownIncompatible != null && state.env.enableValidations) { @@ -2755,14 +2770,19 @@ function computeEffectsForSignature( break; } case 'Impure': { - const values = substitutions.get(effect.into.identifier.id) ?? []; - for (const value of values) { - effects.push({ - kind: effect.kind, - into: value, - reason: effect.reason, - description: effect.description, - }); + if (env.config.validateNoImpureFunctionsInRender) { + const values = substitutions.get(effect.into.identifier.id) ?? []; + for (const value of values) { + effects.push({ + kind: effect.kind, + into: value, + category: effect.category, + reason: effect.reason, + description: effect.description, + sourceMessage: effect.sourceMessage, + usageMessage: effect.usageMessage, + }); + } } break; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureValuesInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureValuesInRender.ts index 78674d87114..31e9f3e9230 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureValuesInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureValuesInRender.ts @@ -5,8 +5,13 @@ * LICENSE file in the root directory of this source tree. */ -import {CompilerDiagnostic, CompilerError, Effect, ErrorCategory} from '..'; -import {HIRFunction, IdentifierId, isUseRefType} from '../HIR'; +import {CompilerDiagnostic, CompilerError, Effect} from '..'; +import { + areEqualSourceLocations, + HIRFunction, + IdentifierId, + isUseRefType, +} from '../HIR'; import { eachInstructionLValue, eachInstructionValueOperand, @@ -246,23 +251,23 @@ function inferImpureValues( continue; } const impureEffect = impure.get(effect.place.identifier.id)!; - error.pushDiagnostic( - CompilerDiagnostic.create({ - category: ErrorCategory.Purity, - reason: 'Cannot access impure value during render', - description: impureEffect.description, - }) - .withDetails({ - kind: 'error', - loc: effect.place.loc, - message: 'Cannot access impure value during render', - }) - .withDetails({ - kind: 'error', - loc: impureEffect.into.loc, - message: impureEffect.reason, - }), - ); + const diagnostic = CompilerDiagnostic.create({ + category: impureEffect.category, + reason: impureEffect.reason, + description: impureEffect.description, + }).withDetails({ + kind: 'error', + loc: effect.place.loc, + message: impureEffect.usageMessage, + }); + if (!areEqualSourceLocations(effect.place.loc, impureEffect.into.loc)) { + diagnostic.withDetails({ + kind: 'error', + loc: impureEffect.into.loc, + message: impureEffect.sourceMessage, + }); + } + error.pushDiagnostic(diagnostic); } } } @@ -274,8 +279,11 @@ function inferImpureValues( impureEffects.push({ kind: 'Impure', into: {...place}, + category: impureEffect.category, reason: impureEffect.reason, description: impureEffect.description, + sourceMessage: impureEffect.sourceMessage, + usageMessage: impureEffect.usageMessage, }); } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.expect.md deleted file mode 100644 index b5fc0a9dc75..00000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.expect.md +++ /dev/null @@ -1,52 +0,0 @@ - -## Input - -```javascript -import {useRef} from 'react'; -import {Stringify} from 'shared-runtime'; - -function Component(props) { - const ref = useRef(props.value); - const object = {}; - object.foo = () => ref.current; - return ; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Component, - params: [{value: 42}], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; -import { useRef } from "react"; -import { Stringify } from "shared-runtime"; - -function Component(props) { - const $ = _c(1); - const ref = useRef(props.value); - let t0; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - const object = {}; - object.foo = () => ref.current; - t0 = ; - $[0] = t0; - } else { - t0 = $[0]; - } - return t0; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Component, - params: [{ value: 42 }], -}; - -``` - -### Eval output -(kind: ok)
{"object":{"foo":{"kind":"Function","result":42}},"shouldInvokeFns":true}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.expect.md index 647cf28f7b3..eefe34a438d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.expect.md @@ -26,9 +26,9 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` -Found 1 error: +Found 2 errors: -Error: Cannot access refs during render +Error: Cannot access ref value during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). @@ -36,7 +36,29 @@ error.invalid-access-ref-in-reducer-init.ts:8:4 6 | (state, action) => state + action, 7 | 0, > 8 | init => ref.current - | ^^^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render + | ^^^^^^^^^^^^^^^^^^^ Ref value is used during render + 9 | ); + 10 | + 11 | return ; + +Error: Cannot access ref value during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). + +error.invalid-access-ref-in-reducer-init.ts:11:27 + 9 | ); + 10 | +> 11 | return ; + | ^^^^^ Ref value is used during render + 12 | } + 13 | + 14 | export const FIXTURE_ENTRYPOINT = { + +error.invalid-access-ref-in-reducer-init.ts:8:4 + 6 | (state, action) => state + action, + 7 | 0, +> 8 | init => ref.current + | ^^^^^^^^^^^^^^^^^^^ Ref is initially accessed 9 | ); 10 | 11 | return ; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.expect.md index 33fcd6d188c..2b82ba0329a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.expect.md @@ -22,9 +22,9 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` -Found 1 error: +Found 2 errors: -Error: Cannot access refs during render +Error: Cannot access ref value during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). @@ -32,7 +32,29 @@ error.invalid-access-ref-in-reducer.ts:5:29 3 | function Component(props) { 4 | const ref = useRef(props.value); > 5 | const [state] = useReducer(() => ref.current, null); - | ^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render + | ^^^^^^^^^^^^^^^^^ Ref value is used during render + 6 | + 7 | return ; + 8 | } + +Error: Cannot access ref value during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). + +error.invalid-access-ref-in-reducer.ts:7:27 + 5 | const [state] = useReducer(() => ref.current, null); + 6 | +> 7 | return ; + | ^^^^^ Ref value is used during render + 8 | } + 9 | + 10 | export const FIXTURE_ENTRYPOINT = { + +error.invalid-access-ref-in-reducer.ts:5:29 + 3 | function Component(props) { + 4 | const ref = useRef(props.value); +> 5 | const [state] = useReducer(() => ref.current, null); + | ^^^^^^^^^^^^^^^^^ Ref is initially accessed 6 | 7 | return ; 8 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.expect.md index 73cead6affc..1e6696d4a4b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.expect.md @@ -20,18 +20,26 @@ function Component() { ``` Found 1 error: -Error: Cannot access refs during render +Error: Cannot access ref value during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). -error.invalid-access-ref-in-render-mutate-object-with-ref-function.ts:7:19 - 5 | const object = {}; +error.invalid-access-ref-in-render-mutate-object-with-ref-function.ts:8:15 6 | object.foo = () => ref.current; -> 7 | const refValue = object.foo(); - | ^^^^^^^^^^ This function accesses a ref value - 8 | return
{refValue}
; + 7 | const refValue = object.foo(); +> 8 | return
{refValue}
; + | ^^^^^^^^ Ref value is used during render 9 | } 10 | + +error.invalid-access-ref-in-render-mutate-object-with-ref-function.ts:6:15 + 4 | const ref = useRef(null); + 5 | const object = {}; +> 6 | object.foo = () => ref.current; + | ^^^^^^^^^^^^^^^^^ Ref is initially accessed + 7 | const refValue = object.foo(); + 8 | return
{refValue}
; + 9 | } ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.expect.md index a10db96463d..024c098ccad 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.expect.md @@ -22,9 +22,9 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` -Found 1 error: +Found 2 errors: -Error: Cannot access refs during render +Error: Cannot access ref value during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). @@ -32,7 +32,29 @@ error.invalid-access-ref-in-state-initializer.ts:5:27 3 | function Component(props) { 4 | const ref = useRef(props.value); > 5 | const [state] = useState(() => ref.current); - | ^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render + | ^^^^^^^^^^^^^^^^^ Ref value is used during render + 6 | + 7 | return ; + 8 | } + +Error: Cannot access ref value during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). + +error.invalid-access-ref-in-state-initializer.ts:7:27 + 5 | const [state] = useState(() => ref.current); + 6 | +> 7 | return ; + | ^^^^^ Ref value is used during render + 8 | } + 9 | + 10 | export const FIXTURE_ENTRYPOINT = { + +error.invalid-access-ref-in-state-initializer.ts:5:27 + 3 | function Component(props) { + 4 | const ref = useRef(props.value); +> 5 | const [state] = useState(() => ref.current); + | ^^^^^^^^^^^^^^^^^ Ref is initially accessed 6 | 7 | return ; 8 | } 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 09a64d4bab2..4f8f295a563 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 @@ -21,17 +21,27 @@ function Component(props) { ``` Found 1 error: -Error: Cannot access refs during render +Error: Cannot access ref value during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). -error.invalid-aliased-ref-in-callback-invoked-during-render-.ts:9:33 - 7 | return ; +error.invalid-aliased-ref-in-callback-invoked-during-render-.ts:7:37 + 5 | const aliasedRef = ref; + 6 | const current = aliasedRef.current; +> 7 | return ; + | ^^^^^^^ Ref value is used during render 8 | }; -> 9 | return {props.items.map(item => renderItem(item))}; - | ^^^^^^^^^^^^^^^^^^^^^^^^ Cannot access ref value during render + 9 | return {props.items.map(item => renderItem(item))}; 10 | } - 11 | + +error.invalid-aliased-ref-in-callback-invoked-during-render-.ts:6:20 + 4 | const renderItem = item => { + 5 | const aliasedRef = ref; +> 6 | const current = aliasedRef.current; + | ^^^^^^^^^^^^^^^^^^ Ref is initially accessed + 7 | return ; + 8 | }; + 9 | return {props.items.map(item => renderItem(item))}; ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-capturing-ref-returning-function-in-rendered-object.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-capturing-ref-returning-function-in-rendered-object.expect.md new file mode 100644 index 00000000000..e23dc709983 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-capturing-ref-returning-function-in-rendered-object.expect.md @@ -0,0 +1,51 @@ + +## Input + +```javascript +import {useRef} from 'react'; +import {Stringify} from 'shared-runtime'; + +function Component(props) { + const ref = useRef(props.value); + const object = {}; + object.foo = () => ref.current; + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot access ref value during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). + +error.invalid-capturing-ref-returning-function-in-rendered-object.ts:8:28 + 6 | const object = {}; + 7 | object.foo = () => ref.current; +> 8 | return ; + | ^^^^^^ Ref value is used during render + 9 | } + 10 | + 11 | export const FIXTURE_ENTRYPOINT = { + +error.invalid-capturing-ref-returning-function-in-rendered-object.ts:7:15 + 5 | const ref = useRef(props.value); + 6 | const object = {}; +> 7 | object.foo = () => ref.current; + | ^^^^^^^^^^^^^^^^^ Ref is initially accessed + 8 | return ; + 9 | } + 10 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-capturing-ref-returning-function-in-rendered-object.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-capturing-ref-returning-function-in-rendered-object.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect-via-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect-via-mutation.expect.md index 0d4a5b06aff..af3ebad63d6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect-via-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect-via-mutation.expect.md @@ -33,6 +33,15 @@ error.invalid-impure-functions-in-render-indirect-via-mutation.ts:10:23 | ^^^^^ Cannot access impure value during render 11 | } 12 | + +error.invalid-impure-functions-in-render-indirect-via-mutation.ts:6:18 + 4 | + 5 | function Component() { +> 6 | const getDate = () => Date.now(); + | ^^^^^^^^^^^^^^^^ `Date.now` is an impure function. + 7 | const now = getDate(); + 8 | const array = []; + 9 | arrayPush(array, now); ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect.expect.md index b5572d81cb8..1f930a582ec 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect.expect.md @@ -32,6 +32,15 @@ error.invalid-impure-functions-in-render-indirect.ts:9:23 | ^^^^^^^ Cannot access impure value during render 10 | } 11 | + +error.invalid-impure-functions-in-render-indirect.ts:6:18 + 4 | + 5 | function Component() { +> 6 | const getDate = () => Date.now(); + | ^^^^^^^^^^^^^^^^ `Date.now` is an impure function. + 7 | const array = makeArray(getDate()); + 8 | const hasDate = identity(array); + 9 | return ; ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-value-in-render-helper.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-value-in-render-helper.expect.md new file mode 100644 index 00000000000..db33136aea2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-value-in-render-helper.expect.md @@ -0,0 +1,66 @@ + +## Input + +```javascript +// @validateNoImpureFunctionsInRender +function Component() { + const now = () => Date.now(); + const render = () => { + return
{now()}
; + }; + return
{render()}
; +} + +``` + + +## Error + +``` +Found 2 errors: + +Error: Cannot access impure value during render + +Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). + +error.invalid-impure-value-in-render-helper.ts:5:17 + 3 | const now = () => Date.now(); + 4 | const render = () => { +> 5 | return
{now()}
; + | ^^^^^ Cannot access impure value during render + 6 | }; + 7 | return
{render()}
; + 8 | } + +error.invalid-impure-value-in-render-helper.ts:3:14 + 1 | // @validateNoImpureFunctionsInRender + 2 | function Component() { +> 3 | const now = () => Date.now(); + | ^^^^^^^^^^^^^^^^ `Date.now` is an impure function. + 4 | const render = () => { + 5 | return
{now()}
; + 6 | }; + +Error: Cannot access impure value during render + +Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). + +error.invalid-impure-value-in-render-helper.ts:7:15 + 5 | return
{now()}
; + 6 | }; +> 7 | return
{render()}
; + | ^^^^^^^^ Cannot access impure value during render + 8 | } + 9 | + +error.invalid-impure-value-in-render-helper.ts:3:14 + 1 | // @validateNoImpureFunctionsInRender + 2 | function Component() { +> 3 | const now = () => Date.now(); + | ^^^^^^^^^^^^^^^^ `Date.now` is an impure function. + 4 | const render = () => { + 5 | return
{now()}
; + 6 | }; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-value-in-render-helper.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-value-in-render-helper.js new file mode 100644 index 00000000000..feecf48e843 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-value-in-render-helper.js @@ -0,0 +1,8 @@ +// @validateNoImpureFunctionsInRender +function Component() { + const now = () => Date.now(); + const render = () => { + return
{now()}
; + }; + return
{render()}
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-destructure.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-destructure.expect.md index cca903de74c..ca63557d388 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-destructure.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-destructure.expect.md @@ -16,15 +16,23 @@ function Component({ref}) { ``` Found 1 error: -Error: Cannot access refs during render +Error: Cannot access ref value during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). +error.invalid-read-ref-prop-in-render-destructure.ts:4:15 + 2 | function Component({ref}) { + 3 | const value = ref.current; +> 4 | return
{value}
; + | ^^^^^ Ref value is used during render + 5 | } + 6 | + error.invalid-read-ref-prop-in-render-destructure.ts:3:16 1 | // @validateRefAccessDuringRender @compilationMode:"infer" 2 | function Component({ref}) { > 3 | const value = ref.current; - | ^^^^^^^^^^^ Cannot access ref value during render + | ^^^^^^^^^^^ Ref is initially accessed 4 | return
{value}
; 5 | } 6 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-property-load.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-property-load.expect.md index 49b8e5d199f..dee36304b21 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-property-load.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-property-load.expect.md @@ -16,15 +16,23 @@ function Component(props) { ``` Found 1 error: -Error: Cannot access refs during render +Error: Cannot access ref value during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). +error.invalid-read-ref-prop-in-render-property-load.ts:4:15 + 2 | function Component(props) { + 3 | const value = props.ref.current; +> 4 | return
{value}
; + | ^^^^^ Ref value is used during render + 5 | } + 6 | + error.invalid-read-ref-prop-in-render-property-load.ts:3:16 1 | // @validateRefAccessDuringRender @compilationMode:"infer" 2 | function Component(props) { > 3 | const value = props.ref.current; - | ^^^^^^^^^^^^^^^^^ Cannot access ref value during render + | ^^^^^^^^^^^^^^^^^ Ref is initially accessed 4 | return
{value}
; 5 | } 6 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-access-render-unary.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-access-render-unary.expect.md index ce1be800a13..62b08209ce2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-access-render-unary.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-access-render-unary.expect.md @@ -22,57 +22,27 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` -Found 4 errors: +Found 1 error: -Error: Cannot access refs during render - -React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). - - 4 | component C() { - 5 | const r = useRef(null); -> 6 | const current = !r.current; - | ^^^^^^^^^ Cannot access ref value during render - 7 | return
{current}
; - 8 | } - 9 | - -To initialize a ref only once, check that the ref is null with the pattern `if (ref.current == null) { ref.current = ... }` - -Error: Cannot access refs during render - -React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). - - 4 | component C() { - 5 | const r = useRef(null); -> 6 | const current = !r.current; - | ^^^^^^^^^^ Cannot access ref value during render - 7 | return
{current}
; - 8 | } - 9 | - -Error: Cannot access refs during render +Error: Cannot access ref value during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). 5 | const r = useRef(null); 6 | const current = !r.current; > 7 | return
{current}
; - | ^^^^^^^ Cannot access ref value during render + | ^^^^^^^ Ref value is used during render 8 | } 9 | 10 | export const FIXTURE_ENTRYPOINT = { -Error: Cannot access refs during render - -React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). - - 5 | const r = useRef(null); - 6 | const current = !r.current; -> 7 | return
{current}
; - | ^^^^^^^ Cannot access ref value during render - 8 | } - 9 | - 10 | export const FIXTURE_ENTRYPOINT = { + 4 | component C() { + 5 | const r = useRef(null); +> 6 | const current = !r.current; + | ^^^^^^^^^ Ref is initially accessed + 7 | return
{current}
; + 8 | } + 9 | ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-in-callback-invoked-during-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-in-callback-invoked-during-render.expect.md index df1e771fa25..95b7b171785 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-in-callback-invoked-during-render.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-in-callback-invoked-during-render.expect.md @@ -20,17 +20,27 @@ function Component(props) { ``` Found 1 error: -Error: Cannot access refs during render +Error: Cannot access ref value during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). -error.invalid-ref-in-callback-invoked-during-render.ts:8:33 - 6 | return ; - 7 | }; -> 8 | return {props.items.map(item => renderItem(item))}; - | ^^^^^^^^^^^^^^^^^^^^^^^^ Cannot access ref value during render - 9 | } - 10 | +error.invalid-ref-in-callback-invoked-during-render.ts:6:37 + 4 | const renderItem = item => { + 5 | const current = ref.current; +> 6 | return ; + | ^^^^^^^ Ref value is used during render + 7 | }; + 8 | return {props.items.map(item => renderItem(item))}; + 9 | } + +error.invalid-ref-in-callback-invoked-during-render.ts:5:20 + 3 | const ref = useRef(null); + 4 | const renderItem = item => { +> 5 | const current = ref.current; + | ^^^^^^^^^^^ Ref is initially accessed + 6 | return ; + 7 | }; + 8 | return {props.items.map(item => renderItem(item))}; ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-value-as-props.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-value-as-props.expect.md index d581232b3a4..322ebdbde76 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-value-as-props.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-value-as-props.expect.md @@ -16,7 +16,7 @@ function Component(props) { ``` Found 1 error: -Error: Cannot access refs during render +Error: Cannot access ref value during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). @@ -24,7 +24,7 @@ error.invalid-ref-value-as-props.ts:4:19 2 | function Component(props) { 3 | const ref = useRef(null); > 4 | return ; - | ^^^^^^^^^^^ Cannot access ref value during render + | ^^^^^^^^^^^ Ref value is used during render 5 | } 6 | ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-event-handler-wrapper.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-event-handler-wrapper.expect.md index 718e2c81419..85fd1764ef2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-event-handler-wrapper.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-event-handler-wrapper.expect.md @@ -38,15 +38,24 @@ export const FIXTURE_ENTRYPOINT = { ``` Found 1 error: -Error: Cannot access refs during render +Error: Cannot access ref value during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). +error.ref-value-in-event-handler-wrapper.ts:19:6 + 17 | <> + 18 | +> 19 | + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Ref value is used during render + 20 | + 21 | ); + 22 | } + error.ref-value-in-event-handler-wrapper.ts:19:35 17 | <> 18 | > 19 | - | ^^^^^^^^^^^ Cannot access ref value during render + | ^^^^^^^^^^^ Ref is initially accessed 20 | 21 | ); 22 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps.expect.md index 2c864f56aff..d5674691bb9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps.expect.md @@ -2,7 +2,7 @@ ## Input ```javascript -// @validateExhaustiveMemoizationDependencies +// @validateExhaustiveMemoizationDependencies @validateRefAccessDuringRender:false import {useMemo} from 'react'; import {Stringify} from 'shared-runtime'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps.js index c0f8d28837a..feba85da7d2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps.js @@ -1,4 +1,4 @@ -// @validateExhaustiveMemoizationDependencies +// @validateExhaustiveMemoizationDependencies @validateRefAccessDuringRender:false import {useMemo} from 'react'; import {Stringify} from 'shared-runtime'; From a974753ddf42b198ce42f934870c7077c1c2a95d Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Tue, 13 Jan 2026 18:56:35 -0800 Subject: [PATCH 3/8] [compiler] Use aliasing effects for impurity inference --- .../src/HIR/Globals.ts | 78 +++-- .../src/HIR/HIR.ts | 10 +- .../Inference/InferMutationAliasingEffects.ts | 83 ++++- .../Inference/InferMutationAliasingRanges.ts | 25 +- .../src/Utils/utils.ts | 8 + .../ValidateNoImpureValuesInRender.ts | 302 +++++++++--------- .../Validation/ValidateNoRefAccessInRender.ts | 203 +----------- .../compiler/error.hook-ref-value.expect.md | 14 +- ...invalid-access-ref-during-render.expect.md | 12 +- ...valid-access-ref-in-reducer-init.expect.md | 19 +- ...or.invalid-access-ref-in-reducer.expect.md | 19 +- ...-mutate-object-with-ref-function.expect.md | 4 +- ...-access-ref-in-state-initializer.expect.md | 19 +- ...ning-function-in-rendered-object.expect.md | 4 +- ...-in-render-indirect-via-mutation.expect.md | 4 +- ...ure-functions-in-render-indirect.expect.md | 4 +- ...ns-in-render-via-function-call-2.expect.md | 16 +- ...ions-in-render-via-function-call.expect.md | 16 +- ...n-render-via-render-helper-typed.expect.md | 50 +++ ...tions-in-render-via-render-helper-typed.js | 14 + ...ions-in-render-via-render-helper.expect.md | 24 +- ...id-impure-value-in-render-helper.expect.md | 27 +- ...d-set-and-read-ref-during-render.expect.md | 19 +- ...ef-nested-property-during-render.expect.md | 19 +- ...ror.ref-initialization-arbitrary.expect.md | 14 +- .../compiler/error.ref-optional.expect.md | 4 +- .../compiler/return-ref-callback.expect.md | 14 +- .../fixtures/compiler/return-ref-callback.js | 6 +- ...lout-validate-ref-current-access.expect.md | 38 ++- 29 files changed, 490 insertions(+), 579 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-render-helper-typed.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-render-helper-typed.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts index 5a4d9cf8c92..b0b074a18ff 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts @@ -664,7 +664,7 @@ const RenderHookAliasing: ( const EffectHookAliasing: AliasingSignatureConfig = { receiver: '@receiver', - params: [], + params: ['@fn', '@deps'], rest: '@rest', returns: '@returns', temporaries: ['@effect'], @@ -675,6 +675,21 @@ const EffectHookAliasing: AliasingSignatureConfig = { value: '@rest', reason: ValueReason.Effect, }, + { + kind: 'Freeze', + value: '@fn', + reason: ValueReason.Effect, + }, + { + kind: 'Freeze', + value: '@deps', + reason: ValueReason.Effect, + }, + // Deps are accessed during render + { + kind: 'Render', + place: '@deps', + }, // Internally creates an effect object that captures the function and deps { kind: 'Create', @@ -688,6 +703,11 @@ const EffectHookAliasing: AliasingSignatureConfig = { from: '@rest', into: '@effect', }, + { + kind: 'Capture', + from: '@fn', + into: '@effect', + }, // Returns undefined { kind: 'Create', @@ -703,6 +723,39 @@ const EffectHookAliasing: AliasingSignatureConfig = { * now that FeatureFlag `enableTreatHooksAsFunctions` is removed we can * use positional params too (?) */ +const useEffectEvent = addHook( + DEFAULT_SHAPES, + { + positionalParams: [], + restParam: Effect.Freeze, + returnType: { + kind: 'Function', + return: {kind: 'Poly'}, + shapeId: BuiltInEffectEventId, + isConstructor: false, + }, + calleeEffect: Effect.Read, + hookKind: 'useEffectEvent', + // Frozen because it should not mutate any locally-bound values + returnValueKind: ValueKind.Frozen, + aliasing: { + receiver: '@receiver', + params: ['@value'], + rest: null, + returns: '@return', + temporaries: [], + effects: [ + {kind: 'Assign', from: '@value', into: '@return'}, + { + kind: 'Freeze', + value: '@value', + reason: ValueReason.HookCaptured, + }, + ], + }, + }, + BuiltInUseEffectEventId, +); const REACT_APIS: Array<[string, BuiltInType]> = [ [ 'useContext', @@ -915,27 +968,8 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ BuiltInFireId, ), ], - [ - 'useEffectEvent', - addHook( - DEFAULT_SHAPES, - { - positionalParams: [], - restParam: Effect.Freeze, - returnType: { - kind: 'Function', - return: {kind: 'Poly'}, - shapeId: BuiltInEffectEventId, - isConstructor: false, - }, - calleeEffect: Effect.Read, - hookKind: 'useEffectEvent', - // Frozen because it should not mutate any locally-bound values - returnValueKind: ValueKind.Frozen, - }, - BuiltInUseEffectEventId, - ), - ], + ['useEffectEvent', useEffectEvent], + ['experimental_useEffectEvent', useEffectEvent], ['AUTODEPS', addObject(DEFAULT_SHAPES, BuiltInAutodepsId, [])], ]; 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 14317cac09d..9e8dae0abd0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -1879,7 +1879,15 @@ export function isRefValueType(id: Identifier): boolean { } export function isUseRefType(id: Identifier): boolean { - return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseRefId'; + return isUseRefType_(id.type); +} + +export function isUseRefType_(type: Type): boolean { + return ( + (type.kind === 'Object' && type.shapeId === 'BuiltInUseRefId') || + (type.kind === 'Phi' && + type.operands.some(operand => isUseRefType_(operand))) + ); } export function isUseStateType(id: Identifier): boolean { 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 908db0eb1f2..e6866a51a3a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts @@ -29,6 +29,7 @@ import { isArrayType, isJsxOrJsxUnionType, isMapType, + isMutableEffect, isPrimitiveType, isRefOrRefValue, isSetType, @@ -587,6 +588,17 @@ function inferBlock( }), ); } + if ( + context.fn.fnType === 'Component' || + isJsxOrJsxUnionType(context.fn.returns.identifier.type) + ) { + terminal.effects.push( + context.internEffect({ + kind: 'Render', + place: terminal.value, + }), + ); + } } } @@ -758,17 +770,7 @@ function applyEffect( break; } case 'ImmutableCapture': { - const kind = state.kind(effect.from).kind; - switch (kind) { - case ValueKind.Global: - case ValueKind.Primitive: { - // no-op: we don't need to track data flow for copy types - break; - } - default: { - effects.push(effect); - } - } + effects.push(effect); break; } case 'CreateFrom': { @@ -1070,6 +1072,17 @@ function applyEffect( reason: new Set(fromValue.reason), }); state.define(effect.into, value); + applyEffect( + context, + state, + { + kind: 'ImmutableCapture', + from: effect.from, + into: effect.into, + }, + initialized, + effects, + ); break; } default: { @@ -1975,6 +1988,11 @@ function computeSignatureForInstruction( value: ValueKind.Primitive, reason: ValueReason.Other, }); + effects.push({ + kind: 'ImmutableCapture', + from: value.object, + into: lvalue, + }); } else { effects.push({ kind: 'CreateFrom', @@ -2180,6 +2198,9 @@ function computeSignatureForInstruction( for (const prop of value.props) { const place = prop.kind === 'JsxAttribute' ? prop.place : prop.argument; + if (isUseRefType(place.identifier)) { + continue; + } if (place.identifier.type.kind === 'Function') { if (isJsxOrJsxUnionType(place.identifier.type.return)) { effects.push({ @@ -2226,6 +2247,11 @@ function computeSignatureForInstruction( value: ValueKind.Primitive, reason: ValueReason.Other, }); + effects.push({ + kind: 'ImmutableCapture', + from: value.value, + into: place, + }); } else if (patternItem.kind === 'Identifier') { effects.push({ kind: 'CreateFrom', @@ -2407,15 +2433,46 @@ function computeSignatureForInstruction( }); break; } + case 'BinaryExpression': { + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Primitive, + reason: ValueReason.Other, + }); + effects.push({ + kind: 'ImmutableCapture', + into: lvalue, + from: value.left, + }); + effects.push({ + kind: 'ImmutableCapture', + into: lvalue, + from: value.right, + }); + break; + } + case 'UnaryExpression': { + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Primitive, + reason: ValueReason.Other, + }); + effects.push({ + kind: 'ImmutableCapture', + into: lvalue, + from: value.value, + }); + break; + } case 'TaggedTemplateExpression': - case 'BinaryExpression': case 'Debugger': case 'JSXText': case 'MetaProperty': case 'Primitive': case 'RegExpLiteral': case 'TemplateLiteral': - case 'UnaryExpression': case 'UnsupportedNode': { effects.push({ kind: 'Create', 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 ae1e07c5a73..f0cab1345db 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts @@ -224,17 +224,20 @@ export function inferMutationAliasingRanges( if (effect.kind === 'Alias') { state.assign(index++, effect.from, effect.into); } else { - CompilerError.invariant(effect.kind === 'Freeze', { - reason: `Unexpected '${effect.kind}' effect for MaybeThrow terminal`, - description: null, - details: [ - { - kind: 'error', - loc: block.terminal.loc, - message: null, - }, - ], - }); + CompilerError.invariant( + effect.kind === 'Freeze' || effect.kind === 'Render', + { + reason: `Unexpected '${effect.kind}' effect for MaybeThrow terminal`, + description: null, + details: [ + { + kind: 'error', + loc: block.terminal.loc, + message: null, + }, + ], + }, + ); } } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts b/compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts index 897614015f5..32f5d6f40f2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts @@ -167,6 +167,14 @@ export function Set_filter( return result; } +export function Set_subtract( + source: ReadonlySet, + other: Iterable, +): Set { + const otherSet = other instanceof Set ? other : new Set(other); + return Set_filter(source, item => !otherSet.has(item)); +} + export function hasNode( input: NodePath, ): input is NodePath> { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureValuesInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureValuesInRender.ts index 31e9f3e9230..3db4dd9ab4c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureValuesInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureValuesInRender.ts @@ -5,11 +5,15 @@ * LICENSE file in the root directory of this source tree. */ +import prettyFormat from 'pretty-format'; import {CompilerDiagnostic, CompilerError, Effect} from '..'; import { areEqualSourceLocations, HIRFunction, IdentifierId, + InstructionId, + isJsxType, + isRefValueType, isUseRefType, } from '../HIR'; import { @@ -20,9 +24,16 @@ import {AliasingEffect} from '../Inference/AliasingEffects'; import {createControlDominators} from '../Inference/ControlDominators'; import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables'; import {Err, Ok, Result} from '../Utils/Result'; -import {assertExhaustive, getOrInsertWith} from '../Utils/utils'; +import { + assertExhaustive, + getOrInsertWith, + Set_filter, + Set_subtract, +} from '../Utils/utils'; +import {printInstruction} from '../HIR/PrintHIR'; type ImpureEffect = Extract; +type RenderEffect = Extract; type FunctionCache = Map>; type ImpuritySignature = {effects: Array; error: CompilerError}; @@ -49,12 +60,106 @@ function inferFunctionExpressionMemo( return getOrInsertWith( getOrInsertWith(cache, fn, () => new Map()), key, - () => { - return inferImpureValues(fn, impure, cache); - }, + () => inferImpureValues(fn, impure, cache), ); } +function processEffects( + id: InstructionId, + effects: Array, + impure: Map, + cache: FunctionCache, +): boolean { + let hasChanges = false; + const rendered: Set = new Set(); + for (const effect of effects) { + if (effect.kind === 'Render') { + rendered.add(effect.place.identifier.id); + } + } + for (const effect of effects) { + switch (effect.kind) { + case 'Alias': + case 'Assign': + case 'Capture': + case 'CreateFrom': + case 'ImmutableCapture': { + const sourceEffect = impure.get(effect.from.identifier.id); + if ( + sourceEffect != null && + !impure.has(effect.into.identifier.id) && + !rendered.has(effect.from.identifier.id) && + !isUseRefType(effect.into.identifier) && + !isJsxType(effect.into.identifier.type) + ) { + impure.set(effect.into.identifier.id, sourceEffect); + hasChanges = true; + } + if ( + sourceEffect == null && + (effect.kind === 'Assign' || effect.kind === 'Capture') && + !impure.has(effect.from.identifier.id) && + !rendered.has(effect.from.identifier.id) && + !isUseRefType(effect.from.identifier) && + isMutable({id}, effect.into) + ) { + const destinationEffect = impure.get(effect.into.identifier.id); + if (destinationEffect != null) { + impure.set(effect.from.identifier.id, destinationEffect); + hasChanges = true; + } + } + break; + } + case 'Impure': { + if (!impure.has(effect.into.identifier.id)) { + impure.set(effect.into.identifier.id, effect); + hasChanges = true; + } + break; + } + case 'Render': { + break; + } + case 'CreateFunction': { + const result = inferFunctionExpressionMemo( + effect.function.loweredFunc.func, + impure, + cache, + ); + if (result.error.hasAnyErrors()) { + break; + } + const impureEffect: ImpureEffect | null = + result.effects.find( + (functionEffect: AliasingEffect): functionEffect is ImpureEffect => + functionEffect.kind === 'Impure' && + functionEffect.into.identifier.id === + effect.function.loweredFunc.func.returns.identifier.id, + ) ?? null; + if (impureEffect != null) { + impure.set(effect.into.identifier.id, impureEffect); + hasChanges = true; + } + break; + } + case 'MaybeAlias': + case 'Apply': + case 'Create': + case 'Freeze': + case 'Mutate': + case 'MutateConditionally': + case 'MutateFrozen': + case 'MutateGlobal': + case 'MutateTransitive': + case 'MutateTransitiveConditionally': { + break; + } + } + } + return hasChanges; +} + function inferImpureValues( fn: HIRFunction, impure: Map, @@ -105,128 +210,53 @@ function inferImpureValues( } for (const instr of block.instructions) { - let impureEffect: ImpureEffect | undefined = instr.effects?.find( - (effect: AliasingEffect): effect is ImpureEffect => - effect.kind === 'Impure', - ); - - if ( - impureEffect == null && - (instr.value.kind === 'FunctionExpression' || - instr.value.kind === 'ObjectMethod') - ) { - impureEffect = instr.value.loweredFunc.func.aliasingEffects?.find( - (effect: AliasingEffect): effect is ImpureEffect => - effect.kind === 'Impure', - ); - if (impureEffect == null) { - const result = inferFunctionExpressionMemo( - instr.value.loweredFunc.func, - impure, - cache, - ); - if (!result.error.hasAnyErrors()) { - impureEffect = result.effects[0]; - } - } - } - - if (impureEffect == null) { - for (const operand of eachInstructionValueOperand(instr.value)) { - const operandEffect = impure.get(operand.identifier.id); - if (operandEffect != null) { - impureEffect = operandEffect; - break; - } - } - } - - if (impureEffect != null) { - for (const lvalue of eachInstructionLValue(instr)) { - if (isUseRefType(lvalue.identifier)) { - continue; - } - if (!impure.has(lvalue.identifier.id)) { - impure.set(lvalue.identifier.id, impureEffect); - hasChanges = true; - } - } - } - if (impureEffect != null || controlImpureEffect != null) { - for (const operand of eachInstructionValueOperand(instr.value)) { - switch (operand.effect) { - case Effect.Capture: - case Effect.Store: - case Effect.ConditionallyMutate: - case Effect.ConditionallyMutateIterator: - case Effect.Mutate: { - if ( - !impure.has(operand.identifier.id) && - isMutable(instr, operand) - ) { - impure.set( - operand.identifier.id, - (impureEffect ?? controlImpureEffect)!, - ); - hasChanges = true; - } - break; - } - case Effect.Freeze: - case Effect.Read: { - // no-op - break; - } - case Effect.Unknown: { - CompilerError.invariant(false, { - reason: 'Unexpected unknown effect', - description: null, - details: [ - { - kind: 'error', - loc: operand.loc, - message: null, - }, - ], - suggestions: null, - }); - } - default: { - assertExhaustive( - operand.effect, - `Unexpected effect kind \`${operand.effect}\``, - ); - } - } - } - } - if (impureEffect == null) { - const lvalueEffect = impure.get(instr.lvalue.identifier.id)!; - if (lvalueEffect != null) { - for (const operand of eachInstructionValueOperand(instr.value)) { - if ( - isMutable(instr, operand) && - !impure.has(operand.identifier.id) - ) { - impure.set(operand.identifier.id, lvalueEffect); - hasChanges = true; - } - } - } - } + const _impure = new Set(impure.keys()); + hasChanges = + processEffects(instr.id, instr.effects ?? [], impure, cache) || + hasChanges; } - - if (block.terminal.kind === 'return') { - const terminalEffect = impure.get(block.terminal.value.identifier.id); - if (terminalEffect != null && !impure.has(fn.returns.identifier.id)) { - impure.set(fn.returns.identifier.id, terminalEffect); - hasChanges = true; - } + if (block.terminal.kind === 'return' && block.terminal.effects != null) { + hasChanges = + processEffects( + block.terminal.id, + block.terminal.effects, + impure, + cache, + ) || hasChanges; } } } while (hasChanges); + fn.env.logger?.debugLogIRs?.({ + kind: 'debug', + name: 'ValidateNoImpureValuesInRender', + value: JSON.stringify(Array.from(impure.keys()).sort(), null, 2), + }); + const error = new CompilerError(); + function validateRenderEffect(effect: RenderEffect): void { + const impureEffect = impure.get(effect.place.identifier.id); + if (impureEffect == null) { + return; + } + const diagnostic = CompilerDiagnostic.create({ + category: impureEffect.category, + reason: impureEffect.reason, + description: impureEffect.description, + }).withDetails({ + kind: 'error', + loc: effect.place.loc, + message: impureEffect.usageMessage, + }); + if (!areEqualSourceLocations(effect.place.loc, impureEffect.into.loc)) { + diagnostic.withDetails({ + kind: 'error', + loc: impureEffect.into.loc, + message: impureEffect.sourceMessage, + }); + } + error.pushDiagnostic(diagnostic); + } for (const block of fn.body.blocks.values()) { for (const instr of block.instructions) { const value = instr.value; @@ -244,30 +274,16 @@ function inferImpureValues( } } for (const effect of instr.effects ?? []) { - if ( - effect.kind !== 'Render' || - !impure.has(effect.place.identifier.id) - ) { - continue; + if (effect.kind === 'Render') { + validateRenderEffect(effect); } - const impureEffect = impure.get(effect.place.identifier.id)!; - const diagnostic = CompilerDiagnostic.create({ - category: impureEffect.category, - reason: impureEffect.reason, - description: impureEffect.description, - }).withDetails({ - kind: 'error', - loc: effect.place.loc, - message: impureEffect.usageMessage, - }); - if (!areEqualSourceLocations(effect.place.loc, impureEffect.into.loc)) { - diagnostic.withDetails({ - kind: 'error', - loc: impureEffect.into.loc, - message: impureEffect.sourceMessage, - }); + } + } + if (block.terminal.kind === 'return' && block.terminal.effects != null) { + for (const effect of block.terminal.effects) { + if (effect.kind === 'Render') { + validateRenderEffect(effect); } - error.pushDiagnostic(diagnostic); } } } @@ -278,7 +294,7 @@ function inferImpureValues( if (impureEffect != null) { impureEffects.push({ kind: 'Impure', - into: {...place}, + into: impureEffect.into, category: impureEffect.category, reason: impureEffect.reason, description: impureEffect.description, diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts index 179f1db7022..da56818bdd4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts @@ -397,16 +397,10 @@ function validateNoRefAccessInRenderImpl( switch (instr.value.kind) { case 'JsxExpression': case 'JsxFragment': { - for (const operand of eachInstructionValueOperand(instr.value)) { - validateNoDirectRefValueAccess(errors, operand, env); - } break; } case 'ComputedLoad': case 'PropertyLoad': { - if (instr.value.kind === 'ComputedLoad') { - validateNoDirectRefValueAccess(errors, instr.value.property, env); - } const objType = env.get(instr.value.object.identifier.id); let lookupType: null | RefAccessType = null; if (objType?.kind === 'Structure') { @@ -499,73 +493,10 @@ function validateNoRefAccessInRenderImpl( instr.value.kind === 'CallExpression' ? instr.value.callee : instr.value.property; - const hookKind = getHookKindForType(fn.env, callee.identifier.type); let returnType: RefAccessType = {kind: 'None'}; const fnType = env.get(callee.identifier.id); - let didError = false; if (fnType?.kind === 'Structure' && fnType.fn !== null) { returnType = fnType.fn.returnType; - if (fnType.fn.readRefEffect) { - didError = true; - errors.pushDiagnostic( - CompilerDiagnostic.create({ - category: ErrorCategory.Refs, - reason: 'Cannot access refs during render', - description: REF_ERROR_DESCRIPTION, - }).withDetails({ - kind: 'error', - loc: callee.loc, - message: `This function accesses a ref value`, - }), - ); - } - } - /* - * If we already reported an error on this instruction, don't report - * duplicate errors - */ - if (!didError) { - const isRefLValue = isUseRefType(instr.lvalue.identifier); - const isEventHandlerLValue = isEventHandlerType( - instr.lvalue.identifier, - ); - for (const operand of eachInstructionValueOperand(instr.value)) { - /** - * By default we check that function call operands are not refs, - * ref values, or functions that can access refs. - */ - if ( - isRefLValue || - isEventHandlerLValue || - (hookKind != null && - hookKind !== 'useState' && - hookKind !== 'useReducer') - ) { - /** - * Allow passing refs or ref-accessing functions when: - * 1. lvalue is a ref (mergeRefs pattern: `mergeRefs(ref1, ref2)`) - * 2. lvalue is an event handler (DOM events execute outside render) - * 3. calling hooks (independently validated for ref safety) - */ - validateNoDirectRefValueAccess(errors, operand, env); - } else if (interpolatedAsJsx.has(instr.lvalue.identifier.id)) { - /** - * Special case: the lvalue is passed as a jsx child - * - * For example `{renderHelper(ref)}`. Here we have more - * context and infer that the ref is being passed to a component-like - * render function which attempts to obey the rules. - */ - validateNoRefValueAccess(errors, env, operand); - } else { - validateNoRefPassedToFunction( - errors, - env, - operand, - operand.loc, - ); - } - } } env.set(instr.lvalue.identifier.id, returnType); break; @@ -574,7 +505,6 @@ function validateNoRefAccessInRenderImpl( case 'ArrayExpression': { const types: Array = []; for (const operand of eachInstructionValueOperand(instr.value)) { - validateNoDirectRefValueAccess(errors, operand, env); types.push(env.get(operand.identifier.id) ?? {kind: 'None'}); } const value = joinRefAccessTypes(...types); @@ -611,17 +541,10 @@ function validateNoRefAccessInRenderImpl( } else { validateNoRefUpdate(errors, env, instr.value.object, instr.loc); } - if ( - instr.value.kind === 'ComputedDelete' || - instr.value.kind === 'ComputedStore' - ) { - validateNoRefValueAccess(errors, env, instr.value.property); - } if ( instr.value.kind === 'ComputedStore' || instr.value.kind === 'PropertyStore' ) { - validateNoDirectRefValueAccess(errors, instr.value.value, env); const type = env.get(instr.value.value.identifier.id); if (type != null && type.kind === 'Structure') { let objectType: RefAccessType = type; @@ -662,27 +585,9 @@ function validateNoRefAccessInRenderImpl( * error for the write to the ref */ env.set(instr.lvalue.identifier.id, {kind: 'Guard', refId}); - errors.pushDiagnostic( - CompilerDiagnostic.create({ - category: ErrorCategory.Refs, - reason: 'Cannot access refs during render', - description: REF_ERROR_DESCRIPTION, - }) - .withDetails({ - kind: 'error', - loc: instr.value.value.loc, - message: `Cannot access ref value during render`, - }) - .withDetails({ - kind: 'hint', - message: - 'To initialize a ref only once, check that the ref is null with the pattern `if (ref.current == null) { ref.current = ... }`', - }), - ); break; } } - validateNoRefValueAccess(errors, env, instr.value.value); break; } case 'BinaryExpression': { @@ -704,26 +609,14 @@ function validateNoRefAccessInRenderImpl( if (refId !== null && nullish) { env.set(instr.lvalue.identifier.id, {kind: 'Guard', refId}); - } else { - for (const operand of eachInstructionValueOperand(instr.value)) { - validateNoRefValueAccess(errors, env, operand); - } } break; } default: { - for (const operand of eachInstructionValueOperand(instr.value)) { - validateNoRefValueAccess(errors, env, operand); - } break; } } - // Guard values are derived from ref.current, so they can only be used in if statement targets - for (const operand of eachInstructionOperand(instr)) { - guardCheck(errors, operand, env); - } - if ( isUseRefType(instr.lvalue.identifier) && env.get(instr.lvalue.identifier.id)?.kind !== 'Ref' @@ -761,15 +654,8 @@ function validateNoRefAccessInRenderImpl( } for (const operand of eachTerminalOperand(block.terminal)) { - if (block.terminal.kind !== 'return') { - validateNoRefValueAccess(errors, env, operand); - if (block.terminal.kind !== 'if') { - guardCheck(errors, operand, env); - } - } else { + if (block.terminal.kind === 'return') { // Allow functions containing refs to be returned, but not direct ref values - validateNoDirectRefValueAccess(errors, operand, env); - guardCheck(errors, operand, env); returnValues.push(env.get(operand.identifier.id)); } } @@ -808,72 +694,6 @@ function destructure( return type; } -function guardCheck(errors: CompilerError, operand: Place, env: Env): void { - if (env.get(operand.identifier.id)?.kind === 'Guard') { - errors.pushDiagnostic( - CompilerDiagnostic.create({ - category: ErrorCategory.Refs, - reason: 'Cannot access refs during render', - description: REF_ERROR_DESCRIPTION, - }).withDetails({ - kind: 'error', - loc: operand.loc, - message: `Cannot access ref value during render`, - }), - ); - } -} - -function validateNoRefValueAccess( - errors: CompilerError, - env: Env, - operand: Place, -): void { - const type = destructure(env.get(operand.identifier.id)); - if ( - type?.kind === 'RefValue' || - (type?.kind === 'Structure' && type.fn?.readRefEffect) - ) { - errors.pushDiagnostic( - CompilerDiagnostic.create({ - category: ErrorCategory.Refs, - reason: 'Cannot access refs during render', - description: REF_ERROR_DESCRIPTION, - }).withDetails({ - kind: 'error', - loc: (type.kind === 'RefValue' && type.loc) || operand.loc, - message: `Cannot access ref value during render`, - }), - ); - } -} - -function validateNoRefPassedToFunction( - errors: CompilerError, - env: Env, - operand: Place, - loc: SourceLocation, -): void { - const type = destructure(env.get(operand.identifier.id)); - if ( - type?.kind === 'Ref' || - type?.kind === 'RefValue' || - (type?.kind === 'Structure' && type.fn?.readRefEffect) - ) { - errors.pushDiagnostic( - CompilerDiagnostic.create({ - category: ErrorCategory.Refs, - reason: 'Cannot access refs during render', - description: REF_ERROR_DESCRIPTION, - }).withDetails({ - kind: 'error', - loc: (type.kind === 'RefValue' && type.loc) || loc, - message: `Passing a ref to a function may read its value during render`, - }), - ); - } -} - function validateNoRefUpdate( errors: CompilerError, env: Env, @@ -896,27 +716,6 @@ function validateNoRefUpdate( } } -function validateNoDirectRefValueAccess( - errors: CompilerError, - operand: Place, - env: Env, -): void { - const type = destructure(env.get(operand.identifier.id)); - if (type?.kind === 'RefValue') { - errors.pushDiagnostic( - CompilerDiagnostic.create({ - category: ErrorCategory.Refs, - reason: 'Cannot access refs during render', - description: REF_ERROR_DESCRIPTION, - }).withDetails({ - kind: 'error', - loc: type.loc ?? operand.loc, - message: `Cannot access ref value during render`, - }), - ); - } -} - export const REF_ERROR_DESCRIPTION = 'React refs are values that are not needed for rendering. Refs should only be accessed ' + 'outside of render, such as in event handlers or effects. ' + 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 cf9a6a5b4c1..263c0ea476c 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,30 +20,26 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` -Found 2 errors: +Found 1 error: -Error: Cannot access refs during render +Error: Cannot access ref value during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). -error.hook-ref-value.ts:5:23 +error.hook-ref-value.ts:5:22 3 | function Component(props) { 4 | const ref = useRef(); > 5 | useEffect(() => {}, [ref.current]); - | ^^^^^^^^^^^ Cannot access ref value during render + | ^^^^^^^^^^^^^ Ref value is used during render 6 | } 7 | 8 | export const FIXTURE_ENTRYPOINT = { -Error: Cannot access refs during render - -React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (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]); - | ^^^^^^^^^^^ Cannot access ref value during render + | ^^^^^^^^^^^ Ref is initially accessed 6 | } 7 | 8 | export const FIXTURE_ENTRYPOINT = { 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 94a9a984c2b..73b27c7b623 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 @@ -17,15 +17,23 @@ function Component(props) { ``` Found 1 error: -Error: Cannot access refs during render +Error: Cannot access ref value during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). +error.invalid-access-ref-during-render.ts:5:9 + 3 | const ref = useRef(null); + 4 | const value = ref.current; +> 5 | return value; + | ^^^^^ Ref value is used during render + 6 | } + 7 | + error.invalid-access-ref-during-render.ts:4:16 2 | function Component(props) { 3 | const ref = useRef(null); > 4 | const value = ref.current; - | ^^^^^^^^^^^ Cannot access ref value during render + | ^^^^^^^^^^^ Ref is initially accessed 5 | return value; 6 | } 7 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.expect.md index eefe34a438d..d8817dc70f6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.expect.md @@ -26,7 +26,7 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` -Found 2 errors: +Found 1 error: Error: Cannot access ref value during render @@ -41,24 +41,11 @@ error.invalid-access-ref-in-reducer-init.ts:8:4 10 | 11 | return ; -Error: Cannot access ref value during render - -React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). - -error.invalid-access-ref-in-reducer-init.ts:11:27 - 9 | ); - 10 | -> 11 | return ; - | ^^^^^ Ref value is used during render - 12 | } - 13 | - 14 | export const FIXTURE_ENTRYPOINT = { - -error.invalid-access-ref-in-reducer-init.ts:8:4 +error.invalid-access-ref-in-reducer-init.ts:8:12 6 | (state, action) => state + action, 7 | 0, > 8 | init => ref.current - | ^^^^^^^^^^^^^^^^^^^ Ref is initially accessed + | ^^^^^^^^^^^ Ref is initially accessed 9 | ); 10 | 11 | return ; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.expect.md index 2b82ba0329a..5a185beeae7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.expect.md @@ -22,7 +22,7 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` -Found 2 errors: +Found 1 error: Error: Cannot access ref value during render @@ -37,24 +37,11 @@ error.invalid-access-ref-in-reducer.ts:5:29 7 | return ; 8 | } -Error: Cannot access ref value during render - -React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). - -error.invalid-access-ref-in-reducer.ts:7:27 - 5 | const [state] = useReducer(() => ref.current, null); - 6 | -> 7 | return ; - | ^^^^^ Ref value is used during render - 8 | } - 9 | - 10 | export const FIXTURE_ENTRYPOINT = { - -error.invalid-access-ref-in-reducer.ts:5:29 +error.invalid-access-ref-in-reducer.ts:5:35 3 | function Component(props) { 4 | const ref = useRef(props.value); > 5 | const [state] = useReducer(() => ref.current, null); - | ^^^^^^^^^^^^^^^^^ Ref is initially accessed + | ^^^^^^^^^^^ Ref is initially accessed 6 | 7 | return ; 8 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.expect.md index 1e6696d4a4b..bbbd18372f8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.expect.md @@ -32,11 +32,11 @@ error.invalid-access-ref-in-render-mutate-object-with-ref-function.ts:8:15 9 | } 10 | -error.invalid-access-ref-in-render-mutate-object-with-ref-function.ts:6:15 +error.invalid-access-ref-in-render-mutate-object-with-ref-function.ts:6:21 4 | const ref = useRef(null); 5 | const object = {}; > 6 | object.foo = () => ref.current; - | ^^^^^^^^^^^^^^^^^ Ref is initially accessed + | ^^^^^^^^^^^ Ref is initially accessed 7 | const refValue = object.foo(); 8 | return
{refValue}
; 9 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.expect.md index 024c098ccad..a3d750f44ee 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.expect.md @@ -22,7 +22,7 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` -Found 2 errors: +Found 1 error: Error: Cannot access ref value during render @@ -37,24 +37,11 @@ error.invalid-access-ref-in-state-initializer.ts:5:27 7 | return ; 8 | } -Error: Cannot access ref value during render - -React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). - -error.invalid-access-ref-in-state-initializer.ts:7:27 - 5 | const [state] = useState(() => ref.current); - 6 | -> 7 | return ; - | ^^^^^ Ref value is used during render - 8 | } - 9 | - 10 | export const FIXTURE_ENTRYPOINT = { - -error.invalid-access-ref-in-state-initializer.ts:5:27 +error.invalid-access-ref-in-state-initializer.ts:5:33 3 | function Component(props) { 4 | const ref = useRef(props.value); > 5 | const [state] = useState(() => ref.current); - | ^^^^^^^^^^^^^^^^^ Ref is initially accessed + | ^^^^^^^^^^^ Ref is initially accessed 6 | 7 | return ; 8 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-capturing-ref-returning-function-in-rendered-object.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-capturing-ref-returning-function-in-rendered-object.expect.md index e23dc709983..1d6ec9e4c0e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-capturing-ref-returning-function-in-rendered-object.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-capturing-ref-returning-function-in-rendered-object.expect.md @@ -38,11 +38,11 @@ error.invalid-capturing-ref-returning-function-in-rendered-object.ts:8:28 10 | 11 | export const FIXTURE_ENTRYPOINT = { -error.invalid-capturing-ref-returning-function-in-rendered-object.ts:7:15 +error.invalid-capturing-ref-returning-function-in-rendered-object.ts:7:21 5 | const ref = useRef(props.value); 6 | const object = {}; > 7 | object.foo = () => ref.current; - | ^^^^^^^^^^^^^^^^^ Ref is initially accessed + | ^^^^^^^^^^^ Ref is initially accessed 8 | return ; 9 | } 10 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect-via-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect-via-mutation.expect.md index af3ebad63d6..d68a7b6fac7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect-via-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect-via-mutation.expect.md @@ -34,11 +34,11 @@ error.invalid-impure-functions-in-render-indirect-via-mutation.ts:10:23 11 | } 12 | -error.invalid-impure-functions-in-render-indirect-via-mutation.ts:6:18 +error.invalid-impure-functions-in-render-indirect-via-mutation.ts:6:24 4 | 5 | function Component() { > 6 | const getDate = () => Date.now(); - | ^^^^^^^^^^^^^^^^ `Date.now` is an impure function. + | ^^^^^^^^^^ `Date.now` is an impure function. 7 | const now = getDate(); 8 | const array = []; 9 | arrayPush(array, now); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect.expect.md index 1f930a582ec..fced0f6281c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect.expect.md @@ -33,11 +33,11 @@ error.invalid-impure-functions-in-render-indirect.ts:9:23 10 | } 11 | -error.invalid-impure-functions-in-render-indirect.ts:6:18 +error.invalid-impure-functions-in-render-indirect.ts:6:24 4 | 5 | function Component() { > 6 | const getDate = () => Date.now(); - | ^^^^^^^^^^^^^^^^ `Date.now` is an impure function. + | ^^^^^^^^^^ `Date.now` is an impure function. 7 | const array = makeArray(getDate()); 8 | const hasDate = identity(array); 9 | return ; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call-2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call-2.expect.md index 895e34ed432..8aaaa839ecf 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call-2.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call-2.expect.md @@ -39,14 +39,14 @@ error.invalid-impure-functions-in-render-via-function-call-2.ts:15:23 16 | } 17 | -error.invalid-impure-functions-in-render-via-function-call-2.ts:10:28 - 8 | // this should error but we currently lose track of the impurity bc - 9 | // the impure value comes from behind a call -> 10 | const array = makeArray(now()); - | ^^^ `Date.now` is an impure function. - 11 | const hasDate = identity(array); - 12 | return hasDate; - 13 | }; +error.invalid-impure-functions-in-render-via-function-call-2.ts:6:20 + 4 | + 5 | function Component() { +> 6 | const now = () => Date.now(); + | ^^^^^^^^^^ `Date.now` is an impure function. + 7 | const f = () => { + 8 | // this should error but we currently lose track of the impurity bc + 9 | // the impure value comes from behind a call ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call.expect.md index 76c2cd9098b..29d379238c0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call.expect.md @@ -37,14 +37,14 @@ error.invalid-impure-functions-in-render-via-function-call.ts:13:23 14 | } 15 | -error.invalid-impure-functions-in-render-via-function-call.ts:8:28 - 6 | const now = Date.now(); - 7 | const f = () => { -> 8 | const array = makeArray(now); - | ^^^ `Date.now` is an impure function. - 9 | const hasDate = identity(array); - 10 | return hasDate; - 11 | }; +error.invalid-impure-functions-in-render-via-function-call.ts:6:14 + 4 | + 5 | function Component() { +> 6 | const now = Date.now(); + | ^^^^^^^^^^ `Date.now` is an impure function. + 7 | const f = () => { + 8 | const array = makeArray(now); + 9 | const hasDate = identity(array); ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-render-helper-typed.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-render-helper-typed.expect.md new file mode 100644 index 00000000000..6e96e63cc4f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-render-helper-typed.expect.md @@ -0,0 +1,50 @@ + +## Input + +```javascript +// @validateNoImpureFunctionsInRender + +import {typedArrayPush, typedIdentity} from 'shared-runtime'; + +function Component() { + const now = Date.now(); + const renderItem = () => { + const array = []; + typedArrayPush(array, now()); + const hasDate = typedIdentity(array); + return ; + }; + return ; +} + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot access impure value during render + +Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). + +error.invalid-impure-functions-in-render-via-render-helper-typed.ts:13:26 + 11 | return ; + 12 | }; +> 13 | return ; + | ^^^^^^^^^^ Cannot access impure value during render + 14 | } + 15 | + +error.invalid-impure-functions-in-render-via-render-helper-typed.ts:6:14 + 4 | + 5 | function Component() { +> 6 | const now = Date.now(); + | ^^^^^^^^^^ `Date.now` is an impure function. + 7 | const renderItem = () => { + 8 | const array = []; + 9 | typedArrayPush(array, now()); +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-render-helper-typed.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-render-helper-typed.js new file mode 100644 index 00000000000..11f14f1bb81 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-render-helper-typed.js @@ -0,0 +1,14 @@ +// @validateNoImpureFunctionsInRender + +import {typedArrayPush, typedIdentity} from 'shared-runtime'; + +function Component() { + const now = Date.now(); + const renderItem = () => { + const array = []; + typedArrayPush(array, now()); + const hasDate = typedIdentity(array); + return ; + }; + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-render-helper.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-render-helper.expect.md index ffed6129747..900437eccd5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-render-helper.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-render-helper.expect.md @@ -22,29 +22,7 @@ function Component() { ## Error ``` -Found 2 errors: - -Error: Cannot access impure value during render - -Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). - -error.invalid-impure-functions-in-render-via-render-helper.ts:10:25 - 8 | const array = makeArray(now); - 9 | const hasDate = identity(array); -> 10 | return ; - | ^^^^^^^ Cannot access impure value during render - 11 | }; - 12 | return ; - 13 | } - -error.invalid-impure-functions-in-render-via-render-helper.ts:6:14 - 4 | - 5 | function Component() { -> 6 | const now = Date.now(); - | ^^^^^^^^^^ `Date.now` is an impure function. - 7 | const renderItem = () => { - 8 | const array = makeArray(now); - 9 | const hasDate = identity(array); +Found 1 error: Error: Cannot access impure value during render diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-value-in-render-helper.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-value-in-render-helper.expect.md index db33136aea2..27e74c33398 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-value-in-render-helper.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-value-in-render-helper.expect.md @@ -17,7 +17,7 @@ function Component() { ## Error ``` -Found 2 errors: +Found 1 error: Error: Cannot access impure value during render @@ -32,32 +32,11 @@ error.invalid-impure-value-in-render-helper.ts:5:17 7 | return
{render()}
; 8 | } -error.invalid-impure-value-in-render-helper.ts:3:14 +error.invalid-impure-value-in-render-helper.ts:3:20 1 | // @validateNoImpureFunctionsInRender 2 | function Component() { > 3 | const now = () => Date.now(); - | ^^^^^^^^^^^^^^^^ `Date.now` is an impure function. - 4 | const render = () => { - 5 | return
{now()}
; - 6 | }; - -Error: Cannot access impure value during render - -Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). - -error.invalid-impure-value-in-render-helper.ts:7:15 - 5 | return
{now()}
; - 6 | }; -> 7 | return
{render()}
; - | ^^^^^^^^ Cannot access impure value during render - 8 | } - 9 | - -error.invalid-impure-value-in-render-helper.ts:3:14 - 1 | // @validateNoImpureFunctionsInRender - 2 | function Component() { -> 3 | const now = () => Date.now(); - | ^^^^^^^^^^^^^^^^ `Date.now` is an impure function. + | ^^^^^^^^^^ `Date.now` is an impure function. 4 | const render = () => { 5 | return
{now()}
; 6 | }; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-set-and-read-ref-during-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-set-and-read-ref-during-render.expect.md index 387dff27bf0..73824914072 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-set-and-read-ref-during-render.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-set-and-read-ref-during-render.expect.md @@ -15,22 +15,9 @@ function Component(props) { ## Error ``` -Found 2 errors: +Found 1 error: -Error: Cannot access refs during render - -React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). - -error.invalid-set-and-read-ref-during-render.ts:4:2 - 2 | function Component(props) { - 3 | const ref = useRef(null); -> 4 | ref.current = props.value; - | ^^^^^^^^^^^ Cannot update ref during render - 5 | return ref.current; - 6 | } - 7 | - -Error: Cannot access refs during render +Error: Cannot access ref value during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). @@ -38,7 +25,7 @@ error.invalid-set-and-read-ref-during-render.ts:5:9 3 | const ref = useRef(null); 4 | ref.current = props.value; > 5 | return ref.current; - | ^^^^^^^^^^^ Cannot access ref value during render + | ^^^^^^^^^^^ Ref value is used during render 6 | } 7 | ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-set-and-read-ref-nested-property-during-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-set-and-read-ref-nested-property-during-render.expect.md index 8ef0e223a86..3b490c7aef9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-set-and-read-ref-nested-property-during-render.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-set-and-read-ref-nested-property-during-render.expect.md @@ -15,30 +15,25 @@ function Component(props) { ## Error ``` -Found 2 errors: +Found 1 error: -Error: Cannot access refs during render +Error: Cannot access ref value during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). -error.invalid-set-and-read-ref-nested-property-during-render.ts:4:2 - 2 | function Component(props) { +error.invalid-set-and-read-ref-nested-property-during-render.ts:5:9 3 | const ref = useRef({inner: null}); -> 4 | ref.current.inner = props.value; - | ^^^^^^^^^^^ Cannot update ref during render - 5 | return ref.current.inner; + 4 | ref.current.inner = props.value; +> 5 | return ref.current.inner; + | ^^^^^^^^^^^^^^^^^ Ref value is used during render 6 | } 7 | -Error: Cannot access refs during render - -React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). - error.invalid-set-and-read-ref-nested-property-during-render.ts:5:9 3 | const ref = useRef({inner: null}); 4 | ref.current.inner = props.value; > 5 | return ref.current.inner; - | ^^^^^^^^^^^^^^^^^ Cannot access ref value during render + | ^^^^^^^^^^^ Ref is initially accessed 6 | } 7 | ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-arbitrary.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-arbitrary.expect.md index 17625298cd7..4f3f14ec533 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-arbitrary.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-arbitrary.expect.md @@ -25,19 +25,7 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` -Found 2 errors: - -Error: Cannot access refs during render - -React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). - - 6 | component C() { - 7 | const r = useRef(DEFAULT_VALUE); -> 8 | if (r.current == DEFAULT_VALUE) { - | ^^^^^^^^^ Cannot access ref value during render - 9 | r.current = 1; - 10 | } - 11 | } +Found 1 error: Error: Cannot access refs during render diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-optional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-optional.expect.md index 94e4b0fc46f..1d4309577e9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-optional.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-optional.expect.md @@ -22,7 +22,7 @@ export const FIXTURE_ENTRYPOINT = { ``` Found 1 error: -Error: Cannot access refs during render +Error: Cannot access ref value during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). @@ -30,7 +30,7 @@ error.ref-optional.ts:5:9 3 | function Component(props) { 4 | const ref = useRef(); > 5 | return ref?.current; - | ^^^^^^^^^^^^ Cannot access ref value during render + | ^^^^^^^^^^^^ Ref value is used during render 6 | } 7 | 8 | export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/return-ref-callback.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/return-ref-callback.expect.md index ed1dfa39ea5..b5b58ab04c6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/return-ref-callback.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/return-ref-callback.expect.md @@ -6,7 +6,7 @@ import {useRef} from 'react'; -component Foo() { +hook useFoo() { const ref = useRef(); const s = () => { @@ -16,6 +16,10 @@ component Foo() { return s; } +component Foo() { + useFoo(); +} + export const FIXTURE_ENTRYPOINT = { fn: Foo, params: [], @@ -30,7 +34,7 @@ import { c as _c } from "react/compiler-runtime"; import { useRef } from "react"; -function Foo() { +function useFoo() { const $ = _c(1); const ref = useRef(); let t0; @@ -44,6 +48,10 @@ function Foo() { return s; } +function Foo() { + useFoo(); +} + export const FIXTURE_ENTRYPOINT = { fn: Foo, params: [], @@ -52,4 +60,4 @@ export const FIXTURE_ENTRYPOINT = { ``` ### Eval output -(kind: ok) "[[ function params=0 ]]" \ No newline at end of file +(kind: ok) \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/return-ref-callback.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/return-ref-callback.js index f1a45ebc4ff..502e7f42fcb 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/return-ref-callback.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/return-ref-callback.js @@ -2,7 +2,7 @@ import {useRef} from 'react'; -component Foo() { +hook useFoo() { const ref = useRef(); const s = () => { @@ -12,6 +12,10 @@ component Foo() { return s; } +component Foo() { + useFoo(); +} + export const FIXTURE_ENTRYPOINT = { fn: Foo, params: [], diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/bailout-validate-ref-current-access.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/bailout-validate-ref-current-access.expect.md index b55526e9211..5e0efa3aa45 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/bailout-validate-ref-current-access.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/bailout-validate-ref-current-access.expect.md @@ -25,22 +25,40 @@ component Component(prop1, ref) { ## Code ```javascript -import { useFire } from "react/compiler-runtime"; +import { c as _c, useFire } from "react/compiler-runtime"; import { fire } from "react"; import { print } from "shared-runtime"; const Component = React.forwardRef(Component_withRef); function Component_withRef(t0, ref) { + const $ = _c(5); const { prop1 } = t0; - const foo = () => { - console.log(prop1); - }; - const t1 = useFire(foo); - useEffect(() => { - t1(prop1); - bar(); - t1(); - }); + let t1; + if ($[0] !== prop1) { + t1 = () => { + console.log(prop1); + }; + $[0] = prop1; + $[1] = t1; + } else { + t1 = $[1]; + } + const foo = t1; + const t2 = useFire(foo); + let t3; + if ($[2] !== prop1 || $[3] !== t2) { + t3 = () => { + t2(prop1); + bar(); + t2(); + }; + $[2] = prop1; + $[3] = t2; + $[4] = t3; + } else { + t3 = $[4]; + } + useEffect(t3); print(ref.current); return null; } From 7505808cd437ed47f6c52def4cbff562bc5a02b8 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Tue, 13 Jan 2026 18:57:40 -0800 Subject: [PATCH 4/8] [compiler] Claude file/settings --- compiler/.claude/settings.local.json | 7 +- compiler/CLAUDE.md | 245 +++++++++++++++++++++++++++ 2 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 compiler/CLAUDE.md diff --git a/compiler/.claude/settings.local.json b/compiler/.claude/settings.local.json index 9bbd18802e4..8571b9558f3 100644 --- a/compiler/.claude/settings.local.json +++ b/compiler/.claude/settings.local.json @@ -1,7 +1,12 @@ { "permissions": { "allow": [ - "Bash(node scripts/enable-feature-flag.js:*)" + "Bash(node scripts/enable-feature-flag.js:*)", + "Bash(yarn snap:*)", + "Bash(for test in \"error.invalid-access-ref-during-render\" \"error.invalid-ref-in-callback-invoked-during-render\" \"error.invalid-impure-functions-in-render-via-render-helper\")", + "Bash(do)", + "Bash(echo:*)", + "Bash(done)" ], "deny": [], "ask": [] diff --git a/compiler/CLAUDE.md b/compiler/CLAUDE.md new file mode 100644 index 00000000000..7a25725c659 --- /dev/null +++ b/compiler/CLAUDE.md @@ -0,0 +1,245 @@ +# React Compiler Knowledge Base + +This document contains knowledge about the React Compiler gathered during development sessions. It serves as a reference for understanding the codebase architecture and key concepts. + +## Project Structure + +- `packages/babel-plugin-react-compiler/` - Main compiler package + - `src/HIR/` - High-level Intermediate Representation types and utilities + - `src/Inference/` - Effect inference passes (aliasing, mutation, etc.) + - `src/Validation/` - Validation passes that check for errors + - `src/Entrypoint/Pipeline.ts` - Main compilation pipeline with pass ordering + - `src/__tests__/fixtures/compiler/` - Test fixtures + - `error.*.js` - Fixtures that should produce compilation errors + - `*.expect.md` - Expected output for each fixture + +## Running Tests + +```bash +# Run all tests +yarn snap + +# Run specific test by pattern +yarn snap -p + +# Update fixture outputs +yarn snap -u +``` + +## Version Control + +This repository uses Sapling (`sl`) for version control. Unlike git, Sapling does not require explicitly adding files to the staging area. + +```bash +# Check status +sl status + +# Commit all changes +sl commit -m "Your commit message" + +# Commit with multi-line message using heredoc +sl commit -m "$(cat <<'EOF' +Summary line + +Detailed description here +EOF +)" +``` + +## Key Concepts + +### HIR (High-level Intermediate Representation) + +The compiler converts source code to HIR for analysis. Key types in `src/HIR/HIR.ts`: + +- **HIRFunction** - A function being compiled + - `body.blocks` - Map of BasicBlocks + - `context` - Captured variables from outer scope + - `params` - Function parameters + - `returns` - The function's return place + - `aliasingEffects` - Effects that describe the function's behavior when called + +- **Instruction** - A single operation + - `lvalue` - The place being assigned to + - `value` - The instruction kind (CallExpression, FunctionExpression, LoadLocal, etc.) + - `effects` - Array of AliasingEffects for this instruction + +- **Terminal** - Block terminators (return, branch, etc.) + - `effects` - Array of AliasingEffects + +- **Place** - A reference to a value + - `identifier.id` - Unique IdentifierId + +- **Phi nodes** - Join points for values from different control flow paths + - Located at `block.phis` + - `phi.place` - The result place + - `phi.operands` - Map of predecessor block to source place + +### AliasingEffects System + +Effects describe data flow and operations. Defined in `src/Inference/AliasingEffects.ts`: + +**Data Flow Effects:** +- `Impure` - Marks a place as containing an impure value (e.g., Date.now() result, ref.current) +- `Capture a -> b` - Value from `a` is captured into `b` (mutable capture) +- `Alias a -> b` - `b` aliases `a` +- `ImmutableCapture a -> b` - Immutable capture (like Capture but read-only) +- `Assign a -> b` - Direct assignment +- `MaybeAlias a -> b` - Possible aliasing +- `CreateFrom a -> b` - Created from source + +**Mutation Effects:** +- `Mutate value` - Value is mutated +- `MutateTransitive value` - Value and transitive captures are mutated +- `MutateConditionally value` - May mutate +- `MutateTransitiveConditionally value` - May mutate transitively + +**Other Effects:** +- `Render place` - Place is used in render context (JSX props, component return) +- `Freeze place` - Place is frozen (made immutable) +- `Create place` - New value created +- `CreateFunction` - Function expression created, includes `captures` array +- `Apply` - Function application with receiver, function, args, and result + +### Hook Aliasing Signatures + +Located in `src/HIR/Globals.ts`, hooks can define custom aliasing signatures to control how data flows through them. + +**Structure:** +```typescript +aliasing: { + receiver: '@receiver', // The hook function itself + params: ['@param0'], // Named positional parameters + rest: '@rest', // Rest parameters (or null) + returns: '@returns', // Return value + temporaries: [], // Temporary values during execution + effects: [ // Array of effects to apply when hook is called + {kind: 'Freeze', value: '@param0', reason: ValueReason.HookCaptured}, + {kind: 'Assign', from: '@param0', into: '@returns'}, + ], +} +``` + +**Common patterns:** + +1. **RenderHookAliasing** (useState, useContext, useMemo, useCallback): + - Freezes arguments (`Freeze @rest`) + - Marks arguments as render-time (`Render @rest`) + - Creates frozen return value + - Aliases arguments to return + +2. **EffectHookAliasing** (useEffect, useLayoutEffect, useInsertionEffect): + - Freezes function and deps + - Creates internal effect object + - Captures function and deps into effect + - Returns undefined + +3. **Event handler hooks** (useEffectEvent): + - Freezes callback (`Freeze @fn`) + - Aliases input to return (`Assign @fn -> @returns`) + - NO Render effect (callback not called during render) + +**Example: useEffectEvent** +```typescript +const UseEffectEventHook = addHook( + DEFAULT_SHAPES, + { + positionalParams: [Effect.Freeze], // Takes one positional param + restParam: null, + returnType: {kind: 'Function', ...}, + calleeEffect: Effect.Read, + hookKind: 'useEffectEvent', + returnValueKind: ValueKind.Frozen, + aliasing: { + receiver: '@receiver', + params: ['@fn'], // Name for the callback parameter + rest: null, + returns: '@returns', + temporaries: [], + effects: [ + {kind: 'Freeze', value: '@fn', reason: ValueReason.HookCaptured}, + {kind: 'Assign', from: '@fn', into: '@returns'}, + // Note: NO Render effect - callback is not called during render + ], + }, + }, + BuiltInUseEffectEventId, +); + +// Add as both names for compatibility +['useEffectEvent', UseEffectEventHook], +['experimental_useEffectEvent', UseEffectEventHook], +``` + +**Key insight:** If a hook is missing an `aliasing` config, it falls back to `DefaultNonmutatingHook` which includes a `Render` effect on all arguments. This can cause false positives for hooks like `useEffectEvent` whose callbacks are not called during render. + +### Effect Inference Pipeline + +Effects are populated by `InferMutationAliasingEffects` (runs before validation): + +1. For `Date.now()`, `Math.random()` etc. - adds `Impure` effect (controlled by `validateNoImpureFunctionsInRender` config) +2. For `ref.current` access - adds `Impure` effect (controlled by `validateRefAccessDuringRender` config) +3. For return terminals - adds `Alias` from return value to `fn.returns` +4. For component/JSX returns - adds `Render` effect +5. For function expressions - adds `CreateFunction` effect with captures + +### Validation: validateNoImpureValuesInRender + +Located at `src/Validation/ValidateNoImpureValuesInRender.ts` + +**Purpose:** Detect when impure values (refs, Date.now results, etc.) flow into render context. + +**Algorithm:** +1. Track impure values in a Map +2. Track functions with impure returns separately (they're not impure values themselves) +3. Fixed-point iteration over all blocks: + - Process phi nodes (any impure operand makes result impure) + - Process instruction effects + - Process terminal effects + - Backwards propagation for mutated LoadLocal values +4. Validate: check all Render effects against impure values + +**Key patterns:** +- `Impure` effect marks the target as impure +- `Capture/Alias/etc` propagates impurity from source to target +- `Apply` propagates impurity from args/receiver to result +- `CreateFunction` propagates impurity from captured values (but NOT from body effects) +- If a value has both `Render` and `Capture` in same instruction, only error on Render (don't cascade) + +**Tracking functions with impure returns:** +- Separate from the `impure` map (function values aren't impure, just their returns) +- Populated when analyzing FunctionExpression bodies +- Used when: + 1. Calling the function - mark call result as impure + 2. Capturing the function - mark target as impure (for object.foo = impureFunc cases) + +**Backwards propagation:** +- When `$x = LoadLocal y` and `$x` is mutated with impure content, mark `y` as impure +- This handles: `const arr = []; arr.push(impure); render(arr)` + +## Known Issues / Edge Cases + +### Function Outlining +After `OutlineFunctions` pass, inner functions are replaced with `LoadGlobal(_temp)`. The validation runs BEFORE outlining, so it sees the original FunctionExpression. But be aware that test output shows post-outlining HIR. + +### SSA and LoadLocal +In SSA form, each `LoadLocal` creates a new identifier. When a loaded value is mutated: +- `$x = LoadLocal y` +- `mutate($x, impure)` +- `$z = LoadLocal y` (different from $x!) +- `render($z)` + +The impurity in $x must propagate back to y, then forward to $z. This requires backwards propagation in the fixed-point loop. + +## Configuration Flags + +In `Environment.ts` / test directives: +- `validateNoImpureFunctionsInRender` - Enable impure function validation (Date.now, Math.random, etc.) +- `validateRefAccessDuringRender` - Enable ref access validation + +## Debugging Tips + +1. Run `yarn snap -p ` to see full HIR output with effects +2. Look for `@aliasingEffects=` on FunctionExpressions +3. Look for `Impure`, `Render`, `Capture` effects on instructions +4. Check the pass ordering in Pipeline.ts to understand when effects are populated vs validated From cb021861a5ba9ae9d235b9e73114b7bbd2872d7c Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 14 Jan 2026 13:10:49 -0800 Subject: [PATCH 5/8] [compiler] Simplify ref mutation validation to single forward pass Rewrites ValidateNoRefAccessInRender to use a simpler single-pass algorithm that only validates ref mutations (reads are now handled separately by ValidateNoImpureValuesInRender). The new approach: - Tracks refs and ref values through the function - Identifies functions that mutate refs (directly or transitively) - Only errors when ref-mutating functions are called at the top level - Supports null-guard exception: mutations inside `if (ref.current == null)` are allowed for the initialization pattern This reduces ~700 lines of complex fixpoint iteration to ~400 lines of straightforward forward data-flow analysis. --- compiler/.claude/settings.local.json | 4 +- .../Validation/ValidateNoRefAccessInRender.ts | 962 ++++++------------ .../error.capture-ref-for-mutation.expect.md | 31 +- .../compiler/error.hook-ref-value.expect.md | 2 +- ...invalid-access-ref-during-render.expect.md | 2 +- ...-callback-invoked-during-render-.expect.md | 2 +- ...rrent-inferred-ref-during-render.expect.md | 8 +- ...-disallow-mutating-ref-in-render.expect.md | 8 +- ...tating-refs-in-render-transitive.expect.md | 26 +- ...d-ref-prop-in-render-destructure.expect.md | 2 +- ...ref-prop-in-render-property-load.expect.md | 2 +- ....invalid-ref-access-render-unary.expect.md | 2 +- ...n-callback-invoked-during-render.expect.md | 2 +- ...lid-ref-initialization-unary-not.expect.md | 20 +- ...error.invalid-ref-value-as-props.expect.md | 2 +- ...d-set-and-read-ref-during-render.expect.md | 2 +- ...ef-nested-property-during-render.expect.md | 2 +- ...rite-but-dont-read-ref-in-render.expect.md | 8 +- ...invalid-write-ref-prop-in-render.expect.md | 8 +- ...ror.ref-initialization-arbitrary.expect.md | 8 +- .../error.ref-initialization-linear.expect.md | 24 +- .../error.ref-initialization-nonif.expect.md | 28 +- .../error.ref-initialization-other.expect.md | 8 +- ...ref-initialization-post-access-2.expect.md | 16 +- ...r.ref-initialization-post-access.expect.md | 8 +- .../compiler/error.ref-optional.expect.md | 2 +- ...ified-later-preserve-memoization.expect.md | 6 +- ...ia-function-preserve-memoization.expect.md | 16 +- ...operty-dont-preserve-memoization.expect.md | 6 +- ...utate-after-useeffect-ref-access.expect.md | 2 +- ...utate-after-useeffect-ref-access.expect.md | 2 +- 31 files changed, 471 insertions(+), 750 deletions(-) diff --git a/compiler/.claude/settings.local.json b/compiler/.claude/settings.local.json index 8571b9558f3..cb3a55acfac 100644 --- a/compiler/.claude/settings.local.json +++ b/compiler/.claude/settings.local.json @@ -6,7 +6,9 @@ "Bash(for test in \"error.invalid-access-ref-during-render\" \"error.invalid-ref-in-callback-invoked-during-render\" \"error.invalid-impure-functions-in-render-via-render-helper\")", "Bash(do)", "Bash(echo:*)", - "Bash(done)" + "Bash(done)", + "Bash(cat:*)", + "Bash(sl revert:*)" ], "deny": [], "ask": [] diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts index da56818bdd4..f49955c4793 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts @@ -5,719 +5,425 @@ * LICENSE file in the root directory of this source tree. */ -import { - CompilerDiagnostic, - CompilerError, - ErrorCategory, -} from '../CompilerError'; +import {CompilerDiagnostic, CompilerError, ErrorCategory} from '../CompilerError'; import { BlockId, HIRFunction, IdentifierId, - Identifier, + Instruction, Place, - SourceLocation, - getHookKindForType, isRefValueType, isUseRefType, } from '../HIR'; -import {BuiltInEventHandlerId} from '../HIR/ObjectShape'; -import { - eachInstructionOperand, - eachInstructionValueOperand, - eachPatternOperand, - eachTerminalOperand, -} from '../HIR/visitors'; +import {eachTerminalSuccessor} from '../HIR/visitors'; import {Err, Ok, Result} from '../Utils/Result'; -import {retainWhere} from '../Utils/utils'; /** - * Validates that a function does not access a ref value during render. This includes a partial check - * for ref values which are accessed indirectly via function expressions. + * Validates that a function does not mutate a ref value during render. + * Reading refs is handled by a separate validation pass. + * + * Mutation is allowed in: + * - Event handlers and effect callbacks (functions not called at top level) + * - Inside `if (ref.current == null)` blocks (null-guard initialization pattern) * * ```javascript - * // ERROR - * const ref = useRef(); - * ref.current; + * // ERROR - direct mutation in render + * const ref = useRef(null); + * ref.current = value; * - * const ref = useRef(); - * foo(ref); // may access .current + * // ERROR - mutation in function called during render + * const fn = () => { ref.current = value; }; + * fn(); // fn is called, so mutation errors * - * // ALLOWED - * const ref = useHookThatReturnsRef(); - * ref.current; - * ``` + * // ALLOWED - mutation in event handler + * const onClick = () => { ref.current = value; }; + * return - - - ); -} - -export const FIXTURE_ENTRYPOINT = { - fn: Component, - params: [{}], -}; - -``` - - -## Error - -``` -Found 1 error: - -Error: Cannot access refs during render - -React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). - -error.ref-value-in-custom-component-event-handler-wrapper.ts:31:41 - 29 | <> - 30 | -> 31 | - | ^^^^^^^^ Passing a ref to a function may read its value during render - 32 | - 33 | - 34 | -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-event-handler-wrapper.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-event-handler-wrapper.expect.md deleted file mode 100644 index 85fd1764ef2..00000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-event-handler-wrapper.expect.md +++ /dev/null @@ -1,64 +0,0 @@ - -## Input - -```javascript -// @enableInferEventHandlers -import {useRef} from 'react'; - -// Simulates a handler wrapper -function handleClick(value: any) { - return () => { - console.log(value); - }; -} - -function Component() { - const ref = useRef(null); - - // This should still error: passing ref.current directly to a wrapper - // The ref value is accessed during render, not in the event handler - return ( - <> - - - - ); -} - -export const FIXTURE_ENTRYPOINT = { - fn: Component, - params: [{}], -}; - -``` - - -## Error - -``` -Found 1 error: - -Error: Cannot access ref value during render - -React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). - -error.ref-value-in-event-handler-wrapper.ts:19:6 - 17 | <> - 18 | -> 19 | - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Ref value is used during render - 20 | - 21 | ); - 22 | } - -error.ref-value-in-event-handler-wrapper.ts:19:35 - 17 | <> - 18 | -> 19 | - | ^^^^^^^^^^^ Ref is initially accessed - 20 | - 21 | ); - 22 | } -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.expect.md index a0c492120a3..e063342e800 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.expect.md @@ -41,7 +41,7 @@ error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoi 12 | 13 | // The ref is modified later, extending its range and preventing memoization of onChange > 14 | ref.current.inner = null; - | ^^^^^^^^^^^ Cannot update ref during render + | ^^^^^^^^^^^^^^^^^ Cannot update ref during render 15 | 16 | return ; 17 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md index bab8aa60f00..9c7fec3b428 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md @@ -44,7 +44,7 @@ error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.t 13 | // The ref is modified later, extending its range and preventing memoization of onChange 14 | const reset = () => { > 15 | ref.current.inner = null; - | ^^^^^^^^^^^ Cannot update ref during render + | ^^^^^^^^^^^^^^^^^ Cannot update ref during render 16 | }; 17 | reset(); 18 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-set-ref-nested-property-dont-preserve-memoization.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-set-ref-nested-property-dont-preserve-memoization.expect.md index b40b0bbf226..3a9f61b35ae 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-set-ref-nested-property-dont-preserve-memoization.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-set-ref-nested-property-dont-preserve-memoization.expect.md @@ -40,7 +40,7 @@ error.useCallback-set-ref-nested-property-dont-preserve-memoization.ts:13:2 11 | }); 12 | > 13 | ref.current.inner = null; - | ^^^^^^^^^^^ Cannot update ref during render + | ^^^^^^^^^^^^^^^^^ Cannot update ref during render 14 | 15 | return ; 16 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.validate-mutate-ref-arg-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.validate-mutate-ref-arg-in-render.expect.md deleted file mode 100644 index 1d5a4a2284c..00000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.validate-mutate-ref-arg-in-render.expect.md +++ /dev/null @@ -1,39 +0,0 @@ - -## Input - -```javascript -// @validateRefAccessDuringRender:true -function Foo(props, ref) { - console.log(ref.current); - return
{props.bar}
; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Foo, - params: [{bar: 'foo'}, {ref: {cuurrent: 1}}], - isComponent: true, -}; - -``` - - -## Error - -``` -Found 1 error: - -Error: Cannot access refs during render - -React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). - -error.validate-mutate-ref-arg-in-render.ts:3:14 - 1 | // @validateRefAccessDuringRender:true - 2 | function Foo(props, ref) { -> 3 | console.log(ref.current); - | ^^^^^^^^^^^ Passing a ref to a function may read its value during render - 4 | return
{props.bar}
; - 5 | } - 6 | -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/impure-functions-in-render-indirect-via-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/impure-functions-in-render-indirect-via-mutation.expect.md new file mode 100644 index 00000000000..5e5f07256da --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/impure-functions-in-render-indirect-via-mutation.expect.md @@ -0,0 +1,58 @@ + +## Input + +```javascript +// @validateNoImpureFunctionsInRender + +import {arrayPush, identity, makeArray} from 'shared-runtime'; + +/** + * Allowed: we don't have sufficient type information to be sure that + * this accesses an impure value during render. The impurity is lost + * when passed through external function calls. + */ +function Component() { + const getDate = () => Date.now(); + const now = getDate(); + const array = []; + arrayPush(array, now); + return ; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateNoImpureFunctionsInRender + +import { arrayPush, identity, makeArray } from "shared-runtime"; + +/** + * Allowed: we don't have sufficient type information to be sure that + * this accesses an impure value during render. The impurity is lost + * when passed through external function calls. + */ +function Component() { + const $ = _c(1); + const getDate = _temp; + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + const now = getDate(); + const array = []; + arrayPush(array, now); + t0 = ; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} +function _temp() { + return Date.now(); +} + +``` + +### 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/error.invalid-impure-functions-in-render-indirect-via-mutation.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/impure-functions-in-render-indirect-via-mutation.js similarity index 57% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect-via-mutation.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/impure-functions-in-render-indirect-via-mutation.js index 18222d860e6..e38bf3e9b63 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect-via-mutation.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/impure-functions-in-render-indirect-via-mutation.js @@ -2,6 +2,11 @@ import {arrayPush, identity, makeArray} from 'shared-runtime'; +/** + * Allowed: we don't have sufficient type information to be sure that + * this accesses an impure value during render. The impurity is lost + * when passed through external function calls. + */ function Component() { const getDate = () => Date.now(); const now = getDate(); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/impure-functions-in-render-indirect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/impure-functions-in-render-indirect.expect.md new file mode 100644 index 00000000000..4718ee7f203 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/impure-functions-in-render-indirect.expect.md @@ -0,0 +1,56 @@ + +## Input + +```javascript +// @validateNoImpureFunctionsInRender + +import {identity, makeArray} from 'shared-runtime'; + +/** + * Allowed: we don't have sufficient type information to be sure that + * this accesses an impure value during render. The impurity is lost + * when passed through external function calls. + */ +function Component() { + const getDate = () => Date.now(); + const array = makeArray(getDate()); + const hasDate = identity(array); + return ; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateNoImpureFunctionsInRender + +import { identity, makeArray } from "shared-runtime"; + +/** + * Allowed: we don't have sufficient type information to be sure that + * this accesses an impure value during render. The impurity is lost + * when passed through external function calls. + */ +function Component() { + const $ = _c(1); + const getDate = _temp; + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + const array = makeArray(getDate()); + const hasDate = identity(array); + t0 = ; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} +function _temp() { + return Date.now(); +} + +``` + +### 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/error.invalid-impure-functions-in-render-indirect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/impure-functions-in-render-indirect.js similarity index 57% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/impure-functions-in-render-indirect.js index 4cf0e46d9d8..285838c7981 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/impure-functions-in-render-indirect.js @@ -2,6 +2,11 @@ import {identity, makeArray} from 'shared-runtime'; +/** + * Allowed: we don't have sufficient type information to be sure that + * this accesses an impure value during render. The impurity is lost + * when passed through external function calls. + */ function Component() { const getDate = () => Date.now(); const array = makeArray(getDate()); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/impure-functions-in-render-via-function-call-2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/impure-functions-in-render-via-function-call-2.expect.md new file mode 100644 index 00000000000..b7ebee784d1 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/impure-functions-in-render-via-function-call-2.expect.md @@ -0,0 +1,64 @@ + +## Input + +```javascript +// @validateNoImpureFunctionsInRender + +import {identity, makeArray} from 'shared-runtime'; + +/** + * Allowed: we don't have sufficient type information to be sure that + * this accesses an impure value during render. The impurity is lost + * when passed through external function calls. + */ +function Component() { + const now = () => Date.now(); + const f = () => { + const array = makeArray(now()); + const hasDate = identity(array); + return hasDate; + }; + const hasDate = f(); + return ; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateNoImpureFunctionsInRender + +import { identity, makeArray } from "shared-runtime"; + +/** + * Allowed: we don't have sufficient type information to be sure that + * this accesses an impure value during render. The impurity is lost + * when passed through external function calls. + */ +function Component() { + const $ = _c(1); + const now = _temp; + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + const f = () => { + const array = makeArray(now()); + const hasDate = identity(array); + return hasDate; + }; + const hasDate_0 = f(); + t0 = ; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} +function _temp() { + return Date.now(); +} + +``` + +### 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/error.invalid-impure-functions-in-render-via-function-call-2.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/impure-functions-in-render-via-function-call-2.js similarity index 62% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call-2.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/impure-functions-in-render-via-function-call-2.js index 9abc485e957..c34d65bd537 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call-2.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/impure-functions-in-render-via-function-call-2.js @@ -2,11 +2,14 @@ import {identity, makeArray} from 'shared-runtime'; +/** + * Allowed: we don't have sufficient type information to be sure that + * this accesses an impure value during render. The impurity is lost + * when passed through external function calls. + */ function Component() { const now = () => Date.now(); const f = () => { - // this should error but we currently lose track of the impurity bc - // the impure value comes from behind a call const array = makeArray(now()); const hasDate = identity(array); return hasDate; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/mutate-after-useeffect-ref-access.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/mutate-after-useeffect-ref-access.expect.md index ea5a887b8bf..b585792f2cd 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/mutate-after-useeffect-ref-access.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/mutate-after-useeffect-ref-access.expect.md @@ -47,7 +47,7 @@ export const FIXTURE_ENTRYPOINT = { ## Logs ``` -{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":158},"end":{"line":11,"column":1,"index":331},"filename":"mutate-after-useeffect-ref-access.ts"},"detail":{"options":{"category":"Refs","reason":"Cannot access refs during render","description":"React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef)","details":[{"kind":"error","loc":{"start":{"line":9,"column":2,"index":289},"end":{"line":9,"column":16,"index":303},"filename":"mutate-after-useeffect-ref-access.ts"},"message":"Cannot update ref during render"}]}}} +{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":158},"end":{"line":11,"column":1,"index":331},"filename":"mutate-after-useeffect-ref-access.ts"},"detail":{"options":{"category":"Refs","reason":"Cannot access refs during render","description":"React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef)","details":[{"kind":"error","loc":{"start":{"line":9,"column":2,"index":289},"end":{"line":9,"column":20,"index":307},"filename":"mutate-after-useeffect-ref-access.ts"},"message":"Cannot update ref during render"}]}}} {"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":8,"column":2,"index":237},"end":{"line":8,"column":50,"index":285},"filename":"mutate-after-useeffect-ref-access.ts"},"decorations":[{"start":{"line":8,"column":24,"index":259},"end":{"line":8,"column":30,"index":265},"filename":"mutate-after-useeffect-ref-access.ts","identifierName":"arrRef"}]} {"kind":"CompileSuccess","fnLoc":{"start":{"line":6,"column":0,"index":158},"end":{"line":11,"column":1,"index":331},"filename":"mutate-after-useeffect-ref-access.ts"},"fnName":"Component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0} ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-ref-access.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-ref-access.expect.md index acf9c28cabd..d79ac677067 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-ref-access.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-ref-access.expect.md @@ -47,7 +47,7 @@ export const FIXTURE_ENTRYPOINT = { ## Logs ``` -{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":190},"end":{"line":11,"column":1,"index":363},"filename":"mutate-after-useeffect-ref-access.ts"},"detail":{"options":{"category":"Refs","reason":"Cannot access refs during render","description":"React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef)","details":[{"kind":"error","loc":{"start":{"line":9,"column":2,"index":321},"end":{"line":9,"column":16,"index":335},"filename":"mutate-after-useeffect-ref-access.ts"},"message":"Cannot update ref during render"}]}}} +{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":190},"end":{"line":11,"column":1,"index":363},"filename":"mutate-after-useeffect-ref-access.ts"},"detail":{"options":{"category":"Refs","reason":"Cannot access refs during render","description":"React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef)","details":[{"kind":"error","loc":{"start":{"line":9,"column":2,"index":321},"end":{"line":9,"column":20,"index":339},"filename":"mutate-after-useeffect-ref-access.ts"},"message":"Cannot update ref during render"}]}}} {"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":8,"column":2,"index":269},"end":{"line":8,"column":50,"index":317},"filename":"mutate-after-useeffect-ref-access.ts"},"decorations":[{"start":{"line":8,"column":24,"index":291},"end":{"line":8,"column":30,"index":297},"filename":"mutate-after-useeffect-ref-access.ts","identifierName":"arrRef"}]} {"kind":"CompileSuccess","fnLoc":{"start":{"line":6,"column":0,"index":190},"end":{"line":11,"column":1,"index":363},"filename":"mutate-after-useeffect-ref-access.ts"},"fnName":"Component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0} ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/pass-ref-to-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/pass-ref-to-function.expect.md new file mode 100644 index 00000000000..df4aa13ddb8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/pass-ref-to-function.expect.md @@ -0,0 +1,45 @@ + +## Input + +```javascript +// @validateRefAccessDuringRender + +/** + * Allowed: we don't have sufficient type information to be sure that + * this accesses a ref during render. The return type of foo() is unknown. + */ +function Component(props) { + const ref = useRef(null); + const x = foo(ref); + return x.current; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender + +/** + * Allowed: we don't have sufficient type information to be sure that + * this accesses a ref during render. The return type of foo() is unknown. + */ +function Component(props) { + const $ = _c(1); + const ref = useRef(null); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = foo(ref); + $[0] = t0; + } else { + t0 = $[0]; + } + const x = t0; + return x.current; +} + +``` + +### 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/pass-ref-to-function.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/pass-ref-to-function.js new file mode 100644 index 00000000000..4bf588acbc3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/pass-ref-to-function.js @@ -0,0 +1,11 @@ +// @validateRefAccessDuringRender + +/** + * Allowed: we don't have sufficient type information to be sure that + * this accesses a ref during render. The return type of foo() is unknown. + */ +function Component(props) { + const ref = useRef(null); + const x = foo(ref); + return x.current; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-mutable-ref-not-preserved.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-mutable-ref-not-preserved.expect.md deleted file mode 100644 index 77f104cab0c..00000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-mutable-ref-not-preserved.expect.md +++ /dev/null @@ -1,42 +0,0 @@ - -## Input - -```javascript -// @validatePreserveExistingMemoizationGuarantees:true - -import {useRef, useMemo} from 'react'; -import {makeArray} from 'shared-runtime'; - -function useFoo() { - const r = useRef(); - return useMemo(() => makeArray(r), []); -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [], -}; - -``` - - -## Error - -``` -Found 1 error: - -Error: Cannot access refs during render - -React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). - -error.maybe-mutable-ref-not-preserved.ts:8:33 - 6 | function useFoo() { - 7 | const r = useRef(); -> 8 | return useMemo(() => makeArray(r), []); - | ^ Passing a ref to a function may read its value during render - 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/preserve-memo-validation/error.useMemo-with-refs.flow.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-with-refs.flow.expect.md deleted file mode 100644 index 0269b22a1f2..00000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-with-refs.flow.expect.md +++ /dev/null @@ -1,37 +0,0 @@ - -## Input - -```javascript -// @flow @validatePreserveExistingMemoizationGuarantees -import {identity} from 'shared-runtime'; - -component Component(disableLocalRef, ref) { - const localRef = useFooRef(); - const mergedRef = useMemo(() => { - return disableLocalRef ? ref : identity(ref, localRef); - }, [disableLocalRef, ref, localRef]); - return
; -} - -``` - - -## Error - -``` -Found 1 error: - -Error: Cannot access refs during render - -React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). - - 5 | const localRef = useFooRef(); - 6 | const mergedRef = useMemo(() => { -> 7 | return disableLocalRef ? ref : identity(ref, localRef); - | ^^^ Passing a ref to a function may read its value during render - 8 | }, [disableLocalRef, ref, localRef]); - 9 | return
; - 10 | } -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-mutable-ref-not-preserved.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-mutable-ref-not-preserved.expect.md new file mode 100644 index 00000000000..f2201cdf288 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-mutable-ref-not-preserved.expect.md @@ -0,0 +1,59 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees:true + +import {useRef, useMemo} from 'react'; +import {makeArray} from 'shared-runtime'; + +/** + * Allowed: we don't have sufficient type information to be sure that + * this accesses an impure value during render. + */ +function useFoo() { + const r = useRef(); + return useMemo(() => makeArray(r), []); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees:true + +import { useRef, useMemo } from "react"; +import { makeArray } from "shared-runtime"; + +/** + * Allowed: we don't have sufficient type information to be sure that + * this accesses an impure value during render. + */ +function useFoo() { + const $ = _c(1); + const r = useRef(); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = makeArray(r); + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; + +``` + +### Eval output +(kind: ok) [{}] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-mutable-ref-not-preserved.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-mutable-ref-not-preserved.ts similarity index 69% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-mutable-ref-not-preserved.ts rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-mutable-ref-not-preserved.ts index 13b8b445827..82751750504 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-mutable-ref-not-preserved.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-mutable-ref-not-preserved.ts @@ -3,6 +3,10 @@ import {useRef, useMemo} from 'react'; import {makeArray} from 'shared-runtime'; +/** + * Allowed: we don't have sufficient type information to be sure that + * this accesses an impure value during render. + */ function useFoo() { const r = useRef(); return useMemo(() => makeArray(r), []); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-with-refs.flow.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-with-refs.flow.expect.md new file mode 100644 index 00000000000..26551994a3e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-with-refs.flow.expect.md @@ -0,0 +1,52 @@ + +## Input + +```javascript +// @flow @validatePreserveExistingMemoizationGuarantees +import {identity} from 'shared-runtime'; + +component Component(disableLocalRef, ref) { + const localRef = useFooRef(); + const mergedRef = useMemo(() => { + return disableLocalRef ? ref : identity(ref, localRef); + }, [disableLocalRef, ref, localRef]); + return
; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { identity } from "shared-runtime"; + +const Component = React.forwardRef(Component_withRef); +function Component_withRef(t0, ref) { + const $ = _c(6); + const { disableLocalRef } = t0; + const localRef = useFooRef(); + let t1; + if ($[0] !== disableLocalRef || $[1] !== localRef || $[2] !== ref) { + t1 = disableLocalRef ? ref : identity(ref, localRef); + $[0] = disableLocalRef; + $[1] = localRef; + $[2] = ref; + $[3] = t1; + } else { + t1 = $[3]; + } + const mergedRef = t1; + let t2; + if ($[4] !== mergedRef) { + t2 =
; + $[4] = mergedRef; + $[5] = t2; + } else { + t2 = $[5]; + } + return t2; +} + +``` + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-with-refs.flow.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-with-refs.flow.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-with-refs.flow.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-with-refs.flow.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-initialization-call-2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-initialization-call-2.expect.md new file mode 100644 index 00000000000..6573a4b1cb5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-initialization-call-2.expect.md @@ -0,0 +1,46 @@ + +## Input + +```javascript +//@flow +import {useRef} from 'react'; + +/** + * Allowed: we aren't sure that the ref.current value flows into the render + * output, so we optimistically assume it's safe + */ +component C() { + const r = useRef(null); + if (r.current == null) { + f(r); + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; + +``` + +## Code + +```javascript +import { useRef } from "react"; + +function C() { + const r = useRef(null); + if (r.current == null) { + f(r); + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; + +``` + +### Eval output +(kind: exception) f is not defined \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call-2.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-initialization-call-2.js similarity index 58% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call-2.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-initialization-call-2.js index 4e5a53cd3f6..8bfd4cd0e3c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call-2.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-initialization-call-2.js @@ -1,6 +1,10 @@ //@flow import {useRef} from 'react'; +/** + * Allowed: we aren't sure that the ref.current value flows into the render + * output, so we optimistically assume it's safe + */ component C() { const r = useRef(null); if (r.current == null) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-initialization-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-initialization-call.expect.md new file mode 100644 index 00000000000..3502bff68d1 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-initialization-call.expect.md @@ -0,0 +1,46 @@ + +## Input + +```javascript +//@flow +import {useRef} from 'react'; + +/** + * Allowed: we aren't sure that the ref.current value flows into the render + * output, so we optimistically assume it's safe + */ +component C() { + const r = useRef(null); + if (r.current == null) { + f(r.current); + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; + +``` + +## Code + +```javascript +import { useRef } from "react"; + +function C() { + const r = useRef(null); + if (r.current == null) { + f(r.current); + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; + +``` + +### Eval output +(kind: exception) f is not defined \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-initialization-call.js similarity index 59% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-initialization-call.js index 50288fafc4a..82192c0681a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-initialization-call.js @@ -1,6 +1,10 @@ //@flow import {useRef} from 'react'; +/** + * Allowed: we aren't sure that the ref.current value flows into the render + * output, so we optimistically assume it's safe + */ component C() { const r = useRef(null); if (r.current == null) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-initialization-post-access-2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-initialization-post-access-2.expect.md new file mode 100644 index 00000000000..eb30bb34445 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-initialization-post-access-2.expect.md @@ -0,0 +1,49 @@ + +## Input + +```javascript +//@flow +import {useRef} from 'react'; + +/** + * Allowed: we aren't sure that the ref.current value flows into the render + * output, so we optimistically assume it's safe + */ +component C() { + const r = useRef(null); + if (r.current == null) { + r.current = 1; + } + f(r.current); +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; + +``` + +## Code + +```javascript +import { useRef } from "react"; + +function C() { + const r = useRef(null); + if (r.current == null) { + r.current = 1; + } + + f(r.current); +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; + +``` + +### Eval output +(kind: exception) f is not defined \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-post-access-2.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-initialization-post-access-2.js similarity index 61% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-post-access-2.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-initialization-post-access-2.js index a8e3b124bfe..2a3e801b190 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-post-access-2.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-initialization-post-access-2.js @@ -1,6 +1,10 @@ //@flow import {useRef} from 'react'; +/** + * Allowed: we aren't sure that the ref.current value flows into the render + * output, so we optimistically assume it's safe + */ component C() { const r = useRef(null); if (r.current == null) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-value-in-custom-component-event-handler-wrapper.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-value-in-custom-component-event-handler-wrapper.expect.md new file mode 100644 index 00000000000..d67a02c57ec --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-value-in-custom-component-event-handler-wrapper.expect.md @@ -0,0 +1,121 @@ + +## Input + +```javascript +// @enableInferEventHandlers +import {useRef} from 'react'; + +// Simulates a custom component wrapper +function CustomForm({onSubmit, children}: any) { + return {children}; +} + +// Simulates react-hook-form's handleSubmit +function handleSubmit(callback: (data: T) => void) { + return (event: any) => { + event.preventDefault(); + callback({} as T); + }; +} + +function Component() { + const ref = useRef(null); + + const onSubmit = (data: any) => { + // Allowed: we aren't sure that the ref.current value flows into the render + // output, so we optimistically assume it's safe + if (ref.current !== null) { + console.log(ref.current.value); + } + }; + + return ( + <> + + + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableInferEventHandlers +import { useRef } from "react"; + +// Simulates a custom component wrapper +function CustomForm(t0) { + const $ = _c(3); + const { onSubmit, children } = t0; + let t1; + if ($[0] !== children || $[1] !== onSubmit) { + t1 =
{children}
; + $[0] = children; + $[1] = onSubmit; + $[2] = t1; + } else { + t1 = $[2]; + } + return t1; +} + +// Simulates react-hook-form's handleSubmit +function handleSubmit(callback) { + const $ = _c(2); + let t0; + if ($[0] !== callback) { + t0 = (event) => { + event.preventDefault(); + callback({} as T); + }; + $[0] = callback; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; +} + +function Component() { + const $ = _c(1); + const ref = useRef(null); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + const onSubmit = (data) => { + if (ref.current !== null) { + console.log(ref.current.value); + } + }; + t0 = ( + <> + + + + + + ); + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], +}; + +``` + +### Eval output +(kind: ok)
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-custom-component-event-handler-wrapper.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-value-in-custom-component-event-handler-wrapper.tsx similarity index 84% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-custom-component-event-handler-wrapper.tsx rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-value-in-custom-component-event-handler-wrapper.tsx index b90a1217165..5874c164558 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-custom-component-event-handler-wrapper.tsx +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-value-in-custom-component-event-handler-wrapper.tsx @@ -18,8 +18,8 @@ function Component() { const ref = useRef(null); const onSubmit = (data: any) => { - // This should error: passing function with ref access to custom component - // event handler, even though it would be safe on a native
+ // Allowed: we aren't sure that the ref.current value flows into the render + // output, so we optimistically assume it's safe if (ref.current !== null) { console.log(ref.current.value); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-value-in-event-handler-wrapper.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-value-in-event-handler-wrapper.expect.md new file mode 100644 index 00000000000..1289ade402e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-value-in-event-handler-wrapper.expect.md @@ -0,0 +1,83 @@ + +## Input + +```javascript +// @enableInferEventHandlers +import {useRef} from 'react'; + +// Simulates a handler wrapper +function handleClick(value: any) { + return () => { + console.log(value); + }; +} + +function Component() { + const ref = useRef(null); + + // Allowed: we aren't sure that the ref.current value flows into the render + // output, so we optimistically assume it's safe + return ( + <> + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableInferEventHandlers +import { useRef } from "react"; + +// Simulates a handler wrapper +function handleClick(value) { + const $ = _c(2); + let t0; + if ($[0] !== value) { + t0 = () => { + console.log(value); + }; + $[0] = value; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; +} + +function Component() { + const $ = _c(1); + const ref = useRef(null); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = ( + <> + + + + ); + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], +}; + +``` + +### Eval output +(kind: ok) \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-event-handler-wrapper.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-value-in-event-handler-wrapper.tsx similarity index 74% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-event-handler-wrapper.tsx rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-value-in-event-handler-wrapper.tsx index 58313e560ce..64410473591 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-event-handler-wrapper.tsx +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-value-in-event-handler-wrapper.tsx @@ -11,8 +11,8 @@ function handleClick(value: any) { function Component() { const ref = useRef(null); - // This should still error: passing ref.current directly to a wrapper - // The ref value is accessed during render, not in the event handler + // Allowed: we aren't sure that the ref.current value flows into the render + // output, so we optimistically assume it's safe return ( <> diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-ref-added-to-dep-without-type-info.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-ref-added-to-dep-without-type-info.expect.md new file mode 100644 index 00000000000..f96ef187dda --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-ref-added-to-dep-without-type-info.expect.md @@ -0,0 +1,59 @@ + +## Input + +```javascript +// @validateRefAccessDuringRender + +/** + * Allowed: we don't have sufficient type information to be sure that + * this accesses a ref during render. Type info is lost when ref is + * stored in an object field. + */ +function Foo({a}) { + const ref = useRef(); + const val = {ref}; + const x = {a, val: val.ref.current}; + + return ; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender + +/** + * Allowed: we don't have sufficient type information to be sure that + * this accesses a ref during render. Type info is lost when ref is + * stored in an object field. + */ +function Foo(t0) { + const $ = _c(3); + const { a } = t0; + const ref = useRef(); + let t1; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t1 = { ref }; + $[0] = t1; + } else { + t1 = $[0]; + } + const val = t1; + let t2; + if ($[1] !== a) { + const x = { a, val: val.ref.current }; + t2 = ; + $[1] = a; + $[2] = t2; + } else { + t2 = $[2]; + } + 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/use-ref-added-to-dep-without-type-info.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-ref-added-to-dep-without-type-info.js new file mode 100644 index 00000000000..2d29c6f15fa --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-ref-added-to-dep-without-type-info.js @@ -0,0 +1,14 @@ +// @validateRefAccessDuringRender + +/** + * Allowed: we don't have sufficient type information to be sure that + * this accesses a ref during render. Type info is lost when ref is + * stored in an object field. + */ +function Foo({a}) { + const ref = useRef(); + const val = {ref}; + const x = {a, val: val.ref.current}; + + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/validate-mutate-ref-arg-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/validate-mutate-ref-arg-in-render.expect.md new file mode 100644 index 00000000000..4c2979ca3a7 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/validate-mutate-ref-arg-in-render.expect.md @@ -0,0 +1,51 @@ + +## Input + +```javascript +// @validateRefAccessDuringRender:true + +function Foo(props, ref) { + // Allowed: the value is not guaranteed to flow into something that's rendered + console.log(ref.current); + return
{props.bar}
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{bar: 'foo'}, {ref: {cuurrent: 1}}], + isComponent: true, +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender:true + +function Foo(props, ref) { + const $ = _c(2); + + console.log(ref.current); + let t0; + if ($[0] !== props.bar) { + t0 =
{props.bar}
; + $[0] = props.bar; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ bar: "foo" }, { ref: { cuurrent: 1 } }], + isComponent: true, +}; + +``` + +### Eval output +(kind: ok)
foo
+logs: [undefined] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.validate-mutate-ref-arg-in-render.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/validate-mutate-ref-arg-in-render.js similarity index 75% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.validate-mutate-ref-arg-in-render.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/validate-mutate-ref-arg-in-render.js index 10218fc6163..55986513a19 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.validate-mutate-ref-arg-in-render.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/validate-mutate-ref-arg-in-render.js @@ -1,5 +1,7 @@ // @validateRefAccessDuringRender:true + function Foo(props, ref) { + // Allowed: the value is not guaranteed to flow into something that's rendered console.log(ref.current); return
{props.bar}
; }