@nudojs/cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/package.json +24 -4
- package/src/abs-route.ts +273 -0
- package/src/builtins/builtin-intl.ts +17 -0
- package/src/builtins/builtin-map.ts +174 -0
- package/src/builtins/builtin-promise.ts +116 -0
- package/src/builtins/builtin-prototype.ts +279 -0
- package/src/builtins/builtin-reflect.ts +30 -0
- package/src/builtins/builtin-regexp.ts +34 -0
- package/src/builtins/builtin-set.ts +131 -0
- package/src/builtins/builtin-static.ts +260 -0
- package/src/builtins/builtin-symbol.ts +16 -0
- package/src/builtins/builtin-url.ts +58 -0
- package/src/builtins/builtin-weak.ts +22 -0
- package/src/builtins/builtin-web.ts +111 -0
- package/src/config.ts +32 -0
- package/src/env-loader.ts +174 -0
- package/src/eval-binary.ts +113 -0
- package/src/evaluator-api.ts +23 -0
- package/src/evaluator.ts +3502 -285
- package/src/index.ts +922 -180
- package/src/narrowing.ts +237 -15
- package/src/phi-from-test.ts +42 -0
- package/src/resolve-npm.ts +63 -0
- package/src/run-test.ts +92 -0
- package/src/term-registry.ts +96 -0
- package/CHANGELOG.md +0 -29
- package/dist/chunk-LGWVGEMB.js +0 -1817
- package/dist/evaluator-api.d.ts +0 -59
- package/dist/evaluator-api.js +0 -30
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -3570
- package/src/__tests__/evaluator-loops.test.ts +0 -163
- package/src/__tests__/evaluator-phase2.test.ts +0 -564
- package/src/__tests__/evaluator-phase3.test.ts +0 -513
- package/src/__tests__/evaluator-refined.test.ts +0 -121
- package/src/__tests__/evaluator-string-methods.test.ts +0 -151
- package/src/__tests__/evaluator.test.ts +0 -243
- package/src/__tests__/narrowing.test.ts +0 -82
- package/tsconfig.json +0 -9
package/src/evaluator.ts
CHANGED
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
type TypeValue,
|
|
4
4
|
T,
|
|
5
5
|
simplifyUnion,
|
|
6
|
-
applyBinaryOp,
|
|
7
6
|
dispatchBinaryOp,
|
|
8
7
|
dispatchMethod,
|
|
9
8
|
dispatchProperty,
|
|
@@ -16,12 +15,442 @@ import {
|
|
|
16
15
|
typeValueToString,
|
|
17
16
|
isSubtypeOf,
|
|
18
17
|
widenLiteral,
|
|
18
|
+
collapseLiteralUnion,
|
|
19
19
|
createTemplate,
|
|
20
|
+
subtractType,
|
|
21
|
+
getFnSig,
|
|
20
22
|
} from "@nudojs/core";
|
|
23
|
+
import { extractInlineDirectives, type InlineDirective } from "@nudojs/parser";
|
|
21
24
|
import { narrow } from "./narrowing.ts";
|
|
25
|
+
import {
|
|
26
|
+
tryAbsObjectSpread,
|
|
27
|
+
tryAbsJoinObjects,
|
|
28
|
+
tryAbsUnary,
|
|
29
|
+
pushPhi,
|
|
30
|
+
popPhi,
|
|
31
|
+
currentPhi,
|
|
32
|
+
resetPhi,
|
|
33
|
+
} from "./abs-route.ts";
|
|
34
|
+
import {
|
|
35
|
+
evalBinaryValue,
|
|
36
|
+
distributeBinaryOverUnion,
|
|
37
|
+
} from "./eval-binary.ts";
|
|
38
|
+
import { phiFromTest, combinePhi } from "./phi-from-test.ts";
|
|
39
|
+
import { tagParamArg } from "./term-registry.ts";
|
|
40
|
+
import { evaluatePromiseStaticMethod, evaluatePromiseInstanceMethod } from "./builtins/builtin-promise.ts";
|
|
41
|
+
import { MAP_INSTANCE_METHODS, createMapType, mapEntriesIterable, exactMapEntries } from "./builtins/builtin-map.ts";
|
|
42
|
+
import { SET_INSTANCE_METHODS, createSetType, setValuesIterable, exactSetValues } from "./builtins/builtin-set.ts";
|
|
43
|
+
import { REGEXP_INSTANCE_METHODS, createRegExpType } from "./builtins/builtin-regexp.ts";
|
|
44
|
+
import { URL_INSTANCE_METHODS, URLSearchParams_INSTANCE_METHODS, createURLType, createURLSearchParamsType } from "./builtins/builtin-url.ts";
|
|
45
|
+
import {
|
|
46
|
+
RESPONSE_INSTANCE_METHODS,
|
|
47
|
+
HEADERS_INSTANCE_METHODS,
|
|
48
|
+
FORMDATA_INSTANCE_METHODS,
|
|
49
|
+
ABORTCONTROLLER_INSTANCE_METHODS,
|
|
50
|
+
createResponseType,
|
|
51
|
+
createHeadersType,
|
|
52
|
+
createFormDataType,
|
|
53
|
+
createAbortControllerType,
|
|
54
|
+
} from "./builtins/builtin-web.ts";
|
|
55
|
+
import { WEAKMAP_INSTANCE_METHODS, WEAKSET_INSTANCE_METHODS, createWeakMapType, createWeakSetType } from "./builtins/builtin-weak.ts";
|
|
56
|
+
import { SYMBOL_STATIC_PROPS } from "./builtins/builtin-symbol.ts";
|
|
57
|
+
import { REFLECT_METHODS } from "./builtins/builtin-reflect.ts";
|
|
58
|
+
import { INTL_DATETIMEFORMAT_METHODS, INTL_NUMBERFORMAT_METHODS, createDateTimeFormatType, createNumberFormatType } from "./builtins/builtin-intl.ts";
|
|
59
|
+
import {
|
|
60
|
+
BUILTIN_ERROR_CLASSES,
|
|
61
|
+
BUILTIN_PROTOTYPE_CLASSES,
|
|
62
|
+
OBJECT_PROTOTYPE_METHODS,
|
|
63
|
+
BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS,
|
|
64
|
+
builtinClassValue,
|
|
65
|
+
builtinPrototype,
|
|
66
|
+
} from "./builtins/builtin-prototype.ts";
|
|
67
|
+
import { BUILTIN_STATIC_METHODS, builtinInstanceTest, jsonToTypeValue } from "./builtins/builtin-static.ts";
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
// Wrapper-prototype fallback for primitive receivers: real JS auto-boxes
|
|
78
|
+
// member access on primitives ('x'.constructor === String), so a named
|
|
79
|
+
// member hit on a string/number/boolean/symbol consults the wrapper's
|
|
80
|
+
// prototype table (the same approximations `X.prototype` uses) plus the
|
|
81
|
+
// wrapper's constructor before anything reports a missing property.
|
|
82
|
+
// hasOwnProp-guarded like every other table lookup here.
|
|
83
|
+
/** 值上命名成员是否可能存在(union 部分成员持有的 no-method 降 warning
|
|
84
|
+
* 用):primitive/literal 走 wrapper 原型近似表('x'.charCodeAt),
|
|
85
|
+
* instance 走近似方法表。诊断侧据此区分"部分成员可能有"与"确定没有"。 */
|
|
86
|
+
export function memberMayExistOn(tv: TypeValue, propName: string): boolean {
|
|
87
|
+
if (tv.kind === "primitive" || tv.kind === "literal") {
|
|
88
|
+
return wrapperPrototypeMember(tv, propName) !== undefined;
|
|
89
|
+
}
|
|
90
|
+
if (tv.kind === "instance") {
|
|
91
|
+
const className = tv.className;
|
|
92
|
+
const table = hasOwnProp(BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS, className)
|
|
93
|
+
? BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS[className]
|
|
94
|
+
: BUILTIN_ERROR_CLASSES.has(className)
|
|
95
|
+
? BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS.Error
|
|
96
|
+
: undefined;
|
|
97
|
+
if (table && hasOwnProp(table, propName)) return true;
|
|
98
|
+
// instance 的方法集是近似(未列出 ≠ 不存在)
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function wrapperPrototypeMember(obj: TypeValue, propName: string): TypeValue | undefined { let className: string | undefined;
|
|
105
|
+
if (obj.kind === "primitive") {
|
|
106
|
+
className = obj.type === "bigint" ? undefined : obj.type[0].toUpperCase() + obj.type.slice(1);
|
|
107
|
+
} else if (obj.kind === "literal") {
|
|
108
|
+
if (typeof obj.value === "string") className = "String";
|
|
109
|
+
else if (typeof obj.value === "number") className = "Number";
|
|
110
|
+
else if (typeof obj.value === "boolean") className = "Boolean";
|
|
111
|
+
}
|
|
112
|
+
if (!className) return undefined;
|
|
113
|
+
if (propName === "constructor") return builtinClassValue(className);
|
|
114
|
+
const table = BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS[className];
|
|
115
|
+
if (table && hasOwnProp(table, propName)) return table[propName];
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
function arrayIsArrayLiteral(v: TypeValue): boolean | undefined {
|
|
125
|
+
let base: TypeValue = v;
|
|
126
|
+
while (base.kind === "refined") base = base.base;
|
|
127
|
+
switch (base.kind) {
|
|
128
|
+
case "array":
|
|
129
|
+
case "tuple": return true;
|
|
130
|
+
case "instance": return base.className === "Array";
|
|
131
|
+
case "object":
|
|
132
|
+
case "function":
|
|
133
|
+
case "promise":
|
|
134
|
+
case "literal":
|
|
135
|
+
case "primitive": return false;
|
|
136
|
+
default: return undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// JavaScript truthiness decided statically. Object-ish kinds (object/array/
|
|
141
|
+
// tuple/function/instance/promise) are always truthy; literals decide on
|
|
142
|
+
// their value; never is unreachable (falsy); symbolic values are null.
|
|
143
|
+
function definiteBoolean(tv: TypeValue): boolean | null {
|
|
144
|
+
if (tv.kind === "union") {
|
|
145
|
+
let allTrue = true;
|
|
146
|
+
let allFalse = true;
|
|
147
|
+
for (const m of tv.members) {
|
|
148
|
+
const b = definiteBoolean(m);
|
|
149
|
+
if (b === null) return null;
|
|
150
|
+
allTrue = allTrue && b === true;
|
|
151
|
+
allFalse = allFalse && b === false;
|
|
152
|
+
}
|
|
153
|
+
if (allTrue) return true;
|
|
154
|
+
if (allFalse) return false;
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
switch (tv.kind) {
|
|
158
|
+
case "literal": return !!tv.value;
|
|
159
|
+
case "object":
|
|
160
|
+
case "array":
|
|
161
|
+
case "tuple":
|
|
162
|
+
case "function":
|
|
163
|
+
case "instance":
|
|
164
|
+
case "promise": return true;
|
|
165
|
+
case "never": return false;
|
|
166
|
+
default: return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 分支合并:双对象先走 Abs join(sum-of-products / 槽位合并),
|
|
172
|
+
* 失败或非对象再 simplifyUnion。保证 join 代数接到 evaluator 主路径。
|
|
173
|
+
*/
|
|
174
|
+
function joinOrUnion(a: TypeValue, b: TypeValue): TypeValue {
|
|
175
|
+
if (a.kind === "object" && b.kind === "object") {
|
|
176
|
+
const joined = tryAbsJoinObjects(a, b);
|
|
177
|
+
if (joined) return joined;
|
|
178
|
+
}
|
|
179
|
+
return simplifyUnion([a, b]);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Receiver-binding for thisVal-dependent Object.prototype methods
|
|
183
|
+
// (toString/valueOf): `const v = obj.valueOf; v()` loses the member-access
|
|
184
|
+
// receiver, so accesses off a concrete receiver return a copy whose impl
|
|
185
|
+
// closes over it. An explicit `.call(x)`/`.apply(x)` thisVal still wins.
|
|
186
|
+
function bindObjectProtoMethod(fn: TypeValue, receiver: TypeValue): TypeValue {
|
|
187
|
+
const sig = getFnSig(fn);
|
|
188
|
+
if (!sig?.impl) return fn;
|
|
189
|
+
return T.fnSig(sig.paramTypes, sig.returnType, sig.throwsType, (args, thisVal) =>
|
|
190
|
+
sig.impl!(args, thisVal ?? receiver));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** `.valueOf` on receivers that don't go through the object/instance
|
|
194
|
+
* property branches: non-nullish literals and primitives return their
|
|
195
|
+
* receiver (JS box-unbox semantics), structured values (array/tuple/
|
|
196
|
+
* function) return themselves, and null/undefined literals produce a call
|
|
197
|
+
* that throws TypeError — exactly like real JS, so an enclosing try/catch
|
|
198
|
+
* binds the error instead of the access poisoning the result with unknown. */
|
|
199
|
+
function valueOfBinding(receiver: TypeValue): TypeValue | undefined {
|
|
200
|
+
switch (receiver.kind) {
|
|
201
|
+
case "literal": {
|
|
202
|
+
const v = receiver.value;
|
|
203
|
+
if (v === null || v === undefined) {
|
|
204
|
+
return T.fnSig([], T.never, T.instanceOf("TypeError"));
|
|
205
|
+
}
|
|
206
|
+
return bindObjectProtoMethod(OBJECT_PROTOTYPE_METHODS["valueOf"], receiver);
|
|
207
|
+
}
|
|
208
|
+
case "primitive":
|
|
209
|
+
case "array":
|
|
210
|
+
case "tuple":
|
|
211
|
+
case "function":
|
|
212
|
+
return bindObjectProtoMethod(OBJECT_PROTOTYPE_METHODS["valueOf"], receiver);
|
|
213
|
+
default:
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Strict equality over prototype singletons (and instances compared against
|
|
219
|
+
// them): definite true/false only when every cross-pair agrees. null means
|
|
220
|
+
// "not decidable here" — the generic boolean fallback applies.
|
|
221
|
+
function builtinProtoIdentityEq(l: TypeValue, r: TypeValue): boolean | null {
|
|
222
|
+
const ls = l.kind === "union" ? l.members : [l];
|
|
223
|
+
const rs = r.kind === "union" ? r.members : [r];
|
|
224
|
+
let anyProto = false;
|
|
225
|
+
for (const m of ls.concat(rs)) {
|
|
226
|
+
if (m.kind !== "instance") return null;
|
|
227
|
+
if ((m as any)._builtinProto) anyProto = true;
|
|
228
|
+
}
|
|
229
|
+
if (!anyProto) return null;
|
|
230
|
+
let allEq = true;
|
|
231
|
+
let anyEq = false;
|
|
232
|
+
for (const lm of ls) {
|
|
233
|
+
for (const rm of rs) {
|
|
234
|
+
const eq = typeValueEquals(lm, rm);
|
|
235
|
+
allEq = allEq && eq;
|
|
236
|
+
anyEq = anyEq || eq;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (allEq) return true;
|
|
240
|
+
if (!anyEq) return false;
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Strict equality over builtin class namespaces (`x.constructor === Array`
|
|
245
|
+
// where both sides are memoized builtinClassValue objects): definite
|
|
246
|
+
// true/false by class name. null means "not decidable here" — the generic
|
|
247
|
+
// boolean fallback applies.
|
|
248
|
+
function builtinClassIdentityEq(l: TypeValue, r: TypeValue): boolean | null {
|
|
249
|
+
const ls = l.kind === "union" ? l.members : [l];
|
|
250
|
+
const rs = r.kind === "union" ? r.members : [r];
|
|
251
|
+
const nameOf = (v: TypeValue): string | null =>
|
|
252
|
+
v.kind === "object" && typeof (v as any)._builtinName === "string" ? (v as any)._builtinName : null;
|
|
253
|
+
const lNames = ls.map(nameOf);
|
|
254
|
+
const rNames = rs.map(nameOf);
|
|
255
|
+
if (!lNames.some(Boolean) && !rNames.some(Boolean)) return null;
|
|
256
|
+
if (lNames.some((n) => n === null) || rNames.some((n) => n === null)) return null;
|
|
257
|
+
let allEq = true;
|
|
258
|
+
let anyEq = false;
|
|
259
|
+
for (const ln of lNames) {
|
|
260
|
+
for (const rn of rNames) {
|
|
261
|
+
const eq = ln === rn;
|
|
262
|
+
allEq = allEq && eq;
|
|
263
|
+
anyEq = anyEq || eq;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (allEq) return true;
|
|
267
|
+
if (!anyEq) return false;
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Well-known symbols (Symbol.iterator, Symbol.asyncIterator, ...) as
|
|
272
|
+
// marked symbol primitives: plain T.symbol cannot name which symbol it
|
|
273
|
+
// is, but `Symbol.iterator in x` keys on exactly that identity.
|
|
274
|
+
const _wellKnownSymbols = new Map<string, TypeValue>();
|
|
275
|
+
function wellKnownSymbol(name: string): TypeValue {
|
|
276
|
+
let v = _wellKnownSymbols.get(name);
|
|
277
|
+
if (v === undefined) {
|
|
278
|
+
v = { ...(T.symbol as unknown as object), _symbolName: name } as unknown as TypeValue;
|
|
279
|
+
_wellKnownSymbols.set(name, v);
|
|
280
|
+
}
|
|
281
|
+
return v;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function wellKnownSymbolName(v: TypeValue): string | undefined {
|
|
285
|
+
let base = v;
|
|
286
|
+
while (base.kind === "refined") base = base.base;
|
|
287
|
+
if (base.kind === "primitive" && base.type === "symbol") {
|
|
288
|
+
return (base as any)._symbolName as string | undefined;
|
|
289
|
+
}
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// `Symbol.iterator in receiver` iterability by receiver kind: true/false
|
|
294
|
+
// for statically-known receivers, undefined = uncertain (unknown class
|
|
295
|
+
// instances could define the protocol themselves).
|
|
296
|
+
function iteratorInReceiver(v: TypeValue): boolean | undefined {
|
|
297
|
+
switch (v.kind) {
|
|
298
|
+
case "array":
|
|
299
|
+
case "tuple":
|
|
300
|
+
return true;
|
|
301
|
+
case "instance":
|
|
302
|
+
if (v.className === "Set" || v.className === "Map" || v.className === "Buffer") return true;
|
|
303
|
+
if (BUILTIN_PROTOTYPE_CLASSES.has(v.className)) return false;
|
|
304
|
+
return undefined;
|
|
305
|
+
case "literal":
|
|
306
|
+
return typeof v.value === "string" ? true : false;
|
|
307
|
+
case "primitive":
|
|
308
|
+
return v.type === "string" ? true : false;
|
|
309
|
+
case "object":
|
|
310
|
+
case "promise":
|
|
311
|
+
case "function":
|
|
312
|
+
return false;
|
|
313
|
+
default:
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// `key in obj` literal decisions for non-union operands:
|
|
319
|
+
// - Well-known symbols decide iterability (Symbol.iterator on
|
|
320
|
+
// arrays/sets/maps/strings → true, plain values → false, unknown-class
|
|
321
|
+
// instances → boolean).
|
|
322
|
+
// - String keys decide against the declared shape (own properties plus
|
|
323
|
+
// inherited Object.prototype/builtin-prototype members); a definite
|
|
324
|
+
// miss is false.
|
|
325
|
+
// - Symbolic keys or uncertain receivers degrade to boolean; unknown
|
|
326
|
+
// receivers stay unknown.
|
|
327
|
+
function evaluateInMember(key: TypeValue, container: TypeValue): TypeValue {
|
|
328
|
+
let k = key;
|
|
329
|
+
while (k.kind === "refined") k = k.base;
|
|
330
|
+
let c = container;
|
|
331
|
+
while (c.kind === "refined") c = c.base;
|
|
332
|
+
|
|
333
|
+
if (c.kind === "unknown") return T.unknown;
|
|
334
|
+
|
|
335
|
+
const symName = wellKnownSymbolName(k);
|
|
336
|
+
if (symName !== undefined) {
|
|
337
|
+
if (symName === "iterator") {
|
|
338
|
+
const iter = iteratorInReceiver(c);
|
|
339
|
+
return iter === undefined ? T.boolean : T.literal(iter);
|
|
340
|
+
}
|
|
341
|
+
// Other well-known symbols (asyncIterator, toStringTag, ...): none of
|
|
342
|
+
// the statically-known receivers carry them; unknown-class instances
|
|
343
|
+
// stay symbolic.
|
|
344
|
+
if (c.kind === "instance" && !BUILTIN_PROTOTYPE_CLASSES.has(c.className)) return T.boolean;
|
|
345
|
+
return T.literal(false);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const strKey = k.kind === "literal" && typeof k.value === "string" ? k.value : undefined;
|
|
349
|
+
if (strKey === undefined) {
|
|
350
|
+
// Symbolic key (string, or a symbol without a name): object-ish
|
|
351
|
+
// receivers still promise a definite boolean; primitives/nullish
|
|
352
|
+
// cannot hold any property.
|
|
353
|
+
switch (c.kind) {
|
|
354
|
+
case "object":
|
|
355
|
+
case "instance":
|
|
356
|
+
case "promise":
|
|
357
|
+
case "function":
|
|
358
|
+
case "array":
|
|
359
|
+
case "tuple":
|
|
360
|
+
return T.boolean;
|
|
361
|
+
case "literal":
|
|
362
|
+
case "primitive":
|
|
363
|
+
return T.literal(false);
|
|
364
|
+
default:
|
|
365
|
+
return T.unknown;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const isArrayIndex = (s: string) => /^\d+$/.test(s);
|
|
370
|
+
switch (c.kind) {
|
|
371
|
+
case "object":
|
|
372
|
+
return T.literal(hasOwnProp(c.properties, strKey) || hasOwnProp(OBJECT_PROTOTYPE_METHODS, strKey));
|
|
373
|
+
case "tuple":
|
|
374
|
+
if (strKey === "length") return T.literal(true);
|
|
375
|
+
if (isArrayIndex(strKey)) return T.literal(Number(strKey) < c.elements.length);
|
|
376
|
+
return T.literal(
|
|
377
|
+
hasOwnProp(OBJECT_PROTOTYPE_METHODS, strKey) ||
|
|
378
|
+
hasOwnProp(BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS.Array, strKey),
|
|
379
|
+
);
|
|
380
|
+
case "array":
|
|
381
|
+
return T.literal(
|
|
382
|
+
strKey === "length" ||
|
|
383
|
+
isArrayIndex(strKey) ||
|
|
384
|
+
hasOwnProp(OBJECT_PROTOTYPE_METHODS, strKey) ||
|
|
385
|
+
hasOwnProp(BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS.Array, strKey),
|
|
386
|
+
);
|
|
387
|
+
case "instance": {
|
|
388
|
+
if (hasOwnProp(c.properties, strKey)) return T.literal(true);
|
|
389
|
+
if (hasOwnProp(OBJECT_PROTOTYPE_METHODS, strKey)) return T.literal(true);
|
|
390
|
+
// hasOwnProp guard: plain [] access on the class key would leak
|
|
391
|
+
// native Object.prototype members.
|
|
392
|
+
const approx = hasOwnProp(BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS, c.className)
|
|
393
|
+
? BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS[c.className]
|
|
394
|
+
: undefined;
|
|
395
|
+
if (approx && hasOwnProp(approx, strKey)) return T.literal(true);
|
|
396
|
+
if ((c.className === "Set" || c.className === "Map") && strKey === "size") return T.literal(true);
|
|
397
|
+
return T.literal(false);
|
|
398
|
+
}
|
|
399
|
+
case "function":
|
|
400
|
+
return T.literal(
|
|
401
|
+
strKey === "prototype" || strKey === "name" || strKey === "length" ||
|
|
402
|
+
strKey === "call" || strKey === "apply" || strKey === "bind" || strKey === "toString",
|
|
403
|
+
);
|
|
404
|
+
case "promise":
|
|
405
|
+
return T.literal(
|
|
406
|
+
strKey === "then" || strKey === "catch" || strKey === "finally" ||
|
|
407
|
+
hasOwnProp(OBJECT_PROTOTYPE_METHODS, strKey),
|
|
408
|
+
);
|
|
409
|
+
case "literal":
|
|
410
|
+
if (typeof c.value === "string") {
|
|
411
|
+
return T.literal(strKey === "length" || (isArrayIndex(strKey) && Number(strKey) < c.value.length));
|
|
412
|
+
}
|
|
413
|
+
return T.literal(false);
|
|
414
|
+
case "primitive":
|
|
415
|
+
if (c.type === "string") return T.literal(strKey === "length" || isArrayIndex(strKey));
|
|
416
|
+
return T.literal(false);
|
|
417
|
+
default:
|
|
418
|
+
return T.boolean;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
22
421
|
|
|
23
422
|
type SourceRange = { start: { line: number; column: number }; end: { line: number; column: number } };
|
|
24
423
|
|
|
424
|
+
let _currentSource = "";
|
|
425
|
+
|
|
426
|
+
export function setCurrentSource(source: string): void {
|
|
427
|
+
_currentSource = source;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
type ActiveReplace = { targetSource: string; typeExpr: TypeValue };
|
|
431
|
+
let _activeReplacements: ActiveReplace[] = [];
|
|
432
|
+
let _activeAsOverride: TypeValue | null = null;
|
|
433
|
+
|
|
434
|
+
function nodeSourceText(node: Node): string | null {
|
|
435
|
+
if (node.start == null || node.end == null || !_currentSource) return null;
|
|
436
|
+
return _currentSource.slice(node.start, node.end);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function normalizeWhitespace(s: string): string {
|
|
440
|
+
return s.replace(/\s+/g, "");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function matchesReplacement(node: Node): TypeValue | null {
|
|
444
|
+
if (_activeReplacements.length === 0) return null;
|
|
445
|
+
const src = nodeSourceText(node);
|
|
446
|
+
if (!src) return null;
|
|
447
|
+
const normalized = normalizeWhitespace(src);
|
|
448
|
+
for (const r of _activeReplacements) {
|
|
449
|
+
if (normalizeWhitespace(r.targetSource) === normalized) return r.typeExpr;
|
|
450
|
+
}
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
|
|
25
454
|
const RETURN_SIGNAL = Symbol("ReturnSignal");
|
|
26
455
|
const BRANCH_SIGNAL = Symbol("BranchSignal");
|
|
27
456
|
const THROW_SIGNAL = Symbol("ThrowSignal");
|
|
@@ -67,36 +496,107 @@ function isThrow(v: unknown): v is ThrowSignal {
|
|
|
67
496
|
return typeof v === "object" && v !== null && THROW_SIGNAL in v;
|
|
68
497
|
}
|
|
69
498
|
|
|
499
|
+
// --- break / continue ---
|
|
500
|
+
// 循环控制语句此前完全未求值(evaluate 无 case,信号丢失后守卫内
|
|
501
|
+
// `break` 之后的语句照常执行 —— reach.js 的 `if (!ref) { …; break }`
|
|
502
|
+
// 因此在 undefined 上求 `ref[key]`,产生假 no-method error)。实现上
|
|
503
|
+
// 借道 Throw 信号管线:break/continue 对象带 THROW_SYMBOL 标记,全部
|
|
504
|
+
// 既有 isThrow 检查点(语句列表、二元短路、调用边界)原样传播它们,
|
|
505
|
+
// 只有循环边界与 switch 吸收。真实 throw 与循环信号用 LOOP_SIGNAL_KIND
|
|
506
|
+
// 区分:前者继续走抛出语义,后者终止/跳过当轮迭代。
|
|
507
|
+
const LOOP_SIGNAL_KIND = Symbol("LoopSignalKind");
|
|
508
|
+
|
|
509
|
+
type LoopSignal = ThrowSignal & {
|
|
510
|
+
readonly [LOOP_SIGNAL_KIND]: "break" | "continue";
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
function makeBreak(): LoopSignal {
|
|
514
|
+
return { [THROW_SIGNAL]: true, [LOOP_SIGNAL_KIND]: "break", thrown: T.never };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function makeContinue(): LoopSignal {
|
|
518
|
+
return { [THROW_SIGNAL]: true, [LOOP_SIGNAL_KIND]: "continue", thrown: T.never };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function loopSignalKind(v: unknown): "break" | "continue" | null {
|
|
522
|
+
if (typeof v !== "object" || v === null) return null;
|
|
523
|
+
const kind = (v as Record<symbol, unknown>)[LOOP_SIGNAL_KIND];
|
|
524
|
+
return kind === "break" || kind === "continue" ? kind : null;
|
|
525
|
+
}
|
|
526
|
+
|
|
70
527
|
type EvalResult = TypeValue | ReturnSignal | BranchSignal | ThrowSignal;
|
|
71
528
|
|
|
72
529
|
const MEMO_IN_PROGRESS = Symbol("MemoInProgress");
|
|
73
530
|
const callMemo = new Map<string, TypeValue | typeof MEMO_IN_PROGRESS>();
|
|
74
531
|
|
|
75
|
-
function buildMemoKey(
|
|
532
|
+
function buildMemoKey(
|
|
533
|
+
fn: TypeValue & { kind: "function" },
|
|
534
|
+
args: TypeValue[],
|
|
535
|
+
thisVal?: TypeValue,
|
|
536
|
+
): string | null {
|
|
76
537
|
const fnName = (fn as any)._memoize as string | undefined;
|
|
77
538
|
if (!fnName) return null;
|
|
78
539
|
const argsKey = args.map(typeValueToString).join(",");
|
|
79
|
-
|
|
540
|
+
// `this` participates in the key: obj1.f() and obj2.f() must not share a
|
|
541
|
+
// memo entry when f reads `this`.
|
|
542
|
+
const thisKey = thisVal ? `this:${typeValueToString(thisVal)};` : "";
|
|
543
|
+
return `${fnName}(${thisKey}${argsKey})`;
|
|
80
544
|
}
|
|
81
545
|
|
|
82
546
|
const moduleCache = new Map<string, Environment>();
|
|
83
547
|
|
|
548
|
+
/** 模块加载链(import / require / @nudo:mock-module mock 文件)深度上限。
|
|
549
|
+
* 超限即截断为空环境并记录诊断——防御病态嵌套链,以及解析器返回未归一化
|
|
550
|
+
* 路径时(同一逻辑文件每次都是「新」路径、缓存永不命中)的真栈溢出。 */
|
|
551
|
+
const MAX_MODULE_LOAD_DEPTH = 16;
|
|
552
|
+
|
|
84
553
|
export function resetMemo(): void {
|
|
85
554
|
callMemo.clear();
|
|
86
555
|
moduleCache.clear();
|
|
556
|
+
// 加载栈由 try/finally 保证配平,这里清空是双保险:任何中途逃逸的帧
|
|
557
|
+
// 都会让后续分析把正常文件误判为环。
|
|
558
|
+
_moduleEvalStack.length = 0;
|
|
559
|
+
_callDepth = 0;
|
|
560
|
+
_totalCalls = 0;
|
|
561
|
+
_activeCallKeys.length = 0;
|
|
87
562
|
}
|
|
88
563
|
|
|
89
|
-
export function setModuleResolver(resolver: ((source: string, fromDir: string) => { ast: Node; filePath: string } | null) | null): void {
|
|
564
|
+
export function setModuleResolver(resolver: ((source: string, fromDir: string) => { ast: Node; filePath: string; json?: unknown } | null) | null): void {
|
|
90
565
|
currentModuleResolver = resolver;
|
|
91
566
|
}
|
|
92
567
|
|
|
93
|
-
let currentModuleResolver: ((source: string, fromDir: string) => { ast: Node; filePath: string } | null) | null = null;
|
|
568
|
+
let currentModuleResolver: ((source: string, fromDir: string) => { ast: Node; filePath: string; json?: unknown } | null) | null = null;
|
|
94
569
|
let currentFileDir = "";
|
|
95
570
|
|
|
571
|
+
let envModules: Record<string, Record<string, TypeValue>> = {};
|
|
572
|
+
let mockModules: Map<string, { fromPath: string; names?: string[] }> = new Map();
|
|
573
|
+
|
|
574
|
+
export function setEnvModules(modules: Record<string, Record<string, TypeValue>>): void {
|
|
575
|
+
envModules = modules;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export function resetEnvModules(): void {
|
|
579
|
+
envModules = {};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export function setMockModules(mocks: Map<string, { fromPath: string; names?: string[] }>): void {
|
|
583
|
+
mockModules = mocks;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export function resetMockModules(): void {
|
|
587
|
+
mockModules = new Map();
|
|
588
|
+
}
|
|
589
|
+
|
|
96
590
|
let _nodeTypeCollector: ((node: Node, tv: TypeValue) => void) | null = null;
|
|
97
591
|
let _sampleCount = 3;
|
|
98
592
|
let _maxConcreteIter = 1000;
|
|
99
593
|
|
|
594
|
+
let _onUnknownBuiltin: ((name: string, loc?: { start: { line: number; column: number }; end: { line: number; column: number } }) => void) | null = null;
|
|
595
|
+
|
|
596
|
+
export function setUnknownBuiltinHandler(handler: ((name: string, loc?: { start: { line: number; column: number }; end: { line: number; column: number } }) => void) | null) {
|
|
597
|
+
_onUnknownBuiltin = handler;
|
|
598
|
+
}
|
|
599
|
+
|
|
100
600
|
export function setSampleCount(count: number): void {
|
|
101
601
|
_sampleCount = count;
|
|
102
602
|
}
|
|
@@ -115,6 +615,277 @@ function recordNodeType(node: Node, tv: TypeValue): void {
|
|
|
115
615
|
}
|
|
116
616
|
}
|
|
117
617
|
|
|
618
|
+
export type CallRecord = {
|
|
619
|
+
fnName: string;
|
|
620
|
+
argTypes: TypeValue[];
|
|
621
|
+
resultType: TypeValue;
|
|
622
|
+
throws: TypeValue;
|
|
623
|
+
/** Line-relative. Per-fn cache replay shifts this by lineDelta — if you
|
|
624
|
+
* add another position field (callee loc, arg loc), extend
|
|
625
|
+
* shiftCallRecordLines in analyzer.ts in the same change. */
|
|
626
|
+
callLoc?: { line: number; column: number };
|
|
627
|
+
targetModule?: string;
|
|
628
|
+
targetExport?: string;
|
|
629
|
+
/** export names the same function value was re-exported under after its
|
|
630
|
+
* defining module (barrel `index.js`, CJS forwarding shims); usage-site
|
|
631
|
+
* records stay name-matchable against them */
|
|
632
|
+
targetAliases?: string[];
|
|
633
|
+
/** module whose evaluation created the function value (definition site).
|
|
634
|
+
* Usage-site collection transitively evaluates the library under require(),
|
|
635
|
+
* so records of the library's INTERNAL calls (no export tag) can still be
|
|
636
|
+
* attributed to the defining file — while test-local functions and builtin
|
|
637
|
+
* statics stay unattributed and cannot pollute same-name candidates. */
|
|
638
|
+
fnModule?: string;
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
// --- Cross-module call attribution ---
|
|
642
|
+
// Module-exported function values get tagged with their (module, export)
|
|
643
|
+
// origin when the module environment is first built (moduleCache fill). The
|
|
644
|
+
// same object reference flows through import bindings to call sites, so
|
|
645
|
+
// recordCall can look the tag up from the evaluated callee value and mark the
|
|
646
|
+
// record as targeting another module. Non-function exports are not tagged.
|
|
647
|
+
|
|
648
|
+
const _exportTags = new WeakMap<object, { module: string; export: string; aliases?: string[] }>();
|
|
649
|
+
|
|
650
|
+
/** Tag a function value with its exporting (module, export). A re-export
|
|
651
|
+
* chain — a barrel `index.js` doing `parseChunked: require('./parse-chunked')`,
|
|
652
|
+
* a CJS shim doing `module.exports = require('../../src/index.js')` — flows
|
|
653
|
+
* ONE function value through several module scopes, and each scope's tagging
|
|
654
|
+
* pass used to overwrite the tag with its own (module, export), so usage-site
|
|
655
|
+
* records ended up pointing at the last intermediary instead of the defining
|
|
656
|
+
* file. Now the first (definition-site) tag sticks; later export names
|
|
657
|
+
* accumulate as aliases. */
|
|
658
|
+
// --- Function definition-site tags ---
|
|
659
|
+
// Which module's evaluation created a function value. Only AST function
|
|
660
|
+
// nodes route through tagFnModule — lazily built builtin tables never enter
|
|
661
|
+
// a module stack, so builtin statics stay unattributed.
|
|
662
|
+
const _fnModuleTags = new WeakMap<object, string>();
|
|
663
|
+
const _moduleEvalStack: string[] = [];
|
|
664
|
+
|
|
665
|
+
function tagFnModule(v: TypeValue): void {
|
|
666
|
+
const top = _moduleEvalStack[_moduleEvalStack.length - 1];
|
|
667
|
+
if (top !== undefined && v.kind === "function") {
|
|
668
|
+
if (!_fnModuleTags.has(v)) _fnModuleTags.set(v, top);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// 执行来源栈:当前正在执行的函数体来自哪个模块(fnModule tag;未 tag 的
|
|
673
|
+
// 函数继承栈顶——本地函数无 tag 时代码在本文件内执行)。recordUnknown 据此
|
|
674
|
+
// 给记录标 originModule,分析侧丢弃使用现场闭包体的泄漏错误。
|
|
675
|
+
const _execOriginStack: (string | undefined)[] = [];
|
|
676
|
+
|
|
677
|
+
/** usage-site 执行标记:外部调用记录的实参里携带的闭包(测试回调等)在
|
|
678
|
+
* case 合成重求值时被执行,其内部错误属于使用现场文件,与本文件分析无关。
|
|
679
|
+
* 分析侧在注入前对全部外部实参调用本函数;执行来源栈读到该标记时
|
|
680
|
+
* recordUnknown.originModule 记为标记值,分析侧据此丢弃。不按模块路径
|
|
681
|
+
* 比较——跨文件依赖(本文件 require 链上加载的模块)的执行记录是合法
|
|
682
|
+
* 的可达分析,不能误伤。 */
|
|
683
|
+
export const USAGE_SITE_MODULE = "__usage_site__";
|
|
684
|
+
|
|
685
|
+
export function setUsageSiteTag(tv: TypeValue, _seen: Set<object> = new Set()): void {
|
|
686
|
+
if (!tv || typeof tv !== "object" || _seen.has(tv)) return;
|
|
687
|
+
_seen.add(tv);
|
|
688
|
+
switch (tv.kind) {
|
|
689
|
+
case "function":
|
|
690
|
+
_fnModuleTags.set(tv, USAGE_SITE_MODULE);
|
|
691
|
+
return;
|
|
692
|
+
case "array":
|
|
693
|
+
return setUsageSiteTag(tv.element, _seen);
|
|
694
|
+
case "tuple":
|
|
695
|
+
for (const e of tv.elements) setUsageSiteTag(e, _seen);
|
|
696
|
+
return;
|
|
697
|
+
case "object":
|
|
698
|
+
for (const v of Object.values(tv.properties)) setUsageSiteTag(v, _seen);
|
|
699
|
+
return;
|
|
700
|
+
case "promise":
|
|
701
|
+
return setUsageSiteTag(tv.value, _seen);
|
|
702
|
+
case "union":
|
|
703
|
+
for (const m of tv.members) setUsageSiteTag(m, _seen);
|
|
704
|
+
return;
|
|
705
|
+
case "refined":
|
|
706
|
+
return setUsageSiteTag(tv.base, _seen);
|
|
707
|
+
default:
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function tagExport(v: TypeValue & { kind: "function" }, filePath: string, name: string): void { const existing = _exportTags.get(v);
|
|
713
|
+
if (existing) {
|
|
714
|
+
if (existing.module === filePath && existing.export === name) return;
|
|
715
|
+
if (!existing.aliases) existing.aliases = [];
|
|
716
|
+
if (!existing.aliases.includes(name)) existing.aliases.push(name);
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
_exportTags.set(v, { module: filePath, export: name });
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function tagModuleExports(env: Environment, filePath: string): void {
|
|
723
|
+
const bindings = env.getOwnBindings();
|
|
724
|
+
for (const [k, v] of Object.entries(bindings)) {
|
|
725
|
+
if (!k.startsWith("__export_")) continue;
|
|
726
|
+
if (v.kind !== "function") continue;
|
|
727
|
+
const name = k === "__export_default" ? "default" : k.slice("__export_".length);
|
|
728
|
+
tagExport(v, filePath, name);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// --- CommonJS module scope ---
|
|
733
|
+
// v1 does not distinguish module systems: every evaluated program (top-level
|
|
734
|
+
// or module load) gets the implicit CJS bindings, so `require` /
|
|
735
|
+
// `module.exports` / `exports.x` patterns analyze even inside ESM files.
|
|
736
|
+
|
|
737
|
+
const _cjsRequireValue = T.fn(["specifier"], { type: "BlockStatement", body: [] } as any, undefined as any);
|
|
738
|
+
|
|
739
|
+
function bindCommonJsGlobals(env: Environment): void {
|
|
740
|
+
if (env.has("module")) return; // already set up as a module scope
|
|
741
|
+
const exportsObj = T.object({});
|
|
742
|
+
// Marker so require() can tell "target never wrote CJS exports" apart from
|
|
743
|
+
// an explicit `module.exports = {...}` replacement.
|
|
744
|
+
(exportsObj as any).__cjsExportsRoot = true;
|
|
745
|
+
const moduleObj = T.object({ exports: exportsObj });
|
|
746
|
+
env.bind("exports", exportsObj);
|
|
747
|
+
env.bind("module", moduleObj);
|
|
748
|
+
env.bind("require", _cjsRequireValue);
|
|
749
|
+
env.bind("__dirname", T.string);
|
|
750
|
+
env.bind("__filename", T.string);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// CJS exports are plain values (`module.exports` itself, or its properties),
|
|
754
|
+
// not `__export_` bindings; tag the function values the same way so
|
|
755
|
+
// cross-module call attribution also works for require()d modules.
|
|
756
|
+
function tagCommonJsExports(env: Environment, filePath: string): void {
|
|
757
|
+
if (!env.has("module")) return;
|
|
758
|
+
const moduleVal = env.lookup("module");
|
|
759
|
+
if (moduleVal.kind !== "object") return;
|
|
760
|
+
const exp = moduleVal.properties["exports"];
|
|
761
|
+
if (!exp) return;
|
|
762
|
+
if (exp.kind === "function") {
|
|
763
|
+
tagExport(exp, filePath, "default");
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
if (exp.kind === "object") {
|
|
767
|
+
for (const [k, v] of Object.entries(exp.properties)) {
|
|
768
|
+
if (v.kind === "function") tagExport(v, filePath, k);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
let _callCollector: ((record: CallRecord) => void) | null = null;
|
|
774
|
+
|
|
775
|
+
export function setCallCollector(collector: ((record: CallRecord) => void) | null): void {
|
|
776
|
+
_callCollector = collector;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function recordCall(
|
|
780
|
+
fnName: string,
|
|
781
|
+
args: TypeValue[],
|
|
782
|
+
result: { value: TypeValue; throws: TypeValue },
|
|
783
|
+
loc: Node["loc"],
|
|
784
|
+
calleeVal?: TypeValue,
|
|
785
|
+
): void {
|
|
786
|
+
if (!_callCollector) return;
|
|
787
|
+
// Synthetic Promise resolve/reject collectors are internal plumbing, not
|
|
788
|
+
// user-code call sites — never feed them into the callsite collector.
|
|
789
|
+
if (calleeVal && _promiseCollectors.has(calleeVal as unknown as object)) return;
|
|
790
|
+
const record: CallRecord = {
|
|
791
|
+
fnName,
|
|
792
|
+
argTypes: args,
|
|
793
|
+
resultType: result.value,
|
|
794
|
+
throws: result.throws,
|
|
795
|
+
callLoc: loc ? { line: loc.start.line, column: loc.start.column } : undefined,
|
|
796
|
+
};
|
|
797
|
+
const tag = calleeVal ? _exportTags.get(calleeVal) : undefined;
|
|
798
|
+
if (tag) {
|
|
799
|
+
record.targetModule = tag.module;
|
|
800
|
+
record.targetExport = tag.export;
|
|
801
|
+
if (tag.aliases && tag.aliases.length > 0) record.targetAliases = [...tag.aliases];
|
|
802
|
+
}
|
|
803
|
+
const fnModule = calleeVal ? _fnModuleTags.get(calleeVal) : undefined;
|
|
804
|
+
if (fnModule) record.fnModule = fnModule;
|
|
805
|
+
_callCollector(record);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
export type UnknownRecord = {
|
|
809
|
+
kind: "method" | "property" | "global";
|
|
810
|
+
name: string;
|
|
811
|
+
receiverType?: TypeValue;
|
|
812
|
+
loc?: { line: number; column: number };
|
|
813
|
+
reason?: string;
|
|
814
|
+
origin?: { line: number; column: number };
|
|
815
|
+
/** 定义当前正在执行的函数的模块(执行来源栈顶)。case 合成重求值会执行
|
|
816
|
+
* 注入实参携带的使用现场闭包体,其内部错误记录属于使用现场文件——
|
|
817
|
+
* 分析侧据此丢弃(行数守卫会被同行号假阳性骗过)。 */
|
|
818
|
+
originModule?: string;
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
let _unknownCollector: ((record: UnknownRecord) => void) | null = null;
|
|
822
|
+
|
|
823
|
+
export function setUnknownCollector(collector: ((record: UnknownRecord) => void) | null): void {
|
|
824
|
+
_unknownCollector = collector;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// --- Provenance side table ---
|
|
828
|
+
// Maps an argument TypeValue (annotated at its call-site expression in
|
|
829
|
+
// evaluateArgs) back to the source location of the expression that produced it.
|
|
830
|
+
// The same object references flow into parameter bindings, so recordUnknown can
|
|
831
|
+
// reverse-lookup where a receiver value originated from. Disabled by default;
|
|
832
|
+
// zero overhead when off.
|
|
833
|
+
|
|
834
|
+
const _originMap = new WeakMap<TypeValue, { line: number; column: number }>();
|
|
835
|
+
let _provenanceEnabled = false;
|
|
836
|
+
|
|
837
|
+
export function setProvenanceTracking(enabled: boolean): void {
|
|
838
|
+
_provenanceEnabled = enabled;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/** 克隆 TypeValue 后拷贝 provenance(tagParamArg 会 clone,丢 origin 会导致 M5 回归) */
|
|
842
|
+
function copyOrigin(from: TypeValue, to: TypeValue): void {
|
|
843
|
+
if (!_provenanceEnabled || !from || !to) return;
|
|
844
|
+
const o = _originMap.get(from);
|
|
845
|
+
if (o) _originMap.set(to, o);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function extractOrigin(
|
|
849
|
+
tv: TypeValue | undefined,
|
|
850
|
+
): { line: number; column: number } | undefined {
|
|
851
|
+
if (!_provenanceEnabled || !tv) return undefined;
|
|
852
|
+
const direct = _originMap.get(tv);
|
|
853
|
+
if (direct) return direct;
|
|
854
|
+
if (tv.kind === "union") {
|
|
855
|
+
for (const member of tv.members) {
|
|
856
|
+
const found = _originMap.get(member);
|
|
857
|
+
if (found) return found;
|
|
858
|
+
}
|
|
859
|
+
} else if (tv.kind === "object") {
|
|
860
|
+
for (const propVal of Object.values(tv.properties)) {
|
|
861
|
+
const found = _originMap.get(propVal);
|
|
862
|
+
if (found) return found;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return undefined;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/** Own-property check for plain record tables. Plain `{}` records inherit
|
|
869
|
+
* Object.prototype, so `in` / bracket access would leak native members
|
|
870
|
+
* (e.g. the real JS `hasOwnProperty`, `toString`) into the type system as
|
|
871
|
+
* bogus values. */
|
|
872
|
+
function hasOwnProp(props: Record<string, unknown>, name: string): boolean {
|
|
873
|
+
return Object.prototype.hasOwnProperty.call(props, name);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function recordUnknown(r: Omit<UnknownRecord, "loc"> & { loc?: Node["loc"] }): void {
|
|
877
|
+
if (!_unknownCollector) return;
|
|
878
|
+
const origin = r.kind === "method" || r.kind === "property"
|
|
879
|
+
? extractOrigin(r.receiverType)
|
|
880
|
+
: undefined;
|
|
881
|
+
_unknownCollector({
|
|
882
|
+
...r,
|
|
883
|
+
origin,
|
|
884
|
+
originModule: _execOriginStack.length > 0 ? _execOriginStack[_execOriginStack.length - 1] : undefined,
|
|
885
|
+
loc: r.loc ? { line: r.loc.start.line, column: r.loc.start.column } : undefined,
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
|
|
118
889
|
function distributeOverUnion(
|
|
119
890
|
tv: TypeValue,
|
|
120
891
|
fn: (member: TypeValue) => TypeValue,
|
|
@@ -125,23 +896,210 @@ function distributeOverUnion(
|
|
|
125
896
|
return fn(tv);
|
|
126
897
|
}
|
|
127
898
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
899
|
+
// --- Computed member access with a non-literal key (key-set evaluation) ---
|
|
900
|
+
//
|
|
901
|
+
// `obj[key]` where the key is a string/number type rather than a known
|
|
902
|
+
// literal: the set of reachable values is exactly the object's own property
|
|
903
|
+
// values (or the tuple's elements). When they are structurally uniform one
|
|
904
|
+
// shared type answers every key; otherwise the union of all values does
|
|
905
|
+
// (bounded, so pathological records cannot blow up the type graph).
|
|
906
|
+
const MAX_KEYSET_PROPS = 16;
|
|
907
|
+
const MAX_KEYSET_MEMBERS = 12;
|
|
908
|
+
const MAX_KEYSET_TABLE_FNS = 8;
|
|
909
|
+
|
|
910
|
+
/** Structural fingerprint for key-set uniformity. Mirrors the analyzer's
|
|
911
|
+
* typeStructureKey semantics (literal values included, so {a:1,b:2} is NOT
|
|
912
|
+
* uniform) but keeps function values identity-distinct: two different
|
|
913
|
+
* closures (a method table's entries) must never count as "uniform", or
|
|
914
|
+
* `Table[f]()` would silently pick one function's result. */
|
|
915
|
+
const keySetObjIds = new WeakMap<object, number>();
|
|
916
|
+
let keySetObjIdSeq = 0;
|
|
917
|
+
|
|
918
|
+
function objectId(tv: object): number {
|
|
919
|
+
let id = keySetObjIds.get(tv);
|
|
920
|
+
if (id === undefined) {
|
|
921
|
+
id = ++keySetObjIdSeq;
|
|
922
|
+
keySetObjIds.set(tv, id);
|
|
923
|
+
}
|
|
924
|
+
return id;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const keySetKeyCache = new WeakMap<object, string>();
|
|
928
|
+
const KEYSET_KEY_NODE_BUDGET = 64;
|
|
929
|
+
|
|
930
|
+
function keySetStructureKeyUncached(tv: TypeValue, budget: { left: number }): string {
|
|
931
|
+
if (budget.left <= 0) return `oversized@${objectId(tv)}`;
|
|
932
|
+
budget.left--;
|
|
933
|
+
switch (tv.kind) {
|
|
934
|
+
case "literal":
|
|
935
|
+
return `lit(${typeof tv.value}:${String(tv.value)})`;
|
|
936
|
+
case "primitive":
|
|
937
|
+
return `prim(${tv.type})`;
|
|
938
|
+
case "array":
|
|
939
|
+
return `arr(${keySetStructureKeyUncached(tv.element, budget)})`;
|
|
940
|
+
case "tuple":
|
|
941
|
+
return `tup(${tv.elements.map((e) => keySetStructureKeyUncached(e, budget)).join(",")})`;
|
|
942
|
+
case "object":
|
|
943
|
+
return `obj(${Object.keys(tv.properties).sort().map((k) => `${k}:${keySetStructureKeyUncached(tv.properties[k], budget)}`).join(",")})`;
|
|
944
|
+
case "promise":
|
|
945
|
+
return `prom(${keySetStructureKeyUncached(tv.value, budget)})`;
|
|
946
|
+
case "instance":
|
|
947
|
+
return `inst(${tv.className})`;
|
|
948
|
+
case "refined":
|
|
949
|
+
return `ref(${keySetStructureKeyUncached(tv.base, budget)})`;
|
|
950
|
+
case "union":
|
|
951
|
+
return `uni(${tv.members.map((m) => keySetStructureKeyUncached(m, budget)).sort().join("|")})`;
|
|
952
|
+
case "function":
|
|
953
|
+
return `fn#${objectId(tv)}`;
|
|
954
|
+
default:
|
|
955
|
+
return tv.kind;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/** Structural fingerprint for key-set uniformity. Mirrors the analyzer's
|
|
960
|
+
* typeStructureKey semantics (literal values included, so {a:1,b:2} is NOT
|
|
961
|
+
* uniform) but keeps function values identity-distinct: two different
|
|
962
|
+
* closures (a method table's entries) must never count as "uniform", or
|
|
963
|
+
* `Table[f]()` would silently pick one function's result. Memoized per
|
|
964
|
+
* type value with a node budget — fixture-scale structures must not pay
|
|
965
|
+
* string building on every dynamic-key access. */
|
|
966
|
+
function keySetStructureKey(tv: TypeValue): string {
|
|
967
|
+
const cached = keySetKeyCache.get(tv);
|
|
968
|
+
if (cached !== undefined) return cached;
|
|
969
|
+
const key = keySetStructureKeyUncached(tv, { left: KEYSET_KEY_NODE_BUDGET });
|
|
970
|
+
keySetKeyCache.set(tv, key);
|
|
971
|
+
return key;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function objectKeySetAccess(obj: TypeValue & { kind: "object" }): TypeValue {
|
|
975
|
+
const keys = Object.keys(obj.properties);
|
|
976
|
+
if (keys.length === 0) return T.undefined;
|
|
977
|
+
if (keys.length > MAX_KEYSET_PROPS) return T.unknown;
|
|
978
|
+
const values = keys.map((k) => obj.properties[k]);
|
|
979
|
+
if (values.every((v) => v.kind === "unknown")) return T.unknown;
|
|
980
|
+
const firstKey = keySetStructureKey(values[0]);
|
|
981
|
+
if (values.every((v) => keySetStructureKey(v) === firstKey)) {
|
|
982
|
+
return values[0];
|
|
983
|
+
}
|
|
984
|
+
// Method tables (`Strings[f]()`) union their member functions; calling a
|
|
985
|
+
// union of N functions runs N evaluations, so keep that budget tighter
|
|
986
|
+
// than the data-member cap.
|
|
987
|
+
if (values.every((v) => v.kind === "function") && values.length > MAX_KEYSET_TABLE_FNS) {
|
|
988
|
+
return T.unknown;
|
|
989
|
+
}
|
|
990
|
+
const union = simplifyUnion(values);
|
|
991
|
+
if (union.kind === "union" && union.members.length > MAX_KEYSET_MEMBERS) return T.unknown;
|
|
992
|
+
return union;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/** Does this key type admit key-set evaluation? Literal keys (and unions
|
|
996
|
+
* that collapse to literals) are handled precisely; everything else falls
|
|
997
|
+
* back to the property/element set. */
|
|
998
|
+
function isDynamicStringKey(propVal: TypeValue): boolean {
|
|
999
|
+
if (propVal.kind === "unknown" || (propVal.kind === "primitive" && propVal.type === "string")) return true;
|
|
1000
|
+
if (propVal.kind === "union") {
|
|
1001
|
+
return propVal.members.some((m) => isDynamicStringKey(m) || (m.kind === "literal" && typeof m.value === "string"));
|
|
1002
|
+
}
|
|
1003
|
+
return false;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function isDynamicNumberKey(propVal: TypeValue): boolean {
|
|
1007
|
+
if (propVal.kind === "unknown" || (propVal.kind === "primitive" && propVal.type === "number")) return true;
|
|
1008
|
+
if (propVal.kind === "union") {
|
|
1009
|
+
return propVal.members.some((m) => isDynamicNumberKey(m) || (m.kind === "literal" && typeof m.value === "number"));
|
|
1010
|
+
}
|
|
1011
|
+
return false;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function unionOfLiteralKind(tv: TypeValue, pred: (v: unknown) => boolean): tv is TypeValue & { kind: "union" } {
|
|
1015
|
+
return tv.kind === "union" && tv.members.every((m) => m.kind === "literal" && pred(m.value));
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/** Computed `obj[key]` on one union member of the receiver. Shared by
|
|
1019
|
+
* MemberExpression / OptionalMemberExpression. `markMiss` records the
|
|
1020
|
+
* unknown-property diagnostic for literal string keys that miss. */
|
|
1021
|
+
function computedMemberOnMember(
|
|
1022
|
+
obj: TypeValue,
|
|
1023
|
+
propVal: TypeValue,
|
|
1024
|
+
markMiss: () => void,
|
|
132
1025
|
): TypeValue {
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
1026
|
+
if (obj.kind === "object") {
|
|
1027
|
+
if (propVal.kind === "literal" && typeof propVal.value === "string") {
|
|
1028
|
+
return (hasOwnProp(obj.properties, propVal.value) ? obj.properties[propVal.value] : undefined) ?? T.undefined;
|
|
1029
|
+
}
|
|
1030
|
+
if (unionOfLiteralKind(propVal, (v) => typeof v === "string")) {
|
|
1031
|
+
return simplifyUnion(
|
|
1032
|
+
(propVal as TypeValue & { kind: "union" }).members.map((m) =>
|
|
1033
|
+
(m as TypeValue & { kind: "literal" }).value as string,
|
|
1034
|
+
).map((k) => (hasOwnProp(obj.properties, k) ? obj.properties[k] : undefined) ?? T.undefined),
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
if (isDynamicStringKey(propVal)) {
|
|
1038
|
+
return objectKeySetAccess(obj);
|
|
1039
|
+
}
|
|
1040
|
+
if (propVal.kind === "literal" && typeof propVal.value === "string") {
|
|
1041
|
+
markMiss();
|
|
1042
|
+
}
|
|
1043
|
+
return T.unknown;
|
|
1044
|
+
}
|
|
1045
|
+
if (obj.kind === "array" || obj.kind === "tuple") {
|
|
1046
|
+
if (propVal.kind === "literal" && typeof propVal.value === "number") {
|
|
1047
|
+
if (obj.kind === "tuple") return obj.elements[propVal.value] ?? T.undefined;
|
|
1048
|
+
return obj.element;
|
|
1049
|
+
}
|
|
1050
|
+
// A string-keyed read off an array/tuple hits no data property (real
|
|
1051
|
+
// JS yields undefined, same as a missed key on an object) — returning
|
|
1052
|
+
// unknown here would poison any union the read flows into.
|
|
1053
|
+
if (propVal.kind === "literal" && typeof propVal.value === "string") {
|
|
1054
|
+
return T.undefined;
|
|
1055
|
+
}
|
|
1056
|
+
if (unionOfLiteralKind(propVal, (v) => typeof v === "number")) {
|
|
1057
|
+
const indices = (propVal as TypeValue & { kind: "union" }).members.map(
|
|
1058
|
+
(m) => (m as TypeValue & { kind: "literal" }).value as number,
|
|
1059
|
+
);
|
|
1060
|
+
if (obj.kind === "tuple") {
|
|
1061
|
+
return simplifyUnion(indices.map((i) => obj.elements[i] ?? T.undefined));
|
|
1062
|
+
}
|
|
1063
|
+
return obj.element;
|
|
1064
|
+
}
|
|
1065
|
+
if (isDynamicNumberKey(propVal)) {
|
|
1066
|
+
// arr[anyNumber]: tuple → union of its elements, array → element type
|
|
1067
|
+
if (obj.kind === "tuple") {
|
|
1068
|
+
return obj.elements.length > 0 ? simplifyUnion(obj.elements) : T.undefined;
|
|
1069
|
+
}
|
|
1070
|
+
return obj.element;
|
|
1071
|
+
}
|
|
1072
|
+
return T.unknown;
|
|
1073
|
+
}
|
|
1074
|
+
if (obj.kind === "instance" && isDynamicStringKey(propVal)) {
|
|
1075
|
+
// Known instances (Map/Set/Buffer singletons...) carry no string-keyed
|
|
1076
|
+
// data properties; a dynamic key resolves to undefined rather than
|
|
1077
|
+
// poisoning enclosing unions with unknown.
|
|
1078
|
+
return T.undefined;
|
|
137
1079
|
}
|
|
138
|
-
if (
|
|
139
|
-
|
|
1080
|
+
if (propVal.kind === "literal" && typeof propVal.value === "string") {
|
|
1081
|
+
markMiss();
|
|
140
1082
|
}
|
|
141
|
-
|
|
142
|
-
|
|
1083
|
+
return T.unknown;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function evaluateComputedMemberAccess(objVal: TypeValue, propVal: TypeValue, node: Node): TypeValue {
|
|
1087
|
+
let propMissed = false;
|
|
1088
|
+
const result = distributeOverUnion(objVal, (obj) =>
|
|
1089
|
+
computedMemberOnMember(obj, propVal, () => {
|
|
1090
|
+
propMissed = true;
|
|
1091
|
+
}),
|
|
1092
|
+
);
|
|
1093
|
+
if (propMissed && propVal.kind === "literal" && typeof propVal.value === "string") {
|
|
1094
|
+
recordUnknown({
|
|
1095
|
+
kind: "property",
|
|
1096
|
+
name: propVal.value,
|
|
1097
|
+
receiverType: objVal,
|
|
1098
|
+
loc: node.loc,
|
|
1099
|
+
reason: `no property '${propVal.value}' on ${objVal.kind}`,
|
|
1100
|
+
});
|
|
143
1101
|
}
|
|
144
|
-
return
|
|
1102
|
+
return result;
|
|
145
1103
|
}
|
|
146
1104
|
|
|
147
1105
|
let _unreachableRanges: SourceRange[] = [];
|
|
@@ -168,8 +1126,24 @@ function evaluateStatements(
|
|
|
168
1126
|
|
|
169
1127
|
for (let i = 0; i < stmts.length; i++) {
|
|
170
1128
|
const stmt = stmts[i];
|
|
1129
|
+
|
|
1130
|
+
const inlineDirectives = extractInlineDirectives(stmt);
|
|
1131
|
+
const savedAs = _activeAsOverride;
|
|
1132
|
+
const savedReplacements = _activeReplacements;
|
|
1133
|
+
|
|
1134
|
+
const asDir = inlineDirectives.find((d): d is InlineDirective & { kind: "as" } => d.kind === "as");
|
|
1135
|
+
const replaceDirs = inlineDirectives.filter((d): d is InlineDirective & { kind: "replace" } => d.kind === "replace");
|
|
1136
|
+
|
|
1137
|
+
_activeAsOverride = asDir?.typeExpr ?? null;
|
|
1138
|
+
_activeReplacements = replaceDirs.length > 0
|
|
1139
|
+
? replaceDirs.map((d) => ({ targetSource: d.targetSource, typeExpr: d.typeExpr }))
|
|
1140
|
+
: [];
|
|
1141
|
+
|
|
171
1142
|
const result = evaluate(stmt, currentEnv);
|
|
172
1143
|
|
|
1144
|
+
_activeAsOverride = savedAs;
|
|
1145
|
+
_activeReplacements = savedReplacements;
|
|
1146
|
+
|
|
173
1147
|
if (isThrow(result)) {
|
|
174
1148
|
collectUnreachable(stmts, i + 1);
|
|
175
1149
|
return result;
|
|
@@ -218,6 +1192,9 @@ function describeParam(p: Node): string {
|
|
|
218
1192
|
}
|
|
219
1193
|
|
|
220
1194
|
export function evaluate(node: Node, env: Environment): EvalResult {
|
|
1195
|
+
const replacement = matchesReplacement(node);
|
|
1196
|
+
if (replacement) return replacement;
|
|
1197
|
+
|
|
221
1198
|
const result = evaluateNode(node, env);
|
|
222
1199
|
if (_nodeTypeCollector && node.loc && !isReturn(result) && !isBranch(result) && !isThrow(result)) {
|
|
223
1200
|
recordNodeType(node, result);
|
|
@@ -234,6 +1211,7 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
|
|
|
234
1211
|
return evaluateStatements(node.body, env);
|
|
235
1212
|
|
|
236
1213
|
case "ExpressionStatement":
|
|
1214
|
+
if (_activeAsOverride) return _activeAsOverride;
|
|
237
1215
|
return evaluate(node.expression, env);
|
|
238
1216
|
|
|
239
1217
|
case "NumericLiteral":
|
|
@@ -248,13 +1226,69 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
|
|
|
248
1226
|
case "NullLiteral":
|
|
249
1227
|
return T.null;
|
|
250
1228
|
|
|
1229
|
+
case "RegExpLiteral":
|
|
1230
|
+
return createRegExpType();
|
|
1231
|
+
|
|
251
1232
|
case "Identifier": {
|
|
252
1233
|
if (node.name === "undefined") return T.undefined;
|
|
1234
|
+
// Check for built-in global objects (env-injected values take priority,
|
|
1235
|
+
// e.g. @nudo:env es binds fnSig-backed globals with precise impls).
|
|
1236
|
+
// hasOwnProp guard: `in` would hit the JS prototype chain and leak
|
|
1237
|
+
// native functions for names like `hasOwnProperty`.
|
|
1238
|
+
if (hasOwnProp(BUILTIN_STATIC_METHODS, node.name) && !env.has(node.name)) {
|
|
1239
|
+
const builtin = BUILTIN_STATIC_METHODS[node.name];
|
|
1240
|
+
if (typeof builtin === "object" && builtin !== null && !("kind" in builtin)) {
|
|
1241
|
+
// It's a namespace object (like Date, Math, JSON); memoized so
|
|
1242
|
+
// `'x'.constructor === String` hits reference equality.
|
|
1243
|
+
return builtinClassValue(node.name);
|
|
1244
|
+
}
|
|
1245
|
+
// It's a direct value (like parseInt, isNaN)
|
|
1246
|
+
return builtin as TypeValue;
|
|
1247
|
+
}
|
|
1248
|
+
// Constructible built-in classes without static-method coverage (Error,
|
|
1249
|
+
// Map, Set, ...) still resolve to a namespace object so `X.prototype`
|
|
1250
|
+
// can be typed as an instance.
|
|
1251
|
+
if (BUILTIN_PROTOTYPE_CLASSES.has(node.name) && !env.has(node.name)) {
|
|
1252
|
+
return builtinClassValue(node.name);
|
|
1253
|
+
}
|
|
1254
|
+
// Check if it looks like a built-in but isn't covered
|
|
1255
|
+
if (node.name[0] === node.name[0].toUpperCase() && !env.has(node.name)) {
|
|
1256
|
+
if (_onUnknownBuiltin) {
|
|
1257
|
+
_onUnknownBuiltin(node.name, node.loc as any);
|
|
1258
|
+
}
|
|
1259
|
+
recordUnknown({
|
|
1260
|
+
kind: "global",
|
|
1261
|
+
name: node.name,
|
|
1262
|
+
loc: node.loc,
|
|
1263
|
+
reason: `unknown global identifier '${node.name}'`,
|
|
1264
|
+
});
|
|
1265
|
+
// Unresolved builtins propagate unknown (not undefined) so property
|
|
1266
|
+
// access on them degrades to unknown instead of a false
|
|
1267
|
+
// "property does not exist on undefined" error.
|
|
1268
|
+
return T.unknown;
|
|
1269
|
+
}
|
|
1270
|
+
// Bare `hasOwnProperty(...)`-style references resolve through the
|
|
1271
|
+
// global object's prototype chain in real JS — Object.prototype
|
|
1272
|
+
// members act as implicit globals (own-guarded against proto leak).
|
|
1273
|
+
if (!env.has(node.name) && hasOwnProp(OBJECT_PROTOTYPE_METHODS, node.name)) {
|
|
1274
|
+
return OBJECT_PROTOTYPE_METHODS[node.name];
|
|
1275
|
+
}
|
|
253
1276
|
return env.lookup(node.name);
|
|
254
1277
|
}
|
|
255
1278
|
|
|
256
|
-
case "ThisExpression":
|
|
257
|
-
|
|
1279
|
+
case "ThisExpression": {
|
|
1280
|
+
// Un-bound `this` (plain function called without a receiver, e.g. a
|
|
1281
|
+
// callsite-synthesized case): the receiver is unknowable statically, so
|
|
1282
|
+
// degrade to unknown — `this.x` then warns (unknown-recv) instead of
|
|
1283
|
+
// erroring with "on type 'undefined'". Explicit bindings (constructors,
|
|
1284
|
+
// obj.f() receivers, .call(thisArg)) take precedence unchanged.
|
|
1285
|
+
const thisVal = env.lookup("this");
|
|
1286
|
+
// T.undefined is `{ kind: "literal", value: undefined }`.
|
|
1287
|
+
if (thisVal === undefined || (thisVal.kind === "literal" && thisVal.value === undefined)) {
|
|
1288
|
+
return T.unknown;
|
|
1289
|
+
}
|
|
1290
|
+
return thisVal;
|
|
1291
|
+
}
|
|
258
1292
|
|
|
259
1293
|
case "TemplateLiteral": {
|
|
260
1294
|
if (node.expressions.length === 0 && node.quasis.length === 1) {
|
|
@@ -291,22 +1325,39 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
|
|
|
291
1325
|
if (node.operator === "instanceof") {
|
|
292
1326
|
return evaluateInstanceof(leftVal, rightVal, node.right, env);
|
|
293
1327
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
1328
|
+
if (node.operator === "in") {
|
|
1329
|
+
return distributeBinaryOverUnion(leftVal, rightVal, (l, r) => evaluateInMember(l, r));
|
|
1330
|
+
}
|
|
1331
|
+
if (node.operator === "===" || node.operator === "!==") {
|
|
1332
|
+
const identity = builtinProtoIdentityEq(leftVal, rightVal);
|
|
1333
|
+
if (identity !== null) {
|
|
1334
|
+
return T.literal(node.operator === "===" ? identity : !identity);
|
|
1335
|
+
}
|
|
1336
|
+
const classIdentity = builtinClassIdentityEq(leftVal, rightVal);
|
|
1337
|
+
if (classIdentity !== null) {
|
|
1338
|
+
return T.literal(node.operator === "===" ? classIdentity : !classIdentity);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// 统一二元:代数优先 → 宽松相等 → 外延 Ops
|
|
1343
|
+
const bin = evalBinaryValue(node.operator, leftVal, rightVal);
|
|
1344
|
+
if (bin !== undefined) return bin;
|
|
1345
|
+
return T.unknown;
|
|
297
1346
|
}
|
|
298
1347
|
|
|
299
1348
|
case "UnaryExpression": {
|
|
300
1349
|
const argVal = evaluate(node.argument, env);
|
|
301
1350
|
if (isReturn(argVal) || isBranch(argVal) || isThrow(argVal)) return argVal;
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
1351
|
+
// 一元语言表面经代数(typeof / ! / -)
|
|
1352
|
+
const unary = tryAbsUnary(node.operator, argVal);
|
|
1353
|
+
if (unary !== undefined) return unary;
|
|
305
1354
|
if (node.operator === "!") {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
1355
|
+
// 代数未接住:对象恒真等 host 知识
|
|
1356
|
+
return distributeOverUnion(argVal, (v) => {
|
|
1357
|
+
const decided = definiteBoolean(v);
|
|
1358
|
+
if (decided !== null) return T.literal(!decided);
|
|
1359
|
+
return Ops.not(v);
|
|
1360
|
+
});
|
|
310
1361
|
}
|
|
311
1362
|
return T.unknown;
|
|
312
1363
|
}
|
|
@@ -321,6 +1372,13 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
|
|
|
321
1372
|
const rv = evaluate(node.right, env);
|
|
322
1373
|
return isReturn(rv) || isBranch(rv) || isThrow(rv) ? rv : rv;
|
|
323
1374
|
}
|
|
1375
|
+
// Object-ish left operands are always truthy: `Buffer && f(x)` is f(x)
|
|
1376
|
+
const leftTruthy = definiteBoolean(leftVal);
|
|
1377
|
+
if (leftTruthy === true) {
|
|
1378
|
+
const rv = evaluate(node.right, env);
|
|
1379
|
+
return isReturn(rv) || isBranch(rv) || isThrow(rv) ? rv : rv;
|
|
1380
|
+
}
|
|
1381
|
+
if (leftTruthy === false) return leftVal;
|
|
324
1382
|
const rv = evaluate(node.right, env);
|
|
325
1383
|
const rightTV = isReturn(rv) || isBranch(rv) || isThrow(rv) ? T.unknown : rv;
|
|
326
1384
|
return simplifyUnion([leftVal, rightTV]);
|
|
@@ -332,6 +1390,12 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
|
|
|
332
1390
|
const rv = evaluate(node.right, env);
|
|
333
1391
|
return isReturn(rv) || isBranch(rv) || isThrow(rv) ? rv : rv;
|
|
334
1392
|
}
|
|
1393
|
+
const leftTruthy = definiteBoolean(leftVal);
|
|
1394
|
+
if (leftTruthy === true) return leftVal;
|
|
1395
|
+
if (leftTruthy === false) {
|
|
1396
|
+
const rv = evaluate(node.right, env);
|
|
1397
|
+
return isReturn(rv) || isBranch(rv) || isThrow(rv) ? rv : rv;
|
|
1398
|
+
}
|
|
335
1399
|
const rv = evaluate(node.right, env);
|
|
336
1400
|
const rightTV = isReturn(rv) || isBranch(rv) || isThrow(rv) ? T.unknown : rv;
|
|
337
1401
|
return simplifyUnion([leftVal, rightTV]);
|
|
@@ -345,9 +1409,17 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
|
|
|
345
1409
|
const rv = evaluate(node.right, env);
|
|
346
1410
|
return isReturn(rv) || isBranch(rv) || isThrow(rv) ? rv : rv;
|
|
347
1411
|
}
|
|
1412
|
+
// For non-literal types, narrow by removing null/undefined from the left side
|
|
1413
|
+
const narrowedLeft = subtractType(leftVal, (m) =>
|
|
1414
|
+
(m.kind === "literal" && (m.value === null || m.value === undefined))
|
|
1415
|
+
);
|
|
1416
|
+
if (narrowedLeft.kind !== "never") {
|
|
1417
|
+
return narrowedLeft;
|
|
1418
|
+
}
|
|
1419
|
+
// If all members were null/undefined, use the right side
|
|
348
1420
|
const rv = evaluate(node.right, env);
|
|
349
1421
|
const rightTV = isReturn(rv) || isBranch(rv) || isThrow(rv) ? T.unknown : rv;
|
|
350
|
-
return
|
|
1422
|
+
return rightTV;
|
|
351
1423
|
}
|
|
352
1424
|
|
|
353
1425
|
return T.unknown;
|
|
@@ -364,12 +1436,19 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
|
|
|
364
1436
|
? evaluate(node.consequent, trueEnv)
|
|
365
1437
|
: evaluate(node.alternate, falseEnv);
|
|
366
1438
|
}
|
|
1439
|
+
// Object-ish / otherwise-decidable tests pick their branch statically
|
|
1440
|
+
const decided = definiteBoolean(testVal);
|
|
1441
|
+
if (decided !== null) {
|
|
1442
|
+
return decided
|
|
1443
|
+
? evaluate(node.consequent, trueEnv)
|
|
1444
|
+
: evaluate(node.alternate, falseEnv);
|
|
1445
|
+
}
|
|
367
1446
|
|
|
368
1447
|
const cResult = evaluate(node.consequent, trueEnv);
|
|
369
1448
|
const aResult = evaluate(node.alternate, falseEnv);
|
|
370
1449
|
const cVal = isReturn(cResult) ? cResult.value : isBranch(cResult) ? cResult.returnedValue : isThrow(cResult) ? T.never : cResult;
|
|
371
1450
|
const aVal = isReturn(aResult) ? aResult.value : isBranch(aResult) ? aResult.returnedValue : isThrow(aResult) ? T.never : aResult;
|
|
372
|
-
return
|
|
1451
|
+
return joinOrUnion(cVal, aVal);
|
|
373
1452
|
}
|
|
374
1453
|
|
|
375
1454
|
case "IfStatement": {
|
|
@@ -378,6 +1457,9 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
|
|
|
378
1457
|
const testVal = evaluate(test, env);
|
|
379
1458
|
if (isReturn(testVal) || isBranch(testVal) || isThrow(testVal)) return testVal;
|
|
380
1459
|
|
|
1460
|
+
// Kernel Φ:从测试表达式提取约束,真/假分支分别合取
|
|
1461
|
+
const phis = phiFromTest(test);
|
|
1462
|
+
|
|
381
1463
|
if (testVal.kind === "literal") {
|
|
382
1464
|
if (testVal.value) {
|
|
383
1465
|
if (node.alternate?.loc) {
|
|
@@ -386,7 +1468,12 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
|
|
|
386
1468
|
end: { line: node.alternate.loc.end.line, column: node.alternate.loc.end.column },
|
|
387
1469
|
});
|
|
388
1470
|
}
|
|
389
|
-
|
|
1471
|
+
pushPhi(combinePhi(currentPhi(), phis.whenTrue));
|
|
1472
|
+
try {
|
|
1473
|
+
return evaluate(node.consequent, trueEnv);
|
|
1474
|
+
} finally {
|
|
1475
|
+
popPhi();
|
|
1476
|
+
}
|
|
390
1477
|
}
|
|
391
1478
|
if (node.consequent.loc) {
|
|
392
1479
|
_unreachableRanges.push({
|
|
@@ -394,15 +1481,52 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
|
|
|
394
1481
|
end: { line: node.consequent.loc.end.line, column: node.consequent.loc.end.column },
|
|
395
1482
|
});
|
|
396
1483
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
1484
|
+
if (!node.alternate) return T.undefined;
|
|
1485
|
+
pushPhi(combinePhi(currentPhi(), phis.whenFalse));
|
|
1486
|
+
try {
|
|
1487
|
+
return evaluate(node.alternate, falseEnv);
|
|
1488
|
+
} finally {
|
|
1489
|
+
popPhi();
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
// Object-ish / otherwise-decidable tests pick their branch statically
|
|
1493
|
+
// (e.g. `if (proto && proto.isImmutable)` once && resolves).
|
|
1494
|
+
const decided = definiteBoolean(testVal);
|
|
1495
|
+
if (decided !== null) {
|
|
1496
|
+
if (decided) {
|
|
1497
|
+
pushPhi(combinePhi(currentPhi(), phis.whenTrue));
|
|
1498
|
+
try {
|
|
1499
|
+
return evaluate(node.consequent, trueEnv);
|
|
1500
|
+
} finally {
|
|
1501
|
+
popPhi();
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
if (!node.alternate) return T.undefined;
|
|
1505
|
+
pushPhi(combinePhi(currentPhi(), phis.whenFalse));
|
|
1506
|
+
try {
|
|
1507
|
+
return evaluate(node.alternate, falseEnv);
|
|
1508
|
+
} finally {
|
|
1509
|
+
popPhi();
|
|
1510
|
+
}
|
|
400
1511
|
}
|
|
401
1512
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
1513
|
+
// 两边都探索:分别用各自 Φ
|
|
1514
|
+
pushPhi(combinePhi(currentPhi(), phis.whenTrue));
|
|
1515
|
+
let consequentResult: EvalResult;
|
|
1516
|
+
try {
|
|
1517
|
+
consequentResult = evaluate(node.consequent, trueEnv);
|
|
1518
|
+
} finally {
|
|
1519
|
+
popPhi();
|
|
1520
|
+
}
|
|
1521
|
+
pushPhi(combinePhi(currentPhi(), phis.whenFalse));
|
|
1522
|
+
let alternateResult: EvalResult | null = null;
|
|
1523
|
+
try {
|
|
1524
|
+
alternateResult = node.alternate
|
|
1525
|
+
? evaluate(node.alternate, falseEnv)
|
|
1526
|
+
: null;
|
|
1527
|
+
} finally {
|
|
1528
|
+
popPhi();
|
|
1529
|
+
}
|
|
406
1530
|
|
|
407
1531
|
const cReturns = isReturn(consequentResult);
|
|
408
1532
|
const cBranches = isBranch(consequentResult);
|
|
@@ -427,21 +1551,21 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
|
|
|
427
1551
|
}
|
|
428
1552
|
|
|
429
1553
|
if (aThrows) {
|
|
430
|
-
const cVal = cReturns ? consequentResult.value
|
|
431
|
-
: cBranches ? consequentResult.returnedValue
|
|
1554
|
+
const cVal = cReturns ? (consequentResult as ReturnSignal).value
|
|
1555
|
+
: cBranches ? (consequentResult as BranchSignal).returnedValue
|
|
432
1556
|
: consequentResult as TypeValue;
|
|
433
1557
|
return makeBranch(cVal, trueEnv);
|
|
434
1558
|
}
|
|
435
1559
|
|
|
436
|
-
const cVal = cReturns ? consequentResult.value
|
|
437
|
-
: cBranches ? consequentResult.returnedValue
|
|
1560
|
+
const cVal = cReturns ? (consequentResult as ReturnSignal).value
|
|
1561
|
+
: cBranches ? (consequentResult as BranchSignal).returnedValue
|
|
438
1562
|
: consequentResult as TypeValue;
|
|
439
1563
|
const aVal = aReturns ? (alternateResult as ReturnSignal).value
|
|
440
1564
|
: aBranches ? (alternateResult as BranchSignal).returnedValue
|
|
441
1565
|
: alternateResult as TypeValue | null;
|
|
442
1566
|
|
|
443
1567
|
if (cReturns && aReturns) {
|
|
444
|
-
return makeReturn(
|
|
1568
|
+
return makeReturn(joinOrUnion(cVal, aVal!));
|
|
445
1569
|
}
|
|
446
1570
|
|
|
447
1571
|
if (cReturns && !node.alternate) {
|
|
@@ -450,7 +1574,7 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
|
|
|
450
1574
|
|
|
451
1575
|
if (cReturns && node.alternate) {
|
|
452
1576
|
if (aReturns) {
|
|
453
|
-
return makeReturn(
|
|
1577
|
+
return makeReturn(joinOrUnion(cVal, aVal!));
|
|
454
1578
|
}
|
|
455
1579
|
return makeBranch(cVal, falseEnv);
|
|
456
1580
|
}
|
|
@@ -466,12 +1590,13 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
|
|
|
466
1590
|
}
|
|
467
1591
|
|
|
468
1592
|
case "BlockStatement": {
|
|
469
|
-
const blockEnv = env.
|
|
1593
|
+
const blockEnv = env.fork();
|
|
470
1594
|
const result = evaluateStatements(node.body, blockEnv);
|
|
471
1595
|
return result;
|
|
472
1596
|
}
|
|
473
1597
|
|
|
474
1598
|
case "ReturnStatement": {
|
|
1599
|
+
if (_activeAsOverride) return makeReturn(_activeAsOverride);
|
|
475
1600
|
const arg = node.argument;
|
|
476
1601
|
if (!arg) return makeReturn(T.undefined);
|
|
477
1602
|
const val = evaluate(arg, env);
|
|
@@ -481,8 +1606,13 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
|
|
|
481
1606
|
|
|
482
1607
|
case "VariableDeclaration": {
|
|
483
1608
|
for (const decl of node.declarations) {
|
|
484
|
-
const init = decl.init ? evaluate(decl.init, env) : T.undefined;
|
|
1609
|
+
const init = _activeAsOverride ?? (decl.init ? evaluate(decl.init, env) : T.undefined);
|
|
485
1610
|
if (isReturn(init) || isBranch(init) || isThrow(init)) return init;
|
|
1611
|
+
// Name anonymous function values after their binding so recursion
|
|
1612
|
+
// truncation records can cite e.g. `recursion:fn`.
|
|
1613
|
+
if (init.kind === "function" && decl.id.type === "Identifier" && !(init as any)._name) {
|
|
1614
|
+
(init as any)._name = decl.id.name;
|
|
1615
|
+
}
|
|
486
1616
|
bindPattern(decl.id, init, env);
|
|
487
1617
|
if (decl.id.type === "Identifier") {
|
|
488
1618
|
recordNodeType(decl.id, init);
|
|
@@ -493,16 +1623,40 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
|
|
|
493
1623
|
|
|
494
1624
|
case "AssignmentExpression": {
|
|
495
1625
|
if (node.left.type === "Identifier") {
|
|
496
|
-
const
|
|
497
|
-
if (isReturn(
|
|
1626
|
+
const rightVal = evaluate(node.right, env);
|
|
1627
|
+
if (isReturn(rightVal) || isBranch(rightVal) || isThrow(rightVal)) return rightVal;
|
|
1628
|
+
|
|
1629
|
+
// Handle compound assignment operators
|
|
1630
|
+
let val = rightVal;
|
|
1631
|
+
if (node.operator !== "=") {
|
|
1632
|
+
const leftVal = env.lookup(node.left.name);
|
|
1633
|
+
if (leftVal && leftVal.kind !== "unknown") {
|
|
1634
|
+
// Extract the binary operator (e.g., "+=" -> "+")
|
|
1635
|
+
const binaryOp = node.operator.slice(0, -1);
|
|
1636
|
+
val = evalBinaryValue(binaryOp, leftVal, rightVal) ?? T.unknown;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
498
1640
|
if (!env.update(node.left.name, val)) {
|
|
499
1641
|
env.bind(node.left.name, val);
|
|
500
1642
|
}
|
|
1643
|
+
if (val.kind === "function" && !(val as any)._name) {
|
|
1644
|
+
(val as any)._name = node.left.name;
|
|
1645
|
+
}
|
|
501
1646
|
return val;
|
|
502
1647
|
}
|
|
503
1648
|
if (node.left.type === "MemberExpression") {
|
|
504
1649
|
const val = evaluate(node.right, env);
|
|
505
1650
|
if (isReturn(val) || isBranch(val) || isThrow(val)) return val;
|
|
1651
|
+
// `obj.method = function () {...}` — name the function after the
|
|
1652
|
+
// assigned property (e.g. `recursion:internals.clone`).
|
|
1653
|
+
if (val.kind === "function" && !(val as any)._name) {
|
|
1654
|
+
const memberKey = getMemberKey(node.left, env);
|
|
1655
|
+
if (memberKey !== null) {
|
|
1656
|
+
const objKey = node.left.object.type === "Identifier" ? `${node.left.object.name}.` : "";
|
|
1657
|
+
(val as any)._name = `${objKey}${memberKey}`;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
506
1660
|
const objVal = evaluate(node.left.object, env);
|
|
507
1661
|
if (isReturn(objVal) || isBranch(objVal) || isThrow(objVal)) return val;
|
|
508
1662
|
if (objVal.kind === "object") {
|
|
@@ -570,7 +1724,10 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
|
|
|
570
1724
|
const paramNames = node.params.map(describeParam);
|
|
571
1725
|
const fnType = T.fn(paramNames, node.body, env);
|
|
572
1726
|
(fnType as any)._paramPatterns = node.params;
|
|
1727
|
+
if (node.id) (fnType as any)._name = node.id.name;
|
|
573
1728
|
if (node.async) (fnType as any)._async = true;
|
|
1729
|
+
(fnType as any)._hasArguments = true;
|
|
1730
|
+
tagFnModule(fnType);
|
|
574
1731
|
env.bind(node.id.name, fnType);
|
|
575
1732
|
return T.undefined;
|
|
576
1733
|
}
|
|
@@ -581,7 +1738,10 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
|
|
|
581
1738
|
const body = node.body;
|
|
582
1739
|
const fnType = T.fn(paramNames, body, env);
|
|
583
1740
|
(fnType as any)._paramPatterns = node.params;
|
|
1741
|
+
if ((node as any).id) (fnType as any)._name = (node as any).id.name;
|
|
584
1742
|
if (node.async) (fnType as any)._async = true;
|
|
1743
|
+
(fnType as any)._hasArguments = true;
|
|
1744
|
+
tagFnModule(fnType);
|
|
585
1745
|
return fnType;
|
|
586
1746
|
}
|
|
587
1747
|
|
|
@@ -656,14 +1816,92 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
|
|
|
656
1816
|
if (methodResult !== null) return methodResult;
|
|
657
1817
|
}
|
|
658
1818
|
|
|
1819
|
+
// CommonJS require(specifier) — resolved through the same module
|
|
1820
|
+
// resolver/cache as ESM import. The identity check keeps user-defined
|
|
1821
|
+
// `require` bindings on the normal call path.
|
|
1822
|
+
if (
|
|
1823
|
+
callee.type === "Identifier" &&
|
|
1824
|
+
callee.name === "require" &&
|
|
1825
|
+
env.lookup("require") === _cjsRequireValue
|
|
1826
|
+
) {
|
|
1827
|
+
return evaluateRequireCall(node, env);
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// Handle built-in global functions (only if not overridden in environment, e.g., by mocks)
|
|
1831
|
+
if (callee.type === "Identifier" && !env.has(callee.name)) {
|
|
1832
|
+
const builtinResult = evaluateBuiltinCall(callee.name, node.arguments as Node[], env);
|
|
1833
|
+
if (builtinResult !== null) return builtinResult;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
const calleeVal = evaluate(callee, env);
|
|
1837
|
+
if (isReturn(calleeVal) || isBranch(calleeVal) || isThrow(calleeVal)) return calleeVal;
|
|
1838
|
+
|
|
1839
|
+
const argVals = evaluateArgs(node.arguments as Node[], env);
|
|
1840
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
1841
|
+
|
|
1842
|
+
const thisVal = (callee.type === "MemberExpression" || callee.type === "OptionalMemberExpression")
|
|
1843
|
+
? ((callee as any)._receiverVal as TypeValue | undefined)
|
|
1844
|
+
: undefined;
|
|
1845
|
+
if (calleeVal.kind === "function") {
|
|
1846
|
+
const full = callFunctionFull(calleeVal, argVals as TypeValue[], thisVal);
|
|
1847
|
+
if (_callCollector && callee.type === "Identifier") {
|
|
1848
|
+
recordCall(callee.name, argVals as TypeValue[], full, node.loc, calleeVal);
|
|
1849
|
+
} else if (_callCollector && (callee.type === "MemberExpression" || callee.type === "OptionalMemberExpression") && callee.property.type === "Identifier") {
|
|
1850
|
+
// 成员调用(ns.fn()/obj.fn()):跨文件调用点发现的来源——
|
|
1851
|
+
// 配合 _exportTags 还原 (module, export) 归属
|
|
1852
|
+
recordCall(callee.property.name, argVals as TypeValue[], full, node.loc, calleeVal);
|
|
1853
|
+
}
|
|
1854
|
+
if (full.value.kind === "never" && full.throws.kind !== "never") {
|
|
1855
|
+
const callLoc = node.loc ? {
|
|
1856
|
+
start: { line: node.loc.start.line, column: node.loc.start.column },
|
|
1857
|
+
end: { line: node.loc.end.line, column: node.loc.end.column },
|
|
1858
|
+
} : full.throwLoc;
|
|
1859
|
+
return makeThrow(full.throws, callLoc);
|
|
1860
|
+
}
|
|
1861
|
+
return full.value;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
return distributeOverUnion(calleeVal, (fn) => {
|
|
1865
|
+
if (fn.kind !== "function") return T.unknown;
|
|
1866
|
+
return callFunction(fn, argVals as TypeValue[], thisVal);
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
case "OptionalCallExpression": {
|
|
1871
|
+
const callee = node.callee as Node;
|
|
1872
|
+
|
|
1873
|
+
if (callee.type === "OptionalMemberExpression" || callee.type === "MemberExpression") {
|
|
1874
|
+
const objVal = evaluate(callee.object, env);
|
|
1875
|
+
if (isReturn(objVal) || isBranch(objVal) || isThrow(objVal)) return objVal;
|
|
1876
|
+
if (objVal.kind === "literal" && (objVal.value === null || objVal.value === undefined)) {
|
|
1877
|
+
return T.undefined;
|
|
1878
|
+
}
|
|
1879
|
+
const methodResult = evaluateMethodCall(callee as Node & { type: "MemberExpression" }, node.arguments as Node[], env);
|
|
1880
|
+
if (methodResult !== null) return methodResult;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
659
1883
|
const calleeVal = evaluate(callee, env);
|
|
660
1884
|
if (isReturn(calleeVal) || isBranch(calleeVal) || isThrow(calleeVal)) return calleeVal;
|
|
661
1885
|
|
|
1886
|
+
if (calleeVal.kind === "literal" && (calleeVal.value === null || calleeVal.value === undefined)) {
|
|
1887
|
+
return T.undefined;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
662
1890
|
const argVals = evaluateArgs(node.arguments as Node[], env);
|
|
663
1891
|
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
664
1892
|
|
|
1893
|
+
const thisVal = (callee.type === "MemberExpression" || callee.type === "OptionalMemberExpression")
|
|
1894
|
+
? ((callee as any)._receiverVal as TypeValue | undefined)
|
|
1895
|
+
: undefined;
|
|
665
1896
|
if (calleeVal.kind === "function") {
|
|
666
|
-
const full = callFunctionFull(calleeVal, argVals as TypeValue[]);
|
|
1897
|
+
const full = callFunctionFull(calleeVal, argVals as TypeValue[], thisVal);
|
|
1898
|
+
if (_callCollector && callee.type === "Identifier") {
|
|
1899
|
+
recordCall(callee.name, argVals as TypeValue[], full, node.loc, calleeVal);
|
|
1900
|
+
} else if (_callCollector && (callee.type === "MemberExpression" || callee.type === "OptionalMemberExpression") && callee.property.type === "Identifier") {
|
|
1901
|
+
// 成员调用(ns.fn()/obj.fn()):跨文件调用点发现的来源——
|
|
1902
|
+
// 配合 _exportTags 还原 (module, export) 归属
|
|
1903
|
+
recordCall(callee.property.name, argVals as TypeValue[], full, node.loc, calleeVal);
|
|
1904
|
+
}
|
|
667
1905
|
if (full.value.kind === "never" && full.throws.kind !== "never") {
|
|
668
1906
|
const callLoc = node.loc ? {
|
|
669
1907
|
start: { line: node.loc.start.line, column: node.loc.start.column },
|
|
@@ -676,49 +1914,327 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
|
|
|
676
1914
|
|
|
677
1915
|
return distributeOverUnion(calleeVal, (fn) => {
|
|
678
1916
|
if (fn.kind !== "function") return T.unknown;
|
|
679
|
-
return callFunction(fn, argVals as TypeValue[]);
|
|
1917
|
+
return callFunction(fn, argVals as TypeValue[], thisVal);
|
|
680
1918
|
});
|
|
681
1919
|
}
|
|
682
1920
|
|
|
683
1921
|
case "MemberExpression": {
|
|
684
1922
|
const objVal = evaluate(node.object, env);
|
|
685
1923
|
if (isReturn(objVal) || isBranch(objVal) || isThrow(objVal)) return objVal;
|
|
1924
|
+
// Stash the receiver so an enclosing CallExpression (`obj.f()`) can
|
|
1925
|
+
// bind it as the callee's `this` without re-evaluating the object.
|
|
1926
|
+
(node as any)._receiverVal = objVal;
|
|
686
1927
|
|
|
687
1928
|
if (node.computed) {
|
|
688
1929
|
const propVal = evaluate(node.property, env);
|
|
689
1930
|
if (isReturn(propVal) || isBranch(propVal) || isThrow(propVal)) return propVal;
|
|
690
|
-
return
|
|
691
|
-
|
|
692
|
-
|
|
1931
|
+
return evaluateComputedMemberAccess(objVal, propVal, node);
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
if (node.property.type === "Identifier") {
|
|
1935
|
+
const propName = node.property.name;
|
|
1936
|
+
let propMissed = false;
|
|
1937
|
+
const result = distributeOverUnion(objVal, (obj) => {
|
|
1938
|
+
// `x.valueOf` off non-object receivers (literals, primitives,
|
|
1939
|
+
// arrays, functions) — see valueOfBinding.
|
|
1940
|
+
if (propName === "valueOf") {
|
|
1941
|
+
const bound = valueOfBinding(obj);
|
|
1942
|
+
if (bound !== undefined) return bound;
|
|
1943
|
+
}
|
|
1944
|
+
// Check for built-in static methods (e.g., Date.now, Math.floor)
|
|
1945
|
+
const builtinName = (obj as any)._builtinName as string | undefined;
|
|
1946
|
+
if (builtinName && hasOwnProp(BUILTIN_STATIC_METHODS, builtinName)) {
|
|
1947
|
+
const builtin = BUILTIN_STATIC_METHODS[builtinName];
|
|
1948
|
+
// hasOwnProp guard: `in` would leak native Object.prototype
|
|
1949
|
+
// members (e.g. `JSON.toString` → the real JS function).
|
|
1950
|
+
if (typeof builtin === "object" && hasOwnProp(builtin, propName)) {
|
|
1951
|
+
// Well-known symbols get named copies so `Symbol.iterator
|
|
1952
|
+
// in x` can decide iterability.
|
|
1953
|
+
if (builtinName === "Symbol" && hasOwnProp(SYMBOL_STATIC_PROPS, propName)) {
|
|
1954
|
+
return wellKnownSymbol(propName);
|
|
1955
|
+
}
|
|
1956
|
+
return (builtin as Record<string, TypeValue>)[propName];
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
// `X.prototype` on a constructible built-in resolves to the cached
|
|
1960
|
+
// singleton instance of X (stable identity across evaluations)
|
|
1961
|
+
if (propName === "prototype" && builtinName && BUILTIN_PROTOTYPE_CLASSES.has(builtinName)) {
|
|
1962
|
+
return builtinPrototype(builtinName);
|
|
693
1963
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
return
|
|
1964
|
+
// Check for Map.size property
|
|
1965
|
+
if (obj.kind === "instance" && obj.className === "Map" && propName === "size") {
|
|
1966
|
+
return T.number;
|
|
1967
|
+
}
|
|
1968
|
+
// Check for Set.size property
|
|
1969
|
+
if (obj.kind === "instance" && obj.className === "Set" && propName === "size") {
|
|
1970
|
+
return T.number;
|
|
1971
|
+
}
|
|
1972
|
+
if (obj.kind === "object") {
|
|
1973
|
+
const ownVal = hasOwnProp(obj.properties, propName) ? obj.properties[propName] : undefined;
|
|
1974
|
+
if (ownVal) return ownVal;
|
|
1975
|
+
// Every object inherits Object.prototype members (see table note).
|
|
1976
|
+
const protoFn = hasOwnProp(OBJECT_PROTOTYPE_METHODS, propName)
|
|
1977
|
+
? OBJECT_PROTOTYPE_METHODS[propName]
|
|
1978
|
+
: undefined;
|
|
1979
|
+
if (protoFn) return bindObjectProtoMethod(protoFn, obj);
|
|
1980
|
+
return T.undefined;
|
|
1981
|
+
}
|
|
1982
|
+
if (obj.kind === "instance") {
|
|
1983
|
+
// Own-property lookup only: a plain `{}` properties record would
|
|
1984
|
+
// otherwise leak native Object.prototype members (e.g. the real
|
|
1985
|
+
// JS `toString` function) into the type system.
|
|
1986
|
+
const own = hasOwnProp(obj.properties, propName) ? obj.properties[propName] : undefined;
|
|
1987
|
+
if (own) return own;
|
|
1988
|
+
// Instances inherit Object.prototype members too: keeps
|
|
1989
|
+
// `Object.prototype.hasOwnProperty` (and the toString brand-check
|
|
1990
|
+
// idiom) typeable instead of degrading to undefined.
|
|
1991
|
+
const protoFn = hasOwnProp(OBJECT_PROTOTYPE_METHODS, propName)
|
|
1992
|
+
? OBJECT_PROTOTYPE_METHODS[propName]
|
|
1993
|
+
: undefined;
|
|
1994
|
+
if (protoFn) return bindObjectProtoMethod(protoFn, obj);
|
|
1995
|
+
// `x.constructor` resolves to the class namespace: instances
|
|
1996
|
+
// and prototype singletons share the memoized builtinClassValue
|
|
1997
|
+
// (Array.prototype.constructor === Array), so reflection like
|
|
1998
|
+
// `new (Object.getPrototypeOf(x).constructor)(n)` keeps going.
|
|
1999
|
+
if (propName === "constructor") return builtinClassValue(obj.className);
|
|
2000
|
+
// instance 的方法集是声明的近似:未列出的方法返回 unknown-result
|
|
2001
|
+
// 函数值,让 `X.prototype.m.call(...)` 反射惯用法继续求值。
|
|
2002
|
+
const knownInstanceMethods: Record<string, TypeValue> = {
|
|
2003
|
+
values: T.fnSig([], T.array(T.unknown)),
|
|
2004
|
+
keys: T.fnSig([], T.array(T.unknown)),
|
|
2005
|
+
entries: T.fnSig([], T.array(T.tuple([T.unknown, T.unknown]))),
|
|
2006
|
+
get: T.fnSig([T.unknown], T.unknown),
|
|
2007
|
+
has: T.fnSig([T.unknown], T.boolean),
|
|
2008
|
+
add: T.fnSig([T.unknown], T.unknown),
|
|
2009
|
+
set: T.fnSig([T.unknown, T.unknown], T.unknown),
|
|
2010
|
+
delete: T.fnSig([T.unknown], T.boolean),
|
|
2011
|
+
forEach: T.fnSig([T.unknown], T.undefined),
|
|
2012
|
+
call: T.fnSig([T.unknown], T.unknown),
|
|
2013
|
+
apply: T.fnSig([T.unknown], T.unknown),
|
|
2014
|
+
bind: T.fnSig([T.unknown], T.unknown),
|
|
2015
|
+
};
|
|
2016
|
+
// hasOwnProp:普通 [] 访问会命中 JS 原型链(valueOf/hasOwnProperty
|
|
2017
|
+
// 等),把原生函数当 TypeValue 泄漏进类型系统
|
|
2018
|
+
const knownFn = hasOwnProp(knownInstanceMethods, propName)
|
|
2019
|
+
? knownInstanceMethods[propName]
|
|
2020
|
+
: undefined;
|
|
2021
|
+
if (knownFn) return knownFn;
|
|
2022
|
+
return T.fnSig([T.unknown], T.unknown);
|
|
2023
|
+
}
|
|
2024
|
+
if (propName === "length" && (obj.kind === "array" || obj.kind === "tuple")) {
|
|
2025
|
+
return obj.kind === "tuple" ? T.literal(obj.elements.length) : T.number;
|
|
2026
|
+
}
|
|
2027
|
+
if (obj.kind === "array" || obj.kind === "tuple") {
|
|
2028
|
+
// Feature-detection probes (`if (value.toJSON) …`) read arrays
|
|
2029
|
+
// too: a missed named property is undefined (like objects),
|
|
2030
|
+
// never unknown — unknown here poisons guarded unions.
|
|
2031
|
+
return T.undefined;
|
|
697
2032
|
}
|
|
2033
|
+
if (propName === "length" && obj.kind === "literal" && typeof obj.value === "string") {
|
|
2034
|
+
return T.literal(obj.value.length);
|
|
2035
|
+
}
|
|
2036
|
+
if (propName === "length" && obj.kind === "primitive" && obj.type === "string") {
|
|
2037
|
+
return T.number;
|
|
2038
|
+
}
|
|
2039
|
+
// Promise receivers carry Object.prototype members (valueOf
|
|
2040
|
+
// returns the receiver itself) plus then/catch/finally — the
|
|
2041
|
+
// same approximation the instance branch uses. Without this, a
|
|
2042
|
+
// Promise-typed union member poisons `x.valueOf` to unknown.
|
|
2043
|
+
if (obj.kind === "promise") {
|
|
2044
|
+
const protoFn = hasOwnProp(OBJECT_PROTOTYPE_METHODS, propName)
|
|
2045
|
+
? OBJECT_PROTOTYPE_METHODS[propName]
|
|
2046
|
+
: undefined;
|
|
2047
|
+
if (protoFn) return bindObjectProtoMethod(protoFn, obj);
|
|
2048
|
+
const promiseMethods = BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS.Promise;
|
|
2049
|
+
if (promiseMethods && hasOwnProp(promiseMethods, propName)) {
|
|
2050
|
+
return promiseMethods[propName];
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
// Primitive receivers auto-box through their wrapper prototype
|
|
2054
|
+
// ('x'.constructor === String): consult it before reporting a miss.
|
|
2055
|
+
// A wrapper miss on a wrapper-able primitive (string/number/
|
|
2056
|
+
// boolean/symbol) is definite absence — undefined, not unknown —
|
|
2057
|
+
// so `if (value.toJSON)` feature probes resolve their false
|
|
2058
|
+
// branch (mirrors the array treatment above).
|
|
2059
|
+
if (
|
|
2060
|
+
obj.kind === "primitive" ||
|
|
2061
|
+
(obj.kind === "literal" &&
|
|
2062
|
+
(typeof obj.value === "string" || typeof obj.value === "number" || typeof obj.value === "boolean"))
|
|
2063
|
+
) {
|
|
2064
|
+
const wrapper = wrapperPrototypeMember(obj, propName);
|
|
2065
|
+
return wrapper !== undefined ? wrapper : T.undefined;
|
|
2066
|
+
}
|
|
2067
|
+
const wrapper = wrapperPrototypeMember(obj, propName);
|
|
2068
|
+
if (wrapper !== undefined) return wrapper;
|
|
2069
|
+
if (obj.kind === "refined") {
|
|
2070
|
+
const result = dispatchProperty(obj, propName);
|
|
2071
|
+
if (result !== undefined) return result;
|
|
2072
|
+
}
|
|
2073
|
+
propMissed = true;
|
|
698
2074
|
return T.unknown;
|
|
699
2075
|
});
|
|
2076
|
+
if (propMissed) {
|
|
2077
|
+
recordUnknown({
|
|
2078
|
+
kind: "property",
|
|
2079
|
+
name: propName,
|
|
2080
|
+
receiverType: objVal,
|
|
2081
|
+
loc: node.loc,
|
|
2082
|
+
reason: `no property '${propName}' on ${objVal.kind}`,
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
return result;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
return T.unknown;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
case "OptionalMemberExpression": {
|
|
2092
|
+
const objVal = evaluate(node.object, env);
|
|
2093
|
+
if (isReturn(objVal) || isBranch(objVal) || isThrow(objVal)) return objVal;
|
|
2094
|
+
|
|
2095
|
+
// If object is null or undefined, short-circuit to undefined
|
|
2096
|
+
if (objVal.kind === "literal" && (objVal.value === null || objVal.value === undefined)) {
|
|
2097
|
+
return T.undefined;
|
|
2098
|
+
}
|
|
2099
|
+
(node as any)._receiverVal = objVal;
|
|
2100
|
+
|
|
2101
|
+
// Otherwise, evaluate like a normal member expression
|
|
2102
|
+
if (node.computed) {
|
|
2103
|
+
const propVal = evaluate(node.property, env);
|
|
2104
|
+
if (isReturn(propVal) || isBranch(propVal) || isThrow(propVal)) return propVal;
|
|
2105
|
+
return evaluateComputedMemberAccess(objVal, propVal, node);
|
|
700
2106
|
}
|
|
701
2107
|
|
|
702
2108
|
if (node.property.type === "Identifier") {
|
|
703
2109
|
const propName = node.property.name;
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
2110
|
+
let propMissed = false;
|
|
2111
|
+
const result = distributeOverUnion(objVal, (obj) => {
|
|
2112
|
+
// `x.valueOf` off non-object receivers (literals, primitives,
|
|
2113
|
+
// arrays, functions) — see valueOfBinding.
|
|
2114
|
+
if (propName === "valueOf") {
|
|
2115
|
+
const bound = valueOfBinding(obj);
|
|
2116
|
+
if (bound !== undefined) return bound;
|
|
2117
|
+
}
|
|
2118
|
+
// `X.prototype` on a constructible built-in resolves to the cached
|
|
2119
|
+
// singleton instance of X (stable identity across evaluations)
|
|
2120
|
+
const optBuiltinName = (obj as any)._builtinName as string | undefined;
|
|
2121
|
+
if (propName === "prototype" && optBuiltinName && BUILTIN_PROTOTYPE_CLASSES.has(optBuiltinName)) {
|
|
2122
|
+
return builtinPrototype(optBuiltinName);
|
|
2123
|
+
}
|
|
2124
|
+
if (obj.kind === "object") {
|
|
2125
|
+
const ownVal = hasOwnProp(obj.properties, propName) ? obj.properties[propName] : undefined;
|
|
2126
|
+
if (ownVal) return ownVal;
|
|
2127
|
+
// Every object inherits Object.prototype members (see table note).
|
|
2128
|
+
const protoFn = hasOwnProp(OBJECT_PROTOTYPE_METHODS, propName)
|
|
2129
|
+
? OBJECT_PROTOTYPE_METHODS[propName]
|
|
2130
|
+
: undefined;
|
|
2131
|
+
if (protoFn) return bindObjectProtoMethod(protoFn, obj);
|
|
2132
|
+
return T.undefined;
|
|
2133
|
+
}
|
|
2134
|
+
if (obj.kind === "instance") {
|
|
2135
|
+
// Own-property lookup only (see MemberExpression note): a plain
|
|
2136
|
+
// `{}` properties record would otherwise leak native
|
|
2137
|
+
// Object.prototype members into the type system.
|
|
2138
|
+
const own = hasOwnProp(obj.properties, propName) ? obj.properties[propName] : undefined;
|
|
2139
|
+
if (own) return own;
|
|
2140
|
+
// Instances inherit Object.prototype members too: keeps
|
|
2141
|
+
// `Object.prototype.hasOwnProperty` (and the toString brand-check
|
|
2142
|
+
// idiom) typeable instead of degrading to undefined.
|
|
2143
|
+
const protoFn = hasOwnProp(OBJECT_PROTOTYPE_METHODS, propName)
|
|
2144
|
+
? OBJECT_PROTOTYPE_METHODS[propName]
|
|
2145
|
+
: undefined;
|
|
2146
|
+
if (protoFn) return bindObjectProtoMethod(protoFn, obj);
|
|
2147
|
+
// `x.constructor` resolves to the class namespace: instances
|
|
2148
|
+
// and prototype singletons share the memoized builtinClassValue
|
|
2149
|
+
// (Array.prototype.constructor === Array), so reflection like
|
|
2150
|
+
// `new (Object.getPrototypeOf(x).constructor)(n)` keeps going.
|
|
2151
|
+
if (propName === "constructor") return builtinClassValue(obj.className);
|
|
2152
|
+
// instance 的方法集是声明的近似:未列出的方法返回 unknown-result
|
|
2153
|
+
// 函数值,让 `X.prototype.m.call(...)` 反射惯用法继续求值。
|
|
2154
|
+
const knownInstanceMethods: Record<string, TypeValue> = {
|
|
2155
|
+
values: T.fnSig([], T.array(T.unknown)),
|
|
2156
|
+
keys: T.fnSig([], T.array(T.unknown)),
|
|
2157
|
+
entries: T.fnSig([], T.array(T.tuple([T.unknown, T.unknown]))),
|
|
2158
|
+
get: T.fnSig([T.unknown], T.unknown),
|
|
2159
|
+
has: T.fnSig([T.unknown], T.boolean),
|
|
2160
|
+
add: T.fnSig([T.unknown], T.unknown),
|
|
2161
|
+
set: T.fnSig([T.unknown, T.unknown], T.unknown),
|
|
2162
|
+
delete: T.fnSig([T.unknown], T.boolean),
|
|
2163
|
+
forEach: T.fnSig([T.unknown], T.undefined),
|
|
2164
|
+
call: T.fnSig([T.unknown], T.unknown),
|
|
2165
|
+
apply: T.fnSig([T.unknown], T.unknown),
|
|
2166
|
+
bind: T.fnSig([T.unknown], T.unknown),
|
|
2167
|
+
};
|
|
2168
|
+
// hasOwnProp:普通 [] 访问会命中 JS 原型链(valueOf/hasOwnProperty
|
|
2169
|
+
// 等),把原生函数当 TypeValue 泄漏进类型系统
|
|
2170
|
+
const knownFn = hasOwnProp(knownInstanceMethods, propName)
|
|
2171
|
+
? knownInstanceMethods[propName]
|
|
2172
|
+
: undefined;
|
|
2173
|
+
if (knownFn) return knownFn;
|
|
2174
|
+
return T.fnSig([T.unknown], T.unknown);
|
|
2175
|
+
}
|
|
707
2176
|
if (propName === "length" && (obj.kind === "array" || obj.kind === "tuple")) {
|
|
708
2177
|
return obj.kind === "tuple" ? T.literal(obj.elements.length) : T.number;
|
|
709
2178
|
}
|
|
2179
|
+
if (obj.kind === "array" || obj.kind === "tuple") {
|
|
2180
|
+
// Feature-detection probes (`if (value.toJSON) …`) read arrays
|
|
2181
|
+
// too: a missed named property is undefined (like objects),
|
|
2182
|
+
// never unknown — unknown here poisons guarded unions.
|
|
2183
|
+
return T.undefined;
|
|
2184
|
+
}
|
|
710
2185
|
if (propName === "length" && obj.kind === "literal" && typeof obj.value === "string") {
|
|
711
2186
|
return T.literal(obj.value.length);
|
|
712
2187
|
}
|
|
713
2188
|
if (propName === "length" && obj.kind === "primitive" && obj.type === "string") {
|
|
714
2189
|
return T.number;
|
|
715
2190
|
}
|
|
2191
|
+
// Promise receivers carry Object.prototype members (valueOf
|
|
2192
|
+
// returns the receiver itself) plus then/catch/finally — the
|
|
2193
|
+
// same approximation the instance branch uses. Without this, a
|
|
2194
|
+
// Promise-typed union member poisons `x.valueOf` to unknown.
|
|
2195
|
+
if (obj.kind === "promise") {
|
|
2196
|
+
const protoFn = hasOwnProp(OBJECT_PROTOTYPE_METHODS, propName)
|
|
2197
|
+
? OBJECT_PROTOTYPE_METHODS[propName]
|
|
2198
|
+
: undefined;
|
|
2199
|
+
if (protoFn) return bindObjectProtoMethod(protoFn, obj);
|
|
2200
|
+
const promiseMethods = BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS.Promise;
|
|
2201
|
+
if (promiseMethods && hasOwnProp(promiseMethods, propName)) {
|
|
2202
|
+
return promiseMethods[propName];
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
// Primitive receivers auto-box through their wrapper prototype
|
|
2206
|
+
// ('x'.constructor === String): consult it before reporting a miss.
|
|
2207
|
+
// A wrapper miss on a wrapper-able primitive (string/number/
|
|
2208
|
+
// boolean/symbol) is definite absence — undefined, not unknown —
|
|
2209
|
+
// so `if (value.toJSON)` feature probes resolve their false
|
|
2210
|
+
// branch (mirrors the array treatment above).
|
|
2211
|
+
if (
|
|
2212
|
+
obj.kind === "primitive" ||
|
|
2213
|
+
(obj.kind === "literal" &&
|
|
2214
|
+
(typeof obj.value === "string" || typeof obj.value === "number" || typeof obj.value === "boolean"))
|
|
2215
|
+
) {
|
|
2216
|
+
const wrapper = wrapperPrototypeMember(obj, propName);
|
|
2217
|
+
return wrapper !== undefined ? wrapper : T.undefined;
|
|
2218
|
+
}
|
|
2219
|
+
const wrapper = wrapperPrototypeMember(obj, propName);
|
|
2220
|
+
if (wrapper !== undefined) return wrapper;
|
|
716
2221
|
if (obj.kind === "refined") {
|
|
717
2222
|
const result = dispatchProperty(obj, propName);
|
|
718
2223
|
if (result !== undefined) return result;
|
|
719
2224
|
}
|
|
2225
|
+
propMissed = true;
|
|
720
2226
|
return T.unknown;
|
|
721
2227
|
});
|
|
2228
|
+
if (propMissed) {
|
|
2229
|
+
recordUnknown({
|
|
2230
|
+
kind: "property",
|
|
2231
|
+
name: propName,
|
|
2232
|
+
receiverType: objVal,
|
|
2233
|
+
loc: node.loc,
|
|
2234
|
+
reason: `no property '${propName}' on ${objVal.kind}`,
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
return result;
|
|
722
2238
|
}
|
|
723
2239
|
|
|
724
2240
|
return T.unknown;
|
|
@@ -745,11 +2261,53 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
|
|
|
745
2261
|
if (isReturn(val) || isBranch(val) || isThrow(val)) return val;
|
|
746
2262
|
props[key] = val;
|
|
747
2263
|
}
|
|
2264
|
+
} else if (prop.type === "ObjectMethod") {
|
|
2265
|
+
// Handle shorthand method syntax: { method() { ... } }
|
|
2266
|
+
const key = prop.key.type === "Identifier"
|
|
2267
|
+
? prop.key.name
|
|
2268
|
+
: prop.key.type === "StringLiteral"
|
|
2269
|
+
? prop.key.value
|
|
2270
|
+
: null;
|
|
2271
|
+
if (key) {
|
|
2272
|
+
const params = prop.params.map((p: Node) => {
|
|
2273
|
+
if (p.type === "Identifier") return p.name;
|
|
2274
|
+
if (p.type === "RestElement" && p.argument.type === "Identifier") return `...${p.argument.name}`;
|
|
2275
|
+
return `__param`;
|
|
2276
|
+
});
|
|
2277
|
+
props[key] = T.fn(params, prop.body, env);
|
|
2278
|
+
}
|
|
748
2279
|
} else if (prop.type === "SpreadElement") {
|
|
749
2280
|
const spreadVal = evaluate(prop.argument, env);
|
|
750
2281
|
if (isReturn(spreadVal) || isBranch(spreadVal) || isThrow(spreadVal)) return spreadVal;
|
|
2282
|
+
// M4:代数 spread(保留字面量/term);失败回退 Object.assign
|
|
2283
|
+
const absSpread = tryAbsObjectSpread(T.object(props), spreadVal);
|
|
2284
|
+
if (absSpread !== undefined && absSpread.kind === "object") {
|
|
2285
|
+
// 合并到 props 继续处理后续属性
|
|
2286
|
+
for (const [k, v] of Object.entries(absSpread.properties)) {
|
|
2287
|
+
props[k] = v;
|
|
2288
|
+
}
|
|
2289
|
+
continue;
|
|
2290
|
+
}
|
|
751
2291
|
if (spreadVal.kind === "object") {
|
|
752
2292
|
Object.assign(props, spreadVal.properties);
|
|
2293
|
+
} else if (spreadVal.kind === "union") {
|
|
2294
|
+
const objectMembers = spreadVal.members.filter((m: TypeValue) => m.kind === "object");
|
|
2295
|
+
if (objectMembers.length > 0) {
|
|
2296
|
+
const allKeys = new Set<string>();
|
|
2297
|
+
for (const m of objectMembers) {
|
|
2298
|
+
if (m.kind === "object") {
|
|
2299
|
+
for (const k of Object.keys(m.properties)) allKeys.add(k);
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
for (const key of allKeys) {
|
|
2303
|
+
const values = objectMembers
|
|
2304
|
+
.filter((m: TypeValue) => m.kind === "object" && key in (m as any).properties)
|
|
2305
|
+
.map((m: TypeValue) => (m as any).properties[key]);
|
|
2306
|
+
if (values.length > 0) {
|
|
2307
|
+
props[key] = simplifyUnion(values);
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
753
2311
|
}
|
|
754
2312
|
}
|
|
755
2313
|
}
|
|
@@ -763,194 +2321,736 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
|
|
|
763
2321
|
elements.push(T.undefined);
|
|
764
2322
|
continue;
|
|
765
2323
|
}
|
|
766
|
-
if (elem.type === "SpreadElement") {
|
|
767
|
-
const spreadVal = evaluate(elem.argument, env);
|
|
768
|
-
if (isReturn(spreadVal) || isBranch(spreadVal) || isThrow(spreadVal)) return spreadVal;
|
|
769
|
-
if (spreadVal.kind === "tuple") {
|
|
770
|
-
elements.push(...spreadVal.elements);
|
|
771
|
-
} else if (spreadVal.kind === "array") {
|
|
772
|
-
return T.array(simplifyUnion([...elements, spreadVal.element]));
|
|
773
|
-
} else {
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
2324
|
+
if (elem.type === "SpreadElement") {
|
|
2325
|
+
const spreadVal = evaluate(elem.argument, env);
|
|
2326
|
+
if (isReturn(spreadVal) || isBranch(spreadVal) || isThrow(spreadVal)) return spreadVal;
|
|
2327
|
+
if (spreadVal.kind === "tuple") {
|
|
2328
|
+
elements.push(...spreadVal.elements);
|
|
2329
|
+
} else if (spreadVal.kind === "array") {
|
|
2330
|
+
return T.array(simplifyUnion([...elements, spreadVal.element]));
|
|
2331
|
+
} else {
|
|
2332
|
+
// Set/Map instances spread through their exact side tables
|
|
2333
|
+
// (`[...new Set([1,2])]` → [1, 2]) or their element typeArgs.
|
|
2334
|
+
const iterable = iterableSpreadValue(spreadVal);
|
|
2335
|
+
if (iterable) {
|
|
2336
|
+
if (iterable.kind === "tuple") {
|
|
2337
|
+
elements.push(...iterable.elements);
|
|
2338
|
+
} else if (iterable.kind === "array") {
|
|
2339
|
+
return T.array(simplifyUnion([...elements, iterable.element]));
|
|
2340
|
+
} else {
|
|
2341
|
+
elements.push(T.unknown);
|
|
2342
|
+
}
|
|
2343
|
+
} else {
|
|
2344
|
+
elements.push(T.unknown);
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
continue;
|
|
2348
|
+
}
|
|
2349
|
+
const val = evaluate(elem as Node, env);
|
|
2350
|
+
if (isReturn(val) || isBranch(val) || isThrow(val)) return val;
|
|
2351
|
+
elements.push(val);
|
|
2352
|
+
}
|
|
2353
|
+
return T.tuple(elements);
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
case "ThrowStatement": {
|
|
2357
|
+
const argVal = node.argument ? evaluate(node.argument, env) : T.undefined;
|
|
2358
|
+
if (isReturn(argVal) || isBranch(argVal) || isThrow(argVal)) return argVal;
|
|
2359
|
+
const throwLoc = node.loc ? {
|
|
2360
|
+
start: { line: node.loc.start.line, column: node.loc.start.column },
|
|
2361
|
+
end: { line: node.loc.end.line, column: node.loc.end.column },
|
|
2362
|
+
} : undefined;
|
|
2363
|
+
return makeThrow(argVal, throwLoc);
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
case "BreakStatement":
|
|
2367
|
+
return makeBreak();
|
|
2368
|
+
|
|
2369
|
+
case "ContinueStatement":
|
|
2370
|
+
return makeContinue();
|
|
2371
|
+
|
|
2372
|
+
case "TryStatement": {
|
|
2373
|
+
return evaluateTryStatement(node, env);
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
case "NewExpression": {
|
|
2377
|
+
return evaluateNewExpression(node, env);
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
case "SwitchStatement": {
|
|
2381
|
+
return evaluateSwitchStatement(node, env);
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
case "UpdateExpression": {
|
|
2385
|
+
if (node.argument.type === "Identifier") {
|
|
2386
|
+
const current = env.lookup(node.argument.name);
|
|
2387
|
+
if (current.kind === "literal" && typeof current.value === "number") {
|
|
2388
|
+
const newVal = node.operator === "++"
|
|
2389
|
+
? T.literal(current.value + 1)
|
|
2390
|
+
: T.literal(current.value - 1);
|
|
2391
|
+
if (!env.update(node.argument.name, newVal)) {
|
|
2392
|
+
env.bind(node.argument.name, newVal);
|
|
2393
|
+
}
|
|
2394
|
+
return node.prefix ? newVal : current;
|
|
2395
|
+
}
|
|
2396
|
+
if (!env.update(node.argument.name, T.number)) {
|
|
2397
|
+
env.bind(node.argument.name, T.number);
|
|
2398
|
+
}
|
|
2399
|
+
return T.number;
|
|
2400
|
+
}
|
|
2401
|
+
return T.number;
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
default:
|
|
2405
|
+
return T.unknown;
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
function getMemberKey(node: Node & { type: "MemberExpression" }, env: Environment): string | null {
|
|
2410
|
+
if (!node.computed && node.property.type === "Identifier") {
|
|
2411
|
+
return node.property.name;
|
|
2412
|
+
}
|
|
2413
|
+
if (node.computed) {
|
|
2414
|
+
const propVal = evaluate(node.property, env);
|
|
2415
|
+
if (!isReturn(propVal) && !isBranch(propVal) && !isThrow(propVal) && propVal.kind === "literal") {
|
|
2416
|
+
return String(propVal.value);
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
return null;
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
function bindPattern(pattern: Node, value: TypeValue, env: Environment): void {
|
|
2423
|
+
if (pattern.type === "Identifier") {
|
|
2424
|
+
env.bind(pattern.name, value);
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
if (pattern.type === "RestElement") {
|
|
2429
|
+
// Handle rest parameters: (...args) => ...
|
|
2430
|
+
// The value should be a tuple of all remaining arguments
|
|
2431
|
+
if (pattern.argument.type === "Identifier") {
|
|
2432
|
+
env.bind(pattern.argument.name, value);
|
|
2433
|
+
}
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
if (pattern.type === "AssignmentPattern") {
|
|
2438
|
+
const defaultVal = evaluate(pattern.right, env);
|
|
2439
|
+
const resolved = (value.kind === "literal" && value.value === undefined)
|
|
2440
|
+
? (!isReturn(defaultVal) && !isBranch(defaultVal) && !isThrow(defaultVal) ? defaultVal : T.unknown)
|
|
2441
|
+
: value;
|
|
2442
|
+
bindPattern(pattern.left, resolved, env);
|
|
2443
|
+
return;
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
if (pattern.type === "ObjectPattern") {
|
|
2447
|
+
const restKeys: string[] = [];
|
|
2448
|
+
for (const prop of pattern.properties) {
|
|
2449
|
+
if (prop.type === "RestElement") {
|
|
2450
|
+
if (value.kind === "object") {
|
|
2451
|
+
const remaining: Record<string, TypeValue> = {};
|
|
2452
|
+
for (const [k, v] of Object.entries(value.properties)) {
|
|
2453
|
+
if (!restKeys.includes(k)) remaining[k] = v;
|
|
2454
|
+
}
|
|
2455
|
+
bindPattern(prop.argument, T.object(remaining), env);
|
|
2456
|
+
} else {
|
|
2457
|
+
bindPattern(prop.argument, T.object({}), env);
|
|
2458
|
+
}
|
|
2459
|
+
continue;
|
|
2460
|
+
}
|
|
2461
|
+
if (prop.type !== "ObjectProperty") continue;
|
|
2462
|
+
const key = prop.key.type === "Identifier"
|
|
2463
|
+
? prop.key.name
|
|
2464
|
+
: prop.key.type === "StringLiteral"
|
|
2465
|
+
? prop.key.value
|
|
2466
|
+
: null;
|
|
2467
|
+
if (!key) continue;
|
|
2468
|
+
restKeys.push(key);
|
|
2469
|
+
// Own-property guard first (a plain record would leak native JS
|
|
2470
|
+
// functions for names like "hasOwnProperty"), then Object.prototype
|
|
2471
|
+
// fallback — real destructuring reads through the prototype chain.
|
|
2472
|
+
const propVal = value.kind === "object"
|
|
2473
|
+
? (hasOwnProp(value.properties, key)
|
|
2474
|
+
? value.properties[key]
|
|
2475
|
+
: (hasOwnProp(OBJECT_PROTOTYPE_METHODS, key) ? bindObjectProtoMethod(OBJECT_PROTOTYPE_METHODS[key], value) : T.undefined))
|
|
2476
|
+
: T.unknown;
|
|
2477
|
+
bindPattern(prop.value as Node, propVal, env);
|
|
2478
|
+
}
|
|
2479
|
+
return;
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
if (pattern.type === "ArrayPattern") {
|
|
2483
|
+
for (let i = 0; i < pattern.elements.length; i++) {
|
|
2484
|
+
const elem = pattern.elements[i];
|
|
2485
|
+
if (!elem) continue;
|
|
2486
|
+
if (elem.type === "RestElement") {
|
|
2487
|
+
if (value.kind === "tuple") {
|
|
2488
|
+
bindPattern(elem.argument, T.tuple(value.elements.slice(i)), env);
|
|
2489
|
+
} else if (value.kind === "array") {
|
|
2490
|
+
bindPattern(elem.argument, value, env);
|
|
2491
|
+
} else {
|
|
2492
|
+
bindPattern(elem.argument, T.tuple([]), env);
|
|
2493
|
+
}
|
|
2494
|
+
continue;
|
|
2495
|
+
}
|
|
2496
|
+
const elemVal = value.kind === "tuple"
|
|
2497
|
+
? (value.elements[i] ?? T.undefined)
|
|
2498
|
+
: value.kind === "array"
|
|
2499
|
+
? value.element
|
|
2500
|
+
: T.unknown;
|
|
2501
|
+
bindPattern(elem, elemVal, env);
|
|
2502
|
+
}
|
|
2503
|
+
return;
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
function evaluateArgs(args: Node[], env: Environment): TypeValue[] | ReturnSignal | BranchSignal | ThrowSignal {
|
|
2508
|
+
const result: TypeValue[] = [];
|
|
2509
|
+
for (const arg of args) {
|
|
2510
|
+
if (arg.type === "SpreadElement") {
|
|
2511
|
+
const spreadVal = evaluate(arg.argument, env);
|
|
2512
|
+
if (isReturn(spreadVal) || isBranch(spreadVal) || isThrow(spreadVal)) return spreadVal;
|
|
2513
|
+
if (spreadVal.kind === "tuple") {
|
|
2514
|
+
result.push(...spreadVal.elements);
|
|
2515
|
+
} else if (spreadVal.kind === "array") {
|
|
2516
|
+
result.push(spreadVal.element);
|
|
2517
|
+
} else {
|
|
2518
|
+
// Set/Map instances spread through their exact side tables or
|
|
2519
|
+
// their element typeArgs (one element, like the array case).
|
|
2520
|
+
const iterable = iterableSpreadValue(spreadVal);
|
|
2521
|
+
if (iterable) {
|
|
2522
|
+
if (iterable.kind === "tuple") {
|
|
2523
|
+
result.push(...iterable.elements);
|
|
2524
|
+
} else if (iterable.kind === "array") {
|
|
2525
|
+
result.push(iterable.element);
|
|
2526
|
+
} else {
|
|
2527
|
+
result.push(T.unknown);
|
|
2528
|
+
}
|
|
2529
|
+
} else {
|
|
2530
|
+
result.push(T.unknown);
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
continue;
|
|
2534
|
+
}
|
|
2535
|
+
const v = evaluate(arg, env);
|
|
2536
|
+
if (isReturn(v) || isBranch(v) || isThrow(v)) return v;
|
|
2537
|
+
if (_provenanceEnabled && arg.loc) {
|
|
2538
|
+
_originMap.set(v, { line: arg.loc.start.line, column: arg.loc.start.column });
|
|
2539
|
+
}
|
|
2540
|
+
result.push(v);
|
|
2541
|
+
}
|
|
2542
|
+
return result;
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
function evaluateBuiltinCall(
|
|
2546
|
+
name: string,
|
|
2547
|
+
args: Node[],
|
|
2548
|
+
env: Environment,
|
|
2549
|
+
): EvalResult | null {
|
|
2550
|
+
// Type conversion functions
|
|
2551
|
+
if (name === "String" || name === "Number" || name === "Boolean") {
|
|
2552
|
+
const argVals = evaluateArgs(args, env);
|
|
2553
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
2554
|
+
if (argVals.length === 0) {
|
|
2555
|
+
if (name === "String") return T.literal("");
|
|
2556
|
+
if (name === "Number") return T.literal(0);
|
|
2557
|
+
if (name === "Boolean") return T.literal(false);
|
|
2558
|
+
}
|
|
2559
|
+
// For literals, try to convert
|
|
2560
|
+
if (argVals.length === 1) {
|
|
2561
|
+
const arg = argVals[0];
|
|
2562
|
+
if (name === "String") {
|
|
2563
|
+
if (arg.kind === "literal") return T.literal(String(arg.value));
|
|
2564
|
+
return T.string;
|
|
2565
|
+
}
|
|
2566
|
+
if (name === "Number") {
|
|
2567
|
+
if (arg.kind === "literal") {
|
|
2568
|
+
// Number() converts literals to their numeric value
|
|
2569
|
+
if (arg.value === null) return T.literal(0);
|
|
2570
|
+
if (arg.value === undefined) return T.literal(NaN);
|
|
2571
|
+
if (typeof arg.value === "boolean") return T.literal(arg.value ? 1 : 0);
|
|
2572
|
+
if (typeof arg.value === "number") return arg;
|
|
2573
|
+
if (typeof arg.value === "string") {
|
|
2574
|
+
const num = Number(arg.value);
|
|
2575
|
+
if (!isNaN(num)) return T.literal(num);
|
|
2576
|
+
return T.literal(NaN);
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
return T.number;
|
|
2580
|
+
}
|
|
2581
|
+
if (name === "Boolean") {
|
|
2582
|
+
if (arg.kind === "literal") return T.literal(Boolean(arg.value));
|
|
2583
|
+
return T.boolean;
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
if (name === "String") return T.string;
|
|
2587
|
+
if (name === "Number") return T.number;
|
|
2588
|
+
if (name === "Boolean") return T.boolean;
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
// parseInt and parseFloat
|
|
2592
|
+
if (name === "parseInt" || name === "parseFloat") {
|
|
2593
|
+
const argVals = evaluateArgs(args, env);
|
|
2594
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
2595
|
+
return T.number;
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
// isNaN and isFinite
|
|
2599
|
+
if (name === "isNaN" || name === "isFinite") {
|
|
2600
|
+
const argVals = evaluateArgs(args, env);
|
|
2601
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
2602
|
+
return T.boolean;
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
// encodeURIComponent, decodeURIComponent, encodeURI, decodeURI
|
|
2606
|
+
if (name === "encodeURIComponent" || name === "decodeURIComponent" ||
|
|
2607
|
+
name === "encodeURI" || name === "decodeURI") {
|
|
2608
|
+
const argVals = evaluateArgs(args, env);
|
|
2609
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
2610
|
+
return T.string;
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
// Math functions (accessed via member expression, not here)
|
|
2614
|
+
if (name === "Math") {
|
|
2615
|
+
return null;
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
// console functions (accessed via member expression, not here)
|
|
2619
|
+
if (name === "console") {
|
|
2620
|
+
return null;
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
// fetch global function
|
|
2624
|
+
if (name === "fetch") {
|
|
2625
|
+
return T.promise(createResponseType());
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
return null;
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
// --- Function.prototype.call / apply / bind ---
|
|
2632
|
+
// When the receiver of a method call is itself a function value,
|
|
2633
|
+
// `f.call(thisArg, ...args)` / `f.apply(thisArg, argsArray)` re-invoke that
|
|
2634
|
+
// function with the args following the leading thisArg; `f.bind(...)`
|
|
2635
|
+
// approximates to the original function value. Non-function receivers keep
|
|
2636
|
+
// the existing fallback.
|
|
2637
|
+
function evaluateFunctionPrototypeMethod(
|
|
2638
|
+
fnVal: TypeValue & { kind: "function" },
|
|
2639
|
+
methodName: string,
|
|
2640
|
+
argVals: TypeValue[],
|
|
2641
|
+
): EvalResult | null {
|
|
2642
|
+
if (methodName === "bind") return fnVal;
|
|
2643
|
+
if (methodName === "call") {
|
|
2644
|
+
const full = callFunctionFull(fnVal, argVals.slice(1), argVals[0]);
|
|
2645
|
+
// A definitely-throwing callee (e.g. `null.valueOf` binding) must raise
|
|
2646
|
+
// a ThrowSignal, not degrade to never — enclosing try/catch depends on it.
|
|
2647
|
+
if (full.value.kind === "never" && full.throws.kind !== "never") {
|
|
2648
|
+
return makeThrow(full.throws, full.throwLoc);
|
|
2649
|
+
}
|
|
2650
|
+
return full.value;
|
|
2651
|
+
}
|
|
2652
|
+
if (methodName === "apply") {
|
|
2653
|
+
const listArg = argVals[1];
|
|
2654
|
+
let spreadArgs: TypeValue[];
|
|
2655
|
+
if (listArg?.kind === "tuple") {
|
|
2656
|
+
spreadArgs = listArg.elements;
|
|
2657
|
+
} else {
|
|
2658
|
+
// Unknown-length array (or non-array): approximate with the element
|
|
2659
|
+
// type (or unknown) repeated to the callee's arity.
|
|
2660
|
+
const el = listArg?.kind === "array" ? listArg.element : T.unknown;
|
|
2661
|
+
spreadArgs = fnVal.params.map(() => el);
|
|
2662
|
+
}
|
|
2663
|
+
const full = callFunctionFull(fnVal, spreadArgs, argVals[0]);
|
|
2664
|
+
if (full.value.kind === "never" && full.throws.kind !== "never") {
|
|
2665
|
+
return makeThrow(full.throws, full.throwLoc);
|
|
2666
|
+
}
|
|
2667
|
+
return full.value;
|
|
2668
|
+
}
|
|
2669
|
+
return null;
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
function evaluateMethodForMember(
|
|
2673
|
+
objVal: TypeValue,
|
|
2674
|
+
methodName: string,
|
|
2675
|
+
argVals: TypeValue[],
|
|
2676
|
+
callee: Node & { type: "MemberExpression" },
|
|
2677
|
+
env: Environment,
|
|
2678
|
+
): EvalResult | null {
|
|
2679
|
+
// Function.prototype.call/apply/bind on function-valued union members
|
|
2680
|
+
if (objVal.kind === "function") {
|
|
2681
|
+
const fnProto = evaluateFunctionPrototypeMethod(objVal, methodName, argVals);
|
|
2682
|
+
if (fnProto !== null) return fnProto;
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
// Promise instance methods
|
|
2686
|
+
if (objVal.kind === "promise") {
|
|
2687
|
+
if (methodName === "then") {
|
|
2688
|
+
const chained = applyPromiseThenCallback(objVal, argVals);
|
|
2689
|
+
if (chained !== null) return chained;
|
|
2690
|
+
}
|
|
2691
|
+
const result = evaluatePromiseInstanceMethod(objVal, methodName, argVals);
|
|
2692
|
+
if (result !== null) return result;
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
// Instance methods (Map, Set, RegExp, etc.)
|
|
2696
|
+
if (objVal.kind === "instance") {
|
|
2697
|
+
const classMethods: Record<string, Record<string, (...args: TypeValue[]) => TypeValue>> = {
|
|
2698
|
+
Map: MAP_INSTANCE_METHODS,
|
|
2699
|
+
Set: SET_INSTANCE_METHODS,
|
|
2700
|
+
RegExp: REGEXP_INSTANCE_METHODS,
|
|
2701
|
+
};
|
|
2702
|
+
const methods = classMethods[objVal.className];
|
|
2703
|
+
if (methods) {
|
|
2704
|
+
const method = methods[methodName];
|
|
2705
|
+
if (method) return method(...argVals, objVal);
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
// Array/tuple methods(union 分布路径):与主路径共用 evaluateArrayMethodValues
|
|
2710
|
+
// ——实参在此已是 TypeValue,回调类 map/filter/reduce 等同样从函数值签名
|
|
2711
|
+
// 求值(mock 箭头 / 具名函数 / 内联箭头一致),不再保守降级 unknown。
|
|
2712
|
+
if (objVal.kind === "array" || objVal.kind === "tuple") {
|
|
2713
|
+
return evaluateArrayMethodValues(objVal, methodName, argVals);
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
// String methods
|
|
2717
|
+
if (isStringLike(objVal)) {
|
|
2718
|
+
return evaluateStringMethod(objVal, methodName, argVals);
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
return null;
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
function evaluateMethodCall(
|
|
2725
|
+
callee: Node & { type: "MemberExpression" },
|
|
2726
|
+
args: Node[],
|
|
2727
|
+
env: Environment,
|
|
2728
|
+
): EvalResult | null {
|
|
2729
|
+
const objVal = evaluate(callee.object, env);
|
|
2730
|
+
if (isReturn(objVal) || isBranch(objVal) || isThrow(objVal)) return objVal;
|
|
2731
|
+
const methodName = !callee.computed && callee.property.type === "Identifier"
|
|
2732
|
+
? callee.property.name
|
|
2733
|
+
: null;
|
|
2734
|
+
if (!methodName) return null;
|
|
2735
|
+
|
|
2736
|
+
// Distribute method calls over union types
|
|
2737
|
+
if (objVal.kind === "union") {
|
|
2738
|
+
const argVals = evaluateArgs(args, env);
|
|
2739
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
2740
|
+
let methodMissed = false;
|
|
2741
|
+
const result = distributeOverUnion(objVal, (member) => {
|
|
2742
|
+
// Create a temporary env with the member bound, then re-evaluate the method call
|
|
2743
|
+
const memberResult = evaluateMethodForMember(member, methodName, argVals as TypeValue[], callee, env);
|
|
2744
|
+
if (
|
|
2745
|
+
memberResult === null &&
|
|
2746
|
+
member.kind !== "object" && member.kind !== "instance" &&
|
|
2747
|
+
member.kind !== "refined" && member.kind !== "promise"
|
|
2748
|
+
) {
|
|
2749
|
+
methodMissed = true;
|
|
2750
|
+
}
|
|
2751
|
+
// A throwing member contributes no value; return/branch signals
|
|
2752
|
+
// contribute their carried value (the potential throw is dropped
|
|
2753
|
+
// here; the single-callee path raises it properly).
|
|
2754
|
+
if (memberResult !== null && typeof memberResult === "object") {
|
|
2755
|
+
if (THROW_SIGNAL in memberResult) return T.never;
|
|
2756
|
+
if (RETURN_SIGNAL in memberResult) return memberResult.value;
|
|
2757
|
+
if (BRANCH_SIGNAL in memberResult) return memberResult.returnedValue;
|
|
2758
|
+
}
|
|
2759
|
+
return memberResult ?? T.unknown;
|
|
2760
|
+
});
|
|
2761
|
+
if (methodMissed) {
|
|
2762
|
+
recordUnknown({
|
|
2763
|
+
kind: "method",
|
|
2764
|
+
name: methodName,
|
|
2765
|
+
receiverType: objVal,
|
|
2766
|
+
loc: callee.loc,
|
|
2767
|
+
reason: `no method '${methodName}' on ${objVal.kind}`,
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
return result;
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
// Function.prototype.call/apply/bind re-invoking a function value
|
|
2774
|
+
if (objVal.kind === "function") {
|
|
2775
|
+
const argVals = evaluateArgs(args, env);
|
|
2776
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
2777
|
+
const fnProto = evaluateFunctionPrototypeMethod(objVal, methodName, argVals as TypeValue[]);
|
|
2778
|
+
if (fnProto !== null) return fnProto;
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
// Handle console methods (no return value)
|
|
2782
|
+
if (callee.object.type === "Identifier" && callee.object.name === "console" && !env.has("console")) {
|
|
2783
|
+
const argVals = evaluateArgs(args, env);
|
|
2784
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
2785
|
+
return T.undefined;
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
// Handle Math methods
|
|
2789
|
+
if (callee.object.type === "Identifier" && callee.object.name === "Math" && !env.has("Math")) {
|
|
2790
|
+
const argVals = evaluateArgs(args, env);
|
|
2791
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
2792
|
+
// Math methods return numbers
|
|
2793
|
+
if (["abs", "ceil", "floor", "round", "sqrt", "pow", "min", "max",
|
|
2794
|
+
"random", "log", "log2", "log10", "exp", "sin", "cos", "tan",
|
|
2795
|
+
"asin", "acos", "atan", "atan2"].includes(methodName)) {
|
|
2796
|
+
return T.number;
|
|
2797
|
+
}
|
|
2798
|
+
// Math constants
|
|
2799
|
+
if (["PI", "E", "LN2", "LN10", "LOG2E", "LOG10E", "SQRT1_2", "SQRT2"].includes(methodName)) {
|
|
2800
|
+
return T.number;
|
|
2801
|
+
}
|
|
2802
|
+
return T.number;
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
// Handle Date methods
|
|
2806
|
+
if (callee.object.type === "Identifier" && callee.object.name === "Date" && !env.has("Date")) {
|
|
2807
|
+
const argVals = evaluateArgs(args, env);
|
|
2808
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
2809
|
+
// Date static methods
|
|
2810
|
+
if (["now", "parse", "UTC"].includes(methodName)) {
|
|
2811
|
+
return T.number;
|
|
2812
|
+
}
|
|
2813
|
+
// Date constructor
|
|
2814
|
+
if (methodName === "constructor") {
|
|
2815
|
+
return T.instanceOf("Date", {
|
|
2816
|
+
getTime: T.fn(["...args"], { type: "BlockStatement", body: [] } as any, undefined as any),
|
|
2817
|
+
getFullYear: T.fn(["...args"], { type: "BlockStatement", body: [] } as any, undefined as any),
|
|
2818
|
+
toISOString: T.fn(["...args"], { type: "BlockStatement", body: [] } as any, undefined as any),
|
|
2819
|
+
});
|
|
2820
|
+
}
|
|
2821
|
+
return T.unknown;
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
// Handle JSON methods
|
|
2825
|
+
if (callee.object.type === "Identifier" && callee.object.name === "JSON" && !env.has("JSON")) {
|
|
2826
|
+
const argVals = evaluateArgs(args, env);
|
|
2827
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
2828
|
+
// JSON.parse: the table entry is a fnSig whose impl decodes literal
|
|
2829
|
+
// string arguments precisely (jsonToTypeValue); non-literal / invalid /
|
|
2830
|
+
// reviver-bearing calls fall back to its unknown return type.
|
|
2831
|
+
if (methodName === "parse") {
|
|
2832
|
+
return callFunction(
|
|
2833
|
+
(BUILTIN_STATIC_METHODS.JSON as Record<string, TypeValue>).parse as TypeValue & {
|
|
2834
|
+
kind: "function";
|
|
2835
|
+
},
|
|
2836
|
+
argVals as TypeValue[],
|
|
2837
|
+
);
|
|
2838
|
+
}
|
|
2839
|
+
// JSON.stringify returns string
|
|
2840
|
+
if (methodName === "stringify") {
|
|
2841
|
+
return T.string;
|
|
2842
|
+
}
|
|
2843
|
+
return T.unknown;
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
// Handle Array methods (Array.from, Array.isArray, etc.)
|
|
2847
|
+
if (callee.object.type === "Identifier" && callee.object.name === "Array" && !env.has("Array")) {
|
|
2848
|
+
const argVals = evaluateArgs(args, env);
|
|
2849
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
2850
|
+
if (methodName === "from") {
|
|
2851
|
+
if (argVals.length > 0) {
|
|
2852
|
+
const iterable = argVals[0];
|
|
2853
|
+
// Array.from(Set) -> array of Set's element type
|
|
2854
|
+
if (iterable.kind === "instance" && iterable.className === "Set") {
|
|
2855
|
+
const typeArgs = (iterable as any)._typeArgs;
|
|
2856
|
+
if (typeArgs?.T) return T.array(typeArgs.T);
|
|
2857
|
+
}
|
|
2858
|
+
// Array.from(tuple) -> tuple (preserve types)
|
|
2859
|
+
if (iterable.kind === "tuple") {
|
|
2860
|
+
return iterable;
|
|
2861
|
+
}
|
|
2862
|
+
// Array.from(array) -> array
|
|
2863
|
+
if (iterable.kind === "array") {
|
|
2864
|
+
return iterable;
|
|
777
2865
|
}
|
|
778
|
-
const val = evaluate(elem as Node, env);
|
|
779
|
-
if (isReturn(val) || isBranch(val) || isThrow(val)) return val;
|
|
780
|
-
elements.push(val);
|
|
781
2866
|
}
|
|
782
|
-
return T.
|
|
2867
|
+
return T.array(T.unknown);
|
|
783
2868
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
}
|
|
792
|
-
return makeThrow(argVal, throwLoc);
|
|
2869
|
+
if (methodName === "isArray") {
|
|
2870
|
+
// Structurally-known receivers decide literally (arrays/tuples →
|
|
2871
|
+
// true, plain objects/instances-of-other-classes → false); unknown
|
|
2872
|
+
// receivers keep the symbolic boolean.
|
|
2873
|
+
return distributeOverUnion((argVals as TypeValue[])[0] ?? T.undefined, (v) => {
|
|
2874
|
+
const lit = arrayIsArrayLiteral(v);
|
|
2875
|
+
return lit === undefined ? T.boolean : T.literal(lit);
|
|
2876
|
+
});
|
|
793
2877
|
}
|
|
2878
|
+
return T.unknown;
|
|
2879
|
+
}
|
|
794
2880
|
|
|
795
|
-
|
|
796
|
-
|
|
2881
|
+
// Handle Promise methods
|
|
2882
|
+
if (callee.object.type === "Identifier" && callee.object.name === "Promise" && !env.has("Promise")) {
|
|
2883
|
+
const argVals = evaluateArgs(args, env);
|
|
2884
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
2885
|
+
const result = evaluatePromiseStaticMethod(methodName, argVals as TypeValue[]);
|
|
2886
|
+
if (result !== null) return result;
|
|
2887
|
+
return T.unknown;
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
// Handle Symbol methods
|
|
2891
|
+
if (callee.object.type === "Identifier" && callee.object.name === "Symbol" && !env.has("Symbol")) {
|
|
2892
|
+
const argVals = evaluateArgs(args, env);
|
|
2893
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
2894
|
+
if (methodName === "for") return T.symbol;
|
|
2895
|
+
if (methodName === "keyFor") return T.union(T.string, T.undefined);
|
|
2896
|
+
return T.unknown;
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
// Handle Reflect methods
|
|
2900
|
+
if (callee.object.type === "Identifier" && callee.object.name === "Reflect" && !env.has("Reflect")) {
|
|
2901
|
+
const argVals = evaluateArgs(args, env);
|
|
2902
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
2903
|
+
const method = (REFLECT_METHODS as Record<string, (...args: TypeValue[]) => TypeValue>)[methodName];
|
|
2904
|
+
if (method) return method(...(argVals as TypeValue[]));
|
|
2905
|
+
return T.unknown;
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2908
|
+
// Handle Promise instance methods (.then, .catch, .finally)
|
|
2909
|
+
if (objVal.kind === "promise") {
|
|
2910
|
+
const argVals = evaluateArgs(args, env);
|
|
2911
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
2912
|
+
if (methodName === "then") {
|
|
2913
|
+
const chained = applyPromiseThenCallback(objVal, argVals as TypeValue[]);
|
|
2914
|
+
if (chained !== null) return chained;
|
|
797
2915
|
}
|
|
2916
|
+
const result = evaluatePromiseInstanceMethod(objVal, methodName, argVals as TypeValue[]);
|
|
2917
|
+
if (result !== null) return result;
|
|
2918
|
+
}
|
|
798
2919
|
|
|
799
|
-
|
|
800
|
-
|
|
2920
|
+
// Handle Map instance methods
|
|
2921
|
+
if (objVal.kind === "instance" && objVal.className === "Map") {
|
|
2922
|
+
const argVals = evaluateArgs(args, env);
|
|
2923
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
2924
|
+
const method = MAP_INSTANCE_METHODS[methodName];
|
|
2925
|
+
if (method) {
|
|
2926
|
+
return method(...(argVals as TypeValue[]), objVal);
|
|
801
2927
|
}
|
|
2928
|
+
}
|
|
802
2929
|
|
|
803
|
-
|
|
804
|
-
|
|
2930
|
+
// Handle Set instance methods
|
|
2931
|
+
if (objVal.kind === "instance" && objVal.className === "Set") {
|
|
2932
|
+
const argVals = evaluateArgs(args, env);
|
|
2933
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
2934
|
+
const method = SET_INSTANCE_METHODS[methodName];
|
|
2935
|
+
if (method) {
|
|
2936
|
+
return method(...(argVals as TypeValue[]), objVal);
|
|
805
2937
|
}
|
|
2938
|
+
}
|
|
806
2939
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
if (!env.update(node.argument.name, newVal)) {
|
|
815
|
-
env.bind(node.argument.name, newVal);
|
|
816
|
-
}
|
|
817
|
-
return node.prefix ? newVal : current;
|
|
818
|
-
}
|
|
819
|
-
if (!env.update(node.argument.name, T.number)) {
|
|
820
|
-
env.bind(node.argument.name, T.number);
|
|
821
|
-
}
|
|
822
|
-
return T.number;
|
|
823
|
-
}
|
|
824
|
-
return T.number;
|
|
2940
|
+
// Handle RegExp instance methods
|
|
2941
|
+
if (objVal.kind === "instance" && objVal.className === "RegExp") {
|
|
2942
|
+
const argVals = evaluateArgs(args, env);
|
|
2943
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
2944
|
+
const method = REGEXP_INSTANCE_METHODS[methodName];
|
|
2945
|
+
if (method) {
|
|
2946
|
+
return method(...(argVals as TypeValue[]), objVal);
|
|
825
2947
|
}
|
|
2948
|
+
}
|
|
826
2949
|
|
|
827
|
-
|
|
828
|
-
|
|
2950
|
+
// Handle URL instance methods
|
|
2951
|
+
if (objVal.kind === "instance" && objVal.className === "URL") {
|
|
2952
|
+
const argVals = evaluateArgs(args, env);
|
|
2953
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
2954
|
+
const method = URL_INSTANCE_METHODS[methodName];
|
|
2955
|
+
if (method) {
|
|
2956
|
+
return method(...(argVals as TypeValue[]), objVal);
|
|
2957
|
+
}
|
|
829
2958
|
}
|
|
830
|
-
}
|
|
831
2959
|
|
|
832
|
-
|
|
833
|
-
if (
|
|
834
|
-
|
|
2960
|
+
// Handle URLSearchParams instance methods
|
|
2961
|
+
if (objVal.kind === "instance" && objVal.className === "URLSearchParams") {
|
|
2962
|
+
const argVals = evaluateArgs(args, env);
|
|
2963
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
2964
|
+
const method = URLSearchParams_INSTANCE_METHODS[methodName];
|
|
2965
|
+
if (method) {
|
|
2966
|
+
return method(...(argVals as TypeValue[]), objVal);
|
|
2967
|
+
}
|
|
835
2968
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
2969
|
+
|
|
2970
|
+
// Handle Response instance methods
|
|
2971
|
+
if (objVal.kind === "instance" && objVal.className === "Response") {
|
|
2972
|
+
const argVals = evaluateArgs(args, env);
|
|
2973
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
2974
|
+
const method = RESPONSE_INSTANCE_METHODS[methodName];
|
|
2975
|
+
if (method) {
|
|
2976
|
+
return method(...(argVals as TypeValue[]), objVal);
|
|
840
2977
|
}
|
|
841
2978
|
}
|
|
842
|
-
return null;
|
|
843
|
-
}
|
|
844
2979
|
|
|
845
|
-
|
|
846
|
-
if (
|
|
847
|
-
|
|
848
|
-
return;
|
|
2980
|
+
// Handle Headers instance methods
|
|
2981
|
+
if (objVal.kind === "instance" && objVal.className === "Headers") {
|
|
2982
|
+
const argVals = evaluateArgs(args, env);
|
|
2983
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
2984
|
+
const method = HEADERS_INSTANCE_METHODS[methodName];
|
|
2985
|
+
if (method) {
|
|
2986
|
+
return method(...(argVals as TypeValue[]), objVal);
|
|
2987
|
+
}
|
|
849
2988
|
}
|
|
850
2989
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
const
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
2990
|
+
// Handle FormData instance methods
|
|
2991
|
+
if (objVal.kind === "instance" && objVal.className === "FormData") {
|
|
2992
|
+
const argVals = evaluateArgs(args, env);
|
|
2993
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
2994
|
+
const method = FORMDATA_INSTANCE_METHODS[methodName];
|
|
2995
|
+
if (method) {
|
|
2996
|
+
return method(...(argVals as TypeValue[]), objVal);
|
|
2997
|
+
}
|
|
858
2998
|
}
|
|
859
2999
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
if (!restKeys.includes(k)) remaining[k] = v;
|
|
868
|
-
}
|
|
869
|
-
bindPattern(prop.argument, T.object(remaining), env);
|
|
870
|
-
} else {
|
|
871
|
-
bindPattern(prop.argument, T.object({}), env);
|
|
872
|
-
}
|
|
873
|
-
continue;
|
|
874
|
-
}
|
|
875
|
-
if (prop.type !== "ObjectProperty") continue;
|
|
876
|
-
const key = prop.key.type === "Identifier"
|
|
877
|
-
? prop.key.name
|
|
878
|
-
: prop.key.type === "StringLiteral"
|
|
879
|
-
? prop.key.value
|
|
880
|
-
: null;
|
|
881
|
-
if (!key) continue;
|
|
882
|
-
restKeys.push(key);
|
|
883
|
-
const propVal = value.kind === "object"
|
|
884
|
-
? (value.properties[key] ?? T.undefined)
|
|
885
|
-
: T.unknown;
|
|
886
|
-
bindPattern(prop.value as Node, propVal, env);
|
|
3000
|
+
// Handle AbortController instance methods
|
|
3001
|
+
if (objVal.kind === "instance" && objVal.className === "AbortController") {
|
|
3002
|
+
const argVals = evaluateArgs(args, env);
|
|
3003
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
3004
|
+
const method = ABORTCONTROLLER_INSTANCE_METHODS[methodName];
|
|
3005
|
+
if (method) {
|
|
3006
|
+
return method(...(argVals as TypeValue[]), objVal);
|
|
887
3007
|
}
|
|
888
|
-
return;
|
|
889
3008
|
}
|
|
890
3009
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
} else if (value.kind === "array") {
|
|
899
|
-
bindPattern(elem.argument, value, env);
|
|
900
|
-
} else {
|
|
901
|
-
bindPattern(elem.argument, T.tuple([]), env);
|
|
902
|
-
}
|
|
903
|
-
continue;
|
|
904
|
-
}
|
|
905
|
-
const elemVal = value.kind === "tuple"
|
|
906
|
-
? (value.elements[i] ?? T.undefined)
|
|
907
|
-
: value.kind === "array"
|
|
908
|
-
? value.element
|
|
909
|
-
: T.unknown;
|
|
910
|
-
bindPattern(elem, elemVal, env);
|
|
3010
|
+
// Handle WeakMap instance methods
|
|
3011
|
+
if (objVal.kind === "instance" && objVal.className === "WeakMap") {
|
|
3012
|
+
const argVals = evaluateArgs(args, env);
|
|
3013
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
3014
|
+
const method = WEAKMAP_INSTANCE_METHODS[methodName];
|
|
3015
|
+
if (method) {
|
|
3016
|
+
return method(...(argVals as TypeValue[]), objVal);
|
|
911
3017
|
}
|
|
912
|
-
return;
|
|
913
3018
|
}
|
|
914
|
-
}
|
|
915
3019
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
if (
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
result.push(...spreadVal.elements);
|
|
924
|
-
} else if (spreadVal.kind === "array") {
|
|
925
|
-
result.push(spreadVal.element);
|
|
926
|
-
} else {
|
|
927
|
-
result.push(T.unknown);
|
|
928
|
-
}
|
|
929
|
-
continue;
|
|
3020
|
+
// Handle WeakSet instance methods
|
|
3021
|
+
if (objVal.kind === "instance" && objVal.className === "WeakSet") {
|
|
3022
|
+
const argVals = evaluateArgs(args, env);
|
|
3023
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
3024
|
+
const method = WEAKSET_INSTANCE_METHODS[methodName];
|
|
3025
|
+
if (method) {
|
|
3026
|
+
return method(...(argVals as TypeValue[]), objVal);
|
|
930
3027
|
}
|
|
931
|
-
const v = evaluate(arg, env);
|
|
932
|
-
if (isReturn(v) || isBranch(v) || isThrow(v)) return v;
|
|
933
|
-
result.push(v);
|
|
934
3028
|
}
|
|
935
|
-
return result;
|
|
936
|
-
}
|
|
937
3029
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
3030
|
+
// Handle DateTimeFormat instance methods
|
|
3031
|
+
if (objVal.kind === "instance" && objVal.className === "DateTimeFormat") {
|
|
3032
|
+
const argVals = evaluateArgs(args, env);
|
|
3033
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
3034
|
+
const method = INTL_DATETIMEFORMAT_METHODS[methodName];
|
|
3035
|
+
if (method) {
|
|
3036
|
+
return method(...(argVals as TypeValue[]), objVal);
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
945
3039
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
3040
|
+
// Handle NumberFormat instance methods
|
|
3041
|
+
if (objVal.kind === "instance" && objVal.className === "NumberFormat") {
|
|
3042
|
+
const argVals = evaluateArgs(args, env);
|
|
3043
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
3044
|
+
const method = INTL_NUMBERFORMAT_METHODS[methodName];
|
|
3045
|
+
if (method) {
|
|
3046
|
+
return method(...(argVals as TypeValue[]), objVal);
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
950
3049
|
|
|
951
3050
|
if (
|
|
952
3051
|
callee.object.type === "Identifier" &&
|
|
953
3052
|
callee.object.name === "Object" &&
|
|
3053
|
+
!env.has("Object") &&
|
|
954
3054
|
args.length >= 1
|
|
955
3055
|
) {
|
|
956
3056
|
const argVal = evaluate(args[0], env);
|
|
@@ -959,7 +3059,17 @@ function evaluateMethodCall(
|
|
|
959
3059
|
}
|
|
960
3060
|
|
|
961
3061
|
if (objVal.kind === "array" || objVal.kind === "tuple") {
|
|
962
|
-
|
|
3062
|
+
const arrResult = evaluateArrayMethod(objVal, methodName, args, env);
|
|
3063
|
+
if (arrResult === null) {
|
|
3064
|
+
recordUnknown({
|
|
3065
|
+
kind: "method",
|
|
3066
|
+
name: methodName,
|
|
3067
|
+
receiverType: objVal,
|
|
3068
|
+
loc: callee.loc,
|
|
3069
|
+
reason: `no method '${methodName}' on ${objVal.kind}`,
|
|
3070
|
+
});
|
|
3071
|
+
}
|
|
3072
|
+
return arrResult;
|
|
963
3073
|
}
|
|
964
3074
|
|
|
965
3075
|
if (isStringLike(objVal)) {
|
|
@@ -971,7 +3081,36 @@ function evaluateMethodCall(
|
|
|
971
3081
|
if (refined !== undefined) return refined;
|
|
972
3082
|
}
|
|
973
3083
|
|
|
974
|
-
|
|
3084
|
+
const strResult = evaluateStringMethod(objVal, methodName, argVals as TypeValue[]);
|
|
3085
|
+
if (strResult === null) {
|
|
3086
|
+
recordUnknown({
|
|
3087
|
+
kind: "method",
|
|
3088
|
+
name: methodName,
|
|
3089
|
+
receiverType: objVal,
|
|
3090
|
+
loc: callee.loc,
|
|
3091
|
+
reason: `no method '${methodName}' on ${objVal.kind}`,
|
|
3092
|
+
});
|
|
3093
|
+
}
|
|
3094
|
+
return strResult;
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
// Number receivers: toString/valueOf exist on every JS number. Without
|
|
3098
|
+
// this, deepEqual-style `obj.toString()` comparators reaching a numeric
|
|
3099
|
+
// union member record a false no-method error.
|
|
3100
|
+
if (isNumberLike(objVal)) {
|
|
3101
|
+
const argVals = evaluateArgs(args, env);
|
|
3102
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
3103
|
+
if (methodName === "toString") {
|
|
3104
|
+
return objVal.kind === "literal" && typeof objVal.value === "number"
|
|
3105
|
+
? T.literal(String(objVal.value))
|
|
3106
|
+
: T.string;
|
|
3107
|
+
}
|
|
3108
|
+
if (methodName === "valueOf") {
|
|
3109
|
+
return objVal.kind === "literal" && typeof objVal.value === "number"
|
|
3110
|
+
? objVal
|
|
3111
|
+
: T.number;
|
|
3112
|
+
}
|
|
3113
|
+
if (methodName === "toFixed" || methodName === "toPrecision") return T.string;
|
|
975
3114
|
}
|
|
976
3115
|
|
|
977
3116
|
if (objVal.kind === "refined") {
|
|
@@ -981,6 +3120,23 @@ function evaluateMethodCall(
|
|
|
981
3120
|
if (result !== undefined) return result;
|
|
982
3121
|
}
|
|
983
3122
|
|
|
3123
|
+
// Method dispatch fallback: no handler matched. For receivers whose property
|
|
3124
|
+
// lookup may still succeed (object/instance/refined/promise — resolved by the
|
|
3125
|
+
// MemberExpression evaluation downstream), stay silent; for everything else
|
|
3126
|
+
// this call will degrade to T.unknown, so record it.
|
|
3127
|
+
if (
|
|
3128
|
+
objVal.kind !== "object" && objVal.kind !== "instance" &&
|
|
3129
|
+
objVal.kind !== "refined" && objVal.kind !== "promise"
|
|
3130
|
+
) {
|
|
3131
|
+
recordUnknown({
|
|
3132
|
+
kind: "method",
|
|
3133
|
+
name: methodName,
|
|
3134
|
+
receiverType: objVal,
|
|
3135
|
+
loc: callee.loc,
|
|
3136
|
+
reason: `no method '${methodName}' on ${objVal.kind}`,
|
|
3137
|
+
});
|
|
3138
|
+
}
|
|
3139
|
+
|
|
984
3140
|
return null;
|
|
985
3141
|
}
|
|
986
3142
|
|
|
@@ -991,6 +3147,13 @@ function isStringLike(tv: TypeValue): boolean {
|
|
|
991
3147
|
return false;
|
|
992
3148
|
}
|
|
993
3149
|
|
|
3150
|
+
function isNumberLike(tv: TypeValue): boolean {
|
|
3151
|
+
if (tv.kind === "literal" && typeof tv.value === "number") return true;
|
|
3152
|
+
if (tv.kind === "primitive" && tv.type === "number") return true;
|
|
3153
|
+
if (tv.kind === "refined") return isNumberLike(tv.base);
|
|
3154
|
+
return false;
|
|
3155
|
+
}
|
|
3156
|
+
|
|
994
3157
|
function evaluateStringMethod(
|
|
995
3158
|
receiver: TypeValue,
|
|
996
3159
|
method: string,
|
|
@@ -1014,6 +3177,9 @@ function evaluateStringMethodLiteral(
|
|
|
1014
3177
|
};
|
|
1015
3178
|
|
|
1016
3179
|
switch (method) {
|
|
3180
|
+
// String.prototype.toString/valueOf return the receiver string itself
|
|
3181
|
+
case "toString":
|
|
3182
|
+
case "valueOf": return T.literal(str);
|
|
1017
3183
|
case "toUpperCase": return T.literal(str.toUpperCase());
|
|
1018
3184
|
case "toLowerCase": return T.literal(str.toLowerCase());
|
|
1019
3185
|
case "trim": return T.literal(str.trim());
|
|
@@ -1125,6 +3291,9 @@ function evaluateStringMethodAbstract(
|
|
|
1125
3291
|
_args: TypeValue[],
|
|
1126
3292
|
): TypeValue | null {
|
|
1127
3293
|
switch (method) {
|
|
3294
|
+
case "toString":
|
|
3295
|
+
case "valueOf":
|
|
3296
|
+
return T.string;
|
|
1128
3297
|
case "toUpperCase":
|
|
1129
3298
|
case "toLowerCase":
|
|
1130
3299
|
case "trim":
|
|
@@ -1160,6 +3329,58 @@ function evaluateObjectStaticMethod(
|
|
|
1160
3329
|
method: string,
|
|
1161
3330
|
obj: TypeValue,
|
|
1162
3331
|
): TypeValue | null {
|
|
3332
|
+
// Shape-aware receivers list their declared own-property names:
|
|
3333
|
+
// instances carry their properties record (usually empty for fresh
|
|
3334
|
+
// collections), tuples their indices. Key loops (`for (const key of
|
|
3335
|
+
// Object.keys(x))`) then iterate concrete keys instead of symbolic
|
|
3336
|
+
// strings.
|
|
3337
|
+
if (obj.kind === "instance") {
|
|
3338
|
+
if (method === "keys" || method === "getOwnPropertyNames") {
|
|
3339
|
+
return T.tuple(Object.keys(obj.properties).map((k) => T.literal(k)));
|
|
3340
|
+
}
|
|
3341
|
+
if (method === "values") return T.tuple(Object.values(obj.properties));
|
|
3342
|
+
if (method === "entries") {
|
|
3343
|
+
return T.tuple(
|
|
3344
|
+
Object.keys(obj.properties).map((k) => T.tuple([T.literal(k), obj.properties[k]])),
|
|
3345
|
+
);
|
|
3346
|
+
}
|
|
3347
|
+
return null;
|
|
3348
|
+
}
|
|
3349
|
+
if (obj.kind === "union") {
|
|
3350
|
+
// union 分发:逐成员 keys/values/entries。成员多时逐成员 tuple 的
|
|
3351
|
+
// union 会超预算——keys 恒为 string[](sound 下界),values/entries
|
|
3352
|
+
// 同理回退宽类型而非 unknown(hoek utils.keys 的 30+ 成员 union 曾
|
|
3353
|
+
// 在无分支路径上退化)。
|
|
3354
|
+
const per = obj.members.map((m) => evaluateObjectStaticMethod(method, m));
|
|
3355
|
+
if (method === "keys" || method === "getOwnPropertyNames") {
|
|
3356
|
+
const literals: TypeValue[] = [];
|
|
3357
|
+
for (const r of per) {
|
|
3358
|
+
if (r?.kind === "tuple") literals.push(...r.elements);
|
|
3359
|
+
else return T.array(T.string);
|
|
3360
|
+
}
|
|
3361
|
+
if (literals.length > 64) return T.array(T.string);
|
|
3362
|
+
return T.tuple(literals);
|
|
3363
|
+
}
|
|
3364
|
+
if (method === "values") {
|
|
3365
|
+
const vals = per.filter((r) => r && r.kind !== "unknown").flatMap((r) => (r!.kind === "tuple" ? r.elements : [r!]));
|
|
3366
|
+
if (per.some((r) => !r || r.kind === "unknown") || vals.length > 64) return T.array(T.unknown);
|
|
3367
|
+
return T.tuple(vals);
|
|
3368
|
+
}
|
|
3369
|
+
if (method === "entries") {
|
|
3370
|
+
return T.array(T.tuple([T.string, T.unknown]));
|
|
3371
|
+
}
|
|
3372
|
+
return null;
|
|
3373
|
+
}
|
|
3374
|
+
if (obj.kind === "tuple") {
|
|
3375
|
+
if (method === "keys" || method === "getOwnPropertyNames") {
|
|
3376
|
+
return T.tuple(obj.elements.map((_, i) => T.literal(String(i))));
|
|
3377
|
+
}
|
|
3378
|
+
if (method === "values") return T.tuple([...obj.elements]);
|
|
3379
|
+
if (method === "entries") {
|
|
3380
|
+
return T.tuple(obj.elements.map((el, i) => T.tuple([T.literal(String(i)), el])));
|
|
3381
|
+
}
|
|
3382
|
+
return null;
|
|
3383
|
+
}
|
|
1163
3384
|
if (obj.kind !== "object") {
|
|
1164
3385
|
if (method === "keys") return T.array(T.string);
|
|
1165
3386
|
if (method === "values") return T.array(T.unknown);
|
|
@@ -1181,6 +3402,9 @@ function evaluateObjectStaticMethod(
|
|
|
1181
3402
|
keys.map((k) => T.tuple([T.literal(k), obj.properties[k]])),
|
|
1182
3403
|
);
|
|
1183
3404
|
}
|
|
3405
|
+
if (method === "getOwnPropertyNames") {
|
|
3406
|
+
return T.tuple(keys.map((k) => T.literal(k)));
|
|
3407
|
+
}
|
|
1184
3408
|
return null;
|
|
1185
3409
|
}
|
|
1186
3410
|
|
|
@@ -1192,12 +3416,23 @@ function evaluateArrayMethod(
|
|
|
1192
3416
|
): EvalResult | null {
|
|
1193
3417
|
const argVals = evaluateArgs(args, env);
|
|
1194
3418
|
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
3419
|
+
return evaluateArrayMethodValues(arr, method, argVals as TypeValue[]);
|
|
3420
|
+
}
|
|
1195
3421
|
|
|
1196
|
-
|
|
3422
|
+
/** Array/tuple 方法求值主体:实参已经是 TypeValue(AST 路径与 union 分布
|
|
3423
|
+
* 路径共用)。回调类方法(map/filter/reduce/...)拿到 kind "function" 的
|
|
3424
|
+
* 实参时经 callFunction 从其签名/闭包求值——mock 箭头、具名函数与内联
|
|
3425
|
+
* 箭头在此走同一机制。 */
|
|
3426
|
+
function evaluateArrayMethodValues(
|
|
3427
|
+
arr: TypeValue & { kind: "array" | "tuple" },
|
|
3428
|
+
method: string,
|
|
3429
|
+
argVals: TypeValue[],
|
|
3430
|
+
): EvalResult | null {
|
|
3431
|
+
const callbackFn = argVals[0];
|
|
1197
3432
|
|
|
1198
3433
|
if (method === "push") {
|
|
1199
3434
|
if (arr.kind === "tuple") {
|
|
1200
|
-
arr.elements.push(...
|
|
3435
|
+
arr.elements.push(...argVals);
|
|
1201
3436
|
return T.literal(arr.elements.length);
|
|
1202
3437
|
}
|
|
1203
3438
|
return T.number;
|
|
@@ -1207,13 +3442,29 @@ function evaluateArrayMethod(
|
|
|
1207
3442
|
return arr.kind === "tuple" ? T.literal(arr.elements.length) : T.number;
|
|
1208
3443
|
}
|
|
1209
3444
|
|
|
3445
|
+
if (method === "pop" || method === "shift") {
|
|
3446
|
+
if (arr.kind === "tuple") {
|
|
3447
|
+
// 抽象数组不模拟顺序语义:返回元素 union(空 tuple 理论返回 undefined,union 进去保持 sound)
|
|
3448
|
+
if (arr.elements.length === 0) return T.undefined;
|
|
3449
|
+
return T.union(...arr.elements);
|
|
3450
|
+
}
|
|
3451
|
+
return T.unknown;
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
if (method === "unshift") {
|
|
3455
|
+
if (arr.kind === "tuple") {
|
|
3456
|
+
return T.literal(arr.elements.length + argVals.length);
|
|
3457
|
+
}
|
|
3458
|
+
return T.number;
|
|
3459
|
+
}
|
|
3460
|
+
|
|
1210
3461
|
if (method === "indexOf" || method === "lastIndexOf") {
|
|
1211
3462
|
return T.number;
|
|
1212
3463
|
}
|
|
1213
3464
|
|
|
1214
3465
|
if (method === "includes") {
|
|
1215
|
-
if (arr.kind === "tuple" &&
|
|
1216
|
-
const searchVal =
|
|
3466
|
+
if (arr.kind === "tuple" && argVals[0]?.kind === "literal") {
|
|
3467
|
+
const searchVal = argVals[0];
|
|
1217
3468
|
const found = arr.elements.some((e) => typeValueEquals(e, searchVal));
|
|
1218
3469
|
return T.literal(found);
|
|
1219
3470
|
}
|
|
@@ -1227,7 +3478,7 @@ function evaluateArrayMethod(
|
|
|
1227
3478
|
if (method === "concat") {
|
|
1228
3479
|
if (arr.kind === "tuple") {
|
|
1229
3480
|
const otherElements: TypeValue[] = [];
|
|
1230
|
-
for (const a of argVals
|
|
3481
|
+
for (const a of argVals) {
|
|
1231
3482
|
if (a.kind === "tuple") otherElements.push(...a.elements);
|
|
1232
3483
|
else if (a.kind === "array") return T.array(simplifyUnion([...arr.elements, a.element]));
|
|
1233
3484
|
else otherElements.push(a);
|
|
@@ -1239,8 +3490,8 @@ function evaluateArrayMethod(
|
|
|
1239
3490
|
|
|
1240
3491
|
if (method === "slice") {
|
|
1241
3492
|
if (arr.kind === "tuple") {
|
|
1242
|
-
const start =
|
|
1243
|
-
const end =
|
|
3493
|
+
const start = argVals[0];
|
|
3494
|
+
const end = argVals[1];
|
|
1244
3495
|
const startIdx = start?.kind === "literal" && typeof start.value === "number" ? start.value : 0;
|
|
1245
3496
|
const endIdx = end?.kind === "literal" && typeof end.value === "number" ? end.value : arr.elements.length;
|
|
1246
3497
|
return T.tuple(arr.elements.slice(startIdx, endIdx));
|
|
@@ -1248,12 +3499,23 @@ function evaluateArrayMethod(
|
|
|
1248
3499
|
return T.array(arr.element);
|
|
1249
3500
|
}
|
|
1250
3501
|
|
|
3502
|
+
// 回调实参是「函数 union」(cond ? f : g 透传给 HOF):对每个成员签名
|
|
3503
|
+
// 分别求值再 union 结果——与非函数守卫(unknown 回调)不同,这里每个
|
|
3504
|
+
// 成员都有完整的 TypeValue 信息,不该整体降级。
|
|
3505
|
+
if (callbackFn && callbackFn.kind === "union" && callbackFn.members.length > 0 &&
|
|
3506
|
+
callbackFn.members.every((m) => m.kind === "function")) {
|
|
3507
|
+
return distributeOverUnion(callbackFn, (member) => {
|
|
3508
|
+
const r = evaluateArrayMethodValues(arr, method, [member, ...argVals.slice(1)]);
|
|
3509
|
+
return r === null ? T.unknown : (r as TypeValue);
|
|
3510
|
+
});
|
|
3511
|
+
}
|
|
3512
|
+
|
|
1251
3513
|
if (!callbackFn || callbackFn.kind !== "function") {
|
|
1252
3514
|
if (method === "map") return arr.kind === "tuple" ? T.tuple(arr.elements.map(() => T.unknown)) : T.array(T.unknown);
|
|
1253
3515
|
if (method === "filter") return arr.kind === "tuple" ? T.array(simplifyUnion(arr.elements)) : arr;
|
|
1254
3516
|
if (method === "find") return arr.kind === "tuple" ? simplifyUnion([...arr.elements, T.undefined]) : simplifyUnion([arr.element, T.undefined]);
|
|
1255
3517
|
if (method === "some" || method === "every") return T.boolean;
|
|
1256
|
-
if (method === "reduce") return
|
|
3518
|
+
if (method === "reduce") return argVals[1] ?? T.unknown;
|
|
1257
3519
|
if (method === "forEach") return T.undefined;
|
|
1258
3520
|
if (method === "flatMap") return T.array(T.unknown);
|
|
1259
3521
|
return null;
|
|
@@ -1261,23 +3523,36 @@ function evaluateArrayMethod(
|
|
|
1261
3523
|
|
|
1262
3524
|
const fn = callbackFn as TypeValue & { kind: "function" };
|
|
1263
3525
|
|
|
3526
|
+
// HOF:给回调元素挂 term 身份,使 Φ 中的约束可传播到 map/reduce 体
|
|
3527
|
+
const tagEl = (el: TypeValue, name: string): TypeValue => {
|
|
3528
|
+
const tagged = tagParamArg(el, name);
|
|
3529
|
+
copyOrigin(el, tagged);
|
|
3530
|
+
return tagged;
|
|
3531
|
+
};
|
|
3532
|
+
|
|
1264
3533
|
if (method === "map") {
|
|
1265
3534
|
if (arr.kind === "tuple") {
|
|
1266
3535
|
const mapped = arr.elements.map((el, i) =>
|
|
1267
|
-
callFunction(fn, [el, T.literal(i), arr]),
|
|
3536
|
+
callFunction(fn, [tagEl(el, fn.params[0] ?? "item"), T.literal(i), arr]),
|
|
1268
3537
|
);
|
|
1269
3538
|
return T.tuple(mapped);
|
|
1270
3539
|
}
|
|
1271
|
-
return T.array(
|
|
3540
|
+
return T.array(
|
|
3541
|
+
callFunction(fn, [tagEl(arr.element, fn.params[0] ?? "item"), T.number, arr]),
|
|
3542
|
+
);
|
|
1272
3543
|
}
|
|
1273
3544
|
|
|
1274
3545
|
if (method === "filter") {
|
|
1275
3546
|
if (arr.kind === "tuple") {
|
|
1276
3547
|
const kept: TypeValue[] = [];
|
|
1277
3548
|
for (let i = 0; i < arr.elements.length; i++) {
|
|
1278
|
-
const result = callFunction(fn, [
|
|
3549
|
+
const result = callFunction(fn, [
|
|
3550
|
+
tagEl(arr.elements[i]!, fn.params[0] ?? "item"),
|
|
3551
|
+
T.literal(i),
|
|
3552
|
+
arr,
|
|
3553
|
+
]);
|
|
1279
3554
|
if (result.kind === "literal" && !result.value) continue;
|
|
1280
|
-
kept.push(arr.elements[i]);
|
|
3555
|
+
kept.push(arr.elements[i]!);
|
|
1281
3556
|
}
|
|
1282
3557
|
if (kept.length === 0) return T.tuple([]);
|
|
1283
3558
|
return T.array(simplifyUnion(kept));
|
|
@@ -1286,17 +3561,35 @@ function evaluateArrayMethod(
|
|
|
1286
3561
|
}
|
|
1287
3562
|
|
|
1288
3563
|
if (method === "reduce") {
|
|
1289
|
-
const init =
|
|
3564
|
+
const init = argVals[1];
|
|
1290
3565
|
if (arr.kind === "tuple") {
|
|
1291
3566
|
let acc = init ?? arr.elements[0] ?? T.unknown;
|
|
1292
3567
|
const startIdx = init ? 0 : 1;
|
|
1293
3568
|
for (let i = startIdx; i < arr.elements.length; i++) {
|
|
1294
|
-
acc = callFunction(fn, [
|
|
3569
|
+
acc = callFunction(fn, [
|
|
3570
|
+
acc,
|
|
3571
|
+
tagEl(arr.elements[i]!, fn.params[1] ?? "item"),
|
|
3572
|
+
T.literal(i),
|
|
3573
|
+
arr,
|
|
3574
|
+
]);
|
|
1295
3575
|
}
|
|
1296
3576
|
return acc;
|
|
1297
3577
|
}
|
|
1298
|
-
|
|
1299
|
-
|
|
3578
|
+
// 抽象数组:不动点迭代(代数纪律)。unknown 不 join 进 acc。
|
|
3579
|
+
const acc0 = init ?? arr.element;
|
|
3580
|
+
let acc = acc0;
|
|
3581
|
+
for (let i = 0; i < 6; i++) {
|
|
3582
|
+
const next = callFunction(fn, [
|
|
3583
|
+
acc,
|
|
3584
|
+
tagEl(arr.element, fn.params[1] ?? "item"),
|
|
3585
|
+
T.number,
|
|
3586
|
+
arr,
|
|
3587
|
+
]);
|
|
3588
|
+
if (typeValueEquals(acc, next)) return next;
|
|
3589
|
+
if (next.kind === "unknown") return acc;
|
|
3590
|
+
acc = simplifyUnion([acc, next]);
|
|
3591
|
+
}
|
|
3592
|
+
return acc;
|
|
1300
3593
|
}
|
|
1301
3594
|
|
|
1302
3595
|
if (method === "find") {
|
|
@@ -1349,11 +3642,62 @@ function evaluateArrayMethod(
|
|
|
1349
3642
|
return null;
|
|
1350
3643
|
}
|
|
1351
3644
|
|
|
3645
|
+
// Iterable spread of Set/Map instances: exact side tables spread as
|
|
3646
|
+
// tuples ([...new Set([1,2])] → [1, 2]; Maps as [k, v] pairs), otherwise
|
|
3647
|
+
// the element typeArgs approximate like an array of that element type.
|
|
3648
|
+
// Oversized side tables degrade to the array approximation.
|
|
3649
|
+
const MAX_ITERABLE_SPREAD = 1000;
|
|
3650
|
+
function iterableSpreadValue(v: TypeValue): TypeValue | undefined {
|
|
3651
|
+
if (v.kind === "instance" && v.className === "Set") {
|
|
3652
|
+
const values = exactSetValues(v);
|
|
3653
|
+
if (values) {
|
|
3654
|
+
return values.length <= MAX_ITERABLE_SPREAD ? T.tuple([...values]) : setValuesIterable(v);
|
|
3655
|
+
}
|
|
3656
|
+
const t = (v as any)._typeArgs?.T;
|
|
3657
|
+
return t && t.kind !== "unknown" ? T.array(t) : undefined;
|
|
3658
|
+
}
|
|
3659
|
+
if (v.kind === "instance" && v.className === "Map") {
|
|
3660
|
+
const entries = exactMapEntries(v);
|
|
3661
|
+
if (entries && entries.length > MAX_ITERABLE_SPREAD) {
|
|
3662
|
+
const ta = (v as any)._typeArgs ?? {};
|
|
3663
|
+
return T.array(T.tuple([ta.K ?? T.unknown, ta.V ?? T.unknown]));
|
|
3664
|
+
}
|
|
3665
|
+
return mapEntriesIterable(v);
|
|
3666
|
+
}
|
|
3667
|
+
return undefined;
|
|
3668
|
+
}
|
|
3669
|
+
|
|
1352
3670
|
function evaluateForOf(
|
|
1353
3671
|
node: Node & { type: "ForOfStatement" },
|
|
1354
3672
|
iterable: TypeValue,
|
|
1355
3673
|
env: Environment,
|
|
1356
3674
|
): EvalResult {
|
|
3675
|
+
// A union iterable distributes: every member's elements flow through the
|
|
3676
|
+
// body in sequence (concatenated), not just the first member's. Branch
|
|
3677
|
+
// envs chain member-to-member exactly like element-to-element inside one
|
|
3678
|
+
// tuple iteration, so side effects on shared values (array push) and
|
|
3679
|
+
// outer-var reassignments accumulate across members. Non-iterable
|
|
3680
|
+
// members (number, …) fall through their own recursion harmlessly.
|
|
3681
|
+
if (iterable.kind === "union") {
|
|
3682
|
+
const returnValues: TypeValue[] = [];
|
|
3683
|
+
let currentEnv = env;
|
|
3684
|
+
for (const member of iterable.members) {
|
|
3685
|
+
const memberResult = evaluateForOf(node, member, currentEnv);
|
|
3686
|
+
if (isReturn(memberResult)) {
|
|
3687
|
+
returnValues.push(memberResult.value);
|
|
3688
|
+
return makeReturn(simplifyUnion(returnValues));
|
|
3689
|
+
}
|
|
3690
|
+
if (isBranch(memberResult)) {
|
|
3691
|
+
returnValues.push(memberResult.returnedValue);
|
|
3692
|
+
currentEnv = memberResult.fallthroughEnv;
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
if (returnValues.length > 0) {
|
|
3696
|
+
return makeBranch(simplifyUnion(returnValues), currentEnv);
|
|
3697
|
+
}
|
|
3698
|
+
return T.undefined;
|
|
3699
|
+
}
|
|
3700
|
+
|
|
1357
3701
|
if (iterable.kind === "tuple") {
|
|
1358
3702
|
const returnValues: TypeValue[] = [];
|
|
1359
3703
|
let currentEnv = env;
|
|
@@ -1369,6 +3713,7 @@ function evaluateForOf(
|
|
|
1369
3713
|
returnValues.push(result.returnedValue);
|
|
1370
3714
|
currentEnv = result.fallthroughEnv;
|
|
1371
3715
|
}
|
|
3716
|
+
if (loopSignalKind(result) === "break") break;
|
|
1372
3717
|
}
|
|
1373
3718
|
if (returnValues.length > 0) {
|
|
1374
3719
|
return makeBranch(simplifyUnion(returnValues), currentEnv);
|
|
@@ -1377,13 +3722,37 @@ function evaluateForOf(
|
|
|
1377
3722
|
}
|
|
1378
3723
|
|
|
1379
3724
|
if (iterable.kind === "array") {
|
|
1380
|
-
const loopEnv = env.
|
|
3725
|
+
const loopEnv = env.fork();
|
|
1381
3726
|
bindForLoopVar(node.left, iterable.element, loopEnv);
|
|
1382
3727
|
const result = evaluate(node.body, loopEnv);
|
|
1383
3728
|
if (isReturn(result)) return makeBranch(result.value, env);
|
|
1384
3729
|
return T.undefined;
|
|
1385
3730
|
}
|
|
1386
3731
|
|
|
3732
|
+
// Set instances iterate their exact value side table (`new Set([1,2])`
|
|
3733
|
+
// yields 1 then 2); no/partial table keeps the body unexecuted.
|
|
3734
|
+
if (iterable.kind === "instance" && iterable.className === "Set") {
|
|
3735
|
+
const values = exactSetValues(iterable);
|
|
3736
|
+
if (values && values.length > 0) {
|
|
3737
|
+
return evaluateForOf(node, T.tuple([...values]), env);
|
|
3738
|
+
}
|
|
3739
|
+
return T.undefined;
|
|
3740
|
+
}
|
|
3741
|
+
|
|
3742
|
+
// Map instances iterate their exact entry side table as [key, value]
|
|
3743
|
+
// tuples (`for (const [k, v] of map)`); same no/partial-table rule.
|
|
3744
|
+
if (iterable.kind === "instance" && iterable.className === "Map") {
|
|
3745
|
+
const entries = exactMapEntries(iterable);
|
|
3746
|
+
if (entries && entries.length > 0) {
|
|
3747
|
+
return evaluateForOf(
|
|
3748
|
+
node,
|
|
3749
|
+
T.tuple(entries.map((e) => T.tuple([e.key, e.value]))),
|
|
3750
|
+
env,
|
|
3751
|
+
);
|
|
3752
|
+
}
|
|
3753
|
+
return T.undefined;
|
|
3754
|
+
}
|
|
3755
|
+
|
|
1387
3756
|
return T.undefined;
|
|
1388
3757
|
}
|
|
1389
3758
|
|
|
@@ -1409,6 +3778,7 @@ function evaluateForIn(
|
|
|
1409
3778
|
returnValues.push(result.returnedValue);
|
|
1410
3779
|
currentEnv = result.fallthroughEnv;
|
|
1411
3780
|
}
|
|
3781
|
+
if (loopSignalKind(result) === "break") break;
|
|
1412
3782
|
}
|
|
1413
3783
|
if (returnValues.length > 0) {
|
|
1414
3784
|
return makeBranch(simplifyUnion(returnValues), currentEnv);
|
|
@@ -1417,7 +3787,7 @@ function evaluateForIn(
|
|
|
1417
3787
|
}
|
|
1418
3788
|
}
|
|
1419
3789
|
|
|
1420
|
-
const loopEnv = env.
|
|
3790
|
+
const loopEnv = env.fork();
|
|
1421
3791
|
bindForLoopVar(node.left, T.string, loopEnv);
|
|
1422
3792
|
evaluate(node.body, loopEnv);
|
|
1423
3793
|
return T.undefined;
|
|
@@ -1471,7 +3841,7 @@ function evaluateForStatement(
|
|
|
1471
3841
|
node: Node & { type: "ForStatement" },
|
|
1472
3842
|
env: Environment,
|
|
1473
3843
|
): EvalResult {
|
|
1474
|
-
const loopEnv = env.
|
|
3844
|
+
const loopEnv = env.fork();
|
|
1475
3845
|
|
|
1476
3846
|
if (node.init) {
|
|
1477
3847
|
const initResult = evaluate(node.init, loopEnv);
|
|
@@ -1482,6 +3852,22 @@ function evaluateForStatement(
|
|
|
1482
3852
|
const returnValues: TypeValue[] = [];
|
|
1483
3853
|
let concreteCompleted = false;
|
|
1484
3854
|
|
|
3855
|
+
// let/const 声明的循环变量每轮迭代获得新绑定(真实 JS 的 per-iteration
|
|
3856
|
+
// scope):body 内创建的闭包捕获当轮副本而非共享 cell,var 保持共享
|
|
3857
|
+
// cell(闭包看到终值)。迭代结束后把副本值拷回 loopEnv,test/update/
|
|
3858
|
+
// widening 依旧能看到 body 对循环变量的修改(`for (let i = 0; i < n;)
|
|
3859
|
+
// { ...; i++ }` 这类无 update 子句的循环靠它推进)。
|
|
3860
|
+
const perIteration =
|
|
3861
|
+
node.init?.type === "VariableDeclaration" && node.init.kind !== "var";
|
|
3862
|
+
const runBody = (scope: Environment): EvalResult => {
|
|
3863
|
+
if (!perIteration) return evaluate(node.body, scope);
|
|
3864
|
+
const iterEnv = scope.fork();
|
|
3865
|
+
for (const name of varNames) iterEnv.bind(name, scope.lookup(name));
|
|
3866
|
+
const result = evaluate(node.body, iterEnv);
|
|
3867
|
+
for (const name of varNames) scope.update(name, iterEnv.lookup(name));
|
|
3868
|
+
return result;
|
|
3869
|
+
};
|
|
3870
|
+
|
|
1485
3871
|
for (let i = 0; i < _maxConcreteIter; i++) {
|
|
1486
3872
|
if (node.test) {
|
|
1487
3873
|
const testVal = evaluate(node.test, loopEnv);
|
|
@@ -1490,7 +3876,7 @@ function evaluateForStatement(
|
|
|
1490
3876
|
if (testVal.kind !== "literal") break;
|
|
1491
3877
|
}
|
|
1492
3878
|
|
|
1493
|
-
const bodyResult =
|
|
3879
|
+
const bodyResult = runBody(loopEnv);
|
|
1494
3880
|
if (isReturn(bodyResult)) {
|
|
1495
3881
|
returnValues.push(bodyResult.value);
|
|
1496
3882
|
concreteCompleted = true;
|
|
@@ -1499,6 +3885,12 @@ function evaluateForStatement(
|
|
|
1499
3885
|
if (isBranch(bodyResult)) {
|
|
1500
3886
|
returnValues.push(bodyResult.returnedValue);
|
|
1501
3887
|
}
|
|
3888
|
+
// break 终止迭代(变量按 break 时的值流出循环);continue 落到
|
|
3889
|
+
// update 子句 —— 与真实 JS for 的 continue 语义一致。
|
|
3890
|
+
if (loopSignalKind(bodyResult) === "break") {
|
|
3891
|
+
concreteCompleted = true;
|
|
3892
|
+
break;
|
|
3893
|
+
}
|
|
1502
3894
|
|
|
1503
3895
|
if (node.update) {
|
|
1504
3896
|
const updateResult = evaluate(node.update, loopEnv);
|
|
@@ -1511,7 +3903,7 @@ function evaluateForStatement(
|
|
|
1511
3903
|
const prevSnap = snapshotVars(varNames, loopEnv);
|
|
1512
3904
|
|
|
1513
3905
|
for (let i = 0; i < 10; i++) {
|
|
1514
|
-
|
|
3906
|
+
runBody(loopEnv);
|
|
1515
3907
|
if (node.update) evaluate(node.update, loopEnv);
|
|
1516
3908
|
widenVars(varNames, loopEnv);
|
|
1517
3909
|
const currSnap = snapshotVars(varNames, loopEnv);
|
|
@@ -1550,6 +3942,7 @@ function evaluateWhileStatement(
|
|
|
1550
3942
|
if (isBranch(bodyResult)) {
|
|
1551
3943
|
returnValues.push(bodyResult.returnedValue);
|
|
1552
3944
|
}
|
|
3945
|
+
if (loopSignalKind(bodyResult) === "break") break;
|
|
1553
3946
|
}
|
|
1554
3947
|
|
|
1555
3948
|
if (returnValues.length > 0) {
|
|
@@ -1573,6 +3966,7 @@ function evaluateDoWhileStatement(
|
|
|
1573
3966
|
if (isBranch(bodyResult)) {
|
|
1574
3967
|
returnValues.push(bodyResult.returnedValue);
|
|
1575
3968
|
}
|
|
3969
|
+
if (loopSignalKind(bodyResult) === "break") break;
|
|
1576
3970
|
|
|
1577
3971
|
const tv = evaluate(node.test, env);
|
|
1578
3972
|
if (isReturn(tv) || isBranch(tv) || isThrow(tv)) return tv;
|
|
@@ -1586,23 +3980,293 @@ function evaluateDoWhileStatement(
|
|
|
1586
3980
|
return T.undefined;
|
|
1587
3981
|
}
|
|
1588
3982
|
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
3983
|
+
/** 诊断用的模块短名:UnknownRecord.name 会决定诊断 range 的列宽
|
|
3984
|
+
* (analyzer 按 name.length 折叠 end 列),完整路径会把列号撑爆。 */
|
|
3985
|
+
function moduleLabel(pathOrSpecifier: string): string {
|
|
3986
|
+
const base = pathOrSpecifier.replace(/^.*\//, "");
|
|
3987
|
+
return base || pathOrSpecifier;
|
|
3988
|
+
}
|
|
1592
3989
|
|
|
1593
|
-
|
|
1594
|
-
|
|
3990
|
+
/** 相对说明符的完整候选路径(evaluator 不引 node:path,手工拼接并归一化
|
|
3991
|
+
* "./" 与 ".." 段,保证诊断里是干净的绝对路径)。 */
|
|
3992
|
+
function attemptedFullPath(fromDir: string, specifier: string): string {
|
|
3993
|
+
if (specifier.startsWith("/")) return specifier;
|
|
3994
|
+
const base = specifier.startsWith("./") ? specifier.slice(2) : specifier;
|
|
3995
|
+
const joined = fromDir ? `${fromDir.replace(/\/+$/, "")}/${base}` : base;
|
|
3996
|
+
const segments: string[] = [];
|
|
3997
|
+
for (const seg of joined.split("/")) {
|
|
3998
|
+
if (seg === "" || seg === ".") continue;
|
|
3999
|
+
if (seg === "..") segments.pop();
|
|
4000
|
+
else segments.push(seg);
|
|
4001
|
+
}
|
|
4002
|
+
return (joined.startsWith("/") ? "/" : "") + segments.join("/");
|
|
4003
|
+
}
|
|
1595
4004
|
|
|
4005
|
+
/** Load (or fetch from cache) the environment for a resolved module file.
|
|
4006
|
+
* Shared by ESM import and CJS require — one loading path, one cache.
|
|
4007
|
+
* loc 为触发本次加载的 import/require 结点,用于守卫诊断定位。 */
|
|
4008
|
+
function loadModuleEnv(resolved: { ast: Node; filePath: string }, loc?: Node["loc"]): Environment {
|
|
4009
|
+
// 循环加载守卫——必须先于缓存查询:进行中的文件在求值前就已预置进
|
|
4010
|
+
// moduleCache,缓存命中分支区分不了「已加载完成」与「环形依赖回到自身」。
|
|
4011
|
+
// 命中环时返回进行中的部分环境(ESM 环形依赖语义,类型不炸),同时记录
|
|
4012
|
+
// 指明环链的诊断(a -> b -> a)。
|
|
4013
|
+
const cycleIndex = _moduleEvalStack.indexOf(resolved.filePath);
|
|
4014
|
+
if (cycleIndex !== -1) {
|
|
4015
|
+
const chain = [..._moduleEvalStack.slice(cycleIndex), resolved.filePath];
|
|
4016
|
+
recordUnknown({
|
|
4017
|
+
kind: "global",
|
|
4018
|
+
name: `module-cycle:${moduleLabel(resolved.filePath)}`,
|
|
4019
|
+
loc,
|
|
4020
|
+
reason: `Circular module load: ${chain.join(" -> ")} (bindings inside the cycle resolve to their partially evaluated types)`,
|
|
4021
|
+
});
|
|
4022
|
+
return moduleCache.get(resolved.filePath) ?? freshStubModuleEnv(resolved);
|
|
4023
|
+
}
|
|
1596
4024
|
let moduleEnv = moduleCache.get(resolved.filePath);
|
|
1597
4025
|
if (!moduleEnv) {
|
|
4026
|
+
// 深度上限:每个真正新载入的文件占一帧,超过 MAX_MODULE_LOAD_DEPTH
|
|
4027
|
+
// 即截断为空环境(导出全为 unknown),报错指明链深与完整链。
|
|
4028
|
+
if (_moduleEvalStack.length >= MAX_MODULE_LOAD_DEPTH) {
|
|
4029
|
+
const chain = [..._moduleEvalStack, resolved.filePath];
|
|
4030
|
+
recordUnknown({
|
|
4031
|
+
kind: "global",
|
|
4032
|
+
name: `module-depth:${moduleLabel(resolved.filePath)}`,
|
|
4033
|
+
loc,
|
|
4034
|
+
reason: `Module load chain too deep (depth ${chain.length} > ${MAX_MODULE_LOAD_DEPTH} max): ${chain.join(" -> ")} (loading truncated, deeper modules typed as unknown)`,
|
|
4035
|
+
});
|
|
4036
|
+
return freshStubModuleEnv(resolved);
|
|
4037
|
+
}
|
|
1598
4038
|
moduleEnv = createEnvironment();
|
|
1599
4039
|
moduleCache.set(resolved.filePath, moduleEnv);
|
|
1600
4040
|
const savedDir = currentFileDir;
|
|
1601
4041
|
currentFileDir = resolved.filePath.replace(/\/[^/]+$/, "");
|
|
1602
|
-
|
|
4042
|
+
_moduleEvalStack.push(resolved.filePath);
|
|
4043
|
+
try {
|
|
4044
|
+
evaluateProgram(resolved.ast, moduleEnv);
|
|
4045
|
+
} finally {
|
|
4046
|
+
_moduleEvalStack.pop();
|
|
4047
|
+
}
|
|
1603
4048
|
currentFileDir = savedDir;
|
|
4049
|
+
tagModuleExports(moduleEnv, resolved.filePath);
|
|
4050
|
+
tagCommonJsExports(moduleEnv, resolved.filePath);
|
|
4051
|
+
}
|
|
4052
|
+
return moduleEnv;
|
|
4053
|
+
}
|
|
4054
|
+
|
|
4055
|
+
/** 截断/环回退用的空环境:入缓存保证后续请求拿到同一份「全 unknown」结果。 */
|
|
4056
|
+
function freshStubModuleEnv(resolved: { ast: Node; filePath: string }): Environment {
|
|
4057
|
+
const stub = createEnvironment();
|
|
4058
|
+
moduleCache.set(resolved.filePath, stub);
|
|
4059
|
+
return stub;
|
|
4060
|
+
}
|
|
4061
|
+
|
|
4062
|
+
/** Build an exports namespace object from a module's `__export_` bindings
|
|
4063
|
+
* (ESM exports required from CJS). Null when the module exports nothing. */
|
|
4064
|
+
function namespaceFromExportBindings(env: Environment): TypeValue | null {
|
|
4065
|
+
const exports: Record<string, TypeValue> = {};
|
|
4066
|
+
const bindings = env.getOwnBindings();
|
|
4067
|
+
for (const [k, v] of Object.entries(bindings)) {
|
|
4068
|
+
if (!k.startsWith("__export_")) continue;
|
|
4069
|
+
exports[k === "__export_default" ? "default" : k.slice("__export_".length)] = v;
|
|
4070
|
+
}
|
|
4071
|
+
if (Object.keys(exports).length === 0) return null;
|
|
4072
|
+
return T.object(exports);
|
|
4073
|
+
}
|
|
4074
|
+
|
|
4075
|
+
/** The value of require() for an evaluated module: `module.exports` when the
|
|
4076
|
+
* target assigned it, the (possibly `exports.x`-mutated) exports object
|
|
4077
|
+
* otherwise, and the ESM namespace when the target is an ES module. */
|
|
4078
|
+
function commonJsExportsValue(moduleEnv: Environment): TypeValue {
|
|
4079
|
+
if (moduleEnv.has("module")) {
|
|
4080
|
+
const moduleVal = moduleEnv.lookup("module");
|
|
4081
|
+
if (moduleVal.kind === "object") {
|
|
4082
|
+
const exp = moduleVal.properties["exports"];
|
|
4083
|
+
if (exp) {
|
|
4084
|
+
if (
|
|
4085
|
+
exp.kind === "object" &&
|
|
4086
|
+
(exp as any).__cjsExportsRoot === true &&
|
|
4087
|
+
Object.keys(exp.properties).length === 0
|
|
4088
|
+
) {
|
|
4089
|
+
// Target never wrote CJS exports — surface ESM exports instead.
|
|
4090
|
+
return namespaceFromExportBindings(moduleEnv) ?? exp;
|
|
4091
|
+
}
|
|
4092
|
+
return exp;
|
|
4093
|
+
}
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
4096
|
+
return T.unknown;
|
|
4097
|
+
}
|
|
4098
|
+
|
|
4099
|
+
|
|
4100
|
+
function evaluateRequireCall(node: Node & { type: "CallExpression" }, _env: Environment): EvalResult { const arg = (node.arguments as Node[])[0];
|
|
4101
|
+
if (!arg || arg.type !== "StringLiteral") {
|
|
4102
|
+
recordUnknown({
|
|
4103
|
+
kind: "global",
|
|
4104
|
+
name: "require",
|
|
4105
|
+
loc: node.loc,
|
|
4106
|
+
reason: "require() with non-literal specifier",
|
|
4107
|
+
});
|
|
4108
|
+
return T.unknown;
|
|
4109
|
+
}
|
|
4110
|
+
const specifier = arg.value as string;
|
|
4111
|
+
// Bare specifiers (npm packages) are not resolved in v1.
|
|
4112
|
+
if (!specifier.startsWith(".") && !specifier.startsWith("/")) {
|
|
4113
|
+
recordUnknown({
|
|
4114
|
+
kind: "global",
|
|
4115
|
+
name: `require('${specifier}')`,
|
|
4116
|
+
loc: node.loc,
|
|
4117
|
+
reason: `bare module specifier '${specifier}' is not resolved (npm packages unsupported)`,
|
|
4118
|
+
});
|
|
4119
|
+
return T.unknown;
|
|
4120
|
+
}
|
|
4121
|
+
if (!currentModuleResolver) return T.unknown;
|
|
4122
|
+
const resolved = currentModuleResolver(specifier, currentFileDir);
|
|
4123
|
+
if (!resolved) {
|
|
4124
|
+
// 缺失文件:报错带完整候选路径(module-missing 前缀走 analyzer 专属
|
|
4125
|
+
// 文案通道,reason 不会被通用 unknown-global 文案吞掉)。
|
|
4126
|
+
recordUnknown({
|
|
4127
|
+
kind: "global",
|
|
4128
|
+
name: `module-missing:require('${specifier}')`,
|
|
4129
|
+
loc: node.loc,
|
|
4130
|
+
reason: `Cannot resolve require('${specifier}'): no file at ${attemptedFullPath(currentFileDir, specifier)} (with or without .js/.ts/.mjs extension)`,
|
|
4131
|
+
});
|
|
4132
|
+
return T.unknown;
|
|
4133
|
+
}
|
|
4134
|
+
// .json 模块(如 require('../package.json')):JSON 值直接映射为字面量类型
|
|
4135
|
+
if ("json" in resolved && resolved.json !== undefined) {
|
|
4136
|
+
return jsonToTypeValue(resolved.json);
|
|
4137
|
+
}
|
|
4138
|
+
return commonJsExportsValue(loadModuleEnv(resolved, node.loc));
|
|
4139
|
+
}
|
|
4140
|
+
|
|
4141
|
+
function evaluateImportDeclaration(node: Node & { type: "ImportDeclaration" }, env: Environment): EvalResult {
|
|
4142
|
+
const source = node.source.value;
|
|
4143
|
+
|
|
4144
|
+
const mockModule = mockModules.get(source);
|
|
4145
|
+
if (mockModule && currentModuleResolver) {
|
|
4146
|
+
const mockResolved = currentModuleResolver(mockModule.fromPath, currentFileDir);
|
|
4147
|
+
if (mockResolved) {
|
|
4148
|
+
const mockEnv = loadModuleEnv(mockResolved, node.loc);
|
|
4149
|
+
|
|
4150
|
+
if (mockModule.names) {
|
|
4151
|
+
const originalResolved = currentModuleResolver(source, currentFileDir);
|
|
4152
|
+
let originalEnv: Environment | undefined;
|
|
4153
|
+
if (originalResolved) {
|
|
4154
|
+
originalEnv = loadModuleEnv(originalResolved, node.loc);
|
|
4155
|
+
}
|
|
4156
|
+
|
|
4157
|
+
for (const spec of node.specifiers) {
|
|
4158
|
+
if (spec.type === "ImportSpecifier") {
|
|
4159
|
+
const importedName = spec.imported.type === "Identifier" ? spec.imported.name : null;
|
|
4160
|
+
if (importedName && mockModule.names.includes(importedName)) {
|
|
4161
|
+
const val = mockEnv.has(`__export_${importedName}`) ? mockEnv.lookup(`__export_${importedName}`) : T.unknown;
|
|
4162
|
+
env.bind(spec.local.name, val);
|
|
4163
|
+
} else if (importedName && originalEnv) {
|
|
4164
|
+
const val = originalEnv.has(`__export_${importedName}`) ? originalEnv.lookup(`__export_${importedName}`) : T.unknown;
|
|
4165
|
+
env.bind(spec.local.name, val);
|
|
4166
|
+
}
|
|
4167
|
+
} else if (spec.type === "ImportDefaultSpecifier") {
|
|
4168
|
+
const sourceEnv = originalEnv ?? mockEnv;
|
|
4169
|
+
const val = sourceEnv.has(`__export_default`) ? sourceEnv.lookup(`__export_default`) : T.unknown;
|
|
4170
|
+
env.bind(spec.local.name, val);
|
|
4171
|
+
} else if (spec.type === "ImportNamespaceSpecifier") {
|
|
4172
|
+
const exports: Record<string, TypeValue> = {};
|
|
4173
|
+
const sourceEnv = originalEnv ?? mockEnv;
|
|
4174
|
+
const bindings = sourceEnv.getOwnBindings();
|
|
4175
|
+
for (const [k, v] of Object.entries(bindings)) {
|
|
4176
|
+
if (k.startsWith("__export_") && k !== "__export_default") {
|
|
4177
|
+
const name = k.slice("__export_".length);
|
|
4178
|
+
exports[name] = mockModule.names.includes(name)
|
|
4179
|
+
? (mockEnv.has(k) ? mockEnv.lookup(k) : v)
|
|
4180
|
+
: v;
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
const mockBindings = mockEnv.getOwnBindings();
|
|
4184
|
+
for (const [k, v] of Object.entries(mockBindings)) {
|
|
4185
|
+
if (k.startsWith("__export_") && k !== "__export_default") {
|
|
4186
|
+
const name = k.slice("__export_".length);
|
|
4187
|
+
if (mockModule.names.includes(name)) {
|
|
4188
|
+
exports[name] = v;
|
|
4189
|
+
}
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
env.bind(spec.local.name, T.object(exports));
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
return T.undefined;
|
|
4196
|
+
}
|
|
4197
|
+
|
|
4198
|
+
for (const spec of node.specifiers) {
|
|
4199
|
+
if (spec.type === "ImportDefaultSpecifier") {
|
|
4200
|
+
const val = mockEnv.has(`__export_default`) ? mockEnv.lookup(`__export_default`) : T.unknown;
|
|
4201
|
+
env.bind(spec.local.name, val);
|
|
4202
|
+
} else if (spec.type === "ImportSpecifier") {
|
|
4203
|
+
const importedName = spec.imported.type === "Identifier" ? spec.imported.name : null;
|
|
4204
|
+
if (importedName) {
|
|
4205
|
+
const val = mockEnv.has(`__export_${importedName}`) ? mockEnv.lookup(`__export_${importedName}`) : T.unknown;
|
|
4206
|
+
env.bind(spec.local.name, val);
|
|
4207
|
+
}
|
|
4208
|
+
} else if (spec.type === "ImportNamespaceSpecifier") {
|
|
4209
|
+
const exports: Record<string, TypeValue> = {};
|
|
4210
|
+
const bindings = mockEnv.getOwnBindings();
|
|
4211
|
+
for (const [k, v] of Object.entries(bindings)) {
|
|
4212
|
+
if (k.startsWith("__export_") && k !== "__export_default") {
|
|
4213
|
+
exports[k.slice("__export_".length)] = v;
|
|
4214
|
+
}
|
|
4215
|
+
}
|
|
4216
|
+
env.bind(spec.local.name, T.object(exports));
|
|
4217
|
+
}
|
|
4218
|
+
}
|
|
4219
|
+
return T.undefined;
|
|
4220
|
+
}
|
|
4221
|
+
// mock 文件本身解析失败:此前静默回落到原模块(mock 被无声忽略)。
|
|
4222
|
+
// 现在记录带完整候选路径的诊断后仍回落,分析不中断。
|
|
4223
|
+
recordUnknown({
|
|
4224
|
+
kind: "global",
|
|
4225
|
+
name: `module-missing:${moduleLabel(mockModule.fromPath)}`,
|
|
4226
|
+
loc: node.loc,
|
|
4227
|
+
reason: `@nudo:mock-module target for "${source}" not found: no file at ${attemptedFullPath(currentFileDir, mockModule.fromPath)} (with or without .js/.ts/.mjs extension); falling back to the original module`,
|
|
4228
|
+
});
|
|
4229
|
+
}
|
|
4230
|
+
|
|
4231
|
+
const envModule = envModules[source];
|
|
4232
|
+
if (envModule) {
|
|
4233
|
+
for (const spec of node.specifiers) {
|
|
4234
|
+
if (spec.type === "ImportDefaultSpecifier") {
|
|
4235
|
+
const defaultExport = envModule["default"];
|
|
4236
|
+
env.bind(spec.local.name, defaultExport ?? T.unknown);
|
|
4237
|
+
} else if (spec.type === "ImportSpecifier") {
|
|
4238
|
+
const importedName = spec.imported.type === "Identifier" ? spec.imported.name : null;
|
|
4239
|
+
if (importedName) {
|
|
4240
|
+
env.bind(spec.local.name, envModule[importedName] ?? T.unknown);
|
|
4241
|
+
}
|
|
4242
|
+
} else if (spec.type === "ImportNamespaceSpecifier") {
|
|
4243
|
+
const { default: _default, ...rest } = envModule;
|
|
4244
|
+
env.bind(spec.local.name, T.object(rest));
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
return T.undefined;
|
|
4248
|
+
}
|
|
4249
|
+
|
|
4250
|
+
if (!currentModuleResolver) return T.undefined;
|
|
4251
|
+
|
|
4252
|
+
const resolved = currentModuleResolver(source, currentFileDir);
|
|
4253
|
+
if (!resolved) {
|
|
4254
|
+
// 相对/绝对说明符解析失败:此前完全静默(绑定凭空缺失,只剩下游
|
|
4255
|
+
// 「unknown global」噪音)。缺失文件报错带完整候选路径;裸说明符
|
|
4256
|
+
// (npm 包)保持静默,与 require() 的 bare 分支口径一致。
|
|
4257
|
+
if (source.startsWith(".") || source.startsWith("/")) {
|
|
4258
|
+
recordUnknown({
|
|
4259
|
+
kind: "global",
|
|
4260
|
+
name: `module-missing:${moduleLabel(source)}`,
|
|
4261
|
+
loc: node.loc,
|
|
4262
|
+
reason: `Cannot resolve import '${source}': no file at ${attemptedFullPath(currentFileDir, source)} (with or without .js/.ts/.mjs extension)`,
|
|
4263
|
+
});
|
|
4264
|
+
}
|
|
4265
|
+
return T.undefined;
|
|
1604
4266
|
}
|
|
1605
4267
|
|
|
4268
|
+
const moduleEnv = loadModuleEnv(resolved, node.loc);
|
|
4269
|
+
|
|
1606
4270
|
for (const spec of node.specifiers) {
|
|
1607
4271
|
if (spec.type === "ImportDefaultSpecifier") {
|
|
1608
4272
|
const val = moduleEnv.has(`__export_default`) ? moduleEnv.lookup(`__export_default`) : T.unknown;
|
|
@@ -1643,7 +4307,9 @@ function evaluateClassDeclaration(node: Node & { type: "ClassDeclaration" }, env
|
|
|
1643
4307
|
);
|
|
1644
4308
|
const fnType = T.fn(paramNames, member.body, env) as TypeValue & { kind: "function" };
|
|
1645
4309
|
(fnType as any)._paramPatterns = member.params;
|
|
4310
|
+
(fnType as any)._name = `${className}.${methodName}`;
|
|
1646
4311
|
if (member.async) (fnType as any)._async = true;
|
|
4312
|
+
tagFnModule(fnType);
|
|
1647
4313
|
|
|
1648
4314
|
if (member.kind === "constructor") {
|
|
1649
4315
|
constructorFn = fnType;
|
|
@@ -1671,26 +4337,320 @@ function evaluateInstanceof(left: TypeValue, _right: TypeValue, rightNode: Node,
|
|
|
1671
4337
|
isSubtypeOf(lv, T.instanceOf(className));
|
|
1672
4338
|
return T.literal(matches);
|
|
1673
4339
|
}
|
|
4340
|
+
// Structurally-known receivers decide `x instanceof C` literally:
|
|
4341
|
+
// plain objects are never Date/Buffer/..., arrays are Array/Object, ...
|
|
4342
|
+
// (hoek getInternalProto's instanceof chain keys off this precision).
|
|
4343
|
+
const structural = builtinInstanceTest(lv, className);
|
|
4344
|
+
if (structural !== undefined) return T.literal(structural);
|
|
1674
4345
|
return T.boolean;
|
|
1675
4346
|
});
|
|
1676
4347
|
}
|
|
1677
4348
|
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
4349
|
+
// --- new Promise(executor) ---
|
|
4350
|
+
|
|
4351
|
+
// Budgets for the static resolve-site scan: resolve calls hiding inside
|
|
4352
|
+
// never-executed callbacks are invisible to the executor run, so the
|
|
4353
|
+
// executor AST is walked for them. Bounded to keep large executors from
|
|
4354
|
+
// ballooning analysis time.
|
|
4355
|
+
const PROMISE_SCAN_MAX_SITES = 16;
|
|
4356
|
+
const PROMISE_SCAN_MAX_NODES = 2000;
|
|
4357
|
+
|
|
4358
|
+
// Synthetic resolve/reject collectors handed to executors (see
|
|
4359
|
+
// makePromiseCollector). Calls to them are internal plumbing; recordCall
|
|
4360
|
+
// consults this set to avoid emitting them as user call sites.
|
|
4361
|
+
const _promiseCollectors = new WeakSet<object>();
|
|
4362
|
+
|
|
4363
|
+
function makePromiseCollector(sink: TypeValue[]): TypeValue {
|
|
4364
|
+
const collector = T.fnSig([T.unknown], T.undefined, T.never, (args) => {
|
|
4365
|
+
sink.push(args[0] ?? T.undefined);
|
|
4366
|
+
return T.undefined;
|
|
4367
|
+
});
|
|
4368
|
+
_promiseCollectors.add(collector as unknown as object);
|
|
4369
|
+
return collector;
|
|
4370
|
+
}
|
|
4371
|
+
|
|
4372
|
+
// Resolve sites hiding in never-executed callbacks (`setTimeout(() =>
|
|
4373
|
+
// resolve(x), 1)`, `emitter.on('end', () => resolve(x))`) never fire during
|
|
4374
|
+
// the executor run; walk the executor subtree for `resolve(...)` calls —
|
|
4375
|
+
// matched by the executor's actual first-parameter name — and collect each
|
|
4376
|
+
// site's first argument node. Sites with spread/no arguments are skipped.
|
|
4377
|
+
function findResolveArgSites(root: Node, resolveName: string): Node[] {
|
|
4378
|
+
const sites: Node[] = [];
|
|
4379
|
+
let visited = 0;
|
|
4380
|
+
const stack: Node[] = [root];
|
|
4381
|
+
while (stack.length > 0 && sites.length < PROMISE_SCAN_MAX_SITES) {
|
|
4382
|
+
const n = stack.pop()!;
|
|
4383
|
+
if (++visited > PROMISE_SCAN_MAX_NODES) break;
|
|
4384
|
+
if (n.type === "CallExpression" || n.type === "OptionalCallExpression") {
|
|
4385
|
+
const c = (n as any).callee as Node | undefined;
|
|
4386
|
+
if (c?.type === "Identifier" && c.name === resolveName) {
|
|
4387
|
+
const first = (n as any).arguments?.[0] as Node | undefined;
|
|
4388
|
+
if (first && first.type !== "SpreadElement") sites.push(first);
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
for (const key of Object.keys(n)) {
|
|
4392
|
+
if (key === "loc" || key === "leadingComments" || key === "trailingComments" || key === "innerComments") continue;
|
|
4393
|
+
const v = (n as any)[key];
|
|
4394
|
+
if (Array.isArray(v)) {
|
|
4395
|
+
for (const el of v) {
|
|
4396
|
+
if (el && typeof el === "object" && typeof (el as any).type === "string") stack.push(el as Node);
|
|
4397
|
+
}
|
|
4398
|
+
} else if (v && typeof v === "object" && typeof v.type === "string") {
|
|
4399
|
+
stack.push(v as Node);
|
|
4400
|
+
}
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
return sites;
|
|
4404
|
+
}
|
|
4405
|
+
|
|
4406
|
+
// Returns the promise's fulfillment type. The executor call environment is
|
|
4407
|
+
// built here (not buried in callFunctionUnchecked) so the static scan can
|
|
4408
|
+
// evaluate resolve-site arguments in the post-execution executor scope,
|
|
4409
|
+
// where executor locals and the closure chain (outer params, e.g. hoek's
|
|
4410
|
+
// wait(timeout, returnValue)) are both visible.
|
|
4411
|
+
function evaluatePromiseExecutor(executor: TypeValue & { kind: "function" }): TypeValue {
|
|
4412
|
+
const resolveCollected: TypeValue[] = [];
|
|
4413
|
+
// reject is collected too (complete executor semantics) but only resolve
|
|
4414
|
+
// determines the fulfillment type.
|
|
4415
|
+
const rejectCollected: TypeValue[] = [];
|
|
4416
|
+
const callEnv = executor.closure.extend({});
|
|
4417
|
+
bindFunctionParams(executor, [makePromiseCollector(resolveCollected), makePromiseCollector(rejectCollected)], callEnv);
|
|
4418
|
+
|
|
4419
|
+
// try/finally 语义下 resolve 位点(位于 try 内)先于 finally 执行,但
|
|
4420
|
+
// 这里是先执行完整个 body 再扫描——finally 的副作用(如 finally
|
|
4421
|
+
// { parser = null })会毒化扫描求值(parser.finish() 变 no-method on
|
|
4422
|
+
// null)。快照执行前的绑定链;扫描时当前值被 null/undefined 化而快照
|
|
4423
|
+
// 值非空的绑定回退快照值(finally-nullification 的签名)。循环累加
|
|
4424
|
+
// (当前值更丰富)不受影响。
|
|
4425
|
+
const preBodySnapshot = callEnv.snapshot();
|
|
4426
|
+
|
|
4427
|
+
// Execute the executor body synchronously; direct resolve/reject calls
|
|
4428
|
+
// hit the collectors through normal parameter binding.
|
|
4429
|
+
if (_callDepth < MAX_CALL_DEPTH && _totalCalls < MAX_TOTAL_CALLS) {
|
|
4430
|
+
_callDepth++;
|
|
4431
|
+
_totalCalls++;
|
|
4432
|
+
const savedUnreachable = _unreachableRanges;
|
|
4433
|
+
_unreachableRanges = [];
|
|
4434
|
+
try {
|
|
4435
|
+
evaluate(executor.body, callEnv);
|
|
4436
|
+
} finally {
|
|
4437
|
+
_unreachableRanges = savedUnreachable;
|
|
4438
|
+
_callDepth--;
|
|
4439
|
+
}
|
|
4440
|
+
}
|
|
4441
|
+
|
|
4442
|
+
const isNullish = (v: TypeValue): boolean =>
|
|
4443
|
+
v.kind === "literal" && (v.value === null || v.value === undefined);
|
|
4444
|
+
const scanEnv: Environment = {
|
|
4445
|
+
lookup: (name: string) => {
|
|
4446
|
+
const cur = callEnv.lookup(name);
|
|
4447
|
+
if (isNullish(cur)) {
|
|
4448
|
+
const snap = preBodySnapshot.lookup(name);
|
|
4449
|
+
if (!isNullish(snap) && snap !== T.undefined) return snap;
|
|
4450
|
+
}
|
|
4451
|
+
return cur;
|
|
4452
|
+
},
|
|
4453
|
+
bind: (name, value) => callEnv.bind(name, value),
|
|
4454
|
+
update: (name, value) => callEnv.update(name, value),
|
|
4455
|
+
extend: (b) => callEnv.extend(b),
|
|
4456
|
+
fork: () => callEnv.fork(),
|
|
4457
|
+
has: (name) => callEnv.has(name),
|
|
4458
|
+
snapshot: () => callEnv.snapshot(),
|
|
4459
|
+
getOwnBindings: () => callEnv.getOwnBindings(),
|
|
4460
|
+
};
|
|
4461
|
+
|
|
4462
|
+
const firstPattern = ((executor as any)._paramPatterns as Node[] | undefined)?.[0];
|
|
4463
|
+
if (firstPattern?.type === "Identifier") {
|
|
4464
|
+
for (const argExpr of findResolveArgSites(executor.body, firstPattern.name)) {
|
|
4465
|
+
const argVal = evaluate(argExpr, scanEnv);
|
|
4466
|
+
if (isReturn(argVal) || isBranch(argVal) || isThrow(argVal)) continue;
|
|
4467
|
+
resolveCollected.push(argVal);
|
|
4468
|
+
}
|
|
4469
|
+
}
|
|
4470
|
+
|
|
4471
|
+
// No fulfillment value observed → never-resolving promise (hoek block()'s
|
|
4472
|
+
// `new Promise(Ignore)`); T.union flattens/dedups the collected values.
|
|
4473
|
+
return T.promise(T.union(...resolveCollected));
|
|
4474
|
+
}
|
|
4475
|
+
|
|
4476
|
+
// .then(cb): run the callback on the resolved value through the main call
|
|
4477
|
+
// path so the callback's return type flows into the chained promise; a
|
|
4478
|
+
// callback returning a promise flattens (runtime then-semantics). Returns
|
|
4479
|
+
// null when not applicable (non-then method, non-callable handler).
|
|
4480
|
+
function applyPromiseThenCallback(
|
|
4481
|
+
promiseValue: TypeValue & { kind: "promise" },
|
|
4482
|
+
argVals: TypeValue[],
|
|
4483
|
+
): TypeValue | null {
|
|
4484
|
+
const cb = argVals[0];
|
|
4485
|
+
if (!cb || cb.kind !== "function") return null;
|
|
4486
|
+
const result = callFunctionFull(cb, [promiseValue.value]).value;
|
|
4487
|
+
return result.kind === "promise" ? result : T.promise(result);
|
|
4488
|
+
}
|
|
4489
|
+
|
|
4490
|
+
// Bind call arguments to a function value's parameters (rest parameters
|
|
4491
|
+
// collect the remaining arguments into a tuple; patterns delegate to
|
|
4492
|
+
// bindPattern). Shared by the direct call path, class construction, and
|
|
4493
|
+
// the Promise executor run.
|
|
4494
|
+
function bindFunctionParams(
|
|
4495
|
+
fn: TypeValue & { kind: "function" },
|
|
4496
|
+
args: TypeValue[],
|
|
4497
|
+
callEnv: Environment,
|
|
4498
|
+
): void {
|
|
4499
|
+
const paramPatterns = (fn as any)._paramPatterns as Node[] | undefined;
|
|
4500
|
+
for (let i = 0; i < fn.params.length; i++) {
|
|
4501
|
+
const paramName = fn.params[i];
|
|
4502
|
+
// Check if this is a rest parameter (starts with ...)
|
|
4503
|
+
if (paramName.startsWith("...")) {
|
|
4504
|
+
const restValue = T.tuple(args.slice(i));
|
|
4505
|
+
if (paramPatterns && paramPatterns[i]) {
|
|
4506
|
+
bindPattern(paramPatterns[i], restValue, callEnv);
|
|
4507
|
+
} else {
|
|
4508
|
+
callEnv.bind(paramName.slice(3), restValue); // Remove "..." prefix
|
|
4509
|
+
}
|
|
4510
|
+
} else {
|
|
4511
|
+
let argVal = args[i] ?? T.undefined;
|
|
4512
|
+
{
|
|
4513
|
+
const tagged = tagParamArg(argVal, paramName);
|
|
4514
|
+
copyOrigin(argVal, tagged);
|
|
4515
|
+
argVal = tagged;
|
|
4516
|
+
}
|
|
4517
|
+
if (paramPatterns && paramPatterns[i]) {
|
|
4518
|
+
bindPattern(paramPatterns[i], argVal, callEnv);
|
|
4519
|
+
} else {
|
|
4520
|
+
callEnv.bind(paramName, argVal);
|
|
4521
|
+
}
|
|
4522
|
+
}
|
|
4523
|
+
}
|
|
4524
|
+
// 非箭头函数的 arguments 对象:tuple(实参) —— length/索引/迭代/展开
|
|
4525
|
+
// 经由既有 tuple 机制直接可用。箭头函数不绑(词法作用域回退外层,
|
|
4526
|
+
// 真实 JS 语义);_hasArguments 在函数值创建位点按节点类型标记。
|
|
4527
|
+
if ((fn as any)._hasArguments) {
|
|
4528
|
+
callEnv.bind("arguments", T.tuple(args));
|
|
4529
|
+
}
|
|
4530
|
+
}
|
|
1681
4531
|
|
|
1682
4532
|
function evaluateNewExpression(node: Node & { type: "NewExpression" }, env: Environment): EvalResult {
|
|
1683
4533
|
const callee = node.callee as Node;
|
|
1684
|
-
if (callee.type === "Identifier" && BUILTIN_ERROR_CLASSES.has(callee.name)) {
|
|
4534
|
+
if (callee.type === "Identifier" && BUILTIN_ERROR_CLASSES.has(callee.name) && !env.has(callee.name)) {
|
|
1685
4535
|
const argVals = evaluateArgs(node.arguments as Node[], env);
|
|
1686
4536
|
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
1687
4537
|
const msgVal = (argVals as TypeValue[])[0] ?? T.undefined;
|
|
1688
4538
|
return T.instanceOf(callee.name, { message: msgVal });
|
|
1689
4539
|
}
|
|
1690
4540
|
|
|
4541
|
+
// Handle new Map()
|
|
4542
|
+
if (callee.type === "Identifier" && callee.name === "Map" && !env.has("Map")) {
|
|
4543
|
+
const argVals = evaluateArgs(node.arguments as Node[], env);
|
|
4544
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
4545
|
+
return createMapType(argVals as TypeValue[]);
|
|
4546
|
+
}
|
|
4547
|
+
|
|
4548
|
+
// Handle new Set()
|
|
4549
|
+
if (callee.type === "Identifier" && callee.name === "Set" && !env.has("Set")) {
|
|
4550
|
+
const argVals = evaluateArgs(node.arguments as Node[], env);
|
|
4551
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
4552
|
+
return createSetType(argVals as TypeValue[]);
|
|
4553
|
+
}
|
|
4554
|
+
|
|
4555
|
+
// Handle new RegExp()
|
|
4556
|
+
if (callee.type === "Identifier" && callee.name === "RegExp" && !env.has("RegExp")) {
|
|
4557
|
+
return createRegExpType();
|
|
4558
|
+
}
|
|
4559
|
+
|
|
4560
|
+
// Handle new URL()
|
|
4561
|
+
if (callee.type === "Identifier" && callee.name === "URL" && !env.has("URL")) {
|
|
4562
|
+
return createURLType();
|
|
4563
|
+
}
|
|
4564
|
+
|
|
4565
|
+
// Handle new URLSearchParams()
|
|
4566
|
+
if (callee.type === "Identifier" && callee.name === "URLSearchParams" && !env.has("URLSearchParams")) {
|
|
4567
|
+
return createURLSearchParamsType();
|
|
4568
|
+
}
|
|
4569
|
+
|
|
4570
|
+
// Handle new Response()
|
|
4571
|
+
if (callee.type === "Identifier" && callee.name === "Response" && !env.has("Response")) {
|
|
4572
|
+
return createResponseType();
|
|
4573
|
+
}
|
|
4574
|
+
|
|
4575
|
+
// Handle new Headers()
|
|
4576
|
+
if (callee.type === "Identifier" && callee.name === "Headers" && !env.has("Headers")) {
|
|
4577
|
+
return createHeadersType();
|
|
4578
|
+
}
|
|
4579
|
+
|
|
4580
|
+
// Handle new FormData()
|
|
4581
|
+
if (callee.type === "Identifier" && callee.name === "FormData" && !env.has("FormData")) {
|
|
4582
|
+
return createFormDataType();
|
|
4583
|
+
}
|
|
4584
|
+
|
|
4585
|
+
// Handle new AbortController()
|
|
4586
|
+
if (callee.type === "Identifier" && callee.name === "AbortController" && !env.has("AbortController")) {
|
|
4587
|
+
return createAbortControllerType();
|
|
4588
|
+
}
|
|
4589
|
+
|
|
4590
|
+
// Handle new WeakMap()
|
|
4591
|
+
if (callee.type === "Identifier" && callee.name === "WeakMap" && !env.has("WeakMap")) {
|
|
4592
|
+
return createWeakMapType();
|
|
4593
|
+
}
|
|
4594
|
+
|
|
4595
|
+
// Handle new WeakSet()
|
|
4596
|
+
if (callee.type === "Identifier" && callee.name === "WeakSet" && !env.has("WeakSet")) {
|
|
4597
|
+
return createWeakSetType();
|
|
4598
|
+
}
|
|
4599
|
+
|
|
4600
|
+
// Handle new Promise(executor): run the executor with resolve/reject
|
|
4601
|
+
// collectors, then statically scan for resolve sites hiding inside
|
|
4602
|
+
// never-executed callbacks (setTimeout / emitter.on) to recover the
|
|
4603
|
+
// fulfillment type. Unknown/non-function executor still yields an honest
|
|
4604
|
+
// Promise<unknown> instead of a bare unknown.
|
|
4605
|
+
if (callee.type === "Identifier" && callee.name === "Promise" && !env.has("Promise")) {
|
|
4606
|
+
const argVals = evaluateArgs(node.arguments as Node[], env);
|
|
4607
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
4608
|
+
const executor = (argVals as TypeValue[])[0];
|
|
4609
|
+
if (executor && executor.kind === "function") {
|
|
4610
|
+
return evaluatePromiseExecutor(executor);
|
|
4611
|
+
}
|
|
4612
|
+
return T.promise(T.unknown);
|
|
4613
|
+
}
|
|
4614
|
+
|
|
4615
|
+
// Handle new Intl.DateTimeFormat() and new Intl.NumberFormat()
|
|
4616
|
+
if (callee.type === "MemberExpression" && !callee.computed) {
|
|
4617
|
+
const obj = callee.object as Node;
|
|
4618
|
+
const prop = callee.property as Node;
|
|
4619
|
+
if (obj.type === "Identifier" && obj.name === "Intl" && !env.has("Intl") && prop.type === "Identifier") {
|
|
4620
|
+
if (prop.name === "DateTimeFormat") {
|
|
4621
|
+
return createDateTimeFormatType();
|
|
4622
|
+
}
|
|
4623
|
+
if (prop.name === "NumberFormat") {
|
|
4624
|
+
return createNumberFormatType();
|
|
4625
|
+
}
|
|
4626
|
+
}
|
|
4627
|
+
}
|
|
4628
|
+
|
|
1691
4629
|
const calleeVal = evaluate(callee, env);
|
|
1692
4630
|
if (isReturn(calleeVal) || isBranch(calleeVal) || isThrow(calleeVal)) return calleeVal;
|
|
1693
4631
|
|
|
4632
|
+
// Reflection constructors: `new (X.prototype)()` (hoek clone's
|
|
4633
|
+
// `new Types.set()`) builds a fresh instance of the prototype's class;
|
|
4634
|
+
// collection classes get their side-table-carrying factories.
|
|
4635
|
+
const protoClassName = calleeVal.kind === "instance" ? (calleeVal as any)._builtinProto as string | undefined : undefined;
|
|
4636
|
+
if (protoClassName) {
|
|
4637
|
+
if (protoClassName === "Set") return createSetType();
|
|
4638
|
+
if (protoClassName === "Map") return createMapType();
|
|
4639
|
+
if (protoClassName === "Array") return T.array(T.unknown);
|
|
4640
|
+
return T.instanceOf(protoClassName);
|
|
4641
|
+
}
|
|
4642
|
+
|
|
4643
|
+
// Class-namespace constructors (`new (x.constructor)(n)` where the
|
|
4644
|
+
// constructor is a memoized builtinClassValue): Array approximates to
|
|
4645
|
+
// unknown[], Object to a plain object, everything else to its class
|
|
4646
|
+
// instance.
|
|
4647
|
+
const classNamespace = calleeVal.kind === "object" ? (calleeVal as any)._builtinName as string | undefined : undefined;
|
|
4648
|
+
if (classNamespace) {
|
|
4649
|
+
if (classNamespace === "Array") return T.array(T.unknown);
|
|
4650
|
+
if (classNamespace === "Object") return T.object({});
|
|
4651
|
+
return T.instanceOf(classNamespace);
|
|
4652
|
+
}
|
|
4653
|
+
|
|
1694
4654
|
if (calleeVal.kind === "function") {
|
|
1695
4655
|
const argVals = evaluateArgs(node.arguments as Node[], env);
|
|
1696
4656
|
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
@@ -1701,15 +4661,7 @@ function evaluateNewExpression(node: Node & { type: "NewExpression" }, env: Envi
|
|
|
1701
4661
|
const constructEnv = calleeVal.closure.extend({});
|
|
1702
4662
|
const thisObj = T.object(instanceProps);
|
|
1703
4663
|
constructEnv.bind("this", thisObj);
|
|
1704
|
-
|
|
1705
|
-
for (let i = 0; i < calleeVal.params.length; i++) {
|
|
1706
|
-
const argVal = (argVals as TypeValue[])[i] ?? T.undefined;
|
|
1707
|
-
if (paramPatterns?.[i]) {
|
|
1708
|
-
bindPattern(paramPatterns[i], argVal, constructEnv);
|
|
1709
|
-
} else {
|
|
1710
|
-
constructEnv.bind(calleeVal.params[i], argVal);
|
|
1711
|
-
}
|
|
1712
|
-
}
|
|
4664
|
+
bindFunctionParams(calleeVal, argVals as TypeValue[], constructEnv);
|
|
1713
4665
|
const result = evaluate(calleeVal.body, constructEnv);
|
|
1714
4666
|
if (isThrow(result)) return result;
|
|
1715
4667
|
const finalThis = constructEnv.lookup("this");
|
|
@@ -1727,7 +4679,7 @@ function evaluateNewExpression(node: Node & { type: "NewExpression" }, env: Envi
|
|
|
1727
4679
|
}
|
|
1728
4680
|
|
|
1729
4681
|
function evaluateTryStatement(node: Node & { type: "TryStatement" }, env: Environment): EvalResult {
|
|
1730
|
-
const tryResult = evaluateStatements(node.block.body, env.
|
|
4682
|
+
const tryResult = evaluateStatements(node.block.body, env.fork());
|
|
1731
4683
|
|
|
1732
4684
|
const thrownType = isThrow(tryResult) ? tryResult.thrown : null;
|
|
1733
4685
|
|
|
@@ -1739,9 +4691,13 @@ function evaluateTryStatement(node: Node & { type: "TryStatement" }, env: Enviro
|
|
|
1739
4691
|
? tryResult
|
|
1740
4692
|
: tryResult;
|
|
1741
4693
|
|
|
4694
|
+
// The handler only runs when the try block actually threw along the
|
|
4695
|
+
// evaluated paths. Evaluating it unconditionally unions the catch value
|
|
4696
|
+
// into every try/catch (with `err` bound to unknown when nothing threw),
|
|
4697
|
+
// which poisons guards like hoek's `try { return v.call(o) } catch …`.
|
|
1742
4698
|
let catchResult: EvalResult | null = null;
|
|
1743
|
-
if (node.handler && thrownType) {
|
|
1744
|
-
const catchEnv = env.
|
|
4699
|
+
if (node.handler && thrownType !== null) {
|
|
4700
|
+
const catchEnv = env.fork();
|
|
1745
4701
|
if (node.handler.param) {
|
|
1746
4702
|
bindPattern(node.handler.param, thrownType, catchEnv);
|
|
1747
4703
|
}
|
|
@@ -1749,7 +4705,7 @@ function evaluateTryStatement(node: Node & { type: "TryStatement" }, env: Enviro
|
|
|
1749
4705
|
}
|
|
1750
4706
|
|
|
1751
4707
|
if (node.finalizer) {
|
|
1752
|
-
const finallyResult = evaluateStatements(node.finalizer.body, env.
|
|
4708
|
+
const finallyResult = evaluateStatements(node.finalizer.body, env.fork());
|
|
1753
4709
|
if (isThrow(finallyResult)) return finallyResult;
|
|
1754
4710
|
if (isReturn(finallyResult)) return finallyResult;
|
|
1755
4711
|
}
|
|
@@ -1798,7 +4754,11 @@ function evaluateSwitchStatement(node: Node & { type: "SwitchStatement" }, env:
|
|
|
1798
4754
|
}
|
|
1799
4755
|
if (matched) {
|
|
1800
4756
|
const result = evaluateStatements(caseNode.consequent, env);
|
|
1801
|
-
if (isThrow(result))
|
|
4757
|
+
if (isThrow(result)) {
|
|
4758
|
+
// switch 是 break 的边界:吸收后正常落穿
|
|
4759
|
+
if (loopSignalKind(result) === "break") break;
|
|
4760
|
+
return result;
|
|
4761
|
+
}
|
|
1802
4762
|
if (isReturn(result)) {
|
|
1803
4763
|
returnValues.push(result.value);
|
|
1804
4764
|
break;
|
|
@@ -1817,7 +4777,28 @@ function evaluateSwitchStatement(node: Node & { type: "SwitchStatement" }, env:
|
|
|
1817
4777
|
|
|
1818
4778
|
const returnValues: TypeValue[] = [];
|
|
1819
4779
|
for (const caseNode of node.cases) {
|
|
1820
|
-
|
|
4780
|
+
let caseEnv = env;
|
|
4781
|
+
if (caseNode.test) {
|
|
4782
|
+
const testVal = evaluate(caseNode.test, env);
|
|
4783
|
+
if (isReturn(testVal) || isBranch(testVal) || isThrow(testVal)) return testVal;
|
|
4784
|
+
|
|
4785
|
+
// Try to narrow discriminant for this case using existing narrow()
|
|
4786
|
+
// Construct a synthetic BinaryExpression: discriminant === caseTest
|
|
4787
|
+
const syntheticTest = {
|
|
4788
|
+
type: "BinaryExpression",
|
|
4789
|
+
operator: "===",
|
|
4790
|
+
left: node.discriminant,
|
|
4791
|
+
right: caseNode.test,
|
|
4792
|
+
start: 0,
|
|
4793
|
+
end: 0,
|
|
4794
|
+
loc: null,
|
|
4795
|
+
} as unknown as Node;
|
|
4796
|
+
const [narrowedEnv] = narrow(syntheticTest, env);
|
|
4797
|
+
caseEnv = narrowedEnv;
|
|
4798
|
+
}
|
|
4799
|
+
|
|
4800
|
+
const result = evaluateStatements(caseNode.consequent, caseEnv);
|
|
4801
|
+
if (loopSignalKind(result) === "break") break;
|
|
1821
4802
|
if (isThrow(result)) continue;
|
|
1822
4803
|
if (isReturn(result)) {
|
|
1823
4804
|
returnValues.push(result.value);
|
|
@@ -1840,19 +4821,210 @@ type CallResult = {
|
|
|
1840
4821
|
throwLoc?: SourceRange;
|
|
1841
4822
|
};
|
|
1842
4823
|
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
4824
|
+
// --- Recursion termination: cycle detection + depth/work budgets ---
|
|
4825
|
+
// Abstract interpretation unrolls calls instead of iterating to a fixpoint,
|
|
4826
|
+
// so recursive functions invoked under unconstrained arguments (e.g. an
|
|
4827
|
+
// entry@ case where `seen.has(obj)` cannot concretely short-circuit) expand
|
|
4828
|
+
// forever. Three guards cut the expansion, all returning T.unknown — the
|
|
4829
|
+
// call-side dual of loop widening: the result simply drops the deeper
|
|
4830
|
+
// iterations' effects instead of guessing them, a sound over-approximation.
|
|
4831
|
+
//
|
|
4832
|
+
// 1. Cycle detection: a call whose (function value, abstract args) pair is
|
|
4833
|
+
// already being evaluated higher on the stack makes no abstract progress
|
|
4834
|
+
// (clone(obj, seen) re-invoking itself with identical types). Unrolling it
|
|
4835
|
+
// anyway fans out exponentially, so the re-entrant edge is cut at its
|
|
4836
|
+
// first recurrence — the same treatment the call memo applies on
|
|
4837
|
+
// MEMO_IN_PROGRESS. Accumulator-growing recursion (deepEqual's `seen`
|
|
4838
|
+
// tuple getting one entry longer per level) never repeats a key verbatim,
|
|
4839
|
+
// so cycle keys are *widened*: tuples longer than CYCLE_TUPLE_CAP and
|
|
4840
|
+
// objects with more than CYCLE_PROP_CAP properties all normalize to the
|
|
4841
|
+
// same key, making accumulator growth collapse after a few levels.
|
|
4842
|
+
// Args-changing recursion (factorial(5) -> factorial(4)) is unaffected
|
|
4843
|
+
// because each level still has a distinct widened key.
|
|
4844
|
+
// 2. Depth budget: backstop for recursion whose argument types keep morphing
|
|
4845
|
+
// without structurally repeating (f(n) -> f([n]) -> f([[n]]) ...) beyond
|
|
4846
|
+
// what the widening recognizes.
|
|
4847
|
+
// 3. Total call budget: absolute bound on the unrolled call tree so no
|
|
4848
|
+
// pathological branching shape can exhaust memory; analysis of any file
|
|
4849
|
+
// stays linear in this budget.
|
|
4850
|
+
const MAX_CALL_DEPTH = 64;
|
|
4851
|
+
const MAX_TOTAL_CALLS = 200_000;
|
|
4852
|
+
const CYCLE_TUPLE_CAP = 4;
|
|
4853
|
+
const CYCLE_PROP_CAP = 8;
|
|
4854
|
+
let _callDepth = 0;
|
|
4855
|
+
let _totalCalls = 0;
|
|
4856
|
+
const _activeCallKeys: string[] = [];
|
|
4857
|
+
const _fnCallIds = new WeakMap<object, string>();
|
|
4858
|
+
let _fnCallIdSeq = 0;
|
|
4859
|
+
|
|
4860
|
+
// Observed return values per function value: completed (non-truncated)
|
|
4861
|
+
// calls accumulate their results so a budget-truncated re-entry — deep
|
|
4862
|
+
// recursion or the re-evaluation fan-out heterogeneous union arguments
|
|
4863
|
+
// trigger — degrades to "what this function has actually returned" instead
|
|
4864
|
+
// of poisoning the caller's union with unknown. Keyed on the fn value, so
|
|
4865
|
+
// entries die with it; no reset hook needed.
|
|
4866
|
+
const _fnResultObs = new WeakMap<object, TypeValue[]>();
|
|
4867
|
+
const FN_RESULT_OBS_CAP = 16;
|
|
4868
|
+
|
|
4869
|
+
function recordFnResultObs(fn: TypeValue & { kind: "function" }, value: TypeValue): void {
|
|
4870
|
+
// never (always throws) and unknown carry no shape information; recording
|
|
4871
|
+
// unknown would just re-poison the fallback union.
|
|
4872
|
+
if (value.kind === "never" || value.kind === "unknown") return;
|
|
4873
|
+
let obs = _fnResultObs.get(fn);
|
|
4874
|
+
if (!obs) {
|
|
4875
|
+
obs = [];
|
|
4876
|
+
_fnResultObs.set(fn, obs);
|
|
4877
|
+
}
|
|
4878
|
+
// Cap keeps the table bounded; dedupe keeps one dominant result from
|
|
4879
|
+
// exhausting the cap before a second shape is ever seen.
|
|
4880
|
+
if (obs.length >= FN_RESULT_OBS_CAP) return;
|
|
4881
|
+
if (obs.some((o) => typeValueEquals(o, value))) return;
|
|
4882
|
+
obs.push(value);
|
|
4883
|
+
}
|
|
4884
|
+
|
|
4885
|
+
function observedResultFallback(fn: TypeValue & { kind: "function" }): TypeValue | undefined {
|
|
4886
|
+
const obs = _fnResultObs.get(fn);
|
|
4887
|
+
if (!obs || obs.length === 0) return undefined;
|
|
4888
|
+
// Union of observed results; >4 same-base literal members collapse to
|
|
4889
|
+
// the base type so long observation tails stay a primitive, not a
|
|
4890
|
+
// sixteen-way literal union.
|
|
4891
|
+
const union = collapseLiteralUnion(simplifyUnion(obs), 4);
|
|
4892
|
+
return union.kind === "never" ? undefined : union;
|
|
4893
|
+
}
|
|
4894
|
+
|
|
4895
|
+
// Memoized per type value: the same argument values are keyed on every
|
|
4896
|
+
// call of a loop (fixture-scale structures made this the dominant cost of
|
|
4897
|
+
// callsite harvesting). Stale entries after in-place property mutation can
|
|
4898
|
+
// only trigger earlier cycle-truncation (sound: result widens to unknown),
|
|
4899
|
+
// never a missed one that would over-run the depth budget.
|
|
4900
|
+
const cycleArgKeyCache = new WeakMap<object, string>();
|
|
4901
|
+
|
|
4902
|
+
function cycleArgKey(tv: TypeValue): string {
|
|
4903
|
+
if (tv.kind === "tuple" || tv.kind === "object" || tv.kind === "union") {
|
|
4904
|
+
const cached = cycleArgKeyCache.get(tv);
|
|
4905
|
+
if (cached !== undefined) return cached;
|
|
4906
|
+
const key = cycleArgKeyUncached(tv);
|
|
4907
|
+
cycleArgKeyCache.set(tv, key);
|
|
4908
|
+
return key;
|
|
4909
|
+
}
|
|
4910
|
+
return cycleArgKeyUncached(tv);
|
|
4911
|
+
}
|
|
4912
|
+
|
|
4913
|
+
function cycleArgKeyUncached(tv: TypeValue): string {
|
|
4914
|
+
if (tv.kind === "tuple") {
|
|
4915
|
+
if (tv.elements.length > CYCLE_TUPLE_CAP) {
|
|
4916
|
+
return `[${tv.elements.slice(0, CYCLE_TUPLE_CAP).map(cycleArgKey).join(",")},…widened]`;
|
|
4917
|
+
}
|
|
4918
|
+
return `[${tv.elements.map(cycleArgKey).join(",")}]`;
|
|
4919
|
+
}
|
|
4920
|
+
if (tv.kind === "object") {
|
|
4921
|
+
const entries = Object.entries(tv.properties);
|
|
4922
|
+
if (entries.length > CYCLE_PROP_CAP) {
|
|
4923
|
+
return `{${entries.slice(0, CYCLE_PROP_CAP).map(([k, v]) => `${k}:${cycleArgKey(v)}`).join(",")},…widened}`;
|
|
1853
4924
|
}
|
|
4925
|
+
return `{${entries.map(([k, v]) => `${k}:${cycleArgKey(v)}`).join(",")}}`;
|
|
4926
|
+
}
|
|
4927
|
+
if (tv.kind === "union") {
|
|
4928
|
+
return tv.members.map(cycleArgKey).join("|");
|
|
4929
|
+
}
|
|
4930
|
+
// Cheap leaf tokens: full typeValueToString rendering of instances and
|
|
4931
|
+
// functions (method tables!) dominates key-building on fixture-scale args.
|
|
4932
|
+
// A coarser token can only merge distinct states into one cycle key —
|
|
4933
|
+
// earlier truncation, which widens to unknown (sound).
|
|
4934
|
+
if (tv.kind === "instance") {
|
|
4935
|
+
const propNames = Object.keys(tv.properties).slice(0, CYCLE_PROP_CAP).join(",");
|
|
4936
|
+
return `inst(${tv.className}{${propNames}})`;
|
|
4937
|
+
}
|
|
4938
|
+
if (tv.kind === "function") return `fn#${objectId(tv)}`;
|
|
4939
|
+
if (tv.kind === "refined") return cycleArgKey(tv.base);
|
|
4940
|
+
if (tv.kind === "promise") return `prom(${cycleArgKey(tv.value)})`;
|
|
4941
|
+
if (tv.kind === "literal") {
|
|
4942
|
+
const s = String(tv.value);
|
|
4943
|
+
return `lit(${typeof tv.value}:${s.length > 24 ? s.slice(0, 24) + "…" : s})`;
|
|
4944
|
+
}
|
|
4945
|
+
if (tv.kind === "primitive") return `prim(${tv.type})`;
|
|
4946
|
+
return tv.kind;
|
|
4947
|
+
}
|
|
4948
|
+
|
|
4949
|
+
function fnCallKey(fn: TypeValue & { kind: "function" }, args: TypeValue[], thisVal?: TypeValue): string {
|
|
4950
|
+
let id = _fnCallIds.get(fn);
|
|
4951
|
+
if (!id) {
|
|
4952
|
+
id = `fn#${++_fnCallIdSeq}`;
|
|
4953
|
+
_fnCallIds.set(fn, id);
|
|
4954
|
+
}
|
|
4955
|
+
const thisKey = thisVal ? `@${cycleArgKey(thisVal)}` : "";
|
|
4956
|
+
return `${id}${thisKey}(${args.map(cycleArgKey).join(",")})`;
|
|
4957
|
+
}
|
|
4958
|
+
|
|
4959
|
+
function callBudgetExhausted(fnName: string, loc: Node["loc"]): CallResult {
|
|
4960
|
+
// kind "global" reuses the existing UnknownRecord union (the analyzer
|
|
4961
|
+
// renders it as a warning-level record; no analyzer change needed).
|
|
4962
|
+
recordUnknown({
|
|
4963
|
+
kind: "global",
|
|
4964
|
+
name: `recursion:${fnName}`,
|
|
4965
|
+
loc,
|
|
4966
|
+
reason: `recursive call truncated to unknown (cycle, depth > ${MAX_CALL_DEPTH}, or work budget)`,
|
|
4967
|
+
});
|
|
4968
|
+
return { value: T.unknown, throws: T.never };
|
|
4969
|
+
}
|
|
4970
|
+
|
|
4971
|
+
function callFunctionFull(
|
|
4972
|
+
fn: TypeValue & { kind: "function" },
|
|
4973
|
+
args: TypeValue[],
|
|
4974
|
+
thisVal?: TypeValue,
|
|
4975
|
+
): CallResult {
|
|
4976
|
+
const callKey = fnCallKey(fn, args, thisVal);
|
|
4977
|
+
const fnName = (fn as any)._name ?? "<anonymous>";
|
|
4978
|
+
if (_activeCallKeys.includes(callKey) || _callDepth >= MAX_CALL_DEPTH || _totalCalls >= MAX_TOTAL_CALLS) {
|
|
4979
|
+
// Diagnostic unchanged (truncation stays visible); the value degrades to
|
|
4980
|
+
// this function's observed results before unknown. The truncated call
|
|
4981
|
+
// shape itself stays unmemoized — this is not its true result.
|
|
4982
|
+
const exhausted = callBudgetExhausted(fnName, fn.body?.loc);
|
|
4983
|
+
const observed = observedResultFallback(fn);
|
|
4984
|
+
return observed !== undefined ? { value: observed, throws: exhausted.throws } : exhausted;
|
|
4985
|
+
}
|
|
4986
|
+
_totalCalls++;
|
|
4987
|
+
_callDepth++;
|
|
4988
|
+
_activeCallKeys.push(callKey);
|
|
4989
|
+
const fnOrigin = _fnModuleTags.get(fn);
|
|
4990
|
+
_execOriginStack.push(fnOrigin ?? _execOriginStack[_execOriginStack.length - 1]);
|
|
4991
|
+
try {
|
|
4992
|
+
const result = callFunctionUnchecked(fn, args, thisVal);
|
|
4993
|
+
recordFnResultObs(fn, result.value);
|
|
4994
|
+
return result;
|
|
4995
|
+
} finally {
|
|
4996
|
+
_execOriginStack.pop();
|
|
4997
|
+
_activeCallKeys.pop();
|
|
4998
|
+
_callDepth--;
|
|
4999
|
+
}
|
|
5000
|
+
}
|
|
5001
|
+
|
|
5002
|
+
function callFunctionUnchecked(
|
|
5003
|
+
fn: TypeValue & { kind: "function" },
|
|
5004
|
+
args: TypeValue[],
|
|
5005
|
+
thisVal?: TypeValue,
|
|
5006
|
+
): CallResult {
|
|
5007
|
+
// Check for direct return value (used by sinon mocks)
|
|
5008
|
+
const directReturn = (fn as any)._directReturn as TypeValue | undefined;
|
|
5009
|
+
if (directReturn) {
|
|
5010
|
+
return { value: directReturn, throws: T.never };
|
|
5011
|
+
}
|
|
5012
|
+
|
|
5013
|
+
// Check for function signature impl (used by @nudo:env / @nudo:mock-module)
|
|
5014
|
+
const sig = getFnSig(fn);
|
|
5015
|
+
if (sig) {
|
|
5016
|
+
const implResult = sig.impl?.(args, thisVal);
|
|
5017
|
+
return { value: implResult ?? sig.returnType, throws: sig.throwsType };
|
|
1854
5018
|
}
|
|
1855
5019
|
|
|
5020
|
+
const callEnv = fn.closure.extend({});
|
|
5021
|
+
// `obj.f()` binds the receiver as the callee's `this` before parameters.
|
|
5022
|
+
// Arrow callees ignore it in real JS; their bodies still resolve `this`
|
|
5023
|
+
// through the closure chain here — accepted approximation.
|
|
5024
|
+
if (thisVal) callEnv.bind("this", thisVal);
|
|
5025
|
+
const isAsync = !!(fn as any)._async;
|
|
5026
|
+
bindFunctionParams(fn, args, callEnv);
|
|
5027
|
+
|
|
1856
5028
|
const savedUnreachable = _unreachableRanges;
|
|
1857
5029
|
_unreachableRanges = [];
|
|
1858
5030
|
|
|
@@ -1892,8 +5064,12 @@ function callFunctionFull(fn: TypeValue & { kind: "function" }, args: TypeValue[
|
|
|
1892
5064
|
return { value: wrapped, throws: T.never };
|
|
1893
5065
|
}
|
|
1894
5066
|
|
|
1895
|
-
function callFunction(
|
|
1896
|
-
|
|
5067
|
+
function callFunction(
|
|
5068
|
+
fn: TypeValue & { kind: "function" },
|
|
5069
|
+
args: TypeValue[],
|
|
5070
|
+
thisVal?: TypeValue,
|
|
5071
|
+
): TypeValue {
|
|
5072
|
+
return callFunctionFull(fn, args, thisVal).value;
|
|
1897
5073
|
}
|
|
1898
5074
|
|
|
1899
5075
|
export function evaluateFunction(
|
|
@@ -1909,24 +5085,65 @@ export function evaluateFunctionFull(
|
|
|
1909
5085
|
args: TypeValue[],
|
|
1910
5086
|
env: Environment,
|
|
1911
5087
|
): CallResult {
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
5088
|
+
// Unwrap export declarations to get the actual function
|
|
5089
|
+
let actualNode = fnNode;
|
|
5090
|
+
if (fnNode.type === "ExportNamedDeclaration" && fnNode.declaration) {
|
|
5091
|
+
actualNode = fnNode.declaration;
|
|
5092
|
+
} else if (fnNode.type === "ExportDefaultDeclaration") {
|
|
5093
|
+
actualNode = fnNode.declaration;
|
|
5094
|
+
}
|
|
5095
|
+
|
|
5096
|
+
if (actualNode.type === "FunctionDeclaration" || actualNode.type === "FunctionExpression" || actualNode.type === "ArrowFunctionExpression") {
|
|
5097
|
+
if (_callDepth >= MAX_CALL_DEPTH) {
|
|
5098
|
+
return callBudgetExhausted((actualNode as any).id?.name ?? "<anonymous>", actualNode.loc);
|
|
5099
|
+
}
|
|
5100
|
+
_callDepth++;
|
|
5101
|
+
try {
|
|
5102
|
+
const callEnv = env.fork();
|
|
5103
|
+
const isAsync = !!(actualNode as any).async;
|
|
5104
|
+
// Named functions can reference themselves; a bare case-evaluation
|
|
5105
|
+
// environment (analyzer's globalEnv normally has the binding, the
|
|
5106
|
+
// directive harness does not) still needs the self-reference for
|
|
5107
|
+
// recursion like `deepClone(obj[key])`.
|
|
5108
|
+
const fnName = (actualNode as any).id?.name as string | undefined;
|
|
5109
|
+
if (fnName) {
|
|
5110
|
+
const paramNames = (actualNode.params as Node[]).map(describeParam);
|
|
5111
|
+
const selfRef = T.fn(paramNames, actualNode.body, callEnv);
|
|
5112
|
+
(selfRef as any)._paramPatterns = actualNode.params;
|
|
5113
|
+
(selfRef as any)._name = fnName;
|
|
5114
|
+
if (isAsync) (selfRef as any)._async = true;
|
|
5115
|
+
callEnv.bind(fnName, selfRef);
|
|
5116
|
+
}
|
|
5117
|
+
for (let i = 0; i < actualNode.params.length; i++) {
|
|
5118
|
+
const pname = describeParam(actualNode.params[i]!);
|
|
5119
|
+
let argVal = args[i] ?? T.undefined;
|
|
5120
|
+
{
|
|
5121
|
+
const tagged = tagParamArg(argVal, pname);
|
|
5122
|
+
copyOrigin(argVal, tagged);
|
|
5123
|
+
argVal = tagged;
|
|
5124
|
+
}
|
|
5125
|
+
bindPattern(actualNode.params[i]!, argVal, callEnv);
|
|
5126
|
+
}
|
|
5127
|
+
const result = evaluate(actualNode.body, callEnv);
|
|
5128
|
+
if (isThrow(result)) return { value: T.never, throws: result.thrown, throwLoc: result.loc };
|
|
5129
|
+
const value = isReturn(result) ? result.value
|
|
5130
|
+
: isBranch(result) ? result.returnedValue
|
|
5131
|
+
: result;
|
|
5132
|
+
// For async functions, unwrap Promise values to avoid double wrapping
|
|
5133
|
+
const wrapped = isAsync
|
|
5134
|
+
? (value.kind === "promise" ? value : T.promise(value))
|
|
5135
|
+
: value;
|
|
5136
|
+
return { value: wrapped, throws: T.never };
|
|
5137
|
+
} finally {
|
|
5138
|
+
_callDepth--;
|
|
5139
|
+
}
|
|
1925
5140
|
}
|
|
1926
5141
|
return { value: T.unknown, throws: T.never };
|
|
1927
5142
|
}
|
|
1928
5143
|
|
|
1929
5144
|
export function evaluateProgram(node: Node, env: Environment): TypeValue {
|
|
5145
|
+
bindCommonJsGlobals(env);
|
|
5146
|
+
resetPhi();
|
|
1930
5147
|
const result = evaluate(node, env);
|
|
1931
5148
|
if (isReturn(result)) return result.value;
|
|
1932
5149
|
if (isBranch(result)) return result.returnedValue;
|