From 6cf5bd90135823d249fb5270896f238d04ec296c Mon Sep 17 00:00:00 2001 From: Mike Vitousek Date: Fri, 11 Oct 2024 16:14:32 -0700 Subject: [PATCH] [compiler] Allow refs to be lazily initialized during render Summary: The official guidance for useRef notes an exception to the rule that refs cannot be accessed during render: to avoid recreating the ref's contents, you can test that the ref is uninitialized and then initialize it using an if statement: ``` if (ref.current == null) { ref.current = SomeExpensiveOperation() } ``` The compiler didn't recognize this exception, however, leading to code that obeyed all the official guidance for refs being rejected by the compiler. This PR fixes that, by extending the ref validation machinery with an awareness of guard operations that allow lazy initialization. We now understand `== null` and similar operations, when applied to a ref and consumed by an if terminal, as marking the consequent of the if as a block in which the ref can be safely written to. In order to do so we need to create a notion of ref ids, which link different usages of the same ref via both the ref and the ref value. ghstack-source-id: d2729274f351e1eb0268f28f629fa4c2568ebc4d Pull Request resolved: https://github.com/facebook/react/pull/31188 --- .../Validation/ValidateNoRefAccesInRender.ts | 188 +++++++++++++++--- .../allow-ref-initialization.expect.md | 42 ++++ .../compiler/allow-ref-initialization.js | 14 ++ ...ror.ref-initialization-arbitrary.expect.md | 39 ++++ .../error.ref-initialization-arbitrary.js | 16 ++ .../error.ref-initialization-call-2.expect.md | 35 ++++ .../error.ref-initialization-call-2.js | 14 ++ .../error.ref-initialization-call.expect.md | 35 ++++ .../compiler/error.ref-initialization-call.js | 14 ++ .../error.ref-initialization-linear.expect.md | 36 ++++ .../error.ref-initialization-linear.js | 15 ++ .../error.ref-initialization-nonif.expect.md | 38 ++++ .../error.ref-initialization-nonif.js | 15 ++ .../error.ref-initialization-other.expect.md | 36 ++++ .../error.ref-initialization-other.js | 15 ++ ...ref-initialization-post-access-2.expect.md | 36 ++++ .../error.ref-initialization-post-access-2.js | 15 ++ ...r.ref-initialization-post-access.expect.md | 36 ++++ .../error.ref-initialization-post-access.js | 15 ++ 19 files changed, 628 insertions(+), 26 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-initialization.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-initialization.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-arbitrary.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-arbitrary.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call-2.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call-2.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-linear.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-linear.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-nonif.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-nonif.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-other.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-other.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-post-access-2.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-post-access-2.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-post-access.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-post-access.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts index 7fb835e0496c6..efc8211f937b7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts @@ -7,8 +7,8 @@ import {CompilerError, ErrorSeverity} from '../CompilerError'; import { + BlockId, HIRFunction, - Identifier, IdentifierId, Place, SourceLocation, @@ -17,6 +17,7 @@ import { isUseRefType, } from '../HIR'; import { + eachInstructionOperand, eachInstructionValueOperand, eachPatternOperand, eachTerminalOperand, @@ -44,11 +45,32 @@ import {Err, Ok, Result} from '../Utils/Result'; * or based on property name alone (`foo.current` might be a ref). */ -type RefAccessType = {kind: 'None'} | RefAccessRefType; +const opaqueRefId = Symbol(); +type RefId = number & {[opaqueRefId]: 'RefId'}; + +function makeRefId(id: number): RefId { + CompilerError.invariant(id >= 0 && Number.isInteger(id), { + reason: 'Expected identifier id to be a non-negative integer', + description: null, + loc: null, + suggestions: null, + }); + return id as RefId; +} +let _refId = 0; +function nextRefId(): RefId { + return makeRefId(_refId++); +} + +type RefAccessType = + | {kind: 'None'} + | {kind: 'Nullable'} + | {kind: 'Guard'; refId: RefId} + | RefAccessRefType; type RefAccessRefType = - | {kind: 'Ref'} - | {kind: 'RefValue'; loc?: SourceLocation} + | {kind: 'Ref'; refId: RefId} + | {kind: 'RefValue'; loc?: SourceLocation; refId?: RefId} | {kind: 'Structure'; value: null | RefAccessRefType; fn: null | RefFnType}; type RefFnType = {readRefEffect: boolean; returnType: RefAccessType}; @@ -82,11 +104,11 @@ export function validateNoRefAccessInRender(fn: HIRFunction): void { validateNoRefAccessInRenderImpl(fn, env).unwrap(); } -function refTypeOfType(identifier: Identifier): RefAccessType { - if (isRefValueType(identifier)) { +function refTypeOfType(place: Place): RefAccessType { + if (isRefValueType(place.identifier)) { return {kind: 'RefValue'}; - } else if (isUseRefType(identifier)) { - return {kind: 'Ref'}; + } else if (isUseRefType(place.identifier)) { + return {kind: 'Ref', refId: nextRefId()}; } else { return {kind: 'None'}; } @@ -101,6 +123,14 @@ function tyEqual(a: RefAccessType, b: RefAccessType): boolean { return true; case 'Ref': return true; + case 'Nullable': + return true; + case 'Guard': + CompilerError.invariant(b.kind === 'Guard', { + reason: 'Expected ref value', + loc: null, + }); + return a.refId === b.refId; case 'RefValue': CompilerError.invariant(b.kind === 'RefValue', { reason: 'Expected ref value', @@ -133,11 +163,17 @@ function joinRefAccessTypes(...types: Array): RefAccessType { b: RefAccessRefType, ): RefAccessRefType { if (a.kind === 'RefValue') { - return a; + if (b.kind === 'RefValue' && a.refId === b.refId) { + return a; + } + return {kind: 'RefValue'}; } else if (b.kind === 'RefValue') { return b; } else if (a.kind === 'Ref' || b.kind === 'Ref') { - return {kind: 'Ref'}; + if (a.kind === 'Ref' && b.kind === 'Ref' && a.refId === b.refId) { + return a; + } + return {kind: 'Ref', refId: nextRefId()}; } else { CompilerError.invariant( a.kind === 'Structure' && b.kind === 'Structure', @@ -178,6 +214,16 @@ function joinRefAccessTypes(...types: Array): RefAccessType { return b; } else if (b.kind === 'None') { return a; + } else if (a.kind === 'Guard' || b.kind === 'Guard') { + if (a.kind === 'Guard' && b.kind === 'Guard' && a.refId === b.refId) { + return a; + } + return {kind: 'None'}; + } else if (a.kind === 'Nullable' || b.kind === 'Nullable') { + if (a.kind === 'Nullable' && b.kind === 'Nullable') { + return a; + } + return {kind: 'None'}; } else { return joinRefAccessRefTypes(a, b); } @@ -198,13 +244,14 @@ function validateNoRefAccessInRenderImpl( } else { place = param.place; } - const type = refTypeOfType(place.identifier); + const type = refTypeOfType(place); env.set(place.identifier.id, type); } for (let i = 0; (i == 0 || env.hasChanged()) && i < 10; i++) { env.resetChanged(); returnValues = []; + const safeBlocks = new Map(); const errors = new CompilerError(); for (const [, block] of fn.body.blocks) { for (const phi of block.phis) { @@ -238,11 +285,15 @@ function validateNoRefAccessInRenderImpl( if (objType?.kind === 'Structure') { lookupType = objType.value; } else if (objType?.kind === 'Ref') { - lookupType = {kind: 'RefValue', loc: instr.loc}; + lookupType = { + kind: 'RefValue', + loc: instr.loc, + refId: objType.refId, + }; } env.set( instr.lvalue.identifier.id, - lookupType ?? refTypeOfType(instr.lvalue.identifier), + lookupType ?? refTypeOfType(instr.lvalue), ); break; } @@ -251,7 +302,7 @@ function validateNoRefAccessInRenderImpl( env.set( instr.lvalue.identifier.id, env.get(instr.value.place.identifier.id) ?? - refTypeOfType(instr.lvalue.identifier), + refTypeOfType(instr.lvalue), ); break; } @@ -260,12 +311,12 @@ function validateNoRefAccessInRenderImpl( env.set( instr.value.lvalue.place.identifier.id, env.get(instr.value.value.identifier.id) ?? - refTypeOfType(instr.value.lvalue.place.identifier), + refTypeOfType(instr.value.lvalue.place), ); env.set( instr.lvalue.identifier.id, env.get(instr.value.value.identifier.id) ?? - refTypeOfType(instr.lvalue.identifier), + refTypeOfType(instr.lvalue), ); break; } @@ -277,13 +328,10 @@ function validateNoRefAccessInRenderImpl( } env.set( instr.lvalue.identifier.id, - lookupType ?? refTypeOfType(instr.lvalue.identifier), + lookupType ?? refTypeOfType(instr.lvalue), ); for (const lval of eachPatternOperand(instr.value.lvalue.pattern)) { - env.set( - lval.identifier.id, - lookupType ?? refTypeOfType(lval.identifier), - ); + env.set(lval.identifier.id, lookupType ?? refTypeOfType(lval)); } break; } @@ -354,7 +402,11 @@ function validateNoRefAccessInRenderImpl( types.push(env.get(operand.identifier.id) ?? {kind: 'None'}); } const value = joinRefAccessTypes(...types); - if (value.kind === 'None') { + if ( + value.kind === 'None' || + value.kind === 'Guard' || + value.kind === 'Nullable' + ) { env.set(instr.lvalue.identifier.id, {kind: 'None'}); } else { env.set(instr.lvalue.identifier.id, { @@ -369,7 +421,18 @@ function validateNoRefAccessInRenderImpl( case 'PropertyStore': case 'ComputedDelete': case 'ComputedStore': { - validateNoRefAccess(errors, env, instr.value.object, instr.loc); + const safe = safeBlocks.get(block.id); + const target = env.get(instr.value.object.identifier.id); + if ( + instr.value.kind === 'PropertyStore' && + safe != null && + target?.kind === 'Ref' && + target.refId === safe + ) { + safeBlocks.delete(block.id); + } else { + validateNoRefAccess(errors, env, instr.value.object, instr.loc); + } for (const operand of eachInstructionValueOperand(instr.value)) { if (operand === instr.value.object) { continue; @@ -381,6 +444,38 @@ function validateNoRefAccessInRenderImpl( case 'StartMemoize': case 'FinishMemoize': break; + case 'Primitive': { + if (instr.value.value == null) { + env.set(instr.lvalue.identifier.id, {kind: 'Nullable'}); + } + break; + } + case 'BinaryExpression': { + const left = env.get(instr.value.left.identifier.id); + const right = env.get(instr.value.right.identifier.id); + let nullish: boolean = false; + let refId: RefId | null = null; + if (left?.kind === 'RefValue' && left.refId != null) { + refId = left.refId; + } else if (right?.kind === 'RefValue' && right.refId != null) { + refId = right.refId; + } + + if (left?.kind === 'Nullable') { + nullish = true; + } else if (right?.kind === 'Nullable') { + nullish = true; + } + + if (refId !== null && nullish) { + env.set(instr.lvalue.identifier.id, {kind: 'Guard', refId}); + } else { + for (const operand of eachInstructionValueOperand(instr.value)) { + validateNoRefValueAccess(errors, env, operand); + } + } + break; + } default: { for (const operand of eachInstructionValueOperand(instr.value)) { validateNoRefValueAccess(errors, env, operand); @@ -388,16 +483,28 @@ function validateNoRefAccessInRenderImpl( break; } } - if (isUseRefType(instr.lvalue.identifier)) { + + // Guard values are derived from ref.current, so they can only be used in if statement targets + for (const operand of eachInstructionOperand(instr)) { + guardCheck(errors, operand, env); + } + + if ( + isUseRefType(instr.lvalue.identifier) && + env.get(instr.lvalue.identifier.id)?.kind !== 'Ref' + ) { env.set( instr.lvalue.identifier.id, joinRefAccessTypes( env.get(instr.lvalue.identifier.id) ?? {kind: 'None'}, - {kind: 'Ref'}, + {kind: 'Ref', refId: nextRefId()}, ), ); } - if (isRefValueType(instr.lvalue.identifier)) { + if ( + isRefValueType(instr.lvalue.identifier) && + env.get(instr.lvalue.identifier.id)?.kind !== 'RefValue' + ) { env.set( instr.lvalue.identifier.id, joinRefAccessTypes( @@ -407,12 +514,24 @@ 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); + } + } + for (const operand of eachTerminalOperand(block.terminal)) { if (block.terminal.kind !== 'return') { validateNoRefValueAccess(errors, env, operand); + if (block.terminal.kind !== 'if') { + guardCheck(errors, operand, env); + } } else { // Allow functions containing refs to be returned, but not direct ref values validateNoDirectRefValueAccess(errors, operand, env); + guardCheck(errors, operand, env); returnValues.push(env.get(operand.identifier.id)); } } @@ -444,6 +563,23 @@ function destructure( return type; } +function guardCheck(errors: CompilerError, operand: Place, env: Env): void { + if (env.get(operand.identifier.id)?.kind === 'Guard') { + errors.push({ + severity: ErrorSeverity.InvalidReact, + reason: + 'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)', + loc: operand.loc, + description: + operand.identifier.name !== null && + operand.identifier.name.kind === 'named' + ? `Cannot access ref value \`${operand.identifier.name.value}\`` + : null, + suggestions: null, + }); + } +} + function validateNoRefValueAccess( errors: CompilerError, env: Env, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-initialization.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-initialization.expect.md new file mode 100644 index 0000000000000..560cef900ffa0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-initialization.expect.md @@ -0,0 +1,42 @@ + +## Input + +```javascript +//@flow +import {useRef} from 'react'; + +component C() { + const r = useRef(null); + if (r.current == null) { + r.current = 1; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; + +``` + +## Code + +```javascript +import { useRef } from "react"; + +function C() { + const r = useRef(null); + if (r.current == null) { + r.current = 1; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; + +``` + +### Eval output +(kind: ok) \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-initialization.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-initialization.js new file mode 100644 index 0000000000000..7c7c3d9cb90ba --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-initialization.js @@ -0,0 +1,14 @@ +//@flow +import {useRef} from 'react'; + +component C() { + const r = useRef(null); + if (r.current == null) { + r.current = 1; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-arbitrary.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-arbitrary.expect.md new file mode 100644 index 0000000000000..5509375b24699 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-arbitrary.expect.md @@ -0,0 +1,39 @@ + +## Input + +```javascript +//@flow +import {useRef} from 'react'; + +const DEFAULT_VALUE = 1; + +component C() { + const r = useRef(DEFAULT_VALUE); + if (r.current == DEFAULT_VALUE) { + r.current = 1; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; + +``` + + +## Error + +``` + 6 | component C() { + 7 | const r = useRef(DEFAULT_VALUE); +> 8 | if (r.current == DEFAULT_VALUE) { + | ^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (8:8) + +InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (9:9) + 9 | r.current = 1; + 10 | } + 11 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-arbitrary.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-arbitrary.js new file mode 100644 index 0000000000000..f7e9a9dccb2d3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-arbitrary.js @@ -0,0 +1,16 @@ +//@flow +import {useRef} from 'react'; + +const DEFAULT_VALUE = 1; + +component C() { + const r = useRef(DEFAULT_VALUE); + if (r.current == DEFAULT_VALUE) { + r.current = 1; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call-2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call-2.expect.md new file mode 100644 index 0000000000000..4adc9e052b96b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call-2.expect.md @@ -0,0 +1,35 @@ + +## Input + +```javascript +//@flow +import {useRef} from 'react'; + +component C() { + const r = useRef(null); + if (r.current == null) { + f(r); + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; + +``` + + +## Error + +``` + 5 | const r = useRef(null); + 6 | if (r.current == null) { +> 7 | f(r); + | ^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (7:7) + 8 | } + 9 | } + 10 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call-2.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call-2.js new file mode 100644 index 0000000000000..4e5a53cd3f62b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call-2.js @@ -0,0 +1,14 @@ +//@flow +import {useRef} from 'react'; + +component C() { + const r = useRef(null); + if (r.current == null) { + f(r); + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call.expect.md new file mode 100644 index 0000000000000..97179bb05da00 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call.expect.md @@ -0,0 +1,35 @@ + +## Input + +```javascript +//@flow +import {useRef} from 'react'; + +component C() { + const r = useRef(null); + if (r.current == null) { + f(r.current); + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; + +``` + + +## Error + +``` + 5 | const r = useRef(null); + 6 | if (r.current == null) { +> 7 | f(r.current); + | ^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (7:7) + 8 | } + 9 | } + 10 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call.js new file mode 100644 index 0000000000000..50288fafc4a00 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call.js @@ -0,0 +1,14 @@ +//@flow +import {useRef} from 'react'; + +component C() { + const r = useRef(null); + if (r.current == null) { + f(r.current); + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-linear.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-linear.expect.md new file mode 100644 index 0000000000000..873b4dc8e93c6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-linear.expect.md @@ -0,0 +1,36 @@ + +## Input + +```javascript +//@flow +import {useRef} from 'react'; + +component C() { + const r = useRef(null); + if (r.current == null) { + r.current = 42; + r.current = 42; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; + +``` + + +## Error + +``` + 6 | if (r.current == null) { + 7 | r.current = 42; +> 8 | r.current = 42; + | ^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (8:8) + 9 | } + 10 | } + 11 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-linear.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-linear.js new file mode 100644 index 0000000000000..23e4952045592 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-linear.js @@ -0,0 +1,15 @@ +//@flow +import {useRef} from 'react'; + +component C() { + const r = useRef(null); + if (r.current == null) { + r.current = 42; + r.current = 42; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-nonif.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-nonif.expect.md new file mode 100644 index 0000000000000..9cba870e72cd6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-nonif.expect.md @@ -0,0 +1,38 @@ + +## Input + +```javascript +//@flow +import {useRef} from 'react'; + +component C() { + const r = useRef(null); + const guard = r.current == null; + if (guard) { + r.current = 1; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; + +``` + + +## Error + +``` + 4 | component C() { + 5 | const r = useRef(null); +> 6 | const guard = r.current == null; + | ^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (6:6) + +InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `guard` (7:7) + 7 | if (guard) { + 8 | r.current = 1; + 9 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-nonif.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-nonif.js new file mode 100644 index 0000000000000..94216b3bacb32 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-nonif.js @@ -0,0 +1,15 @@ +//@flow +import {useRef} from 'react'; + +component C() { + const r = useRef(null); + const guard = r.current == null; + if (guard) { + r.current = 1; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-other.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-other.expect.md new file mode 100644 index 0000000000000..c24682e27161d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-other.expect.md @@ -0,0 +1,36 @@ + +## Input + +```javascript +//@flow +import {useRef} from 'react'; + +component C() { + const r = useRef(null); + const r2 = useRef(null); + if (r.current == null) { + r2.current = 1; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; + +``` + + +## Error + +``` + 6 | const r2 = useRef(null); + 7 | if (r.current == null) { +> 8 | r2.current = 1; + | ^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (8:8) + 9 | } + 10 | } + 11 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-other.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-other.js new file mode 100644 index 0000000000000..58abd12ac80cb --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-other.js @@ -0,0 +1,15 @@ +//@flow +import {useRef} from 'react'; + +component C() { + const r = useRef(null); + const r2 = useRef(null); + if (r.current == null) { + r2.current = 1; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-post-access-2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-post-access-2.expect.md new file mode 100644 index 0000000000000..90dbcab06ee1e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-post-access-2.expect.md @@ -0,0 +1,36 @@ + +## Input + +```javascript +//@flow +import {useRef} from 'react'; + +component C() { + const r = useRef(null); + if (r.current == null) { + r.current = 1; + } + f(r.current); +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; + +``` + + +## Error + +``` + 7 | r.current = 1; + 8 | } +> 9 | f(r.current); + | ^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (9:9) + 10 | } + 11 | + 12 | export const FIXTURE_ENTRYPOINT = { +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-post-access-2.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-post-access-2.js new file mode 100644 index 0000000000000..a8e3b124bfeb6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-post-access-2.js @@ -0,0 +1,15 @@ +//@flow +import {useRef} from 'react'; + +component C() { + const r = useRef(null); + if (r.current == null) { + r.current = 1; + } + f(r.current); +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-post-access.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-post-access.expect.md new file mode 100644 index 0000000000000..ca10ca5750e55 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-post-access.expect.md @@ -0,0 +1,36 @@ + +## Input + +```javascript +//@flow +import {useRef} from 'react'; + +component C() { + const r = useRef(null); + if (r.current == null) { + r.current = 1; + } + r.current = 1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; + +``` + + +## Error + +``` + 7 | r.current = 1; + 8 | } +> 9 | r.current = 1; + | ^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (9:9) + 10 | } + 11 | + 12 | export const FIXTURE_ENTRYPOINT = { +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-post-access.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-post-access.js new file mode 100644 index 0000000000000..ba476e6e422e1 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-post-access.js @@ -0,0 +1,15 @@ +//@flow +import {useRef} from 'react'; + +component C() { + const r = useRef(null); + if (r.current == null) { + r.current = 1; + } + r.current = 1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +};