@tilua/parser 3.1.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 +29 -10
- package/dist/index.cjs +310 -33
- package/dist/index.d.cts +12 -3
- package/dist/index.d.ts +12 -3
- package/dist/index.js +310 -33
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -596,6 +596,8 @@ var UNUSED_EXPECT_ERROR = "Unused '@tilua-expect-error' directive";
|
|
|
596
596
|
|
|
597
597
|
// src/ast/builders.ts
|
|
598
598
|
var NO_DOTS = "tilua has no bare '...': take a rest parameter, '...args: T[]', or 'scriptArgs' for the script's own";
|
|
599
|
+
var NO_MEMBER_NAME = "Expected a name after '.'";
|
|
600
|
+
var NO_TYPEOF_CALL = "'typeof' is an operator, not a function: write 'typeof v'";
|
|
599
601
|
var ACCESSIBILITIES = /* @__PURE__ */ new Set(["public", "private", "protected"]);
|
|
600
602
|
var CLASS_MODIFIERS = /* @__PURE__ */ new Set([...ACCESSIBILITIES, "static", "abstract", "override", "readonly"]);
|
|
601
603
|
function bindThis(func, at) {
|
|
@@ -2133,6 +2135,7 @@ var Parser = class {
|
|
|
2133
2135
|
* `:` Identifier followed by one of Lua's call forms. */
|
|
2134
2136
|
startsMethodCall(offset = 0) {
|
|
2135
2137
|
if (this.peek(offset + 1).type !== "Identifier") return false;
|
|
2138
|
+
if (this.startsOwnLine(this.peek(offset), this.peek(offset + 1))) return false;
|
|
2136
2139
|
const after = this.peek(offset + 2);
|
|
2137
2140
|
if (after.type === "Punctuator") {
|
|
2138
2141
|
const v = String(after.value);
|
|
@@ -2158,6 +2161,23 @@ var Parser = class {
|
|
|
2158
2161
|
const isString = t.type === "InterpolatedString" || t.type === "Literal" && t.kind === "string";
|
|
2159
2162
|
return isString && this.punctuatorAt(1, ":") && this.startsMethodCall(1);
|
|
2160
2163
|
}
|
|
2164
|
+
/** Is there no member name at `offset`, after the `.` or `:` before it? */
|
|
2165
|
+
namelessAfter(offset) {
|
|
2166
|
+
const name = this.peek(offset);
|
|
2167
|
+
if (name.type !== "Identifier") return true;
|
|
2168
|
+
const before = offset === 0 ? this.previous() : this.peek(offset - 1);
|
|
2169
|
+
return before !== void 0 && this.startsOwnLine(before, name);
|
|
2170
|
+
}
|
|
2171
|
+
/** Recovery only. `obj.` may go on to its name on a later line, as in
|
|
2172
|
+
* JavaScript — but while `obj.` is still being typed, the line below is
|
|
2173
|
+
* whatever statement was already there, and reading it as the name turns
|
|
2174
|
+
* one unfinished line into errors about the next. So, as a missing `}`
|
|
2175
|
+
* is placed, the indentation says which: a line that carries the access
|
|
2176
|
+
* on is indented deeper than the line it carries on from. */
|
|
2177
|
+
startsOwnLine(before, name) {
|
|
2178
|
+
if (!this.recover || name.line.start <= before.line.end) return false;
|
|
2179
|
+
return this.indentOf(name) <= this.indentOf(before);
|
|
2180
|
+
}
|
|
2161
2181
|
/** Does the next token start right where the current one ends? */
|
|
2162
2182
|
touchesNext() {
|
|
2163
2183
|
const current = this.current();
|
|
@@ -2199,6 +2219,7 @@ var Parser = class {
|
|
|
2199
2219
|
const t = this.current();
|
|
2200
2220
|
if (t.type === "Keyword" && t.value === "not") return "not";
|
|
2201
2221
|
if (t.type === "Operator" && (t.value === "-" || t.value === "#")) return t.value;
|
|
2222
|
+
if (t.type === "Identifier" && t.value === "typeof") return "typeof";
|
|
2202
2223
|
return null;
|
|
2203
2224
|
}
|
|
2204
2225
|
/** Runs `fn` inside a bracket, where a `:` can only ever be a method
|
|
@@ -2298,6 +2319,10 @@ var Parser = class {
|
|
|
2298
2319
|
const op = this.isUnaryOperator();
|
|
2299
2320
|
if (op) {
|
|
2300
2321
|
const opTok = this.advance();
|
|
2322
|
+
const next = this.current();
|
|
2323
|
+
if (op === "typeof" && this.checkPunctuator("(") && next.line.start === opTok.line.end && next.column.start === opTok.column.end) {
|
|
2324
|
+
this.problemAt(next, NO_TYPEOF_CALL);
|
|
2325
|
+
}
|
|
2301
2326
|
const argument = this.parseExpression(UNARY_PRECEDENCE);
|
|
2302
2327
|
return {
|
|
2303
2328
|
type: "UnaryExpression",
|
|
@@ -2467,6 +2492,13 @@ var Parser = class {
|
|
|
2467
2492
|
if (this.checkPunctuator("?") && this.touchesNext()) {
|
|
2468
2493
|
const next = this.peek(1);
|
|
2469
2494
|
const punct = next.type === "Punctuator" ? String(next.value) : void 0;
|
|
2495
|
+
if (punct === "." && this.recover && this.namelessAfter(2) && !this.punctuatorAt(2, "[") && !this.punctuatorAt(2, "(")) {
|
|
2496
|
+
this.advance();
|
|
2497
|
+
this.advance();
|
|
2498
|
+
this.problemAt(this.previous(), NO_MEMBER_NAME);
|
|
2499
|
+
base = { type: "ErrorExpression", expression: base, ...spanFrom(base, this.previous()) };
|
|
2500
|
+
break;
|
|
2501
|
+
}
|
|
2470
2502
|
if (punct === "." && this.peek(2).type === "Identifier") {
|
|
2471
2503
|
this.advance();
|
|
2472
2504
|
this.advance();
|
|
@@ -2521,9 +2553,9 @@ var Parser = class {
|
|
|
2521
2553
|
}
|
|
2522
2554
|
}
|
|
2523
2555
|
if (this.matchPunctuator(".")) {
|
|
2524
|
-
if (this.
|
|
2525
|
-
this.
|
|
2526
|
-
base = { type: "ErrorExpression", ...spanFrom(base, this.previous()) };
|
|
2556
|
+
if (this.namelessAfter(0)) {
|
|
2557
|
+
this.problemAt(this.previous(), NO_MEMBER_NAME);
|
|
2558
|
+
base = { type: "ErrorExpression", expression: base, ...spanFrom(base, this.previous()) };
|
|
2527
2559
|
break;
|
|
2528
2560
|
}
|
|
2529
2561
|
const prop = this.parseIdentifier();
|
|
@@ -2536,6 +2568,12 @@ var Parser = class {
|
|
|
2536
2568
|
base = { type: "IndexExpression", object: base, index, ...spanFrom(base, this.previous()) };
|
|
2537
2569
|
continue;
|
|
2538
2570
|
}
|
|
2571
|
+
if (this.recover && !this.ternaryColons && this.checkPunctuator(":") && this.peek(1).line.start > this.current().line.end && !this.startsMethodCall()) {
|
|
2572
|
+
this.advance();
|
|
2573
|
+
this.problemAt(this.previous(), "Expected a name after ':'");
|
|
2574
|
+
base = { type: "ErrorExpression", expression: base, ...spanFrom(base, this.previous()) };
|
|
2575
|
+
break;
|
|
2576
|
+
}
|
|
2539
2577
|
if (this.checkPunctuator(":") && this.startsMethodCall() && this.takeMethodColon()) {
|
|
2540
2578
|
this.advance();
|
|
2541
2579
|
const method = this.parseIdentifier();
|
|
@@ -3379,6 +3417,7 @@ var Parser = class {
|
|
|
3379
3417
|
return { type: "TupleTypeNode", elements, ...rest ? { rest } : {}, ...spanFrom(t, this.previous()) };
|
|
3380
3418
|
}
|
|
3381
3419
|
if (t.type === "Identifier" && t.value === "typeof" && this.peek(1).type === "Punctuator" && this.peek(1).value === "(") {
|
|
3420
|
+
this.problemAt(this.peek(1), NO_TYPEOF_CALL);
|
|
3382
3421
|
this.advance();
|
|
3383
3422
|
this.advance();
|
|
3384
3423
|
const expression = this.parseExpression();
|
|
@@ -3759,7 +3798,7 @@ var BinaryOperators = [
|
|
|
3759
3798
|
"and",
|
|
3760
3799
|
"or"
|
|
3761
3800
|
];
|
|
3762
|
-
var UnaryOperators = ["-", "not", "#"];
|
|
3801
|
+
var UnaryOperators = ["-", "not", "#", "typeof"];
|
|
3763
3802
|
|
|
3764
3803
|
// src/ast/analyzeScopes.ts
|
|
3765
3804
|
function getBinding(analysis, id) {
|
|
@@ -4386,7 +4425,9 @@ var Analyzer = class {
|
|
|
4386
4425
|
case "BooleanLiteral":
|
|
4387
4426
|
case "NumberLiteral":
|
|
4388
4427
|
case "StringLiteral":
|
|
4428
|
+
return;
|
|
4389
4429
|
case "ErrorExpression":
|
|
4430
|
+
if (expr.expression) this.visitExpression(expr.expression, scope);
|
|
4390
4431
|
return;
|
|
4391
4432
|
case "InterpolatedStringExpression":
|
|
4392
4433
|
for (const part of expr.parts) {
|
|
@@ -5692,6 +5733,13 @@ function mergeEnv(a, b, base) {
|
|
|
5692
5733
|
}
|
|
5693
5734
|
return out;
|
|
5694
5735
|
}
|
|
5736
|
+
function joinNarrowings(into, a, aFrom, b, bFrom) {
|
|
5737
|
+
for (const [key, left] of a) {
|
|
5738
|
+
const right = b.get(key);
|
|
5739
|
+
if (right === void 0 || left === aFrom.get(key) || right === bFrom.get(key)) continue;
|
|
5740
|
+
into.set(key, union([left, right]));
|
|
5741
|
+
}
|
|
5742
|
+
}
|
|
5695
5743
|
function mergeIndexer(a, b) {
|
|
5696
5744
|
return a ? { key: union([a.key, b.key]), value: union([a.value, b.value]) } : b;
|
|
5697
5745
|
}
|
|
@@ -5971,6 +6019,10 @@ function unwrapParens(e) {
|
|
|
5971
6019
|
while (e.type === "ParenthesizedExpression") e = e.expression;
|
|
5972
6020
|
return e;
|
|
5973
6021
|
}
|
|
6022
|
+
function memberLabel(label, key) {
|
|
6023
|
+
if (label === void 0 || key === void 0) return void 0;
|
|
6024
|
+
return typeof key === "number" ? `${label}[${key}]` : `${label}.${key}`;
|
|
6025
|
+
}
|
|
5974
6026
|
function expressionLabel(e, depth = 0) {
|
|
5975
6027
|
if (depth > 6) return void 0;
|
|
5976
6028
|
const args = (list) => {
|
|
@@ -6042,6 +6094,7 @@ var TypeAnalyzer = class {
|
|
|
6042
6094
|
metatables = [];
|
|
6043
6095
|
metatableCache = /* @__PURE__ */ new WeakMap();
|
|
6044
6096
|
methodSources = /* @__PURE__ */ new Map();
|
|
6097
|
+
typeofTables = /* @__PURE__ */ new Map();
|
|
6045
6098
|
/** Public: each alias resolved once (generic aliases keep their params as
|
|
6046
6099
|
* `typeParam` nodes in the body). */
|
|
6047
6100
|
aliases = new LazyMap();
|
|
@@ -6120,7 +6173,7 @@ var TypeAnalyzer = class {
|
|
|
6120
6173
|
const given = this.options.globalTypes?.[name];
|
|
6121
6174
|
if (given) this.bindingType.set(id, given);
|
|
6122
6175
|
else if (this.libGlobalTypes.has(name)) this.bindingType.defer(id, () => this.libGlobalTypes.get(name) ?? anyType);
|
|
6123
|
-
else if (LANGUAGE_GLOBALS.includes(name)) this.bindingType.set(id, arrayOf(
|
|
6176
|
+
else if (LANGUAGE_GLOBALS.includes(name)) this.bindingType.set(id, arrayOf(declaredUnknownType));
|
|
6124
6177
|
else this.bindingType.set(id, anyType);
|
|
6125
6178
|
}
|
|
6126
6179
|
setAliasExpander((t) => this.expand(t));
|
|
@@ -6143,6 +6196,7 @@ var TypeAnalyzer = class {
|
|
|
6143
6196
|
aliases: this.resolveDeferredAliases(),
|
|
6144
6197
|
loops: this.loops,
|
|
6145
6198
|
methodSources: this.methodSources,
|
|
6199
|
+
typeofTables: this.typeofTables,
|
|
6146
6200
|
metatableMembers: (type) => this.metatableMembers(type),
|
|
6147
6201
|
diagnostics: this.diagnostics
|
|
6148
6202
|
};
|
|
@@ -6993,7 +7047,12 @@ var TypeAnalyzer = class {
|
|
|
6993
7047
|
this.aliases.defer(name, () => this.classType(cls));
|
|
6994
7048
|
continue;
|
|
6995
7049
|
}
|
|
6996
|
-
if (this.dependsOnTypeQuery(name))
|
|
7050
|
+
if (this.dependsOnTypeQuery(name)) {
|
|
7051
|
+
if (def.runtimeClass) {
|
|
7052
|
+
this.aliases.defer(name, () => this.withTypeParams(def.params, () => this.resolveDef(def)));
|
|
7053
|
+
}
|
|
7054
|
+
continue;
|
|
7055
|
+
}
|
|
6997
7056
|
this.withTypeParams(def.params, () => {
|
|
6998
7057
|
this.aliases.set(name, this.resolveDef(def));
|
|
6999
7058
|
});
|
|
@@ -7001,14 +7060,16 @@ var TypeAnalyzer = class {
|
|
|
7001
7060
|
}
|
|
7002
7061
|
typeQueryDependents = /* @__PURE__ */ new Map();
|
|
7003
7062
|
/** Does alias `name` contain a `typeof`, itself or through an alias it
|
|
7004
|
-
* names?
|
|
7063
|
+
* names? Its parameters count: `class C<K extends Config["key"]>`
|
|
7064
|
+
* resolves the bound along with the class. */
|
|
7005
7065
|
dependsOnTypeQuery(name, visiting = /* @__PURE__ */ new Set()) {
|
|
7006
7066
|
const known = this.typeQueryDependents.get(name);
|
|
7007
7067
|
if (known !== void 0) return known;
|
|
7008
7068
|
const def = this.aliasDefs.get(name);
|
|
7009
7069
|
if (!def || def.class || visiting.has(name)) return false;
|
|
7010
7070
|
visiting.add(name);
|
|
7011
|
-
const
|
|
7071
|
+
const nodes = [def.node, ...def.params.flatMap((p) => [p.constraint, p.default])];
|
|
7072
|
+
const result = nodes.some(containsTypeQuery) || referencedTypeNames(nodes).some((other) => other !== name && this.dependsOnTypeQuery(other, visiting));
|
|
7012
7073
|
visiting.delete(name);
|
|
7013
7074
|
this.typeQueryDependents.set(name, result);
|
|
7014
7075
|
return result;
|
|
@@ -7733,7 +7794,7 @@ var TypeAnalyzer = class {
|
|
|
7733
7794
|
}
|
|
7734
7795
|
}
|
|
7735
7796
|
const mode = this.initIsAsConst(source) ? "asconst" : !isFreshLiteralExpr(source) ? "keep" : stmt.kind === "const" ? "const" : "widen";
|
|
7736
|
-
this.bindPattern(target, inferred, env, mode);
|
|
7797
|
+
this.bindPattern(target, inferred, env, mode, source && expressionLabel(source));
|
|
7737
7798
|
if (stmt.kind === "const") {
|
|
7738
7799
|
this.correlateDestructuring(target, inferred, env);
|
|
7739
7800
|
this.correlateIndexed(target, source, env);
|
|
@@ -7826,9 +7887,10 @@ var TypeAnalyzer = class {
|
|
|
7826
7887
|
} else if (target.type === "MemberExpression" || target.type === "IndexExpression") {
|
|
7827
7888
|
this.infer(target, env);
|
|
7828
7889
|
this.checkReadonlyAssign(target, env);
|
|
7890
|
+
this.checkMemberAssign(stmt, target, vt, env);
|
|
7829
7891
|
this.assignToRef(target, isFreshLiteralExpr(source) ? widen(vt) : vt, env);
|
|
7830
7892
|
} else if (target.type === "ObjectPattern" || target.type === "ArrayPattern") {
|
|
7831
|
-
this.reassignPattern(target, vt, env);
|
|
7893
|
+
this.reassignPattern(target, vt, env, expressionLabel(source));
|
|
7832
7894
|
}
|
|
7833
7895
|
return;
|
|
7834
7896
|
}
|
|
@@ -9194,7 +9256,8 @@ var TypeAnalyzer = class {
|
|
|
9194
9256
|
}
|
|
9195
9257
|
/** Like `bindPattern`, but the leaves are *existing* bindings (a `{a} = t`
|
|
9196
9258
|
* assignment). Updates their flow type; keeps a declared annotation. */
|
|
9197
|
-
reassignPattern(target, valueType, env) {
|
|
9259
|
+
reassignPattern(target, valueType, env, label) {
|
|
9260
|
+
if (target.type === "ObjectPattern" || target.type === "ArrayPattern") valueType = this.destructured(target, valueType, label);
|
|
9198
9261
|
switch (target.type) {
|
|
9199
9262
|
case "IdentifierPattern": {
|
|
9200
9263
|
const id = this.scopes.bindingOf.get(target);
|
|
@@ -9219,21 +9282,43 @@ var TypeAnalyzer = class {
|
|
|
9219
9282
|
for (const p of target.properties) {
|
|
9220
9283
|
const key = !p.computed && p.key.type === "Identifier" ? p.key.name : !p.computed && p.key.type === "StringLiteral" ? p.key.value : void 0;
|
|
9221
9284
|
const pt = key !== void 0 ? this.propertyType(valueType, key) : unknownType;
|
|
9222
|
-
this.reassignPattern(p.value, this.withDefault(pt, p.default, env), env);
|
|
9285
|
+
this.reassignPattern(p.value, this.withDefault(pt, p.default, env), env, memberLabel(label, key));
|
|
9223
9286
|
}
|
|
9224
9287
|
if (target.rest) this.reassignPattern(target.rest, this.withoutKeys(valueType, target.properties), env);
|
|
9225
9288
|
return;
|
|
9226
9289
|
}
|
|
9227
9290
|
case "ArrayPattern": {
|
|
9228
9291
|
target.elements.forEach((el, i) => {
|
|
9229
|
-
if (el) this.reassignPattern(el.value, this.withDefault(this.elementType(valueType, i), el.default, env), env);
|
|
9292
|
+
if (el) this.reassignPattern(el.value, this.withDefault(this.elementType(valueType, i), el.default, env), env, memberLabel(label, i + 1));
|
|
9230
9293
|
});
|
|
9231
9294
|
if (target.rest) this.reassignPattern(target.rest, this.restAfter(valueType, target.elements.length), env);
|
|
9232
9295
|
return;
|
|
9233
9296
|
}
|
|
9234
9297
|
}
|
|
9235
9298
|
}
|
|
9236
|
-
|
|
9299
|
+
destructuredReported = /* @__PURE__ */ new WeakSet();
|
|
9300
|
+
/** What a pattern reads from. `const { a } = v` is `v.a`, and fails where
|
|
9301
|
+
* that would: on a `v` that may be nil, and on one that is `unknown`.
|
|
9302
|
+
* `label` is the value as it would be written — `args[1]` for the inner
|
|
9303
|
+
* pattern of `const [{ a }] = args`. */
|
|
9304
|
+
destructured(pattern, raw, label) {
|
|
9305
|
+
let type = raw;
|
|
9306
|
+
const report = (message) => {
|
|
9307
|
+
if (!this.emitDiagnostics || this.destructuredReported.has(pattern)) return;
|
|
9308
|
+
this.destructuredReported.add(pattern);
|
|
9309
|
+
this.diagnostics.push({ node: pattern, message: `${label === void 0 ? "Object" : `'${label}'`} ${message}` });
|
|
9310
|
+
};
|
|
9311
|
+
if (this.includesNil(type)) {
|
|
9312
|
+
const t = this.expand(type);
|
|
9313
|
+
report(t.kind === "primitive" ? "is nil" : "is possibly nil. Check it first, or give it a default");
|
|
9314
|
+
type = withoutNil(t);
|
|
9315
|
+
}
|
|
9316
|
+
const read = this.expand(type);
|
|
9317
|
+
if (read.kind === "unknown" && read.declared) report("is of type 'unknown'");
|
|
9318
|
+
return type;
|
|
9319
|
+
}
|
|
9320
|
+
bindPattern(target, valueType, env, mode, label) {
|
|
9321
|
+
if (target.type === "ObjectPattern" || target.type === "ArrayPattern") valueType = this.destructured(target, valueType, label);
|
|
9237
9322
|
switch (target.type) {
|
|
9238
9323
|
case "IdentifierPattern": {
|
|
9239
9324
|
const id = this.bindingIdByName(target.name, target);
|
|
@@ -9254,7 +9339,7 @@ var TypeAnalyzer = class {
|
|
|
9254
9339
|
for (const p of target.properties) {
|
|
9255
9340
|
const key = !p.computed && p.key.type === "Identifier" ? p.key.name : !p.computed && p.key.type === "StringLiteral" ? p.key.value : void 0;
|
|
9256
9341
|
const propType = key !== void 0 ? this.propertyType(valueType, key) : unknownType;
|
|
9257
|
-
this.bindPattern(p.value, this.withDefault(propType, p.default, env), env, mode);
|
|
9342
|
+
this.bindPattern(p.value, this.withDefault(propType, p.default, env), env, mode, memberLabel(label, key));
|
|
9258
9343
|
}
|
|
9259
9344
|
if (target.rest) this.bindPattern(target.rest, this.withoutKeys(valueType, target.properties), env, mode);
|
|
9260
9345
|
return;
|
|
@@ -9262,7 +9347,7 @@ var TypeAnalyzer = class {
|
|
|
9262
9347
|
case "ArrayPattern": {
|
|
9263
9348
|
target.elements.forEach((el, i) => {
|
|
9264
9349
|
if (!el) return;
|
|
9265
|
-
this.bindPattern(el.value, this.withDefault(this.elementType(valueType, i), el.default, env), env, mode);
|
|
9350
|
+
this.bindPattern(el.value, this.withDefault(this.elementType(valueType, i), el.default, env), env, mode, memberLabel(label, i + 1));
|
|
9266
9351
|
});
|
|
9267
9352
|
if (target.rest) this.bindPattern(target.rest, this.restAfter(valueType, target.elements.length), env, mode);
|
|
9268
9353
|
return;
|
|
@@ -9827,6 +9912,7 @@ var TypeAnalyzer = class {
|
|
|
9827
9912
|
}
|
|
9828
9913
|
// Broken syntax is reported by the parser; nothing more to say.
|
|
9829
9914
|
case "ErrorExpression":
|
|
9915
|
+
if (expr.expression) this.infer(expr.expression, env);
|
|
9830
9916
|
return anyType;
|
|
9831
9917
|
case "Identifier": {
|
|
9832
9918
|
const id = this.bindingIdOf(expr);
|
|
@@ -9890,6 +9976,14 @@ var TypeAnalyzer = class {
|
|
|
9890
9976
|
this.checkOperands(expr, "#", arg);
|
|
9891
9977
|
return numberType;
|
|
9892
9978
|
}
|
|
9979
|
+
case "typeof": {
|
|
9980
|
+
const names = this.typeofNamesOf(arg);
|
|
9981
|
+
const tables = names && [...names].filter((name) => name === "array" || name === "object");
|
|
9982
|
+
if (tables?.length === 1) this.typeofTables.set(expr, tables[0]);
|
|
9983
|
+
else this.typeofTables.delete(expr);
|
|
9984
|
+
const answers = names?.size ? [...names] : [...this.typeofTypes().keys()];
|
|
9985
|
+
return union(answers.map((name) => literal(name)));
|
|
9986
|
+
}
|
|
9893
9987
|
}
|
|
9894
9988
|
return arg;
|
|
9895
9989
|
}
|
|
@@ -9932,7 +10026,9 @@ var TypeAnalyzer = class {
|
|
|
9932
10026
|
return booleanType;
|
|
9933
10027
|
case "==":
|
|
9934
10028
|
case "~=":
|
|
9935
|
-
this.
|
|
10029
|
+
if (!this.checkTypeofName(expr, expr.left, expr.right) && !this.checkTypeofName(expr, expr.right, expr.left)) {
|
|
10030
|
+
this.checkOverlap(expr, op, l, r);
|
|
10031
|
+
}
|
|
9936
10032
|
return booleanType;
|
|
9937
10033
|
case "+":
|
|
9938
10034
|
case "-":
|
|
@@ -10133,6 +10229,7 @@ var TypeAnalyzer = class {
|
|
|
10133
10229
|
const fromMetatable = this.receiverMetatableMember(objType, expr.method.name);
|
|
10134
10230
|
if (fromMetatable) this.methodSources.set(expr, fromMetatable.source);
|
|
10135
10231
|
const method = fromMetatable?.type ?? this.propertyType(objType, expr.method.name);
|
|
10232
|
+
this.typeOf.set(expr.method, method);
|
|
10136
10233
|
const united = this.unionSignatures(method);
|
|
10137
10234
|
const fns = united ?? this.overloadsOf(method);
|
|
10138
10235
|
const explicit = this.explicitTypeArguments(expr, fns);
|
|
@@ -10273,6 +10370,43 @@ var TypeAnalyzer = class {
|
|
|
10273
10370
|
message: `Cannot assign to '${name}' because it is a read-only property`
|
|
10274
10371
|
});
|
|
10275
10372
|
}
|
|
10373
|
+
/** `t.x = v` against what `x` was declared as. Only a property the
|
|
10374
|
+
* table's type names is checked: a key it does not name is the Lua
|
|
10375
|
+
* table growing, which `checkMissingMember` is not about. */
|
|
10376
|
+
checkMemberAssign(stmt, target, value, env) {
|
|
10377
|
+
if (!this.emitDiagnostics) return;
|
|
10378
|
+
let name;
|
|
10379
|
+
if (target.type === "MemberExpression") {
|
|
10380
|
+
name = target.property.name;
|
|
10381
|
+
} else {
|
|
10382
|
+
const index = this.expand(this.infer(target.index, env));
|
|
10383
|
+
if (index.kind !== "literal" || typeof index.value !== "string") return;
|
|
10384
|
+
name = index.value;
|
|
10385
|
+
}
|
|
10386
|
+
if (target.object.type === "Identifier") {
|
|
10387
|
+
const id = this.bindingIdOf(target.object);
|
|
10388
|
+
if (id !== void 0 && this.scopes.bindings.get(id)?.declaredBy === "namespace") return;
|
|
10389
|
+
}
|
|
10390
|
+
const declared = this.declaredProperty(this.expand(this.infer(target.object, env)), name);
|
|
10391
|
+
if (!declared || declared.kind === "any" || declared.kind === "unknown") return;
|
|
10392
|
+
if (this.fitsAnnotation(stmt.value, declared, value, env)) return;
|
|
10393
|
+
this.diagnostics.push({
|
|
10394
|
+
node: stmt,
|
|
10395
|
+
message: `Type '${briefType(value)}' is not assignable to '${briefType(declared)}'` + this.explainMismatch(value, declared)
|
|
10396
|
+
});
|
|
10397
|
+
}
|
|
10398
|
+
/** The type a property was declared with, where the type names it. */
|
|
10399
|
+
declaredProperty(t, name) {
|
|
10400
|
+
if (t.kind === "object") {
|
|
10401
|
+
const p = t.properties.get(name);
|
|
10402
|
+
return p && (p.optional ? optional(p.type) : p.type);
|
|
10403
|
+
}
|
|
10404
|
+
if (t.kind === "intersection") {
|
|
10405
|
+
const parts = t.types.flatMap((m) => this.declaredProperty(this.expand(m), name) ?? []);
|
|
10406
|
+
return parts.length ? intersection(parts) : void 0;
|
|
10407
|
+
}
|
|
10408
|
+
return void 0;
|
|
10409
|
+
}
|
|
10276
10410
|
isReadonlyProperty(t, name) {
|
|
10277
10411
|
if (t.kind === "object") return t.properties.get(name)?.readonly === true;
|
|
10278
10412
|
if (t.kind === "intersection" || t.kind === "union") {
|
|
@@ -10668,13 +10802,23 @@ var TypeAnalyzer = class {
|
|
|
10668
10802
|
if (cond.type === "BinaryExpression") {
|
|
10669
10803
|
const { operator: op, left, right } = cond;
|
|
10670
10804
|
if (op === "and") {
|
|
10671
|
-
|
|
10672
|
-
|
|
10805
|
+
const before = forkEnv(env);
|
|
10806
|
+
const leftFailed = forkEnv(env);
|
|
10807
|
+
this.applyNarrowing(left, env, t, leftFailed);
|
|
10808
|
+
const between = forkEnv(t);
|
|
10809
|
+
const rightFailed = forkEnv(t);
|
|
10810
|
+
this.applyNarrowing(right, t, t, rightFailed);
|
|
10811
|
+
joinNarrowings(f, leftFailed, before, rightFailed, between);
|
|
10673
10812
|
return;
|
|
10674
10813
|
}
|
|
10675
10814
|
if (op === "or") {
|
|
10676
|
-
|
|
10677
|
-
|
|
10815
|
+
const before = forkEnv(env);
|
|
10816
|
+
const leftHeld = forkEnv(env);
|
|
10817
|
+
this.applyNarrowing(left, env, leftHeld, f);
|
|
10818
|
+
const between = forkEnv(f);
|
|
10819
|
+
const rightHeld = forkEnv(f);
|
|
10820
|
+
this.applyNarrowing(right, f, rightHeld, f);
|
|
10821
|
+
joinNarrowings(t, leftHeld, before, rightHeld, between);
|
|
10678
10822
|
return;
|
|
10679
10823
|
}
|
|
10680
10824
|
if (op === "==" || op === "~=") {
|
|
@@ -10693,11 +10837,13 @@ var TypeAnalyzer = class {
|
|
|
10693
10837
|
this.narrowOptionalLinks(cond, env, t);
|
|
10694
10838
|
}
|
|
10695
10839
|
/** `a == b` / `a ~= b`. Handles, in order: a declaration-driven
|
|
10696
|
-
* `typeof
|
|
10840
|
+
* `typeof x == "..."` test, a literal/`nil` comparison against a
|
|
10697
10841
|
* reference, and a reference-to-reference comparison. */
|
|
10698
10842
|
narrowByComparison(left, right, eq, env, t, f) {
|
|
10699
10843
|
const yes = eq ? t : f;
|
|
10700
10844
|
const no = eq ? f : t;
|
|
10845
|
+
if (this.narrowByTypeof(left, right, yes, no, env)) return;
|
|
10846
|
+
if (this.narrowByTypeof(right, left, yes, no, env)) return;
|
|
10701
10847
|
if (this.narrowByCallResult(left, right, yes, no, env)) return;
|
|
10702
10848
|
if (this.narrowByCallResult(right, left, yes, no, env)) return;
|
|
10703
10849
|
const litOf = (e) => {
|
|
@@ -10724,18 +10870,149 @@ var TypeAnalyzer = class {
|
|
|
10724
10870
|
this.narrowRef(right, env, yes, no, (cur) => ({ yes: narrowTo(cur, lt), no: cur }));
|
|
10725
10871
|
}
|
|
10726
10872
|
}
|
|
10727
|
-
|
|
10728
|
-
|
|
10729
|
-
|
|
10730
|
-
|
|
10731
|
-
|
|
10732
|
-
|
|
10733
|
-
|
|
10734
|
-
|
|
10873
|
+
// --------------------------------------------------------
|
|
10874
|
+
// typeof
|
|
10875
|
+
// --------------------------------------------------------
|
|
10876
|
+
//
|
|
10877
|
+
// `typeof v` answers what Luau's `typeof` does, except for a table: that
|
|
10878
|
+
// is an `"array"` or an `"object"`. The names of a runtime's own values —
|
|
10879
|
+
// Roblox's `"Instance"`, `"Vector3"` — are a library's to give: it
|
|
10880
|
+
// declares `type TypeofNames = { Vector3: Vector3, Instance: Instance }`,
|
|
10881
|
+
// each property the name `typeof` answers for a value of that type. A
|
|
10882
|
+
// value several of them fit is the first one's.
|
|
10883
|
+
typeofTypesCache;
|
|
10884
|
+
/** Every name `typeof` answers, and what a value it answered that for
|
|
10885
|
+
* is — `unknown` where nothing more is known. */
|
|
10886
|
+
typeofTypes() {
|
|
10887
|
+
if (this.typeofTypesCache) return this.typeofTypesCache;
|
|
10888
|
+
const types = /* @__PURE__ */ new Map([
|
|
10889
|
+
["nil", nilType],
|
|
10890
|
+
["boolean", booleanType],
|
|
10891
|
+
["number", numberType],
|
|
10892
|
+
["string", stringType],
|
|
10893
|
+
["function", fn([], declaredUnknownType, declaredUnknownType)],
|
|
10894
|
+
["array", arrayOf(declaredUnknownType)],
|
|
10895
|
+
["object", objectType([], { key: declaredUnknownType, value: declaredUnknownType })],
|
|
10896
|
+
["thread", threadType],
|
|
10897
|
+
["buffer", bufferType],
|
|
10898
|
+
["userdata", unknownType]
|
|
10899
|
+
]);
|
|
10900
|
+
for (const [name, type] of this.typeofLibraryNames()) if (!types.has(name)) types.set(name, type);
|
|
10901
|
+
return this.typeofTypesCache = types;
|
|
10902
|
+
}
|
|
10903
|
+
typeofLibraryCache;
|
|
10904
|
+
typeofLibraryNames() {
|
|
10905
|
+
if (this.typeofLibraryCache) return this.typeofLibraryCache;
|
|
10906
|
+
const declared = this.aliases.get("TypeofNames");
|
|
10907
|
+
const names = declared && this.expand(declared);
|
|
10908
|
+
return this.typeofLibraryCache = names?.kind === "object" ? [...names.properties].map(([name, property]) => [name, property.type]) : [];
|
|
10909
|
+
}
|
|
10910
|
+
/** The names `typeof` can answer for a value of type `raw`, or
|
|
10911
|
+
* `undefined` where the type does not say. */
|
|
10912
|
+
typeofNamesOf(raw, depth = 0) {
|
|
10913
|
+
if (depth > 8) return void 0;
|
|
10914
|
+
const t = this.expand(raw);
|
|
10915
|
+
switch (t.kind) {
|
|
10916
|
+
case "never":
|
|
10917
|
+
return /* @__PURE__ */ new Set();
|
|
10918
|
+
case "primitive":
|
|
10919
|
+
return /* @__PURE__ */ new Set([t.name]);
|
|
10920
|
+
case "literal":
|
|
10921
|
+
return /* @__PURE__ */ new Set([t.base]);
|
|
10922
|
+
case "templateLiteral":
|
|
10923
|
+
return /* @__PURE__ */ new Set(["string"]);
|
|
10924
|
+
case "array":
|
|
10925
|
+
case "tuple":
|
|
10926
|
+
return /* @__PURE__ */ new Set(["array"]);
|
|
10927
|
+
case "function":
|
|
10928
|
+
return /* @__PURE__ */ new Set(["function"]);
|
|
10929
|
+
case "object": {
|
|
10930
|
+
const named = t.class && this.typeofLibraryNames().find(([, type]) => isAssignable(t, type));
|
|
10931
|
+
return /* @__PURE__ */ new Set([named ? named[0] : "object"]);
|
|
10932
|
+
}
|
|
10933
|
+
case "union": {
|
|
10934
|
+
const all = /* @__PURE__ */ new Set();
|
|
10935
|
+
for (const member of t.types) {
|
|
10936
|
+
const names = this.typeofNamesOf(member, depth + 1);
|
|
10937
|
+
if (!names) return void 0;
|
|
10938
|
+
for (const name of names) all.add(name);
|
|
10939
|
+
}
|
|
10940
|
+
return all;
|
|
10941
|
+
}
|
|
10942
|
+
case "intersection": {
|
|
10943
|
+
const known = t.types.map((part) => this.typeofNamesOf(part, depth + 1)).filter((names) => names !== void 0);
|
|
10944
|
+
if (!known.length) return void 0;
|
|
10945
|
+
const common = [...known[0]].filter((name) => known.every((names) => names.has(name)));
|
|
10946
|
+
return new Set(common.length ? common : known[0]);
|
|
10947
|
+
}
|
|
10948
|
+
case "typeParam":
|
|
10949
|
+
return t.constraint ? this.typeofNamesOf(t.constraint, depth + 1) : void 0;
|
|
10950
|
+
case "difference":
|
|
10951
|
+
return this.typeofNamesOf(t.base, depth + 1);
|
|
10952
|
+
default:
|
|
10953
|
+
return void 0;
|
|
10954
|
+
}
|
|
10955
|
+
}
|
|
10956
|
+
/** `typeof v == "table"`: a name `typeof` never answers. Returns true if
|
|
10957
|
+
* the comparison needs no further checking. */
|
|
10958
|
+
checkTypeofName(node, typeofSide, nameSide) {
|
|
10959
|
+
const test = unwrapParens(typeofSide);
|
|
10960
|
+
if (test.type !== "UnaryExpression" || test.operator !== "typeof") return false;
|
|
10961
|
+
const name = this.asLiteral(unwrapParens(nameSide));
|
|
10962
|
+
if (typeof name !== "string") return false;
|
|
10963
|
+
if (name === "nil") return true;
|
|
10964
|
+
if (this.typeofTypes().has(name)) return false;
|
|
10965
|
+
if (this.emitDiagnostics) {
|
|
10966
|
+
this.diagnostics.push({
|
|
10967
|
+
node,
|
|
10968
|
+
message: name === "table" ? `'typeof' never answers "table": a table is an "array" or an "object"` : `'typeof' never answers "${name}"`
|
|
10969
|
+
});
|
|
10970
|
+
}
|
|
10971
|
+
return true;
|
|
10972
|
+
}
|
|
10973
|
+
/** `typeof x == "number"`: `x` is what answers that name where the test
|
|
10974
|
+
* holds, and what does not where it fails. Returns true if `testSide` is
|
|
10975
|
+
* a `typeof` compared with a name. */
|
|
10976
|
+
narrowByTypeof(testSide, nameSide, yes, no, env) {
|
|
10977
|
+
const test = unwrapParens(testSide);
|
|
10978
|
+
if (test.type !== "UnaryExpression" || test.operator !== "typeof") return false;
|
|
10979
|
+
const name = this.asLiteral(unwrapParens(nameSide));
|
|
10980
|
+
if (typeof name !== "string") return false;
|
|
10981
|
+
const answered = this.typeofTypes().get(name);
|
|
10982
|
+
if (!answered) return true;
|
|
10983
|
+
const opaque = answered.kind !== "unknown";
|
|
10984
|
+
const subtracts = opaque && name !== "array" && name !== "object";
|
|
10985
|
+
const members = (cur) => {
|
|
10986
|
+
const t = this.expand(cur);
|
|
10987
|
+
return t.kind === "union" ? t.types.flatMap(members) : [t];
|
|
10988
|
+
};
|
|
10989
|
+
if (this.refKeyOf(test.argument) !== void 0) {
|
|
10990
|
+
this.narrowRef(test.argument, env, yes, no, (cur) => {
|
|
10991
|
+
const kept = [];
|
|
10992
|
+
const dropped = [];
|
|
10993
|
+
for (const member of members(cur)) {
|
|
10994
|
+
const names = this.typeofNamesOf(member);
|
|
10995
|
+
if (!names) {
|
|
10996
|
+
kept.push(opaque ? narrowTo(member, answered) : member);
|
|
10997
|
+
dropped.push(subtracts ? narrowExclude(member, answered) : member);
|
|
10998
|
+
continue;
|
|
10999
|
+
}
|
|
11000
|
+
if (names.has(name)) kept.push(member);
|
|
11001
|
+
if (!(names.size === 1 && names.has(name))) dropped.push(member);
|
|
11002
|
+
}
|
|
11003
|
+
return { yes: union(kept), no: union(dropped) };
|
|
11004
|
+
});
|
|
11005
|
+
}
|
|
11006
|
+
this.narrowOptionalLinks(test.argument, env, name === "nil" ? no : yes);
|
|
11007
|
+
return true;
|
|
11008
|
+
}
|
|
11009
|
+
/** Declaration-driven `kind(x) == "number"`.
|
|
10735
11010
|
*
|
|
10736
|
-
*
|
|
10737
|
-
*
|
|
10738
|
-
*
|
|
11011
|
+
* A function declared as an overload set whose members return
|
|
11012
|
+
* *string-literal* types (`(value: number) => "number"`, ...) says by its
|
|
11013
|
+
* answer what it was given. So for `f(x) == "lit"` we keep the parameter
|
|
11014
|
+
* types of the overloads that can return `"lit"`, and that union is what
|
|
11015
|
+
* `x` narrows to. Returns true if it handled the comparison. */
|
|
10739
11016
|
narrowByCallResult(callSide, litSide, yes, no, env) {
|
|
10740
11017
|
if (callSide.type !== "CallExpression" || callSide.arguments.length !== 1) return false;
|
|
10741
11018
|
const name = this.asLiteral(litSide);
|