luaut-parser 3.0.0 → 4.0.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 +173 -11
- package/dist/index.cjs +2007 -293
- package/dist/index.d.cts +234 -12
- package/dist/index.d.ts +234 -12
- package/dist/index.js +2002 -293
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -99,11 +99,17 @@ var LexError = class extends Error {
|
|
|
99
99
|
line;
|
|
100
100
|
column;
|
|
101
101
|
};
|
|
102
|
-
function tokenize(source) {
|
|
102
|
+
function tokenize(source, options = {}) {
|
|
103
|
+
const { errors, comments } = options;
|
|
103
104
|
const tokens = [];
|
|
104
105
|
let cursor = 0;
|
|
105
106
|
let line = 1;
|
|
106
107
|
let column = 1;
|
|
108
|
+
function fail(message, atLine, atColumn) {
|
|
109
|
+
const error = new LexError(message, atLine, atColumn);
|
|
110
|
+
if (!errors) throw error;
|
|
111
|
+
errors.push(error);
|
|
112
|
+
}
|
|
107
113
|
function peek(offset = 0) {
|
|
108
114
|
return source[cursor + offset] ?? "";
|
|
109
115
|
}
|
|
@@ -162,7 +168,8 @@ function tokenize(source) {
|
|
|
162
168
|
let content = "";
|
|
163
169
|
while (true) {
|
|
164
170
|
if (isAtEnd()) {
|
|
165
|
-
|
|
171
|
+
fail("Unterminated long bracket", line, column);
|
|
172
|
+
return content;
|
|
166
173
|
}
|
|
167
174
|
if (peek() === "]") {
|
|
168
175
|
const save = cursor;
|
|
@@ -198,16 +205,21 @@ function tokenize(source) {
|
|
|
198
205
|
continue;
|
|
199
206
|
}
|
|
200
207
|
if (ch === "-" && peek(1) === "-") {
|
|
208
|
+
const startLine = line;
|
|
209
|
+
const startColumn = column;
|
|
201
210
|
advance();
|
|
202
211
|
advance();
|
|
203
212
|
if (peek() === "[") {
|
|
204
213
|
const level = tryLongBracketOpen();
|
|
205
214
|
if (level !== null) {
|
|
206
|
-
readLongBracketContent(level);
|
|
215
|
+
const text = readLongBracketContent(level);
|
|
216
|
+
comments?.push({ text, line: startLine, column: startColumn, endLine: line });
|
|
207
217
|
continue;
|
|
208
218
|
}
|
|
209
219
|
}
|
|
220
|
+
const textStart = cursor;
|
|
210
221
|
skipLineComment();
|
|
222
|
+
comments?.push({ text: source.slice(textStart, cursor).replace(/\r$/, ""), line: startLine, column: startColumn, endLine: startLine });
|
|
211
223
|
continue;
|
|
212
224
|
}
|
|
213
225
|
break;
|
|
@@ -318,17 +330,15 @@ function tokenize(source) {
|
|
|
318
330
|
const quote = advance();
|
|
319
331
|
let value = "";
|
|
320
332
|
while (true) {
|
|
321
|
-
if (isAtEnd()) {
|
|
322
|
-
throw new LexError("Unterminated string", line, column);
|
|
323
|
-
}
|
|
324
333
|
const ch = peek();
|
|
334
|
+
if (isAtEnd() || ch === "\n") {
|
|
335
|
+
fail("Unterminated string", line, column);
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
325
338
|
if (ch === quote) {
|
|
326
339
|
advance();
|
|
327
340
|
break;
|
|
328
341
|
}
|
|
329
|
-
if (ch === "\n") {
|
|
330
|
-
throw new LexError("Unterminated string", line, column);
|
|
331
|
-
}
|
|
332
342
|
if (ch === "\\") {
|
|
333
343
|
advance();
|
|
334
344
|
value += readEscapeSequence();
|
|
@@ -377,7 +387,9 @@ function tokenize(source) {
|
|
|
377
387
|
}
|
|
378
388
|
while (true) {
|
|
379
389
|
if (isAtEnd()) {
|
|
380
|
-
|
|
390
|
+
fail("Unterminated interpolated string", line, column);
|
|
391
|
+
flushString();
|
|
392
|
+
break;
|
|
381
393
|
}
|
|
382
394
|
const ch = peek();
|
|
383
395
|
if (ch === "`") {
|
|
@@ -397,10 +409,15 @@ function tokenize(source) {
|
|
|
397
409
|
advance();
|
|
398
410
|
advance();
|
|
399
411
|
const exprStart = cursor;
|
|
412
|
+
const exprLine = line;
|
|
413
|
+
const exprColumn = column;
|
|
400
414
|
let depth = 1;
|
|
415
|
+
let closed = true;
|
|
401
416
|
while (depth > 0) {
|
|
402
417
|
if (isAtEnd()) {
|
|
403
|
-
|
|
418
|
+
fail("Unterminated interpolation expression", line, column);
|
|
419
|
+
closed = false;
|
|
420
|
+
break;
|
|
404
421
|
}
|
|
405
422
|
if (peek() === "{") depth++;
|
|
406
423
|
if (peek() === "}") {
|
|
@@ -410,8 +427,12 @@ function tokenize(source) {
|
|
|
410
427
|
advance();
|
|
411
428
|
}
|
|
412
429
|
const exprRaw = source.slice(exprStart, cursor);
|
|
430
|
+
parts.push({ kind: "expression", raw: exprRaw, line: exprLine, column: exprColumn });
|
|
431
|
+
if (!closed) {
|
|
432
|
+
flushString();
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
413
435
|
advance();
|
|
414
|
-
parts.push({ kind: "expression", raw: exprRaw });
|
|
415
436
|
continue;
|
|
416
437
|
}
|
|
417
438
|
const chStart = cursor;
|
|
@@ -480,7 +501,9 @@ function tokenize(source) {
|
|
|
480
501
|
};
|
|
481
502
|
}
|
|
482
503
|
}
|
|
483
|
-
|
|
504
|
+
fail(`Unexpected character '${peek()}'`, line, column);
|
|
505
|
+
advance();
|
|
506
|
+
return void 0;
|
|
484
507
|
}
|
|
485
508
|
while (true) {
|
|
486
509
|
skipWhitespaceAndComments();
|
|
@@ -513,7 +536,8 @@ function tokenize(source) {
|
|
|
513
536
|
tokens.push(readIdentifierOrKeyword());
|
|
514
537
|
continue;
|
|
515
538
|
}
|
|
516
|
-
|
|
539
|
+
const symbol = readOperatorOrPunctuator();
|
|
540
|
+
if (symbol) tokens.push(symbol);
|
|
517
541
|
}
|
|
518
542
|
tokens.push({
|
|
519
543
|
type: "EOF",
|
|
@@ -523,6 +547,52 @@ function tokenize(source) {
|
|
|
523
547
|
return tokens;
|
|
524
548
|
}
|
|
525
549
|
|
|
550
|
+
// src/ast/directives.ts
|
|
551
|
+
var DIRECTIVE = /^\s*@luaut-(nocheck|ignore|expect-error)(?![\w-])/;
|
|
552
|
+
function readDirectives(comments, tokens) {
|
|
553
|
+
const codeLines = [...new Set(tokens.filter((t) => t.type !== "EOF").map((t) => t.line.start))].sort((a, b) => a - b);
|
|
554
|
+
const firstCode = codeLines[0] ?? Infinity;
|
|
555
|
+
const all = [];
|
|
556
|
+
let nocheck = false;
|
|
557
|
+
for (const comment of comments) {
|
|
558
|
+
const match = DIRECTIVE.exec(comment.text);
|
|
559
|
+
if (!match) continue;
|
|
560
|
+
const kind = match[1];
|
|
561
|
+
if (kind === "nocheck") {
|
|
562
|
+
if (comment.line < firstCode) nocheck = true;
|
|
563
|
+
all.push({ kind, line: comment.line, column: comment.column });
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
const target = codeLines.find((l) => l > comment.endLine);
|
|
567
|
+
all.push({ kind, line: comment.line, column: comment.column, target });
|
|
568
|
+
}
|
|
569
|
+
return { nocheck, all };
|
|
570
|
+
}
|
|
571
|
+
function directivesOf(source) {
|
|
572
|
+
const comments = [];
|
|
573
|
+
const errors = [];
|
|
574
|
+
const tokens = tokenize(source, { errors, comments });
|
|
575
|
+
return readDirectives(comments, tokens);
|
|
576
|
+
}
|
|
577
|
+
function applyDirectives(directives, diagnostics, lineOf) {
|
|
578
|
+
if (directives.nocheck) return { kept: [], unusedExpectErrors: [] };
|
|
579
|
+
const covering = /* @__PURE__ */ new Map();
|
|
580
|
+
for (const d of directives.all) {
|
|
581
|
+
if (d.target === void 0) continue;
|
|
582
|
+
covering.set(d.target, [...covering.get(d.target) ?? [], d]);
|
|
583
|
+
}
|
|
584
|
+
const used = /* @__PURE__ */ new Set();
|
|
585
|
+
const kept = diagnostics.filter((diagnostic) => {
|
|
586
|
+
const on = covering.get(lineOf(diagnostic));
|
|
587
|
+
if (!on) return true;
|
|
588
|
+
for (const d of on) used.add(d);
|
|
589
|
+
return false;
|
|
590
|
+
});
|
|
591
|
+
const unusedExpectErrors = directives.all.filter((d) => d.kind === "expect-error" && !used.has(d));
|
|
592
|
+
return { kept, unusedExpectErrors };
|
|
593
|
+
}
|
|
594
|
+
var UNUSED_EXPECT_ERROR = "Unused '@luaut-expect-error' directive";
|
|
595
|
+
|
|
526
596
|
// src/ast/builders.ts
|
|
527
597
|
var ParseError = class extends Error {
|
|
528
598
|
constructor(message, line, column) {
|
|
@@ -541,6 +611,25 @@ function spanFrom(start, end) {
|
|
|
541
611
|
column: { start: start.column.start, end: end.column.end }
|
|
542
612
|
};
|
|
543
613
|
}
|
|
614
|
+
function shiftSpans(node, line, column) {
|
|
615
|
+
const visit = (value) => {
|
|
616
|
+
if (!value || typeof value !== "object") return;
|
|
617
|
+
if (Array.isArray(value)) {
|
|
618
|
+
for (const item of value) visit(item);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
const span = value;
|
|
622
|
+
if (span.line && span.column) {
|
|
623
|
+
if (span.column.start !== void 0 && span.line.start === 1) span.column.start += column - 1;
|
|
624
|
+
if (span.column.end !== void 0 && span.line.end === 1) span.column.end += column - 1;
|
|
625
|
+
span.line.start += line - 1;
|
|
626
|
+
span.line.end += line - 1;
|
|
627
|
+
}
|
|
628
|
+
for (const child of Object.values(value)) visit(child);
|
|
629
|
+
};
|
|
630
|
+
visit(node);
|
|
631
|
+
return node;
|
|
632
|
+
}
|
|
544
633
|
function tokenIdentifier(t) {
|
|
545
634
|
return { type: "Identifier", name: t.value, ...spanFrom(t, t) };
|
|
546
635
|
}
|
|
@@ -573,15 +662,39 @@ var BINARY_PRECEDENCE = {
|
|
|
573
662
|
var RIGHT_ASSOCIATIVE = /* @__PURE__ */ new Set(["..", "^"]);
|
|
574
663
|
var UNARY_PRECEDENCE = 7;
|
|
575
664
|
var COMPOUND_ASSIGN_OPS = /* @__PURE__ */ new Set(["+=", "-=", "*=", "/=", "//=", "%=", "^=", "..="]);
|
|
665
|
+
var STATEMENT_KEYWORDS = /* @__PURE__ */ new Set([
|
|
666
|
+
"const",
|
|
667
|
+
"let",
|
|
668
|
+
"while",
|
|
669
|
+
"for",
|
|
670
|
+
"return",
|
|
671
|
+
"do",
|
|
672
|
+
"repeat",
|
|
673
|
+
"break",
|
|
674
|
+
"continue",
|
|
675
|
+
"import",
|
|
676
|
+
"export",
|
|
677
|
+
"end",
|
|
678
|
+
"else",
|
|
679
|
+
"elseif",
|
|
680
|
+
"until",
|
|
681
|
+
"then"
|
|
682
|
+
]);
|
|
576
683
|
var Parser = class {
|
|
577
684
|
tokens;
|
|
578
685
|
cursor = 0;
|
|
579
686
|
recover;
|
|
687
|
+
indentation;
|
|
580
688
|
/** Populated in recovery mode. */
|
|
581
689
|
errors = [];
|
|
690
|
+
/** Recovery found a block without its `end`. */
|
|
691
|
+
missingEnd = false;
|
|
692
|
+
/** The column of the first token on each line, for `indentation`. */
|
|
693
|
+
lineIndent;
|
|
582
694
|
constructor(tokens, options = {}) {
|
|
583
695
|
this.tokens = tokens;
|
|
584
696
|
this.recover = options.recover ?? false;
|
|
697
|
+
this.indentation = this.recover && (options.indentation ?? false);
|
|
585
698
|
}
|
|
586
699
|
current() {
|
|
587
700
|
return this.tokens[this.cursor];
|
|
@@ -622,6 +735,10 @@ var Parser = class {
|
|
|
622
735
|
const t = this.current();
|
|
623
736
|
return (t.type === "Identifier" || t.type === "Keyword") && t.value === value;
|
|
624
737
|
}
|
|
738
|
+
checkPunctuatorAt(offset, value) {
|
|
739
|
+
const t = this.peek(offset);
|
|
740
|
+
return t.type === "Punctuator" && t.value === value;
|
|
741
|
+
}
|
|
625
742
|
checkIdentifierValue(value) {
|
|
626
743
|
const t = this.current();
|
|
627
744
|
return t.type === "Identifier" && t.value === value;
|
|
@@ -667,46 +784,194 @@ var Parser = class {
|
|
|
667
784
|
const t = this.current();
|
|
668
785
|
const err = new ParseError(`${message}, got '${this.describeToken(t)}'`, t.line.start, t.column.start);
|
|
669
786
|
if (this.recover) {
|
|
670
|
-
this.
|
|
787
|
+
this.record(err);
|
|
671
788
|
throw new ParseRecover(err.message);
|
|
672
789
|
}
|
|
673
790
|
throw err;
|
|
674
791
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
792
|
+
// ============================================================
|
|
793
|
+
// Recovery
|
|
794
|
+
// ============================================================
|
|
795
|
+
//
|
|
796
|
+
// In recovery mode a syntax error costs as little of the tree as it can.
|
|
797
|
+
// A broken expression becomes an `ErrorExpression` where it stood; a broken
|
|
798
|
+
// field, element or argument is skipped up to the next `,`; a missing `)`,
|
|
799
|
+
// `}`, `then`, `do` or `end` is recorded and parsing goes on as if it were
|
|
800
|
+
// there. Only what none of these cover abandons a whole statement.
|
|
801
|
+
/** An error at the position of the one before it is the same problem seen
|
|
802
|
+
* again, and is not recorded twice. */
|
|
803
|
+
record(error) {
|
|
804
|
+
const last = this.errors[this.errors.length - 1];
|
|
805
|
+
if (last && last.line === error.line && last.column === error.column) return;
|
|
806
|
+
this.errors.push(error);
|
|
807
|
+
}
|
|
808
|
+
/** Record an error without abandoning what is being parsed. */
|
|
809
|
+
softError(message) {
|
|
810
|
+
const t = this.current();
|
|
811
|
+
this.record(new ParseError(`${message}, got '${this.describeToken(t)}'`, t.line.start, t.column.start));
|
|
812
|
+
}
|
|
813
|
+
/** `parse()`; in recovery mode, when it fails, skip to where parsing can go
|
|
814
|
+
* on and return `fallback` instead. */
|
|
815
|
+
attempt(parse2, stop, fallback) {
|
|
816
|
+
if (!this.recover) return parse2();
|
|
817
|
+
const from = this.cursor;
|
|
818
|
+
const start = this.current();
|
|
819
|
+
try {
|
|
820
|
+
return parse2();
|
|
821
|
+
} catch (e) {
|
|
822
|
+
if (e instanceof ParseError) this.record(e);
|
|
823
|
+
else if (!(e instanceof ParseRecover)) throw e;
|
|
824
|
+
this.skip(stop, from, "expression");
|
|
825
|
+
return fallback(start, from);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
/** An expression, or an `ErrorExpression` over what could not be parsed. */
|
|
829
|
+
expressionOr(stop) {
|
|
830
|
+
return this.attempt(() => this.parseExpression(), stop, (start, from) => this.errorExpression(start, from));
|
|
831
|
+
}
|
|
832
|
+
expressionListOr(stop) {
|
|
833
|
+
const item = () => this.expressionOr(() => stop() || this.checkPunctuator(","));
|
|
834
|
+
const list = [item()];
|
|
835
|
+
while (this.matchPunctuator(",")) list.push(item());
|
|
836
|
+
return list;
|
|
837
|
+
}
|
|
838
|
+
/** A type annotation, or none when it could not be parsed. */
|
|
839
|
+
typeOr(stop) {
|
|
840
|
+
return this.attempt(() => this.parseType(), stop, () => void 0);
|
|
841
|
+
}
|
|
842
|
+
errorExpression(start, from) {
|
|
843
|
+
if (this.cursor > from) return { type: "ErrorExpression", ...spanFrom(start, this.previous()) };
|
|
844
|
+
return {
|
|
845
|
+
type: "ErrorExpression",
|
|
846
|
+
line: { start: start.line.start, end: start.line.start },
|
|
847
|
+
column: { start: start.column.start, end: start.column.start }
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
/** A closing bracket; in recovery mode a missing one is recorded and the
|
|
851
|
+
* construct ends where it is. */
|
|
852
|
+
expectCloser(value) {
|
|
853
|
+
if (this.matchPunctuator(value)) return;
|
|
854
|
+
if (!this.recover) this.error(`Expected '${value}'`);
|
|
855
|
+
this.softError(`Expected '${value}'`);
|
|
856
|
+
}
|
|
857
|
+
/** `then` / `do` / `in`; in recovery mode a missing one is recorded and
|
|
858
|
+
* what follows is read as if it were there. */
|
|
859
|
+
expectKeywordSoft(value) {
|
|
860
|
+
if (this.matchKeyword(value)) return;
|
|
861
|
+
if (!this.recover) this.error(`Expected keyword '${value}'`);
|
|
862
|
+
this.softError(`Expected keyword '${value}'`);
|
|
863
|
+
}
|
|
864
|
+
/** The `end` of the block `opener` began. */
|
|
865
|
+
expectEnd(opener) {
|
|
866
|
+
if (this.checkKeyword("end") && !this.endBelongsOutside(opener)) {
|
|
705
867
|
this.advance();
|
|
706
|
-
|
|
707
|
-
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
if (!this.recover) this.error("Expected keyword 'end'");
|
|
871
|
+
this.softError(`Expected 'end' to close '${this.describeToken(opener)}' on line ${opener.line.start}`);
|
|
872
|
+
this.missingEnd = true;
|
|
873
|
+
}
|
|
874
|
+
/** Indentation mode: an `end` indented less than the line that opened the
|
|
875
|
+
* block closes something outside it. */
|
|
876
|
+
endBelongsOutside(opener) {
|
|
877
|
+
if (!this.indentation) return false;
|
|
878
|
+
const t = this.current();
|
|
879
|
+
return t.line.start > opener.line.start && t.column.start < this.indentOf(opener);
|
|
880
|
+
}
|
|
881
|
+
/** Indentation mode: a statement indented no deeper than the line that
|
|
882
|
+
* opened the block is past the block. */
|
|
883
|
+
dedentedPast(opener) {
|
|
884
|
+
if (!this.indentation || !opener) return false;
|
|
885
|
+
const t = this.current();
|
|
886
|
+
return t.line.start > opener.line.start && t.column.start <= this.indentOf(opener);
|
|
887
|
+
}
|
|
888
|
+
indentOf(token) {
|
|
889
|
+
if (!this.lineIndent) {
|
|
890
|
+
this.lineIndent = /* @__PURE__ */ new Map();
|
|
891
|
+
for (const t of this.tokens) {
|
|
892
|
+
if (!this.lineIndent.has(t.line.start)) this.lineIndent.set(t.line.start, t.column.start);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return this.lineIndent.get(token.line.start) ?? token.column.start;
|
|
896
|
+
}
|
|
897
|
+
/** Is the current token on a later line than the one before it? */
|
|
898
|
+
onNewLine() {
|
|
899
|
+
const previous = this.previous();
|
|
900
|
+
return previous !== void 0 && this.current().line.start > previous.line.end;
|
|
901
|
+
}
|
|
902
|
+
/** Recovery: move past what could not be parsed.
|
|
903
|
+
*
|
|
904
|
+
* Skipping stops at a token `stop` accepts, at a bracket closing something
|
|
905
|
+
* opened before the skip, or at a keyword that starts a statement. The
|
|
906
|
+
* tokens from `from` on — including those the failed attempt already
|
|
907
|
+
* consumed — count towards nesting, so a bracket or a `function ... end`
|
|
908
|
+
* is skipped whole and an `end` or `}` inside it cannot end what encloses
|
|
909
|
+
* it. A statement keyword inside brackets but outside any function means a
|
|
910
|
+
* bracket was never closed, and it stops the skip as well. */
|
|
911
|
+
skip(stop, from, mode) {
|
|
912
|
+
const closers = [];
|
|
913
|
+
for (let i = from; i < this.cursor; i++) this.nest(this.tokens[i], closers, mode);
|
|
914
|
+
while (!this.isAtEnd()) {
|
|
915
|
+
if (this.stopsSkip(closers, stop, mode)) return;
|
|
916
|
+
const t = this.advance();
|
|
917
|
+
this.nest(t, closers, mode);
|
|
918
|
+
if (mode === "statement" && closers.length === 0 && t.type === "Punctuator" && t.value === ";") return;
|
|
708
919
|
}
|
|
709
920
|
}
|
|
921
|
+
nest(t, closers, mode) {
|
|
922
|
+
const value = t.value;
|
|
923
|
+
const popTo = (closer) => {
|
|
924
|
+
const at = closers.lastIndexOf(closer);
|
|
925
|
+
if (at >= 0) closers.length = at;
|
|
926
|
+
};
|
|
927
|
+
if (t.type === "Punctuator") {
|
|
928
|
+
if (value === "(") closers.push(")");
|
|
929
|
+
else if (value === "[") closers.push("]");
|
|
930
|
+
else if (value === "{") closers.push("}");
|
|
931
|
+
else if (value === ")" || value === "]" || value === "}") popTo(value);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
if (t.type !== "Keyword") return;
|
|
935
|
+
const inBody = closers.includes("end") || closers.includes("until") || mode === "statement" && closers.length === 0;
|
|
936
|
+
switch (value) {
|
|
937
|
+
case "function":
|
|
938
|
+
closers.push("end");
|
|
939
|
+
return;
|
|
940
|
+
case "if":
|
|
941
|
+
closers.push(inBody ? "end" : "else");
|
|
942
|
+
return;
|
|
943
|
+
case "do":
|
|
944
|
+
if (inBody) closers.push("end");
|
|
945
|
+
return;
|
|
946
|
+
case "repeat":
|
|
947
|
+
if (inBody) closers.push("until");
|
|
948
|
+
return;
|
|
949
|
+
case "else":
|
|
950
|
+
if (closers[closers.length - 1] === "else") closers.pop();
|
|
951
|
+
return;
|
|
952
|
+
case "end":
|
|
953
|
+
popTo("end");
|
|
954
|
+
return;
|
|
955
|
+
case "until":
|
|
956
|
+
popTo("until");
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
stopsSkip(closers, stop, mode) {
|
|
961
|
+
const t = this.current();
|
|
962
|
+
const value = t.value;
|
|
963
|
+
const inFunction = closers.includes("end") || closers.includes("until");
|
|
964
|
+
if (!inFunction && t.type === "Keyword" && typeof value === "string") {
|
|
965
|
+
const inIfExpression = closers.includes("else") && (value === "then" || value === "elseif" || value === "else");
|
|
966
|
+
if (STATEMENT_KEYWORDS.has(value) && !inIfExpression && !(mode === "statement" && value === "then")) return true;
|
|
967
|
+
if (mode === "statement" && closers.length === 0 && (value === "if" || value === "function" && this.peek(1).type === "Identifier")) return true;
|
|
968
|
+
}
|
|
969
|
+
if (closers.length) return false;
|
|
970
|
+
if (t.type === "Punctuator" && (value === ")" || value === "]" || value === "}")) return true;
|
|
971
|
+
if (mode === "statement" && t.type === "Punctuator" && value === "@") return true;
|
|
972
|
+
if (mode === "expression" && t.type === "Punctuator" && value === ";") return true;
|
|
973
|
+
return stop();
|
|
974
|
+
}
|
|
710
975
|
describeToken(t) {
|
|
711
976
|
if (t.type === "EOF") return "<eof>";
|
|
712
977
|
if ("value" in t) return String(t.value);
|
|
@@ -719,16 +984,13 @@ var Parser = class {
|
|
|
719
984
|
const start = this.current();
|
|
720
985
|
const body = this.parseBlock();
|
|
721
986
|
if (!this.isAtEnd()) {
|
|
722
|
-
if (this.recover)
|
|
723
|
-
|
|
724
|
-
this.
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
t.column.start
|
|
728
|
-
));
|
|
729
|
-
} else {
|
|
730
|
-
this.error("Expected end of file");
|
|
987
|
+
if (!this.recover) this.error("Expected end of file");
|
|
988
|
+
while (!this.isAtEnd()) {
|
|
989
|
+
this.softError("Expected end of file");
|
|
990
|
+
this.advance();
|
|
991
|
+
body.statements.push(...this.parseBlock().statements);
|
|
731
992
|
}
|
|
993
|
+
Object.assign(body, spanFrom(body, this.previous() ?? start));
|
|
732
994
|
}
|
|
733
995
|
return { type: "Program", body, ...spanFrom(start, this.previous() ?? start) };
|
|
734
996
|
}
|
|
@@ -738,11 +1000,14 @@ var Parser = class {
|
|
|
738
1000
|
isBlockEnd() {
|
|
739
1001
|
return this.isAtEnd() || this.checkKeyword("end") || this.checkKeyword("else") || this.checkKeyword("elseif") || this.checkKeyword("until");
|
|
740
1002
|
}
|
|
741
|
-
|
|
1003
|
+
/** `opener` is the token that began the block (`if`, `function`, ...), for
|
|
1004
|
+
* indentation recovery. */
|
|
1005
|
+
parseBlock(opener) {
|
|
742
1006
|
const start = this.current();
|
|
743
1007
|
const statements = [];
|
|
744
1008
|
while (!this.isBlockEnd()) {
|
|
745
1009
|
if (this.matchPunctuator(";")) continue;
|
|
1010
|
+
if (this.dedentedPast(opener)) break;
|
|
746
1011
|
if (this.recover) {
|
|
747
1012
|
const at = this.cursor;
|
|
748
1013
|
const errStart = this.current();
|
|
@@ -756,11 +1021,11 @@ var Parser = class {
|
|
|
756
1021
|
} catch (e) {
|
|
757
1022
|
if (e instanceof ParseRecover) {
|
|
758
1023
|
} else if (e instanceof ParseError) {
|
|
759
|
-
this.
|
|
1024
|
+
this.record(e);
|
|
760
1025
|
} else {
|
|
761
1026
|
throw e;
|
|
762
1027
|
}
|
|
763
|
-
this.
|
|
1028
|
+
this.skip(() => false, at, "statement");
|
|
764
1029
|
if (this.cursor === at) {
|
|
765
1030
|
if (this.isAtEnd()) break;
|
|
766
1031
|
this.advance();
|
|
@@ -838,6 +1103,10 @@ var Parser = class {
|
|
|
838
1103
|
}
|
|
839
1104
|
}
|
|
840
1105
|
}
|
|
1106
|
+
if (this.recover && t.type === "Identifier" && t.value === "local" && (this.peek(1).type === "Identifier" || this.checkPunctuatorAt(1, "{") || this.checkPunctuatorAt(1, "["))) {
|
|
1107
|
+
this.softError("luaut has no 'local'; declare with 'const' or 'let'");
|
|
1108
|
+
return this.parseVariableDeclaration("let");
|
|
1109
|
+
}
|
|
841
1110
|
if (t.type === "Identifier" && t.value === "type" && this.peek(1).type === "Identifier") {
|
|
842
1111
|
return this.parseTypeAliasStatement();
|
|
843
1112
|
}
|
|
@@ -1026,7 +1295,7 @@ var Parser = class {
|
|
|
1026
1295
|
return { type: "ExportStatement", declaration, ...spanFrom(start, this.previous()) };
|
|
1027
1296
|
}
|
|
1028
1297
|
if (this.checkKeyword("function")) {
|
|
1029
|
-
const declaration = this.parseFunctionStatement();
|
|
1298
|
+
const declaration = this.parseFunctionStatement(true);
|
|
1030
1299
|
if (declaration.type !== "FunctionDeclaration") this.error("An exported function needs a plain name: 'export function name()'");
|
|
1031
1300
|
return { type: "ExportStatement", declaration, ...spanFrom(start, this.previous()) };
|
|
1032
1301
|
}
|
|
@@ -1057,9 +1326,11 @@ var Parser = class {
|
|
|
1057
1326
|
}
|
|
1058
1327
|
// `const x = ...` / `let x, y = ...`.
|
|
1059
1328
|
// luaut has no `local` — `const` bindings are immutable, `let` mutable.
|
|
1060
|
-
|
|
1329
|
+
/** `kind` reads the leading word as that keyword (recovery's `local`). */
|
|
1330
|
+
parseVariableDeclaration(as) {
|
|
1061
1331
|
const start = this.current();
|
|
1062
|
-
const
|
|
1332
|
+
const word = this.advance().value;
|
|
1333
|
+
const kind = as ?? word;
|
|
1063
1334
|
if (this.checkKeyword("function")) {
|
|
1064
1335
|
this.error(`A function is declared as 'function name()'; '${kind}' does not apply to functions`);
|
|
1065
1336
|
}
|
|
@@ -1069,9 +1340,10 @@ var Parser = class {
|
|
|
1069
1340
|
}
|
|
1070
1341
|
let init = [];
|
|
1071
1342
|
if (this.matchOperator("=")) {
|
|
1072
|
-
init = this.
|
|
1343
|
+
init = this.expressionListOr(() => false);
|
|
1073
1344
|
} else if (kind === "const") {
|
|
1074
|
-
this.error("'const' declaration requires an initializer");
|
|
1345
|
+
if (!this.recover) this.error("'const' declaration requires an initializer");
|
|
1346
|
+
this.softError("'const' declaration requires an initializer");
|
|
1075
1347
|
}
|
|
1076
1348
|
return { type: "VariableDeclaration", kind, names, init, ...spanFrom(start, this.previous()) };
|
|
1077
1349
|
}
|
|
@@ -1079,64 +1351,73 @@ var Parser = class {
|
|
|
1079
1351
|
const start = this.current();
|
|
1080
1352
|
this.expectKeyword("if");
|
|
1081
1353
|
const clauses = [];
|
|
1082
|
-
const
|
|
1083
|
-
this.
|
|
1084
|
-
|
|
1354
|
+
const untilThen = () => this.checkKeyword("then");
|
|
1355
|
+
const cond = this.expressionOr(untilThen);
|
|
1356
|
+
this.expectKeywordSoft("then");
|
|
1357
|
+
const body = this.parseBlock(start);
|
|
1085
1358
|
clauses.push({ type: "IfClause", condition: cond, body, ...spanFrom(cond, this.previous()) });
|
|
1086
1359
|
while (this.checkKeyword("elseif")) {
|
|
1087
1360
|
const clauseStart = this.current();
|
|
1088
1361
|
this.advance();
|
|
1089
|
-
const c = this.
|
|
1090
|
-
this.
|
|
1091
|
-
const b = this.parseBlock();
|
|
1362
|
+
const c = this.expressionOr(untilThen);
|
|
1363
|
+
this.expectKeywordSoft("then");
|
|
1364
|
+
const b = this.parseBlock(start);
|
|
1092
1365
|
clauses.push({ type: "IfClause", condition: c, body: b, ...spanFrom(clauseStart, this.previous()) });
|
|
1093
1366
|
}
|
|
1094
1367
|
let alternate;
|
|
1095
1368
|
if (this.matchKeyword("else")) {
|
|
1096
|
-
alternate = this.parseBlock();
|
|
1369
|
+
alternate = this.parseBlock(start);
|
|
1097
1370
|
}
|
|
1098
|
-
this.
|
|
1371
|
+
this.expectEnd(start);
|
|
1099
1372
|
return { type: "IfStatement", clauses, alternate, ...spanFrom(start, this.previous()) };
|
|
1100
1373
|
}
|
|
1101
1374
|
parseWhileStatement() {
|
|
1102
1375
|
const start = this.current();
|
|
1103
1376
|
this.expectKeyword("while");
|
|
1104
|
-
const condition = this.
|
|
1105
|
-
this.
|
|
1106
|
-
const body = this.parseBlock();
|
|
1107
|
-
this.
|
|
1377
|
+
const condition = this.expressionOr(() => this.checkKeyword("do"));
|
|
1378
|
+
this.expectKeywordSoft("do");
|
|
1379
|
+
const body = this.parseBlock(start);
|
|
1380
|
+
this.expectEnd(start);
|
|
1108
1381
|
return { type: "WhileStatement", condition, body, ...spanFrom(start, this.previous()) };
|
|
1109
1382
|
}
|
|
1110
1383
|
parseRepeatStatement() {
|
|
1111
1384
|
const start = this.current();
|
|
1112
1385
|
this.expectKeyword("repeat");
|
|
1113
|
-
const body = this.parseBlock();
|
|
1114
|
-
|
|
1115
|
-
|
|
1386
|
+
const body = this.parseBlock(start);
|
|
1387
|
+
let condition;
|
|
1388
|
+
if (this.checkKeyword("until") || !this.recover) {
|
|
1389
|
+
this.expectKeyword("until");
|
|
1390
|
+
condition = this.expressionOr(() => false);
|
|
1391
|
+
} else {
|
|
1392
|
+
this.softError(`Expected 'until' to close 'repeat' on line ${start.line.start}`);
|
|
1393
|
+
this.missingEnd = true;
|
|
1394
|
+
condition = this.errorExpression(this.current(), this.cursor);
|
|
1395
|
+
}
|
|
1116
1396
|
return { type: "RepeatStatement", body, condition, ...spanFrom(start, this.previous()) };
|
|
1117
1397
|
}
|
|
1118
1398
|
parseDoStatement() {
|
|
1119
1399
|
const start = this.current();
|
|
1120
1400
|
this.expectKeyword("do");
|
|
1121
|
-
const body = this.parseBlock();
|
|
1122
|
-
this.
|
|
1401
|
+
const body = this.parseBlock(start);
|
|
1402
|
+
this.expectEnd(start);
|
|
1123
1403
|
return { type: "DoStatement", body, ...spanFrom(start, this.previous()) };
|
|
1124
1404
|
}
|
|
1125
1405
|
parseForStatement() {
|
|
1126
1406
|
const start = this.current();
|
|
1127
1407
|
this.expectKeyword("for");
|
|
1128
1408
|
const first = this.parseBindingTarget(true);
|
|
1409
|
+
const untilDo = () => this.checkKeyword("do");
|
|
1129
1410
|
if (first.type === "IdentifierPattern" && this.matchOperator("=")) {
|
|
1130
|
-
const from = this.
|
|
1411
|
+
const from = this.expressionOr(() => untilDo() || this.checkPunctuator(","));
|
|
1131
1412
|
this.expectPunctuator(",");
|
|
1132
|
-
const to = this.
|
|
1413
|
+
const to = this.expressionOr(() => untilDo() || this.checkPunctuator(","));
|
|
1133
1414
|
let step;
|
|
1134
1415
|
if (this.matchPunctuator(",")) {
|
|
1135
|
-
step = this.
|
|
1416
|
+
step = this.expressionOr(untilDo);
|
|
1136
1417
|
}
|
|
1137
|
-
this.
|
|
1138
|
-
const body2 = this.parseBlock();
|
|
1139
|
-
this.
|
|
1418
|
+
this.expectKeywordSoft("do");
|
|
1419
|
+
const body2 = this.parseBlock(start);
|
|
1420
|
+
this.expectEnd(start);
|
|
1140
1421
|
return {
|
|
1141
1422
|
type: "NumericForStatement",
|
|
1142
1423
|
variable: this.identifierPatternToTypedIdentifier(first),
|
|
@@ -1152,10 +1433,10 @@ var Parser = class {
|
|
|
1152
1433
|
variables.push(this.parseBindingTarget(true));
|
|
1153
1434
|
}
|
|
1154
1435
|
this.expectKeyword("in");
|
|
1155
|
-
const iterators = this.
|
|
1156
|
-
this.
|
|
1157
|
-
const body = this.parseBlock();
|
|
1158
|
-
this.
|
|
1436
|
+
const iterators = this.expressionListOr(untilDo);
|
|
1437
|
+
this.expectKeywordSoft("do");
|
|
1438
|
+
const body = this.parseBlock(start);
|
|
1439
|
+
this.expectEnd(start);
|
|
1159
1440
|
return {
|
|
1160
1441
|
type: "GenericForStatement",
|
|
1161
1442
|
variables,
|
|
@@ -1166,28 +1447,36 @@ var Parser = class {
|
|
|
1166
1447
|
}
|
|
1167
1448
|
/** `function name() end` declares `name`; `function a.b() end` and
|
|
1168
1449
|
* `function T:m() end` define a member. */
|
|
1169
|
-
|
|
1450
|
+
/** `exported` — the `export` before this `function` has been consumed, so
|
|
1451
|
+
* each overload signature after it must carry one as well. */
|
|
1452
|
+
parseFunctionStatement(exported = false) {
|
|
1170
1453
|
const start = this.current();
|
|
1171
1454
|
this.expectKeyword("function");
|
|
1172
1455
|
const target = this.parseFunctionName();
|
|
1173
1456
|
const isMethod = target.method !== void 0;
|
|
1174
1457
|
const simpleName = !isMethod && target.path.length === 0 ? target.base.name : void 0;
|
|
1175
1458
|
const signatures = [];
|
|
1459
|
+
let written = target.base;
|
|
1176
1460
|
while (true) {
|
|
1177
1461
|
const head = this.parseFunctionHead();
|
|
1178
1462
|
if (simpleName !== void 0 && this.isOverloadContinuation(simpleName)) {
|
|
1179
|
-
signatures.push(this.headToSignature(head));
|
|
1463
|
+
signatures.push({ ...this.headToSignature(head), name: written });
|
|
1464
|
+
const nextExported = this.matchKeyword("export");
|
|
1465
|
+
if (nextExported !== exported) {
|
|
1466
|
+
this.problem("Overload signatures must all be exported or non-exported");
|
|
1467
|
+
}
|
|
1180
1468
|
this.expectKeyword("function");
|
|
1181
|
-
this.parseFunctionName();
|
|
1469
|
+
written = this.parseFunctionName().base;
|
|
1182
1470
|
continue;
|
|
1183
1471
|
}
|
|
1184
|
-
const func = this.headToBody(head);
|
|
1472
|
+
const func = this.headToBody(head, start);
|
|
1185
1473
|
if (simpleName !== void 0) {
|
|
1186
1474
|
return {
|
|
1187
1475
|
type: "FunctionDeclaration",
|
|
1188
1476
|
name: target.base,
|
|
1189
1477
|
func,
|
|
1190
1478
|
signatures: signatures.length ? signatures : void 0,
|
|
1479
|
+
implementationName: signatures.length ? written : void 0,
|
|
1191
1480
|
...spanFrom(start, this.previous())
|
|
1192
1481
|
};
|
|
1193
1482
|
}
|
|
@@ -1209,7 +1498,18 @@ var Parser = class {
|
|
|
1209
1498
|
* declaration for the same simple `name` (making the head an overload
|
|
1210
1499
|
* signature rather than an implementation)? */
|
|
1211
1500
|
isOverloadContinuation(name) {
|
|
1212
|
-
|
|
1501
|
+
const named = (offset) => this.peek(offset).type === "Identifier" && this.peek(offset).value === name;
|
|
1502
|
+
if (this.checkKeyword("function")) return named(1);
|
|
1503
|
+
return this.checkKeyword("export") && this.peek(1).type === "Keyword" && this.peek(1).value === "function" && named(2);
|
|
1504
|
+
}
|
|
1505
|
+
/** A mistake that does not stop the parse: refused outside recovery, where
|
|
1506
|
+
* the compiler must not accept it, and recorded inside. The message says
|
|
1507
|
+
* what is wrong on its own — no token is appended. */
|
|
1508
|
+
problem(message) {
|
|
1509
|
+
const t = this.current();
|
|
1510
|
+
const error = new ParseError(message, t.line.start, t.column.start);
|
|
1511
|
+
if (!this.recover) throw error;
|
|
1512
|
+
this.record(error);
|
|
1213
1513
|
}
|
|
1214
1514
|
parseFunctionName() {
|
|
1215
1515
|
const start = this.current();
|
|
@@ -1245,7 +1545,7 @@ var Parser = class {
|
|
|
1245
1545
|
this.expectKeyword("return");
|
|
1246
1546
|
let args = [];
|
|
1247
1547
|
if (this.isExpressionStart()) {
|
|
1248
|
-
args = this.
|
|
1548
|
+
args = this.expressionListOr(() => false);
|
|
1249
1549
|
}
|
|
1250
1550
|
return { type: "ReturnStatement", arguments: args, ...spanFrom(start, this.previous()) };
|
|
1251
1551
|
}
|
|
@@ -1277,7 +1577,7 @@ var Parser = class {
|
|
|
1277
1577
|
targets.push(this.parseAssignTarget());
|
|
1278
1578
|
}
|
|
1279
1579
|
this.expectOperator("=");
|
|
1280
|
-
const values = this.
|
|
1580
|
+
const values = this.expressionListOr(() => false);
|
|
1281
1581
|
return { type: "AssignmentStatement", targets, values, ...spanFrom(start, this.previous()) };
|
|
1282
1582
|
}
|
|
1283
1583
|
const first = this.parsePrefixExpression();
|
|
@@ -1286,14 +1586,16 @@ var Parser = class {
|
|
|
1286
1586
|
while (this.matchPunctuator(",")) {
|
|
1287
1587
|
targets.push(this.parseAssignTarget());
|
|
1288
1588
|
}
|
|
1589
|
+
for (const target of targets) this.rejectOptionalTarget(target);
|
|
1289
1590
|
this.expectOperator("=");
|
|
1290
|
-
const values = this.
|
|
1591
|
+
const values = this.expressionListOr(() => false);
|
|
1291
1592
|
return { type: "AssignmentStatement", targets, values, ...spanFrom(start, this.previous()) };
|
|
1292
1593
|
}
|
|
1293
1594
|
const t = this.current();
|
|
1294
1595
|
if (t.type === "Operator" && COMPOUND_ASSIGN_OPS.has(t.value)) {
|
|
1596
|
+
this.rejectOptionalTarget(first);
|
|
1295
1597
|
const op = this.advance().value;
|
|
1296
|
-
const value = this.
|
|
1598
|
+
const value = this.expressionOr(() => false);
|
|
1297
1599
|
return {
|
|
1298
1600
|
type: "CompoundAssignmentStatement",
|
|
1299
1601
|
operator: op,
|
|
@@ -1331,15 +1633,43 @@ var Parser = class {
|
|
|
1331
1633
|
* rather than the `:` of a ternary (`cond ? obj : other`)? Lua requires a
|
|
1332
1634
|
* method call to be called, so the answer is exact rather than heuristic:
|
|
1333
1635
|
* `:` Identifier followed by one of Lua's call forms. */
|
|
1334
|
-
startsMethodCall() {
|
|
1335
|
-
if (this.peek(1).type !== "Identifier") return false;
|
|
1336
|
-
const after = this.peek(2);
|
|
1636
|
+
startsMethodCall(offset = 0) {
|
|
1637
|
+
if (this.peek(offset + 1).type !== "Identifier") return false;
|
|
1638
|
+
const after = this.peek(offset + 2);
|
|
1337
1639
|
if (after.type === "Punctuator") {
|
|
1338
1640
|
const v = String(after.value);
|
|
1339
1641
|
return v === "(" || v === "{";
|
|
1340
1642
|
}
|
|
1341
1643
|
if (after.type === "InterpolatedString") return true;
|
|
1342
|
-
|
|
1644
|
+
if (after.type === "Literal" && after.kind === "string") return true;
|
|
1645
|
+
if (after.type === "Operator" && String(after.value) === "<") {
|
|
1646
|
+
const save = this.cursor;
|
|
1647
|
+
this.cursor += offset + 2;
|
|
1648
|
+
const found = this.tryCallTypeArguments() !== void 0;
|
|
1649
|
+
this.cursor = save;
|
|
1650
|
+
return found;
|
|
1651
|
+
}
|
|
1652
|
+
return false;
|
|
1653
|
+
}
|
|
1654
|
+
/** Does the next token start right where the current one ends? */
|
|
1655
|
+
touchesNext() {
|
|
1656
|
+
const current = this.current();
|
|
1657
|
+
const next = this.peek(1);
|
|
1658
|
+
return current.line.end === next.line.start && current.column.end === next.column.start;
|
|
1659
|
+
}
|
|
1660
|
+
/** `a?.b = 1` cannot be written: there may be nothing to assign to. */
|
|
1661
|
+
rejectOptionalTarget(target) {
|
|
1662
|
+
for (let e = target; e && typeof e === "object"; ) {
|
|
1663
|
+
const node = e;
|
|
1664
|
+
if (node.optional) {
|
|
1665
|
+
const at = target;
|
|
1666
|
+
const err = new ParseError("An optional chain cannot be assigned to", at.line.start, at.column.start);
|
|
1667
|
+
if (!this.recover) throw err;
|
|
1668
|
+
this.record(err);
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
e = node.type === "MemberExpression" || node.type === "IndexExpression" || node.type === "MethodCallExpression" ? node.object : node.type === "CallExpression" ? node.callee : void 0;
|
|
1672
|
+
}
|
|
1343
1673
|
}
|
|
1344
1674
|
isUnaryOperator() {
|
|
1345
1675
|
const t = this.current();
|
|
@@ -1456,7 +1786,7 @@ var Parser = class {
|
|
|
1456
1786
|
}
|
|
1457
1787
|
if (t.type === "Keyword" && t.value === "function") {
|
|
1458
1788
|
this.advance();
|
|
1459
|
-
const func = this.parseFunctionBody();
|
|
1789
|
+
const func = this.parseFunctionBody(t);
|
|
1460
1790
|
return { type: "FunctionExpression", func, ...spanFrom(t, this.previous()) };
|
|
1461
1791
|
}
|
|
1462
1792
|
if (t.type === "Keyword" && t.value === "if") {
|
|
@@ -1479,7 +1809,19 @@ var Parser = class {
|
|
|
1479
1809
|
if (p.kind === "string") {
|
|
1480
1810
|
parts.push({ kind: "string", value: p.value, raw: p.raw });
|
|
1481
1811
|
} else {
|
|
1482
|
-
|
|
1812
|
+
let expression;
|
|
1813
|
+
try {
|
|
1814
|
+
expression = shiftSpans(parseExpressionFromSource(p.raw), p.line, p.column);
|
|
1815
|
+
} catch (e) {
|
|
1816
|
+
if (!this.recover || !(e instanceof ParseError || e instanceof LexError)) throw e;
|
|
1817
|
+
const at = token;
|
|
1818
|
+
this.record(new ParseError(
|
|
1819
|
+
`In '\${${p.raw}}': ${e.message.replace(/ \(\d+:\d+\)$/, "")}`,
|
|
1820
|
+
at.line.start,
|
|
1821
|
+
at.column.start
|
|
1822
|
+
));
|
|
1823
|
+
expression = { type: "ErrorExpression", ...spanFrom(at, at) };
|
|
1824
|
+
}
|
|
1483
1825
|
parts.push({ kind: "expression", expression });
|
|
1484
1826
|
}
|
|
1485
1827
|
}
|
|
@@ -1517,7 +1859,53 @@ var Parser = class {
|
|
|
1517
1859
|
this.error("Expected identifier or '('");
|
|
1518
1860
|
}
|
|
1519
1861
|
while (true) {
|
|
1862
|
+
if (this.checkPunctuator("?") && this.touchesNext()) {
|
|
1863
|
+
const next = this.peek(1);
|
|
1864
|
+
const punct = next.type === "Punctuator" ? String(next.value) : void 0;
|
|
1865
|
+
if (punct === "." && this.peek(2).type === "Identifier") {
|
|
1866
|
+
this.advance();
|
|
1867
|
+
this.advance();
|
|
1868
|
+
const prop = this.parseIdentifier();
|
|
1869
|
+
base = { type: "MemberExpression", object: base, property: prop, optional: true, ...spanFrom(base, prop) };
|
|
1870
|
+
continue;
|
|
1871
|
+
}
|
|
1872
|
+
if (punct === "." && this.punctuatorAt(2, "(")) {
|
|
1873
|
+
this.advance();
|
|
1874
|
+
this.advance();
|
|
1875
|
+
const args = this.parseCallArguments();
|
|
1876
|
+
base = {
|
|
1877
|
+
type: "CallExpression",
|
|
1878
|
+
callee: base,
|
|
1879
|
+
arguments: args,
|
|
1880
|
+
optional: true,
|
|
1881
|
+
...spanFrom(base, this.previous())
|
|
1882
|
+
};
|
|
1883
|
+
continue;
|
|
1884
|
+
}
|
|
1885
|
+
if (punct === ":" && this.startsMethodCall(1)) {
|
|
1886
|
+
this.advance();
|
|
1887
|
+
this.advance();
|
|
1888
|
+
const method = this.parseIdentifier();
|
|
1889
|
+
const typeArguments = this.tryCallTypeArguments();
|
|
1890
|
+
const args = this.parseCallArguments();
|
|
1891
|
+
base = {
|
|
1892
|
+
type: "MethodCallExpression",
|
|
1893
|
+
object: base,
|
|
1894
|
+
method,
|
|
1895
|
+
arguments: args,
|
|
1896
|
+
typeArguments,
|
|
1897
|
+
optional: true,
|
|
1898
|
+
...spanFrom(base, this.previous())
|
|
1899
|
+
};
|
|
1900
|
+
continue;
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1520
1903
|
if (this.matchPunctuator(".")) {
|
|
1904
|
+
if (this.recover && !this.checkType("Identifier")) {
|
|
1905
|
+
this.softError("Expected identifier");
|
|
1906
|
+
base = { type: "ErrorExpression", ...spanFrom(base, this.previous()) };
|
|
1907
|
+
break;
|
|
1908
|
+
}
|
|
1521
1909
|
const prop = this.parseIdentifier();
|
|
1522
1910
|
base = { type: "MemberExpression", object: base, property: prop, ...spanFrom(base, prop) };
|
|
1523
1911
|
continue;
|
|
@@ -1531,17 +1919,33 @@ var Parser = class {
|
|
|
1531
1919
|
if (this.checkPunctuator(":") && this.startsMethodCall()) {
|
|
1532
1920
|
this.advance();
|
|
1533
1921
|
const method = this.parseIdentifier();
|
|
1922
|
+
const typeArguments = this.tryCallTypeArguments();
|
|
1534
1923
|
const args = this.parseCallArguments();
|
|
1535
1924
|
base = {
|
|
1536
1925
|
type: "MethodCallExpression",
|
|
1537
1926
|
object: base,
|
|
1538
1927
|
method,
|
|
1539
1928
|
arguments: args,
|
|
1929
|
+
typeArguments,
|
|
1540
1930
|
...spanFrom(base, this.previous())
|
|
1541
1931
|
};
|
|
1542
1932
|
continue;
|
|
1543
1933
|
}
|
|
1544
|
-
if (this.
|
|
1934
|
+
if (this.checkOperator("<")) {
|
|
1935
|
+
const typeArguments = this.tryCallTypeArguments();
|
|
1936
|
+
if (typeArguments) {
|
|
1937
|
+
const args = this.parseCallArguments();
|
|
1938
|
+
base = {
|
|
1939
|
+
type: "CallExpression",
|
|
1940
|
+
callee: base,
|
|
1941
|
+
arguments: args,
|
|
1942
|
+
typeArguments,
|
|
1943
|
+
...spanFrom(base, this.previous())
|
|
1944
|
+
};
|
|
1945
|
+
continue;
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
if (this.startsCallArguments()) {
|
|
1545
1949
|
const args = this.parseCallArguments();
|
|
1546
1950
|
base = {
|
|
1547
1951
|
type: "CallExpression",
|
|
@@ -1555,6 +1959,11 @@ var Parser = class {
|
|
|
1555
1959
|
}
|
|
1556
1960
|
return base;
|
|
1557
1961
|
}
|
|
1962
|
+
/** Is the token `ahead` places on the punctuator `value`? */
|
|
1963
|
+
punctuatorAt(ahead, value) {
|
|
1964
|
+
const token = this.peek(ahead);
|
|
1965
|
+
return token.type === "Punctuator" && token.value === value;
|
|
1966
|
+
}
|
|
1558
1967
|
/** An assignment target after the first: a prefix expression (`a.b`,
|
|
1559
1968
|
* `a[i]`, `a`) or a nested destructuring pattern. */
|
|
1560
1969
|
parseAssignTarget() {
|
|
@@ -1562,14 +1971,52 @@ var Parser = class {
|
|
|
1562
1971
|
if (this.checkPunctuator("[")) return this.parseArrayPattern();
|
|
1563
1972
|
return this.parsePrefixExpression();
|
|
1564
1973
|
}
|
|
1974
|
+
/** Does a call's argument list start here? Lua's three forms: `(`, a
|
|
1975
|
+
* string, or a table. */
|
|
1976
|
+
startsCallArguments() {
|
|
1977
|
+
return this.checkPunctuator("(") || this.checkPunctuator("{") || this.checkType("InterpolatedString") || this.checkType("Literal") && this.current().kind === "string";
|
|
1978
|
+
}
|
|
1979
|
+
/** `f<A, B>(x)` — type arguments, when that is what this is. `a < b > (c)`
|
|
1980
|
+
* is three operators, and only what follows the `>` tells them apart, so
|
|
1981
|
+
* this reads ahead and puts the cursor back when the guess was wrong. */
|
|
1982
|
+
tryCallTypeArguments() {
|
|
1983
|
+
if (!this.checkOperator("<")) return void 0;
|
|
1984
|
+
const start = this.cursor;
|
|
1985
|
+
const errors = this.errors.length;
|
|
1986
|
+
try {
|
|
1987
|
+
this.advance();
|
|
1988
|
+
const list = [this.parseTypeArgument()];
|
|
1989
|
+
while (this.matchPunctuator(",") && !this.checkOperator(">")) list.push(this.parseTypeArgument());
|
|
1990
|
+
this.expectOperator(">");
|
|
1991
|
+
if (!this.startsCallArguments()) throw new ParseRecover("not a call");
|
|
1992
|
+
return list;
|
|
1993
|
+
} catch (e) {
|
|
1994
|
+
if (!(e instanceof ParseError || e instanceof ParseRecover)) throw e;
|
|
1995
|
+
this.cursor = start;
|
|
1996
|
+
this.errors.length = errors;
|
|
1997
|
+
return void 0;
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
1565
2000
|
parseCallArguments() {
|
|
1566
2001
|
if (this.matchPunctuator("(")) {
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
2002
|
+
const list = [];
|
|
2003
|
+
const stop = () => this.checkPunctuator(",");
|
|
2004
|
+
if (!this.checkPunctuator(")")) {
|
|
2005
|
+
while (true) {
|
|
2006
|
+
if (this.recover && this.onNewLine() && this.startsTableField() && !this.startsMethodCall(1)) break;
|
|
2007
|
+
const before = this.cursor;
|
|
2008
|
+
const argument = this.expressionOr(stop);
|
|
2009
|
+
if (argument.type !== "ErrorExpression" || this.cursor > before || list.length) list.push(argument);
|
|
2010
|
+
if (this.matchPunctuator(",") && !this.checkPunctuator(")")) continue;
|
|
2011
|
+
if (!this.recover || this.checkPunctuator(")")) break;
|
|
2012
|
+
if (this.onNewLine() && (this.checkType("Identifier") || this.checkType("Keyword"))) break;
|
|
2013
|
+
this.softError("Expected ',' or ')'");
|
|
2014
|
+
this.skip(stop, this.cursor, "expression");
|
|
2015
|
+
if (this.matchPunctuator(",")) continue;
|
|
2016
|
+
break;
|
|
2017
|
+
}
|
|
1570
2018
|
}
|
|
1571
|
-
|
|
1572
|
-
this.expectPunctuator(")");
|
|
2019
|
+
this.expectCloser(")");
|
|
1573
2020
|
return list;
|
|
1574
2021
|
}
|
|
1575
2022
|
const t = this.current();
|
|
@@ -1596,57 +2043,89 @@ var Parser = class {
|
|
|
1596
2043
|
const start = this.current();
|
|
1597
2044
|
this.expectPunctuator("{");
|
|
1598
2045
|
const fields = [];
|
|
2046
|
+
const stop = () => this.checkPunctuator(",") || this.checkPunctuator(";") || this.onNewLine() && this.startsTableField();
|
|
1599
2047
|
while (!this.checkPunctuator("}")) {
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
this.expectPunctuator(":");
|
|
1608
|
-
const value = this.parseExpression();
|
|
1609
|
-
fields.push({ type: "TableFieldComputed", key, value });
|
|
1610
|
-
} else if (this.checkType("Literal") && this.current().kind === "string") {
|
|
1611
|
-
const t = this.advance();
|
|
1612
|
-
const key = { type: "StringLiteral", value: t.value, raw: t.raw, ...spanFrom(t, t) };
|
|
1613
|
-
this.expectPunctuator(":");
|
|
1614
|
-
const value = this.parseExpression();
|
|
1615
|
-
fields.push({ type: "TableFieldNamed", key, value });
|
|
1616
|
-
} else if (this.checkType("Identifier") && this.peek(1).type === "Punctuator" && this.peek(1).value === ":") {
|
|
1617
|
-
const key = this.parseIdentifier();
|
|
1618
|
-
this.expectPunctuator(":");
|
|
1619
|
-
const value = this.parseExpression();
|
|
1620
|
-
fields.push({ type: "TableFieldNamed", key, value });
|
|
1621
|
-
} else if (this.checkType("Identifier")) {
|
|
1622
|
-
const name = this.parseIdentifier();
|
|
1623
|
-
fields.push({ type: "TableFieldShorthand", name });
|
|
1624
|
-
} else {
|
|
1625
|
-
this.error("Expected object field ('key: value', '[expr]: value', shorthand, or '...spread'); use '[...]' for arrays");
|
|
2048
|
+
const field = this.attempt(() => this.parseTableField(stop), stop, () => void 0);
|
|
2049
|
+
if (field) fields.push(field);
|
|
2050
|
+
if (this.matchPunctuator(",") || this.matchPunctuator(";")) continue;
|
|
2051
|
+
if (!this.recover || this.checkPunctuator("}")) break;
|
|
2052
|
+
if (this.onNewLine() && this.startsTableField()) {
|
|
2053
|
+
this.softError("Expected ','");
|
|
2054
|
+
continue;
|
|
1626
2055
|
}
|
|
2056
|
+
if (this.isAtEnd() || this.onNewLine() && this.checkType("Keyword")) break;
|
|
2057
|
+
this.softError("Expected ',' or '}'");
|
|
2058
|
+
const before = this.cursor;
|
|
2059
|
+
this.skip(stop, this.cursor, "expression");
|
|
1627
2060
|
if (this.matchPunctuator(",") || this.matchPunctuator(";")) continue;
|
|
2061
|
+
if (this.cursor > before && this.onNewLine() && this.startsTableField()) continue;
|
|
1628
2062
|
break;
|
|
1629
2063
|
}
|
|
1630
|
-
this.
|
|
2064
|
+
this.expectCloser("}");
|
|
1631
2065
|
return { type: "TableExpression", fields, ...spanFrom(start, this.previous()) };
|
|
1632
2066
|
}
|
|
2067
|
+
/** Does a `key: value` field, or a spread, start here? */
|
|
2068
|
+
startsTableField() {
|
|
2069
|
+
const next = this.peek(1);
|
|
2070
|
+
const colon = next.type === "Punctuator" && next.value === ":";
|
|
2071
|
+
if (this.checkType("Identifier")) return colon;
|
|
2072
|
+
if (this.checkType("Literal") && this.current().kind === "string") return colon;
|
|
2073
|
+
return this.checkOperator("...");
|
|
2074
|
+
}
|
|
2075
|
+
parseTableField(stop) {
|
|
2076
|
+
if (this.checkOperator("...")) {
|
|
2077
|
+
this.advance();
|
|
2078
|
+
return { type: "TableFieldSpread", argument: this.expressionOr(stop) };
|
|
2079
|
+
}
|
|
2080
|
+
if (this.matchPunctuator("[")) {
|
|
2081
|
+
const key = this.expressionOr(() => this.checkPunctuator("]"));
|
|
2082
|
+
this.expectPunctuator("]");
|
|
2083
|
+
this.expectPunctuator(":");
|
|
2084
|
+
return { type: "TableFieldComputed", key, value: this.expressionOr(stop) };
|
|
2085
|
+
}
|
|
2086
|
+
if (this.checkType("Literal") && this.current().kind === "string") {
|
|
2087
|
+
const t = this.advance();
|
|
2088
|
+
const key = { type: "StringLiteral", value: t.value, raw: t.raw, ...spanFrom(t, t) };
|
|
2089
|
+
this.expectPunctuator(":");
|
|
2090
|
+
return { type: "TableFieldNamed", key, value: this.expressionOr(stop) };
|
|
2091
|
+
}
|
|
2092
|
+
if (this.checkType("Identifier") && this.peek(1).type === "Punctuator" && this.peek(1).value === ":") {
|
|
2093
|
+
const key = this.parseIdentifier();
|
|
2094
|
+
this.expectPunctuator(":");
|
|
2095
|
+
return { type: "TableFieldNamed", key, value: this.expressionOr(stop) };
|
|
2096
|
+
}
|
|
2097
|
+
if (this.checkType("Identifier")) {
|
|
2098
|
+
return { type: "TableFieldShorthand", name: this.parseIdentifier() };
|
|
2099
|
+
}
|
|
2100
|
+
this.error("Expected object field ('key: value', '[expr]: value', shorthand, or '...spread'); use '[...]' for arrays");
|
|
2101
|
+
}
|
|
1633
2102
|
// `[1, 2, 3]` — array literal (trailing comma allowed).
|
|
1634
2103
|
parseArrayExpression() {
|
|
1635
2104
|
const start = this.current();
|
|
1636
2105
|
this.expectPunctuator("[");
|
|
1637
2106
|
const elements = [];
|
|
2107
|
+
const stop = () => this.checkPunctuator(",");
|
|
1638
2108
|
while (!this.checkPunctuator("]")) {
|
|
1639
2109
|
if (this.checkOperator("...")) {
|
|
1640
2110
|
const dots = this.advance();
|
|
1641
|
-
const argument = this.
|
|
2111
|
+
const argument = this.expressionOr(stop);
|
|
1642
2112
|
elements.push({ type: "SpreadElement", argument, ...spanFrom(dots, argument) });
|
|
1643
2113
|
} else {
|
|
1644
|
-
elements.push(this.
|
|
2114
|
+
elements.push(this.expressionOr(stop));
|
|
2115
|
+
}
|
|
2116
|
+
if (this.matchPunctuator(",")) continue;
|
|
2117
|
+
if (!this.recover || this.checkPunctuator("]")) break;
|
|
2118
|
+
if (this.onNewLine() && this.isExpressionStart() && !this.checkType("Keyword")) {
|
|
2119
|
+
this.softError("Expected ','");
|
|
2120
|
+
continue;
|
|
1645
2121
|
}
|
|
2122
|
+
if (this.isAtEnd() || this.onNewLine() && this.checkType("Keyword")) break;
|
|
2123
|
+
this.softError("Expected ',' or ']'");
|
|
2124
|
+
this.skip(stop, this.cursor, "expression");
|
|
1646
2125
|
if (this.matchPunctuator(",")) continue;
|
|
1647
2126
|
break;
|
|
1648
2127
|
}
|
|
1649
|
-
this.
|
|
2128
|
+
this.expectCloser("]");
|
|
1650
2129
|
return { type: "ArrayExpression", elements, ...spanFrom(start, this.previous()) };
|
|
1651
2130
|
}
|
|
1652
2131
|
// ============================================================
|
|
@@ -1679,7 +2158,7 @@ var Parser = class {
|
|
|
1679
2158
|
};
|
|
1680
2159
|
}
|
|
1681
2160
|
if (topLevel && this.matchPunctuator(":")) {
|
|
1682
|
-
target.typeAnnotation = this.
|
|
2161
|
+
target.typeAnnotation = this.typeOr(() => this.checkOperator("=") || this.checkPunctuator(","));
|
|
1683
2162
|
}
|
|
1684
2163
|
return target;
|
|
1685
2164
|
}
|
|
@@ -1838,12 +2317,13 @@ var Parser = class {
|
|
|
1838
2317
|
}
|
|
1839
2318
|
const optional2 = this.matchPunctuator("?");
|
|
1840
2319
|
let typeAnnotation;
|
|
2320
|
+
const paramEnd = () => this.checkPunctuator(",");
|
|
1841
2321
|
if (this.matchPunctuator(":")) {
|
|
1842
|
-
typeAnnotation = this.
|
|
2322
|
+
typeAnnotation = this.typeOr(() => paramEnd() || this.checkOperator("="));
|
|
1843
2323
|
}
|
|
1844
2324
|
let def;
|
|
1845
2325
|
if (this.matchOperator("=")) {
|
|
1846
|
-
def = this.
|
|
2326
|
+
def = this.expressionOr(paramEnd);
|
|
1847
2327
|
}
|
|
1848
2328
|
params.push({
|
|
1849
2329
|
type: "FunctionParameter",
|
|
@@ -1854,7 +2334,7 @@ var Parser = class {
|
|
|
1854
2334
|
optional: optional2 || void 0,
|
|
1855
2335
|
...spanFrom(paramStart, this.previous())
|
|
1856
2336
|
});
|
|
1857
|
-
if (this.matchPunctuator(",")) continue;
|
|
2337
|
+
if (this.matchPunctuator(",") && !this.checkPunctuator(")")) continue;
|
|
1858
2338
|
break;
|
|
1859
2339
|
}
|
|
1860
2340
|
}
|
|
@@ -1863,7 +2343,9 @@ var Parser = class {
|
|
|
1863
2343
|
let predicate;
|
|
1864
2344
|
if (this.matchPunctuator(":")) {
|
|
1865
2345
|
predicate = this.tryParseTypePredicate();
|
|
1866
|
-
if (!predicate)
|
|
2346
|
+
if (!predicate) {
|
|
2347
|
+
returnType = this.attempt(() => this.parseTypeOrTypePackReference(), () => false, () => void 0);
|
|
2348
|
+
}
|
|
1867
2349
|
}
|
|
1868
2350
|
return { start, generics, params, hasVarargs, varargTypeAnnotation, returnType, predicate };
|
|
1869
2351
|
}
|
|
@@ -1909,10 +2391,10 @@ var Parser = class {
|
|
|
1909
2391
|
}
|
|
1910
2392
|
return void 0;
|
|
1911
2393
|
}
|
|
1912
|
-
parseFunctionBody() {
|
|
2394
|
+
parseFunctionBody(opener) {
|
|
1913
2395
|
const head = this.parseFunctionHead();
|
|
1914
|
-
const body = this.parseBlock();
|
|
1915
|
-
this.
|
|
2396
|
+
const body = this.parseBlock(opener);
|
|
2397
|
+
this.expectEnd(opener);
|
|
1916
2398
|
return {
|
|
1917
2399
|
type: "FunctionBody",
|
|
1918
2400
|
generics: head.generics,
|
|
@@ -1937,9 +2419,9 @@ var Parser = class {
|
|
|
1937
2419
|
...spanFrom(head.start, this.previous())
|
|
1938
2420
|
};
|
|
1939
2421
|
}
|
|
1940
|
-
headToBody(head) {
|
|
1941
|
-
const body = this.parseBlock();
|
|
1942
|
-
this.
|
|
2422
|
+
headToBody(head, opener) {
|
|
2423
|
+
const body = this.parseBlock(opener);
|
|
2424
|
+
this.expectEnd(opener);
|
|
1943
2425
|
return {
|
|
1944
2426
|
type: "FunctionBody",
|
|
1945
2427
|
generics: head.generics,
|
|
@@ -2146,7 +2628,7 @@ var Parser = class {
|
|
|
2146
2628
|
this.advance();
|
|
2147
2629
|
if (!this.checkOperator(">")) {
|
|
2148
2630
|
typeArguments.push(this.parseTypeArgument());
|
|
2149
|
-
while (this.matchPunctuator(",")) {
|
|
2631
|
+
while (this.matchPunctuator(",") && !this.checkOperator(">")) {
|
|
2150
2632
|
typeArguments.push(this.parseTypeArgument());
|
|
2151
2633
|
}
|
|
2152
2634
|
}
|
|
@@ -2197,7 +2679,7 @@ var Parser = class {
|
|
|
2197
2679
|
optional: optional2 || void 0,
|
|
2198
2680
|
...spanFrom(paramStart, this.previous())
|
|
2199
2681
|
});
|
|
2200
|
-
if (this.matchPunctuator(",")) continue;
|
|
2682
|
+
if (this.matchPunctuator(",") && !this.checkPunctuator(")")) continue;
|
|
2201
2683
|
break;
|
|
2202
2684
|
}
|
|
2203
2685
|
}
|
|
@@ -2412,7 +2894,7 @@ var Parser = class {
|
|
|
2412
2894
|
default: def,
|
|
2413
2895
|
...spanFrom(nameTok, this.previous())
|
|
2414
2896
|
});
|
|
2415
|
-
if (this.matchPunctuator(",")) continue;
|
|
2897
|
+
if (this.matchPunctuator(",") && !this.checkOperator(">")) continue;
|
|
2416
2898
|
break;
|
|
2417
2899
|
}
|
|
2418
2900
|
this.expectOperator(">");
|
|
@@ -2439,23 +2921,23 @@ function parseExpressionFromSource(raw) {
|
|
|
2439
2921
|
return expr;
|
|
2440
2922
|
}
|
|
2441
2923
|
function parseWithRecovery(source) {
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
const
|
|
2458
|
-
return { program, errors:
|
|
2924
|
+
const lexErrors = [];
|
|
2925
|
+
const comments = [];
|
|
2926
|
+
const tokens = tokenize(source, { errors: lexErrors, comments });
|
|
2927
|
+
const lexed = lexErrors.map((e) => new ParseError(e.message.replace(/ \(\d+:\d+\)$/, ""), e.line, e.column));
|
|
2928
|
+
const first = new Parser(tokens, { recover: true });
|
|
2929
|
+
let program = first.parseProgram();
|
|
2930
|
+
let errors = first.errors;
|
|
2931
|
+
if (first.missingEnd) {
|
|
2932
|
+
const second = new Parser(tokens, { recover: true, indentation: true });
|
|
2933
|
+
const reparsed = second.parseProgram();
|
|
2934
|
+
if (second.errors.length <= errors.length) {
|
|
2935
|
+
program = reparsed;
|
|
2936
|
+
errors = second.errors;
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
const all = [...lexed, ...errors].sort((a, b) => a.line - b.line || a.column - b.column);
|
|
2940
|
+
return { program, errors: all, directives: readDirectives(comments, tokens) };
|
|
2459
2941
|
}
|
|
2460
2942
|
|
|
2461
2943
|
// src/ast/nodes.ts
|
|
@@ -2494,19 +2976,24 @@ function childScope(parent) {
|
|
|
2494
2976
|
return { parent, declarations: /* @__PURE__ */ new Map() };
|
|
2495
2977
|
}
|
|
2496
2978
|
var Analyzer = class {
|
|
2497
|
-
nextId = 0;
|
|
2498
|
-
bindingOf = /* @__PURE__ */ new Map();
|
|
2499
|
-
bindings = /* @__PURE__ */ new Map();
|
|
2500
|
-
diagnostics = [];
|
|
2501
|
-
globalScope = { parent: null, declarations: /* @__PURE__ */ new Map() };
|
|
2502
2979
|
constructor(options) {
|
|
2980
|
+
this.options = options;
|
|
2503
2981
|
for (const name of options.builtinGlobals ?? []) {
|
|
2504
2982
|
const id = this.getOrCreateGlobalBinding(name);
|
|
2505
2983
|
this.bindings.get(id).isBuiltin = true;
|
|
2506
2984
|
}
|
|
2507
2985
|
}
|
|
2986
|
+
options;
|
|
2987
|
+
nextId = 0;
|
|
2988
|
+
bindingOf = /* @__PURE__ */ new Map();
|
|
2989
|
+
bindings = /* @__PURE__ */ new Map();
|
|
2990
|
+
diagnostics = [];
|
|
2991
|
+
globalScope = { parent: null, declarations: /* @__PURE__ */ new Map() };
|
|
2508
2992
|
run(program) {
|
|
2509
|
-
this.
|
|
2993
|
+
this.moduleScope = childScope(this.globalScope);
|
|
2994
|
+
this.visitBlock(program.body, this.moduleScope);
|
|
2995
|
+
this.resolveForwardReferences();
|
|
2996
|
+
if (this.options.reportUndeclared) this.reportUndeclared(program);
|
|
2510
2997
|
return {
|
|
2511
2998
|
bindingOf: this.bindingOf,
|
|
2512
2999
|
bindings: this.bindings,
|
|
@@ -2514,6 +3001,99 @@ var Analyzer = class {
|
|
|
2514
3001
|
globalsByName: this.globalScope.declarations
|
|
2515
3002
|
};
|
|
2516
3003
|
}
|
|
3004
|
+
// ---------------- hoisting ----------------
|
|
3005
|
+
//
|
|
3006
|
+
// As in TypeScript, and as the bundle runs a module:
|
|
3007
|
+
//
|
|
3008
|
+
// - a function declaration is visible to its whole block, before it too;
|
|
3009
|
+
// - a name the module declares at its top level is visible to code that
|
|
3010
|
+
// runs later — function bodies, and `typeof` in a type — even where that
|
|
3011
|
+
// code is written above the declaration. A bundle declares every
|
|
3012
|
+
// top-level name before any of the module runs, so this is what happens.
|
|
3013
|
+
//
|
|
3014
|
+
// A read of a later `const` straight in the module's own flow is not
|
|
3015
|
+
// resolved to it: that still reads what was there before.
|
|
3016
|
+
moduleScope = this.globalScope;
|
|
3017
|
+
/** How many function bodies enclose the walk. */
|
|
3018
|
+
functionDepth = 0;
|
|
3019
|
+
/** Function declarations already declared by their block's hoisting, with
|
|
3020
|
+
* the function depth of that block. */
|
|
3021
|
+
hoisted = /* @__PURE__ */ new Map();
|
|
3022
|
+
/** Names that resolved to a global from code that runs later, with the
|
|
3023
|
+
* scope they were read in. */
|
|
3024
|
+
deferredGlobals = [];
|
|
3025
|
+
hoistFunctions(block, scope) {
|
|
3026
|
+
for (const statement of block.statements) {
|
|
3027
|
+
const declaration = statement.type === "ExportStatement" ? statement.declaration : statement;
|
|
3028
|
+
if (declaration.type !== "FunctionDeclaration") continue;
|
|
3029
|
+
this.declare(scope, declaration.name.name, "local", declaration.name, true, "function");
|
|
3030
|
+
this.hoisted.set(declaration.name, scope === this.moduleScope ? -1 : this.functionDepth);
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
/** Inside a function, a function declared further down its block is
|
|
3034
|
+
* hoisted only as a name: code that runs later (another function's body)
|
|
3035
|
+
* can call it, but a call straight in the block before the declaration
|
|
3036
|
+
* finds nothing there yet. At a module's top level the whole function is
|
|
3037
|
+
* hoisted, and this does not apply. */
|
|
3038
|
+
checkUseBeforeDefine(identifier, id) {
|
|
3039
|
+
const binding = this.bindings.get(id);
|
|
3040
|
+
if (binding.declaredBy !== "function" || this.typeQueryDepth > 0) return;
|
|
3041
|
+
const declaration = binding.declarationNode;
|
|
3042
|
+
const depth = declaration && this.hoisted.get(declaration);
|
|
3043
|
+
if (depth === void 0 || depth !== this.functionDepth) return;
|
|
3044
|
+
const before = identifier.line.start < declaration.line.start || identifier.line.start === declaration.line.start && identifier.column.start < declaration.column.start;
|
|
3045
|
+
if (!before) return;
|
|
3046
|
+
this.diagnostics.push({
|
|
3047
|
+
node: identifier,
|
|
3048
|
+
message: `'${binding.name}' is used before its definition: inside a function, a function declared further down is only there once its declaration has run`,
|
|
3049
|
+
kind: "use-before-define"
|
|
3050
|
+
});
|
|
3051
|
+
}
|
|
3052
|
+
noteDeferred(identifier, scope, id, assignment) {
|
|
3053
|
+
if (this.functionDepth === 0 && this.typeQueryDepth === 0) return;
|
|
3054
|
+
if (this.bindings.get(id).kind !== "global") return;
|
|
3055
|
+
this.deferredGlobals.push({ node: identifier, scope, assignment });
|
|
3056
|
+
}
|
|
3057
|
+
/** Point each deferred read of a global at the declaration of that name
|
|
3058
|
+
* that turned up later — in the module, or in any block around the code
|
|
3059
|
+
* that reads it. Such code runs after the declaration has: a closure
|
|
3060
|
+
* written inside a value reads the name the value is bound to. */
|
|
3061
|
+
resolveForwardReferences() {
|
|
3062
|
+
for (const { node, scope, assignment } of this.deferredGlobals) {
|
|
3063
|
+
const localId = this.lookup(scope, node.name);
|
|
3064
|
+
const globalId = this.bindingOf.get(node);
|
|
3065
|
+
if (localId === void 0 || globalId === void 0 || localId === globalId) continue;
|
|
3066
|
+
const global = this.bindings.get(globalId);
|
|
3067
|
+
const at = global.references.indexOf(node);
|
|
3068
|
+
if (at >= 0) global.references.splice(at, 1);
|
|
3069
|
+
if (global.declarationNode === node) global.declarationNode = void 0;
|
|
3070
|
+
if (!global.isBuiltin && !global.references.length && global.declarationNode === void 0) {
|
|
3071
|
+
this.bindings.delete(globalId);
|
|
3072
|
+
this.globalScope.declarations.delete(node.name);
|
|
3073
|
+
}
|
|
3074
|
+
this.bindingOf.set(node, localId);
|
|
3075
|
+
this.bindings.get(localId).references.push(node);
|
|
3076
|
+
if (assignment) this.checkConstAssign(localId, node);
|
|
3077
|
+
else if (this.typeQueryDepth === 0) this.checkTypeOnly(localId, node);
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
/** Every read of a global nothing declares. A global assigned somewhere
|
|
3081
|
+
* in the file (`x = 1`) is Lua's implicit global, and is left alone. */
|
|
3082
|
+
reportUndeclared(program) {
|
|
3083
|
+
const declared = /* @__PURE__ */ new Set();
|
|
3084
|
+
for (const statement of program.body.statements) {
|
|
3085
|
+
if (statement.type === "DeclareStatement") declared.add(statement.name);
|
|
3086
|
+
}
|
|
3087
|
+
const found = [];
|
|
3088
|
+
for (const binding of this.bindings.values()) {
|
|
3089
|
+
if (!isUnassignedGlobal(binding) || declared.has(binding.name)) continue;
|
|
3090
|
+
for (const reference of binding.references) {
|
|
3091
|
+
found.push({ node: reference, message: `Cannot find name '${binding.name}'`, kind: "undeclared" });
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
found.sort((a, b) => a.node.line.start - b.node.line.start || a.node.column.start - b.node.column.start);
|
|
3095
|
+
this.diagnostics.push(...found);
|
|
3096
|
+
}
|
|
2517
3097
|
// ---------------- declaration / resolution primitives ----------------
|
|
2518
3098
|
declare(scope, name, kind, node, isConst = false, declaredBy) {
|
|
2519
3099
|
if (scope.declarations.has(name) && scope !== this.globalScope) {
|
|
@@ -2559,6 +3139,8 @@ var Analyzer = class {
|
|
|
2559
3139
|
this.bindingOf.set(identifier, id);
|
|
2560
3140
|
this.bindings.get(id).references.push(identifier);
|
|
2561
3141
|
if (this.typeQueryDepth === 0) this.checkTypeOnly(id, identifier);
|
|
3142
|
+
this.checkUseBeforeDefine(identifier, id);
|
|
3143
|
+
this.noteDeferred(identifier, scope, id, false);
|
|
2562
3144
|
}
|
|
2563
3145
|
/** Inside `typeof x` in a type, where a type-only import may be named. */
|
|
2564
3146
|
typeQueryDepth = 0;
|
|
@@ -2589,6 +3171,7 @@ var Analyzer = class {
|
|
|
2589
3171
|
this.recordPossibleGlobalDefinition(id, identifier);
|
|
2590
3172
|
this.checkTypeOnly(id, identifier);
|
|
2591
3173
|
this.checkConstAssign(id, identifier);
|
|
3174
|
+
this.noteDeferred(identifier, scope, id, true);
|
|
2592
3175
|
}
|
|
2593
3176
|
/** `Module.x = 1` through `import * as Module`: a module's exports belong
|
|
2594
3177
|
* to it and are read-only, as in ES modules. Deeper writes (`Module.x.y`)
|
|
@@ -2681,6 +3264,7 @@ var Analyzer = class {
|
|
|
2681
3264
|
}
|
|
2682
3265
|
// ---------------- blocks / statements ----------------
|
|
2683
3266
|
visitBlock(block, scope) {
|
|
3267
|
+
this.hoistFunctions(block, scope);
|
|
2684
3268
|
for (const stmt of block.statements) this.visitStatement(stmt, scope);
|
|
2685
3269
|
}
|
|
2686
3270
|
/** Visits a block in a *fresh child scope* of `scope` — the common case
|
|
@@ -2699,7 +3283,11 @@ var Analyzer = class {
|
|
|
2699
3283
|
return;
|
|
2700
3284
|
}
|
|
2701
3285
|
case "FunctionDeclaration": {
|
|
2702
|
-
this.declare(scope, stmt.name.name, "local", stmt.name, true, "function");
|
|
3286
|
+
if (!this.hoisted.has(stmt.name)) this.declare(scope, stmt.name.name, "local", stmt.name, true, "function");
|
|
3287
|
+
for (const signature of stmt.signatures ?? []) {
|
|
3288
|
+
if (signature.name && signature.name !== stmt.name) this.reference(scope, signature.name);
|
|
3289
|
+
}
|
|
3290
|
+
if (stmt.implementationName) this.reference(scope, stmt.implementationName);
|
|
2703
3291
|
for (const signature of stmt.signatures ?? []) this.visitSignature(signature, scope);
|
|
2704
3292
|
this.visitFunctionBody(stmt.func, scope);
|
|
2705
3293
|
return;
|
|
@@ -2800,7 +3388,11 @@ var Analyzer = class {
|
|
|
2800
3388
|
return;
|
|
2801
3389
|
case "TypeAliasStatement":
|
|
2802
3390
|
case "ExportTypeAliasStatement":
|
|
2803
|
-
this.
|
|
3391
|
+
this.visitGenerics(
|
|
3392
|
+
(stmt.type === "TypeAliasStatement" ? stmt : stmt.alias).generics,
|
|
3393
|
+
scope
|
|
3394
|
+
);
|
|
3395
|
+
this.visitType(stmt.type === "TypeAliasStatement" ? stmt.definition : stmt.alias.definition, scope);
|
|
2804
3396
|
return;
|
|
2805
3397
|
case "ImportStatement": {
|
|
2806
3398
|
const typeOnly = stmt.isTypeOnly ? "type" : void 0;
|
|
@@ -2838,6 +3430,7 @@ var Analyzer = class {
|
|
|
2838
3430
|
// ---------------- functions ----------------
|
|
2839
3431
|
visitFunctionBody(func, outerScope, isMethod = false) {
|
|
2840
3432
|
const fnScope = childScope(outerScope);
|
|
3433
|
+
this.visitGenerics(func.generics, fnScope);
|
|
2841
3434
|
func.params.forEach((param, i) => {
|
|
2842
3435
|
const kind = isMethod && i === 0 ? "self" : "param";
|
|
2843
3436
|
this.visitType(param.typeAnnotation, fnScope);
|
|
@@ -2850,14 +3443,28 @@ var Analyzer = class {
|
|
|
2850
3443
|
});
|
|
2851
3444
|
this.visitType(func.varargTypeAnnotation, fnScope);
|
|
2852
3445
|
this.visitType(func.returnType, fnScope);
|
|
2853
|
-
this.
|
|
3446
|
+
this.functionDepth++;
|
|
3447
|
+
try {
|
|
3448
|
+
this.visitBlock(func.body, fnScope);
|
|
3449
|
+
} finally {
|
|
3450
|
+
this.functionDepth--;
|
|
3451
|
+
}
|
|
2854
3452
|
}
|
|
2855
3453
|
/** An overload signature: no body and no bindings, but its types can hold
|
|
2856
3454
|
* a `typeof x`. */
|
|
2857
3455
|
visitSignature(signature, scope) {
|
|
3456
|
+
this.visitGenerics(signature.generics, scope);
|
|
2858
3457
|
for (const param of signature.params) this.visitType(param.typeAnnotation, scope);
|
|
2859
3458
|
this.visitType(signature.returnType, scope);
|
|
2860
3459
|
}
|
|
3460
|
+
/** `<K extends typeof config>` — a constraint is a type like any other,
|
|
3461
|
+
* and the `typeof` in it reads a value. */
|
|
3462
|
+
visitGenerics(generics, scope) {
|
|
3463
|
+
for (const generic of generics ?? []) {
|
|
3464
|
+
this.visitType(generic.constraint, scope);
|
|
3465
|
+
this.visitType(generic.default, scope);
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
2861
3468
|
/** Resolve the value references inside a type. Only `typeof x` has any —
|
|
2862
3469
|
* everything else in a type names types, which live in their own
|
|
2863
3470
|
* namespace and are not this pass's business. */
|
|
@@ -2895,6 +3502,7 @@ var Analyzer = class {
|
|
|
2895
3502
|
case "NumberLiteral":
|
|
2896
3503
|
case "StringLiteral":
|
|
2897
3504
|
case "VarargExpression":
|
|
3505
|
+
case "ErrorExpression":
|
|
2898
3506
|
return;
|
|
2899
3507
|
case "InterpolatedStringExpression":
|
|
2900
3508
|
for (const part of expr.parts) {
|
|
@@ -2979,6 +3587,37 @@ function analyzeScopes(program, options = {}) {
|
|
|
2979
3587
|
return new Analyzer(options).run(program);
|
|
2980
3588
|
}
|
|
2981
3589
|
|
|
3590
|
+
// src/ast/prelude.ts
|
|
3591
|
+
var PRELUDE_SOURCE = `
|
|
3592
|
+
-- In Luau only \`nil\` and \`false\` are falsy: \`0\` and \`""\` are truthy.
|
|
3593
|
+
-- These are what truthiness narrowing computes, made available to write down.
|
|
3594
|
+
type Falsy = nil | false
|
|
3595
|
+
type Truthy<T> = T - Falsy
|
|
3596
|
+
|
|
3597
|
+
-- \`-\` is set difference. Over a union it drops members; over a concrete type
|
|
3598
|
+
-- it simplifies away; over an opaque type (\`unknown\`, an unresolved parameter)
|
|
3599
|
+
-- it is kept, so \`Exclude<unknown, 1>\` stays \`unknown - 1\`.
|
|
3600
|
+
type Exclude<T, U> = T - U
|
|
3601
|
+
type Extract<T, U> = T extends U ? T : never
|
|
3602
|
+
type NonNullable<T> = T - nil
|
|
3603
|
+
|
|
3604
|
+
type ReturnType<T> = T extends (...unknown) -> infer R ? R : never
|
|
3605
|
+
type Parameters<T> = T extends (...infer P) -> unknown ? P : never
|
|
3606
|
+
|
|
3607
|
+
type Partial<T> = { [K in keyof T]?: T[K] }
|
|
3608
|
+
type Required<T> = { [K in keyof T]-?: T[K] }
|
|
3609
|
+
type Readonly<T> = { readonly [K in keyof T]: T[K] }
|
|
3610
|
+
type Mutable<T> = { -readonly [K in keyof T]: T[K] }
|
|
3611
|
+
|
|
3612
|
+
type Pick<T, K> = { [P in K]: T[P] }
|
|
3613
|
+
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>
|
|
3614
|
+
type Record<K, V> = { [P in K]: V }
|
|
3615
|
+
`;
|
|
3616
|
+
var prelude;
|
|
3617
|
+
function preludeProgram() {
|
|
3618
|
+
return prelude ??= parse(PRELUDE_SOURCE);
|
|
3619
|
+
}
|
|
3620
|
+
|
|
2982
3621
|
// src/ast/typeModel.ts
|
|
2983
3622
|
function isClassType(t) {
|
|
2984
3623
|
return t.kind === "object" && t.class !== void 0;
|
|
@@ -3054,6 +3693,7 @@ function substitute(t, subst) {
|
|
|
3054
3693
|
varargs,
|
|
3055
3694
|
returns: substitute(t.returns, inner),
|
|
3056
3695
|
typeParams: t.typeParams,
|
|
3696
|
+
typeParamDefaults: t.typeParamDefaults,
|
|
3057
3697
|
predicate: t.predicate && {
|
|
3058
3698
|
...t.predicate,
|
|
3059
3699
|
type: t.predicate.type && substitute(t.predicate.type, inner)
|
|
@@ -3312,6 +3952,15 @@ function isAssignableInner(a, b) {
|
|
|
3312
3952
|
}
|
|
3313
3953
|
if (!isAssignable(ap.type, bp.type)) return false;
|
|
3314
3954
|
}
|
|
3955
|
+
if (b.indexer) {
|
|
3956
|
+
for (const [name, ap] of a.properties) {
|
|
3957
|
+
if (b.properties.has(name) || !isAssignable(literal(name), b.indexer.key)) continue;
|
|
3958
|
+
if (!isAssignable(ap.type, b.indexer.value)) return false;
|
|
3959
|
+
}
|
|
3960
|
+
if (a.indexer && isAssignable(a.indexer.key, b.indexer.key) && !isAssignable(a.indexer.value, b.indexer.value)) {
|
|
3961
|
+
return false;
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
3315
3964
|
return true;
|
|
3316
3965
|
}
|
|
3317
3966
|
if (a.kind === "function") {
|
|
@@ -3461,10 +4110,14 @@ function containsFreeTypeParam(t, seen, bound) {
|
|
|
3461
4110
|
return containsTypeParam(t.base, seen, bound) || containsTypeParam(t.excluded, seen, bound);
|
|
3462
4111
|
case "indexedAccess":
|
|
3463
4112
|
return containsTypeParam(t.objectType, seen, bound) || containsTypeParam(t.indexType, seen, bound);
|
|
3464
|
-
case "conditional":
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
4113
|
+
case "conditional": {
|
|
4114
|
+
const inner = t.inferVars.length ? /* @__PURE__ */ new Set([...bound, ...t.inferVars]) : bound;
|
|
4115
|
+
return containsTypeParam(t.checkType, seen, bound) || containsTypeParam(t.extendsType, seen, inner) || containsTypeParam(t.trueType, seen, inner) || containsTypeParam(t.falseType, seen, bound);
|
|
4116
|
+
}
|
|
4117
|
+
case "mapped": {
|
|
4118
|
+
const inner = /* @__PURE__ */ new Set([...bound, t.parameter]);
|
|
4119
|
+
return containsTypeParam(t.constraint, seen, bound) || !!t.nameType && containsTypeParam(t.nameType, seen, inner) || containsTypeParam(t.template, seen, inner) || !!t.source && containsTypeParam(t.source, seen, bound);
|
|
4120
|
+
}
|
|
3468
4121
|
default:
|
|
3469
4122
|
return false;
|
|
3470
4123
|
}
|
|
@@ -3538,9 +4191,52 @@ function escapeRegExp(s) {
|
|
|
3538
4191
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3539
4192
|
}
|
|
3540
4193
|
function overlaps(a, b) {
|
|
4194
|
+
if (a.kind === "union") return a.types.some((m) => overlaps(m, b));
|
|
4195
|
+
if (b.kind === "union") return b.types.some((m) => overlaps(a, m));
|
|
3541
4196
|
return isAssignable(a, b) || isAssignable(b, a);
|
|
3542
4197
|
}
|
|
3543
4198
|
var formatCache = /* @__PURE__ */ new WeakMap();
|
|
4199
|
+
function briefConstraint(t) {
|
|
4200
|
+
if (t.kind === "union" && t.types.length > 8) {
|
|
4201
|
+
return `${t.types.slice(0, 6).map(formatType).join(" | ")} | ... ${t.types.length - 6} more`;
|
|
4202
|
+
}
|
|
4203
|
+
return formatType(t);
|
|
4204
|
+
}
|
|
4205
|
+
function collectTypeParams(t, out, seen = /* @__PURE__ */ new Set()) {
|
|
4206
|
+
if (!t || seen.has(t)) return;
|
|
4207
|
+
seen.add(t);
|
|
4208
|
+
switch (t.kind) {
|
|
4209
|
+
case "typeParam":
|
|
4210
|
+
if (t.constraint && !out.has(t.name)) out.set(t.name, t.constraint);
|
|
4211
|
+
collectTypeParams(t.constraint, out, seen);
|
|
4212
|
+
return;
|
|
4213
|
+
case "array":
|
|
4214
|
+
collectTypeParams(t.element, out, seen);
|
|
4215
|
+
return;
|
|
4216
|
+
case "tuple":
|
|
4217
|
+
for (const e of t.elements) collectTypeParams(e, out, seen);
|
|
4218
|
+
return;
|
|
4219
|
+
case "union":
|
|
4220
|
+
case "intersection":
|
|
4221
|
+
for (const m of t.types) collectTypeParams(m, out, seen);
|
|
4222
|
+
return;
|
|
4223
|
+
case "keyof":
|
|
4224
|
+
collectTypeParams(t.target, out, seen);
|
|
4225
|
+
return;
|
|
4226
|
+
case "indexedAccess":
|
|
4227
|
+
collectTypeParams(t.objectType, out, seen);
|
|
4228
|
+
collectTypeParams(t.indexType, out, seen);
|
|
4229
|
+
return;
|
|
4230
|
+
case "genericRef":
|
|
4231
|
+
for (const a of t.typeArguments) collectTypeParams(a, out, seen);
|
|
4232
|
+
return;
|
|
4233
|
+
case "object":
|
|
4234
|
+
for (const [, p] of t.properties) collectTypeParams(p.type, out, seen);
|
|
4235
|
+
return;
|
|
4236
|
+
default:
|
|
4237
|
+
return;
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
3544
4240
|
function formatType(t) {
|
|
3545
4241
|
const cached = formatCache.get(t);
|
|
3546
4242
|
if (cached !== void 0) return cached;
|
|
@@ -3577,7 +4273,14 @@ function formatTypeUncached(t) {
|
|
|
3577
4273
|
const consts = new Set(
|
|
3578
4274
|
t.params.filter((p) => p.type.kind === "typeParam" && p.type.isConst).map((p) => p.type.name)
|
|
3579
4275
|
);
|
|
3580
|
-
const
|
|
4276
|
+
const constraints = /* @__PURE__ */ new Map();
|
|
4277
|
+
for (const part of [...t.params.map((p) => p.type), t.varargs, t.returns]) {
|
|
4278
|
+
collectTypeParams(part, constraints);
|
|
4279
|
+
}
|
|
4280
|
+
const gen = t.typeParams?.length ? `<${t.typeParams.map((n) => {
|
|
4281
|
+
const constraint = constraints.get(n);
|
|
4282
|
+
return `${consts.has(n) ? "const " : ""}${n}${constraint ? ` extends ${briefConstraint(constraint)}` : ""}`;
|
|
4283
|
+
}).join(", ")}>` : "";
|
|
3581
4284
|
const ps = t.params.map((p) => `${p.name ? p.name + ": " : ""}${formatType(p.type)}`);
|
|
3582
4285
|
if (t.varargs) ps.push(`...${formatType(t.varargs)}`);
|
|
3583
4286
|
return `${gen}(${ps.join(", ")}) -> ${formatPredicate(t) ?? formatType(t.returns)}`;
|
|
@@ -3793,6 +4496,12 @@ function isFreshLiteralExpr(e) {
|
|
|
3793
4496
|
return isFreshLiteralExpr(e.expression);
|
|
3794
4497
|
case "UnaryExpression":
|
|
3795
4498
|
return isFreshLiteralExpr(e.argument);
|
|
4499
|
+
// `let n = 5 satisfies number` widens like `let n = 5`. An object or
|
|
4500
|
+
// array has already taken its literals from the contract, and keeps them.
|
|
4501
|
+
case "SatisfiesExpression": {
|
|
4502
|
+
const inner = unwrapParens(e.expression);
|
|
4503
|
+
return inner.type !== "TableExpression" && inner.type !== "ArrayExpression" && isFreshLiteralExpr(inner);
|
|
4504
|
+
}
|
|
3796
4505
|
default:
|
|
3797
4506
|
return false;
|
|
3798
4507
|
}
|
|
@@ -3902,6 +4611,48 @@ var AliasMap = class extends Map {
|
|
|
3902
4611
|
return this.entries();
|
|
3903
4612
|
}
|
|
3904
4613
|
};
|
|
4614
|
+
function unwrapParens(e) {
|
|
4615
|
+
while (e.type === "ParenthesizedExpression") e = e.expression;
|
|
4616
|
+
return e;
|
|
4617
|
+
}
|
|
4618
|
+
function expressionLabel(e, depth = 0) {
|
|
4619
|
+
if (depth > 6) return void 0;
|
|
4620
|
+
const args = (list) => {
|
|
4621
|
+
const parts = list.map((a) => a.type === "StringLiteral" ? JSON.stringify(a.value) : a.type === "NumberLiteral" ? a.raw : a.type === "Identifier" ? a.name : void 0);
|
|
4622
|
+
return parts.every((p) => p !== void 0) && parts.join(", ").length <= 40 ? `(${parts.join(", ")})` : "(...)";
|
|
4623
|
+
};
|
|
4624
|
+
switch (e.type) {
|
|
4625
|
+
case "Identifier":
|
|
4626
|
+
return e.name;
|
|
4627
|
+
case "MemberExpression": {
|
|
4628
|
+
const o = expressionLabel(e.object, depth + 1);
|
|
4629
|
+
return o === void 0 ? void 0 : `${o}${e.optional ? "?." : "."}${e.property.name}`;
|
|
4630
|
+
}
|
|
4631
|
+
case "MethodCallExpression": {
|
|
4632
|
+
const o = expressionLabel(e.object, depth + 1);
|
|
4633
|
+
return o === void 0 ? void 0 : `${o}${e.optional ? "?:" : ":"}${e.method.name}${args(e.arguments)}`;
|
|
4634
|
+
}
|
|
4635
|
+
case "CallExpression": {
|
|
4636
|
+
const o = expressionLabel(e.callee, depth + 1);
|
|
4637
|
+
return o === void 0 ? void 0 : `${o}${e.optional ? "?." : ""}${args(e.arguments)}`;
|
|
4638
|
+
}
|
|
4639
|
+
case "IndexExpression": {
|
|
4640
|
+
const o = expressionLabel(e.object, depth + 1);
|
|
4641
|
+
const i = e.index.type === "StringLiteral" ? JSON.stringify(e.index.value) : e.index.type === "NumberLiteral" ? e.index.raw : e.index.type === "Identifier" ? e.index.name : "...";
|
|
4642
|
+
return o === void 0 ? void 0 : `${o}[${i}]`;
|
|
4643
|
+
}
|
|
4644
|
+
case "ParenthesizedExpression": {
|
|
4645
|
+
const inner = expressionLabel(e.expression, depth + 1);
|
|
4646
|
+
return inner === void 0 ? void 0 : `(${inner})`;
|
|
4647
|
+
}
|
|
4648
|
+
default:
|
|
4649
|
+
return void 0;
|
|
4650
|
+
}
|
|
4651
|
+
}
|
|
4652
|
+
function withoutNil(t) {
|
|
4653
|
+
if (t.kind !== "union") return t.kind === "primitive" && t.name === "nil" ? neverType : t;
|
|
4654
|
+
return union(t.types.filter((m) => !(m.kind === "primitive" && m.name === "nil")));
|
|
4655
|
+
}
|
|
3905
4656
|
var METAMETHODS = {
|
|
3906
4657
|
"+": "__add",
|
|
3907
4658
|
"-": "__sub",
|
|
@@ -3975,14 +4726,16 @@ var TypeAnalyzer = class {
|
|
|
3975
4726
|
/** Recursion guard for `preVisitBody`. */
|
|
3976
4727
|
preVisitDepth = 0;
|
|
3977
4728
|
run() {
|
|
3978
|
-
|
|
4729
|
+
this.registerAliasDefs(preludeProgram().body, true);
|
|
4730
|
+
for (const lib of this.options.libs ?? []) this.registerAliasDefs(lib.body, true);
|
|
3979
4731
|
this.registerAliasDefs(this.program.body);
|
|
3980
4732
|
for (const lib of this.options.libs ?? []) this.harvestDeclares(lib.body);
|
|
3981
|
-
this.harvestDeclares(this.program.body);
|
|
3982
4733
|
this.registerImportedTypes();
|
|
3983
4734
|
this.resolveAllAliases();
|
|
4735
|
+
this.harvestDeclares(this.program.body, true);
|
|
3984
4736
|
this.indexDeclarations();
|
|
3985
4737
|
for (const [name, id] of this.scopes.globalsByName) {
|
|
4738
|
+
if (this.deferredDeclares.has(name) && !this.options.globalTypes?.[name]) continue;
|
|
3986
4739
|
const t = this.options.globalTypes?.[name] ?? this.libGlobalTypes.get(name) ?? anyType;
|
|
3987
4740
|
this.bindingType.set(id, t);
|
|
3988
4741
|
}
|
|
@@ -3990,6 +4743,8 @@ var TypeAnalyzer = class {
|
|
|
3990
4743
|
try {
|
|
3991
4744
|
const env = /* @__PURE__ */ new Map();
|
|
3992
4745
|
this.visitBlock(this.program.body, env);
|
|
4746
|
+
this.resolveDeferredDeclares();
|
|
4747
|
+
if (this.options.reportUnknownTypes) this.reportUnknownTypes();
|
|
3993
4748
|
} finally {
|
|
3994
4749
|
setAliasExpander(void 0);
|
|
3995
4750
|
}
|
|
@@ -4050,10 +4805,28 @@ var TypeAnalyzer = class {
|
|
|
4050
4805
|
}
|
|
4051
4806
|
}
|
|
4052
4807
|
}
|
|
4053
|
-
|
|
4808
|
+
/** `layering` is on for the prelude and for definitions files: a second
|
|
4809
|
+
* library that declares an alias already declared *adds* to it, the way a
|
|
4810
|
+
* second `declare` of a table's name does, so `@luaut/roblox` can give
|
|
4811
|
+
* `StringMethods` Luau's `split` without restating Lua's. The file being
|
|
4812
|
+
* analysed is not a layer: its own alias replaces what the libraries
|
|
4813
|
+
* gave, which is how a project opts out of a set. */
|
|
4814
|
+
registerAliasDefs(block, layering = false) {
|
|
4054
4815
|
for (const stmt of block.statements) {
|
|
4055
4816
|
const alias = stmt.type === "TypeAliasStatement" ? stmt : stmt.type === "ExportTypeAliasStatement" ? stmt.alias : void 0;
|
|
4056
|
-
if (alias)
|
|
4817
|
+
if (alias) {
|
|
4818
|
+
const previous = layering ? this.aliasDefs.get(alias.name.name) : void 0;
|
|
4819
|
+
const node = previous && !previous.class ? {
|
|
4820
|
+
type: "IntersectionTypeNode",
|
|
4821
|
+
types: [previous.node, alias.definition],
|
|
4822
|
+
line: alias.definition.line,
|
|
4823
|
+
column: alias.definition.column
|
|
4824
|
+
} : alias.definition;
|
|
4825
|
+
this.aliasDefs.set(alias.name.name, {
|
|
4826
|
+
params: previous && !previous.class && previous.params.length ? previous.params : alias.generics,
|
|
4827
|
+
node
|
|
4828
|
+
});
|
|
4829
|
+
}
|
|
4057
4830
|
if (stmt.type === "DeclareClassStatement") {
|
|
4058
4831
|
this.aliasDefs.set(stmt.name.name, { params: [], node: stmt.body, class: stmt });
|
|
4059
4832
|
}
|
|
@@ -4152,13 +4925,38 @@ var TypeAnalyzer = class {
|
|
|
4152
4925
|
* string. Any other value is simply redeclared: a sourcemap's
|
|
4153
4926
|
* `declare script: <this file's instance>` replaces the library's
|
|
4154
4927
|
* `declare script: LuaSourceContainer`. */
|
|
4155
|
-
|
|
4928
|
+
/** Program `declare`s whose type depends on a value's, by name. */
|
|
4929
|
+
deferredDeclares = /* @__PURE__ */ new Map();
|
|
4930
|
+
/** A library that declares a name a second time adds to it rather than
|
|
4931
|
+
* replacing it: `declare table: { find: ... }` on top of Lua's `table`
|
|
4932
|
+
* leaves both members there, the way overloads of a function accumulate.
|
|
4933
|
+
* This is what lets one definitions file build on another's — Luau's on
|
|
4934
|
+
* Lua's, Roblox's on Luau's. A property declared twice takes its later
|
|
4935
|
+
* type. Classes stay as they are: they come from one generated file and
|
|
4936
|
+
* merging them would only blur it. */
|
|
4937
|
+
mergeDeclared(prev, next) {
|
|
4938
|
+
if (!prev || prev.kind !== "object" || next.kind !== "object") return next;
|
|
4939
|
+
if (prev.class || next.class) return next;
|
|
4940
|
+
return objectType(
|
|
4941
|
+
[...prev.properties, ...next.properties],
|
|
4942
|
+
next.indexer ?? prev.indexer,
|
|
4943
|
+
next.frozen ?? prev.frozen
|
|
4944
|
+
);
|
|
4945
|
+
}
|
|
4946
|
+
harvestDeclares(block, own = false) {
|
|
4156
4947
|
for (const stmt of block.statements) {
|
|
4157
4948
|
if (stmt.type !== "DeclareStatement") continue;
|
|
4949
|
+
if (own && (containsTypeQuery(stmt.valueType) || referencedTypeNames(stmt.valueType).some((name) => this.dependsOnTypeQuery(name)))) {
|
|
4950
|
+
this.deferredDeclares.set(stmt.name, stmt);
|
|
4951
|
+
continue;
|
|
4952
|
+
}
|
|
4158
4953
|
const t = this.resolveType(stmt.valueType);
|
|
4159
4954
|
const prev = this.libGlobalTypes.get(stmt.name);
|
|
4160
4955
|
const overload = prev && stmt.valueType.type === "FunctionTypeNode" && (prev.kind === "function" || prev.kind === "intersection");
|
|
4161
|
-
this.libGlobalTypes.set(
|
|
4956
|
+
this.libGlobalTypes.set(
|
|
4957
|
+
stmt.name,
|
|
4958
|
+
overload ? intersection([prev, t]) : this.mergeDeclared(prev, t)
|
|
4959
|
+
);
|
|
4162
4960
|
}
|
|
4163
4961
|
}
|
|
4164
4962
|
resolveAllAliases() {
|
|
@@ -4168,14 +4966,154 @@ var TypeAnalyzer = class {
|
|
|
4168
4966
|
this.aliases.defer(name, () => this.classType(cls));
|
|
4169
4967
|
continue;
|
|
4170
4968
|
}
|
|
4171
|
-
if (
|
|
4969
|
+
if (this.dependsOnTypeQuery(name)) continue;
|
|
4172
4970
|
this.withTypeParams(def.params, () => {
|
|
4173
4971
|
this.aliases.set(name, this.resolveDef(def));
|
|
4174
4972
|
});
|
|
4175
4973
|
}
|
|
4176
4974
|
}
|
|
4975
|
+
typeQueryDependents = /* @__PURE__ */ new Map();
|
|
4976
|
+
/** Does alias `name` contain a `typeof`, itself or through an alias it
|
|
4977
|
+
* names? */
|
|
4978
|
+
dependsOnTypeQuery(name, visiting = /* @__PURE__ */ new Set()) {
|
|
4979
|
+
const known = this.typeQueryDependents.get(name);
|
|
4980
|
+
if (known !== void 0) return known;
|
|
4981
|
+
const def = this.aliasDefs.get(name);
|
|
4982
|
+
if (!def || def.class || visiting.has(name)) return false;
|
|
4983
|
+
visiting.add(name);
|
|
4984
|
+
const result = containsTypeQuery(def.node) || referencedTypeNames(def.node).some((other) => other !== name && this.dependsOnTypeQuery(other, visiting));
|
|
4985
|
+
visiting.delete(name);
|
|
4986
|
+
this.typeQueryDependents.set(name, result);
|
|
4987
|
+
return result;
|
|
4988
|
+
}
|
|
4177
4989
|
/** The aliases `resolveAllAliases` left for later, now that every binding
|
|
4178
4990
|
* has its type. */
|
|
4991
|
+
/** Names this file imports. A module that could not be found is reported
|
|
4992
|
+
* as the missing module it is; the names it was to bring are not also
|
|
4993
|
+
* typos. */
|
|
4994
|
+
importedNames() {
|
|
4995
|
+
if (this.imported) return this.imported;
|
|
4996
|
+
this.imported = /* @__PURE__ */ new Set();
|
|
4997
|
+
for (const statement of this.program.body.statements) {
|
|
4998
|
+
if (statement.type !== "ImportStatement") continue;
|
|
4999
|
+
if (statement.defaultImport) this.imported.add(statement.defaultImport.name);
|
|
5000
|
+
if (statement.namespaceImport) this.imported.add(statement.namespaceImport.name);
|
|
5001
|
+
for (const specifier of statement.specifiers) this.imported.add(specifier.local.name);
|
|
5002
|
+
}
|
|
5003
|
+
return this.imported;
|
|
5004
|
+
}
|
|
5005
|
+
imported;
|
|
5006
|
+
/** What a `return` gives, against what the function declared. */
|
|
5007
|
+
checkReturn(stmt, declared, types, sources, env) {
|
|
5008
|
+
if (!declared || !this.emitDiagnostics) return;
|
|
5009
|
+
if (declared.kind === "any" || declared.kind === "unknown" || this.namesNothing(declared)) return;
|
|
5010
|
+
const actual = stmt.arguments.length === 0 ? nilType : types.length === 1 ? types[0] : tuple([...types], true);
|
|
5011
|
+
const source = stmt.arguments.length === 1 ? sources[0] : void 0;
|
|
5012
|
+
const fits = source ? this.fitsAnnotation(source, declared, actual, env) : isAssignable(actual, declared) || isAssignable(widen(actual), declared);
|
|
5013
|
+
if (fits) return;
|
|
5014
|
+
this.diagnostics.push({
|
|
5015
|
+
node: stmt,
|
|
5016
|
+
message: `Type '${formatType(actual)}' is not assignable to '${briefType(declared)}'`
|
|
5017
|
+
});
|
|
5018
|
+
}
|
|
5019
|
+
/** A function that declared what it returns but never does. Only a body
|
|
5020
|
+
* with no `return` at all is reported: anything subtler needs to know
|
|
5021
|
+
* which paths can run off the end, and a wrong guess there is worse than
|
|
5022
|
+
* a missing complaint. */
|
|
5023
|
+
checkReturnsAtAll(func, declared) {
|
|
5024
|
+
if (!declared || !this.emitDiagnostics) return;
|
|
5025
|
+
if (func.predicate) return;
|
|
5026
|
+
if (declared.kind === "any" || declared.kind === "unknown" || declared.kind === "never") return;
|
|
5027
|
+
if (isAssignable(nilType, declared) || this.namesNothing(declared)) return;
|
|
5028
|
+
let found = false;
|
|
5029
|
+
const walk = (statements) => {
|
|
5030
|
+
for (const statement of statements) {
|
|
5031
|
+
if (found) return;
|
|
5032
|
+
if (statement.type === "ReturnStatement") {
|
|
5033
|
+
found = true;
|
|
5034
|
+
return;
|
|
5035
|
+
}
|
|
5036
|
+
for (const value of Object.values(statement)) {
|
|
5037
|
+
if (value && typeof value === "object" && "statements" in value) {
|
|
5038
|
+
walk(value.statements);
|
|
5039
|
+
} else if (Array.isArray(value)) {
|
|
5040
|
+
for (const item of value) {
|
|
5041
|
+
const block = item;
|
|
5042
|
+
if (block?.body?.statements) walk(block.body.statements);
|
|
5043
|
+
}
|
|
5044
|
+
}
|
|
5045
|
+
}
|
|
5046
|
+
}
|
|
5047
|
+
};
|
|
5048
|
+
walk(func.body.statements);
|
|
5049
|
+
if (found) return;
|
|
5050
|
+
this.diagnostics.push({
|
|
5051
|
+
node: func.body,
|
|
5052
|
+
message: `A function that returns '${briefType(declared)}' must return a value`
|
|
5053
|
+
});
|
|
5054
|
+
}
|
|
5055
|
+
/** Does this type rest on a name nothing declares? Such a type says
|
|
5056
|
+
* nothing about what fits it, so checking against it only piles a second
|
|
5057
|
+
* complaint on top of "Cannot find name". */
|
|
5058
|
+
namesNothing(t, seen = /* @__PURE__ */ new Set()) {
|
|
5059
|
+
if (seen.has(t)) return false;
|
|
5060
|
+
seen.add(t);
|
|
5061
|
+
if (t.kind === "genericRef") {
|
|
5062
|
+
return !this.aliasDefs.has(t.name) && !this.importedTypes.has(t.name) && this.options.libTypes?.[t.name] === void 0;
|
|
5063
|
+
}
|
|
5064
|
+
switch (t.kind) {
|
|
5065
|
+
case "union":
|
|
5066
|
+
case "intersection":
|
|
5067
|
+
return t.types.some((m) => this.namesNothing(m, seen));
|
|
5068
|
+
case "array":
|
|
5069
|
+
return this.namesNothing(t.element, seen);
|
|
5070
|
+
case "tuple":
|
|
5071
|
+
return t.elements.some((e) => this.namesNothing(e, seen));
|
|
5072
|
+
case "object":
|
|
5073
|
+
if (t.class) return false;
|
|
5074
|
+
return [...t.properties.values()].some((v) => this.namesNothing(v.type, seen));
|
|
5075
|
+
default:
|
|
5076
|
+
return false;
|
|
5077
|
+
}
|
|
5078
|
+
}
|
|
5079
|
+
/** Every type name in the program that resolved to nothing — a typo, or a
|
|
5080
|
+
* library the config does not load. A name that resolves to a type
|
|
5081
|
+
* parameter, an alias (even one still being resolved), an imported type or
|
|
5082
|
+
* a primitive is fine; what is left is a reference that stayed itself. */
|
|
5083
|
+
reportUnknownTypes() {
|
|
5084
|
+
if (!this.emitDiagnostics) return;
|
|
5085
|
+
const reported = /* @__PURE__ */ new Set();
|
|
5086
|
+
const visit = (node) => {
|
|
5087
|
+
if (!node || typeof node !== "object") return;
|
|
5088
|
+
if (Array.isArray(node)) {
|
|
5089
|
+
for (const item of node) visit(item);
|
|
5090
|
+
return;
|
|
5091
|
+
}
|
|
5092
|
+
const record = node;
|
|
5093
|
+
if (record.type === "TypeReference" && typeof record.base === "string") {
|
|
5094
|
+
const name = typeof record.namespace === "string" ? `${record.namespace}.${record.base}` : record.base;
|
|
5095
|
+
const resolved = this.typeOfTypeNode.get(node);
|
|
5096
|
+
const unresolved = resolved?.kind === "genericRef" && resolved.name === name && !this.aliasDefs.has(name) && !this.importedTypes.has(name) && this.options.libTypes?.[name] === void 0 && !STRING_INTRINSICS.has(name) && !this.importedNames().has(name.split(".")[0]);
|
|
5097
|
+
const at = node;
|
|
5098
|
+
const key = `${at.line.start}:${at.column.start}`;
|
|
5099
|
+
if (unresolved && !reported.has(key)) {
|
|
5100
|
+
reported.add(key);
|
|
5101
|
+
this.diagnostics.push({ node, message: `Cannot find name '${name}'` });
|
|
5102
|
+
}
|
|
5103
|
+
}
|
|
5104
|
+
for (const [key, value] of Object.entries(node)) {
|
|
5105
|
+
if (key !== "line" && key !== "column" && value && typeof value === "object") visit(value);
|
|
5106
|
+
}
|
|
5107
|
+
};
|
|
5108
|
+
visit(this.program.body);
|
|
5109
|
+
}
|
|
5110
|
+
/** Deferred `declare`s nothing used, typed now for tools that ask. */
|
|
5111
|
+
resolveDeferredDeclares() {
|
|
5112
|
+
for (const name of this.deferredDeclares.keys()) {
|
|
5113
|
+
const id = this.scopes.globalsByName.get(name);
|
|
5114
|
+
if (id !== void 0 && !this.bindingType.has(id)) this.bindingType.set(id, this.declaredAhead(id) ?? anyType);
|
|
5115
|
+
}
|
|
5116
|
+
}
|
|
4179
5117
|
resolveDeferredAliases() {
|
|
4180
5118
|
for (const [name, def] of this.aliasDefs) {
|
|
4181
5119
|
if (this.aliases.has(name)) continue;
|
|
@@ -4358,13 +5296,13 @@ var TypeAnalyzer = class {
|
|
|
4358
5296
|
type: p.optional ? optional(this.resolveType(p.typeAnnotation)) : this.resolveType(p.typeAnnotation),
|
|
4359
5297
|
optional: p.optional
|
|
4360
5298
|
}));
|
|
4361
|
-
return fn(
|
|
5299
|
+
return this.withTypeParamDefaults(fn(
|
|
4362
5300
|
params,
|
|
4363
5301
|
this.resolveType(node.returnType),
|
|
4364
5302
|
node.hasVarargs ? node.varargType ? this.resolveType(node.varargType) : anyType : void 0,
|
|
4365
5303
|
names,
|
|
4366
5304
|
this.resolvePredicate(node.predicate, params)
|
|
4367
|
-
);
|
|
5305
|
+
), node.generics);
|
|
4368
5306
|
});
|
|
4369
5307
|
}
|
|
4370
5308
|
case "TypeofTypeNode": {
|
|
@@ -4610,7 +5548,11 @@ var TypeAnalyzer = class {
|
|
|
4610
5548
|
}
|
|
4611
5549
|
reduceConditional(t) {
|
|
4612
5550
|
const checkType = this.reduceType(t.checkType);
|
|
4613
|
-
|
|
5551
|
+
const extendsType = this.reduceType(t.extendsType);
|
|
5552
|
+
const free = new Set(t.inferVars);
|
|
5553
|
+
if (containsTypeParam(checkType) || containsTypeParam(extendsType, /* @__PURE__ */ new Set(), free)) {
|
|
5554
|
+
return { ...t, checkType, extendsType };
|
|
5555
|
+
}
|
|
4614
5556
|
if (t.distributeParam && checkType.kind === "union") {
|
|
4615
5557
|
return union(checkType.types.map((m) => this.branchOf(t, m)));
|
|
4616
5558
|
}
|
|
@@ -4715,15 +5657,22 @@ var TypeAnalyzer = class {
|
|
|
4715
5657
|
const source = sources[i];
|
|
4716
5658
|
if (this.emitDiagnostics && target.type === "IdentifierPattern" && target.typeAnnotation && source) {
|
|
4717
5659
|
const declared = this.resolveType(target.typeAnnotation);
|
|
4718
|
-
if (declared.kind !== "any" && !this.fitsAnnotation(source, declared, inferred, env)) {
|
|
5660
|
+
if (declared.kind !== "any" && !this.namesNothing(declared) && !this.fitsAnnotation(source, declared, inferred, env)) {
|
|
4719
5661
|
this.diagnostics.push({
|
|
4720
5662
|
node: stmt,
|
|
4721
5663
|
message: `Type '${formatType(inferred)}' is not assignable to '${formatType(declared)}'`
|
|
4722
5664
|
});
|
|
5665
|
+
} else if (declared.kind !== "any") {
|
|
5666
|
+
this.reportExcessProperties(source, declared);
|
|
4723
5667
|
}
|
|
4724
5668
|
}
|
|
4725
5669
|
const mode = this.initIsAsConst(source) ? "asconst" : !isFreshLiteralExpr(source) ? "keep" : stmt.kind === "const" ? "const" : "widen";
|
|
4726
5670
|
this.bindPattern(target, inferred, env, mode);
|
|
5671
|
+
if (stmt.kind === "const") {
|
|
5672
|
+
this.correlateDestructuring(target, inferred, env);
|
|
5673
|
+
this.correlateIndexed(target, source, env);
|
|
5674
|
+
this.aliasReference(target, source);
|
|
5675
|
+
}
|
|
4727
5676
|
});
|
|
4728
5677
|
return;
|
|
4729
5678
|
}
|
|
@@ -4731,6 +5680,7 @@ var TypeAnalyzer = class {
|
|
|
4731
5680
|
this.checkParamOrder(stmt.func.params, stmt);
|
|
4732
5681
|
for (const sig of stmt.signatures ?? []) this.checkParamOrder(sig.params, stmt);
|
|
4733
5682
|
const id = this.bindingIdByName(stmt.name.name, stmt.name);
|
|
5683
|
+
this.paramsFromSignatures(stmt.func, stmt.signatures);
|
|
4734
5684
|
const fnType = stmt.signatures?.length ? intersection(stmt.signatures.map((s) => this.signatureToFnType(s))) : this.inferFunctionBody(stmt.func, env);
|
|
4735
5685
|
if (id !== void 0) {
|
|
4736
5686
|
this.bindingType.set(id, fnType);
|
|
@@ -4746,6 +5696,7 @@ var TypeAnalyzer = class {
|
|
|
4746
5696
|
const memberName = stmt.target.method?.name ?? (stmt.target.path.length === 1 ? stmt.target.path[0].name : void 0);
|
|
4747
5697
|
if (memberName === void 0 && stmt.target.path.length === 0) {
|
|
4748
5698
|
if (targetId !== void 0) {
|
|
5699
|
+
this.paramsFromSignatures(stmt.func, stmt.signatures);
|
|
4749
5700
|
const fnType = stmt.signatures?.length ? intersection(stmt.signatures.map((s) => this.signatureToFnType(s))) : this.inferFunctionBody(stmt.func, env);
|
|
4750
5701
|
this.bindingType.set(targetId, fnType);
|
|
4751
5702
|
this.setBinding(env, targetId, fnType);
|
|
@@ -4755,6 +5706,7 @@ var TypeAnalyzer = class {
|
|
|
4755
5706
|
}
|
|
4756
5707
|
const recv = targetId === void 0 ? anyType : this.currentType(targetId, env);
|
|
4757
5708
|
this.withSelfType(stmt.isMethod ? recv : void 0, () => {
|
|
5709
|
+
this.paramsFromSignatures(stmt.func, stmt.signatures);
|
|
4758
5710
|
const fnType = stmt.signatures?.length ? intersection(stmt.signatures.map((s) => this.signatureToFnType(s))) : this.inferFunctionBody(stmt.func, env);
|
|
4759
5711
|
if (memberName !== void 0 && targetId !== void 0) {
|
|
4760
5712
|
const grown = intersection([
|
|
@@ -4786,6 +5738,7 @@ var TypeAnalyzer = class {
|
|
|
4786
5738
|
if (target.type === "Identifier") {
|
|
4787
5739
|
const id = this.bindingIdOf(target);
|
|
4788
5740
|
if (id !== void 0) {
|
|
5741
|
+
this.uncorrelate(id);
|
|
4789
5742
|
const next = isFreshLiteralExpr(source) ? widen(vt) : vt;
|
|
4790
5743
|
if (this.annotated.has(id)) {
|
|
4791
5744
|
const declared = this.bindingType.get(id);
|
|
@@ -4859,16 +5812,40 @@ var TypeAnalyzer = class {
|
|
|
4859
5812
|
case "GenericForStatement": {
|
|
4860
5813
|
const iterTypes = stmt.iterators.map((it) => this.infer(it, env));
|
|
4861
5814
|
const bodyEnv = forkEnv(env);
|
|
4862
|
-
const
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
5815
|
+
const rows = stmt.variables.length >= 2 ? this.iterationRows(stmt.iterators[0], iterTypes[0]) : void 0;
|
|
5816
|
+
if (rows) {
|
|
5817
|
+
const [key, value] = stmt.variables;
|
|
5818
|
+
this.bindPattern(key, union(rows.map((r) => r[0])), bodyEnv, "keep");
|
|
5819
|
+
this.bindPattern(value, union(rows.map((r) => r[1])), bodyEnv, "keep");
|
|
5820
|
+
stmt.variables.slice(2).forEach((v) => this.bindPattern(v, unknownType, bodyEnv, "widen"));
|
|
5821
|
+
const keyId = key.type === "IdentifierPattern" ? this.bindingIdByName(key.name, key) : void 0;
|
|
5822
|
+
const valueId = value.type === "IdentifierPattern" ? this.bindingIdByName(value.name, value) : void 0;
|
|
5823
|
+
if (keyId !== void 0 && valueId !== void 0) this.correlateBindings(bodyEnv, [keyId, valueId], rows);
|
|
5824
|
+
} else {
|
|
5825
|
+
const [keyT, valT] = this.iterationTypes(stmt.iterators[0], iterTypes[0], stmt.variables.length);
|
|
5826
|
+
stmt.variables.forEach((v, i) => {
|
|
5827
|
+
this.bindPattern(v, i === 0 ? keyT : i === 1 ? valT : unknownType, bodyEnv, "widen");
|
|
5828
|
+
});
|
|
5829
|
+
}
|
|
4866
5830
|
this.visitBlock(stmt.body, bodyEnv);
|
|
4867
5831
|
return;
|
|
4868
5832
|
}
|
|
4869
|
-
case "ReturnStatement":
|
|
4870
|
-
|
|
5833
|
+
case "ReturnStatement": {
|
|
5834
|
+
const declared = this.declaredReturns[this.declaredReturns.length - 1];
|
|
5835
|
+
if (declared) {
|
|
5836
|
+
if (stmt.arguments.length === 1) {
|
|
5837
|
+
this.applyContext(stmt.arguments[0], declared);
|
|
5838
|
+
} else if (declared.kind === "tuple" && declared.isPack) {
|
|
5839
|
+
stmt.arguments.forEach((a, i) => this.applyContext(a, declared.elements[i]));
|
|
5840
|
+
}
|
|
5841
|
+
}
|
|
5842
|
+
const { types, sources } = this.valueList(stmt.arguments, env);
|
|
5843
|
+
this.checkReturn(stmt, declared, types, sources, env);
|
|
5844
|
+
if (this.returnTypes) {
|
|
5845
|
+
this.returnTypes.push(stmt.arguments.length === 0 ? nilType : types.length === 1 ? types[0] : tuple([...types], true));
|
|
5846
|
+
}
|
|
4871
5847
|
return;
|
|
5848
|
+
}
|
|
4872
5849
|
case "ExportStatement":
|
|
4873
5850
|
this.visitStatement(stmt.declaration, env);
|
|
4874
5851
|
return;
|
|
@@ -5051,6 +6028,7 @@ var TypeAnalyzer = class {
|
|
|
5051
6028
|
}
|
|
5052
6029
|
if (p.typeAnnotation) {
|
|
5053
6030
|
const t = this.resolveType(p.typeAnnotation);
|
|
6031
|
+
if (p.default) this.applyContext(p.default, t);
|
|
5054
6032
|
return p.optional ? optional(t) : t;
|
|
5055
6033
|
}
|
|
5056
6034
|
if (p.pattern) return this.patternToType(p.pattern, env);
|
|
@@ -5069,6 +6047,8 @@ var TypeAnalyzer = class {
|
|
|
5069
6047
|
let e = expr;
|
|
5070
6048
|
while (e.type === "ParenthesizedExpression") e = e.expression;
|
|
5071
6049
|
if (!expected) return;
|
|
6050
|
+
this.expectedTypeOf.set(expr, expected);
|
|
6051
|
+
this.expectedTypeOf.set(e, expected);
|
|
5072
6052
|
if (e.type === "ArrayExpression") return this.applyArrayContext(e, expected);
|
|
5073
6053
|
if (e.type === "TableExpression") return this.applyTableContext(e, expected);
|
|
5074
6054
|
if (e.type !== "FunctionExpression") return;
|
|
@@ -5111,13 +6091,15 @@ var TypeAnalyzer = class {
|
|
|
5111
6091
|
const objects = this.expectedMembers(expected).filter((m) => m.kind === "object");
|
|
5112
6092
|
if (!objects.length) return;
|
|
5113
6093
|
for (const field of e.fields) {
|
|
5114
|
-
if (field.type !== "TableFieldNamed") continue;
|
|
5115
|
-
const key = field.key.type === "Identifier" ? field.key.name : field.key.value;
|
|
6094
|
+
if (field.type !== "TableFieldNamed" && field.type !== "TableFieldShorthand") continue;
|
|
6095
|
+
const key = field.type === "TableFieldShorthand" ? field.name.name : field.key.type === "Identifier" ? field.key.name : field.key.value;
|
|
5116
6096
|
const types = objects.flatMap((o) => {
|
|
5117
6097
|
const property = o.properties.get(key);
|
|
5118
6098
|
return property ? [property.type] : o.indexer ? [o.indexer.value] : [];
|
|
5119
6099
|
});
|
|
5120
|
-
if (types.length)
|
|
6100
|
+
if (types.length) {
|
|
6101
|
+
this.applyContext(field.type === "TableFieldShorthand" ? field.name : field.value, union(types));
|
|
6102
|
+
}
|
|
5121
6103
|
}
|
|
5122
6104
|
}
|
|
5123
6105
|
/** The members of an expected type worth matching a literal against:
|
|
@@ -5154,11 +6136,28 @@ var TypeAnalyzer = class {
|
|
|
5154
6136
|
}
|
|
5155
6137
|
return tuple(target.elements.map((el) => el ? leaf(el.value, el.default) : anyType));
|
|
5156
6138
|
}
|
|
6139
|
+
/** The type of `...` in each function body being walked. */
|
|
6140
|
+
varargs = [];
|
|
6141
|
+
/** What each function body being walked declared it returns. */
|
|
6142
|
+
declaredReturns = [];
|
|
6143
|
+
/** Run `body` with `...` and `return` as `func` declares them. */
|
|
6144
|
+
withVarargs(func, body) {
|
|
6145
|
+
this.varargs.push(func.hasVarargs ? func.varargTypeAnnotation ? this.resolveType(func.varargTypeAnnotation) : anyType : void 0);
|
|
6146
|
+
this.declaredReturns.push(func.predicate ? booleanType : func.returnType ? this.resolveType(func.returnType) : void 0);
|
|
6147
|
+
try {
|
|
6148
|
+
return body();
|
|
6149
|
+
} finally {
|
|
6150
|
+
this.varargs.pop();
|
|
6151
|
+
this.declaredReturns.pop();
|
|
6152
|
+
}
|
|
6153
|
+
}
|
|
5157
6154
|
visitFunctionBodyInner(func, outerEnv) {
|
|
5158
6155
|
const env = forkEnv(outerEnv);
|
|
5159
6156
|
for (const p of func.params) {
|
|
5160
6157
|
if (p.pattern) {
|
|
5161
|
-
|
|
6158
|
+
const type = this.paramType(p, env);
|
|
6159
|
+
this.bindPattern(p.pattern, type, env, "widen");
|
|
6160
|
+
this.correlateDestructuring(p.pattern, type, env);
|
|
5162
6161
|
continue;
|
|
5163
6162
|
}
|
|
5164
6163
|
const id = this.bindingIdByName(p.name, p);
|
|
@@ -5169,13 +6168,16 @@ var TypeAnalyzer = class {
|
|
|
5169
6168
|
if (p.typeAnnotation) this.annotated.add(id);
|
|
5170
6169
|
}
|
|
5171
6170
|
}
|
|
5172
|
-
this.
|
|
6171
|
+
this.withVarargs(func, () => this.collectReturns(void 0, () => {
|
|
6172
|
+
this.visitBlock(func.body, env);
|
|
6173
|
+
this.checkReturnsAtAll(func, this.declaredReturns[this.declaredReturns.length - 1]);
|
|
6174
|
+
}));
|
|
5173
6175
|
}
|
|
5174
6176
|
/** Return type of calling `f` with `argTypes`. For a generic function,
|
|
5175
6177
|
* infers the type parameters from the arguments and substitutes. */
|
|
5176
|
-
callReturn(f, argTypes) {
|
|
6178
|
+
callReturn(f, argTypes, explicit) {
|
|
5177
6179
|
if (!f.typeParams?.length) return f.returns;
|
|
5178
|
-
return this.reduceType(substitute(f.returns, this.inferTypeArgs(f, argTypes)));
|
|
6180
|
+
return this.reduceType(substitute(f.returns, this.inferTypeArgs(f, argTypes, explicit)));
|
|
5179
6181
|
}
|
|
5180
6182
|
/** Infer a generic call's type arguments from the argument types.
|
|
5181
6183
|
*
|
|
@@ -5183,16 +6185,47 @@ var TypeAnalyzer = class {
|
|
|
5183
6185
|
* `1` — *except* against a parameter whose constraint is made of literal
|
|
5184
6186
|
* types, where the literal is the whole point. That is what lets
|
|
5185
6187
|
* `<K extends keyof T>(name: K) -> T[K]` pick out one property. */
|
|
5186
|
-
|
|
5187
|
-
|
|
6188
|
+
/** The type arguments a call writes out, checked for count. */
|
|
6189
|
+
explicitTypeArguments(expr, fns) {
|
|
6190
|
+
const written = expr.typeArguments;
|
|
6191
|
+
if (!written?.length) return void 0;
|
|
6192
|
+
const resolved = written.map((node) => this.resolveType(node));
|
|
6193
|
+
const most = Math.max(0, ...fns.map((f) => f.typeParams?.length ?? 0));
|
|
6194
|
+
if (this.emitDiagnostics && resolved.length > most) {
|
|
6195
|
+
this.diagnostics.push({
|
|
6196
|
+
node: written[most],
|
|
6197
|
+
message: most === 0 ? "This call takes no type arguments" : `Expected ${most} type argument${most === 1 ? "" : "s"}, got ${resolved.length}`
|
|
6198
|
+
});
|
|
6199
|
+
}
|
|
6200
|
+
return resolved;
|
|
6201
|
+
}
|
|
6202
|
+
/** `<T = Instance>`: what a call falls back to for a parameter it neither
|
|
6203
|
+
* is given nor can infer. */
|
|
6204
|
+
withTypeParamDefaults(type, generics) {
|
|
6205
|
+
if (type.kind !== "function") return type;
|
|
6206
|
+
const defaults = {};
|
|
6207
|
+
for (const generic of generics) {
|
|
6208
|
+
if (generic.default && !generic.isPack) defaults[generic.name] = this.resolveType(generic.default);
|
|
6209
|
+
}
|
|
6210
|
+
return Object.keys(defaults).length ? { ...type, typeParamDefaults: defaults } : type;
|
|
6211
|
+
}
|
|
6212
|
+
inferTypeArgs(f, argTypes, explicit) {
|
|
5188
6213
|
const subst = /* @__PURE__ */ new Map();
|
|
6214
|
+
if (explicit?.length) {
|
|
6215
|
+
(f.typeParams ?? []).forEach((name, i) => {
|
|
6216
|
+
if (explicit[i]) subst.set(name, explicit[i]);
|
|
6217
|
+
});
|
|
6218
|
+
}
|
|
6219
|
+
const vars = new Set((f.typeParams ?? []).filter((name) => !subst.has(name)));
|
|
5189
6220
|
f.params.forEach((p, i) => {
|
|
5190
6221
|
const arg = argTypes[i];
|
|
5191
6222
|
if (arg === void 0) return;
|
|
5192
6223
|
const param = p.type.kind === "typeParam" && p.type.constraint ? { ...p.type, constraint: this.reduceType(p.type.constraint) } : p.type;
|
|
5193
6224
|
unify(p.type, keepsLiterals(param) ? arg : widen(arg), vars, subst);
|
|
5194
6225
|
});
|
|
5195
|
-
for (const name of f.typeParams ?? [])
|
|
6226
|
+
for (const name of f.typeParams ?? []) {
|
|
6227
|
+
if (!subst.has(name)) subst.set(name, f.typeParamDefaults?.[name] ?? unknownType);
|
|
6228
|
+
}
|
|
5196
6229
|
return subst;
|
|
5197
6230
|
}
|
|
5198
6231
|
/** Re-infer the arguments that land on a `<const T>` parameter, keeping
|
|
@@ -5223,6 +6256,32 @@ var TypeAnalyzer = class {
|
|
|
5223
6256
|
}
|
|
5224
6257
|
return void 0;
|
|
5225
6258
|
}
|
|
6259
|
+
/** An overload set called with a union argument, one member at a time.
|
|
6260
|
+
*
|
|
6261
|
+
* One signature for the whole union is often only the catch-all:
|
|
6262
|
+
* `typeof(v)` with `v: Part | nil` accepts nothing more specific than
|
|
6263
|
+
* `typeof<T>(value: T): string`. Each member on its own picks `"Instance"`
|
|
6264
|
+
* and `"nil"`, and that union is what the call returns — whenever every
|
|
6265
|
+
* member picks a signature listed ahead of the whole union's. Otherwise
|
|
6266
|
+
* (a signature taking the union as it is, or a member nothing accepts)
|
|
6267
|
+
* this returns `undefined` and the ordinary pick stands. */
|
|
6268
|
+
distributedReturn(fns, argTypes, picked, argsFor) {
|
|
6269
|
+
if (fns.length < 2) return void 0;
|
|
6270
|
+
const position = argTypes.findIndex((t) => this.expand(t).kind === "union");
|
|
6271
|
+
if (position < 0) return void 0;
|
|
6272
|
+
const members = this.expand(argTypes[position]).types;
|
|
6273
|
+
if (members.length > 32) return void 0;
|
|
6274
|
+
const rank = (f) => ((f.typeParams?.length ?? 0) > 0 ? fns.length : 0) + fns.indexOf(f);
|
|
6275
|
+
const limit = picked ? rank(picked) : Infinity;
|
|
6276
|
+
const results = [];
|
|
6277
|
+
for (const member of members) {
|
|
6278
|
+
const args = argTypes.map((t, i) => i === position ? member : t);
|
|
6279
|
+
const chosen = this.pickOverload(fns, args, (f) => argsFor(f, args));
|
|
6280
|
+
if (!chosen || rank(chosen) >= limit) return void 0;
|
|
6281
|
+
results.push(this.callReturn(chosen, argsFor(chosen, args)));
|
|
6282
|
+
}
|
|
6283
|
+
return union(results);
|
|
6284
|
+
}
|
|
5226
6285
|
/** Can this signature be called with these argument types? The signature's
|
|
5227
6286
|
* own type parameters stand for what the call would infer, so each is
|
|
5228
6287
|
* checked only against its constraint — `<K extends keyof Services>`
|
|
@@ -5258,7 +6317,7 @@ var TypeAnalyzer = class {
|
|
|
5258
6317
|
for (const child of Object.values(value)) walk(child);
|
|
5259
6318
|
};
|
|
5260
6319
|
for (const p of f.params) if (containsTypeParam(p.type)) walk(p.type);
|
|
5261
|
-
return f.params.map((p) => substitute(p.type, bounds));
|
|
6320
|
+
return f.params.map((p) => this.reduceType(substitute(p.type, bounds)));
|
|
5262
6321
|
}
|
|
5263
6322
|
/** Record what each written argument is expected to be — see
|
|
5264
6323
|
* `TypeAnalysis.expectedTypeOf`. */
|
|
@@ -5275,6 +6334,28 @@ var TypeAnalyzer = class {
|
|
|
5275
6334
|
}
|
|
5276
6335
|
/** No signature accepts the call, and the argument count is not the
|
|
5277
6336
|
* problem: say which argument is wrong, the way TypeScript does. */
|
|
6337
|
+
/** Check what was written against the parameters as this call's own type
|
|
6338
|
+
* arguments make them read: `pick("Bones", "C")` is wrong only once `P`
|
|
6339
|
+
* is known to be `"Bones"`. Picking the overload goes by each parameter's
|
|
6340
|
+
* constraint, which is deliberately looser than that. */
|
|
6341
|
+
checkInferredArguments(call, written, f, argTypes, self) {
|
|
6342
|
+
if (!this.emitDiagnostics || !f.typeParams?.length) return;
|
|
6343
|
+
const subst = this.inferTypeArgs(f, [...argTypes]);
|
|
6344
|
+
for (const bound of subst.values()) if (bound.kind === "unknown") return;
|
|
6345
|
+
for (let i = 0; i < f.params.length; i++) {
|
|
6346
|
+
const arg = argTypes[i];
|
|
6347
|
+
const declared = f.params[i].type;
|
|
6348
|
+
if (arg === void 0 || !containsTypeParam(declared)) continue;
|
|
6349
|
+
const expected = this.reduceType(substitute(declared, subst));
|
|
6350
|
+
if (containsTypeParam(expected) || expected.kind === "any" || expected.kind === "unknown") continue;
|
|
6351
|
+
if (isAssignable(arg, expected) || isAssignable(widen(arg), expected)) continue;
|
|
6352
|
+
this.diagnostics.push({
|
|
6353
|
+
node: written[i - self] ?? call,
|
|
6354
|
+
message: `Argument of type '${formatType(arg)}' is not assignable to parameter of type '${briefType(expected)}'`
|
|
6355
|
+
});
|
|
6356
|
+
return;
|
|
6357
|
+
}
|
|
6358
|
+
}
|
|
5278
6359
|
reportArguments(call, written, fns, argsFor, selfOf) {
|
|
5279
6360
|
if (!this.emitDiagnostics) return;
|
|
5280
6361
|
if (fns.length > 1) {
|
|
@@ -5344,9 +6425,33 @@ var TypeAnalyzer = class {
|
|
|
5344
6425
|
});
|
|
5345
6426
|
return false;
|
|
5346
6427
|
}
|
|
6428
|
+
/** An overload set's implementation handles every signature, so a bare
|
|
6429
|
+
* parameter of it holds whatever those signatures allow there:
|
|
6430
|
+
* `function f(Stat, ...)` under 36 `Stat: "..."` signatures is the union
|
|
6431
|
+
* of all 36. TypeScript leaves such a parameter `any`; this says what it
|
|
6432
|
+
* can actually be. An annotation, a pattern or a default still wins. */
|
|
6433
|
+
paramsFromSignatures(func, signatures) {
|
|
6434
|
+
if (!signatures?.length) return;
|
|
6435
|
+
const resolved = signatures.map((sig) => this.signatureToFnType(sig));
|
|
6436
|
+
func.params.forEach((param, i) => {
|
|
6437
|
+
if (param.typeAnnotation || param.pattern || param.default) return;
|
|
6438
|
+
const candidates = [];
|
|
6439
|
+
for (const signature of resolved) {
|
|
6440
|
+
if (signature.kind !== "function") continue;
|
|
6441
|
+
const own = signature.params[i];
|
|
6442
|
+
if (own) candidates.push(own.optional ? optional(own.type) : own.type);
|
|
6443
|
+
else if (signature.varargs) candidates.push(signature.varargs);
|
|
6444
|
+
}
|
|
6445
|
+
if (candidates.length) this.contextualParams.set(param, union(candidates));
|
|
6446
|
+
});
|
|
6447
|
+
}
|
|
5347
6448
|
signatureToFnType(sig) {
|
|
5348
6449
|
const names = sig.generics.map((g) => g.name);
|
|
5349
|
-
|
|
6450
|
+
const record = (type) => {
|
|
6451
|
+
this.typeOfTypeNode.set(sig, type);
|
|
6452
|
+
return type;
|
|
6453
|
+
};
|
|
6454
|
+
return record(this.withTypeParams(sig.generics, () => {
|
|
5350
6455
|
const params = sig.params.map((p) => ({
|
|
5351
6456
|
name: p.pattern ? void 0 : p.name,
|
|
5352
6457
|
type: this.paramType(p, /* @__PURE__ */ new Map()),
|
|
@@ -5359,7 +6464,7 @@ var TypeAnalyzer = class {
|
|
|
5359
6464
|
names,
|
|
5360
6465
|
this.resolvePredicate(sig.predicate, params)
|
|
5361
6466
|
);
|
|
5362
|
-
});
|
|
6467
|
+
}));
|
|
5363
6468
|
}
|
|
5364
6469
|
/** Turn a parsed `v is T` / `asserts v` annotation into a `TypePredicate`,
|
|
5365
6470
|
* resolving the named parameter to its index. A guard naming a parameter
|
|
@@ -5404,8 +6509,11 @@ var TypeAnalyzer = class {
|
|
|
5404
6509
|
} else if (func.predicate) {
|
|
5405
6510
|
returns = booleanType;
|
|
5406
6511
|
} else {
|
|
5407
|
-
|
|
5408
|
-
returns = this.
|
|
6512
|
+
const collected = [];
|
|
6513
|
+
returns = this.withVarargs(func, () => this.silently(() => {
|
|
6514
|
+
this.collectReturns(collected, () => this.preVisitBody(func.body, bodyEnv));
|
|
6515
|
+
return collected.length ? union(collected) : this.inferReturnType(func.body, bodyEnv);
|
|
6516
|
+
}));
|
|
5409
6517
|
}
|
|
5410
6518
|
return fn(
|
|
5411
6519
|
params,
|
|
@@ -5416,6 +6524,29 @@ var TypeAnalyzer = class {
|
|
|
5416
6524
|
);
|
|
5417
6525
|
});
|
|
5418
6526
|
}
|
|
6527
|
+
/** Where the return types of the function being walked are collected, so
|
|
6528
|
+
* each is read where it is written — inside the branch that narrowed it —
|
|
6529
|
+
* rather than in whatever state the body ends in. */
|
|
6530
|
+
returnTypes;
|
|
6531
|
+
collectReturns(into, body) {
|
|
6532
|
+
const previous = this.returnTypes;
|
|
6533
|
+
this.returnTypes = into;
|
|
6534
|
+
try {
|
|
6535
|
+
return body();
|
|
6536
|
+
} finally {
|
|
6537
|
+
this.returnTypes = previous;
|
|
6538
|
+
}
|
|
6539
|
+
}
|
|
6540
|
+
/** Run something without reporting what it finds. */
|
|
6541
|
+
silently(body) {
|
|
6542
|
+
const wasEmitting = this.emitDiagnostics;
|
|
6543
|
+
this.emitDiagnostics = false;
|
|
6544
|
+
try {
|
|
6545
|
+
return body();
|
|
6546
|
+
} finally {
|
|
6547
|
+
this.emitDiagnostics = wasEmitting;
|
|
6548
|
+
}
|
|
6549
|
+
}
|
|
5419
6550
|
/** Populate binding types for a function body without reporting anything,
|
|
5420
6551
|
* purely so an un-annotated return type can see its own locals. Bounded:
|
|
5421
6552
|
* nested functions stop pre-visiting after a couple of levels, since the
|
|
@@ -5432,6 +6563,157 @@ var TypeAnalyzer = class {
|
|
|
5432
6563
|
this.preVisitDepth--;
|
|
5433
6564
|
}
|
|
5434
6565
|
}
|
|
6566
|
+
/** The `[key, value]` pairs iterating a record yields, one per property —
|
|
6567
|
+
* for `pairs(t)`, `next, t` and `for k, v in t` over an object type with
|
|
6568
|
+
* no indexer. `undefined` for anything else (an array, a dictionary, an
|
|
6569
|
+
* iterator function), whose keys have no names to list. */
|
|
6570
|
+
iterationRows(iterNode, iterType) {
|
|
6571
|
+
let source;
|
|
6572
|
+
if (iterNode?.type === "CallExpression" && iterNode.callee.type === "Identifier" && iterNode.arguments[0]) {
|
|
6573
|
+
if (iterNode.callee.name !== "pairs" && iterNode.callee.name !== "next") return void 0;
|
|
6574
|
+
source = this.typeOf.get(iterNode.arguments[0]);
|
|
6575
|
+
} else {
|
|
6576
|
+
source = iterType;
|
|
6577
|
+
}
|
|
6578
|
+
const t = source && this.expand(source);
|
|
6579
|
+
if (!t || t.kind !== "object" || t.class || t.indexer || !t.properties.size) return void 0;
|
|
6580
|
+
return [...t.properties].map(([name, property]) => [
|
|
6581
|
+
literal(name),
|
|
6582
|
+
property.optional ? optional(property.type) : property.type
|
|
6583
|
+
]);
|
|
6584
|
+
}
|
|
6585
|
+
/** Bindings that hold parts of one value: the key and value of a `pairs`
|
|
6586
|
+
* row, or the names destructured from one union member. By flow key.
|
|
6587
|
+
* Which rows are still possible is itself flow state, kept in `env` under
|
|
6588
|
+
* `group` as a union of tuples, so it narrows and merges like any type. */
|
|
6589
|
+
correlations = /* @__PURE__ */ new Map();
|
|
6590
|
+
correlateBindings(env, ids, rows) {
|
|
6591
|
+
const keys = ids.map(bindKey);
|
|
6592
|
+
const group = `rows(${keys.join(",")})`;
|
|
6593
|
+
keys.forEach((key, index) => this.correlations.set(key, { group, index, keys, rows }));
|
|
6594
|
+
env.set(group, union(rows.map((row) => tuple(row))));
|
|
6595
|
+
}
|
|
6596
|
+
/** `key` was just narrowed to `narrowed` in `env`: narrow that column of
|
|
6597
|
+
* every row, drop the rows it rules out, and give the other bindings what
|
|
6598
|
+
* the remaining rows hold. */
|
|
6599
|
+
correlate(env, key, narrowed) {
|
|
6600
|
+
const entry = this.correlations.get(key);
|
|
6601
|
+
if (!entry) return;
|
|
6602
|
+
const state = env.get(entry.group);
|
|
6603
|
+
const current = state && (state.kind === "union" ? state.types : [state]).every((t) => t.kind === "tuple") ? (state.kind === "union" ? state.types : [state]).map((t) => t.elements) : entry.rows;
|
|
6604
|
+
const kept = [];
|
|
6605
|
+
for (const row of current) {
|
|
6606
|
+
const column = narrowTo(row[entry.index], narrowed);
|
|
6607
|
+
if (column.kind !== "never") kept.push(row.map((t, i) => i === entry.index ? column : t));
|
|
6608
|
+
}
|
|
6609
|
+
env.set(entry.group, kept.length ? union(kept.map((row) => tuple(row))) : neverType);
|
|
6610
|
+
entry.keys.forEach((other, j) => {
|
|
6611
|
+
if (j !== entry.index) env.set(other, kept.length ? union(kept.map((row) => row[j])) : neverType);
|
|
6612
|
+
});
|
|
6613
|
+
}
|
|
6614
|
+
/** Stop correlating a binding once it is assigned: its value no longer
|
|
6615
|
+
* comes from the row. */
|
|
6616
|
+
uncorrelate(id) {
|
|
6617
|
+
const entry = this.correlations.get(bindKey(id));
|
|
6618
|
+
if (entry) for (const key of entry.keys) this.correlations.delete(key);
|
|
6619
|
+
}
|
|
6620
|
+
/** `const { kind, payload } = action` over a union of objects: one row per
|
|
6621
|
+
* member, so testing `kind` narrows `payload` (TypeScript's destructured
|
|
6622
|
+
* discriminated unions). Only plain `name` / `key: name` properties take
|
|
6623
|
+
* part. */
|
|
6624
|
+
/** Names that denote one and the same value: `const c = player.Character`
|
|
6625
|
+
* makes `c` and `player.Character` two spellings of one reference. Kept
|
|
6626
|
+
* as an undirected graph of flow keys. */
|
|
6627
|
+
refAliases = /* @__PURE__ */ new Map();
|
|
6628
|
+
/** `const c = a.b` — `c` cannot be re-bound and the path was read once, so
|
|
6629
|
+
* a test of either name is a test of the same value. Only property paths
|
|
6630
|
+
* take part: `const c = other` would tie `c` to a name that may itself be
|
|
6631
|
+
* assigned a different value later. */
|
|
6632
|
+
aliasReference(target, init) {
|
|
6633
|
+
if (target.type !== "IdentifierPattern" || !init) return;
|
|
6634
|
+
const source = unwrapParens(init);
|
|
6635
|
+
if (source.type !== "MemberExpression" && source.type !== "IndexExpression") return;
|
|
6636
|
+
const path = this.refKeyOf(source);
|
|
6637
|
+
const id = this.bindingIdByName(target.name, target);
|
|
6638
|
+
if (path === void 0 || id === void 0) return;
|
|
6639
|
+
const name = bindKey(id);
|
|
6640
|
+
for (const [a, b] of [[name, path], [path, name]]) {
|
|
6641
|
+
const set = this.refAliases.get(a) ?? /* @__PURE__ */ new Set();
|
|
6642
|
+
set.add(b);
|
|
6643
|
+
this.refAliases.set(a, set);
|
|
6644
|
+
}
|
|
6645
|
+
}
|
|
6646
|
+
/** A reference was narrowed: give every other spelling of the same value
|
|
6647
|
+
* the same news. Walks the alias graph, so a path with two names told by
|
|
6648
|
+
* one of them reaches the other. Each alias keeps whatever it already
|
|
6649
|
+
* knew — the narrowing only ever cuts the type further down. */
|
|
6650
|
+
propagateAliases(env, into, key, narrowed) {
|
|
6651
|
+
if (!this.refAliases.size) return;
|
|
6652
|
+
const seen = /* @__PURE__ */ new Set([key]);
|
|
6653
|
+
const queue = [[key, narrowed]];
|
|
6654
|
+
const learn = (at, t) => {
|
|
6655
|
+
seen.add(at);
|
|
6656
|
+
this.setRef(into, at, t);
|
|
6657
|
+
this.correlate(into, at, t);
|
|
6658
|
+
queue.push([at, t]);
|
|
6659
|
+
};
|
|
6660
|
+
for (let at = 0; at < queue.length; at++) {
|
|
6661
|
+
const [from, t] = queue[at];
|
|
6662
|
+
for (const other of this.refAliases.get(from) ?? []) {
|
|
6663
|
+
if (seen.has(other)) continue;
|
|
6664
|
+
const current = into.get(other) ?? env.get(other) ?? this.declaredAtRef(other);
|
|
6665
|
+
const next = narrowTo(current, t);
|
|
6666
|
+
learn(other, next.kind === "never" ? t : next);
|
|
6667
|
+
for (let child = other, value = into.get(other); ; ) {
|
|
6668
|
+
const cut = child.lastIndexOf(".");
|
|
6669
|
+
if (cut <= 0) break;
|
|
6670
|
+
const parent = child.slice(0, cut);
|
|
6671
|
+
if (seen.has(parent)) break;
|
|
6672
|
+
const had = into.get(parent) ?? env.get(parent) ?? this.declaredAtRef(parent);
|
|
6673
|
+
value = this.filterByProperty(had, child.slice(cut + 1), value);
|
|
6674
|
+
learn(parent, value);
|
|
6675
|
+
child = parent;
|
|
6676
|
+
}
|
|
6677
|
+
}
|
|
6678
|
+
}
|
|
6679
|
+
}
|
|
6680
|
+
/** `const path = paths[stat]` where `stat` is one of several keys: which
|
|
6681
|
+
* value came back says which key was asked for. Testing the value then
|
|
6682
|
+
* narrows the key — the `else` of `if path then` leaves exactly the keys
|
|
6683
|
+
* the table does not have. */
|
|
6684
|
+
correlateIndexed(target, init, env) {
|
|
6685
|
+
if (target.type !== "IdentifierPattern" || !init) return;
|
|
6686
|
+
const source = unwrapParens(init);
|
|
6687
|
+
if (source.type !== "IndexExpression" || source.index.type !== "Identifier") return;
|
|
6688
|
+
const valueId = this.bindingIdByName(target.name, target);
|
|
6689
|
+
const keyId = this.bindingIdOf(source.index);
|
|
6690
|
+
if (valueId === void 0 || keyId === void 0) return;
|
|
6691
|
+
const key = this.expand(this.currentType(keyId, env));
|
|
6692
|
+
if (key.kind !== "union" || key.types.length < 2 || key.types.length > 64) return;
|
|
6693
|
+
if (!key.types.every((m) => m.kind === "literal")) return;
|
|
6694
|
+
const object = this.expand(this.typeOf.get(source.object) ?? unknownType);
|
|
6695
|
+
if (object.kind !== "object") return;
|
|
6696
|
+
this.correlateBindings(env, [keyId, valueId], key.types.map((m) => [m, this.indexedType(object, m)]));
|
|
6697
|
+
}
|
|
6698
|
+
correlateDestructuring(pattern, source, env) {
|
|
6699
|
+
if (pattern.type !== "ObjectPattern") return;
|
|
6700
|
+
const members = this.expand(source);
|
|
6701
|
+
if (members.kind !== "union") return;
|
|
6702
|
+
const objects = members.types.map((m) => this.expand(m));
|
|
6703
|
+
if (objects.length < 2 || objects.some((m) => m.kind !== "object")) return;
|
|
6704
|
+
const ids = [];
|
|
6705
|
+
const names = [];
|
|
6706
|
+
for (const property of pattern.properties) {
|
|
6707
|
+
if (property.computed || property.default || property.value.type !== "IdentifierPattern") return;
|
|
6708
|
+
const name = property.key.type === "Identifier" ? property.key.name : property.key.type === "StringLiteral" ? property.key.value : void 0;
|
|
6709
|
+
const id = this.bindingIdByName(property.value.name, property.value);
|
|
6710
|
+
if (name === void 0 || id === void 0) return;
|
|
6711
|
+
ids.push(id);
|
|
6712
|
+
names.push(name);
|
|
6713
|
+
}
|
|
6714
|
+
if (ids.length < 2) return;
|
|
6715
|
+
this.correlateBindings(env, ids, objects.map((member) => names.map((name) => this.propertyType(member, name))));
|
|
6716
|
+
}
|
|
5435
6717
|
/** `(keyType, valueType)` yielded by a generic-for iterator. Handles
|
|
5436
6718
|
* `ipairs`/`pairs`/`next(t)` and Luau generalized iteration (`for … in t`).
|
|
5437
6719
|
* `varCount` is how many loop variables were written. */
|
|
@@ -5496,6 +6778,18 @@ var TypeAnalyzer = class {
|
|
|
5496
6778
|
if (init.type === "ArrayExpression") return isAssignable(this.inferArray(init, env, true), declared);
|
|
5497
6779
|
return false;
|
|
5498
6780
|
}
|
|
6781
|
+
/** `{ a, ...rest }`: what `rest` holds — the value without the properties
|
|
6782
|
+
* the pattern already took. */
|
|
6783
|
+
withoutKeys(raw, properties) {
|
|
6784
|
+
const taken = new Set(properties.flatMap((p) => !p.computed && p.key.type === "Identifier" ? [p.key.name] : !p.computed && p.key.type === "StringLiteral" ? [p.key.value] : []));
|
|
6785
|
+
if (!taken.size) return raw;
|
|
6786
|
+
const t = this.expand(raw);
|
|
6787
|
+
if (t.kind === "union") return union(t.types.map((m) => this.withoutKeys(m, properties)));
|
|
6788
|
+
if (t.kind !== "object") return raw;
|
|
6789
|
+
const kept = [...t.properties].filter(([name]) => !taken.has(name));
|
|
6790
|
+
if (kept.length === t.properties.size) return raw;
|
|
6791
|
+
return objectType(kept, t.indexer, t.frozen);
|
|
6792
|
+
}
|
|
5499
6793
|
/** Fold a destructuring default (`{ a = 1 }`) into the property's type:
|
|
5500
6794
|
* the default applies when the source value is missing/`nil`. */
|
|
5501
6795
|
withDefault(base, def, env) {
|
|
@@ -5527,7 +6821,7 @@ var TypeAnalyzer = class {
|
|
|
5527
6821
|
const pt = key !== void 0 ? this.propertyType(valueType, key) : unknownType;
|
|
5528
6822
|
this.reassignPattern(p.value, this.withDefault(pt, p.default, env), env);
|
|
5529
6823
|
}
|
|
5530
|
-
if (target.rest) this.reassignPattern(target.rest, valueType, env);
|
|
6824
|
+
if (target.rest) this.reassignPattern(target.rest, this.withoutKeys(valueType, target.properties), env);
|
|
5531
6825
|
return;
|
|
5532
6826
|
}
|
|
5533
6827
|
case "ArrayPattern": {
|
|
@@ -5562,7 +6856,7 @@ var TypeAnalyzer = class {
|
|
|
5562
6856
|
const propType = key !== void 0 ? this.propertyType(valueType, key) : unknownType;
|
|
5563
6857
|
this.bindPattern(p.value, this.withDefault(propType, p.default, env), env, mode);
|
|
5564
6858
|
}
|
|
5565
|
-
if (target.rest) this.bindPattern(target.rest, valueType, env, mode);
|
|
6859
|
+
if (target.rest) this.bindPattern(target.rest, this.withoutKeys(valueType, target.properties), env, mode);
|
|
5566
6860
|
return;
|
|
5567
6861
|
}
|
|
5568
6862
|
case "ArrayPattern": {
|
|
@@ -5604,13 +6898,34 @@ var TypeAnalyzer = class {
|
|
|
5604
6898
|
this.resolvingAliases.delete(t.name);
|
|
5605
6899
|
}
|
|
5606
6900
|
}
|
|
6901
|
+
/** `names:filter(f)`, `text:trim()` — the methods arrays and strings have.
|
|
6902
|
+
* They are written in the prelude as `ArrayMethods<T>` and
|
|
6903
|
+
* `StringMethods`, so a file (or a type library) that declares one of
|
|
6904
|
+
* those names again replaces the whole set, and nothing here is a special
|
|
6905
|
+
* case in the analyzer. The build lowers each call to a plain function. */
|
|
6906
|
+
builtInMethod(t, name) {
|
|
6907
|
+
const element = t.kind === "array" ? t.element : t.kind === "tuple" ? union(t.elements) : void 0;
|
|
6908
|
+
const methodTable = element !== void 0 ? "ArrayMethods" : t.kind === "primitive" && t.name === "string" || t.kind === "literal" && t.base === "string" ? "StringMethods" : void 0;
|
|
6909
|
+
const def = methodTable === void 0 ? void 0 : this.aliasDefs.get(methodTable);
|
|
6910
|
+
if (!def || def.class) return void 0;
|
|
6911
|
+
const table = this.expand(this.instantiateAlias(def, element !== void 0 ? [element] : []));
|
|
6912
|
+
const parts = table.kind === "intersection" ? table.types.map((m) => this.expand(m)) : [table];
|
|
6913
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
6914
|
+
const part = parts[i];
|
|
6915
|
+
const property = part.kind === "object" ? part.properties.get(name) : void 0;
|
|
6916
|
+
if (property) return property.type;
|
|
6917
|
+
}
|
|
6918
|
+
return void 0;
|
|
6919
|
+
}
|
|
5607
6920
|
propertyType(raw, name) {
|
|
5608
|
-
const t = this.expand(raw);
|
|
6921
|
+
const t = this.deferredAccess(this.expand(raw));
|
|
5609
6922
|
if (t.kind === "object") {
|
|
5610
6923
|
const p = t.properties.get(name);
|
|
5611
6924
|
if (p) return p.optional ? optional(p.type) : p.type;
|
|
5612
6925
|
if (t.indexer) return t.indexer.value;
|
|
5613
6926
|
}
|
|
6927
|
+
const built = this.builtInMethod(t, name);
|
|
6928
|
+
if (built) return built;
|
|
5614
6929
|
if (t.kind === "union") return union(t.types.map((m) => this.propertyType(m, name)));
|
|
5615
6930
|
if (t.kind === "intersection") {
|
|
5616
6931
|
const parts = t.types.map((m) => this.propertyType(m, name)).filter((p) => p.kind !== "unknown");
|
|
@@ -5628,21 +6943,37 @@ var TypeAnalyzer = class {
|
|
|
5628
6943
|
const t = this.expand(raw);
|
|
5629
6944
|
if (t.kind === "any") return anyType;
|
|
5630
6945
|
if (t.kind === "union") return union(t.types.map((m) => this.indexedType(m, idx)));
|
|
5631
|
-
|
|
5632
|
-
if (
|
|
6946
|
+
const index = this.expand(idx);
|
|
6947
|
+
if (index.kind === "union") return union(index.types.map((m) => this.indexedType(t, m)));
|
|
6948
|
+
if (t.kind === "difference") return this.indexedType(t.base, index);
|
|
6949
|
+
if (t.kind === "typeParam" && t.constraint) return this.indexedType(t.constraint, index);
|
|
5633
6950
|
if (t.kind === "array") return t.element;
|
|
5634
6951
|
if (t.kind === "tuple") {
|
|
5635
|
-
if (
|
|
5636
|
-
return t.elements[
|
|
6952
|
+
if (index.kind === "literal" && typeof index.value === "number") {
|
|
6953
|
+
return t.elements[index.value - 1] ?? nilType;
|
|
5637
6954
|
}
|
|
5638
6955
|
return union(t.elements);
|
|
5639
6956
|
}
|
|
5640
6957
|
if (t.kind === "object") {
|
|
5641
|
-
if (
|
|
6958
|
+
if (index.kind === "literal" && typeof index.value === "string") {
|
|
6959
|
+
const property = t.properties.get(index.value);
|
|
6960
|
+
if (property) return property.optional ? optional(property.type) : property.type;
|
|
6961
|
+
if (t.indexer && isAssignable(index, t.indexer.key)) return t.indexer.value;
|
|
6962
|
+
return nilType;
|
|
6963
|
+
}
|
|
6964
|
+
if (containsTypeParam(index)) return this.reduceType({ kind: "indexedAccess", objectType: t, indexType: index });
|
|
5642
6965
|
if (t.indexer) return t.indexer.value;
|
|
5643
6966
|
}
|
|
5644
6967
|
return unknownType;
|
|
5645
6968
|
}
|
|
6969
|
+
/** What a deferred `T[K]` can be: every property its index could name.
|
|
6970
|
+
* Reading a member of one, or calling it, sees that. */
|
|
6971
|
+
deferredAccess(t) {
|
|
6972
|
+
if (t.kind !== "indexedAccess") return t;
|
|
6973
|
+
const index = t.indexType.kind === "typeParam" && t.indexType.constraint ? t.indexType.constraint : t.indexType;
|
|
6974
|
+
if (containsTypeParam(index)) return unknownType;
|
|
6975
|
+
return this.accessType(t.objectType, index);
|
|
6976
|
+
}
|
|
5646
6977
|
elementType(raw, index) {
|
|
5647
6978
|
const t = this.expand(raw);
|
|
5648
6979
|
if (t.kind === "array") return t.element;
|
|
@@ -5675,7 +7006,11 @@ var TypeAnalyzer = class {
|
|
|
5675
7006
|
for (const part of expr.parts) if (part.kind === "expression") this.infer(part.expression, env);
|
|
5676
7007
|
return stringType;
|
|
5677
7008
|
}
|
|
7009
|
+
// `...` holds what the function declared it takes.
|
|
5678
7010
|
case "VarargExpression":
|
|
7011
|
+
return this.varargs[this.varargs.length - 1] ?? anyType;
|
|
7012
|
+
// Broken syntax is reported by the parser; nothing more to say.
|
|
7013
|
+
case "ErrorExpression":
|
|
5679
7014
|
return anyType;
|
|
5680
7015
|
case "Identifier": {
|
|
5681
7016
|
const id = this.bindingIdOf(expr);
|
|
@@ -5703,12 +7038,20 @@ var TypeAnalyzer = class {
|
|
|
5703
7038
|
case "SatisfiesExpression": {
|
|
5704
7039
|
const declared = this.resolveType(expr.typeAnnotation);
|
|
5705
7040
|
this.applyContext(expr.expression, declared);
|
|
5706
|
-
|
|
5707
|
-
|
|
7041
|
+
if (declared.kind === "any") return this.infer(expr.expression, env);
|
|
7042
|
+
const written = unwrapParens(expr.expression);
|
|
7043
|
+
const fresh = written.type === "TableExpression" || written.type === "ArrayExpression";
|
|
7044
|
+
const narrow = fresh ? this.inferAsConst(expr.expression, env) : this.infer(expr.expression, env);
|
|
7045
|
+
const actual = fresh ? this.keepContextualLiterals(narrow, declared) : narrow;
|
|
7046
|
+
this.typeOf.set(expr.expression, actual);
|
|
7047
|
+
if (!this.emitDiagnostics) return actual;
|
|
7048
|
+
if (!isAssignable(narrow, declared) && !isAssignable(actual, declared)) {
|
|
5708
7049
|
this.diagnostics.push({
|
|
5709
7050
|
node: expr,
|
|
5710
|
-
message: `Type '${formatType(actual)}' does not satisfy '${formatType(declared)}'`
|
|
7051
|
+
message: `Type '${formatType(actual)}' does not satisfy the expected type '${formatType(declared)}'`
|
|
5711
7052
|
});
|
|
7053
|
+
} else {
|
|
7054
|
+
this.reportExcessProperties(expr.expression, declared);
|
|
5712
7055
|
}
|
|
5713
7056
|
return actual;
|
|
5714
7057
|
}
|
|
@@ -5742,6 +7085,10 @@ var TypeAnalyzer = class {
|
|
|
5742
7085
|
}
|
|
5743
7086
|
const l = this.infer(expr.left, env);
|
|
5744
7087
|
const r = this.infer(expr.right, env);
|
|
7088
|
+
if (op === "==" || op === "~=") {
|
|
7089
|
+
if (unwrapParens(expr.right).type === "StringLiteral") this.expectedTypeOf.set(unwrapParens(expr.right), l);
|
|
7090
|
+
if (unwrapParens(expr.left).type === "StringLiteral") this.expectedTypeOf.set(unwrapParens(expr.left), r);
|
|
7091
|
+
}
|
|
5745
7092
|
switch (op) {
|
|
5746
7093
|
case "..":
|
|
5747
7094
|
return this.operatorResult(expr, op, l, r) ?? stringType;
|
|
@@ -5764,57 +7111,25 @@ var TypeAnalyzer = class {
|
|
|
5764
7111
|
return union([l, r]);
|
|
5765
7112
|
}
|
|
5766
7113
|
case "MemberExpression": {
|
|
5767
|
-
const obj = this.
|
|
7114
|
+
const { type: obj, shortCircuits } = this.chainObject(expr, expr.object, env);
|
|
5768
7115
|
const key = this.refKeyOf(expr);
|
|
5769
7116
|
const narrowed = key === void 0 ? void 0 : env.get(key);
|
|
5770
|
-
return narrowed ?? this.propertyType(obj, expr.property.name);
|
|
7117
|
+
return this.chainResult(expr, narrowed ?? this.propertyType(obj, expr.property.name), shortCircuits);
|
|
5771
7118
|
}
|
|
5772
7119
|
case "IndexExpression": {
|
|
5773
|
-
const obj = this.
|
|
7120
|
+
const { type: obj, shortCircuits } = this.chainObject(expr, expr.object, env);
|
|
5774
7121
|
const idx = this.infer(expr.index, env);
|
|
5775
7122
|
const key = this.refKeyOf(expr);
|
|
5776
7123
|
const narrowed = key === void 0 ? void 0 : env.get(key);
|
|
5777
|
-
return narrowed ?? this.indexedType(obj, idx);
|
|
7124
|
+
return this.chainResult(expr, narrowed ?? this.indexedType(obj, idx), shortCircuits);
|
|
5778
7125
|
}
|
|
5779
7126
|
case "CallExpression": {
|
|
5780
|
-
const callee = this.
|
|
5781
|
-
|
|
5782
|
-
const expected = this.expectedArguments(expr.arguments, fns, () => 0);
|
|
5783
|
-
expr.arguments.forEach((a, i) => this.applyContext(a, expected[i]));
|
|
5784
|
-
const argTypes = expr.arguments.map((a) => this.infer(a, env));
|
|
5785
|
-
if (fns.length) {
|
|
5786
|
-
this.recordExpected(expr.arguments, fns, () => 0);
|
|
5787
|
-
const arityFits = this.checkArity(expr, fns, argTypes.length, 0);
|
|
5788
|
-
const picked = this.pickOverload(fns, argTypes);
|
|
5789
|
-
if (picked) {
|
|
5790
|
-
return this.callReturn(picked, this.constArgs(picked, expr.arguments, argTypes, env));
|
|
5791
|
-
}
|
|
5792
|
-
if (arityFits) this.reportArguments(expr, expr.arguments, fns, () => argTypes, () => 0);
|
|
5793
|
-
return union(fns.map((f) => this.callReturn(f, argTypes)));
|
|
5794
|
-
}
|
|
5795
|
-
return callee.kind === "any" ? anyType : unknownType;
|
|
7127
|
+
const { type: callee, shortCircuits } = this.chainObject(expr, expr.callee, env);
|
|
7128
|
+
return this.chainResult(expr, this.inferCall(expr, callee, env), shortCircuits);
|
|
5796
7129
|
}
|
|
5797
7130
|
case "MethodCallExpression": {
|
|
5798
|
-
const objType = this.
|
|
5799
|
-
|
|
5800
|
-
const expected = this.expectedArguments(expr.arguments, fns, (f) => this.takesSelf(f) ? 1 : 0);
|
|
5801
|
-
expr.arguments.forEach((a, i) => this.applyContext(a, expected[i]));
|
|
5802
|
-
const argTypes = expr.arguments.map((a) => this.infer(a, env));
|
|
5803
|
-
if (fns.length) {
|
|
5804
|
-
const withSelf = (f) => this.takesSelf(f) ? [objType, ...argTypes] : argTypes;
|
|
5805
|
-
const selfOf = (f) => this.takesSelf(f) ? 1 : 0;
|
|
5806
|
-
this.recordExpected(expr.arguments, fns, selfOf);
|
|
5807
|
-
const arityFits = this.checkArity(expr, fns, argTypes.length, this.takesSelf(fns[0]) ? 1 : 0);
|
|
5808
|
-
const picked = this.pickOverload(fns, argTypes, withSelf);
|
|
5809
|
-
if (picked) {
|
|
5810
|
-
const self = this.takesSelf(picked) ? 1 : 0;
|
|
5811
|
-
const written = this.constArgs(picked, expr.arguments, argTypes, env, self);
|
|
5812
|
-
return this.callReturn(picked, this.takesSelf(picked) ? [objType, ...written] : written);
|
|
5813
|
-
}
|
|
5814
|
-
if (arityFits) this.reportArguments(expr, expr.arguments, fns, withSelf, selfOf);
|
|
5815
|
-
return union(fns.map((f) => this.callReturn(f, withSelf(f))));
|
|
5816
|
-
}
|
|
5817
|
-
return objType.kind === "any" ? anyType : unknownType;
|
|
7131
|
+
const { type: objType, shortCircuits } = this.chainObject(expr, expr.object, env);
|
|
7132
|
+
return this.chainResult(expr, this.inferMethodCall(expr, objType, env), shortCircuits);
|
|
5818
7133
|
}
|
|
5819
7134
|
case "IfElseExpression": {
|
|
5820
7135
|
const branches = [];
|
|
@@ -5830,6 +7145,123 @@ var TypeAnalyzer = class {
|
|
|
5830
7145
|
}
|
|
5831
7146
|
}
|
|
5832
7147
|
}
|
|
7148
|
+
inferCall(expr, callee, env) {
|
|
7149
|
+
const fns = this.overloadsOf(callee);
|
|
7150
|
+
const explicit = this.explicitTypeArguments(expr, fns);
|
|
7151
|
+
const expected = this.expectedArguments(expr.arguments, fns, () => 0);
|
|
7152
|
+
expr.arguments.forEach((a, i) => this.applyContext(a, expected[i]));
|
|
7153
|
+
const argTypes = expr.arguments.map((a) => this.infer(a, env));
|
|
7154
|
+
if (fns.length) {
|
|
7155
|
+
this.recordExpected(expr.arguments, fns, () => 0);
|
|
7156
|
+
const arityFits = this.checkArity(expr, fns, argTypes.length, 0);
|
|
7157
|
+
const picked = this.pickOverload(fns, argTypes);
|
|
7158
|
+
const distributed = this.distributedReturn(fns, argTypes, picked, (_, args) => args);
|
|
7159
|
+
if (distributed) return distributed;
|
|
7160
|
+
if (picked) {
|
|
7161
|
+
this.checkInferredArguments(expr, expr.arguments, picked, argTypes, 0);
|
|
7162
|
+
return this.callReturn(picked, this.constArgs(picked, expr.arguments, argTypes, env), explicit);
|
|
7163
|
+
}
|
|
7164
|
+
if (arityFits) this.reportArguments(expr, expr.arguments, fns, () => argTypes, () => 0);
|
|
7165
|
+
return union(fns.map((f) => this.callReturn(f, argTypes, explicit)));
|
|
7166
|
+
}
|
|
7167
|
+
return callee.kind === "any" ? anyType : unknownType;
|
|
7168
|
+
}
|
|
7169
|
+
inferMethodCall(expr, objType, env) {
|
|
7170
|
+
const fns = this.overloadsOf(this.propertyType(objType, expr.method.name));
|
|
7171
|
+
const explicit = this.explicitTypeArguments(expr, fns);
|
|
7172
|
+
const expected = this.expectedArguments(expr.arguments, fns, (f) => this.takesSelf(f) ? 1 : 0);
|
|
7173
|
+
expr.arguments.forEach((a, i) => this.applyContext(a, expected[i]));
|
|
7174
|
+
const argTypes = expr.arguments.map((a) => this.infer(a, env));
|
|
7175
|
+
if (fns.length) {
|
|
7176
|
+
const withSelf = (f) => this.takesSelf(f) ? [objType, ...argTypes] : argTypes;
|
|
7177
|
+
const selfOf = (f) => this.takesSelf(f) ? 1 : 0;
|
|
7178
|
+
this.recordExpected(expr.arguments, fns, selfOf);
|
|
7179
|
+
const arityFits = this.checkArity(expr, fns, argTypes.length, this.takesSelf(fns[0]) ? 1 : 0);
|
|
7180
|
+
const picked = this.pickOverload(fns, argTypes, withSelf);
|
|
7181
|
+
const distributed = this.distributedReturn(
|
|
7182
|
+
fns,
|
|
7183
|
+
argTypes,
|
|
7184
|
+
picked,
|
|
7185
|
+
(f, args) => this.takesSelf(f) ? [objType, ...args] : args
|
|
7186
|
+
);
|
|
7187
|
+
if (distributed) return distributed;
|
|
7188
|
+
if (picked) {
|
|
7189
|
+
const self = this.takesSelf(picked) ? 1 : 0;
|
|
7190
|
+
this.checkInferredArguments(expr, expr.arguments, picked, withSelf(picked), self);
|
|
7191
|
+
const written = this.constArgs(picked, expr.arguments, argTypes, env, self);
|
|
7192
|
+
return this.callReturn(picked, this.takesSelf(picked) ? [objType, ...written] : written, explicit);
|
|
7193
|
+
}
|
|
7194
|
+
if (arityFits) this.reportArguments(expr, expr.arguments, fns, withSelf, selfOf);
|
|
7195
|
+
return union(fns.map((f) => this.callReturn(f, withSelf(f), explicit)));
|
|
7196
|
+
}
|
|
7197
|
+
return objType.kind === "any" ? anyType : unknownType;
|
|
7198
|
+
}
|
|
7199
|
+
// --------------------------------------------------------
|
|
7200
|
+
// Optional chains
|
|
7201
|
+
// --------------------------------------------------------
|
|
7202
|
+
//
|
|
7203
|
+
// `a?.b.c`: when `a` is nil the whole chain is nil and `.c` never runs.
|
|
7204
|
+
// So a link reads its object without the `nil` a `?.` earlier in the chain
|
|
7205
|
+
// added — that nil has already left the chain — and the chain's outermost
|
|
7206
|
+
// link carries it again. Parentheses end a chain: `(a?.b).c` reads `.c`
|
|
7207
|
+
// from `B | nil`.
|
|
7208
|
+
/** The type of a link's non-nil object, for each link that is past a `?.`:
|
|
7209
|
+
* what the chain holds when it has not short-circuited. */
|
|
7210
|
+
chainValue = /* @__PURE__ */ new WeakMap();
|
|
7211
|
+
/** The object a link reads from, and whether the chain can short-circuit
|
|
7212
|
+
* by this link. */
|
|
7213
|
+
chainObject(link, object, env) {
|
|
7214
|
+
const full = this.infer(object, env);
|
|
7215
|
+
const inChain = this.chainValue.get(object);
|
|
7216
|
+
let type = inChain ?? full;
|
|
7217
|
+
if (link.optional) {
|
|
7218
|
+
type = withoutNil(type);
|
|
7219
|
+
} else if (this.includesNil(type)) {
|
|
7220
|
+
this.reportNilAccess(object, type);
|
|
7221
|
+
type = withoutNil(this.expand(type));
|
|
7222
|
+
}
|
|
7223
|
+
return { type, shortCircuits: inChain !== void 0 || link.optional === true };
|
|
7224
|
+
}
|
|
7225
|
+
/** Objects already reported as possibly nil: a loop body is visited more
|
|
7226
|
+
* than once. */
|
|
7227
|
+
nilAccessReported = /* @__PURE__ */ new WeakSet();
|
|
7228
|
+
includesNil(raw) {
|
|
7229
|
+
const t = this.expand(raw);
|
|
7230
|
+
if (t.kind === "primitive") return t.name === "nil";
|
|
7231
|
+
return t.kind === "union" && t.types.some((m) => m.kind === "primitive" && m.name === "nil");
|
|
7232
|
+
}
|
|
7233
|
+
reportNilAccess(object, type) {
|
|
7234
|
+
if (!this.emitDiagnostics || this.nilAccessReported.has(object)) return;
|
|
7235
|
+
this.nilAccessReported.add(object);
|
|
7236
|
+
const label = expressionLabel(object);
|
|
7237
|
+
const t = this.expand(type);
|
|
7238
|
+
const nilOnly = t.kind === "primitive" && t.name === "nil";
|
|
7239
|
+
const subject = label === void 0 ? "Object" : `'${label}'`;
|
|
7240
|
+
this.diagnostics.push({
|
|
7241
|
+
node: object,
|
|
7242
|
+
message: nilOnly ? `${subject} is nil` : `${subject} is possibly nil. Check it first, or use '?.' / '?:'`
|
|
7243
|
+
});
|
|
7244
|
+
}
|
|
7245
|
+
chainResult(link, value, shortCircuits) {
|
|
7246
|
+
if (!shortCircuits) return value;
|
|
7247
|
+
this.chainValue.set(link, value);
|
|
7248
|
+
return union([value, nilType]);
|
|
7249
|
+
}
|
|
7250
|
+
/** The chain around `cond` did not short-circuit — it produced a truthy
|
|
7251
|
+
* value, or any value but nil — so every object a `?.` in it tested is not
|
|
7252
|
+
* nil in `env`. */
|
|
7253
|
+
narrowOptionalLinks(cond, env, into) {
|
|
7254
|
+
for (let e = cond; ; ) {
|
|
7255
|
+
const link = e;
|
|
7256
|
+
const object = e.type === "CallExpression" ? e.callee : e.type === "MemberExpression" || e.type === "IndexExpression" || e.type === "MethodCallExpression" ? e.object : void 0;
|
|
7257
|
+
if (!object) return;
|
|
7258
|
+
if (link.optional) {
|
|
7259
|
+
const key = this.refKeyOf(object);
|
|
7260
|
+
if (key !== void 0) this.setRef(into, key, withoutNil(this.typeAtRef(object, into)));
|
|
7261
|
+
}
|
|
7262
|
+
e = object;
|
|
7263
|
+
}
|
|
7264
|
+
}
|
|
5833
7265
|
inferArray(expr, env, asConst) {
|
|
5834
7266
|
const contextual = this.contextualArrays.get(expr);
|
|
5835
7267
|
if (contextual && !asConst) return contextual;
|
|
@@ -5847,7 +7279,21 @@ var TypeAnalyzer = class {
|
|
|
5847
7279
|
}
|
|
5848
7280
|
}
|
|
5849
7281
|
if (asConst && !hadSpread) return tuple(elems);
|
|
5850
|
-
return arrayOf(elems.length ? union(elems.map((t) =>
|
|
7282
|
+
return arrayOf(elems.length ? union(elems.map((t, i) => {
|
|
7283
|
+
const element = expr.elements[i];
|
|
7284
|
+
return asConst || !element || element.type === "SpreadElement" ? t : this.widenUnlessAsked(t, element);
|
|
7285
|
+
})) : unknownType);
|
|
7286
|
+
}
|
|
7287
|
+
/** A literal written inside a fresh table or array widens — `{ n = 1 }` is
|
|
7288
|
+
* `{ n: number }` — unless the surroundings said a literal belongs there.
|
|
7289
|
+
* `request({ Method: "GET" })` keeps `"GET"` when `Method` is a union of
|
|
7290
|
+
* string literals, exactly as TypeScript's contextual typing does, and
|
|
7291
|
+
* goes on widening to `string` when the parameter only says `string`.
|
|
7292
|
+
* The context was recorded by `applyContext` before the value was
|
|
7293
|
+
* inferred, so this is a lookup rather than a second pass. */
|
|
7294
|
+
widenUnlessAsked(value, at) {
|
|
7295
|
+
const wanted = this.expectedTypeOf.get(at);
|
|
7296
|
+
return wanted === void 0 ? widen(value) : this.keepContextualLiterals(value, wanted);
|
|
5851
7297
|
}
|
|
5852
7298
|
inferObject(expr, env, asConst) {
|
|
5853
7299
|
const entries = [];
|
|
@@ -5855,16 +7301,24 @@ var TypeAnalyzer = class {
|
|
|
5855
7301
|
for (const field of expr.fields) {
|
|
5856
7302
|
if (field.type === "TableFieldNamed") {
|
|
5857
7303
|
const key = field.key.type === "Identifier" ? field.key.name : field.key.value;
|
|
5858
|
-
const v = asConst ? this.inferAsConst(field.value, env) :
|
|
7304
|
+
const v = asConst ? this.inferAsConst(field.value, env) : this.widenUnlessAsked(this.infer(field.value, env), field.value);
|
|
5859
7305
|
entries.push([key, { type: v, optional: false, readonly: asConst }]);
|
|
5860
7306
|
} else if (field.type === "TableFieldShorthand") {
|
|
5861
7307
|
const v = this.infer(field.name, env);
|
|
5862
|
-
entries.push([field.name.name, {
|
|
7308
|
+
entries.push([field.name.name, {
|
|
7309
|
+
type: asConst ? v : this.widenUnlessAsked(v, field.name),
|
|
7310
|
+
optional: false,
|
|
7311
|
+
readonly: asConst
|
|
7312
|
+
}]);
|
|
5863
7313
|
} else if (field.type === "TableFieldComputed") {
|
|
5864
7314
|
const k = this.infer(field.key, env);
|
|
5865
7315
|
const v = this.infer(field.value, env);
|
|
5866
7316
|
if (k.kind === "literal" && typeof k.value === "string") {
|
|
5867
|
-
entries.push([k.value, {
|
|
7317
|
+
entries.push([k.value, {
|
|
7318
|
+
type: asConst ? v : this.widenUnlessAsked(v, field.value),
|
|
7319
|
+
optional: false,
|
|
7320
|
+
readonly: asConst
|
|
7321
|
+
}]);
|
|
5868
7322
|
} else {
|
|
5869
7323
|
indexer = mergeIndexer(indexer, { key: widen(k), value: asConst ? v : widen(v) });
|
|
5870
7324
|
}
|
|
@@ -5878,6 +7332,123 @@ var TypeAnalyzer = class {
|
|
|
5878
7332
|
}
|
|
5879
7333
|
return objectType(entries, indexer, asConst || void 0);
|
|
5880
7334
|
}
|
|
7335
|
+
/** A value inferred `as const`, widened back wherever `context` does not
|
|
7336
|
+
* ask for a literal: `satisfies`' result type. A property keeps `"circle"`
|
|
7337
|
+
* when the contract's property admits string literals, and becomes
|
|
7338
|
+
* `string` when it is only `string`; a tuple becomes an array unless the
|
|
7339
|
+
* contract is a tuple; nothing stays readonly. */
|
|
7340
|
+
keepContextualLiterals(value, context) {
|
|
7341
|
+
const ctx = context === void 0 ? void 0 : this.expand(context);
|
|
7342
|
+
switch (value.kind) {
|
|
7343
|
+
case "literal":
|
|
7344
|
+
return ctx && this.admitsLiteral(ctx, value.base) ? value : widen(value);
|
|
7345
|
+
case "object": {
|
|
7346
|
+
if (value.class) return value;
|
|
7347
|
+
const entries = [...value.properties].map(([name, property]) => [
|
|
7348
|
+
name,
|
|
7349
|
+
{ ...property, readonly: false, type: this.keepContextualLiterals(property.type, ctx && this.contextProperty(ctx, name)) }
|
|
7350
|
+
]);
|
|
7351
|
+
const indexer = value.indexer && {
|
|
7352
|
+
key: widen(value.indexer.key),
|
|
7353
|
+
value: this.keepContextualLiterals(value.indexer.value, ctx && this.contextIndexValue(ctx))
|
|
7354
|
+
};
|
|
7355
|
+
return objectType(entries, indexer);
|
|
7356
|
+
}
|
|
7357
|
+
case "tuple": {
|
|
7358
|
+
const tupleContext = ctx && this.membersOf(ctx).find((m) => m.kind === "tuple");
|
|
7359
|
+
if (tupleContext?.kind === "tuple") {
|
|
7360
|
+
return tuple(value.elements.map((e, i) => this.keepContextualLiterals(e, tupleContext.elements[i])), value.isPack);
|
|
7361
|
+
}
|
|
7362
|
+
const arrayContext = ctx && this.membersOf(ctx).find((m) => m.kind === "array");
|
|
7363
|
+
const element = arrayContext?.kind === "array" ? arrayContext.element : void 0;
|
|
7364
|
+
if (!value.elements.length) return arrayContext ?? arrayOf(unknownType);
|
|
7365
|
+
return arrayOf(union(value.elements.map((e) => this.keepContextualLiterals(e, element))));
|
|
7366
|
+
}
|
|
7367
|
+
case "array": {
|
|
7368
|
+
const arrayContext = ctx && this.membersOf(ctx).find((m) => m.kind === "array");
|
|
7369
|
+
return arrayOf(this.keepContextualLiterals(value.element, arrayContext?.kind === "array" ? arrayContext.element : void 0));
|
|
7370
|
+
}
|
|
7371
|
+
case "union":
|
|
7372
|
+
return union(value.types.map((t) => this.keepContextualLiterals(t, context)));
|
|
7373
|
+
default:
|
|
7374
|
+
return value;
|
|
7375
|
+
}
|
|
7376
|
+
}
|
|
7377
|
+
membersOf(t) {
|
|
7378
|
+
const x = this.expand(t);
|
|
7379
|
+
return x.kind === "union" ? x.types.map((m) => this.expand(m)) : [x];
|
|
7380
|
+
}
|
|
7381
|
+
/** Does a contract accept literals of `base` as such? */
|
|
7382
|
+
admitsLiteral(ctx, base) {
|
|
7383
|
+
return this.membersOf(ctx).some((m) => m.kind === "literal" && m.base === base || m.kind === "templateLiteral" && base === "string");
|
|
7384
|
+
}
|
|
7385
|
+
/** What a contract expects of property `name`, over every object it allows. */
|
|
7386
|
+
contextProperty(ctx, name) {
|
|
7387
|
+
const found = [];
|
|
7388
|
+
for (const m of this.membersOf(ctx)) {
|
|
7389
|
+
if (m.kind !== "object") continue;
|
|
7390
|
+
const property = m.properties.get(name);
|
|
7391
|
+
if (property) found.push(property.type);
|
|
7392
|
+
else if (m.indexer) found.push(m.indexer.value);
|
|
7393
|
+
}
|
|
7394
|
+
return found.length ? union(found) : void 0;
|
|
7395
|
+
}
|
|
7396
|
+
contextIndexValue(ctx) {
|
|
7397
|
+
const found = this.membersOf(ctx).flatMap((m) => m.kind === "object" && m.indexer ? [m.indexer.value] : []);
|
|
7398
|
+
return found.length ? union(found) : void 0;
|
|
7399
|
+
}
|
|
7400
|
+
/** Fields reported by `reportExcessProperties`, once each: a loop body is
|
|
7401
|
+
* visited more than once. */
|
|
7402
|
+
excessReported = /* @__PURE__ */ new WeakSet();
|
|
7403
|
+
/** TypeScript's excess property check. An object literal written straight
|
|
7404
|
+
* into a typed place — an annotation, `satisfies` — may only name
|
|
7405
|
+
* properties that place knows: anything else is almost always a typo.
|
|
7406
|
+
* A nested literal is checked against the property it is written for.
|
|
7407
|
+
* A target with an indexer, a class, or a member whose shape is not known
|
|
7408
|
+
* accepts anything. */
|
|
7409
|
+
/** The keys an index signature covers, when it covers a countable set of
|
|
7410
|
+
* them: `[("a" | "b")]` yes, `[string]` no. */
|
|
7411
|
+
finiteKeys(key) {
|
|
7412
|
+
const t = this.expand(key);
|
|
7413
|
+
const parts = t.kind === "union" ? t.types : [t];
|
|
7414
|
+
const out = /* @__PURE__ */ new Set();
|
|
7415
|
+
for (const part of parts.map((m) => this.expand(m))) {
|
|
7416
|
+
if (part.kind !== "literal" || typeof part.value === "boolean") return void 0;
|
|
7417
|
+
out.add(String(part.value));
|
|
7418
|
+
}
|
|
7419
|
+
return out.size ? out : void 0;
|
|
7420
|
+
}
|
|
7421
|
+
reportExcessProperties(expression, target) {
|
|
7422
|
+
let literal2 = unwrapParens(expression);
|
|
7423
|
+
while (literal2.type === "AsConstExpression") literal2 = unwrapParens(literal2.expression);
|
|
7424
|
+
if (literal2.type !== "TableExpression" || !this.emitDiagnostics) return;
|
|
7425
|
+
const members = this.membersOf(target);
|
|
7426
|
+
const shapes = members.filter((m) => m.kind === "object");
|
|
7427
|
+
if (!shapes.length || shapes.some((o) => o.class)) return;
|
|
7428
|
+
const keySets = shapes.map((o) => o.indexer && this.finiteKeys(o.indexer.key));
|
|
7429
|
+
if (shapes.some((o, i) => o.indexer && !keySets[i])) return;
|
|
7430
|
+
if (members.some((m) => m.kind === "any" || m.kind === "unknown" || m.kind === "typeParam" || m.kind === "intersection")) return;
|
|
7431
|
+
for (const field of literal2.fields) {
|
|
7432
|
+
if (field.type !== "TableFieldNamed" && field.type !== "TableFieldShorthand") continue;
|
|
7433
|
+
const key = field.type === "TableFieldNamed" ? field.key : field.name;
|
|
7434
|
+
const name = key.type === "Identifier" ? key.name : key.value;
|
|
7435
|
+
const expected = shapes.flatMap((o, i) => {
|
|
7436
|
+
const property = o.properties.get(name);
|
|
7437
|
+
if (property) return [property.type];
|
|
7438
|
+
return o.indexer && keySets[i].has(name) ? [o.indexer.value] : [];
|
|
7439
|
+
});
|
|
7440
|
+
if (!expected.length) {
|
|
7441
|
+
if (this.excessReported.has(key)) continue;
|
|
7442
|
+
this.excessReported.add(key);
|
|
7443
|
+
this.diagnostics.push({
|
|
7444
|
+
node: key,
|
|
7445
|
+
message: `Object literal may only specify known properties, and '${name}' does not exist in type '${formatType(target)}'`
|
|
7446
|
+
});
|
|
7447
|
+
continue;
|
|
7448
|
+
}
|
|
7449
|
+
if (field.type === "TableFieldNamed") this.reportExcessProperties(field.value, union(expected));
|
|
7450
|
+
}
|
|
7451
|
+
}
|
|
5881
7452
|
inferAsConst(expr, env) {
|
|
5882
7453
|
switch (expr.type) {
|
|
5883
7454
|
case "ArrayExpression":
|
|
@@ -5935,12 +7506,13 @@ var TypeAnalyzer = class {
|
|
|
5935
7506
|
}
|
|
5936
7507
|
if (cond.type === "CallExpression" || cond.type === "MethodCallExpression") {
|
|
5937
7508
|
this.narrowByPredicateCall(cond, env, t, f);
|
|
5938
|
-
|
|
7509
|
+
} else {
|
|
7510
|
+
this.narrowRef(cond, env, t, f, (cur) => ({
|
|
7511
|
+
yes: narrowTruthy(cur),
|
|
7512
|
+
no: narrowFalsy(cur)
|
|
7513
|
+
}));
|
|
5939
7514
|
}
|
|
5940
|
-
this.
|
|
5941
|
-
yes: narrowTruthy(cur),
|
|
5942
|
-
no: narrowFalsy(cur)
|
|
5943
|
-
}));
|
|
7515
|
+
this.narrowOptionalLinks(cond, env, t);
|
|
5944
7516
|
}
|
|
5945
7517
|
/** `a == b` / `a ~= b`. Handles, in order: a declaration-driven
|
|
5946
7518
|
* `typeof(x) == "..."` test, a literal/`nil` comparison against a
|
|
@@ -5957,11 +7529,14 @@ var TypeAnalyzer = class {
|
|
|
5957
7529
|
};
|
|
5958
7530
|
for (const [ref, other] of [[left, right], [right, left]]) {
|
|
5959
7531
|
const value = litOf(other);
|
|
5960
|
-
if (value === void 0
|
|
5961
|
-
this.
|
|
5962
|
-
yes
|
|
5963
|
-
|
|
5964
|
-
|
|
7532
|
+
if (value === void 0) continue;
|
|
7533
|
+
if (this.refKeyOf(ref) !== void 0) {
|
|
7534
|
+
this.narrowRef(ref, env, yes, no, (cur) => ({
|
|
7535
|
+
yes: narrowTo(cur, value),
|
|
7536
|
+
no: narrowExclude(cur, value)
|
|
7537
|
+
}));
|
|
7538
|
+
}
|
|
7539
|
+
this.narrowOptionalLinks(ref, env, value.kind === "primitive" && value.name === "nil" ? no : yes);
|
|
5965
7540
|
return;
|
|
5966
7541
|
}
|
|
5967
7542
|
if (this.refKeyOf(left) !== void 0 && this.refKeyOf(right) !== void 0) {
|
|
@@ -6016,11 +7591,16 @@ var TypeAnalyzer = class {
|
|
|
6016
7591
|
predicateCallTarget(cond, env) {
|
|
6017
7592
|
let callee;
|
|
6018
7593
|
let args;
|
|
7594
|
+
let selfType;
|
|
6019
7595
|
if (cond.type === "CallExpression") {
|
|
6020
|
-
callee = this.typeOf.get(cond.callee) ?? this.typeAtRef(cond.callee, env);
|
|
7596
|
+
callee = this.chainValue.get(cond.callee) ?? this.typeOf.get(cond.callee) ?? this.typeAtRef(cond.callee, env);
|
|
6021
7597
|
args = cond.arguments;
|
|
6022
7598
|
} else if (cond.type === "MethodCallExpression") {
|
|
6023
|
-
|
|
7599
|
+
let objType = this.chainValue.get(cond.object) ?? this.typeOf.get(cond.object) ?? this.typeAtRef(cond.object, env);
|
|
7600
|
+
if (cond.optional) {
|
|
7601
|
+
objType = withoutNil(objType);
|
|
7602
|
+
selfType = objType;
|
|
7603
|
+
}
|
|
6024
7604
|
callee = this.propertyType(objType, cond.method.name);
|
|
6025
7605
|
const first = this.overloadsOf(callee)[0];
|
|
6026
7606
|
args = first && this.takesSelf(first) ? [cond.object, ...cond.arguments] : cond.arguments;
|
|
@@ -6028,7 +7608,7 @@ var TypeAnalyzer = class {
|
|
|
6028
7608
|
return void 0;
|
|
6029
7609
|
}
|
|
6030
7610
|
const overloads = this.overloadsOf(callee);
|
|
6031
|
-
const argTypes = args.map((a) => this.typeOf.get(a) ?? this.typeAtRef(a, env));
|
|
7611
|
+
const argTypes = args.map((a) => (selfType && cond.type === "MethodCallExpression" && a === cond.object ? selfType : void 0) ?? this.typeOf.get(a) ?? this.typeAtRef(a, env));
|
|
6032
7612
|
const picked = this.pickOverload(overloads, argTypes);
|
|
6033
7613
|
const candidates = picked ? [picked, ...overloads.filter((f) => f !== picked)] : overloads;
|
|
6034
7614
|
for (const f of candidates) {
|
|
@@ -6135,6 +7715,10 @@ var TypeAnalyzer = class {
|
|
|
6135
7715
|
const { yes, no } = refine(cur);
|
|
6136
7716
|
this.setRef(t, key, yes);
|
|
6137
7717
|
this.setRef(f, key, no);
|
|
7718
|
+
this.correlate(t, key, yes);
|
|
7719
|
+
this.correlate(f, key, no);
|
|
7720
|
+
this.propagateAliases(env, t, key, yes);
|
|
7721
|
+
this.propagateAliases(env, f, key, no);
|
|
6138
7722
|
const inner = expr.type === "ParenthesizedExpression" ? expr.expression : expr;
|
|
6139
7723
|
if (inner.type !== "MemberExpression" && inner.type !== "IndexExpression") return;
|
|
6140
7724
|
const parentKey = this.refKeyOf(inner.object);
|
|
@@ -6142,18 +7726,19 @@ var TypeAnalyzer = class {
|
|
|
6142
7726
|
const step = key.slice(parentKey.length);
|
|
6143
7727
|
if (!step.startsWith(".")) return;
|
|
6144
7728
|
const prop = step.slice(1);
|
|
7729
|
+
const optional2 = inner.type === "MemberExpression" && inner.optional === true;
|
|
6145
7730
|
this.narrowRef(inner.object, env, t, f, (parentType) => ({
|
|
6146
|
-
yes: this.filterByProperty(parentType, prop, yes),
|
|
6147
|
-
no: this.filterByProperty(parentType, prop, no)
|
|
7731
|
+
yes: this.filterByProperty(parentType, prop, yes, optional2),
|
|
7732
|
+
no: this.filterByProperty(parentType, prop, no, optional2)
|
|
6148
7733
|
}));
|
|
6149
7734
|
}
|
|
6150
7735
|
/** Keep the union members of `parent` whose `prop` can still hold `want`.
|
|
6151
7736
|
* Leaves a non-union (or a union nothing matches) alone: over-narrowing a
|
|
6152
7737
|
* plain object to `never` because of a property test would be worse than
|
|
6153
7738
|
* learning nothing. */
|
|
6154
|
-
filterByProperty(parent, prop, want) {
|
|
7739
|
+
filterByProperty(parent, prop, want, optional2 = false) {
|
|
6155
7740
|
if (parent.kind !== "union" || want.kind === "never") return parent;
|
|
6156
|
-
const kept = parent.types.filter((m) => overlaps(this.propertyType(m, prop), want));
|
|
7741
|
+
const kept = parent.types.filter((m) => m.kind === "primitive" && m.name === "nil" ? optional2 && overlaps(nilType, want) : overlaps(this.propertyType(m, prop), want));
|
|
6157
7742
|
return kept.length ? union(kept) : parent;
|
|
6158
7743
|
}
|
|
6159
7744
|
/** Record a narrowing. Deliberately does *not* discard what is known about
|
|
@@ -6165,6 +7750,15 @@ var TypeAnalyzer = class {
|
|
|
6165
7750
|
setRef(env, key, t) {
|
|
6166
7751
|
env.set(key, t);
|
|
6167
7752
|
}
|
|
7753
|
+
/** An assignment to a path (or to anything it hangs off) means the name
|
|
7754
|
+
* that copied it no longer holds that value: forget the alias. */
|
|
7755
|
+
unalias(key) {
|
|
7756
|
+
for (const k of [...this.refAliases.keys()]) {
|
|
7757
|
+
if (k !== key && !k.startsWith(`${key}.`) && !k.startsWith(`${key}#`)) continue;
|
|
7758
|
+
for (const other of this.refAliases.get(k) ?? []) this.refAliases.get(other)?.delete(k);
|
|
7759
|
+
this.refAliases.delete(k);
|
|
7760
|
+
}
|
|
7761
|
+
}
|
|
6168
7762
|
/** Drop every narrowing recorded for a path strictly under `key`. */
|
|
6169
7763
|
invalidateBelow(env, key) {
|
|
6170
7764
|
for (const k of [...env.keys()]) {
|
|
@@ -6177,6 +7771,7 @@ var TypeAnalyzer = class {
|
|
|
6177
7771
|
const key = this.refKeyOf(expr);
|
|
6178
7772
|
if (key === void 0) return;
|
|
6179
7773
|
this.invalidateBelow(env, key);
|
|
7774
|
+
this.unalias(key);
|
|
6180
7775
|
env.set(key, value);
|
|
6181
7776
|
}
|
|
6182
7777
|
// --------------------------------------------------------
|
|
@@ -6257,7 +7852,85 @@ var TypeAnalyzer = class {
|
|
|
6257
7852
|
/** The type a binding has *here*: its flow-narrowed type if the current
|
|
6258
7853
|
* environment has one, else its declared/inferred type. */
|
|
6259
7854
|
currentType(id, env) {
|
|
6260
|
-
return env.get(bindKey(id)) ?? this.bindingType.get(id) ?? anyType;
|
|
7855
|
+
return env.get(bindKey(id)) ?? this.bindingType.get(id) ?? this.declaredAhead(id) ?? anyType;
|
|
7856
|
+
}
|
|
7857
|
+
// --------------------------------------------------------
|
|
7858
|
+
// Hoisting
|
|
7859
|
+
// --------------------------------------------------------
|
|
7860
|
+
//
|
|
7861
|
+
// Scope analysis lets code see a function declared later in its block,
|
|
7862
|
+
// and a module's top-level names from function bodies and `typeof` written
|
|
7863
|
+
// above them. The walk has not reached those declarations yet when such a
|
|
7864
|
+
// reference is met, so their type is worked out from the declaration on
|
|
7865
|
+
// the spot — its annotation, or its body or initializer — as TypeScript
|
|
7866
|
+
// does. The walk reaching the declaration later types it for real.
|
|
7867
|
+
/** Declarations a reference may meet before the walk does. */
|
|
7868
|
+
aheadDeclarations;
|
|
7869
|
+
computingAhead = /* @__PURE__ */ new Set();
|
|
7870
|
+
declaredAhead(id) {
|
|
7871
|
+
this.aheadDeclarations ??= this.indexAheadDeclarations();
|
|
7872
|
+
const found = this.aheadDeclarations.get(id);
|
|
7873
|
+
if (!found || this.computingAhead.has(id)) return void 0;
|
|
7874
|
+
this.computingAhead.add(id);
|
|
7875
|
+
const wasEmitting = this.emitDiagnostics;
|
|
7876
|
+
this.emitDiagnostics = false;
|
|
7877
|
+
try {
|
|
7878
|
+
const { statement, index } = found;
|
|
7879
|
+
let type;
|
|
7880
|
+
if (statement.type === "DeclareStatement") {
|
|
7881
|
+
type = this.resolveType(statement.valueType);
|
|
7882
|
+
} else if (statement.type === "FunctionDeclaration") {
|
|
7883
|
+
type = statement.signatures?.length ? intersection(statement.signatures.map((sig) => this.signatureToFnType(sig))) : this.inferFunctionBody(statement.func, /* @__PURE__ */ new Map());
|
|
7884
|
+
} else if (statement.type === "VariableDeclaration") {
|
|
7885
|
+
const target = statement.names[index];
|
|
7886
|
+
if (target.type === "IdentifierPattern" && target.typeAnnotation) {
|
|
7887
|
+
type = this.resolveType(target.typeAnnotation);
|
|
7888
|
+
} else if (statement.init[index]) {
|
|
7889
|
+
const value = this.infer(statement.init[index], /* @__PURE__ */ new Map());
|
|
7890
|
+
type = statement.kind === "const" ? value : widen(value);
|
|
7891
|
+
}
|
|
7892
|
+
}
|
|
7893
|
+
if (type) this.bindingType.set(id, type);
|
|
7894
|
+
return type;
|
|
7895
|
+
} finally {
|
|
7896
|
+
this.emitDiagnostics = wasEmitting;
|
|
7897
|
+
this.computingAhead.delete(id);
|
|
7898
|
+
}
|
|
7899
|
+
}
|
|
7900
|
+
/** Every function declaration, and every plain name the module declares
|
|
7901
|
+
* at its top level. */
|
|
7902
|
+
indexAheadDeclarations() {
|
|
7903
|
+
const out = /* @__PURE__ */ new Map();
|
|
7904
|
+
for (const statement of this.program.body.statements) {
|
|
7905
|
+
const declaration = statement.type === "ExportStatement" ? statement.declaration : statement;
|
|
7906
|
+
if (declaration.type !== "VariableDeclaration") continue;
|
|
7907
|
+
declaration.names.forEach((target, index) => {
|
|
7908
|
+
if (target.type !== "IdentifierPattern") return;
|
|
7909
|
+
const id = this.bindingIdByName(target.name, target);
|
|
7910
|
+
if (id !== void 0) out.set(id, { statement: declaration, index });
|
|
7911
|
+
});
|
|
7912
|
+
}
|
|
7913
|
+
const visit = (node) => {
|
|
7914
|
+
if (!node || typeof node !== "object") return;
|
|
7915
|
+
if (Array.isArray(node)) {
|
|
7916
|
+
for (const item of node) visit(item);
|
|
7917
|
+
return;
|
|
7918
|
+
}
|
|
7919
|
+
const record = node;
|
|
7920
|
+
if (record.type === "FunctionDeclaration" && record.name) {
|
|
7921
|
+
const id = this.bindingIdByName(record.name.name, record.name);
|
|
7922
|
+
if (id !== void 0) out.set(id, { statement: node, index: 0 });
|
|
7923
|
+
}
|
|
7924
|
+
for (const [key, value] of Object.entries(node)) {
|
|
7925
|
+
if (key !== "line" && key !== "column" && value && typeof value === "object") visit(value);
|
|
7926
|
+
}
|
|
7927
|
+
};
|
|
7928
|
+
visit(this.program.body);
|
|
7929
|
+
for (const [name, statement] of this.deferredDeclares) {
|
|
7930
|
+
const id = this.scopes.globalsByName.get(name);
|
|
7931
|
+
if (id !== void 0) out.set(id, { statement, index: 0 });
|
|
7932
|
+
}
|
|
7933
|
+
return out;
|
|
6261
7934
|
}
|
|
6262
7935
|
/** Bind or rebind a whole variable: any narrowing recorded for a path
|
|
6263
7936
|
* *under* it (`x.a`, `x[1]`) described the old value and must go. */
|
|
@@ -6284,12 +7957,28 @@ var TypeAnalyzer = class {
|
|
|
6284
7957
|
return this.bindingByDecl.get(node) ?? this.bindingByPos.get(posKey(name, node.line.start, node.column.start));
|
|
6285
7958
|
}
|
|
6286
7959
|
};
|
|
7960
|
+
function referencedTypeNames(node, out = []) {
|
|
7961
|
+
if (!node || typeof node !== "object") return out;
|
|
7962
|
+
if (Array.isArray(node)) {
|
|
7963
|
+
for (const item of node) referencedTypeNames(item, out);
|
|
7964
|
+
return out;
|
|
7965
|
+
}
|
|
7966
|
+
const record = node;
|
|
7967
|
+
if (record.type === "TypeReference" && typeof record.base === "string") {
|
|
7968
|
+
out.push(typeof record.namespace === "string" ? `${record.namespace}.${record.base}` : record.base);
|
|
7969
|
+
}
|
|
7970
|
+
for (const [key, value] of Object.entries(node)) {
|
|
7971
|
+
if (key !== "line" && key !== "column" && value && typeof value === "object") referencedTypeNames(value, out);
|
|
7972
|
+
}
|
|
7973
|
+
return out;
|
|
7974
|
+
}
|
|
6287
7975
|
function containsTypeQuery(node) {
|
|
6288
7976
|
if (!node || typeof node !== "object") return false;
|
|
6289
7977
|
if (Array.isArray(node)) return node.some(containsTypeQuery);
|
|
6290
7978
|
if (node.type === "TypeofTypeNode") return true;
|
|
6291
7979
|
return Object.values(node).some(containsTypeQuery);
|
|
6292
7980
|
}
|
|
7981
|
+
var STRING_INTRINSICS = /* @__PURE__ */ new Set(["Uppercase", "Lowercase", "Capitalize", "Uncapitalize"]);
|
|
6293
7982
|
function briefType(t) {
|
|
6294
7983
|
if (t.kind === "union" && t.types.length > 8) {
|
|
6295
7984
|
const shown = t.types.slice(0, 6).map(formatType).join(" | ");
|
|
@@ -6454,6 +8143,7 @@ function offsetPosition(source, offset) {
|
|
|
6454
8143
|
import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
6455
8144
|
function resolveTypeLibraries(config, host = nodeHost) {
|
|
6456
8145
|
const files = [];
|
|
8146
|
+
const lowerings = [];
|
|
6457
8147
|
const problems = [];
|
|
6458
8148
|
const loaded = /* @__PURE__ */ new Set();
|
|
6459
8149
|
const addFile = (file) => {
|
|
@@ -6471,6 +8161,8 @@ function resolveTypeLibraries(config, host = nodeHost) {
|
|
|
6471
8161
|
if (found) addPackage(found.directory, found.file, visiting);
|
|
6472
8162
|
}
|
|
6473
8163
|
addFile(entryFile);
|
|
8164
|
+
const lowering = loweringModule(directory, host, problems, config);
|
|
8165
|
+
if (lowering) lowerings.push(lowering);
|
|
6474
8166
|
};
|
|
6475
8167
|
for (const entry of config.types) {
|
|
6476
8168
|
const relative = entry.startsWith("./") || entry.startsWith("../") || entry.startsWith("/") || /^[A-Za-z]:[\\/]/.test(entry);
|
|
@@ -6497,7 +8189,19 @@ function resolveTypeLibraries(config, host = nodeHost) {
|
|
|
6497
8189
|
});
|
|
6498
8190
|
}
|
|
6499
8191
|
}
|
|
6500
|
-
return { files, problems };
|
|
8192
|
+
return { files, lowerings, problems };
|
|
8193
|
+
}
|
|
8194
|
+
function loweringModule(directory, host, problems, config) {
|
|
8195
|
+
const manifest = readJson(join2(directory, "package.json"), host);
|
|
8196
|
+
const declared = manifest?.luaut?.lowering;
|
|
8197
|
+
if (typeof declared !== "string") return void 0;
|
|
8198
|
+
const from = typeof manifest?.name === "string" ? manifest.name : directory;
|
|
8199
|
+
const file = resolve2(directory, declared);
|
|
8200
|
+
if (host.readFile(file) === void 0) {
|
|
8201
|
+
problems.push({ file: config.path, message: `'${from}' names a lowering module '${declared}', which is not there` });
|
|
8202
|
+
return void 0;
|
|
8203
|
+
}
|
|
8204
|
+
return { file, from };
|
|
6501
8205
|
}
|
|
6502
8206
|
var ENTRY_FILE = "index.d.luaut";
|
|
6503
8207
|
function packageEntry(directory, host) {
|
|
@@ -6677,18 +8381,22 @@ export {
|
|
|
6677
8381
|
Keywords,
|
|
6678
8382
|
LexError,
|
|
6679
8383
|
Operators,
|
|
8384
|
+
PRELUDE_SOURCE,
|
|
6680
8385
|
ParseError,
|
|
6681
8386
|
Punctuators,
|
|
8387
|
+
UNUSED_EXPECT_ERROR,
|
|
6682
8388
|
UnaryOperators,
|
|
6683
8389
|
analyzeScopes,
|
|
6684
8390
|
analyzeTypes,
|
|
6685
8391
|
anyType,
|
|
8392
|
+
applyDirectives,
|
|
6686
8393
|
arrayOf,
|
|
6687
8394
|
booleanType,
|
|
6688
8395
|
bufferType,
|
|
6689
8396
|
containsTypeParam,
|
|
6690
8397
|
index_default as default,
|
|
6691
8398
|
difference,
|
|
8399
|
+
directivesOf,
|
|
6692
8400
|
equalTypes,
|
|
6693
8401
|
falsyType,
|
|
6694
8402
|
findConfig,
|
|
@@ -6724,6 +8432,7 @@ export {
|
|
|
6724
8432
|
parseTokens,
|
|
6725
8433
|
parseWithRecovery,
|
|
6726
8434
|
primitive,
|
|
8435
|
+
readDirectives,
|
|
6727
8436
|
resolveModulePath,
|
|
6728
8437
|
resolveTypeLibraries,
|
|
6729
8438
|
setAliasExpander,
|