fastscript 2.0.0 → 3.0.1
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/CHANGELOG.md +12 -0
- package/README.md +669 -600
- package/node_modules/@fastscript/core-private/src/fs-error-codes.mjs +2 -2
- package/node_modules/@fastscript/core-private/src/fs-normalize.mjs +125 -91
- package/node_modules/@fastscript/core-private/src/fs-parser.mjs +197 -57
- package/node_modules/@fastscript/core-private/src/typecheck.mjs +1466 -1462
- package/package.json +41 -5
- package/src/benchmark-discipline.mjs +39 -0
- package/src/cli.mjs +37 -2
- package/src/compatibility-governance.mjs +257 -0
- package/src/conversion-manifest.mjs +101 -0
- package/src/diagnostics.mjs +100 -0
- package/src/fs-error-codes.mjs +2 -2
- package/src/fs-normalize.mjs +39 -7
- package/src/generated/compatibility-registry-report.mjs +815 -0
- package/src/generated/docs-search-index.mjs +1137 -275
- package/src/migrate-rollback.mjs +144 -0
- package/src/migrate.mjs +1275 -47
- package/src/migration-wizard.mjs +37 -11
- package/src/module-loader.mjs +13 -1
- package/src/permissions-cli.mjs +112 -0
- package/src/profile.mjs +95 -0
- package/src/regression-guard.mjs +245 -0
- package/src/runtime-permissions.mjs +299 -0
- package/src/trace.mjs +95 -0
- package/src/validate.mjs +10 -0
|
@@ -1,1464 +1,1468 @@
|
|
|
1
|
-
|
|
2
|
-
import { extname, join, resolve } from "node:path";
|
|
3
|
-
import { parseFastScript } from "./fs-parser.mjs";
|
|
4
|
-
import { resolveErrorMeta } from "./fs-error-codes.mjs";
|
|
5
|
-
import { inferRouteMeta, inferRouteParamTypes, sortRoutesByPriority } from "./routes.mjs";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
function createLineStarts(source) {
|
|
38
|
-
const text = String(source ?? "");
|
|
39
|
-
const out = [0];
|
|
40
|
-
for (let i = 0; i < text.length; i += 1) {
|
|
41
|
-
if (text[i] === "\n") out.push(i + 1);
|
|
42
|
-
}
|
|
43
|
-
return out;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function lineFromOffset(lineStarts, offset) {
|
|
47
|
-
let lo = 0;
|
|
48
|
-
let hi = lineStarts.length - 1;
|
|
49
|
-
while (lo <= hi) {
|
|
50
|
-
const mid = (lo + hi) >> 1;
|
|
51
|
-
const start = lineStarts[mid];
|
|
52
|
-
const next = lineStarts[mid + 1] ?? Number.POSITIVE_INFINITY;
|
|
53
|
-
if (offset < start) hi = mid - 1;
|
|
54
|
-
else if (offset >= next) lo = mid + 1;
|
|
55
|
-
else return { line: mid + 1, column: offset - start + 1 };
|
|
56
|
-
}
|
|
57
|
-
return { line: 1, column: 1 };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function spanFromNode(node, sourceLength = 0) {
|
|
61
|
-
const start = Math.max(0, Math.min(sourceLength, Number(node?.fsRange?.[0] ?? node?.start ?? 0)));
|
|
62
|
-
const end = Math.max(start, Math.min(sourceLength, Number(node?.fsRange?.[1] ?? node?.end ?? start)));
|
|
63
|
-
return { start, end };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function createDiagnostic({ file, source, lineStarts, code, message, severity, hint, span, related = [] }) {
|
|
67
|
-
const meta = resolveErrorMeta(code);
|
|
68
|
-
const sourceLength = source.length;
|
|
69
|
-
const start = Math.max(0, Math.min(sourceLength, Number(span?.start ?? 0)));
|
|
70
|
-
const end = Math.max(start, Math.min(sourceLength, Number(span?.end ?? start + 1)));
|
|
71
|
-
const loc = lineFromOffset(lineStarts, start);
|
|
72
|
-
return {
|
|
73
|
-
file,
|
|
74
|
-
code,
|
|
75
|
-
severity: severity || meta.severity || "error",
|
|
76
|
-
message: message || meta.message,
|
|
77
|
-
hint: hint || meta.hint || "",
|
|
78
|
-
span: { start, end },
|
|
79
|
-
line: loc.line,
|
|
80
|
-
column: loc.column,
|
|
81
|
-
related: related.map((entry) => {
|
|
82
|
-
const relStart = Math.max(0, Math.min(sourceLength, Number(entry.span?.start ?? 0)));
|
|
83
|
-
const relLoc = lineFromOffset(lineStarts, relStart);
|
|
84
|
-
return {
|
|
85
|
-
file: entry.file || file,
|
|
86
|
-
message: entry.message || "Related location",
|
|
87
|
-
line: relLoc.line,
|
|
88
|
-
column: relLoc.column,
|
|
89
|
-
span: {
|
|
90
|
-
start: relStart,
|
|
91
|
-
end: Math.max(relStart, Math.min(sourceLength, Number(entry.span?.end ?? relStart + 1))),
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
|
-
}),
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function dedupeDiagnostics(diagnostics) {
|
|
99
|
-
const seen = new Set();
|
|
100
|
-
const out = [];
|
|
101
|
-
for (const diagnostic of diagnostics) {
|
|
102
|
-
const key = `${diagnostic.file}|${diagnostic.code}|${diagnostic.severity}|${diagnostic.message}|${diagnostic.span.start}|${diagnostic.span.end}`;
|
|
103
|
-
if (seen.has(key)) continue;
|
|
104
|
-
seen.add(key);
|
|
105
|
-
out.push(diagnostic);
|
|
106
|
-
}
|
|
107
|
-
out.sort((a, b) => {
|
|
108
|
-
if (a.file !== b.file) return a.file.localeCompare(b.file);
|
|
109
|
-
if (a.span.start !== b.span.start) return a.span.start - b.span.start;
|
|
110
|
-
if (a.code !== b.code) return a.code.localeCompare(b.code);
|
|
111
|
-
return a.message.localeCompare(b.message);
|
|
112
|
-
});
|
|
113
|
-
return out;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function makeFnType(params, returnType, minArgs = params.length, maxArgs = params.length) {
|
|
117
|
-
return { kind: "function", params, returnType: returnType || T_UNKNOWN, minArgs, maxArgs };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function makeArrayType(elementType = T_UNKNOWN) {
|
|
121
|
-
return { kind: "array", elementType };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function makeObjectType(properties = {}) {
|
|
125
|
-
return { kind: "object", properties: { ...properties } };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function typeFromLiteral(value) {
|
|
129
|
-
if (value === null) return T_NULL;
|
|
130
|
-
if (value === undefined) return T_UNDEFINED;
|
|
131
|
-
const t = typeof value;
|
|
132
|
-
if (t === "string") return T_STRING;
|
|
133
|
-
if (t === "number") return T_NUMBER;
|
|
134
|
-
if (t === "boolean") return T_BOOLEAN;
|
|
135
|
-
if (t === "bigint") return { kind: "bigint" };
|
|
136
|
-
return T_UNKNOWN;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function typeToString(type) {
|
|
140
|
-
if (!type) return "unknown";
|
|
141
|
-
if (type.kind === "union") return type.types.map(typeToString).join(" | ");
|
|
142
|
-
if (type.kind === "array") return `${typeToString(type.elementType)}[]`;
|
|
143
|
-
if (type.kind === "object") {
|
|
144
|
-
const entries = Object.entries(type.properties || {});
|
|
145
|
-
if (!entries.length) return "object";
|
|
146
|
-
return `{ ${entries.map(([k, v]) => `${k}: ${typeToString(v)}`).join("; ")} }`;
|
|
147
|
-
}
|
|
148
|
-
if (type.kind === "function") {
|
|
149
|
-
return `(${type.params.map(typeToString).join(", ")}) => ${typeToString(type.returnType)}`;
|
|
150
|
-
}
|
|
151
|
-
return type.kind;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function typeEquals(a, b) {
|
|
155
|
-
if (!a || !b) return false;
|
|
156
|
-
if (a.kind !== b.kind) return false;
|
|
157
|
-
if (a.kind === "array") return typeEquals(a.elementType, b.elementType);
|
|
158
|
-
if (a.kind === "function") {
|
|
159
|
-
if (a.params.length !== b.params.length) return false;
|
|
160
|
-
for (let i = 0; i < a.params.length; i += 1) {
|
|
161
|
-
if (!typeEquals(a.params[i], b.params[i])) return false;
|
|
162
|
-
}
|
|
163
|
-
return typeEquals(a.returnType, b.returnType);
|
|
164
|
-
}
|
|
165
|
-
if (a.kind === "object") {
|
|
166
|
-
const aKeys = Object.keys(a.properties || {}).sort();
|
|
167
|
-
const bKeys = Object.keys(b.properties || {}).sort();
|
|
168
|
-
if (aKeys.length !== bKeys.length) return false;
|
|
169
|
-
for (let i = 0; i < aKeys.length; i += 1) {
|
|
170
|
-
if (aKeys[i] !== bKeys[i]) return false;
|
|
171
|
-
if (!typeEquals(a.properties[aKeys[i]], b.properties[bKeys[i]])) return false;
|
|
172
|
-
}
|
|
173
|
-
return true;
|
|
174
|
-
}
|
|
175
|
-
if (a.kind === "union") {
|
|
176
|
-
if (a.types.length !== b.types.length) return false;
|
|
177
|
-
return a.types.every((candidate, index) => typeEquals(candidate, b.types[index]));
|
|
178
|
-
}
|
|
179
|
-
return true;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function unionTypes(types) {
|
|
183
|
-
const expanded = [];
|
|
184
|
-
for (const type of types) {
|
|
185
|
-
if (!type) continue;
|
|
186
|
-
if (type.kind === "union") expanded.push(...type.types);
|
|
187
|
-
else expanded.push(type);
|
|
188
|
-
}
|
|
189
|
-
if (!expanded.length) return T_UNKNOWN;
|
|
190
|
-
if (expanded.some((type) => type.kind === "any")) return T_ANY;
|
|
191
|
-
|
|
192
|
-
const unique = [];
|
|
193
|
-
for (const candidate of expanded) {
|
|
194
|
-
if (!unique.some((existing) => typeEquals(existing, candidate))) unique.push(candidate);
|
|
195
|
-
}
|
|
196
|
-
if (unique.length === 1) return unique[0];
|
|
197
|
-
return { kind: "union", types: unique.sort((a, b) => typeToString(a).localeCompare(typeToString(b))) };
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function copyType(type) {
|
|
201
|
-
if (!type || typeof type !== "object") return T_UNKNOWN;
|
|
202
|
-
if (type.kind === "union") return unionTypes(type.types.map(copyType));
|
|
203
|
-
if (type.kind === "array") return makeArrayType(copyType(type.elementType));
|
|
204
|
-
if (type.kind === "object") {
|
|
205
|
-
const out = {};
|
|
206
|
-
for (const [key, value] of Object.entries(type.properties || {})) out[key] = copyType(value);
|
|
207
|
-
return makeObjectType(out);
|
|
208
|
-
}
|
|
209
|
-
if (type.kind === "function") return makeFnType(type.params.map(copyType), copyType(type.returnType), type.minArgs, type.maxArgs);
|
|
210
|
-
return { ...type };
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function isAssignable(source, target) {
|
|
214
|
-
if (!source || !target) return true;
|
|
215
|
-
if (source.kind === "any" || target.kind === "any") return true;
|
|
216
|
-
if (source.kind === "unknown" || target.kind === "unknown") return true;
|
|
217
|
-
if (target.kind === "union") return target.types.some((candidate) => isAssignable(source, candidate));
|
|
218
|
-
if (source.kind === "union") return source.types.every((candidate) => isAssignable(candidate, target));
|
|
219
|
-
if (source.kind === target.kind) {
|
|
220
|
-
if (source.kind === "array") return isAssignable(source.elementType, target.elementType);
|
|
221
|
-
if (source.kind === "object") {
|
|
222
|
-
const targetProps = target.properties || {};
|
|
223
|
-
const sourceProps = source.properties || {};
|
|
224
|
-
for (const [key, targetType] of Object.entries(targetProps)) {
|
|
225
|
-
if (!(key in sourceProps)) return false;
|
|
226
|
-
if (!isAssignable(sourceProps[key], targetType)) return false;
|
|
227
|
-
}
|
|
228
|
-
return true;
|
|
229
|
-
}
|
|
230
|
-
if (source.kind === "function") {
|
|
231
|
-
if (source.minArgs > target.maxArgs) return false;
|
|
232
|
-
if (source.maxArgs < target.minArgs) return false;
|
|
233
|
-
return isAssignable(source.returnType, target.returnType);
|
|
234
|
-
}
|
|
235
|
-
return true;
|
|
236
|
-
}
|
|
237
|
-
if (source.kind === "null" && target.kind === "object") return true;
|
|
238
|
-
return false;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
class Scope {
|
|
242
|
-
constructor(parent = null, kind = "block") {
|
|
243
|
-
this.id = ++scopeCounter;
|
|
244
|
-
this.parent = parent;
|
|
245
|
-
this.kind = kind;
|
|
246
|
-
this.symbols = new Map();
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
declare(symbol) {
|
|
250
|
-
this.symbols.set(symbol.name, symbol);
|
|
251
|
-
return symbol;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
lookup(name) {
|
|
255
|
-
if (this.symbols.has(name)) return this.symbols.get(name);
|
|
256
|
-
if (this.parent) return this.parent.lookup(name);
|
|
257
|
-
return null;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function createSymbol({ name, type, kind, mutable, span, file, runtime = "universal", runtimes = null }) {
|
|
262
|
-
const normalizedRuntimes = Array.isArray(runtimes) && runtimes.length ? [...new Set(runtimes)] : [runtime || "universal"];
|
|
263
|
-
return { name, type: type || T_UNKNOWN, kind, mutable, span, file, runtime: normalizedRuntimes[0], runtimes: normalizedRuntimes };
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function inferAllowedRuntimeContextsFromFile(file = "") {
|
|
267
|
-
const normalized = String(file).replace(/\\/g, "/").toLowerCase();
|
|
268
|
-
if (normalized.endsWith(".client.fs")) return ["universal", "browser"];
|
|
269
|
-
if (normalized.endsWith(".server.fs")) return ["universal", "server"];
|
|
270
|
-
if (normalized.endsWith(".edge.fs")) return ["universal", "edge"];
|
|
271
|
-
if (/(^|\/)(app\/)?api\//.test(normalized)) return ["universal", "server"];
|
|
272
|
-
if (/(^|\/)(app\/)?pages\//.test(normalized)) return ["universal", "browser", "server"];
|
|
273
|
-
return ["universal"];
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function isRuntimeCompatible(symbolRuntimes = ["universal"], allowedContexts = ["universal"]) {
|
|
277
|
-
const runtimes = Array.isArray(symbolRuntimes) && symbolRuntimes.length ? symbolRuntimes : ["universal"];
|
|
278
|
-
const contexts = Array.isArray(allowedContexts) && allowedContexts.length ? allowedContexts : ["universal"];
|
|
279
|
-
if (runtimes.includes("universal")) return true;
|
|
280
|
-
return runtimes.some((runtime) => contexts.includes(runtime));
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function collectPatternBindings(pattern, out = []) {
|
|
284
|
-
if (!pattern) return out;
|
|
285
|
-
switch (pattern.type) {
|
|
286
|
-
case "Identifier":
|
|
287
|
-
out.push({ name: pattern.name, node: pattern });
|
|
288
|
-
break;
|
|
289
|
-
case "AssignmentPattern":
|
|
290
|
-
collectPatternBindings(pattern.left, out);
|
|
291
|
-
break;
|
|
292
|
-
case "RestElement":
|
|
293
|
-
collectPatternBindings(pattern.argument, out);
|
|
294
|
-
break;
|
|
295
|
-
case "ArrayPattern":
|
|
296
|
-
for (const element of pattern.elements || []) collectPatternBindings(element, out);
|
|
297
|
-
break;
|
|
298
|
-
case "ObjectPattern":
|
|
299
|
-
for (const property of pattern.properties || []) {
|
|
300
|
-
if (property.type === "RestElement") collectPatternBindings(property.argument, out);
|
|
301
|
-
else collectPatternBindings(property.value || property.key, out);
|
|
302
|
-
}
|
|
303
|
-
break;
|
|
304
|
-
default:
|
|
305
|
-
break;
|
|
306
|
-
}
|
|
307
|
-
return out;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function declareBuiltins(scope, file) {
|
|
311
|
-
scope.declare(createSymbol({ name: "Number", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_NUMBER, 1, 1) }));
|
|
312
|
-
scope.declare(createSymbol({ name: "String", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_STRING, 1, 1) }));
|
|
313
|
-
scope.declare(createSymbol({ name: "Boolean", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_BOOLEAN, 1, 1) }));
|
|
314
|
-
scope.declare(createSymbol({ name: "Array", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
315
|
-
scope.declare(createSymbol({ name: "Object", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
316
|
-
scope.declare(createSymbol({ name: "JSON", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
317
|
-
scope.declare(createSymbol({ name: "Math", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
318
|
-
scope.declare(createSymbol({ name: "Date", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
319
|
-
scope.declare(createSymbol({ name: "Promise", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
320
|
-
scope.declare(createSymbol({ name: "Set", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
321
|
-
scope.declare(createSymbol({ name: "Map", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
322
|
-
scope.declare(createSymbol({ name: "Error", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
323
|
-
scope.declare(createSymbol({ name: "RegExp", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
324
|
-
scope.declare(createSymbol({ name: "Intl", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
325
|
-
scope.declare(createSymbol({ name: "URL", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_OBJECT, 1, Number.MAX_SAFE_INTEGER) }));
|
|
326
|
-
scope.declare(createSymbol({ name: "process", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "server" }));
|
|
327
|
-
scope.declare(createSymbol({ name: "Buffer", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "server" }));
|
|
328
|
-
scope.declare(createSymbol({ name: "__dirname", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_STRING, runtime: "server" }));
|
|
329
|
-
scope.declare(createSymbol({ name: "__filename", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_STRING, runtime: "server" }));
|
|
330
|
-
scope.declare(createSymbol({ name: "require", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_UNKNOWN, 1, Number.MAX_SAFE_INTEGER), runtime: "server" }));
|
|
331
|
-
scope.declare(createSymbol({ name: "module", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "server" }));
|
|
332
|
-
scope.declare(createSymbol({ name: "exports", kind: "builtin", mutable: true, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "server" }));
|
|
333
|
-
scope.declare(createSymbol({ name: "globalThis", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
334
|
-
scope.declare(createSymbol({ name: "window", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "browser" }));
|
|
335
|
-
scope.declare(createSymbol({ name: "document", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "browser" }));
|
|
336
|
-
scope.declare(createSymbol({ name: "location", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "browser" }));
|
|
337
|
-
scope.declare(createSymbol({ name: "navigator", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtimes: ["browser", "worker"] }));
|
|
338
|
-
scope.declare(createSymbol({ name: "history", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "browser" }));
|
|
339
|
-
scope.declare(createSymbol({ name: "localStorage", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "browser" }));
|
|
340
|
-
scope.declare(createSymbol({ name: "sessionStorage", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "browser" }));
|
|
341
|
-
scope.declare(createSymbol({ name: "indexedDB", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtimes: ["browser", "worker"] }));
|
|
342
|
-
scope.declare(createSymbol({ name: "self", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtimes: ["browser", "worker", "edge"] }));
|
|
343
|
-
scope.declare(createSymbol({ name: "console", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
344
|
-
scope.declare(createSymbol({ name: "Request", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
345
|
-
scope.declare(createSymbol({ name: "Response", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
346
|
-
scope.declare(createSymbol({ name: "Headers", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
347
|
-
scope.declare(createSymbol({ name: "FormData", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
348
|
-
scope.declare(createSymbol({ name: "AbortController", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
349
|
-
scope.declare(createSymbol({ name: "AbortSignal", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
350
|
-
scope.declare(createSymbol({ name: "Blob", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
351
|
-
scope.declare(createSymbol({ name: "File", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
352
|
-
scope.declare(createSymbol({ name: "ReadableStream", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
353
|
-
scope.declare(createSymbol({ name: "WritableStream", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
354
|
-
scope.declare(createSymbol({ name: "TransformStream", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
355
|
-
scope.declare(createSymbol({ name: "crypto", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
356
|
-
scope.declare(createSymbol({ name: "Event", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "browser" }));
|
|
357
|
-
scope.declare(createSymbol({ name: "CustomEvent", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "browser" }));
|
|
358
|
-
scope.declare(createSymbol({ name: "TextEncoder", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([], T_OBJECT, 0, 0) }));
|
|
359
|
-
scope.declare(createSymbol({ name: "TextDecoder", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([], T_OBJECT, 0, 0) }));
|
|
360
|
-
scope.declare(createSymbol({ name: "setTimeout", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_NUMBER, 1, Number.MAX_SAFE_INTEGER) }));
|
|
361
|
-
scope.declare(createSymbol({ name: "clearTimeout", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_VOID, 1, 1) }));
|
|
362
|
-
scope.declare(createSymbol({ name: "setInterval", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_NUMBER, 1, Number.MAX_SAFE_INTEGER) }));
|
|
363
|
-
scope.declare(createSymbol({ name: "clearInterval", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_VOID, 1, 1) }));
|
|
364
|
-
scope.declare(createSymbol({ name: "queueMicrotask", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_VOID, 1, 1) }));
|
|
365
|
-
scope.declare(createSymbol({ name: "URLSearchParams", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_OBJECT, 1, 1) }));
|
|
366
|
-
scope.declare(createSymbol({ name: "fetch", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_UNKNOWN, 1, Number.MAX_SAFE_INTEGER) }));
|
|
367
|
-
scope.declare(createSymbol({ name: "atob", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_STRING, 1, 1) }));
|
|
368
|
-
scope.declare(createSymbol({ name: "btoa", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_STRING, 1, 1) }));
|
|
369
|
-
scope.declare(createSymbol({ name: "encodeURIComponent", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_STRING, 1, 1) }));
|
|
370
|
-
scope.declare(createSymbol({ name: "decodeURIComponent", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_STRING, 1, 1) }));
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function symbolSuggestion(name, scope) {
|
|
374
|
-
const candidates = [];
|
|
375
|
-
let cursor = scope;
|
|
376
|
-
while (cursor) {
|
|
377
|
-
for (const candidate of cursor.symbols.keys()) candidates.push(candidate);
|
|
378
|
-
cursor = cursor.parent;
|
|
379
|
-
}
|
|
380
|
-
const lowered = String(name || "").toLowerCase();
|
|
381
|
-
const hits = [...new Set(candidates)]
|
|
382
|
-
.filter((candidate) => candidate.toLowerCase().startsWith(lowered[0] || ""))
|
|
383
|
-
.slice(0, 3);
|
|
384
|
-
return hits.length ? hits.join(", ") : "";
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function reportUnknownSymbol(identifier, scope, state) {
|
|
388
|
-
const span = spanFromNode(identifier, state.source.length);
|
|
389
|
-
const suggestion = symbolSuggestion(identifier.name, scope);
|
|
390
|
-
state.diagnostics.push(
|
|
391
|
-
createDiagnostic({
|
|
392
|
-
file: state.file,
|
|
393
|
-
source: state.source,
|
|
394
|
-
lineStarts: state.lineStarts,
|
|
395
|
-
code: "FS4101",
|
|
396
|
-
span,
|
|
397
|
-
message: `Unknown symbol \`${identifier.name}\`.`,
|
|
398
|
-
hint: suggestion ? `Did you mean: ${suggestion}` : resolveErrorMeta("FS4101").hint,
|
|
399
|
-
}),
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function reportRuntimeScopeError(identifier, symbol, state) {
|
|
404
|
-
const span = spanFromNode(identifier, state.source.length);
|
|
405
|
-
const available = Array.isArray(symbol.runtimes) && symbol.runtimes.length ? symbol.runtimes : [symbol.runtime || "universal"];
|
|
406
|
-
const context = (state.allowedRuntimes || ["universal"]).join("|");
|
|
407
|
-
state.diagnostics.push(
|
|
408
|
-
createDiagnostic({
|
|
409
|
-
file: state.file,
|
|
410
|
-
source: state.source,
|
|
411
|
-
lineStarts: state.lineStarts,
|
|
412
|
-
code: "FS4201",
|
|
413
|
-
span,
|
|
414
|
-
message: `Symbol \`${identifier.name}\` is not available in ${context} context.`,
|
|
415
|
-
hint: `This symbol is only available in: ${available.join(", ")}`,
|
|
416
|
-
}),
|
|
417
|
-
);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function inferFunctionFromNode(node, scope, state) {
|
|
421
|
-
const params = [];
|
|
422
|
-
let required = 0;
|
|
423
|
-
for (const param of node.params || []) {
|
|
424
|
-
if (param.type === "AssignmentPattern") {
|
|
425
|
-
params.push(T_UNKNOWN);
|
|
426
|
-
continue;
|
|
427
|
-
}
|
|
428
|
-
required += 1;
|
|
429
|
-
params.push(T_UNKNOWN);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const fnScope = new Scope(scope, "function");
|
|
433
|
-
state.scopes.push(fnScope);
|
|
434
|
-
for (const param of node.params || []) {
|
|
435
|
-
const bindings = collectPatternBindings(param);
|
|
436
|
-
for (const binding of bindings) {
|
|
437
|
-
fnScope.declare(createSymbol({
|
|
438
|
-
name: binding.name,
|
|
439
|
-
type: T_UNKNOWN,
|
|
440
|
-
kind: "param",
|
|
441
|
-
mutable: true,
|
|
442
|
-
span: spanFromNode(binding.node, state.source.length),
|
|
443
|
-
file: state.file,
|
|
444
|
-
}));
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
const fnContext = { returns: [] };
|
|
449
|
-
if (node.body?.type === "BlockStatement") {
|
|
450
|
-
for (const statement of node.body.body || []) {
|
|
451
|
-
analyzeStatement(statement, fnScope, state, fnContext);
|
|
452
|
-
}
|
|
453
|
-
} else if (node.body) {
|
|
454
|
-
fnContext.returns.push(inferExpression(node.body, fnScope, state, fnContext));
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
const returnType = fnContext.returns.length ? unionTypes(fnContext.returns) : T_VOID;
|
|
458
|
-
return makeFnType(params, returnType, required, params.length);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function hoistDeclarations(body, scope, state) {
|
|
462
|
-
for (const statement of body || []) {
|
|
463
|
-
if (statement.type === "FunctionDeclaration" && statement.id?.name) {
|
|
464
|
-
scope.declare(createSymbol({
|
|
465
|
-
name: statement.id.name,
|
|
466
|
-
type: makeFnType(new Array(statement.params?.length || 0).fill(T_UNKNOWN), T_UNKNOWN),
|
|
467
|
-
kind: "function",
|
|
468
|
-
mutable: false,
|
|
469
|
-
span: spanFromNode(statement.id, state.source.length),
|
|
470
|
-
file: state.file,
|
|
471
|
-
}));
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
if (statement.type === "VariableDeclaration" && statement.kind === "var") {
|
|
475
|
-
for (const declaration of statement.declarations || []) {
|
|
476
|
-
const bindings = collectPatternBindings(declaration.id);
|
|
477
|
-
for (const binding of bindings) {
|
|
478
|
-
if (scope.symbols.has(binding.name)) continue;
|
|
479
|
-
scope.declare(createSymbol({
|
|
480
|
-
name: binding.name,
|
|
481
|
-
type: T_UNKNOWN,
|
|
482
|
-
kind: "var",
|
|
483
|
-
mutable: true,
|
|
484
|
-
span: spanFromNode(binding.node, state.source.length),
|
|
485
|
-
file: state.file,
|
|
486
|
-
}));
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
}
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { extname, join, resolve } from "node:path";
|
|
3
|
+
import { parseFastScript } from "./fs-parser.mjs";
|
|
4
|
+
import { resolveErrorMeta } from "./fs-error-codes.mjs";
|
|
5
|
+
import { inferRouteMeta, inferRouteParamTypes, sortRoutesByPriority } from "./routes.mjs";
|
|
6
|
+
import { stripTypeScriptHints } from "./fs-normalize.mjs";
|
|
7
|
+
|
|
8
|
+
const APP_DIR = resolve("app");
|
|
9
|
+
const PAGES_DIR = resolve("app/pages");
|
|
10
|
+
const OUT_DIR = resolve(".fastscript");
|
|
11
|
+
const TYPES_PATH = join(OUT_DIR, "route-params.d.ts");
|
|
12
|
+
const REPORT_PATH = join(OUT_DIR, "typecheck-report.json");
|
|
13
|
+
|
|
14
|
+
const T_ANY = Object.freeze({ kind: "any" });
|
|
15
|
+
const T_UNKNOWN = Object.freeze({ kind: "unknown" });
|
|
16
|
+
const T_VOID = Object.freeze({ kind: "void" });
|
|
17
|
+
const T_UNDEFINED = Object.freeze({ kind: "undefined" });
|
|
18
|
+
const T_NULL = Object.freeze({ kind: "null" });
|
|
19
|
+
const T_BOOLEAN = Object.freeze({ kind: "boolean" });
|
|
20
|
+
const T_NUMBER = Object.freeze({ kind: "number" });
|
|
21
|
+
const T_STRING = Object.freeze({ kind: "string" });
|
|
22
|
+
const T_OBJECT = Object.freeze({ kind: "object" });
|
|
23
|
+
|
|
24
|
+
let scopeCounter = 0;
|
|
25
|
+
|
|
26
|
+
function walk(dir) {
|
|
27
|
+
if (!existsSync(dir)) return [];
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const entry of readdirSync(dir)) {
|
|
30
|
+
const full = join(dir, entry);
|
|
31
|
+
const st = statSync(full);
|
|
32
|
+
if (st.isDirectory()) out.push(...walk(full));
|
|
33
|
+
else if (st.isFile()) out.push(full);
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
491
36
|
}
|
|
492
|
-
|
|
493
|
-
function
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
37
|
+
|
|
38
|
+
function createLineStarts(source) {
|
|
39
|
+
const text = String(source ?? "");
|
|
40
|
+
const out = [0];
|
|
41
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
42
|
+
if (text[i] === "\n") out.push(i + 1);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function lineFromOffset(lineStarts, offset) {
|
|
48
|
+
let lo = 0;
|
|
49
|
+
let hi = lineStarts.length - 1;
|
|
50
|
+
while (lo <= hi) {
|
|
51
|
+
const mid = (lo + hi) >> 1;
|
|
52
|
+
const start = lineStarts[mid];
|
|
53
|
+
const next = lineStarts[mid + 1] ?? Number.POSITIVE_INFINITY;
|
|
54
|
+
if (offset < start) hi = mid - 1;
|
|
55
|
+
else if (offset >= next) lo = mid + 1;
|
|
56
|
+
else return { line: mid + 1, column: offset - start + 1 };
|
|
57
|
+
}
|
|
58
|
+
return { line: 1, column: 1 };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function spanFromNode(node, sourceLength = 0) {
|
|
62
|
+
const start = Math.max(0, Math.min(sourceLength, Number(node?.fsRange?.[0] ?? node?.start ?? 0)));
|
|
63
|
+
const end = Math.max(start, Math.min(sourceLength, Number(node?.fsRange?.[1] ?? node?.end ?? start)));
|
|
64
|
+
return { start, end };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createDiagnostic({ file, source, lineStarts, code, message, severity, hint, span, related = [] }) {
|
|
68
|
+
const meta = resolveErrorMeta(code);
|
|
69
|
+
const sourceLength = source.length;
|
|
70
|
+
const start = Math.max(0, Math.min(sourceLength, Number(span?.start ?? 0)));
|
|
71
|
+
const end = Math.max(start, Math.min(sourceLength, Number(span?.end ?? start + 1)));
|
|
72
|
+
const loc = lineFromOffset(lineStarts, start);
|
|
73
|
+
return {
|
|
74
|
+
file,
|
|
75
|
+
code,
|
|
76
|
+
severity: severity || meta.severity || "error",
|
|
77
|
+
message: message || meta.message,
|
|
78
|
+
hint: hint || meta.hint || "",
|
|
79
|
+
span: { start, end },
|
|
80
|
+
line: loc.line,
|
|
81
|
+
column: loc.column,
|
|
82
|
+
related: related.map((entry) => {
|
|
83
|
+
const relStart = Math.max(0, Math.min(sourceLength, Number(entry.span?.start ?? 0)));
|
|
84
|
+
const relLoc = lineFromOffset(lineStarts, relStart);
|
|
85
|
+
return {
|
|
86
|
+
file: entry.file || file,
|
|
87
|
+
message: entry.message || "Related location",
|
|
88
|
+
line: relLoc.line,
|
|
89
|
+
column: relLoc.column,
|
|
90
|
+
span: {
|
|
91
|
+
start: relStart,
|
|
92
|
+
end: Math.max(relStart, Math.min(sourceLength, Number(entry.span?.end ?? relStart + 1))),
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function dedupeDiagnostics(diagnostics) {
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
const out = [];
|
|
102
|
+
for (const diagnostic of diagnostics) {
|
|
103
|
+
const key = `${diagnostic.file}|${diagnostic.code}|${diagnostic.severity}|${diagnostic.message}|${diagnostic.span.start}|${diagnostic.span.end}`;
|
|
104
|
+
if (seen.has(key)) continue;
|
|
105
|
+
seen.add(key);
|
|
106
|
+
out.push(diagnostic);
|
|
107
|
+
}
|
|
108
|
+
out.sort((a, b) => {
|
|
109
|
+
if (a.file !== b.file) return a.file.localeCompare(b.file);
|
|
110
|
+
if (a.span.start !== b.span.start) return a.span.start - b.span.start;
|
|
111
|
+
if (a.code !== b.code) return a.code.localeCompare(b.code);
|
|
112
|
+
return a.message.localeCompare(b.message);
|
|
113
|
+
});
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function makeFnType(params, returnType, minArgs = params.length, maxArgs = params.length) {
|
|
118
|
+
return { kind: "function", params, returnType: returnType || T_UNKNOWN, minArgs, maxArgs };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function makeArrayType(elementType = T_UNKNOWN) {
|
|
122
|
+
return { kind: "array", elementType };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function makeObjectType(properties = {}) {
|
|
126
|
+
return { kind: "object", properties: { ...properties } };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function typeFromLiteral(value) {
|
|
130
|
+
if (value === null) return T_NULL;
|
|
131
|
+
if (value === undefined) return T_UNDEFINED;
|
|
132
|
+
const t = typeof value;
|
|
133
|
+
if (t === "string") return T_STRING;
|
|
134
|
+
if (t === "number") return T_NUMBER;
|
|
135
|
+
if (t === "boolean") return T_BOOLEAN;
|
|
136
|
+
if (t === "bigint") return { kind: "bigint" };
|
|
137
|
+
return T_UNKNOWN;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function typeToString(type) {
|
|
141
|
+
if (!type) return "unknown";
|
|
142
|
+
if (type.kind === "union") return type.types.map(typeToString).join(" | ");
|
|
143
|
+
if (type.kind === "array") return `${typeToString(type.elementType)}[]`;
|
|
144
|
+
if (type.kind === "object") {
|
|
145
|
+
const entries = Object.entries(type.properties || {});
|
|
146
|
+
if (!entries.length) return "object";
|
|
147
|
+
return `{ ${entries.map(([k, v]) => `${k}: ${typeToString(v)}`).join("; ")} }`;
|
|
148
|
+
}
|
|
149
|
+
if (type.kind === "function") {
|
|
150
|
+
return `(${type.params.map(typeToString).join(", ")}) => ${typeToString(type.returnType)}`;
|
|
151
|
+
}
|
|
152
|
+
return type.kind;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function typeEquals(a, b) {
|
|
156
|
+
if (!a || !b) return false;
|
|
157
|
+
if (a.kind !== b.kind) return false;
|
|
158
|
+
if (a.kind === "array") return typeEquals(a.elementType, b.elementType);
|
|
159
|
+
if (a.kind === "function") {
|
|
160
|
+
if (a.params.length !== b.params.length) return false;
|
|
161
|
+
for (let i = 0; i < a.params.length; i += 1) {
|
|
162
|
+
if (!typeEquals(a.params[i], b.params[i])) return false;
|
|
163
|
+
}
|
|
164
|
+
return typeEquals(a.returnType, b.returnType);
|
|
165
|
+
}
|
|
166
|
+
if (a.kind === "object") {
|
|
167
|
+
const aKeys = Object.keys(a.properties || {}).sort();
|
|
168
|
+
const bKeys = Object.keys(b.properties || {}).sort();
|
|
169
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
170
|
+
for (let i = 0; i < aKeys.length; i += 1) {
|
|
171
|
+
if (aKeys[i] !== bKeys[i]) return false;
|
|
172
|
+
if (!typeEquals(a.properties[aKeys[i]], b.properties[bKeys[i]])) return false;
|
|
173
|
+
}
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
if (a.kind === "union") {
|
|
177
|
+
if (a.types.length !== b.types.length) return false;
|
|
178
|
+
return a.types.every((candidate, index) => typeEquals(candidate, b.types[index]));
|
|
179
|
+
}
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function unionTypes(types) {
|
|
184
|
+
const expanded = [];
|
|
185
|
+
for (const type of types) {
|
|
186
|
+
if (!type) continue;
|
|
187
|
+
if (type.kind === "union") expanded.push(...type.types);
|
|
188
|
+
else expanded.push(type);
|
|
189
|
+
}
|
|
190
|
+
if (!expanded.length) return T_UNKNOWN;
|
|
191
|
+
if (expanded.some((type) => type.kind === "any")) return T_ANY;
|
|
192
|
+
|
|
193
|
+
const unique = [];
|
|
194
|
+
for (const candidate of expanded) {
|
|
195
|
+
if (!unique.some((existing) => typeEquals(existing, candidate))) unique.push(candidate);
|
|
196
|
+
}
|
|
197
|
+
if (unique.length === 1) return unique[0];
|
|
198
|
+
return { kind: "union", types: unique.sort((a, b) => typeToString(a).localeCompare(typeToString(b))) };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function copyType(type) {
|
|
202
|
+
if (!type || typeof type !== "object") return T_UNKNOWN;
|
|
203
|
+
if (type.kind === "union") return unionTypes(type.types.map(copyType));
|
|
204
|
+
if (type.kind === "array") return makeArrayType(copyType(type.elementType));
|
|
205
|
+
if (type.kind === "object") {
|
|
206
|
+
const out = {};
|
|
207
|
+
for (const [key, value] of Object.entries(type.properties || {})) out[key] = copyType(value);
|
|
208
|
+
return makeObjectType(out);
|
|
209
|
+
}
|
|
210
|
+
if (type.kind === "function") return makeFnType(type.params.map(copyType), copyType(type.returnType), type.minArgs, type.maxArgs);
|
|
211
|
+
return { ...type };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isAssignable(source, target) {
|
|
215
|
+
if (!source || !target) return true;
|
|
216
|
+
if (source.kind === "any" || target.kind === "any") return true;
|
|
217
|
+
if (source.kind === "unknown" || target.kind === "unknown") return true;
|
|
218
|
+
if (target.kind === "union") return target.types.some((candidate) => isAssignable(source, candidate));
|
|
219
|
+
if (source.kind === "union") return source.types.every((candidate) => isAssignable(candidate, target));
|
|
220
|
+
if (source.kind === target.kind) {
|
|
221
|
+
if (source.kind === "array") return isAssignable(source.elementType, target.elementType);
|
|
222
|
+
if (source.kind === "object") {
|
|
223
|
+
const targetProps = target.properties || {};
|
|
224
|
+
const sourceProps = source.properties || {};
|
|
225
|
+
for (const [key, targetType] of Object.entries(targetProps)) {
|
|
226
|
+
if (!(key in sourceProps)) return false;
|
|
227
|
+
if (!isAssignable(sourceProps[key], targetType)) return false;
|
|
228
|
+
}
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
if (source.kind === "function") {
|
|
232
|
+
if (source.minArgs > target.maxArgs) return false;
|
|
233
|
+
if (source.maxArgs < target.minArgs) return false;
|
|
234
|
+
return isAssignable(source.returnType, target.returnType);
|
|
235
|
+
}
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
if (source.kind === "null" && target.kind === "object") return true;
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
class Scope {
|
|
243
|
+
constructor(parent = null, kind = "block") {
|
|
244
|
+
this.id = ++scopeCounter;
|
|
245
|
+
this.parent = parent;
|
|
246
|
+
this.kind = kind;
|
|
247
|
+
this.symbols = new Map();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
declare(symbol) {
|
|
251
|
+
this.symbols.set(symbol.name, symbol);
|
|
252
|
+
return symbol;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
lookup(name) {
|
|
256
|
+
if (this.symbols.has(name)) return this.symbols.get(name);
|
|
257
|
+
if (this.parent) return this.parent.lookup(name);
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function createSymbol({ name, type, kind, mutable, span, file, runtime = "universal", runtimes = null }) {
|
|
263
|
+
const normalizedRuntimes = Array.isArray(runtimes) && runtimes.length ? [...new Set(runtimes)] : [runtime || "universal"];
|
|
264
|
+
return { name, type: type || T_UNKNOWN, kind, mutable, span, file, runtime: normalizedRuntimes[0], runtimes: normalizedRuntimes };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function inferAllowedRuntimeContextsFromFile(file = "") {
|
|
268
|
+
const normalized = String(file).replace(/\\/g, "/").toLowerCase();
|
|
269
|
+
if (normalized.endsWith(".client.fs")) return ["universal", "browser"];
|
|
270
|
+
if (normalized.endsWith(".server.fs")) return ["universal", "server"];
|
|
271
|
+
if (normalized.endsWith(".edge.fs")) return ["universal", "edge"];
|
|
272
|
+
if (/(^|\/)(app\/)?api\//.test(normalized)) return ["universal", "server"];
|
|
273
|
+
if (/(^|\/)(app\/)?pages\//.test(normalized)) return ["universal", "browser", "server"];
|
|
274
|
+
return ["universal"];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function isRuntimeCompatible(symbolRuntimes = ["universal"], allowedContexts = ["universal"]) {
|
|
278
|
+
const runtimes = Array.isArray(symbolRuntimes) && symbolRuntimes.length ? symbolRuntimes : ["universal"];
|
|
279
|
+
const contexts = Array.isArray(allowedContexts) && allowedContexts.length ? allowedContexts : ["universal"];
|
|
280
|
+
if (runtimes.includes("universal")) return true;
|
|
281
|
+
return runtimes.some((runtime) => contexts.includes(runtime));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function collectPatternBindings(pattern, out = []) {
|
|
285
|
+
if (!pattern) return out;
|
|
286
|
+
switch (pattern.type) {
|
|
287
|
+
case "Identifier":
|
|
288
|
+
out.push({ name: pattern.name, node: pattern });
|
|
289
|
+
break;
|
|
290
|
+
case "AssignmentPattern":
|
|
291
|
+
collectPatternBindings(pattern.left, out);
|
|
292
|
+
break;
|
|
293
|
+
case "RestElement":
|
|
294
|
+
collectPatternBindings(pattern.argument, out);
|
|
295
|
+
break;
|
|
296
|
+
case "ArrayPattern":
|
|
297
|
+
for (const element of pattern.elements || []) collectPatternBindings(element, out);
|
|
298
|
+
break;
|
|
299
|
+
case "ObjectPattern":
|
|
300
|
+
for (const property of pattern.properties || []) {
|
|
301
|
+
if (property.type === "RestElement") collectPatternBindings(property.argument, out);
|
|
302
|
+
else collectPatternBindings(property.value || property.key, out);
|
|
303
|
+
}
|
|
304
|
+
break;
|
|
305
|
+
default:
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
return out;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function declareBuiltins(scope, file) {
|
|
312
|
+
scope.declare(createSymbol({ name: "Number", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_NUMBER, 1, 1) }));
|
|
313
|
+
scope.declare(createSymbol({ name: "String", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_STRING, 1, 1) }));
|
|
314
|
+
scope.declare(createSymbol({ name: "Boolean", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_BOOLEAN, 1, 1) }));
|
|
315
|
+
scope.declare(createSymbol({ name: "Array", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
316
|
+
scope.declare(createSymbol({ name: "Object", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
317
|
+
scope.declare(createSymbol({ name: "JSON", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
318
|
+
scope.declare(createSymbol({ name: "Math", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
319
|
+
scope.declare(createSymbol({ name: "Date", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
320
|
+
scope.declare(createSymbol({ name: "Promise", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
321
|
+
scope.declare(createSymbol({ name: "Set", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
322
|
+
scope.declare(createSymbol({ name: "Map", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
323
|
+
scope.declare(createSymbol({ name: "Error", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
324
|
+
scope.declare(createSymbol({ name: "RegExp", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
325
|
+
scope.declare(createSymbol({ name: "Intl", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
326
|
+
scope.declare(createSymbol({ name: "URL", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_OBJECT, 1, Number.MAX_SAFE_INTEGER) }));
|
|
327
|
+
scope.declare(createSymbol({ name: "process", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "server" }));
|
|
328
|
+
scope.declare(createSymbol({ name: "Buffer", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "server" }));
|
|
329
|
+
scope.declare(createSymbol({ name: "__dirname", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_STRING, runtime: "server" }));
|
|
330
|
+
scope.declare(createSymbol({ name: "__filename", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_STRING, runtime: "server" }));
|
|
331
|
+
scope.declare(createSymbol({ name: "require", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_UNKNOWN, 1, Number.MAX_SAFE_INTEGER), runtime: "server" }));
|
|
332
|
+
scope.declare(createSymbol({ name: "module", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "server" }));
|
|
333
|
+
scope.declare(createSymbol({ name: "exports", kind: "builtin", mutable: true, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "server" }));
|
|
334
|
+
scope.declare(createSymbol({ name: "globalThis", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
335
|
+
scope.declare(createSymbol({ name: "window", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "browser" }));
|
|
336
|
+
scope.declare(createSymbol({ name: "document", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "browser" }));
|
|
337
|
+
scope.declare(createSymbol({ name: "location", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "browser" }));
|
|
338
|
+
scope.declare(createSymbol({ name: "navigator", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtimes: ["browser", "worker"] }));
|
|
339
|
+
scope.declare(createSymbol({ name: "history", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "browser" }));
|
|
340
|
+
scope.declare(createSymbol({ name: "localStorage", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "browser" }));
|
|
341
|
+
scope.declare(createSymbol({ name: "sessionStorage", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "browser" }));
|
|
342
|
+
scope.declare(createSymbol({ name: "indexedDB", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtimes: ["browser", "worker"] }));
|
|
343
|
+
scope.declare(createSymbol({ name: "self", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtimes: ["browser", "worker", "edge"] }));
|
|
344
|
+
scope.declare(createSymbol({ name: "console", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
345
|
+
scope.declare(createSymbol({ name: "Request", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
346
|
+
scope.declare(createSymbol({ name: "Response", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
347
|
+
scope.declare(createSymbol({ name: "Headers", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
348
|
+
scope.declare(createSymbol({ name: "FormData", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
349
|
+
scope.declare(createSymbol({ name: "AbortController", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
350
|
+
scope.declare(createSymbol({ name: "AbortSignal", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
351
|
+
scope.declare(createSymbol({ name: "Blob", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
352
|
+
scope.declare(createSymbol({ name: "File", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
353
|
+
scope.declare(createSymbol({ name: "ReadableStream", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
354
|
+
scope.declare(createSymbol({ name: "WritableStream", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
355
|
+
scope.declare(createSymbol({ name: "TransformStream", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
356
|
+
scope.declare(createSymbol({ name: "crypto", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
|
|
357
|
+
scope.declare(createSymbol({ name: "Event", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "browser" }));
|
|
358
|
+
scope.declare(createSymbol({ name: "CustomEvent", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "browser" }));
|
|
359
|
+
scope.declare(createSymbol({ name: "TextEncoder", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([], T_OBJECT, 0, 0) }));
|
|
360
|
+
scope.declare(createSymbol({ name: "TextDecoder", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([], T_OBJECT, 0, 0) }));
|
|
361
|
+
scope.declare(createSymbol({ name: "setTimeout", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_NUMBER, 1, Number.MAX_SAFE_INTEGER) }));
|
|
362
|
+
scope.declare(createSymbol({ name: "clearTimeout", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_VOID, 1, 1) }));
|
|
363
|
+
scope.declare(createSymbol({ name: "setInterval", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_NUMBER, 1, Number.MAX_SAFE_INTEGER) }));
|
|
364
|
+
scope.declare(createSymbol({ name: "clearInterval", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_VOID, 1, 1) }));
|
|
365
|
+
scope.declare(createSymbol({ name: "queueMicrotask", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_VOID, 1, 1) }));
|
|
366
|
+
scope.declare(createSymbol({ name: "URLSearchParams", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_OBJECT, 1, 1) }));
|
|
367
|
+
scope.declare(createSymbol({ name: "fetch", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_UNKNOWN, 1, Number.MAX_SAFE_INTEGER) }));
|
|
368
|
+
scope.declare(createSymbol({ name: "atob", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_STRING, 1, 1) }));
|
|
369
|
+
scope.declare(createSymbol({ name: "btoa", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_STRING, 1, 1) }));
|
|
370
|
+
scope.declare(createSymbol({ name: "encodeURIComponent", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_STRING, 1, 1) }));
|
|
371
|
+
scope.declare(createSymbol({ name: "decodeURIComponent", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_STRING, 1, 1) }));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function symbolSuggestion(name, scope) {
|
|
375
|
+
const candidates = [];
|
|
376
|
+
let cursor = scope;
|
|
377
|
+
while (cursor) {
|
|
378
|
+
for (const candidate of cursor.symbols.keys()) candidates.push(candidate);
|
|
379
|
+
cursor = cursor.parent;
|
|
380
|
+
}
|
|
381
|
+
const lowered = String(name || "").toLowerCase();
|
|
382
|
+
const hits = [...new Set(candidates)]
|
|
383
|
+
.filter((candidate) => candidate.toLowerCase().startsWith(lowered[0] || ""))
|
|
384
|
+
.slice(0, 3);
|
|
385
|
+
return hits.length ? hits.join(", ") : "";
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function reportUnknownSymbol(identifier, scope, state) {
|
|
389
|
+
const span = spanFromNode(identifier, state.source.length);
|
|
390
|
+
const suggestion = symbolSuggestion(identifier.name, scope);
|
|
391
|
+
state.diagnostics.push(
|
|
392
|
+
createDiagnostic({
|
|
393
|
+
file: state.file,
|
|
394
|
+
source: state.source,
|
|
395
|
+
lineStarts: state.lineStarts,
|
|
396
|
+
code: "FS4101",
|
|
397
|
+
span,
|
|
398
|
+
message: `Unknown symbol \`${identifier.name}\`.`,
|
|
399
|
+
hint: suggestion ? `Did you mean: ${suggestion}` : resolveErrorMeta("FS4101").hint,
|
|
400
|
+
}),
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function reportRuntimeScopeError(identifier, symbol, state) {
|
|
405
|
+
const span = spanFromNode(identifier, state.source.length);
|
|
406
|
+
const available = Array.isArray(symbol.runtimes) && symbol.runtimes.length ? symbol.runtimes : [symbol.runtime || "universal"];
|
|
407
|
+
const context = (state.allowedRuntimes || ["universal"]).join("|");
|
|
408
|
+
state.diagnostics.push(
|
|
409
|
+
createDiagnostic({
|
|
410
|
+
file: state.file,
|
|
411
|
+
source: state.source,
|
|
412
|
+
lineStarts: state.lineStarts,
|
|
413
|
+
code: "FS4201",
|
|
414
|
+
span,
|
|
415
|
+
message: `Symbol \`${identifier.name}\` is not available in ${context} context.`,
|
|
416
|
+
hint: `This symbol is only available in: ${available.join(", ")}`,
|
|
417
|
+
}),
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function inferFunctionFromNode(node, scope, state) {
|
|
422
|
+
const params = [];
|
|
423
|
+
let required = 0;
|
|
424
|
+
for (const param of node.params || []) {
|
|
425
|
+
if (param.type === "AssignmentPattern") {
|
|
426
|
+
params.push(T_UNKNOWN);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
required += 1;
|
|
430
|
+
params.push(T_UNKNOWN);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const fnScope = new Scope(scope, "function");
|
|
434
|
+
state.scopes.push(fnScope);
|
|
435
|
+
for (const param of node.params || []) {
|
|
436
|
+
const bindings = collectPatternBindings(param);
|
|
437
|
+
for (const binding of bindings) {
|
|
438
|
+
fnScope.declare(createSymbol({
|
|
439
|
+
name: binding.name,
|
|
440
|
+
type: T_UNKNOWN,
|
|
441
|
+
kind: "param",
|
|
442
|
+
mutable: true,
|
|
443
|
+
span: spanFromNode(binding.node, state.source.length),
|
|
444
|
+
file: state.file,
|
|
445
|
+
}));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const fnContext = { returns: [] };
|
|
450
|
+
if (node.body?.type === "BlockStatement") {
|
|
451
|
+
for (const statement of node.body.body || []) {
|
|
452
|
+
analyzeStatement(statement, fnScope, state, fnContext);
|
|
453
|
+
}
|
|
454
|
+
} else if (node.body) {
|
|
455
|
+
fnContext.returns.push(inferExpression(node.body, fnScope, state, fnContext));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const returnType = fnContext.returns.length ? unionTypes(fnContext.returns) : T_VOID;
|
|
459
|
+
return makeFnType(params, returnType, required, params.length);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function hoistDeclarations(body, scope, state) {
|
|
463
|
+
for (const statement of body || []) {
|
|
464
|
+
if (statement.type === "FunctionDeclaration" && statement.id?.name) {
|
|
465
|
+
scope.declare(createSymbol({
|
|
466
|
+
name: statement.id.name,
|
|
467
|
+
type: makeFnType(new Array(statement.params?.length || 0).fill(T_UNKNOWN), T_UNKNOWN),
|
|
468
|
+
kind: "function",
|
|
469
|
+
mutable: false,
|
|
470
|
+
span: spanFromNode(statement.id, state.source.length),
|
|
471
|
+
file: state.file,
|
|
472
|
+
}));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (statement.type === "VariableDeclaration" && statement.kind === "var") {
|
|
476
|
+
for (const declaration of statement.declarations || []) {
|
|
477
|
+
const bindings = collectPatternBindings(declaration.id);
|
|
478
|
+
for (const binding of bindings) {
|
|
479
|
+
if (scope.symbols.has(binding.name)) continue;
|
|
480
|
+
scope.declare(createSymbol({
|
|
481
|
+
name: binding.name,
|
|
482
|
+
type: T_UNKNOWN,
|
|
483
|
+
kind: "var",
|
|
484
|
+
mutable: true,
|
|
485
|
+
span: spanFromNode(binding.node, state.source.length),
|
|
486
|
+
file: state.file,
|
|
487
|
+
}));
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function inferExpression(node, scope, state, fnContext) {
|
|
495
|
+
if (!node) return T_UNKNOWN;
|
|
496
|
+
|
|
497
|
+
switch (node.type) {
|
|
498
|
+
case "Literal":
|
|
499
|
+
return typeFromLiteral(node.value);
|
|
500
|
+
case "TemplateLiteral":
|
|
501
|
+
for (const expression of node.expressions || []) inferExpression(expression, scope, state, fnContext);
|
|
502
|
+
return T_STRING;
|
|
503
|
+
case "Identifier": {
|
|
504
|
+
const symbol = scope.lookup(node.name);
|
|
505
|
+
if (!symbol) {
|
|
506
|
+
reportUnknownSymbol(node, scope, state);
|
|
507
|
+
return T_UNKNOWN;
|
|
508
|
+
}
|
|
509
|
+
if (!isRuntimeCompatible(symbol.runtimes, state.allowedRuntimes)) {
|
|
510
|
+
reportRuntimeScopeError(node, symbol, state);
|
|
511
|
+
}
|
|
512
|
+
return copyType(symbol.type);
|
|
513
|
+
}
|
|
514
|
+
case "ArrayExpression": {
|
|
515
|
+
const elementTypes = (node.elements || []).filter(Boolean).map((element) => inferExpression(element, scope, state, fnContext));
|
|
516
|
+
return makeArrayType(elementTypes.length ? unionTypes(elementTypes) : T_UNKNOWN);
|
|
517
|
+
}
|
|
518
|
+
case "ObjectExpression": {
|
|
519
|
+
const props = {};
|
|
520
|
+
for (const property of node.properties || []) {
|
|
521
|
+
if (property.type === "Property") {
|
|
522
|
+
let keyName = null;
|
|
523
|
+
if (property.computed) {
|
|
524
|
+
inferExpression(property.key, scope, state, fnContext);
|
|
525
|
+
} else if (property.key.type === "Identifier") {
|
|
526
|
+
keyName = property.key.name;
|
|
527
|
+
} else if (property.key.type === "Literal") {
|
|
528
|
+
keyName = String(property.key.value);
|
|
529
|
+
}
|
|
530
|
+
const valueType = inferExpression(property.value, scope, state, fnContext);
|
|
531
|
+
if (keyName) props[keyName] = valueType;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return makeObjectType(props);
|
|
535
|
+
}
|
|
536
|
+
case "UnaryExpression": {
|
|
537
|
+
const argType = inferExpression(node.argument, scope, state, fnContext);
|
|
538
|
+
if (["+", "-", "~"].includes(node.operator)) {
|
|
539
|
+
if (!isAssignable(argType, T_NUMBER)) {
|
|
540
|
+
state.diagnostics.push(createDiagnostic({
|
|
541
|
+
file: state.file,
|
|
542
|
+
source: state.source,
|
|
543
|
+
lineStarts: state.lineStarts,
|
|
544
|
+
code: "FS4107",
|
|
545
|
+
span: spanFromNode(node.argument, state.source.length),
|
|
546
|
+
message: `Operator \`${node.operator}\` expects a numeric operand, got ${typeToString(argType)}.`,
|
|
547
|
+
}));
|
|
548
|
+
}
|
|
549
|
+
return T_NUMBER;
|
|
550
|
+
}
|
|
551
|
+
if (node.operator === "!") return T_BOOLEAN;
|
|
552
|
+
if (node.operator === "typeof") return T_STRING;
|
|
553
|
+
if (node.operator === "void") return T_UNDEFINED;
|
|
554
|
+
return T_UNKNOWN;
|
|
555
|
+
}
|
|
556
|
+
case "BinaryExpression": {
|
|
557
|
+
const leftType = inferExpression(node.left, scope, state, fnContext);
|
|
558
|
+
const rightType = inferExpression(node.right, scope, state, fnContext);
|
|
559
|
+
if (node.operator === "+") {
|
|
560
|
+
if (leftType.kind === "string" || rightType.kind === "string") return T_STRING;
|
|
561
|
+
if (!isAssignable(leftType, T_NUMBER) || !isAssignable(rightType, T_NUMBER)) {
|
|
562
|
+
state.diagnostics.push(createDiagnostic({
|
|
563
|
+
file: state.file,
|
|
564
|
+
source: state.source,
|
|
565
|
+
lineStarts: state.lineStarts,
|
|
566
|
+
code: "FS4107",
|
|
567
|
+
span: spanFromNode(node, state.source.length),
|
|
568
|
+
message: `Operator \`+\` expected number or string-compatible operands, got ${typeToString(leftType)} and ${typeToString(rightType)}.`,
|
|
569
|
+
}));
|
|
570
|
+
}
|
|
571
|
+
return T_NUMBER;
|
|
572
|
+
}
|
|
573
|
+
if (["-", "*", "/", "%", "**", "<", ">", "<=", ">="].includes(node.operator)) {
|
|
574
|
+
if (!isAssignable(leftType, T_NUMBER) || !isAssignable(rightType, T_NUMBER)) {
|
|
575
|
+
state.diagnostics.push(createDiagnostic({
|
|
576
|
+
file: state.file,
|
|
577
|
+
source: state.source,
|
|
578
|
+
lineStarts: state.lineStarts,
|
|
579
|
+
code: "FS4107",
|
|
580
|
+
span: spanFromNode(node, state.source.length),
|
|
581
|
+
message: `Operator \`${node.operator}\` requires numeric operands, got ${typeToString(leftType)} and ${typeToString(rightType)}.`,
|
|
582
|
+
}));
|
|
583
|
+
}
|
|
584
|
+
return ["<", ">", "<=", ">="].includes(node.operator) ? T_BOOLEAN : T_NUMBER;
|
|
585
|
+
}
|
|
586
|
+
if (["==", "!=", "===", "!=="].includes(node.operator)) return T_BOOLEAN;
|
|
587
|
+
return T_UNKNOWN;
|
|
588
|
+
}
|
|
589
|
+
case "LogicalExpression": {
|
|
590
|
+
const leftType = inferExpression(node.left, scope, state, fnContext);
|
|
591
|
+
const rightType = inferExpression(node.right, scope, state, fnContext);
|
|
592
|
+
if (node.operator === "&&" || node.operator === "||" || node.operator === "??") return unionTypes([leftType, rightType]);
|
|
593
|
+
return T_UNKNOWN;
|
|
1024
594
|
}
|
|
1025
|
-
case "
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
scope
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
if (node.
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
name:
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
case "
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
if (
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
595
|
+
case "ConditionalExpression": {
|
|
596
|
+
inferExpression(node.test, scope, state, fnContext);
|
|
597
|
+
return unionTypes([
|
|
598
|
+
inferExpression(node.consequent, scope, state, fnContext),
|
|
599
|
+
inferExpression(node.alternate, scope, state, fnContext),
|
|
600
|
+
]);
|
|
601
|
+
}
|
|
602
|
+
case "AssignmentExpression": {
|
|
603
|
+
const valueType = inferExpression(node.right, scope, state, fnContext);
|
|
604
|
+
if (node.left.type === "Identifier") {
|
|
605
|
+
const symbol = scope.lookup(node.left.name);
|
|
606
|
+
if (!symbol) {
|
|
607
|
+
reportUnknownSymbol(node.left, scope, state);
|
|
608
|
+
return valueType;
|
|
609
|
+
}
|
|
610
|
+
if (!symbol.mutable) {
|
|
611
|
+
state.diagnostics.push(createDiagnostic({
|
|
612
|
+
file: state.file,
|
|
613
|
+
source: state.source,
|
|
614
|
+
lineStarts: state.lineStarts,
|
|
615
|
+
code: "FS4102",
|
|
616
|
+
span: spanFromNode(node.left, state.source.length),
|
|
617
|
+
message: `Cannot assign to constant binding \`${node.left.name}\`.`,
|
|
618
|
+
related: [{ message: `\`${node.left.name}\` was declared here.`, span: symbol.span, file: symbol.file }],
|
|
619
|
+
}));
|
|
620
|
+
return valueType;
|
|
621
|
+
}
|
|
622
|
+
if (symbol.type.kind !== "unknown" && symbol.type.kind !== "any" && !isAssignable(valueType, symbol.type)) {
|
|
623
|
+
state.diagnostics.push(createDiagnostic({
|
|
624
|
+
file: state.file,
|
|
625
|
+
source: state.source,
|
|
626
|
+
lineStarts: state.lineStarts,
|
|
627
|
+
code: "FS4103",
|
|
628
|
+
span: spanFromNode(node.right, state.source.length),
|
|
629
|
+
message: `Cannot assign ${typeToString(valueType)} to \`${symbol.name}\` (${typeToString(symbol.type)}).`,
|
|
630
|
+
related: [{ message: `\`${symbol.name}\` declared with type ${typeToString(symbol.type)}.`, span: symbol.span, file: symbol.file }],
|
|
631
|
+
}));
|
|
632
|
+
} else if (symbol.type.kind === "unknown") {
|
|
633
|
+
symbol.type = valueType;
|
|
634
|
+
} else {
|
|
635
|
+
symbol.type = unionTypes([symbol.type, valueType]);
|
|
636
|
+
}
|
|
637
|
+
} else if (node.left.type === "MemberExpression") {
|
|
638
|
+
const objectType = inferExpression(node.left.object, scope, state, fnContext);
|
|
639
|
+
const propName = !node.left.computed && node.left.property?.type === "Identifier"
|
|
640
|
+
? node.left.property.name
|
|
641
|
+
: (node.left.property?.type === "Literal" ? String(node.left.property.value) : null);
|
|
642
|
+
if (propName && objectType.kind === "object") {
|
|
643
|
+
const current = objectType.properties[propName];
|
|
644
|
+
objectType.properties[propName] = current ? unionTypes([current, valueType]) : valueType;
|
|
645
|
+
} else if (objectType.kind === "array" && propName === "length" && !isAssignable(valueType, T_NUMBER)) {
|
|
646
|
+
state.diagnostics.push(createDiagnostic({
|
|
647
|
+
file: state.file,
|
|
648
|
+
source: state.source,
|
|
649
|
+
lineStarts: state.lineStarts,
|
|
650
|
+
code: "FS4103",
|
|
651
|
+
span: spanFromNode(node.right, state.source.length),
|
|
652
|
+
message: `Cannot assign ${typeToString(valueType)} to array length (number).`,
|
|
653
|
+
}));
|
|
654
|
+
}
|
|
655
|
+
} else {
|
|
656
|
+
inferExpression(node.left, scope, state, fnContext);
|
|
657
|
+
}
|
|
658
|
+
return valueType;
|
|
659
|
+
}
|
|
660
|
+
case "UpdateExpression": {
|
|
661
|
+
if (node.argument.type === "Identifier") {
|
|
662
|
+
const symbol = scope.lookup(node.argument.name);
|
|
663
|
+
if (!symbol) reportUnknownSymbol(node.argument, scope, state);
|
|
664
|
+
else if (!symbol.mutable) {
|
|
665
|
+
state.diagnostics.push(createDiagnostic({
|
|
666
|
+
file: state.file,
|
|
667
|
+
source: state.source,
|
|
668
|
+
lineStarts: state.lineStarts,
|
|
669
|
+
code: "FS4102",
|
|
670
|
+
span: spanFromNode(node.argument, state.source.length),
|
|
671
|
+
message: `Cannot update constant binding \`${node.argument.name}\`.`,
|
|
672
|
+
related: [{ message: "Declared here.", span: symbol.span, file: symbol.file }],
|
|
673
|
+
}));
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
inferExpression(node.argument, scope, state, fnContext);
|
|
677
|
+
return T_NUMBER;
|
|
678
|
+
}
|
|
679
|
+
case "CallExpression": {
|
|
680
|
+
const argTypes = (node.arguments || []).map((arg) => inferExpression(arg, scope, state, fnContext));
|
|
681
|
+
if (node.callee?.type === "MemberExpression") {
|
|
682
|
+
const objectIdentifier = node.callee.object?.type === "Identifier" ? node.callee.object.name : null;
|
|
683
|
+
const staticPropName = !node.callee.computed && node.callee.property?.type === "Identifier"
|
|
684
|
+
? node.callee.property.name
|
|
685
|
+
: (node.callee.property?.type === "Literal" ? String(node.callee.property.value) : null);
|
|
686
|
+
if (objectIdentifier && staticPropName) {
|
|
687
|
+
if (objectIdentifier === "Array" && staticPropName === "from") return makeArrayType(T_UNKNOWN);
|
|
688
|
+
if (objectIdentifier === "Array" && staticPropName === "isArray") return T_BOOLEAN;
|
|
689
|
+
if (objectIdentifier === "Array" && staticPropName === "of") return makeArrayType(argTypes.length ? unionTypes(argTypes) : T_UNKNOWN);
|
|
690
|
+
if (objectIdentifier === "Object" && staticPropName === "keys") return makeArrayType(T_STRING);
|
|
691
|
+
if (objectIdentifier === "Object" && staticPropName === "values") return makeArrayType(T_UNKNOWN);
|
|
692
|
+
if (objectIdentifier === "Object" && staticPropName === "entries") return makeArrayType(makeArrayType(T_UNKNOWN));
|
|
693
|
+
if (objectIdentifier === "Object" && staticPropName === "assign") return argTypes[0] || T_OBJECT;
|
|
694
|
+
if (objectIdentifier === "Object" && staticPropName === "fromEntries") return T_OBJECT;
|
|
695
|
+
if (objectIdentifier === "Object" && staticPropName === "hasOwn") return T_BOOLEAN;
|
|
696
|
+
if (objectIdentifier === "Promise" && staticPropName === "all") return T_OBJECT;
|
|
697
|
+
if (objectIdentifier === "Promise" && ["allSettled", "any", "race", "resolve", "reject"].includes(staticPropName)) return T_OBJECT;
|
|
698
|
+
if (objectIdentifier === "Date" && staticPropName === "now") return T_NUMBER;
|
|
699
|
+
if (objectIdentifier === "Date" && ["parse", "UTC"].includes(staticPropName)) return T_NUMBER;
|
|
700
|
+
if (objectIdentifier === "JSON" && staticPropName === "parse") return T_UNKNOWN;
|
|
701
|
+
if (objectIdentifier === "JSON" && staticPropName === "stringify") return T_STRING;
|
|
702
|
+
if (objectIdentifier === "Math" && staticPropName === "max") return T_NUMBER;
|
|
703
|
+
if (objectIdentifier === "Math" && ["min", "abs", "floor", "ceil", "round", "trunc", "sqrt", "pow", "sign"].includes(staticPropName)) return T_NUMBER;
|
|
704
|
+
if (objectIdentifier === "String" && ["fromCharCode", "fromCodePoint", "raw"].includes(staticPropName)) return T_STRING;
|
|
705
|
+
if (objectIdentifier === "Number" && ["isFinite", "isInteger", "isNaN", "isSafeInteger"].includes(staticPropName)) return T_BOOLEAN;
|
|
706
|
+
if (objectIdentifier === "Number" && ["parseFloat", "parseInt"].includes(staticPropName)) return T_NUMBER;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const objectType = inferExpression(node.callee.object, scope, state, fnContext);
|
|
710
|
+
const propName = !node.callee.computed && node.callee.property?.type === "Identifier"
|
|
711
|
+
? node.callee.property.name
|
|
712
|
+
: (node.callee.property?.type === "Literal" ? String(node.callee.property.value) : null);
|
|
713
|
+
if (objectIdentifier && propName) {
|
|
714
|
+
if (objectIdentifier === "document" && ["querySelector", "getElementById", "createElement"].includes(propName)) return T_OBJECT;
|
|
715
|
+
if (objectIdentifier === "document" && ["querySelectorAll", "getElementsByClassName", "getElementsByTagName"].includes(propName)) return makeArrayType(T_OBJECT);
|
|
716
|
+
if (objectIdentifier === "document" && ["createTextNode", "createDocumentFragment"].includes(propName)) return T_OBJECT;
|
|
717
|
+
if (objectIdentifier === "history" && ["pushState", "replaceState", "back", "forward", "go"].includes(propName)) return T_VOID;
|
|
718
|
+
if (objectIdentifier === "localStorage" && propName === "getItem") return unionTypes([T_STRING, T_NULL]);
|
|
719
|
+
if (objectIdentifier === "localStorage" && ["setItem", "removeItem", "clear"].includes(propName)) return T_VOID;
|
|
720
|
+
if (objectIdentifier === "sessionStorage" && propName === "getItem") return unionTypes([T_STRING, T_NULL]);
|
|
721
|
+
if (objectIdentifier === "sessionStorage" && ["setItem", "removeItem", "clear"].includes(propName)) return T_VOID;
|
|
722
|
+
if (objectIdentifier === "window" && ["scrollTo", "scrollBy", "close", "open"].includes(propName)) return T_VOID;
|
|
723
|
+
if (objectIdentifier === "window" && propName === "addEventListener") return T_VOID;
|
|
724
|
+
if (objectIdentifier === "window" && propName === "removeEventListener") return T_VOID;
|
|
725
|
+
if (objectIdentifier === "window" && propName === "dispatchEvent") return T_BOOLEAN;
|
|
726
|
+
if (objectIdentifier === "location" && propName === "toString") return T_STRING;
|
|
727
|
+
}
|
|
728
|
+
if (objectType.kind === "array" && propName) {
|
|
729
|
+
if (propName === "push") {
|
|
730
|
+
for (let i = 0; i < argTypes.length; i += 1) {
|
|
731
|
+
if (!isAssignable(argTypes[i], objectType.elementType)) {
|
|
732
|
+
state.diagnostics.push(createDiagnostic({
|
|
733
|
+
file: state.file,
|
|
734
|
+
source: state.source,
|
|
735
|
+
lineStarts: state.lineStarts,
|
|
736
|
+
code: "FS4103",
|
|
737
|
+
span: spanFromNode(node.arguments[i], state.source.length),
|
|
738
|
+
message: `Array push expects ${typeToString(objectType.elementType)}, got ${typeToString(argTypes[i])}.`,
|
|
739
|
+
}));
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return T_NUMBER;
|
|
743
|
+
}
|
|
744
|
+
if (propName === "map" && argTypes[0]?.kind === "function") return makeArrayType(argTypes[0].returnType || T_UNKNOWN);
|
|
745
|
+
if (propName === "map") return makeArrayType(T_UNKNOWN);
|
|
746
|
+
if (propName === "filter") return makeArrayType(objectType.elementType);
|
|
747
|
+
if (propName === "slice") return makeArrayType(objectType.elementType);
|
|
748
|
+
if (propName === "concat") return makeArrayType(objectType.elementType);
|
|
749
|
+
if (propName === "flat") return makeArrayType(T_UNKNOWN);
|
|
750
|
+
if (propName === "flatMap") return makeArrayType(T_UNKNOWN);
|
|
751
|
+
if (propName === "find") return unionTypes([objectType.elementType, T_UNDEFINED]);
|
|
752
|
+
if (propName === "findIndex") return T_NUMBER;
|
|
753
|
+
if (propName === "some") return T_BOOLEAN;
|
|
754
|
+
if (propName === "every") return T_BOOLEAN;
|
|
755
|
+
if (propName === "forEach") return T_VOID;
|
|
756
|
+
if (propName === "reduce") return argTypes.length >= 2 ? argTypes[1] : objectType.elementType;
|
|
757
|
+
if (propName === "join") return T_STRING;
|
|
758
|
+
if (propName === "includes") return T_BOOLEAN;
|
|
759
|
+
if (propName === "indexOf" || propName === "lastIndexOf") return T_NUMBER;
|
|
760
|
+
if (propName === "pop") return unionTypes([objectType.elementType, T_UNDEFINED]);
|
|
761
|
+
}
|
|
762
|
+
if (objectType.kind === "string" && propName) {
|
|
763
|
+
if (["toString", "toLocaleString", "trim", "trimStart", "trimEnd", "trimLeft", "trimRight", "toLowerCase", "toUpperCase", "toLocaleLowerCase", "toLocaleUpperCase", "slice", "substring", "concat", "padStart", "padEnd", "repeat", "replace", "replaceAll"].includes(propName)) return T_STRING;
|
|
764
|
+
if (["includes", "startsWith", "endsWith"].includes(propName)) return T_BOOLEAN;
|
|
765
|
+
if (["indexOf", "lastIndexOf", "charCodeAt", "codePointAt", "search", "localeCompare"].includes(propName)) return T_NUMBER;
|
|
766
|
+
if (["split", "match", "matchAll"].includes(propName)) return makeArrayType(T_UNKNOWN);
|
|
767
|
+
if (propName === "charAt") return T_STRING;
|
|
768
|
+
}
|
|
769
|
+
if (objectType.kind === "object" && propName) {
|
|
770
|
+
if (["addEventListener", "removeEventListener"].includes(propName)) return T_VOID;
|
|
771
|
+
if (propName === "dispatchEvent") return T_BOOLEAN;
|
|
772
|
+
if (["querySelector", "closest"].includes(propName)) return T_OBJECT;
|
|
773
|
+
if (propName === "querySelectorAll") return makeArrayType(T_OBJECT);
|
|
774
|
+
if (["appendChild", "removeChild", "replaceChild", "insertBefore", "insertAdjacentElement"].includes(propName)) return T_OBJECT;
|
|
775
|
+
if (propName === "insertAdjacentHTML") return T_VOID;
|
|
776
|
+
if (propName === "remove") return T_VOID;
|
|
777
|
+
if (["setAttribute", "removeAttribute"].includes(propName)) return T_VOID;
|
|
778
|
+
if (propName === "getAttribute") return unionTypes([T_STRING, T_NULL]);
|
|
779
|
+
if (propName === "hasAttribute") return T_BOOLEAN;
|
|
780
|
+
if (["matches", "contains", "toggle"].includes(propName)) return T_BOOLEAN;
|
|
781
|
+
if (["add", "delete"].includes(propName)) return T_OBJECT;
|
|
782
|
+
if (["get", "has"].includes(propName)) return unionTypes([T_UNKNOWN, T_BOOLEAN]);
|
|
783
|
+
if (["append", "set", "sort", "forEach", "submit", "reset"].includes(propName)) return T_VOID;
|
|
784
|
+
if (propName === "getAll") return makeArrayType(T_UNKNOWN);
|
|
785
|
+
if (["entries", "keys", "values"].includes(propName)) return makeArrayType(T_UNKNOWN);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
const calleeType = inferExpression(node.callee, scope, state, fnContext);
|
|
789
|
+
if (calleeType.kind !== "function") {
|
|
790
|
+
if (calleeType.kind !== "unknown" && calleeType.kind !== "any") {
|
|
791
|
+
state.diagnostics.push(createDiagnostic({
|
|
792
|
+
file: state.file,
|
|
793
|
+
source: state.source,
|
|
794
|
+
lineStarts: state.lineStarts,
|
|
795
|
+
code: "FS4106",
|
|
796
|
+
span: spanFromNode(node.callee, state.source.length),
|
|
797
|
+
message: `Attempted to call value of type ${typeToString(calleeType)}.`,
|
|
798
|
+
}));
|
|
799
|
+
}
|
|
800
|
+
return T_UNKNOWN;
|
|
801
|
+
}
|
|
802
|
+
if (argTypes.length < calleeType.minArgs || argTypes.length > calleeType.maxArgs) {
|
|
803
|
+
const expected = calleeType.minArgs === calleeType.maxArgs ? `${calleeType.minArgs}` : `${calleeType.minArgs}-${calleeType.maxArgs}`;
|
|
804
|
+
state.diagnostics.push(createDiagnostic({
|
|
805
|
+
file: state.file,
|
|
806
|
+
source: state.source,
|
|
807
|
+
lineStarts: state.lineStarts,
|
|
808
|
+
code: "FS4104",
|
|
809
|
+
span: spanFromNode(node, state.source.length),
|
|
810
|
+
message: `Expected ${expected} argument(s), got ${argTypes.length}.`,
|
|
811
|
+
}));
|
|
812
|
+
}
|
|
813
|
+
for (let i = 0; i < Math.min(argTypes.length, calleeType.params.length); i += 1) {
|
|
814
|
+
if (!isAssignable(argTypes[i], calleeType.params[i])) {
|
|
815
|
+
state.diagnostics.push(createDiagnostic({
|
|
816
|
+
file: state.file,
|
|
817
|
+
source: state.source,
|
|
818
|
+
lineStarts: state.lineStarts,
|
|
819
|
+
code: "FS4103",
|
|
820
|
+
span: spanFromNode(node.arguments[i], state.source.length),
|
|
821
|
+
message: `Argument ${i + 1} expects ${typeToString(calleeType.params[i])}, got ${typeToString(argTypes[i])}.`,
|
|
822
|
+
}));
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
// Heuristic for common higher-order combinators:
|
|
826
|
+
// if a function returns a still-unknown function shape and receives a concrete function arg,
|
|
827
|
+
// propagate the argument's function type as the call result.
|
|
828
|
+
if (
|
|
829
|
+
calleeType.returnType?.kind === "function"
|
|
830
|
+
&& argTypes[0]?.kind === "function"
|
|
831
|
+
&& calleeType.returnType.params.every((param) => param?.kind === "unknown")
|
|
832
|
+
&& calleeType.returnType.returnType?.kind === "unknown"
|
|
833
|
+
) {
|
|
834
|
+
return copyType(argTypes[0]);
|
|
835
|
+
}
|
|
836
|
+
return copyType(calleeType.returnType);
|
|
837
|
+
}
|
|
838
|
+
case "FunctionExpression":
|
|
839
|
+
case "ArrowFunctionExpression":
|
|
840
|
+
return inferFunctionFromNode(node, scope, state);
|
|
841
|
+
case "MemberExpression":
|
|
842
|
+
{
|
|
843
|
+
const objectIdentifier = node.object?.type === "Identifier" ? node.object.name : null;
|
|
844
|
+
const objectType = inferExpression(node.object, scope, state, fnContext);
|
|
845
|
+
if (node.computed) {
|
|
846
|
+
const propType = inferExpression(node.property, scope, state, fnContext);
|
|
847
|
+
if (objectType.kind === "array" && isAssignable(propType, T_NUMBER)) return objectType.elementType;
|
|
848
|
+
return T_UNKNOWN;
|
|
849
|
+
}
|
|
850
|
+
const propName = node.property?.type === "Identifier" ? node.property.name : null;
|
|
851
|
+
if (!propName) return T_UNKNOWN;
|
|
852
|
+
if (objectIdentifier && objectType.kind === "object") {
|
|
853
|
+
if (objectIdentifier === "location" && ["href", "origin", "protocol", "host", "hostname", "port", "pathname", "search", "hash"].includes(propName)) return T_STRING;
|
|
854
|
+
if (objectIdentifier === "window" && ["innerWidth", "innerHeight"].includes(propName)) return T_NUMBER;
|
|
855
|
+
if (objectIdentifier === "window" && propName === "location") return T_OBJECT;
|
|
856
|
+
if (objectIdentifier === "window" && propName === "history") return T_OBJECT;
|
|
857
|
+
if (objectIdentifier === "window" && ["localStorage", "sessionStorage"].includes(propName)) return T_OBJECT;
|
|
858
|
+
if (objectIdentifier === "document" && ["body", "head", "documentElement"].includes(propName)) return T_OBJECT;
|
|
859
|
+
if (objectIdentifier === "document" && propName === "title") return T_STRING;
|
|
860
|
+
if (objectIdentifier === "document" && propName === "cookie") return T_STRING;
|
|
861
|
+
if (objectIdentifier === "document" && propName === "readyState") return T_STRING;
|
|
862
|
+
if (objectIdentifier === "history" && propName === "length") return T_NUMBER;
|
|
863
|
+
if (["localStorage", "sessionStorage"].includes(objectIdentifier) && propName === "length") return T_NUMBER;
|
|
864
|
+
}
|
|
865
|
+
if (objectType.kind === "array") {
|
|
866
|
+
if (propName === "length") return T_NUMBER;
|
|
867
|
+
return T_UNKNOWN;
|
|
868
|
+
}
|
|
869
|
+
if (objectType.kind === "string" && propName === "length") return T_NUMBER;
|
|
870
|
+
if (objectType.kind === "object" && objectType.properties?.[propName]) {
|
|
871
|
+
return copyType(objectType.properties[propName]);
|
|
872
|
+
}
|
|
873
|
+
return T_UNKNOWN;
|
|
874
|
+
}
|
|
875
|
+
case "AwaitExpression":
|
|
876
|
+
return inferExpression(node.argument, scope, state, fnContext);
|
|
877
|
+
case "NewExpression":
|
|
878
|
+
inferExpression(node.callee, scope, state, fnContext);
|
|
879
|
+
for (const arg of node.arguments || []) inferExpression(arg, scope, state, fnContext);
|
|
880
|
+
return T_OBJECT;
|
|
881
|
+
case "SequenceExpression":
|
|
882
|
+
if (!node.expressions?.length) return T_UNKNOWN;
|
|
883
|
+
for (let i = 0; i < node.expressions.length - 1; i += 1) inferExpression(node.expressions[i], scope, state, fnContext);
|
|
884
|
+
return inferExpression(node.expressions[node.expressions.length - 1], scope, state, fnContext);
|
|
885
|
+
case "ChainExpression":
|
|
886
|
+
return inferExpression(node.expression, scope, state, fnContext);
|
|
887
|
+
default:
|
|
888
|
+
return T_UNKNOWN;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function analyzeVariableDeclaration(node, scope, state, fnContext) {
|
|
893
|
+
function bindingTypeForPattern(pattern, valueType) {
|
|
894
|
+
if (!pattern) return valueType;
|
|
895
|
+
if (pattern.type === "Identifier") return valueType;
|
|
896
|
+
if (pattern.type === "AssignmentPattern") return bindingTypeForPattern(pattern.left, valueType);
|
|
897
|
+
if (pattern.type === "RestElement") return bindingTypeForPattern(pattern.argument, valueType);
|
|
898
|
+
if (pattern.type === "ArrayPattern") return valueType?.kind === "array" ? valueType.elementType || T_UNKNOWN : T_UNKNOWN;
|
|
899
|
+
if (pattern.type === "ObjectPattern") return T_UNKNOWN;
|
|
900
|
+
return valueType || T_UNKNOWN;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function declarePatternBindings(pattern, valueType, declarationKind) {
|
|
904
|
+
if (!pattern) return;
|
|
905
|
+
if (pattern.type === "Identifier") {
|
|
906
|
+
const existing = declarationKind === "var" ? scope.lookup(pattern.name) : scope.symbols.get(pattern.name);
|
|
907
|
+
if (existing && declarationKind !== "var") {
|
|
908
|
+
existing.type = unionTypes([existing.type, valueType || T_UNKNOWN]);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
scope.declare(createSymbol({
|
|
912
|
+
name: pattern.name,
|
|
913
|
+
type: valueType || T_UNKNOWN,
|
|
914
|
+
kind: declarationKind,
|
|
915
|
+
mutable: declarationKind !== "const",
|
|
916
|
+
span: spanFromNode(pattern, state.source.length),
|
|
917
|
+
file: state.file,
|
|
918
|
+
}));
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
if (pattern.type === "AssignmentPattern") {
|
|
922
|
+
declarePatternBindings(pattern.left, valueType, declarationKind);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
if (pattern.type === "RestElement") {
|
|
926
|
+
declarePatternBindings(pattern.argument, valueType, declarationKind);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
if (pattern.type === "ArrayPattern") {
|
|
930
|
+
const elementType = valueType?.kind === "array" ? valueType.elementType || T_UNKNOWN : T_UNKNOWN;
|
|
931
|
+
for (const element of pattern.elements || []) {
|
|
932
|
+
declarePatternBindings(element, bindingTypeForPattern(element, elementType), declarationKind);
|
|
933
|
+
}
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
if (pattern.type === "ObjectPattern") {
|
|
937
|
+
for (const property of pattern.properties || []) {
|
|
938
|
+
if (property.type === "RestElement") {
|
|
939
|
+
declarePatternBindings(property.argument, valueType?.kind === "object" ? valueType : T_UNKNOWN, declarationKind);
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
let keyName = null;
|
|
943
|
+
if (!property.computed && property.key?.type === "Identifier") keyName = property.key.name;
|
|
944
|
+
else if (property.key?.type === "Literal") keyName = String(property.key.value);
|
|
945
|
+
const propType = keyName && valueType?.kind === "object" ? valueType.properties?.[keyName] || T_UNKNOWN : T_UNKNOWN;
|
|
946
|
+
declarePatternBindings(property.value || property.key, bindingTypeForPattern(property.value || property.key, propType), declarationKind);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
for (const declaration of node.declarations || []) {
|
|
952
|
+
const initType = declaration.init ? inferExpression(declaration.init, scope, state, fnContext) : T_UNKNOWN;
|
|
953
|
+
declarePatternBindings(declaration.id, initType, node.kind);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function analyzeStatement(node, scope, state, fnContext) {
|
|
958
|
+
if (!node) return;
|
|
959
|
+
switch (node.type) {
|
|
960
|
+
case "BlockStatement": {
|
|
961
|
+
const blockScope = new Scope(scope, "block");
|
|
962
|
+
state.scopes.push(blockScope);
|
|
963
|
+
for (const statement of node.body || []) analyzeStatement(statement, blockScope, state, fnContext);
|
|
964
|
+
break;
|
|
965
|
+
}
|
|
966
|
+
case "VariableDeclaration":
|
|
967
|
+
analyzeVariableDeclaration(node, scope, state, fnContext);
|
|
968
|
+
break;
|
|
969
|
+
case "ExpressionStatement":
|
|
970
|
+
inferExpression(node.expression, scope, state, fnContext);
|
|
971
|
+
break;
|
|
972
|
+
case "ReturnStatement":
|
|
973
|
+
if (fnContext) {
|
|
974
|
+
const returnType = node.argument ? inferExpression(node.argument, scope, state, fnContext) : T_VOID;
|
|
975
|
+
fnContext.returns.push(returnType);
|
|
976
|
+
}
|
|
977
|
+
break;
|
|
978
|
+
case "IfStatement":
|
|
979
|
+
inferExpression(node.test, scope, state, fnContext);
|
|
980
|
+
analyzeStatement(node.consequent, new Scope(scope, "if"), state, fnContext);
|
|
981
|
+
if (node.alternate) analyzeStatement(node.alternate, new Scope(scope, "else"), state, fnContext);
|
|
982
|
+
break;
|
|
983
|
+
case "ForStatement": {
|
|
984
|
+
const forScope = new Scope(scope, "for");
|
|
985
|
+
state.scopes.push(forScope);
|
|
986
|
+
if (node.init) {
|
|
987
|
+
if (node.init.type === "VariableDeclaration") analyzeVariableDeclaration(node.init, forScope, state, fnContext);
|
|
988
|
+
else inferExpression(node.init, forScope, state, fnContext);
|
|
989
|
+
}
|
|
990
|
+
if (node.test) inferExpression(node.test, forScope, state, fnContext);
|
|
991
|
+
if (node.update) inferExpression(node.update, forScope, state, fnContext);
|
|
992
|
+
analyzeStatement(node.body, forScope, state, fnContext);
|
|
993
|
+
break;
|
|
994
|
+
}
|
|
995
|
+
case "ForInStatement":
|
|
996
|
+
case "ForOfStatement": {
|
|
997
|
+
const forScope = new Scope(scope, "for");
|
|
998
|
+
state.scopes.push(forScope);
|
|
999
|
+
if (node.left?.type === "VariableDeclaration") analyzeVariableDeclaration(node.left, forScope, state, fnContext);
|
|
1000
|
+
else if (node.left) inferExpression(node.left, forScope, state, fnContext);
|
|
1001
|
+
inferExpression(node.right, forScope, state, fnContext);
|
|
1002
|
+
analyzeStatement(node.body, forScope, state, fnContext);
|
|
1003
|
+
break;
|
|
1004
|
+
}
|
|
1005
|
+
case "WhileStatement":
|
|
1006
|
+
case "DoWhileStatement":
|
|
1007
|
+
inferExpression(node.test, scope, state, fnContext);
|
|
1008
|
+
analyzeStatement(node.body, new Scope(scope, "loop"), state, fnContext);
|
|
1009
|
+
break;
|
|
1010
|
+
case "FunctionDeclaration": {
|
|
1011
|
+
if (node.id?.name) {
|
|
1012
|
+
const symbol = scope.lookup(node.id.name) || scope.declare(createSymbol({
|
|
1013
|
+
name: node.id.name,
|
|
1014
|
+
type: T_UNKNOWN,
|
|
1015
|
+
kind: "function",
|
|
1016
|
+
mutable: false,
|
|
1017
|
+
span: spanFromNode(node.id, state.source.length),
|
|
1018
|
+
file: state.file,
|
|
1019
|
+
}));
|
|
1020
|
+
symbol.type = inferFunctionFromNode(node, scope, state);
|
|
1021
|
+
} else {
|
|
1022
|
+
inferFunctionFromNode(node, scope, state);
|
|
1023
|
+
}
|
|
1024
|
+
break;
|
|
1025
|
+
}
|
|
1026
|
+
case "ImportDeclaration":
|
|
1027
|
+
for (const specifier of node.specifiers || []) {
|
|
1028
|
+
const local = specifier.local;
|
|
1029
|
+
if (!local?.name) continue;
|
|
1030
|
+
scope.declare(createSymbol({
|
|
1031
|
+
name: local.name,
|
|
1032
|
+
type: T_UNKNOWN,
|
|
1033
|
+
kind: "import",
|
|
1034
|
+
mutable: false,
|
|
1035
|
+
span: spanFromNode(local, state.source.length),
|
|
1036
|
+
file: state.file,
|
|
1037
|
+
}));
|
|
1038
|
+
}
|
|
1039
|
+
break;
|
|
1040
|
+
case "ExportNamedDeclaration":
|
|
1041
|
+
case "ExportDefaultDeclaration":
|
|
1042
|
+
if (node.declaration) analyzeStatement(node.declaration, scope, state, fnContext);
|
|
1043
|
+
break;
|
|
1044
|
+
case "SwitchStatement":
|
|
1045
|
+
inferExpression(node.discriminant, scope, state, fnContext);
|
|
1046
|
+
for (const switchCase of node.cases || []) {
|
|
1047
|
+
if (switchCase.test) inferExpression(switchCase.test, scope, state, fnContext);
|
|
1048
|
+
for (const statement of switchCase.consequent || []) analyzeStatement(statement, new Scope(scope, "switch"), state, fnContext);
|
|
1049
|
+
}
|
|
1050
|
+
break;
|
|
1051
|
+
case "TryStatement":
|
|
1052
|
+
analyzeStatement(node.block, new Scope(scope, "try"), state, fnContext);
|
|
1053
|
+
if (node.handler) {
|
|
1054
|
+
const catchScope = new Scope(scope, "catch");
|
|
1055
|
+
state.scopes.push(catchScope);
|
|
1056
|
+
if (node.handler.param?.type === "Identifier") {
|
|
1057
|
+
catchScope.declare(createSymbol({
|
|
1058
|
+
name: node.handler.param.name,
|
|
1059
|
+
type: T_UNKNOWN,
|
|
1060
|
+
kind: "catch",
|
|
1061
|
+
mutable: true,
|
|
1062
|
+
span: spanFromNode(node.handler.param, state.source.length),
|
|
1063
|
+
file: state.file,
|
|
1064
|
+
}));
|
|
1065
|
+
}
|
|
1066
|
+
analyzeStatement(node.handler.body, catchScope, state, fnContext);
|
|
1067
|
+
}
|
|
1068
|
+
if (node.finalizer) analyzeStatement(node.finalizer, new Scope(scope, "finally"), state, fnContext);
|
|
1069
|
+
break;
|
|
1070
|
+
case "ThrowStatement":
|
|
1071
|
+
if (node.argument) inferExpression(node.argument, scope, state, fnContext);
|
|
1072
|
+
break;
|
|
1073
|
+
case "ClassDeclaration":
|
|
1074
|
+
if (node.id?.name) {
|
|
1075
|
+
scope.declare(createSymbol({
|
|
1076
|
+
name: node.id.name,
|
|
1077
|
+
type: T_OBJECT,
|
|
1078
|
+
kind: "class",
|
|
1079
|
+
mutable: false,
|
|
1080
|
+
span: spanFromNode(node.id, state.source.length),
|
|
1081
|
+
file: state.file,
|
|
1082
|
+
}));
|
|
1083
|
+
}
|
|
1084
|
+
break;
|
|
1085
|
+
default:
|
|
1086
|
+
break;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function analyzeFileTypes(file, source) {
|
|
1091
|
+
const text = String(source ?? "");
|
|
1092
|
+
const prepared = stripTypeScriptHints(text);
|
|
1093
|
+
const lineStarts = createLineStarts(prepared);
|
|
1094
|
+
const parsed = parseFastScript(prepared, { file, mode: "lenient", recover: true });
|
|
1095
|
+
const diagnostics = [...parsed.diagnostics];
|
|
1096
|
+
|
|
1097
|
+
if (!parsed.estree || !Array.isArray(parsed.estree.body)) {
|
|
1098
|
+
return { file, diagnostics, scopes: [], symbols: [], astVersion: parsed.version };
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
scopeCounter = 0;
|
|
1102
|
+
const rootScope = new Scope(null, "module");
|
|
1103
|
+
const state = { file, source: prepared, lineStarts, diagnostics, scopes: [rootScope], allowedRuntimes: inferAllowedRuntimeContextsFromFile(file) };
|
|
1104
|
+
|
|
1105
|
+
declareBuiltins(rootScope, file);
|
|
1106
|
+
hoistDeclarations(parsed.estree.body, rootScope, state);
|
|
1107
|
+
for (const statement of parsed.estree.body) analyzeStatement(statement, rootScope, state, null);
|
|
1108
|
+
|
|
1109
|
+
const symbols = [];
|
|
1110
|
+
for (const scope of state.scopes) {
|
|
1111
|
+
for (const symbol of scope.symbols.values()) {
|
|
1112
|
+
symbols.push({
|
|
1113
|
+
scopeId: scope.id,
|
|
1114
|
+
scopeKind: scope.kind,
|
|
1115
|
+
name: symbol.name,
|
|
1116
|
+
kind: symbol.kind,
|
|
1117
|
+
mutable: symbol.mutable,
|
|
1118
|
+
type: typeToString(symbol.type),
|
|
1119
|
+
span: symbol.span,
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
return {
|
|
1125
|
+
file,
|
|
1126
|
+
diagnostics: dedupeDiagnostics(state.diagnostics),
|
|
1127
|
+
scopes: state.scopes.map((scope) => ({
|
|
1128
|
+
id: scope.id,
|
|
1129
|
+
kind: scope.kind,
|
|
1130
|
+
parentId: scope.parent?.id || null,
|
|
1131
|
+
symbols: [...scope.symbols.values()].map((symbol) => ({
|
|
1132
|
+
name: symbol.name,
|
|
1133
|
+
kind: symbol.kind,
|
|
1134
|
+
mutable: symbol.mutable,
|
|
1135
|
+
type: typeToString(symbol.type),
|
|
1136
|
+
span: symbol.span,
|
|
1137
|
+
})),
|
|
1138
|
+
})),
|
|
1139
|
+
symbols,
|
|
1140
|
+
astVersion: parsed.version,
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function buildTypeFile(routes) {
|
|
1145
|
+
const lines = [];
|
|
1146
|
+
lines.push("/* auto-generated by fastscript typecheck */");
|
|
1147
|
+
lines.push("export type FastScriptRouteParams = {");
|
|
1148
|
+
for (const route of routes) {
|
|
1149
|
+
const params = inferRouteParamTypes(route.routePath, route.paramTypes);
|
|
1150
|
+
const body = Object.keys(params).length
|
|
1151
|
+
? `{ ${Object.entries(params).map(([k, v]) => `${k}: ${v}`).join("; ")} }`
|
|
1152
|
+
: "{}";
|
|
1153
|
+
lines.push(` \"${route.routePath}\": ${body};`);
|
|
1154
|
+
}
|
|
1155
|
+
lines.push("};");
|
|
1156
|
+
lines.push("");
|
|
1157
|
+
lines.push("export type FastScriptRouteLoaderData = {");
|
|
1158
|
+
for (const route of routes) {
|
|
1159
|
+
lines.push(` \"${route.routePath}\": ${route.loaderDataType || "{}"};`);
|
|
1160
|
+
}
|
|
1161
|
+
lines.push("};");
|
|
1162
|
+
lines.push("");
|
|
1163
|
+
lines.push("export type FastScriptRouteContext<P extends keyof FastScriptRouteParams = keyof FastScriptRouteParams> = {");
|
|
1164
|
+
lines.push(" path: P;");
|
|
1165
|
+
lines.push(" params: FastScriptRouteParams[P];");
|
|
1166
|
+
lines.push(" data: FastScriptRouteLoaderData[P];");
|
|
1167
|
+
lines.push("};");
|
|
1168
|
+
lines.push("");
|
|
1169
|
+
return lines.join("\n");
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function routeConflicts(routes) {
|
|
1173
|
+
const seen = new Map();
|
|
1174
|
+
const conflicts = [];
|
|
1175
|
+
for (const route of routes) {
|
|
1176
|
+
const key = `${route.routePath}|${route.slot || "default"}`;
|
|
1177
|
+
const previous = seen.get(key);
|
|
1178
|
+
if (previous) {
|
|
1179
|
+
conflicts.push({
|
|
1180
|
+
code: "FS4001",
|
|
1181
|
+
severity: "error",
|
|
1182
|
+
message: `Duplicate route mapping: ${route.routePath} (${route.slot || "default"})`,
|
|
1183
|
+
file: route.file,
|
|
1184
|
+
line: 1,
|
|
1185
|
+
column: 1,
|
|
1186
|
+
span: { start: 0, end: 1 },
|
|
1187
|
+
related: [{ file: previous.file, line: 1, column: 1, message: "First route declared here.", span: { start: 0, end: 1 } }],
|
|
1188
|
+
});
|
|
1189
|
+
} else {
|
|
1190
|
+
seen.set(key, route);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
return conflicts;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function usageHints(routes) {
|
|
1197
|
+
const hints = [];
|
|
1198
|
+
for (const route of routes) {
|
|
1199
|
+
let source = "";
|
|
1200
|
+
try {
|
|
1201
|
+
source = readFileSync(route.file, "utf8");
|
|
1202
|
+
} catch {
|
|
1203
|
+
source = "";
|
|
1204
|
+
}
|
|
1205
|
+
for (const param of route.params) {
|
|
1206
|
+
if (source.includes(`params.${param}`)) continue;
|
|
1207
|
+
hints.push({
|
|
1208
|
+
code: "FS4002",
|
|
1209
|
+
severity: "warning",
|
|
1210
|
+
message: `Route param \`${param}\` is declared but not referenced in ${route.file}.`,
|
|
1211
|
+
file: route.file,
|
|
1212
|
+
line: 1,
|
|
1213
|
+
column: 1,
|
|
1214
|
+
span: { start: 0, end: 1 },
|
|
1215
|
+
related: [],
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
return hints;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function mergePropertyType(map, key, type) {
|
|
1223
|
+
const existing = map[key];
|
|
1224
|
+
if (!existing) {
|
|
1225
|
+
map[key] = type;
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
if (existing === type) return;
|
|
1229
|
+
const left = existing.split("|").map((part) => part.trim()).filter(Boolean);
|
|
1230
|
+
const right = String(type).split("|").map((part) => part.trim()).filter(Boolean);
|
|
1231
|
+
map[key] = [...new Set([...left, ...right])].sort().join(" | ");
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function astTypeLiteral(node) {
|
|
1235
|
+
if (!node) return "unknown";
|
|
1236
|
+
switch (node.type) {
|
|
1237
|
+
case "Literal":
|
|
1238
|
+
if (node.value === null) return "null";
|
|
1239
|
+
if (typeof node.value === "string") return "string";
|
|
1240
|
+
if (typeof node.value === "number") return "number";
|
|
1241
|
+
if (typeof node.value === "boolean") return "boolean";
|
|
1242
|
+
return "unknown";
|
|
1243
|
+
case "TemplateLiteral":
|
|
1244
|
+
return "string";
|
|
1245
|
+
case "ArrayExpression": {
|
|
1246
|
+
const itemTypes = (node.elements || []).filter(Boolean).map((entry) => astTypeLiteral(entry));
|
|
1247
|
+
const unique = [...new Set(itemTypes)];
|
|
1248
|
+
return `${(unique.length ? unique.join(" | ") : "unknown")}[]`;
|
|
1249
|
+
}
|
|
1250
|
+
case "ObjectExpression": {
|
|
1251
|
+
const props = [];
|
|
1252
|
+
for (const property of node.properties || []) {
|
|
1253
|
+
if (property.type !== "Property") continue;
|
|
1254
|
+
let key = null;
|
|
1255
|
+
if (!property.computed && property.key?.type === "Identifier") key = property.key.name;
|
|
1256
|
+
else if (property.key?.type === "Literal") key = String(property.key.value);
|
|
1257
|
+
if (!key) continue;
|
|
1258
|
+
props.push(`${key}: ${astTypeLiteral(property.value)}`);
|
|
1259
|
+
}
|
|
1260
|
+
if (!props.length) return "{}";
|
|
1261
|
+
return `{ ${props.join("; ")} }`;
|
|
1262
|
+
}
|
|
1263
|
+
case "Identifier":
|
|
1264
|
+
if (node.name === "undefined") return "undefined";
|
|
1265
|
+
return "unknown";
|
|
1266
|
+
case "UnaryExpression":
|
|
1267
|
+
if (node.operator === "!") return "boolean";
|
|
1268
|
+
if (node.operator === "typeof") return "string";
|
|
1269
|
+
if (["+", "-", "~"].includes(node.operator)) return "number";
|
|
1270
|
+
return astTypeLiteral(node.argument);
|
|
1271
|
+
case "BinaryExpression":
|
|
1272
|
+
if (["==", "!=", "===", "!==", "<", ">", "<=", ">="].includes(node.operator)) return "boolean";
|
|
1273
|
+
if (node.operator === "+" && (astTypeLiteral(node.left) === "string" || astTypeLiteral(node.right) === "string")) return "string";
|
|
1274
|
+
return "number";
|
|
1275
|
+
case "LogicalExpression":
|
|
1276
|
+
return `${astTypeLiteral(node.left)} | ${astTypeLiteral(node.right)}`;
|
|
1277
|
+
case "ConditionalExpression":
|
|
1278
|
+
return `${astTypeLiteral(node.consequent)} | ${astTypeLiteral(node.alternate)}`;
|
|
1279
|
+
case "ArrowFunctionExpression":
|
|
1280
|
+
case "FunctionExpression":
|
|
1281
|
+
return "function";
|
|
1282
|
+
case "NewExpression":
|
|
1283
|
+
return "object";
|
|
1284
|
+
default:
|
|
1285
|
+
return "unknown";
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function collectReturnObjects(node, out = []) {
|
|
1290
|
+
if (!node || typeof node !== "object") return out;
|
|
1291
|
+
if (node.type === "ReturnStatement" && node.argument?.type === "ObjectExpression") {
|
|
1292
|
+
out.push(node.argument);
|
|
1293
|
+
}
|
|
1294
|
+
for (const value of Object.values(node)) {
|
|
1295
|
+
if (!value) continue;
|
|
1296
|
+
if (Array.isArray(value)) {
|
|
1297
|
+
for (const entry of value) collectReturnObjects(entry, out);
|
|
1298
|
+
} else if (typeof value === "object") {
|
|
1299
|
+
collectReturnObjects(value, out);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
return out;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function inferRouteLoaderDataShape(file) {
|
|
1306
|
+
let source = "";
|
|
1307
|
+
try {
|
|
1308
|
+
source = readFileSync(file, "utf8");
|
|
1309
|
+
} catch {
|
|
1310
|
+
return { hasLoader: false, typeLiteral: "unknown", fields: {} };
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
let ast = null;
|
|
1314
|
+
try {
|
|
1315
|
+
ast = parseFastScript(source, { file, mode: "lenient", recover: true });
|
|
1316
|
+
} catch {
|
|
1317
|
+
return { hasLoader: false, typeLiteral: "unknown", fields: {} };
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
const body = ast?.estree?.body || [];
|
|
1321
|
+
let loadNode = null;
|
|
1322
|
+
for (const node of body) {
|
|
1323
|
+
if (node.type === "ExportNamedDeclaration") {
|
|
1324
|
+
const decl = node.declaration;
|
|
1325
|
+
if (decl?.type === "FunctionDeclaration" && decl.id?.name === "load") {
|
|
1326
|
+
loadNode = decl;
|
|
1327
|
+
break;
|
|
1328
|
+
}
|
|
1329
|
+
if (decl?.type === "VariableDeclaration") {
|
|
1330
|
+
for (const entry of decl.declarations || []) {
|
|
1331
|
+
if (entry.id?.type === "Identifier" && entry.id.name === "load" && entry.init && ["ArrowFunctionExpression", "FunctionExpression"].includes(entry.init.type)) {
|
|
1332
|
+
loadNode = entry.init;
|
|
1333
|
+
break;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
if (node.type === "FunctionDeclaration" && node.id?.name === "load") {
|
|
1339
|
+
loadNode = node;
|
|
1340
|
+
break;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
if (!loadNode) return { hasLoader: false, typeLiteral: "{}", fields: {} };
|
|
1345
|
+
|
|
1346
|
+
const returns = collectReturnObjects(loadNode.body || loadNode, []);
|
|
1347
|
+
if (!returns.length) return { hasLoader: true, typeLiteral: "unknown", fields: {} };
|
|
1348
|
+
|
|
1349
|
+
const fields = {};
|
|
1350
|
+
for (const objectNode of returns) {
|
|
1351
|
+
for (const property of objectNode.properties || []) {
|
|
1352
|
+
if (property.type !== "Property") continue;
|
|
1353
|
+
let key = null;
|
|
1354
|
+
if (!property.computed && property.key?.type === "Identifier") key = property.key.name;
|
|
1355
|
+
else if (property.key?.type === "Literal") key = String(property.key.value);
|
|
1356
|
+
if (!key) continue;
|
|
1357
|
+
mergePropertyType(fields, key, astTypeLiteral(property.value));
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const entries = Object.entries(fields);
|
|
1362
|
+
if (!entries.length) return { hasLoader: true, typeLiteral: "{}", fields };
|
|
1363
|
+
const typeLiteral = `{ ${entries.map(([key, value]) => `${key}: ${value}`).join("; ")} }`;
|
|
1364
|
+
return { hasLoader: true, typeLiteral, fields };
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function formatTypeDiagnostic(diagnostic) {
|
|
1368
|
+
const path = diagnostic.file || "<memory>";
|
|
1369
|
+
const base = `${path}:${diagnostic.line || 1}:${diagnostic.column || 1} ${diagnostic.code} ${diagnostic.severity || "error"} ${diagnostic.message}`;
|
|
1370
|
+
const hint = diagnostic.hint ? ` hint=${diagnostic.hint}` : "";
|
|
1371
|
+
const related = (diagnostic.related || [])
|
|
1372
|
+
.map((entry) => ` related=${entry.file}:${entry.line}:${entry.column} ${entry.message}`)
|
|
1373
|
+
.join("");
|
|
1374
|
+
return `${base}${hint}${related}`;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
function parseArgs(args) {
|
|
1378
|
+
const out = { mode: "fail", path: APP_DIR };
|
|
1379
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1380
|
+
if (args[i] === "--mode") out.mode = (args[i + 1] || out.mode).toLowerCase();
|
|
1381
|
+
if (args[i] === "--path") out.path = resolve(args[i + 1] || out.path);
|
|
1382
|
+
}
|
|
1383
|
+
return out;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function summarizeSeverity(diagnostics) {
|
|
1387
|
+
const summary = { error: 0, warning: 0 };
|
|
1388
|
+
for (const diagnostic of diagnostics) {
|
|
1389
|
+
if (diagnostic.severity === "warning") summary.warning += 1;
|
|
1390
|
+
else summary.error += 1;
|
|
1391
|
+
}
|
|
1392
|
+
return summary;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
export async function runTypeCheck(args = []) {
|
|
1396
|
+
const options = parseArgs(args);
|
|
1397
|
+
if (!existsSync(options.path)) {
|
|
1398
|
+
throw new Error(`Missing typecheck path: ${options.path}`);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
const files = walk(options.path).filter((file) => extname(file) === ".fs");
|
|
1402
|
+
const fileReports = [];
|
|
1403
|
+
const diagnostics = [];
|
|
1404
|
+
|
|
1405
|
+
for (const file of files) {
|
|
1406
|
+
const source = readFileSync(file, "utf8");
|
|
1407
|
+
const report = analyzeFileTypes(file, source);
|
|
1408
|
+
fileReports.push(report);
|
|
1409
|
+
diagnostics.push(...report.diagnostics);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
const candidatePagesDir = join(options.path, "pages");
|
|
1413
|
+
const pagesDir = existsSync(candidatePagesDir) ? candidatePagesDir : PAGES_DIR;
|
|
1414
|
+
const pageFiles = walk(pagesDir).filter((file) => [".fs", ".js"].includes(extname(file)));
|
|
1415
|
+
const routes = pageFiles
|
|
1416
|
+
.filter((file) => !file.endsWith("_layout.fs") && !file.endsWith("_layout.js"))
|
|
1417
|
+
.filter((file) => !file.endsWith("404.fs") && !file.endsWith("404.js"))
|
|
1418
|
+
.map((file) => {
|
|
1419
|
+
const route = inferRouteMeta(file, pagesDir);
|
|
1420
|
+
const loader = inferRouteLoaderDataShape(file);
|
|
1421
|
+
return {
|
|
1422
|
+
...route,
|
|
1423
|
+
hasLoader: loader.hasLoader,
|
|
1424
|
+
loaderDataType: loader.typeLiteral,
|
|
1425
|
+
loaderDataFields: loader.fields,
|
|
1426
|
+
};
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1429
|
+
const sortedRoutes = sortRoutesByPriority(routes.map((route) => ({ ...route, path: route.routePath })))
|
|
1430
|
+
.map((route) => ({ ...route, routePath: route.path }));
|
|
1431
|
+
|
|
1432
|
+
diagnostics.push(...routeConflicts(sortedRoutes), ...usageHints(sortedRoutes));
|
|
1433
|
+
const dedupedDiagnostics = dedupeDiagnostics(diagnostics);
|
|
1434
|
+
const severity = summarizeSeverity(dedupedDiagnostics);
|
|
1435
|
+
|
|
1436
|
+
mkdirSync(OUT_DIR, { recursive: true });
|
|
1437
|
+
writeFileSync(TYPES_PATH, buildTypeFile(sortedRoutes), "utf8");
|
|
1438
|
+
|
|
1439
|
+
const report = {
|
|
1440
|
+
mode: options.mode,
|
|
1441
|
+
generatedAt: new Date().toISOString(),
|
|
1442
|
+
files: fileReports,
|
|
1443
|
+
routes: sortedRoutes,
|
|
1444
|
+
diagnostics: dedupedDiagnostics,
|
|
1445
|
+
summary: {
|
|
1446
|
+
files: files.length,
|
|
1447
|
+
routeCount: sortedRoutes.length,
|
|
1448
|
+
errors: severity.error,
|
|
1449
|
+
warnings: severity.warning,
|
|
1450
|
+
},
|
|
1451
|
+
};
|
|
1452
|
+
|
|
1453
|
+
writeFileSync(REPORT_PATH, JSON.stringify(report, null, 2), "utf8");
|
|
1454
|
+
|
|
1455
|
+
for (const diagnostic of dedupedDiagnostics) {
|
|
1456
|
+
console.log(formatTypeDiagnostic(diagnostic));
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
if (severity.error > 0 && options.mode !== "pass") {
|
|
1460
|
+
const error = new Error(`typecheck failed: ${severity.error} error(s), ${severity.warning} warning(s)`);
|
|
1461
|
+
error.status = 1;
|
|
1462
|
+
error.details = dedupedDiagnostics;
|
|
1463
|
+
throw error;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
console.log(`typecheck complete: files=${files.length}, routes=${sortedRoutes.length}, errors=${severity.error}, warnings=${severity.warning}, mode=${options.mode}`);
|
|
1467
|
+
}
|
|
1468
|
+
|