diff --git a/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts b/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts index c7c8d6a161e..4a10c2f6125 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 1b76dfb43e1..4d9a791f62b 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'; @@ -293,10 +292,6 @@ function runWithEnvironment( env.logErrors(validateNoJSXInTryStatement(hir)); } - if (env.config.validateNoImpureFunctionsInRender) { - validateNoImpureFunctionsInRender(hir).unwrap(); - } - validateNoFreezingKnownMutableFunctions(hir).unwrap(); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts index 441b5d5452a..5a4d9cf8c92 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts @@ -38,7 +38,7 @@ import { addObject, } from './ObjectShape'; import {BuiltInType, ObjectType, PolyType} from './Types'; -import {TypeConfig} from './TypeSchema'; +import {AliasingSignatureConfig, TypeConfig} from './TypeSchema'; import {assertExhaustive} from '../Utils/utils'; import {isHookName} from './Environment'; import {CompilerError, SourceLocation} from '..'; @@ -626,6 +626,78 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [ // TODO: rest of Global objects ]; +const RenderHookAliasing: ( + reason: ValueReason, +) => AliasingSignatureConfig = reason => ({ + receiver: '@receiver', + params: [], + rest: '@rest', + returns: '@returns', + temporaries: [], + effects: [ + // Freeze the arguments + { + kind: 'Freeze', + value: '@rest', + reason: ValueReason.HookCaptured, + }, + // Render the arguments + { + kind: 'Render', + place: '@rest', + }, + // Returns a frozen value + { + kind: 'Create', + into: '@returns', + value: ValueKind.Frozen, + reason, + }, + // May alias any arguments into the return + { + kind: 'Alias', + from: '@rest', + into: '@returns', + }, + ], +}); + +const EffectHookAliasing: AliasingSignatureConfig = { + receiver: '@receiver', + params: [], + rest: '@rest', + returns: '@returns', + temporaries: ['@effect'], + effects: [ + // Freezes the function and deps + { + kind: 'Freeze', + value: '@rest', + reason: ValueReason.Effect, + }, + // Internally creates an effect object that captures the function and deps + { + kind: 'Create', + into: '@effect', + value: ValueKind.Frozen, + reason: ValueReason.KnownReturnSignature, + }, + // The effect stores the function and dependencies + { + kind: 'Capture', + from: '@rest', + into: '@effect', + }, + // Returns undefined + { + kind: 'Create', + into: '@returns', + value: ValueKind.Primitive, + reason: ValueReason.KnownReturnSignature, + }, + ], +}; + /* * TODO(mofeiZ): We currently only store rest param effects for hooks. * now that FeatureFlag `enableTreatHooksAsFunctions` is removed we can @@ -644,6 +716,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ hookKind: 'useContext', returnValueKind: ValueKind.Frozen, returnValueReason: ValueReason.Context, + aliasing: RenderHookAliasing(ValueReason.Context), }, BuiltInUseContextHookId, ), @@ -658,6 +731,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ hookKind: 'useState', returnValueKind: ValueKind.Frozen, returnValueReason: ValueReason.State, + aliasing: RenderHookAliasing(ValueReason.State), }), ], [ @@ -670,6 +744,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ hookKind: 'useActionState', returnValueKind: ValueKind.Frozen, returnValueReason: ValueReason.State, + aliasing: RenderHookAliasing(ValueReason.HookCaptured), }), ], [ @@ -682,6 +757,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ hookKind: 'useReducer', returnValueKind: ValueKind.Frozen, returnValueReason: ValueReason.ReducerState, + aliasing: RenderHookAliasing(ValueReason.ReducerState), }), ], [ @@ -715,6 +791,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ calleeEffect: Effect.Read, hookKind: 'useMemo', returnValueKind: ValueKind.Frozen, + aliasing: RenderHookAliasing(ValueReason.HookCaptured), }), ], [ @@ -726,6 +803,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ calleeEffect: Effect.Read, hookKind: 'useCallback', returnValueKind: ValueKind.Frozen, + aliasing: RenderHookAliasing(ValueReason.HookCaptured), }), ], [ @@ -739,41 +817,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ calleeEffect: Effect.Read, hookKind: 'useEffect', returnValueKind: ValueKind.Frozen, - aliasing: { - receiver: '@receiver', - params: [], - rest: '@rest', - returns: '@returns', - temporaries: ['@effect'], - effects: [ - // Freezes the function and deps - { - kind: 'Freeze', - value: '@rest', - reason: ValueReason.Effect, - }, - // Internally creates an effect object that captures the function and deps - { - kind: 'Create', - into: '@effect', - value: ValueKind.Frozen, - reason: ValueReason.KnownReturnSignature, - }, - // The effect stores the function and dependencies - { - kind: 'Capture', - from: '@rest', - into: '@effect', - }, - // Returns undefined - { - kind: 'Create', - into: '@returns', - value: ValueKind.Primitive, - reason: ValueReason.KnownReturnSignature, - }, - ], - }, + aliasing: EffectHookAliasing, }, BuiltInUseEffectHookId, ), @@ -789,6 +833,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ calleeEffect: Effect.Read, hookKind: 'useLayoutEffect', returnValueKind: ValueKind.Frozen, + aliasing: EffectHookAliasing, }, BuiltInUseLayoutEffectHookId, ), @@ -804,6 +849,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ calleeEffect: Effect.Read, hookKind: 'useInsertionEffect', returnValueKind: ValueKind.Frozen, + aliasing: EffectHookAliasing, }, BuiltInUseInsertionEffectHookId, ), @@ -817,6 +863,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ calleeEffect: Effect.Read, hookKind: 'useTransition', returnValueKind: ValueKind.Frozen, + aliasing: RenderHookAliasing(ValueReason.HookCaptured), }), ], [ @@ -829,6 +876,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ hookKind: 'useOptimistic', returnValueKind: ValueKind.Frozen, returnValueReason: ValueReason.State, + aliasing: RenderHookAliasing(ValueReason.HookCaptured), }), ], [ @@ -842,6 +890,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ returnType: {kind: 'Poly'}, calleeEffect: Effect.Read, returnValueKind: ValueKind.Frozen, + aliasing: RenderHookAliasing(ValueReason.HookCaptured), }, BuiltInUseOperatorId, ), diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts index c92f9e55623..d44659991e3 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,11 @@ * LICENSE file in the root directory of this source tree. */ -import {CompilerError} from '../CompilerError'; +import { + CompilerDiagnostic, + CompilerError, + ErrorCategory, +} from '../CompilerError'; import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects'; import {assertExhaustive} from '../Utils/utils'; import { @@ -190,16 +194,28 @@ function parseAliasingSignatureConfig( }; } case 'Impure': { - const place = lookup(effect.place); + const into = lookup(effect.into); return { kind: 'Impure', - place, - error: CompilerError.throwTodo({ - reason: 'Support impure effect declarations', - loc: GeneratedSource, + into, + error: CompilerDiagnostic.create({ + category: ErrorCategory.Purity, + reason: 'Cannot call impure function during render', + description: effect.reason, + }).withDetails({ + kind: 'error', + loc, + message: 'Cannot call impure function', }), }; } + case 'Render': { + const place = lookup(effect.place); + return { + kind: 'Render', + place, + }; + } case 'Apply': { const receiver = lookup(effect.receiver); const fn = lookup(effect.function); @@ -1513,6 +1529,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..21bf9a7a313 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.error.reason}`; } 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..fe13197ca76 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,23 @@ export const ApplyEffectSchema: z.ZodType = z.object({ export type ImpureEffectConfig = { kind: 'Impure'; - place: string; + into: string; + reason: string; }; export const ImpureEffectSchema: z.ZodType = z.object({ kind: z.literal('Impure'), + into: LifetimeIdSchema, + reason: z.string(), +}); + +export type RenderEffectConfig = { + kind: 'Render'; + place: string; +}; + +export const RenderEffectSchema: z.ZodType = z.object({ + kind: z.literal('Render'), place: LifetimeIdSchema, }); @@ -204,7 +216,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..8510630d22e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/AliasingEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/AliasingEffects.ts @@ -162,7 +162,7 @@ export type AliasingEffect = /** * Indicates a side-effect that is not safe during render */ - | {kind: 'Impure'; place: Place; error: CompilerDiagnostic} + | {kind: 'Impure'; into: Place; error: CompilerDiagnostic} /** * Indicates that a given place is accessed during render. Used to distingush * hook arguments that are known to be called immediately vs those used for @@ -222,6 +222,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.error.severity, + effect.error.reason, + effect.error.description, + printSourceLocation(effect.error.primaryLocation() ?? GeneratedSource), + ].join(':'); case 'Render': { return [effect.kind, effect.place.identifier.id].join(':'); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts index 4a027b87b6a..5f12b31130d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts @@ -27,11 +27,11 @@ import { InstructionKind, InstructionValue, isArrayType, - isJsxType, isMapType, isPrimitiveType, isRefOrRefValue, isSetType, + isUseRefType, makeIdentifierId, Phi, Place, @@ -70,6 +70,7 @@ import { MutationReason, } from './AliasingEffects'; import {ErrorCategory} from '../CompilerError'; +import {REF_ERROR_DESCRIPTION} from '../Validation/ValidateNoRefAccessInRender'; const DEBUG = false; @@ -569,14 +570,21 @@ function inferBlock( terminal.effects = effects.length !== 0 ? effects : null; } } else if (terminal.kind === 'return') { + terminal.effects = [ + context.internEffect({ + kind: 'Alias', + from: terminal.value, + into: context.fn.returns, + }), + ]; if (!context.isFuctionExpression) { - terminal.effects = [ + terminal.effects.push( context.internEffect({ kind: 'Freeze', value: terminal.value, reason: ValueReason.JsxCaptured, }), - ]; + ); } } } @@ -1973,6 +1981,24 @@ function computeSignatureForInstruction( into: lvalue, }); } + if ( + env.config.validateRefAccessDuringRender && + isUseRefType(value.object.identifier) + ) { + effects.push({ + kind: 'Impure', + into: lvalue, + error: CompilerDiagnostic.create({ + category: ErrorCategory.Refs, + reason: 'Cannot access refs during render', + description: REF_ERROR_DESCRIPTION, + }).withDetails({ + kind: 'error', + loc: value.loc, + message: `Cannot access ref value during render`, + }), + }); + } break; } case 'PropertyStore': @@ -2155,21 +2181,13 @@ function computeSignatureForInstruction( } } for (const prop of value.props) { - if ( - prop.kind === 'JsxAttribute' && - prop.place.identifier.type.kind === 'Function' && - (isJsxType(prop.place.identifier.type.return) || - (prop.place.identifier.type.return.kind === 'Phi' && - prop.place.identifier.type.return.operands.some(operand => - isJsxType(operand), - ))) - ) { - // Any props which return jsx are assumed to be called during render - effects.push({ - kind: 'Render', - place: prop.place, - }); + if (prop.kind === 'JsxAttribute' && /^on[A-Z]/.test(prop.name)) { + continue; } + effects.push({ + kind: 'Render', + place: prop.kind === 'JsxAttribute' ? prop.place : prop.argument, + }); } } break; @@ -2436,7 +2454,7 @@ function computeEffectsForLegacySignature( if (signature.impure && state.env.config.validateNoImpureFunctionsInRender) { effects.push({ kind: 'Impure', - place: receiver, + into: lvalue, error: CompilerDiagnostic.create({ category: ErrorCategory.Purity, reason: 'Cannot call impure function during render', @@ -2748,7 +2766,13 @@ function computeEffectsForSignature( } break; } - case 'Impure': + case 'Impure': { + const values = substitutions.get(effect.into.identifier.id) ?? []; + for (const value of values) { + effects.push({kind: effect.kind, into: value, error: effect.error}); + } + 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..97aa8dbcc97 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,6 +108,10 @@ export function inferMutationAliasingRanges( reason: MutationReason | null; }> = []; const renders: Array<{index: number; place: Place}> = []; + const impure: Array<{ + index: number; + effect: Extract; + }> = []; let index = 0; @@ -143,6 +151,19 @@ export function inferMutationAliasingRanges( kind: 'Function', function: effect.function.loweredFunc.func, }); + /* + * TODO: this is a hack to ensure that render helpers actually + * throw errors if they access impure values. We need a more + * consistent mechanism for determining functions that are + * render functions, and then should have a consistent way to + * check that all render functions follow the rules. + */ + for (const functionEffect of effect.function.loweredFunc.func + .aliasingEffects ?? []) { + if (functionEffect.kind === 'Render') { + renders.push({index: index++, place: functionEffect.place}); + } + } } else if (effect.kind === 'CreateFrom') { state.createFrom(index++, effect.from, effect.into); } else if (effect.kind === 'Assign') { @@ -197,14 +218,17 @@ 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); + } else if (effect.kind === 'Impure') { + impure.push({ + index: index++, + effect, + }); } } } @@ -214,10 +238,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') && @@ -243,7 +263,20 @@ export function inferMutationAliasingRanges( } } + for (const {index, effect} of impure) { + if (DEBUG) { + console.log( + `[${index}] impure ${printIdentifier(effect.into.identifier)}`, + ); + } + state.impure(index, effect); + } 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 +288,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 +556,23 @@ 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.impure != null) { + if (node.impure.kind === 'Impure') { + functionEffects.push({...node.impure, into: place}); + } else { + const source = state.nodes.get(node.impure.node); + if (source != null && source.impure?.kind === 'Impure') { + functionEffects.push({...source.impure, into: place}); + } + } + } + if (node != null && node.render != null) { + functionEffects.push({ + kind: 'Render', + place: place, + }); + } tracked.push(place); } for (const into of tracked) { @@ -577,7 +635,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 +669,83 @@ type Node = { | {kind: 'Object'} | {kind: 'Phi'} | {kind: 'Function'; function: HIRFunction}; + impure: + | Extract + | {kind: 'Indirect'; node: Identifier} + | null; + 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}`); + } + if (node.impure != null) { + out.push( + ` impure=${node.impure.kind === 'Impure' ? node.impure.error.reason : 'source=$' + node.impure.node.id}`, + ); + } + 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 +759,8 @@ class AliasingState { lastMutated: 0, mutationReason: null, value, + impure: null, + render: null, }); } @@ -681,9 +813,12 @@ class AliasingState { } } - render(index: number, start: Identifier, errors: CompilerError): void { + impure( + index: number, + effect: Extract, + ): void { const seen = new Set(); - const queue: Array = [start]; + const queue: Array = [effect.into.identifier]; while (queue.length !== 0) { const current = queue.pop()!; if (seen.has(current)) { @@ -691,11 +826,71 @@ class AliasingState { } seen.add(current); const node = this.nodes.get(current); - if (node == null || node.transitive != null || node.local != null) { + if (node == null || node.impure != null || isUseRefType(node.id)) { continue; } - if (node.value.kind === 'Function') { - appendFunctionErrors(errors, node.value.function); + node.impure = + node.id.id === effect.into.identifier.id + ? effect + : {kind: 'Indirect', node: effect.into.identifier}; + for (const edge of node.edges) { + if (index < edge.index) { + queue.push(edge.node); + } + } + } + } + + render(index: number, start: Place, errors: CompilerError): void { + const seen = new Set(); + const queue: Array = [start.identifier]; + while (queue.length !== 0) { + const current = queue.pop()!; + if (seen.has(current)) { + continue; + } + seen.add(current); + const node = this.nodes.get(current); + if (node == null || isUseRefType(node.id)) { + if (DEBUG) { + console.log(` render ${printIdentifier(current)}: skip mutated/ref`); + } + continue; + } + 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.impure != null && node.impure.kind === 'Impure') { + const diagnostic = node.impure.error.clone(); + if (node.id.id !== start.identifier.id) { + diagnostic.options.details.unshift({ + kind: 'error', + loc: start.loc, + message: 'Cannot reference impure value during render', + }); + } + errors.pushDiagnostic(diagnostic); + node.impure = null; + } + if (node.render == null) { + node.render = start; } for (const [alias, when] of node.createdFrom) { if (when >= index) { @@ -709,6 +904,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; @@ -761,6 +962,18 @@ class AliasingState { ) { appendFunctionErrors(errors, node.value.function); } + if (node.impure != null && node.impure.kind === 'Impure') { + const diagnostic = node.impure.error.clone(); + if (node.id.id !== start.id) { + diagnostic.options.details.unshift({ + kind: 'error', + loc: start.loc, + message: 'Cannot reference impure value during render', + }); + } + errors.pushDiagnostic(diagnostic); + node.impure = null; + } if (transitive) { if (node.transitive == null || node.transitive.kind < kind) { node.transitive = {kind, loc}; 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/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/__tests__/fixtures/compiler/error.assign-global-in-component-tag-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-component-tag-function.expect.md index b7b707b3e74..f0472f7bca6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-component-tag-function.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-component-tag-function.expect.md @@ -5,6 +5,7 @@ function Component() { const Foo = () => { someGlobal = true; + return
; }; return ; } @@ -26,9 +27,9 @@ error.assign-global-in-component-tag-function.ts:3:4 2 | const Foo = () => { > 3 | someGlobal = true; | ^^^^^^^^^^ `someGlobal` cannot be reassigned - 4 | }; - 5 | return ; - 6 | } + 4 | return
; + 5 | }; + 6 | return ; ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-component-tag-function.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-component-tag-function.js index 2982fdf7085..eaf1eceac2a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-component-tag-function.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-component-tag-function.js @@ -1,6 +1,7 @@ function Component() { const Foo = () => { someGlobal = true; + return
; }; return ; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-children.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-children.expect.md index 1f5ac0c83df..d57997b8fa6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-children.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-children.expect.md @@ -5,6 +5,7 @@ function Component() { const foo = () => { someGlobal = true; + return
; }; // Children are generally access/called during render, so // modifying a global in a children function is almost @@ -29,9 +30,9 @@ error.assign-global-in-jsx-children.ts:3:4 2 | const foo = () => { > 3 | someGlobal = true; | ^^^^^^^^^^ `someGlobal` cannot be reassigned - 4 | }; - 5 | // Children are generally access/called during render, so - 6 | // modifying a global in a children function is almost + 4 | return
; + 5 | }; + 6 | // Children are generally access/called during render, so ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-children.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-children.js index 82554e8ac43..1def89dd7d5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-children.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-children.js @@ -1,6 +1,7 @@ function Component() { const foo = () => { someGlobal = true; + return
; }; // Children are generally access/called during render, so // modifying a global in a children function is almost diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect-via-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect-via-mutation.expect.md new file mode 100644 index 00000000000..8deceb57b43 --- /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,48 @@ + +## 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 call impure function 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). + +error.invalid-impure-functions-in-render-indirect-via-mutation.ts:9:19 + 7 | const now = getDate(); + 8 | const array = []; +> 9 | arrayPush(array, now); + | ^^^ Cannot reference impure value during render + 10 | return ; + 11 | } + 12 | + +error.invalid-impure-functions-in-render-indirect-via-mutation.ts:6:24 + 4 | + 5 | function Component() { +> 6 | const getDate = () => Date.now(); + | ^^^^^^^^^^ Cannot call 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..28504f6d535 --- /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,38 @@ + +## 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 call impure function 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). + +error.invalid-impure-functions-in-render-indirect.ts:6:24 + 4 | + 5 | function Component() { +> 6 | const getDate = () => Date.now(); + | ^^^^^^^^^^ Cannot call 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.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..53913dd1731 --- /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,51 @@ + +## 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 call impure function 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). + +error.invalid-impure-functions-in-render-via-function-call.ts:12:18 + 10 | return hasDate; + 11 | }; +> 12 | const hasDate = f(); + | ^ Cannot reference impure value during render + 13 | return ; + 14 | } + 15 | + +error.invalid-impure-functions-in-render-via-function-call.ts:6:14 + 4 | + 5 | function Component() { +> 6 | const now = Date.now(); + | ^^^^^^^^^^ Cannot call impure function + 7 | const f = () => { + 8 | const array = makeArray(now); + 9 | const hasDate = identity(array); +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call.js new file mode 100644 index 00000000000..0ec57a4de35 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-function-call.js @@ -0,0 +1,14 @@ +// @validateNoImpureFunctionsInRender + +import {identity, makeArray} from 'shared-runtime'; + +function Component() { + const now = Date.now(); + const f = () => { + const array = makeArray(now); + const hasDate = identity(array); + return hasDate; + }; + const hasDate = f(); + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-via-render-helper.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..fdbbc34f689 --- /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,50 @@ + +## Input + +```javascript +// @validateNoImpureFunctionsInRender + +import {identity, makeArray} from 'shared-runtime'; + +function Component() { + const now = Date.now(); + const renderItem = () => { + const array = makeArray(now); + const hasDate = identity(array); + return ; + }; + return ; +} + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot call impure function 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). + +error.invalid-impure-functions-in-render-via-render-helper.ts:8:28 + 6 | const now = Date.now(); + 7 | const renderItem = () => { +> 8 | const array = makeArray(now); + | ^^^ Cannot reference impure value during render + 9 | const hasDate = identity(array); + 10 | return ; + 11 | }; + +error.invalid-impure-functions-in-render-via-render-helper.ts:6:14 + 4 | + 5 | function Component() { +> 6 | const now = Date.now(); + | ^^^^^^^^^^ Cannot call 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..c91df50aa88 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 @@ -23,6 +23,14 @@ Error: Cannot call impure function 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). +error.invalid-impure-functions-in-render.ts:7:20 + 5 | const now = performance.now(); + 6 | const rand = Math.random(); +> 7 | return ; + | ^^^^ Cannot reference impure value during render + 8 | } + 9 | + error.invalid-impure-functions-in-render.ts:4:15 2 | 3 | function Component() { @@ -36,6 +44,14 @@ Error: Cannot call impure function 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). +error.invalid-impure-functions-in-render.ts:7:31 + 5 | const now = performance.now(); + 6 | const rand = Math.random(); +> 7 | return ; + | ^^^ Cannot reference impure value during render + 8 | } + 9 | + error.invalid-impure-functions-in-render.ts:5:14 3 | function Component() { 4 | const date = Date.now(); @@ -49,6 +65,14 @@ Error: Cannot call impure function 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). +error.invalid-impure-functions-in-render.ts:7:42 + 5 | const now = performance.now(); + 6 | const rand = Math.random(); +> 7 | return ; + | ^^^^ Cannot reference 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(); 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..66909a99ed5 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 @@ -23,6 +23,14 @@ Error: Cannot call impure function 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). +error.invalid-impure-functions-in-render.ts:7:20 + 5 | const now = performance.now(); + 6 | const rand = Math.random(); +> 7 | return ; + | ^^^^ Cannot reference impure value during render + 8 | } + 9 | + error.invalid-impure-functions-in-render.ts:4:15 2 | 3 | function Component() { @@ -36,6 +44,14 @@ Error: Cannot call impure function 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). +error.invalid-impure-functions-in-render.ts:7:31 + 5 | const now = performance.now(); + 6 | const rand = Math.random(); +> 7 | return ; + | ^^^ Cannot reference impure value during render + 8 | } + 9 | + error.invalid-impure-functions-in-render.ts:5:14 3 | function Component() { 4 | const date = Date.now(); @@ -49,6 +65,14 @@ Error: Cannot call impure function 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). +error.invalid-impure-functions-in-render.ts:7:42 + 5 | const now = performance.now(); + 6 | const rand = Math.random(); +> 7 | return ; + | ^^^^ Cannot reference 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(); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-impure-functions-in-render-via-function-call-2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-impure-functions-in-render-via-function-call-2.expect.md new file mode 100644 index 00000000000..2de3eaf3255 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-impure-functions-in-render-via-function-call-2.expect.md @@ -0,0 +1,57 @@ + +## 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 ; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateNoImpureFunctionsInRender + +import { identity, makeArray } from "shared-runtime"; + +function Component() { + const $ = _c(1); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + const now = _temp; + const f = () => { + const array = makeArray(now()); + const hasDate = identity(array); + return hasDate; + }; + + const hasDate_0 = f(); + t0 = ; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} +function _temp() { + return Date.now(); +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-impure-functions-in-render-via-function-call-2.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.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/todo.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/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..2b33b78433a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-use-impure-value-in-ref.expect.md @@ -0,0 +1,53 @@ + +## 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(3); + let t0; + let t1; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + const f = _temp; + t0 = useRef; + t1 = f(); + $[0] = t0; + $[1] = t1; + } else { + t0 = $[0]; + t1 = $[1]; + } + const ref = t0(t1); + let t2; + if ($[2] === Symbol.for("react.memo_cache_sentinel")) { + t2 =
; + $[2] = t2; + } else { + t2 = $[2]; + } + return t2; +} +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
; +}