From 4395689980a3e7d771675c99e4de42f40ea5bf0d Mon Sep 17 00:00:00 2001 From: Joseph Savona <6425824+josephsavona@users.noreply.github.com> Date: Tue, 29 Jul 2025 10:53:13 -0700 Subject: [PATCH 1/4] [compiler] ref guards apply up to fallthrough of the test (#34024) Fixes #30782 When developers do an `if (ref.current == null)` guard for lazy ref initialization, the "safe" blocks should extend up to the if's fallthrough. Previously we only allowed writing to the ref in the if consequent, but this meant that you couldn't use a ternary, logical, etc in the if body. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34024). * #34027 * #34026 * #34025 * __->__ #34024 --- .../Validation/ValidateNoRefAccessInRender.ts | 23 ++++--- ...lazy-initialization-with-logical.expect.md | 68 +++++++++++++++++++ ...ow-ref-lazy-initialization-with-logical.js | 24 +++++++ 3 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.js 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 571fe61c81..70aa4eeea8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts @@ -27,6 +27,7 @@ import { eachTerminalOperand, } from '../HIR/visitors'; import {Err, Ok, Result} from '../Utils/Result'; +import {retainWhere} from '../Utils/utils'; /** * Validates that a function does not access a ref value during render. This includes a partial check @@ -279,9 +280,10 @@ function validateNoRefAccessInRenderImpl( for (let i = 0; (i == 0 || env.hasChanged()) && i < 10; i++) { env.resetChanged(); returnValues = []; - const safeBlocks = new Map(); + const safeBlocks: Array<{block: BlockId; ref: RefId}> = []; const errors = new CompilerError(); for (const [, block] of fn.body.blocks) { + retainWhere(safeBlocks, entry => entry.block !== block.id); for (const phi of block.phis) { env.set( phi.place.identifier.id, @@ -503,15 +505,17 @@ function validateNoRefAccessInRenderImpl( case 'PropertyStore': case 'ComputedDelete': case 'ComputedStore': { - const safe = safeBlocks.get(block.id); const target = env.get(instr.value.object.identifier.id); + let safe: (typeof safeBlocks)['0'] | null | undefined = null; if ( instr.value.kind === 'PropertyStore' && - safe != null && - target?.kind === 'Ref' && - target.refId === safe + target != null && + target.kind === 'Ref' ) { - safeBlocks.delete(block.id); + safe = safeBlocks.find(entry => entry.ref === target.refId); + } + if (safe != null) { + retainWhere(safeBlocks, entry => entry !== safe); } else { validateNoRefUpdate(errors, env, instr.value.object, instr.loc); } @@ -599,8 +603,11 @@ function validateNoRefAccessInRenderImpl( if (block.terminal.kind === 'if') { const test = env.get(block.terminal.test.identifier.id); - if (test?.kind === 'Guard') { - safeBlocks.set(block.terminal.consequent, test.refId); + if ( + test?.kind === 'Guard' && + safeBlocks.find(entry => entry.ref === test.refId) == null + ) { + safeBlocks.push({block: block.terminal.fallthrough, ref: test.refId}); } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.expect.md new file mode 100644 index 0000000000..3540e842f6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.expect.md @@ -0,0 +1,68 @@ + +## Input + +```javascript +// @validateRefAccessDuringRender + +import {useRef} from 'react'; + +function Component(props) { + const ref = useRef(null); + if (ref.current == null) { + // the logical means the ref write is in a different block + // from the if consequent. this tests that the "safe" blocks + // extend up to the if's fallthrough + ref.current = props.unknownKey ?? props.value; + } + return ; +} + +function Child({ref}) { + 'use no memo'; + return ref.current; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender + +import { useRef } from "react"; + +function Component(props) { + const $ = _c(1); + const ref = useRef(null); + if (ref.current == null) { + ref.current = props.unknownKey ?? props.value; + } + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = ; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +function Child({ ref }) { + "use no memo"; + return ref.current; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ value: 42 }], +}; + +``` + +### Eval output +(kind: ok) 42 \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.js new file mode 100644 index 0000000000..2e1b03a28d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.js @@ -0,0 +1,24 @@ +// @validateRefAccessDuringRender + +import {useRef} from 'react'; + +function Component(props) { + const ref = useRef(null); + if (ref.current == null) { + // the logical means the ref write is in a different block + // from the if consequent. this tests that the "safe" blocks + // extend up to the if's fallthrough + ref.current = props.unknownKey ?? props.value; + } + return ; +} + +function Child({ref}) { + 'use no memo'; + return ref.current; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; From b56252672a0f43551bf5430baaabde3aed898389 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Tue, 29 Jul 2025 10:55:24 -0700 Subject: [PATCH 2/4] [compiler] disallow ref access in state initializer, reducer/initializer Per title, disallow ref access in `useState()` initializer function, `useReducer()` reducer, and `useReducer()` init function. --- .../Validation/ValidateNoRefAccessInRender.ts | 7 ++- ...valid-access-ref-in-reducer-init.expect.md | 45 +++++++++++++++++++ ...rror.invalid-access-ref-in-reducer-init.js | 17 +++++++ ...or.invalid-access-ref-in-reducer.expect.md | 41 +++++++++++++++++ .../error.invalid-access-ref-in-reducer.js | 13 ++++++ ...-access-ref-in-state-initializer.expect.md | 41 +++++++++++++++++ ...invalid-access-ref-in-state-initializer.js | 13 ++++++ 7 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.js 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 70aa4eeea8..c10d1bc07e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts @@ -434,7 +434,12 @@ function validateNoRefAccessInRenderImpl( * By default we check that function call operands are not refs, * ref values, or functions that can access refs. */ - if (isRefLValue || hookKind != null) { + if ( + isRefLValue || + (hookKind != null && + hookKind !== 'useState' && + hookKind !== 'useReducer') + ) { /** * Special cases: * 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 new file mode 100644 index 0000000000..29fe24a220 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.expect.md @@ -0,0 +1,45 @@ + +## Input + +```javascript +import {useReducer, useRef} from 'react'; + +function Component(props) { + const ref = useRef(props.value); + const [state] = useReducer( + (state, action) => state + action, + 0, + init => ref.current + ); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot access refs during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) + +error.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 + 9 | ); + 10 | + 11 | return ; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.js new file mode 100644 index 0000000000..df10b8a9eb --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.js @@ -0,0 +1,17 @@ +import {useReducer, useRef} from 'react'; + +function Component(props) { + const ref = useRef(props.value); + const [state] = useReducer( + (state, action) => state + action, + 0, + init => ref.current + ); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; 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 new file mode 100644 index 0000000000..f23560b4f6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.expect.md @@ -0,0 +1,41 @@ + +## Input + +```javascript +import {useReducer, useRef} from 'react'; + +function Component(props) { + const ref = useRef(props.value); + const [state] = useReducer(() => ref.current, null); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot access refs during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) + +error.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 + 6 | + 7 | return ; + 8 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.js new file mode 100644 index 0000000000..135a78e0ba --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.js @@ -0,0 +1,13 @@ +import {useReducer, useRef} from 'react'; + +function Component(props) { + const ref = useRef(props.value); + const [state] = useReducer(() => ref.current, null); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; 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 new file mode 100644 index 0000000000..dd6a64d9db --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.expect.md @@ -0,0 +1,41 @@ + +## Input + +```javascript +import {useRef, useState} from 'react'; + +function Component(props) { + const ref = useRef(props.value); + const [state] = useState(() => ref.current); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot access refs during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) + +error.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 + 6 | + 7 | return ; + 8 | } +``` + + \ 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.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.js new file mode 100644 index 0000000000..c3f233023e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.js @@ -0,0 +1,13 @@ +import {useRef, useState} from 'react'; + +function Component(props) { + const ref = useRef(props.value); + const [state] = useState(() => ref.current); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; From b5aa8e784356d6e7c3031060b70ab3e4ccc928b7 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Tue, 29 Jul 2025 10:55:24 -0700 Subject: [PATCH 3/4] [compiler] Allow assigning ref-accessing functions to objects if not mutated Allows assigning a ref-accessing function to an object so long as that object is not subsequently transitively mutated. We should likely rewrite the ref validation to use the new mutation/aliasing effects, which would provide a more consistent behavior across instruction types and require fewer special cases like this. --- .../Validation/ValidateNoRefAccessInRender.ts | 92 ++++++++++++++++--- ...o-object-property-if-not-mutated.expect.md | 52 +++++++++++ ...ction-to-object-property-if-not-mutated.js | 14 +++ ...-mutate-object-with-ref-function.expect.md | 37 ++++++++ ...-render-mutate-object-with-ref-function.js | 9 ++ ...f-added-to-dep-without-type-info.expect.md | 11 +-- 6 files changed, 196 insertions(+), 19 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.js 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 c10d1bc07e..e1c17625f4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts @@ -80,8 +80,18 @@ type RefAccessRefType = type RefFnType = {readRefEffect: boolean; returnType: RefAccessType}; -class Env extends Map { +class Env { #changed = false; + #data: Map = new Map(); + #temporaries: Map = new Map(); + + lookup(place: Place): Place { + return this.#temporaries.get(place.identifier.id) ?? place; + } + + define(place: Place, value: Place): void { + this.#temporaries.set(place.identifier.id, value); + } resetChanged(): void { this.#changed = false; @@ -91,8 +101,14 @@ class Env extends Map { return this.#changed; } - override set(key: IdentifierId, value: RefAccessType): this { - const cur = this.get(key); + get(key: IdentifierId): RefAccessType | undefined { + const operandId = this.#temporaries.get(key)?.identifier.id ?? key; + return this.#data.get(operandId); + } + + set(key: IdentifierId, value: RefAccessType): this { + const operandId = this.#temporaries.get(key)?.identifier.id ?? key; + const cur = this.#data.get(operandId); const widenedValue = joinRefAccessTypes(value, cur ?? {kind: 'None'}); if ( !(cur == null && widenedValue.kind === 'None') && @@ -100,7 +116,8 @@ class Env extends Map { ) { this.#changed = true; } - return super.set(key, widenedValue); + this.#data.set(operandId, widenedValue); + return this; } } @@ -108,9 +125,48 @@ export function validateNoRefAccessInRender( fn: HIRFunction, ): Result { const env = new Env(); + collectTemporariesSidemap(fn, env); return validateNoRefAccessInRenderImpl(fn, env).map(_ => undefined); } +function collectTemporariesSidemap(fn: HIRFunction, env: Env): void { + for (const block of fn.body.blocks.values()) { + for (const instr of block.instructions) { + const {lvalue, value} = instr; + switch (value.kind) { + case 'LoadLocal': { + const temp = env.lookup(value.place); + if (temp != null) { + env.define(lvalue, temp); + } + break; + } + case 'StoreLocal': { + const temp = env.lookup(value.value); + if (temp != null) { + env.define(lvalue, temp); + env.define(value.lvalue.place, temp); + } + break; + } + case 'PropertyLoad': { + if ( + isUseRefType(value.object.identifier) && + value.property === 'current' + ) { + continue; + } + const temp = env.lookup(value.object); + if (temp != null) { + env.define(lvalue, temp); + } + break; + } + } + } + } +} + function refTypeOfType(place: Place): RefAccessType { if (isRefValueType(place.identifier)) { return {kind: 'RefValue'}; @@ -524,11 +580,25 @@ function validateNoRefAccessInRenderImpl( } else { validateNoRefUpdate(errors, env, instr.value.object, instr.loc); } - for (const operand of eachInstructionValueOperand(instr.value)) { - if (operand === instr.value.object) { - continue; + if ( + instr.value.kind === 'ComputedDelete' || + instr.value.kind === 'ComputedStore' + ) { + validateNoRefValueAccess(errors, env, instr.value.property); + } + if ( + instr.value.kind === 'ComputedStore' || + instr.value.kind === 'PropertyStore' + ) { + validateNoDirectRefValueAccess(errors, instr.value.value, env); + const type = env.get(instr.value.value.identifier.id); + if (type != null && type.kind === 'Structure') { + let objectType: RefAccessType = type; + if (target != null) { + objectType = joinRefAccessTypes(objectType, target); + } + env.set(instr.value.object.identifier.id, objectType); } - validateNoRefValueAccess(errors, env, operand); } break; } @@ -730,11 +800,7 @@ function validateNoRefUpdate( loc: SourceLocation, ): void { const type = destructure(env.get(operand.identifier.id)); - if ( - type?.kind === 'Ref' || - type?.kind === 'RefValue' || - (type?.kind === 'Structure' && type.fn?.readRefEffect) - ) { + if (type?.kind === 'Ref' || type?.kind === 'RefValue') { errors.pushDiagnostic( CompilerDiagnostic.create({ severity: ErrorSeverity.InvalidReact, 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 new file mode 100644 index 0000000000..b5fc0a9dc7 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.expect.md @@ -0,0 +1,52 @@ + +## 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/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.js new file mode 100644 index 0000000000..2c84772dca --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.js @@ -0,0 +1,14 @@ +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}], +}; 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 new file mode 100644 index 0000000000..a70fcf39b3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.expect.md @@ -0,0 +1,37 @@ + +## Input + +```javascript +import {useRef} from 'react'; + +function Component() { + const ref = useRef(null); + const object = {}; + object.foo = () => ref.current; + const refValue = object.foo(); + return
{refValue}
; +} + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot access refs during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) + +error.invalid-access-ref-in-render-mutate-object-with-ref-function.ts:7:19 + 5 | const object = {}; + 6 | object.foo = () => ref.current; +> 7 | const refValue = object.foo(); + | ^^^^^^^^^^ This function accesses a ref value + 8 | return
{refValue}
; + 9 | } + 10 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.js new file mode 100644 index 0000000000..9d3faac764 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.js @@ -0,0 +1,9 @@ +import {useRef} from 'react'; + +function Component() { + const ref = useRef(null); + const object = {}; + object.foo = () => ref.current; + const refValue = object.foo(); + return
{refValue}
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-use-ref-added-to-dep-without-type-info.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-use-ref-added-to-dep-without-type-info.expect.md index 753db32fbd..f41ae64ce7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-use-ref-added-to-dep-without-type-info.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-use-ref-added-to-dep-without-type-info.expect.md @@ -41,14 +41,13 @@ Error: Cannot access refs during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) -error.invalid-use-ref-added-to-dep-without-type-info.ts:10:21 - 8 | // however, this is an instance of accessing a ref during render and is disallowed - 9 | // under React's rules, so we reject this input -> 10 | const x = {a, val: val.ref.current}; - | ^^^^^^^^^^^^^^^ Cannot access ref value during render +error.invalid-use-ref-added-to-dep-without-type-info.ts:12:28 + 10 | const x = {a, val: val.ref.current}; 11 | - 12 | return ; +> 12 | return ; + | ^ Cannot access ref value during render 13 | } + 14 | ``` \ No newline at end of file From 077316fb61399b99ca6c740a848a3cc18c54baba Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Tue, 29 Jul 2025 10:55:24 -0700 Subject: [PATCH 4/4] [compiler] Detect known incompatible libraries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A few libraries are known to be incompatible with memoization, whether manually via `useMemo()` or via React Compiler. This puts us in a tricky situation. On the one hand, we understand that these libraries were developed prior to our documenting the [Rules of React](https://react.dev/reference/rules), and their designs were the result of trying to deliver a great experience for their users and balance multiple priorities around DX, performance, etc. At the same time, using these libraries with memoization — and in particular with automatic memoization via React Compiler — can break apps by causing the components using these APIs not to update. Concretely, the APIs have in common that they return a function which returns different values over time, but where the function itself does not change. Memoizing the result on the identity of the function will mean that the value never changes. Developers reasonable interpret this as "React Compiler broke my code". Of course, the best solution is to work with developers of these libraries to address the root cause, and we're doing that. We've previously discussed this situation with both of the respective libraries: * React Hook Form: https://github.com/react-hook-form/react-hook-form/issues/11910#issuecomment-2135608761 * TanStack Table: https://github.com/facebook/react/issues/33057#issuecomment-2840600158 and https://github.com/TanStack/table/issues/5567 In the meantime we need to make sure that React Compiler can work out of the box as much as possible. This means teaching it about popular libraries that cannot be memoized. We also can't silently skip compilation, as this confuses users, so we need these error messages to be visible to users. To that end, this PR adds: * A flag to mark functions/hooks as incompatible * Validation against use of such functions * A default type provider to provide declarations for two known-incompatible libraries Note that Mobx is also incompatible, but the `observable()` function is called outside of the component itself, so the compiler cannot currently detect it. We may add validation for such APIs in the future. Again, we really empathize with the developers of these libraries. We've tried to word the error message non-judgementally, because we get that it's hard! We're open to feedback about the error message, please let us know. --- .../src/HIR/DefaultModuleTypeProvider.ts | 81 +++++++++++++++++++ .../src/HIR/Environment.ts | 5 +- .../src/HIR/Globals.ts | 2 + .../src/HIR/ObjectShape.ts | 1 + .../src/HIR/TypeSchema.ts | 4 + .../Inference/InferMutationAliasingEffects.ts | 20 +++++ ...alid-known-incompatible-function.expect.md | 34 ++++++++ ...ror.invalid-known-incompatible-function.js | 6 ++ ...ncompatible-hook-return-property.expect.md | 33 ++++++++ ...known-incompatible-hook-return-property.js | 6 ++ ....invalid-known-incompatible-hook.expect.md | 34 ++++++++ .../error.invalid-known-incompatible-hook.js | 6 ++ .../sprout/shared-runtime-type-provider.ts | 45 +++++++++++ 13 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/HIR/DefaultModuleTypeProvider.ts create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-function.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-function.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook-return-property.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook-return-property.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/DefaultModuleTypeProvider.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/DefaultModuleTypeProvider.ts new file mode 100644 index 0000000000..0cd65d947f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/DefaultModuleTypeProvider.ts @@ -0,0 +1,81 @@ +/** + * 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 {Effect, ValueKind} from '..'; +import {TypeConfig} from './TypeSchema'; + +/** + * Libraries developed before we officially documented the [Rules of React](https://react.dev/reference/rules) + * implement APIs which cannot be memoized safely, either via manual or automatic memoization. + * + * Any non-hook API that is designed to be called during render (not events/effects) should be safe to memoize: + * + * ```js + * function Component() { + * const {someFunction} = useLibrary(); + * // it should always be safe to memoize functions like this + * const result = useMemo(() => someFunction(), [someFunction]); + * } + * ``` + * + * However, some APIs implement "interior mutability" — mutating values rather than copying into a new value + * and setting state with the new value — which defaults such memoization. With this pattern, the function + * (`someFunction()` in the example) could return different values even though the function itself is the same. + * + * Given that we didn't have the Rules of React precisely documented prior to the introduction of React compiler, + * it's understandable that some libraries accidentally shipped APIs that break this rule. However, developers + * can easily run into pitfalls with these APIs. They may manually memoize them, which can break their app. Or + * they may try using React Compiler, and think that the compiler has broken their code. + * + * The React team is open to collaborating with library authors to help develop compatible versions of these APIs, + * and we have already reached out to the teams who own any API listed here to ensure they are aware of the issue. + */ +export function defaultModuleTypeProvider( + moduleName: string, +): TypeConfig | null { + switch (moduleName) { + case 'react-hook-form': { + return { + kind: 'object', + properties: { + useForm: { + kind: 'hook', + returnType: { + kind: 'object', + properties: { + watch: { + kind: 'function', + positionalParams: [], + restParam: Effect.Read, + calleeEffect: Effect.Read, + returnType: {kind: 'type', name: 'Any'}, + returnValueKind: ValueKind.Mutable, + knownIncompatible: `React Hook Form's \`useForm()\` API returns a \`watch()\` function which cannot be memoized safely.`, + }, + }, + }, + }, + }, + }; + } + case '@tanstack/react-table': { + return { + kind: 'object', + properties: { + useReactTable: { + kind: 'hook', + positionalParams: [], + restParam: Effect.Read, + returnType: {kind: 'type', name: 'Any'}, + knownIncompatible: `TanStack Table's \`useReactTable()\` API returns functions that cannot be memoized safely`, + }, + }, + }; + } + } + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index f94870fc03..80ea2180fb 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -49,6 +49,7 @@ import { } from './ObjectShape'; import {Scope as BabelScope, NodePath} from '@babel/traverse'; import {TypeSchema} from './TypeSchema'; +import {defaultModuleTypeProvider} from './DefaultModuleTypeProvider'; export const ReactElementSymbolSchema = z.object({ elementSymbol: z.union([ @@ -157,7 +158,9 @@ export const EnvironmentConfigSchema = z.object({ * A function that, given the name of a module, can optionally return a description * of that module's type signature. */ - moduleTypeProvider: z.nullable(z.function().args(z.string())).default(null), + moduleTypeProvider: z + .nullable(z.function().args(z.string())) + .default(defaultModuleTypeProvider), /** * A list of functions which the application compiles as macros, where 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 c3eadb89f5..89d1529180 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts @@ -908,6 +908,7 @@ export function installTypeConfig( mutableOnlyIfOperandsAreMutable: typeConfig.mutableOnlyIfOperandsAreMutable === true, aliasing: typeConfig.aliasing, + knownIncompatible: typeConfig.knownIncompatible ?? null, }); } case 'hook': { @@ -926,6 +927,7 @@ export function installTypeConfig( returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen, noAlias: typeConfig.noAlias === true, aliasing: typeConfig.aliasing, + knownIncompatible: typeConfig.knownIncompatible ?? null, }); } case 'object': { 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 eaf728db95..b20bebbae3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts @@ -331,6 +331,7 @@ export type FunctionSignature = { mutableOnlyIfOperandsAreMutable?: boolean; impure?: boolean; + knownIncompatible?: string | null | undefined; canonicalName?: string; 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 5945e3a078..8f28a1357b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts @@ -236,6 +236,7 @@ export type FunctionTypeConfig = { impure?: boolean | null | undefined; canonicalName?: string | null | undefined; aliasing?: AliasingSignatureConfig | null | undefined; + knownIncompatible?: string | null | undefined; }; export const FunctionTypeSchema: z.ZodType = z.object({ kind: z.literal('function'), @@ -249,6 +250,7 @@ export const FunctionTypeSchema: z.ZodType = z.object({ impure: z.boolean().nullable().optional(), canonicalName: z.string().nullable().optional(), aliasing: AliasingSignatureSchema.nullable().optional(), + knownIncompatible: z.string().nullable().optional(), }); export type HookTypeConfig = { @@ -259,6 +261,7 @@ export type HookTypeConfig = { returnValueKind?: ValueKind | null | undefined; noAlias?: boolean | null | undefined; aliasing?: AliasingSignatureConfig | null | undefined; + knownIncompatible?: string | null | undefined; }; export const HookTypeSchema: z.ZodType = z.object({ kind: z.literal('hook'), @@ -268,6 +271,7 @@ export const HookTypeSchema: z.ZodType = z.object({ returnValueKind: ValueKindSchema.nullable().optional(), noAlias: z.boolean().nullable().optional(), aliasing: AliasingSignatureSchema.nullable().optional(), + knownIncompatible: z.string().nullable().optional(), }); export type BuiltInTypeConfig = 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 2adf78fe05..0edde82db9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts @@ -2120,6 +2120,26 @@ function computeEffectsForLegacySignature( }), }); } + if (signature.knownIncompatible != null) { + const errors = new CompilerError(); + errors.pushDiagnostic( + CompilerDiagnostic.create({ + severity: ErrorSeverity.InvalidReact, + category: 'Use of incompatible library', + description: [ + 'This API returns functions which cannot be memoized without leading to stale UI. ' + + 'To prevent this, by default React Compiler will skip memoizing this component/hook. ' + + 'However, you may see issues if values from this API are passed to other components/hooks that are ' + + 'memoized.', + ].join(''), + }).withDetail({ + kind: 'error', + loc: receiver.loc, + message: signature.knownIncompatible, + }), + ); + throw errors; + } const stores: Array = []; const captures: Array = []; function visit(place: Place, effect: Effect): void { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-function.expect.md new file mode 100644 index 0000000000..fc1afa7b66 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-function.expect.md @@ -0,0 +1,34 @@ + +## Input + +```javascript +import {knownIncompatible} from 'ReactCompilerKnownIncompatibleTest'; + +function Component() { + const data = knownIncompatible(); + return
Error
; +} + +``` + + +## Error + +``` +Found 1 error: + +Error: Use of incompatible library + +This API returns functions which cannot be memoized without leading to stale UI. To prevent this, by default React Compiler will skip memoizing this component/hook. However, you may see issues if values from this API are passed to other components/hooks that are memoized. + +error.invalid-known-incompatible-function.ts:4:15 + 2 | + 3 | function Component() { +> 4 | const data = knownIncompatible(); + | ^^^^^^^^^^^^^^^^^ useKnownIncompatible is known to be incompatible + 5 | return
Error
; + 6 | } + 7 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-function.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-function.js new file mode 100644 index 0000000000..778b6dd045 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-function.js @@ -0,0 +1,6 @@ +import {knownIncompatible} from 'ReactCompilerKnownIncompatibleTest'; + +function Component() { + const data = knownIncompatible(); + return
Error
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook-return-property.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook-return-property.expect.md new file mode 100644 index 0000000000..7ef43ca82b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook-return-property.expect.md @@ -0,0 +1,33 @@ + +## Input + +```javascript +import {useKnownIncompatibleIndirect} from 'ReactCompilerKnownIncompatibleTest'; + +function Component() { + const {incompatible} = useKnownIncompatibleIndirect(); + return
{incompatible()}
; +} + +``` + + +## Error + +``` +Found 1 error: + +Error: Use of incompatible library + +This API returns functions which cannot be memoized without leading to stale UI. To prevent this, by default React Compiler will skip memoizing this component/hook. However, you may see issues if values from this API are passed to other components/hooks that are memoized. + +error.invalid-known-incompatible-hook-return-property.ts:5:15 + 3 | function Component() { + 4 | const {incompatible} = useKnownIncompatibleIndirect(); +> 5 | return
{incompatible()}
; + | ^^^^^^^^^^^^ useKnownIncompatibleIndirect returns an incompatible() function that is known incompatible + 6 | } + 7 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook-return-property.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook-return-property.js new file mode 100644 index 0000000000..1160ccb4dc --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook-return-property.js @@ -0,0 +1,6 @@ +import {useKnownIncompatibleIndirect} from 'ReactCompilerKnownIncompatibleTest'; + +function Component() { + const {incompatible} = useKnownIncompatibleIndirect(); + return
{incompatible()}
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook.expect.md new file mode 100644 index 0000000000..4a2e85581c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook.expect.md @@ -0,0 +1,34 @@ + +## Input + +```javascript +import {useKnownIncompatible} from 'ReactCompilerKnownIncompatibleTest'; + +function Component() { + const data = useKnownIncompatible(); + return
Error
; +} + +``` + + +## Error + +``` +Found 1 error: + +Error: Use of incompatible library + +This API returns functions which cannot be memoized without leading to stale UI. To prevent this, by default React Compiler will skip memoizing this component/hook. However, you may see issues if values from this API are passed to other components/hooks that are memoized. + +error.invalid-known-incompatible-hook.ts:4:15 + 2 | + 3 | function Component() { +> 4 | const data = useKnownIncompatible(); + | ^^^^^^^^^^^^^^^^^^^^ useKnownIncompatible is known to be incompatible + 5 | return
Error
; + 6 | } + 7 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook.js new file mode 100644 index 0000000000..618516c55c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook.js @@ -0,0 +1,6 @@ +import {useKnownIncompatible} from 'ReactCompilerKnownIncompatibleTest'; + +function Component() { + const data = useKnownIncompatible(); + return
Error
; +} diff --git a/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts b/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts index 58b007c1c7..b01a204e78 100644 --- a/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts +++ b/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts @@ -198,6 +198,51 @@ export function makeSharedRuntimeTypeProvider({ }, }, }; + } else if (moduleName === 'ReactCompilerKnownIncompatibleTest') { + /** + * Fake module used for testing validation of known incompatible + * API validation + */ + return { + kind: 'object', + properties: { + useKnownIncompatible: { + kind: 'hook', + positionalParams: [], + restParam: EffectEnum.Read, + returnType: {kind: 'type', name: 'Any'}, + knownIncompatible: `useKnownIncompatible is known to be incompatible`, + }, + useKnownIncompatibleIndirect: { + kind: 'hook', + positionalParams: [], + restParam: EffectEnum.Read, + returnType: { + kind: 'object', + properties: { + incompatible: { + kind: 'function', + positionalParams: [], + restParam: EffectEnum.Read, + calleeEffect: EffectEnum.Read, + returnType: {kind: 'type', name: 'Any'}, + returnValueKind: ValueKindEnum.Mutable, + knownIncompatible: `useKnownIncompatibleIndirect returns an incompatible() function that is known incompatible`, + }, + }, + }, + }, + knownIncompatible: { + kind: 'function', + positionalParams: [], + restParam: EffectEnum.Read, + calleeEffect: EffectEnum.Read, + returnType: {kind: 'type', name: 'Any'}, + returnValueKind: ValueKindEnum.Mutable, + knownIncompatible: `useKnownIncompatible is known to be incompatible`, + }, + }, + }; } else if (moduleName === 'ReactCompilerTest') { /** * Fake module used for testing validation that type providers return hook