5
* LICENSE file in the root directory of this source tree.
6
*/
7
8
+import {CompilerError} from '..';
9
import {
10
convertHoistedLValueKind,
11
IdentifierId,
12
+ InstructionId,
13
InstructionKind,
14
+ Place,
15
ReactiveFunction,
16
ReactiveInstruction,
17
ReactiveScopeBlock,
27
/*
28
* Prunes DeclareContexts lowered for HoistedConsts, and transforms any references back to its
29
* original instruction kind.
30
+ *
31
+ * Also detects and bails out on context variables which are:
32
+ * - function declarations, which are hoisted by JS engines to the nearest block scope
33
+ * - referenced before they are defined (i.e. having a `DeclareContext HoistedConst`)
34
+ * - declared
35
+ *
36
+ * This is because React Compiler converts a `function foo()` function declaration to
37
+ * 1. a `let foo;` declaration before reactive memo blocks
38
+ * 2. a `foo = function foo() {}` assignment within the block
39
+ *
40
+ * This means references before the assignment are invalid (see fixture
41
+ * error.todo-functiondecl-hoisting)
42
*/
43
export function pruneHoistedContexts(fn: ReactiveFunction): void {
44
visitReactiveFunction(fn, new Visitor(), {
45
activeScopes: empty(),
46
+ uninitialized: new Map(),
47
});
48
}
49
50
type VisitorState = {
51
activeScopes: Stack<Set<IdentifierId>>;
52
+ uninitialized: Map<
53
+ IdentifierId,
54
+ | {
55
+ kind: 'unknown-kind';
56
+ }
57
+ | {
58
+ kind: 'func';
59
+ definition: Place | null;
60
+ }
61
+ >;
62
};
63
64
class Visitor extends ReactiveFunctionTransform<VisitorState> {
66
state.activeScopes = state.activeScopes.push(
67
new Set(scope.scope.declarations.keys()),
68
);
69
+ /**
70
+ * Add declared but not initialized / assigned variables. This may include
71
+ * function declarations that escape the memo block.
72
+ */
73
+ for (const decl of scope.scope.declarations.values()) {
74
+ state.uninitialized.set(decl.identifier.id, {kind: 'unknown-kind'});
75
+ }
76
this.traverseScope(scope, state);
77
state.activeScopes.pop();
78
+ for (const decl of scope.scope.declarations.values()) {
79
+ state.uninitialized.delete(decl.identifier.id);
80
+ }
81
+ }
82
+ override visitPlace(
83
+ _id: InstructionId,
84
+ place: Place,
85
+ state: VisitorState,
86
+ ): void {
87
+ const maybeHoistedFn = state.uninitialized.get(place.identifier.id);
88
+ if (
89
+ maybeHoistedFn?.kind === 'func' &&
90
+ maybeHoistedFn.definition !== place
91
+ ) {
92
+ CompilerError.throwTodo({
93
+ reason: '[PruneHoistedContexts] Rewrite hoisted function references',
94
+ loc: place.loc,
95
+ });
96
+ }
97
}
98
override transformInstruction(
99
instruction: ReactiveInstruction,
100
state: VisitorState,
101
): Transformed<ReactiveStatement> {
50
- this.visitInstruction(instruction, state);
51
-
102
/**
103
* Remove hoisted declarations to preserve TDZ
104
*/
107
instruction.value.lvalue.kind,
108
);
109
if (maybeNonHoisted != null) {
110
+ if (
111
+ maybeNonHoisted === InstructionKind.Function &&
112
+ state.uninitialized.has(instruction.value.lvalue.place.identifier.id)
113
+ ) {
114
+ state.uninitialized.set(
115
+ instruction.value.lvalue.place.identifier.id,
116
+ {
117
+ kind: 'func',
118
+ definition: null,
119
+ },
120
+ );
121
+ }
122
return {kind: 'remove'};
123
}
124
}
127
instruction.value.lvalue.kind !== InstructionKind.Reassign
128
) {
129
/**
68
- * Rewrite StoreContexts let/const/functions that will be pre-declared in
130
+ * Rewrite StoreContexts let/const that will be pre-declared in
131
* codegen to reassignments.
132
*/
133
const lvalueId = instruction.value.lvalue.place.identifier.id;
135
scope.has(lvalueId),
136
);
137
if (isDeclaredByScope) {
76
- instruction.value.lvalue.kind = InstructionKind.Reassign;
138
+ if (
139
+ instruction.value.lvalue.kind === InstructionKind.Let ||
140
+ instruction.value.lvalue.kind === InstructionKind.Const
141
+ ) {
142
+ instruction.value.lvalue.kind = InstructionKind.Reassign;
143
+ } else if (instruction.value.lvalue.kind === InstructionKind.Function) {
144
+ const maybeHoistedFn = state.uninitialized.get(lvalueId);
145
+ if (maybeHoistedFn != null) {
146
+ CompilerError.invariant(maybeHoistedFn.kind === 'func', {
147
+ reason: '[PruneHoistedContexts] Unexpected hoisted function',
148
+ loc: instruction.loc,
149
+ });
150
+ maybeHoistedFn.definition = instruction.value.lvalue.place;
151
+ /**
152
+ * References to hoisted functions are now "safe" as variable assignments
153
+ * have finished.
154
+ */
155
+ state.uninitialized.delete(lvalueId);
156
+ }
157
+ } else {
158
+ CompilerError.throwTodo({
159
+ reason: '[PruneHoistedContexts] Unexpected kind',
160
+ description: `(${instruction.value.lvalue.kind})`,
161
+ loc: instruction.loc,
162
+ });
163
+ }
164
}
165
}
166
167
+ this.visitInstruction(instruction, state);
168
return {kind: 'keep'};
169
}
170
}