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..d579968e178 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} @@ -271,10 +271,6 @@ function runWithEnvironment( assertValidMutableRanges(hir); } - if (env.config.validateRefAccessDuringRender) { - validateNoRefAccessInRender(hir).unwrap(); - } - if (env.config.validateNoSetStateInRender) { validateNoSetStateInRender(hir).unwrap(); } @@ -296,8 +292,15 @@ function runWithEnvironment( env.logErrors(validateNoJSXInTryStatement(hir)); } - if (env.config.validateNoImpureFunctionsInRender) { - validateNoImpureFunctionsInRender(hir).unwrap(); + 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/Globals.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts index 441b5d5452a..b0b074a18ff 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,11 +626,136 @@ 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: ['@fn', '@deps'], + rest: '@rest', + returns: '@returns', + temporaries: ['@effect'], + effects: [ + // Freezes the function and deps + { + kind: 'Freeze', + 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', + into: '@effect', + value: ValueKind.Frozen, + reason: ValueReason.KnownReturnSignature, + }, + // The effect stores the function and dependencies + { + kind: 'Capture', + from: '@rest', + into: '@effect', + }, + { + kind: 'Capture', + from: '@fn', + 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 * 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', @@ -644,6 +769,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ hookKind: 'useContext', returnValueKind: ValueKind.Frozen, returnValueReason: ValueReason.Context, + aliasing: RenderHookAliasing(ValueReason.Context), }, BuiltInUseContextHookId, ), @@ -658,6 +784,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ hookKind: 'useState', returnValueKind: ValueKind.Frozen, returnValueReason: ValueReason.State, + aliasing: RenderHookAliasing(ValueReason.State), }), ], [ @@ -670,6 +797,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ hookKind: 'useActionState', returnValueKind: ValueKind.Frozen, returnValueReason: ValueReason.State, + aliasing: RenderHookAliasing(ValueReason.HookCaptured), }), ], [ @@ -682,6 +810,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ hookKind: 'useReducer', returnValueKind: ValueKind.Frozen, returnValueReason: ValueReason.ReducerState, + aliasing: RenderHookAliasing(ValueReason.ReducerState), }), ], [ @@ -715,6 +844,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ calleeEffect: Effect.Read, hookKind: 'useMemo', returnValueKind: ValueKind.Frozen, + aliasing: RenderHookAliasing(ValueReason.HookCaptured), }), ], [ @@ -726,6 +856,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ calleeEffect: Effect.Read, hookKind: 'useCallback', returnValueKind: ValueKind.Frozen, + aliasing: RenderHookAliasing(ValueReason.HookCaptured), }), ], [ @@ -739,41 +870,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 +886,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ calleeEffect: Effect.Read, hookKind: 'useLayoutEffect', returnValueKind: ValueKind.Frozen, + aliasing: EffectHookAliasing, }, BuiltInUseLayoutEffectHookId, ), @@ -804,6 +902,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ calleeEffect: Effect.Read, hookKind: 'useInsertionEffect', returnValueKind: ValueKind.Frozen, + aliasing: EffectHookAliasing, }, BuiltInUseInsertionEffectHookId, ), @@ -817,6 +916,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ calleeEffect: Effect.Read, hookKind: 'useTransition', returnValueKind: ValueKind.Frozen, + aliasing: RenderHookAliasing(ValueReason.HookCaptured), }), ], [ @@ -829,6 +929,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ hookKind: 'useOptimistic', returnValueKind: ValueKind.Frozen, returnValueReason: ValueReason.State, + aliasing: RenderHookAliasing(ValueReason.HookCaptured), }), ], [ @@ -842,6 +943,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ returnType: {kind: 'Poly'}, calleeEffect: Effect.Read, returnValueKind: ValueKind.Frozen, + aliasing: RenderHookAliasing(ValueReason.HookCaptured), }, BuiltInUseOperatorId, ), @@ -866,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 c396f6b0881..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 { @@ -1890,6 +1898,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 +2073,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 c92f9e55623..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 { @@ -190,14 +190,22 @@ function parseAliasingSignatureConfig( }; } case 'Impure': { - const place = lookup(effect.place); + const into = lookup(effect.into); return { kind: 'Impure', + into, + category: ErrorCategory.Purity, + description: effect.description, + reason: effect.reason, + sourceMessage: effect.sourceMessage, + usageMessage: effect.usageMessage, + }; + } + 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 +1521,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..a88d70aa7e4 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,29 @@ export const ApplyEffectSchema: z.ZodType = z.object({ export type ImpureEffectConfig = { kind: 'Impure'; - place: string; + into: string; + reason: string; + description: string; + sourceMessage: string; + usageMessage: string; }; export const ImpureEffectSchema: z.ZodType = z.object({ kind: z.literal('Impure'), + into: LifetimeIdSchema, + reason: z.string(), + description: z.string(), + sourceMessage: z.string(), + usageMessage: z.string(), +}); + +export type RenderEffectConfig = { + kind: 'Render'; + place: string; +}; + +export const RenderEffectSchema: z.ZodType = z.object({ + kind: z.literal('Render'), place: LifetimeIdSchema, }); @@ -204,7 +222,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..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'; place: Place; error: CompilerDiagnostic} + | { + 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 @@ -222,6 +230,14 @@ 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, + 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/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..e6866a51a3a 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,13 @@ import { InstructionKind, InstructionValue, isArrayType, - isJsxType, + isJsxOrJsxUnionType, isMapType, + isMutableEffect, isPrimitiveType, isRefOrRefValue, isSetType, + isUseRefType, makeIdentifierId, Phi, Place, @@ -70,6 +72,7 @@ import { MutationReason, } from './AliasingEffects'; import {ErrorCategory} from '../CompilerError'; +import {REF_ERROR_DESCRIPTION} from '../Validation/ValidateNoRefAccessInRender'; const DEBUG = false; @@ -569,14 +572,32 @@ 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, }), - ]; + ); + } + if ( + context.fn.fnType === 'Component' || + isJsxOrJsxUnionType(context.fn.returns.identifier.type) + ) { + terminal.effects.push( + context.internEffect({ + kind: 'Render', + place: terminal.value, + }), + ); } } } @@ -749,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': { @@ -1061,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: { @@ -1966,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', @@ -1973,6 +2000,20 @@ function computeSignatureForInstruction( into: lvalue, }); } + if ( + env.config.validateRefAccessDuringRender && + isUseRefType(value.object.identifier) + ) { + 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; } case 'PropertyStore': @@ -2137,6 +2178,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 @@ -2145,29 +2195,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) { - effects.push({ - kind: 'Render', - place: child, - }); - } - } 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 + 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({ + kind: 'Render', + place, + }); + } + } else { effects.push({ kind: 'Render', - place: prop.place, + place, }); } } @@ -2203,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', @@ -2384,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', @@ -2423,7 +2503,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 +2516,18 @@ 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, + 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) { @@ -2748,7 +2826,23 @@ function computeEffectsForSignature( } break; } - case 'Impure': + case 'Impure': { + 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; + } 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..f0cab1345db 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') && @@ -227,23 +224,31 @@ 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, + }, + ], + }, + ); } } } } 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 +260,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 +528,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 +597,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 +631,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 +712,7 @@ class AliasingState { lastMutated: 0, mutationReason: null, value, + render: null, }); } @@ -681,9 +765,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 +775,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 +816,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/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/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..3db4dd9ab4c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureValuesInRender.ts @@ -0,0 +1,307 @@ +/** + * 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 prettyFormat from 'pretty-format'; +import {CompilerDiagnostic, CompilerError, Effect} from '..'; +import { + areEqualSourceLocations, + HIRFunction, + IdentifierId, + InstructionId, + isJsxType, + isRefValueType, + 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, + 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}; + +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, + () => 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, + 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) { + const _impure = new Set(impure.keys()); + hasChanges = + processEffects(instr.id, instr.effects ?? [], impure, cache) || + hasChanges; + } + 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; + 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') { + validateRenderEffect(effect); + } + } + } + if (block.terminal.kind === 'return' && block.terminal.effects != null) { + for (const effect of block.terminal.effects) { + if (effect.kind === 'Render') { + validateRenderEffect(effect); + } + } + } + } + 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: impureEffect.into, + category: impureEffect.category, + reason: impureEffect.reason, + description: impureEffect.description, + sourceMessage: impureEffect.sourceMessage, + usageMessage: impureEffect.usageMessage, + }); + } + } + 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..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: 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: 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: 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: 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: 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, @@ -886,7 +706,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, @@ -896,28 +716,7 @@ 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: ERROR_DESCRIPTION, - }).withDetails({ - kind: 'error', - loc: type.loc ?? operand.loc, - message: `Cannot access ref value during render`, - }), - ); - } -} - -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/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.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.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 647cf28f7b3..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 @@ -28,7 +28,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). @@ -36,7 +36,16 @@ 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.invalid-access-ref-in-reducer-init.ts:8:12 + 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..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 @@ -24,7 +24,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). @@ -32,7 +32,16 @@ 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.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 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..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 @@ -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:21 + 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..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 @@ -24,7 +24,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). @@ -32,7 +32,16 @@ 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.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 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..1d6ec9e4c0e --- /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:21 + 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 new file mode 100644 index 00000000000..d68a7b6fac7 --- /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,47 @@ + +## 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 | + +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. + 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-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..fced0f6281c --- /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,46 @@ + +## 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 | + +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. + 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-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..8aaaa839ecf --- /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: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-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..29d379238c0 --- /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: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-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-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 new file mode 100644 index 00000000000..900437eccd5 --- /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,49 @@ + +## 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 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.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/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..27e74c33398 --- /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,45 @@ + +## Input + +```javascript +// @validateNoImpureFunctionsInRender +function Component() { + const now = () => Date.now(); + const render = () => { + return
{now()}
; + }; + return
{render()}
; +} + +``` + + +## 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-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:20 + 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.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/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'; 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/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; } 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'), ], }, ],