Compare commits

..

3 Commits

Author SHA1 Message Date
Joe Savona
a974753ddf [compiler] Use aliasing effects for impurity inference 2026-01-13 18:56:35 -08:00
Joe Savona
2c4a3b9587 [compiler] Validate ref reads in render via improved impure value validation
Updates to guard against *reading* refs during render via the improved validateNoImpureValuesInRender() pass. InferMutationAliasingEffects generates `Impure` effects for ref reads, and then this pass validates that those accesses don't flow into `Render` effects. We now call the impure value validation first so that it takes priority over validateNoRefAccessInRender - the latter still has all the existing logic for now, but we can dramatically simplify it in a follow-up PR so that it only has to validate against ref writes.
2026-01-12 16:38:51 -08:00
Joe Savona
8428a7ec5f [compiler] Improve impurity/ref tracking
note: This implements the idea discussed in https://github.com/reactwg/react/discussions/389#discussioncomment-14252280

Currently we create `Impure` effects for impure functions like `Date.now()` or `Math.random()`, and then throw if the effect is reachable during render. However, impurity is a property of the resulting value: if the value isn't accessed during render then it's okay: maybe you're console-logging the time while debugging (fine), or storing the impure value into a ref and only accessing it in an effect or event handler (totally ok).

This PR updates to validate that impure values are not transitively consumed during render, building on the `Render` effect. The validation currently uses an algorithm very similar to that of InferReactivePlaces - building a set of known impure values, starting with `Impure` effects as the sources and then flowing outward via data flow and mutations. If a transitively impure value reaches a `Render` effect, it's an error. We record both the source of the impure value and where it gets rendered to make it easier to understand and fix errors:

```
Error: Cannot access impure value during render

Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).

error.invalid-impure-functions-in-render-via-render-helper.ts:10:25
   8 |     const array = makeArray(now);
   9 |     const hasDate = identity(array);
> 10 |     return <Bar hasDate={hasDate} />;
     |                          ^^^^^^^ Cannot access impure value during render
  11 |   };
  12 |   return <Foo renderItem={renderItem} />;
  13 | }

error.invalid-impure-functions-in-render-via-render-helper.ts:6:14
  4 |
  5 | function Component() {
> 6 |   const now = Date.now();
    |               ^^^^^^^^^^ `Date.now` is an impure function.
  7 |   const renderItem = () => {
  8 |     const array = makeArray(now);
  9 |     const hasDate = identity(array);
```

Impure values are allowed to flow into refs, meaning that we now allow `useRef(Date.now())` or `useRef(localFunctionThatReturnsMathDotRandom())` which would have errored previously. The next PR reuses this improved impurity tracking to validate ref access in render as well.
2026-01-12 16:38:51 -08:00
250 changed files with 2914 additions and 5794 deletions

View File

@@ -593,7 +593,6 @@ module.exports = {
mixin$Animatable: 'readonly',
MouseEventHandler: 'readonly',
NavigateEvent: 'readonly',
Partial: 'readonly',
PerformanceMeasureOptions: 'readonly',
PropagationPhases: 'readonly',
PropertyDescriptor: 'readonly',

View File

@@ -278,7 +278,6 @@ jobs:
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd compiler install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: node --version
- run: yarn test ${{ matrix.params }} --ci --shard=${{ matrix.shard }}
# Hardcoded to improve parallelism
@@ -446,7 +445,6 @@ jobs:
merge-multiple: true
- name: Display structure of build
run: ls -R build
- run: node --version
- run: yarn test --build ${{ matrix.test_params }} --shard=${{ matrix.shard }} --ci
test_build_devtools:
@@ -491,7 +489,6 @@ jobs:
merge-multiple: true
- name: Display structure of build
run: ls -R build
- run: node --version
- run: yarn test --build --project=devtools -r=experimental --shard=${{ matrix.shard }} --ci
process_artifacts_combined:

2
.gitignore vendored
View File

@@ -24,8 +24,6 @@ chrome-user-data
*.swp
*.swo
/tmp
/.worktrees
.claude/*.local.*
packages/react-devtools-core/dist
packages/react-devtools-extensions/chrome/build

View File

@@ -1,8 +1,6 @@
{
"permissions": {
"allow": [
"Bash(yarn snap:*)",
"Bash(yarn snap:build)",
"Bash(node scripts/enable-feature-flag.js:*)"
],
"deny": [],

2
compiler/.gitignore vendored
View File

@@ -8,9 +8,7 @@ dist
.vscode
!packages/playground/.vscode
testfilter.txt
.claude/settings.local.json
# forgive
*.vsix
.vscode-test

View File

@@ -1,221 +0,0 @@
# React Compiler Knowledge Base
This document contains knowledge about the React Compiler gathered during development sessions. It serves as a reference for understanding the codebase architecture and key concepts.
## Project Structure
- `packages/babel-plugin-react-compiler/` - Main compiler package
- `src/HIR/` - High-level Intermediate Representation types and utilities
- `src/Inference/` - Effect inference passes (aliasing, mutation, etc.)
- `src/Validation/` - Validation passes that check for errors
- `src/Entrypoint/Pipeline.ts` - Main compilation pipeline with pass ordering
- `src/__tests__/fixtures/compiler/` - Test fixtures
- `error.todo-*.js` - Unsupported feature, correctly throws Todo error (graceful bailout)
- `error.bug-*.js` - Known bug, throws wrong error type or incorrect behavior
- `*.expect.md` - Expected output for each fixture
## Running Tests
```bash
# Run all tests
yarn snap
# Run tests matching a pattern
# Example: yarn snap -p 'error.*'
yarn snap -p <pattern>
# Run a single fixture in debug mode. Use the path relative to the __tests__/fixtures/compiler directory
# For each step of compilation, outputs the step name and state of the compiled program
# Example: yarn snap -p simple.js -d
yarn snap -p <file-basename> -d
# Update fixture outputs (also works with -p)
yarn snap -u
```
## Version Control
This repository uses Sapling (`sl`) for version control. Sapling is similar to Mercurial: there is not staging area, but new/deleted files must be explicitlyu added/removed.
```bash
# Check status
sl status
# Add new files, remove deleted files
sl addremove
# Commit all changes
sl commit -m "Your commit message"
# Commit with multi-line message using heredoc
sl commit -m "$(cat <<'EOF'
Summary line
Detailed description here
EOF
)"
```
## Key Concepts
### HIR (High-level Intermediate Representation)
The compiler converts source code to HIR for analysis. Key types in `src/HIR/HIR.ts`:
- **HIRFunction** - A function being compiled
- `body.blocks` - Map of BasicBlocks
- `context` - Captured variables from outer scope
- `params` - Function parameters
- `returns` - The function's return place
- `aliasingEffects` - Effects that describe the function's behavior when called
- **Instruction** - A single operation
- `lvalue` - The place being assigned to
- `value` - The instruction kind (CallExpression, FunctionExpression, LoadLocal, etc.)
- `effects` - Array of AliasingEffects for this instruction
- **Terminal** - Block terminators (return, branch, etc.)
- `effects` - Array of AliasingEffects
- **Place** - A reference to a value
- `identifier.id` - Unique IdentifierId
- **Phi nodes** - Join points for values from different control flow paths
- Located at `block.phis`
- `phi.place` - The result place
- `phi.operands` - Map of predecessor block to source place
### AliasingEffects System
Effects describe data flow and operations. Defined in `src/Inference/AliasingEffects.ts`:
**Data Flow Effects:**
- `Impure` - Marks a place as containing an impure value (e.g., Date.now() result, ref.current)
- `Capture a -> b` - Value from `a` is captured into `b` (mutable capture)
- `Alias a -> b` - `b` aliases `a`
- `ImmutableCapture a -> b` - Immutable capture (like Capture but read-only)
- `Assign a -> b` - Direct assignment
- `MaybeAlias a -> b` - Possible aliasing
- `CreateFrom a -> b` - Created from source
**Mutation Effects:**
- `Mutate value` - Value is mutated
- `MutateTransitive value` - Value and transitive captures are mutated
- `MutateConditionally value` - May mutate
- `MutateTransitiveConditionally value` - May mutate transitively
**Other Effects:**
- `Render place` - Place is used in render context (JSX props, component return)
- `Freeze place` - Place is frozen (made immutable)
- `Create place` - New value created
- `CreateFunction` - Function expression created, includes `captures` array
- `Apply` - Function application with receiver, function, args, and result
### Hook Aliasing Signatures
Located in `src/HIR/Globals.ts`, hooks can define custom aliasing signatures to control how data flows through them.
**Structure:**
```typescript
aliasing: {
receiver: '@receiver', // The hook function itself
params: ['@param0'], // Named positional parameters
rest: '@rest', // Rest parameters (or null)
returns: '@returns', // Return value
temporaries: [], // Temporary values during execution
effects: [ // Array of effects to apply when hook is called
{kind: 'Freeze', value: '@param0', reason: ValueReason.HookCaptured},
{kind: 'Assign', from: '@param0', into: '@returns'},
],
}
```
**Common patterns:**
1. **RenderHookAliasing** (useState, useContext, useMemo, useCallback):
- Freezes arguments (`Freeze @rest`)
- Marks arguments as render-time (`Render @rest`)
- Creates frozen return value
- Aliases arguments to return
2. **EffectHookAliasing** (useEffect, useLayoutEffect, useInsertionEffect):
- Freezes function and deps
- Creates internal effect object
- Captures function and deps into effect
- Returns undefined
3. **Event handler hooks** (useEffectEvent):
- Freezes callback (`Freeze @fn`)
- Aliases input to return (`Assign @fn -> @returns`)
- NO Render effect (callback not called during render)
**Example: useEffectEvent**
```typescript
const UseEffectEventHook = addHook(
DEFAULT_SHAPES,
{
positionalParams: [Effect.Freeze], // Takes one positional param
restParam: null,
returnType: {kind: 'Function', ...},
calleeEffect: Effect.Read,
hookKind: 'useEffectEvent',
returnValueKind: ValueKind.Frozen,
aliasing: {
receiver: '@receiver',
params: ['@fn'], // Name for the callback parameter
rest: null,
returns: '@returns',
temporaries: [],
effects: [
{kind: 'Freeze', value: '@fn', reason: ValueReason.HookCaptured},
{kind: 'Assign', from: '@fn', into: '@returns'},
// Note: NO Render effect - callback is not called during render
],
},
},
BuiltInUseEffectEventId,
);
// Add as both names for compatibility
['useEffectEvent', UseEffectEventHook],
['experimental_useEffectEvent', UseEffectEventHook],
```
**Key insight:** If a hook is missing an `aliasing` config, it falls back to `DefaultNonmutatingHook` which includes a `Render` effect on all arguments. This can cause false positives for hooks like `useEffectEvent` whose callbacks are not called during render.
## Feature Flags
Feature flags are configured in `src/HIR/Environment.ts`, for example `enableJsxOutlining`. Test fixtures can override the active feature flags used for that fixture via a comment pragma on the first line of the fixture input, for example:
```javascript
// enableJsxOutlining @enableChangeVariableCodegen:false
...code...
```
Would enable the `enableJsxOutlining` feature and disable the `enableChangeVariableCodegen` feature.
## Debugging Tips
1. Run `yarn snap -p <fixture>` to see full HIR output with effects
2. Look for `@aliasingEffects=` on FunctionExpressions
3. Look for `Impure`, `Render`, `Capture` effects on instructions
4. Check the pass ordering in Pipeline.ts to understand when effects are populated vs validated
## Error Handling for Unsupported Features
When the compiler encounters an unsupported but known pattern, use `CompilerError.throwTodo()` instead of `CompilerError.invariant()`. Todo errors cause graceful bailouts in production; Invariant errors are hard failures indicating unexpected/invalid states.
```typescript
// Unsupported but expected pattern - graceful bailout
CompilerError.throwTodo({
reason: `Support [description of unsupported feature]`,
loc: terminal.loc,
});
// Invariant is for truly unexpected/invalid states - hard failure
CompilerError.invariant(false, {
reason: `Unexpected [thing]`,
loc: terminal.loc,
});
```

View File

@@ -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;
}

View File

@@ -96,7 +96,6 @@ import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHI
import {outlineJSX} from '../Optimization/OutlineJsx';
import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls';
import {transformFire} from '../Transform';
import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureFunctionsInRender';
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
@@ -107,6 +106,7 @@ import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
import {optimizeForSSR} from '../Optimization/OptimizeForSSR';
import {validateExhaustiveDependencies} from '../Validation/ValidateExhaustiveDependencies';
import {validateSourceLocations} from '../Validation/ValidateSourceLocations';
import {validateNoImpureValuesInRender} from '../Validation/ValidateNoImpureValuesInRender';
export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -271,10 +271,6 @@ function runWithEnvironment(
assertValidMutableRanges(hir);
}
if (env.config.validateRefAccessDuringRender) {
validateNoRefAccessInRender(hir).unwrap();
}
if (env.config.validateNoSetStateInRender) {
validateNoSetStateInRender(hir).unwrap();
}
@@ -296,8 +292,15 @@ function runWithEnvironment(
env.logErrors(validateNoJSXInTryStatement(hir));
}
if (env.config.validateNoImpureFunctionsInRender) {
validateNoImpureFunctionsInRender(hir).unwrap();
if (
env.config.validateNoImpureFunctionsInRender ||
env.config.validateRefAccessDuringRender
) {
validateNoImpureValuesInRender(hir).unwrap();
}
if (env.config.validateRefAccessDuringRender) {
validateNoRefAccessInRender(hir).unwrap();
}
validateNoFreezingKnownMutableFunctions(hir).unwrap();

View File

@@ -38,7 +38,7 @@ import {
addObject,
} from './ObjectShape';
import {BuiltInType, ObjectType, PolyType} from './Types';
import {TypeConfig} from './TypeSchema';
import {AliasingSignatureConfig, TypeConfig} from './TypeSchema';
import {assertExhaustive} from '../Utils/utils';
import {isHookName} from './Environment';
import {CompilerError, SourceLocation} from '..';
@@ -626,11 +626,136 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
// TODO: rest of Global objects
];
const RenderHookAliasing: (
reason: ValueReason,
) => AliasingSignatureConfig = reason => ({
receiver: '@receiver',
params: [],
rest: '@rest',
returns: '@returns',
temporaries: [],
effects: [
// Freeze the arguments
{
kind: 'Freeze',
value: '@rest',
reason: ValueReason.HookCaptured,
},
// Render the arguments
{
kind: 'Render',
place: '@rest',
},
// Returns a frozen value
{
kind: 'Create',
into: '@returns',
value: ValueKind.Frozen,
reason,
},
// May alias any arguments into the return
{
kind: 'Alias',
from: '@rest',
into: '@returns',
},
],
});
const EffectHookAliasing: AliasingSignatureConfig = {
receiver: '@receiver',
params: ['@fn', '@deps'],
rest: '@rest',
returns: '@returns',
temporaries: ['@effect'],
effects: [
// Freezes the function and deps
{
kind: 'Freeze',
value: '@rest',
reason: ValueReason.Effect,
},
{
kind: 'Freeze',
value: '@fn',
reason: ValueReason.Effect,
},
{
kind: 'Freeze',
value: '@deps',
reason: ValueReason.Effect,
},
// Deps are accessed during render
{
kind: 'Render',
place: '@deps',
},
// Internally creates an effect object that captures the function and deps
{
kind: 'Create',
into: '@effect',
value: ValueKind.Frozen,
reason: ValueReason.KnownReturnSignature,
},
// The effect stores the function and dependencies
{
kind: 'Capture',
from: '@rest',
into: '@effect',
},
{
kind: 'Capture',
from: '@fn',
into: '@effect',
},
// Returns undefined
{
kind: 'Create',
into: '@returns',
value: ValueKind.Primitive,
reason: ValueReason.KnownReturnSignature,
},
],
};
/*
* TODO(mofeiZ): We currently only store rest param effects for hooks.
* now that FeatureFlag `enableTreatHooksAsFunctions` is removed we can
* use positional params too (?)
*/
const useEffectEvent = addHook(
DEFAULT_SHAPES,
{
positionalParams: [],
restParam: Effect.Freeze,
returnType: {
kind: 'Function',
return: {kind: 'Poly'},
shapeId: BuiltInEffectEventId,
isConstructor: false,
},
calleeEffect: Effect.Read,
hookKind: 'useEffectEvent',
// Frozen because it should not mutate any locally-bound values
returnValueKind: ValueKind.Frozen,
aliasing: {
receiver: '@receiver',
params: ['@value'],
rest: null,
returns: '@return',
temporaries: [],
effects: [
{kind: 'Assign', from: '@value', into: '@return'},
{
kind: 'Freeze',
value: '@value',
reason: ValueReason.HookCaptured,
},
],
},
},
BuiltInUseEffectEventId,
);
const REACT_APIS: Array<[string, BuiltInType]> = [
[
'useContext',
@@ -644,6 +769,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
hookKind: 'useContext',
returnValueKind: ValueKind.Frozen,
returnValueReason: ValueReason.Context,
aliasing: RenderHookAliasing(ValueReason.Context),
},
BuiltInUseContextHookId,
),
@@ -658,6 +784,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
hookKind: 'useState',
returnValueKind: ValueKind.Frozen,
returnValueReason: ValueReason.State,
aliasing: RenderHookAliasing(ValueReason.State),
}),
],
[
@@ -670,6 +797,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
hookKind: 'useActionState',
returnValueKind: ValueKind.Frozen,
returnValueReason: ValueReason.State,
aliasing: RenderHookAliasing(ValueReason.HookCaptured),
}),
],
[
@@ -682,6 +810,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
hookKind: 'useReducer',
returnValueKind: ValueKind.Frozen,
returnValueReason: ValueReason.ReducerState,
aliasing: RenderHookAliasing(ValueReason.ReducerState),
}),
],
[
@@ -715,6 +844,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
calleeEffect: Effect.Read,
hookKind: 'useMemo',
returnValueKind: ValueKind.Frozen,
aliasing: RenderHookAliasing(ValueReason.HookCaptured),
}),
],
[
@@ -726,6 +856,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
calleeEffect: Effect.Read,
hookKind: 'useCallback',
returnValueKind: ValueKind.Frozen,
aliasing: RenderHookAliasing(ValueReason.HookCaptured),
}),
],
[
@@ -739,41 +870,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
calleeEffect: Effect.Read,
hookKind: 'useEffect',
returnValueKind: ValueKind.Frozen,
aliasing: {
receiver: '@receiver',
params: [],
rest: '@rest',
returns: '@returns',
temporaries: ['@effect'],
effects: [
// Freezes the function and deps
{
kind: 'Freeze',
value: '@rest',
reason: ValueReason.Effect,
},
// Internally creates an effect object that captures the function and deps
{
kind: 'Create',
into: '@effect',
value: ValueKind.Frozen,
reason: ValueReason.KnownReturnSignature,
},
// The effect stores the function and dependencies
{
kind: 'Capture',
from: '@rest',
into: '@effect',
},
// Returns undefined
{
kind: 'Create',
into: '@returns',
value: ValueKind.Primitive,
reason: ValueReason.KnownReturnSignature,
},
],
},
aliasing: EffectHookAliasing,
},
BuiltInUseEffectHookId,
),
@@ -789,6 +886,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
calleeEffect: Effect.Read,
hookKind: 'useLayoutEffect',
returnValueKind: ValueKind.Frozen,
aliasing: EffectHookAliasing,
},
BuiltInUseLayoutEffectHookId,
),
@@ -804,6 +902,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
calleeEffect: Effect.Read,
hookKind: 'useInsertionEffect',
returnValueKind: ValueKind.Frozen,
aliasing: EffectHookAliasing,
},
BuiltInUseInsertionEffectHookId,
),
@@ -817,6 +916,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
calleeEffect: Effect.Read,
hookKind: 'useTransition',
returnValueKind: ValueKind.Frozen,
aliasing: RenderHookAliasing(ValueReason.HookCaptured),
}),
],
[
@@ -829,6 +929,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
hookKind: 'useOptimistic',
returnValueKind: ValueKind.Frozen,
returnValueReason: ValueReason.State,
aliasing: RenderHookAliasing(ValueReason.HookCaptured),
}),
],
[
@@ -842,6 +943,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
returnType: {kind: 'Poly'},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Frozen,
aliasing: RenderHookAliasing(ValueReason.HookCaptured),
},
BuiltInUseOperatorId,
),
@@ -866,27 +968,8 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
BuiltInFireId,
),
],
[
'useEffectEvent',
addHook(
DEFAULT_SHAPES,
{
positionalParams: [],
restParam: Effect.Freeze,
returnType: {
kind: 'Function',
return: {kind: 'Poly'},
shapeId: BuiltInEffectEventId,
isConstructor: false,
},
calleeEffect: Effect.Read,
hookKind: 'useEffectEvent',
// Frozen because it should not mutate any locally-bound values
returnValueKind: ValueKind.Frozen,
},
BuiltInUseEffectEventId,
),
],
['useEffectEvent', useEffectEvent],
['experimental_useEffectEvent', useEffectEvent],
['AUTODEPS', addObject(DEFAULT_SHAPES, BuiltInAutodepsId, [])],
];

View File

@@ -1879,7 +1879,15 @@ export function isRefValueType(id: Identifier): boolean {
}
export function isUseRefType(id: Identifier): boolean {
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseRefId';
return isUseRefType_(id.type);
}
export function isUseRefType_(type: Type): boolean {
return (
(type.kind === 'Object' && type.shapeId === 'BuiltInUseRefId') ||
(type.kind === 'Phi' &&
type.operands.some(operand => isUseRefType_(operand)))
);
}
export function isUseStateType(id: Identifier): boolean {
@@ -1890,6 +1898,13 @@ export function isJsxType(type: Type): boolean {
return type.kind === 'Object' && type.shapeId === 'BuiltInJsx';
}
export function isJsxOrJsxUnionType(type: Type): boolean {
return (
(type.kind === 'Object' && type.shapeId === 'BuiltInJsx') ||
(type.kind === 'Phi' && type.operands.some(op => isJsxOrJsxUnionType(op)))
);
}
export function isRefOrRefValue(id: Identifier): boolean {
return isUseRefType(id) || isRefValueType(id);
}
@@ -2058,4 +2073,23 @@ export function getHookKindForType(
return null;
}
export function areEqualSourceLocations(
loc1: SourceLocation,
loc2: SourceLocation,
): boolean {
if (typeof loc1 === 'symbol' || typeof loc2 === 'symbol') {
return false;
}
return (
loc1.filename === loc2.filename &&
loc1.identifierName === loc2.identifierName &&
loc1.start.line === loc2.start.line &&
loc1.start.column === loc2.start.column &&
loc1.start.index === loc2.start.index &&
loc1.end.line === loc2.end.line &&
loc1.end.column === loc2.end.column &&
loc1.end.index === loc2.end.index
);
}
export * from './Types';

View File

@@ -988,7 +988,7 @@ export function createTemporaryPlace(
identifier: makeTemporaryIdentifier(env.nextIdentifierId, loc),
reactive: false,
effect: Effect.Unknown,
loc: GeneratedSource,
loc,
};
}

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError} from '../CompilerError';
import {CompilerError, ErrorCategory} from '../CompilerError';
import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects';
import {assertExhaustive} from '../Utils/utils';
import {
@@ -190,14 +190,22 @@ function parseAliasingSignatureConfig(
};
}
case 'Impure': {
const place = lookup(effect.place);
const into = lookup(effect.into);
return {
kind: 'Impure',
into,
category: ErrorCategory.Purity,
description: effect.description,
reason: effect.reason,
sourceMessage: effect.sourceMessage,
usageMessage: effect.usageMessage,
};
}
case 'Render': {
const place = lookup(effect.place);
return {
kind: 'Render',
place,
error: CompilerError.throwTodo({
reason: 'Support impure effect declarations',
loc: GeneratedSource,
}),
};
}
case 'Apply': {
@@ -1513,6 +1521,11 @@ export const DefaultNonmutatingHook = addHook(
value: '@rest',
reason: ValueReason.HookCaptured,
},
// Render the arguments
{
kind: 'Render',
place: '@rest',
},
// Returns a frozen value
{
kind: 'Create',

View File

@@ -1009,7 +1009,7 @@ export function printAliasingEffect(effect: AliasingEffect): string {
return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
}
case 'Impure': {
return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
return `Impure ${printPlaceForAliasEffect(effect.into)} reason=${effect.reason} description=${effect.description}`;
}
case 'Render': {
return `Render ${printPlaceForAliasEffect(effect.place)}`;

View File

@@ -185,11 +185,29 @@ export const ApplyEffectSchema: z.ZodType<ApplyEffectConfig> = z.object({
export type ImpureEffectConfig = {
kind: 'Impure';
place: string;
into: string;
reason: string;
description: string;
sourceMessage: string;
usageMessage: string;
};
export const ImpureEffectSchema: z.ZodType<ImpureEffectConfig> = z.object({
kind: z.literal('Impure'),
into: LifetimeIdSchema,
reason: z.string(),
description: z.string(),
sourceMessage: z.string(),
usageMessage: z.string(),
});
export type RenderEffectConfig = {
kind: 'Render';
place: string;
};
export const RenderEffectSchema: z.ZodType<RenderEffectConfig> = z.object({
kind: z.literal('Render'),
place: LifetimeIdSchema,
});
@@ -204,7 +222,8 @@ export type AliasingEffectConfig =
| ImpureEffectConfig
| MutateEffectConfig
| MutateTransitiveConditionallyConfig
| ApplyEffectConfig;
| ApplyEffectConfig
| RenderEffectConfig;
export const AliasingEffectSchema: z.ZodType<AliasingEffectConfig> = z.union([
FreezeEffectSchema,

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerDiagnostic} from '../CompilerError';
import {CompilerDiagnostic, ErrorCategory} from '../CompilerError';
import {
FunctionExpression,
GeneratedSource,
@@ -162,7 +162,15 @@ export type AliasingEffect =
/**
* Indicates a side-effect that is not safe during render
*/
| {kind: 'Impure'; place: Place; error: CompilerDiagnostic}
| {
kind: 'Impure';
into: Place;
category: ErrorCategory;
reason: string;
description: string;
usageMessage: string;
sourceMessage: string;
}
/**
* Indicates that a given place is accessed during render. Used to distingush
* hook arguments that are known to be called immediately vs those used for
@@ -222,6 +230,14 @@ export function hashEffect(effect: AliasingEffect): string {
return [effect.kind, effect.value.identifier.id, effect.reason].join(':');
}
case 'Impure':
return [
effect.kind,
effect.into.identifier.id,
effect.reason,
effect.description,
effect.usageMessage,
effect.sourceMessage,
].join(':');
case 'Render': {
return [effect.kind, effect.place.identifier.id].join(':');
}

View File

@@ -8,7 +8,7 @@
import {BlockId, computePostDominatorTree, HIRFunction, Place} from '../HIR';
import {PostDominator} from '../HIR/Dominator';
export type ControlDominators = (id: BlockId) => boolean;
export type ControlDominators = (id: BlockId) => Place | null;
/**
* Returns an object that lazily calculates whether particular blocks are controlled
@@ -23,7 +23,7 @@ export function createControlDominators(
});
const postDominatorFrontierCache = new Map<BlockId, Set<BlockId>>();
function isControlledBlock(id: BlockId): boolean {
function isControlledBlock(id: BlockId): Place | null {
let controlBlocks = postDominatorFrontierCache.get(id);
if (controlBlocks === undefined) {
controlBlocks = postDominatorFrontier(fn, postDominators, id);
@@ -35,24 +35,24 @@ export function createControlDominators(
case 'if':
case 'branch': {
if (isControlVariable(controlBlock.terminal.test)) {
return true;
return controlBlock.terminal.test;
}
break;
}
case 'switch': {
if (isControlVariable(controlBlock.terminal.test)) {
return true;
return controlBlock.terminal.test;
}
for (const case_ of controlBlock.terminal.cases) {
if (case_.test !== null && isControlVariable(case_.test)) {
return true;
return case_.test;
}
}
break;
}
}
}
return false;
return null;
}
return isControlledBlock;

View File

@@ -27,11 +27,13 @@ import {
InstructionKind,
InstructionValue,
isArrayType,
isJsxType,
isJsxOrJsxUnionType,
isMapType,
isMutableEffect,
isPrimitiveType,
isRefOrRefValue,
isSetType,
isUseRefType,
makeIdentifierId,
Phi,
Place,
@@ -70,6 +72,7 @@ import {
MutationReason,
} from './AliasingEffects';
import {ErrorCategory} from '../CompilerError';
import {REF_ERROR_DESCRIPTION} from '../Validation/ValidateNoRefAccessInRender';
const DEBUG = false;
@@ -569,14 +572,32 @@ function inferBlock(
terminal.effects = effects.length !== 0 ? effects : null;
}
} else if (terminal.kind === 'return') {
terminal.effects = [
context.internEffect({
kind: 'Alias',
from: terminal.value,
into: context.fn.returns,
}),
];
if (!context.isFuctionExpression) {
terminal.effects = [
terminal.effects.push(
context.internEffect({
kind: 'Freeze',
value: terminal.value,
reason: ValueReason.JsxCaptured,
}),
];
);
}
if (
context.fn.fnType === 'Component' ||
isJsxOrJsxUnionType(context.fn.returns.identifier.type)
) {
terminal.effects.push(
context.internEffect({
kind: 'Render',
place: terminal.value,
}),
);
}
}
}
@@ -749,17 +770,7 @@ function applyEffect(
break;
}
case 'ImmutableCapture': {
const kind = state.kind(effect.from).kind;
switch (kind) {
case ValueKind.Global:
case ValueKind.Primitive: {
// no-op: we don't need to track data flow for copy types
break;
}
default: {
effects.push(effect);
}
}
effects.push(effect);
break;
}
case 'CreateFrom': {
@@ -1061,6 +1072,17 @@ function applyEffect(
reason: new Set(fromValue.reason),
});
state.define(effect.into, value);
applyEffect(
context,
state,
{
kind: 'ImmutableCapture',
from: effect.from,
into: effect.into,
},
initialized,
effects,
);
break;
}
default: {
@@ -1966,6 +1988,11 @@ function computeSignatureForInstruction(
value: ValueKind.Primitive,
reason: ValueReason.Other,
});
effects.push({
kind: 'ImmutableCapture',
from: value.object,
into: lvalue,
});
} else {
effects.push({
kind: 'CreateFrom',
@@ -1973,6 +2000,20 @@ function computeSignatureForInstruction(
into: lvalue,
});
}
if (
env.config.validateRefAccessDuringRender &&
isUseRefType(value.object.identifier)
) {
effects.push({
kind: 'Impure',
into: lvalue,
category: ErrorCategory.Refs,
reason: `Cannot access ref value during render`,
description: REF_ERROR_DESCRIPTION,
sourceMessage: `Ref is initially accessed`,
usageMessage: `Ref value is used during render`,
});
}
break;
}
case 'PropertyStore':
@@ -2137,6 +2178,15 @@ function computeSignatureForInstruction(
into: lvalue,
});
}
if (value.children != null) {
// Children are typically called during render, not used as an event/effect callback
for (const child of value.children) {
effects.push({
kind: 'Render',
place: child,
});
}
}
if (value.kind === 'JsxExpression') {
if (value.tag.kind === 'Identifier') {
// Tags are render function, by definition they're called during render
@@ -2145,29 +2195,23 @@ function computeSignatureForInstruction(
place: value.tag,
});
}
if (value.children != null) {
// Children are typically called during render, not used as an event/effect callback
for (const child of value.children) {
effects.push({
kind: 'Render',
place: child,
});
}
}
for (const prop of value.props) {
if (
prop.kind === 'JsxAttribute' &&
prop.place.identifier.type.kind === 'Function' &&
(isJsxType(prop.place.identifier.type.return) ||
(prop.place.identifier.type.return.kind === 'Phi' &&
prop.place.identifier.type.return.operands.some(operand =>
isJsxType(operand),
)))
) {
// Any props which return jsx are assumed to be called during render
const place =
prop.kind === 'JsxAttribute' ? prop.place : prop.argument;
if (isUseRefType(place.identifier)) {
continue;
}
if (place.identifier.type.kind === 'Function') {
if (isJsxOrJsxUnionType(place.identifier.type.return)) {
effects.push({
kind: 'Render',
place,
});
}
} else {
effects.push({
kind: 'Render',
place: prop.place,
place,
});
}
}
@@ -2203,6 +2247,11 @@ function computeSignatureForInstruction(
value: ValueKind.Primitive,
reason: ValueReason.Other,
});
effects.push({
kind: 'ImmutableCapture',
from: value.value,
into: place,
});
} else if (patternItem.kind === 'Identifier') {
effects.push({
kind: 'CreateFrom',
@@ -2384,15 +2433,46 @@ function computeSignatureForInstruction(
});
break;
}
case 'BinaryExpression': {
effects.push({
kind: 'Create',
into: lvalue,
value: ValueKind.Primitive,
reason: ValueReason.Other,
});
effects.push({
kind: 'ImmutableCapture',
into: lvalue,
from: value.left,
});
effects.push({
kind: 'ImmutableCapture',
into: lvalue,
from: value.right,
});
break;
}
case 'UnaryExpression': {
effects.push({
kind: 'Create',
into: lvalue,
value: ValueKind.Primitive,
reason: ValueReason.Other,
});
effects.push({
kind: 'ImmutableCapture',
into: lvalue,
from: value.value,
});
break;
}
case 'TaggedTemplateExpression':
case 'BinaryExpression':
case 'Debugger':
case 'JSXText':
case 'MetaProperty':
case 'Primitive':
case 'RegExpLiteral':
case 'TemplateLiteral':
case 'UnaryExpression':
case 'UnsupportedNode': {
effects.push({
kind: 'Create',
@@ -2423,7 +2503,7 @@ function computeEffectsForLegacySignature(
lvalue: Place,
receiver: Place,
args: Array<Place | SpreadPattern | Hole>,
loc: SourceLocation,
_loc: SourceLocation,
): Array<AliasingEffect> {
const returnValueReason = signature.returnValueReason ?? ValueReason.Other;
const effects: Array<AliasingEffect> = [];
@@ -2436,20 +2516,18 @@ function computeEffectsForLegacySignature(
if (signature.impure && state.env.config.validateNoImpureFunctionsInRender) {
effects.push({
kind: 'Impure',
place: receiver,
error: CompilerDiagnostic.create({
category: ErrorCategory.Purity,
reason: 'Cannot call impure function during render',
description:
(signature.canonicalName != null
? `\`${signature.canonicalName}\` is an impure function. `
: '') +
'Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
}).withDetails({
kind: 'error',
loc,
message: 'Cannot call impure function',
}),
into: lvalue,
category: ErrorCategory.Purity,
reason: 'Cannot access impure value during render',
description:
'Calling an impure function can produce unstable results that update ' +
'unpredictably when the component happens to re-render. ' +
'(https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
sourceMessage:
signature.canonicalName != null
? `\`${signature.canonicalName}\` is an impure function.`
: 'This function is impure',
usageMessage: 'Cannot access impure value during render',
});
}
if (signature.knownIncompatible != null && state.env.enableValidations) {
@@ -2748,7 +2826,23 @@ function computeEffectsForSignature(
}
break;
}
case 'Impure':
case 'Impure': {
if (env.config.validateNoImpureFunctionsInRender) {
const values = substitutions.get(effect.into.identifier.id) ?? [];
for (const value of values) {
effects.push({
kind: effect.kind,
into: value,
category: effect.category,
reason: effect.reason,
description: effect.description,
sourceMessage: effect.sourceMessage,
usageMessage: effect.usageMessage,
});
}
}
break;
}
case 'MutateFrozen':
case 'MutateGlobal': {
const values = substitutions.get(effect.place.identifier.id) ?? [];

View File

@@ -19,6 +19,7 @@ import {
ValueReason,
Place,
isPrimitiveType,
isUseRefType,
} from '../HIR/HIR';
import {
eachInstructionLValue,
@@ -28,6 +29,9 @@ import {
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
import {Err, Ok, Result} from '../Utils/Result';
import {AliasingEffect, MutationReason} from './AliasingEffects';
import {printIdentifier, printType} from '../HIR/PrintHIR';
const DEBUG = false;
/**
* This pass builds an abstract model of the heap and interprets the effects of the
@@ -104,7 +108,6 @@ export function inferMutationAliasingRanges(
reason: MutationReason | null;
}> = [];
const renders: Array<{index: number; place: Place}> = [];
let index = 0;
const errors = new CompilerError();
@@ -197,14 +200,12 @@ export function inferMutationAliasingRanges(
});
} else if (
effect.kind === 'MutateFrozen' ||
effect.kind === 'MutateGlobal' ||
effect.kind === 'Impure'
effect.kind === 'MutateGlobal'
) {
errors.pushDiagnostic(effect.error);
functionEffects.push(effect);
} else if (effect.kind === 'Render') {
renders.push({index: index++, place: effect.place});
functionEffects.push(effect);
}
}
}
@@ -214,10 +215,6 @@ export function inferMutationAliasingRanges(
state.assign(index, from, into);
}
}
if (block.terminal.kind === 'return') {
state.assign(index++, block.terminal.value, fn.returns);
}
if (
(block.terminal.kind === 'maybe-throw' ||
block.terminal.kind === 'return') &&
@@ -227,23 +224,31 @@ export function inferMutationAliasingRanges(
if (effect.kind === 'Alias') {
state.assign(index++, effect.from, effect.into);
} else {
CompilerError.invariant(effect.kind === 'Freeze', {
reason: `Unexpected '${effect.kind}' effect for MaybeThrow terminal`,
description: null,
details: [
{
kind: 'error',
loc: block.terminal.loc,
message: null,
},
],
});
CompilerError.invariant(
effect.kind === 'Freeze' || effect.kind === 'Render',
{
reason: `Unexpected '${effect.kind}' effect for MaybeThrow terminal`,
description: null,
details: [
{
kind: 'error',
loc: block.terminal.loc,
message: null,
},
],
},
);
}
}
}
}
for (const mutation of mutations) {
if (DEBUG) {
console.log(
`[${mutation.index}] mutate ${printIdentifier(mutation.place.identifier)}`,
);
}
state.mutate(
mutation.index,
mutation.place.identifier,
@@ -255,8 +260,16 @@ export function inferMutationAliasingRanges(
errors,
);
}
if (DEBUG) {
console.log(state.debug());
}
for (const render of renders) {
state.render(render.index, render.place.identifier, errors);
if (DEBUG) {
console.log(
`[${render.index}] render ${printIdentifier(render.place.identifier)}`,
);
}
state.render(render.index, render.place, errors);
}
for (const param of [...fn.context, ...fn.params]) {
const place = param.kind === 'Identifier' ? param : param.place;
@@ -515,6 +528,13 @@ export function inferMutationAliasingRanges(
const ignoredErrors = new CompilerError();
for (const param of [...fn.params, ...fn.context, fn.returns]) {
const place = param.kind === 'Identifier' ? param : param.place;
const node = state.nodes.get(place.identifier);
if (node != null && node.render != null) {
functionEffects.push({
kind: 'Render',
place: place,
});
}
tracked.push(place);
}
for (const into of tracked) {
@@ -577,7 +597,6 @@ export function inferMutationAliasingRanges(
function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
for (const effect of fn.aliasingEffects ?? []) {
switch (effect.kind) {
case 'Impure':
case 'MutateFrozen':
case 'MutateGlobal': {
errors.pushDiagnostic(effect.error);
@@ -612,10 +631,74 @@ type Node = {
| {kind: 'Object'}
| {kind: 'Phi'}
| {kind: 'Function'; function: HIRFunction};
render: Place | null;
};
function _printNode(node: Node): string {
const out: Array<string> = [];
debugNode(out, node);
return out.join('\n');
}
function debugNode(out: Array<string>, node: Node): void {
out.push(
printIdentifier(node.id) +
printType(node.id.type) +
` lastMutated=[${node.lastMutated}]`,
);
if (node.transitive != null) {
out.push(` transitive=${node.transitive.kind}`);
}
if (node.local != null) {
out.push(` local=${node.local.kind}`);
}
if (node.mutationReason != null) {
out.push(` mutationReason=${node.mutationReason?.kind}`);
}
const edges: Array<{
index: number;
direction: '<=' | '=>';
kind: string;
id: Identifier;
}> = [];
for (const [alias, index] of node.createdFrom) {
edges.push({index, direction: '<=', kind: 'createFrom', id: alias});
}
for (const [alias, index] of node.aliases) {
edges.push({index, direction: '<=', kind: 'alias', id: alias});
}
for (const [alias, index] of node.maybeAliases) {
edges.push({index, direction: '<=', kind: 'alias?', id: alias});
}
for (const [alias, index] of node.captures) {
edges.push({index, direction: '<=', kind: 'capture', id: alias});
}
for (const edge of node.edges) {
edges.push({
index: edge.index,
direction: '=>',
kind: edge.kind,
id: edge.node,
});
}
edges.sort((a, b) => a.index - b.index);
for (const edge of edges) {
out.push(
` [${edge.index}] ${edge.direction} ${edge.kind} ${printIdentifier(edge.id)}`,
);
}
}
class AliasingState {
nodes: Map<Identifier, Node> = new Map();
debug(): string {
const items: Array<string> = [];
for (const [_id, node] of this.nodes) {
debugNode(items, node);
}
return items.join('\n');
}
create(place: Place, value: Node['value']): void {
this.nodes.set(place.identifier, {
id: place.identifier,
@@ -629,6 +712,7 @@ class AliasingState {
lastMutated: 0,
mutationReason: null,
value,
render: null,
});
}
@@ -681,9 +765,9 @@ class AliasingState {
}
}
render(index: number, start: Identifier, errors: CompilerError): void {
render(index: number, start: Place, errors: CompilerError): void {
const seen = new Set<Identifier>();
const queue: Array<Identifier> = [start];
const queue: Array<Identifier> = [start.identifier];
while (queue.length !== 0) {
const current = queue.pop()!;
if (seen.has(current)) {
@@ -691,11 +775,34 @@ class AliasingState {
}
seen.add(current);
const node = this.nodes.get(current);
if (node == null || node.transitive != null || node.local != null) {
if (node == null || isUseRefType(node.id)) {
if (DEBUG) {
console.log(` render ${printIdentifier(current)}: skip mutated/ref`);
}
continue;
}
if (node.value.kind === 'Function') {
appendFunctionErrors(errors, node.value.function);
if (
node.local == null &&
node.transitive == null &&
node.value.kind === 'Function'
) {
const returns = node.value.function.returns;
if (
isJsxType(returns.identifier.type) ||
(returns.identifier.type.kind === 'Phi' &&
returns.identifier.type.operands.some(operand =>
isJsxType(operand),
))
) {
appendFunctionErrors(errors, node.value.function);
}
if (DEBUG) {
console.log(` render ${printIdentifier(current)}: skip function`);
}
continue;
}
if (node.render == null) {
node.render = start;
}
for (const [alias, when] of node.createdFrom) {
if (when >= index) {
@@ -709,6 +816,12 @@ class AliasingState {
}
queue.push(alias);
}
for (const [alias, when] of node.maybeAliases) {
if (when >= index) {
continue;
}
queue.push(alias);
}
for (const [capture, when] of node.captures) {
if (when >= index) {
continue;

View File

@@ -167,6 +167,14 @@ export function Set_filter<T>(
return result;
}
export function Set_subtract<T>(
source: ReadonlySet<T>,
other: Iterable<T>,
): Set<T> {
const otherSet = other instanceof Set ? other : new Set(other);
return Set_filter(source, item => !otherSet.has(item));
}
export function hasNode<T>(
input: NodePath<T | null | undefined>,
): input is NodePath<NonNullable<T>> {

View File

@@ -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<void, CompilerError> {
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();
}

View File

@@ -0,0 +1,307 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import prettyFormat from 'pretty-format';
import {CompilerDiagnostic, CompilerError, Effect} from '..';
import {
areEqualSourceLocations,
HIRFunction,
IdentifierId,
InstructionId,
isJsxType,
isRefValueType,
isUseRefType,
} from '../HIR';
import {
eachInstructionLValue,
eachInstructionValueOperand,
} from '../HIR/visitors';
import {AliasingEffect} from '../Inference/AliasingEffects';
import {createControlDominators} from '../Inference/ControlDominators';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
import {Err, Ok, Result} from '../Utils/Result';
import {
assertExhaustive,
getOrInsertWith,
Set_filter,
Set_subtract,
} from '../Utils/utils';
import {printInstruction} from '../HIR/PrintHIR';
type ImpureEffect = Extract<AliasingEffect, {kind: 'Impure'}>;
type RenderEffect = Extract<AliasingEffect, {kind: 'Render'}>;
type FunctionCache = Map<HIRFunction, Map<string, ImpuritySignature>>;
type ImpuritySignature = {effects: Array<ImpureEffect>; error: CompilerError};
export function validateNoImpureValuesInRender(
fn: HIRFunction,
): Result<void, CompilerError> {
const impure = new Map<IdentifierId, ImpureEffect>();
const result = inferImpureValues(fn, impure, new Map());
if (result.error.hasAnyErrors()) {
return Err(result.error);
}
return Ok(undefined);
}
function inferFunctionExpressionMemo(
fn: HIRFunction,
impure: Map<IdentifierId, ImpureEffect>,
cache: FunctionCache,
): ImpuritySignature {
const key = fn.context
.map(place => `${place.identifier.id}:${impure.has(place.identifier.id)}`)
.join(',');
return getOrInsertWith(
getOrInsertWith(cache, fn, () => new Map()),
key,
() => inferImpureValues(fn, impure, cache),
);
}
function processEffects(
id: InstructionId,
effects: Array<AliasingEffect>,
impure: Map<IdentifierId, ImpureEffect>,
cache: FunctionCache,
): boolean {
let hasChanges = false;
const rendered: Set<IdentifierId> = new Set();
for (const effect of effects) {
if (effect.kind === 'Render') {
rendered.add(effect.place.identifier.id);
}
}
for (const effect of effects) {
switch (effect.kind) {
case 'Alias':
case 'Assign':
case 'Capture':
case 'CreateFrom':
case 'ImmutableCapture': {
const sourceEffect = impure.get(effect.from.identifier.id);
if (
sourceEffect != null &&
!impure.has(effect.into.identifier.id) &&
!rendered.has(effect.from.identifier.id) &&
!isUseRefType(effect.into.identifier) &&
!isJsxType(effect.into.identifier.type)
) {
impure.set(effect.into.identifier.id, sourceEffect);
hasChanges = true;
}
if (
sourceEffect == null &&
(effect.kind === 'Assign' || effect.kind === 'Capture') &&
!impure.has(effect.from.identifier.id) &&
!rendered.has(effect.from.identifier.id) &&
!isUseRefType(effect.from.identifier) &&
isMutable({id}, effect.into)
) {
const destinationEffect = impure.get(effect.into.identifier.id);
if (destinationEffect != null) {
impure.set(effect.from.identifier.id, destinationEffect);
hasChanges = true;
}
}
break;
}
case 'Impure': {
if (!impure.has(effect.into.identifier.id)) {
impure.set(effect.into.identifier.id, effect);
hasChanges = true;
}
break;
}
case 'Render': {
break;
}
case 'CreateFunction': {
const result = inferFunctionExpressionMemo(
effect.function.loweredFunc.func,
impure,
cache,
);
if (result.error.hasAnyErrors()) {
break;
}
const impureEffect: ImpureEffect | null =
result.effects.find(
(functionEffect: AliasingEffect): functionEffect is ImpureEffect =>
functionEffect.kind === 'Impure' &&
functionEffect.into.identifier.id ===
effect.function.loweredFunc.func.returns.identifier.id,
) ?? null;
if (impureEffect != null) {
impure.set(effect.into.identifier.id, impureEffect);
hasChanges = true;
}
break;
}
case 'MaybeAlias':
case 'Apply':
case 'Create':
case 'Freeze':
case 'Mutate':
case 'MutateConditionally':
case 'MutateFrozen':
case 'MutateGlobal':
case 'MutateTransitive':
case 'MutateTransitiveConditionally': {
break;
}
}
}
return hasChanges;
}
function inferImpureValues(
fn: HIRFunction,
impure: Map<IdentifierId, ImpureEffect>,
cache: FunctionCache,
): ImpuritySignature {
const getBlockControl = createControlDominators(fn, place => {
return impure.has(place.identifier.id);
});
let hasChanges = false;
do {
hasChanges = false;
for (const block of fn.body.blocks.values()) {
const controlPlace = getBlockControl(block.id);
const controlImpureEffect =
controlPlace != null ? impure.get(controlPlace.identifier.id) : null;
for (const phi of block.phis) {
if (impure.has(phi.place.identifier.id)) {
// Already marked impure on a previous pass
continue;
}
let impureEffect = null;
for (const [, operand] of phi.operands) {
const operandEffect = impure.get(operand.identifier.id);
if (operandEffect != null) {
impureEffect = operandEffect;
break;
}
}
if (impureEffect != null) {
impure.set(phi.place.identifier.id, impureEffect);
hasChanges = true;
} else {
for (const [pred] of phi.operands) {
const predControl = getBlockControl(pred);
if (predControl != null) {
const predEffect = impure.get(predControl.identifier.id);
if (predEffect != null) {
impure.set(phi.place.identifier.id, predEffect);
hasChanges = true;
break;
}
}
}
}
}
for (const instr of block.instructions) {
const _impure = new Set(impure.keys());
hasChanges =
processEffects(instr.id, instr.effects ?? [], impure, cache) ||
hasChanges;
}
if (block.terminal.kind === 'return' && block.terminal.effects != null) {
hasChanges =
processEffects(
block.terminal.id,
block.terminal.effects,
impure,
cache,
) || hasChanges;
}
}
} while (hasChanges);
fn.env.logger?.debugLogIRs?.({
kind: 'debug',
name: 'ValidateNoImpureValuesInRender',
value: JSON.stringify(Array.from(impure.keys()).sort(), null, 2),
});
const error = new CompilerError();
function validateRenderEffect(effect: RenderEffect): void {
const impureEffect = impure.get(effect.place.identifier.id);
if (impureEffect == null) {
return;
}
const diagnostic = CompilerDiagnostic.create({
category: impureEffect.category,
reason: impureEffect.reason,
description: impureEffect.description,
}).withDetails({
kind: 'error',
loc: effect.place.loc,
message: impureEffect.usageMessage,
});
if (!areEqualSourceLocations(effect.place.loc, impureEffect.into.loc)) {
diagnostic.withDetails({
kind: 'error',
loc: impureEffect.into.loc,
message: impureEffect.sourceMessage,
});
}
error.pushDiagnostic(diagnostic);
}
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const value = instr.value;
if (
value.kind === 'FunctionExpression' ||
value.kind === 'ObjectMethod'
) {
const result = inferFunctionExpressionMemo(
value.loweredFunc.func,
impure,
cache,
);
if (result.error.hasAnyErrors()) {
error.merge(result.error);
}
}
for (const effect of instr.effects ?? []) {
if (effect.kind === 'Render') {
validateRenderEffect(effect);
}
}
}
if (block.terminal.kind === 'return' && block.terminal.effects != null) {
for (const effect of block.terminal.effects) {
if (effect.kind === 'Render') {
validateRenderEffect(effect);
}
}
}
}
const impureEffects: Array<ImpureEffect> = [];
for (const param of [...fn.context, ...fn.params, fn.returns]) {
const place = param.kind === 'Identifier' ? param : param.place;
const impureEffect = impure.get(place.identifier.id);
if (impureEffect != null) {
impureEffects.push({
kind: 'Impure',
into: impureEffect.into,
category: impureEffect.category,
reason: impureEffect.reason,
description: impureEffect.description,
sourceMessage: impureEffect.sourceMessage,
usageMessage: impureEffect.usageMessage,
});
}
}
return {effects: impureEffects, error};
}

View File

@@ -397,16 +397,10 @@ function validateNoRefAccessInRenderImpl(
switch (instr.value.kind) {
case 'JsxExpression':
case 'JsxFragment': {
for (const operand of eachInstructionValueOperand(instr.value)) {
validateNoDirectRefValueAccess(errors, operand, env);
}
break;
}
case 'ComputedLoad':
case 'PropertyLoad': {
if (instr.value.kind === 'ComputedLoad') {
validateNoDirectRefValueAccess(errors, instr.value.property, env);
}
const objType = env.get(instr.value.object.identifier.id);
let lookupType: null | RefAccessType = null;
if (objType?.kind === 'Structure') {
@@ -499,73 +493,10 @@ function validateNoRefAccessInRenderImpl(
instr.value.kind === 'CallExpression'
? instr.value.callee
: instr.value.property;
const hookKind = getHookKindForType(fn.env, callee.identifier.type);
let returnType: RefAccessType = {kind: 'None'};
const fnType = env.get(callee.identifier.id);
let didError = false;
if (fnType?.kind === 'Structure' && fnType.fn !== null) {
returnType = fnType.fn.returnType;
if (fnType.fn.readRefEffect) {
didError = true;
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Refs,
reason: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetails({
kind: 'error',
loc: callee.loc,
message: `This function accesses a ref value`,
}),
);
}
}
/*
* If we already reported an error on this instruction, don't report
* duplicate errors
*/
if (!didError) {
const isRefLValue = isUseRefType(instr.lvalue.identifier);
const isEventHandlerLValue = isEventHandlerType(
instr.lvalue.identifier,
);
for (const operand of eachInstructionValueOperand(instr.value)) {
/**
* By default we check that function call operands are not refs,
* ref values, or functions that can access refs.
*/
if (
isRefLValue ||
isEventHandlerLValue ||
(hookKind != null &&
hookKind !== 'useState' &&
hookKind !== 'useReducer')
) {
/**
* Allow passing refs or ref-accessing functions when:
* 1. lvalue is a ref (mergeRefs pattern: `mergeRefs(ref1, ref2)`)
* 2. lvalue is an event handler (DOM events execute outside render)
* 3. calling hooks (independently validated for ref safety)
*/
validateNoDirectRefValueAccess(errors, operand, env);
} else if (interpolatedAsJsx.has(instr.lvalue.identifier.id)) {
/**
* Special case: the lvalue is passed as a jsx child
*
* For example `<Foo>{renderHelper(ref)}</Foo>`. Here we have more
* context and infer that the ref is being passed to a component-like
* render function which attempts to obey the rules.
*/
validateNoRefValueAccess(errors, env, operand);
} else {
validateNoRefPassedToFunction(
errors,
env,
operand,
operand.loc,
);
}
}
}
env.set(instr.lvalue.identifier.id, returnType);
break;
@@ -574,7 +505,6 @@ function validateNoRefAccessInRenderImpl(
case 'ArrayExpression': {
const types: Array<RefAccessType> = [];
for (const operand of eachInstructionValueOperand(instr.value)) {
validateNoDirectRefValueAccess(errors, operand, env);
types.push(env.get(operand.identifier.id) ?? {kind: 'None'});
}
const value = joinRefAccessTypes(...types);
@@ -611,17 +541,10 @@ function validateNoRefAccessInRenderImpl(
} else {
validateNoRefUpdate(errors, env, instr.value.object, instr.loc);
}
if (
instr.value.kind === 'ComputedDelete' ||
instr.value.kind === 'ComputedStore'
) {
validateNoRefValueAccess(errors, env, instr.value.property);
}
if (
instr.value.kind === 'ComputedStore' ||
instr.value.kind === 'PropertyStore'
) {
validateNoDirectRefValueAccess(errors, instr.value.value, env);
const type = env.get(instr.value.value.identifier.id);
if (type != null && type.kind === 'Structure') {
let objectType: RefAccessType = type;
@@ -662,27 +585,9 @@ function validateNoRefAccessInRenderImpl(
* error for the write to the ref
*/
env.set(instr.lvalue.identifier.id, {kind: 'Guard', refId});
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Refs,
reason: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
})
.withDetails({
kind: 'error',
loc: instr.value.value.loc,
message: `Cannot access ref value during render`,
})
.withDetails({
kind: 'hint',
message:
'To initialize a ref only once, check that the ref is null with the pattern `if (ref.current == null) { ref.current = ... }`',
}),
);
break;
}
}
validateNoRefValueAccess(errors, env, instr.value.value);
break;
}
case 'BinaryExpression': {
@@ -704,26 +609,14 @@ function validateNoRefAccessInRenderImpl(
if (refId !== null && nullish) {
env.set(instr.lvalue.identifier.id, {kind: 'Guard', refId});
} else {
for (const operand of eachInstructionValueOperand(instr.value)) {
validateNoRefValueAccess(errors, env, operand);
}
}
break;
}
default: {
for (const operand of eachInstructionValueOperand(instr.value)) {
validateNoRefValueAccess(errors, env, operand);
}
break;
}
}
// Guard values are derived from ref.current, so they can only be used in if statement targets
for (const operand of eachInstructionOperand(instr)) {
guardCheck(errors, operand, env);
}
if (
isUseRefType(instr.lvalue.identifier) &&
env.get(instr.lvalue.identifier.id)?.kind !== 'Ref'
@@ -761,15 +654,8 @@ function validateNoRefAccessInRenderImpl(
}
for (const operand of eachTerminalOperand(block.terminal)) {
if (block.terminal.kind !== 'return') {
validateNoRefValueAccess(errors, env, operand);
if (block.terminal.kind !== 'if') {
guardCheck(errors, operand, env);
}
} else {
if (block.terminal.kind === 'return') {
// Allow functions containing refs to be returned, but not direct ref values
validateNoDirectRefValueAccess(errors, operand, env);
guardCheck(errors, operand, env);
returnValues.push(env.get(operand.identifier.id));
}
}
@@ -808,72 +694,6 @@ function destructure(
return type;
}
function guardCheck(errors: CompilerError, operand: Place, env: Env): void {
if (env.get(operand.identifier.id)?.kind === 'Guard') {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Refs,
reason: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetails({
kind: 'error',
loc: operand.loc,
message: `Cannot access ref value during render`,
}),
);
}
}
function validateNoRefValueAccess(
errors: CompilerError,
env: Env,
operand: Place,
): void {
const type = destructure(env.get(operand.identifier.id));
if (
type?.kind === 'RefValue' ||
(type?.kind === 'Structure' && type.fn?.readRefEffect)
) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Refs,
reason: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetails({
kind: 'error',
loc: (type.kind === 'RefValue' && type.loc) || operand.loc,
message: `Cannot access ref value during render`,
}),
);
}
}
function validateNoRefPassedToFunction(
errors: CompilerError,
env: Env,
operand: Place,
loc: SourceLocation,
): void {
const type = destructure(env.get(operand.identifier.id));
if (
type?.kind === 'Ref' ||
type?.kind === 'RefValue' ||
(type?.kind === 'Structure' && type.fn?.readRefEffect)
) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Refs,
reason: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetails({
kind: 'error',
loc: (type.kind === 'RefValue' && type.loc) || loc,
message: `Passing a ref to a function may read its value during render`,
}),
);
}
}
function validateNoRefUpdate(
errors: CompilerError,
env: Env,
@@ -886,7 +706,7 @@ function validateNoRefUpdate(
CompilerDiagnostic.create({
category: ErrorCategory.Refs,
reason: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
description: REF_ERROR_DESCRIPTION,
}).withDetails({
kind: 'error',
loc: (type.kind === 'RefValue' && type.loc) || loc,
@@ -896,28 +716,7 @@ function validateNoRefUpdate(
}
}
function validateNoDirectRefValueAccess(
errors: CompilerError,
operand: Place,
env: Env,
): void {
const type = destructure(env.get(operand.identifier.id));
if (type?.kind === 'RefValue') {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Refs,
reason: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetails({
kind: 'error',
loc: type.loc ?? operand.loc,
message: `Cannot access ref value during render`,
}),
);
}
}
const ERROR_DESCRIPTION =
export const REF_ERROR_DESCRIPTION =
'React refs are values that are not needed for rendering. Refs should only be accessed ' +
'outside of render, such as in event handlers or effects. ' +
'Accessing a ref value (the `current` property) during render can cause your component ' +

View File

@@ -202,10 +202,10 @@ function getSetStateCall(
);
};
const isRefControlledBlock: (id: BlockId) => boolean =
const isRefControlledBlock: (id: BlockId) => Place | null =
enableAllowSetStateFromRefsInEffects
? createControlDominators(fn, place => isDerivedFromRef(place))
: (): boolean => false;
: (): Place | null => null;
for (const [, block] of fn.body.blocks) {
if (enableAllowSetStateFromRefsInEffects) {
@@ -224,7 +224,7 @@ function getSetStateCall(
refDerivedValues.add(phi.place.identifier.id);
} else {
for (const [pred] of phi.operands) {
if (isRefControlledBlock(pred)) {
if (isRefControlledBlock(pred) != null) {
refDerivedValues.add(phi.place.identifier.id);
break;
}
@@ -337,7 +337,7 @@ function getSetStateCall(
* be needed when initial layout measurements from refs need to be stored in state.
*/
return null;
} else if (isRefControlledBlock(block.id)) {
} else if (isRefControlledBlock(block.id) != null) {
continue;
}
}

View File

@@ -1,52 +0,0 @@
## Input
```javascript
import {useRef} from 'react';
import {Stringify} from 'shared-runtime';
function Component(props) {
const ref = useRef(props.value);
const object = {};
object.foo = () => ref.current;
return <Stringify object={object} shouldInvokeFns={true} />;
}
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 = <Stringify object={object} shouldInvokeFns={true} />;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 42 }],
};
```
### Eval output
(kind: ok) <div>{"object":{"foo":{"kind":"Function","result":42}},"shouldInvokeFns":true}</div>

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
class Component {
_renderMessage = () => {
const Message = () => {
@@ -22,7 +22,7 @@ class Component {
## Code
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
class Component {
_renderMessage = () => {
const Message = () => {

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
class Component {
_renderMessage = () => {
const Message = () => {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @customOptOutDirectives:["use todo memo"]
// @customOptOutDirectives:["use todo memo"]
function Component() {
'use todo memo';
return <div>hello world!</div>;
@@ -18,7 +18,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @expectNothingCompiled @customOptOutDirectives:["use todo memo"]
// @customOptOutDirectives:["use todo memo"]
function Component() {
"use todo memo";
return <div>hello world!</div>;

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @customOptOutDirectives:["use todo memo"]
// @customOptOutDirectives:["use todo memo"]
function Component() {
'use todo memo';
return <div>hello world!</div>;

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
@@ -37,7 +37,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @expectNothingCompiled @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
import { useMemo } from "react";
import { makeObject_Primitives, ValidateMemoization } from "shared-runtime";

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';

View File

@@ -5,6 +5,7 @@
function Component() {
const Foo = () => {
someGlobal = true;
return <div />;
};
return <Foo />;
}
@@ -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 <Foo />;
6 | }
4 | return <div />;
5 | };
6 | return <Foo />;
```

View File

@@ -1,6 +1,7 @@
function Component() {
const Foo = () => {
someGlobal = true;
return <div />;
};
return <Foo />;
}

View File

@@ -5,6 +5,7 @@
function Component() {
const foo = () => {
someGlobal = true;
return <div />;
};
// 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 <div />;
5 | };
6 | // Children are generally access/called during render, so
```

View File

@@ -1,6 +1,7 @@
function Component() {
const foo = () => {
someGlobal = true;
return <div />;
};
// Children are generally access/called during render, so
// modifying a global in a children function is almost

View File

@@ -20,30 +20,26 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
Found 2 errors:
Found 1 error:
Error: Cannot access refs during render
Error: Cannot access ref value during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
error.hook-ref-value.ts:5:23
error.hook-ref-value.ts:5:22
3 | function Component(props) {
4 | const ref = useRef();
> 5 | useEffect(() => {}, [ref.current]);
| ^^^^^^^^^^^ Cannot access ref value during render
| ^^^^^^^^^^^^^ Ref value is used during render
6 | }
7 |
8 | export const FIXTURE_ENTRYPOINT = {
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
error.hook-ref-value.ts:5:23
3 | function Component(props) {
4 | const ref = useRef();
> 5 | useEffect(() => {}, [ref.current]);
| ^^^^^^^^^^^ Cannot access ref value during render
| ^^^^^^^^^^^ Ref is initially accessed
6 | }
7 |
8 | export const FIXTURE_ENTRYPOINT = {

View File

@@ -17,15 +17,23 @@ function Component(props) {
```
Found 1 error:
Error: Cannot access refs during render
Error: Cannot access ref value during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
error.invalid-access-ref-during-render.ts:5:9
3 | const ref = useRef(null);
4 | const value = ref.current;
> 5 | return value;
| ^^^^^ Ref value is used during render
6 | }
7 |
error.invalid-access-ref-during-render.ts:4:16
2 | function Component(props) {
3 | const ref = useRef(null);
> 4 | const value = ref.current;
| ^^^^^^^^^^^ Cannot access ref value during render
| ^^^^^^^^^^^ Ref is initially accessed
5 | return value;
6 | }
7 |

View File

@@ -28,7 +28,7 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
Error: Cannot access refs during render
Error: Cannot access ref value during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
@@ -36,7 +36,16 @@ error.invalid-access-ref-in-reducer-init.ts:8:4
6 | (state, action) => state + action,
7 | 0,
> 8 | init => ref.current
| ^^^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render
| ^^^^^^^^^^^^^^^^^^^ Ref value is used during render
9 | );
10 |
11 | return <Stringify state={state} />;
error.invalid-access-ref-in-reducer-init.ts:8:12
6 | (state, action) => state + action,
7 | 0,
> 8 | init => ref.current
| ^^^^^^^^^^^ Ref is initially accessed
9 | );
10 |
11 | return <Stringify state={state} />;

View File

@@ -24,7 +24,7 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
Error: Cannot access refs during render
Error: Cannot access ref value during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
@@ -32,7 +32,16 @@ error.invalid-access-ref-in-reducer.ts:5:29
3 | function Component(props) {
4 | const ref = useRef(props.value);
> 5 | const [state] = useReducer(() => ref.current, null);
| ^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render
| ^^^^^^^^^^^^^^^^^ Ref value is used during render
6 |
7 | return <Stringify state={state} />;
8 | }
error.invalid-access-ref-in-reducer.ts:5:35
3 | function Component(props) {
4 | const ref = useRef(props.value);
> 5 | const [state] = useReducer(() => ref.current, null);
| ^^^^^^^^^^^ Ref is initially accessed
6 |
7 | return <Stringify state={state} />;
8 | }

View File

@@ -20,18 +20,26 @@ function Component() {
```
Found 1 error:
Error: Cannot access refs during render
Error: Cannot access ref value during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
error.invalid-access-ref-in-render-mutate-object-with-ref-function.ts:7:19
5 | const object = {};
error.invalid-access-ref-in-render-mutate-object-with-ref-function.ts:8:15
6 | object.foo = () => ref.current;
> 7 | const refValue = object.foo();
| ^^^^^^^^^^ This function accesses a ref value
8 | return <div>{refValue}</div>;
7 | const refValue = object.foo();
> 8 | return <div>{refValue}</div>;
| ^^^^^^^^ Ref value is used during render
9 | }
10 |
error.invalid-access-ref-in-render-mutate-object-with-ref-function.ts:6:21
4 | const ref = useRef(null);
5 | const object = {};
> 6 | object.foo = () => ref.current;
| ^^^^^^^^^^^ Ref is initially accessed
7 | const refValue = object.foo();
8 | return <div>{refValue}</div>;
9 | }
```

View File

@@ -24,7 +24,7 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
Error: Cannot access refs during render
Error: Cannot access ref value during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
@@ -32,7 +32,16 @@ error.invalid-access-ref-in-state-initializer.ts:5:27
3 | function Component(props) {
4 | const ref = useRef(props.value);
> 5 | const [state] = useState(() => ref.current);
| ^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render
| ^^^^^^^^^^^^^^^^^ Ref value is used during render
6 |
7 | return <Stringify state={state} />;
8 | }
error.invalid-access-ref-in-state-initializer.ts:5:33
3 | function Component(props) {
4 | const ref = useRef(props.value);
> 5 | const [state] = useState(() => ref.current);
| ^^^^^^^^^^^ Ref is initially accessed
6 |
7 | return <Stringify state={state} />;
8 | }

View File

@@ -21,17 +21,27 @@ function Component(props) {
```
Found 1 error:
Error: Cannot access refs during render
Error: Cannot access ref value during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
error.invalid-aliased-ref-in-callback-invoked-during-render-.ts:9:33
7 | return <Foo item={item} current={current} />;
error.invalid-aliased-ref-in-callback-invoked-during-render-.ts:7:37
5 | const aliasedRef = ref;
6 | const current = aliasedRef.current;
> 7 | return <Foo item={item} current={current} />;
| ^^^^^^^ Ref value is used during render
8 | };
> 9 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
| ^^^^^^^^^^^^^^^^^^^^^^^^ Cannot access ref value during render
9 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
10 | }
11 |
error.invalid-aliased-ref-in-callback-invoked-during-render-.ts:6:20
4 | const renderItem = item => {
5 | const aliasedRef = ref;
> 6 | const current = aliasedRef.current;
| ^^^^^^^^^^^^^^^^^^ Ref is initially accessed
7 | return <Foo item={item} current={current} />;
8 | };
9 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
```

View File

@@ -0,0 +1,51 @@
## Input
```javascript
import {useRef} from 'react';
import {Stringify} from 'shared-runtime';
function Component(props) {
const ref = useRef(props.value);
const object = {};
object.foo = () => ref.current;
return <Stringify object={object} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};
```
## Error
```
Found 1 error:
Error: Cannot access ref value during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
error.invalid-capturing-ref-returning-function-in-rendered-object.ts:8:28
6 | const object = {};
7 | object.foo = () => ref.current;
> 8 | return <Stringify object={object} shouldInvokeFns={true} />;
| ^^^^^^ Ref value is used during render
9 | }
10 |
11 | export const FIXTURE_ENTRYPOINT = {
error.invalid-capturing-ref-returning-function-in-rendered-object.ts:7:21
5 | const ref = useRef(props.value);
6 | const object = {};
> 7 | object.foo = () => ref.current;
| ^^^^^^^^^^^ Ref is initially accessed
8 | return <Stringify object={object} shouldInvokeFns={true} />;
9 | }
10 |
```

View File

@@ -0,0 +1,47 @@
## Input
```javascript
// @validateNoImpureFunctionsInRender
import {arrayPush, identity, makeArray} from 'shared-runtime';
function Component() {
const getDate = () => Date.now();
const now = getDate();
const array = [];
arrayPush(array, now);
return <Foo hasDate={array} />;
}
```
## Error
```
Found 1 error:
Error: Cannot access impure value during render
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
error.invalid-impure-functions-in-render-indirect-via-mutation.ts:10:23
8 | const array = [];
9 | arrayPush(array, now);
> 10 | return <Foo hasDate={array} />;
| ^^^^^ Cannot access impure value during render
11 | }
12 |
error.invalid-impure-functions-in-render-indirect-via-mutation.ts:6:24
4 |
5 | function Component() {
> 6 | const getDate = () => Date.now();
| ^^^^^^^^^^ `Date.now` is an impure function.
7 | const now = getDate();
8 | const array = [];
9 | arrayPush(array, now);
```

View File

@@ -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 <Foo hasDate={array} />;
}

View File

@@ -0,0 +1,46 @@
## Input
```javascript
// @validateNoImpureFunctionsInRender
import {identity, makeArray} from 'shared-runtime';
function Component() {
const getDate = () => Date.now();
const array = makeArray(getDate());
const hasDate = identity(array);
return <Foo hasDate={hasDate} />;
}
```
## Error
```
Found 1 error:
Error: Cannot access impure value during render
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
error.invalid-impure-functions-in-render-indirect.ts:9:23
7 | const array = makeArray(getDate());
8 | const hasDate = identity(array);
> 9 | return <Foo hasDate={hasDate} />;
| ^^^^^^^ Cannot access impure value during render
10 | }
11 |
error.invalid-impure-functions-in-render-indirect.ts:6:24
4 |
5 | function Component() {
> 6 | const getDate = () => Date.now();
| ^^^^^^^^^^ `Date.now` is an impure function.
7 | const array = makeArray(getDate());
8 | const hasDate = identity(array);
9 | return <Foo hasDate={hasDate} />;
```

View File

@@ -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 <Foo hasDate={hasDate} />;
}

View File

@@ -0,0 +1,52 @@
## Input
```javascript
// @validateNoImpureFunctionsInRender
import {identity, makeArray} from 'shared-runtime';
function Component() {
const now = () => Date.now();
const f = () => {
// this should error but we currently lose track of the impurity bc
// the impure value comes from behind a call
const array = makeArray(now());
const hasDate = identity(array);
return hasDate;
};
const hasDate = f();
return <Foo hasDate={hasDate} />;
}
```
## Error
```
Found 1 error:
Error: Cannot access impure value during render
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
error.invalid-impure-functions-in-render-via-function-call-2.ts:15:23
13 | };
14 | const hasDate = f();
> 15 | return <Foo hasDate={hasDate} />;
| ^^^^^^^ Cannot access impure value during render
16 | }
17 |
error.invalid-impure-functions-in-render-via-function-call-2.ts:6:20
4 |
5 | function Component() {
> 6 | const now = () => Date.now();
| ^^^^^^^^^^ `Date.now` is an impure function.
7 | const f = () => {
8 | // this should error but we currently lose track of the impurity bc
9 | // the impure value comes from behind a call
```

View File

@@ -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 <Foo hasDate={hasDate} />;
}

View File

@@ -0,0 +1,50 @@
## Input
```javascript
// @validateNoImpureFunctionsInRender
import {identity, makeArray} from 'shared-runtime';
function Component() {
const now = Date.now();
const f = () => {
const array = makeArray(now);
const hasDate = identity(array);
return hasDate;
};
const hasDate = f();
return <Foo hasDate={hasDate} />;
}
```
## Error
```
Found 1 error:
Error: Cannot access impure value during render
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
error.invalid-impure-functions-in-render-via-function-call.ts:13:23
11 | };
12 | const hasDate = f();
> 13 | return <Foo hasDate={hasDate} />;
| ^^^^^^^ Cannot access impure value during render
14 | }
15 |
error.invalid-impure-functions-in-render-via-function-call.ts:6:14
4 |
5 | function Component() {
> 6 | const now = Date.now();
| ^^^^^^^^^^ `Date.now` is an impure function.
7 | const f = () => {
8 | const array = makeArray(now);
9 | const hasDate = identity(array);
```

View File

@@ -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 <Foo hasDate={hasDate} />;
}

View File

@@ -0,0 +1,50 @@
## Input
```javascript
// @validateNoImpureFunctionsInRender
import {typedArrayPush, typedIdentity} from 'shared-runtime';
function Component() {
const now = Date.now();
const renderItem = () => {
const array = [];
typedArrayPush(array, now());
const hasDate = typedIdentity(array);
return <Bar hasDate={hasDate} />;
};
return <Foo renderItem={renderItem} />;
}
```
## Error
```
Found 1 error:
Error: Cannot access impure value during render
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
error.invalid-impure-functions-in-render-via-render-helper-typed.ts:13:26
11 | return <Bar hasDate={hasDate} />;
12 | };
> 13 | return <Foo renderItem={renderItem} />;
| ^^^^^^^^^^ Cannot access impure value during render
14 | }
15 |
error.invalid-impure-functions-in-render-via-render-helper-typed.ts:6:14
4 |
5 | function Component() {
> 6 | const now = Date.now();
| ^^^^^^^^^^ `Date.now` is an impure function.
7 | const renderItem = () => {
8 | const array = [];
9 | typedArrayPush(array, now());
```

View File

@@ -0,0 +1,14 @@
// @validateNoImpureFunctionsInRender
import {typedArrayPush, typedIdentity} from 'shared-runtime';
function Component() {
const now = Date.now();
const renderItem = () => {
const array = [];
typedArrayPush(array, now());
const hasDate = typedIdentity(array);
return <Bar hasDate={hasDate} />;
};
return <Foo renderItem={renderItem} />;
}

View File

@@ -0,0 +1,49 @@
## Input
```javascript
// @validateNoImpureFunctionsInRender
import {identity, makeArray} from 'shared-runtime';
function Component() {
const now = Date.now();
const renderItem = () => {
const array = makeArray(now);
const hasDate = identity(array);
return <Bar hasDate={hasDate} />;
};
return <Foo renderItem={renderItem} />;
}
```
## Error
```
Found 1 error:
Error: Cannot access impure value during render
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
error.invalid-impure-functions-in-render-via-render-helper.ts:12:26
10 | return <Bar hasDate={hasDate} />;
11 | };
> 12 | return <Foo renderItem={renderItem} />;
| ^^^^^^^^^^ Cannot access impure value during render
13 | }
14 |
error.invalid-impure-functions-in-render-via-render-helper.ts:6:14
4 |
5 | function Component() {
> 6 | const now = Date.now();
| ^^^^^^^^^^ `Date.now` is an impure function.
7 | const renderItem = () => {
8 | const array = makeArray(now);
9 | const hasDate = identity(array);
```

View File

@@ -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 <Bar hasDate={hasDate} />;
};
return <Foo renderItem={renderItem} />;
}

View File

@@ -19,41 +19,65 @@ function Component() {
```
Found 3 errors:
Error: Cannot call impure function during render
Error: Cannot access impure value during render
`Date.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
error.invalid-impure-functions-in-render.ts:7:20
5 | const now = performance.now();
6 | const rand = Math.random();
> 7 | return <Foo date={date} now={now} rand={rand} />;
| ^^^^ Cannot access impure value during render
8 | }
9 |
error.invalid-impure-functions-in-render.ts:4:15
2 |
3 | function Component() {
> 4 | const date = Date.now();
| ^^^^^^^^^^ Cannot call impure function
| ^^^^^^^^^^ `Date.now` is an impure function.
5 | const now = performance.now();
6 | const rand = Math.random();
7 | return <Foo date={date} now={now} rand={rand} />;
Error: Cannot call impure function during render
Error: Cannot access impure value during render
`performance.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
error.invalid-impure-functions-in-render.ts:7:31
5 | const now = performance.now();
6 | const rand = Math.random();
> 7 | return <Foo date={date} now={now} rand={rand} />;
| ^^^ Cannot access impure value during render
8 | }
9 |
error.invalid-impure-functions-in-render.ts:5:14
3 | function Component() {
4 | const date = Date.now();
> 5 | const now = performance.now();
| ^^^^^^^^^^^^^^^^^ Cannot call impure function
| ^^^^^^^^^^^^^^^^^ `performance.now` is an impure function.
6 | const rand = Math.random();
7 | return <Foo date={date} now={now} rand={rand} />;
8 | }
Error: Cannot call impure function during render
Error: Cannot access impure value during render
`Math.random` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
error.invalid-impure-functions-in-render.ts:7:42
5 | const now = performance.now();
6 | const rand = Math.random();
> 7 | return <Foo date={date} now={now} rand={rand} />;
| ^^^^ Cannot access impure value during render
8 | }
9 |
error.invalid-impure-functions-in-render.ts:6:15
4 | const date = Date.now();
5 | const now = performance.now();
> 6 | const rand = Math.random();
| ^^^^^^^^^^^^^ Cannot call impure function
| ^^^^^^^^^^^^^ `Math.random` is an impure function.
7 | return <Foo date={date} now={now} rand={rand} />;
8 | }
9 |

View File

@@ -0,0 +1,45 @@
## Input
```javascript
// @validateNoImpureFunctionsInRender
function Component() {
const now = () => Date.now();
const render = () => {
return <div>{now()}</div>;
};
return <div>{render()}</div>;
}
```
## Error
```
Found 1 error:
Error: Cannot access impure value during render
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
error.invalid-impure-value-in-render-helper.ts:5:17
3 | const now = () => Date.now();
4 | const render = () => {
> 5 | return <div>{now()}</div>;
| ^^^^^ Cannot access impure value during render
6 | };
7 | return <div>{render()}</div>;
8 | }
error.invalid-impure-value-in-render-helper.ts:3:20
1 | // @validateNoImpureFunctionsInRender
2 | function Component() {
> 3 | const now = () => Date.now();
| ^^^^^^^^^^ `Date.now` is an impure function.
4 | const render = () => {
5 | return <div>{now()}</div>;
6 | };
```

View File

@@ -0,0 +1,8 @@
// @validateNoImpureFunctionsInRender
function Component() {
const now = () => Date.now();
const render = () => {
return <div>{now()}</div>;
};
return <div>{render()}</div>;
}

View File

@@ -16,15 +16,23 @@ function Component({ref}) {
```
Found 1 error:
Error: Cannot access refs during render
Error: Cannot access ref value during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
error.invalid-read-ref-prop-in-render-destructure.ts:4:15
2 | function Component({ref}) {
3 | const value = ref.current;
> 4 | return <div>{value}</div>;
| ^^^^^ Ref value is used during render
5 | }
6 |
error.invalid-read-ref-prop-in-render-destructure.ts:3:16
1 | // @validateRefAccessDuringRender @compilationMode:"infer"
2 | function Component({ref}) {
> 3 | const value = ref.current;
| ^^^^^^^^^^^ Cannot access ref value during render
| ^^^^^^^^^^^ Ref is initially accessed
4 | return <div>{value}</div>;
5 | }
6 |

View File

@@ -16,15 +16,23 @@ function Component(props) {
```
Found 1 error:
Error: Cannot access refs during render
Error: Cannot access ref value during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
error.invalid-read-ref-prop-in-render-property-load.ts:4:15
2 | function Component(props) {
3 | const value = props.ref.current;
> 4 | return <div>{value}</div>;
| ^^^^^ Ref value is used during render
5 | }
6 |
error.invalid-read-ref-prop-in-render-property-load.ts:3:16
1 | // @validateRefAccessDuringRender @compilationMode:"infer"
2 | function Component(props) {
> 3 | const value = props.ref.current;
| ^^^^^^^^^^^^^^^^^ Cannot access ref value during render
| ^^^^^^^^^^^^^^^^^ Ref is initially accessed
4 | return <div>{value}</div>;
5 | }
6 |

View File

@@ -22,57 +22,27 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
Found 4 errors:
Found 1 error:
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
4 | component C() {
5 | const r = useRef(null);
> 6 | const current = !r.current;
| ^^^^^^^^^ Cannot access ref value during render
7 | return <div>{current}</div>;
8 | }
9 |
To initialize a ref only once, check that the ref is null with the pattern `if (ref.current == null) { ref.current = ... }`
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
4 | component C() {
5 | const r = useRef(null);
> 6 | const current = !r.current;
| ^^^^^^^^^^ Cannot access ref value during render
7 | return <div>{current}</div>;
8 | }
9 |
Error: Cannot access refs during render
Error: Cannot access ref value during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
5 | const r = useRef(null);
6 | const current = !r.current;
> 7 | return <div>{current}</div>;
| ^^^^^^^ Cannot access ref value during render
| ^^^^^^^ Ref value is used during render
8 | }
9 |
10 | export const FIXTURE_ENTRYPOINT = {
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
5 | const r = useRef(null);
6 | const current = !r.current;
> 7 | return <div>{current}</div>;
| ^^^^^^^ Cannot access ref value during render
8 | }
9 |
10 | export const FIXTURE_ENTRYPOINT = {
4 | component C() {
5 | const r = useRef(null);
> 6 | const current = !r.current;
| ^^^^^^^^^ Ref is initially accessed
7 | return <div>{current}</div>;
8 | }
9 |
```

View File

@@ -20,17 +20,27 @@ function Component(props) {
```
Found 1 error:
Error: Cannot access refs during render
Error: Cannot access ref value during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
error.invalid-ref-in-callback-invoked-during-render.ts:8:33
6 | return <Foo item={item} current={current} />;
7 | };
> 8 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
| ^^^^^^^^^^^^^^^^^^^^^^^^ Cannot access ref value during render
9 | }
10 |
error.invalid-ref-in-callback-invoked-during-render.ts:6:37
4 | const renderItem = item => {
5 | const current = ref.current;
> 6 | return <Foo item={item} current={current} />;
| ^^^^^^^ Ref value is used during render
7 | };
8 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
9 | }
error.invalid-ref-in-callback-invoked-during-render.ts:5:20
3 | const ref = useRef(null);
4 | const renderItem = item => {
> 5 | const current = ref.current;
| ^^^^^^^^^^^ Ref is initially accessed
6 | return <Foo item={item} current={current} />;
7 | };
8 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
```

View File

@@ -16,7 +16,7 @@ function Component(props) {
```
Found 1 error:
Error: Cannot access refs during render
Error: Cannot access ref value during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
@@ -24,7 +24,7 @@ error.invalid-ref-value-as-props.ts:4:19
2 | function Component(props) {
3 | const ref = useRef(null);
> 4 | return <Foo ref={ref.current} />;
| ^^^^^^^^^^^ Cannot access ref value during render
| ^^^^^^^^^^^ Ref value is used during render
5 | }
6 |
```

View File

@@ -15,22 +15,9 @@ function Component(props) {
## Error
```
Found 2 errors:
Found 1 error:
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
error.invalid-set-and-read-ref-during-render.ts:4:2
2 | function Component(props) {
3 | const ref = useRef(null);
> 4 | ref.current = props.value;
| ^^^^^^^^^^^ Cannot update ref during render
5 | return ref.current;
6 | }
7 |
Error: Cannot access refs during render
Error: Cannot access ref value during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
@@ -38,7 +25,7 @@ error.invalid-set-and-read-ref-during-render.ts:5:9
3 | const ref = useRef(null);
4 | ref.current = props.value;
> 5 | return ref.current;
| ^^^^^^^^^^^ Cannot access ref value during render
| ^^^^^^^^^^^ Ref value is used during render
6 | }
7 |
```

View File

@@ -15,22 +15,9 @@ function Component(props) {
## Error
```
Found 2 errors:
Found 1 error:
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
error.invalid-set-and-read-ref-nested-property-during-render.ts:4:2
2 | function Component(props) {
3 | const ref = useRef({inner: null});
> 4 | ref.current.inner = props.value;
| ^^^^^^^^^^^ Cannot update ref during render
5 | return ref.current.inner;
6 | }
7 |
Error: Cannot access refs during render
Error: Cannot access ref value during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
@@ -38,7 +25,15 @@ error.invalid-set-and-read-ref-nested-property-during-render.ts:5:9
3 | const ref = useRef({inner: null});
4 | ref.current.inner = props.value;
> 5 | return ref.current.inner;
| ^^^^^^^^^^^^^^^^^ Cannot access ref value during render
| ^^^^^^^^^^^^^^^^^ Ref value is used during render
6 | }
7 |
error.invalid-set-and-read-ref-nested-property-during-render.ts:5:9
3 | const ref = useRef({inner: null});
4 | ref.current.inner = props.value;
> 5 | return ref.current.inner;
| ^^^^^^^^^^^ Ref is initially accessed
6 | }
7 |
```

View File

@@ -25,19 +25,7 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
Found 2 errors:
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
6 | component C() {
7 | const r = useRef(DEFAULT_VALUE);
> 8 | if (r.current == DEFAULT_VALUE) {
| ^^^^^^^^^ Cannot access ref value during render
9 | r.current = 1;
10 | }
11 | }
Found 1 error:
Error: Cannot access refs during render

View File

@@ -22,7 +22,7 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
Error: Cannot access refs during render
Error: Cannot access ref value during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
@@ -30,7 +30,7 @@ error.ref-optional.ts:5:9
3 | function Component(props) {
4 | const ref = useRef();
> 5 | return ref?.current;
| ^^^^^^^^^^^^ Cannot access ref value during render
| ^^^^^^^^^^^^ Ref value is used during render
6 | }
7 |
8 | export const FIXTURE_ENTRYPOINT = {

View File

@@ -38,15 +38,24 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
Error: Cannot access refs during render
Error: Cannot access ref value during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
error.ref-value-in-event-handler-wrapper.ts:19:6
17 | <>
18 | <input ref={ref} />
> 19 | <button onClick={handleClick(ref.current)}>Click</button>
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Ref value is used during render
20 | </>
21 | );
22 | }
error.ref-value-in-event-handler-wrapper.ts:19:35
17 | <>
18 | <input ref={ref} />
> 19 | <button onClick={handleClick(ref.current)}>Click</button>
| ^^^^^^^^^^^ Cannot access ref value during render
| ^^^^^^^^^^^ Ref is initially accessed
20 | </>
21 | );
22 | }

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validateExhaustiveMemoizationDependencies
// @validateExhaustiveMemoizationDependencies @validateRefAccessDuringRender:false
import {useMemo} from 'react';
import {Stringify} from 'shared-runtime';

View File

@@ -1,4 +1,4 @@
// @validateExhaustiveMemoizationDependencies
// @validateExhaustiveMemoizationDependencies @validateRefAccessDuringRender:false
import {useMemo} from 'react';
import {Stringify} from 'shared-runtime';

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @gating
// @gating
import {isForgetEnabled_Fixtures} from 'ReactForgetFeatureFlag';
export default 42;
@@ -12,7 +12,7 @@ export default 42;
## Code
```javascript
// @expectNothingCompiled @gating
// @gating
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
export default 42;

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @gating
// @gating
import {isForgetEnabled_Fixtures} from 'ReactForgetFeatureFlag';
export default 42;

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Takes multiple parameters - not a component!
function Component(foo, bar) {
return <div />;
@@ -18,7 +18,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Takes multiple parameters - not a component!
function Component(foo, bar) {
return <div />;

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Takes multiple parameters - not a component!
function Component(foo, bar) {
return <div />;

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
import {useIdentity, identity} from 'shared-runtime';
function Component(fakeProps: number) {
@@ -20,7 +20,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
import { useIdentity, identity } from "shared-runtime";
function Component(fakeProps: number) {

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
import {useIdentity, identity} from 'shared-runtime';
function Component(fakeProps: number) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
function Component(props) {
const result = f(props);
function helper() {
@@ -26,7 +26,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
function Component(props) {
const result = f(props);
function helper() {

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
function Component(props) {
const result = f(props);
function helper() {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
function Component(props) {
const ignore = <foo />;
return {foo: f(props)};
@@ -22,7 +22,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
function Component(props) {
const ignore = <foo />;
return { foo: f(props) };

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
function Component(props) {
const ignore = <foo />;
return {foo: f(props)};

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// This component is skipped bc it doesn't call any hooks or
// use JSX:
function Component(props) {
@@ -14,7 +14,7 @@ function Component(props) {
## Code
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// This component is skipped bc it doesn't call any hooks or
// use JSX:
function Component(props) {

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// This component is skipped bc it doesn't call any hooks or
// use JSX:
function Component(props) {

View File

@@ -19,41 +19,65 @@ function Component() {
```
Found 3 errors:
Error: Cannot call impure function during render
Error: Cannot access impure value during render
`Date.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
error.invalid-impure-functions-in-render.ts:7:20
5 | const now = performance.now();
6 | const rand = Math.random();
> 7 | return <Foo date={date} now={now} rand={rand} />;
| ^^^^ Cannot access impure value during render
8 | }
9 |
error.invalid-impure-functions-in-render.ts:4:15
2 |
3 | function Component() {
> 4 | const date = Date.now();
| ^^^^^^^^^^ Cannot call impure function
| ^^^^^^^^^^ `Date.now` is an impure function.
5 | const now = performance.now();
6 | const rand = Math.random();
7 | return <Foo date={date} now={now} rand={rand} />;
Error: Cannot call impure function during render
Error: Cannot access impure value during render
`performance.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
error.invalid-impure-functions-in-render.ts:7:31
5 | const now = performance.now();
6 | const rand = Math.random();
> 7 | return <Foo date={date} now={now} rand={rand} />;
| ^^^ Cannot access impure value during render
8 | }
9 |
error.invalid-impure-functions-in-render.ts:5:14
3 | function Component() {
4 | const date = Date.now();
> 5 | const now = performance.now();
| ^^^^^^^^^^^^^^^^^ Cannot call impure function
| ^^^^^^^^^^^^^^^^^ `performance.now` is an impure function.
6 | const rand = Math.random();
7 | return <Foo date={date} now={now} rand={rand} />;
8 | }
Error: Cannot call impure function during render
Error: Cannot access impure value during render
`Math.random` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
error.invalid-impure-functions-in-render.ts:7:42
5 | const now = performance.now();
6 | const rand = Math.random();
> 7 | return <Foo date={date} now={now} rand={rand} />;
| ^^^^ Cannot access impure value during render
8 | }
9 |
error.invalid-impure-functions-in-render.ts:6:15
4 | const date = Date.now();
5 | const now = performance.now();
> 6 | const rand = Math.random();
| ^^^^^^^^^^^^^ Cannot call impure function
| ^^^^^^^^^^^^^ `Math.random` is an impure function.
7 | return <Foo date={date} now={now} rand={rand} />;
8 | }
9 |

View File

@@ -6,7 +6,7 @@
import {useRef} from 'react';
component Foo() {
hook useFoo() {
const ref = useRef();
const s = () => {
@@ -16,6 +16,10 @@ component Foo() {
return s;
}
component Foo() {
useFoo();
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [],
@@ -30,7 +34,7 @@ import { c as _c } from "react/compiler-runtime";
import { useRef } from "react";
function Foo() {
function useFoo() {
const $ = _c(1);
const ref = useRef();
let t0;
@@ -44,6 +48,10 @@ function Foo() {
return s;
}
function Foo() {
useFoo();
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [],
@@ -52,4 +60,4 @@ export const FIXTURE_ENTRYPOINT = {
```
### Eval output
(kind: ok) "[[ function params=0 ]]"
(kind: ok)

View File

@@ -2,7 +2,7 @@
import {useRef} from 'react';
component Foo() {
hook useFoo() {
const ref = useRef();
const s = () => {
@@ -12,6 +12,10 @@ component Foo() {
return s;
}
component Foo() {
useFoo();
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [],

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Regression test for some internal code.
// This shows how the "callback rule" is more relaxed,
// and doesn't kick in unless we're confident we're in
@@ -20,7 +20,7 @@ function makeListener(instance) {
## Code
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Regression test for some internal code.
// This shows how the "callback rule" is more relaxed,
// and doesn't kick in unless we're confident we're in

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Regression test for some internal code.
// This shows how the "callback rule" is more relaxed,
// and doesn't kick in unless we're confident we're in

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Valid because hooks can call hooks.
function createHook() {
return function useHook() {
@@ -16,7 +16,7 @@ function createHook() {
## Code
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Valid because hooks can call hooks.
function createHook() {
return function useHook() {

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Valid because hooks can call hooks.
function createHook() {
return function useHook() {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Valid because hooks can use hooks.
function createHook() {
return function useHookWithHook() {
@@ -15,7 +15,7 @@ function createHook() {
## Code
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Valid because hooks can use hooks.
function createHook() {
return function useHookWithHook() {

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Valid because hooks can use hooks.
function createHook() {
return function useHookWithHook() {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Valid because components can use hooks.
function createComponentWithHook() {
return function ComponentWithHook() {
@@ -15,7 +15,7 @@ function createComponentWithHook() {
## Code
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Valid because components can use hooks.
function createComponentWithHook() {
return function ComponentWithHook() {

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Valid because components can use hooks.
function createComponentWithHook() {
return function ComponentWithHook() {

View File

@@ -2,7 +2,6 @@
## Input
```javascript
// @expectNothingCompiled
// Valid because they're not matching use[A-Z].
fooState();
_use();
@@ -16,7 +15,6 @@ jest.useFakeTimer();
## Code
```javascript
// @expectNothingCompiled
// Valid because they're not matching use[A-Z].
fooState();
_use();

View File

@@ -1,4 +1,3 @@
// @expectNothingCompiled
// Valid because they're not matching use[A-Z].
fooState();
_use();

View File

@@ -2,7 +2,6 @@
## Input
```javascript
// @expectNothingCompiled
// Valid because classes can call functions.
// We don't consider these to be hooks.
class C {
@@ -17,7 +16,6 @@ class C {
## Code
```javascript
// @expectNothingCompiled
// Valid because classes can call functions.
// We don't consider these to be hooks.
class C {

View File

@@ -1,4 +1,3 @@
// @expectNothingCompiled
// Valid because classes can call functions.
// We don't consider these to be hooks.
class C {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// This is valid because "use"-prefixed functions called in
// unnamed function arguments are not assumed to be hooks.
unknownFunction(function (foo, bar) {
@@ -16,7 +16,7 @@ unknownFunction(function (foo, bar) {
## Code
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// This is valid because "use"-prefixed functions called in
// unnamed function arguments are not assumed to be hooks.
unknownFunction(function (foo, bar) {

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// This is valid because "use"-prefixed functions called in
// unnamed function arguments are not assumed to be hooks.
unknownFunction(function (foo, bar) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
// Invalid because it's dangerous.
@@ -22,7 +22,7 @@ useCustomHook();
## Code
```javascript
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
// Invalid because it's dangerous.

Some files were not shown because too many files have changed in this diff Show More