Compare commits

..

2 Commits

Author SHA1 Message Date
Joe Savona
758301f70f [compiler][rfc] Switch eslint-plugin-react-compiler to single rule for Meta compat
Now that the official way to use React Compiler's linting is via `eslint-plugin-react-hooks` v7, eslint-plugin-react-compiler isn't strictly necessary anymore. However, at Meta our linting setup makes it a bit tedious to use the current eprh setup with lots of rules. Here I'm experimenting with making eslint-plugin-react-compiler just report all issues under one rule, to make it a bit easier to sync internally.

Unclear if we even need to land this or just use it to help figure out the migration.
2025-10-17 16:54:20 -07:00
Joe Savona
48b52d896e [compiler] Fix false positive for useMemo reassigning context vars
Within a function expresssion local variables use StoreContext, not StoreLocal, so the reassignment check here was firing too often. We should only report an error for variables that are declared outside the function, ie part of its `context`.
2025-10-17 16:54:19 -07:00
249 changed files with 5646 additions and 9069 deletions

View File

@@ -11,7 +11,7 @@ body:
options:
- label: React Compiler core (the JS output is incorrect, or your app works incorrectly after optimization)
- label: babel-plugin-react-compiler (build issue installing or using the Babel plugin)
- label: eslint-plugin-react-hooks (build issue installing or using the eslint plugin)
- label: eslint-plugin-react-compiler (build issue installing or using the eslint plugin)
- label: react-compiler-healthcheck (build issue installing or using the healthcheck script)
- type: input
attributes:

View File

@@ -162,13 +162,10 @@ jobs:
mv build/facebook-react-native/react-is/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-is/
mv build/facebook-react-native/react-test-renderer/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-test-renderer/
# Delete the OSS renderers, these are sync'd to RN separately.
# Delete OSS renderer. OSS renderer is synced through internal script.
RENDERER_FOLDER=$BASE_FOLDER/react-native-github/Libraries/Renderer/implementations/
rm $RENDERER_FOLDER/ReactFabric-{dev,prod,profiling}.js
# Delete the legacy renderer shim, this is not sync'd and will get deleted in the future.
SHIM_FOLDER=$BASE_FOLDER/react-native-github/Libraries/Renderer/shims/
rm $SHIM_FOLDER/ReactNative.js
rm $RENDERER_FOLDER/ReactNativeRenderer-{dev,prod,profiling}.js
# Copy eslint-plugin-react-hooks
# NOTE: This is different from www, here we include the full package

View File

@@ -33,7 +33,7 @@ const canaryChannelLabel = 'canary';
const rcNumber = 0;
const stablePackages = {
'eslint-plugin-react-hooks': '7.1.0',
'eslint-plugin-react-hooks': '7.0.0',
'jest-react': '0.18.0',
react: ReactVersion,
'react-art': ReactVersion,

View File

@@ -14,7 +14,6 @@ import React, {
unstable_ViewTransition as ViewTransition,
unstable_addTransitionType as addTransitionType,
startTransition,
Activity,
} from 'react';
import {Resizable} from 're-resizable';
import {useStore, useStoreDispatch} from '../StoreContext';
@@ -35,8 +34,12 @@ export default function ConfigEditor({
const [isExpanded, setIsExpanded] = useState(false);
return (
// TODO: Use <Activity> when it is compatible with Monaco: https://github.com/suren-atoyan/monaco-react/issues/753
<>
<Activity mode={isExpanded ? 'visible' : 'hidden'}>
<div
style={{
display: isExpanded ? 'block' : 'none',
}}>
<ExpandedEditor
onToggle={() => {
startTransition(() => {
@@ -46,8 +49,11 @@ export default function ConfigEditor({
}}
formattedAppliedConfig={formattedAppliedConfig}
/>
</Activity>
<Activity mode={isExpanded ? 'hidden' : 'visible'}>
</div>
<div
style={{
display: !isExpanded ? 'block' : 'none',
}}>
<CollapsedEditor
onToggle={() => {
startTransition(() => {
@@ -56,7 +62,7 @@ export default function ConfigEditor({
});
}}
/>
</Activity>
</div>
</>
);
}
@@ -116,8 +122,7 @@ function ExpandedEditor({
return (
<ViewTransition
enter={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}
exit={{[CONFIG_PANEL_TRANSITION]: 'slide-out', default: 'none'}}>
update={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}>
<Resizable
minWidth={300}
maxWidth={600}

View File

@@ -26,8 +26,8 @@
"@babel/traverse": "^7.18.9",
"@babel/types": "7.26.3",
"@heroicons/react": "^1.0.6",
"@monaco-editor/react": "^4.8.0-rc.2",
"@playwright/test": "^1.56.1",
"@monaco-editor/react": "^4.4.6",
"@playwright/test": "^1.51.1",
"@use-gesture/react": "^10.2.22",
"hermes-eslint": "^0.25.0",
"hermes-parser": "^0.25.0",
@@ -40,13 +40,13 @@
"prettier": "^3.3.3",
"pretty-format": "^29.3.1",
"re-resizable": "^6.9.16",
"react": "19.2",
"react-dom": "19.2"
"react": "19.1.1",
"react-dom": "19.1.1"
},
"devDependencies": {
"@types/node": "18.11.9",
"@types/react": "19.2",
"@types/react-dom": "19.2",
"@types/react": "19.1.13",
"@types/react-dom": "19.1.9",
"autoprefixer": "^10.4.13",
"clsx": "^1.2.1",
"concurrently": "^7.4.0",
@@ -58,7 +58,7 @@
"wait-on": "^7.2.0"
},
"resolutions": {
"@types/react": "19.2",
"@types/react-dom": "19.2"
"@types/react": "19.1.12",
"@types/react-dom": "19.1.9"
}
}

View File

@@ -79,15 +79,6 @@
::view-transition-group(.slide-in) {
z-index: 1;
}
::view-transition-old(.slide-out) {
animation-name: slideOutLeft;
}
::view-transition-new(.slide-out) {
animation-name: slideInLeft;
}
::view-transition-group(.slide-out) {
z-index: 1;
}
@keyframes slideOutLeft {
from {

View File

@@ -701,19 +701,19 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@monaco-editor/loader@^1.6.1":
version "1.6.1"
resolved "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.6.1.tgz#c99177d87765abf10de31a0086084e714acfbc0f"
integrity sha512-w3tEnj9HYEC73wtjdpR089AqkUPskFRcdkxsiSFt3SoUc3OHpmu+leP94CXBm4mHfefmhsdfI0ZQu6qJ0wgtPg==
"@monaco-editor/loader@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.4.0.tgz#f08227057331ec890fa1e903912a5b711a2ad558"
integrity sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==
dependencies:
state-local "^1.0.6"
"@monaco-editor/react@^4.8.0-rc.2":
version "4.8.0-rc.2"
resolved "https://registry.npmjs.org/@monaco-editor/react/-/react-4.8.0-rc.2.tgz#e9acf652e23e9f640671a69875f496dde7f098aa"
integrity sha512-RzFHKBCnRA4RnozaG/EPhKsbkhX5wcApSa5MElR/AD2ojxhMY+QP+G8aJpxALCnIwKs6L0dec5MJ0nAjMUEqnA==
"@monaco-editor/react@^4.4.6":
version "4.6.0"
resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.6.0.tgz#bcc68671e358a21c3814566b865a54b191e24119"
integrity sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==
dependencies:
"@monaco-editor/loader" "^1.6.1"
"@monaco-editor/loader" "^1.4.0"
"@next/env@15.6.0-canary.7":
version "15.6.0-canary.7"
@@ -798,12 +798,12 @@
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@playwright/test@^1.56.1":
version "1.56.1"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.56.1.tgz#6e3bf3d0c90c5cf94bf64bdb56fd15a805c8bd3f"
integrity sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==
"@playwright/test@^1.51.1":
version "1.51.1"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.51.1.tgz#75357d513221a7be0baad75f01e966baf9c41a2e"
integrity sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q==
dependencies:
playwright "1.56.1"
playwright "1.51.1"
"@rtsao/scc@^1.1.0":
version "1.1.0"
@@ -854,15 +854,22 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4"
integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==
"@types/react-dom@19.2":
version "19.2.2"
resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz#a4cc874797b7ddc9cb180ef0d5dc23f596fc2332"
integrity sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==
"@types/react-dom@19.1.9":
version "19.1.9"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.1.9.tgz#5ab695fce1e804184767932365ae6569c11b4b4b"
integrity sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==
"@types/react@19.2":
version "19.2.2"
resolved "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz#ba123a75d4c2a51158697160a4ea2ff70aa6bf36"
integrity sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==
"@types/react@19.1.12":
version "19.1.12"
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.12.tgz#7bfaa76aabbb0b4fe0493c21a3a7a93d33e8937b"
integrity sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==
dependencies:
csstype "^3.0.2"
"@types/react@19.1.13":
version "19.1.13"
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.13.tgz#fc650ffa680d739a25a530f5d7ebe00cdd771883"
integrity sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==
dependencies:
csstype "^3.0.2"
@@ -3453,17 +3460,17 @@ pirates@^4.0.1:
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9"
integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==
playwright-core@1.56.1:
version "1.56.1"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.56.1.tgz#24a66481e5cd33a045632230aa2c4f0cb6b1db3d"
integrity sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==
playwright-core@1.51.1:
version "1.51.1"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.51.1.tgz#d57f0393e02416f32a47cf82b27533656a8acce1"
integrity sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==
playwright@1.56.1:
version "1.56.1"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.56.1.tgz#62e3b99ddebed0d475e5936a152c88e68be55fbf"
integrity sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==
playwright@1.51.1:
version "1.51.1"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.51.1.tgz#ae1467ee318083968ad28d6990db59f47a55390f"
integrity sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==
dependencies:
playwright-core "1.56.1"
playwright-core "1.51.1"
optionalDependencies:
fsevents "2.3.2"
@@ -3582,12 +3589,12 @@ 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:
version "19.2.0"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz#00ed1e959c365e9a9d48f8918377465466ec3af8"
integrity sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==
react-dom@19.1.1:
version "19.1.1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.1.1.tgz#2daa9ff7f3ae384aeb30e76d5ee38c046dc89893"
integrity sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==
dependencies:
scheduler "^0.27.0"
scheduler "^0.26.0"
react-is@^16.13.1:
version "16.13.1"
@@ -3599,10 +3606,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:
version "19.2.0"
resolved "https://registry.npmjs.org/react/-/react-19.2.0.tgz#d33dd1721698f4376ae57a54098cb47fc75d93a5"
integrity sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==
react@19.1.1:
version "19.1.1"
resolved "https://registry.yarnpkg.com/react/-/react-19.1.1.tgz#06d9149ec5e083a67f9a1e39ce97b06a03b644af"
integrity sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==
read-cache@^1.0.0:
version "1.0.0"
@@ -3778,10 +3785,10 @@ safe-regex-test@^1.1.0:
es-errors "^1.3.0"
is-regex "^1.2.1"
scheduler@^0.27.0:
version "0.27.0"
resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd"
integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==
scheduler@^0.26.0:
version "0.26.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.26.0.tgz#4ce8a8c2a2095f13ea11bf9a445be50c555d6337"
integrity sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==
semver@^6.3.1:
version "6.3.1"

View File

@@ -12,28 +12,6 @@ import {Err, Ok, Result} from './Utils/Result';
import {assertExhaustive} from './Utils/utils';
import invariant from 'invariant';
// Number of context lines to display above the source of an error
const CODEFRAME_LINES_ABOVE = 2;
// Number of context lines to display below the source of an error
const CODEFRAME_LINES_BELOW = 3;
/*
* Max number of lines for the _source_ of an error, before we abbreviate
* the display of the source portion
*/
const CODEFRAME_MAX_LINES = 10;
/*
* When the error source exceeds the above threshold, how many lines of
* the source should be displayed? We show:
* - CODEFRAME_LINES_ABOVE context lines
* - CODEFRAME_ABBREVIATED_SOURCE_LINES of the error
* - '...' ellipsis
* - CODEFRAME_ABBREVIATED_SOURCE_LINES of the error
* - CODEFRAME_LINES_BELOW context lines
*
* This value must be at least 2 or else we'll cut off important parts of the error message
*/
const CODEFRAME_ABBREVIATED_SOURCE_LINES = 5;
export enum ErrorSeverity {
/**
* An actionable error that the developer can fix. For example, product code errors should be
@@ -518,7 +496,7 @@ function printCodeFrame(
loc: t.SourceLocation,
message: string,
): string {
const printed = codeFrameColumns(
return codeFrameColumns(
source,
{
start: {
@@ -532,25 +510,8 @@ function printCodeFrame(
},
{
message,
linesAbove: CODEFRAME_LINES_ABOVE,
linesBelow: CODEFRAME_LINES_BELOW,
},
);
const lines = printed.split(/\r?\n/);
if (loc.end.line - loc.start.line < CODEFRAME_MAX_LINES) {
return printed;
}
const pipeIndex = lines[0].indexOf('|');
return [
...lines.slice(
0,
CODEFRAME_LINES_ABOVE + CODEFRAME_ABBREVIATED_SOURCE_LINES,
),
' '.repeat(pipeIndex) + '…',
...lines.slice(
-(CODEFRAME_LINES_BELOW + CODEFRAME_ABBREVIATED_SOURCE_LINES),
),
].join('\n');
}
function printErrorSummary(category: ErrorCategory, message: string): string {

View File

@@ -103,7 +103,6 @@ import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoF
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
import {validateNoDerivedComputationsInEffects_exp} from '../Validation/ValidateNoDerivedComputationsInEffects_exp';
import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
export type CompilerPipelineValue =
@@ -276,10 +275,6 @@ function runWithEnvironment(
validateNoDerivedComputationsInEffects(hir);
}
if (env.config.validateNoDerivedComputationsInEffects_exp) {
env.logErrors(validateNoDerivedComputationsInEffects_exp(hir));
}
if (env.config.validateNoSetStateInEffects) {
env.logErrors(validateNoSetStateInEffects(hir, env));
}

View File

@@ -1568,6 +1568,20 @@ function lowerObjectPropertyKey(
name: key.node.value,
};
} else if (property.node.computed && key.isExpression()) {
if (!key.isIdentifier() && !key.isMemberExpression()) {
/*
* NOTE: allowing complex key expressions can trigger a bug where a mutation is made conditional
* see fixture
* error.object-expression-computed-key-modified-during-after-construction.js
*/
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`,
category: ErrorCategory.Todo,
loc: key.node.loc ?? null,
suggestions: null,
});
return null;
}
const place = lowerExpressionToTemporary(builder, key);
return {
kind: 'computed',

View File

@@ -324,12 +324,6 @@ export const EnvironmentConfigSchema = z.object({
*/
validateNoDerivedComputationsInEffects: z.boolean().default(false),
/**
* Experimental: Validates that effects are not used to calculate derived data which could instead be computed
* during render. Generates a custom error message for each type of violation.
*/
validateNoDerivedComputationsInEffects_exp: z.boolean().default(false),
/**
* Validates against creating JSX within a try block and recommends using an error boundary
* instead.

View File

@@ -1,690 +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 {Result} from '../Utils/Result';
import {CompilerDiagnostic, CompilerError, Effect} from '..';
import {ErrorCategory} from '../CompilerError';
import {
BlockId,
FunctionExpression,
HIRFunction,
IdentifierId,
isSetStateType,
isUseEffectHookType,
Place,
CallExpression,
Instruction,
isUseStateType,
BasicBlock,
isUseRefType,
GeneratedSource,
SourceLocation,
} from '../HIR';
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
import {assertExhaustive} from '../Utils/utils';
type TypeOfValue = 'ignored' | 'fromProps' | 'fromState' | 'fromPropsAndState';
type DerivationMetadata = {
typeOfValue: TypeOfValue;
place: Place;
sourcesIds: Set<IdentifierId>;
isStateSource: boolean;
};
type ValidationContext = {
readonly functions: Map<IdentifierId, FunctionExpression>;
readonly errors: CompilerError;
readonly derivationCache: DerivationCache;
readonly effects: Set<HIRFunction>;
readonly setStateCache: Map<string | undefined | null, Array<Place>>;
readonly effectSetStateCache: Map<string | undefined | null, Array<Place>>;
};
class DerivationCache {
hasChanges: boolean = false;
cache: Map<IdentifierId, DerivationMetadata> = new Map();
private previousCache: Map<IdentifierId, DerivationMetadata> | null = null;
takeSnapshot(): void {
this.previousCache = new Map();
for (const [key, value] of this.cache.entries()) {
this.previousCache.set(key, {
place: value.place,
sourcesIds: new Set(value.sourcesIds),
typeOfValue: value.typeOfValue,
isStateSource: value.isStateSource,
});
}
}
checkForChanges(): void {
if (this.previousCache === null) {
this.hasChanges = true;
return;
}
for (const [key, value] of this.cache.entries()) {
const previousValue = this.previousCache.get(key);
if (
previousValue === undefined ||
!this.isDerivationEqual(previousValue, value)
) {
this.hasChanges = true;
return;
}
}
if (this.cache.size !== this.previousCache.size) {
this.hasChanges = true;
return;
}
this.hasChanges = false;
}
snapshot(): boolean {
const hasChanges = this.hasChanges;
this.hasChanges = false;
return hasChanges;
}
addDerivationEntry(
derivedVar: Place,
sourcesIds: Set<IdentifierId>,
typeOfValue: TypeOfValue,
isStateSource: boolean,
): void {
let finalIsSource = isStateSource;
if (!finalIsSource) {
for (const sourceId of sourcesIds) {
const sourceMetadata = this.cache.get(sourceId);
if (
sourceMetadata?.isStateSource &&
sourceMetadata.place.identifier.name?.kind !== 'named'
) {
finalIsSource = true;
break;
}
}
}
this.cache.set(derivedVar.identifier.id, {
place: derivedVar,
sourcesIds: sourcesIds,
typeOfValue: typeOfValue ?? 'ignored',
isStateSource: finalIsSource,
});
}
private isDerivationEqual(
a: DerivationMetadata,
b: DerivationMetadata,
): boolean {
if (a.typeOfValue !== b.typeOfValue) {
return false;
}
if (a.sourcesIds.size !== b.sourcesIds.size) {
return false;
}
for (const id of a.sourcesIds) {
if (!b.sourcesIds.has(id)) {
return false;
}
}
return true;
}
}
function isNamedIdentifier(place: Place): place is Place & {
identifier: {name: NonNullable<Place['identifier']['name']>};
} {
return (
place.identifier.name !== null && place.identifier.name.kind === 'named'
);
}
/**
* Validates that useEffect is not used for derived computations which could/should
* be performed in render.
*
* See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state
*
* Example:
*
* ```
* // 🔴 Avoid: redundant state and unnecessary Effect
* const [fullName, setFullName] = useState('');
* useEffect(() => {
* setFullName(firstName + ' ' + lastName);
* }, [firstName, lastName]);
* ```
*
* Instead use:
*
* ```
* // ✅ Good: calculated during rendering
* const fullName = firstName + ' ' + lastName;
* ```
*/
export function validateNoDerivedComputationsInEffects_exp(
fn: HIRFunction,
): Result<void, CompilerError> {
const functions: Map<IdentifierId, FunctionExpression> = new Map();
const derivationCache = new DerivationCache();
const errors = new CompilerError();
const effects: Set<HIRFunction> = new Set();
const setStateCache: Map<string | undefined | null, Array<Place>> = new Map();
const effectSetStateCache: Map<
string | undefined | null,
Array<Place>
> = new Map();
const context: ValidationContext = {
functions,
errors,
derivationCache,
effects,
setStateCache,
effectSetStateCache,
};
if (fn.fnType === 'Hook') {
for (const param of fn.params) {
if (param.kind === 'Identifier') {
context.derivationCache.cache.set(param.identifier.id, {
place: param,
sourcesIds: new Set(),
typeOfValue: 'fromProps',
isStateSource: true,
});
}
}
} else if (fn.fnType === 'Component') {
const props = fn.params[0];
if (props != null && props.kind === 'Identifier') {
context.derivationCache.cache.set(props.identifier.id, {
place: props,
sourcesIds: new Set(),
typeOfValue: 'fromProps',
isStateSource: true,
});
}
}
let isFirstPass = true;
do {
context.derivationCache.takeSnapshot();
for (const block of fn.body.blocks.values()) {
recordPhiDerivations(block, context);
for (const instr of block.instructions) {
recordInstructionDerivations(instr, context, isFirstPass);
}
}
context.derivationCache.checkForChanges();
isFirstPass = false;
} while (context.derivationCache.snapshot());
for (const effect of effects) {
validateEffect(effect, context);
}
return errors.asResult();
}
function recordPhiDerivations(
block: BasicBlock,
context: ValidationContext,
): void {
for (const phi of block.phis) {
let typeOfValue: TypeOfValue = 'ignored';
let sourcesIds: Set<IdentifierId> = new Set();
for (const operand of phi.operands.values()) {
const operandMetadata = context.derivationCache.cache.get(
operand.identifier.id,
);
if (operandMetadata === undefined) {
continue;
}
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
sourcesIds.add(operand.identifier.id);
}
if (typeOfValue !== 'ignored') {
context.derivationCache.addDerivationEntry(
phi.place,
sourcesIds,
typeOfValue,
false,
);
}
}
}
function joinValue(
lvalueType: TypeOfValue,
valueType: TypeOfValue,
): TypeOfValue {
if (lvalueType === 'ignored') return valueType;
if (valueType === 'ignored') return lvalueType;
if (lvalueType === valueType) return lvalueType;
return 'fromPropsAndState';
}
function recordInstructionDerivations(
instr: Instruction,
context: ValidationContext,
isFirstPass: boolean,
): void {
let typeOfValue: TypeOfValue = 'ignored';
let isSource: boolean = false;
const sources: Set<IdentifierId> = new Set();
const {lvalue, value} = instr;
if (value.kind === 'FunctionExpression') {
context.functions.set(lvalue.identifier.id, value);
for (const [, block] of value.loweredFunc.func.body.blocks) {
recordPhiDerivations(block, context);
for (const instr of block.instructions) {
recordInstructionDerivations(instr, context, isFirstPass);
}
}
} else if (value.kind === 'CallExpression' || value.kind === 'MethodCall') {
const callee =
value.kind === 'CallExpression' ? value.callee : value.property;
if (
isUseEffectHookType(callee.identifier) &&
value.args.length === 2 &&
value.args[0].kind === 'Identifier' &&
value.args[1].kind === 'Identifier'
) {
const effectFunction = context.functions.get(value.args[0].identifier.id);
if (effectFunction != null) {
context.effects.add(effectFunction.loweredFunc.func);
}
} else if (isUseStateType(lvalue.identifier) && value.args.length > 0) {
isSource = true;
typeOfValue = joinValue(typeOfValue, 'fromState');
}
}
for (const operand of eachInstructionOperand(instr)) {
if (
isSetStateType(operand.identifier) &&
operand.loc !== GeneratedSource &&
isFirstPass
) {
if (context.setStateCache.has(operand.loc.identifierName)) {
context.setStateCache.get(operand.loc.identifierName)!.push(operand);
} else {
context.setStateCache.set(operand.loc.identifierName, [operand]);
}
}
const operandMetadata = context.derivationCache.cache.get(
operand.identifier.id,
);
if (operandMetadata === undefined) {
continue;
}
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
sources.add(operand.identifier.id);
}
if (typeOfValue === 'ignored') {
return;
}
for (const lvalue of eachInstructionLValue(instr)) {
context.derivationCache.addDerivationEntry(
lvalue,
sources,
typeOfValue,
isSource,
);
}
for (const operand of eachInstructionOperand(instr)) {
switch (operand.effect) {
case Effect.Capture:
case Effect.Store:
case Effect.ConditionallyMutate:
case Effect.ConditionallyMutateIterator:
case Effect.Mutate: {
if (isMutable(instr, operand)) {
if (context.derivationCache.cache.has(operand.identifier.id)) {
const operandMetadata = context.derivationCache.cache.get(
operand.identifier.id,
);
if (operandMetadata !== undefined) {
operandMetadata.typeOfValue = joinValue(
typeOfValue,
operandMetadata.typeOfValue,
);
}
} else {
context.derivationCache.addDerivationEntry(
operand,
sources,
typeOfValue,
false,
);
}
}
break;
}
case Effect.Freeze:
case Effect.Read: {
// no-op
break;
}
case Effect.Unknown: {
CompilerError.invariant(false, {
reason: 'Unexpected unknown effect',
description: null,
details: [
{
kind: 'error',
loc: operand.loc,
message: 'Unexpected unknown effect',
},
],
});
}
default: {
assertExhaustive(
operand.effect,
`Unexpected effect kind \`${operand.effect}\``,
);
}
}
}
}
type TreeNode = {
name: string;
typeOfValue: TypeOfValue;
isSource: boolean;
children: Array<TreeNode>;
};
function buildTreeNode(
sourceId: IdentifierId,
context: ValidationContext,
visited: Set<string> = new Set(),
): Array<TreeNode> {
const sourceMetadata = context.derivationCache.cache.get(sourceId);
if (!sourceMetadata) {
return [];
}
if (sourceMetadata.isStateSource && isNamedIdentifier(sourceMetadata.place)) {
return [
{
name: sourceMetadata.place.identifier.name.value,
typeOfValue: sourceMetadata.typeOfValue,
isSource: sourceMetadata.isStateSource,
children: [],
},
];
}
const children: Array<TreeNode> = [];
const namedSiblings: Set<string> = new Set();
for (const childId of sourceMetadata.sourcesIds) {
const childNodes = buildTreeNode(
childId,
context,
new Set([
...visited,
...(isNamedIdentifier(sourceMetadata.place)
? [sourceMetadata.place.identifier.name.value]
: []),
]),
);
if (childNodes) {
for (const childNode of childNodes) {
if (!namedSiblings.has(childNode.name)) {
children.push(childNode);
namedSiblings.add(childNode.name);
}
}
}
}
if (
isNamedIdentifier(sourceMetadata.place) &&
!visited.has(sourceMetadata.place.identifier.name.value)
) {
return [
{
name: sourceMetadata.place.identifier.name.value,
typeOfValue: sourceMetadata.typeOfValue,
isSource: sourceMetadata.isStateSource,
children: children,
},
];
}
return children;
}
function renderTree(
node: TreeNode,
indent: string = '',
isLast: boolean = true,
propsSet: Set<string>,
stateSet: Set<string>,
): string {
const prefix = indent + (isLast ? '└── ' : '├── ');
const childIndent = indent + (isLast ? ' ' : '│ ');
let result = `${prefix}${node.name}`;
if (node.isSource) {
let typeLabel: string;
if (node.typeOfValue === 'fromProps') {
propsSet.add(node.name);
typeLabel = 'Prop';
} else if (node.typeOfValue === 'fromState') {
stateSet.add(node.name);
typeLabel = 'State';
} else {
propsSet.add(node.name);
stateSet.add(node.name);
typeLabel = 'Prop and State';
}
result += ` (${typeLabel})`;
}
if (node.children.length > 0) {
result += '\n';
node.children.forEach((child, index) => {
const isLastChild = index === node.children.length - 1;
result += renderTree(child, childIndent, isLastChild, propsSet, stateSet);
if (index < node.children.length - 1) {
result += '\n';
}
});
}
return result;
}
function validateEffect(
effectFunction: HIRFunction,
context: ValidationContext,
): void {
const seenBlocks: Set<BlockId> = new Set();
const effectDerivedSetStateCalls: Array<{
value: CallExpression;
loc: SourceLocation;
sourceIds: Set<IdentifierId>;
typeOfValue: TypeOfValue;
}> = [];
const globals: Set<IdentifierId> = new Set();
for (const block of effectFunction.body.blocks.values()) {
for (const pred of block.preds) {
if (!seenBlocks.has(pred)) {
// skip if block has a back edge
return;
}
}
for (const instr of block.instructions) {
// Early return if any instruction is deriving a value from a ref
if (isUseRefType(instr.lvalue.identifier)) {
return;
}
for (const operand of eachInstructionOperand(instr)) {
if (
isSetStateType(operand.identifier) &&
operand.loc !== GeneratedSource
) {
if (context.effectSetStateCache.has(operand.loc.identifierName)) {
context.effectSetStateCache
.get(operand.loc.identifierName)!
.push(operand);
} else {
context.effectSetStateCache.set(operand.loc.identifierName, [
operand,
]);
}
}
}
if (
instr.value.kind === 'CallExpression' &&
isSetStateType(instr.value.callee.identifier) &&
instr.value.args.length === 1 &&
instr.value.args[0].kind === 'Identifier'
) {
const argMetadata = context.derivationCache.cache.get(
instr.value.args[0].identifier.id,
);
if (argMetadata !== undefined) {
effectDerivedSetStateCalls.push({
value: instr.value,
loc: instr.value.callee.loc,
sourceIds: argMetadata.sourcesIds,
typeOfValue: argMetadata.typeOfValue,
});
}
} else if (instr.value.kind === 'CallExpression') {
const calleeMetadata = context.derivationCache.cache.get(
instr.value.callee.identifier.id,
);
if (
calleeMetadata !== undefined &&
(calleeMetadata.typeOfValue === 'fromProps' ||
calleeMetadata.typeOfValue === 'fromPropsAndState')
) {
// If the callee is a prop we can't confidently say that it should be derived in render
return;
}
if (globals.has(instr.value.callee.identifier.id)) {
// If the callee is a global we can't confidently say that it should be derived in render
return;
}
} else if (instr.value.kind === 'LoadGlobal') {
globals.add(instr.lvalue.identifier.id);
for (const operand of eachInstructionOperand(instr)) {
globals.add(operand.identifier.id);
}
}
}
seenBlocks.add(block.id);
}
for (const derivedSetStateCall of effectDerivedSetStateCalls) {
if (
derivedSetStateCall.loc !== GeneratedSource &&
context.effectSetStateCache.has(derivedSetStateCall.loc.identifierName) &&
context.setStateCache.has(derivedSetStateCall.loc.identifierName) &&
context.effectSetStateCache.get(derivedSetStateCall.loc.identifierName)!
.length ===
context.setStateCache.get(derivedSetStateCall.loc.identifierName)!
.length -
1
) {
const propsSet = new Set<string>();
const stateSet = new Set<string>();
const rootNodesMap = new Map<string, TreeNode>();
for (const id of derivedSetStateCall.sourceIds) {
const nodes = buildTreeNode(id, context);
for (const node of nodes) {
if (!rootNodesMap.has(node.name)) {
rootNodesMap.set(node.name, node);
}
}
}
const rootNodes = Array.from(rootNodesMap.values());
const trees = rootNodes.map((node, index) =>
renderTree(
node,
'',
index === rootNodes.length - 1,
propsSet,
stateSet,
),
);
const propsArr = Array.from(propsSet);
const stateArr = Array.from(stateSet);
let rootSources = '';
if (propsArr.length > 0) {
rootSources += `Props: [${propsArr.join(', ')}]`;
}
if (stateArr.length > 0) {
if (rootSources) rootSources += '\n';
rootSources += `State: [${stateArr.join(', ')}]`;
}
const description = `Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
This setState call is setting a derived value that depends on the following reactive sources:
${rootSources}
Data Flow Tree:
${trees.join('\n')}
See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state`;
context.errors.pushDiagnostic(
CompilerDiagnostic.create({
description: description,
category: ErrorCategory.EffectDerivationsOfState,
reason:
'You might not need an effect. Derive values in render, not effects.',
}).withDetails({
kind: 'error',
loc: derivedSetStateCall.value.callee.loc,
message: 'This should be computed during render, not in an effect',
}),
);
}
}
}

View File

@@ -1,86 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({value, enabled}) {
const [localValue, setLocalValue] = useState('');
useEffect(() => {
if (enabled) {
setLocalValue(value);
} else {
setLocalValue('disabled');
}
}, [value, enabled]);
return <div>{localValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 'test', enabled: true}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import { useEffect, useState } from "react";
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");
}
};
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 = {
fn: Component,
params: [{ value: "test", enabled: true }],
};
```
## 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":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
(kind: ok) <div>test</div>

View File

@@ -1,21 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({value, enabled}) {
const [localValue, setLocalValue] = useState('');
useEffect(() => {
if (enabled) {
setLocalValue(value);
} else {
setLocalValue('disabled');
}
}, [value, enabled]);
return <div>{localValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 'test', enabled: true}],
};

View File

@@ -1,78 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
export default function Component({input = 'empty'}) {
const [currInput, setCurrInput] = useState(input);
const localConst = 'local const';
useEffect(() => {
setCurrInput(input + localConst);
}, [input, localConst]);
return <div>{currInput}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{input: 'test'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import { useEffect, useState } from "react";
export default function Component(t0) {
const $ = _c(5);
const { input: t1 } = t0;
const input = t1 === undefined ? "empty" : t1;
const [currInput, setCurrInput] = useState(input);
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 = {
fn: Component,
params: [{ input: "test" }],
};
```
## 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":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
(kind: ok) <div>testlocal const</div>

View File

@@ -1,18 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
export default function Component({input = 'empty'}) {
const [currInput, setCurrInput] = useState(input);
const localConst = 'local const';
useEffect(() => {
setCurrInput(input + localConst);
}, [input, localConst]);
return <div>{currInput}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{input: 'test'}],
};

View File

@@ -1,77 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({shouldChange}) {
const [count, setCount] = useState(0);
useEffect(() => {
if (shouldChange) {
setCount(count + 1);
}
}, [count]);
return <div>{count}</div>;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import { useEffect, useState } from "react";
function Component(t0) {
const $ = _c(7);
const { shouldChange } = t0;
const [count, setCount] = useState(0);
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;
}
```
## 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":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
(kind: exception) Fixture not implemented

View File

@@ -1,15 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({shouldChange}) {
const [count, setCount] = useState(0);
useEffect(() => {
if (shouldChange) {
setCount(count + 1);
}
}, [count]);
return <div>{count}</div>;
}

View File

@@ -1,115 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({firstName}) {
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>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{firstName: 'John'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import { useEffect, useState } from "react";
function Component(t0) {
const $ = _c(12);
const { firstName } = t0;
const [lastName, setLastName] = useState("Doe");
const [fullName, setFullName] = useState("John");
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 = {
fn: Component,
params: [{ firstName: "John" }],
};
```
## 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":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
(kind: ok) <div><input value="Doe"><div>John D. Doe</div></div>

View File

@@ -1,25 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({firstName}) {
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>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{firstName: 'John'}],
};

View File

@@ -1,90 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({initialName}) {
const [name, setName] = useState('');
useEffect(() => {
setName(initialName);
}, [initialName]);
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{initialName: 'John'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import { useEffect, useState } from "react";
function Component(t0) {
const $ = _c(6);
const { initialName } = t0;
const [name, setName] = useState("");
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 = {
fn: Component,
params: [{ initialName: "John" }],
};
```
## Logs
```
{"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
(kind: ok) <div><input value="John"></div>

View File

@@ -1,21 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({initialName}) {
const [name, setName] = useState('');
useEffect(() => {
setName(initialName);
}, [initialName]);
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{initialName: 'John'}],
};

View File

@@ -1,57 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp
function Component({value}) {
const [checked, setChecked] = useState('');
useEffect(() => {
setChecked(value === '' ? [] : value.split(','));
}, [value]);
return <div>{checked}</div>;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
function Component(t0) {
const $ = _c(5);
const { value } = t0;
const [checked, setChecked] = useState("");
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;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,11 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp
function Component({value}) {
const [checked, setChecked] = useState('');
useEffect(() => {
setChecked(value === '' ? [] : value.split(','));
}, [value]);
return <div>{checked}</div>;
}

View File

@@ -1,92 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function MockComponent({onSet}) {
return <div onClick={() => onSet('clicked')}>Mock Component</div>;
}
function Component({propValue}) {
const [value, setValue] = useState(null);
useEffect(() => {
setValue(propValue);
}, [propValue]);
return <MockComponent onSet={setValue} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{propValue: 'test'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import { useEffect, useState } from "react";
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(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 = {
fn: Component,
params: [{ propValue: "test" }],
};
```
## Logs
```
{"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
(kind: ok) <div>Mock Component</div>

View File

@@ -1,20 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function MockComponent({onSet}) {
return <div onClick={() => onSet('clicked')}>Mock Component</div>;
}
function Component({propValue}) {
const [value, setValue] = useState(null);
useEffect(() => {
setValue(propValue);
}, [propValue]);
return <MockComponent onSet={setValue} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{propValue: 'test'}],
};

View File

@@ -1,78 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({value}) {
const [localValue, setLocalValue] = useState('');
useEffect(() => {
setLocalValue(value);
document.title = `Value: ${value}`;
}, [value]);
return <div>{localValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 'test'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import { useEffect, useState } from "react";
function Component(t0) {
const $ = _c(5);
const { value } = t0;
const [localValue, setLocalValue] = useState("");
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 = {
fn: Component,
params: [{ value: "test" }],
};
```
## 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":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
(kind: ok) <div>test</div>

View File

@@ -1,18 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({value}) {
const [localValue, setLocalValue] = useState('');
useEffect(() => {
setLocalValue(value);
document.title = `Value: ${value}`;
}, [value]);
return <div>{localValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 'test'}],
};

View File

@@ -1,79 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState, useRef} from 'react';
export default function Component({test}) {
const [local, setLocal] = useState('');
const myRef = useRef(null);
useEffect(() => {
setLocal(myRef.current + test);
}, [test]);
return <>{local}</>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{test: 'testString'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import { useEffect, useState, useRef } from "react";
export default function Component(t0) {
const $ = _c(5);
const { test } = t0;
const [local, setLocal] = useState("");
const myRef = useRef(null);
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 = {
fn: Component,
params: [{ test: "testString" }],
};
```
## Logs
```
{"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
(kind: ok) nulltestString

View File

@@ -1,19 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState, useRef} from 'react';
export default function Component({test}) {
const [local, setLocal] = useState('');
const myRef = useRef(null);
useEffect(() => {
setLocal(myRef.current + test);
}, [test]);
return <>{local}</>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{test: 'testString'}],
};

View File

@@ -1,93 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({propValue}) {
const [value, setValue] = useState(null);
function localFunction() {
console.log('local function');
}
useEffect(() => {
setValue(propValue);
localFunction();
}, [propValue]);
return <div>{value}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{propValue: 'test'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import { useEffect, useState } from "react";
function Component(t0) {
const $ = _c(6);
const { propValue } = t0;
const [value, setValue] = useState(null);
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = function localFunction() {
console.log("local function");
};
$[0] = t1;
} else {
t1 = $[0];
}
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 = {
fn: Component,
params: [{ propValue: "test" }],
};
```
## 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":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
(kind: ok) <div>test</div>
logs: ['local function']

View File

@@ -1,22 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({propValue}) {
const [value, setValue] = useState(null);
function localFunction() {
console.log('local function');
}
useEffect(() => {
setValue(propValue);
localFunction();
}, [propValue]);
return <div>{value}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{propValue: 'test'}],
};

View File

@@ -1,82 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({propValue, onChange}) {
const [value, setValue] = useState(null);
useEffect(() => {
setValue(propValue);
onChange();
}, [propValue]);
return <div>{value}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{propValue: 'test', onChange: () => {}}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import { useEffect, useState } from "react";
function Component(t0) {
const $ = _c(7);
const { propValue, onChange } = t0;
const [value, setValue] = useState(null);
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 = {
fn: Component,
params: [{ propValue: "test", onChange: () => {} }],
};
```
## Logs
```
{"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
(kind: ok) <div>test</div>

View File

@@ -1,17 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({propValue, onChange}) {
const [value, setValue] = useState(null);
useEffect(() => {
setValue(propValue);
onChange();
}, [propValue]);
return <div>{value}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{propValue: 'test', onChange: () => {}}],
};

View File

@@ -1,76 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({propValue}) {
const [value, setValue] = useState(null);
useEffect(() => {
setValue(propValue);
globalCall();
}, [propValue]);
return <div>{value}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{propValue: 'test'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import { useEffect, useState } from "react";
function Component(t0) {
const $ = _c(5);
const { propValue } = t0;
const [value, setValue] = useState(null);
let t1;
let t2;
if ($[0] !== propValue) {
t1 = () => {
setValue(propValue);
globalCall();
};
t2 = [propValue];
$[0] = propValue;
$[1] = t1;
$[2] = t2;
} else {
t1 = $[1];
t2 = $[2];
}
useEffect(t1, t2);
let t3;
if ($[3] !== value) {
t3 = <div>{value}</div>;
$[3] = value;
$[4] = t3;
} else {
t3 = $[4];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ propValue: "test" }],
};
```
## Logs
```
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":12,"column":1,"index":298},"filename":"effect-with-global-function-call-no-error.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output
(kind: exception) globalCall is not defined

View File

@@ -1,17 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({propValue}) {
const [value, setValue] = useState(null);
useEffect(() => {
setValue(propValue);
globalCall();
}, [propValue]);
return <div>{value}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{propValue: 'test'}],
};

View File

@@ -1,80 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component() {
const [firstName, setFirstName] = useState('Taylor');
const lastName = 'Swift';
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return <div>{fullName}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import { useEffect, useState } from "react";
function Component() {
const $ = _c(5);
const [firstName] = useState("Taylor");
const [fullName, setFullName] = useState("");
let t0;
let t1;
if ($[0] !== firstName) {
t0 = () => {
setFullName(firstName + " " + "Swift");
};
t1 = [firstName, "Swift"];
$[0] = firstName;
$[1] = t0;
$[2] = t1;
} else {
t0 = $[1];
t1 = $[2];
}
useEffect(t0, t1);
let t2;
if ($[3] !== fullName) {
t2 = <div>{fullName}</div>;
$[3] = fullName;
$[4] = t2;
} else {
t2 = $[4];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
};
```
## 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: [firstName]\n\nData Flow Tree:\n└── firstName (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":341},"end":{"line":11,"column":15,"index":352},"filename":"invalid-derived-computation-in-effect.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":15,"column":1,"index":445},"filename":"invalid-derived-computation-in-effect.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output
(kind: ok) <div>Taylor Swift</div>

View File

@@ -1,20 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component() {
const [firstName, setFirstName] = useState('Taylor');
const lastName = 'Swift';
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return <div>{fullName}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
};

View File

@@ -1,79 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
export default function Component(props) {
const [displayValue, setDisplayValue] = useState('');
useEffect(() => {
const computed = props.prefix + props.value + props.suffix;
setDisplayValue(computed);
}, [props.prefix, props.value, props.suffix]);
return <div>{displayValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prefix: '[', value: 'test', suffix: ']'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import { useEffect, useState } from "react";
export default function Component(props) {
const $ = _c(7);
const [displayValue, setDisplayValue] = useState("");
let t0;
let t1;
if ($[0] !== props.prefix || $[1] !== props.suffix || $[2] !== props.value) {
t0 = () => {
const computed = props.prefix + props.value + props.suffix;
setDisplayValue(computed);
};
t1 = [props.prefix, props.value, props.suffix];
$[0] = props.prefix;
$[1] = props.suffix;
$[2] = props.value;
$[3] = t0;
$[4] = t1;
} else {
t0 = $[3];
t1 = $[4];
}
useEffect(t0, t1);
let t2;
if ($[5] !== displayValue) {
t2 = <div>{displayValue}</div>;
$[5] = displayValue;
$[6] = t2;
} else {
t2 = $[6];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ prefix: "[", value: "test", suffix: "]" }],
};
```
## 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: [props]\n\nData Flow Tree:\n└── computed\n └── props (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":19,"index":310},"filename":"invalid-derived-state-from-computed-props.ts","identifierName":"setDisplayValue"},"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":409},"filename":"invalid-derived-state-from-computed-props.ts"},"fnName":"Component","memoSlots":7,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output
(kind: ok) <div>[test]</div>

View File

@@ -1,18 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
export default function Component(props) {
const [displayValue, setDisplayValue] = useState('');
useEffect(() => {
const computed = props.prefix + props.value + props.suffix;
setDisplayValue(computed);
}, [props.prefix, props.value, props.suffix]);
return <div>{displayValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prefix: '[', value: 'test', suffix: ']'}],
};

View File

@@ -1,81 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
export default function Component({props}) {
const [fullName, setFullName] = useState(
props.firstName + ' ' + props.lastName
);
useEffect(() => {
setFullName(props.firstName + ' ' + props.lastName);
}, [props.firstName, props.lastName]);
return <div>{fullName}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{props: {firstName: 'John', lastName: 'Doe'}}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import { useEffect, useState } from "react";
export default function Component(t0) {
const $ = _c(6);
const { props } = t0;
const [fullName, setFullName] = useState(
props.firstName + " " + props.lastName,
);
let t1;
let t2;
if ($[0] !== props.firstName || $[1] !== props.lastName) {
t1 = () => {
setFullName(props.firstName + " " + props.lastName);
};
t2 = [props.firstName, props.lastName];
$[0] = props.firstName;
$[1] = props.lastName;
$[2] = t1;
$[3] = t2;
} else {
t1 = $[2];
t2 = $[3];
}
useEffect(t1, t2);
let t3;
if ($[4] !== fullName) {
t3 = <div>{fullName}</div>;
$[4] = fullName;
$[5] = t3;
} else {
t3 = $[5];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ props: { firstName: "John", lastName: "Doe" } }],
};
```
## 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: [props]\n\nData Flow Tree:\n└── props (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":10,"column":4,"index":269},"end":{"line":10,"column":15,"index":280},"filename":"invalid-derived-state-from-destructured-props.ts","identifierName":"setFullName"},"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":14,"column":1,"index":397},"filename":"invalid-derived-state-from-destructured-props.ts"},"fnName":"Component","memoSlots":6,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output
(kind: ok) <div>John Doe</div>

View File

@@ -1,19 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
export default function Component({props}) {
const [fullName, setFullName] = useState(
props.firstName + ' ' + props.lastName
);
useEffect(() => {
setFullName(props.firstName + ' ' + props.lastName);
}, [props.firstName, props.lastName]);
return <div>{fullName}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{props: {firstName: 'John', lastName: 'Doe'}}],
};

View File

@@ -1,88 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState, useRef} from 'react';
export default function Component({test}) {
const [local, setLocal] = useState(0);
const myRef = useRef(null);
useEffect(() => {
if (myRef.current) {
setLocal(test);
} else {
setLocal(test + test);
}
}, [test]);
return <>{local}</>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{test: 4}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import { useEffect, useState, useRef } from "react";
export default function Component(t0) {
const $ = _c(5);
const { test } = t0;
const [local, setLocal] = useState(0);
const myRef = useRef(null);
let t1;
let t2;
if ($[0] !== test) {
t1 = () => {
if (myRef.current) {
setLocal(test);
} else {
setLocal(test + 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 = {
fn: Component,
params: [{ test: 4 }],
};
```
## Logs
```
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":15,"index":130},"end":{"line":18,"column":1,"index":386},"filename":"ref-conditional-in-effect-no-error.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output
(kind: ok) 8

View File

@@ -1,23 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState, useRef} from 'react';
export default function Component({test}) {
const [local, setLocal] = useState(0);
const myRef = useRef(null);
useEffect(() => {
if (myRef.current) {
setLocal(test);
} else {
setLocal(test + test);
}
}, [test]);
return <>{local}</>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{test: 4}],
};

View File

@@ -3,8 +3,6 @@
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function BadExample() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
@@ -12,7 +10,7 @@ function BadExample() {
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
setFullName(capitalize(firstName + ' ' + lastName));
}, [firstName, lastName]);
return <div>{fullName}</div>;
@@ -28,14 +26,14 @@ Found 1 error:
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
error.invalid-derived-computation-in-effect.ts:11:4
9 | const [fullName, setFullName] = useState('');
10 | useEffect(() => {
> 11 | setFullName(firstName + ' ' + lastName);
error.invalid-derived-computation-in-effect.ts:9:4
7 | const [fullName, setFullName] = useState('');
8 | useEffect(() => {
> 9 | setFullName(capitalize(firstName + ' ' + lastName));
| ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
12 | }, [firstName, lastName]);
13 |
14 | return <div>{fullName}</div>;
10 | }, [firstName, lastName]);
11 |
12 | return <div>{fullName}</div>;
```

View File

@@ -1,6 +1,4 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function BadExample() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
@@ -8,7 +6,7 @@ function BadExample() {
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
setFullName(capitalize(firstName + ' ' + lastName));
}, [firstName, lastName]);
return <div>{fullName}</div>;

View File

@@ -60,7 +60,29 @@ This argument is a function which may reassign or mutate `cache` after render, w
> 22 | // The original issue is that `cache` was not memoized together with the returned
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 23 | // function. This was because neither appears to ever be mutated — the function
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 24 | // is known to mutate `cache` but the function isn't called.
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 25 | //
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 26 | // The fix is to detect cases like this — functions that are mutable but not called -
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 27 | // and ensure that their mutable captures are aliased together into the same scope.
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 28 | const cache = new WeakMap<TInput, TOutput>();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 29 | return input => {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 30 | let output = cache.get(input);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 31 | if (output == null) {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 32 | output = map(input);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 33 | cache.set(input, output);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 34 | }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 35 | return output;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 36 | };

View File

@@ -0,0 +1,41 @@
## Input
```javascript
import {identity, mutate, mutateAndReturn} from 'shared-runtime';
function Component(props) {
const key = {};
const context = {
[(mutate(key), key)]: identity([props.value]),
};
mutate(key);
return context;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};
```
## Error
```
Found 1 error:
Todo: (BuildHIR::lowerExpression) Expected Identifier, got SequenceExpression key in ObjectExpression
error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr.ts:6:6
4 | const key = {};
5 | const context = {
> 6 | [(mutate(key), key)]: identity([props.value]),
| ^^^^^^^^^^^^^^^^ (BuildHIR::lowerExpression) Expected Identifier, got SequenceExpression key in ObjectExpression
7 | };
8 | mutate(key);
9 | return context;
```

View File

@@ -6,11 +6,10 @@ function Component(props) {
[(mutate(key), key)]: identity([props.value]),
};
mutate(key);
return [context, key];
return context;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
sequentialRenders: [{value: 42}, {value: 42}],
};

View File

@@ -0,0 +1,41 @@
## Input
```javascript
import {identity, mutate, mutateAndReturn} from 'shared-runtime';
function Component(props) {
const key = {};
const context = {
[mutateAndReturn(key)]: identity([props.value]),
};
mutate(key);
return context;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};
```
## Error
```
Found 1 error:
Todo: (BuildHIR::lowerExpression) Expected Identifier, got CallExpression key in ObjectExpression
error.todo-object-expression-computed-key-modified-during-after-construction.ts:6:5
4 | const key = {};
5 | const context = {
> 6 | [mutateAndReturn(key)]: identity([props.value]),
| ^^^^^^^^^^^^^^^^^^^^ (BuildHIR::lowerExpression) Expected Identifier, got CallExpression key in ObjectExpression
7 | };
8 | mutate(key);
9 | return context;
```

View File

@@ -0,0 +1,40 @@
## Input
```javascript
import {identity, mutate, mutateAndReturn} from 'shared-runtime';
function Component(props) {
const key = {};
const context = {
[mutateAndReturn(key)]: identity([props.value]),
};
return context;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};
```
## Error
```
Found 1 error:
Todo: (BuildHIR::lowerExpression) Expected Identifier, got CallExpression key in ObjectExpression
error.todo-object-expression-computed-key-mutate-key-while-constructing-object.ts:6:5
4 | const key = {};
5 | const context = {
> 6 | [mutateAndReturn(key)]: identity([props.value]),
| ^^^^^^^^^^^^^^^^^^^^ (BuildHIR::lowerExpression) Expected Identifier, got CallExpression key in ObjectExpression
7 | };
8 | return context;
9 | }
```

View File

@@ -0,0 +1,42 @@
## Input
```javascript
import {identity, mutate, mutateAndReturn} from 'shared-runtime';
function Component(props) {
const obj = {mutateAndReturn};
const key = {};
const context = {
[obj.mutateAndReturn(key)]: identity([props.value]),
};
mutate(key);
return context;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};
```
## Error
```
Found 1 error:
Todo: (BuildHIR::lowerExpression) Expected Identifier, got CallExpression key in ObjectExpression
error.todo-object-expression-member-expr-call.ts:7:5
5 | const key = {};
6 | const context = {
> 7 | [obj.mutateAndReturn(key)]: identity([props.value]),
| ^^^^^^^^^^^^^^^^^^^^^^^^ (BuildHIR::lowerExpression) Expected Identifier, got CallExpression key in ObjectExpression
8 | };
9 | mutate(key);
10 | return context;
```

View File

@@ -64,7 +64,20 @@ error.todo-preserve-memo-deps-mixed-optional-nonoptional-property-chain.ts:7:25
> 8 | return identity({
| ^^^^^^^^^^^^^^^^^^^^^
> 9 | callback: () => {
| ^^^^^^^^^^^^^^^^^^^^^
> 10 | // This is a bug in our dependency inference: we stop capturing dependencies
| ^^^^^^^^^^^^^^^^^^^^^
> 11 | // after x.a.b?.c. But what this dependency is telling us is that if `x.a.b`
| ^^^^^^^^^^^^^^^^^^^^^
> 12 | // was non-nullish, then we can access `.c.d?.e`. Thus we should take the
| ^^^^^^^^^^^^^^^^^^^^^
> 13 | // full property chain, exactly as-is with optionals/non-optionals, as a
| ^^^^^^^^^^^^^^^^^^^^^
> 14 | // dependency
| ^^^^^^^^^^^^^^^^^^^^^
> 15 | return identity(x.a.b?.c.d?.e);
| ^^^^^^^^^^^^^^^^^^^^^
> 16 | },
| ^^^^^^^^^^^^^^^^^^^^^
> 17 | });
| ^^^^^^^^^^^^^^^^^^^^^

View File

@@ -46,7 +46,18 @@ error.todo-syntax.ts:11:2
> 12 | () => {
| ^^^^^^^^^^^
> 13 | try {
| ^^^^^^^^^^^
> 14 | console.log(prop1);
| ^^^^^^^^^^^
> 15 | } finally {
| ^^^^^^^^^^^
> 16 | console.log('exiting');
| ^^^^^^^^^^^
> 17 | }
| ^^^^^^^^^^^
> 18 | },
| ^^^^^^^^^^^
> 19 | [prop1],
| ^^^^^^^^^^^
> 20 | AUTODEPS
| ^^^^^^^^^^^

View File

@@ -1,56 +0,0 @@
## Input
```javascript
import {identity, mutate, mutateAndReturn} from 'shared-runtime';
function Component(props) {
const key = {};
const context = {
[(mutate(key), key)]: identity([props.value]),
};
mutate(key);
return [context, key];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
sequentialRenders: [{value: 42}, {value: 42}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { identity, mutate, mutateAndReturn } from "shared-runtime";
function Component(props) {
const $ = _c(2);
let t0;
if ($[0] !== props.value) {
const key = {};
const context = { [(mutate(key), key)]: identity([props.value]) };
mutate(key);
t0 = [context, key];
$[0] = props.value;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 42 }],
sequentialRenders: [{ value: 42 }, { value: 42 }],
};
```
### Eval output
(kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
[{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]

View File

@@ -1,55 +0,0 @@
## Input
```javascript
import {identity, mutate, mutateAndReturn} from 'shared-runtime';
function Component(props) {
const key = {};
const context = {
[mutateAndReturn(key)]: identity([props.value]),
};
mutate(key);
return context;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
sequentialRenders: [{value: 42}, {value: 42}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { identity, mutate, mutateAndReturn } from "shared-runtime";
function Component(props) {
const $ = _c(2);
let context;
if ($[0] !== props.value) {
const key = {};
context = { [mutateAndReturn(key)]: identity([props.value]) };
mutate(key);
$[0] = props.value;
$[1] = context;
} else {
context = $[1];
}
return context;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 42 }],
sequentialRenders: [{ value: 42 }, { value: 42 }],
};
```
### Eval output
(kind: ok) {"[object Object]":[42]}
{"[object Object]":[42]}

View File

@@ -1,67 +0,0 @@
## Input
```javascript
import {identity, mutate, mutateAndReturn} from 'shared-runtime';
function Component(props) {
const key = {};
const context = {
[mutateAndReturn(key)]: identity([props.value]),
};
return context;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { identity, mutate, mutateAndReturn } from "shared-runtime";
function Component(props) {
const $ = _c(5);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const key = {};
t0 = mutateAndReturn(key);
$[0] = t0;
} else {
t0 = $[0];
}
let t1;
if ($[1] !== props.value) {
t1 = identity([props.value]);
$[1] = props.value;
$[2] = t1;
} else {
t1 = $[2];
}
let t2;
if ($[3] !== t1) {
t2 = { [t0]: t1 };
$[3] = t1;
$[4] = t2;
} else {
t2 = $[4];
}
const context = t2;
return context;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 42 }],
};
```
### Eval output
(kind: ok) {"[object Object]":[42]}

View File

@@ -1,54 +0,0 @@
## Input
```javascript
import {identity, mutate, mutateAndReturn} from 'shared-runtime';
function Component(props) {
const obj = {mutateAndReturn};
const key = {};
const context = {
[obj.mutateAndReturn(key)]: identity([props.value]),
};
mutate(key);
return context;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { identity, mutate, mutateAndReturn } from "shared-runtime";
function Component(props) {
const $ = _c(2);
let context;
if ($[0] !== props.value) {
const obj = { mutateAndReturn };
const key = {};
context = { [obj.mutateAndReturn(key)]: identity([props.value]) };
mutate(key);
$[0] = props.value;
$[1] = context;
} else {
context = $[1];
}
return context;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 42 }],
};
```
### Eval output
(kind: ok) {"[object Object]":[42]}

View File

@@ -29,13 +29,10 @@ export {
ProgramContext,
tryFindDirectiveEnablingMemoization as findDirectiveEnablingMemoization,
findDirectiveDisablingMemoization,
defaultOptions,
type CompilerPipelineValue,
type Logger,
type LoggerEvent,
type PluginOptions,
type AutoDepsDecorationsEvent,
type CompileSuccessEvent,
} from './Entrypoint';
export {
Effect,

View File

@@ -5,22 +5,15 @@
* LICENSE file in the root directory of this source tree.
*/
import {
ErrorCategory,
getRuleForCategory,
} from 'babel-plugin-react-compiler/src/CompilerError';
import {normalizeIndent, testRule, makeTestCaseError} from './shared-utils';
import {allRules} from '../src/rules/ReactCompilerRule';
import ReactCompilerRule from '../src/rules/ReactCompilerRule';
testRule(
'no impure function calls rule',
allRules[getRuleForCategory(ErrorCategory.Purity).name].rule,
{
valid: [],
invalid: [
{
name: 'Known impure function calls are caught',
code: normalizeIndent`
testRule('no impure function calls rule', ReactCompilerRule, {
valid: [],
invalid: [
{
name: 'Known impure function calls are caught',
code: normalizeIndent`
function Component() {
const date = Date.now();
const now = performance.now();
@@ -28,12 +21,11 @@ testRule(
return <Foo date={date} now={now} rand={rand} />;
}
`,
errors: [
makeTestCaseError('Cannot call impure function during render'),
makeTestCaseError('Cannot call impure function during render'),
makeTestCaseError('Cannot call impure function during render'),
],
},
],
},
);
errors: [
makeTestCaseError('Cannot call impure function during render'),
makeTestCaseError('Cannot call impure function during render'),
makeTestCaseError('Cannot call impure function during render'),
],
},
],
});

View File

@@ -5,30 +5,23 @@
* LICENSE file in the root directory of this source tree.
*/
import {
ErrorCategory,
getRuleForCategory,
} from 'babel-plugin-react-compiler/src/CompilerError';
import {normalizeIndent, makeTestCaseError, testRule} from './shared-utils';
import {allRules} from '../src/rules/ReactCompilerRule';
import {AllRules} from '../src/rules/ReactCompilerRule';
testRule(
'rules-of-hooks',
allRules[getRuleForCategory(ErrorCategory.Hooks).name].rule,
{
valid: [
{
name: 'Basic example',
code: normalizeIndent`
testRule('rules-of-hooks', AllRules, {
valid: [
{
name: 'Basic example',
code: normalizeIndent`
function Component() {
useHook();
return <div>Hello world</div>;
}
`,
},
{
name: 'Violation with Flow suppression',
code: `
},
{
name: 'Violation with Flow suppression',
code: `
// Valid since error already suppressed with flow.
function useHook() {
if (cond) {
@@ -37,11 +30,11 @@ testRule(
}
}
`,
},
{
// OK because invariants are only meant for the compiler team's consumption
name: '[Invariant] Defined after use',
code: normalizeIndent`
},
{
// OK because invariants are only meant for the compiler team's consumption
name: '[Invariant] Defined after use',
code: normalizeIndent`
function Component(props) {
let y = function () {
m(x);
@@ -52,49 +45,42 @@ testRule(
return y;
}
`,
},
{
name: "Classes don't throw",
code: normalizeIndent`
},
{
name: "Classes don't throw",
code: normalizeIndent`
class Foo {
#bar() {}
}
`,
},
],
invalid: [
{
name: 'Simple violation',
code: normalizeIndent`
},
],
invalid: [
{
name: 'Simple violation',
code: normalizeIndent`
function useConditional() {
if (cond) {
useConditionalHook();
}
}
`,
errors: [
makeTestCaseError(
'Hooks must always be called in a consistent order',
),
],
},
{
name: 'Multiple diagnostics within the same function are surfaced',
code: normalizeIndent`
errors: [
makeTestCaseError('Hooks must always be called in a consistent order'),
],
},
{
name: 'Multiple diagnostics within the same function are surfaced',
code: normalizeIndent`
function useConditional() {
cond ?? useConditionalHook();
props.cond && useConditionalHook();
return <div>Hello world</div>;
}`,
errors: [
makeTestCaseError(
'Hooks must always be called in a consistent order',
),
makeTestCaseError(
'Hooks must always be called in a consistent order',
),
],
},
],
},
);
errors: [
makeTestCaseError('Hooks must always be called in a consistent order'),
makeTestCaseError('Hooks must always be called in a consistent order'),
],
},
],
});

View File

@@ -5,22 +5,15 @@
* LICENSE file in the root directory of this source tree.
*/
import {
ErrorCategory,
getRuleForCategory,
} from 'babel-plugin-react-compiler/src/CompilerError';
import {normalizeIndent, testRule, makeTestCaseError} from './shared-utils';
import {allRules} from '../src/rules/ReactCompilerRule';
import ReactCompilerRule from '../src/rules/ReactCompilerRule';
testRule(
'no ambiguous JSX rule',
allRules[getRuleForCategory(ErrorCategory.ErrorBoundaries).name].rule,
{
valid: [],
invalid: [
{
name: 'JSX in try blocks are warned against',
code: normalizeIndent`
testRule('no ambiguous JSX rule', ReactCompilerRule, {
valid: [],
invalid: [
{
name: 'JSX in try blocks are warned against',
code: normalizeIndent`
function Component(props) {
let el;
try {
@@ -31,8 +24,7 @@ testRule(
return el;
}
`,
errors: [makeTestCaseError('Avoid constructing JSX within try/catch')],
},
],
},
);
errors: [makeTestCaseError('Avoid constructing JSX within try/catch')],
},
],
});

View File

@@ -4,22 +4,15 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
ErrorCategory,
getRuleForCategory,
} from 'babel-plugin-react-compiler/src/CompilerError';
import {normalizeIndent, makeTestCaseError, testRule} from './shared-utils';
import {allRules} from '../src/rules/ReactCompilerRule';
import ReactCompilerRule from '../src/rules/ReactCompilerRule';
testRule(
'no-capitalized-calls',
allRules[getRuleForCategory(ErrorCategory.CapitalizedCalls).name].rule,
{
valid: [],
invalid: [
{
name: 'Simple violation',
code: normalizeIndent`
testRule('no-capitalized-calls', ReactCompilerRule, {
valid: [],
invalid: [
{
name: 'Simple violation',
code: normalizeIndent`
import Child from './Child';
function Component() {
return <>
@@ -27,15 +20,13 @@ testRule(
</>;
}
`,
errors: [
makeTestCaseError(
'Capitalized functions are reserved for components',
),
],
},
{
name: 'Method call violation',
code: normalizeIndent`
errors: [
makeTestCaseError('Capitalized functions are reserved for components'),
],
},
{
name: 'Method call violation',
code: normalizeIndent`
import myModule from './MyModule';
function Component() {
return <>
@@ -43,15 +34,13 @@ testRule(
</>;
}
`,
errors: [
makeTestCaseError(
'Capitalized functions are reserved for components',
),
],
},
{
name: 'Multiple diagnostics within the same function are surfaced',
code: normalizeIndent`
errors: [
makeTestCaseError('Capitalized functions are reserved for components'),
],
},
{
name: 'Multiple diagnostics within the same function are surfaced',
code: normalizeIndent`
import Child1 from './Child1';
import MyModule from './MyModule';
function Component() {
@@ -60,12 +49,9 @@ testRule(
{MyModule.Child2()}
</>;
}`,
errors: [
makeTestCaseError(
'Capitalized functions are reserved for components',
),
],
},
],
},
);
errors: [
makeTestCaseError('Capitalized functions are reserved for components'),
],
},
],
});

View File

@@ -5,30 +5,22 @@
* LICENSE file in the root directory of this source tree.
*/
import {
ErrorCategory,
getRuleForCategory,
} from 'babel-plugin-react-compiler/src/CompilerError';
import {normalizeIndent, testRule, makeTestCaseError} from './shared-utils';
import {allRules} from '../src/rules/ReactCompilerRule';
import ReactCompilerRule from '../src/rules/ReactCompilerRule';
testRule(
'no ref access in render rule',
allRules[getRuleForCategory(ErrorCategory.Refs).name].rule,
{
valid: [],
invalid: [
{
name: 'validate against simple ref access in render',
code: normalizeIndent`
testRule('no ref access in render rule', ReactCompilerRule, {
valid: [],
invalid: [
{
name: 'validate against simple ref access in render',
code: normalizeIndent`
function Component(props) {
const ref = useRef(null);
const value = ref.current;
return value;
}
`,
errors: [makeTestCaseError('Cannot access refs during render')],
},
],
},
);
errors: [makeTestCaseError('Cannot access refs during render')],
},
],
});

View File

@@ -5,19 +5,10 @@
* LICENSE file in the root directory of this source tree.
*/
import {
ErrorCategory,
getRuleForCategory,
} from 'babel-plugin-react-compiler/src/CompilerError';
import {
normalizeIndent,
testRule,
makeTestCaseError,
TestRecommendedRules,
} from './shared-utils';
import {allRules} from '../src/rules/ReactCompilerRule';
import {normalizeIndent, testRule, makeTestCaseError} from './shared-utils';
import {AllRules} from '../src/rules/ReactCompilerRule';
testRule('plugin-recommended', TestRecommendedRules, {
testRule('plugin-recommended', AllRules, {
valid: [
{
name: 'Basic example with component syntax',

View File

@@ -6,11 +6,8 @@
*/
import {RuleTester} from 'eslint';
import {
CompilerTestCases,
normalizeIndent,
TestRecommendedRules,
} from './shared-utils';
import {CompilerTestCases, normalizeIndent} from './shared-utils';
import ReactCompilerRule from '../src/rules/ReactCompilerRule';
const tests: CompilerTestCases = {
valid: [
@@ -62,4 +59,4 @@ const eslintTester = new RuleTester({
// @ts-ignore[2353] - outdated types
parser: require.resolve('@typescript-eslint/parser'),
});
eslintTester.run('react-compiler', TestRecommendedRules, tests);
eslintTester.run('react-compiler', ReactCompilerRule, tests);

View File

@@ -1,8 +1,5 @@
import {RuleTester as ESLintTester, Rule} from 'eslint';
import {type ErrorCategory} from 'babel-plugin-react-compiler/src/CompilerError';
import escape from 'regexp.escape';
import {configs} from '../src/index';
import {allRules} from '../src/rules/ReactCompilerRule';
/**
* A string template tag that removes padding from the left side of multi-line strings
@@ -46,31 +43,4 @@ export function testRule(
eslintTester.run(name, rule, tests);
}
/**
* Aggregates all recommended rules from the plugin.
*/
export const TestRecommendedRules: Rule.RuleModule = {
meta: {
type: 'problem',
docs: {
description: 'Disallow capitalized function calls',
category: 'Possible Errors',
recommended: true,
},
// validation is done at runtime with zod
schema: [{type: 'object', additionalProperties: true}],
},
create(context) {
for (const ruleConfig of Object.values(
configs.recommended.plugins['react-compiler'].rules,
)) {
const listener = ruleConfig.rule.create(context);
if (Object.entries(listener).length !== 0) {
throw new Error('TODO: handle rules that return listeners to eslint');
}
}
return {};
},
};
test('no test', () => {});

View File

@@ -5,37 +5,24 @@
* LICENSE file in the root directory of this source tree.
*/
import {type Linter} from 'eslint';
import {
allRules,
mapErrorSeverityToESlint,
recommendedRules,
} from './rules/ReactCompilerRule';
import ReactCompilerRule from './rules/ReactCompilerRule';
const meta = {
name: 'eslint-plugin-react-compiler',
};
const configs = {
recommended: {
plugins: {
'react-compiler': {
rules: allRules,
module.exports = {
rules: {
'react-compiler': ReactCompilerRule,
},
configs: {
recommended: {
plugins: {
'react-compiler': {
rules: {
'react-compiler': ReactCompilerRule,
},
},
},
rules: {
'react-compiler/react-compiler': 'error',
},
},
rules: Object.fromEntries(
Object.entries(recommendedRules).map(([name, ruleConfig]) => {
return [
'react-compiler/' + name,
mapErrorSeverityToESlint(ruleConfig.severity),
];
}),
) as Record<string, Linter.StringSeverity>,
},
};
const rules = Object.fromEntries(
Object.entries(allRules).map(([name, {rule}]) => [name, rule]),
);
export {configs, rules, meta};

View File

@@ -14,7 +14,7 @@ import {
import type {Linter, Rule} from 'eslint';
import runReactCompiler, {RunCacheEntry} from '../shared/RunReactCompiler';
import {
ErrorSeverity,
ErrorCategory,
LintRulePreset,
LintRules,
type LintRule,
@@ -108,14 +108,15 @@ function hasFlowSuppression(
return false;
}
function makeRule(rule: LintRule): Rule.RuleModule {
function makeRule(rules: Array<LintRule>): Rule.RuleModule {
const categories = new Set(rules.map(rule => rule.category));
const create = (context: Rule.RuleContext): Rule.RuleListener => {
const result = getReactCompilerResult(context);
for (const event of result.events) {
if (event.kind === 'CompileError') {
const detail = event.detail;
if (detail.category === rule.category) {
if (categories.has(detail.category)) {
const loc = detail.primaryLocation();
if (loc == null || typeof loc === 'symbol') {
continue;
@@ -150,8 +151,8 @@ function makeRule(rule: LintRule): Rule.RuleModule {
meta: {
type: 'problem',
docs: {
description: rule.description,
recommended: rule.preset === LintRulePreset.Recommended,
description: 'React Compiler diagnostics',
recommended: true,
},
fixable: 'code',
hasSuggestions: true,
@@ -162,47 +163,13 @@ function makeRule(rule: LintRule): Rule.RuleModule {
};
}
type RulesConfig = {
[name: string]: {rule: Rule.RuleModule; severity: ErrorSeverity};
};
export default makeRule(
LintRules.filter(
rule =>
rule.preset === LintRulePreset.Recommended ||
rule.preset === LintRulePreset.RecommendedLatest ||
rule.category === ErrorCategory.CapitalizedCalls,
),
);
export const allRules: RulesConfig = LintRules.reduce((acc, rule) => {
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
return acc;
}, {} as RulesConfig);
export const recommendedRules: RulesConfig = LintRules.filter(
rule => rule.preset === LintRulePreset.Recommended,
).reduce((acc, rule) => {
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
return acc;
}, {} as RulesConfig);
export const recommendedLatestRules: RulesConfig = LintRules.filter(
rule =>
rule.preset === LintRulePreset.Recommended ||
rule.preset === LintRulePreset.RecommendedLatest,
).reduce((acc, rule) => {
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
return acc;
}, {} as RulesConfig);
export function mapErrorSeverityToESlint(
severity: ErrorSeverity,
): Linter.StringSeverity {
switch (severity) {
case ErrorSeverity.Error: {
return 'error';
}
case ErrorSeverity.Warning: {
return 'warn';
}
case ErrorSeverity.Hint:
case ErrorSeverity.Off: {
return 'off';
}
default: {
assertExhaustive(severity, `Unhandled severity: ${severity}`);
}
}
}
export const AllRules = makeRule(LintRules);

View File

@@ -36,14 +36,13 @@
},
"scripts": {
"build": "yarn run compile",
"build:compiler": "yarn workspace babel-plugin-react-compiler build --dts",
"compile": "rimraf dist && concurrently -n server,client \"scripts/build.mjs -t server\" \"scripts/build.mjs -t client\"",
"dev": "yarn run package && yarn run install-ext",
"install-ext": "code --install-extension react-forgive-0.0.0.vsix",
"lint": "echo 'no tests'",
"package": "rm -f react-forgive-0.0.0.vsix && vsce package --yarn",
"postinstall": "cd client && yarn install && cd ../server && yarn install && cd ..",
"pretest": "yarn run build:compiler && yarn run compile && yarn run lint",
"pretest": "yarn run compile && yarn run lint",
"test": "vscode-test",
"vscode:prepublish": "yarn run compile",
"watch": "scripts/build.mjs --watch"

View File

@@ -18,6 +18,7 @@
"@babel/parser": "^7.26.0",
"@babel/plugin-syntax-typescript": "^7.25.9",
"@babel/types": "^7.26.0",
"cosmiconfig": "^9.0.0",
"prettier": "^3.3.3",
"vscode-languageserver": "^9.0.1",
"vscode-languageserver-textdocument": "^1.0.12"

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {type SourceLocation} from 'babel-plugin-react-compiler';
import {SourceLocation} from 'babel-plugin-react-compiler/src';
import {type Range} from 'vscode-languageserver';
export function babelLocationToRange(loc: SourceLocation): Range | null {

View File

@@ -9,7 +9,7 @@ import type * as BabelCore from '@babel/core';
import {parseAsync, transformFromAstAsync} from '@babel/core';
import BabelPluginReactCompiler, {
type PluginOptions,
} from 'babel-plugin-react-compiler';
} from 'babel-plugin-react-compiler/src';
import * as babelParser from 'prettier/plugins/babel.js';
import estreeParser from 'prettier/plugins/estree';
import * as typescriptParser from 'prettier/plugins/typescript';

View File

@@ -0,0 +1,25 @@
/**
* 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 {
parsePluginOptions,
type PluginOptions,
} from 'babel-plugin-react-compiler/src';
import {cosmiconfigSync} from 'cosmiconfig';
export function resolveReactConfig(projectPath: string): PluginOptions | null {
const explorerSync = cosmiconfigSync('react', {
searchStrategy: 'project',
cache: true,
});
const result = explorerSync.search(projectPath);
if (result != null) {
return parsePluginOptions(result.config);
} else {
return null;
}
}

View File

@@ -20,12 +20,13 @@ import {
TextDocumentSyncKind,
} from 'vscode-languageserver/node';
import {compile, lastResult} from './compiler';
import {type PluginOptions} from 'babel-plugin-react-compiler/src';
import {resolveReactConfig} from './compiler/options';
import {
type CompileSuccessEvent,
type LoggerEvent,
type PluginOptions,
defaultOptions,
} from 'babel-plugin-react-compiler';
} from 'babel-plugin-react-compiler/src/Entrypoint/Options';
import {babelLocationToRange, getRangeFirstCharacter} from './compiler/compat';
import {
type AutoDepsDecorationsLSPEvent,
@@ -63,7 +64,8 @@ type CodeActionLSPEvent = {
};
connection.onInitialize((_params: InitializeParams) => {
compilerOptions = defaultOptions;
// TODO(@poteto) get config fr
compilerOptions = resolveReactConfig('.') ?? defaultOptions;
compilerOptions = {
...compilerOptions,
environment: {
@@ -74,21 +76,21 @@ connection.onInitialize((_params: InitializeParams) => {
importSpecifierName: 'useEffect',
source: 'react',
},
autodepsIndex: 1,
numRequiredArgs: 1,
},
{
function: {
importSpecifierName: 'useSpecialEffect',
source: 'shared-runtime',
},
autodepsIndex: 2,
numRequiredArgs: 2,
},
{
function: {
importSpecifierName: 'default',
source: 'useEffectWrapper',
},
autodepsIndex: 1,
numRequiredArgs: 1,
},
],
},

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {type AutoDepsDecorationsEvent} from 'babel-plugin-react-compiler';
import {type AutoDepsDecorationsEvent} from 'babel-plugin-react-compiler/src/Entrypoint';
import {type Position} from 'vscode-languageserver-textdocument';
import {RequestType} from 'vscode-languageserver/node';
import {type Range, sourceLocationToRange} from '../utils/range';

View File

@@ -10,7 +10,7 @@
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.24"
"@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0", "@babel/code-frame@^7.26.2":
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0", "@babel/code-frame@^7.26.2":
version "7.26.2"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85"
integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==
@@ -188,6 +188,11 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
argparse@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
browserslist@^4.24.0:
version "4.24.3"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.3.tgz#5fc2725ca8fb3c1432e13dac278c7cc103e026d2"
@@ -198,6 +203,11 @@ browserslist@^4.24.0:
node-releases "^2.0.19"
update-browserslist-db "^1.1.1"
callsites@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
caniuse-lite@^1.0.30001688:
version "1.0.30001690"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz#f2d15e3aaf8e18f76b2b8c1481abde063b8104c8"
@@ -208,6 +218,16 @@ convert-source-map@^2.0.0:
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
cosmiconfig@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-9.0.0.tgz#34c3fc58287b915f3ae905ab6dc3de258b55ad9d"
integrity sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==
dependencies:
env-paths "^2.2.1"
import-fresh "^3.3.0"
js-yaml "^4.1.0"
parse-json "^5.2.0"
debug@^4.1.0, debug@^4.3.1:
version "4.4.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
@@ -220,6 +240,18 @@ electron-to-chromium@^1.5.73:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.74.tgz#cb886b504a6467e4c00bea3317edb38393c53413"
integrity sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw==
env-paths@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"
integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==
error-ex@^1.3.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
dependencies:
is-arrayish "^0.2.1"
escalade@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
@@ -235,21 +267,51 @@ globals@^11.1.0:
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
import-fresh@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
dependencies:
parent-module "^1.0.0"
resolve-from "^4.0.0"
is-arrayish@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==
js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
js-yaml@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
dependencies:
argparse "^2.0.1"
jsesc@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d"
integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==
json-parse-even-better-errors@^2.3.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
json5@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
lines-and-columns@^1.1.6:
version "1.2.4"
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
lru-cache@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
@@ -267,6 +329,23 @@ node-releases@^2.0.19:
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314"
integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==
parent-module@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
dependencies:
callsites "^3.0.0"
parse-json@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==
dependencies:
"@babel/code-frame" "^7.0.0"
error-ex "^1.3.1"
json-parse-even-better-errors "^2.3.0"
lines-and-columns "^1.1.6"
picocolors@^1.0.0, picocolors@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
@@ -277,6 +356,11 @@ prettier@^3.3.3:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.4.2.tgz#a5ce1fb522a588bf2b78ca44c6e6fe5aa5a2b13f"
integrity sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==
resolve-from@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
semver@^6.3.1:
version "6.3.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"

View File

@@ -152,7 +152,6 @@
"download-build-in-codesandbox-ci": "yarn build --type=node react/index react.react-server react-dom/index react-dom/client react-dom/src/server react-dom/test-utils react-dom.react-server scheduler/index react/jsx-runtime react/jsx-dev-runtime react-server-dom-webpack",
"check-release-dependencies": "node ./scripts/release/check-release-dependencies",
"generate-inline-fizz-runtime": "node ./scripts/rollup/generate-inline-fizz-runtime.js",
"generate-changelog": "node ./scripts/tasks/generate-changelog/index.js",
"flags": "node ./scripts/flags/flags.js"
},
"resolutions": {

View File

@@ -1,10 +1,3 @@
## 7.0.1
- Disallowed passing inline `useEffectEvent` values as JSX props to guard against accidental propagation. ([#34820](https://github.com/facebook/react/pull/34820) by [@jf-eirinha](https://github.com/jf-eirinha))
- Switch to `export =` so eslint-plugin-react-hooks emits correct types for consumers in Node16 ESM projects. ([#34949](https://github.com/facebook/react/pull/34949) by [@karlhorky](https://github.com/karlhorky))
- Tightened the typing of `configs.flat` so the `configs` export is always defined. ([#34950](https://github.com/facebook/react/pull/34950) by [@poteto](https://github.com/poteto))
- Fix named import runtime errors. ([#34951](https://github.com/facebook/react/pull/34951), [#34953](https://github.com/facebook/react/pull/34953) by [@karlhorky](https://github.com/karlhorky))
## 7.0.0
This release slims down presets to just 2 configurations (`recommended` and `recommended-latest`), and all compiler rules are enabled by default.

View File

@@ -585,29 +585,6 @@ const allTests = {
code: normalizeIndent`
// Valid: useEffectEvent can be called in custom effect hooks configured via ESLint settings
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
useMyEffect(() => {
onClick();
});
useServerEffect(() => {
onClick();
});
}
`,
settings: {
'react-hooks': {
additionalEffectHooks: '(useMyEffect|useServerEffect)',
},
},
},
{
syntax: 'flow',
code: normalizeIndent`
// Component syntax version
// Valid: useEffectEvent can be called in custom effect hooks configured via ESLint settings
component MyComponent(theme: any) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
@@ -641,24 +618,6 @@ const allTests = {
}
`,
},
{
syntax: 'flow',
code: normalizeIndent`
// Component syntax version
// Valid because functions created with useEffectEvent can be called in a useEffect.
component MyComponent(theme: any) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
useEffect(() => {
onClick();
});
React.useEffect(() => {
onClick();
});
}
`,
},
{
code: normalizeIndent`
// Valid because functions created with useEffectEvent can be passed by reference in useEffect
@@ -685,34 +644,6 @@ const allTests = {
}
`,
},
{
syntax: 'flow',
code: normalizeIndent`
// Component syntax version
// Valid because functions created with useEffectEvent can be passed by reference in useEffect
// and useEffectEvent.
component MyComponent(theme: any) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
const onClick2 = useEffectEvent(() => {
debounce(onClick);
debounce(() => onClick());
debounce(() => { onClick() });
deboucne(() => debounce(onClick));
});
useEffect(() => {
let id = setInterval(() => onClick(), 100);
return () => clearInterval(onClick);
}, []);
React.useEffect(() => {
let id = setInterval(() => onClick(), 100);
return () => clearInterval(onClick);
}, []);
return null;
}
`,
},
{
code: normalizeIndent`
function MyComponent({ theme }) {
@@ -725,20 +656,6 @@ const allTests = {
}
`,
},
{
syntax: 'flow',
code: normalizeIndent`
// Component syntax version
component MyComponent(theme: any) {
useEffect(() => {
onClick();
});
const onClick = useEffectEvent(() => {
showNotification(theme);
});
}
`,
},
{
code: normalizeIndent`
function MyComponent({ theme }) {
@@ -756,25 +673,6 @@ const allTests = {
}
`,
},
{
syntax: 'flow',
code: normalizeIndent`
// Component syntax version
component MyComponent(theme: any) {
// Can receive arguments
const onEvent = useEffectEvent((text) => {
console.log(text);
});
useEffect(() => {
onEvent('Hello world');
});
React.useEffect(() => {
onEvent('Hello world');
});
}
`,
},
{
code: normalizeIndent`
// Valid because functions created with useEffectEvent can be called in useLayoutEffect.
@@ -791,24 +689,6 @@ const allTests = {
}
`,
},
{
syntax: 'flow',
code: normalizeIndent`
// Component syntax version
// Valid because functions created with useEffectEvent can be called in useLayoutEffect.
component MyComponent(theme: any) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
useLayoutEffect(() => {
onClick();
});
React.useLayoutEffect(() => {
onClick();
});
}
`,
},
{
code: normalizeIndent`
// Valid because functions created with useEffectEvent can be called in useInsertionEffect.
@@ -825,24 +705,6 @@ const allTests = {
}
`,
},
{
syntax: 'flow',
code: normalizeIndent`
// Component syntax version
// Valid because functions created with useEffectEvent can be called in useInsertionEffect.
component MyComponent(theme) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
useInsertionEffect(() => {
onClick();
});
React.useInsertionEffect(() => {
onClick();
});
}
`,
},
{
code: normalizeIndent`
// Valid because functions created with useEffectEvent can be passed by reference in useLayoutEffect
@@ -877,42 +739,6 @@ const allTests = {
}
`,
},
{
syntax: 'flow',
code: normalizeIndent`
// Component syntax version
// Valid because functions created with useEffectEvent can be passed by reference in useLayoutEffect.
// and useInsertionEffect.
component MyComponent(theme: any) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
const onClick2 = useEffectEvent(() => {
debounce(onClick);
debounce(() => onClick());
debounce(() => { onClick() });
deboucne(() => debounce(onClick));
});
useLayoutEffect(() => {
let id = setInterval(() => onClick(), 100);
return () => clearInterval(onClick);
}, []);
React.useLayoutEffect(() => {
let id = setInterval(() => onClick(), 100);
return () => clearInterval(onClick);
}, []);
useInsertionEffect(() => {
let id = setInterval(() => onClick(), 100);
return () => clearInterval(onClick);
}, []);
React.useInsertionEffect(() => {
let id = setInterval(() => onClick(), 100);
return () => clearInterval(onClick);
}, []);
return null;
}
`,
},
],
invalid: [
{
@@ -1699,22 +1525,6 @@ const allTests = {
`,
errors: [useEffectEventError('onClick', true)],
},
{
syntax: 'flow',
code: normalizeIndent`
// Component syntax version
// Invalid: useEffectEvent should not be callable in regular custom hooks without additional configuration
component MyComponent() {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
useCustomHook(() => {
onClick();
});
}
`,
errors: [useEffectEventError('onClick', true)],
},
{
code: normalizeIndent`
// Invalid: useEffectEvent should not be callable in hooks not matching the settings regex
@@ -1734,27 +1544,6 @@ const allTests = {
},
errors: [useEffectEventError('onClick', true)],
},
{
syntax: 'flow',
code: normalizeIndent`
// Component syntax version
// Invalid: useEffectEvent should not be callable in hooks not matching the settings regex
component MyComponent(theme: any) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
useWrongHook(() => {
onClick();
});
}
`,
settings: {
'react-hooks': {
additionalEffectHooks: 'useMyEffect',
},
},
errors: [useEffectEventError('onClick', true)],
},
{
code: normalizeIndent`
function MyComponent({ theme }) {
@@ -1766,19 +1555,6 @@ const allTests = {
`,
errors: [useEffectEventError('onClick', false)],
},
{
syntax: 'flow',
code: normalizeIndent`
// Component syntax version
component MyComponent(theme: any) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
return <Child onClick={onClick}></Child>;
}
`,
errors: [useEffectEventError('onClick', false)],
},
{
code: normalizeIndent`
// Invalid because useEffectEvent is being passed down
@@ -1790,19 +1566,6 @@ const allTests = {
`,
errors: [{...useEffectEventError(null, false), line: 4}],
},
{
syntax: 'flow',
code: normalizeIndent`
// Component syntax version
// Invalid because useEffectEvent is being passed down
component MyComponent(theme: any) {
return <Child onClick={useEffectEvent(() => {
showNotification(theme);
})} />;
}
`,
errors: [{...useEffectEventError(null, false), line: 5}],
},
{
code: normalizeIndent`
// This should error even though it shares an identifier name with the below
@@ -1838,43 +1601,6 @@ const allTests = {
{...useEffectEventError('onClick', true), line: 15},
],
},
{
syntax: 'flow',
code: normalizeIndent`
// Component syntax version
// This should error even though it shares an identifier name with the below
component MyComponent(theme: any) {
const onClick = useEffectEvent(() => {
showNotification(theme)
});
return <Child onClick={onClick} />
}
// The useEffectEvent function shares an identifier name with the above
component MyOtherComponent(theme: any) {
const onClick = useEffectEvent(() => {
showNotification(theme)
});
return <Child onClick={() => onClick()} />
}
// The useEffectEvent function shares an identifier name with the above
component MyLastComponent(theme: any) {
const onClick = useEffectEvent(() => {
showNotification(theme)
});
useEffect(() => {
onClick(); // No error here, errors on all other uses
onClick;
})
return <Child />
}
`,
errors: [
{...useEffectEventError('onClick', false), line: 8},
{...useEffectEventError('onClick', true), line: 16},
],
},
{
code: normalizeIndent`
const MyComponent = ({ theme }) => {
@@ -1899,21 +1625,6 @@ const allTests = {
`,
errors: [{...useEffectEventError('onClick', false), line: 7}],
},
{
syntax: 'flow',
code: normalizeIndent`
// Component syntax version
// Invalid because onClick is being aliased to foo but not invoked
component MyComponent(theme: any) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
let foo = onClick;
return <Bar onClick={foo} />
}
`,
errors: [{...useEffectEventError('onClick', false), line: 8}],
},
{
code: normalizeIndent`
// Should error because it's being passed down to JSX, although it's been referenced once
@@ -1930,24 +1641,6 @@ const allTests = {
`,
errors: [useEffectEventError('onClick', false)],
},
{
syntax: 'flow',
code: normalizeIndent`
// Component syntax version
// Should error because it's being passed down to JSX, although it's been referenced once
// in an effect
component MyComponent(theme: any) {
const onClick = useEffectEvent(() => {
showNotification(them);
});
useEffect(() => {
setTimeout(onClick, 100);
});
return <Child onClick={onClick} />
}
`,
errors: [useEffectEventError('onClick', false)],
},
{
code: normalizeIndent`
// Invalid because functions created with useEffectEvent cannot be called in arbitrary closures.
@@ -1983,43 +1676,6 @@ const allTests = {
`It cannot be assigned to a variable or passed down.`,
],
},
{
syntax: 'flow',
code: normalizeIndent`
// Hook syntax version
// Invalid because functions created with useEffectEvent cannot be called in arbitrary closures.
hook useMyHook(theme: any) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
// error message 1
const onClick2 = () => { onClick() };
// error message 2
const onClick3 = useCallback(() => onClick(), []);
// error message 3
const onClick4 = onClick;
return <>
{/** error message 4 */}
<Child onClick={onClick}></Child>
<Child onClick={onClick2}></Child>
<Child onClick={onClick3}></Child>
</>;
}
`,
// Explicitly test error messages here for various cases
errors: [
`\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
'Effects and Effect Events in the same component.',
`\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
'Effects and Effect Events in the same component.',
`\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
`Effects and Effect Events in the same component. ` +
`It cannot be assigned to a variable or passed down.`,
`\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
`Effects and Effect Events in the same component. ` +
`It cannot be assigned to a variable or passed down.`,
],
},
],
};

View File

@@ -5,6 +5,4 @@
* LICENSE file in the root directory of this source tree.
*/
import reactHooks from './cjs/eslint-plugin-react-hooks';
export = reactHooks;
export {default} from './cjs/eslint-plugin-react-hooks';

View File

@@ -14,13 +14,3 @@ if (process.env.NODE_ENV === 'production') {
} else {
module.exports = require('./cjs/eslint-plugin-react-hooks.development.js');
}
// Hint to Nodes cjs-module-lexer to make named imports work
// https://github.com/facebook/react/issues/34801#issuecomment-3433478810
// eslint-disable-next-line ft-flow/no-unused-expressions
0 &&
(module.exports = {
meta: true,
rules: true,
configs: true,
});

View File

@@ -71,10 +71,7 @@ const configs = {
plugins,
rules: recommendedLatestRuleConfigs,
},
flat: {} as {
recommended: ReactHooksFlatConfig;
'recommended-latest': ReactHooksFlatConfig;
},
flat: {} as Record<string, ReactHooksFlatConfig>,
};
const plugin = {

View File

@@ -833,18 +833,6 @@ const rule = {
recordAllUseEffectEventFunctions(getScope(node));
}
},
// @ts-expect-error parser-hermes produces these node types
ComponentDeclaration(node) {
// component MyComponent() { const onClick = useEffectEvent(...) }
recordAllUseEffectEventFunctions(getScope(node));
},
// @ts-expect-error parser-hermes produces these node types
HookDeclaration(node) {
// hook useMyHook() { const onClick = useEffectEvent(...) }
recordAllUseEffectEventFunctions(getScope(node));
},
};
},
} satisfies Rule.RuleModule;

View File

@@ -64,7 +64,7 @@ function normalizeIOInfo(config: DebugInfoConfig, ioInfo) {
if (promise) {
promise.then(); // init
if (promise.status === 'fulfilled') {
if (ioInfo.name === 'rsc stream') {
if (ioInfo.name === 'RSC stream') {
copy.byteSize = 0;
copy.value = {
value: 'stream',
@@ -117,7 +117,7 @@ export function getDebugInfo(config: DebugInfoConfig, obj) {
for (let i = 0; i < debugInfo.length; i++) {
if (
debugInfo[i].awaited &&
debugInfo[i].awaited.name === 'rsc stream' &&
debugInfo[i].awaited.name === 'RSC stream' &&
config.ignoreRscStreamInfo
) {
// Ignore RSC stream I/O info.

View File

@@ -39,9 +39,12 @@ import type {
EncodeFormActionCallback,
} from './ReactFlightReplyClient';
import type {Postpone} from 'react/src/ReactPostpone';
import type {TemporaryReferenceSet} from './ReactFlightTemporaryReferences';
import {
enablePostpone,
enableProfilerTimer,
enableComponentPerformanceTrack,
enableAsyncDebugInfo,
@@ -86,6 +89,7 @@ import {
import {
REACT_LAZY_TYPE,
REACT_ELEMENT_TYPE,
REACT_POSTPONE_TYPE,
ASYNC_ITERATOR,
REACT_FRAGMENT_TYPE,
} from 'shared/ReactSymbols';
@@ -363,7 +367,6 @@ type Response = {
_debugRootStack?: null | Error, // DEV-only
_debugRootTask?: null | ConsoleTask, // DEV-only
_debugStartTime: number, // DEV-only
_debugEndTime?: number, // DEV-only
_debugIOStarted: boolean, // DEV-only
_debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only
_debugChannel?: void | DebugChannel, // DEV-only
@@ -497,33 +500,6 @@ function createErrorChunk<T>(
return new ReactPromise(ERRORED, null, error);
}
function filterDebugInfo(
response: Response,
value: {_debugInfo: ReactDebugInfo, ...},
) {
if (response._debugEndTime === null) {
// No end time was defined, so we keep all debug info entries.
return;
}
// Remove any debug info entries after the defined end time. For async info
// that means we're including anything that was awaited before the end time,
// but it doesn't need to be resolved before the end time.
const relativeEndTime =
response._debugEndTime -
// $FlowFixMe[prop-missing]
performance.timeOrigin;
const debugInfo = [];
for (let i = 0; i < value._debugInfo.length; i++) {
const info = value._debugInfo[i];
if (typeof info.time === 'number' && info.time > relativeEndTime) {
break;
}
debugInfo.push(info);
}
value._debugInfo = debugInfo;
}
function moveDebugInfoFromChunkToInnerValue<T>(
chunk: InitializedChunk<T> | InitializedStreamChunk<any>,
value: T,
@@ -558,17 +534,7 @@ function moveDebugInfoFromChunkToInnerValue<T>(
}
}
function processChunkDebugInfo<T>(
response: Response,
chunk: InitializedChunk<T> | InitializedStreamChunk<any>,
value: T,
): void {
filterDebugInfo(response, chunk);
moveDebugInfoFromChunkToInnerValue(chunk, value);
}
function wakeChunk<T>(
response: Response,
listeners: Array<InitializationReference | (T => mixed)>,
value: T,
chunk: InitializedChunk<T>,
@@ -578,17 +544,16 @@ function wakeChunk<T>(
if (typeof listener === 'function') {
listener(value);
} else {
fulfillReference(response, listener, value, chunk);
fulfillReference(listener, value, chunk);
}
}
if (__DEV__) {
processChunkDebugInfo(response, chunk, value);
moveDebugInfoFromChunkToInnerValue(chunk, value);
}
}
function rejectChunk(
response: Response,
listeners: Array<InitializationReference | (mixed => mixed)>,
error: mixed,
): void {
@@ -597,7 +562,7 @@ function rejectChunk(
if (typeof listener === 'function') {
listener(error);
} else {
rejectReference(response, listener.handler, error);
rejectReference(listener, error);
}
}
}
@@ -630,14 +595,13 @@ function resolveBlockedCycle<T>(
}
function wakeChunkIfInitialized<T>(
response: Response,
chunk: SomeChunk<T>,
resolveListeners: Array<InitializationReference | (T => mixed)>,
rejectListeners: null | Array<InitializationReference | (mixed => mixed)>,
): void {
switch (chunk.status) {
case INITIALIZED:
wakeChunk(response, resolveListeners, chunk.value, chunk);
wakeChunk(resolveListeners, chunk.value, chunk);
break;
case BLOCKED:
// It is possible that we're blocked on our own chunk if it's a cycle.
@@ -651,7 +615,7 @@ function wakeChunkIfInitialized<T>(
if (cyclicHandler !== null) {
// This reference points back to this chunk. We can resolve the cycle by
// using the value from that handler.
fulfillReference(response, reference, cyclicHandler.value, chunk);
fulfillReference(reference, cyclicHandler.value, chunk);
resolveListeners.splice(i, 1);
i--;
if (rejectListeners !== null) {
@@ -660,23 +624,6 @@ function wakeChunkIfInitialized<T>(
rejectListeners.splice(rejectionIdx, 1);
}
}
// The status might have changed after fulfilling the reference.
switch ((chunk: SomeChunk<T>).status) {
case INITIALIZED:
const initializedChunk: InitializedChunk<T> = (chunk: any);
wakeChunk(
response,
resolveListeners,
initializedChunk.value,
initializedChunk,
);
return;
case ERRORED:
if (rejectListeners !== null) {
rejectChunk(response, rejectListeners, chunk.reason);
}
return;
}
}
}
}
@@ -703,7 +650,7 @@ function wakeChunkIfInitialized<T>(
break;
case ERRORED:
if (rejectListeners) {
rejectChunk(response, rejectListeners, chunk.reason);
rejectChunk(rejectListeners, chunk.reason);
}
break;
}
@@ -761,7 +708,7 @@ function triggerErrorOnChunk<T>(
erroredChunk.status = ERRORED;
erroredChunk.reason = error;
if (listeners !== null) {
rejectChunk(response, listeners, error);
rejectChunk(listeners, error);
}
}
@@ -869,7 +816,7 @@ function resolveModelChunk<T>(
// longer be rendered or might not be the highest pri.
initializeModelChunk(resolvedChunk);
// The status might have changed after initialization.
wakeChunkIfInitialized(response, chunk, resolveListeners, rejectListeners);
wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners);
}
}
@@ -898,11 +845,12 @@ function resolveModuleChunk<T>(
}
if (resolveListeners !== null) {
initializeModuleChunk(resolvedChunk);
wakeChunkIfInitialized(response, chunk, resolveListeners, rejectListeners);
wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners);
}
}
type InitializationReference = {
response: Response, // TODO: Remove Response from here and pass it through instead.
handler: InitializationHandler,
parentObject: Object,
key: string,
@@ -1041,7 +989,7 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
if (typeof listener === 'function') {
listener(value);
} else {
fulfillReference(response, listener, value, cyclicChunk);
fulfillReference(listener, value, cyclicChunk);
}
}
}
@@ -1062,7 +1010,7 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
initializedChunk.value = value;
if (__DEV__) {
processChunkDebugInfo(response, initializedChunk, value);
moveDebugInfoFromChunkToInnerValue(initializedChunk, value);
}
} catch (error) {
const erroredChunk: ErroredChunk<T> = (chunk: any);
@@ -1449,12 +1397,11 @@ function getChunk(response: Response, id: number): SomeChunk<any> {
}
function fulfillReference(
response: Response,
reference: InitializationReference,
value: any,
fulfilledChunk: SomeChunk<any>,
): void {
const {handler, parentObject, key, map, path} = reference;
const {response, handler, parentObject, key, map, path} = reference;
for (let i = 1; i < path.length; i++) {
while (
@@ -1524,11 +1471,7 @@ function fulfillReference(
return;
}
default: {
rejectReference(
response,
reference.handler,
referencedChunk.reason,
);
rejectReference(reference, referencedChunk.reason);
return;
}
}
@@ -1626,20 +1569,21 @@ function fulfillReference(
initializedChunk.value = handler.value;
initializedChunk.reason = handler.reason; // Used by streaming chunks
if (resolveListeners !== null) {
wakeChunk(response, resolveListeners, handler.value, initializedChunk);
wakeChunk(resolveListeners, handler.value, initializedChunk);
} else {
if (__DEV__) {
processChunkDebugInfo(response, initializedChunk, handler.value);
moveDebugInfoFromChunkToInnerValue(initializedChunk, handler.value);
}
}
}
}
function rejectReference(
response: Response,
handler: InitializationHandler,
reference: InitializationReference,
error: mixed,
): void {
const {handler, response} = reference;
if (handler.errored) {
// We've already errored. We could instead build up an AggregateError
// but if there are multiple errors we just take the first one like
@@ -1730,6 +1674,7 @@ function waitForReference<T>(
}
const reference: InitializationReference = {
response,
handler,
parentObject,
key,
@@ -1877,10 +1822,10 @@ function loadServerReference<A: Iterable<any>, T>(
initializedChunk.status = INITIALIZED;
initializedChunk.value = handler.value;
if (resolveListeners !== null) {
wakeChunk(response, resolveListeners, handler.value, initializedChunk);
wakeChunk(resolveListeners, handler.value, initializedChunk);
} else {
if (__DEV__) {
processChunkDebugInfo(response, initializedChunk, handler.value);
moveDebugInfoFromChunkToInnerValue(initializedChunk, handler.value);
}
}
}
@@ -2616,8 +2561,6 @@ function ResponseInstance(
findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only
replayConsole: boolean, // DEV-only
environmentName: void | string, // DEV-only
debugStartTime: void | number, // DEV-only
debugEndTime: void | number, // DEV-only
debugChannel: void | DebugChannel, // DEV-only
) {
const chunks: Map<number, SomeChunk<any>> = new Map();
@@ -2678,14 +2621,12 @@ function ResponseInstance(
// Note: createFromFetch allows this to be marked at the start of the fetch
// where as if you use createFromReadableStream from the body of the fetch
// then the start time is when the headers resolved.
this._debugStartTime =
debugStartTime == null ? performance.now() : debugStartTime;
this._debugStartTime = performance.now();
this._debugIOStarted = false;
// We consider everything before the first setTimeout task to be cached data
// and is not considered I/O required to load the stream.
setTimeout(markIOStarted.bind(this), 0);
}
this._debugEndTime = debugEndTime == null ? null : debugEndTime;
this._debugFindSourceMapURL = findSourceMapURL;
this._debugChannel = debugChannel;
this._blockedConsole = null;
@@ -2728,8 +2669,6 @@ export function createResponse(
findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only
replayConsole: boolean, // DEV-only
environmentName: void | string, // DEV-only
debugStartTime: void | number, // DEV-only
debugEndTime: void | number, // DEV-only
debugChannel: void | DebugChannel, // DEV-only
): WeakResponse {
return getWeakResponse(
@@ -2745,8 +2684,6 @@ export function createResponse(
findSourceMapURL,
replayConsole,
environmentName,
debugStartTime,
debugEndTime,
debugChannel,
),
);
@@ -2780,7 +2717,7 @@ export function createStreamState(
(debugValuePromise: any).status = 'fulfilled';
(debugValuePromise: any).value = streamDebugValue;
streamState._debugInfo = {
name: 'rsc stream',
name: 'RSC stream',
start: response._debugStartTime,
end: response._debugStartTime, // will be updated once we finish a chunk
byteSize: 0, // will be updated as we resolve a data chunk
@@ -3118,10 +3055,10 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
resolvedChunk.value = stream;
resolvedChunk.reason = controller;
if (resolveListeners !== null) {
wakeChunk(response, resolveListeners, chunk.value, (chunk: any));
wakeChunk(resolveListeners, chunk.value, (chunk: any));
} else {
if (__DEV__) {
processChunkDebugInfo(response, resolvedChunk, stream);
moveDebugInfoFromChunkToInnerValue(resolvedChunk, stream);
}
}
}
@@ -3261,12 +3198,7 @@ function startAsyncIterable<T>(
initializedChunk.status = INITIALIZED;
initializedChunk.value = {done: false, value: value};
if (resolveListeners !== null) {
wakeChunkIfInitialized(
response,
chunk,
resolveListeners,
rejectListeners,
);
wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners);
}
}
nextWriteIndex++;
@@ -3456,6 +3388,88 @@ function resolveErrorDev(
return error;
}
function resolvePostponeProd(
response: Response,
id: number,
streamState: StreamState,
): void {
if (__DEV__) {
// These errors should never make it into a build so we don't need to encode them in codes.json
// eslint-disable-next-line react-internal/prod-error-codes
throw new Error(
'resolvePostponeProd should never be called in development mode. Use resolvePostponeDev instead. This is a bug in React.',
);
}
const error = new Error(
'A Server Component was postponed. The reason is omitted in production' +
' builds to avoid leaking sensitive details.',
);
const postponeInstance: Postpone = (error: any);
postponeInstance.$$typeof = REACT_POSTPONE_TYPE;
postponeInstance.stack = 'Error: ' + error.message;
const chunks = response._chunks;
const chunk = chunks.get(id);
if (!chunk) {
const newChunk: ErroredChunk<any> = createErrorChunk(
response,
postponeInstance,
);
chunks.set(id, newChunk);
} else {
triggerErrorOnChunk(response, chunk, postponeInstance);
}
}
function resolvePostponeDev(
response: Response,
id: number,
reason: string,
stack: ReactStackTrace,
env: string,
streamState: StreamState,
): void {
if (!__DEV__) {
// These errors should never make it into a build so we don't need to encode them in codes.json
// eslint-disable-next-line react-internal/prod-error-codes
throw new Error(
'resolvePostponeDev should never be called in production mode. Use resolvePostponeProd instead. This is a bug in React.',
);
}
let postponeInstance: Postpone;
const callStack = buildFakeCallStack(
response,
stack,
env,
false,
// $FlowFixMe[incompatible-use]
Error.bind(null, reason || ''),
);
const rootTask = response._debugRootTask;
if (rootTask != null) {
postponeInstance = rootTask.run(callStack);
} else {
postponeInstance = callStack();
}
postponeInstance.$$typeof = REACT_POSTPONE_TYPE;
const chunks = response._chunks;
const chunk = chunks.get(id);
if (!chunk) {
const newChunk: ErroredChunk<any> = createErrorChunk(
response,
postponeInstance,
);
if (__DEV__) {
resolveChunkDebugInfo(response, streamState, newChunk);
}
chunks.set(id, newChunk);
} else {
if (__DEV__) {
resolveChunkDebugInfo(response, streamState, chunk);
}
triggerErrorOnChunk(response, chunk, postponeInstance);
}
}
function resolveErrorModel(
response: Response,
id: number,
@@ -4807,6 +4821,25 @@ function processFullStringRow(
return;
}
// Fallthrough
case 80 /* "P" */: {
if (enablePostpone) {
if (__DEV__) {
const postponeInfo = JSON.parse(row);
resolvePostponeDev(
response,
id,
postponeInfo.reason,
postponeInfo.stack,
postponeInfo.env,
streamState,
);
} else {
resolvePostponeProd(response, id, streamState);
}
return;
}
}
// Fallthrough
default: /* """ "{" "[" "t" "f" "n" "0" - "9" */ {
if (__DEV__ && row === '') {
resolveDebugHalt(response, id);

View File

@@ -1,6 +1,6 @@
{
"name": "react-devtools-core",
"version": "7.0.1",
"version": "7.0.0",
"description": "Use react-devtools outside of the browser",
"license": "MIT",
"main": "./dist/backend.js",

View File

@@ -2,8 +2,8 @@
"manifest_version": 3,
"name": "React Developer Tools",
"description": "Adds React debugging tools to the Chrome Developer Tools.",
"version": "7.0.1",
"version_name": "7.0.1",
"version": "7.0.0",
"version_name": "7.0.0",
"minimum_chrome_version": "114",
"icons": {
"16": "icons/16-production.png",

View File

@@ -2,8 +2,8 @@
"manifest_version": 3,
"name": "React Developer Tools",
"description": "Adds React debugging tools to the Microsoft Edge Developer Tools.",
"version": "7.0.1",
"version_name": "7.0.1",
"version": "7.0.0",
"version_name": "7.0.0",
"minimum_chrome_version": "114",
"icons": {
"16": "icons/16-production.png",

View File

@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "React Developer Tools",
"description": "Adds React debugging tools to the Firefox Developer Tools.",
"version": "7.0.1",
"version": "7.0.0",
"browser_specific_settings": {
"gecko": {
"id": "@react-devtools",

View File

@@ -1,14 +1,5 @@
/**
* 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.
*
* @flow
*/
export function cloneStyleTags(): Array<HTMLLinkElement | HTMLStyleElement> {
const tags: Array<HTMLLinkElement | HTMLStyleElement> = [];
function cloneStyleTags() {
const linkTags = [];
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const linkTag of document.getElementsByTagName('link')) {
@@ -20,23 +11,11 @@ export function cloneStyleTags(): Array<HTMLLinkElement | HTMLStyleElement> {
newLinkTag.setAttribute(attribute.nodeName, attribute.nodeValue);
}
tags.push(newLinkTag);
linkTags.push(newLinkTag);
}
}
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const styleTag of document.getElementsByTagName('style')) {
const newStyleTag = document.createElement('style');
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const attribute of styleTag.attributes) {
newStyleTag.setAttribute(attribute.nodeName, attribute.nodeValue);
}
newStyleTag.textContent = styleTag.textContent;
tags.push(newStyleTag);
}
return tags;
return linkTags;
}
export default cloneStyleTags;

View File

@@ -33,7 +33,7 @@ import {
import {viewAttributeSource} from './sourceSelection';
import {startReactPolling} from './reactPolling';
import {cloneStyleTags} from './cloneStyleTags';
import cloneStyleTags from './cloneStyleTags';
import fetchFileWithCaching from './fetchFileWithCaching';
import injectBackendManager from './injectBackendManager';
import registerEventsLogger from './registerEventsLogger';

View File

@@ -6,7 +6,7 @@
*/
const {execSync} = require('child_process');
const {existsSync, readFileSync} = require('fs');
const {readFileSync} = require('fs');
const {resolve} = require('path');
const GITHUB_URL = 'https://github.com/facebook/react';
@@ -18,26 +18,8 @@ function getGitCommit() {
.trim();
} catch (error) {
// Mozilla runs this command from a git archive.
// In that context, there is no Git context.
// Using the commit hash specified to download-experimental-build.js script as a fallback.
// Try to read from build/COMMIT_SHA file
const commitShaPath = resolve(__dirname, '..', '..', 'build', 'COMMIT_SHA');
if (!existsSync(commitShaPath)) {
throw new Error(
'Could not find build/COMMIT_SHA file. Did you run scripts/release/download-experimental-build.js script?',
);
}
try {
const commitHash = readFileSync(commitShaPath, 'utf8').trim();
// Return short hash (first 7 characters) to match abbreviated commit hash format
return commitHash.slice(0, 7);
} catch (readError) {
throw new Error(
`Failed to read build/COMMIT_SHA file: ${readError.message}`,
);
}
// In that context, there is no Git revision.
return null;
}
}

View File

@@ -285,7 +285,7 @@ module.exports = {
{
loader: 'css-loader',
options: {
sourceMap: __DEV__,
sourceMap: true,
modules: true,
localIdentName: '[local]___[hash:base64:5]',
},

View File

@@ -1,6 +1,6 @@
{
"name": "react-devtools-inline",
"version": "7.0.1",
"version": "7.0.0",
"description": "Embed react-devtools within a website",
"license": "MIT",
"main": "./dist/backend.js",

View File

@@ -23,7 +23,6 @@
"json5": "^2.2.3",
"local-storage-fallback": "^4.1.1",
"react-virtualized-auto-sizer": "^1.0.23",
"react-window": "^1.8.10",
"rbush": "4.0.1"
"react-window": "^1.8.10"
}
}

View File

@@ -27,7 +27,7 @@ describe('Bridge', () => {
// Check that we're wired up correctly.
bridge.send('reloadAppForProfiling');
jest.runAllTimers();
expect(wall.send).toHaveBeenCalledWith('reloadAppForProfiling', undefined);
expect(wall.send).toHaveBeenCalledWith('reloadAppForProfiling');
// Should flush pending messages and then shut down.
wall.send.mockClear();
@@ -37,7 +37,7 @@ describe('Bridge', () => {
jest.runAllTimers();
expect(wall.send).toHaveBeenCalledWith('update', '1');
expect(wall.send).toHaveBeenCalledWith('update', '2');
expect(wall.send).toHaveBeenCalledWith('shutdown', undefined);
expect(wall.send).toHaveBeenCalledWith('shutdown');
expect(shutdownCallback).toHaveBeenCalledTimes(1);
// Verify that the Bridge doesn't send messages after shutdown.

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