@slim-lang/core 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +666 -0
- package/package.json +55 -0
- package/packages/slim/.spm +7 -0
- package/packages/slim/converters/main.slim +106 -0
- package/packages/slim/helpers/array.slim +25 -0
- package/packages/slim/helpers/path.slim +3 -0
- package/packages/slim/helpers/request.slim +102 -0
- package/packages/slim/helpers/string.slim +27 -0
- package/packages/slim/main.slim +42 -0
- package/packages/slim/parse/main.slim +25 -0
- package/packages/slim/server/main.slim +423 -0
- package/packages/slim/time/main.slim +66 -0
- package/packages/slim/types/common.slim +6 -0
- package/packages/slim/types/formats.slim +23 -0
- package/packages/slim/types/hash.slim +6 -0
- package/packages/slim/types/mails.slim +3 -0
- package/packages/slim/types/numerical.slim +9 -0
- package/packages/slim/types/time.slim +3 -0
- package/run-dev-slim.js +133 -0
- package/run-slim.js +20 -0
- package/src/bin/api/github_auth.js +89 -0
- package/src/bin/api/github_get.js +139 -0
- package/src/bin/api/github_req.js +455 -0
- package/src/bin/api/lock.js +37 -0
- package/src/bin/api/spm.js +103 -0
- package/src/bin/api/storage.js +30 -0
- package/src/bin/cli.js +404 -0
- package/src/bin/config.default.json +5 -0
- package/src/bin/helpers.js +147 -0
- package/src/bin/parsers/spm.js +174 -0
- package/src/bin/spm.js +519 -0
- package/src/checker.js +926 -0
- package/src/compile.js +230 -0
- package/src/external/classErrors.js +202 -0
- package/src/external/client.js +38 -0
- package/src/external/core.js +861 -0
- package/src/external/defaults.js +25 -0
- package/src/external/helpers.js +541 -0
- package/src/external/slim-globals.d.ts +65 -0
- package/src/external/types.js +38 -0
- package/src/format.js +81 -0
- package/src/handlers/errorHandler.js +43 -0
- package/src/handlers/parser/components.js +250 -0
- package/src/handlers/parserHandler.js +793 -0
- package/src/jsdoc.js +273 -0
- package/src/lexer.js +174 -0
- package/src/modulePaths.js +74 -0
- package/src/parser.js +818 -0
- package/src/repl.js +32 -0
- package/src/sourcemap.js +0 -0
- package/src/test-runner.js +62 -0
- package/src/transform.js +765 -0
package/src/checker.js
ADDED
|
@@ -0,0 +1,926 @@
|
|
|
1
|
+
import _traverse from "@babel/traverse"
|
|
2
|
+
import * as t from "@babel/types"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { computeLineStarts, offsetToLineCol } from "./sourcemap.js"
|
|
5
|
+
|
|
6
|
+
const traverse = _traverse.default ?? _traverse
|
|
7
|
+
|
|
8
|
+
// Check annotations before lowering; report only definite type conflicts.
|
|
9
|
+
|
|
10
|
+
const NUMERIC = new Set(["int", "float", "number"])
|
|
11
|
+
const KNOWN = new Set([
|
|
12
|
+
"int", "float", "number", "string", "bool", "null", "undefined",
|
|
13
|
+
"object", "array", "function", "any", "element"
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
function splitUnion(label) {
|
|
17
|
+
const parts = []
|
|
18
|
+
let depth = 0
|
|
19
|
+
let current = ""
|
|
20
|
+
|
|
21
|
+
for (const char of label) {
|
|
22
|
+
if (char === "<" || char === "[" || char === "(") depth++
|
|
23
|
+
else if (char === ">" || char === "]" || char === ")") depth--
|
|
24
|
+
else if (char === "|" && depth === 0) { parts.push(current.trim()); current = ""; continue }
|
|
25
|
+
current += char
|
|
26
|
+
}
|
|
27
|
+
if (current.trim()) parts.push(current.trim())
|
|
28
|
+
return parts
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function expand(label) {
|
|
32
|
+
const trimmed = label.trim()
|
|
33
|
+
if (trimmed.endsWith("?")) return `${trimmed.slice(0, -1).trim()} | null | undefined`
|
|
34
|
+
return trimmed
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isIntersection(label) {
|
|
38
|
+
return label.includes("&") && !label.includes("|")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function elementOf(label) {
|
|
42
|
+
if (!label.endsWith("[]")) return null
|
|
43
|
+
const inner = label.slice(0, -2).trim()
|
|
44
|
+
if (inner.startsWith("(") && inner.endsWith(")")) return inner.slice(1, -1).trim()
|
|
45
|
+
return inner
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function baseName(label) {
|
|
49
|
+
return label.replace(/(\[\])+$/, "").replace(/<[\s\S]*>$/, "").trim()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function splitList(text) {
|
|
53
|
+
const parts = []
|
|
54
|
+
let depth = 0
|
|
55
|
+
let current = ""
|
|
56
|
+
|
|
57
|
+
for (const char of text) {
|
|
58
|
+
if (char === "<" || char === "[" || char === "(") depth++
|
|
59
|
+
else if (char === ">" || char === "]" || char === ")") depth--
|
|
60
|
+
else if (char === "," && depth === 0) { parts.push(current.trim()); current = ""; continue }
|
|
61
|
+
current += char
|
|
62
|
+
}
|
|
63
|
+
if (current.trim()) parts.push(current.trim())
|
|
64
|
+
return parts
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function genericOf(label) {
|
|
68
|
+
const open = label.indexOf("<")
|
|
69
|
+
if (open === -1 || !label.endsWith(">")) return null
|
|
70
|
+
return {
|
|
71
|
+
container: label.slice(0, open).trim(),
|
|
72
|
+
args: splitList(label.slice(open + 1, -1))
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function tupleOf(label) {
|
|
77
|
+
if (!label.startsWith("[") || !label.endsWith("]") || label.length <= 2) return null
|
|
78
|
+
return splitList(label.slice(1, -1))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalize(label) {
|
|
82
|
+
const trimmed = label.trim()
|
|
83
|
+
const generic = genericOf(trimmed)
|
|
84
|
+
if (generic && generic.container === "Array" && generic.args.length === 1) {
|
|
85
|
+
return `${normalize(generic.args[0])}[]`
|
|
86
|
+
}
|
|
87
|
+
return trimmed
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Resolve inherited fields so structs can cross module boundaries.
|
|
91
|
+
function resolveStruct(name, env, seen = new Set()) {
|
|
92
|
+
const definition = env.structs.get(name)
|
|
93
|
+
if (!definition) return null
|
|
94
|
+
if (definition.resolved) return definition
|
|
95
|
+
|
|
96
|
+
if (!definition.parent) {
|
|
97
|
+
return { ...definition, ancestors: new Set(), resolved: true }
|
|
98
|
+
}
|
|
99
|
+
if (seen.has(name)) return null
|
|
100
|
+
|
|
101
|
+
seen.add(name)
|
|
102
|
+
const parent = resolveStruct(definition.parent, env, seen)
|
|
103
|
+
if (!parent) return null
|
|
104
|
+
|
|
105
|
+
const fields = new Map(parent.fields)
|
|
106
|
+
for (const [field, info] of definition.fields) fields.set(field, info)
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
fields,
|
|
110
|
+
defaults: new Set([...parent.defaults, ...definition.defaults]),
|
|
111
|
+
methods: new Set([...parent.methods, ...definition.methods]),
|
|
112
|
+
ancestors: new Set([definition.parent, ...parent.ancestors]),
|
|
113
|
+
parent: null,
|
|
114
|
+
resolved: true
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function inherits(name, ancestor, env) {
|
|
119
|
+
if (name === ancestor) return true
|
|
120
|
+
return resolveStruct(name, env)?.ancestors.has(ancestor) ?? false
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function createEnvironment() {
|
|
124
|
+
return {
|
|
125
|
+
structs: new Map(),
|
|
126
|
+
enums: new Map(),
|
|
127
|
+
customTypes: new Set(),
|
|
128
|
+
functions: new Map(),
|
|
129
|
+
bindings: new Map(),
|
|
130
|
+
classes: new Set(),
|
|
131
|
+
methodOwners: new Map(),
|
|
132
|
+
returns: new Map(),
|
|
133
|
+
functionsByName: new Map(),
|
|
134
|
+
importedFunctions: new Map()
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Module declarations keyed by absolute source path.
|
|
139
|
+
const moduleTypes = new Map()
|
|
140
|
+
|
|
141
|
+
function signatureFor(name, path_, env) {
|
|
142
|
+
const binding = path_.scope.getBinding(name)
|
|
143
|
+
if (binding) return env.functions.get(binding) ?? null
|
|
144
|
+
return env.importedFunctions.get(name) ?? null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const BUILTIN_CLASSES = new Set(["Map", "Set", "Date", "Promise", "RegExp", "Error"])
|
|
148
|
+
|
|
149
|
+
function isOpen(label, env) {
|
|
150
|
+
const base = baseName(label)
|
|
151
|
+
if (!base) return true
|
|
152
|
+
// Custom validators shadow built-ins and cannot be evaluated statically.
|
|
153
|
+
if (env.customTypes.has(base)) return true
|
|
154
|
+
if (KNOWN.has(base) || BUILTIN_CLASSES.has(base)) return false
|
|
155
|
+
if (env.structs.has(base) || env.enums.has(base)) return false
|
|
156
|
+
if (label.includes("::")) return !env.enums.has(label.split("::")[0])
|
|
157
|
+
return true
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function acceptsAtom(target, source, env) {
|
|
161
|
+
if (target === source) return true
|
|
162
|
+
if (target === "any" || source === "any") return true
|
|
163
|
+
|
|
164
|
+
const targetElement = elementOf(target)
|
|
165
|
+
const sourceElement = elementOf(source)
|
|
166
|
+
if (targetElement) {
|
|
167
|
+
if (source === "array") return true
|
|
168
|
+
if (!sourceElement) return isOpen(source, env)
|
|
169
|
+
if (sourceElement === "null") return true
|
|
170
|
+
return accepts(targetElement, sourceElement, env, true)
|
|
171
|
+
}
|
|
172
|
+
if (target === "array") return !!sourceElement || source === "array" || isOpen(source, env)
|
|
173
|
+
if (sourceElement) return target === "object" || isOpen(target, env)
|
|
174
|
+
|
|
175
|
+
if (isOpen(target, env) || isOpen(source, env)) return true
|
|
176
|
+
|
|
177
|
+
if (NUMERIC.has(target) && NUMERIC.has(source)) {
|
|
178
|
+
if (target === "int") return source === "int"
|
|
179
|
+
return true
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const targetTuple = tupleOf(target)
|
|
183
|
+
const sourceTuple = tupleOf(source)
|
|
184
|
+
if (targetTuple || sourceTuple) {
|
|
185
|
+
if (!targetTuple || !sourceTuple) return false
|
|
186
|
+
if (targetTuple.length !== sourceTuple.length) return false
|
|
187
|
+
return targetTuple.every((part, index) => accepts(part, sourceTuple[index], env, true))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const targetGeneric = genericOf(target)
|
|
191
|
+
if (targetGeneric) {
|
|
192
|
+
return baseName(target) === baseName(source)
|
|
193
|
+
}
|
|
194
|
+
if (genericOf(source)) return baseName(target) === baseName(source)
|
|
195
|
+
|
|
196
|
+
if (BUILTIN_CLASSES.has(target)) return target === source
|
|
197
|
+
if (BUILTIN_CLASSES.has(source)) return false
|
|
198
|
+
|
|
199
|
+
if (target === "object") return source === "object" || env.structs.has(source)
|
|
200
|
+
if (env.structs.has(target)) {
|
|
201
|
+
return source === "object" || inherits(source, target, env)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (target.includes("::")) {
|
|
205
|
+
const [enumName] = target.split("::")
|
|
206
|
+
const definition = env.enums.get(enumName)
|
|
207
|
+
if (!definition) return true
|
|
208
|
+
if (source === target || source === enumName) return true
|
|
209
|
+
return definition.valueTypes.has(source)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (env.enums.has(target)) {
|
|
213
|
+
const definition = env.enums.get(target)
|
|
214
|
+
return source.startsWith(`${target}::`) || definition.valueTypes.has(source)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return false
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// A union needs one compatible arm; array literals need every element to fit.
|
|
221
|
+
export function accepts(target, source, env, requireAll = false) {
|
|
222
|
+
if (!target || !source) return true
|
|
223
|
+
|
|
224
|
+
const targetLabel = expand(target)
|
|
225
|
+
const sourceLabel = expand(source)
|
|
226
|
+
if (isIntersection(targetLabel) || isIntersection(sourceLabel)) return true
|
|
227
|
+
|
|
228
|
+
const targets = splitUnion(targetLabel).map(normalize)
|
|
229
|
+
const sources = splitUnion(sourceLabel).map(normalize)
|
|
230
|
+
const fits = one => targets.some(other => acceptsAtom(other, one, env))
|
|
231
|
+
|
|
232
|
+
return requireAll ? sources.every(fits) : sources.some(fits)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function unionOf(left, right) {
|
|
236
|
+
if (!left || !right) return null
|
|
237
|
+
if (left === right) return left
|
|
238
|
+
return `${left} | ${right}`
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function typedVariableLabel(node) {
|
|
242
|
+
if (!t.isCallExpression(node)) return null
|
|
243
|
+
if (!t.isIdentifier(node.callee, { name: "__typed_variable__" })) return null
|
|
244
|
+
return t.isStringLiteral(node.arguments[1]) ? node.arguments[1].value : null
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function structLiteralLabel(node, env) {
|
|
248
|
+
if (!t.isCallExpression(node)) return null
|
|
249
|
+
const callee = node.callee
|
|
250
|
+
if (!t.isMemberExpression(callee) || callee.computed) return null
|
|
251
|
+
if (!t.isIdentifier(callee.property, { name: "new" })) return null
|
|
252
|
+
if (!t.isIdentifier(callee.object) || !env.structs.has(callee.object.name)) return null
|
|
253
|
+
return callee.object.name
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function infer(node, path_, env) {
|
|
257
|
+
if (!node) return null
|
|
258
|
+
|
|
259
|
+
if (t.isNumericLiteral(node)) return Number.isInteger(node.value) ? "int" : "float"
|
|
260
|
+
if (t.isStringLiteral(node) || t.isTemplateLiteral(node)) return "string"
|
|
261
|
+
if (t.isBooleanLiteral(node)) return "bool"
|
|
262
|
+
if (t.isNullLiteral(node)) return "null"
|
|
263
|
+
if (t.isIdentifier(node, { name: "undefined" })) return "undefined"
|
|
264
|
+
if (t.isObjectExpression(node)) return "object"
|
|
265
|
+
if (t.isFunctionExpression(node) || t.isArrowFunctionExpression(node)) return "function"
|
|
266
|
+
|
|
267
|
+
if (t.isArrayExpression(node)) {
|
|
268
|
+
if (node.elements.length === 0) return "null[]"
|
|
269
|
+
|
|
270
|
+
const kinds = new Set()
|
|
271
|
+
for (const item of node.elements) {
|
|
272
|
+
if (!item || t.isSpreadElement(item)) return "array"
|
|
273
|
+
const kind = infer(item, path_, env)
|
|
274
|
+
if (!kind) return "array"
|
|
275
|
+
kinds.add(kind)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (kinds.size === 1) return `${[...kinds][0]}[]`
|
|
279
|
+
return `(${[...kinds].join(" | ")})[]`
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const structInstance = structLiteralLabel(node, env)
|
|
283
|
+
if (structInstance) return structInstance
|
|
284
|
+
|
|
285
|
+
if (t.isNewExpression(node) && t.isIdentifier(node.callee)) {
|
|
286
|
+
const name = node.callee.name
|
|
287
|
+
if (env.classes.has(name) || BUILTIN_CLASSES.has(name)) return name
|
|
288
|
+
return null
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (t.isIdentifier(node)) {
|
|
292
|
+
const binding = path_.scope.getBinding(node.name)
|
|
293
|
+
return binding ? env.bindings.get(binding) ?? null : null
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (t.isThisExpression(node)) {
|
|
297
|
+
const fn = path_.getFunctionParent()
|
|
298
|
+
return fn ? env.methodOwners.get(fn.node) ?? null : null
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (t.isMemberExpression(node) && node.computed) {
|
|
302
|
+
const objectType = normalize(infer(node.object, path_, env) ?? "")
|
|
303
|
+
if (!objectType) return null
|
|
304
|
+
|
|
305
|
+
const element = elementOf(objectType)
|
|
306
|
+
if (element) return element
|
|
307
|
+
|
|
308
|
+
const tuple = tupleOf(objectType)
|
|
309
|
+
if (tuple && t.isNumericLiteral(node.property)) return tuple[node.property.value] ?? null
|
|
310
|
+
return null
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (t.isMemberExpression(node) && t.isIdentifier(node.property)) {
|
|
314
|
+
if (t.isIdentifier(node.object) && env.enums.has(node.object.name)) {
|
|
315
|
+
return `${node.object.name}::${node.property.name}`
|
|
316
|
+
}
|
|
317
|
+
const objectType = infer(node.object, path_, env)
|
|
318
|
+
const definition = objectType ? resolveStruct(objectType, env) : null
|
|
319
|
+
return definition?.fields.get(node.property.name)?.type ?? null
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (t.isUnaryExpression(node)) {
|
|
323
|
+
if (node.operator === "!") return "bool"
|
|
324
|
+
if (node.operator === "-" || node.operator === "+") {
|
|
325
|
+
const operand = infer(node.argument, path_, env)
|
|
326
|
+
return NUMERIC.has(operand) ? operand : null
|
|
327
|
+
}
|
|
328
|
+
if (node.operator === "typeof") return "string"
|
|
329
|
+
return null
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (t.isBinaryExpression(node)) {
|
|
333
|
+
const operator = node.operator
|
|
334
|
+
if (["==", "!=", "===", "!==", "<", ">", "<=", ">=", "instanceof", "in"].includes(operator)) {
|
|
335
|
+
return "bool"
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const left = infer(node.left, path_, env)
|
|
339
|
+
const right = infer(node.right, path_, env)
|
|
340
|
+
|
|
341
|
+
if (operator === "+") {
|
|
342
|
+
if (left === "string" || right === "string") return "string"
|
|
343
|
+
if (NUMERIC.has(left) && NUMERIC.has(right)) {
|
|
344
|
+
return left === "int" && right === "int" ? "int" : "float"
|
|
345
|
+
}
|
|
346
|
+
return null
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (["-", "*", "%", "**"].includes(operator)) {
|
|
350
|
+
if (NUMERIC.has(left) && NUMERIC.has(right)) {
|
|
351
|
+
return left === "int" && right === "int" ? "int" : "float"
|
|
352
|
+
}
|
|
353
|
+
return null
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (operator === "/") return NUMERIC.has(left) && NUMERIC.has(right) ? "float" : null
|
|
357
|
+
return null
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (t.isLogicalExpression(node)) {
|
|
361
|
+
return unionOf(infer(node.left, path_, env), infer(node.right, path_, env))
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (t.isConditionalExpression(node)) {
|
|
365
|
+
return unionOf(infer(node.consequent, path_, env), infer(node.alternate, path_, env))
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (t.isAwaitExpression(node)) return null
|
|
369
|
+
|
|
370
|
+
if (t.isCallExpression(node) && t.isIdentifier(node.callee)) {
|
|
371
|
+
const label = typedVariableLabel(node)
|
|
372
|
+
if (label) return label
|
|
373
|
+
|
|
374
|
+
return signatureFor(node.callee.name, path_, env)?.returns ?? null
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return null
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function declaredReturn(fn) {
|
|
381
|
+
if (!t.isBlockStatement(fn.body)) return null
|
|
382
|
+
|
|
383
|
+
for (const statement of fn.body.body) {
|
|
384
|
+
if (!t.isExpressionStatement(statement)) continue
|
|
385
|
+
const call = statement.expression
|
|
386
|
+
if (!t.isCallExpression(call)) continue
|
|
387
|
+
if (!t.isIdentifier(call.callee, { name: "__declare_return__" })) continue
|
|
388
|
+
if (t.isStringLiteral(call.arguments[0])) return call.arguments[0].value
|
|
389
|
+
}
|
|
390
|
+
return null
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function collectParameters(fn) {
|
|
394
|
+
const declared = new Map()
|
|
395
|
+
|
|
396
|
+
if (t.isBlockStatement(fn.body)) {
|
|
397
|
+
for (const statement of fn.body.body) {
|
|
398
|
+
if (!t.isExpressionStatement(statement)) continue
|
|
399
|
+
const call = statement.expression
|
|
400
|
+
if (!t.isCallExpression(call)) continue
|
|
401
|
+
if (!t.isIdentifier(call.callee, { name: "__typed_parameter__" })) continue
|
|
402
|
+
|
|
403
|
+
const [value, label, , optional] = call.arguments
|
|
404
|
+
if (!t.isIdentifier(value) || !t.isStringLiteral(label)) continue
|
|
405
|
+
declared.set(value.name, {
|
|
406
|
+
type: label.value,
|
|
407
|
+
optional: t.isBooleanLiteral(optional) ? optional.value : false
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
let rest = false
|
|
413
|
+
const parameters = fn.params.map(param => {
|
|
414
|
+
if (t.isRestElement(param)) { rest = true; return { name: null, type: null, required: false } }
|
|
415
|
+
if (t.isAssignmentPattern(param) && t.isIdentifier(param.left)) {
|
|
416
|
+
return { name: param.left.name, type: declared.get(param.left.name)?.type ?? null, required: false }
|
|
417
|
+
}
|
|
418
|
+
if (t.isIdentifier(param)) {
|
|
419
|
+
const info = declared.get(param.name)
|
|
420
|
+
return { name: param.name, type: info?.type ?? null, required: !info?.optional }
|
|
421
|
+
}
|
|
422
|
+
return { name: null, type: null, required: false }
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
const returns = declaredReturn(fn)
|
|
426
|
+
return { parameters, rest, returns, typed: declared.size > 0 || !!returns }
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function functionName(path_) {
|
|
430
|
+
if (t.isFunctionDeclaration(path_.node) && path_.node.id) return path_.node.id.name
|
|
431
|
+
if (path_.parentPath?.isVariableDeclarator() && t.isIdentifier(path_.parent.id)) {
|
|
432
|
+
return path_.parent.id.name
|
|
433
|
+
}
|
|
434
|
+
if (path_.parentPath?.isObjectProperty()) {
|
|
435
|
+
const key = path_.parent.key
|
|
436
|
+
if (t.isStringLiteral(key)) return key.value
|
|
437
|
+
if (t.isIdentifier(key)) return key.name
|
|
438
|
+
}
|
|
439
|
+
if ((t.isClassMethod(path_.node) || t.isObjectMethod(path_.node)) && t.isIdentifier(path_.node.key)) {
|
|
440
|
+
return path_.node.key.name
|
|
441
|
+
}
|
|
442
|
+
return "function"
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function collect(ast, env) {
|
|
446
|
+
traverse(ast, {
|
|
447
|
+
ClassDeclaration(path_) {
|
|
448
|
+
if (path_.node.id) env.classes.add(path_.node.id.name)
|
|
449
|
+
},
|
|
450
|
+
|
|
451
|
+
VariableDeclarator(path_) {
|
|
452
|
+
const { id, init } = path_.node
|
|
453
|
+
if (!t.isIdentifier(id)) return
|
|
454
|
+
|
|
455
|
+
const binding = path_.scope.getBinding(id.name)
|
|
456
|
+
|
|
457
|
+
if (t.isCallExpression(init) && t.isIdentifier(init.callee)) {
|
|
458
|
+
const name = init.callee.name
|
|
459
|
+
const [nameArg, schema] = init.arguments
|
|
460
|
+
|
|
461
|
+
if (name === "__def_struct__" && t.isStringLiteral(nameArg) && t.isObjectExpression(schema)) {
|
|
462
|
+
const fields = new Map()
|
|
463
|
+
for (const property of schema.properties) {
|
|
464
|
+
if (!t.isObjectProperty(property) || !t.isStringLiteral(property.value)) continue
|
|
465
|
+
const key = t.isStringLiteral(property.key) ? property.key.value
|
|
466
|
+
: t.isIdentifier(property.key) ? property.key.name : null
|
|
467
|
+
if (key === null) continue
|
|
468
|
+
const optional = key.startsWith("*")
|
|
469
|
+
fields.set(optional ? key.slice(1).trim() : key, {
|
|
470
|
+
type: property.value.value,
|
|
471
|
+
optional
|
|
472
|
+
})
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const defaults = new Set()
|
|
476
|
+
if (t.isObjectExpression(init.arguments[3])) {
|
|
477
|
+
for (const property of init.arguments[3].properties) {
|
|
478
|
+
if (!t.isObjectProperty(property)) continue
|
|
479
|
+
const key = t.isStringLiteral(property.key) ? property.key.value
|
|
480
|
+
: t.isIdentifier(property.key) ? property.key.name : null
|
|
481
|
+
if (key !== null) defaults.add(key)
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const methods = new Set()
|
|
486
|
+
if (t.isObjectExpression(init.arguments[5])) {
|
|
487
|
+
for (const property of init.arguments[5].properties) {
|
|
488
|
+
if (!t.isObjectProperty(property)) continue
|
|
489
|
+
const key = t.isStringLiteral(property.key) ? property.key.value
|
|
490
|
+
: t.isIdentifier(property.key) ? property.key.name : null
|
|
491
|
+
if (key === null) continue
|
|
492
|
+
methods.add(key)
|
|
493
|
+
if (t.isFunction(property.value)) {
|
|
494
|
+
env.methodOwners.set(property.value, nameArg.value)
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const parent = t.isStringLiteral(init.arguments[4]) ? init.arguments[4].value : null
|
|
500
|
+
env.structs.set(nameArg.value, { fields, defaults, methods, parent })
|
|
501
|
+
return
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (name === "__def_enum__" && t.isStringLiteral(nameArg) && t.isObjectExpression(schema)) {
|
|
505
|
+
const members = new Set()
|
|
506
|
+
const valueTypes = new Set()
|
|
507
|
+
for (const property of schema.properties) {
|
|
508
|
+
if (!t.isObjectProperty(property)) continue
|
|
509
|
+
const key = t.isStringLiteral(property.key) ? property.key.value
|
|
510
|
+
: t.isIdentifier(property.key) ? property.key.name : null
|
|
511
|
+
if (key !== null) members.add(key)
|
|
512
|
+
if (t.isStringLiteral(property.value)) valueTypes.add("string")
|
|
513
|
+
else if (t.isNumericLiteral(property.value)) {
|
|
514
|
+
valueTypes.add(Number.isInteger(property.value.value) ? "int" : "float")
|
|
515
|
+
} else valueTypes.add("any")
|
|
516
|
+
}
|
|
517
|
+
env.enums.set(nameArg.value, { members, valueTypes })
|
|
518
|
+
return
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (name === "__type_def__" && t.isStringLiteral(nameArg)) {
|
|
522
|
+
env.customTypes.add(nameArg.value)
|
|
523
|
+
return
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const label = typedVariableLabel(init)
|
|
527
|
+
if (label && binding) env.bindings.set(binding, label)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const instance = structLiteralLabel(init, env)
|
|
531
|
+
if (instance && binding) env.bindings.set(binding, instance)
|
|
532
|
+
|
|
533
|
+
if ((t.isArrowFunctionExpression(init) || t.isFunctionExpression(init)) && binding) {
|
|
534
|
+
const signature = collectParameters(init)
|
|
535
|
+
env.functions.set(binding, signature)
|
|
536
|
+
if (binding.scope.path.isProgram()) env.functionsByName.set(id.name, signature)
|
|
537
|
+
}
|
|
538
|
+
},
|
|
539
|
+
|
|
540
|
+
FunctionDeclaration(path_) {
|
|
541
|
+
if (!path_.node.id) return
|
|
542
|
+
const name = path_.node.id.name
|
|
543
|
+
const binding = path_.scope.getBinding(name)
|
|
544
|
+
if (!binding) return
|
|
545
|
+
|
|
546
|
+
const signature = collectParameters(path_.node)
|
|
547
|
+
env.functions.set(binding, signature)
|
|
548
|
+
if (binding.scope.path.isProgram()) env.functionsByName.set(name, signature)
|
|
549
|
+
},
|
|
550
|
+
|
|
551
|
+
Function(path_) {
|
|
552
|
+
const { parameters, returns } = collectParameters(path_.node)
|
|
553
|
+
for (const parameter of parameters) {
|
|
554
|
+
if (!parameter.name || !parameter.type) continue
|
|
555
|
+
const binding = path_.scope.getBinding(parameter.name)
|
|
556
|
+
if (binding) env.bindings.set(binding, parameter.type)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (returns) {
|
|
560
|
+
env.returns.set(path_.node, { label: returns, name: functionName(path_) })
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
})
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function describe(label) {
|
|
567
|
+
return label.includes("|") || label.includes("&") ? `(${label})` : label
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function checkStructLiteral(structName, node, path_, env, report) {
|
|
571
|
+
const definition = resolveStruct(structName, env)
|
|
572
|
+
if (!definition) return
|
|
573
|
+
|
|
574
|
+
const seen = new Set()
|
|
575
|
+
for (const property of node.properties) {
|
|
576
|
+
if (!t.isObjectProperty(property)) return
|
|
577
|
+
const key = t.isIdentifier(property.key) && !property.computed ? property.key.name
|
|
578
|
+
: t.isStringLiteral(property.key) ? property.key.value : null
|
|
579
|
+
if (key === null) return
|
|
580
|
+
|
|
581
|
+
seen.add(key)
|
|
582
|
+
const field = definition.fields.get(key)
|
|
583
|
+
if (!field) {
|
|
584
|
+
report(property, `"${structName}" has no field "${key}"`)
|
|
585
|
+
continue
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const value = infer(property.value, path_, env)
|
|
589
|
+
if (!accepts(field.type, value, env)) {
|
|
590
|
+
report(property.value,
|
|
591
|
+
`"${structName}.${key}" expects ${describe(field.type)}, got ${describe(value)}`)
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
for (const [name, field] of definition.fields) {
|
|
596
|
+
if (field.optional || seen.has(name) || definition.defaults.has(name)) continue
|
|
597
|
+
report(node, `"${structName}" is missing field "${name}" of type ${describe(field.type)}`)
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Inspect lowered match expressions to preserve exhaustiveness checks.
|
|
602
|
+
function matchExpression(node) {
|
|
603
|
+
if (!t.isCallExpression(node) || node.arguments.length !== 1) return null
|
|
604
|
+
|
|
605
|
+
const callee = node.callee
|
|
606
|
+
if (!t.isArrowFunctionExpression(callee) || callee.params.length !== 1) return null
|
|
607
|
+
if (!t.isIdentifier(callee.params[0], { name: "__match" })) return null
|
|
608
|
+
if (!t.isBlockStatement(callee.body)) return null
|
|
609
|
+
|
|
610
|
+
const covered = []
|
|
611
|
+
let wildcard = false
|
|
612
|
+
let guarded = false
|
|
613
|
+
|
|
614
|
+
for (const statement of callee.body.body) {
|
|
615
|
+
if (t.isBlockStatement(statement)) { guarded = true; continue }
|
|
616
|
+
|
|
617
|
+
if (t.isIfStatement(statement)) {
|
|
618
|
+
const test = statement.test
|
|
619
|
+
if (t.isCallExpression(test) && t.isIdentifier(test.callee, { name: "__match_eq__" })) {
|
|
620
|
+
covered.push(test.arguments[1])
|
|
621
|
+
} else guarded = true
|
|
622
|
+
continue
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (t.isReturnStatement(statement)) {
|
|
626
|
+
wildcard = !t.isIdentifier(statement.argument, { name: "undefined" })
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
return { covered, wildcard, guarded, scrutinee: node.arguments[0] }
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function checkExhaustive(node, match, path_, env, report) {
|
|
634
|
+
if (match.guarded || match.wildcard) return
|
|
635
|
+
|
|
636
|
+
const label = infer(match.scrutinee, path_, env)
|
|
637
|
+
if (!label) return
|
|
638
|
+
|
|
639
|
+
const enumName = label.includes("::") ? label.split("::")[0] : label
|
|
640
|
+
const definition = env.enums.get(enumName)
|
|
641
|
+
if (!definition) return
|
|
642
|
+
|
|
643
|
+
const covered = new Set()
|
|
644
|
+
for (const arm of match.covered) {
|
|
645
|
+
if (!t.isMemberExpression(arm) || arm.computed) return
|
|
646
|
+
if (!t.isIdentifier(arm.object, { name: enumName })) return
|
|
647
|
+
if (!t.isIdentifier(arm.property)) return
|
|
648
|
+
covered.add(arm.property.name)
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const missing = [...definition.members].filter(member => !covered.has(member))
|
|
652
|
+
if (missing.length === 0) return
|
|
653
|
+
|
|
654
|
+
report(node, `match on "${enumName}" does not handle ${missing.map(member => `${enumName}.${member}`).join(", ")}` +
|
|
655
|
+
` — add the missing case${missing.length === 1 ? "" : "s"} or a "_" fallback`)
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function checkValue(label, node, path_, env, report, describeTarget, phrase = "expects") {
|
|
659
|
+
if (t.isObjectExpression(node) && env.structs.has(label)) {
|
|
660
|
+
checkStructLiteral(label, node, path_, env, report)
|
|
661
|
+
return
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const actual = infer(node, path_, env)
|
|
665
|
+
if (!accepts(label, actual, env)) {
|
|
666
|
+
report(node, `${describeTarget} ${phrase} ${describe(label)}, got ${describe(actual)}`)
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function verify(ast, env, report) {
|
|
671
|
+
traverse(ast, {
|
|
672
|
+
VariableDeclarator(path_) {
|
|
673
|
+
const { id, init } = path_.node
|
|
674
|
+
if (!t.isIdentifier(id) || !t.isCallExpression(init)) return
|
|
675
|
+
|
|
676
|
+
const label = typedVariableLabel(init)
|
|
677
|
+
if (!label) return
|
|
678
|
+
|
|
679
|
+
checkValue(label, init.arguments[0], path_, env, report, `"${id.name}"`)
|
|
680
|
+
},
|
|
681
|
+
|
|
682
|
+
AssignmentExpression(path_) {
|
|
683
|
+
const { node } = path_
|
|
684
|
+
if (!t.isIdentifier(node.left) || node.operator !== "=") return
|
|
685
|
+
|
|
686
|
+
const binding = path_.scope.getBinding(node.left.name)
|
|
687
|
+
const label = binding ? env.bindings.get(binding) : null
|
|
688
|
+
if (!label) return
|
|
689
|
+
|
|
690
|
+
checkValue(label, node.right, path_, env, report, `"${node.left.name}"`)
|
|
691
|
+
},
|
|
692
|
+
|
|
693
|
+
CallExpression(path_) {
|
|
694
|
+
const { node } = path_
|
|
695
|
+
|
|
696
|
+
const match = matchExpression(node)
|
|
697
|
+
if (match) checkExhaustive(node, match, path_, env, report)
|
|
698
|
+
|
|
699
|
+
const constructed = structLiteralLabel(node, env)
|
|
700
|
+
if (constructed && t.isObjectExpression(node.arguments[0])) {
|
|
701
|
+
const definition = resolveStruct(constructed, env)
|
|
702
|
+
if (!definition) return
|
|
703
|
+
|
|
704
|
+
for (const property of node.arguments[0].properties) {
|
|
705
|
+
if (!t.isObjectProperty(property)) return
|
|
706
|
+
const key = t.isIdentifier(property.key) && !property.computed ? property.key.name
|
|
707
|
+
: t.isStringLiteral(property.key) ? property.key.value : null
|
|
708
|
+
if (key === null) return
|
|
709
|
+
|
|
710
|
+
const field = definition.fields.get(key)
|
|
711
|
+
if (!field) {
|
|
712
|
+
report(property, `"${constructed}" has no field "${key}"`)
|
|
713
|
+
continue
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const value = infer(property.value, path_, env)
|
|
717
|
+
if (!accepts(field.type, value, env)) {
|
|
718
|
+
report(property.value,
|
|
719
|
+
`"${constructed}.${key}" expects ${describe(field.type)}, got ${describe(value)}`)
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (!t.isIdentifier(node.callee)) return
|
|
726
|
+
|
|
727
|
+
const signature = signatureFor(node.callee.name, path_, env)
|
|
728
|
+
if (!signature || !signature.typed) return
|
|
729
|
+
if (node.arguments.some(argument => t.isSpreadElement(argument))) return
|
|
730
|
+
|
|
731
|
+
const name = node.callee.name
|
|
732
|
+
const required = signature.parameters.filter(parameter => parameter.required).length
|
|
733
|
+
|
|
734
|
+
if (node.arguments.length < required) {
|
|
735
|
+
report(node, `"${name}" expects ${required} argument${required === 1 ? "" : "s"}, got ${node.arguments.length}`)
|
|
736
|
+
return
|
|
737
|
+
}
|
|
738
|
+
if (!signature.rest && node.arguments.length > signature.parameters.length) {
|
|
739
|
+
report(node,
|
|
740
|
+
`"${name}" takes ${signature.parameters.length} argument${signature.parameters.length === 1 ? "" : "s"}, got ${node.arguments.length}`)
|
|
741
|
+
return
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
node.arguments.forEach((argument, index) => {
|
|
745
|
+
const parameter = signature.parameters[index]
|
|
746
|
+
if (!parameter?.type) return
|
|
747
|
+
checkValue(parameter.type, argument, path_, env, report,
|
|
748
|
+
`argument "${parameter.name}" of "${name}"`)
|
|
749
|
+
})
|
|
750
|
+
},
|
|
751
|
+
|
|
752
|
+
ReturnStatement(path_) {
|
|
753
|
+
const fn = path_.getFunctionParent()
|
|
754
|
+
const declared = fn ? env.returns.get(fn.node) : null
|
|
755
|
+
if (!declared) return
|
|
756
|
+
|
|
757
|
+
const value = path_.node.argument
|
|
758
|
+
if (!value) {
|
|
759
|
+
if (!accepts(declared.label, "undefined", env)) {
|
|
760
|
+
report(path_.node,
|
|
761
|
+
`"${declared.name}" must return ${describe(declared.label)}, got undefined`)
|
|
762
|
+
}
|
|
763
|
+
return
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
checkValue(declared.label, value, path_, env, report, `"${declared.name}"`, "must return")
|
|
767
|
+
},
|
|
768
|
+
|
|
769
|
+
MemberExpression(path_) {
|
|
770
|
+
const { node } = path_
|
|
771
|
+
if (node.computed || !t.isIdentifier(node.property)) return
|
|
772
|
+
if (!t.isIdentifier(node.object) && !t.isThisExpression(node.object)) return
|
|
773
|
+
if (t.isAssignmentExpression(path_.parent) && path_.parent.left === node) return
|
|
774
|
+
|
|
775
|
+
const label = infer(node.object, path_, env)
|
|
776
|
+
const definition = label ? resolveStruct(label, env) : null
|
|
777
|
+
if (!definition) return
|
|
778
|
+
|
|
779
|
+
const field = node.property.name
|
|
780
|
+
if (definition.fields.has(field) || definition.methods.has(field)) return
|
|
781
|
+
|
|
782
|
+
report(node.property, `"${label}" has no field "${field}"`)
|
|
783
|
+
}
|
|
784
|
+
})
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function exportedNames(ast) {
|
|
788
|
+
const names = new Map()
|
|
789
|
+
|
|
790
|
+
traverse(ast, {
|
|
791
|
+
ExportNamedDeclaration(path_) {
|
|
792
|
+
const declaration = path_.node.declaration
|
|
793
|
+
|
|
794
|
+
if (t.isVariableDeclaration(declaration)) {
|
|
795
|
+
for (const declarator of declaration.declarations) {
|
|
796
|
+
if (t.isIdentifier(declarator.id)) names.set(declarator.id.name, declarator.id.name)
|
|
797
|
+
}
|
|
798
|
+
} else if ((t.isFunctionDeclaration(declaration) || t.isClassDeclaration(declaration)) && declaration.id) {
|
|
799
|
+
names.set(declaration.id.name, declaration.id.name)
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
for (const specifier of path_.node.specifiers ?? []) {
|
|
803
|
+
if (!t.isExportSpecifier(specifier)) continue
|
|
804
|
+
const exported = t.isIdentifier(specifier.exported)
|
|
805
|
+
? specifier.exported.name
|
|
806
|
+
: specifier.exported.value
|
|
807
|
+
names.set(exported, specifier.local.name)
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
})
|
|
811
|
+
|
|
812
|
+
return names
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function buildModuleTypes(ast, env) {
|
|
816
|
+
const record = {
|
|
817
|
+
structs: new Map(),
|
|
818
|
+
enums: new Map(),
|
|
819
|
+
customTypes: new Set(),
|
|
820
|
+
functions: new Map(),
|
|
821
|
+
ambient: { structs: new Map(), enums: new Map(), customTypes: new Set() }
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
for (const name of env.structs.keys()) {
|
|
825
|
+
const resolved = resolveStruct(name, env)
|
|
826
|
+
if (resolved) record.ambient.structs.set(name, resolved)
|
|
827
|
+
}
|
|
828
|
+
for (const [name, definition] of env.enums) record.ambient.enums.set(name, definition)
|
|
829
|
+
for (const name of env.customTypes) record.ambient.customTypes.add(name)
|
|
830
|
+
|
|
831
|
+
for (const [exported, local] of exportedNames(ast)) {
|
|
832
|
+
const struct = record.ambient.structs.get(local)
|
|
833
|
+
if (struct) record.structs.set(exported, struct)
|
|
834
|
+
if (env.enums.has(local)) record.enums.set(exported, env.enums.get(local))
|
|
835
|
+
if (env.customTypes.has(local)) record.customTypes.add(exported)
|
|
836
|
+
if (env.functionsByName.has(local)) record.functions.set(exported, env.functionsByName.get(local))
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return record
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Import known declarations; unresolved modules remain unchecked.
|
|
843
|
+
function seedImports(env, imports, wildcards) {
|
|
844
|
+
const merge = (record, name, local) => {
|
|
845
|
+
if (record.structs.has(name)) env.structs.set(local, record.structs.get(name))
|
|
846
|
+
if (record.enums.has(name)) env.enums.set(local, record.enums.get(name))
|
|
847
|
+
if (record.customTypes.has(name)) env.customTypes.add(local)
|
|
848
|
+
if (record.functions.has(name)) env.importedFunctions.set(local, record.functions.get(name))
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const sources = new Set([...wildcards, ...imports.map(entry => entry.source)])
|
|
852
|
+
|
|
853
|
+
// Local declarations override explicit and transitive imports.
|
|
854
|
+
for (const source of sources) {
|
|
855
|
+
const record = moduleTypes.get(source)
|
|
856
|
+
if (!record) continue
|
|
857
|
+
|
|
858
|
+
for (const [name, definition] of record.ambient.structs) {
|
|
859
|
+
if (!env.structs.has(name)) env.structs.set(name, definition)
|
|
860
|
+
}
|
|
861
|
+
for (const [name, definition] of record.ambient.enums) {
|
|
862
|
+
if (!env.enums.has(name)) env.enums.set(name, definition)
|
|
863
|
+
}
|
|
864
|
+
for (const name of record.ambient.customTypes) env.customTypes.add(name)
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
for (const source of wildcards) {
|
|
868
|
+
const record = moduleTypes.get(source)
|
|
869
|
+
if (!record) continue
|
|
870
|
+
for (const name of record.structs.keys()) merge(record, name, name)
|
|
871
|
+
for (const name of record.enums.keys()) merge(record, name, name)
|
|
872
|
+
for (const name of record.customTypes) merge(record, name, name)
|
|
873
|
+
for (const name of record.functions.keys()) merge(record, name, name)
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
for (const { local, imported, source } of imports) {
|
|
877
|
+
const record = moduleTypes.get(source)
|
|
878
|
+
if (record) merge(record, imported, local)
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
export function checkTypes(ast, { mapped, originalCode, sourceFile, imports = [], wildcards = [] }) {
|
|
883
|
+
const env = createEnvironment()
|
|
884
|
+
const diagnostics = []
|
|
885
|
+
const originalLines = computeLineStarts(originalCode)
|
|
886
|
+
const sourceLines = originalCode.split("\n")
|
|
887
|
+
|
|
888
|
+
const report = (node, message) => {
|
|
889
|
+
const offset = typeof node.start === "number" ? node.start : 0
|
|
890
|
+
const origin = mapped.origin[offset]
|
|
891
|
+
const location = origin >= 0 ? offsetToLineCol(origin, originalLines) : null
|
|
892
|
+
|
|
893
|
+
diagnostics.push({
|
|
894
|
+
message,
|
|
895
|
+
sourceFile,
|
|
896
|
+
line: location ? location.line + 1 : 0,
|
|
897
|
+
column: location ? location.column + 1 : 0,
|
|
898
|
+
sourceLine: location ? sourceLines[location.line] ?? "" : ""
|
|
899
|
+
})
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
seedImports(env, imports, wildcards)
|
|
903
|
+
collect(ast, env)
|
|
904
|
+
moduleTypes.set(path.resolve(sourceFile), buildModuleTypes(ast, env))
|
|
905
|
+
|
|
906
|
+
verify(ast, env, report)
|
|
907
|
+
|
|
908
|
+
diagnostics.sort((a, b) => a.line - b.line || a.column - b.column)
|
|
909
|
+
return diagnostics
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
export function formatDiagnostics(diagnostics) {
|
|
913
|
+
return diagnostics.map(({ message, sourceFile, line, column, sourceLine }) => {
|
|
914
|
+
const text = sourceLine.trim()
|
|
915
|
+
const indent = sourceLine.length - sourceLine.trimStart().length
|
|
916
|
+
const pointer = " ".repeat(Math.max(0, column - 1 - indent)) + "^"
|
|
917
|
+
|
|
918
|
+
return [
|
|
919
|
+
`TypeError: ${message}`,
|
|
920
|
+
` at ${sourceFile}:${line}:${column}`,
|
|
921
|
+
"",
|
|
922
|
+
` ${text}`,
|
|
923
|
+
` ${pointer}`
|
|
924
|
+
].join("\n")
|
|
925
|
+
}).join("\n\n")
|
|
926
|
+
}
|