-
-
Notifications
You must be signed in to change notification settings - Fork 5.6k
/
util.ts
227 lines (202 loc) 路 6.79 KB
/
util.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
import type { NodePath, Visitor } from "@babel/traverse";
import { types as t } from "@babel/core";
import { requeueComputedKeyAndDecorators } from "@babel/helper-environment-visitor";
function isNameOrLength(key: t.Node): boolean {
if (t.isIdentifier(key)) {
return key.name === "name" || key.name === "length";
}
if (t.isStringLiteral(key)) {
return key.value === "name" || key.value === "length";
}
return false;
}
function isStaticFieldWithValue(
node: t.Node,
): node is t.ClassProperty | t.ClassPrivateProperty {
return (
(t.isClassProperty(node) || t.isClassPrivateProperty(node)) &&
node.static &&
!!node.value
);
}
const hasReferenceVisitor: Visitor<{ name: string; ref: () => void }> = {
ReferencedIdentifier(path, state) {
if (path.node.name === state.name) {
state.ref();
path.stop();
}
},
Scope(path, { name }) {
if (path.scope.hasOwnBinding(name)) {
path.skip();
}
},
};
function isReferenceOrThis(node: t.Node, name?: string) {
return t.isThisExpression(node) || (name && t.isIdentifier(node, { name }));
}
const hasReferenceOrThisVisitor: Visitor<{ name?: string; ref: () => void }> = {
"ThisExpression|ReferencedIdentifier"(path, state) {
if (isReferenceOrThis(path.node, state.name)) {
state.ref();
path.stop();
}
},
FunctionParent(path, state) {
if (path.isArrowFunctionExpression()) return;
if (state.name && !path.scope.hasOwnBinding(state.name)) {
path.traverse(hasReferenceVisitor, state);
}
path.skip();
if (path.isMethod()) {
requeueComputedKeyAndDecorators(path);
}
},
};
type ClassElementWithComputedKeySupport = Extract<
t.ClassBody["body"][number],
{ computed?: boolean }
>;
/**
* This function returns an array containing the indexes of class elements
* that might be affected by https://crbug.com/v8/12421 bug.
*
* This bug affects public static class fields that have the same name as an
* existing non-writable property with the same name. This usually happens when
* the static field is named 'length' or 'name', since it clashes with the
* predefined fn.length and fn.name properties. We must also compile static
* fields with computed key, because they might end up being named 'length' or
* 'name'.
*
* However, this bug can potentially affect public static fields with any name.
* Consider this example:
*
* class A {
* static {
* Object.defineProperty(A, "readonly", {
* value: 1,
* writable: false,
* configurable: true
* })
* }
*
* static readonly = 2;
* }
*
* When initializing the 'static readonly' field, the class already has a
* non-writable property named 'readonly' and thus V8 9.7 incorrectly throws.
*
* To avoid unconditionally compiling every public static field, we track how
* the class is referenced during definition & static evaluation: any side
* effect after a reference to the class can potentially define a non-writable
* conficting property, so subsequent public static fields must be compiled.
* The class could be referenced using the class name in computed keys, which
* run before static fields, or using either the class name or 'this' in static
* fields (both public and private) and static blocks.
*
* We don't need to check if computed keys referencing the class have any side
* effect, because during the computed keys evaluation the internal class
* binding is in TDZ. However, the first side effect in a static field/block
* could have access to a function defined in a computed key that modifies the
* class.
*
* This logic is already quite complex, so we assume that static blocks always
* have side effects and reference the class (the reason to use them is to
* perform additional initialization logic on the class anyway), so that we do
* not have to check their contents.
*/
export function getPotentiallyBuggyFieldsIndexes(path: NodePath<t.Class>) {
const buggyPublicStaticFieldsIndexes: number[] = [];
let classReferenced = false;
const className = path.node.id?.name;
const hasReferenceState = {
name: className,
ref: () => (classReferenced = true),
};
if (className) {
for (const el of path.get("body.body")) {
if ((el.node as ClassElementWithComputedKeySupport).computed) {
// Since .traverse skips the top-level node, it doesn't detect
// a reference happening immediately:
// class A { [A]() {} }
// However, it's a TDZ error so it's ok not to consider this case.
(el as NodePath<ClassElementWithComputedKeySupport>)
.get("key")
.traverse(hasReferenceVisitor, hasReferenceState);
if (classReferenced) break;
}
}
}
let nextPotentiallyBuggy = false;
const { body } = path.node.body;
for (let i = 0; i < body.length; i++) {
const node = body[i];
if (!nextPotentiallyBuggy) {
if (t.isStaticBlock(node)) {
classReferenced = true;
nextPotentiallyBuggy = true;
} else if (isStaticFieldWithValue(node)) {
if (!classReferenced) {
if (isReferenceOrThis(node.value, className)) {
classReferenced = true;
} else {
(
path.get(`body.body.${i}.value`) as NodePath<t.Expression>
).traverse(hasReferenceOrThisVisitor, hasReferenceState);
}
}
if (classReferenced) {
nextPotentiallyBuggy = !path.scope.isPure(node.value);
}
}
}
if (
t.isClassProperty(node, { static: true }) &&
(nextPotentiallyBuggy || node.computed || isNameOrLength(node.key))
) {
buggyPublicStaticFieldsIndexes.push(i);
}
}
return buggyPublicStaticFieldsIndexes;
}
export function getNameOrLengthStaticFieldsIndexes(path: NodePath<t.Class>) {
const indexes: number[] = [];
const { body } = path.node.body;
for (let i = 0; i < body.length; i++) {
const node = body[i];
if (
t.isClassProperty(node, { static: true, computed: false }) &&
isNameOrLength(node.key)
) {
indexes.push(i);
}
}
return indexes;
}
type Range = [start: number, end: number];
/**
* Converts a sorted list of numbers into a list of (inclusive-exclusive)
* ranges representing the same numbers.
*
* @example toRanges([1, 3, 4, 5, 8, 9]) -> [[1, 2], [3, 6], [8, 10]]
*/
export function toRanges(nums: number[]): Range[] {
const ranges: Range[] = [];
if (nums.length === 0) return ranges;
let start = nums[0];
let end = start + 1;
for (let i = 1; i < nums.length; i++) {
if (nums[i] <= nums[i - 1]) {
throw new Error("Internal Babel error: nums must be in ascending order");
}
if (nums[i] === end) {
end++;
} else {
ranges.push([start, end]);
start = nums[i];
end = start + 1;
}
}
ranges.push([start, end]);
return ranges;
}