Compare commits

..

1 Commits

Author SHA1 Message Date
Joe Savona
290174c38f [compiler] Adjustments to exhaustive deps messages, disable the lint rule
Similar to ValidateHookUsage, we implement this check in the compiler for safety but (for now) continue to rely on the existing rule for actually reporting errors to users.
2025-11-24 12:19:54 -08:00
681 changed files with 7121 additions and 13372 deletions

View File

@@ -331,7 +331,6 @@ module.exports = {
'packages/react-server-dom-turbopack/**/*.js',
'packages/react-server-dom-parcel/**/*.js',
'packages/react-server-dom-fb/**/*.js',
'packages/react-server-dom-unbundled/**/*.js',
'packages/react-test-renderer/**/*.js',
'packages/react-debug-tools/**/*.js',
'packages/react-devtools-extensions/**/*.js',
@@ -635,7 +634,6 @@ module.exports = {
FocusOptions: 'readonly',
OptionalEffectTiming: 'readonly',
__REACT_ROOT_PATH_TEST__: 'readonly',
spyOnDev: 'readonly',
spyOnDevAndProd: 'readonly',
spyOnProd: 'readonly',

View File

@@ -3,10 +3,6 @@ name: (Runtime) Build and Test
on:
push:
branches: [main]
tags:
# To get CI for backport releases.
# This will duplicate CI for releases from main which is acceptable
- "v*"
pull_request:
paths-ignore:
- compiler/**
@@ -45,7 +41,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
lookup-only: true
- uses: actions/setup-node@v4
if: steps.node_modules.outputs.cache-hit != 'true'
@@ -59,8 +55,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Save cache
@@ -69,7 +67,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
runtime_compiler_node_modules_cache:
name: Cache Runtime, Compiler node_modules
@@ -84,7 +82,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
lookup-only: true
- uses: actions/setup-node@v4
if: steps.node_modules.outputs.cache-hit != 'true'
@@ -100,8 +98,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
restore-keys: |
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-and-compiler-node_modules-v6-
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd compiler install --frozen-lockfile
@@ -112,7 +112,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# ----- FLOW -----
discover_flow_inline_configs:
@@ -154,8 +154,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -182,8 +184,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -212,7 +216,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -270,8 +274,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
restore-keys: |
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-and-compiler-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -300,7 +306,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
- name: Install runtime dependencies
run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
@@ -343,8 +349,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
restore-keys: |
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-and-compiler-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -382,6 +390,9 @@ jobs:
-r=experimental --env=development,
-r=experimental --env=production,
# Dev Tools
--project=devtools -r=experimental,
# TODO: Update test config to support www build tests
# - "-r=www-classic --env=development --variant=false"
# - "-r=www-classic --env=production --variant=false"
@@ -429,8 +440,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
restore-keys: |
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-and-compiler-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -447,50 +460,6 @@ jobs:
run: ls -R build
- run: yarn test --build ${{ matrix.test_params }} --shard=${{ matrix.shard }} --ci
test_build_devtools:
name: yarn test-build (devtools)
needs: [build_and_lint, runtime_node_modules_cache]
strategy:
fail-fast: false
matrix:
shard:
- 1/5
- 2/5
- 3/5
- 4/5
- 5/5
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache/restore@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Restore archived build
uses: actions/download-artifact@v4
with:
pattern: _build_*
path: build
merge-multiple: true
- name: Display structure of build
run: ls -R build
- run: yarn test --build --project=devtools -r=experimental --shard=${{ matrix.shard }} --ci
process_artifacts_combined:
name: Process artifacts combined
needs: [build_and_lint, runtime_node_modules_cache]
@@ -514,8 +483,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -577,8 +548,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -615,8 +588,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -765,8 +740,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -825,8 +802,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile

View File

@@ -1,9 +1,3 @@
## 19.2.1 (Dec 3, 2025)
### React Server Components
- Bring React Server Component fixes to Server Actions (@sebmarkbage [#35277](https://github.com/facebook/react/pull/35277))
## 19.2.0 (October 1st, 2025)
Below is a list of all new features, APIs, and bug fixes.
@@ -77,12 +71,6 @@ Read the [React 19.2 release post](https://react.dev/blog/2025/10/01/react-19-2)
- [createContainer](https://github.com/facebook/react/blob/v19.2.0/packages/react-reconciler/src/ReactFiberReconciler.js#L255-L261) and [createHydrationContainer](https://github.com/facebook/react/blob/v19.2.0/packages/react-reconciler/src/ReactFiberReconciler.js#L305-L312) had their parameter order adjusted after `on*` handlers to account for upcoming experimental APIs
## 19.1.2 (Dec 3, 2025)
### React Server Components
- Bring React Server Component fixes to Server Actions (@sebmarkbage [#35277](https://github.com/facebook/react/pull/35277))
## 19.1.1 (July 28, 2025)
### React
@@ -135,12 +123,6 @@ An Owner Stack is a string representing the components that are directly respons
* Exposed `registerServerReference` in client builds to handle server references in different environments. [#32534](https://github.com/facebook/react/pull/32534)
* Added react-server-dom-parcel package which integrates Server Components with the [Parcel bundler](https://parceljs.org/) [#31725](https://github.com/facebook/react/pull/31725), [#32132](https://github.com/facebook/react/pull/32132), [#31799](https://github.com/facebook/react/pull/31799), [#32294](https://github.com/facebook/react/pull/32294), [#31741](https://github.com/facebook/react/pull/31741)
## 19.0.1 (Dec 3, 2025)
### React Server Components
- Bring React Server Component fixes to Server Actions (@sebmarkbage [#35277](https://github.com/facebook/react/pull/35277))
## 19.0.0 (December 5, 2024)
Below is a list of all new features, APIs, deprecations, and breaking changes. Read [React 19 release post](https://react.dev/blog/2024/04/25/react-19) and [React 19 upgrade guide](https://react.dev/blog/2024/04/25/react-19-upgrade-guide) for more information.

View File

@@ -1,20 +0,0 @@
{
"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:*)",
"Bash(yarn workspace snap run build:*)",
"Bash(yarn tsc:*)",
"Bash(yarn snap:build)",
"Bash(timeout 30 yarn snap:*)"
],
"deny": [],
"ask": []
}
}

View File

@@ -1,245 +0,0 @@
# React Compiler Knowledge Base
This document contains knowledge about the React Compiler gathered during development sessions. It serves as a reference for understanding the codebase architecture and key concepts.
## Project Structure
- `packages/babel-plugin-react-compiler/` - Main compiler package
- `src/HIR/` - High-level Intermediate Representation types and utilities
- `src/Inference/` - Effect inference passes (aliasing, mutation, etc.)
- `src/Validation/` - Validation passes that check for errors
- `src/Entrypoint/Pipeline.ts` - Main compilation pipeline with pass ordering
- `src/__tests__/fixtures/compiler/` - Test fixtures
- `error.*.js` - Fixtures that should produce compilation errors
- `*.expect.md` - Expected output for each fixture
## Running Tests
```bash
# Run all tests
yarn snap
# Run specific test by pattern
yarn snap -p <pattern>
# Update fixture outputs
yarn snap -u
```
## Version Control
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
# Commit all changes
sl commit -m "Your commit message"
# Commit with multi-line message using heredoc
sl commit -m "$(cat <<'EOF'
Summary line
Detailed description here
EOF
)"
```
## Key Concepts
### HIR (High-level Intermediate Representation)
The compiler converts source code to HIR for analysis. Key types in `src/HIR/HIR.ts`:
- **HIRFunction** - A function being compiled
- `body.blocks` - Map of BasicBlocks
- `context` - Captured variables from outer scope
- `params` - Function parameters
- `returns` - The function's return place
- `aliasingEffects` - Effects that describe the function's behavior when called
- **Instruction** - A single operation
- `lvalue` - The place being assigned to
- `value` - The instruction kind (CallExpression, FunctionExpression, LoadLocal, etc.)
- `effects` - Array of AliasingEffects for this instruction
- **Terminal** - Block terminators (return, branch, etc.)
- `effects` - Array of AliasingEffects
- **Place** - A reference to a value
- `identifier.id` - Unique IdentifierId
- **Phi nodes** - Join points for values from different control flow paths
- Located at `block.phis`
- `phi.place` - The result place
- `phi.operands` - Map of predecessor block to source place
### AliasingEffects System
Effects describe data flow and operations. Defined in `src/Inference/AliasingEffects.ts`:
**Data Flow Effects:**
- `Impure` - Marks a place as containing an impure value (e.g., Date.now() result, ref.current)
- `Capture a -> b` - Value from `a` is captured into `b` (mutable capture)
- `Alias a -> b` - `b` aliases `a`
- `ImmutableCapture a -> b` - Immutable capture (like Capture but read-only)
- `Assign a -> b` - Direct assignment
- `MaybeAlias a -> b` - Possible aliasing
- `CreateFrom a -> b` - Created from source
**Mutation Effects:**
- `Mutate value` - Value is mutated
- `MutateTransitive value` - Value and transitive captures are mutated
- `MutateConditionally value` - May mutate
- `MutateTransitiveConditionally value` - May mutate transitively
**Other Effects:**
- `Render place` - Place is used in render context (JSX props, component return)
- `Freeze place` - Place is frozen (made immutable)
- `Create place` - New value created
- `CreateFunction` - Function expression created, includes `captures` array
- `Apply` - Function application with receiver, function, args, and result
### Hook Aliasing Signatures
Located in `src/HIR/Globals.ts`, hooks can define custom aliasing signatures to control how data flows through them.
**Structure:**
```typescript
aliasing: {
receiver: '@receiver', // The hook function itself
params: ['@param0'], // Named positional parameters
rest: '@rest', // Rest parameters (or null)
returns: '@returns', // Return value
temporaries: [], // Temporary values during execution
effects: [ // Array of effects to apply when hook is called
{kind: 'Freeze', value: '@param0', reason: ValueReason.HookCaptured},
{kind: 'Assign', from: '@param0', into: '@returns'},
],
}
```
**Common patterns:**
1. **RenderHookAliasing** (useState, useContext, useMemo, useCallback):
- Freezes arguments (`Freeze @rest`)
- Marks arguments as render-time (`Render @rest`)
- Creates frozen return value
- Aliases arguments to return
2. **EffectHookAliasing** (useEffect, useLayoutEffect, useInsertionEffect):
- Freezes function and deps
- Creates internal effect object
- Captures function and deps into effect
- Returns undefined
3. **Event handler hooks** (useEffectEvent):
- Freezes callback (`Freeze @fn`)
- Aliases input to return (`Assign @fn -> @returns`)
- NO Render effect (callback not called during render)
**Example: useEffectEvent**
```typescript
const UseEffectEventHook = addHook(
DEFAULT_SHAPES,
{
positionalParams: [Effect.Freeze], // Takes one positional param
restParam: null,
returnType: {kind: 'Function', ...},
calleeEffect: Effect.Read,
hookKind: 'useEffectEvent',
returnValueKind: ValueKind.Frozen,
aliasing: {
receiver: '@receiver',
params: ['@fn'], // Name for the callback parameter
rest: null,
returns: '@returns',
temporaries: [],
effects: [
{kind: 'Freeze', value: '@fn', reason: ValueReason.HookCaptured},
{kind: 'Assign', from: '@fn', into: '@returns'},
// Note: NO Render effect - callback is not called during render
],
},
},
BuiltInUseEffectEventId,
);
// Add as both names for compatibility
['useEffectEvent', UseEffectEventHook],
['experimental_useEffectEvent', UseEffectEventHook],
```
**Key insight:** If a hook is missing an `aliasing` config, it falls back to `DefaultNonmutatingHook` which includes a `Render` effect on all arguments. This can cause false positives for hooks like `useEffectEvent` whose callbacks are not called during render.
### Effect Inference Pipeline
Effects are populated by `InferMutationAliasingEffects` (runs before validation):
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
### Validation: validateNoImpureValuesInRender
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
1. Run `yarn snap -p <fixture>` to see full HIR output with effects
2. Look for `@aliasingEffects=` on FunctionExpressions
3. Look for `Impure`, `Render`, `Capture` effects on instructions
4. Check the pass ordering in Pipeline.ts to understand when effects are populated vs validated

View File

@@ -12,7 +12,6 @@
# next.js
/.next/
/out/
/next-env.d.ts
# production
/build

View File

@@ -14,6 +14,7 @@ import React, {
unstable_ViewTransition as ViewTransition,
unstable_addTransitionType as addTransitionType,
startTransition,
Activity,
} from 'react';
import {Resizable} from 're-resizable';
import {useStore, useStoreDispatch} from '../StoreContext';
@@ -33,14 +34,9 @@ export default function ConfigEditor({
}): React.ReactElement {
const [isExpanded, setIsExpanded] = useState(false);
// TODO: Add back <Activity> after upgrading next.js
return (
<>
<div
style={{
display: isExpanded ? 'block' : 'none',
}}>
{/* <Activity mode={isExpanded ? 'visible' : 'hidden'}> */}
<Activity mode={isExpanded ? 'visible' : 'hidden'}>
<ExpandedEditor
onToggle={() => {
startTransition(() => {
@@ -50,13 +46,8 @@ export default function ConfigEditor({
}}
formattedAppliedConfig={formattedAppliedConfig}
/>
</div>
<div
style={{
display: !isExpanded ? 'block' : 'none',
}}>
{/* </Activity>
<Activity mode={isExpanded ? 'hidden' : 'visible'}></Activity> */}
</Activity>
<Activity mode={isExpanded ? 'hidden' : 'visible'}>
<CollapsedEditor
onToggle={() => {
startTransition(() => {
@@ -65,8 +56,7 @@ export default function ConfigEditor({
});
}}
/>
</div>
{/* </Activity> */}
</Activity>
</>
);
}
@@ -126,9 +116,8 @@ function ExpandedEditor({
return (
<ViewTransition
update={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}>
{/* enter={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}
exit={{[CONFIG_PANEL_TRANSITION]: 'slide-out', default: 'none'}}> */}
enter={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}
exit={{[CONFIG_PANEL_TRANSITION]: 'slide-out', default: 'none'}}>
<Resizable
minWidth={300}
maxWidth={600}

View File

@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -35,13 +35,13 @@
"lru-cache": "^11.2.2",
"lz-string": "^1.5.0",
"monaco-editor": "^0.52.0",
"next": "15.5.9",
"next": "15.6.0-canary.7",
"notistack": "^3.0.0-alpha.7",
"prettier": "^3.3.3",
"pretty-format": "^29.3.1",
"re-resizable": "^6.9.16",
"react": "19.2.3",
"react-dom": "19.2.3"
"react": "19.2",
"react-dom": "19.2"
},
"devDependencies": {
"@types/node": "18.11.9",

View File

@@ -19,7 +19,7 @@
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{

View File

@@ -715,10 +715,10 @@
dependencies:
"@monaco-editor/loader" "^1.6.1"
"@next/env@15.5.9":
version "15.5.9"
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.5.9.tgz#53c2c34dc17cd87b61f70c6cc211e303123b2ab8"
integrity sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==
"@next/env@15.6.0-canary.7":
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.6.0-canary.7.tgz#cdbf2967a9437ef09eef755e203f315acc4d8d8f"
integrity sha512-LNZ7Yd3Cl9rKvjYdeJmszf2HmSDP76SQmfafKep2Ux16ZXKoN5OjwVHFTltKNdsB3vt2t+XJzLP2rhw5lBoFBA==
"@next/eslint-plugin-next@15.5.2":
version "15.5.2"
@@ -727,45 +727,45 @@
dependencies:
fast-glob "3.3.1"
"@next/swc-darwin-arm64@15.5.7":
version "15.5.7"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz#f0c9ccfec2cd87cbd4b241ce4c779a7017aed958"
integrity sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==
"@next/swc-darwin-arm64@15.6.0-canary.7":
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.6.0-canary.7.tgz#628cd34ce9120000f1cb5b08963426431174fc57"
integrity sha512-POsBrxhrR3qvqXV+JZ6ZoBc8gJf8rhYe+OedceI1piPVqtJYOJa3EB4eaqcc+kMsllKRrH/goNlhLwtyhE+0Qg==
"@next/swc-darwin-x64@15.5.7":
version "15.5.7"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz#18009e9fcffc5c0687cc9db24182ddeac56280d9"
integrity sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==
"@next/swc-darwin-x64@15.6.0-canary.7":
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.6.0-canary.7.tgz#37d4ebab14da74a2f8028daf6d76aab410153e06"
integrity sha512-lmk9ysBuSiPlAJZTCo/3O4mXNFosg6EDIf4GrmynIwCG2as6/KxzyD1WqFp56Exp8eFDjP7SFapD10sV43vCsA==
"@next/swc-linux-arm64-gnu@15.5.7":
version "15.5.7"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz#fe7c7e08264cf522d4e524299f6d3e63d68d579a"
integrity sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==
"@next/swc-linux-arm64-gnu@15.6.0-canary.7":
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.6.0-canary.7.tgz#ce700cc0e0d24763136838223105a524b36694fa"
integrity sha512-why8k6d0SBm3AKoOD5S7ir3g+BF34l9oFKIoZrLaZaKBvNGpFcjc7Ovc2TunNMeaMJzv9k1dHYSap0EI5oSuzg==
"@next/swc-linux-arm64-musl@15.5.7":
version "15.5.7"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz#94228fe293475ec34a5a54284e1056876f43a3cf"
integrity sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==
"@next/swc-linux-arm64-musl@15.6.0-canary.7":
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.6.0-canary.7.tgz#c791b8e15bf2c338b4cc0387fe7afb3ef83ecfcf"
integrity sha512-HzvTRsKvYj32Va4YuJN3n3xOxvk+6QwB63d/EsgmdkeA/vrqciUAmJDYpuzZEvRc3Yp2nyPq8KZxtHAr6ISZ2Q==
"@next/swc-linux-x64-gnu@15.5.7":
version "15.5.7"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz#078c71201dfe7fcfb8fa6dc92aae6c94bc011cdc"
integrity sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==
"@next/swc-linux-x64-gnu@15.6.0-canary.7":
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.6.0-canary.7.tgz#c01c3a3d8e71660c49298dd053d078379b6b5919"
integrity sha512-6yRFrg2qWXOqa+1BI53J9EmHWFzKg9U2r+5R7n7BFUp8PH5SC92WBsmYTnh/RkvAYvdupiVzMervwwswCs6kFg==
"@next/swc-linux-x64-musl@15.5.7":
version "15.5.7"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz#72947f5357f9226292353e0bb775643da3c7a182"
integrity sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==
"@next/swc-linux-x64-musl@15.6.0-canary.7":
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.6.0-canary.7.tgz#3f4b39faef4a5f88b13e4c726b008ddc9717f819"
integrity sha512-O/JjvOvNK/Wao/OIQaA6evDkxkmFFQgJ1/hI1dVk6/PAeKmW2/Q+6Dodh97eAkOwedS1ZdQl2mojf87TzLvzdQ==
"@next/swc-win32-arm64-msvc@15.5.7":
version "15.5.7"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz#397b912cd51c6a80e32b9c0507ecd82514353941"
integrity sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==
"@next/swc-win32-arm64-msvc@15.6.0-canary.7":
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.6.0-canary.7.tgz#9bc5da0907b7ce67eedda02a6d56a09d9a539ccf"
integrity sha512-p9DvrDgnePofZCtiWVY7qZtwXxiOGJlAyy2LoGPYSGOUDhjbTG8j6XMUFXpV9UwpH+l7st522psO1BVzbpT8IQ==
"@next/swc-win32-x64-msvc@15.5.7":
version "15.5.7"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz#e02b543d9dc6c1631d4ac239cb1177245dfedfe4"
integrity sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==
"@next/swc-win32-x64-msvc@15.6.0-canary.7":
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.6.0-canary.7.tgz#5b271c591ccbe67d5fa966dd22db86c547414fd1"
integrity sha512-f1ywT3xWu4StWKA1mZRyGfelu/h+W0OEEyBxQNXzXyYa0VGZb9LyCNb5cYoNKBm0Bw18Hp1PVe0bHuusemGCcw==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
@@ -3204,25 +3204,25 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
next@15.5.9:
version "15.5.9"
resolved "https://registry.yarnpkg.com/next/-/next-15.5.9.tgz#1b80d05865cc27e710fb4dcfc6fd9e726ed12ad4"
integrity sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==
next@15.6.0-canary.7:
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/next/-/next-15.6.0-canary.7.tgz#bfc2ac3c9a78e23d550c303d18247a263e6b5bc1"
integrity sha512-4ukX2mxat9wWT6E0Gw/3TOR9ULV1q399E42F86cwsPSFgTWa04ABhcTqO0r9J/QR1YWPR8WEgh9qUzmWA/1yEw==
dependencies:
"@next/env" "15.5.9"
"@next/env" "15.6.0-canary.7"
"@swc/helpers" "0.5.15"
caniuse-lite "^1.0.30001579"
postcss "8.4.31"
styled-jsx "5.1.6"
optionalDependencies:
"@next/swc-darwin-arm64" "15.5.7"
"@next/swc-darwin-x64" "15.5.7"
"@next/swc-linux-arm64-gnu" "15.5.7"
"@next/swc-linux-arm64-musl" "15.5.7"
"@next/swc-linux-x64-gnu" "15.5.7"
"@next/swc-linux-x64-musl" "15.5.7"
"@next/swc-win32-arm64-msvc" "15.5.7"
"@next/swc-win32-x64-msvc" "15.5.7"
"@next/swc-darwin-arm64" "15.6.0-canary.7"
"@next/swc-darwin-x64" "15.6.0-canary.7"
"@next/swc-linux-arm64-gnu" "15.6.0-canary.7"
"@next/swc-linux-arm64-musl" "15.6.0-canary.7"
"@next/swc-linux-x64-gnu" "15.6.0-canary.7"
"@next/swc-linux-x64-musl" "15.6.0-canary.7"
"@next/swc-win32-arm64-msvc" "15.6.0-canary.7"
"@next/swc-win32-x64-msvc" "15.6.0-canary.7"
sharp "^0.34.3"
node-releases@^2.0.18:
@@ -3582,10 +3582,10 @@ re-resizable@^6.9.16:
resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.10.0.tgz#d684a096ab438f1a93f59ad3a580a206b0ce31ee"
integrity sha512-hysSK0xmA5nz24HBVztlk4yCqCLCvS32E6ZpWxVKop9x3tqCa4yAj1++facrmkOf62JsJHjmjABdKxXofYioCw==
react-dom@19.2.3:
version "19.2.3"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.3.tgz#f0b61d7e5c4a86773889fcc1853af3ed5f215b17"
integrity sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==
react-dom@19.2:
version "19.2.0"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz#00ed1e959c365e9a9d48f8918377465466ec3af8"
integrity sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==
dependencies:
scheduler "^0.27.0"
@@ -3599,10 +3599,10 @@ react-is@^18.0.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
react@19.2.3:
version "19.2.3"
resolved "https://registry.yarnpkg.com/react/-/react-19.2.3.tgz#d83e5e8e7a258cf6b4fe28640515f99b87cd19b8"
integrity sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==
react@19.2:
version "19.2.0"
resolved "https://registry.npmjs.org/react/-/react-19.2.0.tgz#d33dd1721698f4376ae57a54098cb47fc75d93a5"
integrity sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==
read-cache@^1.0.0:
version "1.0.0"

View File

@@ -132,12 +132,6 @@ 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;
}
@@ -607,8 +601,7 @@ function printErrorSummary(category: ErrorCategory, message: string): string {
case ErrorCategory.Syntax:
case ErrorCategory.UseMemo:
case ErrorCategory.VoidUseMemo:
case ErrorCategory.MemoDependencies:
case ErrorCategory.EffectExhaustiveDependencies: {
case ErrorCategory.MemoDependencies: {
heading = 'Error';
break;
}
@@ -690,10 +683,6 @@ export enum ErrorCategory {
* Checks for memoized effect deps
*/
EffectDependencies = 'EffectDependencies',
/**
* Checks for exhaustive and extraneous effect dependencies
*/
EffectExhaustiveDependencies = 'EffectExhaustiveDependencies',
/**
* Checks for no setState in effect bodies
*/
@@ -849,16 +838,6 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
preset: LintRulePreset.Off,
};
}
case ErrorCategory.EffectExhaustiveDependencies: {
return {
category,
severity: ErrorSeverity.Error,
name: 'exhaustive-effect-dependencies',
description:
'Validates that effect dependencies are exhaustive and without extraneous values',
preset: LintRulePreset.Off,
};
}
case ErrorCategory.EffectDerivationsOfState: {
return {
category,
@@ -875,9 +854,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
severity: ErrorSeverity.Error,
name: 'set-state-in-effect',
description:
'Validates against calling setState synchronously in an effect. ' +
'This can indicate non-local derived data, a derived event pattern, or ' +
'improper external data synchronization.',
'Validates against calling setState synchronously in an effect, which can lead to re-renders that degrade performance',
preset: LintRulePreset.Recommended,
};
}

View File

@@ -96,6 +96,7 @@ 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';
@@ -106,7 +107,6 @@ 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,36 +271,30 @@ function runWithEnvironment(
assertValidMutableRanges(hir);
}
if (env.config.validateRefAccessDuringRender) {
validateNoRefAccessInRender(hir).unwrap();
}
if (env.config.validateNoSetStateInRender) {
validateNoSetStateInRender(hir).unwrap();
}
if (
env.config.validateNoDerivedComputationsInEffects_exp &&
env.outputMode === 'lint'
) {
if (env.config.validateNoDerivedComputationsInEffects_exp) {
env.logErrors(validateNoDerivedComputationsInEffects_exp(hir));
} else if (env.config.validateNoDerivedComputationsInEffects) {
validateNoDerivedComputationsInEffects(hir);
}
if (env.config.validateNoSetStateInEffects && env.outputMode === 'lint') {
if (env.config.validateNoSetStateInEffects) {
env.logErrors(validateNoSetStateInEffects(hir, env));
}
if (env.config.validateNoJSXInTryStatements && env.outputMode === 'lint') {
if (env.config.validateNoJSXInTryStatements) {
env.logErrors(validateNoJSXInTryStatement(hir));
}
if (
env.config.validateNoImpureFunctionsInRender ||
env.config.validateRefAccessDuringRender
) {
validateNoImpureValuesInRender(hir).unwrap();
}
if (env.config.validateRefAccessDuringRender) {
validateNoRefAccessInRender(hir).unwrap();
if (env.config.validateNoImpureFunctionsInRender) {
validateNoImpureFunctionsInRender(hir).unwrap();
}
validateNoFreezingKnownMutableFunctions(hir).unwrap();
@@ -310,10 +304,7 @@ function runWithEnvironment(
log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
if (env.enableValidations) {
if (
env.config.validateExhaustiveMemoizationDependencies ||
env.config.validateExhaustiveEffectDependencies
) {
if (env.config.validateExhaustiveMemoizationDependencies) {
// NOTE: this relies on reactivity inference running first
validateExhaustiveDependencies(hir).unwrap();
}
@@ -326,11 +317,7 @@ function runWithEnvironment(
value: hir,
});
if (
env.enableValidations &&
env.config.validateStaticComponents &&
env.outputMode === 'lint'
) {
if (env.enableValidations && env.config.validateStaticComponents) {
env.logErrors(validateStaticComponents(hir));
}

View File

@@ -4026,7 +4026,6 @@ function lowerAssignment(
pattern: {
kind: 'ArrayPattern',
items,
loc: lvalue.node.loc ?? GeneratedSource,
},
},
value,
@@ -4204,7 +4203,6 @@ function lowerAssignment(
pattern: {
kind: 'ObjectPattern',
properties,
loc: lvalue.node.loc ?? GeneratedSource,
},
},
value,

View File

@@ -221,19 +221,7 @@ export const EnvironmentConfigSchema = z.object({
/**
* Validate that dependencies supplied to manual memoization calls are exhaustive.
*/
validateExhaustiveMemoizationDependencies: z.boolean().default(true),
/**
* Validate that dependencies supplied to effect hooks are exhaustive.
* Can be:
* - 'off': No validation (default)
* - 'all': Validate and report both missing and extra dependencies
* - 'missing-only': Only report missing dependencies
* - 'extra-only': Only report extra/unnecessary dependencies
*/
validateExhaustiveEffectDependencies: z
.enum(['off', 'all', 'missing-only', 'extra-only'])
.default('off'),
validateExhaustiveMemoizationDependencies: z.boolean().default(false),
/**
* When this is true, rather than pruning existing manual memoization but ensuring or validating
@@ -330,12 +318,6 @@ export const EnvironmentConfigSchema = z.object({
*/
validateNoSetStateInRender: z.boolean().default(true),
/**
* When enabled, changes the behavior of validateNoSetStateInRender to recommend
* using useKeyedState instead of the manual pattern for resetting state.
*/
enableUseKeyedState: z.boolean().default(false),
/**
* Validates that setState is not called synchronously within an effect (useEffect and friends).
* Scheduling a setState (with an event listener, subscription, etc) is valid.
@@ -707,16 +689,6 @@ export const EnvironmentConfigSchema = z.object({
*/
enableAllowSetStateFromRefsInEffects: z.boolean().default(true),
/**
* When enabled, provides verbose error messages for setState calls within effects,
* presenting multiple possible fixes to the user/agent since we cannot statically
* determine which specific use-case applies:
* 1. Non-local derived data - requires restructuring state ownership
* 2. Derived event pattern - detecting when a prop changes
* 3. Force update / external sync - should use useSyncExternalStore
*/
enableVerboseNoSetStateInEffect: z.boolean().default(false),
/**
* Enables inference of event handler types for JSX props on built-in DOM elements.
* When enabled, functions passed to event handler props (props starting with "on")

View File

@@ -38,7 +38,7 @@ import {
addObject,
} from './ObjectShape';
import {BuiltInType, ObjectType, PolyType} from './Types';
import {AliasingSignatureConfig, TypeConfig} from './TypeSchema';
import {TypeConfig} from './TypeSchema';
import {assertExhaustive} from '../Utils/utils';
import {isHookName} from './Environment';
import {CompilerError, SourceLocation} from '..';
@@ -626,136 +626,11 @@ 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',
@@ -769,7 +644,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
hookKind: 'useContext',
returnValueKind: ValueKind.Frozen,
returnValueReason: ValueReason.Context,
aliasing: RenderHookAliasing(ValueReason.Context),
},
BuiltInUseContextHookId,
),
@@ -784,7 +658,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
hookKind: 'useState',
returnValueKind: ValueKind.Frozen,
returnValueReason: ValueReason.State,
aliasing: RenderHookAliasing(ValueReason.State),
}),
],
[
@@ -797,7 +670,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
hookKind: 'useActionState',
returnValueKind: ValueKind.Frozen,
returnValueReason: ValueReason.State,
aliasing: RenderHookAliasing(ValueReason.HookCaptured),
}),
],
[
@@ -810,7 +682,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
hookKind: 'useReducer',
returnValueKind: ValueKind.Frozen,
returnValueReason: ValueReason.ReducerState,
aliasing: RenderHookAliasing(ValueReason.ReducerState),
}),
],
[
@@ -822,22 +693,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
calleeEffect: Effect.Read,
hookKind: 'useRef',
returnValueKind: ValueKind.Mutable,
aliasing: {
receiver: '@receiver',
params: [],
rest: '@rest',
returns: '@return',
temporaries: [],
effects: [
{
kind: 'Create',
into: '@return',
value: ValueKind.Mutable,
reason: ValueReason.KnownReturnSignature,
},
{kind: 'Capture', from: '@rest', into: '@return'},
],
},
}),
],
[
@@ -860,7 +715,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
calleeEffect: Effect.Read,
hookKind: 'useMemo',
returnValueKind: ValueKind.Frozen,
aliasing: RenderHookAliasing(ValueReason.HookCaptured),
}),
],
[
@@ -868,16 +722,10 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
addHook(DEFAULT_SHAPES, {
positionalParams: [],
restParam: Effect.Freeze,
returnType: {
kind: 'Function',
isConstructor: false,
return: {kind: 'Poly'},
shapeId: null,
},
returnType: {kind: 'Poly'},
calleeEffect: Effect.Read,
hookKind: 'useCallback',
returnValueKind: ValueKind.Frozen,
aliasing: RenderHookAliasing(ValueReason.HookCaptured),
}),
],
[
@@ -891,7 +739,41 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
calleeEffect: Effect.Read,
hookKind: 'useEffect',
returnValueKind: ValueKind.Frozen,
aliasing: EffectHookAliasing,
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,
},
],
},
},
BuiltInUseEffectHookId,
),
@@ -907,7 +789,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
calleeEffect: Effect.Read,
hookKind: 'useLayoutEffect',
returnValueKind: ValueKind.Frozen,
aliasing: EffectHookAliasing,
},
BuiltInUseLayoutEffectHookId,
),
@@ -923,7 +804,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
calleeEffect: Effect.Read,
hookKind: 'useInsertionEffect',
returnValueKind: ValueKind.Frozen,
aliasing: EffectHookAliasing,
},
BuiltInUseInsertionEffectHookId,
),
@@ -937,7 +817,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
calleeEffect: Effect.Read,
hookKind: 'useTransition',
returnValueKind: ValueKind.Frozen,
aliasing: RenderHookAliasing(ValueReason.HookCaptured),
}),
],
[
@@ -950,7 +829,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
hookKind: 'useOptimistic',
returnValueKind: ValueKind.Frozen,
returnValueReason: ValueReason.State,
aliasing: RenderHookAliasing(ValueReason.HookCaptured),
}),
],
[
@@ -964,7 +842,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
returnType: {kind: 'Poly'},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Frozen,
aliasing: RenderHookAliasing(ValueReason.HookCaptured),
},
BuiltInUseOperatorId,
),
@@ -989,8 +866,27 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
BuiltInFireId,
),
],
['useEffectEvent', useEffectEvent],
['experimental_useEffectEvent', useEffectEvent],
[
'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,
),
],
['AUTODEPS', addObject(DEFAULT_SHAPES, BuiltInAutodepsId, [])],
];

View File

@@ -694,13 +694,11 @@ export type SpreadPattern = {
export type ArrayPattern = {
kind: 'ArrayPattern';
items: Array<Place | SpreadPattern | Hole>;
loc: SourceLocation;
};
export type ObjectPattern = {
kind: 'ObjectPattern';
properties: Array<ObjectProperty | SpreadPattern>;
loc: SourceLocation;
};
export type ObjectPropertyKey =
@@ -805,11 +803,9 @@ export type ManualMemoDependency = {
| {
kind: 'NamedLocal';
value: Place;
constant: boolean;
}
| {kind: 'Global'; identifierName: string};
path: DependencyPath;
loc: SourceLocation;
};
export type StartMemoize = {
@@ -1879,15 +1875,7 @@ export function isRefValueType(id: Identifier): boolean {
}
export function isUseRefType(id: Identifier): boolean {
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)))
);
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseRefId';
}
export function isUseStateType(id: Identifier): boolean {
@@ -1898,13 +1886,6 @@ 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);
}
@@ -2040,11 +2021,6 @@ export function isUseInsertionEffectHookType(id: Identifier): boolean {
id.type.shapeId === 'BuiltInUseInsertionEffectHook'
);
}
export function isUseEffectEventType(id: Identifier): boolean {
return (
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInUseEffectEvent'
);
}
export function isUseContextHookType(id: Identifier): boolean {
return (
@@ -2073,23 +2049,4 @@ export function getHookKindForType(
return null;
}
export function areEqualSourceLocations(
loc1: SourceLocation,
loc2: SourceLocation,
): boolean {
if (typeof loc1 === 'symbol' || typeof loc2 === 'symbol') {
return false;
}
return (
loc1.filename === loc2.filename &&
loc1.identifierName === loc2.identifierName &&
loc1.start.line === loc2.start.line &&
loc1.start.column === loc2.start.column &&
loc1.start.index === loc2.start.index &&
loc1.end.line === loc2.end.line &&
loc1.end.column === loc2.end.column &&
loc1.end.index === loc2.end.index
);
}
export * from './Types';

View File

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

View File

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

View File

@@ -983,15 +983,15 @@ export function printAliasingEffect(effect: AliasingEffect): string {
return `...${printPlaceForAliasEffect(arg.place)}`;
})
.join(', ');
// let signature = '';
// if (effect.signature != null) {
// if (effect.signature.aliasing != null) {
// signature = printAliasingSignature(effect.signature.aliasing);
// } else {
// signature = JSON.stringify(effect.signature, null, 2);
// }
// }
return `Apply ${printPlaceForAliasEffect(effect.into)} = ${receiverCallee}(${args})`;
let signature = '';
if (effect.signature != null) {
if (effect.signature.aliasing != null) {
signature = printAliasingSignature(effect.signature.aliasing);
} else {
signature = JSON.stringify(effect.signature, null, 2);
}
}
return `Apply ${printPlaceForAliasEffect(effect.into)} = ${receiverCallee}(${args})${signature != '' ? '\n ' : ''}${signature}`;
}
case 'Freeze': {
return `Freeze ${printPlaceForAliasEffect(effect.value)} ${effect.reason}`;
@@ -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.into)} reason=${effect.reason} description=${effect.description}`;
return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
}
case 'Render': {
return `Render ${printPlaceForAliasEffect(effect.place)}`;

View File

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

View File

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

View File

@@ -8,7 +8,7 @@
import {BlockId, computePostDominatorTree, HIRFunction, Place} from '../HIR';
import {PostDominator} from '../HIR/Dominator';
export type ControlDominators = (id: BlockId) => Place | null;
export type ControlDominators = (id: BlockId) => boolean;
/**
* 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): Place | null {
function isControlledBlock(id: BlockId): boolean {
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 controlBlock.terminal.test;
return true;
}
break;
}
case 'switch': {
if (isControlVariable(controlBlock.terminal.test)) {
return controlBlock.terminal.test;
return true;
}
for (const case_ of controlBlock.terminal.cases) {
if (case_.test !== null && isControlVariable(case_.test)) {
return case_.test;
return true;
}
}
break;
}
}
}
return null;
return false;
}
return isControlledBlock;

View File

@@ -65,7 +65,6 @@ export function collectMaybeMemoDependencies(
identifierName: value.binding.name,
},
path: [],
loc: value.loc,
};
}
case 'PropertyLoad': {
@@ -75,7 +74,6 @@ export function collectMaybeMemoDependencies(
root: object.root,
// TODO: determine if the access is optional
path: [...object.path, {property: value.property, optional}],
loc: value.loc,
};
}
break;
@@ -94,10 +92,8 @@ export function collectMaybeMemoDependencies(
root: {
kind: 'NamedLocal',
value: {...value.place},
constant: false,
},
path: [],
loc: value.place.loc,
};
}
break;

View File

@@ -27,13 +27,11 @@ import {
InstructionKind,
InstructionValue,
isArrayType,
isJsxOrJsxUnionType,
isJsxType,
isMapType,
isMutableEffect,
isPrimitiveType,
isRefOrRefValue,
isSetType,
isUseRefType,
makeIdentifierId,
Phi,
Place,
@@ -72,7 +70,6 @@ import {
MutationReason,
} from './AliasingEffects';
import {ErrorCategory} from '../CompilerError';
import {REF_ERROR_DESCRIPTION} from '../Validation/ValidateNoRefAccessInRender';
const DEBUG = false;
@@ -572,32 +569,14 @@ 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.push(
terminal.effects = [
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,
}),
);
];
}
}
}
@@ -770,7 +749,17 @@ function applyEffect(
break;
}
case 'ImmutableCapture': {
effects.push(effect);
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);
}
}
break;
}
case 'CreateFrom': {
@@ -1072,17 +1061,6 @@ 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: {
@@ -1094,8 +1072,6 @@ function applyEffect(
break;
}
case 'Apply': {
effects.push(effect);
const functionValues = state.values(effect.function);
if (
functionValues.length === 1 &&
@@ -1990,11 +1966,6 @@ function computeSignatureForInstruction(
value: ValueKind.Primitive,
reason: ValueReason.Other,
});
effects.push({
kind: 'ImmutableCapture',
from: value.object,
into: lvalue,
});
} else {
effects.push({
kind: 'CreateFrom',
@@ -2002,20 +1973,6 @@ 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':
@@ -2180,15 +2137,6 @@ 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
@@ -2197,17 +2145,29 @@ function computeSignatureForInstruction(
place: value.tag,
});
}
for (const prop of value.props) {
const place =
prop.kind === 'JsxAttribute' ? prop.place : prop.argument;
if (isUseRefType(place.identifier)) {
continue;
}
if (place.identifier.type.kind !== 'Function') {
// Functions are checked independently
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,
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
effects.push({
kind: 'Render',
place: prop.place,
});
}
}
@@ -2243,11 +2203,6 @@ 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',
@@ -2429,46 +2384,15 @@ 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',
@@ -2499,7 +2423,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> = [];
@@ -2512,18 +2436,20 @@ function computeEffectsForLegacySignature(
if (signature.impure && state.env.config.validateNoImpureFunctionsInRender) {
effects.push({
kind: 'Impure',
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',
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',
}),
});
}
if (signature.knownIncompatible != null && state.env.enableValidations) {
@@ -2822,23 +2748,7 @@ function computeEffectsForSignature(
}
break;
}
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 'Impure':
case 'MutateFrozen':
case 'MutateGlobal': {
const values = substitutions.get(effect.place.identifier.id) ?? [];

View File

@@ -19,8 +19,6 @@ import {
ValueReason,
Place,
isPrimitiveType,
isUseRefType,
isJsxOrJsxUnionType,
} from '../HIR/HIR';
import {
eachInstructionLValue,
@@ -30,9 +28,6 @@ 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
@@ -109,6 +104,7 @@ export function inferMutationAliasingRanges(
reason: MutationReason | null;
}> = [];
const renders: Array<{index: number; place: Place}> = [];
let index = 0;
const errors = new CompilerError();
@@ -201,12 +197,14 @@ export function inferMutationAliasingRanges(
});
} else if (
effect.kind === 'MutateFrozen' ||
effect.kind === 'MutateGlobal'
effect.kind === 'MutateGlobal' ||
effect.kind === 'Impure'
) {
errors.pushDiagnostic(effect.error);
functionEffects.push(effect);
} else if (effect.kind === 'Render') {
renders.push({index: index++, place: effect.place});
functionEffects.push(effect);
}
}
}
@@ -216,6 +214,10 @@ 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') &&
@@ -225,31 +227,23 @@ export function inferMutationAliasingRanges(
if (effect.kind === 'Alias') {
state.assign(index++, effect.from, effect.into);
} else {
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,
},
],
},
);
CompilerError.invariant(effect.kind === 'Freeze', {
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,
@@ -261,16 +255,8 @@ export function inferMutationAliasingRanges(
errors,
);
}
if (DEBUG) {
console.log(state.debug());
}
for (const render of renders) {
if (DEBUG) {
console.log(
`[${render.index}] render ${printIdentifier(render.place.identifier)}`,
);
}
state.render(render.index, render.place, errors);
state.render(render.index, render.place.identifier, errors);
}
for (const param of [...fn.context, ...fn.params]) {
const place = param.kind === 'Identifier' ? param : param.place;
@@ -397,7 +383,17 @@ export function inferMutationAliasingRanges(
break;
}
case 'Apply': {
break;
CompilerError.invariant(false, {
reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,
description: null,
details: [
{
kind: 'error',
loc: effect.function.loc,
message: null,
},
],
});
}
case 'MutateTransitive':
case 'MutateConditionally':
@@ -519,13 +515,6 @@ 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) {
@@ -579,12 +568,7 @@ export function inferMutationAliasingRanges(
}
}
if (
errors.hasAnyErrors() &&
(fn.fnType === 'Component' ||
isJsxOrJsxUnionType(fn.returns.identifier.type) ||
!isFunctionExpression)
) {
if (errors.hasAnyErrors() && !isFunctionExpression) {
return Err(errors);
}
return Ok(functionEffects);
@@ -593,6 +577,7 @@ 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);
@@ -627,74 +612,10 @@ 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,
@@ -708,7 +629,6 @@ class AliasingState {
lastMutated: 0,
mutationReason: null,
value,
render: null,
});
}
@@ -761,9 +681,9 @@ class AliasingState {
}
}
render(index: number, start: Place, errors: CompilerError): void {
render(index: number, start: Identifier, errors: CompilerError): void {
const seen = new Set<Identifier>();
const queue: Array<Identifier> = [start.identifier];
const queue: Array<Identifier> = [start];
while (queue.length !== 0) {
const current = queue.pop()!;
if (seen.has(current)) {
@@ -771,34 +691,11 @@ class AliasingState {
}
seen.add(current);
const node = this.nodes.get(current);
if (node == null || isUseRefType(node.id)) {
if (DEBUG) {
console.log(` render ${printIdentifier(current)}: skip mutated/ref`);
}
if (node == null || node.transitive != null || node.local != null) {
continue;
}
if (
node.local == null &&
node.transitive == null &&
node.value.kind === 'Function'
) {
const returns = node.value.function.returns;
if (
isJsxType(returns.identifier.type) ||
(returns.identifier.type.kind === 'Phi' &&
returns.identifier.type.operands.some(operand =>
isJsxType(operand),
))
) {
appendFunctionErrors(errors, node.value.function);
}
if (DEBUG) {
console.log(` render ${printIdentifier(current)}: skip function`);
}
continue;
}
if (node.render == null) {
node.render = start;
if (node.value.kind === 'Function') {
appendFunctionErrors(errors, node.value.function);
}
for (const [alias, when] of node.createdFrom) {
if (when >= index) {
@@ -812,12 +709,6 @@ class AliasingState {
}
queue.push(alias);
}
for (const [alias, when] of node.maybeAliases) {
if (when >= index) {
continue;
}
queue.push(alias);
}
for (const [capture, when] of node.captures) {
if (when >= index) {
continue;

View File

@@ -609,19 +609,6 @@ function evaluateInstruction(
constantPropagationImpl(value.loweredFunc.func, constants);
return null;
}
case 'StartMemoize': {
if (value.deps != null) {
for (const dep of value.deps) {
if (dep.root.kind === 'NamedLocal') {
const placeValue = read(constants, dep.root.value);
if (placeValue != null && placeValue.kind === 'Primitive') {
dep.root.constant = true;
}
}
}
}
return null;
}
default: {
// TODO: handle more cases
return null;

View File

@@ -515,7 +515,6 @@ function emitDestructureProps(
pattern: {
kind: 'ObjectPattern',
properties,
loc: GeneratedSource,
},
kind: InstructionKind.Let,
},

View File

@@ -702,7 +702,7 @@ function codegenReactiveScope(
outputComments.push(name.name);
if (!cx.hasDeclared(identifier)) {
statements.push(
t.variableDeclaration('let', [createVariableDeclarator(name, null)]),
t.variableDeclaration('let', [t.variableDeclarator(name)]),
);
}
cacheLoads.push({name, index, value: wrapCacheDep(cx, name)});
@@ -1387,7 +1387,7 @@ function codegenInstructionNullable(
suggestions: null,
});
return createVariableDeclaration(instr.loc, 'const', [
createVariableDeclarator(codegenLValue(cx, lvalue), value),
t.variableDeclarator(codegenLValue(cx, lvalue), value),
]);
}
case InstructionKind.Function: {
@@ -1451,7 +1451,7 @@ function codegenInstructionNullable(
suggestions: null,
});
return createVariableDeclaration(instr.loc, 'let', [
createVariableDeclarator(codegenLValue(cx, lvalue), value),
t.variableDeclarator(codegenLValue(cx, lvalue), value),
]);
}
case InstructionKind.Reassign: {
@@ -1691,9 +1691,6 @@ function withLoc<T extends (...args: Array<any>) => t.Node>(
};
}
const createIdentifier = withLoc(t.identifier);
const createArrayPattern = withLoc(t.arrayPattern);
const createObjectPattern = withLoc(t.objectPattern);
const createBinaryExpression = withLoc(t.binaryExpression);
const createExpressionStatement = withLoc(t.expressionStatement);
const _createLabelledStatement = withLoc(t.labeledStatement);
@@ -1725,31 +1722,6 @@ const createTryStatement = withLoc(t.tryStatement);
const createBreakStatement = withLoc(t.breakStatement);
const createContinueStatement = withLoc(t.continueStatement);
function createVariableDeclarator(
id: t.LVal,
init?: t.Expression | null,
): t.VariableDeclarator {
const node = t.variableDeclarator(id, init);
/*
* The variable declarator location is not preserved in HIR, however, we can use the
* start location of the id and the end location of the init to recreate the
* exact original variable declarator location.
*
* Or if init is null, we likely have a declaration without an initializer, so we can use the id.loc.end as the end location.
*/
if (id.loc && (init === null || init?.loc)) {
node.loc = {
start: id.loc.start,
end: init?.loc?.end ?? id.loc.end,
filename: id.loc.filename,
identifierName: undefined,
};
}
return node;
}
function createHookGuard(
guard: ExternalFunction,
context: ProgramContext,
@@ -1857,7 +1829,7 @@ function codegenInstruction(
);
} else {
return createVariableDeclaration(instr.loc, 'const', [
createVariableDeclarator(
t.variableDeclarator(
convertIdentifier(instr.lvalue.identifier),
expressionValue,
),
@@ -2784,7 +2756,7 @@ function codegenArrayPattern(
): t.ArrayPattern {
const hasHoles = !pattern.items.every(e => e.kind !== 'Hole');
if (hasHoles) {
const result = createArrayPattern(pattern.loc, []);
const result = t.arrayPattern([]);
/*
* Older versions of babel have a validation bug fixed by
* https://github.com/babel/babel/pull/10917
@@ -2805,8 +2777,7 @@ function codegenArrayPattern(
}
return result;
} else {
return createArrayPattern(
pattern.loc,
return t.arrayPattern(
pattern.items.map(item => {
if (item.kind === 'Hole') {
return null;
@@ -2826,8 +2797,7 @@ function codegenLValue(
return codegenArrayPattern(cx, pattern);
}
case 'ObjectPattern': {
return createObjectPattern(
pattern.loc,
return t.objectPattern(
pattern.properties.map(property => {
if (property.kind === 'ObjectProperty') {
const key = codegenObjectPropertyKey(cx, property.key);
@@ -2946,7 +2916,7 @@ function convertIdentifier(identifier: Identifier): t.Identifier {
suggestions: null,
},
);
return createIdentifier(identifier.loc, identifier.name.value);
return t.identifier(identifier.name.value);
}
function compareScopeDependency(

View File

@@ -389,6 +389,14 @@ export function findDisjointMutableValues(
*/
operand.identifier.mutableRange.start > 0
) {
if (
instr.value.kind === 'FunctionExpression' ||
instr.value.kind === 'ObjectMethod'
) {
if (operand.identifier.type.kind === 'Primitive') {
continue;
}
}
operands.push(operand.identifier);
}
}

View File

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

View File

@@ -10,7 +10,6 @@ import {
CompilerDiagnostic,
CompilerError,
CompilerSuggestionOperation,
Effect,
SourceLocation,
} from '..';
import {CompilerSuggestion, ErrorCategory} from '../CompilerError';
@@ -19,13 +18,10 @@ import {
BlockId,
DependencyPath,
FinishMemoize,
GeneratedSource,
HIRFunction,
Identifier,
IdentifierId,
InstructionKind,
isEffectEventFunctionType,
isPrimitiveType,
isStableType,
isSubPath,
isSubPathIgnoringOptionals,
@@ -43,7 +39,6 @@ import {
} from '../HIR/visitors';
import {Result} from '../Utils/Result';
import {retainWhere} from '../Utils/utils';
import {isEffectHook} from './ValidateMemoizedEffectDependencies';
const DEBUG = false;
@@ -58,18 +53,20 @@ const DEBUG = false;
* - If the manual dependencies had extraneous deps, then auto memoization
* will remove them and cause the value to update *less* frequently.
*
* The implementation compares the manual dependencies against the values
* actually used within the memoization function
* - For each value V referenced in the memo function, either:
* - If the value is non-reactive *and* a known stable type, then the
* value may optionally be specified as an exact dependency.
* - Otherwise, report an error unless there is a manual dependency that will
* invalidate whenever V invalidates. If `x.y.z` is referenced, there must
* be a manual dependency for `x.y.z`, `x.y`, or `x`. Note that we assume
* no interior mutability, ie we assume that any changes to inner paths must
* always cause the other path to change as well.
* - Any dependencies that do not correspond to a value referenced in the memo
* function are considered extraneous and throw an error
* We consider a value V as missing if ALL of the following conditions are met:
* - V is reactive
* - There is no manual dependency path P such that whenever V would change,
* P would also change. If V is `x.y.z`, this means there must be some
* path P that is either `x.y.z`, `x.y`, or `x`. Note that we assume no
* interior mutability, such that a shorter path "covers" changes to longer
* more precise paths.
*
* We consider a value V extraneous if either of the folowing are true:
* - V is a reactive local that is unreferenced
* - V is a global that is unreferenced
*
* In other words, we allow extraneous non-reactive values since we know they cannot
* impact how often the memoization would run.
*
* ## TODO: Invalid, Complex Deps
*
@@ -89,7 +86,6 @@ const DEBUG = false;
export function validateExhaustiveDependencies(
fn: HIRFunction,
): Result<void, CompilerError> {
const env = fn.env;
const reactive = collectReactiveIdentifiersHIR(fn);
const temporaries: Map<IdentifierId, Temporary> = new Map();
@@ -131,19 +127,195 @@ export function validateExhaustiveDependencies(
loc: value.loc,
},
);
if (env.config.validateExhaustiveMemoizationDependencies) {
visitCandidateDependency(value.decl, temporaries, dependencies, locals);
const inferred: Array<InferredDependency> = Array.from(dependencies);
const diagnostic = validateDependencies(
inferred,
startMemo.deps ?? [],
reactive,
startMemo.depsLoc,
ErrorCategory.MemoDependencies,
'all',
visitCandidateDependency(value.decl, temporaries, dependencies, locals);
const inferred: Array<InferredDependency> = Array.from(dependencies);
// Sort dependencies by name and path, with shorter/non-optional paths first
inferred.sort((a, b) => {
if (a.kind === 'Global' && b.kind == 'Global') {
return a.binding.name.localeCompare(b.binding.name);
} else if (a.kind == 'Local' && b.kind == 'Local') {
CompilerError.simpleInvariant(
a.identifier.name != null &&
a.identifier.name.kind === 'named' &&
b.identifier.name != null &&
b.identifier.name.kind === 'named',
{
reason: 'Expected dependencies to be named variables',
loc: a.loc,
},
);
if (a.identifier.id !== b.identifier.id) {
return a.identifier.name.value.localeCompare(b.identifier.name.value);
}
if (a.path.length !== b.path.length) {
// if a's path is shorter this returns a negative, sorting a first
return a.path.length - b.path.length;
}
for (let i = 0; i < a.path.length; i++) {
const aProperty = a.path[i];
const bProperty = b.path[i];
const aOptional = aProperty.optional ? 0 : 1;
const bOptional = bProperty.optional ? 0 : 1;
if (aOptional !== bOptional) {
// sort non-optionals first
return aOptional - bOptional;
} else if (aProperty.property !== bProperty.property) {
return String(aProperty.property).localeCompare(
String(bProperty.property),
);
}
}
return 0;
} else {
const aName =
a.kind === 'Global' ? a.binding.name : a.identifier.name?.value;
const bName =
b.kind === 'Global' ? b.binding.name : b.identifier.name?.value;
if (aName != null && bName != null) {
return aName.localeCompare(bName);
}
return 0;
}
});
// remove redundant inferred dependencies
retainWhere(inferred, (dep, ix) => {
const match = inferred.findIndex(prevDep => {
return (
isEqualTemporary(prevDep, dep) ||
(prevDep.kind === 'Local' &&
dep.kind === 'Local' &&
prevDep.identifier.id === dep.identifier.id &&
isSubPath(prevDep.path, dep.path))
);
});
// only retain entries that don't have a prior match
return match === -1 || match >= ix;
});
// Validate that all manual dependencies belong there
if (DEBUG) {
console.log('manual');
console.log(
(startMemo.deps ?? [])
.map(x => ' ' + printManualMemoDependency(x))
.join('\n'),
);
if (diagnostic != null) {
console.log('inferred');
console.log(
inferred.map(x => ' ' + printInferredDependency(x)).join('\n'),
);
}
const manualDependencies = startMemo.deps ?? [];
const matched: Set<ManualMemoDependency> = new Set();
const missing: Array<Extract<InferredDependency, {kind: 'Local'}>> = [];
const extra: Array<ManualMemoDependency> = [];
for (const inferredDependency of inferred) {
if (inferredDependency.kind === 'Global') {
for (const manualDependency of manualDependencies) {
if (
manualDependency.root.kind === 'Global' &&
manualDependency.root.identifierName ===
inferredDependency.binding.name
) {
matched.add(manualDependency);
extra.push(manualDependency);
}
}
continue;
}
CompilerError.simpleInvariant(inferredDependency.kind === 'Local', {
reason: 'Unexpected function dependency',
loc: value.loc,
});
const isRequiredDependency = reactive.has(
inferredDependency.identifier.id,
);
let hasMatchingManualDependency = false;
for (const manualDependency of manualDependencies) {
if (
manualDependency.root.kind === 'NamedLocal' &&
manualDependency.root.value.identifier.id ===
inferredDependency.identifier.id &&
(areEqualPaths(manualDependency.path, inferredDependency.path) ||
isSubPathIgnoringOptionals(
manualDependency.path,
inferredDependency.path,
))
) {
hasMatchingManualDependency = true;
matched.add(manualDependency);
if (!isRequiredDependency) {
extra.push(manualDependency);
}
}
}
if (isRequiredDependency && !hasMatchingManualDependency) {
missing.push(inferredDependency);
}
}
for (const dep of startMemo.deps ?? []) {
if (matched.has(dep)) {
continue;
}
extra.push(dep);
}
/**
* Per docblock, we only consider dependencies as extraneous if
* they are unused globals or reactive locals. Notably, this allows
* non-reactive locals.
*/
retainWhere(extra, dep => {
return dep.root.kind === 'Global' || dep.root.value.reactive;
});
if (missing.length !== 0 || extra.length !== 0) {
let suggestions: Array<CompilerSuggestion> | null = null;
if (startMemo.depsLoc != null && typeof startMemo.depsLoc !== 'symbol') {
suggestions = [
{
description: 'Update dependencies',
range: [startMemo.depsLoc.start.index, startMemo.depsLoc.end.index],
op: CompilerSuggestionOperation.Replace,
text: `[${inferred.map(printInferredDependency).join(', ')}]`,
},
];
}
if (missing.length !== 0) {
const diagnostic = CompilerDiagnostic.create({
category: ErrorCategory.MemoDependencies,
reason: 'Found missing memoization dependencies',
description:
'Missing dependencies can cause a value not to update when those inputs change, ' +
'resulting in stale UI',
suggestions,
});
for (const dep of missing) {
let reactiveStableValueHint = '';
if (isStableType(dep.identifier)) {
reactiveStableValueHint =
'. Refs, setState functions, and other "stable" values generally do not need to be added as dependencies, but this variable may change over time to point to different values';
}
diagnostic.withDetails({
kind: 'error',
message: `Missing dependency \`${printInferredDependency(dep)}\`${reactiveStableValueHint}`,
loc: dep.loc,
});
}
error.pushDiagnostic(diagnostic);
} else if (extra.length !== 0) {
const diagnostic = CompilerDiagnostic.create({
category: ErrorCategory.MemoDependencies,
reason: 'Found unnecessary memoization dependencies',
description:
'Unnecessary dependencies can cause a value to update more often than necessary, ' +
'causing performance regressions and effects to fire more often than expected',
});
diagnostic.withDetails({
kind: 'error',
message: `Unnecessary dependencies ${extra.map(dep => `\`${printManualMemoDependency(dep)}\``).join(', ')}`,
loc: startMemo.depsLoc ?? value.loc,
});
error.pushDiagnostic(diagnostic);
}
}
@@ -159,347 +331,18 @@ export function validateExhaustiveDependencies(
{
onStartMemoize,
onFinishMemoize,
onEffect: (inferred, manual, manualMemoLoc) => {
if (env.config.validateExhaustiveEffectDependencies === 'off') {
return;
}
if (DEBUG) {
console.log(Array.from(inferred, printInferredDependency));
console.log(Array.from(manual, printInferredDependency));
}
const manualDeps: Array<ManualMemoDependency> = [];
for (const dep of manual) {
if (dep.kind === 'Local') {
manualDeps.push({
root: {
kind: 'NamedLocal',
constant: false,
value: {
effect: Effect.Read,
identifier: dep.identifier,
kind: 'Identifier',
loc: dep.loc,
reactive: reactive.has(dep.identifier.id),
},
},
path: dep.path,
loc: dep.loc,
});
} else {
manualDeps.push({
root: {
kind: 'Global',
identifierName: dep.binding.name,
},
path: [],
loc: GeneratedSource,
});
}
}
const effectReportMode =
typeof env.config.validateExhaustiveEffectDependencies === 'string'
? env.config.validateExhaustiveEffectDependencies
: 'all';
const diagnostic = validateDependencies(
Array.from(inferred),
manualDeps,
reactive,
manualMemoLoc,
ErrorCategory.EffectExhaustiveDependencies,
effectReportMode,
);
if (diagnostic != null) {
error.pushDiagnostic(diagnostic);
}
},
},
false, // isFunctionExpression
);
return error.asResult();
}
function validateDependencies(
inferred: Array<InferredDependency>,
manualDependencies: Array<ManualMemoDependency>,
reactive: Set<IdentifierId>,
manualMemoLoc: SourceLocation | null,
category:
| ErrorCategory.MemoDependencies
| ErrorCategory.EffectExhaustiveDependencies,
exhaustiveDepsReportMode: 'all' | 'missing-only' | 'extra-only',
): CompilerDiagnostic | null {
// Sort dependencies by name and path, with shorter/non-optional paths first
inferred.sort((a, b) => {
if (a.kind === 'Global' && b.kind == 'Global') {
return a.binding.name.localeCompare(b.binding.name);
} else if (a.kind == 'Local' && b.kind == 'Local') {
CompilerError.simpleInvariant(
a.identifier.name != null &&
a.identifier.name.kind === 'named' &&
b.identifier.name != null &&
b.identifier.name.kind === 'named',
{
reason: 'Expected dependencies to be named variables',
loc: a.loc,
},
);
if (a.identifier.id !== b.identifier.id) {
return a.identifier.name.value.localeCompare(b.identifier.name.value);
}
if (a.path.length !== b.path.length) {
// if a's path is shorter this returns a negative, sorting a first
return a.path.length - b.path.length;
}
for (let i = 0; i < a.path.length; i++) {
const aProperty = a.path[i];
const bProperty = b.path[i];
const aOptional = aProperty.optional ? 0 : 1;
const bOptional = bProperty.optional ? 0 : 1;
if (aOptional !== bOptional) {
// sort non-optionals first
return aOptional - bOptional;
} else if (aProperty.property !== bProperty.property) {
return String(aProperty.property).localeCompare(
String(bProperty.property),
);
}
}
return 0;
} else {
const aName =
a.kind === 'Global' ? a.binding.name : a.identifier.name?.value;
const bName =
b.kind === 'Global' ? b.binding.name : b.identifier.name?.value;
if (aName != null && bName != null) {
return aName.localeCompare(bName);
}
return 0;
}
});
// remove redundant inferred dependencies
retainWhere(inferred, (dep, ix) => {
const match = inferred.findIndex(prevDep => {
return (
isEqualTemporary(prevDep, dep) ||
(prevDep.kind === 'Local' &&
dep.kind === 'Local' &&
prevDep.identifier.id === dep.identifier.id &&
isSubPath(prevDep.path, dep.path))
);
});
// only retain entries that don't have a prior match
return match === -1 || match >= ix;
});
// Validate that all manual dependencies belong there
if (DEBUG) {
console.log('manual');
console.log(
manualDependencies
.map(x => ' ' + printManualMemoDependency(x))
.join('\n'),
);
console.log('inferred');
console.log(
inferred.map(x => ' ' + printInferredDependency(x)).join('\n'),
);
}
const matched: Set<ManualMemoDependency> = new Set();
const missing: Array<Extract<InferredDependency, {kind: 'Local'}>> = [];
const extra: Array<ManualMemoDependency> = [];
for (const inferredDependency of inferred) {
if (inferredDependency.kind === 'Global') {
for (const manualDependency of manualDependencies) {
if (
manualDependency.root.kind === 'Global' &&
manualDependency.root.identifierName ===
inferredDependency.binding.name
) {
matched.add(manualDependency);
extra.push(manualDependency);
}
}
continue;
}
CompilerError.simpleInvariant(inferredDependency.kind === 'Local', {
reason: 'Unexpected function dependency',
loc: inferredDependency.loc,
});
/**
* Skip effect event functions as they are not valid dependencies
*/
if (isEffectEventFunctionType(inferredDependency.identifier)) {
continue;
}
let hasMatchingManualDependency = false;
for (const manualDependency of manualDependencies) {
if (
manualDependency.root.kind === 'NamedLocal' &&
manualDependency.root.value.identifier.id ===
inferredDependency.identifier.id &&
(areEqualPaths(manualDependency.path, inferredDependency.path) ||
isSubPathIgnoringOptionals(
manualDependency.path,
inferredDependency.path,
))
) {
hasMatchingManualDependency = true;
matched.add(manualDependency);
}
}
if (
hasMatchingManualDependency ||
isOptionalDependency(inferredDependency, reactive)
) {
continue;
}
missing.push(inferredDependency);
}
for (const dep of manualDependencies) {
if (matched.has(dep)) {
continue;
}
if (dep.root.kind === 'NamedLocal' && dep.root.constant) {
CompilerError.simpleInvariant(
!dep.root.value.reactive && isPrimitiveType(dep.root.value.identifier),
{
reason: 'Expected constant-folded dependency to be non-reactive',
loc: dep.root.value.loc,
},
);
/*
* Constant primitives can get constant-folded, which means we won't
* see a LoadLocal for the value within the memo function.
*/
continue;
}
extra.push(dep);
}
// Filter based on report mode
const filteredMissing =
exhaustiveDepsReportMode === 'extra-only' ? [] : missing;
const filteredExtra =
exhaustiveDepsReportMode === 'missing-only' ? [] : extra;
if (filteredMissing.length !== 0 || filteredExtra.length !== 0) {
let suggestion: CompilerSuggestion | null = null;
if (
manualMemoLoc != null &&
typeof manualMemoLoc !== 'symbol' &&
manualMemoLoc.start.index != null &&
manualMemoLoc.end.index != null
) {
suggestion = {
description: 'Update dependencies',
range: [manualMemoLoc.start.index, manualMemoLoc.end.index],
op: CompilerSuggestionOperation.Replace,
text: `[${inferred
.filter(
dep =>
dep.kind === 'Local' &&
!isOptionalDependency(dep, reactive) &&
!isEffectEventFunctionType(dep.identifier),
)
.map(printInferredDependency)
.join(', ')}]`,
};
}
const diagnostic = createDiagnostic(
category,
filteredMissing,
filteredExtra,
suggestion,
);
for (const dep of filteredMissing) {
let reactiveStableValueHint = '';
if (isStableType(dep.identifier)) {
reactiveStableValueHint =
'. Refs, setState functions, and other "stable" values generally do not need to be added ' +
'as dependencies, but this variable may change over time to point to different values';
}
diagnostic.withDetails({
kind: 'error',
message: `Missing dependency \`${printInferredDependency(dep)}\`${reactiveStableValueHint}`,
loc: dep.loc,
});
}
for (const dep of filteredExtra) {
if (dep.root.kind === 'Global') {
diagnostic.withDetails({
kind: 'error',
message:
`Unnecessary dependency \`${printManualMemoDependency(dep)}\`. ` +
'Values declared outside of a component/hook should not be listed as ' +
'dependencies as the component will not re-render if they change',
loc: dep.loc ?? manualMemoLoc,
});
} else {
const root = dep.root.value;
const matchingInferred = inferred.find(
(
inferredDep,
): inferredDep is Extract<InferredDependency, {kind: 'Local'}> => {
return (
inferredDep.kind === 'Local' &&
inferredDep.identifier.id === root.identifier.id &&
isSubPathIgnoringOptionals(inferredDep.path, dep.path)
);
},
);
if (
matchingInferred != null &&
isEffectEventFunctionType(matchingInferred.identifier)
) {
diagnostic.withDetails({
kind: 'error',
message:
`Functions returned from \`useEffectEvent\` must not be included in the dependency array. ` +
`Remove \`${printManualMemoDependency(dep)}\` from the dependencies.`,
loc: dep.loc ?? manualMemoLoc,
});
} else if (
matchingInferred != null &&
!isOptionalDependency(matchingInferred, reactive)
) {
diagnostic.withDetails({
kind: 'error',
message:
`Overly precise dependency \`${printManualMemoDependency(dep)}\`, ` +
`use \`${printInferredDependency(matchingInferred)}\` instead`,
loc: dep.loc ?? manualMemoLoc,
});
} else {
/**
* Else this dependency doesn't correspond to anything referenced in the memo function,
* or is an optional dependency so we don't want to suggest adding it
*/
diagnostic.withDetails({
kind: 'error',
message: `Unnecessary dependency \`${printManualMemoDependency(dep)}\``,
loc: dep.loc ?? manualMemoLoc,
});
}
}
}
if (suggestion != null) {
diagnostic.withDetails({
kind: 'hint',
message: `Inferred dependencies: \`${suggestion.text}\``,
});
}
return diagnostic;
}
return null;
}
function addDependency(
dep: Temporary,
dependencies: Set<InferredDependency>,
locals: Set<IdentifierId>,
): void {
if (dep.kind === 'Aggregate') {
if (dep.kind === 'Function') {
for (const x of dep.dependencies) {
addDependency(x, dependencies, locals);
}
@@ -582,14 +425,9 @@ function collectDependencies(
dependencies: Set<InferredDependency>,
locals: Set<IdentifierId>,
) => void;
onEffect: (
inferred: Set<InferredDependency>,
manual: Set<InferredDependency>,
manualMemoLoc: SourceLocation | null,
) => void;
} | null,
isFunctionExpression: boolean,
): Extract<Temporary, {kind: 'Aggregate'}> {
): Extract<Temporary, {kind: 'Function'}> {
const optionals = findOptionalPlaces(fn);
if (DEBUG) {
console.log(prettyFormat(optionals));
@@ -608,25 +446,25 @@ function collectDependencies(
}
for (const block of fn.body.blocks.values()) {
for (const phi of block.phis) {
const deps: Array<InferredDependency> = [];
let deps: Array<Temporary> | null = null;
for (const operand of phi.operands.values()) {
const dep = temporaries.get(operand.identifier.id);
if (dep == null) {
continue;
}
if (dep.kind === 'Aggregate') {
deps.push(...dep.dependencies);
if (deps == null) {
deps = [dep];
} else {
deps.push(dep);
}
}
if (deps.length === 0) {
if (deps == null) {
continue;
} else if (deps.length === 1) {
temporaries.set(phi.place.identifier.id, deps[0]!);
} else {
temporaries.set(phi.place.identifier.id, {
kind: 'Aggregate',
kind: 'Function',
dependencies: new Set(deps),
});
}
@@ -644,6 +482,9 @@ function collectDependencies(
}
case 'LoadContext':
case 'LoadLocal': {
if (locals.has(value.place.identifier.id)) {
break;
}
const temp = temporaries.get(value.place.identifier.id);
if (temp != null) {
if (temp.kind === 'Local') {
@@ -652,9 +493,6 @@ function collectDependencies(
} else {
temporaries.set(lvalue.identifier.id, temp);
}
if (locals.has(value.place.identifier.id)) {
locals.add(lvalue.identifier.id);
}
}
break;
}
@@ -790,55 +628,10 @@ function collectDependencies(
}
break;
}
case 'ArrayExpression': {
const arrayDeps: Set<InferredDependency> = new Set();
for (const item of value.elements) {
if (item.kind === 'Hole') {
continue;
}
const place = item.kind === 'Identifier' ? item : item.place;
// Visit with alternative deps/locals to record manual dependencies
visitCandidateDependency(place, temporaries, arrayDeps, new Set());
// Visit normally to propagate inferred dependencies upward
visit(place);
}
temporaries.set(lvalue.identifier.id, {
kind: 'Aggregate',
dependencies: arrayDeps,
loc: value.loc,
});
break;
}
case 'CallExpression':
case 'MethodCall': {
const receiver =
value.kind === 'CallExpression' ? value.callee : value.property;
const onEffect = callbacks?.onEffect;
if (onEffect != null && isEffectHook(receiver.identifier)) {
const [fn, deps] = value.args;
if (fn?.kind === 'Identifier' && deps?.kind === 'Identifier') {
const fnDeps = temporaries.get(fn.identifier.id);
const manualDeps = temporaries.get(deps.identifier.id);
if (
fnDeps?.kind === 'Aggregate' &&
manualDeps?.kind === 'Aggregate'
) {
onEffect(
fnDeps.dependencies,
manualDeps.dependencies,
manualDeps.loc ?? null,
);
}
}
}
// Ignore the method itself
for (const operand of eachInstructionValueOperand(value)) {
if (
value.kind === 'MethodCall' &&
operand.identifier.id === value.property.identifier.id
) {
if (operand.identifier.id === value.property.identifier.id) {
continue;
}
visit(operand);
@@ -862,7 +655,7 @@ function collectDependencies(
visit(operand);
}
}
return {kind: 'Aggregate', dependencies};
return {kind: 'Function', dependencies};
}
function printInferredDependency(dep: InferredDependency): string {
@@ -900,7 +693,7 @@ function printManualMemoDependency(dep: ManualMemoDependency): string {
function isEqualTemporary(a: Temporary, b: Temporary): boolean {
switch (a.kind) {
case 'Aggregate': {
case 'Function': {
return false;
}
case 'Global': {
@@ -925,11 +718,7 @@ type Temporary =
context: boolean;
loc: SourceLocation;
}
| {
kind: 'Aggregate';
dependencies: Set<InferredDependency>;
loc?: SourceLocation;
};
| {kind: 'Function'; dependencies: Set<Temporary>};
type InferredDependency = Extract<Temporary, {kind: 'Local' | 'Global'}>;
function collectReactiveIdentifiersHIR(fn: HIRFunction): Set<IdentifierId> {
@@ -1033,75 +822,3 @@ export function findOptionalPlaces(
}
return optionals;
}
function isOptionalDependency(
inferredDependency: Extract<InferredDependency, {kind: 'Local'}>,
reactive: Set<IdentifierId>,
): boolean {
return (
!reactive.has(inferredDependency.identifier.id) &&
(isStableType(inferredDependency.identifier) ||
isPrimitiveType(inferredDependency.identifier))
);
}
function createDiagnostic(
category:
| ErrorCategory.MemoDependencies
| ErrorCategory.EffectExhaustiveDependencies,
missing: Array<InferredDependency>,
extra: Array<ManualMemoDependency>,
suggestion: CompilerSuggestion | null,
): CompilerDiagnostic {
let reason: string;
let description: string;
function joinMissingExtraDetail(
missingString: string,
extraString: string,
joinStr: string,
): string {
return [
missing.length !== 0 ? missingString : null,
extra.length !== 0 ? extraString : null,
]
.filter(Boolean)
.join(joinStr);
}
switch (category) {
case ErrorCategory.MemoDependencies: {
reason = `Found ${joinMissingExtraDetail('missing', 'extra', '/')} memoization dependencies`;
description = joinMissingExtraDetail(
'Missing dependencies can cause a value to update less often than it should, resulting in stale UI',
'Extra dependencies can cause a value to update more often than it should, resulting in performance' +
' problems such as excessive renders or effects firing too often',
'. ',
);
break;
}
case ErrorCategory.EffectExhaustiveDependencies: {
reason = `Found ${joinMissingExtraDetail('missing', 'extra', '/')} effect dependencies`;
description = joinMissingExtraDetail(
'Missing dependencies can cause an effect to fire less often than it should',
'Extra dependencies can cause an effect to fire more often than it should, resulting' +
' in performance problems such as excessive renders and side effects',
'. ',
);
break;
}
default: {
CompilerError.simpleInvariant(false, {
reason: `Unexpected error category: ${category}`,
loc: GeneratedSource,
});
}
}
return CompilerDiagnostic.create({
category,
reason,
description,
suggestions: suggestion != null ? [suggestion] : null,
});
}

View File

@@ -0,0 +1,59 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {CompilerDiagnostic, CompilerError} from '..';
import {ErrorCategory} from '../CompilerError';
import {HIRFunction} from '../HIR';
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
import {Result} from '../Utils/Result';
/**
* Checks that known-impure functions are not called during render. Examples of invalid functions to
* call during render are `Math.random()` and `Date.now()`. Users may extend this set of
* impure functions via a module type provider and specifying functions with `impure: true`.
*
* TODO: add best-effort analysis of functions which are called during render. We have variations of
* this in several of our validation passes and should unify those analyses into a reusable helper
* and use it here.
*/
export function validateNoImpureFunctionsInRender(
fn: HIRFunction,
): Result<void, CompilerError> {
const errors = new CompilerError();
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
const value = instr.value;
if (value.kind === 'MethodCall' || value.kind == 'CallExpression') {
const callee =
value.kind === 'MethodCall' ? value.property : value.callee;
const signature = getFunctionCallSignature(
fn.env,
callee.identifier.type,
);
if (signature != null && signature.impure === true) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Purity,
reason: 'Cannot call impure function during render',
description:
(signature.canonicalName != null
? `\`${signature.canonicalName}\` is an impure function. `
: '') +
'Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
suggestions: null,
}).withDetails({
kind: 'error',
loc: callee.loc,
message: 'Cannot call impure function',
}),
);
}
}
}
}
return errors.asResult();
}

View File

@@ -1,387 +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, Effect} from '..';
import {
areEqualSourceLocations,
HIRFunction,
IdentifierId,
InstructionId,
isJsxType,
isUseRefType,
} from '../HIR';
import {AliasingEffect, hashEffect} from '../Inference/AliasingEffects';
import {createControlDominators} from '../Inference/ControlDominators';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
import {Err, Ok, Result} from '../Utils/Result';
import {getOrInsertWith} from '../Utils/utils';
import {printFunction} 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: Map<IdentifierId, ImpureEffect>;
error: CompilerError;
returns: IdentifierId;
};
export function validateNoImpureValuesInRender(
fn: HIRFunction,
): Result<void, CompilerError> {
const impure = new Map<IdentifierId, ImpureEffect>();
const impureFunctions = new Map<IdentifierId, ImpuritySignature>();
const result = inferImpureValues(fn, impure, impureFunctions, new Map());
if (result.error.hasAnyErrors()) {
return Err(result.error);
}
return Ok(undefined);
}
function inferFunctionExpressionMemo(
fn: HIRFunction,
impure: Map<IdentifierId, ImpureEffect>,
impureFunctions: Map<IdentifierId, ImpuritySignature>,
cache: FunctionCache,
): ImpuritySignature {
const key = fn.context
.map(
place =>
`${place.identifier.id}:${impure.has(place.identifier.id)}:${Array.from(
impureFunctions.get(place.identifier.id)?.effects ?? new Map(),
)
.map(([id, effect]) => `${id}=>${effect.into.identifier.id}`)
.join(',')}`,
)
.join(',');
return getOrInsertWith(
getOrInsertWith(cache, fn, () => new Map()),
key,
() => inferImpureValues(fn, impure, impureFunctions, cache),
);
}
function processEffects(
id: InstructionId,
effects: Array<AliasingEffect>,
impure: Map<IdentifierId, ImpureEffect>,
impureFunctions: Map<IdentifierId, ImpuritySignature>,
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)
) {
// console.log(
// `${effect.kind} $${effect.into.identifier.id} <= $${effect.from.identifier.id} ($${sourceEffect.into.identifier.id} forward)`,
// );
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) {
// console.log(
// `${effect.kind} $${effect.into.identifier.id} => $${effect.from.identifier.id} ($${destinationEffect.into.identifier.id} backward)`,
// );
impure.set(effect.from.identifier.id, destinationEffect);
hasChanges = true;
}
}
if (
(effect.kind === 'Alias' ||
effect.kind === 'Assign' ||
effect.kind === 'ImmutableCapture') &&
!rendered.has(effect.into.identifier.id) &&
!isJsxType(effect.into.identifier.type)
) {
const functionEffect = impureFunctions.get(effect.from.identifier.id);
if (
functionEffect != null &&
!impureFunctions.has(effect.into.identifier.id)
// ||
// !areEqualFunctionSignatures(
// impureFunctions.get(effect.into.identifier.id)!.effects,
// functionEffect.effects,
// )
) {
// console.log(
// `${effect.kind} $${effect.into.identifier.id} <= $${effect.from.identifier.id} (function)`,
// );
impureFunctions.set(effect.into.identifier.id, functionEffect);
hasChanges = true;
}
}
break;
}
case 'Impure': {
if (!impure.has(effect.into.identifier.id)) {
// console.log(`Impure $${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,
impureFunctions,
cache,
);
if (result.error.hasAnyErrors()) {
break;
}
const previousResult = impureFunctions.get(effect.into.identifier.id);
if (
previousResult == null ||
!areEqualFunctionSignatures(result.effects, previousResult.effects)
) {
// console.log(`Function $${effect.into.identifier.id}`);
impureFunctions.set(effect.into.identifier.id, result);
hasChanges = true;
}
break;
}
case 'Apply': {
const functionSignature = impureFunctions.get(
effect.function.identifier.id,
);
if (functionSignature != null) {
for (const [id, functionEffect] of functionSignature.effects) {
if (!impure.has(id)) {
impure.set(id, functionEffect);
hasChanges = true;
}
if (
id === functionSignature.returns &&
!impure.has(effect.into.identifier.id)
) {
impure.set(effect.into.identifier.id, functionEffect);
hasChanges = true;
}
}
}
break;
}
case 'MaybeAlias':
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>,
impureFunctions: Map<IdentifierId, ImpuritySignature>,
cache: FunctionCache,
): ImpuritySignature {
const getBlockControl = createControlDominators(fn, place => {
return impure.has(place.identifier.id);
});
let hasChanges = false;
let iterations = 0;
do {
hasChanges = false;
if (iterations++ > 100) {
throw new Error('too many iterations');
}
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,
impureFunctions,
cache,
) || hasChanges;
}
if (block.terminal.kind === 'return' && block.terminal.effects != null) {
hasChanges =
processEffects(
block.terminal.id,
block.terminal.effects,
impure,
impureFunctions,
cache,
) || hasChanges;
}
}
} while (hasChanges);
fn.env.logger?.debugLogIRs?.({
kind: 'debug',
name: 'ValidateNoImpureValuesInRender',
value: JSON.stringify(Array.from(impure.keys()).sort(), null, 2),
});
fn.env.logger?.debugLogIRs?.({
kind: 'debug',
name: 'ValidateNoImpureValuesInRender (function)',
value: JSON.stringify(Array.from(impureFunctions.keys()).sort(), null, 2),
});
const error = new CompilerError();
function validateRenderEffect(effect: RenderEffect): void {
let impureEffect = impure.get(effect.place.identifier.id);
if (impureEffect == null) {
const functionSignature = impureFunctions.get(effect.place.identifier.id);
impureEffect = functionSignature?.effects.get(functionSignature.returns);
}
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,
impureFunctions,
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: Map<IdentifierId, ImpureEffect> = new Map();
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.set(place.identifier.id, impureEffect);
}
}
return {effects: impureEffects, error, returns: fn.returns.identifier.id};
}
function areEqualFunctionSignatures(
sig1: Map<IdentifierId, ImpureEffect>,
sig2: Map<IdentifierId, ImpureEffect>,
): boolean {
return (
sig1.size === sig2.size &&
Array.from(sig1).every(
([id, effect]) =>
sig2.has(id) && hashEffect(effect) === hashEffect(sig2.get(id)!),
)
);
}

View File

@@ -1,137 +0,0 @@
# ValidateNoRefAccessInRender
This document summarizes the design and key learnings for the ref mutation validation pass.
## Purpose
Validates that a function does not mutate a ref value during render. This ensures React components follow the rules of React by not writing to `ref.current` during the render phase.
## Key Concepts
### Ref vs RefValue
- **Ref**: The ref object itself (e.g., `useRef()` return value). Has type `React.RefObject<T>`.
- **RefValue**: The `.current` property of a ref. This is the mutable value that should not be accessed during render.
The validation tracks both using a `RefInfo` type with a `refId` that correlates refs with their `.current` values.
### What Constitutes a Mutation
A mutation is any `PropertyStore` or `ComputedStore` instruction where:
1. The target object is a known ref (tracked in the `refs` map)
2. OR the target object has a ref type (`isUseRefType`)
### Allowed Patterns
1. **Event handlers and effect callbacks**: Functions that are not called at the top level during render can mutate refs freely.
2. **Null-guard initialization**: The pattern `if (ref.current == null) { ref.current = value; }` is allowed because it's a common lazy initialization pattern that only runs once.
## Algorithm: Single Forward Data-Flow Pass
The validation uses a single forward pass over all blocks:
### Phase 1: Track Refs
- Initialize refs from function params and context (captured variables)
- Process phi nodes to propagate ref info through control flow joins
- Track refs through LoadLocal, StoreLocal, PropertyLoad operations
### Phase 2: Detect Null Guards
- Track nullable values (null literals, undefined)
- Track binary comparisons of `ref.current` to null (`==`, `===`, `!=`, `!==`)
- Mark blocks as "safe" for specific refs when inside null-guard branches
- Propagate safety through control flow until fallthrough
### Phase 3: Validate Mutations
- For PropertyStore/ComputedStore on refs:
- If inside a null-guard for this ref: allow (but track for duplicate detection)
- If at top level: error immediately
- If in nested function: track for later (error if function is called)
### Phase 4: Track Ref-Mutating Functions
- When a FunctionExpression mutates a ref, track it in `refMutatingFunctions`
- When such a function is called at top level, report the error at the mutation site
## Key Data Structures
```typescript
// Correlates refs with their .current values
type RefInfo = {
kind: 'Ref' | 'RefValue';
refId: number;
};
// Tracks null-guard conditions
type GuardInfo = {
refId: number;
isEquality: boolean; // true for ==, ===; false for !=, !==
};
// Information about a mutation (for error reporting)
type MutationInfo = {
loc: SourceLocation;
isCurrentProperty: boolean;
};
```
## Error Reporting
### Error Location
Errors highlight the **entire instruction** (e.g., `ref.current = value`), not just the ref identifier. This is achieved by using `instr.loc` instead of `value.object.loc`.
### Duplicate Initialization
When a ref is initialized more than once inside a null-guard:
1. Primary error: Points to the second initialization
2. Secondary error: Points to the first initialization with "Ref was first initialized here"
### Transitive Mutations
When a function that mutates refs is called during render:
- The error points to the mutation site inside the function
- Not the call site (the call site is what triggers the check)
## Edge Cases and Patterns
### Unary NOT on Guards
The `!` operator inverts guard polarity:
```javascript
if (!ref.current) { ... } // Same as: if (ref.current == null)
```
### Nested Functions
Functions defined during render but not called are allowed to mutate refs:
```javascript
// OK - onClick is not called during render
const onClick = () => { ref.current = value; };
return <button onClick={onClick} />;
```
### Props with Ref Type
Refs can come from props. The validation handles `props.ref` by checking type information.
## Limitations / Known Gaps
The following patterns are NOT currently validated by this pass:
1. **Impure values in render**: `Date.now()`, `Math.random()` flowing into render context (handled by `ValidateNoImpureValuesInRender`)
2. **useState/useReducer callbacks**: These hooks call their initializer functions during render, so ref access inside them should error. This requires special hook semantics.
3. **Ref reads during render**: This pass focuses on mutations. Ref reads are handled separately.
## Testing
Test fixtures use naming conventions:
- `error.*.ts` - Fixtures expected to produce compilation errors
- Regular names - Fixtures expected to compile successfully
Run tests with:
```bash
yarn snap -p <pattern> --nodebug # Run specific tests
yarn snap -p <pattern> --nodebug -u # Update expected output
```

View File

@@ -16,7 +16,6 @@ import {
IdentifierId,
isSetStateType,
isUseEffectHookType,
isUseEffectEventType,
isUseInsertionEffectHookType,
isUseLayoutEffectHookType,
isUseRefType,
@@ -99,20 +98,7 @@ export function validateNoSetStateInEffects(
instr.value.kind === 'MethodCall'
? instr.value.receiver
: instr.value.callee;
if (isUseEffectEventType(callee.identifier)) {
const arg = instr.value.args[0];
if (arg !== undefined && arg.kind === 'Identifier') {
const setState = setStateFunctions.get(arg.identifier.id);
if (setState !== undefined) {
/**
* This effect event function calls setState synchonously,
* treat it as a setState function for transitive tracking
*/
setStateFunctions.set(instr.lvalue.identifier.id, setState);
}
}
} else if (
if (
isUseEffectHookType(callee.identifier) ||
isUseLayoutEffectHookType(callee.identifier) ||
isUseInsertionEffectHookType(callee.identifier)
@@ -121,58 +107,26 @@ export function validateNoSetStateInEffects(
if (arg !== undefined && arg.kind === 'Identifier') {
const setState = setStateFunctions.get(arg.identifier.id);
if (setState !== undefined) {
const enableVerbose =
env.config.enableVerboseNoSetStateInEffect;
if (enableVerbose) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.EffectSetState,
reason:
'Calling setState synchronously within an effect can trigger cascading renders',
description:
'Effects are intended to synchronize state between React and external systems. ' +
'Calling setState synchronously causes cascading renders that hurt performance.\n\n' +
'This pattern may indicate one of several issues:\n\n' +
'**1. Non-local derived data**: If the value being set could be computed from props/state ' +
'but requires data from a parent component, consider restructuring state ownership so the ' +
'derivation can happen during render in the component that owns the relevant state.\n\n' +
"**2. Derived event pattern**: If you're detecting when a prop changes (e.g., `isPlaying` " +
'transitioning from false to true), this often indicates the parent should provide an event ' +
'callback (like `onPlay`) instead of just the current state. Request access to the original event.\n\n' +
"**3. Force update / external sync**: If you're forcing a re-render to sync with an external " +
'data source (mutable values outside React), use `useSyncExternalStore` to properly subscribe ' +
'to external state changes.\n\n' +
'See: https://react.dev/learn/you-might-not-need-an-effect',
suggestions: null,
}).withDetails({
kind: 'error',
loc: setState.loc,
message:
'Avoid calling setState() directly within an effect',
}),
);
} else {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.EffectSetState,
reason:
'Calling setState synchronously within an effect can trigger cascading renders',
description:
'Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. ' +
'In general, the body of an effect should do one or both of the following:\n' +
'* Update external systems with the latest state from React.\n' +
'* Subscribe for updates from some external system, calling setState in a callback function when external state changes.\n\n' +
'Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. ' +
'(https://react.dev/learn/you-might-not-need-an-effect)',
suggestions: null,
}).withDetails({
kind: 'error',
loc: setState.loc,
message:
'Avoid calling setState() directly within an effect',
}),
);
}
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.EffectSetState,
reason:
'Calling setState synchronously within an effect can trigger cascading renders',
description:
'Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. ' +
'In general, the body of an effect should do one or both of the following:\n' +
'* Update external systems with the latest state from React.\n' +
'* Subscribe for updates from some external system, calling setState in a callback function when external state changes.\n\n' +
'Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. ' +
'(https://react.dev/learn/you-might-not-need-an-effect)',
suggestions: null,
}).withDetails({
kind: 'error',
loc: setState.loc,
message:
'Avoid calling setState() directly within an effect',
}),
);
}
}
}
@@ -202,10 +156,10 @@ function getSetStateCall(
);
};
const isRefControlledBlock: (id: BlockId) => Place | null =
const isRefControlledBlock: (id: BlockId) => boolean =
enableAllowSetStateFromRefsInEffects
? createControlDominators(fn, place => isDerivedFromRef(place))
: (): Place | null => null;
: (): boolean => false;
for (const [, block] of fn.body.blocks) {
if (enableAllowSetStateFromRefsInEffects) {
@@ -224,7 +178,7 @@ function getSetStateCall(
refDerivedValues.add(phi.place.identifier.id);
} else {
for (const [pred] of phi.operands) {
if (isRefControlledBlock(pred) != null) {
if (isRefControlledBlock(pred)) {
refDerivedValues.add(phi.place.identifier.id);
break;
}
@@ -337,7 +291,7 @@ function getSetStateCall(
* be needed when initial layout measurements from refs need to be stored in state.
*/
return null;
} else if (isRefControlledBlock(block.id) != null) {
} else if (isRefControlledBlock(block.id)) {
continue;
}
}

View File

@@ -155,40 +155,20 @@ function validateNoSetStateInRenderImpl(
}),
);
} else if (unconditionalBlocks.has(block.id)) {
const enableUseKeyedState = fn.env.config.enableUseKeyedState;
if (enableUseKeyedState) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.RenderSetState,
reason: 'Cannot call setState during render',
description:
'Calling setState during render may trigger an infinite loop.\n' +
'* To reset state when other state/props change, use `const [state, setState] = useKeyedState(initialState, key)` to reset `state` when `key` changes.\n' +
'* To derive data from other state/props, compute the derived data during render without using state',
suggestions: null,
}).withDetails({
kind: 'error',
loc: callee.loc,
message: 'Found setState() in render',
}),
);
} else {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.RenderSetState,
reason: 'Cannot call setState during render',
description:
'Calling setState during render may trigger an infinite loop.\n' +
'* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders\n' +
'* To derive data from other state/props, compute the derived data during render without using state',
suggestions: null,
}).withDetails({
kind: 'error',
loc: callee.loc,
message: 'Found setState() in render',
}),
);
}
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.RenderSetState,
reason:
'Calling setState during render may trigger an infinite loop',
description:
'Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState)',
suggestions: null,
}).withDetails({
kind: 'error',
loc: callee.loc,
message: 'Found setState() in render',
}),
);
}
}
break;

View File

@@ -242,7 +242,6 @@ function validateInferredDep(
normalizedDep = {
root: maybeNormalizedRoot.root,
path: [...maybeNormalizedRoot.path, ...dep.path],
loc: maybeNormalizedRoot.loc,
};
} else {
CompilerError.invariant(dep.identifier.name?.kind === 'named', {
@@ -268,10 +267,8 @@ function validateInferredDep(
effect: Effect.Read,
reactive: false,
},
constant: false,
},
path: [...dep.path],
loc: GeneratedSource,
};
}
for (const decl of declsWithinMemoBlock) {
@@ -382,10 +379,8 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
root: {
kind: 'NamedLocal',
value: storeTarget,
constant: false,
},
path: [],
loc: storeTarget.loc,
});
}
}
@@ -413,10 +408,8 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
root: {
kind: 'NamedLocal',
value: {...lvalue},
constant: false,
},
path: [],
loc: lvalue.loc,
});
}
}

View File

@@ -27,11 +27,7 @@ import {Result} from '../Utils/Result';
/**
* Some common node types that are important for coverage tracking.
* Based on istanbul-lib-instrument + some other common nodes we expect to be present in the generated AST.
*
* Note: For VariableDeclaration, VariableDeclarator, and Identifier, we enforce stricter validation
* that requires both the source location AND node type to match in the generated AST. This ensures
* that variable declarations maintain their structural integrity through compilation.
* Based on istanbul-lib-instrument
*/
const IMPORTANT_INSTRUMENTED_TYPES = new Set([
'ArrowFunctionExpression',
@@ -58,14 +54,6 @@ const IMPORTANT_INSTRUMENTED_TYPES = new Set([
'LabeledStatement',
'ConditionalExpression',
'LogicalExpression',
/**
* Note: these aren't important for coverage tracking,
* but we still want to track them to ensure we aren't regressing them when
* we fix the source location tracking for other nodes.
*/
'VariableDeclaration',
'Identifier',
]);
/**
@@ -126,13 +114,10 @@ export function validateSourceLocations(
): Result<void, CompilerError> {
const errors = new CompilerError();
/*
* Step 1: Collect important locations from the original source
* Note: Multiple node types can share the same location (e.g. VariableDeclarator and Identifier)
*/
// Step 1: Collect important locations from the original source
const importantOriginalLocations = new Map<
string,
{loc: t.SourceLocation; nodeTypes: Set<string>}
{loc: t.SourceLocation; nodeType: string}
>();
func.traverse({
@@ -152,31 +137,20 @@ export function validateSourceLocations(
// Collect the location if it exists
if (node.loc) {
const key = locationKey(node.loc);
const existing = importantOriginalLocations.get(key);
if (existing) {
existing.nodeTypes.add(node.type);
} else {
importantOriginalLocations.set(key, {
loc: node.loc,
nodeTypes: new Set([node.type]),
});
}
importantOriginalLocations.set(key, {
loc: node.loc,
nodeType: node.type,
});
}
},
});
// Step 2: Collect all locations from the generated AST with their node types
const generatedLocations = new Map<string, Set<string>>();
// Step 2: Collect all locations from the generated AST
const generatedLocations = new Set<string>();
function collectGeneratedLocations(node: t.Node): void {
if (node.loc) {
const key = locationKey(node.loc);
const nodeTypes = generatedLocations.get(key);
if (nodeTypes) {
nodeTypes.add(node.type);
} else {
generatedLocations.set(key, new Set([node.type]));
}
generatedLocations.add(locationKey(node.loc));
}
// Use Babel's VISITOR_KEYS to traverse only actual node properties
@@ -209,86 +183,22 @@ export function validateSourceLocations(
collectGeneratedLocations(outlined.fn.body);
}
/*
* Step 3: Validate that all important locations are preserved
* For certain node types, also validate that the node type matches
*/
const strictNodeTypes = new Set([
'VariableDeclaration',
'VariableDeclarator',
'Identifier',
]);
const reportMissingLocation = (
loc: t.SourceLocation,
nodeType: string,
): void => {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Todo,
reason: 'Important source location missing in generated code',
description:
`Source location for ${nodeType} is missing in the generated output. This can cause coverage instrumentation ` +
`to fail to track this code properly, resulting in inaccurate coverage reports.`,
}).withDetails({
kind: 'error',
loc,
message: null,
}),
);
};
const reportWrongNodeType = (
loc: t.SourceLocation,
expectedType: string,
actualTypes: Set<string>,
): void => {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Todo,
reason:
'Important source location has wrong node type in generated code',
description:
`Source location for ${expectedType} exists in the generated output but with wrong node type(s): ${Array.from(actualTypes).join(', ')}. ` +
`This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports.`,
}).withDetails({
kind: 'error',
loc,
message: null,
}),
);
};
for (const [key, {loc, nodeTypes}] of importantOriginalLocations) {
const generatedNodeTypes = generatedLocations.get(key);
if (!generatedNodeTypes) {
// Location is completely missing
reportMissingLocation(loc, Array.from(nodeTypes).join(', '));
} else {
// Location exists, check each node type
for (const nodeType of nodeTypes) {
if (
strictNodeTypes.has(nodeType) &&
!generatedNodeTypes.has(nodeType)
) {
/*
* For strict node types, the specific node type must be present
* Check if any generated node type is also an important original node type
*/
const hasValidNodeType = Array.from(generatedNodeTypes).some(
genType => nodeTypes.has(genType),
);
if (hasValidNodeType) {
// At least one generated node type is valid (also in original), so this is just missing
reportMissingLocation(loc, nodeType);
} else {
// None of the generated node types are in original - this is wrong node type
reportWrongNodeType(loc, nodeType, generatedNodeTypes);
}
}
}
// Step 3: Validate that all important locations are preserved
for (const [key, {loc, nodeType}] of importantOriginalLocations) {
if (!generatedLocations.has(key)) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Todo,
reason: 'Important source location missing in generated code',
description:
`Source location for ${nodeType} is missing in the generated output. This can cause coverage instrumentation ` +
`to fail to track this code properly, resulting in inaccurate coverage reports.`,
}).withDetails({
kind: 'error',
loc,
message: null,
}),
);
}
}

View File

@@ -1,51 +0,0 @@
## Input
```javascript
import {useRef} from 'react';
/**
* Allowed: we don't have sufficient type information to be sure that
* this accesses an impure value during render.
*/
function Component() {
const ref = useRef(null);
const object = {};
object.foo = () => ref.current;
const refValue = object.foo();
return <div>{refValue}</div>;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useRef } from "react";
/**
* Allowed: we don't have sufficient type information to be sure that
* this accesses an impure value during render.
*/
function Component() {
const $ = _c(2);
const ref = useRef(null);
const object = {};
object.foo = () => ref.current;
const refValue = object.foo();
let t0;
if ($[0] !== refValue) {
t0 = <div>{refValue}</div>;
$[0] = refValue;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -35,8 +35,10 @@ function Component() {
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const a = makeObject_Primitives();
const x = [];
x.push(a);
mutate(x);
t0 = [x, a];
$[0] = t0;

View File

@@ -33,6 +33,7 @@ function Component() {
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
const x = [];
x.push(a);
t1 = [x, a];
$[1] = t1;
} else {

View File

@@ -85,10 +85,14 @@ function Component(t0) {
let t1;
if ($[0] !== prop) {
const obj = shallowCopy(prop);
const aliasedObj = identity(obj);
const getId = () => obj.id;
mutate(aliasedObj);
setPropertyByKey(aliasedObj, "id", prop.id + 1);
t1 = <Stringify getId={getId} shouldInvokeFns={true} />;
$[0] = prop;
$[1] = t1;

View File

@@ -181,9 +181,12 @@ function Component(t0) {
if ($[0] !== prop) {
const obj = shallowCopy(prop);
const aliasedObj = identity(obj);
const id = [obj.id];
mutate(aliasedObj);
setPropertyByKey(aliasedObj, "id", prop.id + 1);
t1 = <Stringify id={id} />;
$[0] = prop;
$[1] = t1;

View File

@@ -54,6 +54,7 @@ function Foo(t0) {
let t1;
if ($[0] !== cond1 || $[1] !== cond2) {
const arr = makeArray({ a: 2 }, 2, []);
t1 = cond1 ? (
<>
<div>{identity("foo")}</div>

View File

@@ -5,10 +5,6 @@
import {useRef} from 'react';
import {Stringify} from 'shared-runtime';
/**
* Allowed: we don't have sufficient type information to be sure that
* this accesses an impure value during render.
*/
function Component(props) {
const ref = useRef(props.value);
const object = {};
@@ -30,10 +26,6 @@ import { c as _c } from "react/compiler-runtime";
import { useRef } from "react";
import { Stringify } from "shared-runtime";
/**
* Allowed: we don't have sufficient type information to be sure that
* this accesses an impure value during render.
*/
function Component(props) {
const $ = _c(1);
const ref = useRef(props.value);

View File

@@ -1,10 +1,6 @@
import {useRef} from 'react';
import {Stringify} from 'shared-runtime';
/**
* Allowed: we don't have sufficient type information to be sure that
* this accesses an impure value during render.
*/
function Component(props) {
const ref = useRef(props.value);
const object = {};

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
// @enablePreserveExistingMemoizationGuarantees:false
import {useMemo} from 'react';
const someGlobal = {value: 0};
@@ -33,7 +33,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false
import { useMemo } from "react";
const someGlobal = { value: 0 };

View File

@@ -1,4 +1,4 @@
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
// @enablePreserveExistingMemoizationGuarantees:false
import {useMemo} from 'react';
const someGlobal = {value: 0};

View File

@@ -49,6 +49,7 @@ function Component() {
ref.current = "";
}
};
t0 = () => {
setRef();
};

View File

@@ -49,6 +49,7 @@ function Component() {
ref.current.value = "";
}
};
t0 = () => {
setRef();
};

View File

@@ -74,6 +74,7 @@ function Component() {
console.log(ref.current.value);
}
};
t0 = (
<>
<input ref={ref} />

View File

@@ -36,6 +36,7 @@ function useArrayOfRef() {
const callback = (value) => {
ref.current = value;
};
t0 = [callback];
$[0] = t0;
} else {

View File

@@ -35,6 +35,7 @@ function Component(props) {
const arr = [...bar(props)];
return arr.at(x);
};
t1 = fn();
$[2] = props;
$[3] = x;

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
// @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
@@ -36,7 +36,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
import { useMemo } from "react";
import { makeObject_Primitives, ValidateMemoization } from "shared-runtime";

View File

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

View File

@@ -61,6 +61,7 @@ function useBar(t0) {
if ($[0] !== arg) {
const s = new Set([1, 5, 4]);
const mutableIterator = s.values();
t1 = [arg, ...mutableIterator];
$[0] = arg;
$[1] = t1;

View File

@@ -2,7 +2,6 @@
## Input
```javascript
// @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
function Component(props) {
@@ -31,7 +30,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies:false
import { c as _c } from "react/compiler-runtime";
import { useMemo } from "react";
function Component(props) {

View File

@@ -1,4 +1,3 @@
// @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
function Component(props) {

View File

@@ -28,6 +28,7 @@ function Component(props) {
const a = [];
const b = {};
foo(a, b);
foo(b);
t0 = <div a={a} b={b} />;
$[0] = t0;

View File

@@ -45,6 +45,7 @@ function useKeyCommand() {
const nextPosition = direction === "left" ? addOne(position) : position;
currentPosition.current = nextPosition;
};
const moveLeft = { handler: handleKey("left") };
const moveRight = { handler: handleKey("right") };
t0 = [moveLeft, moveRight];

View File

@@ -1,74 +0,0 @@
## Input
```javascript
import {useRef} from 'react';
import {addOne} from 'shared-runtime';
/**
* Allowed: we don't have sufficient type information to be sure that
* this accesses an impure value during render.
*/
function useKeyCommand() {
const currentPosition = useRef(0);
const handleKey = direction => () => {
const position = currentPosition.current;
const nextPosition = direction === 'left' ? addOne(position) : position;
currentPosition.current = nextPosition;
};
const moveLeft = {
handler: handleKey('left')(),
};
const moveRight = {
handler: handleKey('right')(),
};
return [moveLeft, moveRight];
}
export const FIXTURE_ENTRYPOINT = {
fn: useKeyCommand,
params: [],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useRef } from "react";
import { addOne } from "shared-runtime";
/**
* Allowed: we don't have sufficient type information to be sure that
* this accesses an impure value during render.
*/
function useKeyCommand() {
const $ = _c(1);
const currentPosition = useRef(0);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const handleKey = (direction) => () => {
const position = currentPosition.current;
const nextPosition = direction === "left" ? addOne(position) : position;
currentPosition.current = nextPosition;
};
const moveLeft = { handler: handleKey("left")() };
const moveRight = { handler: handleKey("right")() };
t0 = [moveLeft, moveRight];
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: useKeyCommand,
params: [],
};
```
### Eval output
(kind: ok) [{},{}]

View File

@@ -45,6 +45,7 @@ function Component(t0) {
z.a = 2;
mutate(y.b);
};
x();
t1 = [y, z];
$[0] = a;

View File

@@ -29,6 +29,7 @@ function MyComponentName(props) {
const x = {};
foo(x, props.a);
foo(x, props.b);
y = [];
y.push(x);
$[0] = props.a;

View File

@@ -34,6 +34,7 @@ function useTest() {
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
let w = {};
const t1 = (w = 42);
const t2 = w;

View File

@@ -34,6 +34,7 @@ function useTest() {
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const w = {};
const t1 = (w.x = 42);
const t2 = w.x;

View File

@@ -44,9 +44,11 @@ function ComponentA(props) {
if (b) {
a.push(props.p0);
}
if (props.p1) {
b.push(props.p2);
}
t0 = <Foo a={a} b={b} />;
$[0] = props.p0;
$[1] = props.p1;
@@ -67,9 +69,11 @@ function ComponentB(props) {
if (mayMutate(b)) {
a.push(props.p0);
}
if (props.p1) {
b.push(props.p2);
}
t0 = <Foo a={a} b={b} />;
$[0] = props.p0;
$[1] = props.p1;

View File

@@ -28,6 +28,7 @@ function Component(props) {
const a = [];
const b = {};
new Foo(a, b);
new Foo(b);
t0 = <div a={a} b={b} />;
$[0] = t0;

View File

@@ -11,7 +11,7 @@ function Component(props) {
Component = useMemo(() => {
return Component;
}, [Component]);
});
return <Component {...props} />;
}
@@ -36,7 +36,6 @@ function Component(props) {
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
Component = Stringify;
Component;
Component = Component;
$[0] = Component;
} else {

View File

@@ -7,7 +7,7 @@ function Component(props) {
Component = useMemo(() => {
return Component;
}, [Component]);
});
return <Component {...props} />;
}

View File

@@ -34,6 +34,7 @@ function Component(props) {
const callback = () => {
console.log(x);
};
x = {};
t0 = <Stringify callback={callback} shouldInvokeFns={true} />;
$[0] = t0;

View File

@@ -75,6 +75,7 @@ function Component(props) {
let t0;
if ($[0] !== post) {
const allUrls = [];
const { media: t1, comments: t2, urls: t3 } = post;
const media = t1 === undefined ? null : t1;
let t4;
@@ -101,6 +102,7 @@ function Component(props) {
if (!comments.length) {
return;
}
console.log(comments.length);
};
$[6] = comments.length;
@@ -109,6 +111,7 @@ function Component(props) {
t6 = $[7];
}
const onClick = t6;
allUrls.push(...urls);
t0 = <Stringify media={media} allUrls={allUrls} onClick={onClick} />;
$[0] = post;

View File

@@ -53,6 +53,7 @@ function Component(props) {
let t0;
if ($[0] !== post) {
const allUrls = [];
const { media, comments, urls } = post;
let t1;
if ($[2] !== comments.length) {
@@ -60,6 +61,7 @@ function Component(props) {
if (!comments.length) {
return;
}
console.log(comments.length);
};
$[2] = comments.length;
@@ -68,6 +70,7 @@ function Component(props) {
t1 = $[3];
}
const onClick = t1;
allUrls.push(...urls);
t0 = <Media media={media} onClick={onClick} />;
$[0] = post;

View File

@@ -57,6 +57,7 @@ function Component(t0) {
let y;
if ($[0] !== a || $[1] !== b || $[2] !== c) {
x = [];
if (a) {
let t1;
if ($[5] !== b) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({value, enabled}) {
@@ -29,21 +29,43 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import { useEffect, useState } from "react";
function Component({ value, enabled }) {
function Component(t0) {
const $ = _c(6);
const { value, enabled } = t0;
const [localValue, setLocalValue] = useState("");
let t1;
let t2;
if ($[0] !== enabled || $[1] !== value) {
t1 = () => {
if (enabled) {
setLocalValue(value);
} else {
setLocalValue("disabled");
}
};
useEffect(() => {
if (enabled) {
setLocalValue(value);
} else {
setLocalValue("disabled");
}
}, [value, enabled]);
return <div>{localValue}</div>;
t2 = [value, enabled];
$[0] = enabled;
$[1] = value;
$[2] = t1;
$[3] = t2;
} else {
t1 = $[2];
t2 = $[3];
}
useEffect(t1, t2);
let t3;
if ($[4] !== localValue) {
t3 = <div>{localValue}</div>;
$[4] = localValue;
$[5] = t3;
} else {
t3 = $[5];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
@@ -56,8 +78,8 @@ export const FIXTURE_ENTRYPOINT = {
## Logs
```
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [value]\n\nData Flow Tree:\n└── value (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":9,"column":6,"index":263},"end":{"line":9,"column":19,"index":276},"filename":"derived-state-conditionally-in-effect.ts","identifierName":"setLocalValue"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":126},"end":{"line":16,"column":1,"index":397},"filename":"derived-state-conditionally-in-effect.ts"},"fnName":"Component","memoSlots":6,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [value]\n\nData Flow Tree:\n└── value (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":9,"column":6,"index":244},"end":{"line":9,"column":19,"index":257},"filename":"derived-state-conditionally-in-effect.ts","identifierName":"setLocalValue"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":16,"column":1,"index":378},"filename":"derived-state-conditionally-in-effect.ts"},"fnName":"Component","memoSlots":6,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({value, enabled}) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
export default function Component({input = 'empty'}) {
@@ -26,18 +26,38 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import { useEffect, useState } from "react";
export default function Component({ input = "empty" }) {
export default function Component(t0) {
const $ = _c(5);
const { input: t1 } = t0;
const input = t1 === undefined ? "empty" : t1;
const [currInput, setCurrInput] = useState(input);
const localConst = "local const";
useEffect(() => {
setCurrInput(input + localConst);
}, [input, localConst]);
return <div>{currInput}</div>;
let t2;
let t3;
if ($[0] !== input) {
t2 = () => {
setCurrInput(input + "local const");
};
t3 = [input, "local const"];
$[0] = input;
$[1] = t2;
$[2] = t3;
} else {
t2 = $[1];
t3 = $[2];
}
useEffect(t2, t3);
let t4;
if ($[3] !== currInput) {
t4 = <div>{currInput}</div>;
$[3] = currInput;
$[4] = t4;
} else {
t4 = $[4];
}
return t4;
}
export const FIXTURE_ENTRYPOINT = {
@@ -50,8 +70,8 @@ export const FIXTURE_ENTRYPOINT = {
## Logs
```
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [input]\n\nData Flow Tree:\n└── input (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":9,"column":4,"index":295},"end":{"line":9,"column":16,"index":307},"filename":"derived-state-from-default-props.ts","identifierName":"setCurrInput"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":15,"index":141},"end":{"line":13,"column":1,"index":391},"filename":"derived-state-from-default-props.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [input]\n\nData Flow Tree:\n└── input (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":9,"column":4,"index":276},"end":{"line":9,"column":16,"index":288},"filename":"derived-state-from-default-props.ts","identifierName":"setCurrInput"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":15,"index":122},"end":{"line":13,"column":1,"index":372},"filename":"derived-state-from-default-props.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
export default function Component({input = 'empty'}) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
@@ -23,20 +23,45 @@ function Component({shouldChange}) {
## Code
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import { useEffect, useState } from "react";
function Component({ shouldChange }) {
function Component(t0) {
const $ = _c(7);
const { shouldChange } = t0;
const [count, setCount] = useState(0);
useEffect(() => {
if (shouldChange) {
setCount(count + 1);
}
}, [count]);
return <div>{count}</div>;
let t1;
if ($[0] !== count || $[1] !== shouldChange) {
t1 = () => {
if (shouldChange) {
setCount(count + 1);
}
};
$[0] = count;
$[1] = shouldChange;
$[2] = t1;
} else {
t1 = $[2];
}
let t2;
if ($[3] !== count) {
t2 = [count];
$[3] = count;
$[4] = t2;
} else {
t2 = $[4];
}
useEffect(t1, t2);
let t3;
if ($[5] !== count) {
t3 = <div>{count}</div>;
$[5] = count;
$[6] = t3;
} else {
t3 = $[6];
}
return t3;
}
```
@@ -44,8 +69,8 @@ function Component({ shouldChange }) {
## Logs
```
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nState: [count]\n\nData Flow Tree:\n└── count (State)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":10,"column":6,"index":256},"end":{"line":10,"column":14,"index":264},"filename":"derived-state-from-local-state-in-effect.ts","identifierName":"setCount"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":127},"end":{"line":15,"column":1,"index":329},"filename":"derived-state-from-local-state-in-effect.ts"},"fnName":"Component","memoSlots":7,"memoBlocks":3,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nState: [count]\n\nData Flow Tree:\n└── count (State)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":10,"column":6,"index":237},"end":{"line":10,"column":14,"index":245},"filename":"derived-state-from-local-state-in-effect.ts","identifierName":"setCount"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":108},"end":{"line":15,"column":1,"index":310},"filename":"derived-state-from-local-state-in-effect.ts"},"fnName":"Component","memoSlots":7,"memoBlocks":3,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({firstName}) {
@@ -33,25 +33,68 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import { useEffect, useState } from "react";
function Component({ firstName }) {
function Component(t0) {
const $ = _c(12);
const { firstName } = t0;
const [lastName, setLastName] = useState("Doe");
const [fullName, setFullName] = useState("John");
const middleName = "D.";
useEffect(() => {
setFullName(firstName + " " + middleName + " " + lastName);
}, [firstName, middleName, lastName]);
return (
<div>
<input value={lastName} onChange={(e) => setLastName(e.target.value)} />
<div>{fullName}</div>
</div>
);
let t1;
let t2;
if ($[0] !== firstName || $[1] !== lastName) {
t1 = () => {
setFullName(firstName + " " + "D." + " " + lastName);
};
t2 = [firstName, "D.", lastName];
$[0] = firstName;
$[1] = lastName;
$[2] = t1;
$[3] = t2;
} else {
t1 = $[2];
t2 = $[3];
}
useEffect(t1, t2);
let t3;
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
t3 = (e) => setLastName(e.target.value);
$[4] = t3;
} else {
t3 = $[4];
}
let t4;
if ($[5] !== lastName) {
t4 = <input value={lastName} onChange={t3} />;
$[5] = lastName;
$[6] = t4;
} else {
t4 = $[6];
}
let t5;
if ($[7] !== fullName) {
t5 = <div>{fullName}</div>;
$[7] = fullName;
$[8] = t5;
} else {
t5 = $[8];
}
let t6;
if ($[9] !== t4 || $[10] !== t5) {
t6 = (
<div>
{t4}
{t5}
</div>
);
$[9] = t4;
$[10] = t5;
$[11] = t6;
} else {
t6 = $[11];
}
return t6;
}
export const FIXTURE_ENTRYPOINT = {
@@ -64,8 +107,8 @@ export const FIXTURE_ENTRYPOINT = {
## Logs
```
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [firstName]\nState: [lastName]\n\nData Flow Tree:\n├── firstName (Prop)\n└── lastName (State)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":11,"column":4,"index":316},"end":{"line":11,"column":15,"index":327},"filename":"derived-state-from-prop-local-state-and-component-scope.ts","identifierName":"setFullName"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":126},"end":{"line":20,"column":1,"index":561},"filename":"derived-state-from-prop-local-state-and-component-scope.ts"},"fnName":"Component","memoSlots":12,"memoBlocks":5,"memoValues":6,"prunedMemoBlocks":0,"prunedMemoValues":0}
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [firstName]\nState: [lastName]\n\nData Flow Tree:\n├── firstName (Prop)\n└── lastName (State)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":11,"column":4,"index":297},"end":{"line":11,"column":15,"index":308},"filename":"derived-state-from-prop-local-state-and-component-scope.ts","identifierName":"setFullName"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":20,"column":1,"index":542},"filename":"derived-state-from-prop-local-state-and-component-scope.ts"},"fnName":"Component","memoSlots":12,"memoBlocks":5,"memoValues":6,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({firstName}) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({initialName}) {
@@ -29,21 +29,48 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import { useEffect, useState } from "react";
function Component({ initialName }) {
function Component(t0) {
const $ = _c(6);
const { initialName } = t0;
const [name, setName] = useState("");
useEffect(() => {
setName(initialName);
}, [initialName]);
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
</div>
);
let t1;
let t2;
if ($[0] !== initialName) {
t1 = () => {
setName(initialName);
};
t2 = [initialName];
$[0] = initialName;
$[1] = t1;
$[2] = t2;
} else {
t1 = $[1];
t2 = $[2];
}
useEffect(t1, t2);
let t3;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t3 = (e) => setName(e.target.value);
$[3] = t3;
} else {
t3 = $[3];
}
let t4;
if ($[4] !== name) {
t4 = (
<div>
<input value={name} onChange={t3} />
</div>
);
$[4] = name;
$[5] = t4;
} else {
t4 = $[5];
}
return t4;
}
export const FIXTURE_ENTRYPOINT = {
@@ -56,7 +83,7 @@ export const FIXTURE_ENTRYPOINT = {
## Logs
```
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":126},"end":{"line":16,"column":1,"index":378},"filename":"derived-state-from-prop-setter-call-outside-effect-no-error.ts"},"fnName":"Component","memoSlots":6,"memoBlocks":3,"memoValues":4,"prunedMemoBlocks":0,"prunedMemoValues":0}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":16,"column":1,"index":359},"filename":"derived-state-from-prop-setter-call-outside-effect-no-error.ts"},"fnName":"Component","memoSlots":6,"memoBlocks":3,"memoValues":4,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({initialName}) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @outputMode:"lint"
// @validateNoDerivedComputationsInEffects_exp
function Component({value}) {
const [checked, setChecked] = useState('');
@@ -19,16 +19,36 @@ function Component({value}) {
## Code
```javascript
// @validateNoDerivedComputationsInEffects_exp @outputMode:"lint"
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
function Component({ value }) {
function Component(t0) {
const $ = _c(5);
const { value } = t0;
const [checked, setChecked] = useState("");
useEffect(() => {
setChecked(value === "" ? [] : value.split(","));
}, [value]);
return <div>{checked}</div>;
let t1;
let t2;
if ($[0] !== value) {
t1 = () => {
setChecked(value === "" ? [] : value.split(","));
};
t2 = [value];
$[0] = value;
$[1] = t1;
$[2] = t2;
} else {
t1 = $[1];
t2 = $[2];
}
useEffect(t1, t2);
let t3;
if ($[3] !== checked) {
t3 = <div>{checked}</div>;
$[3] = checked;
$[4] = t3;
} else {
t3 = $[4];
}
return t3;
}
```

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp @outputMode:"lint"
// @validateNoDerivedComputationsInEffects_exp
function Component({value}) {
const [checked, setChecked] = useState('');

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function MockComponent({onSet}) {
@@ -28,20 +28,50 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import { useEffect, useState } from "react";
function MockComponent({ onSet }) {
return <div onClick={() => onSet("clicked")}>Mock Component</div>;
function MockComponent(t0) {
const $ = _c(2);
const { onSet } = t0;
let t1;
if ($[0] !== onSet) {
t1 = <div onClick={() => onSet("clicked")}>Mock Component</div>;
$[0] = onSet;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
function Component({ propValue }) {
const [value, setValue] = useState(null);
useEffect(() => {
setValue(propValue);
}, [propValue]);
return <MockComponent onSet={setValue} />;
function Component(t0) {
const $ = _c(4);
const { propValue } = t0;
const [, setValue] = useState(null);
let t1;
let t2;
if ($[0] !== propValue) {
t1 = () => {
setValue(propValue);
};
t2 = [propValue];
$[0] = propValue;
$[1] = t1;
$[2] = t2;
} else {
t1 = $[1];
t2 = $[2];
}
useEffect(t1, t2);
let t3;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t3 = <MockComponent onSet={setValue} />;
$[3] = t3;
} else {
t3 = $[3];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
@@ -54,8 +84,8 @@ export const FIXTURE_ENTRYPOINT = {
## Logs
```
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":126},"end":{"line":6,"column":1,"index":230},"filename":"derived-state-from-prop-setter-used-outside-effect-no-error.ts"},"fnName":"MockComponent","memoSlots":2,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":8,"column":0,"index":232},"end":{"line":15,"column":1,"index":421},"filename":"derived-state-from-prop-setter-used-outside-effect-no-error.ts"},"fnName":"Component","memoSlots":4,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":6,"column":1,"index":211},"filename":"derived-state-from-prop-setter-used-outside-effect-no-error.ts"},"fnName":"MockComponent","memoSlots":2,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":8,"column":0,"index":213},"end":{"line":15,"column":1,"index":402},"filename":"derived-state-from-prop-setter-used-outside-effect-no-error.ts"},"fnName":"Component","memoSlots":4,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function MockComponent({onSet}) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({value}) {
@@ -26,18 +26,38 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import { useEffect, useState } from "react";
function Component({ value }) {
function Component(t0) {
const $ = _c(5);
const { value } = t0;
const [localValue, setLocalValue] = useState("");
useEffect(() => {
setLocalValue(value);
document.title = `Value: ${value}`;
}, [value]);
return <div>{localValue}</div>;
let t1;
let t2;
if ($[0] !== value) {
t1 = () => {
setLocalValue(value);
document.title = `Value: ${value}`;
};
t2 = [value];
$[0] = value;
$[1] = t1;
$[2] = t2;
} else {
t1 = $[1];
t2 = $[2];
}
useEffect(t1, t2);
let t3;
if ($[3] !== localValue) {
t3 = <div>{localValue}</div>;
$[3] = localValue;
$[4] = t3;
} else {
t3 = $[4];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
@@ -50,8 +70,8 @@ export const FIXTURE_ENTRYPOINT = {
## Logs
```
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [value]\n\nData Flow Tree:\n└── value (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":8,"column":4,"index":233},"end":{"line":8,"column":17,"index":246},"filename":"derived-state-from-prop-with-side-effect.ts","identifierName":"setLocalValue"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":126},"end":{"line":13,"column":1,"index":346},"filename":"derived-state-from-prop-with-side-effect.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [value]\n\nData Flow Tree:\n└── value (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":8,"column":4,"index":214},"end":{"line":8,"column":17,"index":227},"filename":"derived-state-from-prop-with-side-effect.ts","identifierName":"setLocalValue"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":13,"column":1,"index":327},"filename":"derived-state-from-prop-with-side-effect.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({value}) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState, useRef} from 'react';
export default function Component({test}) {
@@ -27,19 +27,39 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import { useEffect, useState, useRef } from "react";
export default function Component({ test }) {
export default function Component(t0) {
const $ = _c(5);
const { test } = t0;
const [local, setLocal] = useState("");
const myRef = useRef(null);
useEffect(() => {
setLocal(myRef.current + test);
}, [test]);
return <>{local}</>;
let t1;
let t2;
if ($[0] !== test) {
t1 = () => {
setLocal(myRef.current + test);
};
t2 = [test];
$[0] = test;
$[1] = t1;
$[2] = t2;
} else {
t1 = $[1];
t2 = $[2];
}
useEffect(t1, t2);
let t3;
if ($[3] !== local) {
t3 = <>{local}</>;
$[3] = local;
$[4] = t3;
} else {
t3 = $[4];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
@@ -52,7 +72,7 @@ export const FIXTURE_ENTRYPOINT = {
## Logs
```
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":15,"index":149},"end":{"line":14,"column":1,"index":347},"filename":"derived-state-from-ref-and-state-no-error.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":15,"index":130},"end":{"line":14,"column":1,"index":328},"filename":"derived-state-from-ref-and-state-no-error.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState, useRef} from 'react';
export default function Component({test}) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({propValue}) {
@@ -30,22 +30,48 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import { useEffect, useState } from "react";
function Component({ propValue }) {
function Component(t0) {
const $ = _c(6);
const { propValue } = t0;
const [value, setValue] = useState(null);
function localFunction() {
console.log("local function");
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = function localFunction() {
console.log("local function");
};
$[0] = t1;
} else {
t1 = $[0];
}
useEffect(() => {
setValue(propValue);
localFunction();
}, [propValue]);
return <div>{value}</div>;
const localFunction = t1;
let t2;
let t3;
if ($[1] !== propValue) {
t2 = () => {
setValue(propValue);
localFunction();
};
t3 = [propValue];
$[1] = propValue;
$[2] = t2;
$[3] = t3;
} else {
t2 = $[2];
t3 = $[3];
}
useEffect(t2, t3);
let t4;
if ($[4] !== value) {
t4 = <div>{value}</div>;
$[4] = value;
$[5] = t4;
} else {
t4 = $[5];
}
return t4;
}
export const FIXTURE_ENTRYPOINT = {
@@ -58,8 +84,8 @@ export const FIXTURE_ENTRYPOINT = {
## Logs
```
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [propValue]\n\nData Flow Tree:\n└── propValue (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":12,"column":4,"index":298},"end":{"line":12,"column":12,"index":306},"filename":"effect-contains-local-function-call.ts","identifierName":"setValue"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":126},"end":{"line":17,"column":1,"index":390},"filename":"effect-contains-local-function-call.ts"},"fnName":"Component","memoSlots":6,"memoBlocks":3,"memoValues":4,"prunedMemoBlocks":0,"prunedMemoValues":0}
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [propValue]\n\nData Flow Tree:\n└── propValue (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":12,"column":4,"index":279},"end":{"line":12,"column":12,"index":287},"filename":"effect-contains-local-function-call.ts","identifierName":"setValue"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":17,"column":1,"index":371},"filename":"effect-contains-local-function-call.ts"},"fnName":"Component","memoSlots":6,"memoBlocks":3,"memoValues":4,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({propValue}) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({propValue, onChange}) {
@@ -25,17 +25,43 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import { useEffect, useState } from "react";
function Component({ propValue, onChange }) {
function Component(t0) {
const $ = _c(7);
const { propValue, onChange } = t0;
const [value, setValue] = useState(null);
useEffect(() => {
setValue(propValue);
onChange();
}, [propValue]);
return <div>{value}</div>;
let t1;
if ($[0] !== onChange || $[1] !== propValue) {
t1 = () => {
setValue(propValue);
onChange();
};
$[0] = onChange;
$[1] = propValue;
$[2] = t1;
} else {
t1 = $[2];
}
let t2;
if ($[3] !== propValue) {
t2 = [propValue];
$[3] = propValue;
$[4] = t2;
} else {
t2 = $[4];
}
useEffect(t1, t2);
let t3;
if ($[5] !== value) {
t3 = <div>{value}</div>;
$[5] = value;
$[6] = t3;
} else {
t3 = $[6];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
@@ -48,8 +74,8 @@ export const FIXTURE_ENTRYPOINT = {
## Logs
```
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":126},"end":{"line":12,"column":1,"index":325},"filename":"effect-contains-prop-function-call-no-error.ts"},"fnName":"Component","memoSlots":7,"memoBlocks":3,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":16,"column":41,"index":421},"end":{"line":16,"column":49,"index":429},"filename":"effect-contains-prop-function-call-no-error.ts"},"fnName":null,"memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":12,"column":1,"index":306},"filename":"effect-contains-prop-function-call-no-error.ts"},"fnName":"Component","memoSlots":7,"memoBlocks":3,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":16,"column":41,"index":402},"end":{"line":16,"column":49,"index":410},"filename":"effect-contains-prop-function-call-no-error.ts"},"fnName":null,"memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({propValue, onChange}) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
function Component({prop}) {
const [s, setS] = useState(0);
@@ -18,15 +18,36 @@ function Component({prop}) {
## Code
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
function Component({ prop }) {
const [s, setS] = useState(0);
useEffect(() => {
setS(prop);
}, [prop, setS]);
return <div>{prop}</div>;
function Component(t0) {
const $ = _c(5);
const { prop } = t0;
const [, setS] = useState(0);
let t1;
let t2;
if ($[0] !== prop) {
t1 = () => {
setS(prop);
};
t2 = [prop, setS];
$[0] = prop;
$[1] = t1;
$[2] = t2;
} else {
t1 = $[1];
t2 = $[2];
}
useEffect(t1, t2);
let t3;
if ($[3] !== prop) {
t3 = <div>{prop}</div>;
$[3] = prop;
$[4] = t3;
} else {
t3 = $[4];
}
return t3;
}
```
@@ -34,8 +55,8 @@ function Component({ prop }) {
## Logs
```
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [prop]\n\nData Flow Tree:\n└── prop (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":6,"column":4,"index":169},"end":{"line":6,"column":8,"index":173},"filename":"effect-used-in-dep-array-still-errors.ts","identifierName":"setS"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":3,"column":0,"index":83},"end":{"line":10,"column":1,"index":231},"filename":"effect-used-in-dep-array-still-errors.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [prop]\n\nData Flow Tree:\n└── prop (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":6,"column":4,"index":150},"end":{"line":6,"column":8,"index":154},"filename":"effect-used-in-dep-array-still-errors.ts","identifierName":"setS"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":3,"column":0,"index":64},"end":{"line":10,"column":1,"index":212},"filename":"effect-used-in-dep-array-still-errors.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output

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