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..5a4d9cf8c92 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts @@ -38,7 +38,7 @@ import { addObject, } from './ObjectShape'; import {BuiltInType, ObjectType, PolyType} from './Types'; -import {TypeConfig} from './TypeSchema'; +import {AliasingSignatureConfig, TypeConfig} from './TypeSchema'; import {assertExhaustive} from '../Utils/utils'; import {isHookName} from './Environment'; import {CompilerError, SourceLocation} from '..'; @@ -626,6 +626,78 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [ // TODO: rest of Global objects ]; +const RenderHookAliasing: ( + reason: ValueReason, +) => AliasingSignatureConfig = reason => ({ + receiver: '@receiver', + params: [], + rest: '@rest', + returns: '@returns', + temporaries: [], + effects: [ + // Freeze the arguments + { + kind: 'Freeze', + value: '@rest', + reason: ValueReason.HookCaptured, + }, + // Render the arguments + { + kind: 'Render', + place: '@rest', + }, + // Returns a frozen value + { + kind: 'Create', + into: '@returns', + value: ValueKind.Frozen, + reason, + }, + // May alias any arguments into the return + { + kind: 'Alias', + from: '@rest', + into: '@returns', + }, + ], +}); + +const EffectHookAliasing: AliasingSignatureConfig = { + receiver: '@receiver', + params: [], + rest: '@rest', + returns: '@returns', + temporaries: ['@effect'], + effects: [ + // Freezes the function and deps + { + kind: 'Freeze', + value: '@rest', + reason: ValueReason.Effect, + }, + // Internally creates an effect object that captures the function and deps + { + kind: 'Create', + into: '@effect', + value: ValueKind.Frozen, + reason: ValueReason.KnownReturnSignature, + }, + // The effect stores the function and dependencies + { + kind: 'Capture', + from: '@rest', + into: '@effect', + }, + // Returns undefined + { + kind: 'Create', + into: '@returns', + value: ValueKind.Primitive, + reason: ValueReason.KnownReturnSignature, + }, + ], +}; + /* * TODO(mofeiZ): We currently only store rest param effects for hooks. * now that FeatureFlag `enableTreatHooksAsFunctions` is removed we can @@ -644,6 +716,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ hookKind: 'useContext', returnValueKind: ValueKind.Frozen, returnValueReason: ValueReason.Context, + aliasing: RenderHookAliasing(ValueReason.Context), }, BuiltInUseContextHookId, ), @@ -658,6 +731,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ hookKind: 'useState', returnValueKind: ValueKind.Frozen, returnValueReason: ValueReason.State, + aliasing: RenderHookAliasing(ValueReason.State), }), ], [ @@ -670,6 +744,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ hookKind: 'useActionState', returnValueKind: ValueKind.Frozen, returnValueReason: ValueReason.State, + aliasing: RenderHookAliasing(ValueReason.HookCaptured), }), ], [ @@ -682,6 +757,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ hookKind: 'useReducer', returnValueKind: ValueKind.Frozen, returnValueReason: ValueReason.ReducerState, + aliasing: RenderHookAliasing(ValueReason.ReducerState), }), ], [ @@ -715,6 +791,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ calleeEffect: Effect.Read, hookKind: 'useMemo', returnValueKind: ValueKind.Frozen, + aliasing: RenderHookAliasing(ValueReason.HookCaptured), }), ], [ @@ -726,6 +803,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ calleeEffect: Effect.Read, hookKind: 'useCallback', returnValueKind: ValueKind.Frozen, + aliasing: RenderHookAliasing(ValueReason.HookCaptured), }), ], [ @@ -739,41 +817,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ calleeEffect: Effect.Read, hookKind: 'useEffect', returnValueKind: ValueKind.Frozen, - aliasing: { - receiver: '@receiver', - params: [], - rest: '@rest', - returns: '@returns', - temporaries: ['@effect'], - effects: [ - // Freezes the function and deps - { - kind: 'Freeze', - value: '@rest', - reason: ValueReason.Effect, - }, - // Internally creates an effect object that captures the function and deps - { - kind: 'Create', - into: '@effect', - value: ValueKind.Frozen, - reason: ValueReason.KnownReturnSignature, - }, - // The effect stores the function and dependencies - { - kind: 'Capture', - from: '@rest', - into: '@effect', - }, - // Returns undefined - { - kind: 'Create', - into: '@returns', - value: ValueKind.Primitive, - reason: ValueReason.KnownReturnSignature, - }, - ], - }, + aliasing: EffectHookAliasing, }, BuiltInUseEffectHookId, ), @@ -789,6 +833,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ calleeEffect: Effect.Read, hookKind: 'useLayoutEffect', returnValueKind: ValueKind.Frozen, + aliasing: EffectHookAliasing, }, BuiltInUseLayoutEffectHookId, ), @@ -804,6 +849,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ calleeEffect: Effect.Read, hookKind: 'useInsertionEffect', returnValueKind: ValueKind.Frozen, + aliasing: EffectHookAliasing, }, BuiltInUseInsertionEffectHookId, ), @@ -817,6 +863,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ calleeEffect: Effect.Read, hookKind: 'useTransition', returnValueKind: ValueKind.Frozen, + aliasing: RenderHookAliasing(ValueReason.HookCaptured), }), ], [ @@ -829,6 +876,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ hookKind: 'useOptimistic', returnValueKind: ValueKind.Frozen, returnValueReason: ValueReason.State, + aliasing: RenderHookAliasing(ValueReason.HookCaptured), }), ], [ @@ -842,6 +890,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ returnType: {kind: 'Poly'}, calleeEffect: Effect.Read, returnValueKind: ValueKind.Frozen, + aliasing: RenderHookAliasing(ValueReason.HookCaptured), }, BuiltInUseOperatorId, ), diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts index c396f6b0881..14317cac09d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -1890,6 +1890,13 @@ export function isJsxType(type: Type): boolean { return type.kind === 'Object' && type.shapeId === 'BuiltInJsx'; } +export function isJsxOrJsxUnionType(type: Type): boolean { + return ( + (type.kind === 'Object' && type.shapeId === 'BuiltInJsx') || + (type.kind === 'Phi' && type.operands.some(op => isJsxOrJsxUnionType(op))) + ); +} + export function isRefOrRefValue(id: Identifier): boolean { return isUseRefType(id) || isRefValueType(id); } @@ -2058,4 +2065,23 @@ export function getHookKindForType( return null; } +export function areEqualSourceLocations( + loc1: SourceLocation, + loc2: SourceLocation, +): boolean { + if (typeof loc1 === 'symbol' || typeof loc2 === 'symbol') { + return false; + } + return ( + loc1.filename === loc2.filename && + loc1.identifierName === loc2.identifierName && + loc1.start.line === loc2.start.line && + loc1.start.column === loc2.start.column && + loc1.start.index === loc2.start.index && + loc1.end.line === loc2.end.line && + loc1.end.column === loc2.end.column && + loc1.end.index === loc2.end.index + ); +} + export * from './Types'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts index d3ecb2abdcd..1a2a56a7f79 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts @@ -988,7 +988,7 @@ export function createTemporaryPlace( identifier: makeTemporaryIdentifier(env.nextIdentifierId, loc), reactive: false, effect: Effect.Unknown, - loc: GeneratedSource, + loc, }; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts index 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..908db0eb1f2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts @@ -27,11 +27,12 @@ import { InstructionKind, InstructionValue, isArrayType, - isJsxType, + isJsxOrJsxUnionType, isMapType, isPrimitiveType, isRefOrRefValue, isSetType, + isUseRefType, makeIdentifierId, Phi, Place, @@ -70,6 +71,7 @@ import { MutationReason, } from './AliasingEffects'; import {ErrorCategory} from '../CompilerError'; +import {REF_ERROR_DESCRIPTION} from '../Validation/ValidateNoRefAccessInRender'; const DEBUG = false; @@ -569,14 +571,21 @@ function inferBlock( terminal.effects = effects.length !== 0 ? effects : null; } } else if (terminal.kind === 'return') { + terminal.effects = [ + context.internEffect({ + kind: 'Alias', + from: terminal.value, + into: context.fn.returns, + }), + ]; if (!context.isFuctionExpression) { - terminal.effects = [ + terminal.effects.push( context.internEffect({ kind: 'Freeze', value: terminal.value, reason: ValueReason.JsxCaptured, }), - ]; + ); } } } @@ -1973,6 +1982,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 +2160,15 @@ function computeSignatureForInstruction( into: lvalue, }); } + if (value.children != null) { + // Children are typically called during render, not used as an event/effect callback + for (const child of value.children) { + effects.push({ + kind: 'Render', + place: child, + }); + } + } if (value.kind === 'JsxExpression') { if (value.tag.kind === 'Identifier') { // Tags are render function, by definition they're called during render @@ -2145,29 +2177,20 @@ 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 (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, }); } } @@ -2423,7 +2446,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 +2459,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 +2769,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..ae1e07c5a73 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts @@ -19,6 +19,7 @@ import { ValueReason, Place, isPrimitiveType, + isUseRefType, } from '../HIR/HIR'; import { eachInstructionLValue, @@ -28,6 +29,9 @@ import { import {assertExhaustive, getOrInsertWith} from '../Utils/utils'; import {Err, Ok, Result} from '../Utils/Result'; import {AliasingEffect, MutationReason} from './AliasingEffects'; +import {printIdentifier, printType} from '../HIR/PrintHIR'; + +const DEBUG = false; /** * This pass builds an abstract model of the heap and interprets the effects of the @@ -104,7 +108,6 @@ export function inferMutationAliasingRanges( reason: MutationReason | null; }> = []; const renders: Array<{index: number; place: Place}> = []; - let index = 0; const errors = new CompilerError(); @@ -197,14 +200,12 @@ export function inferMutationAliasingRanges( }); } else if ( effect.kind === 'MutateFrozen' || - effect.kind === 'MutateGlobal' || - effect.kind === 'Impure' + effect.kind === 'MutateGlobal' ) { errors.pushDiagnostic(effect.error); functionEffects.push(effect); } else if (effect.kind === 'Render') { renders.push({index: index++, place: effect.place}); - functionEffects.push(effect); } } } @@ -214,10 +215,6 @@ export function inferMutationAliasingRanges( state.assign(index, from, into); } } - if (block.terminal.kind === 'return') { - state.assign(index++, block.terminal.value, fn.returns); - } - if ( (block.terminal.kind === 'maybe-throw' || block.terminal.kind === 'return') && @@ -244,6 +241,11 @@ export function inferMutationAliasingRanges( } for (const mutation of mutations) { + if (DEBUG) { + console.log( + `[${mutation.index}] mutate ${printIdentifier(mutation.place.identifier)}`, + ); + } state.mutate( mutation.index, mutation.place.identifier, @@ -255,8 +257,16 @@ export function inferMutationAliasingRanges( errors, ); } + if (DEBUG) { + console.log(state.debug()); + } for (const render of renders) { - state.render(render.index, render.place.identifier, errors); + if (DEBUG) { + console.log( + `[${render.index}] render ${printIdentifier(render.place.identifier)}`, + ); + } + state.render(render.index, render.place, errors); } for (const param of [...fn.context, ...fn.params]) { const place = param.kind === 'Identifier' ? param : param.place; @@ -515,6 +525,13 @@ export function inferMutationAliasingRanges( const ignoredErrors = new CompilerError(); for (const param of [...fn.params, ...fn.context, fn.returns]) { const place = param.kind === 'Identifier' ? param : param.place; + const node = state.nodes.get(place.identifier); + if (node != null && node.render != null) { + functionEffects.push({ + kind: 'Render', + place: place, + }); + } tracked.push(place); } for (const into of tracked) { @@ -577,7 +594,6 @@ export function inferMutationAliasingRanges( function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void { for (const effect of fn.aliasingEffects ?? []) { switch (effect.kind) { - case 'Impure': case 'MutateFrozen': case 'MutateGlobal': { errors.pushDiagnostic(effect.error); @@ -612,10 +628,74 @@ type Node = { | {kind: 'Object'} | {kind: 'Phi'} | {kind: 'Function'; function: HIRFunction}; + render: Place | null; }; + +function _printNode(node: Node): string { + const out: Array = []; + debugNode(out, node); + return out.join('\n'); +} +function debugNode(out: Array, node: Node): void { + out.push( + printIdentifier(node.id) + + printType(node.id.type) + + ` lastMutated=[${node.lastMutated}]`, + ); + if (node.transitive != null) { + out.push(` transitive=${node.transitive.kind}`); + } + if (node.local != null) { + out.push(` local=${node.local.kind}`); + } + if (node.mutationReason != null) { + out.push(` mutationReason=${node.mutationReason?.kind}`); + } + const edges: Array<{ + index: number; + direction: '<=' | '=>'; + kind: string; + id: Identifier; + }> = []; + for (const [alias, index] of node.createdFrom) { + edges.push({index, direction: '<=', kind: 'createFrom', id: alias}); + } + for (const [alias, index] of node.aliases) { + edges.push({index, direction: '<=', kind: 'alias', id: alias}); + } + for (const [alias, index] of node.maybeAliases) { + edges.push({index, direction: '<=', kind: 'alias?', id: alias}); + } + for (const [alias, index] of node.captures) { + edges.push({index, direction: '<=', kind: 'capture', id: alias}); + } + for (const edge of node.edges) { + edges.push({ + index: edge.index, + direction: '=>', + kind: edge.kind, + id: edge.node, + }); + } + edges.sort((a, b) => a.index - b.index); + for (const edge of edges) { + out.push( + ` [${edge.index}] ${edge.direction} ${edge.kind} ${printIdentifier(edge.id)}`, + ); + } +} + class AliasingState { nodes: Map = new Map(); + debug(): string { + const items: Array = []; + for (const [_id, node] of this.nodes) { + debugNode(items, node); + } + return items.join('\n'); + } + create(place: Place, value: Node['value']): void { this.nodes.set(place.identifier, { id: place.identifier, @@ -629,6 +709,7 @@ class AliasingState { lastMutated: 0, mutationReason: null, value, + render: null, }); } @@ -681,9 +762,9 @@ class AliasingState { } } - render(index: number, start: Identifier, errors: CompilerError): void { + render(index: number, start: Place, errors: CompilerError): void { const seen = new Set(); - const queue: Array = [start]; + const queue: Array = [start.identifier]; while (queue.length !== 0) { const current = queue.pop()!; if (seen.has(current)) { @@ -691,11 +772,34 @@ class AliasingState { } seen.add(current); const node = this.nodes.get(current); - if (node == null || node.transitive != null || node.local != null) { + if (node == null || isUseRefType(node.id)) { + if (DEBUG) { + console.log(` render ${printIdentifier(current)}: skip mutated/ref`); + } continue; } - if (node.value.kind === 'Function') { - appendFunctionErrors(errors, node.value.function); + if ( + node.local == null && + node.transitive == null && + node.value.kind === 'Function' + ) { + const returns = node.value.function.returns; + if ( + isJsxType(returns.identifier.type) || + (returns.identifier.type.kind === 'Phi' && + returns.identifier.type.operands.some(operand => + isJsxType(operand), + )) + ) { + appendFunctionErrors(errors, node.value.function); + } + if (DEBUG) { + console.log(` render ${printIdentifier(current)}: skip function`); + } + continue; + } + if (node.render == null) { + node.render = start; } for (const [alias, when] of node.createdFrom) { if (when >= index) { @@ -709,6 +813,12 @@ class AliasingState { } queue.push(alias); } + for (const [alias, when] of node.maybeAliases) { + if (when >= index) { + continue; + } + queue.push(alias); + } for (const [capture, when] of node.captures) { if (when >= index) { continue; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureFunctionsInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureFunctionsInRender.ts deleted file mode 100644 index ca0612d80ce..00000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureFunctionsInRender.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import {CompilerDiagnostic, CompilerError} from '..'; -import {ErrorCategory} from '../CompilerError'; -import {HIRFunction} from '../HIR'; -import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects'; -import {Result} from '../Utils/Result'; - -/** - * Checks that known-impure functions are not called during render. Examples of invalid functions to - * call during render are `Math.random()` and `Date.now()`. Users may extend this set of - * impure functions via a module type provider and specifying functions with `impure: true`. - * - * TODO: add best-effort analysis of functions which are called during render. We have variations of - * this in several of our validation passes and should unify those analyses into a reusable helper - * and use it here. - */ -export function validateNoImpureFunctionsInRender( - fn: HIRFunction, -): Result { - const errors = new CompilerError(); - for (const [, block] of fn.body.blocks) { - for (const instr of block.instructions) { - const value = instr.value; - if (value.kind === 'MethodCall' || value.kind == 'CallExpression') { - const callee = - value.kind === 'MethodCall' ? value.property : value.callee; - const signature = getFunctionCallSignature( - fn.env, - callee.identifier.type, - ); - if (signature != null && signature.impure === true) { - errors.pushDiagnostic( - CompilerDiagnostic.create({ - category: ErrorCategory.Purity, - reason: 'Cannot call impure function during render', - description: - (signature.canonicalName != null - ? `\`${signature.canonicalName}\` is an impure function. ` - : '') + - 'Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)', - suggestions: null, - }).withDetails({ - kind: 'error', - loc: callee.loc, - message: 'Cannot call impure function', - }), - ); - } - } - } - } - return errors.asResult(); -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureValuesInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureValuesInRender.ts new file mode 100644 index 00000000000..31e9f3e9230 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureValuesInRender.ts @@ -0,0 +1,291 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {CompilerDiagnostic, CompilerError, Effect} from '..'; +import { + areEqualSourceLocations, + HIRFunction, + IdentifierId, + isUseRefType, +} from '../HIR'; +import { + eachInstructionLValue, + eachInstructionValueOperand, +} from '../HIR/visitors'; +import {AliasingEffect} from '../Inference/AliasingEffects'; +import {createControlDominators} from '../Inference/ControlDominators'; +import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables'; +import {Err, Ok, Result} from '../Utils/Result'; +import {assertExhaustive, getOrInsertWith} from '../Utils/utils'; + +type ImpureEffect = Extract; +type FunctionCache = Map>; +type ImpuritySignature = {effects: Array; error: CompilerError}; + +export function validateNoImpureValuesInRender( + fn: HIRFunction, +): Result { + const impure = new Map(); + const result = inferImpureValues(fn, impure, new Map()); + + if (result.error.hasAnyErrors()) { + return Err(result.error); + } + return Ok(undefined); +} + +function inferFunctionExpressionMemo( + fn: HIRFunction, + impure: Map, + cache: FunctionCache, +): ImpuritySignature { + const key = fn.context + .map(place => `${place.identifier.id}:${impure.has(place.identifier.id)}`) + .join(','); + return getOrInsertWith( + getOrInsertWith(cache, fn, () => new Map()), + key, + () => { + return inferImpureValues(fn, impure, cache); + }, + ); +} + +function inferImpureValues( + fn: HIRFunction, + impure: Map, + cache: FunctionCache, +): ImpuritySignature { + const getBlockControl = createControlDominators(fn, place => { + return impure.has(place.identifier.id); + }); + + let hasChanges = false; + do { + hasChanges = false; + + for (const block of fn.body.blocks.values()) { + const controlPlace = getBlockControl(block.id); + const controlImpureEffect = + controlPlace != null ? impure.get(controlPlace.identifier.id) : null; + + for (const phi of block.phis) { + if (impure.has(phi.place.identifier.id)) { + // Already marked impure on a previous pass + continue; + } + let impureEffect = null; + for (const [, operand] of phi.operands) { + const operandEffect = impure.get(operand.identifier.id); + if (operandEffect != null) { + impureEffect = operandEffect; + break; + } + } + if (impureEffect != null) { + impure.set(phi.place.identifier.id, impureEffect); + hasChanges = true; + } else { + for (const [pred] of phi.operands) { + const predControl = getBlockControl(pred); + if (predControl != null) { + const predEffect = impure.get(predControl.identifier.id); + if (predEffect != null) { + impure.set(phi.place.identifier.id, predEffect); + hasChanges = true; + break; + } + } + } + } + } + + for (const instr of block.instructions) { + let impureEffect: ImpureEffect | undefined = instr.effects?.find( + (effect: AliasingEffect): effect is ImpureEffect => + effect.kind === 'Impure', + ); + + if ( + impureEffect == null && + (instr.value.kind === 'FunctionExpression' || + instr.value.kind === 'ObjectMethod') + ) { + impureEffect = instr.value.loweredFunc.func.aliasingEffects?.find( + (effect: AliasingEffect): effect is ImpureEffect => + effect.kind === 'Impure', + ); + if (impureEffect == null) { + const result = inferFunctionExpressionMemo( + instr.value.loweredFunc.func, + impure, + cache, + ); + if (!result.error.hasAnyErrors()) { + impureEffect = result.effects[0]; + } + } + } + + if (impureEffect == null) { + for (const operand of eachInstructionValueOperand(instr.value)) { + const operandEffect = impure.get(operand.identifier.id); + if (operandEffect != null) { + impureEffect = operandEffect; + break; + } + } + } + + if (impureEffect != null) { + for (const lvalue of eachInstructionLValue(instr)) { + if (isUseRefType(lvalue.identifier)) { + continue; + } + if (!impure.has(lvalue.identifier.id)) { + impure.set(lvalue.identifier.id, impureEffect); + hasChanges = true; + } + } + } + if (impureEffect != null || controlImpureEffect != null) { + for (const operand of eachInstructionValueOperand(instr.value)) { + switch (operand.effect) { + case Effect.Capture: + case Effect.Store: + case Effect.ConditionallyMutate: + case Effect.ConditionallyMutateIterator: + case Effect.Mutate: { + if ( + !impure.has(operand.identifier.id) && + isMutable(instr, operand) + ) { + impure.set( + operand.identifier.id, + (impureEffect ?? controlImpureEffect)!, + ); + hasChanges = true; + } + break; + } + case Effect.Freeze: + case Effect.Read: { + // no-op + break; + } + case Effect.Unknown: { + CompilerError.invariant(false, { + reason: 'Unexpected unknown effect', + description: null, + details: [ + { + kind: 'error', + loc: operand.loc, + message: null, + }, + ], + suggestions: null, + }); + } + default: { + assertExhaustive( + operand.effect, + `Unexpected effect kind \`${operand.effect}\``, + ); + } + } + } + } + if (impureEffect == null) { + const lvalueEffect = impure.get(instr.lvalue.identifier.id)!; + if (lvalueEffect != null) { + for (const operand of eachInstructionValueOperand(instr.value)) { + if ( + isMutable(instr, operand) && + !impure.has(operand.identifier.id) + ) { + impure.set(operand.identifier.id, lvalueEffect); + hasChanges = true; + } + } + } + } + } + + if (block.terminal.kind === 'return') { + const terminalEffect = impure.get(block.terminal.value.identifier.id); + if (terminalEffect != null && !impure.has(fn.returns.identifier.id)) { + impure.set(fn.returns.identifier.id, terminalEffect); + hasChanges = true; + } + } + } + } while (hasChanges); + + const error = new CompilerError(); + for (const block of fn.body.blocks.values()) { + for (const instr of block.instructions) { + const value = instr.value; + if ( + value.kind === 'FunctionExpression' || + value.kind === 'ObjectMethod' + ) { + const result = inferFunctionExpressionMemo( + value.loweredFunc.func, + impure, + cache, + ); + if (result.error.hasAnyErrors()) { + error.merge(result.error); + } + } + for (const effect of instr.effects ?? []) { + if ( + effect.kind !== 'Render' || + !impure.has(effect.place.identifier.id) + ) { + continue; + } + const impureEffect = impure.get(effect.place.identifier.id)!; + 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); + } + } + } + const impureEffects: Array = []; + for (const param of [...fn.context, ...fn.params, fn.returns]) { + const place = param.kind === 'Identifier' ? param : param.place; + const impureEffect = impure.get(place.identifier.id); + if (impureEffect != null) { + impureEffects.push({ + kind: 'Impure', + into: {...place}, + 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..179f1db7022 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts @@ -511,7 +511,7 @@ function validateNoRefAccessInRenderImpl( CompilerDiagnostic.create({ category: ErrorCategory.Refs, reason: 'Cannot access refs during render', - description: ERROR_DESCRIPTION, + description: REF_ERROR_DESCRIPTION, }).withDetails({ kind: 'error', loc: callee.loc, @@ -666,7 +666,7 @@ function validateNoRefAccessInRenderImpl( CompilerDiagnostic.create({ category: ErrorCategory.Refs, reason: 'Cannot access refs during render', - description: ERROR_DESCRIPTION, + description: REF_ERROR_DESCRIPTION, }) .withDetails({ kind: 'error', @@ -814,7 +814,7 @@ function guardCheck(errors: CompilerError, operand: Place, env: Env): void { CompilerDiagnostic.create({ category: ErrorCategory.Refs, reason: 'Cannot access refs during render', - description: ERROR_DESCRIPTION, + description: REF_ERROR_DESCRIPTION, }).withDetails({ kind: 'error', loc: operand.loc, @@ -838,7 +838,7 @@ function validateNoRefValueAccess( CompilerDiagnostic.create({ category: ErrorCategory.Refs, reason: 'Cannot access refs during render', - description: ERROR_DESCRIPTION, + description: REF_ERROR_DESCRIPTION, }).withDetails({ kind: 'error', loc: (type.kind === 'RefValue' && type.loc) || operand.loc, @@ -864,7 +864,7 @@ function validateNoRefPassedToFunction( CompilerDiagnostic.create({ category: ErrorCategory.Refs, reason: 'Cannot access refs during render', - description: ERROR_DESCRIPTION, + description: REF_ERROR_DESCRIPTION, }).withDetails({ kind: 'error', loc: (type.kind === 'RefValue' && type.loc) || loc, @@ -886,7 +886,7 @@ function validateNoRefUpdate( CompilerDiagnostic.create({ category: ErrorCategory.Refs, reason: 'Cannot access refs during render', - description: ERROR_DESCRIPTION, + description: REF_ERROR_DESCRIPTION, }).withDetails({ kind: 'error', loc: (type.kind === 'RefValue' && type.loc) || loc, @@ -907,7 +907,7 @@ function validateNoDirectRefValueAccess( CompilerDiagnostic.create({ category: ErrorCategory.Refs, reason: 'Cannot access refs during render', - description: ERROR_DESCRIPTION, + description: REF_ERROR_DESCRIPTION, }).withDetails({ kind: 'error', loc: type.loc ?? operand.loc, @@ -917,7 +917,7 @@ function validateNoDirectRefValueAccess( } } -const ERROR_DESCRIPTION = +export const REF_ERROR_DESCRIPTION = 'React refs are values that are not needed for rendering. Refs should only be accessed ' + 'outside of render, such as in event handlers or effects. ' + 'Accessing a ref value (the `current` property) during render can cause your component ' + diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInEffects.ts index 91de8f20671..06a5c6b9b29 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInEffects.ts @@ -202,10 +202,10 @@ function getSetStateCall( ); }; - const isRefControlledBlock: (id: BlockId) => boolean = + const isRefControlledBlock: (id: BlockId) => Place | null = enableAllowSetStateFromRefsInEffects ? createControlDominators(fn, place => isDerivedFromRef(place)) - : (): boolean => false; + : (): Place | null => null; for (const [, block] of fn.body.blocks) { if (enableAllowSetStateFromRefsInEffects) { @@ -224,7 +224,7 @@ function getSetStateCall( refDerivedValues.add(phi.place.identifier.id); } else { for (const [pred] of phi.operands) { - if (isRefControlledBlock(pred)) { + if (isRefControlledBlock(pred) != null) { refDerivedValues.add(phi.place.identifier.id); break; } @@ -337,7 +337,7 @@ function getSetStateCall( * be needed when initial layout measurements from refs need to be stored in state. */ return null; - } else if (isRefControlledBlock(block.id)) { + } else if (isRefControlledBlock(block.id) != null) { continue; } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/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.invalid-access-ref-in-reducer-init.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.expect.md index 647cf28f7b3..eefe34a438d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.expect.md @@ -26,9 +26,9 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` -Found 1 error: +Found 2 errors: -Error: Cannot access refs during render +Error: Cannot access ref value during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). @@ -36,7 +36,29 @@ error.invalid-access-ref-in-reducer-init.ts:8:4 6 | (state, action) => state + action, 7 | 0, > 8 | init => ref.current - | ^^^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render + | ^^^^^^^^^^^^^^^^^^^ Ref value is used during render + 9 | ); + 10 | + 11 | return ; + +Error: Cannot access ref value during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). + +error.invalid-access-ref-in-reducer-init.ts:11:27 + 9 | ); + 10 | +> 11 | return ; + | ^^^^^ Ref value is used during render + 12 | } + 13 | + 14 | export const FIXTURE_ENTRYPOINT = { + +error.invalid-access-ref-in-reducer-init.ts:8:4 + 6 | (state, action) => state + action, + 7 | 0, +> 8 | init => ref.current + | ^^^^^^^^^^^^^^^^^^^ Ref is initially accessed 9 | ); 10 | 11 | return ; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.expect.md index 33fcd6d188c..2b82ba0329a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.expect.md @@ -22,9 +22,9 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` -Found 1 error: +Found 2 errors: -Error: Cannot access refs during render +Error: Cannot access ref value during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). @@ -32,7 +32,29 @@ error.invalid-access-ref-in-reducer.ts:5:29 3 | function Component(props) { 4 | const ref = useRef(props.value); > 5 | const [state] = useReducer(() => ref.current, null); - | ^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render + | ^^^^^^^^^^^^^^^^^ Ref value is used during render + 6 | + 7 | return ; + 8 | } + +Error: Cannot access ref value during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). + +error.invalid-access-ref-in-reducer.ts:7:27 + 5 | const [state] = useReducer(() => ref.current, null); + 6 | +> 7 | return ; + | ^^^^^ Ref value is used during render + 8 | } + 9 | + 10 | export const FIXTURE_ENTRYPOINT = { + +error.invalid-access-ref-in-reducer.ts:5:29 + 3 | function Component(props) { + 4 | const ref = useRef(props.value); +> 5 | const [state] = useReducer(() => ref.current, null); + | ^^^^^^^^^^^^^^^^^ Ref is initially accessed 6 | 7 | return ; 8 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.expect.md index 73cead6affc..1e6696d4a4b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.expect.md @@ -20,18 +20,26 @@ function Component() { ``` Found 1 error: -Error: Cannot access refs during render +Error: Cannot access ref value during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). -error.invalid-access-ref-in-render-mutate-object-with-ref-function.ts:7:19 - 5 | const object = {}; +error.invalid-access-ref-in-render-mutate-object-with-ref-function.ts:8:15 6 | object.foo = () => ref.current; -> 7 | const refValue = object.foo(); - | ^^^^^^^^^^ This function accesses a ref value - 8 | return
{refValue}
; + 7 | const refValue = object.foo(); +> 8 | return
{refValue}
; + | ^^^^^^^^ Ref value is used during render 9 | } 10 | + +error.invalid-access-ref-in-render-mutate-object-with-ref-function.ts:6:15 + 4 | const ref = useRef(null); + 5 | const object = {}; +> 6 | object.foo = () => ref.current; + | ^^^^^^^^^^^^^^^^^ Ref is initially accessed + 7 | const refValue = object.foo(); + 8 | return
{refValue}
; + 9 | } ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.expect.md index a10db96463d..024c098ccad 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.expect.md @@ -22,9 +22,9 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` -Found 1 error: +Found 2 errors: -Error: Cannot access refs during render +Error: Cannot access ref value during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). @@ -32,7 +32,29 @@ error.invalid-access-ref-in-state-initializer.ts:5:27 3 | function Component(props) { 4 | const ref = useRef(props.value); > 5 | const [state] = useState(() => ref.current); - | ^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render + | ^^^^^^^^^^^^^^^^^ Ref value is used during render + 6 | + 7 | return ; + 8 | } + +Error: Cannot access ref value during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). + +error.invalid-access-ref-in-state-initializer.ts:7:27 + 5 | const [state] = useState(() => ref.current); + 6 | +> 7 | return ; + | ^^^^^ Ref value is used during render + 8 | } + 9 | + 10 | export const FIXTURE_ENTRYPOINT = { + +error.invalid-access-ref-in-state-initializer.ts:5:27 + 3 | function Component(props) { + 4 | const ref = useRef(props.value); +> 5 | const [state] = useState(() => ref.current); + | ^^^^^^^^^^^^^^^^^ Ref is initially accessed 6 | 7 | return ; 8 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-aliased-ref-in-callback-invoked-during-render-.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-aliased-ref-in-callback-invoked-during-render-.expect.md index 09a64d4bab2..4f8f295a563 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-aliased-ref-in-callback-invoked-during-render-.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-aliased-ref-in-callback-invoked-during-render-.expect.md @@ -21,17 +21,27 @@ function Component(props) { ``` Found 1 error: -Error: Cannot access refs during render +Error: Cannot access ref value during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). -error.invalid-aliased-ref-in-callback-invoked-during-render-.ts:9:33 - 7 | return ; +error.invalid-aliased-ref-in-callback-invoked-during-render-.ts:7:37 + 5 | const aliasedRef = ref; + 6 | const current = aliasedRef.current; +> 7 | return ; + | ^^^^^^^ Ref value is used during render 8 | }; -> 9 | return {props.items.map(item => renderItem(item))}; - | ^^^^^^^^^^^^^^^^^^^^^^^^ Cannot access ref value during render + 9 | return {props.items.map(item => renderItem(item))}; 10 | } - 11 | + +error.invalid-aliased-ref-in-callback-invoked-during-render-.ts:6:20 + 4 | const renderItem = item => { + 5 | const aliasedRef = ref; +> 6 | const current = aliasedRef.current; + | ^^^^^^^^^^^^^^^^^^ Ref is initially accessed + 7 | return ; + 8 | }; + 9 | return {props.items.map(item => renderItem(item))}; ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-capturing-ref-returning-function-in-rendered-object.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-capturing-ref-returning-function-in-rendered-object.expect.md new file mode 100644 index 00000000000..e23dc709983 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-capturing-ref-returning-function-in-rendered-object.expect.md @@ -0,0 +1,51 @@ + +## Input + +```javascript +import {useRef} from 'react'; +import {Stringify} from 'shared-runtime'; + +function Component(props) { + const ref = useRef(props.value); + const object = {}; + object.foo = () => ref.current; + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot access ref value during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). + +error.invalid-capturing-ref-returning-function-in-rendered-object.ts:8:28 + 6 | const object = {}; + 7 | object.foo = () => ref.current; +> 8 | return ; + | ^^^^^^ Ref value is used during render + 9 | } + 10 | + 11 | export const FIXTURE_ENTRYPOINT = { + +error.invalid-capturing-ref-returning-function-in-rendered-object.ts:7:15 + 5 | const ref = useRef(props.value); + 6 | const object = {}; +> 7 | object.foo = () => ref.current; + | ^^^^^^^^^^^^^^^^^ Ref is initially accessed + 8 | return ; + 9 | } + 10 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-capturing-ref-returning-function-in-rendered-object.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-capturing-ref-returning-function-in-rendered-object.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect-via-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect-via-mutation.expect.md new file mode 100644 index 00000000000..af3ebad63d6 --- /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:18 + 4 | + 5 | function Component() { +> 6 | const getDate = () => Date.now(); + | ^^^^^^^^^^^^^^^^ `Date.now` is an impure function. + 7 | const now = getDate(); + 8 | const array = []; + 9 | arrayPush(array, now); +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect-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..1f930a582ec --- /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:18 + 4 | + 5 | function Component() { +> 6 | const getDate = () => Date.now(); + | ^^^^^^^^^^^^^^^^ `Date.now` is an impure function. + 7 | const array = makeArray(getDate()); + 8 | const hasDate = identity(array); + 9 | return ; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect.js new file mode 100644 index 00000000000..4cf0e46d9d8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect.js @@ -0,0 +1,10 @@ +// @validateNoImpureFunctionsInRender + +import {identity, makeArray} from 'shared-runtime'; + +function Component() { + const getDate = () => Date.now(); + const array = makeArray(getDate()); + const hasDate = identity(array); + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call-2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call-2.expect.md new file mode 100644 index 00000000000..895e34ed432 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call-2.expect.md @@ -0,0 +1,52 @@ + +## Input + +```javascript +// @validateNoImpureFunctionsInRender + +import {identity, makeArray} from 'shared-runtime'; + +function Component() { + const now = () => Date.now(); + const f = () => { + // this should error but we currently lose track of the impurity bc + // the impure value comes from behind a call + const array = makeArray(now()); + const hasDate = identity(array); + return hasDate; + }; + const hasDate = f(); + return ; +} + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot access impure value during render + +Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). + +error.invalid-impure-functions-in-render-via-function-call-2.ts:15:23 + 13 | }; + 14 | const hasDate = f(); +> 15 | return ; + | ^^^^^^^ Cannot access impure value during render + 16 | } + 17 | + +error.invalid-impure-functions-in-render-via-function-call-2.ts:10:28 + 8 | // this should error but we currently lose track of the impurity bc + 9 | // the impure value comes from behind a call +> 10 | const array = makeArray(now()); + | ^^^ `Date.now` is an impure function. + 11 | const hasDate = identity(array); + 12 | return hasDate; + 13 | }; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call-2.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call-2.js new file mode 100644 index 00000000000..9abc485e957 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call-2.js @@ -0,0 +1,16 @@ +// @validateNoImpureFunctionsInRender + +import {identity, makeArray} from 'shared-runtime'; + +function Component() { + const now = () => Date.now(); + const f = () => { + // this should error but we currently lose track of the impurity bc + // the impure value comes from behind a call + const array = makeArray(now()); + const hasDate = identity(array); + return hasDate; + }; + const hasDate = f(); + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call.expect.md new file mode 100644 index 00000000000..76c2cd9098b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call.expect.md @@ -0,0 +1,50 @@ + +## Input + +```javascript +// @validateNoImpureFunctionsInRender + +import {identity, makeArray} from 'shared-runtime'; + +function Component() { + const now = Date.now(); + const f = () => { + const array = makeArray(now); + const hasDate = identity(array); + return hasDate; + }; + const hasDate = f(); + return ; +} + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot access impure value during render + +Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). + +error.invalid-impure-functions-in-render-via-function-call.ts:13:23 + 11 | }; + 12 | const hasDate = f(); +> 13 | return ; + | ^^^^^^^ Cannot access impure value during render + 14 | } + 15 | + +error.invalid-impure-functions-in-render-via-function-call.ts:8:28 + 6 | const now = Date.now(); + 7 | const f = () => { +> 8 | const array = makeArray(now); + | ^^^ `Date.now` is an impure function. + 9 | const hasDate = identity(array); + 10 | return hasDate; + 11 | }; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call.js new file mode 100644 index 00000000000..0ec57a4de35 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call.js @@ -0,0 +1,14 @@ +// @validateNoImpureFunctionsInRender + +import {identity, makeArray} from 'shared-runtime'; + +function Component() { + const now = Date.now(); + const f = () => { + const array = makeArray(now); + const hasDate = identity(array); + return hasDate; + }; + const hasDate = f(); + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-render-helper.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-render-helper.expect.md new file mode 100644 index 00000000000..ffed6129747 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-render-helper.expect.md @@ -0,0 +1,71 @@ + +## Input + +```javascript +// @validateNoImpureFunctionsInRender + +import {identity, makeArray} from 'shared-runtime'; + +function Component() { + const now = Date.now(); + const renderItem = () => { + const array = makeArray(now); + const hasDate = identity(array); + return ; + }; + return ; +} + +``` + + +## Error + +``` +Found 2 errors: + +Error: Cannot access impure value during render + +Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). + +error.invalid-impure-functions-in-render-via-render-helper.ts:10:25 + 8 | const array = makeArray(now); + 9 | const hasDate = identity(array); +> 10 | return ; + | ^^^^^^^ Cannot access impure value during render + 11 | }; + 12 | return ; + 13 | } + +error.invalid-impure-functions-in-render-via-render-helper.ts:6:14 + 4 | + 5 | function Component() { +> 6 | const now = Date.now(); + | ^^^^^^^^^^ `Date.now` is an impure function. + 7 | const renderItem = () => { + 8 | const array = makeArray(now); + 9 | const hasDate = identity(array); + +Error: Cannot access impure value during render + +Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). + +error.invalid-impure-functions-in-render-via-render-helper.ts:12:26 + 10 | return ; + 11 | }; +> 12 | return ; + | ^^^^^^^^^^ Cannot access impure value during render + 13 | } + 14 | + +error.invalid-impure-functions-in-render-via-render-helper.ts:6:14 + 4 | + 5 | function Component() { +> 6 | const now = Date.now(); + | ^^^^^^^^^^ `Date.now` is an impure function. + 7 | const renderItem = () => { + 8 | const array = makeArray(now); + 9 | const hasDate = identity(array); +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-render-helper.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-render-helper.js new file mode 100644 index 00000000000..d1d6fe7a035 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-render-helper.js @@ -0,0 +1,13 @@ +// @validateNoImpureFunctionsInRender + +import {identity, makeArray} from 'shared-runtime'; + +function Component() { + const now = Date.now(); + const renderItem = () => { + const array = makeArray(now); + const hasDate = identity(array); + return ; + }; + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render.expect.md index 255da7389b3..c413d9acd33 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render.expect.md @@ -19,41 +19,65 @@ function Component() { ``` Found 3 errors: -Error: Cannot call impure function during render +Error: Cannot access impure value during render -`Date.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). +Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). + +error.invalid-impure-functions-in-render.ts:7:20 + 5 | const now = performance.now(); + 6 | const rand = Math.random(); +> 7 | return ; + | ^^^^ Cannot access impure value during render + 8 | } + 9 | error.invalid-impure-functions-in-render.ts:4:15 2 | 3 | function Component() { > 4 | const date = Date.now(); - | ^^^^^^^^^^ Cannot call impure function + | ^^^^^^^^^^ `Date.now` is an impure function. 5 | const now = performance.now(); 6 | const rand = Math.random(); 7 | return ; -Error: Cannot call impure function during render +Error: Cannot access impure value during render -`performance.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). +Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). + +error.invalid-impure-functions-in-render.ts:7:31 + 5 | const now = performance.now(); + 6 | const rand = Math.random(); +> 7 | return ; + | ^^^ Cannot access impure value during render + 8 | } + 9 | error.invalid-impure-functions-in-render.ts:5:14 3 | function Component() { 4 | const date = Date.now(); > 5 | const now = performance.now(); - | ^^^^^^^^^^^^^^^^^ Cannot call impure function + | ^^^^^^^^^^^^^^^^^ `performance.now` is an impure function. 6 | const rand = Math.random(); 7 | return ; 8 | } -Error: Cannot call impure function during render +Error: Cannot access impure value during render -`Math.random` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). +Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). + +error.invalid-impure-functions-in-render.ts:7:42 + 5 | const now = performance.now(); + 6 | const rand = Math.random(); +> 7 | return ; + | ^^^^ Cannot access impure value during render + 8 | } + 9 | error.invalid-impure-functions-in-render.ts:6:15 4 | const date = Date.now(); 5 | const now = performance.now(); > 6 | const rand = Math.random(); - | ^^^^^^^^^^^^^ Cannot call impure function + | ^^^^^^^^^^^^^ `Math.random` is an impure function. 7 | return ; 8 | } 9 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-value-in-render-helper.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-value-in-render-helper.expect.md new file mode 100644 index 00000000000..db33136aea2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-value-in-render-helper.expect.md @@ -0,0 +1,66 @@ + +## Input + +```javascript +// @validateNoImpureFunctionsInRender +function Component() { + const now = () => Date.now(); + const render = () => { + return
{now()}
; + }; + return
{render()}
; +} + +``` + + +## Error + +``` +Found 2 errors: + +Error: Cannot access impure value during render + +Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). + +error.invalid-impure-value-in-render-helper.ts:5:17 + 3 | const now = () => Date.now(); + 4 | const render = () => { +> 5 | return
{now()}
; + | ^^^^^ Cannot access impure value during render + 6 | }; + 7 | return
{render()}
; + 8 | } + +error.invalid-impure-value-in-render-helper.ts:3:14 + 1 | // @validateNoImpureFunctionsInRender + 2 | function Component() { +> 3 | const now = () => Date.now(); + | ^^^^^^^^^^^^^^^^ `Date.now` is an impure function. + 4 | const render = () => { + 5 | return
{now()}
; + 6 | }; + +Error: Cannot access impure value during render + +Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). + +error.invalid-impure-value-in-render-helper.ts:7:15 + 5 | return
{now()}
; + 6 | }; +> 7 | return
{render()}
; + | ^^^^^^^^ Cannot access impure value during render + 8 | } + 9 | + +error.invalid-impure-value-in-render-helper.ts:3:14 + 1 | // @validateNoImpureFunctionsInRender + 2 | function Component() { +> 3 | const now = () => Date.now(); + | ^^^^^^^^^^^^^^^^ `Date.now` is an impure function. + 4 | const render = () => { + 5 | return
{now()}
; + 6 | }; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-value-in-render-helper.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-value-in-render-helper.js new file mode 100644 index 00000000000..feecf48e843 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-value-in-render-helper.js @@ -0,0 +1,8 @@ +// @validateNoImpureFunctionsInRender +function Component() { + const now = () => Date.now(); + const render = () => { + return
{now()}
; + }; + return
{render()}
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-destructure.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-destructure.expect.md index cca903de74c..ca63557d388 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-destructure.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-destructure.expect.md @@ -16,15 +16,23 @@ function Component({ref}) { ``` Found 1 error: -Error: Cannot access refs during render +Error: Cannot access ref value during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). +error.invalid-read-ref-prop-in-render-destructure.ts:4:15 + 2 | function Component({ref}) { + 3 | const value = ref.current; +> 4 | return
{value}
; + | ^^^^^ Ref value is used during render + 5 | } + 6 | + error.invalid-read-ref-prop-in-render-destructure.ts:3:16 1 | // @validateRefAccessDuringRender @compilationMode:"infer" 2 | function Component({ref}) { > 3 | const value = ref.current; - | ^^^^^^^^^^^ Cannot access ref value during render + | ^^^^^^^^^^^ Ref is initially accessed 4 | return
{value}
; 5 | } 6 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-property-load.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-property-load.expect.md index 49b8e5d199f..dee36304b21 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-property-load.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-property-load.expect.md @@ -16,15 +16,23 @@ function Component(props) { ``` Found 1 error: -Error: Cannot access refs during render +Error: Cannot access ref value during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). +error.invalid-read-ref-prop-in-render-property-load.ts:4:15 + 2 | function Component(props) { + 3 | const value = props.ref.current; +> 4 | return
{value}
; + | ^^^^^ Ref value is used during render + 5 | } + 6 | + error.invalid-read-ref-prop-in-render-property-load.ts:3:16 1 | // @validateRefAccessDuringRender @compilationMode:"infer" 2 | function Component(props) { > 3 | const value = props.ref.current; - | ^^^^^^^^^^^^^^^^^ Cannot access ref value during render + | ^^^^^^^^^^^^^^^^^ Ref is initially accessed 4 | return
{value}
; 5 | } 6 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-access-render-unary.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-access-render-unary.expect.md index ce1be800a13..62b08209ce2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-access-render-unary.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-access-render-unary.expect.md @@ -22,57 +22,27 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` -Found 4 errors: +Found 1 error: -Error: Cannot access refs during render - -React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). - - 4 | component C() { - 5 | const r = useRef(null); -> 6 | const current = !r.current; - | ^^^^^^^^^ Cannot access ref value during render - 7 | return
{current}
; - 8 | } - 9 | - -To initialize a ref only once, check that the ref is null with the pattern `if (ref.current == null) { ref.current = ... }` - -Error: Cannot access refs during render - -React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). - - 4 | component C() { - 5 | const r = useRef(null); -> 6 | const current = !r.current; - | ^^^^^^^^^^ Cannot access ref value during render - 7 | return
{current}
; - 8 | } - 9 | - -Error: Cannot access refs during render +Error: Cannot access ref value during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). 5 | const r = useRef(null); 6 | const current = !r.current; > 7 | return
{current}
; - | ^^^^^^^ Cannot access ref value during render + | ^^^^^^^ Ref value is used during render 8 | } 9 | 10 | export const FIXTURE_ENTRYPOINT = { -Error: Cannot access refs during render - -React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). - - 5 | const r = useRef(null); - 6 | const current = !r.current; -> 7 | return
{current}
; - | ^^^^^^^ Cannot access ref value during render - 8 | } - 9 | - 10 | export const FIXTURE_ENTRYPOINT = { + 4 | component C() { + 5 | const r = useRef(null); +> 6 | const current = !r.current; + | ^^^^^^^^^ Ref is initially accessed + 7 | return
{current}
; + 8 | } + 9 | ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-in-callback-invoked-during-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-in-callback-invoked-during-render.expect.md index df1e771fa25..95b7b171785 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-in-callback-invoked-during-render.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-in-callback-invoked-during-render.expect.md @@ -20,17 +20,27 @@ function Component(props) { ``` Found 1 error: -Error: Cannot access refs during render +Error: Cannot access ref value during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). -error.invalid-ref-in-callback-invoked-during-render.ts:8:33 - 6 | return ; - 7 | }; -> 8 | return {props.items.map(item => renderItem(item))}; - | ^^^^^^^^^^^^^^^^^^^^^^^^ Cannot access ref value during render - 9 | } - 10 | +error.invalid-ref-in-callback-invoked-during-render.ts:6:37 + 4 | const renderItem = item => { + 5 | const current = ref.current; +> 6 | return ; + | ^^^^^^^ Ref value is used during render + 7 | }; + 8 | return {props.items.map(item => renderItem(item))}; + 9 | } + +error.invalid-ref-in-callback-invoked-during-render.ts:5:20 + 3 | const ref = useRef(null); + 4 | const renderItem = item => { +> 5 | const current = ref.current; + | ^^^^^^^^^^^ Ref is initially accessed + 6 | return ; + 7 | }; + 8 | return {props.items.map(item => renderItem(item))}; ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-value-as-props.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-value-as-props.expect.md index d581232b3a4..322ebdbde76 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-value-as-props.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-value-as-props.expect.md @@ -16,7 +16,7 @@ function Component(props) { ``` Found 1 error: -Error: Cannot access refs during render +Error: Cannot access ref value during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). @@ -24,7 +24,7 @@ error.invalid-ref-value-as-props.ts:4:19 2 | function Component(props) { 3 | const ref = useRef(null); > 4 | return ; - | ^^^^^^^^^^^ Cannot access ref value during render + | ^^^^^^^^^^^ Ref value is used during render 5 | } 6 | ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-event-handler-wrapper.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-event-handler-wrapper.expect.md index 718e2c81419..85fd1764ef2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-event-handler-wrapper.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-event-handler-wrapper.expect.md @@ -38,15 +38,24 @@ export const FIXTURE_ENTRYPOINT = { ``` Found 1 error: -Error: Cannot access refs during render +Error: Cannot access ref value during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). +error.ref-value-in-event-handler-wrapper.ts:19:6 + 17 | <> + 18 | +> 19 | + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Ref value is used during render + 20 | + 21 | ); + 22 | } + error.ref-value-in-event-handler-wrapper.ts:19:35 17 | <> 18 | > 19 | - | ^^^^^^^^^^^ Cannot access ref value during render + | ^^^^^^^^^^^ Ref is initially accessed 20 | 21 | ); 22 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps.expect.md index 2c864f56aff..d5674691bb9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps.expect.md @@ -2,7 +2,7 @@ ## Input ```javascript -// @validateExhaustiveMemoizationDependencies +// @validateExhaustiveMemoizationDependencies @validateRefAccessDuringRender:false import {useMemo} from 'react'; import {Stringify} from 'shared-runtime'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps.js index c0f8d28837a..feba85da7d2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps.js @@ -1,4 +1,4 @@ -// @validateExhaustiveMemoizationDependencies +// @validateExhaustiveMemoizationDependencies @validateRefAccessDuringRender:false import {useMemo} from 'react'; import {Stringify} from 'shared-runtime'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-impure-functions-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-impure-functions-in-render.expect.md index 1241971d827..aabc8d2baea 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-impure-functions-in-render.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-impure-functions-in-render.expect.md @@ -19,41 +19,65 @@ function Component() { ``` Found 3 errors: -Error: Cannot call impure function during render +Error: Cannot access impure value during render -`Date.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). +Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). + +error.invalid-impure-functions-in-render.ts:7:20 + 5 | const now = performance.now(); + 6 | const rand = Math.random(); +> 7 | return ; + | ^^^^ Cannot access impure value during render + 8 | } + 9 | error.invalid-impure-functions-in-render.ts:4:15 2 | 3 | function Component() { > 4 | const date = Date.now(); - | ^^^^^^^^^^ Cannot call impure function + | ^^^^^^^^^^ `Date.now` is an impure function. 5 | const now = performance.now(); 6 | const rand = Math.random(); 7 | return ; -Error: Cannot call impure function during render +Error: Cannot access impure value during render -`performance.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). +Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). + +error.invalid-impure-functions-in-render.ts:7:31 + 5 | const now = performance.now(); + 6 | const rand = Math.random(); +> 7 | return ; + | ^^^ Cannot access impure value during render + 8 | } + 9 | error.invalid-impure-functions-in-render.ts:5:14 3 | function Component() { 4 | const date = Date.now(); > 5 | const now = performance.now(); - | ^^^^^^^^^^^^^^^^^ Cannot call impure function + | ^^^^^^^^^^^^^^^^^ `performance.now` is an impure function. 6 | const rand = Math.random(); 7 | return ; 8 | } -Error: Cannot call impure function during render +Error: Cannot access impure value during render -`Math.random` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). +Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). + +error.invalid-impure-functions-in-render.ts:7:42 + 5 | const now = performance.now(); + 6 | const rand = Math.random(); +> 7 | return ; + | ^^^^ Cannot access impure value during render + 8 | } + 9 | error.invalid-impure-functions-in-render.ts:6:15 4 | const date = Date.now(); 5 | const now = performance.now(); > 6 | const rand = Math.random(); - | ^^^^^^^^^^^^^ Cannot call impure function + | ^^^^^^^^^^^^^ `Math.random` is an impure function. 7 | return ; 8 | } 9 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-use-impure-value-in-ref.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-use-impure-value-in-ref.expect.md new file mode 100644 index 00000000000..6e0bf7d018f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-use-impure-value-in-ref.expect.md @@ -0,0 +1,49 @@ + +## Input + +```javascript +// @validateNoImpureFunctionsInRender +import {useIdentity} from 'shared-runtime'; + +function Component() { + const f = () => Math.random(); + const ref = useRef(f()); + return
; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateNoImpureFunctionsInRender +import { useIdentity } from "shared-runtime"; + +function Component() { + const $ = _c(2); + const f = _temp; + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = f(); + $[0] = t0; + } else { + t0 = $[0]; + } + const ref = useRef(t0); + let t1; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t1 =
; + $[1] = t1; + } else { + t1 = $[1]; + } + return t1; +} +function _temp() { + return Math.random(); +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-use-impure-value-in-ref.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-use-impure-value-in-ref.js new file mode 100644 index 00000000000..4002865548a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-use-impure-value-in-ref.js @@ -0,0 +1,8 @@ +// @validateNoImpureFunctionsInRender +import {useIdentity} from 'shared-runtime'; + +function Component() { + const f = () => Math.random(); + const ref = useRef(f()); + return
; +} diff --git a/compiler/packages/eslint-plugin-react-compiler/__tests__/ImpureFunctionCallsRule-test.ts b/compiler/packages/eslint-plugin-react-compiler/__tests__/ImpureFunctionCallsRule-test.ts index f89b049d100..aa27b28822b 100644 --- a/compiler/packages/eslint-plugin-react-compiler/__tests__/ImpureFunctionCallsRule-test.ts +++ b/compiler/packages/eslint-plugin-react-compiler/__tests__/ImpureFunctionCallsRule-test.ts @@ -29,9 +29,9 @@ testRule( } `, errors: [ - makeTestCaseError('Cannot call impure function during render'), - makeTestCaseError('Cannot call impure function during render'), - makeTestCaseError('Cannot call impure function during render'), + makeTestCaseError('Cannot access impure value during render'), + makeTestCaseError('Cannot access impure value during render'), + makeTestCaseError('Cannot access impure value during render'), ], }, ],