main
ts 842 lines 23.1 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 import {Result} from '../Utils/Result';
9 import {CompilerDiagnostic, CompilerError, Effect} from '..';
10 import {ErrorCategory} from '../CompilerError';
11 import {
12 BlockId,
13 FunctionExpression,
14 HIRFunction,
15 IdentifierId,
16 isSetStateType,
17 isUseEffectHookType,
18 Place,
19 CallExpression,
20 Instruction,
21 isUseStateType,
22 BasicBlock,
23 isUseRefType,
24 SourceLocation,
25 ArrayExpression,
26 } from '../HIR';
27 import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
28 import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
29 import {assertExhaustive} from '../Utils/utils';
30
31 type TypeOfValue = 'ignored' | 'fromProps' | 'fromState' | 'fromPropsAndState';
32
33 type DerivationMetadata = {
34 typeOfValue: TypeOfValue;
35 place: Place;
36 sourcesIds: Set<IdentifierId>;
37 isStateSource: boolean;
38 };
39
40 type EffectMetadata = {
41 effect: HIRFunction;
42 dependencies: ArrayExpression;
43 };
44
45 type ValidationContext = {
46 readonly functions: Map<IdentifierId, FunctionExpression>;
47 readonly candidateDependencies: Map<IdentifierId, ArrayExpression>;
48 readonly errors: CompilerError;
49 readonly derivationCache: DerivationCache;
50 readonly effectsCache: Map<IdentifierId, EffectMetadata>;
51 readonly setStateLoads: Map<IdentifierId, IdentifierId | null>;
52 readonly setStateUsages: Map<IdentifierId, Set<SourceLocation>>;
53 };
54
55 const MAX_FIXPOINT_ITERATIONS = 100;
56
57 class DerivationCache {
58 hasChanges: boolean = false;
59 cache: Map<IdentifierId, DerivationMetadata> = new Map();
60 private previousCache: Map<IdentifierId, DerivationMetadata> | null = null;
61
62 takeSnapshot(): void {
63 this.previousCache = new Map();
64 for (const [key, value] of this.cache.entries()) {
65 this.previousCache.set(key, {
66 place: value.place,
67 sourcesIds: new Set(value.sourcesIds),
68 typeOfValue: value.typeOfValue,
69 isStateSource: value.isStateSource,
70 });
71 }
72 }
73
74 checkForChanges(): void {
75 if (this.previousCache === null) {
76 this.hasChanges = true;
77 return;
78 }
79
80 for (const [key, value] of this.cache.entries()) {
81 const previousValue = this.previousCache.get(key);
82 if (
83 previousValue === undefined ||
84 !this.isDerivationEqual(previousValue, value)
85 ) {
86 this.hasChanges = true;
87 return;
88 }
89 }
90
91 if (this.cache.size !== this.previousCache.size) {
92 this.hasChanges = true;
93 return;
94 }
95
96 this.hasChanges = false;
97 }
98
99 snapshot(): boolean {
100 const hasChanges = this.hasChanges;
101 this.hasChanges = false;
102 return hasChanges;
103 }
104
105 addDerivationEntry(
106 derivedVar: Place,
107 sourcesIds: Set<IdentifierId>,
108 typeOfValue: TypeOfValue,
109 isStateSource: boolean,
110 ): void {
111 let finalIsSource = isStateSource;
112 if (!finalIsSource) {
113 for (const sourceId of sourcesIds) {
114 const sourceMetadata = this.cache.get(sourceId);
115 if (
116 sourceMetadata?.isStateSource &&
117 sourceMetadata.place.identifier.name?.kind !== 'named'
118 ) {
119 finalIsSource = true;
120 break;
121 }
122 }
123 }
124
125 this.cache.set(derivedVar.identifier.id, {
126 place: derivedVar,
127 sourcesIds: sourcesIds,
128 typeOfValue: typeOfValue ?? 'ignored',
129 isStateSource: finalIsSource,
130 });
131 }
132
133 private isDerivationEqual(
134 a: DerivationMetadata,
135 b: DerivationMetadata,
136 ): boolean {
137 if (a.typeOfValue !== b.typeOfValue) {
138 return false;
139 }
140 if (a.sourcesIds.size !== b.sourcesIds.size) {
141 return false;
142 }
143 for (const id of a.sourcesIds) {
144 if (!b.sourcesIds.has(id)) {
145 return false;
146 }
147 }
148 return true;
149 }
150 }
151
152 function isNamedIdentifier(place: Place): place is Place & {
153 identifier: {name: NonNullable<Place['identifier']['name']>};
154 } {
155 return (
156 place.identifier.name !== null && place.identifier.name.kind === 'named'
157 );
158 }
159
160 /**
161 * Validates that useEffect is not used for derived computations which could/should
162 * be performed in render.
163 *
164 * See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state
165 *
166 * Example:
167 *
168 * ```
169 * // 🔴 Avoid: redundant state and unnecessary Effect
170 * const [fullName, setFullName] = useState('');
171 * useEffect(() => {
172 * setFullName(firstName + ' ' + lastName);
173 * }, [firstName, lastName]);
174 * ```
175 *
176 * Instead use:
177 *
178 * ```
179 * // ✅ Good: calculated during rendering
180 * const fullName = firstName + ' ' + lastName;
181 * ```
182 */
183 export function validateNoDerivedComputationsInEffects_exp(
184 fn: HIRFunction,
185 ): Result<void, CompilerError> {
186 const functions: Map<IdentifierId, FunctionExpression> = new Map();
187 const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
188 const derivationCache = new DerivationCache();
189 const errors = new CompilerError();
190 const effectsCache: Map<IdentifierId, EffectMetadata> = new Map();
191
192 const setStateLoads: Map<IdentifierId, IdentifierId> = new Map();
193 const setStateUsages: Map<IdentifierId, Set<SourceLocation>> = new Map();
194
195 const context: ValidationContext = {
196 functions,
197 candidateDependencies,
198 errors,
199 derivationCache,
200 effectsCache,
201 setStateLoads,
202 setStateUsages,
203 };
204
205 if (fn.fnType === 'Hook') {
206 for (const param of fn.params) {
207 if (param.kind === 'Identifier') {
208 context.derivationCache.cache.set(param.identifier.id, {
209 place: param,
210 sourcesIds: new Set(),
211 typeOfValue: 'fromProps',
212 isStateSource: true,
213 });
214 }
215 }
216 } else if (fn.fnType === 'Component') {
217 const props = fn.params[0];
218 if (props != null && props.kind === 'Identifier') {
219 context.derivationCache.cache.set(props.identifier.id, {
220 place: props,
221 sourcesIds: new Set(),
222 typeOfValue: 'fromProps',
223 isStateSource: true,
224 });
225 }
226 }
227
228 let isFirstPass = true;
229 let iterationCount = 0;
230 do {
231 context.derivationCache.takeSnapshot();
232
233 for (const block of fn.body.blocks.values()) {
234 recordPhiDerivations(block, context);
235 for (const instr of block.instructions) {
236 recordInstructionDerivations(instr, context, isFirstPass);
237 }
238 }
239
240 context.derivationCache.checkForChanges();
241 isFirstPass = false;
242 iterationCount++;
243 CompilerError.invariant(iterationCount < MAX_FIXPOINT_ITERATIONS, {
244 reason:
245 '[ValidateNoDerivedComputationsInEffects] Fixpoint iteration failed to converge.',
246 description: `Fixpoint iteration exceeded ${MAX_FIXPOINT_ITERATIONS} iterations while tracking derivations. This suggests a cyclic dependency in the derivation cache.`,
247 loc: fn.loc,
248 });
249 } while (context.derivationCache.snapshot());
250
251 for (const [, effect] of effectsCache) {
252 validateEffect(effect.effect, effect.dependencies, context);
253 }
254
255 return errors.asResult();
256 }
257
258 function recordPhiDerivations(
259 block: BasicBlock,
260 context: ValidationContext,
261 ): void {
262 for (const phi of block.phis) {
263 let typeOfValue: TypeOfValue = 'ignored';
264 let sourcesIds: Set<IdentifierId> = new Set();
265 for (const operand of phi.operands.values()) {
266 const operandMetadata = context.derivationCache.cache.get(
267 operand.identifier.id,
268 );
269
270 if (operandMetadata === undefined) {
271 continue;
272 }
273
274 typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
275 sourcesIds.add(operand.identifier.id);
276 }
277
278 if (typeOfValue !== 'ignored') {
279 context.derivationCache.addDerivationEntry(
280 phi.place,
281 sourcesIds,
282 typeOfValue,
283 false,
284 );
285 }
286 }
287 }
288
289 function joinValue(
290 lvalueType: TypeOfValue,
291 valueType: TypeOfValue,
292 ): TypeOfValue {
293 if (lvalueType === 'ignored') return valueType;
294 if (valueType === 'ignored') return lvalueType;
295 if (lvalueType === valueType) return lvalueType;
296 return 'fromPropsAndState';
297 }
298
299 function getRootSetState(
300 key: IdentifierId,
301 loads: Map<IdentifierId, IdentifierId | null>,
302 visited: Set<IdentifierId> = new Set(),
303 ): IdentifierId | null {
304 if (visited.has(key)) {
305 return null;
306 }
307 visited.add(key);
308
309 const parentId = loads.get(key);
310
311 if (parentId === undefined) {
312 return null;
313 }
314
315 if (parentId === null) {
316 return key;
317 }
318
319 return getRootSetState(parentId, loads, visited);
320 }
321
322 function maybeRecordSetState(
323 instr: Instruction,
324 loads: Map<IdentifierId, IdentifierId | null>,
325 usages: Map<IdentifierId, Set<SourceLocation>>,
326 ): void {
327 for (const operand of eachInstructionLValue(instr)) {
328 if (
329 instr.value.kind === 'LoadLocal' &&
330 loads.has(instr.value.place.identifier.id)
331 ) {
332 loads.set(operand.identifier.id, instr.value.place.identifier.id);
333 } else {
334 if (isSetStateType(operand.identifier)) {
335 // this is a root setState
336 loads.set(operand.identifier.id, null);
337 }
338 }
339
340 const rootSetState = getRootSetState(operand.identifier.id, loads);
341 if (rootSetState !== null && usages.get(rootSetState) === undefined) {
342 usages.set(rootSetState, new Set([operand.loc]));
343 }
344 }
345 }
346
347 function recordInstructionDerivations(
348 instr: Instruction,
349 context: ValidationContext,
350 isFirstPass: boolean,
351 ): void {
352 maybeRecordSetState(instr, context.setStateLoads, context.setStateUsages);
353
354 let typeOfValue: TypeOfValue = 'ignored';
355 let isSource: boolean = false;
356 const sources: Set<IdentifierId> = new Set();
357 const {lvalue, value} = instr;
358 if (value.kind === 'FunctionExpression') {
359 context.functions.set(lvalue.identifier.id, value);
360 for (const [, block] of value.loweredFunc.func.body.blocks) {
361 recordPhiDerivations(block, context);
362 for (const instr of block.instructions) {
363 recordInstructionDerivations(instr, context, isFirstPass);
364 }
365 }
366 } else if (value.kind === 'CallExpression' || value.kind === 'MethodCall') {
367 const callee =
368 value.kind === 'CallExpression' ? value.callee : value.property;
369 if (
370 isUseEffectHookType(callee.identifier) &&
371 value.args.length === 2 &&
372 value.args[0].kind === 'Identifier' &&
373 value.args[1].kind === 'Identifier'
374 ) {
375 const effectFunction = context.functions.get(value.args[0].identifier.id);
376 const deps = context.candidateDependencies.get(
377 value.args[1].identifier.id,
378 );
379 if (effectFunction != null && deps != null) {
380 context.effectsCache.set(value.args[0].identifier.id, {
381 effect: effectFunction.loweredFunc.func,
382 dependencies: deps,
383 });
384 }
385 } else if (isUseStateType(lvalue.identifier)) {
386 typeOfValue = 'fromState';
387 context.derivationCache.addDerivationEntry(
388 lvalue,
389 new Set(),
390 typeOfValue,
391 true,
392 );
393 return;
394 }
395 } else if (value.kind === 'ArrayExpression') {
396 context.candidateDependencies.set(lvalue.identifier.id, value);
397 }
398
399 for (const operand of eachInstructionOperand(instr)) {
400 if (context.setStateLoads.has(operand.identifier.id)) {
401 const rootSetStateId = getRootSetState(
402 operand.identifier.id,
403 context.setStateLoads,
404 );
405 if (rootSetStateId !== null) {
406 context.setStateUsages.get(rootSetStateId)?.add(operand.loc);
407 }
408 }
409
410 const operandMetadata = context.derivationCache.cache.get(
411 operand.identifier.id,
412 );
413
414 if (operandMetadata === undefined) {
415 continue;
416 }
417
418 typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
419 sources.add(operand.identifier.id);
420 }
421
422 if (typeOfValue === 'ignored') {
423 return;
424 }
425
426 for (const lvalue of eachInstructionLValue(instr)) {
427 context.derivationCache.addDerivationEntry(
428 lvalue,
429 sources,
430 typeOfValue,
431 isSource,
432 );
433 }
434
435 if (value.kind === 'FunctionExpression') {
436 /*
437 * We don't want to record effect mutations of FunctionExpressions the mutations will happen in the
438 * function body and we will record them there.
439 */
440 return;
441 }
442
443 for (const operand of eachInstructionOperand(instr)) {
444 switch (operand.effect) {
445 case Effect.Capture:
446 case Effect.Store:
447 case Effect.ConditionallyMutate:
448 case Effect.ConditionallyMutateIterator:
449 case Effect.Mutate: {
450 if (isMutable(instr, operand)) {
451 if (context.derivationCache.cache.has(operand.identifier.id)) {
452 const operandMetadata = context.derivationCache.cache.get(
453 operand.identifier.id,
454 );
455
456 if (operandMetadata !== undefined) {
457 operandMetadata.typeOfValue = joinValue(
458 typeOfValue,
459 operandMetadata.typeOfValue,
460 );
461 }
462 } else {
463 context.derivationCache.addDerivationEntry(
464 operand,
465 sources,
466 typeOfValue,
467 false,
468 );
469 }
470 }
471 break;
472 }
473 case Effect.Freeze:
474 case Effect.Read: {
475 // no-op
476 break;
477 }
478 case Effect.Unknown: {
479 CompilerError.invariant(false, {
480 reason: 'Unexpected unknown effect',
481 loc: operand.loc,
482 });
483 }
484 default: {
485 assertExhaustive(
486 operand.effect,
487 `Unexpected effect kind \`${operand.effect}\``,
488 );
489 }
490 }
491 }
492 }
493
494 type TreeNode = {
495 name: string;
496 typeOfValue: TypeOfValue;
497 isSource: boolean;
498 children: Array<TreeNode>;
499 };
500
501 function buildTreeNode(
502 sourceId: IdentifierId,
503 context: ValidationContext,
504 visited: Set<string> = new Set(),
505 ): Array<TreeNode> {
506 const sourceMetadata = context.derivationCache.cache.get(sourceId);
507 if (!sourceMetadata) {
508 return [];
509 }
510
511 if (sourceMetadata.isStateSource && isNamedIdentifier(sourceMetadata.place)) {
512 return [
513 {
514 name: sourceMetadata.place.identifier.name.value,
515 typeOfValue: sourceMetadata.typeOfValue,
516 isSource: sourceMetadata.isStateSource,
517 children: [],
518 },
519 ];
520 }
521
522 const children: Array<TreeNode> = [];
523
524 const namedSiblings: Set<string> = new Set();
525 for (const childId of sourceMetadata.sourcesIds) {
526 CompilerError.invariant(childId !== sourceId, {
527 reason:
528 'Unexpected self-reference: a value should not have itself as a source',
529 loc: sourceMetadata.place.loc,
530 });
531
532 const childNodes = buildTreeNode(
533 childId,
534 context,
535 new Set([
536 ...visited,
537 ...(isNamedIdentifier(sourceMetadata.place)
538 ? [sourceMetadata.place.identifier.name.value]
539 : []),
540 ]),
541 );
542 if (childNodes) {
543 for (const childNode of childNodes) {
544 if (!namedSiblings.has(childNode.name)) {
545 children.push(childNode);
546 namedSiblings.add(childNode.name);
547 }
548 }
549 }
550 }
551
552 if (
553 isNamedIdentifier(sourceMetadata.place) &&
554 !visited.has(sourceMetadata.place.identifier.name.value)
555 ) {
556 return [
557 {
558 name: sourceMetadata.place.identifier.name.value,
559 typeOfValue: sourceMetadata.typeOfValue,
560 isSource: sourceMetadata.isStateSource,
561 children: children,
562 },
563 ];
564 }
565
566 return children;
567 }
568
569 function renderTree(
570 node: TreeNode,
571 indent: string = '',
572 isLast: boolean = true,
573 propsSet: Set<string>,
574 stateSet: Set<string>,
575 ): string {
576 const prefix = indent + (isLast ? '└── ' : '├── ');
577 const childIndent = indent + (isLast ? ' ' : '');
578
579 let result = `${prefix}${node.name}`;
580
581 if (node.isSource) {
582 let typeLabel: string;
583 if (node.typeOfValue === 'fromProps') {
584 propsSet.add(node.name);
585 typeLabel = 'Prop';
586 } else if (node.typeOfValue === 'fromState') {
587 stateSet.add(node.name);
588 typeLabel = 'State';
589 } else {
590 propsSet.add(node.name);
591 stateSet.add(node.name);
592 typeLabel = 'Prop and State';
593 }
594 result += ` (${typeLabel})`;
595 }
596
597 if (node.children.length > 0) {
598 result += '\n';
599 node.children.forEach((child, index) => {
600 const isLastChild = index === node.children.length - 1;
601 result += renderTree(child, childIndent, isLastChild, propsSet, stateSet);
602 if (index < node.children.length - 1) {
603 result += '\n';
604 }
605 });
606 }
607
608 return result;
609 }
610
611 function getFnLocalDeps(
612 fn: FunctionExpression | undefined,
613 ): Set<IdentifierId> | undefined {
614 if (!fn) {
615 return undefined;
616 }
617
618 const deps: Set<IdentifierId> = new Set();
619
620 for (const [, block] of fn.loweredFunc.func.body.blocks) {
621 for (const instr of block.instructions) {
622 if (instr.value.kind === 'LoadLocal') {
623 deps.add(instr.value.place.identifier.id);
624 }
625 }
626 }
627
628 return deps;
629 }
630
631 function validateEffect(
632 effectFunction: HIRFunction,
633 dependencies: ArrayExpression,
634 context: ValidationContext,
635 ): void {
636 const seenBlocks: Set<BlockId> = new Set();
637
638 const effectDerivedSetStateCalls: Array<{
639 value: CallExpression;
640 id: IdentifierId;
641 sourceIds: Set<IdentifierId>;
642 typeOfValue: TypeOfValue;
643 }> = [];
644
645 const effectSetStateUsages: Map<
646 IdentifierId,
647 Set<SourceLocation>
648 > = new Map();
649
650 // Consider setStates in the effect's dependency array as being part of effectSetStateUsages
651 for (const dep of dependencies.elements) {
652 if (dep.kind === 'Identifier') {
653 const root = getRootSetState(dep.identifier.id, context.setStateLoads);
654 if (root !== null) {
655 effectSetStateUsages.set(root, new Set([dep.loc]));
656 }
657 }
658 }
659
660 let cleanUpFunctionDeps: Set<IdentifierId> | undefined;
661
662 const globals: Set<IdentifierId> = new Set();
663 for (const block of effectFunction.body.blocks.values()) {
664 /*
665 * if the block is in an effect and is of type return then its an effect's cleanup function
666 * if the cleanup function depends on a value from which effect-set state is derived then
667 * we can't validate
668 */
669 if (
670 block.terminal.kind === 'return' &&
671 block.terminal.returnVariant === 'Explicit'
672 ) {
673 cleanUpFunctionDeps = getFnLocalDeps(
674 context.functions.get(block.terminal.value.identifier.id),
675 );
676 }
677 for (const pred of block.preds) {
678 if (!seenBlocks.has(pred)) {
679 // skip if block has a back edge
680 return;
681 }
682 }
683
684 for (const instr of block.instructions) {
685 // Early return if any instruction is deriving a value from a ref
686 if (isUseRefType(instr.lvalue.identifier)) {
687 return;
688 }
689
690 maybeRecordSetState(instr, context.setStateLoads, effectSetStateUsages);
691
692 for (const operand of eachInstructionOperand(instr)) {
693 if (context.setStateLoads.has(operand.identifier.id)) {
694 const rootSetStateId = getRootSetState(
695 operand.identifier.id,
696 context.setStateLoads,
697 );
698 if (rootSetStateId !== null) {
699 effectSetStateUsages.get(rootSetStateId)?.add(operand.loc);
700 }
701 }
702 }
703
704 if (
705 instr.value.kind === 'CallExpression' &&
706 isSetStateType(instr.value.callee.identifier) &&
707 instr.value.args.length === 1 &&
708 instr.value.args[0].kind === 'Identifier'
709 ) {
710 const calleeMetadata = context.derivationCache.cache.get(
711 instr.value.callee.identifier.id,
712 );
713
714 /*
715 * If the setState comes from a source other than local state skip
716 * since the fix is not to calculate in render
717 */
718 if (calleeMetadata?.typeOfValue != 'fromState') {
719 continue;
720 }
721
722 const argMetadata = context.derivationCache.cache.get(
723 instr.value.args[0].identifier.id,
724 );
725
726 if (argMetadata !== undefined) {
727 effectDerivedSetStateCalls.push({
728 value: instr.value,
729 id: instr.value.callee.identifier.id,
730 sourceIds: argMetadata.sourcesIds,
731 typeOfValue: argMetadata.typeOfValue,
732 });
733 }
734 } else if (instr.value.kind === 'CallExpression') {
735 const calleeMetadata = context.derivationCache.cache.get(
736 instr.value.callee.identifier.id,
737 );
738
739 if (
740 calleeMetadata !== undefined &&
741 (calleeMetadata.typeOfValue === 'fromProps' ||
742 calleeMetadata.typeOfValue === 'fromPropsAndState')
743 ) {
744 // If the callee is a prop we can't confidently say that it should be derived in render
745 return;
746 }
747
748 if (globals.has(instr.value.callee.identifier.id)) {
749 // If the callee is a global we can't confidently say that it should be derived in render
750 return;
751 }
752 } else if (instr.value.kind === 'LoadGlobal') {
753 globals.add(instr.lvalue.identifier.id);
754 for (const operand of eachInstructionOperand(instr)) {
755 globals.add(operand.identifier.id);
756 }
757 }
758 }
759 seenBlocks.add(block.id);
760 }
761
762 for (const derivedSetStateCall of effectDerivedSetStateCalls) {
763 const rootSetStateCall = getRootSetState(
764 derivedSetStateCall.id,
765 context.setStateLoads,
766 );
767
768 if (
769 rootSetStateCall !== null &&
770 effectSetStateUsages.has(rootSetStateCall) &&
771 context.setStateUsages.has(rootSetStateCall) &&
772 effectSetStateUsages.get(rootSetStateCall)!.size ===
773 context.setStateUsages.get(rootSetStateCall)!.size - 1
774 ) {
775 const propsSet = new Set<string>();
776 const stateSet = new Set<string>();
777
778 const rootNodesMap = new Map<string, TreeNode>();
779 for (const id of derivedSetStateCall.sourceIds) {
780 const nodes = buildTreeNode(id, context);
781 for (const node of nodes) {
782 if (!rootNodesMap.has(node.name)) {
783 rootNodesMap.set(node.name, node);
784 }
785 }
786 }
787 const rootNodes = Array.from(rootNodesMap.values());
788
789 const trees = rootNodes.map((node, index) =>
790 renderTree(
791 node,
792 '',
793 index === rootNodes.length - 1,
794 propsSet,
795 stateSet,
796 ),
797 );
798
799 for (const dep of derivedSetStateCall.sourceIds) {
800 if (cleanUpFunctionDeps !== undefined && cleanUpFunctionDeps.has(dep)) {
801 return;
802 }
803 }
804
805 const propsArr = Array.from(propsSet);
806 const stateArr = Array.from(stateSet);
807
808 let rootSources = '';
809 if (propsArr.length > 0) {
810 rootSources += `Props: [${propsArr.join(', ')}]`;
811 }
812 if (stateArr.length > 0) {
813 if (rootSources) rootSources += '\n';
814 rootSources += `State: [${stateArr.join(', ')}]`;
815 }
816
817 const description = `Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
818
819 This setState call is setting a derived value that depends on the following reactive sources:
820
821 ${rootSources}
822
823 Data Flow Tree:
824 ${trees.join('\n')}
825
826 See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state`;
827
828 context.errors.pushDiagnostic(
829 CompilerDiagnostic.create({
830 description: description,
831 category: ErrorCategory.EffectDerivationsOfState,
832 reason:
833 'You might not need an effect. Derive values in render, not effects.',
834 }).withDetails({
835 kind: 'error',
836 loc: derivedSetStateCall.value.callee.loc,
837 message: 'This should be computed during render, not in an effect',
838 }),
839 );
840 }
841 }
842 }