Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae3c5a707f | ||
|
|
cb021861a5 | ||
|
|
7505808cd4 | ||
|
|
a974753ddf | ||
|
|
2c4a3b9587 | ||
|
|
8428a7ec5f |
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(yarn snap:*)",
|
||||
"Bash(yarn snap:build)",
|
||||
"Bash(node scripts/enable-feature-flag.js:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
16
compiler/.claude/settings.local.json
Normal file
16
compiler/.claude/settings.local.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(node scripts/enable-feature-flag.js:*)",
|
||||
"Bash(yarn snap:*)",
|
||||
"Bash(for test in \"error.invalid-access-ref-during-render\" \"error.invalid-ref-in-callback-invoked-during-render\" \"error.invalid-impure-functions-in-render-via-render-helper\")",
|
||||
"Bash(do)",
|
||||
"Bash(echo:*)",
|
||||
"Bash(done)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(sl revert:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
2
compiler/.gitignore
vendored
2
compiler/.gitignore
vendored
@@ -8,9 +8,7 @@ dist
|
||||
.vscode
|
||||
!packages/playground/.vscode
|
||||
testfilter.txt
|
||||
.claude/settings.local.json
|
||||
|
||||
# forgive
|
||||
*.vsix
|
||||
.vscode-test
|
||||
|
||||
|
||||
@@ -10,8 +10,7 @@ This document contains knowledge about the React Compiler gathered during develo
|
||||
- `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
|
||||
- `error.*.js` - Fixtures that should produce compilation errors
|
||||
- `*.expect.md` - Expected output for each fixture
|
||||
|
||||
## Running Tests
|
||||
@@ -20,30 +19,21 @@ This document contains knowledge about the React Compiler gathered during develo
|
||||
# Run all tests
|
||||
yarn snap
|
||||
|
||||
# Run tests matching a pattern
|
||||
# Example: yarn snap -p 'error.*'
|
||||
# Run specific test by pattern
|
||||
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)
|
||||
# Update fixture outputs
|
||||
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.
|
||||
This repository uses Sapling (`sl`) for version control. Unlike git, Sapling does not require explicitly adding files to the staging area.
|
||||
|
||||
```bash
|
||||
# Check status
|
||||
sl status
|
||||
|
||||
# Add new files, remove deleted files
|
||||
sl addremove
|
||||
|
||||
# Commit all changes
|
||||
sl commit -m "Your commit message"
|
||||
|
||||
@@ -183,17 +173,69 @@ const UseEffectEventHook = addHook(
|
||||
|
||||
**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
|
||||
### Effect Inference Pipeline
|
||||
|
||||
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:
|
||||
Effects are populated by `InferMutationAliasingEffects` (runs before validation):
|
||||
|
||||
```javascript
|
||||
// enableJsxOutlining @enableChangeVariableCodegen:false
|
||||
1. For `Date.now()`, `Math.random()` etc. - adds `Impure` effect (controlled by `validateNoImpureFunctionsInRender` config)
|
||||
2. For `ref.current` access - adds `Impure` effect (controlled by `validateRefAccessDuringRender` config)
|
||||
3. For return terminals - adds `Alias` from return value to `fn.returns`
|
||||
4. For component/JSX returns - adds `Render` effect
|
||||
5. For function expressions - adds `CreateFunction` effect with captures
|
||||
|
||||
...code...
|
||||
```
|
||||
### Validation: validateNoImpureValuesInRender
|
||||
|
||||
Would enable the `enableJsxOutlining` feature and disable the `enableChangeVariableCodegen` feature.
|
||||
Located at `src/Validation/ValidateNoImpureValuesInRender.ts`
|
||||
|
||||
**Purpose:** Detect when impure values (refs, Date.now results, etc.) flow into render context.
|
||||
|
||||
**Algorithm:**
|
||||
1. Track impure values in a Map<IdentifierId, ImpureEffect>
|
||||
2. Track functions with impure returns separately (they're not impure values themselves)
|
||||
3. Fixed-point iteration over all blocks:
|
||||
- Process phi nodes (any impure operand makes result impure)
|
||||
- Process instruction effects
|
||||
- Process terminal effects
|
||||
- Backwards propagation for mutated LoadLocal values
|
||||
4. Validate: check all Render effects against impure values
|
||||
|
||||
**Key patterns:**
|
||||
- `Impure` effect marks the target as impure
|
||||
- `Capture/Alias/etc` propagates impurity from source to target
|
||||
- `Apply` propagates impurity from args/receiver to result
|
||||
- `CreateFunction` propagates impurity from captured values (but NOT from body effects)
|
||||
- If a value has both `Render` and `Capture` in same instruction, only error on Render (don't cascade)
|
||||
|
||||
**Tracking functions with impure returns:**
|
||||
- Separate from the `impure` map (function values aren't impure, just their returns)
|
||||
- Populated when analyzing FunctionExpression bodies
|
||||
- Used when:
|
||||
1. Calling the function - mark call result as impure
|
||||
2. Capturing the function - mark target as impure (for object.foo = impureFunc cases)
|
||||
|
||||
**Backwards propagation:**
|
||||
- When `$x = LoadLocal y` and `$x` is mutated with impure content, mark `y` as impure
|
||||
- This handles: `const arr = []; arr.push(impure); render(arr)`
|
||||
|
||||
## Known Issues / Edge Cases
|
||||
|
||||
### Function Outlining
|
||||
After `OutlineFunctions` pass, inner functions are replaced with `LoadGlobal(_temp)`. The validation runs BEFORE outlining, so it sees the original FunctionExpression. But be aware that test output shows post-outlining HIR.
|
||||
|
||||
### SSA and LoadLocal
|
||||
In SSA form, each `LoadLocal` creates a new identifier. When a loaded value is mutated:
|
||||
- `$x = LoadLocal y`
|
||||
- `mutate($x, impure)`
|
||||
- `$z = LoadLocal y` (different from $x!)
|
||||
- `render($z)`
|
||||
|
||||
The impurity in $x must propagate back to y, then forward to $z. This requires backwards propagation in the fixed-point loop.
|
||||
|
||||
## Configuration Flags
|
||||
|
||||
In `Environment.ts` / test directives:
|
||||
- `validateNoImpureFunctionsInRender` - Enable impure function validation (Date.now, Math.random, etc.)
|
||||
- `validateRefAccessDuringRender` - Enable ref access validation
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
@@ -201,21 +243,3 @@ Would enable the `enableJsxOutlining` feature and disable the `enableChangeVaria
|
||||
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,
|
||||
});
|
||||
```
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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, [])],
|
||||
];
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -988,7 +988,7 @@ export function createTemporaryPlace(
|
||||
identifier: makeTemporaryIdentifier(env.nextIdentifierId, loc),
|
||||
reactive: false,
|
||||
effect: Effect.Unknown,
|
||||
loc: GeneratedSource,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)}`;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(':');
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) ?? [];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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 />;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
function Component() {
|
||||
const Foo = () => {
|
||||
someGlobal = true;
|
||||
return <div />;
|
||||
};
|
||||
return <Foo />;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -32,33 +32,22 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access refs during render
|
||||
Error: Mutating refs during render is not allowed
|
||||
|
||||
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).
|
||||
React refs are mutable containers that should only be mutated outside of render, such as in event handlers or effects. Mutating a ref during render can cause bugs because the mutation may not be associated with a particular render. See https://react.dev/reference/react/useRef.
|
||||
|
||||
error.capture-ref-for-mutation.ts:12:13
|
||||
error.capture-ref-for-mutation.ts:9:4
|
||||
7 | const position = currentPosition.current;
|
||||
8 | const nextPosition = direction === 'left' ? addOne(position) : position;
|
||||
> 9 | currentPosition.current = nextPosition;
|
||||
| ^^^^^^^^^^^^^^^ Cannot mutate ref during render
|
||||
10 | };
|
||||
11 | const moveLeft = {
|
||||
> 12 | handler: handleKey('left')(),
|
||||
| ^^^^^^^^^^^^^^^^^ This function accesses a ref value
|
||||
13 | };
|
||||
14 | const moveRight = {
|
||||
15 | handler: handleKey('right')(),
|
||||
12 | handler: handleKey('left')(),
|
||||
|
||||
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.capture-ref-for-mutation.ts:15:13
|
||||
13 | };
|
||||
14 | const moveRight = {
|
||||
> 15 | handler: handleKey('right')(),
|
||||
| ^^^^^^^^^^^^^^^^^^ This function accesses a ref value
|
||||
16 | };
|
||||
17 | return [moveLeft, moveRight];
|
||||
18 | }
|
||||
Refs may be mutated during render if initialized with `if (ref.current == null)`
|
||||
```
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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} />;
|
||||
|
||||
@@ -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 | }
|
||||
|
||||
@@ -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 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -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 | }
|
||||
|
||||
@@ -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>;
|
||||
```
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ React refs are values that are not needed for rendering. Refs should only be acc
|
||||
4 | component Example() {
|
||||
5 | const fooRef = makeObject_Primitives();
|
||||
> 6 | fooRef.current = true;
|
||||
| ^^^^^^^^^^^^^^ Cannot update ref during render
|
||||
| ^^^^^^ Cannot update ref during render
|
||||
7 |
|
||||
8 | return <Stringify foo={fooRef} />;
|
||||
9 | }
|
||||
|
||||
@@ -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 |
|
||||
```
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ error.invalid-disallow-mutating-ref-in-render.ts:4:2
|
||||
2 | function Component() {
|
||||
3 | const ref = useRef(null);
|
||||
> 4 | ref.current = false;
|
||||
| ^^^^^^^^^^^ Cannot update ref during render
|
||||
| ^^^ Cannot update ref during render
|
||||
5 |
|
||||
6 | return <button ref={ref} />;
|
||||
7 | }
|
||||
|
||||
@@ -27,14 +27,14 @@ 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-disallow-mutating-refs-in-render-transitive.ts:9:2
|
||||
7 | };
|
||||
8 | const changeRef = setRef;
|
||||
> 9 | changeRef();
|
||||
| ^^^^^^^^^ This function accesses a ref value
|
||||
10 |
|
||||
11 | return <button ref={ref} />;
|
||||
12 | }
|
||||
error.invalid-disallow-mutating-refs-in-render-transitive.ts:6:4
|
||||
4 |
|
||||
5 | const setRef = () => {
|
||||
> 6 | ref.current = false;
|
||||
| ^^^ Cannot update ref during render
|
||||
7 | };
|
||||
8 | const changeRef = setRef;
|
||||
9 | changeRef();
|
||||
```
|
||||
|
||||
|
||||
@@ -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);
|
||||
```
|
||||
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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} />;
|
||||
```
|
||||
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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);
|
||||
```
|
||||
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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());
|
||||
```
|
||||
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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);
|
||||
```
|
||||
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 | };
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// @validateNoImpureFunctionsInRender
|
||||
function Component() {
|
||||
const now = () => Date.now();
|
||||
const render = () => {
|
||||
return <div>{now()}</div>;
|
||||
};
|
||||
return <div>{render()}</div>;
|
||||
}
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
```
|
||||
|
||||
|
||||
@@ -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>;
|
||||
```
|
||||
|
||||
|
||||
@@ -29,15 +29,13 @@ Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
4 | component C() {
|
||||
5 | const r = useRef(null);
|
||||
> 6 | if (!r.current) {
|
||||
| ^^^^^^^^^ Cannot access ref value during render
|
||||
7 | r.current = 1;
|
||||
8 | }
|
||||
9 | }
|
||||
|
||||
To initialize a ref only once, check that the ref is null with the pattern `if (ref.current == null) { ref.current = ... }`
|
||||
5 | const r = useRef(null);
|
||||
6 | if (!r.current) {
|
||||
> 7 | r.current = 1;
|
||||
| ^ Cannot update ref during render
|
||||
8 | }
|
||||
9 | }
|
||||
10 |
|
||||
```
|
||||
|
||||
|
||||
@@ -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 |
|
||||
```
|
||||
|
||||
@@ -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 |
|
||||
```
|
||||
|
||||
@@ -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 |
|
||||
```
|
||||
|
||||
@@ -27,7 +27,7 @@ error.invalid-write-but-dont-read-ref-in-render.ts:5:2
|
||||
3 | const ref = useRef(null);
|
||||
4 | // Writing to a ref in render is against the rules:
|
||||
> 5 | ref.current = value;
|
||||
| ^^^^^^^^^^^ Cannot update ref during render
|
||||
| ^^^ Cannot update ref during render
|
||||
6 | // returning a ref is allowed, so this alone doesn't trigger an error:
|
||||
7 | return ref;
|
||||
8 | }
|
||||
|
||||
@@ -25,7 +25,7 @@ error.invalid-write-ref-prop-in-render.ts:4:2
|
||||
2 | function Component(props) {
|
||||
3 | const ref = props.ref;
|
||||
> 4 | ref.current = true;
|
||||
| ^^^^^^^^^^^ Cannot update ref during render
|
||||
| ^^^ Cannot update ref during render
|
||||
5 | return <div>{value}</div>;
|
||||
6 | }
|
||||
7 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -46,7 +34,7 @@ React refs are values that are not needed for rendering. Refs should only be acc
|
||||
7 | const r = useRef(DEFAULT_VALUE);
|
||||
8 | if (r.current == DEFAULT_VALUE) {
|
||||
> 9 | r.current = 1;
|
||||
| ^^^^^^^^^ Cannot update ref during render
|
||||
| ^ Cannot update ref during render
|
||||
10 | }
|
||||
11 | }
|
||||
12 |
|
||||
|
||||
@@ -24,19 +24,35 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
Found 2 errors:
|
||||
|
||||
Error: Cannot access refs during render
|
||||
Error: Mutating refs during render is not allowed
|
||||
|
||||
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).
|
||||
React refs are mutable containers that should only be mutated outside of render, such as in event handlers or effects. Mutating a ref during render can cause bugs because the mutation may not be associated with a particular render. See https://react.dev/reference/react/useRef.
|
||||
|
||||
5 | const r = useRef(null);
|
||||
6 | if (r.current == null) {
|
||||
> 7 | r.current = 42;
|
||||
| ^ Cannot mutate ref during render
|
||||
8 | r.current = 42;
|
||||
9 | }
|
||||
10 | }
|
||||
|
||||
Refs may be mutated during render if initialized with `if (ref.current == null)`
|
||||
|
||||
Error: Mutating refs during render is not allowed
|
||||
|
||||
React refs are mutable containers that should only be mutated outside of render, such as in event handlers or effects. Mutating a ref during render can cause bugs because the mutation may not be associated with a particular render. See https://react.dev/reference/react/useRef.
|
||||
|
||||
6 | if (r.current == null) {
|
||||
7 | r.current = 42;
|
||||
> 8 | r.current = 42;
|
||||
| ^^^^^^^^^ Cannot update ref during render
|
||||
| ^ Cannot mutate ref during render
|
||||
9 | }
|
||||
10 | }
|
||||
11 |
|
||||
|
||||
Refs may be mutated during render if initialized with `if (ref.current == null)`
|
||||
```
|
||||
|
||||
|
||||
@@ -24,31 +24,19 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## 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).
|
||||
|
||||
4 | component C() {
|
||||
5 | const r = useRef(null);
|
||||
> 6 | const guard = r.current == null;
|
||||
| ^^^^^^^^^^^^^^^^^ Cannot access ref value during render
|
||||
7 | if (guard) {
|
||||
8 | r.current = 1;
|
||||
9 | }
|
||||
|
||||
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 guard = r.current == null;
|
||||
> 7 | if (guard) {
|
||||
| ^^^^^ Cannot access ref value during render
|
||||
8 | r.current = 1;
|
||||
7 | if (guard) {
|
||||
> 8 | r.current = 1;
|
||||
| ^ Cannot update ref during render
|
||||
9 | }
|
||||
10 | }
|
||||
11 |
|
||||
```
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ React refs are values that are not needed for rendering. Refs should only be acc
|
||||
6 | const r2 = useRef(null);
|
||||
7 | if (r.current == null) {
|
||||
> 8 | r2.current = 1;
|
||||
| ^^^^^^^^^^ Cannot update ref during render
|
||||
| ^^ Cannot update ref during render
|
||||
9 | }
|
||||
10 | }
|
||||
11 |
|
||||
|
||||
@@ -26,17 +26,19 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access refs during render
|
||||
Error: Mutating refs during render is not allowed
|
||||
|
||||
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).
|
||||
React refs are mutable containers that should only be mutated outside of render, such as in event handlers or effects. Mutating a ref during render can cause bugs because the mutation may not be associated with a particular render. See https://react.dev/reference/react/useRef.
|
||||
|
||||
7 | r.current = 1;
|
||||
5 | const r = useRef(null);
|
||||
6 | if (r.current == null) {
|
||||
> 7 | r.current = 1;
|
||||
| ^ Cannot mutate ref during render
|
||||
8 | }
|
||||
> 9 | f(r.current);
|
||||
| ^^^^^^^^^ Passing a ref to a function may read its value during render
|
||||
9 | f(r.current);
|
||||
10 | }
|
||||
11 |
|
||||
12 | export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
Refs may be mutated during render if initialized with `if (ref.current == null)`
|
||||
```
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ React refs are values that are not needed for rendering. Refs should only be acc
|
||||
7 | r.current = 1;
|
||||
8 | }
|
||||
> 9 | r.current = 1;
|
||||
| ^^^^^^^^^ Cannot update ref during render
|
||||
| ^ Cannot update ref during render
|
||||
10 | }
|
||||
11 |
|
||||
12 | export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 | }
|
||||
|
||||
@@ -40,14 +40,14 @@ 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.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.ts:17:2
|
||||
15 | ref.current.inner = null;
|
||||
error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.ts:15:4
|
||||
13 | // The ref is modified later, extending its range and preventing memoization of onChange
|
||||
14 | const reset = () => {
|
||||
> 15 | ref.current.inner = null;
|
||||
| ^^^^^^^^^^^ Cannot update ref during render
|
||||
16 | };
|
||||
> 17 | reset();
|
||||
| ^^^^^ This function accesses a ref value
|
||||
17 | reset();
|
||||
18 |
|
||||
19 | return <input onChange={onChange} />;
|
||||
20 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
// @validateExhaustiveMemoizationDependencies @validateRefAccessDuringRender:false
|
||||
import {useMemo} from 'react';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
// @validateExhaustiveMemoizationDependencies @validateRefAccessDuringRender:false
|
||||
import {useMemo} from 'react';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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)
|
||||
@@ -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: [],
|
||||
|
||||
@@ -25,22 +25,40 @@ component Component(prop1, ref) {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { useFire } from "react/compiler-runtime";
|
||||
import { c as _c, useFire } from "react/compiler-runtime";
|
||||
import { fire } from "react";
|
||||
import { print } from "shared-runtime";
|
||||
|
||||
const Component = React.forwardRef(Component_withRef);
|
||||
function Component_withRef(t0, ref) {
|
||||
const $ = _c(5);
|
||||
const { prop1 } = t0;
|
||||
const foo = () => {
|
||||
console.log(prop1);
|
||||
};
|
||||
const t1 = useFire(foo);
|
||||
useEffect(() => {
|
||||
t1(prop1);
|
||||
bar();
|
||||
t1();
|
||||
});
|
||||
let t1;
|
||||
if ($[0] !== prop1) {
|
||||
t1 = () => {
|
||||
console.log(prop1);
|
||||
};
|
||||
$[0] = prop1;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const foo = t1;
|
||||
const t2 = useFire(foo);
|
||||
let t3;
|
||||
if ($[2] !== prop1 || $[3] !== t2) {
|
||||
t3 = () => {
|
||||
t2(prop1);
|
||||
bar();
|
||||
t2();
|
||||
};
|
||||
$[2] = prop1;
|
||||
$[3] = t2;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
useEffect(t3);
|
||||
print(ref.current);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoImpureFunctionsInRender
|
||||
import {useIdentity} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const f = () => Math.random();
|
||||
const ref = useRef(f());
|
||||
return <div ref={ref} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoImpureFunctionsInRender
|
||||
import { useIdentity } from "shared-runtime";
|
||||
|
||||
function Component() {
|
||||
const $ = _c(2);
|
||||
const f = _temp;
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = f();
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const ref = useRef(t0);
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <div ref={ref} />;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
function _temp() {
|
||||
return Math.random();
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,8 @@
|
||||
// @validateNoImpureFunctionsInRender
|
||||
import {useIdentity} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const f = () => Math.random();
|
||||
const ref = useRef(f());
|
||||
return <div ref={ref} />;
|
||||
}
|
||||
@@ -29,9 +29,9 @@ testRule(
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
makeTestCaseError('Cannot call impure function during render'),
|
||||
makeTestCaseError('Cannot call impure function during render'),
|
||||
makeTestCaseError('Cannot call impure function during render'),
|
||||
makeTestCaseError('Cannot access impure value during render'),
|
||||
makeTestCaseError('Cannot access impure value during render'),
|
||||
makeTestCaseError('Cannot access impure value during render'),
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -26,3 +26,5 @@ export const FIXTURES_PATH = path.join(
|
||||
'compiler',
|
||||
);
|
||||
export const SNAPSHOT_EXTENSION = '.expect.md';
|
||||
export const FILTER_FILENAME = 'testfilter.txt';
|
||||
export const FILTER_PATH = path.join(PROJECT_ROOT, FILTER_FILENAME);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import fs from 'fs/promises';
|
||||
import * as glob from 'glob';
|
||||
import path from 'path';
|
||||
import {FIXTURES_PATH, SNAPSHOT_EXTENSION} from './constants';
|
||||
import {FILTER_PATH, FIXTURES_PATH, SNAPSHOT_EXTENSION} from './constants';
|
||||
|
||||
const INPUT_EXTENSIONS = [
|
||||
'.js',
|
||||
@@ -22,9 +22,19 @@ const INPUT_EXTENSIONS = [
|
||||
];
|
||||
|
||||
export type TestFilter = {
|
||||
debug: boolean;
|
||||
paths: Array<string>;
|
||||
};
|
||||
|
||||
async function exists(file: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(file);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function stripExtension(filename: string, extensions: Array<string>): string {
|
||||
for (const ext of extensions) {
|
||||
if (filename.endsWith(ext)) {
|
||||
@@ -34,6 +44,37 @@ function stripExtension(filename: string, extensions: Array<string>): string {
|
||||
return filename;
|
||||
}
|
||||
|
||||
export async function readTestFilter(): Promise<TestFilter | null> {
|
||||
if (!(await exists(FILTER_PATH))) {
|
||||
throw new Error(`testfilter file not found at \`${FILTER_PATH}\``);
|
||||
}
|
||||
|
||||
const input = await fs.readFile(FILTER_PATH, 'utf8');
|
||||
const lines = input.trim().split('\n');
|
||||
|
||||
let debug: boolean = false;
|
||||
const line0 = lines[0];
|
||||
if (line0 != null) {
|
||||
// Try to parse pragmas
|
||||
let consumedLine0 = false;
|
||||
if (line0.indexOf('@only') !== -1) {
|
||||
consumedLine0 = true;
|
||||
}
|
||||
if (line0.indexOf('@debug') !== -1) {
|
||||
debug = true;
|
||||
consumedLine0 = true;
|
||||
}
|
||||
|
||||
if (consumedLine0) {
|
||||
lines.shift();
|
||||
}
|
||||
}
|
||||
return {
|
||||
debug,
|
||||
paths: lines.filter(line => !line.trimStart().startsWith('//')),
|
||||
};
|
||||
}
|
||||
|
||||
export function getBasename(fixture: TestFixture): string {
|
||||
return stripExtension(path.basename(fixture.inputPath), INPUT_EXTENSIONS);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
import watcher from '@parcel/watcher';
|
||||
import path from 'path';
|
||||
import ts from 'typescript';
|
||||
import {FIXTURES_PATH, PROJECT_ROOT} from './constants';
|
||||
import {TestFilter} from './fixture-utils';
|
||||
import {FILTER_FILENAME, FIXTURES_PATH, PROJECT_ROOT} from './constants';
|
||||
import {TestFilter, readTestFilter} from './fixture-utils';
|
||||
import {execSync} from 'child_process';
|
||||
|
||||
export function watchSrc(
|
||||
@@ -117,10 +117,6 @@ export type RunnerState = {
|
||||
lastUpdate: number;
|
||||
mode: RunnerMode;
|
||||
filter: TestFilter | null;
|
||||
debug: boolean;
|
||||
// Input mode for interactive pattern entry
|
||||
inputMode: 'none' | 'pattern';
|
||||
inputBuffer: string;
|
||||
};
|
||||
|
||||
function subscribeFixtures(
|
||||
@@ -146,6 +142,26 @@ function subscribeFixtures(
|
||||
});
|
||||
}
|
||||
|
||||
function subscribeFilterFile(
|
||||
state: RunnerState,
|
||||
onChange: (state: RunnerState) => void,
|
||||
) {
|
||||
watcher.subscribe(PROJECT_ROOT, async (err, events) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
} else if (
|
||||
events.findIndex(event => event.path.includes(FILTER_FILENAME)) !== -1
|
||||
) {
|
||||
if (state.mode.filter) {
|
||||
state.filter = await readTestFilter();
|
||||
state.mode.action = RunnerAction.Test;
|
||||
onChange(state);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function subscribeTsc(
|
||||
state: RunnerState,
|
||||
onChange: (state: RunnerState) => void,
|
||||
@@ -184,67 +200,15 @@ function subscribeKeyEvents(
|
||||
onChange: (state: RunnerState) => void,
|
||||
) {
|
||||
process.stdin.on('keypress', async (str, key) => {
|
||||
// Handle input mode (pattern entry)
|
||||
if (state.inputMode !== 'none') {
|
||||
if (key.name === 'return') {
|
||||
// Enter pressed - process input
|
||||
const pattern = state.inputBuffer.trim();
|
||||
state.inputMode = 'none';
|
||||
state.inputBuffer = '';
|
||||
process.stdout.write('\n');
|
||||
|
||||
if (pattern !== '') {
|
||||
// Set the pattern as filter
|
||||
state.filter = {paths: [pattern]};
|
||||
state.mode.filter = true;
|
||||
state.mode.action = RunnerAction.Test;
|
||||
onChange(state);
|
||||
}
|
||||
// If empty, just exit input mode without changes
|
||||
return;
|
||||
} else if (key.name === 'escape') {
|
||||
// Cancel input mode
|
||||
state.inputMode = 'none';
|
||||
state.inputBuffer = '';
|
||||
process.stdout.write(' (cancelled)\n');
|
||||
return;
|
||||
} else if (key.name === 'backspace') {
|
||||
if (state.inputBuffer.length > 0) {
|
||||
state.inputBuffer = state.inputBuffer.slice(0, -1);
|
||||
// Erase character: backspace, space, backspace
|
||||
process.stdout.write('\b \b');
|
||||
}
|
||||
return;
|
||||
} else if (str && !key.ctrl && !key.meta) {
|
||||
// Regular character - accumulate and echo
|
||||
state.inputBuffer += str;
|
||||
process.stdout.write(str);
|
||||
return;
|
||||
}
|
||||
return; // Ignore other keys in input mode
|
||||
}
|
||||
|
||||
// Normal mode keypress handling
|
||||
if (key.name === 'u') {
|
||||
// u => update fixtures
|
||||
state.mode.action = RunnerAction.Update;
|
||||
} else if (key.name === 'q') {
|
||||
process.exit(0);
|
||||
} else if (key.name === 'a') {
|
||||
// a => exit filter mode and run all tests
|
||||
state.mode.filter = false;
|
||||
state.filter = null;
|
||||
} else if (key.name === 'f') {
|
||||
state.mode.filter = !state.mode.filter;
|
||||
state.filter = state.mode.filter ? await readTestFilter() : null;
|
||||
state.mode.action = RunnerAction.Test;
|
||||
} else if (key.name === 'd') {
|
||||
// d => toggle debug logging
|
||||
state.debug = !state.debug;
|
||||
state.mode.action = RunnerAction.Test;
|
||||
} else if (key.name === 'p') {
|
||||
// p => enter pattern input mode
|
||||
state.inputMode = 'pattern';
|
||||
state.inputBuffer = '';
|
||||
process.stdout.write('Pattern: ');
|
||||
return; // Don't trigger onChange yet
|
||||
} else {
|
||||
// any other key re-runs tests
|
||||
state.mode.action = RunnerAction.Test;
|
||||
@@ -255,33 +219,21 @@ function subscribeKeyEvents(
|
||||
|
||||
export async function makeWatchRunner(
|
||||
onChange: (state: RunnerState) => void,
|
||||
debugMode: boolean,
|
||||
initialPattern?: string,
|
||||
filterMode: boolean,
|
||||
): Promise<void> {
|
||||
// Determine initial filter state
|
||||
let filter: TestFilter | null = null;
|
||||
let filterEnabled = false;
|
||||
|
||||
if (initialPattern) {
|
||||
filter = {paths: [initialPattern]};
|
||||
filterEnabled = true;
|
||||
}
|
||||
|
||||
const state: RunnerState = {
|
||||
const state = {
|
||||
compilerVersion: 0,
|
||||
isCompilerBuildValid: false,
|
||||
lastUpdate: -1,
|
||||
mode: {
|
||||
action: RunnerAction.Test,
|
||||
filter: filterEnabled,
|
||||
filter: filterMode,
|
||||
},
|
||||
filter,
|
||||
debug: debugMode,
|
||||
inputMode: 'none',
|
||||
inputBuffer: '',
|
||||
filter: filterMode ? await readTestFilter() : null,
|
||||
};
|
||||
|
||||
subscribeTsc(state, onChange);
|
||||
subscribeFixtures(state, onChange);
|
||||
subscribeKeyEvents(state, onChange);
|
||||
subscribeFilterFile(state, onChange);
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import * as readline from 'readline';
|
||||
import ts from 'typescript';
|
||||
import yargs from 'yargs';
|
||||
import {hideBin} from 'yargs/helpers';
|
||||
import {PROJECT_ROOT} from './constants';
|
||||
import {TestFilter, getFixtures} from './fixture-utils';
|
||||
import {FILTER_PATH, PROJECT_ROOT} from './constants';
|
||||
import {TestFilter, getFixtures, readTestFilter} from './fixture-utils';
|
||||
import {TestResult, TestResults, report, update} from './reporter';
|
||||
import {
|
||||
RunnerAction,
|
||||
@@ -33,9 +33,9 @@ type RunnerOptions = {
|
||||
sync: boolean;
|
||||
workerThreads: boolean;
|
||||
watch: boolean;
|
||||
filter: boolean;
|
||||
update: boolean;
|
||||
pattern?: string;
|
||||
debug: boolean;
|
||||
};
|
||||
|
||||
const opts: RunnerOptions = yargs
|
||||
@@ -59,16 +59,18 @@ const opts: RunnerOptions = yargs
|
||||
.alias('u', 'update')
|
||||
.describe('update', 'Update fixtures')
|
||||
.default('update', false)
|
||||
.boolean('filter')
|
||||
.describe(
|
||||
'filter',
|
||||
'Only run fixtures which match the contents of testfilter.txt',
|
||||
)
|
||||
.default('filter', false)
|
||||
.string('pattern')
|
||||
.alias('p', 'pattern')
|
||||
.describe(
|
||||
'pattern',
|
||||
'Optional glob pattern to filter fixtures (e.g., "error.*", "use-memo")',
|
||||
)
|
||||
.boolean('debug')
|
||||
.alias('d', 'debug')
|
||||
.describe('debug', 'Enable debug logging to print HIR for each pass')
|
||||
.default('debug', false)
|
||||
.help('help')
|
||||
.strict()
|
||||
.parseSync(hideBin(process.argv)) as RunnerOptions;
|
||||
@@ -80,15 +82,12 @@ async function runFixtures(
|
||||
worker: Worker & typeof runnerWorker,
|
||||
filter: TestFilter | null,
|
||||
compilerVersion: number,
|
||||
debug: boolean,
|
||||
requireSingleFixture: boolean,
|
||||
): Promise<TestResults> {
|
||||
// We could in theory be fancy about tracking the contents of the fixtures
|
||||
// directory via our file subscription, but it's simpler to just re-read
|
||||
// the directory each time.
|
||||
const fixtures = await getFixtures(filter);
|
||||
const isOnlyFixture = filter !== null && fixtures.size === 1;
|
||||
const shouldLog = debug && (!requireSingleFixture || isOnlyFixture);
|
||||
|
||||
let entries: Array<[string, TestResult]>;
|
||||
if (!opts.sync) {
|
||||
@@ -97,7 +96,12 @@ async function runFixtures(
|
||||
for (const [fixtureName, fixture] of fixtures) {
|
||||
work.push(
|
||||
worker
|
||||
.transformFixture(fixture, compilerVersion, shouldLog, true)
|
||||
.transformFixture(
|
||||
fixture,
|
||||
compilerVersion,
|
||||
(filter?.debug ?? false) && isOnlyFixture,
|
||||
true,
|
||||
)
|
||||
.then(result => [fixtureName, result]),
|
||||
);
|
||||
}
|
||||
@@ -109,7 +113,7 @@ async function runFixtures(
|
||||
let output = await runnerWorker.transformFixture(
|
||||
fixture,
|
||||
compilerVersion,
|
||||
shouldLog,
|
||||
(filter?.debug ?? false) && isOnlyFixture,
|
||||
true,
|
||||
);
|
||||
entries.push([fixtureName, output]);
|
||||
@@ -124,7 +128,7 @@ async function onChange(
|
||||
worker: Worker & typeof runnerWorker,
|
||||
state: RunnerState,
|
||||
) {
|
||||
const {compilerVersion, isCompilerBuildValid, mode, filter, debug} = state;
|
||||
const {compilerVersion, isCompilerBuildValid, mode, filter} = state;
|
||||
if (isCompilerBuildValid) {
|
||||
const start = performance.now();
|
||||
|
||||
@@ -138,8 +142,6 @@ async function onChange(
|
||||
worker,
|
||||
mode.filter ? filter : null,
|
||||
compilerVersion,
|
||||
debug,
|
||||
true, // requireSingleFixture in watch mode
|
||||
);
|
||||
const end = performance.now();
|
||||
if (mode.action === RunnerAction.Update) {
|
||||
@@ -157,13 +159,11 @@ async function onChange(
|
||||
console.log(
|
||||
'\n' +
|
||||
(mode.filter
|
||||
? `Current mode = FILTER, pattern = "${filter?.paths[0] ?? ''}".`
|
||||
? `Current mode = FILTER, filter test fixtures by "${FILTER_PATH}".`
|
||||
: 'Current mode = NORMAL, run all test fixtures.') +
|
||||
'\nWaiting for input or file changes...\n' +
|
||||
'u - update all fixtures\n' +
|
||||
`d - toggle (turn ${debug ? 'off' : 'on'}) debug logging\n` +
|
||||
'p - enter pattern to filter fixtures\n' +
|
||||
(mode.filter ? 'a - run all tests (exit filter mode)\n' : '') +
|
||||
`f - toggle (turn ${mode.filter ? 'off' : 'on'}) filter mode\n` +
|
||||
'q - quit\n' +
|
||||
'[any] - rerun tests\n',
|
||||
);
|
||||
@@ -180,12 +180,15 @@ export async function main(opts: RunnerOptions): Promise<void> {
|
||||
worker.getStderr().pipe(process.stderr);
|
||||
worker.getStdout().pipe(process.stdout);
|
||||
|
||||
// Check if watch mode should be enabled
|
||||
const shouldWatch = opts.watch;
|
||||
// If pattern is provided, force watch mode off and use pattern filter
|
||||
const shouldWatch = opts.watch && opts.pattern == null;
|
||||
if (opts.watch && opts.pattern != null) {
|
||||
console.warn('NOTE: --watch is ignored when a --pattern is supplied');
|
||||
}
|
||||
|
||||
if (shouldWatch) {
|
||||
makeWatchRunner(state => onChange(worker, state), opts.debug, opts.pattern);
|
||||
if (opts.pattern) {
|
||||
makeWatchRunner(state => onChange(worker, state), opts.filter);
|
||||
if (opts.filter) {
|
||||
/**
|
||||
* Warm up wormers when in watch mode. Loading the Forget babel plugin
|
||||
* and all of its transitive dependencies takes 1-3s (per worker) on a M1.
|
||||
@@ -233,17 +236,14 @@ export async function main(opts: RunnerOptions): Promise<void> {
|
||||
let testFilter: TestFilter | null = null;
|
||||
if (opts.pattern) {
|
||||
testFilter = {
|
||||
debug: true,
|
||||
paths: [opts.pattern],
|
||||
};
|
||||
} else if (opts.filter) {
|
||||
testFilter = await readTestFilter();
|
||||
}
|
||||
|
||||
const results = await runFixtures(
|
||||
worker,
|
||||
testFilter,
|
||||
0,
|
||||
opts.debug,
|
||||
false, // no requireSingleFixture in non-watch mode
|
||||
);
|
||||
const results = await runFixtures(worker, testFilter, 0);
|
||||
if (opts.update) {
|
||||
update(results);
|
||||
isSuccess = true;
|
||||
|
||||
Reference in New Issue
Block a user