@tilua/parser 2.0.0 → 2.1.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 +16 -3
- package/dist/index.cjs +282 -43
- package/dist/index.d.cts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +282 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -341,9 +341,11 @@ name: T | nil -- must be written, but may be nil
|
|
|
341
341
|
Omitting an argument requires `?` (or a default), as in TypeScript — a
|
|
342
342
|
parameter typed `T | nil` still has to be passed something.
|
|
343
343
|
|
|
344
|
-
**Optional chaining** — `a?.b` and `a?:m(x)` are nil when
|
|
345
|
-
nothing further along the chain runs, arguments
|
|
346
|
-
is a `string | nil
|
|
344
|
+
**Optional chaining** — `a?.b`, `a?.[k]`, `a?.(x)` and `a?:m(x)` are nil when
|
|
345
|
+
`a` is, and then nothing further along the chain runs, arguments and the key
|
|
346
|
+
included: `folder?:FindFirstChild("A")?.Name` is a `string | nil`, and
|
|
347
|
+
`maps?.[key]?.label` reads the key only when `maps` is there. The `?` must
|
|
348
|
+
touch the `.` or `:`; `c ? a : b` stays a
|
|
347
349
|
ternary. Parentheses end a chain. A chain cannot be assigned to (`a?.b = 1` is
|
|
348
350
|
an error). A chain that got through narrows what it tested: inside
|
|
349
351
|
`if (part?.Parent)`, and `if (part?.Name == "Door")`, `part` is not nil.
|
|
@@ -743,6 +745,17 @@ non-nil part.
|
|
|
743
745
|
|
|
744
746
|
Only `nil` and `false` are falsy — `0` and `""` are truthy, unlike JavaScript.
|
|
745
747
|
|
|
748
|
+
**What Luau raises on is said first.** An operator no metamethod answers takes
|
|
749
|
+
numbers — `..` a string or a number, `#` a string or a table — so `n * 2` with
|
|
750
|
+
`n` a `number | nil` is an error where it is written rather than where it runs.
|
|
751
|
+
A comparison two types can never settle (`s == "c"` with `s` a `"a" | "b"`) is
|
|
752
|
+
one too; a test against `nil` never is, since a map's value and an array's
|
|
753
|
+
element are read as what they hold. A name holds nothing until its line has
|
|
754
|
+
run, a list is indexed by position (`xs["a"]` is not a read), `readonly T[]`
|
|
755
|
+
rules out writing to the list as well as passing it where a `number[]` is
|
|
756
|
+
wanted, a key written twice in one table is reported, and a parameter with no
|
|
757
|
+
type says so — it is `any`, and `any` turns off every check made of it.
|
|
758
|
+
|
|
746
759
|
**Types** — unions, intersections, tuples `[A, B]` (with a rest, `[A,
|
|
747
760
|
...B[]]`), `keyof`, `T[K]`, conditional types with
|
|
748
761
|
`infer`, mapped types with `as` remapping, template literal types
|
package/dist/index.cjs
CHANGED
|
@@ -2503,6 +2503,21 @@ var Parser = class {
|
|
|
2503
2503
|
base = { type: "MemberExpression", object: base, property: prop, optional: true, ...spanFrom(base, prop) };
|
|
2504
2504
|
continue;
|
|
2505
2505
|
}
|
|
2506
|
+
if (punct === "." && this.punctuatorAt(2, "[")) {
|
|
2507
|
+
this.advance();
|
|
2508
|
+
this.advance();
|
|
2509
|
+
this.expectPunctuator("[");
|
|
2510
|
+
const index = this.inBrackets(() => this.parseExpression());
|
|
2511
|
+
this.expectPunctuator("]");
|
|
2512
|
+
base = {
|
|
2513
|
+
type: "IndexExpression",
|
|
2514
|
+
object: base,
|
|
2515
|
+
index,
|
|
2516
|
+
optional: true,
|
|
2517
|
+
...spanFrom(base, this.previous())
|
|
2518
|
+
};
|
|
2519
|
+
continue;
|
|
2520
|
+
}
|
|
2506
2521
|
if (punct === "." && this.punctuatorAt(2, "(")) {
|
|
2507
2522
|
this.advance();
|
|
2508
2523
|
this.advance();
|
|
@@ -3256,8 +3271,12 @@ var Parser = class {
|
|
|
3256
3271
|
* `name?: T` for an optional property or parameter. */
|
|
3257
3272
|
parseSuffixType() {
|
|
3258
3273
|
if (this.checkIdentifierValue("readonly")) {
|
|
3274
|
+
const start = this.current();
|
|
3259
3275
|
this.advance();
|
|
3260
|
-
|
|
3276
|
+
const t2 = this.parseSuffixType();
|
|
3277
|
+
if (t2.type === "ArrayTypeNode" || t2.type === "TupleTypeNode") return { ...t2, isReadonly: true };
|
|
3278
|
+
this.problemAt(start, "'readonly' belongs on an array or a tuple: a property is marked where it is written");
|
|
3279
|
+
return t2;
|
|
3261
3280
|
}
|
|
3262
3281
|
let t = this.parsePrimaryType();
|
|
3263
3282
|
while (true) {
|
|
@@ -3808,31 +3827,34 @@ var Analyzer = class {
|
|
|
3808
3827
|
* can call it, but a call straight in the block before the declaration
|
|
3809
3828
|
* finds nothing there yet. At a module's top level the whole function is
|
|
3810
3829
|
* hoisted, and this does not apply. */
|
|
3811
|
-
checkUseBeforeDefine(identifier, id) {
|
|
3830
|
+
checkUseBeforeDefine(identifier, id, at = this.functionDepth) {
|
|
3812
3831
|
const binding = this.bindings.get(id);
|
|
3813
|
-
if (
|
|
3832
|
+
if (this.typeQueryDepth > 0) return;
|
|
3814
3833
|
const declaration = binding.declarationNode;
|
|
3815
|
-
|
|
3816
|
-
|
|
3834
|
+
if (!declaration) return;
|
|
3835
|
+
const hoisted = binding.declaredBy === "function" || binding.declaredBy === "class";
|
|
3836
|
+
const depth = hoisted ? this.hoisted.get(declaration) : this.declaredDepth.get(id);
|
|
3837
|
+
const readable = hoisted || binding.kind === "local";
|
|
3838
|
+
if (!readable || depth === void 0 || depth !== at) return;
|
|
3817
3839
|
const before = identifier.line.start < declaration.line.start || identifier.line.start === declaration.line.start && identifier.column.start < declaration.column.start;
|
|
3818
3840
|
if (!before) return;
|
|
3819
3841
|
this.diagnostics.push({
|
|
3820
3842
|
node: identifier,
|
|
3821
|
-
message: `'${binding.name}' is used before its definition: inside a function, a function declared further down is only there once its declaration has run`,
|
|
3843
|
+
message: hoisted ? `'${binding.name}' is used before its definition: inside a function, a function declared further down is only there once its declaration has run` : `'${binding.name}' is used before its declaration, and holds nothing until that line has run`,
|
|
3822
3844
|
kind: "use-before-define"
|
|
3823
3845
|
});
|
|
3824
3846
|
}
|
|
3825
3847
|
noteDeferred(identifier, scope, id, assignment) {
|
|
3826
3848
|
if (this.functionDepth === 0 && this.typeQueryDepth === 0) return;
|
|
3827
3849
|
if (this.bindings.get(id).kind !== "global") return;
|
|
3828
|
-
this.deferredGlobals.push({ node: identifier, scope, assignment });
|
|
3850
|
+
this.deferredGlobals.push({ node: identifier, scope, assignment, depth: this.functionDepth });
|
|
3829
3851
|
}
|
|
3830
3852
|
/** Point each deferred read of a global at the declaration of that name
|
|
3831
3853
|
* that turned up later — in the module, or in any block around the code
|
|
3832
3854
|
* that reads it. Such code runs after the declaration has: a closure
|
|
3833
3855
|
* written inside a value reads the name the value is bound to. */
|
|
3834
3856
|
resolveForwardReferences() {
|
|
3835
|
-
for (const { node, scope, assignment } of this.deferredGlobals) {
|
|
3857
|
+
for (const { node, scope, assignment, depth } of this.deferredGlobals) {
|
|
3836
3858
|
const localId = this.lookup(scope, node.name);
|
|
3837
3859
|
const globalId = this.bindingOf.get(node);
|
|
3838
3860
|
if (localId === void 0 || globalId === void 0 || localId === globalId) continue;
|
|
@@ -3847,7 +3869,10 @@ var Analyzer = class {
|
|
|
3847
3869
|
this.bindingOf.set(node, localId);
|
|
3848
3870
|
this.bindings.get(localId).references.push(node);
|
|
3849
3871
|
if (assignment) this.checkConstAssign(localId, node);
|
|
3850
|
-
else if (this.typeQueryDepth === 0)
|
|
3872
|
+
else if (this.typeQueryDepth === 0) {
|
|
3873
|
+
this.checkTypeOnly(localId, node);
|
|
3874
|
+
this.checkUseBeforeDefine(node, localId, depth);
|
|
3875
|
+
}
|
|
3851
3876
|
}
|
|
3852
3877
|
}
|
|
3853
3878
|
/** Every read of a global nothing declares. A global assigned somewhere
|
|
@@ -3878,9 +3903,13 @@ var Analyzer = class {
|
|
|
3878
3903
|
}
|
|
3879
3904
|
const id = this.nextId++;
|
|
3880
3905
|
this.bindings.set(id, { id, name, kind, declarationNode: node, references: [], isConst, declaredBy });
|
|
3906
|
+
this.declaredDepth.set(id, this.functionDepth);
|
|
3881
3907
|
scope.declarations.set(name, id);
|
|
3882
3908
|
return id;
|
|
3883
3909
|
}
|
|
3910
|
+
/** Which function body each name was declared in. A read of a `const` from
|
|
3911
|
+
* the same body, but above it, is a read of nothing. */
|
|
3912
|
+
declaredDepth = /* @__PURE__ */ new Map();
|
|
3884
3913
|
resolve(scope, name) {
|
|
3885
3914
|
for (let s = scope; s; s = s.parent) {
|
|
3886
3915
|
const id = s.declarations.get(name);
|
|
@@ -4533,9 +4562,9 @@ function substitute(t, subst) {
|
|
|
4533
4562
|
case "typeParam":
|
|
4534
4563
|
return subst.get(t.name) ?? t;
|
|
4535
4564
|
case "array":
|
|
4536
|
-
return arrayOf(substitute(t.element, subst));
|
|
4565
|
+
return { ...arrayOf(substitute(t.element, subst)), readonly: t.readonly };
|
|
4537
4566
|
case "tuple":
|
|
4538
|
-
return tuple(t.elements.map((e) => substitute(e, subst)), t.rest && substitute(t.rest, subst));
|
|
4567
|
+
return { ...tuple(t.elements.map((e) => substitute(e, subst)), t.rest && substitute(t.rest, subst)), readonly: t.readonly };
|
|
4539
4568
|
case "object": {
|
|
4540
4569
|
const entries = [];
|
|
4541
4570
|
for (const [k, v] of t.properties) entries.push([k, { ...v, type: substitute(v.type, subst) }]);
|
|
@@ -4700,6 +4729,20 @@ function unify(param, arg, vars, out) {
|
|
|
4700
4729
|
return;
|
|
4701
4730
|
}
|
|
4702
4731
|
}
|
|
4732
|
+
function memberKey(t) {
|
|
4733
|
+
switch (t.kind) {
|
|
4734
|
+
case "literal":
|
|
4735
|
+
return `l${t.base}:${String(t.value)}`;
|
|
4736
|
+
case "primitive":
|
|
4737
|
+
return `p${t.name}`;
|
|
4738
|
+
case "any":
|
|
4739
|
+
case "unknown":
|
|
4740
|
+
case "never":
|
|
4741
|
+
return t.kind;
|
|
4742
|
+
default:
|
|
4743
|
+
return formatType(t);
|
|
4744
|
+
}
|
|
4745
|
+
}
|
|
4703
4746
|
function union(types) {
|
|
4704
4747
|
const flat = [];
|
|
4705
4748
|
for (const t of types) {
|
|
@@ -4711,7 +4754,7 @@ function union(types) {
|
|
|
4711
4754
|
const seen = /* @__PURE__ */ new Map();
|
|
4712
4755
|
for (const t of flat) {
|
|
4713
4756
|
if (t.kind === "never") continue;
|
|
4714
|
-
const key =
|
|
4757
|
+
const key = memberKey(t);
|
|
4715
4758
|
if (!seen.has(key)) seen.set(key, t);
|
|
4716
4759
|
}
|
|
4717
4760
|
let members = reduceSubtypes([...seen.values()]);
|
|
@@ -4740,7 +4783,7 @@ function intersection(types) {
|
|
|
4740
4783
|
const seen = /* @__PURE__ */ new Map();
|
|
4741
4784
|
for (const t of flat) {
|
|
4742
4785
|
if (t.kind === "unknown") continue;
|
|
4743
|
-
seen.set(
|
|
4786
|
+
seen.set(memberKey(t), t);
|
|
4744
4787
|
}
|
|
4745
4788
|
const members = [...seen.values()];
|
|
4746
4789
|
if (members.length === 0) return unknownType;
|
|
@@ -4767,9 +4810,9 @@ function widen(t) {
|
|
|
4767
4810
|
case "literal":
|
|
4768
4811
|
return primitive(t.base);
|
|
4769
4812
|
case "array":
|
|
4770
|
-
return arrayOf(widen(t.element));
|
|
4813
|
+
return { ...arrayOf(widen(t.element)), readonly: t.readonly };
|
|
4771
4814
|
case "tuple":
|
|
4772
|
-
return tuple(t.elements.map(widen), t.rest && widen(t.rest));
|
|
4815
|
+
return { ...tuple(t.elements.map(widen), t.rest && widen(t.rest)), readonly: t.readonly };
|
|
4773
4816
|
case "object": {
|
|
4774
4817
|
if (t.frozen || t.class) return t;
|
|
4775
4818
|
const entries = [];
|
|
@@ -4867,6 +4910,7 @@ function isAssignableInner(a, b) {
|
|
|
4867
4910
|
return b.kind === "primitive" && b.name === "string";
|
|
4868
4911
|
}
|
|
4869
4912
|
if (a.kind === "primitive") return b.kind === "primitive" && b.name === a.name;
|
|
4913
|
+
if ((a.kind === "array" || a.kind === "tuple") && a.readonly && (b.kind === "array" || b.kind === "tuple") && !b.readonly) return false;
|
|
4870
4914
|
if (a.kind === "array") {
|
|
4871
4915
|
if (b.kind === "array") return isAssignable(a.element, b.element);
|
|
4872
4916
|
if (b.kind === "tuple") return b.elements.length === 0 && !!b.rest && isAssignable(a.element, b.rest);
|
|
@@ -5212,10 +5256,10 @@ function formatTypeUncached(t) {
|
|
|
5212
5256
|
case "literal":
|
|
5213
5257
|
return t.base === "string" ? JSON.stringify(t.value) : String(t.value);
|
|
5214
5258
|
case "array":
|
|
5215
|
-
return `${formatAtom(t.element)}[]`;
|
|
5259
|
+
return `${t.readonly ? "readonly " : ""}${formatAtom(t.element)}[]`;
|
|
5216
5260
|
case "tuple": {
|
|
5217
5261
|
const inner = [...t.elements.map(formatType), ...t.rest ? [`...${formatAtom(t.rest)}[]`] : []];
|
|
5218
|
-
return
|
|
5262
|
+
return `${t.readonly ? "readonly " : ""}[${inner.join(", ")}]`;
|
|
5219
5263
|
}
|
|
5220
5264
|
case "object": {
|
|
5221
5265
|
if (t.name) {
|
|
@@ -5738,7 +5782,7 @@ function expressionLabel(e, depth = 0) {
|
|
|
5738
5782
|
case "IndexExpression": {
|
|
5739
5783
|
const o = expressionLabel(e.object, depth + 1);
|
|
5740
5784
|
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 : "...";
|
|
5741
|
-
return o === void 0 ? void 0 : `${o}[${i}]`;
|
|
5785
|
+
return o === void 0 ? void 0 : `${o}${e.optional ? "?." : ""}[${i}]`;
|
|
5742
5786
|
}
|
|
5743
5787
|
case "ParenthesizedExpression": {
|
|
5744
5788
|
const inner = expressionLabel(e.expression, depth + 1);
|
|
@@ -5789,7 +5833,8 @@ var TypeAnalyzer = class {
|
|
|
5789
5833
|
/** Put on every ref to one of `aliasDefs`, so a module that imports the
|
|
5790
5834
|
* type expands the ref here rather than by its name there. */
|
|
5791
5835
|
origin = { expand: (t) => this.expand(t) };
|
|
5792
|
-
/** See `resolveClass`.
|
|
5836
|
+
/** See `resolveClass`. Shared with other analyses over the same libraries
|
|
5837
|
+
* when none of their names is redefined here; see `shareableClasses`. */
|
|
5793
5838
|
classTypes = /* @__PURE__ */ new WeakMap();
|
|
5794
5839
|
/** See `instanceType` — one instance type per `class ... end`. */
|
|
5795
5840
|
instanceTypes = /* @__PURE__ */ new WeakMap();
|
|
@@ -5835,6 +5880,11 @@ var TypeAnalyzer = class {
|
|
|
5835
5880
|
/** Recursion guard for `preVisitBody`. */
|
|
5836
5881
|
preVisitDepth = 0;
|
|
5837
5882
|
run() {
|
|
5883
|
+
const shared = shareableClasses(this.options.libs ?? [], this.program);
|
|
5884
|
+
if (shared) {
|
|
5885
|
+
this.classTypes = shared.classTypes;
|
|
5886
|
+
this.classMembers = shared.classMembers;
|
|
5887
|
+
}
|
|
5838
5888
|
this.registerAliasDefs(preludeProgram().body, true);
|
|
5839
5889
|
for (const lib of this.options.libs ?? []) this.registerAliasDefs(lib.body, true);
|
|
5840
5890
|
this.registerAliasDefs(this.program.body);
|
|
@@ -7000,14 +7050,17 @@ var TypeAnalyzer = class {
|
|
|
7000
7050
|
return literal(node.value);
|
|
7001
7051
|
case "TypeLiteralNumber":
|
|
7002
7052
|
return literal(node.value);
|
|
7003
|
-
case "ArrayTypeNode":
|
|
7004
|
-
|
|
7053
|
+
case "ArrayTypeNode": {
|
|
7054
|
+
const made = arrayOf(this.resolveType(node.element));
|
|
7055
|
+
return node.isReadonly ? { ...made, readonly: true } : made;
|
|
7056
|
+
}
|
|
7005
7057
|
case "TupleTypeNode": {
|
|
7006
7058
|
const rest = node.rest && this.expand(this.resolveType(node.rest));
|
|
7007
|
-
|
|
7059
|
+
const made = tuple(
|
|
7008
7060
|
node.elements.map((e) => this.resolveType(e)),
|
|
7009
7061
|
rest ? rest.kind === "array" ? rest.element : rest.kind === "any" ? anyType : unknownType : void 0
|
|
7010
7062
|
);
|
|
7063
|
+
return node.isReadonly && made.kind === "tuple" ? { ...made, readonly: true } : made;
|
|
7011
7064
|
}
|
|
7012
7065
|
case "UnionTypeNode":
|
|
7013
7066
|
return union(node.types.map((t) => this.resolveType(t)));
|
|
@@ -7827,8 +7880,19 @@ var TypeAnalyzer = class {
|
|
|
7827
7880
|
if (p.rest) return arrayOf(unknownType);
|
|
7828
7881
|
if (p.pattern) return this.patternToType(p.pattern, env);
|
|
7829
7882
|
if (p.default) return widen(this.infer(p.default, env));
|
|
7830
|
-
|
|
7883
|
+
const contextual = this.contextualParams.get(p);
|
|
7884
|
+
if (contextual) return contextual;
|
|
7885
|
+
if (this.emitDiagnostics && !receiver && p.name && p.line && !this.untypedParamReported.has(p)) {
|
|
7886
|
+
this.untypedParamReported.add(p);
|
|
7887
|
+
this.diagnostics.push({
|
|
7888
|
+
node: p,
|
|
7889
|
+
message: `Parameter '${p.name}' has no type, so it is 'any': give it one, or a default to read it from`
|
|
7890
|
+
});
|
|
7891
|
+
}
|
|
7892
|
+
return anyType;
|
|
7831
7893
|
}
|
|
7894
|
+
/** Parameters already spoken about: a body is visited more than once. */
|
|
7895
|
+
untypedParamReported = /* @__PURE__ */ new WeakSet();
|
|
7832
7896
|
/** What a function expression's unannotated parameters are, from where
|
|
7833
7897
|
* it is written — see `applyContext`. */
|
|
7834
7898
|
contextualParams = /* @__PURE__ */ new WeakMap();
|
|
@@ -8123,6 +8187,11 @@ var TypeAnalyzer = class {
|
|
|
8123
8187
|
* this returns `undefined` and the ordinary pick stands. */
|
|
8124
8188
|
distributedReturn(fns, argTypes, picked, argsFor) {
|
|
8125
8189
|
if (fns.length < 2) return void 0;
|
|
8190
|
+
if (argTypes.some((t) => this.expand(t).kind === "any")) {
|
|
8191
|
+
const taken = fns.filter((f) => this.overloadAccepts(f, argsFor(f, argTypes)));
|
|
8192
|
+
if (taken.length < 2) return void 0;
|
|
8193
|
+
return union(taken.map((f) => this.callReturn(f, argsFor(f, argTypes))));
|
|
8194
|
+
}
|
|
8126
8195
|
const position = argTypes.findIndex((t) => this.expand(t).kind === "union");
|
|
8127
8196
|
if (position < 0) return void 0;
|
|
8128
8197
|
const members = this.expand(argTypes[position]).types;
|
|
@@ -8171,6 +8240,8 @@ var TypeAnalyzer = class {
|
|
|
8171
8240
|
* lone argument cannot be checked against without false errors. */
|
|
8172
8241
|
boundParams(f) {
|
|
8173
8242
|
if (!f.typeParams?.length) return f.params.map((p) => p.type);
|
|
8243
|
+
const known = this.boundParamsCache.get(f);
|
|
8244
|
+
if (known) return known;
|
|
8174
8245
|
const bounds = new Map(f.typeParams.map((name) => [name, anyType]));
|
|
8175
8246
|
const seen = /* @__PURE__ */ new WeakSet();
|
|
8176
8247
|
const walk = (value) => {
|
|
@@ -8188,8 +8259,11 @@ var TypeAnalyzer = class {
|
|
|
8188
8259
|
for (const child of Object.values(value)) walk(child);
|
|
8189
8260
|
};
|
|
8190
8261
|
for (const p of f.params) if (containsTypeParam(p.type)) walk(p.type);
|
|
8191
|
-
|
|
8262
|
+
const bound = f.params.map((p) => this.reduceType(substitute(p.type, bounds)));
|
|
8263
|
+
this.boundParamsCache.set(f, bound);
|
|
8264
|
+
return bound;
|
|
8192
8265
|
}
|
|
8266
|
+
boundParamsCache = /* @__PURE__ */ new WeakMap();
|
|
8193
8267
|
/** Record what each written argument is expected to be — see
|
|
8194
8268
|
* `TypeAnalysis.expectedTypeOf`. */
|
|
8195
8269
|
recordExpected(written, fns, selfOf, argsOf = () => []) {
|
|
@@ -8653,7 +8727,7 @@ var TypeAnalyzer = class {
|
|
|
8653
8727
|
/** What one `for (const item in source)` hands over each time, and how it
|
|
8654
8728
|
* walks the source. `pairs`/`ipairs` are read from the table they are
|
|
8655
8729
|
* given, so a record's keys stay the literals they are. */
|
|
8656
|
-
iterationOf(source, sourceType) {
|
|
8730
|
+
iterationOf(source, sourceType, report = true) {
|
|
8657
8731
|
if (source.type === "CallExpression" && source.callee.type === "Identifier" && source.arguments[0]) {
|
|
8658
8732
|
const name = source.callee.name;
|
|
8659
8733
|
const src = this.expand(this.typeOf.get(source.arguments[0]) ?? unknownType);
|
|
@@ -8691,7 +8765,14 @@ var TypeAnalyzer = class {
|
|
|
8691
8765
|
if (t.kind === "object") {
|
|
8692
8766
|
return [t.indexer?.value ?? union([...t.properties.values()].map((p) => p.type)), values];
|
|
8693
8767
|
}
|
|
8694
|
-
if (t.kind
|
|
8768
|
+
if (t.kind === "union") {
|
|
8769
|
+
const each = t.types.map((member) => this.iterationOf(source, member, false));
|
|
8770
|
+
const [first] = each;
|
|
8771
|
+
if (first && each.every(([, form]) => form.walks === first[1].walks && form.viaIter === first[1].viaIter)) {
|
|
8772
|
+
return [union(each.map(([item]) => item)), first[1]];
|
|
8773
|
+
}
|
|
8774
|
+
}
|
|
8775
|
+
if (t.kind !== "never" && report && this.emitDiagnostics) {
|
|
8695
8776
|
this.diagnostics.push({
|
|
8696
8777
|
node: source,
|
|
8697
8778
|
message: `Cannot loop over '${formatType(sourceType)}': a loop walks an array, a table, an iterator function or an iteration such as 'pairs(t)'`
|
|
@@ -9060,7 +9141,14 @@ var TypeAnalyzer = class {
|
|
|
9060
9141
|
const names = keys.map(nameOf);
|
|
9061
9142
|
const others = keys.filter((_, i) => names[i] === void 0);
|
|
9062
9143
|
let message;
|
|
9063
|
-
|
|
9144
|
+
const listOnly = (t) => {
|
|
9145
|
+
const x = this.expand(t);
|
|
9146
|
+
if (x.kind === "union") return x.types.every(listOnly);
|
|
9147
|
+
return x.kind === "array" || x.kind === "tuple";
|
|
9148
|
+
};
|
|
9149
|
+
if (names.some((name) => name !== void 0) && listOnly(withoutNil(this.expand(object)))) {
|
|
9150
|
+
message = `Type '${briefType(index)}' cannot be used to index type '${shown}'`;
|
|
9151
|
+
} else if (others.length && this.cannotIndex(object, others)) {
|
|
9064
9152
|
message = `Type '${briefType(index)}' cannot be used to index type '${shown}'`;
|
|
9065
9153
|
} else if (!others.length && names.every((name) => this.memberMissing(object, name))) {
|
|
9066
9154
|
message = keys.length === 1 ? `Property '${names[0]}' does not exist on type '${shown}'` : `None of ${briefType(index)} is a property of type '${shown}'`;
|
|
@@ -9332,10 +9420,18 @@ var TypeAnalyzer = class {
|
|
|
9332
9420
|
switch (expr.operator) {
|
|
9333
9421
|
case "not":
|
|
9334
9422
|
return booleanType;
|
|
9335
|
-
case "-":
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9423
|
+
case "-": {
|
|
9424
|
+
const answered = this.operatorResult(expr, "-", arg, void 0);
|
|
9425
|
+
if (answered) return answered;
|
|
9426
|
+
this.checkOperands(expr, "-", arg);
|
|
9427
|
+
return this.arithmeticOn([arg]);
|
|
9428
|
+
}
|
|
9429
|
+
case "#": {
|
|
9430
|
+
const answered = this.operatorResult(expr, "#", arg, void 0);
|
|
9431
|
+
if (answered) return answered;
|
|
9432
|
+
this.checkOperands(expr, "#", arg);
|
|
9433
|
+
return numberType;
|
|
9434
|
+
}
|
|
9339
9435
|
}
|
|
9340
9436
|
return arg;
|
|
9341
9437
|
}
|
|
@@ -9364,16 +9460,21 @@ var TypeAnalyzer = class {
|
|
|
9364
9460
|
if (unwrapParens(expr.left).type === "StringLiteral") this.expectedTypeOf.set(unwrapParens(expr.left), r);
|
|
9365
9461
|
}
|
|
9366
9462
|
switch (op) {
|
|
9367
|
-
case "..":
|
|
9368
|
-
|
|
9463
|
+
case "..": {
|
|
9464
|
+
const answered = this.operatorResult(expr, op, l, r);
|
|
9465
|
+
if (answered) return answered;
|
|
9466
|
+
this.checkOperands(expr, op, l, r);
|
|
9467
|
+
return stringType;
|
|
9468
|
+
}
|
|
9369
9469
|
case "<":
|
|
9370
9470
|
case ">":
|
|
9371
9471
|
case "<=":
|
|
9372
9472
|
case ">=":
|
|
9373
|
-
this.checkComparison(expr, op, l, r);
|
|
9473
|
+
if (!this.checkComparison(expr, op, l, r)) this.checkOperands(expr, op, l, r);
|
|
9374
9474
|
return booleanType;
|
|
9375
9475
|
case "==":
|
|
9376
9476
|
case "~=":
|
|
9477
|
+
this.checkOverlap(expr, op, l, r);
|
|
9377
9478
|
return booleanType;
|
|
9378
9479
|
case "+":
|
|
9379
9480
|
case "-":
|
|
@@ -9381,8 +9482,12 @@ var TypeAnalyzer = class {
|
|
|
9381
9482
|
case "/":
|
|
9382
9483
|
case "//":
|
|
9383
9484
|
case "%":
|
|
9384
|
-
case "^":
|
|
9385
|
-
|
|
9485
|
+
case "^": {
|
|
9486
|
+
const answered = this.operatorResult(expr, op, l, r);
|
|
9487
|
+
if (answered) return answered;
|
|
9488
|
+
this.checkOperands(expr, op, l, r);
|
|
9489
|
+
return this.arithmeticOn([l, r]);
|
|
9490
|
+
}
|
|
9386
9491
|
}
|
|
9387
9492
|
return union([l, r]);
|
|
9388
9493
|
}
|
|
@@ -9630,6 +9735,14 @@ var TypeAnalyzer = class {
|
|
|
9630
9735
|
name = target.property.name;
|
|
9631
9736
|
object = target.object;
|
|
9632
9737
|
} else if (target.type === "IndexExpression") {
|
|
9738
|
+
const list = this.expand(this.infer(target.object, env));
|
|
9739
|
+
if ((list.kind === "array" || list.kind === "tuple") && list.readonly) {
|
|
9740
|
+
this.diagnostics.push({
|
|
9741
|
+
node: target,
|
|
9742
|
+
message: `Cannot assign to an element of '${formatType(list)}': it is read-only`
|
|
9743
|
+
});
|
|
9744
|
+
return;
|
|
9745
|
+
}
|
|
9633
9746
|
const index = this.expand(this.infer(target.index, env));
|
|
9634
9747
|
if (index.kind !== "literal" || typeof index.value !== "string") return;
|
|
9635
9748
|
name = index.value;
|
|
@@ -9802,9 +9915,27 @@ var TypeAnalyzer = class {
|
|
|
9802
9915
|
if (s.indexer) indexer = mergeIndexer(indexer, s.indexer);
|
|
9803
9916
|
}
|
|
9804
9917
|
}
|
|
9918
|
+
this.checkDuplicateKey(expr, field, entries);
|
|
9805
9919
|
}
|
|
9806
9920
|
return objectType(entries, indexer, asConst || void 0);
|
|
9807
9921
|
}
|
|
9922
|
+
/** `{ a: 1, a: 2 }` — the second wins and the first was written for
|
|
9923
|
+
* nothing, which is a typo far more often than a decision. A spread is
|
|
9924
|
+
* not one: overriding what it brought is exactly what it is for. */
|
|
9925
|
+
checkDuplicateKey(expr, field, entries) {
|
|
9926
|
+
if (!this.emitDiagnostics || this.duplicateKeyReported.has(field)) return;
|
|
9927
|
+
const written = field.type === "TableFieldNamed" ? field.key.type === "Identifier" ? field.key.name : field.key.value : field.type === "TableFieldShorthand" ? field.name.name : void 0;
|
|
9928
|
+
if (written === void 0) return;
|
|
9929
|
+
const spreadBefore = expr.fields.slice(0, expr.fields.indexOf(field)).some((f) => f.type === "TableFieldSpread");
|
|
9930
|
+
if (spreadBefore) return;
|
|
9931
|
+
if (entries.filter(([k]) => k === written).length < 2) return;
|
|
9932
|
+
this.duplicateKeyReported.add(field);
|
|
9933
|
+
this.diagnostics.push({
|
|
9934
|
+
node: field.type === "TableFieldNamed" ? field.key : field.name,
|
|
9935
|
+
message: `'${written}' is given twice in this table; only the last one is kept`
|
|
9936
|
+
});
|
|
9937
|
+
}
|
|
9938
|
+
duplicateKeyReported = /* @__PURE__ */ new WeakSet();
|
|
9808
9939
|
/** A value inferred `as const`, widened back wherever `context` does not
|
|
9809
9940
|
* ask for a literal: `satisfies`' result type. A property keeps `"circle"`
|
|
9810
9941
|
* when the contract's property admits string literals, and becomes
|
|
@@ -10297,27 +10428,84 @@ var TypeAnalyzer = class {
|
|
|
10297
10428
|
arithmeticOn(operands) {
|
|
10298
10429
|
return operands.some((t) => this.expand(t).kind === "any") ? anyType : numberType;
|
|
10299
10430
|
}
|
|
10431
|
+
/** `a == b` where nothing `a` can hold is anything `b` can: the answer is
|
|
10432
|
+
* always the same, so the comparison was not the one meant. Only types
|
|
10433
|
+
* that say what they hold take part — `any`, `unknown` and a type
|
|
10434
|
+
* parameter say nothing, and a table compares by identity, so two of them
|
|
10435
|
+
* may well be the same table. */
|
|
10436
|
+
checkOverlap(node, op, l, r) {
|
|
10437
|
+
if (!this.emitDiagnostics) return;
|
|
10438
|
+
const left = this.expand(l);
|
|
10439
|
+
const right = this.expand(r);
|
|
10440
|
+
const judged = (t) => {
|
|
10441
|
+
const parts = t.kind === "union" ? t.types.map((m) => this.expand(m)) : [t];
|
|
10442
|
+
return parts.every((p) => p.kind === "literal" || p.kind === "primitive");
|
|
10443
|
+
};
|
|
10444
|
+
const isNil = (t) => t.kind === "primitive" && t.name === "nil";
|
|
10445
|
+
if (isNil(left) || isNil(right)) return;
|
|
10446
|
+
if (!judged(left) || !judged(right)) return;
|
|
10447
|
+
if (isAssignable(left, right) || isAssignable(right, left)) return;
|
|
10448
|
+
this.diagnostics.push({
|
|
10449
|
+
node,
|
|
10450
|
+
message: `This comparison is always ${op === "==" ? "false" : "true"}: '${formatType(l)}' and '${formatType(r)}' have no value in common`
|
|
10451
|
+
});
|
|
10452
|
+
}
|
|
10453
|
+
/** What an operator takes, where no metamethod answered for it. Luau
|
|
10454
|
+
* raises on the rest — `n * 2` with `n` nil is an error at the line it
|
|
10455
|
+
* runs, and a nil `n` is exactly what `number | nil` says may happen — so
|
|
10456
|
+
* it is said here instead. `any` passes: nothing is known about it. */
|
|
10457
|
+
checkOperands(node, op, left, right) {
|
|
10458
|
+
if (!this.emitDiagnostics) return;
|
|
10459
|
+
const fits = (t) => {
|
|
10460
|
+
const x = this.expand(t);
|
|
10461
|
+
if (x.kind === "any" || x.kind === "never") return true;
|
|
10462
|
+
switch (op) {
|
|
10463
|
+
// `..` joins strings, and Luau writes a number into one.
|
|
10464
|
+
case "..":
|
|
10465
|
+
return isAssignable(x, stringType) || isAssignable(x, numberType);
|
|
10466
|
+
// `#` counts a string's bytes or a table's entries.
|
|
10467
|
+
case "#":
|
|
10468
|
+
return x.kind === "array" || x.kind === "tuple" || x.kind === "object" || isAssignable(x, stringType);
|
|
10469
|
+
default:
|
|
10470
|
+
return isAssignable(x, numberType);
|
|
10471
|
+
}
|
|
10472
|
+
};
|
|
10473
|
+
if (op === "<" || op === ">" || op === "<=" || op === ">=") {
|
|
10474
|
+
const both = (t) => this.expand(t);
|
|
10475
|
+
const [l, r] = [both(left), both(right)];
|
|
10476
|
+
const loose = (t) => t.kind === "any" || t.kind === "never";
|
|
10477
|
+
const alike = (as) => isAssignable(l, as) && isAssignable(r, as);
|
|
10478
|
+
if (loose(l) || loose(r) || alike(numberType) || alike(stringType)) return;
|
|
10479
|
+
} else if (fits(left) && (right === void 0 || fits(right))) {
|
|
10480
|
+
return;
|
|
10481
|
+
}
|
|
10482
|
+
this.diagnostics.push({
|
|
10483
|
+
node,
|
|
10484
|
+
message: right === void 0 ? `Operator '${op}' cannot be applied to type '${formatType(left)}'` : `Operator '${op}' cannot be applied to types '${formatType(left)}' and '${formatType(right)}'`
|
|
10485
|
+
});
|
|
10486
|
+
}
|
|
10300
10487
|
/** `a < b` between class instances: Luau asks `__lt` (`__le` for `<=`),
|
|
10301
10488
|
* with the operands swapped for `>` and `>=`, and an instance without
|
|
10302
10489
|
* one cannot be compared at all. */
|
|
10303
10490
|
checkComparison(node, op, l, r) {
|
|
10304
|
-
if (!this.emitDiagnostics) return;
|
|
10491
|
+
if (!this.emitDiagnostics) return false;
|
|
10305
10492
|
const left = this.expand(l);
|
|
10306
10493
|
const right = this.expand(r);
|
|
10307
10494
|
const instance = (t) => t.kind === "object" && t.class !== void 0;
|
|
10308
|
-
if (!instance(left) && !instance(right)) return;
|
|
10495
|
+
if (!instance(left) && !instance(right)) return false;
|
|
10309
10496
|
const name = op === "<" || op === ">" ? "__lt" : "__le";
|
|
10310
10497
|
const operands = op === ">" || op === ">=" ? [right, left] : [left, right];
|
|
10311
10498
|
for (const receiver of operands) {
|
|
10312
10499
|
const method = receiver.kind === "object" ? receiver.properties.get(name) : void 0;
|
|
10313
10500
|
if (!method) continue;
|
|
10314
|
-
if (this.pickOverload(this.overloadsOf(method.type), operands)) return;
|
|
10501
|
+
if (this.pickOverload(this.overloadsOf(method.type), operands)) return true;
|
|
10315
10502
|
break;
|
|
10316
10503
|
}
|
|
10317
10504
|
this.diagnostics.push({
|
|
10318
10505
|
node,
|
|
10319
10506
|
message: `Operator '${op}' cannot be applied to types '${formatType(l)}' and '${formatType(r)}'`
|
|
10320
10507
|
});
|
|
10508
|
+
return true;
|
|
10321
10509
|
}
|
|
10322
10510
|
/** What an operator on a value with metamethods gives: `a + b` calls
|
|
10323
10511
|
* `__add` on `a`, or failing that on `b` with the operands swapped — the
|
|
@@ -10604,7 +10792,56 @@ var TypeAnalyzer = class {
|
|
|
10604
10792
|
return this.bindingByDecl.get(node) ?? this.bindingByPos.get(posKey(name, node.line.start, node.column.start));
|
|
10605
10793
|
}
|
|
10606
10794
|
};
|
|
10607
|
-
|
|
10795
|
+
var libraryNames = /* @__PURE__ */ new WeakMap();
|
|
10796
|
+
function namesDeclaredIn(program) {
|
|
10797
|
+
const known = libraryNames.get(program);
|
|
10798
|
+
if (known) return known;
|
|
10799
|
+
const names = /* @__PURE__ */ new Set();
|
|
10800
|
+
for (const stmt of program.body.statements) {
|
|
10801
|
+
const declaration = stmt.type === "ExportStatement" || stmt.type === "ExportDefaultStatement" ? stmt.declaration : stmt;
|
|
10802
|
+
if (declaration.type === "TypeAliasStatement") names.add(declaration.name.name);
|
|
10803
|
+
else if (declaration.type === "ExportTypeAliasStatement") names.add(declaration.alias.name.name);
|
|
10804
|
+
else if (declaration.type === "DeclareClassStatement") names.add(declaration.name.name);
|
|
10805
|
+
else if (declaration.type === "ClassDeclaration" && declaration.name) names.add(declaration.name.name);
|
|
10806
|
+
else if (stmt.type === "ImportStatement") {
|
|
10807
|
+
for (const s of stmt.specifiers) names.add(s.local.name);
|
|
10808
|
+
if (stmt.defaultImport) names.add(stmt.defaultImport.name);
|
|
10809
|
+
if (stmt.namespaceImport) names.add(stmt.namespaceImport.name);
|
|
10810
|
+
}
|
|
10811
|
+
}
|
|
10812
|
+
libraryNames.set(program, names);
|
|
10813
|
+
return names;
|
|
10814
|
+
}
|
|
10815
|
+
var sharedClassesByLib = /* @__PURE__ */ new WeakMap();
|
|
10816
|
+
function shareableClasses(libs, program) {
|
|
10817
|
+
if (!libs.length) return void 0;
|
|
10818
|
+
const mine = namesDeclaredIn(program);
|
|
10819
|
+
if (mine.size) {
|
|
10820
|
+
for (const lib of libs) {
|
|
10821
|
+
for (const name of namesDeclaredIn(lib)) if (mine.has(name)) return void 0;
|
|
10822
|
+
}
|
|
10823
|
+
}
|
|
10824
|
+
const existing = sharedClassesByLib.get(libs[0]);
|
|
10825
|
+
if (existing) {
|
|
10826
|
+
const same = existing.libs.length === libs.length && existing.libs.every((lib, i) => lib === libs[i]);
|
|
10827
|
+
if (same) return existing;
|
|
10828
|
+
return void 0;
|
|
10829
|
+
}
|
|
10830
|
+
const store = { libs: [...libs], classTypes: /* @__PURE__ */ new WeakMap(), classMembers: /* @__PURE__ */ new WeakMap() };
|
|
10831
|
+
sharedClassesByLib.set(libs[0], store);
|
|
10832
|
+
return store;
|
|
10833
|
+
}
|
|
10834
|
+
var referencedNamesOf = /* @__PURE__ */ new WeakMap();
|
|
10835
|
+
var typeQueryIn = /* @__PURE__ */ new WeakMap();
|
|
10836
|
+
function referencedTypeNames(node, out) {
|
|
10837
|
+
if (out === void 0 && node && typeof node === "object") {
|
|
10838
|
+
const known = referencedNamesOf.get(node);
|
|
10839
|
+
if (known) return known;
|
|
10840
|
+
const found = referencedTypeNames(node, []);
|
|
10841
|
+
referencedNamesOf.set(node, found);
|
|
10842
|
+
return found;
|
|
10843
|
+
}
|
|
10844
|
+
out = out ?? [];
|
|
10608
10845
|
if (!node || typeof node !== "object") return out;
|
|
10609
10846
|
if (Array.isArray(node)) {
|
|
10610
10847
|
for (const item of node) referencedTypeNames(item, out);
|
|
@@ -10621,9 +10858,11 @@ function referencedTypeNames(node, out = []) {
|
|
|
10621
10858
|
}
|
|
10622
10859
|
function containsTypeQuery(node) {
|
|
10623
10860
|
if (!node || typeof node !== "object") return false;
|
|
10624
|
-
|
|
10625
|
-
if (
|
|
10626
|
-
|
|
10861
|
+
const known = typeQueryIn.get(node);
|
|
10862
|
+
if (known !== void 0) return known;
|
|
10863
|
+
const found = Array.isArray(node) ? node.some(containsTypeQuery) : node.type === "TypeofTypeNode" || Object.values(node).some(containsTypeQuery);
|
|
10864
|
+
typeQueryIn.set(node, found);
|
|
10865
|
+
return found;
|
|
10627
10866
|
}
|
|
10628
10867
|
var STRING_INTRINSICS = /* @__PURE__ */ new Set(["Uppercase", "Lowercase", "Capitalize", "Uncapitalize"]);
|
|
10629
10868
|
function briefType(t) {
|
package/dist/index.d.cts
CHANGED
|
@@ -704,6 +704,9 @@ interface IndexExpression extends BaseNode {
|
|
|
704
704
|
type: "IndexExpression";
|
|
705
705
|
object: Expression;
|
|
706
706
|
index: Expression;
|
|
707
|
+
/** `object?.[key]` — when `object` is nil, the whole chain this link
|
|
708
|
+
* belongs to is nil and the key is not evaluated. */
|
|
709
|
+
optional?: boolean;
|
|
707
710
|
}
|
|
708
711
|
interface CallExpression extends BaseNode {
|
|
709
712
|
type: "CallExpression";
|
|
@@ -902,6 +905,8 @@ interface TableTypeNode extends BaseNode {
|
|
|
902
905
|
interface ArrayTypeNode extends BaseNode {
|
|
903
906
|
type: "ArrayTypeNode";
|
|
904
907
|
element: TypeNode;
|
|
908
|
+
/** `readonly T[]` — the list may be read, not written to. */
|
|
909
|
+
isReadonly?: boolean;
|
|
905
910
|
}
|
|
906
911
|
/** `[number, string]` — a tuple type; `[string, ...number[]]` ends in a rest
|
|
907
912
|
* of as many more as there are. */
|
|
@@ -910,6 +915,8 @@ interface TupleTypeNode extends BaseNode {
|
|
|
910
915
|
elements: TypeNode[];
|
|
911
916
|
/** `...number[]` at the end: the array type the rest of it is. */
|
|
912
917
|
rest?: TypeNode;
|
|
918
|
+
/** `readonly [A, B]` — see `ArrayTypeNode.isReadonly`. */
|
|
919
|
+
isReadonly?: boolean;
|
|
913
920
|
}
|
|
914
921
|
interface FunctionTypeParameter extends BaseNode {
|
|
915
922
|
type: "FunctionTypeParameter";
|
|
@@ -1119,6 +1126,7 @@ interface ArrayType {
|
|
|
1119
1126
|
kind: "array";
|
|
1120
1127
|
element: Type;
|
|
1121
1128
|
alias?: string;
|
|
1129
|
+
readonly?: boolean;
|
|
1122
1130
|
}
|
|
1123
1131
|
/** `[A, B, C]` — fixed length; `[A, ...B[]]` — at least that, then any
|
|
1124
1132
|
* number of `B`. `rest` is the element type of that tail. A tuple is an
|
|
@@ -1128,6 +1136,7 @@ interface TupleType {
|
|
|
1128
1136
|
elements: Type[];
|
|
1129
1137
|
rest?: Type;
|
|
1130
1138
|
alias?: string;
|
|
1139
|
+
readonly?: boolean;
|
|
1131
1140
|
}
|
|
1132
1141
|
interface ObjectProperty {
|
|
1133
1142
|
type: Type;
|
package/dist/index.d.ts
CHANGED
|
@@ -704,6 +704,9 @@ interface IndexExpression extends BaseNode {
|
|
|
704
704
|
type: "IndexExpression";
|
|
705
705
|
object: Expression;
|
|
706
706
|
index: Expression;
|
|
707
|
+
/** `object?.[key]` — when `object` is nil, the whole chain this link
|
|
708
|
+
* belongs to is nil and the key is not evaluated. */
|
|
709
|
+
optional?: boolean;
|
|
707
710
|
}
|
|
708
711
|
interface CallExpression extends BaseNode {
|
|
709
712
|
type: "CallExpression";
|
|
@@ -902,6 +905,8 @@ interface TableTypeNode extends BaseNode {
|
|
|
902
905
|
interface ArrayTypeNode extends BaseNode {
|
|
903
906
|
type: "ArrayTypeNode";
|
|
904
907
|
element: TypeNode;
|
|
908
|
+
/** `readonly T[]` — the list may be read, not written to. */
|
|
909
|
+
isReadonly?: boolean;
|
|
905
910
|
}
|
|
906
911
|
/** `[number, string]` — a tuple type; `[string, ...number[]]` ends in a rest
|
|
907
912
|
* of as many more as there are. */
|
|
@@ -910,6 +915,8 @@ interface TupleTypeNode extends BaseNode {
|
|
|
910
915
|
elements: TypeNode[];
|
|
911
916
|
/** `...number[]` at the end: the array type the rest of it is. */
|
|
912
917
|
rest?: TypeNode;
|
|
918
|
+
/** `readonly [A, B]` — see `ArrayTypeNode.isReadonly`. */
|
|
919
|
+
isReadonly?: boolean;
|
|
913
920
|
}
|
|
914
921
|
interface FunctionTypeParameter extends BaseNode {
|
|
915
922
|
type: "FunctionTypeParameter";
|
|
@@ -1119,6 +1126,7 @@ interface ArrayType {
|
|
|
1119
1126
|
kind: "array";
|
|
1120
1127
|
element: Type;
|
|
1121
1128
|
alias?: string;
|
|
1129
|
+
readonly?: boolean;
|
|
1122
1130
|
}
|
|
1123
1131
|
/** `[A, B, C]` — fixed length; `[A, ...B[]]` — at least that, then any
|
|
1124
1132
|
* number of `B`. `rest` is the element type of that tail. A tuple is an
|
|
@@ -1128,6 +1136,7 @@ interface TupleType {
|
|
|
1128
1136
|
elements: Type[];
|
|
1129
1137
|
rest?: Type;
|
|
1130
1138
|
alias?: string;
|
|
1139
|
+
readonly?: boolean;
|
|
1131
1140
|
}
|
|
1132
1141
|
interface ObjectProperty {
|
|
1133
1142
|
type: Type;
|
package/dist/index.js
CHANGED
|
@@ -2398,6 +2398,21 @@ var Parser = class {
|
|
|
2398
2398
|
base = { type: "MemberExpression", object: base, property: prop, optional: true, ...spanFrom(base, prop) };
|
|
2399
2399
|
continue;
|
|
2400
2400
|
}
|
|
2401
|
+
if (punct === "." && this.punctuatorAt(2, "[")) {
|
|
2402
|
+
this.advance();
|
|
2403
|
+
this.advance();
|
|
2404
|
+
this.expectPunctuator("[");
|
|
2405
|
+
const index = this.inBrackets(() => this.parseExpression());
|
|
2406
|
+
this.expectPunctuator("]");
|
|
2407
|
+
base = {
|
|
2408
|
+
type: "IndexExpression",
|
|
2409
|
+
object: base,
|
|
2410
|
+
index,
|
|
2411
|
+
optional: true,
|
|
2412
|
+
...spanFrom(base, this.previous())
|
|
2413
|
+
};
|
|
2414
|
+
continue;
|
|
2415
|
+
}
|
|
2401
2416
|
if (punct === "." && this.punctuatorAt(2, "(")) {
|
|
2402
2417
|
this.advance();
|
|
2403
2418
|
this.advance();
|
|
@@ -3151,8 +3166,12 @@ var Parser = class {
|
|
|
3151
3166
|
* `name?: T` for an optional property or parameter. */
|
|
3152
3167
|
parseSuffixType() {
|
|
3153
3168
|
if (this.checkIdentifierValue("readonly")) {
|
|
3169
|
+
const start = this.current();
|
|
3154
3170
|
this.advance();
|
|
3155
|
-
|
|
3171
|
+
const t2 = this.parseSuffixType();
|
|
3172
|
+
if (t2.type === "ArrayTypeNode" || t2.type === "TupleTypeNode") return { ...t2, isReadonly: true };
|
|
3173
|
+
this.problemAt(start, "'readonly' belongs on an array or a tuple: a property is marked where it is written");
|
|
3174
|
+
return t2;
|
|
3156
3175
|
}
|
|
3157
3176
|
let t = this.parsePrimaryType();
|
|
3158
3177
|
while (true) {
|
|
@@ -3703,31 +3722,34 @@ var Analyzer = class {
|
|
|
3703
3722
|
* can call it, but a call straight in the block before the declaration
|
|
3704
3723
|
* finds nothing there yet. At a module's top level the whole function is
|
|
3705
3724
|
* hoisted, and this does not apply. */
|
|
3706
|
-
checkUseBeforeDefine(identifier, id) {
|
|
3725
|
+
checkUseBeforeDefine(identifier, id, at = this.functionDepth) {
|
|
3707
3726
|
const binding = this.bindings.get(id);
|
|
3708
|
-
if (
|
|
3727
|
+
if (this.typeQueryDepth > 0) return;
|
|
3709
3728
|
const declaration = binding.declarationNode;
|
|
3710
|
-
|
|
3711
|
-
|
|
3729
|
+
if (!declaration) return;
|
|
3730
|
+
const hoisted = binding.declaredBy === "function" || binding.declaredBy === "class";
|
|
3731
|
+
const depth = hoisted ? this.hoisted.get(declaration) : this.declaredDepth.get(id);
|
|
3732
|
+
const readable = hoisted || binding.kind === "local";
|
|
3733
|
+
if (!readable || depth === void 0 || depth !== at) return;
|
|
3712
3734
|
const before = identifier.line.start < declaration.line.start || identifier.line.start === declaration.line.start && identifier.column.start < declaration.column.start;
|
|
3713
3735
|
if (!before) return;
|
|
3714
3736
|
this.diagnostics.push({
|
|
3715
3737
|
node: identifier,
|
|
3716
|
-
message: `'${binding.name}' is used before its definition: inside a function, a function declared further down is only there once its declaration has run`,
|
|
3738
|
+
message: hoisted ? `'${binding.name}' is used before its definition: inside a function, a function declared further down is only there once its declaration has run` : `'${binding.name}' is used before its declaration, and holds nothing until that line has run`,
|
|
3717
3739
|
kind: "use-before-define"
|
|
3718
3740
|
});
|
|
3719
3741
|
}
|
|
3720
3742
|
noteDeferred(identifier, scope, id, assignment) {
|
|
3721
3743
|
if (this.functionDepth === 0 && this.typeQueryDepth === 0) return;
|
|
3722
3744
|
if (this.bindings.get(id).kind !== "global") return;
|
|
3723
|
-
this.deferredGlobals.push({ node: identifier, scope, assignment });
|
|
3745
|
+
this.deferredGlobals.push({ node: identifier, scope, assignment, depth: this.functionDepth });
|
|
3724
3746
|
}
|
|
3725
3747
|
/** Point each deferred read of a global at the declaration of that name
|
|
3726
3748
|
* that turned up later — in the module, or in any block around the code
|
|
3727
3749
|
* that reads it. Such code runs after the declaration has: a closure
|
|
3728
3750
|
* written inside a value reads the name the value is bound to. */
|
|
3729
3751
|
resolveForwardReferences() {
|
|
3730
|
-
for (const { node, scope, assignment } of this.deferredGlobals) {
|
|
3752
|
+
for (const { node, scope, assignment, depth } of this.deferredGlobals) {
|
|
3731
3753
|
const localId = this.lookup(scope, node.name);
|
|
3732
3754
|
const globalId = this.bindingOf.get(node);
|
|
3733
3755
|
if (localId === void 0 || globalId === void 0 || localId === globalId) continue;
|
|
@@ -3742,7 +3764,10 @@ var Analyzer = class {
|
|
|
3742
3764
|
this.bindingOf.set(node, localId);
|
|
3743
3765
|
this.bindings.get(localId).references.push(node);
|
|
3744
3766
|
if (assignment) this.checkConstAssign(localId, node);
|
|
3745
|
-
else if (this.typeQueryDepth === 0)
|
|
3767
|
+
else if (this.typeQueryDepth === 0) {
|
|
3768
|
+
this.checkTypeOnly(localId, node);
|
|
3769
|
+
this.checkUseBeforeDefine(node, localId, depth);
|
|
3770
|
+
}
|
|
3746
3771
|
}
|
|
3747
3772
|
}
|
|
3748
3773
|
/** Every read of a global nothing declares. A global assigned somewhere
|
|
@@ -3773,9 +3798,13 @@ var Analyzer = class {
|
|
|
3773
3798
|
}
|
|
3774
3799
|
const id = this.nextId++;
|
|
3775
3800
|
this.bindings.set(id, { id, name, kind, declarationNode: node, references: [], isConst, declaredBy });
|
|
3801
|
+
this.declaredDepth.set(id, this.functionDepth);
|
|
3776
3802
|
scope.declarations.set(name, id);
|
|
3777
3803
|
return id;
|
|
3778
3804
|
}
|
|
3805
|
+
/** Which function body each name was declared in. A read of a `const` from
|
|
3806
|
+
* the same body, but above it, is a read of nothing. */
|
|
3807
|
+
declaredDepth = /* @__PURE__ */ new Map();
|
|
3779
3808
|
resolve(scope, name) {
|
|
3780
3809
|
for (let s = scope; s; s = s.parent) {
|
|
3781
3810
|
const id = s.declarations.get(name);
|
|
@@ -4428,9 +4457,9 @@ function substitute(t, subst) {
|
|
|
4428
4457
|
case "typeParam":
|
|
4429
4458
|
return subst.get(t.name) ?? t;
|
|
4430
4459
|
case "array":
|
|
4431
|
-
return arrayOf(substitute(t.element, subst));
|
|
4460
|
+
return { ...arrayOf(substitute(t.element, subst)), readonly: t.readonly };
|
|
4432
4461
|
case "tuple":
|
|
4433
|
-
return tuple(t.elements.map((e) => substitute(e, subst)), t.rest && substitute(t.rest, subst));
|
|
4462
|
+
return { ...tuple(t.elements.map((e) => substitute(e, subst)), t.rest && substitute(t.rest, subst)), readonly: t.readonly };
|
|
4434
4463
|
case "object": {
|
|
4435
4464
|
const entries = [];
|
|
4436
4465
|
for (const [k, v] of t.properties) entries.push([k, { ...v, type: substitute(v.type, subst) }]);
|
|
@@ -4595,6 +4624,20 @@ function unify(param, arg, vars, out) {
|
|
|
4595
4624
|
return;
|
|
4596
4625
|
}
|
|
4597
4626
|
}
|
|
4627
|
+
function memberKey(t) {
|
|
4628
|
+
switch (t.kind) {
|
|
4629
|
+
case "literal":
|
|
4630
|
+
return `l${t.base}:${String(t.value)}`;
|
|
4631
|
+
case "primitive":
|
|
4632
|
+
return `p${t.name}`;
|
|
4633
|
+
case "any":
|
|
4634
|
+
case "unknown":
|
|
4635
|
+
case "never":
|
|
4636
|
+
return t.kind;
|
|
4637
|
+
default:
|
|
4638
|
+
return formatType(t);
|
|
4639
|
+
}
|
|
4640
|
+
}
|
|
4598
4641
|
function union(types) {
|
|
4599
4642
|
const flat = [];
|
|
4600
4643
|
for (const t of types) {
|
|
@@ -4606,7 +4649,7 @@ function union(types) {
|
|
|
4606
4649
|
const seen = /* @__PURE__ */ new Map();
|
|
4607
4650
|
for (const t of flat) {
|
|
4608
4651
|
if (t.kind === "never") continue;
|
|
4609
|
-
const key =
|
|
4652
|
+
const key = memberKey(t);
|
|
4610
4653
|
if (!seen.has(key)) seen.set(key, t);
|
|
4611
4654
|
}
|
|
4612
4655
|
let members = reduceSubtypes([...seen.values()]);
|
|
@@ -4635,7 +4678,7 @@ function intersection(types) {
|
|
|
4635
4678
|
const seen = /* @__PURE__ */ new Map();
|
|
4636
4679
|
for (const t of flat) {
|
|
4637
4680
|
if (t.kind === "unknown") continue;
|
|
4638
|
-
seen.set(
|
|
4681
|
+
seen.set(memberKey(t), t);
|
|
4639
4682
|
}
|
|
4640
4683
|
const members = [...seen.values()];
|
|
4641
4684
|
if (members.length === 0) return unknownType;
|
|
@@ -4662,9 +4705,9 @@ function widen(t) {
|
|
|
4662
4705
|
case "literal":
|
|
4663
4706
|
return primitive(t.base);
|
|
4664
4707
|
case "array":
|
|
4665
|
-
return arrayOf(widen(t.element));
|
|
4708
|
+
return { ...arrayOf(widen(t.element)), readonly: t.readonly };
|
|
4666
4709
|
case "tuple":
|
|
4667
|
-
return tuple(t.elements.map(widen), t.rest && widen(t.rest));
|
|
4710
|
+
return { ...tuple(t.elements.map(widen), t.rest && widen(t.rest)), readonly: t.readonly };
|
|
4668
4711
|
case "object": {
|
|
4669
4712
|
if (t.frozen || t.class) return t;
|
|
4670
4713
|
const entries = [];
|
|
@@ -4762,6 +4805,7 @@ function isAssignableInner(a, b) {
|
|
|
4762
4805
|
return b.kind === "primitive" && b.name === "string";
|
|
4763
4806
|
}
|
|
4764
4807
|
if (a.kind === "primitive") return b.kind === "primitive" && b.name === a.name;
|
|
4808
|
+
if ((a.kind === "array" || a.kind === "tuple") && a.readonly && (b.kind === "array" || b.kind === "tuple") && !b.readonly) return false;
|
|
4765
4809
|
if (a.kind === "array") {
|
|
4766
4810
|
if (b.kind === "array") return isAssignable(a.element, b.element);
|
|
4767
4811
|
if (b.kind === "tuple") return b.elements.length === 0 && !!b.rest && isAssignable(a.element, b.rest);
|
|
@@ -5107,10 +5151,10 @@ function formatTypeUncached(t) {
|
|
|
5107
5151
|
case "literal":
|
|
5108
5152
|
return t.base === "string" ? JSON.stringify(t.value) : String(t.value);
|
|
5109
5153
|
case "array":
|
|
5110
|
-
return `${formatAtom(t.element)}[]`;
|
|
5154
|
+
return `${t.readonly ? "readonly " : ""}${formatAtom(t.element)}[]`;
|
|
5111
5155
|
case "tuple": {
|
|
5112
5156
|
const inner = [...t.elements.map(formatType), ...t.rest ? [`...${formatAtom(t.rest)}[]`] : []];
|
|
5113
|
-
return
|
|
5157
|
+
return `${t.readonly ? "readonly " : ""}[${inner.join(", ")}]`;
|
|
5114
5158
|
}
|
|
5115
5159
|
case "object": {
|
|
5116
5160
|
if (t.name) {
|
|
@@ -5633,7 +5677,7 @@ function expressionLabel(e, depth = 0) {
|
|
|
5633
5677
|
case "IndexExpression": {
|
|
5634
5678
|
const o = expressionLabel(e.object, depth + 1);
|
|
5635
5679
|
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 : "...";
|
|
5636
|
-
return o === void 0 ? void 0 : `${o}[${i}]`;
|
|
5680
|
+
return o === void 0 ? void 0 : `${o}${e.optional ? "?." : ""}[${i}]`;
|
|
5637
5681
|
}
|
|
5638
5682
|
case "ParenthesizedExpression": {
|
|
5639
5683
|
const inner = expressionLabel(e.expression, depth + 1);
|
|
@@ -5684,7 +5728,8 @@ var TypeAnalyzer = class {
|
|
|
5684
5728
|
/** Put on every ref to one of `aliasDefs`, so a module that imports the
|
|
5685
5729
|
* type expands the ref here rather than by its name there. */
|
|
5686
5730
|
origin = { expand: (t) => this.expand(t) };
|
|
5687
|
-
/** See `resolveClass`.
|
|
5731
|
+
/** See `resolveClass`. Shared with other analyses over the same libraries
|
|
5732
|
+
* when none of their names is redefined here; see `shareableClasses`. */
|
|
5688
5733
|
classTypes = /* @__PURE__ */ new WeakMap();
|
|
5689
5734
|
/** See `instanceType` — one instance type per `class ... end`. */
|
|
5690
5735
|
instanceTypes = /* @__PURE__ */ new WeakMap();
|
|
@@ -5730,6 +5775,11 @@ var TypeAnalyzer = class {
|
|
|
5730
5775
|
/** Recursion guard for `preVisitBody`. */
|
|
5731
5776
|
preVisitDepth = 0;
|
|
5732
5777
|
run() {
|
|
5778
|
+
const shared = shareableClasses(this.options.libs ?? [], this.program);
|
|
5779
|
+
if (shared) {
|
|
5780
|
+
this.classTypes = shared.classTypes;
|
|
5781
|
+
this.classMembers = shared.classMembers;
|
|
5782
|
+
}
|
|
5733
5783
|
this.registerAliasDefs(preludeProgram().body, true);
|
|
5734
5784
|
for (const lib of this.options.libs ?? []) this.registerAliasDefs(lib.body, true);
|
|
5735
5785
|
this.registerAliasDefs(this.program.body);
|
|
@@ -6895,14 +6945,17 @@ var TypeAnalyzer = class {
|
|
|
6895
6945
|
return literal(node.value);
|
|
6896
6946
|
case "TypeLiteralNumber":
|
|
6897
6947
|
return literal(node.value);
|
|
6898
|
-
case "ArrayTypeNode":
|
|
6899
|
-
|
|
6948
|
+
case "ArrayTypeNode": {
|
|
6949
|
+
const made = arrayOf(this.resolveType(node.element));
|
|
6950
|
+
return node.isReadonly ? { ...made, readonly: true } : made;
|
|
6951
|
+
}
|
|
6900
6952
|
case "TupleTypeNode": {
|
|
6901
6953
|
const rest = node.rest && this.expand(this.resolveType(node.rest));
|
|
6902
|
-
|
|
6954
|
+
const made = tuple(
|
|
6903
6955
|
node.elements.map((e) => this.resolveType(e)),
|
|
6904
6956
|
rest ? rest.kind === "array" ? rest.element : rest.kind === "any" ? anyType : unknownType : void 0
|
|
6905
6957
|
);
|
|
6958
|
+
return node.isReadonly && made.kind === "tuple" ? { ...made, readonly: true } : made;
|
|
6906
6959
|
}
|
|
6907
6960
|
case "UnionTypeNode":
|
|
6908
6961
|
return union(node.types.map((t) => this.resolveType(t)));
|
|
@@ -7722,8 +7775,19 @@ var TypeAnalyzer = class {
|
|
|
7722
7775
|
if (p.rest) return arrayOf(unknownType);
|
|
7723
7776
|
if (p.pattern) return this.patternToType(p.pattern, env);
|
|
7724
7777
|
if (p.default) return widen(this.infer(p.default, env));
|
|
7725
|
-
|
|
7778
|
+
const contextual = this.contextualParams.get(p);
|
|
7779
|
+
if (contextual) return contextual;
|
|
7780
|
+
if (this.emitDiagnostics && !receiver && p.name && p.line && !this.untypedParamReported.has(p)) {
|
|
7781
|
+
this.untypedParamReported.add(p);
|
|
7782
|
+
this.diagnostics.push({
|
|
7783
|
+
node: p,
|
|
7784
|
+
message: `Parameter '${p.name}' has no type, so it is 'any': give it one, or a default to read it from`
|
|
7785
|
+
});
|
|
7786
|
+
}
|
|
7787
|
+
return anyType;
|
|
7726
7788
|
}
|
|
7789
|
+
/** Parameters already spoken about: a body is visited more than once. */
|
|
7790
|
+
untypedParamReported = /* @__PURE__ */ new WeakSet();
|
|
7727
7791
|
/** What a function expression's unannotated parameters are, from where
|
|
7728
7792
|
* it is written — see `applyContext`. */
|
|
7729
7793
|
contextualParams = /* @__PURE__ */ new WeakMap();
|
|
@@ -8018,6 +8082,11 @@ var TypeAnalyzer = class {
|
|
|
8018
8082
|
* this returns `undefined` and the ordinary pick stands. */
|
|
8019
8083
|
distributedReturn(fns, argTypes, picked, argsFor) {
|
|
8020
8084
|
if (fns.length < 2) return void 0;
|
|
8085
|
+
if (argTypes.some((t) => this.expand(t).kind === "any")) {
|
|
8086
|
+
const taken = fns.filter((f) => this.overloadAccepts(f, argsFor(f, argTypes)));
|
|
8087
|
+
if (taken.length < 2) return void 0;
|
|
8088
|
+
return union(taken.map((f) => this.callReturn(f, argsFor(f, argTypes))));
|
|
8089
|
+
}
|
|
8021
8090
|
const position = argTypes.findIndex((t) => this.expand(t).kind === "union");
|
|
8022
8091
|
if (position < 0) return void 0;
|
|
8023
8092
|
const members = this.expand(argTypes[position]).types;
|
|
@@ -8066,6 +8135,8 @@ var TypeAnalyzer = class {
|
|
|
8066
8135
|
* lone argument cannot be checked against without false errors. */
|
|
8067
8136
|
boundParams(f) {
|
|
8068
8137
|
if (!f.typeParams?.length) return f.params.map((p) => p.type);
|
|
8138
|
+
const known = this.boundParamsCache.get(f);
|
|
8139
|
+
if (known) return known;
|
|
8069
8140
|
const bounds = new Map(f.typeParams.map((name) => [name, anyType]));
|
|
8070
8141
|
const seen = /* @__PURE__ */ new WeakSet();
|
|
8071
8142
|
const walk = (value) => {
|
|
@@ -8083,8 +8154,11 @@ var TypeAnalyzer = class {
|
|
|
8083
8154
|
for (const child of Object.values(value)) walk(child);
|
|
8084
8155
|
};
|
|
8085
8156
|
for (const p of f.params) if (containsTypeParam(p.type)) walk(p.type);
|
|
8086
|
-
|
|
8157
|
+
const bound = f.params.map((p) => this.reduceType(substitute(p.type, bounds)));
|
|
8158
|
+
this.boundParamsCache.set(f, bound);
|
|
8159
|
+
return bound;
|
|
8087
8160
|
}
|
|
8161
|
+
boundParamsCache = /* @__PURE__ */ new WeakMap();
|
|
8088
8162
|
/** Record what each written argument is expected to be — see
|
|
8089
8163
|
* `TypeAnalysis.expectedTypeOf`. */
|
|
8090
8164
|
recordExpected(written, fns, selfOf, argsOf = () => []) {
|
|
@@ -8548,7 +8622,7 @@ var TypeAnalyzer = class {
|
|
|
8548
8622
|
/** What one `for (const item in source)` hands over each time, and how it
|
|
8549
8623
|
* walks the source. `pairs`/`ipairs` are read from the table they are
|
|
8550
8624
|
* given, so a record's keys stay the literals they are. */
|
|
8551
|
-
iterationOf(source, sourceType) {
|
|
8625
|
+
iterationOf(source, sourceType, report = true) {
|
|
8552
8626
|
if (source.type === "CallExpression" && source.callee.type === "Identifier" && source.arguments[0]) {
|
|
8553
8627
|
const name = source.callee.name;
|
|
8554
8628
|
const src = this.expand(this.typeOf.get(source.arguments[0]) ?? unknownType);
|
|
@@ -8586,7 +8660,14 @@ var TypeAnalyzer = class {
|
|
|
8586
8660
|
if (t.kind === "object") {
|
|
8587
8661
|
return [t.indexer?.value ?? union([...t.properties.values()].map((p) => p.type)), values];
|
|
8588
8662
|
}
|
|
8589
|
-
if (t.kind
|
|
8663
|
+
if (t.kind === "union") {
|
|
8664
|
+
const each = t.types.map((member) => this.iterationOf(source, member, false));
|
|
8665
|
+
const [first] = each;
|
|
8666
|
+
if (first && each.every(([, form]) => form.walks === first[1].walks && form.viaIter === first[1].viaIter)) {
|
|
8667
|
+
return [union(each.map(([item]) => item)), first[1]];
|
|
8668
|
+
}
|
|
8669
|
+
}
|
|
8670
|
+
if (t.kind !== "never" && report && this.emitDiagnostics) {
|
|
8590
8671
|
this.diagnostics.push({
|
|
8591
8672
|
node: source,
|
|
8592
8673
|
message: `Cannot loop over '${formatType(sourceType)}': a loop walks an array, a table, an iterator function or an iteration such as 'pairs(t)'`
|
|
@@ -8955,7 +9036,14 @@ var TypeAnalyzer = class {
|
|
|
8955
9036
|
const names = keys.map(nameOf);
|
|
8956
9037
|
const others = keys.filter((_, i) => names[i] === void 0);
|
|
8957
9038
|
let message;
|
|
8958
|
-
|
|
9039
|
+
const listOnly = (t) => {
|
|
9040
|
+
const x = this.expand(t);
|
|
9041
|
+
if (x.kind === "union") return x.types.every(listOnly);
|
|
9042
|
+
return x.kind === "array" || x.kind === "tuple";
|
|
9043
|
+
};
|
|
9044
|
+
if (names.some((name) => name !== void 0) && listOnly(withoutNil(this.expand(object)))) {
|
|
9045
|
+
message = `Type '${briefType(index)}' cannot be used to index type '${shown}'`;
|
|
9046
|
+
} else if (others.length && this.cannotIndex(object, others)) {
|
|
8959
9047
|
message = `Type '${briefType(index)}' cannot be used to index type '${shown}'`;
|
|
8960
9048
|
} else if (!others.length && names.every((name) => this.memberMissing(object, name))) {
|
|
8961
9049
|
message = keys.length === 1 ? `Property '${names[0]}' does not exist on type '${shown}'` : `None of ${briefType(index)} is a property of type '${shown}'`;
|
|
@@ -9227,10 +9315,18 @@ var TypeAnalyzer = class {
|
|
|
9227
9315
|
switch (expr.operator) {
|
|
9228
9316
|
case "not":
|
|
9229
9317
|
return booleanType;
|
|
9230
|
-
case "-":
|
|
9231
|
-
|
|
9232
|
-
|
|
9233
|
-
|
|
9318
|
+
case "-": {
|
|
9319
|
+
const answered = this.operatorResult(expr, "-", arg, void 0);
|
|
9320
|
+
if (answered) return answered;
|
|
9321
|
+
this.checkOperands(expr, "-", arg);
|
|
9322
|
+
return this.arithmeticOn([arg]);
|
|
9323
|
+
}
|
|
9324
|
+
case "#": {
|
|
9325
|
+
const answered = this.operatorResult(expr, "#", arg, void 0);
|
|
9326
|
+
if (answered) return answered;
|
|
9327
|
+
this.checkOperands(expr, "#", arg);
|
|
9328
|
+
return numberType;
|
|
9329
|
+
}
|
|
9234
9330
|
}
|
|
9235
9331
|
return arg;
|
|
9236
9332
|
}
|
|
@@ -9259,16 +9355,21 @@ var TypeAnalyzer = class {
|
|
|
9259
9355
|
if (unwrapParens(expr.left).type === "StringLiteral") this.expectedTypeOf.set(unwrapParens(expr.left), r);
|
|
9260
9356
|
}
|
|
9261
9357
|
switch (op) {
|
|
9262
|
-
case "..":
|
|
9263
|
-
|
|
9358
|
+
case "..": {
|
|
9359
|
+
const answered = this.operatorResult(expr, op, l, r);
|
|
9360
|
+
if (answered) return answered;
|
|
9361
|
+
this.checkOperands(expr, op, l, r);
|
|
9362
|
+
return stringType;
|
|
9363
|
+
}
|
|
9264
9364
|
case "<":
|
|
9265
9365
|
case ">":
|
|
9266
9366
|
case "<=":
|
|
9267
9367
|
case ">=":
|
|
9268
|
-
this.checkComparison(expr, op, l, r);
|
|
9368
|
+
if (!this.checkComparison(expr, op, l, r)) this.checkOperands(expr, op, l, r);
|
|
9269
9369
|
return booleanType;
|
|
9270
9370
|
case "==":
|
|
9271
9371
|
case "~=":
|
|
9372
|
+
this.checkOverlap(expr, op, l, r);
|
|
9272
9373
|
return booleanType;
|
|
9273
9374
|
case "+":
|
|
9274
9375
|
case "-":
|
|
@@ -9276,8 +9377,12 @@ var TypeAnalyzer = class {
|
|
|
9276
9377
|
case "/":
|
|
9277
9378
|
case "//":
|
|
9278
9379
|
case "%":
|
|
9279
|
-
case "^":
|
|
9280
|
-
|
|
9380
|
+
case "^": {
|
|
9381
|
+
const answered = this.operatorResult(expr, op, l, r);
|
|
9382
|
+
if (answered) return answered;
|
|
9383
|
+
this.checkOperands(expr, op, l, r);
|
|
9384
|
+
return this.arithmeticOn([l, r]);
|
|
9385
|
+
}
|
|
9281
9386
|
}
|
|
9282
9387
|
return union([l, r]);
|
|
9283
9388
|
}
|
|
@@ -9525,6 +9630,14 @@ var TypeAnalyzer = class {
|
|
|
9525
9630
|
name = target.property.name;
|
|
9526
9631
|
object = target.object;
|
|
9527
9632
|
} else if (target.type === "IndexExpression") {
|
|
9633
|
+
const list = this.expand(this.infer(target.object, env));
|
|
9634
|
+
if ((list.kind === "array" || list.kind === "tuple") && list.readonly) {
|
|
9635
|
+
this.diagnostics.push({
|
|
9636
|
+
node: target,
|
|
9637
|
+
message: `Cannot assign to an element of '${formatType(list)}': it is read-only`
|
|
9638
|
+
});
|
|
9639
|
+
return;
|
|
9640
|
+
}
|
|
9528
9641
|
const index = this.expand(this.infer(target.index, env));
|
|
9529
9642
|
if (index.kind !== "literal" || typeof index.value !== "string") return;
|
|
9530
9643
|
name = index.value;
|
|
@@ -9697,9 +9810,27 @@ var TypeAnalyzer = class {
|
|
|
9697
9810
|
if (s.indexer) indexer = mergeIndexer(indexer, s.indexer);
|
|
9698
9811
|
}
|
|
9699
9812
|
}
|
|
9813
|
+
this.checkDuplicateKey(expr, field, entries);
|
|
9700
9814
|
}
|
|
9701
9815
|
return objectType(entries, indexer, asConst || void 0);
|
|
9702
9816
|
}
|
|
9817
|
+
/** `{ a: 1, a: 2 }` — the second wins and the first was written for
|
|
9818
|
+
* nothing, which is a typo far more often than a decision. A spread is
|
|
9819
|
+
* not one: overriding what it brought is exactly what it is for. */
|
|
9820
|
+
checkDuplicateKey(expr, field, entries) {
|
|
9821
|
+
if (!this.emitDiagnostics || this.duplicateKeyReported.has(field)) return;
|
|
9822
|
+
const written = field.type === "TableFieldNamed" ? field.key.type === "Identifier" ? field.key.name : field.key.value : field.type === "TableFieldShorthand" ? field.name.name : void 0;
|
|
9823
|
+
if (written === void 0) return;
|
|
9824
|
+
const spreadBefore = expr.fields.slice(0, expr.fields.indexOf(field)).some((f) => f.type === "TableFieldSpread");
|
|
9825
|
+
if (spreadBefore) return;
|
|
9826
|
+
if (entries.filter(([k]) => k === written).length < 2) return;
|
|
9827
|
+
this.duplicateKeyReported.add(field);
|
|
9828
|
+
this.diagnostics.push({
|
|
9829
|
+
node: field.type === "TableFieldNamed" ? field.key : field.name,
|
|
9830
|
+
message: `'${written}' is given twice in this table; only the last one is kept`
|
|
9831
|
+
});
|
|
9832
|
+
}
|
|
9833
|
+
duplicateKeyReported = /* @__PURE__ */ new WeakSet();
|
|
9703
9834
|
/** A value inferred `as const`, widened back wherever `context` does not
|
|
9704
9835
|
* ask for a literal: `satisfies`' result type. A property keeps `"circle"`
|
|
9705
9836
|
* when the contract's property admits string literals, and becomes
|
|
@@ -10192,27 +10323,84 @@ var TypeAnalyzer = class {
|
|
|
10192
10323
|
arithmeticOn(operands) {
|
|
10193
10324
|
return operands.some((t) => this.expand(t).kind === "any") ? anyType : numberType;
|
|
10194
10325
|
}
|
|
10326
|
+
/** `a == b` where nothing `a` can hold is anything `b` can: the answer is
|
|
10327
|
+
* always the same, so the comparison was not the one meant. Only types
|
|
10328
|
+
* that say what they hold take part — `any`, `unknown` and a type
|
|
10329
|
+
* parameter say nothing, and a table compares by identity, so two of them
|
|
10330
|
+
* may well be the same table. */
|
|
10331
|
+
checkOverlap(node, op, l, r) {
|
|
10332
|
+
if (!this.emitDiagnostics) return;
|
|
10333
|
+
const left = this.expand(l);
|
|
10334
|
+
const right = this.expand(r);
|
|
10335
|
+
const judged = (t) => {
|
|
10336
|
+
const parts = t.kind === "union" ? t.types.map((m) => this.expand(m)) : [t];
|
|
10337
|
+
return parts.every((p) => p.kind === "literal" || p.kind === "primitive");
|
|
10338
|
+
};
|
|
10339
|
+
const isNil = (t) => t.kind === "primitive" && t.name === "nil";
|
|
10340
|
+
if (isNil(left) || isNil(right)) return;
|
|
10341
|
+
if (!judged(left) || !judged(right)) return;
|
|
10342
|
+
if (isAssignable(left, right) || isAssignable(right, left)) return;
|
|
10343
|
+
this.diagnostics.push({
|
|
10344
|
+
node,
|
|
10345
|
+
message: `This comparison is always ${op === "==" ? "false" : "true"}: '${formatType(l)}' and '${formatType(r)}' have no value in common`
|
|
10346
|
+
});
|
|
10347
|
+
}
|
|
10348
|
+
/** What an operator takes, where no metamethod answered for it. Luau
|
|
10349
|
+
* raises on the rest — `n * 2` with `n` nil is an error at the line it
|
|
10350
|
+
* runs, and a nil `n` is exactly what `number | nil` says may happen — so
|
|
10351
|
+
* it is said here instead. `any` passes: nothing is known about it. */
|
|
10352
|
+
checkOperands(node, op, left, right) {
|
|
10353
|
+
if (!this.emitDiagnostics) return;
|
|
10354
|
+
const fits = (t) => {
|
|
10355
|
+
const x = this.expand(t);
|
|
10356
|
+
if (x.kind === "any" || x.kind === "never") return true;
|
|
10357
|
+
switch (op) {
|
|
10358
|
+
// `..` joins strings, and Luau writes a number into one.
|
|
10359
|
+
case "..":
|
|
10360
|
+
return isAssignable(x, stringType) || isAssignable(x, numberType);
|
|
10361
|
+
// `#` counts a string's bytes or a table's entries.
|
|
10362
|
+
case "#":
|
|
10363
|
+
return x.kind === "array" || x.kind === "tuple" || x.kind === "object" || isAssignable(x, stringType);
|
|
10364
|
+
default:
|
|
10365
|
+
return isAssignable(x, numberType);
|
|
10366
|
+
}
|
|
10367
|
+
};
|
|
10368
|
+
if (op === "<" || op === ">" || op === "<=" || op === ">=") {
|
|
10369
|
+
const both = (t) => this.expand(t);
|
|
10370
|
+
const [l, r] = [both(left), both(right)];
|
|
10371
|
+
const loose = (t) => t.kind === "any" || t.kind === "never";
|
|
10372
|
+
const alike = (as) => isAssignable(l, as) && isAssignable(r, as);
|
|
10373
|
+
if (loose(l) || loose(r) || alike(numberType) || alike(stringType)) return;
|
|
10374
|
+
} else if (fits(left) && (right === void 0 || fits(right))) {
|
|
10375
|
+
return;
|
|
10376
|
+
}
|
|
10377
|
+
this.diagnostics.push({
|
|
10378
|
+
node,
|
|
10379
|
+
message: right === void 0 ? `Operator '${op}' cannot be applied to type '${formatType(left)}'` : `Operator '${op}' cannot be applied to types '${formatType(left)}' and '${formatType(right)}'`
|
|
10380
|
+
});
|
|
10381
|
+
}
|
|
10195
10382
|
/** `a < b` between class instances: Luau asks `__lt` (`__le` for `<=`),
|
|
10196
10383
|
* with the operands swapped for `>` and `>=`, and an instance without
|
|
10197
10384
|
* one cannot be compared at all. */
|
|
10198
10385
|
checkComparison(node, op, l, r) {
|
|
10199
|
-
if (!this.emitDiagnostics) return;
|
|
10386
|
+
if (!this.emitDiagnostics) return false;
|
|
10200
10387
|
const left = this.expand(l);
|
|
10201
10388
|
const right = this.expand(r);
|
|
10202
10389
|
const instance = (t) => t.kind === "object" && t.class !== void 0;
|
|
10203
|
-
if (!instance(left) && !instance(right)) return;
|
|
10390
|
+
if (!instance(left) && !instance(right)) return false;
|
|
10204
10391
|
const name = op === "<" || op === ">" ? "__lt" : "__le";
|
|
10205
10392
|
const operands = op === ">" || op === ">=" ? [right, left] : [left, right];
|
|
10206
10393
|
for (const receiver of operands) {
|
|
10207
10394
|
const method = receiver.kind === "object" ? receiver.properties.get(name) : void 0;
|
|
10208
10395
|
if (!method) continue;
|
|
10209
|
-
if (this.pickOverload(this.overloadsOf(method.type), operands)) return;
|
|
10396
|
+
if (this.pickOverload(this.overloadsOf(method.type), operands)) return true;
|
|
10210
10397
|
break;
|
|
10211
10398
|
}
|
|
10212
10399
|
this.diagnostics.push({
|
|
10213
10400
|
node,
|
|
10214
10401
|
message: `Operator '${op}' cannot be applied to types '${formatType(l)}' and '${formatType(r)}'`
|
|
10215
10402
|
});
|
|
10403
|
+
return true;
|
|
10216
10404
|
}
|
|
10217
10405
|
/** What an operator on a value with metamethods gives: `a + b` calls
|
|
10218
10406
|
* `__add` on `a`, or failing that on `b` with the operands swapped — the
|
|
@@ -10499,7 +10687,56 @@ var TypeAnalyzer = class {
|
|
|
10499
10687
|
return this.bindingByDecl.get(node) ?? this.bindingByPos.get(posKey(name, node.line.start, node.column.start));
|
|
10500
10688
|
}
|
|
10501
10689
|
};
|
|
10502
|
-
|
|
10690
|
+
var libraryNames = /* @__PURE__ */ new WeakMap();
|
|
10691
|
+
function namesDeclaredIn(program) {
|
|
10692
|
+
const known = libraryNames.get(program);
|
|
10693
|
+
if (known) return known;
|
|
10694
|
+
const names = /* @__PURE__ */ new Set();
|
|
10695
|
+
for (const stmt of program.body.statements) {
|
|
10696
|
+
const declaration = stmt.type === "ExportStatement" || stmt.type === "ExportDefaultStatement" ? stmt.declaration : stmt;
|
|
10697
|
+
if (declaration.type === "TypeAliasStatement") names.add(declaration.name.name);
|
|
10698
|
+
else if (declaration.type === "ExportTypeAliasStatement") names.add(declaration.alias.name.name);
|
|
10699
|
+
else if (declaration.type === "DeclareClassStatement") names.add(declaration.name.name);
|
|
10700
|
+
else if (declaration.type === "ClassDeclaration" && declaration.name) names.add(declaration.name.name);
|
|
10701
|
+
else if (stmt.type === "ImportStatement") {
|
|
10702
|
+
for (const s of stmt.specifiers) names.add(s.local.name);
|
|
10703
|
+
if (stmt.defaultImport) names.add(stmt.defaultImport.name);
|
|
10704
|
+
if (stmt.namespaceImport) names.add(stmt.namespaceImport.name);
|
|
10705
|
+
}
|
|
10706
|
+
}
|
|
10707
|
+
libraryNames.set(program, names);
|
|
10708
|
+
return names;
|
|
10709
|
+
}
|
|
10710
|
+
var sharedClassesByLib = /* @__PURE__ */ new WeakMap();
|
|
10711
|
+
function shareableClasses(libs, program) {
|
|
10712
|
+
if (!libs.length) return void 0;
|
|
10713
|
+
const mine = namesDeclaredIn(program);
|
|
10714
|
+
if (mine.size) {
|
|
10715
|
+
for (const lib of libs) {
|
|
10716
|
+
for (const name of namesDeclaredIn(lib)) if (mine.has(name)) return void 0;
|
|
10717
|
+
}
|
|
10718
|
+
}
|
|
10719
|
+
const existing = sharedClassesByLib.get(libs[0]);
|
|
10720
|
+
if (existing) {
|
|
10721
|
+
const same = existing.libs.length === libs.length && existing.libs.every((lib, i) => lib === libs[i]);
|
|
10722
|
+
if (same) return existing;
|
|
10723
|
+
return void 0;
|
|
10724
|
+
}
|
|
10725
|
+
const store = { libs: [...libs], classTypes: /* @__PURE__ */ new WeakMap(), classMembers: /* @__PURE__ */ new WeakMap() };
|
|
10726
|
+
sharedClassesByLib.set(libs[0], store);
|
|
10727
|
+
return store;
|
|
10728
|
+
}
|
|
10729
|
+
var referencedNamesOf = /* @__PURE__ */ new WeakMap();
|
|
10730
|
+
var typeQueryIn = /* @__PURE__ */ new WeakMap();
|
|
10731
|
+
function referencedTypeNames(node, out) {
|
|
10732
|
+
if (out === void 0 && node && typeof node === "object") {
|
|
10733
|
+
const known = referencedNamesOf.get(node);
|
|
10734
|
+
if (known) return known;
|
|
10735
|
+
const found = referencedTypeNames(node, []);
|
|
10736
|
+
referencedNamesOf.set(node, found);
|
|
10737
|
+
return found;
|
|
10738
|
+
}
|
|
10739
|
+
out = out ?? [];
|
|
10503
10740
|
if (!node || typeof node !== "object") return out;
|
|
10504
10741
|
if (Array.isArray(node)) {
|
|
10505
10742
|
for (const item of node) referencedTypeNames(item, out);
|
|
@@ -10516,9 +10753,11 @@ function referencedTypeNames(node, out = []) {
|
|
|
10516
10753
|
}
|
|
10517
10754
|
function containsTypeQuery(node) {
|
|
10518
10755
|
if (!node || typeof node !== "object") return false;
|
|
10519
|
-
|
|
10520
|
-
if (
|
|
10521
|
-
|
|
10756
|
+
const known = typeQueryIn.get(node);
|
|
10757
|
+
if (known !== void 0) return known;
|
|
10758
|
+
const found = Array.isArray(node) ? node.some(containsTypeQuery) : node.type === "TypeofTypeNode" || Object.values(node).some(containsTypeQuery);
|
|
10759
|
+
typeQueryIn.set(node, found);
|
|
10760
|
+
return found;
|
|
10522
10761
|
}
|
|
10523
10762
|
var STRING_INTRINSICS = /* @__PURE__ */ new Set(["Uppercase", "Lowercase", "Capitalize", "Uncapitalize"]);
|
|
10524
10763
|
function briefType(t) {
|