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 {CompilerError, Effect, ErrorSeverity} from '..';
9
+import {
10
+ FunctionEffect,
11
+ HIRFunction,
12
+ IdentifierId,
13
+ isMutableEffect,
14
+ isRefOrRefLikeMutableType,
15
+ Place,
16
+} from '../HIR';
17
+import {
18
+ eachInstructionValueOperand,
19
+ eachTerminalOperand,
20
+} from '../HIR/visitors';
21
+import {Result} from '../Utils/Result';
22
+import {Iterable_some} from '../Utils/utils';
23
+
24
+/**
25
+ * Validates that functions with known mutations (ie due to types) cannot be passed
26
+ * where a frozen value is expected. Example:
27
+ *
28
+ * ```
29
+ * function Component() {
30
+ * const cache = new Map();
31
+ * const onClick = () => {
32
+ * cache.set(...);
33
+ * }
34
+ * useHook(onClick); // ERROR: cannot pass a mutable value
35
+ * return <Foo onClick={onClick} /> // ERROR: cannot pass a mutable value
36
+ * }
37
+ * ```
38
+ *
39
+ * Because `onClick` function mutates `cache` when called, `onClick` is equivalent to a mutable
40
+ * variables. But unlike other mutables values like an array, the receiver of the function has
41
+ * no way to avoid mutation — for example, a function can receive an array and choose not to mutate
42
+ * it, but there's no way to know that a function is mutable and avoid calling it.
43
+ *
44
+ * This pass detects functions with *known* mutations (Store or Mutate, not ConditionallyMutate)
45
+ * that are passed where a frozen value is expected and rejects them.
46
+ */
47
+export function validateNoFreezingKnownMutableFunctions(
48
+ fn: HIRFunction,
49
+): Result<void, CompilerError> {
50
+ const errors = new CompilerError();
51
+ const contextMutationEffects: Map<
52
+ IdentifierId,
53
+ Extract<FunctionEffect, {kind: 'ContextMutation'}>
54
+ > = new Map();
55
+
56
+ function visitOperand(operand: Place): void {
57
+ if (operand.effect === Effect.Freeze) {
58
+ const effect = contextMutationEffects.get(operand.identifier.id);
59
+ if (effect != null) {
60
+ errors.push({
61
+ reason: `This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update`,
62
+ description: `Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables`,
63
+ loc: operand.loc,
64
+ severity: ErrorSeverity.InvalidReact,
65
+ });
66
+ errors.push({
67
+ reason: `The function modifies a local variable here`,
68
+ loc: effect.loc,
69
+ severity: ErrorSeverity.InvalidReact,
70
+ });
71
+ }
72
+ }
73
+ }
74
+
75
+ for (const block of fn.body.blocks.values()) {
76
+ for (const instr of block.instructions) {
77
+ const {lvalue, value} = instr;
78
+ switch (value.kind) {
79
+ case 'LoadLocal': {
80
+ const effect = contextMutationEffects.get(value.place.identifier.id);
81
+ if (effect != null) {
82
+ contextMutationEffects.set(lvalue.identifier.id, effect);
83
+ }
84
+ break;
85
+ }
86
+ case 'StoreLocal': {
87
+ const effect = contextMutationEffects.get(value.value.identifier.id);
88
+ if (effect != null) {
89
+ contextMutationEffects.set(lvalue.identifier.id, effect);
90
+ contextMutationEffects.set(
91
+ value.lvalue.place.identifier.id,
92
+ effect,
93
+ );
94
+ }
95
+ break;
96
+ }
97
+ case 'FunctionExpression': {
98
+ const knownMutation = (value.loweredFunc.func.effects ?? []).find(
99
+ effect => {
100
+ return (
101
+ effect.kind === 'ContextMutation' &&
102
+ (effect.effect === Effect.Store ||
103
+ effect.effect === Effect.Mutate) &&
104
+ Iterable_some(effect.places, place => {
105
+ return (
106
+ isMutableEffect(place.effect, place.loc) &&
107
+ !isRefOrRefLikeMutableType(place.identifier.type)
108
+ );
109
+ })
110
+ );
111
+ },
112
+ );
113
+ if (knownMutation && knownMutation.kind === 'ContextMutation') {
114
+ contextMutationEffects.set(lvalue.identifier.id, knownMutation);
115
+ }
116
+ break;
117
+ }
118
+ default: {
119
+ for (const operand of eachInstructionValueOperand(value)) {
120
+ visitOperand(operand);
121
+ }
122
+ }
123
+ }
124
+ }
125
+ for (const operand of eachTerminalOperand(block.terminal)) {
126
+ visitOperand(operand);
127
+ }
128
+ }
129
+ return errors.asResult();
130
+}