@@ -30871,6 +30871,7 @@ const EnvironmentConfigSchema = zod.z.object({
3087130871 validateRefAccessDuringRender: zod.z.boolean().default(true),
3087230872 validateNoSetStateInRender: zod.z.boolean().default(true),
3087330873 validateNoSetStateInEffects: zod.z.boolean().default(false),
30874+ validateNoDerivedComputationsInEffects: zod.z.boolean().default(false),
3087430875 validateNoJSXInTryStatements: zod.z.boolean().default(false),
3087530876 validateStaticComponents: zod.z.boolean().default(false),
3087630877 validateMemoizedEffectDependencies: zod.z.boolean().default(false),
@@ -51050,6 +51051,167 @@ function validateNoFreezingKnownMutableFunctions(fn) {
5105051051 return errors.asResult();
5105151052}
5105251053
51054+ function validateNoDerivedComputationsInEffects(fn) {
51055+ const candidateDependencies = new Map();
51056+ const functions = new Map();
51057+ const locals = new Map();
51058+ const errors = new CompilerError();
51059+ for (const block of fn.body.blocks.values()) {
51060+ for (const instr of block.instructions) {
51061+ const { lvalue, value } = instr;
51062+ if (value.kind === 'LoadLocal') {
51063+ locals.set(lvalue.identifier.id, value.place.identifier.id);
51064+ }
51065+ else if (value.kind === 'ArrayExpression') {
51066+ candidateDependencies.set(lvalue.identifier.id, value);
51067+ }
51068+ else if (value.kind === 'FunctionExpression') {
51069+ functions.set(lvalue.identifier.id, value);
51070+ }
51071+ else if (value.kind === 'CallExpression' ||
51072+ value.kind === 'MethodCall') {
51073+ const callee = value.kind === 'CallExpression' ? value.callee : value.property;
51074+ if (isUseEffectHookType(callee.identifier) &&
51075+ value.args.length === 2 &&
51076+ value.args[0].kind === 'Identifier' &&
51077+ value.args[1].kind === 'Identifier') {
51078+ const effectFunction = functions.get(value.args[0].identifier.id);
51079+ const deps = candidateDependencies.get(value.args[1].identifier.id);
51080+ if (effectFunction != null &&
51081+ deps != null &&
51082+ deps.elements.length !== 0 &&
51083+ deps.elements.every(element => element.kind === 'Identifier')) {
51084+ const dependencies = deps.elements.map(dep => {
51085+ var _a;
51086+ CompilerError.invariant(dep.kind === 'Identifier', {
51087+ reason: `Dependency is checked as a place above`,
51088+ loc: value.loc,
51089+ });
51090+ return (_a = locals.get(dep.identifier.id)) !== null && _a !== void 0 ? _a : dep.identifier.id;
51091+ });
51092+ validateEffect(effectFunction.loweredFunc.func, dependencies, errors);
51093+ }
51094+ }
51095+ }
51096+ }
51097+ }
51098+ if (errors.hasErrors()) {
51099+ throw errors;
51100+ }
51101+ }
51102+ function validateEffect(effectFunction, effectDeps, errors) {
51103+ for (const operand of effectFunction.context) {
51104+ if (isSetStateType(operand.identifier)) {
51105+ continue;
51106+ }
51107+ else if (effectDeps.find(dep => dep === operand.identifier.id) != null) {
51108+ continue;
51109+ }
51110+ else {
51111+ return;
51112+ }
51113+ }
51114+ for (const dep of effectDeps) {
51115+ if (effectFunction.context.find(operand => operand.identifier.id === dep) ==
51116+ null) {
51117+ return;
51118+ }
51119+ }
51120+ const seenBlocks = new Set();
51121+ const values = new Map();
51122+ for (const dep of effectDeps) {
51123+ values.set(dep, [dep]);
51124+ }
51125+ const setStateLocations = [];
51126+ for (const block of effectFunction.body.blocks.values()) {
51127+ for (const pred of block.preds) {
51128+ if (!seenBlocks.has(pred)) {
51129+ return;
51130+ }
51131+ }
51132+ for (const phi of block.phis) {
51133+ const aggregateDeps = new Set();
51134+ for (const operand of phi.operands.values()) {
51135+ const deps = values.get(operand.identifier.id);
51136+ if (deps != null) {
51137+ for (const dep of deps) {
51138+ aggregateDeps.add(dep);
51139+ }
51140+ }
51141+ }
51142+ if (aggregateDeps.size !== 0) {
51143+ values.set(phi.place.identifier.id, Array.from(aggregateDeps));
51144+ }
51145+ }
51146+ for (const instr of block.instructions) {
51147+ switch (instr.value.kind) {
51148+ case 'Primitive':
51149+ case 'JSXText':
51150+ case 'LoadGlobal': {
51151+ break;
51152+ }
51153+ case 'LoadLocal': {
51154+ const deps = values.get(instr.value.place.identifier.id);
51155+ if (deps != null) {
51156+ values.set(instr.lvalue.identifier.id, deps);
51157+ }
51158+ break;
51159+ }
51160+ case 'ComputedLoad':
51161+ case 'PropertyLoad':
51162+ case 'BinaryExpression':
51163+ case 'TemplateLiteral':
51164+ case 'CallExpression':
51165+ case 'MethodCall': {
51166+ const aggregateDeps = new Set();
51167+ for (const operand of eachInstructionValueOperand(instr.value)) {
51168+ const deps = values.get(operand.identifier.id);
51169+ if (deps != null) {
51170+ for (const dep of deps) {
51171+ aggregateDeps.add(dep);
51172+ }
51173+ }
51174+ }
51175+ if (aggregateDeps.size !== 0) {
51176+ values.set(instr.lvalue.identifier.id, Array.from(aggregateDeps));
51177+ }
51178+ if (instr.value.kind === 'CallExpression' &&
51179+ isSetStateType(instr.value.callee.identifier) &&
51180+ instr.value.args.length === 1 &&
51181+ instr.value.args[0].kind === 'Identifier') {
51182+ const deps = values.get(instr.value.args[0].identifier.id);
51183+ if (deps != null && new Set(deps).size === effectDeps.length) {
51184+ setStateLocations.push(instr.value.callee.loc);
51185+ }
51186+ else {
51187+ return;
51188+ }
51189+ }
51190+ break;
51191+ }
51192+ default: {
51193+ return;
51194+ }
51195+ }
51196+ }
51197+ for (const operand of eachTerminalOperand(block.terminal)) {
51198+ if (values.has(operand.identifier.id)) {
51199+ return;
51200+ }
51201+ }
51202+ seenBlocks.add(block.id);
51203+ }
51204+ for (const loc of setStateLocations) {
51205+ errors.push({
51206+ reason: '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)',
51207+ description: null,
51208+ severity: ErrorSeverity.InvalidReact,
51209+ loc,
51210+ suggestions: null,
51211+ });
51212+ }
51213+ }
51214+
5105351215function run(func, config, fnType, mode, programContext, logger, filename, code) {
5105451216 var _a, _b;
5105551217 const contextIdentifiers = findContextIdentifiers(func);
@@ -51172,6 +51334,9 @@ function runWithEnvironment(func, env) {
5117251334 if (env.config.validateNoSetStateInRender) {
5117351335 validateNoSetStateInRender(hir).unwrap();
5117451336 }
51337+ if (env.config.validateNoDerivedComputationsInEffects) {
51338+ validateNoDerivedComputationsInEffects(hir);
51339+ }
5117551340 if (env.config.validateNoSetStateInEffects) {
5117651341 env.logErrors(validateNoSetStateInEffects(hir));
5117751342 }
0 commit comments