xq-lsp 1.8.2 → 1.8.3-next.430
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 +16 -9
- package/builtins/builtins-fn.xq +1 -1
- package/dist/analyzer.d.ts +1 -0
- package/dist/analyzer.js +43 -3
- package/dist/ast-nodes.d.ts +38 -0
- package/dist/ast-nodes.js +71 -0
- package/dist/diagnostics.js +2 -0
- package/dist/functioncall-diagnostics.d.ts +6 -3
- package/dist/functioncall-diagnostics.js +76 -17
- package/dist/inlay-hints.js +28 -82
- package/dist/module-siblings.d.ts +11 -0
- package/dist/module-siblings.js +16 -0
- package/dist/predeclared-namespaces.js +1 -0
- package/dist/prolog-diagnostics.d.ts +10 -0
- package/dist/prolog-diagnostics.js +113 -0
- package/dist/qt4-worker.js +5 -3
- package/dist/runtimes/fonto/fonto.xq +95 -0
- package/dist/semantic-tokens.d.ts +29 -0
- package/dist/semantic-tokens.js +254 -0
- package/dist/server.js +40 -7
- package/dist/typechecker.d.ts +48 -1
- package/dist/typechecker.js +1215 -122
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
- package/src/analyzer.ts +45 -3
- package/src/ast-nodes.ts +85 -0
- package/src/diagnostics.ts +2 -0
- package/src/functioncall-diagnostics.ts +93 -15
- package/src/inlay-hints.ts +42 -93
- package/src/module-siblings.ts +17 -0
- package/src/predeclared-namespaces.ts +1 -0
- package/src/prolog-diagnostics.ts +137 -0
- package/src/qt4-worker.ts +6 -4
- package/src/runtimes/fonto/fonto.xq +95 -0
- package/src/semantic-tokens.ts +289 -0
- package/src/server.ts +40 -6
- package/src/typechecker.ts +1265 -123
- package/src/types.ts +2 -0
package/src/typechecker.ts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import type { Node, NonTerminal } from "xq-parser";
|
|
2
2
|
import type { XQueryType, TypeDiagnostic, FileAnalysis, FunctionSymbol } from "./types.ts";
|
|
3
3
|
import { formatQName, qnameKey } from "./types.ts";
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
findAll,
|
|
6
|
+
isTerminal,
|
|
7
|
+
directChildOf,
|
|
8
|
+
directChildrenOf,
|
|
9
|
+
firstTerminalValue,
|
|
10
|
+
parseEQName,
|
|
11
|
+
resolvePrefix,
|
|
12
|
+
XMLNS_ARRAY,
|
|
13
|
+
XMLNS_FN,
|
|
14
|
+
XMLNS_MAP,
|
|
15
|
+
} from "./analyzer.ts";
|
|
16
|
+
import { asArithmeticExpr, asFunctionCall, asNamedFunctionRef, asVarRef, asVarName, literalKind, isPathExpr, argExpr } from "./ast-nodes.ts";
|
|
6
17
|
|
|
7
18
|
// ── Type constants ───────────────────────────────────────────────────────────
|
|
8
19
|
|
|
@@ -174,11 +185,145 @@ export function isAssignable(from: XQueryType, to: XQueryType): boolean {
|
|
|
174
185
|
return true;
|
|
175
186
|
}
|
|
176
187
|
|
|
188
|
+
// ── Type algebra ──────────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
const EMPTY: XQueryType = { kind: "empty", occurrence: "" };
|
|
191
|
+
const BOOLEAN: XQueryType = { kind: "atomic", name: "xs:boolean", occurrence: "" };
|
|
192
|
+
const STRING: XQueryType = { kind: "atomic", name: "xs:string", occurrence: "" };
|
|
193
|
+
const INTEGER: XQueryType = { kind: "atomic", name: "xs:integer", occurrence: "" };
|
|
194
|
+
|
|
195
|
+
// Cardinality as [min, max] with max 2 standing for "many". `unknown` types are
|
|
196
|
+
// treated as "any number of items" so they never make a combined type look tighter
|
|
197
|
+
// than it is.
|
|
198
|
+
interface Card {
|
|
199
|
+
min: 0 | 1;
|
|
200
|
+
max: 0 | 1 | 2;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const ONE: Card = { min: 1, max: 1 };
|
|
204
|
+
|
|
205
|
+
function cardOf(t: XQueryType): Card {
|
|
206
|
+
if (t.kind === "empty") return { min: 0, max: 0 };
|
|
207
|
+
if (t.kind === "unknown") return { min: 0, max: 2 };
|
|
208
|
+
switch (t.occurrence) {
|
|
209
|
+
case "?":
|
|
210
|
+
return { min: 0, max: 1 };
|
|
211
|
+
case "+":
|
|
212
|
+
return { min: 1, max: 2 };
|
|
213
|
+
case "*":
|
|
214
|
+
return { min: 0, max: 2 };
|
|
215
|
+
default:
|
|
216
|
+
return ONE;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function withCard(t: XQueryType, c: Card): XQueryType {
|
|
221
|
+
if (t.kind === "unknown") return t;
|
|
222
|
+
if (c.max === 0 || t.kind === "empty") return EMPTY;
|
|
223
|
+
const occurrence = c.min === 1 ? (c.max === 1 ? "" : "+") : c.max === 1 ? "?" : "*";
|
|
224
|
+
return { ...t, occurrence };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Cardinality of evaluating `b` once per item of `a` (FLWOR `for`, `!`, path steps). */
|
|
228
|
+
function multiplyCard(a: Card, b: Card): Card {
|
|
229
|
+
const min = a.min === 1 && b.min === 1 ? 1 : 0;
|
|
230
|
+
const max = a.max === 0 || b.max === 0 ? 0 : a.max === 1 && b.max === 1 ? 1 : 2;
|
|
231
|
+
return { min, max };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** The type of a single item of `t` — e.g. what a `for` variable over `t` holds. */
|
|
235
|
+
export function itemTypeOf(t: XQueryType): XQueryType {
|
|
236
|
+
if (t.kind === "unknown" || t.kind === "empty") return t;
|
|
237
|
+
return t.occurrence === "" ? t : { ...t, occurrence: "" };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function derivesFrom(from: string, to: string): boolean {
|
|
241
|
+
for (let cur: string | undefined = from; cur !== undefined; cur = ATOMIC_TYPE_PARENT[cur]) {
|
|
242
|
+
if (cur === to) return true;
|
|
243
|
+
}
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Nearest common supertype in the derivation hierarchy (no promotion), falling back to
|
|
248
|
+
// xs:numeric for mixed numerics (e.g. `if (…) then 1 else 1e0`) and xs:anyAtomicType otherwise.
|
|
249
|
+
function commonAtomicSupertype(a: string, b: string): string {
|
|
250
|
+
for (let cur: string | undefined = a; cur !== undefined && cur !== "xs:anyAtomicType"; cur = ATOMIC_TYPE_PARENT[cur]) {
|
|
251
|
+
if (derivesFrom(b, cur)) return cur;
|
|
252
|
+
}
|
|
253
|
+
if (isAtomicSubtype(a, "xs:numeric") && isAtomicSubtype(b, "xs:numeric")) return "xs:numeric";
|
|
254
|
+
return "xs:anyAtomicType";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Least upper bound of two (non-empty, known) item types, ignoring occurrence.
|
|
258
|
+
function itemUnion(a: XQueryType, b: XQueryType): XQueryType {
|
|
259
|
+
if (a.kind === b.kind) {
|
|
260
|
+
switch (a.kind) {
|
|
261
|
+
case "atomic":
|
|
262
|
+
return { kind: "atomic", name: commonAtomicSupertype(a.name ?? "", b.name ?? ""), occurrence: "" };
|
|
263
|
+
case "node":
|
|
264
|
+
return { kind: "node", name: a.name === b.name ? a.name : "node", occurrence: "" };
|
|
265
|
+
case "map":
|
|
266
|
+
case "array":
|
|
267
|
+
case "function":
|
|
268
|
+
return { kind: a.kind, name: a.name === b.name ? a.name : `${a.kind}(*)`, occurrence: "" };
|
|
269
|
+
default:
|
|
270
|
+
return { kind: a.kind, occurrence: "" };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return { kind: "item", occurrence: "" };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Type of an expression that yields either `a` or `b` (if/else, switch, typeswitch, try/catch). */
|
|
277
|
+
export function choiceType(a: XQueryType, b: XQueryType): XQueryType {
|
|
278
|
+
if (a.kind === "unknown" || b.kind === "unknown") return UNKNOWN;
|
|
279
|
+
if (a.kind === "empty" && b.kind === "empty") return EMPTY;
|
|
280
|
+
const ca = cardOf(a);
|
|
281
|
+
const cb = cardOf(b);
|
|
282
|
+
const card: Card = { min: ca.min === 1 && cb.min === 1 ? 1 : 0, max: ca.max > cb.max ? ca.max : cb.max };
|
|
283
|
+
if (a.kind === "empty") return withCard(b, card);
|
|
284
|
+
if (b.kind === "empty") return withCard(a, card);
|
|
285
|
+
return withCard(itemUnion(a, b), card);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Type of the sequence `(a, b)`. */
|
|
289
|
+
export function concatType(a: XQueryType, b: XQueryType): XQueryType {
|
|
290
|
+
if (a.kind === "unknown" || b.kind === "unknown") return UNKNOWN;
|
|
291
|
+
if (a.kind === "empty") return b;
|
|
292
|
+
if (b.kind === "empty") return a;
|
|
293
|
+
const ca = cardOf(a);
|
|
294
|
+
const cb = cardOf(b);
|
|
295
|
+
return withCard(itemUnion(a, b), { min: 1, max: ca.max + cb.max > 1 ? 2 : 1 });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Splits the parameter list of a parameterized type name, e.g. "map(xs:string, item()*)"
|
|
299
|
+
// → ["xs:string", "item()*"], respecting nested parentheses.
|
|
300
|
+
function typeArgs(name: string): string[] {
|
|
301
|
+
const open = name.indexOf("(");
|
|
302
|
+
const inner = name.slice(open + 1, name.lastIndexOf(")"));
|
|
303
|
+
const args: string[] = [];
|
|
304
|
+
let depth = 0;
|
|
305
|
+
let startIdx = 0;
|
|
306
|
+
for (let i = 0; i < inner.length; i++) {
|
|
307
|
+
if (inner[i] === "(") depth++;
|
|
308
|
+
else if (inner[i] === ")") depth--;
|
|
309
|
+
else if (inner[i] === "," && depth === 0) {
|
|
310
|
+
args.push(inner.slice(startIdx, i).trim());
|
|
311
|
+
startIdx = i + 1;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
args.push(inner.slice(startIdx).trim());
|
|
315
|
+
return args.filter((a) => a !== "");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function typeText(t: XQueryType, fallback: string): string {
|
|
319
|
+
return t.kind === "unknown" ? fallback : formatType(t);
|
|
320
|
+
}
|
|
321
|
+
|
|
177
322
|
// ── Type inference ────────────────────────────────────────────────────────────
|
|
178
323
|
|
|
179
324
|
const LITERAL_TYPE: Record<string, XQueryType> = {
|
|
180
|
-
string:
|
|
181
|
-
integer:
|
|
325
|
+
string: STRING,
|
|
326
|
+
integer: INTEGER,
|
|
182
327
|
decimal: { kind: "atomic", name: "xs:decimal", occurrence: "" },
|
|
183
328
|
double: { kind: "atomic", name: "xs:double", occurrence: "" },
|
|
184
329
|
};
|
|
@@ -186,6 +331,9 @@ const LITERAL_TYPE: Record<string, XQueryType> = {
|
|
|
186
331
|
const NODE_STEP: XQueryType = { kind: "node", name: "node", occurrence: "*" };
|
|
187
332
|
const RANGE_SEQUENCE: XQueryType = { kind: "atomic", name: "xs:integer", occurrence: "*" };
|
|
188
333
|
|
|
334
|
+
/** Scope key under which the context item's type (`.`) is tracked, e.g. inside `!` and predicates. */
|
|
335
|
+
const CONTEXT_ITEM_KEY = ".";
|
|
336
|
+
|
|
189
337
|
// Reuses the existing xs:numeric union check and subtype/promotion chain (isAtomicSubtype)
|
|
190
338
|
// rather than a separate hardcoded numeric ordering.
|
|
191
339
|
function numericAtomicName(t: XQueryType): string | null {
|
|
@@ -198,16 +346,70 @@ function widenNumeric(a: string, b: string): string {
|
|
|
198
346
|
return "xs:double"; // neither promotes to the other — fall back to the widest common type
|
|
199
347
|
}
|
|
200
348
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (
|
|
208
|
-
|
|
349
|
+
const UNTYPED_ATOMIC = "xs:untypedAtomic";
|
|
350
|
+
|
|
351
|
+
// Atomized item type of `t`, keeping its cardinality. Nodes are assumed untyped (not
|
|
352
|
+
// schema-validated), so element/attribute/text/document values atomize to xs:untypedAtomic;
|
|
353
|
+
// comments and processing instructions atomize to xs:string.
|
|
354
|
+
function atomize(t: XQueryType): XQueryType {
|
|
355
|
+
if (t.kind === "atomic" || t.kind === "empty") return t;
|
|
356
|
+
if (t.kind === "node") {
|
|
357
|
+
const name = t.name === "comment" || t.name === "processing-instruction" ? "xs:string" : UNTYPED_ATOMIC;
|
|
358
|
+
return { kind: "atomic", name, occurrence: t.occurrence };
|
|
359
|
+
}
|
|
360
|
+
return UNKNOWN;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Arithmetic and aggregate functions cast xs:untypedAtomic operands to xs:double. */
|
|
364
|
+
function arithmeticOperandName(t: XQueryType): string | undefined {
|
|
365
|
+
return t.name === UNTYPED_ATOMIC ? "xs:double" : t.name;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const YEAR_MONTH = "xs:yearMonthDuration";
|
|
369
|
+
const DAY_TIME = "xs:dayTimeDuration";
|
|
370
|
+
const isDuration = (n: string) => n === YEAR_MONTH || n === DAY_TIME;
|
|
371
|
+
const isDateOrDateTime = (n: string) => n === "xs:date" || n === "xs:dateTime";
|
|
372
|
+
|
|
373
|
+
// Item type of `a op b` for single atomic operands, per the XPath 3.1 operator mapping table
|
|
374
|
+
// (numeric arithmetic, date/time subtraction, date/time ± duration, duration scaling).
|
|
375
|
+
function arithmeticItemName(op: string, a: string, b: string): string | null {
|
|
376
|
+
const aNum = isAtomicSubtype(a, "xs:numeric");
|
|
377
|
+
const bNum = isAtomicSubtype(b, "xs:numeric");
|
|
378
|
+
if (aNum && bNum) {
|
|
379
|
+
if (op === "idiv") return "xs:integer";
|
|
380
|
+
if (op === "div" && isAtomicSubtype(a, "xs:decimal") && isAtomicSubtype(b, "xs:decimal")) {
|
|
381
|
+
return widenNumeric(widenNumeric(a, b), "xs:decimal"); // integer div integer is never exact
|
|
382
|
+
}
|
|
383
|
+
return widenNumeric(a, b);
|
|
209
384
|
}
|
|
210
|
-
|
|
385
|
+
const additive = op === "+" || op === "-";
|
|
386
|
+
if (op === "-" && a === b && (isDateOrDateTime(a) || a === "xs:time")) return DAY_TIME;
|
|
387
|
+
if (additive && isDateOrDateTime(a) && isDuration(b)) return a;
|
|
388
|
+
if (additive && a === "xs:time" && b === DAY_TIME) return a;
|
|
389
|
+
if (op === "+" && isDuration(a) && (isDateOrDateTime(b) || (b === "xs:time" && a === DAY_TIME))) return b;
|
|
390
|
+
if (additive && isDuration(a) && a === b) return a;
|
|
391
|
+
if ((op === "*" || op === "div") && isDuration(a) && bNum) return a;
|
|
392
|
+
if (op === "*" && aNum && isDuration(b)) return b;
|
|
393
|
+
if (op === "div" && isDuration(a) && a === b) return "xs:decimal";
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Result type of a `+`/`-`/`*`/`div`/`idiv`/`mod` operator: operands are atomized, an empty
|
|
398
|
+
// operand gives (), an optional one makes the result optional, and more than one item is a
|
|
399
|
+
// type error (so we don't guess).
|
|
400
|
+
function arithmeticResult(op: string, a: XQueryType, b: XQueryType): XQueryType {
|
|
401
|
+
const ta = atomize(a);
|
|
402
|
+
const tb = atomize(b);
|
|
403
|
+
if (ta.kind === "unknown" || tb.kind === "unknown") return UNKNOWN;
|
|
404
|
+
if (ta.kind === "empty" || tb.kind === "empty") return EMPTY;
|
|
405
|
+
const ca = cardOf(ta);
|
|
406
|
+
const cb = cardOf(tb);
|
|
407
|
+
if (ca.max > 1 || cb.max > 1) return UNKNOWN;
|
|
408
|
+
const aName = arithmeticOperandName(ta);
|
|
409
|
+
const bName = arithmeticOperandName(tb);
|
|
410
|
+
const name = aName && bName ? arithmeticItemName(op, aName, bName) : null;
|
|
411
|
+
if (!name) return UNKNOWN;
|
|
412
|
+
return { kind: "atomic", name, occurrence: ca.min === 1 && cb.min === 1 ? "" : "?" };
|
|
211
413
|
}
|
|
212
414
|
|
|
213
415
|
function allFunctionsFlat(analysis: FileAnalysis, importedAnalyses: Map<string, FileAnalysis>): FunctionSymbol[] {
|
|
@@ -216,18 +418,703 @@ function allFunctionsFlat(analysis: FileAnalysis, importedAnalyses: Map<string,
|
|
|
216
418
|
return fns;
|
|
217
419
|
}
|
|
218
420
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
421
|
+
/** Finds the declaration a call with `argCount` arguments resolves to, honouring variadic functions. */
|
|
422
|
+
export function resolveFunction(
|
|
423
|
+
qname: { namespaceUri: string; localName: string },
|
|
424
|
+
argCount: number,
|
|
425
|
+
allFns: FunctionSymbol[],
|
|
426
|
+
): FunctionSymbol | undefined {
|
|
427
|
+
return allFns.find(
|
|
223
428
|
(f) =>
|
|
224
|
-
f.qname.namespaceUri ===
|
|
225
|
-
f.qname.localName ===
|
|
226
|
-
f.arity ===
|
|
429
|
+
f.qname.namespaceUri === qname.namespaceUri &&
|
|
430
|
+
f.qname.localName === qname.localName &&
|
|
431
|
+
(f.variadic ? argCount >= f.arity : f.arity === argCount),
|
|
227
432
|
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Concatenated terminal text of a node — enough to reconstruct a SequenceType for `parseType`. */
|
|
436
|
+
function nodeText(node: Node): string {
|
|
437
|
+
if (isTerminal(node)) return node.value;
|
|
438
|
+
return (node as NonTerminal).children.map(nodeText).join("");
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function sequenceTypeOf(node: Node | undefined): XQueryType | undefined {
|
|
442
|
+
if (!node) return undefined;
|
|
443
|
+
const text = nodeText(node);
|
|
444
|
+
// Typeswitch case unions (`case xs:integer | xs:string`) are a choice of their members.
|
|
445
|
+
if (text.includes("|")) return text.split("|").map(parseType).reduce(choiceType);
|
|
446
|
+
return parseType(text);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** The declared `as SequenceType` of a binding, parameter or variable declaration, if any. */
|
|
450
|
+
function declaredTypeOf(node: Node): XQueryType | undefined {
|
|
451
|
+
const typeDecl = directChildOf(node, "TypeDeclaration");
|
|
452
|
+
return typeDecl ? sequenceTypeOf(directChildOf(typeDecl, "SequenceType")) : undefined;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Generic built-ins whose declared signature (item()*) loses the argument's item type.
|
|
456
|
+
// Maps local name → the cardinality of the result relative to the first argument's item type.
|
|
457
|
+
const FN_SEQUENCE_PRESERVING: Record<string, Card | "same"> = {
|
|
458
|
+
head: { min: 0, max: 1 },
|
|
459
|
+
"exactly-one": ONE,
|
|
460
|
+
"zero-or-one": { min: 0, max: 1 },
|
|
461
|
+
"one-or-more": { min: 1, max: 2 },
|
|
462
|
+
tail: { min: 0, max: 2 },
|
|
463
|
+
subsequence: { min: 0, max: 2 },
|
|
464
|
+
remove: { min: 0, max: 2 },
|
|
465
|
+
filter: { min: 0, max: 2 },
|
|
466
|
+
reverse: "same",
|
|
467
|
+
unordered: "same",
|
|
468
|
+
sort: "same",
|
|
469
|
+
foot: { min: 0, max: 1 },
|
|
470
|
+
trunk: { min: 0, max: 2 },
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
/** `R` of a single `function(…) as R` item, if known. */
|
|
474
|
+
function functionResultType(f: XQueryType): XQueryType | undefined {
|
|
475
|
+
if (f.kind !== "function" || f.occurrence !== "" || !f.name?.includes(" as ")) return undefined;
|
|
476
|
+
const r = parseType(f.name.slice(f.name.lastIndexOf(" as ") + 4));
|
|
477
|
+
return r.kind === "unknown" ? undefined : r;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** Member types of a single map(K, V) / array(T): `[K, V]` or `[T]`, or undefined for `map(*)` etc. */
|
|
481
|
+
function containerArgs(t: XQueryType, kind: "map" | "array"): XQueryType[] | undefined {
|
|
482
|
+
if (t.kind !== kind || t.occurrence !== "" || !t.name) return undefined;
|
|
483
|
+
const args = typeArgs(t.name).map(parseType);
|
|
484
|
+
if (args.length !== (kind === "map" ? 2 : 1) || args.some((a) => a.kind === "unknown")) return undefined;
|
|
485
|
+
return args;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Aggregates (sum/avg/min/max): the atomized item name with untypedAtomic cast to xs:double,
|
|
489
|
+
// or undefined when it isn't numeric or a duration (or, for min/max, another comparable type).
|
|
490
|
+
function aggregateItemName(t: XQueryType, allowAnyAtomic: boolean): string | undefined {
|
|
491
|
+
const atomized = atomize(t);
|
|
492
|
+
if (atomized.kind !== "atomic") return undefined;
|
|
493
|
+
const name = arithmeticOperandName(atomized);
|
|
494
|
+
if (!name) return undefined;
|
|
495
|
+
if (isAtomicSubtype(name, "xs:numeric") || isDuration(name) || allowAnyAtomic) return name;
|
|
496
|
+
return undefined;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Built-ins whose declared signature (item()*, xs:anyAtomicType*, …) loses information the
|
|
500
|
+
// arguments carry: aggregates, atomization, higher-order functions, and map/array accessors.
|
|
501
|
+
function inferPolymorphicBuiltin(ns: string, name: string, args: XQueryType[]): XQueryType | undefined {
|
|
502
|
+
const [first, second, third] = args;
|
|
503
|
+
if (!first || first.kind === "unknown") return undefined;
|
|
504
|
+
const card = cardOf(first);
|
|
505
|
+
if (ns === XMLNS_FN) {
|
|
506
|
+
if (name in FN_SEQUENCE_PRESERVING) {
|
|
507
|
+
const rule = FN_SEQUENCE_PRESERVING[name];
|
|
508
|
+
return rule === "same" ? first : withCard(first, rule);
|
|
509
|
+
}
|
|
510
|
+
switch (name) {
|
|
511
|
+
case "data":
|
|
512
|
+
return args.length === 1 ? atomize(first) : undefined;
|
|
513
|
+
case "distinct-values": {
|
|
514
|
+
const atomized = atomize(first);
|
|
515
|
+
return atomized.kind === "unknown" ? undefined : withCard(atomized, { min: card.min, max: card.max });
|
|
516
|
+
}
|
|
517
|
+
case "sum": {
|
|
518
|
+
if (first.kind === "empty") return args.length === 1 ? INTEGER : second;
|
|
519
|
+
const item = aggregateItemName(first, false);
|
|
520
|
+
if (!item) return undefined;
|
|
521
|
+
const t: XQueryType = { kind: "atomic", name: item, occurrence: "" };
|
|
522
|
+
// sum(()) is 0, or the explicit zero value when one is given.
|
|
523
|
+
return card.min === 1 || args.length === 1 ? t : second && choiceType(t, second);
|
|
524
|
+
}
|
|
525
|
+
case "avg":
|
|
526
|
+
case "min":
|
|
527
|
+
case "max": {
|
|
528
|
+
if (first.kind === "empty") return EMPTY;
|
|
529
|
+
const item = aggregateItemName(first, name !== "avg");
|
|
530
|
+
if (!item) return undefined;
|
|
531
|
+
const avgName = isAtomicSubtype(item, "xs:decimal") ? "xs:decimal" : item;
|
|
532
|
+
return { kind: "atomic", name: name === "avg" ? avgName : item, occurrence: card.min === 1 ? "" : "?" };
|
|
533
|
+
}
|
|
534
|
+
case "for-each": {
|
|
535
|
+
const r = second && functionResultType(second);
|
|
536
|
+
return r && withCard(r, multiplyCard(card, cardOf(r)));
|
|
537
|
+
}
|
|
538
|
+
case "for-each-pair": {
|
|
539
|
+
const r = third && functionResultType(third);
|
|
540
|
+
if (!r || !second || second.kind === "unknown") return undefined;
|
|
541
|
+
const other = cardOf(second);
|
|
542
|
+
const pairs: Card = { min: card.min === 1 && other.min === 1 ? 1 : 0, max: card.max < other.max ? card.max : other.max };
|
|
543
|
+
return withCard(r, multiplyCard(pairs, cardOf(r)));
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
548
|
+
if (ns === XMLNS_MAP) {
|
|
549
|
+
switch (name) {
|
|
550
|
+
case "entry": {
|
|
551
|
+
const key = atomize(first);
|
|
552
|
+
if (key.kind !== "atomic" || !second || second.kind === "unknown") return undefined;
|
|
553
|
+
return { kind: "map", name: `map(${formatType(itemTypeOf(key))}, ${formatType(second)})`, occurrence: "" };
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
const kv = containerArgs(first, "map");
|
|
557
|
+
if (!kv) return undefined;
|
|
558
|
+
const [key, value] = kv;
|
|
559
|
+
switch (name) {
|
|
560
|
+
case "get":
|
|
561
|
+
return withCard(value, { min: 0, max: cardOf(value).max });
|
|
562
|
+
case "keys":
|
|
563
|
+
return withCard(key, { min: 0, max: 2 });
|
|
564
|
+
case "remove":
|
|
565
|
+
return first;
|
|
566
|
+
case "put": {
|
|
567
|
+
if (!second || !third || second.kind === "unknown" || third.kind === "unknown") return undefined;
|
|
568
|
+
const newKey = atomize(itemTypeOf(second));
|
|
569
|
+
if (newKey.kind !== "atomic") return undefined;
|
|
570
|
+
return { kind: "map", name: `map(${formatType(itemUnion(key, newKey))}, ${formatType(choiceType(value, third))})`, occurrence: "" };
|
|
571
|
+
}
|
|
572
|
+
case "for-each": {
|
|
573
|
+
const r = second && functionResultType(second);
|
|
574
|
+
return r && withCard(r, multiplyCard({ min: 0, max: 2 }, cardOf(r)));
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return undefined;
|
|
578
|
+
}
|
|
579
|
+
if (ns === XMLNS_ARRAY) {
|
|
580
|
+
const members = containerArgs(first, "array");
|
|
581
|
+
if (!members) return undefined;
|
|
582
|
+
const [member] = members;
|
|
583
|
+
switch (name) {
|
|
584
|
+
case "get":
|
|
585
|
+
case "head":
|
|
586
|
+
case "foot":
|
|
587
|
+
return member;
|
|
588
|
+
case "tail":
|
|
589
|
+
case "trunk":
|
|
590
|
+
case "reverse":
|
|
591
|
+
case "subarray":
|
|
592
|
+
case "remove":
|
|
593
|
+
case "filter":
|
|
594
|
+
case "sort":
|
|
595
|
+
return first;
|
|
596
|
+
case "append":
|
|
597
|
+
return second && second.kind !== "unknown" ? { kind: "array", name: `array(${formatType(choiceType(member, second))})`, occurrence: "" } : undefined;
|
|
598
|
+
case "for-each": {
|
|
599
|
+
const r = second && functionResultType(second);
|
|
600
|
+
return r && { kind: "array", name: `array(${formatType(r)})`, occurrence: "" };
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return undefined;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function inferCallResult(
|
|
608
|
+
qname: { namespaceUri: string; localName: string },
|
|
609
|
+
argTypes: () => XQueryType[],
|
|
610
|
+
argCount: number,
|
|
611
|
+
allFns: FunctionSymbol[],
|
|
612
|
+
): XQueryType {
|
|
613
|
+
if (argCount >= 1 && (qname.namespaceUri === XMLNS_FN || qname.namespaceUri === XMLNS_MAP || qname.namespaceUri === XMLNS_ARRAY)) {
|
|
614
|
+
const special = inferPolymorphicBuiltin(qname.namespaceUri, qname.localName, argTypes());
|
|
615
|
+
if (special) return special;
|
|
616
|
+
}
|
|
617
|
+
const fn = resolveFunction(qname, argCount, allFns);
|
|
228
618
|
return fn?.returnType ? parseType(fn.returnType) : UNKNOWN;
|
|
229
619
|
}
|
|
230
620
|
|
|
621
|
+
function functionTypeOf(fn: FunctionSymbol): XQueryType {
|
|
622
|
+
const params = fn.params.map((p) => p.type ?? "item()*").join(", ");
|
|
623
|
+
return { kind: "function", name: `function(${params}) as ${fn.returnType ?? "item()*"}`, occurrence: "" };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Kind of node selected by an axis step: `@x`/`attribute::x` → attribute, a plain name test on
|
|
627
|
+
// any other axis → element, a kind test → that kind, `..` → any node.
|
|
628
|
+
function axisStepType(step: Node): XQueryType {
|
|
629
|
+
const text = nodeText(step);
|
|
630
|
+
if (text.startsWith("..")) return NODE_STEP;
|
|
631
|
+
const kindTests = findAll(step, "KindTest");
|
|
632
|
+
if (kindTests.length > 0) {
|
|
633
|
+
const kind = nodeText(kindTests[0]).replace(/\(.*$/s, "");
|
|
634
|
+
const name = kind === "schema-element" ? "element" : kind === "schema-attribute" ? "attribute" : kind;
|
|
635
|
+
return { kind: "node", name, occurrence: "*" };
|
|
636
|
+
}
|
|
637
|
+
const isAttributeAxis = text.startsWith("@") || text.startsWith("attribute::");
|
|
638
|
+
return { kind: "node", name: isAttributeAxis ? "attribute" : "element", occurrence: "*" };
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function inferPathExpr(nt: NonTerminal, varTypes: Map<string, XQueryType>, analysis: FileAnalysis, allFns: FunctionSymbol[]): XQueryType {
|
|
642
|
+
const steps = nt.children.filter((c) => !isTerminal(c));
|
|
643
|
+
if (steps.length === 0) return { kind: "node", name: "document-node", occurrence: "" }; // lone "/"
|
|
644
|
+
// Every step before the last feeds the path's navigation — if one provably isn't a node
|
|
645
|
+
// (a literal, an atomic function result, …) the path is a type error; don't guess.
|
|
646
|
+
const definitelyNotNodes = steps.slice(0, -1).some((step) => {
|
|
647
|
+
const stepType = inferExprType(step, varTypes, analysis, allFns);
|
|
648
|
+
return stepType.kind !== "unknown" && stepType.kind !== "node" && stepType.kind !== "item";
|
|
649
|
+
});
|
|
650
|
+
if (definitelyNotNodes) return UNKNOWN;
|
|
651
|
+
const last = inferExprType(steps[steps.length - 1], varTypes, analysis, allFns);
|
|
652
|
+
if (last.kind === "node" || last.kind === "atomic") return withCard(last, { min: 0, max: 2 });
|
|
653
|
+
return NODE_STEP;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function inferPostfixExpr(
|
|
657
|
+
nt: NonTerminal,
|
|
658
|
+
varTypes: Map<string, XQueryType>,
|
|
659
|
+
analysis: FileAnalysis,
|
|
660
|
+
allFns: FunctionSymbol[],
|
|
661
|
+
): XQueryType {
|
|
662
|
+
const [base, ...postfixes] = nt.children.filter((c) => !isTerminal(c));
|
|
663
|
+
let t = inferExprType(base, varTypes, analysis, allFns);
|
|
664
|
+
for (const postfix of postfixes) {
|
|
665
|
+
if (t.kind === "unknown") return UNKNOWN;
|
|
666
|
+
const card = cardOf(t);
|
|
667
|
+
if (postfix.type === "Predicate") {
|
|
668
|
+
const expr = directChildOf(postfix, "Expr");
|
|
669
|
+
const predScope = new Map(varTypes);
|
|
670
|
+
predScope.set(CONTEXT_ITEM_KEY, itemTypeOf(t));
|
|
671
|
+
const predType = expr ? inferExprType(expr, predScope, analysis, allFns) : UNKNOWN;
|
|
672
|
+
// A single numeric predicate (`$seq[1]`, `$seq[$i]`) selects at most one item.
|
|
673
|
+
const positional = predType.occurrence === "" && numericAtomicName(predType) !== null;
|
|
674
|
+
t = withCard(t, { min: 0, max: positional && card.max > 1 ? 1 : card.max });
|
|
675
|
+
} else if (postfix.type === "Lookup") {
|
|
676
|
+
t = lookupType(t, postfix);
|
|
677
|
+
} else if (postfix.type === "ArgumentList" && t.kind === "function" && t.occurrence === "" && t.name?.includes(" as ")) {
|
|
678
|
+
t = parseType(t.name.slice(t.name.lastIndexOf(" as ") + 4));
|
|
679
|
+
} else {
|
|
680
|
+
return UNKNOWN;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return t;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// `$m?key` / `$a?1` / `$x?*` on a map(K, V) or array(T).
|
|
687
|
+
function lookupType(t: XQueryType, lookup: Node): XQueryType {
|
|
688
|
+
if ((t.kind !== "map" && t.kind !== "array") || !t.name) return UNKNOWN;
|
|
689
|
+
const args = typeArgs(t.name);
|
|
690
|
+
const memberText = t.kind === "map" ? args[1] : args[0];
|
|
691
|
+
if (!memberText || memberText === "*") return UNKNOWN;
|
|
692
|
+
const member = parseType(memberText);
|
|
693
|
+
if (member.kind === "unknown") return UNKNOWN;
|
|
694
|
+
const wildcard = nodeText(lookup).endsWith("*");
|
|
695
|
+
// A map lookup of a missing key yields (); an array lookup out of bounds is an error.
|
|
696
|
+
const perLookup: Card = wildcard ? { min: 0, max: 2 } : t.kind === "map" ? { min: 0, max: cardOf(member).max } : cardOf(member);
|
|
697
|
+
const each = wildcard || t.kind === "map" ? multiplyCard(perLookup, cardOf(member)) : perLookup;
|
|
698
|
+
return withCard(member, multiplyCard(cardOf(t), each));
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function inferMapConstructor(nt: NonTerminal, varTypes: Map<string, XQueryType>, analysis: FileAnalysis, allFns: FunctionSymbol[]): XQueryType {
|
|
702
|
+
const entries = directChildrenOf(nt, "MapConstructorEntry");
|
|
703
|
+
if (entries.length === 0) return { kind: "map", name: "map(*)", occurrence: "" };
|
|
704
|
+
let key: XQueryType | undefined;
|
|
705
|
+
let value: XQueryType | undefined;
|
|
706
|
+
for (const entry of entries) {
|
|
707
|
+
const k = directChildOf(entry, "MapKeyExpr");
|
|
708
|
+
const v = directChildOf(entry, "MapValueExpr");
|
|
709
|
+
const kt = k ? itemTypeOf(inferExprType(k, varTypes, analysis, allFns)) : UNKNOWN;
|
|
710
|
+
const vt = v ? inferExprType(v, varTypes, analysis, allFns) : UNKNOWN;
|
|
711
|
+
key = key === undefined ? kt : kt.kind === "unknown" || key.kind === "unknown" ? UNKNOWN : itemUnion(key, kt);
|
|
712
|
+
value = value === undefined ? vt : choiceType(value, vt);
|
|
713
|
+
}
|
|
714
|
+
const keyText = key && key.kind === "atomic" ? formatType(key) : "xs:anyAtomicType";
|
|
715
|
+
return { kind: "map", name: `map(${keyText}, ${typeText(value ?? UNKNOWN, "item()*")})`, occurrence: "" };
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function inferArrayConstructor(nt: NonTerminal, varTypes: Map<string, XQueryType>, analysis: FileAnalysis, allFns: FunctionSymbol[]): XQueryType {
|
|
719
|
+
let member: XQueryType | undefined;
|
|
720
|
+
if (nt.type === "SquareArrayConstructor") {
|
|
721
|
+
for (const m of directChildrenOf(nt, "ExprSingle")) {
|
|
722
|
+
const mt = inferExprType(m, varTypes, analysis, allFns);
|
|
723
|
+
member = member === undefined ? mt : choiceType(member, mt);
|
|
724
|
+
}
|
|
725
|
+
} else {
|
|
726
|
+
// array { E } has one member per item of E.
|
|
727
|
+
const enclosed = directChildOf(nt, "EnclosedExpr");
|
|
728
|
+
const et = enclosed ? inferExprType(enclosed, varTypes, analysis, allFns) : EMPTY;
|
|
729
|
+
if (et.kind !== "empty") member = itemTypeOf(et);
|
|
730
|
+
}
|
|
731
|
+
if (member === undefined || member.kind === "empty") return { kind: "array", name: "array(*)", occurrence: "" };
|
|
732
|
+
return { kind: "array", name: `array(${typeText(member, "item()*")})`, occurrence: "" };
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function inferArrowExpr(nt: NonTerminal, varTypes: Map<string, XQueryType>, analysis: FileAnalysis, allFns: FunctionSymbol[]): XQueryType {
|
|
736
|
+
let t: XQueryType | undefined;
|
|
737
|
+
let op: string | undefined;
|
|
738
|
+
for (let i = 0; i < nt.children.length; i++) {
|
|
739
|
+
const c = nt.children[i];
|
|
740
|
+
if (t === undefined) {
|
|
741
|
+
t = inferExprType(c, varTypes, analysis, allFns);
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
if (isTerminal(c)) {
|
|
745
|
+
op = c.value;
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
if (c.type !== "ArrowFunctionSpecifier") continue;
|
|
749
|
+
const eqname = directChildOf(c, "EQName");
|
|
750
|
+
const argList = nt.children[i + 1];
|
|
751
|
+
if (op !== "=>" || !eqname || argList?.type !== "ArgumentList") return UNKNOWN;
|
|
752
|
+
const raw = firstTerminalValue(eqname);
|
|
753
|
+
if (!raw) return UNKNOWN;
|
|
754
|
+
const { prefix, localName, uri } = parseEQName(raw);
|
|
755
|
+
const qname = { localName, namespaceUri: uri ?? resolvePrefix(prefix, analysis) };
|
|
756
|
+
const args = directChildrenOf(argList, "Argument");
|
|
757
|
+
const piped = t;
|
|
758
|
+
t = inferCallResult(
|
|
759
|
+
qname,
|
|
760
|
+
() => [piped, ...args.map((a) => inferExprType(a, varTypes, analysis, allFns))],
|
|
761
|
+
args.length + 1,
|
|
762
|
+
allFns,
|
|
763
|
+
);
|
|
764
|
+
i++; // skip the ArgumentList we just consumed
|
|
765
|
+
}
|
|
766
|
+
return t ?? UNKNOWN;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// ── Scoped constructs (shared by inference and the scope walker) ─────────────
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Lets callers observe the sub-expressions a scoped construct evaluates (with the scope
|
|
773
|
+
* they're evaluated in) and the bindings it introduces. `binding` fires only for bindings
|
|
774
|
+
* without a declared type — those are the ones whose type is inferred.
|
|
775
|
+
*/
|
|
776
|
+
export interface ScopeVisitor {
|
|
777
|
+
expr?(node: Node, scope: Map<string, XQueryType>): void;
|
|
778
|
+
binding?(nameNode: Node, type: XQueryType): void;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function flworClauses(flwor: Node): Node[] {
|
|
782
|
+
const out: Node[] = [];
|
|
783
|
+
const unwrap = (n: Node) => {
|
|
784
|
+
if (isTerminal(n)) return;
|
|
785
|
+
if (n.type === "InitialClause" || n.type === "IntermediateClause") {
|
|
786
|
+
for (const c of (n as NonTerminal).children) unwrap(c);
|
|
787
|
+
} else {
|
|
788
|
+
out.push(n);
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
for (const c of (flwor as NonTerminal).children) unwrap(c);
|
|
792
|
+
return out;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function bindVarName(nameNode: Node | undefined, analysis: FileAnalysis, t: XQueryType, scope: Map<string, XQueryType>): string | null {
|
|
796
|
+
if (!nameNode) return null;
|
|
797
|
+
const raw = firstTerminalValue(nameNode);
|
|
798
|
+
if (!raw) return null;
|
|
799
|
+
const { prefix, localName, uri } = parseEQName(raw);
|
|
800
|
+
const key = qnameKey({ prefix, localName, namespaceUri: uri ?? (prefix ? resolvePrefix(prefix, analysis) : "") });
|
|
801
|
+
scope.set(key, t);
|
|
802
|
+
return key;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function processFLWOR(
|
|
806
|
+
flwor: Node,
|
|
807
|
+
outer: Map<string, XQueryType>,
|
|
808
|
+
analysis: FileAnalysis,
|
|
809
|
+
allFns: FunctionSymbol[],
|
|
810
|
+
visitor: ScopeVisitor = {},
|
|
811
|
+
): XQueryType {
|
|
812
|
+
const scope = new Map(outer);
|
|
813
|
+
const boundHere: string[] = [];
|
|
814
|
+
let card: Card = ONE;
|
|
815
|
+
let result: XQueryType = UNKNOWN;
|
|
816
|
+
|
|
817
|
+
const evaluate = (expr: Node | undefined): XQueryType => {
|
|
818
|
+
if (!expr) return UNKNOWN;
|
|
819
|
+
visitor.expr?.(expr, scope);
|
|
820
|
+
return inferExprType(expr, scope, analysis, allFns);
|
|
821
|
+
};
|
|
822
|
+
const bind = (nameNode: Node | undefined, declared: XQueryType | undefined, inferred: XQueryType) => {
|
|
823
|
+
const key = bindVarName(nameNode, analysis, declared ?? inferred, scope);
|
|
824
|
+
if (key) boundHere.push(key);
|
|
825
|
+
if (!declared && nameNode) visitor.binding?.(nameNode, inferred);
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
for (const clause of flworClauses(flwor)) {
|
|
829
|
+
switch (clause.type) {
|
|
830
|
+
case "ForClause":
|
|
831
|
+
for (const b of directChildrenOf(clause, "ForBinding")) {
|
|
832
|
+
const seq = evaluate(directChildOf(b, "ExprSingle"));
|
|
833
|
+
const allowingEmpty = (b as NonTerminal).children.some((c) => isTerminal(c) && c.value === "allowing");
|
|
834
|
+
let item = itemTypeOf(seq);
|
|
835
|
+
let seqCard = cardOf(seq);
|
|
836
|
+
if (allowingEmpty) {
|
|
837
|
+
item = seqCard.min === 1 ? item : withCard(item, { min: 0, max: 1 });
|
|
838
|
+
seqCard = { min: 1, max: seqCard.max === 0 ? 1 : seqCard.max };
|
|
839
|
+
}
|
|
840
|
+
card = multiplyCard(card, seqCard);
|
|
841
|
+
bind(directChildOf(b, "VarName"), declaredTypeOf(b), item);
|
|
842
|
+
const pos = directChildOf(b, "PositionalVar");
|
|
843
|
+
if (pos) bindVarName(directChildOf(pos, "VarName"), analysis, INTEGER, scope);
|
|
844
|
+
}
|
|
845
|
+
break;
|
|
846
|
+
case "LetClause":
|
|
847
|
+
for (const b of directChildrenOf(clause, "LetBinding")) {
|
|
848
|
+
const t = evaluate(directChildOf(b, "ExprSingle"));
|
|
849
|
+
bind(directChildOf(b, "VarName"), declaredTypeOf(b), t);
|
|
850
|
+
}
|
|
851
|
+
break;
|
|
852
|
+
case "WindowClause":
|
|
853
|
+
for (const w of (clause as NonTerminal).children) {
|
|
854
|
+
if (isTerminal(w)) continue;
|
|
855
|
+
const seq = evaluate(directChildOf(w, "ExprSingle"));
|
|
856
|
+
const item = itemTypeOf(seq);
|
|
857
|
+
bind(directChildOf(w, "VarName"), declaredTypeOf(w), withCard(item, { min: 1, max: 2 }));
|
|
858
|
+
for (const cond of [directChildOf(w, "WindowStartCondition"), directChildOf(w, "WindowEndCondition")]) {
|
|
859
|
+
if (!cond) continue;
|
|
860
|
+
const vars = directChildOf(cond, "WindowVars");
|
|
861
|
+
if (vars) {
|
|
862
|
+
bindVarName(directChildOf(vars, "CurrentItem"), analysis, item, scope);
|
|
863
|
+
const pos = directChildOf(vars, "PositionalVar");
|
|
864
|
+
if (pos) bindVarName(directChildOf(pos, "VarName"), analysis, INTEGER, scope);
|
|
865
|
+
bindVarName(directChildOf(vars, "PreviousItem"), analysis, withCard(item, { min: 0, max: 1 }), scope);
|
|
866
|
+
bindVarName(directChildOf(vars, "NextItem"), analysis, withCard(item, { min: 0, max: 1 }), scope);
|
|
867
|
+
}
|
|
868
|
+
evaluate(directChildOf(cond, "ExprSingle"));
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
card = multiplyCard(card, { min: 0, max: 2 });
|
|
872
|
+
break;
|
|
873
|
+
case "WhereClause": {
|
|
874
|
+
const cond = directChildOf(clause, "ExprSingle");
|
|
875
|
+
evaluate(cond);
|
|
876
|
+
// Later clauses only see tuples for which the condition held.
|
|
877
|
+
if (cond) for (const [k, t] of narrowScope(cond, scope, true, analysis)) scope.set(k, t);
|
|
878
|
+
card = { min: 0, max: card.max };
|
|
879
|
+
break;
|
|
880
|
+
}
|
|
881
|
+
case "CountClause":
|
|
882
|
+
bindVarName(directChildOf(clause, "VarName"), analysis, INTEGER, scope);
|
|
883
|
+
break;
|
|
884
|
+
case "GroupByClause": {
|
|
885
|
+
const keys = new Set<string>();
|
|
886
|
+
for (const spec of findAll(clause, "GroupingSpec")) {
|
|
887
|
+
const groupVar = directChildOf(spec, "GroupingVariable");
|
|
888
|
+
const nameNode = groupVar ? directChildOf(groupVar, "VarName") : undefined;
|
|
889
|
+
const init = directChildOf(spec, "ExprSingle");
|
|
890
|
+
let key: string | null;
|
|
891
|
+
if (init) {
|
|
892
|
+
const t = itemTypeOf(evaluate(init));
|
|
893
|
+
const declared = declaredTypeOf(spec);
|
|
894
|
+
key = bindVarName(nameNode, analysis, declared ?? t, scope);
|
|
895
|
+
if (!declared && nameNode) visitor.binding?.(nameNode, t);
|
|
896
|
+
} else {
|
|
897
|
+
const existing = nameNode ? asVarName(nameNode, analysis) : null;
|
|
898
|
+
key = existing ? qnameKey(existing) : null;
|
|
899
|
+
if (key && scope.has(key)) scope.set(key, itemTypeOf(scope.get(key)!));
|
|
900
|
+
}
|
|
901
|
+
if (key) keys.add(key);
|
|
902
|
+
}
|
|
903
|
+
// Every other variable bound so far now holds the whole group's values.
|
|
904
|
+
for (const key of boundHere) {
|
|
905
|
+
if (keys.has(key)) continue;
|
|
906
|
+
const t = scope.get(key);
|
|
907
|
+
if (t) scope.set(key, withCard(t, { min: cardOf(t).min, max: cardOf(t).max === 0 ? 0 : 2 }));
|
|
908
|
+
}
|
|
909
|
+
break;
|
|
910
|
+
}
|
|
911
|
+
case "ReturnClause":
|
|
912
|
+
result = evaluate(directChildOf(clause, "ExprSingle"));
|
|
913
|
+
break;
|
|
914
|
+
default:
|
|
915
|
+
// order by, and anything we don't model: just evaluate its sub-expressions in scope.
|
|
916
|
+
visitor.expr?.(clause, scope);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (result.kind === "unknown") return UNKNOWN;
|
|
921
|
+
return withCard(result, multiplyCard(card, cardOf(result)));
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function processQuantified(
|
|
925
|
+
node: NonTerminal,
|
|
926
|
+
outer: Map<string, XQueryType>,
|
|
927
|
+
analysis: FileAnalysis,
|
|
928
|
+
allFns: FunctionSymbol[],
|
|
929
|
+
visitor: ScopeVisitor = {},
|
|
930
|
+
): XQueryType {
|
|
931
|
+
const scope = new Map(outer);
|
|
932
|
+
let pendingName: Node | undefined;
|
|
933
|
+
let pendingDeclared: XQueryType | undefined;
|
|
934
|
+
for (const c of node.children) {
|
|
935
|
+
if (c.type === "VarName") {
|
|
936
|
+
pendingName = c;
|
|
937
|
+
pendingDeclared = undefined;
|
|
938
|
+
} else if (c.type === "TypeDeclaration") {
|
|
939
|
+
pendingDeclared = sequenceTypeOf(directChildOf(c, "SequenceType"));
|
|
940
|
+
} else if (c.type === "ExprSingle") {
|
|
941
|
+
visitor.expr?.(c, scope);
|
|
942
|
+
if (pendingName) {
|
|
943
|
+
const t = inferExprType(c, scope, analysis, allFns);
|
|
944
|
+
bindVarName(pendingName, analysis, pendingDeclared ?? itemTypeOf(t), scope);
|
|
945
|
+
pendingName = undefined;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
return BOOLEAN;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function processTypeswitch(
|
|
953
|
+
node: NonTerminal,
|
|
954
|
+
outer: Map<string, XQueryType>,
|
|
955
|
+
analysis: FileAnalysis,
|
|
956
|
+
allFns: FunctionSymbol[],
|
|
957
|
+
visitor: ScopeVisitor = {},
|
|
958
|
+
): XQueryType {
|
|
959
|
+
const operand = directChildOf(node, "Expr");
|
|
960
|
+
if (operand) visitor.expr?.(operand, outer);
|
|
961
|
+
const operandType = operand ? inferExprType(operand, outer, analysis, allFns) : UNKNOWN;
|
|
962
|
+
|
|
963
|
+
let result: XQueryType | undefined;
|
|
964
|
+
const branch = (varName: Node | undefined, varType: XQueryType, expr: Node | undefined) => {
|
|
965
|
+
const scope = new Map(outer);
|
|
966
|
+
bindVarName(varName, analysis, varType, scope);
|
|
967
|
+
if (!expr) return;
|
|
968
|
+
visitor.expr?.(expr, scope);
|
|
969
|
+
const t = inferExprType(expr, scope, analysis, allFns);
|
|
970
|
+
result = result === undefined ? t : choiceType(result, t);
|
|
971
|
+
};
|
|
972
|
+
for (const clause of directChildrenOf(node, "CaseClause")) {
|
|
973
|
+
branch(directChildOf(clause, "VarName"), sequenceTypeOf(directChildOf(clause, "SequenceTypeUnion")) ?? UNKNOWN, directChildOf(clause, "ExprSingle"));
|
|
974
|
+
}
|
|
975
|
+
branch(directChildOf(node, "VarName"), operandType, directChildOf(node, "ExprSingle"));
|
|
976
|
+
return result ?? UNKNOWN;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function collectParamTypes(paramList: Node | undefined, analysis: FileAnalysis, scope: Map<string, XQueryType>): string[] {
|
|
980
|
+
const texts: string[] = [];
|
|
981
|
+
for (const param of paramList ? directChildrenOf(paramList, "Param") : []) {
|
|
982
|
+
const declared = declaredTypeOf(param);
|
|
983
|
+
bindVarName(directChildOf(param, "EQName"), analysis, declared ?? UNKNOWN, scope);
|
|
984
|
+
texts.push(declared ? formatType(declared) : "item()*");
|
|
985
|
+
}
|
|
986
|
+
return texts;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function processInlineFunction(
|
|
990
|
+
node: NonTerminal,
|
|
991
|
+
outer: Map<string, XQueryType>,
|
|
992
|
+
analysis: FileAnalysis,
|
|
993
|
+
allFns: FunctionSymbol[],
|
|
994
|
+
visitor: ScopeVisitor = {},
|
|
995
|
+
): XQueryType {
|
|
996
|
+
// Inline functions close over the variables in scope, but have no context item.
|
|
997
|
+
const scope = new Map(outer);
|
|
998
|
+
scope.delete(CONTEXT_ITEM_KEY);
|
|
999
|
+
const params = collectParamTypes(directChildOf(node, "ParamList"), analysis, scope);
|
|
1000
|
+
const body = directChildOf(node, "FunctionBody");
|
|
1001
|
+
if (body) visitor.expr?.(body, scope);
|
|
1002
|
+
const declared = sequenceTypeOf(directChildOf(node, "SequenceType"));
|
|
1003
|
+
const ret = declared ?? (body ? inferExprType(body, scope, analysis, allFns) : EMPTY);
|
|
1004
|
+
return { kind: "function", name: `function(${params.join(", ")}) as ${typeText(ret, "item()*")}`, occurrence: "" };
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Unwraps the grammar's single-child precedence chain down to the node that does something.
|
|
1008
|
+
function coreExpr(node: Node): Node {
|
|
1009
|
+
let n = node;
|
|
1010
|
+
while (!isTerminal(n) && n.type !== "VarRef") {
|
|
1011
|
+
const ops = nonTerminalChildren(n as NonTerminal);
|
|
1012
|
+
if (ops.length !== 1) break;
|
|
1013
|
+
n = ops[0];
|
|
1014
|
+
}
|
|
1015
|
+
return n;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function varKeyOf(node: Node, analysis: FileAnalysis): string | null {
|
|
1019
|
+
const core = coreExpr(node);
|
|
1020
|
+
const qname = core.type === "VarRef" ? asVarRef(core, analysis) : null;
|
|
1021
|
+
return qname ? qnameKey(qname) : null;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Variable types that hold where `cond` is known to be true (`positive`) or false.
|
|
1026
|
+
* Understands `$x instance of T`, `exists($x)`, `empty($x)`, a bare `$x` (its effective
|
|
1027
|
+
* boolean value is false for ()), `not(…)`, and conjunctions / disjunctions of those.
|
|
1028
|
+
*/
|
|
1029
|
+
function narrowScope(cond: Node, scope: Map<string, XQueryType>, positive: boolean, analysis: FileAnalysis): Map<string, XQueryType> {
|
|
1030
|
+
const core = coreExpr(cond);
|
|
1031
|
+
const ops = isTerminal(core) ? [] : nonTerminalChildren(core as NonTerminal);
|
|
1032
|
+
const nonEmpty = (key: string | null, out: Map<string, XQueryType>) => {
|
|
1033
|
+
const t = key ? out.get(key) : undefined;
|
|
1034
|
+
if (!key || !t || t.kind === "unknown" || t.kind === "empty") return out;
|
|
1035
|
+
const c = cardOf(t);
|
|
1036
|
+
if (c.min === 0) out.set(key, withCard(t, { min: 1, max: c.max }));
|
|
1037
|
+
return out;
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1040
|
+
if ((core.type === "AndExpr" && positive) || (core.type === "OrExpr" && !positive)) {
|
|
1041
|
+
return ops.reduce((s, op) => narrowScope(op, s, positive, analysis), scope);
|
|
1042
|
+
}
|
|
1043
|
+
if (core.type === "VarRef" && positive) return nonEmpty(varKeyOf(core, analysis), new Map(scope));
|
|
1044
|
+
if (core.type === "InstanceofExpr" && positive && ops.length === 2) {
|
|
1045
|
+
const key = varKeyOf(ops[0], analysis);
|
|
1046
|
+
const narrowed = sequenceTypeOf(directChildOf(core, "SequenceType"));
|
|
1047
|
+
const current = key ? scope.get(key) : undefined;
|
|
1048
|
+
if (!key || !narrowed || narrowed.kind === "unknown") return scope;
|
|
1049
|
+
// Only ever make a type more specific: `$int instance of xs:decimal` tells us nothing new.
|
|
1050
|
+
if (current && current.kind !== "unknown" && !isAssignable(narrowed, current)) return scope;
|
|
1051
|
+
return new Map(scope).set(key, narrowed);
|
|
1052
|
+
}
|
|
1053
|
+
if (core.type === "FunctionCall") {
|
|
1054
|
+
const call = asFunctionCall(core, analysis);
|
|
1055
|
+
if (!call || call.qname.namespaceUri !== XMLNS_FN || call.args.length !== 1) return scope;
|
|
1056
|
+
const arg = argExpr(call.args[0]);
|
|
1057
|
+
if (!arg) return scope;
|
|
1058
|
+
const name = call.qname.localName;
|
|
1059
|
+
if (name === "not") return narrowScope(arg, scope, !positive, analysis);
|
|
1060
|
+
if ((name === "exists" && positive) || (name === "empty" && !positive) || (name === "boolean" && positive)) {
|
|
1061
|
+
return nonEmpty(varKeyOf(arg, analysis), new Map(scope));
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return scope;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function processIf(
|
|
1068
|
+
node: NonTerminal,
|
|
1069
|
+
scope: Map<string, XQueryType>,
|
|
1070
|
+
analysis: FileAnalysis,
|
|
1071
|
+
allFns: FunctionSymbol[],
|
|
1072
|
+
visitor: ScopeVisitor = {},
|
|
1073
|
+
): XQueryType {
|
|
1074
|
+
const cond = directChildOf(node, "Expr");
|
|
1075
|
+
if (cond) visitor.expr?.(cond, scope);
|
|
1076
|
+
// `then`/`else` are ExprSingle; XQuery 4's braced `if (c) { … }` has one EnclosedExpr.
|
|
1077
|
+
const branches = [...directChildrenOf(node, "ExprSingle"), ...directChildrenOf(node, "EnclosedExpr")];
|
|
1078
|
+
const types = branches.map((branch, i) => {
|
|
1079
|
+
const branchScope = cond ? narrowScope(cond, scope, i === 0, analysis) : scope;
|
|
1080
|
+
visitor.expr?.(branch, branchScope);
|
|
1081
|
+
return inferExprType(branch, branchScope, analysis, allFns);
|
|
1082
|
+
});
|
|
1083
|
+
if (types.length === 0) return UNKNOWN;
|
|
1084
|
+
return types.length === 1 ? choiceType(types[0], EMPTY) : types.reduce(choiceType);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// `copy $c := E modify U return R` — `$c` is a copy of E, so it has E's type.
|
|
1088
|
+
function processTransform(
|
|
1089
|
+
node: NonTerminal,
|
|
1090
|
+
outer: Map<string, XQueryType>,
|
|
1091
|
+
analysis: FileAnalysis,
|
|
1092
|
+
allFns: FunctionSymbol[],
|
|
1093
|
+
visitor: ScopeVisitor = {},
|
|
1094
|
+
): XQueryType {
|
|
1095
|
+
const scope = new Map(outer);
|
|
1096
|
+
const bindingList = directChildOf(node, "XQUF_CopyBindingList");
|
|
1097
|
+
for (const b of bindingList ? directChildrenOf(bindingList, "XQUF_CopyBinding") : []) {
|
|
1098
|
+
const init = directChildOf(b, "ExprSingle");
|
|
1099
|
+
if (init) visitor.expr?.(init, scope);
|
|
1100
|
+
const t = init ? inferExprType(init, scope, analysis, allFns) : UNKNOWN;
|
|
1101
|
+
const nameNode = directChildOf(b, "VarName");
|
|
1102
|
+
bindVarName(nameNode, analysis, t, scope);
|
|
1103
|
+
if (nameNode) visitor.binding?.(nameNode, t);
|
|
1104
|
+
}
|
|
1105
|
+
const [modifyExpr, returnExpr] = directChildrenOf(node, "ExprSingle");
|
|
1106
|
+
if (modifyExpr) visitor.expr?.(modifyExpr, scope);
|
|
1107
|
+
if (!returnExpr) return UNKNOWN;
|
|
1108
|
+
visitor.expr?.(returnExpr, scope);
|
|
1109
|
+
return inferExprType(returnExpr, scope, analysis, allFns);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// ── Expression inference ──────────────────────────────────────────────────────
|
|
1113
|
+
|
|
1114
|
+
function nonTerminalChildren(nt: NonTerminal): Node[] {
|
|
1115
|
+
return nt.children.filter((c) => !isTerminal(c));
|
|
1116
|
+
}
|
|
1117
|
+
|
|
231
1118
|
export function inferExprType(
|
|
232
1119
|
node: Node,
|
|
233
1120
|
varTypes: Map<string, XQueryType>,
|
|
@@ -239,6 +1126,7 @@ export function inferExprType(
|
|
|
239
1126
|
if (isTerminal(node)) return UNKNOWN;
|
|
240
1127
|
|
|
241
1128
|
const nt = node as NonTerminal;
|
|
1129
|
+
const operands = nonTerminalChildren(nt);
|
|
242
1130
|
|
|
243
1131
|
switch (node.type) {
|
|
244
1132
|
case "Literal":
|
|
@@ -253,48 +1141,155 @@ export function inferExprType(
|
|
|
253
1141
|
const qname = asVarRef(node, analysis);
|
|
254
1142
|
return qname ? (varTypes.get(qnameKey(qname)) ?? UNKNOWN) : UNKNOWN;
|
|
255
1143
|
}
|
|
1144
|
+
case "ContextItemExpr":
|
|
1145
|
+
return varTypes.get(CONTEXT_ITEM_KEY) ?? UNKNOWN;
|
|
1146
|
+
case "ParenthesizedExpr":
|
|
1147
|
+
if (operands.length === 0) return EMPTY;
|
|
1148
|
+
break;
|
|
1149
|
+
case "Expr":
|
|
1150
|
+
// Comma operator: `(a, b, c)`.
|
|
1151
|
+
if (operands.length > 1) {
|
|
1152
|
+
return operands.map((o) => inferExprType(o, varTypes, analysis, allFns)).reduce(concatType);
|
|
1153
|
+
}
|
|
1154
|
+
break;
|
|
1155
|
+
case "IfExpr":
|
|
1156
|
+
return processIf(nt, varTypes, analysis, allFns);
|
|
1157
|
+
case "XQUF_TransformExpr":
|
|
1158
|
+
return processTransform(nt, varTypes, analysis, allFns);
|
|
1159
|
+
case "SwitchExpr": {
|
|
1160
|
+
const results = [
|
|
1161
|
+
...directChildrenOf(node, "SwitchCaseClause").map((c) => directChildOf(c, "ExprSingle")),
|
|
1162
|
+
...directChildrenOf(node, "ExprSingle"),
|
|
1163
|
+
];
|
|
1164
|
+
if (results.some((r) => !r)) return UNKNOWN;
|
|
1165
|
+
return results.map((r) => inferExprType(r!, varTypes, analysis, allFns)).reduce(choiceType);
|
|
1166
|
+
}
|
|
1167
|
+
case "TypeswitchExpr":
|
|
1168
|
+
return processTypeswitch(nt, varTypes, analysis, allFns);
|
|
1169
|
+
case "TryCatchExpr": {
|
|
1170
|
+
const bodies = [...findAll(node, "EnclosedTryTargetExpr"), ...directChildrenOf(node, "CatchClause").map((c) => directChildOf(c, "EnclosedExpr"))];
|
|
1171
|
+
if (bodies.some((b) => !b)) return UNKNOWN;
|
|
1172
|
+
return bodies.map((b) => inferExprType(b!, varTypes, analysis, allFns)).reduce(choiceType);
|
|
1173
|
+
}
|
|
1174
|
+
case "FLWORExpr":
|
|
1175
|
+
return processFLWOR(node, varTypes, analysis, allFns);
|
|
1176
|
+
case "QuantifiedExpr":
|
|
1177
|
+
return processQuantified(nt, varTypes, analysis, allFns);
|
|
1178
|
+
case "InlineFunctionExpr":
|
|
1179
|
+
return processInlineFunction(nt, varTypes, analysis, allFns);
|
|
1180
|
+
case "NamedFunctionRef": {
|
|
1181
|
+
const ref = asNamedFunctionRef(node, analysis);
|
|
1182
|
+
const fn = ref ? resolveFunction(ref.qname, ref.arity, allFns) : undefined;
|
|
1183
|
+
return fn ? functionTypeOf(fn) : { kind: "function", name: "function(*)", occurrence: "" };
|
|
1184
|
+
}
|
|
1185
|
+
case "ComparisonExpr": {
|
|
1186
|
+
if (operands.length < 3) break;
|
|
1187
|
+
// General comparisons (=, <, …) are always a boolean. Value and node comparisons
|
|
1188
|
+
// (eq, is, <<, …) yield () for an empty operand.
|
|
1189
|
+
if (operands[1].type === "GeneralComp") return BOOLEAN;
|
|
1190
|
+
const [a, b] = [operands[0], operands[2]].map((o) => inferExprType(o, varTypes, analysis, allFns));
|
|
1191
|
+
if (a.kind === "unknown" || b.kind === "unknown") return withCard(BOOLEAN, { min: 0, max: 1 });
|
|
1192
|
+
if (a.kind === "empty" || b.kind === "empty") return EMPTY;
|
|
1193
|
+
return cardOf(a).min === 1 && cardOf(b).min === 1 ? BOOLEAN : withCard(BOOLEAN, { min: 0, max: 1 });
|
|
1194
|
+
}
|
|
1195
|
+
case "OrExpr":
|
|
1196
|
+
case "AndExpr":
|
|
1197
|
+
case "InstanceofExpr":
|
|
1198
|
+
case "CastableExpr":
|
|
1199
|
+
if (operands.length > 1) return BOOLEAN;
|
|
1200
|
+
break;
|
|
1201
|
+
case "StringConcatExpr":
|
|
1202
|
+
if (operands.length > 1) return STRING;
|
|
1203
|
+
break;
|
|
1204
|
+
case "StringConstructor":
|
|
1205
|
+
return STRING;
|
|
1206
|
+
case "CastExpr":
|
|
1207
|
+
if (operands.length > 1) return sequenceTypeOf(directChildOf(node, "SingleType")) ?? UNKNOWN;
|
|
1208
|
+
break;
|
|
1209
|
+
case "TreatExpr":
|
|
1210
|
+
if (operands.length > 1) return sequenceTypeOf(directChildOf(node, "SequenceType")) ?? UNKNOWN;
|
|
1211
|
+
break;
|
|
1212
|
+
case "UnaryExpr": {
|
|
1213
|
+
// Every expression passes through UnaryExpr; only a leading +/- makes it arithmetic.
|
|
1214
|
+
const operand = directChildOf(node, "ValueExpr");
|
|
1215
|
+
if (!operand || !nt.children.some(isTerminal)) break;
|
|
1216
|
+
// Same operand rules as binary arithmetic: `-$x` behaves like `0 - $x` type-wise.
|
|
1217
|
+
return arithmeticResult("*", inferExprType(operand, varTypes, analysis, allFns), INTEGER);
|
|
1218
|
+
}
|
|
256
1219
|
case "AdditiveExpr":
|
|
257
1220
|
case "MultiplicativeExpr": {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
let
|
|
261
|
-
for (
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
1221
|
+
const arith = asArithmeticExpr(node);
|
|
1222
|
+
if (!arith) break;
|
|
1223
|
+
let result = inferExprType(arith.operands[0], varTypes, analysis, allFns);
|
|
1224
|
+
for (let i = 0; i < arith.operators.length; i++) {
|
|
1225
|
+
const t = inferExprType(arith.operands[i + 1], varTypes, analysis, allFns);
|
|
1226
|
+
result = arithmeticResult(arith.operators[i], result, t);
|
|
1227
|
+
}
|
|
1228
|
+
return result;
|
|
1229
|
+
}
|
|
1230
|
+
case "IntersectExceptExpr":
|
|
1231
|
+
// `a intersect b` / `a except b` select a subset of `a`.
|
|
1232
|
+
if (operands.length > 1) {
|
|
1233
|
+
const left = inferExprType(operands[0], varTypes, analysis, allFns);
|
|
1234
|
+
return left.kind === "node" ? withCard(left, { min: 0, max: cardOf(left).max }) : NODE_STEP;
|
|
273
1235
|
}
|
|
274
|
-
|
|
1236
|
+
break;
|
|
1237
|
+
case "UnionExpr":
|
|
1238
|
+
if (operands.length > 1) {
|
|
1239
|
+
const types = operands.map((o) => inferExprType(o, varTypes, analysis, allFns));
|
|
1240
|
+
const nodes = types.filter((t) => t.kind === "node");
|
|
1241
|
+
if (nodes.length !== types.length) return NODE_STEP;
|
|
1242
|
+
return withCard(nodes.reduce((a, b) => itemUnion(a, b)), { min: 0, max: 2 });
|
|
1243
|
+
}
|
|
1244
|
+
break;
|
|
1245
|
+
case "SimpleMapExpr": {
|
|
1246
|
+
if (operands.length < 2) break;
|
|
1247
|
+
let t = inferExprType(operands[0], varTypes, analysis, allFns);
|
|
1248
|
+
let card = cardOf(t);
|
|
1249
|
+
for (const next of operands.slice(1)) {
|
|
1250
|
+
if (t.kind === "unknown") return UNKNOWN;
|
|
1251
|
+
const scope = new Map(varTypes);
|
|
1252
|
+
scope.set(CONTEXT_ITEM_KEY, itemTypeOf(t));
|
|
1253
|
+
t = inferExprType(next, scope, analysis, allFns);
|
|
1254
|
+
card = multiplyCard(card, cardOf(t));
|
|
1255
|
+
}
|
|
1256
|
+
return withCard(t, card);
|
|
275
1257
|
}
|
|
1258
|
+
case "ArrowExpr":
|
|
1259
|
+
if (operands.length > 1) return inferArrowExpr(nt, varTypes, analysis, allFns);
|
|
1260
|
+
break;
|
|
1261
|
+
case "PostfixExpr":
|
|
1262
|
+
if (operands.length > 1) return inferPostfixExpr(nt, varTypes, analysis, allFns);
|
|
1263
|
+
break;
|
|
1264
|
+
case "MapConstructor":
|
|
1265
|
+
return inferMapConstructor(nt, varTypes, analysis, allFns);
|
|
1266
|
+
case "SquareArrayConstructor":
|
|
1267
|
+
case "CurlyArrayConstructor":
|
|
1268
|
+
return inferArrayConstructor(nt, varTypes, analysis, allFns);
|
|
276
1269
|
case "FunctionCall": {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
//
|
|
280
|
-
const
|
|
281
|
-
if (
|
|
282
|
-
|
|
1270
|
+
const call = asFunctionCall(node, analysis);
|
|
1271
|
+
if (!call) return UNKNOWN;
|
|
1272
|
+
// Partial application — one or more `?` placeholders: a function over the missing arguments.
|
|
1273
|
+
const placeholders = call.args.map((a) => directChildOf(a, "ArgumentPlaceholder") !== undefined);
|
|
1274
|
+
if (placeholders.some(Boolean)) {
|
|
1275
|
+
const fn = resolveFunction(call.qname, call.args.length, allFns);
|
|
1276
|
+
if (!fn || fn.variadic) return { kind: "function", name: "function(*)", occurrence: "" };
|
|
1277
|
+
const params = fn.params.filter((_, i) => placeholders[i]).map((p) => p.type ?? "item()*");
|
|
1278
|
+
return { kind: "function", name: `function(${params.join(", ")}) as ${fn.returnType ?? "item()*"}`, occurrence: "" };
|
|
1279
|
+
}
|
|
1280
|
+
return inferCallResult(
|
|
1281
|
+
call.qname,
|
|
1282
|
+
() => call.args.map((a) => inferExprType(a, varTypes, analysis, allFns)),
|
|
1283
|
+
call.args.length,
|
|
1284
|
+
allFns,
|
|
1285
|
+
);
|
|
283
1286
|
}
|
|
284
1287
|
case "PathExpr":
|
|
285
|
-
case "RelativePathExpr":
|
|
1288
|
+
case "RelativePathExpr":
|
|
286
1289
|
if (!isPathExpr(node)) break;
|
|
287
|
-
|
|
288
|
-
// provably isn't a node (a literal, an atomic function result, etc.), the whole
|
|
289
|
-
// path can't be node-producing either, regardless of which step it is.
|
|
290
|
-
const steps = nt.children.filter((c) => !isTerminal(c));
|
|
291
|
-
const definitelyNotNodes = steps.some((step) => {
|
|
292
|
-
const stepType = inferExprType(step, varTypes, analysis, allFns);
|
|
293
|
-
return stepType.kind !== "unknown" && stepType.kind !== "node" && stepType.kind !== "item";
|
|
294
|
-
});
|
|
295
|
-
return definitelyNotNodes ? UNKNOWN : NODE_STEP;
|
|
296
|
-
}
|
|
1290
|
+
return inferPathExpr(nt, varTypes, analysis, allFns);
|
|
297
1291
|
case "AxisStep":
|
|
1292
|
+
return axisStepType(node);
|
|
298
1293
|
case "ForwardStep":
|
|
299
1294
|
case "ReverseStep":
|
|
300
1295
|
case "AbbrevForwardStep":
|
|
@@ -310,19 +1305,21 @@ export function inferExprType(
|
|
|
310
1305
|
case "CompTextConstructor":
|
|
311
1306
|
return { kind: "node", name: "text", occurrence: "" };
|
|
312
1307
|
case "CompCommentConstructor":
|
|
1308
|
+
case "DirCommentConstructor":
|
|
313
1309
|
return { kind: "node", name: "comment", occurrence: "" };
|
|
1310
|
+
case "CompPIConstructor":
|
|
1311
|
+
case "DirPIConstructor":
|
|
1312
|
+
return { kind: "node", name: "processing-instruction", occurrence: "" };
|
|
314
1313
|
case "RangeExpr": {
|
|
315
1314
|
// `e1 to e2` — only a genuine range (two operands) produces xs:integer*;
|
|
316
1315
|
// with no "to" present the grammar still wraps a single child, which
|
|
317
1316
|
// falls through to the generic single-child unwrap below.
|
|
318
|
-
|
|
319
|
-
if (kids.length === 2) return RANGE_SEQUENCE;
|
|
1317
|
+
if (operands.length === 2) return RANGE_SEQUENCE;
|
|
320
1318
|
break;
|
|
321
1319
|
}
|
|
322
1320
|
}
|
|
323
1321
|
|
|
324
|
-
|
|
325
|
-
if (ntChildren.length === 1) return inferExprType(ntChildren[0], varTypes, analysis, allFns);
|
|
1322
|
+
if (operands.length === 1) return inferExprType(operands[0], varTypes, analysis, allFns);
|
|
326
1323
|
|
|
327
1324
|
return UNKNOWN;
|
|
328
1325
|
}
|
|
@@ -334,30 +1331,194 @@ export function formatType(t: XQueryType): string {
|
|
|
334
1331
|
if (t.kind === "empty") return "empty-sequence()";
|
|
335
1332
|
if (t.kind === "item") return `item()${t.occurrence}`;
|
|
336
1333
|
if (t.kind === "node") return `${t.name ?? "node"}()${t.occurrence}`;
|
|
1334
|
+
// `function(…) as T*` would read as a function returning T*; parenthesize the item type.
|
|
1335
|
+
if (t.kind === "function" && t.occurrence && t.name?.includes(" as ")) return `(${t.name})${t.occurrence}`;
|
|
337
1336
|
return `${t.name ?? t.kind}${t.occurrence}`;
|
|
338
1337
|
}
|
|
339
1338
|
|
|
340
|
-
// ── Scope-aware
|
|
1339
|
+
// ── Scope-aware walking ───────────────────────────────────────────────────────
|
|
341
1340
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
for
|
|
346
|
-
|
|
347
|
-
|
|
1341
|
+
export interface ScopeWalkHooks {
|
|
1342
|
+
/** Called for every non-terminal, with the variable types in scope at that point. */
|
|
1343
|
+
onNode?(node: Node, scope: Map<string, XQueryType>): void;
|
|
1344
|
+
/** Called for every variable binding without a declared type, with its inferred type. */
|
|
1345
|
+
onBinding?(nameNode: Node, type: XQueryType): void;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function walkScoped(
|
|
1349
|
+
node: Node,
|
|
1350
|
+
scope: Map<string, XQueryType>,
|
|
1351
|
+
analysis: FileAnalysis,
|
|
1352
|
+
allFns: FunctionSymbol[],
|
|
1353
|
+
hooks: ScopeWalkHooks,
|
|
1354
|
+
): void {
|
|
1355
|
+
if (isTerminal(node)) return;
|
|
1356
|
+
const nt = node as NonTerminal;
|
|
1357
|
+
hooks.onNode?.(node, scope);
|
|
1358
|
+
|
|
1359
|
+
const visitor: ScopeVisitor = {
|
|
1360
|
+
expr: (n, s) => walkScoped(n, s, analysis, allFns, hooks),
|
|
1361
|
+
binding: hooks.onBinding,
|
|
1362
|
+
};
|
|
1363
|
+
switch (node.type) {
|
|
1364
|
+
case "FLWORExpr":
|
|
1365
|
+
processFLWOR(node, scope, analysis, allFns, visitor);
|
|
1366
|
+
return;
|
|
1367
|
+
case "QuantifiedExpr":
|
|
1368
|
+
processQuantified(nt, scope, analysis, allFns, visitor);
|
|
1369
|
+
return;
|
|
1370
|
+
case "TypeswitchExpr":
|
|
1371
|
+
processTypeswitch(nt, scope, analysis, allFns, visitor);
|
|
1372
|
+
return;
|
|
1373
|
+
case "InlineFunctionExpr":
|
|
1374
|
+
processInlineFunction(nt, scope, analysis, allFns, visitor);
|
|
1375
|
+
return;
|
|
1376
|
+
case "IfExpr":
|
|
1377
|
+
processIf(nt, scope, analysis, allFns, visitor);
|
|
1378
|
+
return;
|
|
1379
|
+
case "XQUF_TransformExpr":
|
|
1380
|
+
processTransform(nt, scope, analysis, allFns, visitor);
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
for (const child of nt.children) walkScoped(child, scope, analysis, allFns, hooks);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
/**
|
|
1388
|
+
* Fills in `returnType` for functions in this module declared without `as …`, inferred from
|
|
1389
|
+
* their bodies. Iterates to a fixpoint (bounded) so functions calling other untyped functions,
|
|
1390
|
+
* declared before or after them, resolve too; directly recursive functions stay unknown unless
|
|
1391
|
+
* a non-recursive branch alone determines the type. Callers use the returned list in place of
|
|
1392
|
+
* `allFns` so every call site — hints and type checks alike — sees the inferred types.
|
|
1393
|
+
*/
|
|
1394
|
+
export function withInferredReturnTypes(
|
|
1395
|
+
ast: Node,
|
|
1396
|
+
analysis: FileAnalysis,
|
|
1397
|
+
allFns: FunctionSymbol[],
|
|
1398
|
+
outerScope: Map<string, XQueryType> = new Map(),
|
|
1399
|
+
): FunctionSymbol[] {
|
|
1400
|
+
const bodies = new Map<number, { decl: Node; body: Node }>();
|
|
1401
|
+
for (const annotated of findAll(ast, "AnnotatedDecl")) {
|
|
1402
|
+
const decl = directChildOf(annotated, "FunctionDecl");
|
|
1403
|
+
const body = decl ? directChildOf(decl, "FunctionBody") : undefined;
|
|
1404
|
+
if (decl && body && !directChildOf(decl, "SequenceType")) bodies.set(annotated.start, { decl, body });
|
|
1405
|
+
}
|
|
1406
|
+
const untyped = analysis.functions.filter((f) => !f.returnType && f.sourceOffset !== undefined && bodies.has(f.sourceOffset));
|
|
1407
|
+
if (untyped.length === 0) return allFns;
|
|
1408
|
+
|
|
1409
|
+
const inferred = new Map<FunctionSymbol, string>();
|
|
1410
|
+
let fns = allFns;
|
|
1411
|
+
for (let pass = 0; pass < 4; pass++) {
|
|
1412
|
+
let changed = false;
|
|
1413
|
+
const moduleTypes = new Map(outerScope);
|
|
1414
|
+
walkModuleVariables(ast, moduleTypes, analysis, fns, {});
|
|
1415
|
+
for (const fn of untyped) {
|
|
1416
|
+
const { decl, body } = bodies.get(fn.sourceOffset!)!;
|
|
1417
|
+
const scope = new Map(moduleTypes);
|
|
1418
|
+
collectParamTypes(directChildOf(decl, "ParamList"), analysis, scope);
|
|
1419
|
+
const t = inferExprType(body, scope, analysis, fns);
|
|
1420
|
+
const text = formatType(t);
|
|
1421
|
+
// Only keep types that round-trip through the signature parser the call sites use.
|
|
1422
|
+
if (t.kind === "unknown" || parseType(text).kind === "unknown" || inferred.get(fn) === text) continue;
|
|
1423
|
+
inferred.set(fn, text);
|
|
1424
|
+
changed = true;
|
|
1425
|
+
}
|
|
1426
|
+
if (!changed) break;
|
|
1427
|
+
fns = allFns.map((f) => (inferred.has(f) ? { ...f, returnType: inferred.get(f) } : f));
|
|
1428
|
+
}
|
|
1429
|
+
return fns;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
/**
|
|
1433
|
+
* Types of variables visible in this module without being declared in its source: variables
|
|
1434
|
+
* of imported modules and runtime-predeclared ones. A declared `as` type is used as written;
|
|
1435
|
+
* an untyped imported variable is inferred from its initializer when that module's AST is at
|
|
1436
|
+
* hand (it isn't for lightweight glob summaries).
|
|
1437
|
+
*/
|
|
1438
|
+
export function externalVariableTypes(
|
|
1439
|
+
analysis: FileAnalysis,
|
|
1440
|
+
imported: Iterable<FileAnalysis>,
|
|
1441
|
+
allFns: FunctionSymbol[],
|
|
1442
|
+
): Map<string, XQueryType> {
|
|
1443
|
+
const out = new Map<string, XQueryType>();
|
|
1444
|
+
const add = (key: string, t: XQueryType | undefined) => {
|
|
1445
|
+
if (t && t.kind !== "unknown") out.set(key, t);
|
|
1446
|
+
};
|
|
1447
|
+
for (const v of analysis.moduleVariables) {
|
|
1448
|
+
if (v.offset < 0 && v.type) add(qnameKey(v.qname), parseType(v.type));
|
|
1449
|
+
}
|
|
1450
|
+
const seen = new Set<FileAnalysis>([analysis]);
|
|
1451
|
+
for (const module of imported) {
|
|
1452
|
+
if (seen.has(module)) continue; // the same module is registered under its URI and its `at` path
|
|
1453
|
+
seen.add(module);
|
|
1454
|
+
let inferred: Map<string, XQueryType> | undefined;
|
|
1455
|
+
for (const v of module.moduleVariables) {
|
|
1456
|
+
const key = qnameKey(v.qname);
|
|
1457
|
+
if (v.type) {
|
|
1458
|
+
add(key, parseType(v.type));
|
|
1459
|
+
} else if (module.ast) {
|
|
1460
|
+
if (!inferred) {
|
|
1461
|
+
inferred = new Map();
|
|
1462
|
+
walkModuleVariables(module.ast, inferred, module, allFns, {});
|
|
1463
|
+
}
|
|
1464
|
+
add(key, inferred.get(key));
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
return out;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// Module variables in declaration order: declared type, else inferred from the initializer.
|
|
1472
|
+
function walkModuleVariables(
|
|
1473
|
+
ast: Node,
|
|
1474
|
+
moduleTypes: Map<string, XQueryType>,
|
|
1475
|
+
analysis: FileAnalysis,
|
|
1476
|
+
allFns: FunctionSymbol[],
|
|
1477
|
+
hooks: ScopeWalkHooks,
|
|
1478
|
+
): void {
|
|
1479
|
+
for (const decl of findAll(ast, "VarDecl")) {
|
|
1480
|
+
const value = directChildOf(decl, "VarValue");
|
|
1481
|
+
if (value && (hooks.onNode || hooks.onBinding)) walkScoped(value, moduleTypes, analysis, allFns, hooks);
|
|
1482
|
+
const declared = declaredTypeOf(decl);
|
|
1483
|
+
const inferred = value ? inferExprType(value, moduleTypes, analysis, allFns) : UNKNOWN;
|
|
1484
|
+
const nameNode = directChildOf(decl, "VarName");
|
|
1485
|
+
bindVarName(nameNode, analysis, declared ?? inferred, moduleTypes);
|
|
1486
|
+
if (!declared && value && nameNode) hooks.onBinding?.(nameNode, inferred);
|
|
348
1487
|
}
|
|
349
|
-
return types;
|
|
350
1488
|
}
|
|
351
1489
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
1490
|
+
/**
|
|
1491
|
+
* Walks a whole module with lexically correct variable scopes: module variables (declared or
|
|
1492
|
+
* inferred, in declaration order), function parameters inside each function body, and
|
|
1493
|
+
* FLWOR / quantified / typeswitch / inline-function bindings within their own sub-trees.
|
|
1494
|
+
*/
|
|
1495
|
+
export function walkModuleScopes(
|
|
1496
|
+
ast: Node,
|
|
1497
|
+
analysis: FileAnalysis,
|
|
1498
|
+
allFns: FunctionSymbol[],
|
|
1499
|
+
hooks: ScopeWalkHooks,
|
|
1500
|
+
outerScope: Map<string, XQueryType> = new Map(),
|
|
1501
|
+
): void {
|
|
1502
|
+
const moduleTypes = new Map(outerScope);
|
|
1503
|
+
walkModuleVariables(ast, moduleTypes, analysis, allFns, hooks);
|
|
1504
|
+
|
|
1505
|
+
for (const annotated of findAll(ast, "AnnotatedDecl")) {
|
|
1506
|
+
const decl = directChildOf(annotated, "FunctionDecl");
|
|
1507
|
+
const body = decl ? directChildOf(decl, "FunctionBody") : undefined;
|
|
1508
|
+
if (!decl || !body) continue; // external function — nothing to walk
|
|
1509
|
+
const scope = new Map(moduleTypes);
|
|
1510
|
+
collectParamTypes(directChildOf(decl, "ParamList"), analysis, scope);
|
|
1511
|
+
walkScoped(body, scope, analysis, allFns, hooks);
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
// Top-level query body (present in main modules, absent in library modules).
|
|
1515
|
+
for (const queryBody of findAll(ast, "QueryBody")) {
|
|
1516
|
+
walkScoped(queryBody, new Map(moduleTypes), analysis, allFns, hooks);
|
|
358
1517
|
}
|
|
359
1518
|
}
|
|
360
1519
|
|
|
1520
|
+
// ── Type checking ─────────────────────────────────────────────────────────────
|
|
1521
|
+
|
|
361
1522
|
// Check a single FunctionCall node against the known function signatures.
|
|
362
1523
|
function typeCheckCall(
|
|
363
1524
|
callNode: Node,
|
|
@@ -406,42 +1567,28 @@ function typeCheckCall(
|
|
|
406
1567
|
}
|
|
407
1568
|
}
|
|
408
1569
|
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
|
|
413
|
-
function walkScope(
|
|
1570
|
+
// Flags `a op b` chains where an operand is statically a string or boolean: neither is ever a
|
|
1571
|
+
// valid operand of an arithmetic operator (XPTY0004). Deliberately conservative: operands of
|
|
1572
|
+
// unknown or imprecise type, and empty operands, are not reported.
|
|
1573
|
+
function typeCheckArithmetic(
|
|
414
1574
|
node: Node,
|
|
415
|
-
|
|
416
|
-
moduleTypes: Map<string, XQueryType>,
|
|
417
|
-
text: string,
|
|
1575
|
+
varTypes: Map<string, XQueryType>,
|
|
418
1576
|
analysis: FileAnalysis,
|
|
419
1577
|
allFns: FunctionSymbol[],
|
|
420
1578
|
errors: TypeDiagnostic[],
|
|
421
1579
|
): void {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
if (node.type === "LetBinding" || node.type === "ForBinding") {
|
|
435
|
-
const binding = asTypedBinding(node, text, analysis);
|
|
436
|
-
if (binding?.typeStr) scopeTypes.set(qnameKey(binding.qname), parseType(binding.typeStr));
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
if (node.type === "FunctionCall") {
|
|
440
|
-
typeCheckCall(node, scopeTypes, analysis, allFns, errors);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
for (const child of nt.children) {
|
|
444
|
-
walkScope(child, scopeTypes, moduleTypes, text, analysis, allFns, errors);
|
|
1580
|
+
const arith = asArithmeticExpr(node);
|
|
1581
|
+
if (!arith) return;
|
|
1582
|
+
for (const operand of arith.operands) {
|
|
1583
|
+
const t = atomize(inferExprType(operand, varTypes, analysis, allFns));
|
|
1584
|
+
if (t.kind !== "atomic" || !t.name) continue;
|
|
1585
|
+
if (!isAtomicSubtype(t.name, "xs:string") && !isAtomicSubtype(t.name, "xs:boolean")) continue;
|
|
1586
|
+
errors.push({
|
|
1587
|
+
message: `Arithmetic operand of type ${formatType(t)} is not numeric, date/time or duration [XPTY0004]`,
|
|
1588
|
+
code: "XPTY0004",
|
|
1589
|
+
offset: operand.start,
|
|
1590
|
+
length: (operand.end ?? operand.start + 1) - operand.start,
|
|
1591
|
+
});
|
|
445
1592
|
}
|
|
446
1593
|
}
|
|
447
1594
|
|
|
@@ -449,30 +1596,25 @@ function walkScope(
|
|
|
449
1596
|
|
|
450
1597
|
export function checkTypes(
|
|
451
1598
|
ast: Node,
|
|
452
|
-
|
|
1599
|
+
_text: string,
|
|
453
1600
|
analysis: FileAnalysis,
|
|
454
1601
|
importedAnalyses: Map<string, FileAnalysis>,
|
|
455
1602
|
): TypeDiagnostic[] {
|
|
456
1603
|
const errors: TypeDiagnostic[] = [];
|
|
457
|
-
const
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
// Top-level query body (present in main modules, absent in library modules).
|
|
473
|
-
for (const queryBody of findAll(ast, "QueryBody")) {
|
|
474
|
-
walkScope(queryBody, new Map(moduleTypes), moduleTypes, text, analysis, allFns, errors);
|
|
475
|
-
}
|
|
476
|
-
|
|
1604
|
+
const declaredFns = allFunctionsFlat(analysis, importedAnalyses);
|
|
1605
|
+
const outerScope = externalVariableTypes(analysis, importedAnalyses.values(), declaredFns);
|
|
1606
|
+
const allFns = withInferredReturnTypes(ast, analysis, declaredFns, outerScope);
|
|
1607
|
+
walkModuleScopes(
|
|
1608
|
+
ast,
|
|
1609
|
+
analysis,
|
|
1610
|
+
allFns,
|
|
1611
|
+
{
|
|
1612
|
+
onNode: (node, scope) => {
|
|
1613
|
+
if (node.type === "FunctionCall") typeCheckCall(node, scope, analysis, allFns, errors);
|
|
1614
|
+
else typeCheckArithmetic(node, scope, analysis, allFns, errors);
|
|
1615
|
+
},
|
|
1616
|
+
},
|
|
1617
|
+
outerScope,
|
|
1618
|
+
);
|
|
477
1619
|
return errors;
|
|
478
1620
|
}
|