luaut-parser 1.2.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 +103 -28
- package/dist/index.cjs +769 -59
- package/dist/index.d.cts +127 -25
- package/dist/index.d.ts +127 -25
- package/dist/index.js +760 -49
- package/package.json +2 -2
- package/dist/luau.d.luaut +0 -245
- package/dist/roblox.d.luaut +0 -503
package/dist/index.js
CHANGED
|
@@ -852,6 +852,9 @@ var Parser = class {
|
|
|
852
852
|
}
|
|
853
853
|
if (t.type === "Identifier" && t.value === "declare") {
|
|
854
854
|
const p1 = this.peek(1);
|
|
855
|
+
if (p1.type === "Identifier" && p1.value === "class" && this.peek(2).type === "Identifier") {
|
|
856
|
+
return this.parseDeclareClassStatement();
|
|
857
|
+
}
|
|
855
858
|
if (p1.type === "Identifier" || p1.type === "Keyword" && p1.value === "function") {
|
|
856
859
|
return this.parseDeclareStatement();
|
|
857
860
|
}
|
|
@@ -893,6 +896,36 @@ var Parser = class {
|
|
|
893
896
|
const valueType = this.parseType();
|
|
894
897
|
return { type: "DeclareStatement", name: nameTok.value, id: tokenIdentifier(nameTok), valueType, ...spanFrom(start, this.previous()) };
|
|
895
898
|
}
|
|
899
|
+
/** A declared type's name. It may be qualified once — `Enum.Material` —
|
|
900
|
+
* which is how a definitions file names types under a namespace, and how
|
|
901
|
+
* they are then written (`const m: Enum.Material`). */
|
|
902
|
+
parseTypeName() {
|
|
903
|
+
const first = this.expectIdentifier();
|
|
904
|
+
if (this.checkPunctuator(".") && this.peek(1).type === "Identifier") {
|
|
905
|
+
this.advance();
|
|
906
|
+
const second = this.expectIdentifier();
|
|
907
|
+
return { type: "Identifier", name: `${first.value}.${second.value}`, ...spanFrom(first, second) };
|
|
908
|
+
}
|
|
909
|
+
return tokenIdentifier(first);
|
|
910
|
+
}
|
|
911
|
+
// `declare class Name extends Base { member: T, ... }`
|
|
912
|
+
parseDeclareClassStatement() {
|
|
913
|
+
const start = this.current();
|
|
914
|
+
this.advance();
|
|
915
|
+
this.advance();
|
|
916
|
+
const name = this.parseTypeName();
|
|
917
|
+
let superclass;
|
|
918
|
+
if (this.checkIdentifierValue("extends")) {
|
|
919
|
+
this.advance();
|
|
920
|
+
const base = this.parseType();
|
|
921
|
+
if (base.type !== "TypeReference") this.error("A class can only extend another class, written by name");
|
|
922
|
+
superclass = base;
|
|
923
|
+
}
|
|
924
|
+
if (!this.checkPunctuator("{")) this.error("Expected '{' to start the class body");
|
|
925
|
+
const body = this.parseTableType();
|
|
926
|
+
if (body.type !== "TableTypeNode") this.error("A class body lists members ('name: T'), not a mapped type");
|
|
927
|
+
return { type: "DeclareClassStatement", name, superclass, body, ...spanFrom(start, this.previous()) };
|
|
928
|
+
}
|
|
896
929
|
// `import { a, b as c } from '...'` / `import Default from '...'` /
|
|
897
930
|
// `import Default, { a } from '...'`. Compiled away entirely by the
|
|
898
931
|
// bundler — never survives into emitted Luau.
|
|
@@ -1223,8 +1256,7 @@ var Parser = class {
|
|
|
1223
1256
|
parseTypeAliasStatement() {
|
|
1224
1257
|
const start = this.current();
|
|
1225
1258
|
this.advance();
|
|
1226
|
-
const
|
|
1227
|
-
const name = { type: "Identifier", name: nameTok.value, ...spanFrom(nameTok, nameTok) };
|
|
1259
|
+
const name = this.parseTypeName();
|
|
1228
1260
|
let generics = [];
|
|
1229
1261
|
if (this.checkOperator("<")) {
|
|
1230
1262
|
generics = this.parseGenericTypeParameterList();
|
|
@@ -2726,6 +2758,9 @@ var Analyzer = class {
|
|
|
2726
2758
|
case "DeclareStatement":
|
|
2727
2759
|
this.visitType(stmt.valueType, scope);
|
|
2728
2760
|
return;
|
|
2761
|
+
case "DeclareClassStatement":
|
|
2762
|
+
this.visitType(stmt.body, scope);
|
|
2763
|
+
return;
|
|
2729
2764
|
case "TypeAliasStatement":
|
|
2730
2765
|
case "ExportTypeAliasStatement":
|
|
2731
2766
|
this.visitType(stmt.definition, scope);
|
|
@@ -2899,6 +2934,9 @@ function analyzeScopes(program, options = {}) {
|
|
|
2899
2934
|
}
|
|
2900
2935
|
|
|
2901
2936
|
// src/ast/typeModel.ts
|
|
2937
|
+
function isClassType(t) {
|
|
2938
|
+
return t.kind === "object" && t.class !== void 0;
|
|
2939
|
+
}
|
|
2902
2940
|
function typeParam(name, constraint, isConst) {
|
|
2903
2941
|
return { kind: "typeParam", name, constraint, isConst };
|
|
2904
2942
|
}
|
|
@@ -2958,10 +2996,16 @@ function substitute(t, subst) {
|
|
|
2958
2996
|
}
|
|
2959
2997
|
case "function": {
|
|
2960
2998
|
const inner = t.typeParams ? new Map([...subst].filter(([k]) => !t.typeParams.includes(k))) : subst;
|
|
2999
|
+
let params = t.params.map((p) => ({ ...p, type: substitute(p.type, inner) }));
|
|
3000
|
+
let varargs = t.varargs && substitute(t.varargs, inner);
|
|
3001
|
+
if (varargs?.kind === "tuple" && varargs.isPack) {
|
|
3002
|
+
params = [...params, ...varargs.elements.map((type) => ({ type }))];
|
|
3003
|
+
varargs = void 0;
|
|
3004
|
+
}
|
|
2961
3005
|
return {
|
|
2962
3006
|
kind: "function",
|
|
2963
|
-
params
|
|
2964
|
-
varargs
|
|
3007
|
+
params,
|
|
3008
|
+
varargs,
|
|
2965
3009
|
returns: substitute(t.returns, inner),
|
|
2966
3010
|
typeParams: t.typeParams,
|
|
2967
3011
|
predicate: t.predicate && {
|
|
@@ -3041,7 +3085,8 @@ function unify(param, arg, vars, out) {
|
|
|
3041
3085
|
}
|
|
3042
3086
|
return;
|
|
3043
3087
|
case "object":
|
|
3044
|
-
if (
|
|
3088
|
+
if (param.class) return;
|
|
3089
|
+
if (arg.kind === "object" && !arg.class) {
|
|
3045
3090
|
for (const [k, pv] of param.properties) {
|
|
3046
3091
|
const av = arg.properties.get(k);
|
|
3047
3092
|
if (av) unify(pv.type, av.type, vars, out);
|
|
@@ -3124,7 +3169,7 @@ function widen(t) {
|
|
|
3124
3169
|
case "tuple":
|
|
3125
3170
|
return tuple(t.elements.map(widen), t.isPack);
|
|
3126
3171
|
case "object": {
|
|
3127
|
-
if (t.frozen) return t;
|
|
3172
|
+
if (t.frozen || t.class) return t;
|
|
3128
3173
|
const entries = [];
|
|
3129
3174
|
for (const [k, v] of t.properties) entries.push([k, { ...v, type: widen(v.type) }]);
|
|
3130
3175
|
const w = objectType(entries, t.indexer && { key: t.indexer.key, value: widen(t.indexer.value) });
|
|
@@ -3151,6 +3196,10 @@ function isAssignable(rawA, rawB) {
|
|
|
3151
3196
|
if (expandAlias) {
|
|
3152
3197
|
if (a.kind === "genericRef" && b.kind !== "genericRef") a = expandAlias(a);
|
|
3153
3198
|
else if (b.kind === "genericRef" && a.kind !== "genericRef") b = expandAlias(b);
|
|
3199
|
+
else if (a.kind === "genericRef" && b.kind === "genericRef" && a.name !== b.name) {
|
|
3200
|
+
a = expandAlias(a);
|
|
3201
|
+
b = expandAlias(b);
|
|
3202
|
+
}
|
|
3154
3203
|
if (a === b) return true;
|
|
3155
3204
|
}
|
|
3156
3205
|
for (let i = 0; i < comparing.length; i += 2) {
|
|
@@ -3176,7 +3225,11 @@ function isAssignableInner(a, b) {
|
|
|
3176
3225
|
if (a.kind === "union") return a.types.every((t) => isAssignable(t, b));
|
|
3177
3226
|
if (b.kind === "union") return b.types.some((t) => isAssignable(a, t));
|
|
3178
3227
|
if (b.kind === "intersection") return b.types.every((t) => isAssignable(a, t));
|
|
3179
|
-
if (a.kind === "intersection")
|
|
3228
|
+
if (a.kind === "intersection") {
|
|
3229
|
+
if (a.types.some((t) => isAssignable(t, b))) return true;
|
|
3230
|
+
const merged = mergeObjectMembers(a.types);
|
|
3231
|
+
return merged !== void 0 && isAssignable(merged, b);
|
|
3232
|
+
}
|
|
3180
3233
|
if (a.kind === "literal") {
|
|
3181
3234
|
if (b.kind === "literal") return a.value === b.value;
|
|
3182
3235
|
if (b.kind === "primitive") return b.name === a.base;
|
|
@@ -3202,6 +3255,8 @@ function isAssignableInner(a, b) {
|
|
|
3202
3255
|
}
|
|
3203
3256
|
if (a.kind === "object") {
|
|
3204
3257
|
if (b.kind !== "object") return false;
|
|
3258
|
+
if (b.class) return a.class !== void 0 && a.class.ancestors.includes(b.class.name);
|
|
3259
|
+
if (a.class && (b.indexer || b.properties.size === 0)) return false;
|
|
3205
3260
|
for (const [name, bp] of b.properties) {
|
|
3206
3261
|
const ap = a.properties.get(name);
|
|
3207
3262
|
if (!ap) {
|
|
@@ -3344,6 +3399,7 @@ function containsFreeTypeParam(t, seen, bound) {
|
|
|
3344
3399
|
case "intersection":
|
|
3345
3400
|
return t.types.some((m) => containsTypeParam(m, seen, bound));
|
|
3346
3401
|
case "object":
|
|
3402
|
+
if (t.class) return false;
|
|
3347
3403
|
return [...t.properties.values()].some((v) => containsTypeParam(v.type, seen, bound)) || !!t.indexer && (containsTypeParam(t.indexer.key, seen, bound) || containsTypeParam(t.indexer.value, seen, bound));
|
|
3348
3404
|
case "function": {
|
|
3349
3405
|
const inner = t.typeParams?.length ? /* @__PURE__ */ new Set([...bound, ...t.typeParams]) : bound;
|
|
@@ -3531,6 +3587,39 @@ var IDENT_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
|
3531
3587
|
function formatKey(k) {
|
|
3532
3588
|
return IDENT_KEY.test(k) ? k : JSON.stringify(k);
|
|
3533
3589
|
}
|
|
3590
|
+
function mergeObjectMembers(types) {
|
|
3591
|
+
const objects = [];
|
|
3592
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3593
|
+
const collect = (t) => {
|
|
3594
|
+
if (seen.has(t)) return true;
|
|
3595
|
+
seen.add(t);
|
|
3596
|
+
if (t.kind === "genericRef") {
|
|
3597
|
+
const expanded = expandAlias?.(t);
|
|
3598
|
+
return expanded !== void 0 && expanded !== t && collect(expanded);
|
|
3599
|
+
}
|
|
3600
|
+
if (t.kind === "intersection") return t.types.every(collect);
|
|
3601
|
+
if (t.kind === "object" && !t.class) {
|
|
3602
|
+
objects.push(t);
|
|
3603
|
+
return true;
|
|
3604
|
+
}
|
|
3605
|
+
return false;
|
|
3606
|
+
};
|
|
3607
|
+
if (!types.every(collect) || objects.length < 2) return void 0;
|
|
3608
|
+
const properties = /* @__PURE__ */ new Map();
|
|
3609
|
+
let indexer;
|
|
3610
|
+
for (const object of objects) {
|
|
3611
|
+
indexer ??= object.indexer;
|
|
3612
|
+
for (const [name, property] of object.properties) {
|
|
3613
|
+
const existing = properties.get(name);
|
|
3614
|
+
properties.set(name, existing ? {
|
|
3615
|
+
type: intersection([existing.type, property.type]),
|
|
3616
|
+
optional: existing.optional && property.optional,
|
|
3617
|
+
readonly: existing.readonly || property.readonly
|
|
3618
|
+
} : property);
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
return objectType([...properties], indexer);
|
|
3622
|
+
}
|
|
3534
3623
|
|
|
3535
3624
|
// src/ast/analyzeTypes.ts
|
|
3536
3625
|
function analyzeTypes(program, scopes, options = {}) {
|
|
@@ -3721,6 +3810,62 @@ function keepsLiterals(paramType) {
|
|
|
3721
3810
|
const members = paramType.constraint.kind === "union" ? paramType.constraint.types : [paramType.constraint];
|
|
3722
3811
|
return members.some((m) => m.kind === "literal");
|
|
3723
3812
|
}
|
|
3813
|
+
var AliasMap = class extends Map {
|
|
3814
|
+
pending = /* @__PURE__ */ new Map();
|
|
3815
|
+
defer(name, resolve5) {
|
|
3816
|
+
super.delete(name);
|
|
3817
|
+
this.pending.set(name, resolve5);
|
|
3818
|
+
}
|
|
3819
|
+
get(name) {
|
|
3820
|
+
const resolved = super.get(name);
|
|
3821
|
+
if (resolved !== void 0) return resolved;
|
|
3822
|
+
const resolve5 = this.pending.get(name);
|
|
3823
|
+
if (!resolve5) return void 0;
|
|
3824
|
+
this.pending.delete(name);
|
|
3825
|
+
const type = resolve5();
|
|
3826
|
+
super.set(name, type);
|
|
3827
|
+
return type;
|
|
3828
|
+
}
|
|
3829
|
+
has(name) {
|
|
3830
|
+
return super.has(name) || (this.pending?.has(name) ?? false);
|
|
3831
|
+
}
|
|
3832
|
+
set(name, type) {
|
|
3833
|
+
this.pending?.delete(name);
|
|
3834
|
+
return super.set(name, type);
|
|
3835
|
+
}
|
|
3836
|
+
delete(name) {
|
|
3837
|
+
const deferred = this.pending?.delete(name) ?? false;
|
|
3838
|
+
return super.delete(name) || deferred;
|
|
3839
|
+
}
|
|
3840
|
+
get size() {
|
|
3841
|
+
return super.size + (this.pending?.size ?? 0);
|
|
3842
|
+
}
|
|
3843
|
+
keys() {
|
|
3844
|
+
return [...super.keys(), ...this.pending?.keys() ?? []][Symbol.iterator]();
|
|
3845
|
+
}
|
|
3846
|
+
entries() {
|
|
3847
|
+
return [...this.keys()].map((name) => [name, this.get(name)])[Symbol.iterator]();
|
|
3848
|
+
}
|
|
3849
|
+
values() {
|
|
3850
|
+
return [...this.keys()].map((name) => this.get(name))[Symbol.iterator]();
|
|
3851
|
+
}
|
|
3852
|
+
forEach(callback, thisArg) {
|
|
3853
|
+
for (const [name, type] of this.entries()) callback.call(thisArg, type, name, this);
|
|
3854
|
+
}
|
|
3855
|
+
[Symbol.iterator]() {
|
|
3856
|
+
return this.entries();
|
|
3857
|
+
}
|
|
3858
|
+
};
|
|
3859
|
+
var METAMETHODS = {
|
|
3860
|
+
"+": "__add",
|
|
3861
|
+
"-": "__sub",
|
|
3862
|
+
"*": "__mul",
|
|
3863
|
+
"/": "__div",
|
|
3864
|
+
"//": "__idiv",
|
|
3865
|
+
"%": "__mod",
|
|
3866
|
+
"^": "__pow",
|
|
3867
|
+
"..": "__concat"
|
|
3868
|
+
};
|
|
3724
3869
|
function posKey(name, line, column) {
|
|
3725
3870
|
return `${name}@${line}:${column}`;
|
|
3726
3871
|
}
|
|
@@ -3741,9 +3886,12 @@ var TypeAnalyzer = class {
|
|
|
3741
3886
|
expectedTypeOf = /* @__PURE__ */ new Map();
|
|
3742
3887
|
/** Public: each alias resolved once (generic aliases keep their params as
|
|
3743
3888
|
* `typeParam` nodes in the body). */
|
|
3744
|
-
aliases =
|
|
3889
|
+
aliases = new AliasMap();
|
|
3745
3890
|
/** Uninstantiated alias definitions, for `Name<Args>` instantiation. */
|
|
3746
3891
|
aliasDefs = /* @__PURE__ */ new Map();
|
|
3892
|
+
/** See `resolveClass`. */
|
|
3893
|
+
classTypes = /* @__PURE__ */ new WeakMap();
|
|
3894
|
+
classMembers = /* @__PURE__ */ new WeakMap();
|
|
3747
3895
|
/** Generic parameters currently in lexical scope (alias body / generic fn),
|
|
3748
3896
|
* with their `extends` constraints resolved. */
|
|
3749
3897
|
typeParamScope = [];
|
|
@@ -3853,24 +4001,123 @@ var TypeAnalyzer = class {
|
|
|
3853
4001
|
for (const stmt of block.statements) {
|
|
3854
4002
|
const alias = stmt.type === "TypeAliasStatement" ? stmt : stmt.type === "ExportTypeAliasStatement" ? stmt.alias : void 0;
|
|
3855
4003
|
if (alias) this.aliasDefs.set(alias.name.name, { params: alias.generics, node: alias.definition });
|
|
4004
|
+
if (stmt.type === "DeclareClassStatement") {
|
|
4005
|
+
this.aliasDefs.set(stmt.name.name, { params: [], node: stmt.body, class: stmt });
|
|
4006
|
+
}
|
|
3856
4007
|
}
|
|
3857
4008
|
}
|
|
3858
|
-
/**
|
|
3859
|
-
|
|
3860
|
-
|
|
4009
|
+
/** A non-generic definition's type. */
|
|
4010
|
+
resolveDef(def) {
|
|
4011
|
+
return def.class ? this.classType(def.class) : this.resolveType(def.node);
|
|
4012
|
+
}
|
|
4013
|
+
/** One type per class declaration, so every mention of a class is the same
|
|
4014
|
+
* object — its own members included, which refer back to it. */
|
|
4015
|
+
classType(stmt) {
|
|
4016
|
+
return this.classTypes.get(stmt) ?? this.resolveClass(stmt);
|
|
4017
|
+
}
|
|
4018
|
+
/** A class's members are resolved the first time anyone asks for
|
|
4019
|
+
* `properties` — its own from its body, the inherited ones from its
|
|
4020
|
+
* superclass.
|
|
4021
|
+
*
|
|
4022
|
+
* Both have to wait. A definitions file for a whole engine declares
|
|
4023
|
+
* thousands of classes that all refer to one another; resolving each body
|
|
4024
|
+
* as soon as the class is named would resolve every class on every
|
|
4025
|
+
* analysis, when a script touches a handful. And classes refer to one
|
|
4026
|
+
* another constantly — `Object.IsA` mentions a map of every class, each
|
|
4027
|
+
* of which extends `Object` — so while one class resolves, one it extends
|
|
4028
|
+
* may itself be half-resolved; copying its members then would miss some
|
|
4029
|
+
* for good. */
|
|
4030
|
+
resolveClass(stmt) {
|
|
4031
|
+
const name = stmt.name.name;
|
|
4032
|
+
const { ancestors, cyclic } = this.classChain(stmt);
|
|
4033
|
+
const superclass = !cyclic && ancestors.length > 1 ? this.aliasDefs.get(ancestors[1])?.class : void 0;
|
|
4034
|
+
let own;
|
|
4035
|
+
let resolvingOwn = false;
|
|
4036
|
+
const ownMembers = () => {
|
|
4037
|
+
if (own || resolvingOwn) return own;
|
|
4038
|
+
resolvingOwn = true;
|
|
4039
|
+
try {
|
|
4040
|
+
own = this.resolveType(stmt.body);
|
|
4041
|
+
} finally {
|
|
4042
|
+
resolvingOwn = false;
|
|
4043
|
+
}
|
|
4044
|
+
return own;
|
|
4045
|
+
};
|
|
4046
|
+
let complete;
|
|
4047
|
+
const members = () => {
|
|
4048
|
+
if (complete) return complete;
|
|
4049
|
+
const mine = ownMembers();
|
|
4050
|
+
if (!mine) return void 0;
|
|
4051
|
+
const base = superclass ? this.classMembers.get(this.classType(superclass))?.() : void 0;
|
|
4052
|
+
if (superclass && !base) return void 0;
|
|
4053
|
+
return complete = {
|
|
4054
|
+
properties: new Map([...base?.properties ?? [], ...mine.properties]),
|
|
4055
|
+
indexer: mine.indexer ?? base?.indexer
|
|
4056
|
+
};
|
|
4057
|
+
};
|
|
4058
|
+
const type = { kind: "object", name, class: { name, superclass: superclass?.name.name, ancestors } };
|
|
4059
|
+
Object.defineProperties(type, {
|
|
4060
|
+
properties: { enumerable: true, get: () => members()?.properties ?? own?.properties ?? /* @__PURE__ */ new Map() },
|
|
4061
|
+
indexer: { enumerable: true, get: () => members()?.indexer ?? own?.indexer }
|
|
4062
|
+
});
|
|
4063
|
+
this.classTypes.set(stmt, type);
|
|
4064
|
+
this.classMembers.set(type, members);
|
|
4065
|
+
if (this.program.body.statements.includes(stmt)) ownMembers();
|
|
4066
|
+
return type;
|
|
4067
|
+
}
|
|
4068
|
+
/** `extends` must name a class, and the chain must end. */
|
|
4069
|
+
checkClass(stmt) {
|
|
4070
|
+
if (!stmt.superclass || !this.emitDiagnostics) return;
|
|
4071
|
+
const base = stmt.superclass.base;
|
|
4072
|
+
if (!this.aliasDefs.get(base)?.class) {
|
|
4073
|
+
const known = this.aliasDefs.has(base) || this.importedTypes.has(base);
|
|
4074
|
+
this.diagnostics.push({
|
|
4075
|
+
node: stmt.superclass,
|
|
4076
|
+
message: known ? `'${base}' is not a class; a class can only extend another class` : `Cannot find class '${base}'`
|
|
4077
|
+
});
|
|
4078
|
+
} else if (this.classChain(stmt).cyclic) {
|
|
4079
|
+
this.diagnostics.push({ node: stmt.superclass, message: `'${stmt.name.name}' cannot extend itself` });
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
/** The class and the classes it extends, nearest first, read from the
|
|
4083
|
+
* declarations — no type has to be resolved to know them. The walk stops
|
|
4084
|
+
* at a superclass that is not a class. */
|
|
4085
|
+
classChain(stmt) {
|
|
4086
|
+
const ancestors = [stmt.name.name];
|
|
4087
|
+
for (let cls = stmt; cls?.superclass; ) {
|
|
4088
|
+
const base = cls.superclass.base;
|
|
4089
|
+
if (ancestors.includes(base)) return { ancestors, cyclic: true };
|
|
4090
|
+
cls = this.aliasDefs.get(base)?.class;
|
|
4091
|
+
if (!cls) break;
|
|
4092
|
+
ancestors.push(base);
|
|
4093
|
+
}
|
|
4094
|
+
return { ancestors, cyclic: false };
|
|
4095
|
+
}
|
|
4096
|
+
/** Seed global types from `declare` statements. Repeating a function name
|
|
4097
|
+
* builds an *overload set* (an intersection, in declaration order) rather
|
|
4098
|
+
* than replacing — which is how `typeof` gets one signature per result
|
|
4099
|
+
* string. Any other value is simply redeclared: a sourcemap's
|
|
4100
|
+
* `declare script: <this file's instance>` replaces the library's
|
|
4101
|
+
* `declare script: LuaSourceContainer`. */
|
|
3861
4102
|
harvestDeclares(block) {
|
|
3862
4103
|
for (const stmt of block.statements) {
|
|
3863
4104
|
if (stmt.type !== "DeclareStatement") continue;
|
|
3864
4105
|
const t = this.resolveType(stmt.valueType);
|
|
3865
4106
|
const prev = this.libGlobalTypes.get(stmt.name);
|
|
3866
|
-
|
|
4107
|
+
const overload = prev && stmt.valueType.type === "FunctionTypeNode" && (prev.kind === "function" || prev.kind === "intersection");
|
|
4108
|
+
this.libGlobalTypes.set(stmt.name, overload ? intersection([prev, t]) : t);
|
|
3867
4109
|
}
|
|
3868
4110
|
}
|
|
3869
4111
|
resolveAllAliases() {
|
|
3870
4112
|
for (const [name, def] of this.aliasDefs) {
|
|
4113
|
+
if (def.class && !this.program.body.statements.includes(def.class)) {
|
|
4114
|
+
const cls = def.class;
|
|
4115
|
+
this.aliases.defer(name, () => this.classType(cls));
|
|
4116
|
+
continue;
|
|
4117
|
+
}
|
|
3871
4118
|
if (containsTypeQuery(def.node)) continue;
|
|
3872
4119
|
this.withTypeParams(def.params, () => {
|
|
3873
|
-
this.aliases.set(name, this.
|
|
4120
|
+
this.aliases.set(name, this.resolveDef(def));
|
|
3874
4121
|
});
|
|
3875
4122
|
}
|
|
3876
4123
|
}
|
|
@@ -3880,7 +4127,7 @@ var TypeAnalyzer = class {
|
|
|
3880
4127
|
for (const [name, def] of this.aliasDefs) {
|
|
3881
4128
|
if (this.aliases.has(name)) continue;
|
|
3882
4129
|
this.withTypeParams(def.params, () => {
|
|
3883
|
-
this.aliases.set(name, this.
|
|
4130
|
+
this.aliases.set(name, this.resolveDef(def));
|
|
3884
4131
|
});
|
|
3885
4132
|
}
|
|
3886
4133
|
return this.aliases;
|
|
@@ -3908,10 +4155,7 @@ var TypeAnalyzer = class {
|
|
|
3908
4155
|
/** Instantiate a generic alias: `Box<number>` -> `{ value: number }`. */
|
|
3909
4156
|
instantiateAlias(def, args) {
|
|
3910
4157
|
if (this.instantiationDepth > 20) return unknownType;
|
|
3911
|
-
const subst =
|
|
3912
|
-
def.params.forEach((p, i) => {
|
|
3913
|
-
subst.set(p.name, args[i] ?? (p.default ? this.resolveType(p.default) : unknownType));
|
|
3914
|
-
});
|
|
4158
|
+
const subst = this.bindTypeArguments(def.params, args);
|
|
3915
4159
|
this.instantiationDepth++;
|
|
3916
4160
|
try {
|
|
3917
4161
|
const body = this.withTypeParams(def.params, () => this.resolveType(def.node));
|
|
@@ -3920,6 +4164,24 @@ var TypeAnalyzer = class {
|
|
|
3920
4164
|
this.instantiationDepth--;
|
|
3921
4165
|
}
|
|
3922
4166
|
}
|
|
4167
|
+
/** Pair written type arguments with the parameters they instantiate. A
|
|
4168
|
+
* pack parameter (`T...`) takes every argument from its position on, as
|
|
4169
|
+
* one pack: `Signal<Instance, string>` binds `T` to `(Instance, string)`,
|
|
4170
|
+
* and `Signal<()>` to the empty pack. Left out, a parameter takes its
|
|
4171
|
+
* default (`T... = ...any` is `any`), or `unknown`. */
|
|
4172
|
+
bindTypeArguments(params, args) {
|
|
4173
|
+
const subst = /* @__PURE__ */ new Map();
|
|
4174
|
+
params.forEach((p, i) => {
|
|
4175
|
+
let arg = args[i];
|
|
4176
|
+
if (p.isPack && i < args.length) {
|
|
4177
|
+
const rest = args.slice(i);
|
|
4178
|
+
const single = rest.length === 1 ? rest[0] : void 0;
|
|
4179
|
+
arg = single && (single.kind === "tuple" && single.isPack || single.kind === "typeParam" || single.kind === "any") ? single : tuple([...rest], true);
|
|
4180
|
+
}
|
|
4181
|
+
subst.set(p.name, arg ?? (p.default ? this.resolveType(p.default) : unknownType));
|
|
4182
|
+
});
|
|
4183
|
+
return subst;
|
|
4184
|
+
}
|
|
3923
4185
|
// --------------------------------------------------------
|
|
3924
4186
|
// TypeNode -> Type
|
|
3925
4187
|
// --------------------------------------------------------
|
|
@@ -3981,6 +4243,12 @@ var TypeAnalyzer = class {
|
|
|
3981
4243
|
}
|
|
3982
4244
|
const lib = this.options.libTypes?.[node.base];
|
|
3983
4245
|
if (lib) return lib;
|
|
4246
|
+
} else if (this.aliasDefs.has(name)) {
|
|
4247
|
+
return this.expand({
|
|
4248
|
+
kind: "genericRef",
|
|
4249
|
+
name,
|
|
4250
|
+
typeArguments: node.typeArguments.map((a) => this.resolveType(a))
|
|
4251
|
+
});
|
|
3984
4252
|
}
|
|
3985
4253
|
return {
|
|
3986
4254
|
kind: "genericRef",
|
|
@@ -4107,6 +4375,7 @@ var TypeAnalyzer = class {
|
|
|
4107
4375
|
return this.resolveType(node.typeAnnotation);
|
|
4108
4376
|
case "TypePackNode": {
|
|
4109
4377
|
if (node.types.length === 1 && !node.hasVarargs) return this.resolveType(node.types[0]);
|
|
4378
|
+
if (!node.types.length && node.varargType) return this.resolveType(node.varargType);
|
|
4110
4379
|
return tuple(node.types.map((t) => this.resolveType(t)), true);
|
|
4111
4380
|
}
|
|
4112
4381
|
}
|
|
@@ -4130,7 +4399,7 @@ var TypeAnalyzer = class {
|
|
|
4130
4399
|
this.reduceDepth++;
|
|
4131
4400
|
try {
|
|
4132
4401
|
const result = this.reduceTypeInner(t);
|
|
4133
|
-
this.reduceCache.set(t, result);
|
|
4402
|
+
if (result.kind !== "keyof") this.reduceCache.set(t, result);
|
|
4134
4403
|
return result;
|
|
4135
4404
|
} finally {
|
|
4136
4405
|
this.reduceDepth--;
|
|
@@ -4142,6 +4411,7 @@ var TypeAnalyzer = class {
|
|
|
4142
4411
|
case "keyof": {
|
|
4143
4412
|
const target = this.reduceType(t.target);
|
|
4144
4413
|
if (containsTypeParam(target)) return { kind: "keyof", target };
|
|
4414
|
+
if (target.kind === "genericRef" && this.resolvingAliases.has(target.name)) return t;
|
|
4145
4415
|
return this.keysOf(target);
|
|
4146
4416
|
}
|
|
4147
4417
|
case "indexedAccess": {
|
|
@@ -4183,6 +4453,7 @@ var TypeAnalyzer = class {
|
|
|
4183
4453
|
t.predicate
|
|
4184
4454
|
);
|
|
4185
4455
|
case "object": {
|
|
4456
|
+
if (t.class) return t;
|
|
4186
4457
|
const entries = [];
|
|
4187
4458
|
for (const [k, v] of t.properties) entries.push([k, { ...v, type: this.reduceType(v.type) }]);
|
|
4188
4459
|
const reduced = objectType(entries, t.indexer && {
|
|
@@ -4322,6 +4593,7 @@ var TypeAnalyzer = class {
|
|
|
4322
4593
|
t.typeParams
|
|
4323
4594
|
);
|
|
4324
4595
|
case "object": {
|
|
4596
|
+
if (t.class) return t;
|
|
4325
4597
|
const entries = [];
|
|
4326
4598
|
for (const [k, v] of t.properties) entries.push([k, { ...v, type: this.stripInfer(v.type, bindings) }]);
|
|
4327
4599
|
return objectType(entries, t.indexer && {
|
|
@@ -4375,6 +4647,11 @@ var TypeAnalyzer = class {
|
|
|
4375
4647
|
visitStatement(stmt, env) {
|
|
4376
4648
|
switch (stmt.type) {
|
|
4377
4649
|
case "VariableDeclaration": {
|
|
4650
|
+
stmt.names.forEach((target, i) => {
|
|
4651
|
+
if (target.type === "IdentifierPattern" && target.typeAnnotation && stmt.init[i]) {
|
|
4652
|
+
this.applyContext(stmt.init[i], this.resolveType(target.typeAnnotation));
|
|
4653
|
+
}
|
|
4654
|
+
});
|
|
4378
4655
|
const { types: valueTypes, sources } = this.valueList(stmt.init, env);
|
|
4379
4656
|
stmt.names.forEach((target, i) => {
|
|
4380
4657
|
const inferred = valueTypes[i] ?? (stmt.init.length ? unknownType : nilType);
|
|
@@ -4435,6 +4712,16 @@ var TypeAnalyzer = class {
|
|
|
4435
4712
|
return;
|
|
4436
4713
|
}
|
|
4437
4714
|
case "AssignmentStatement": {
|
|
4715
|
+
stmt.targets.forEach((target, i) => {
|
|
4716
|
+
const value = stmt.values[i];
|
|
4717
|
+
if (!value) return;
|
|
4718
|
+
if (target.type === "MemberExpression" || target.type === "IndexExpression") {
|
|
4719
|
+
this.applyContext(value, this.infer(target, env));
|
|
4720
|
+
} else if (target.type === "Identifier") {
|
|
4721
|
+
const id = this.bindingIdOf(target);
|
|
4722
|
+
if (id !== void 0 && this.annotated.has(id)) this.applyContext(value, this.bindingType.get(id));
|
|
4723
|
+
}
|
|
4724
|
+
});
|
|
4438
4725
|
const { types: valueTypes, sources } = this.valueList(stmt.values, env);
|
|
4439
4726
|
stmt.targets.forEach((target, i) => {
|
|
4440
4727
|
const vt = valueTypes[i] ?? unknownType;
|
|
@@ -4579,6 +4866,9 @@ var TypeAnalyzer = class {
|
|
|
4579
4866
|
case "BreakStatement":
|
|
4580
4867
|
this.breakStates[this.breakStates.length - 1]?.push(forkEnv(env));
|
|
4581
4868
|
return;
|
|
4869
|
+
case "DeclareClassStatement":
|
|
4870
|
+
this.checkClass(stmt);
|
|
4871
|
+
return;
|
|
4582
4872
|
case "ContinueStatement":
|
|
4583
4873
|
case "TypeAliasStatement":
|
|
4584
4874
|
case "ExportTypeAliasStatement":
|
|
@@ -4700,7 +4990,46 @@ var TypeAnalyzer = class {
|
|
|
4700
4990
|
}
|
|
4701
4991
|
if (p.pattern) return this.patternToType(p.pattern, env);
|
|
4702
4992
|
if (p.default) return widen(this.infer(p.default, env));
|
|
4703
|
-
return anyType;
|
|
4993
|
+
return this.contextualParams.get(p) ?? anyType;
|
|
4994
|
+
}
|
|
4995
|
+
/** What a function expression's unannotated parameters are, from where
|
|
4996
|
+
* it is written — see `applyContext`. */
|
|
4997
|
+
contextualParams = /* @__PURE__ */ new WeakMap();
|
|
4998
|
+
/** `expected` is the type the surroundings want for `expr`. A function
|
|
4999
|
+
* expression written there takes its unannotated parameters' types from
|
|
5000
|
+
* it, as in TypeScript: `signal:Connect(function(player) ... end)` knows
|
|
5001
|
+
* `player` from `Connect`'s callback type. Anything else is inferred as
|
|
5002
|
+
* usual. */
|
|
5003
|
+
applyContext(expr, expected) {
|
|
5004
|
+
let e = expr;
|
|
5005
|
+
while (e.type === "ParenthesizedExpression") e = e.expression;
|
|
5006
|
+
if (e.type !== "FunctionExpression" || !expected) return;
|
|
5007
|
+
const members = expected.kind === "union" ? expected.types : [expected];
|
|
5008
|
+
const signatures = members.flatMap((m) => this.overloadsOf(this.expand(m)));
|
|
5009
|
+
if (!signatures.length) return;
|
|
5010
|
+
e.func.params.forEach((p, k) => {
|
|
5011
|
+
if (p.typeAnnotation || p.pattern || p.default) return;
|
|
5012
|
+
const candidates = [];
|
|
5013
|
+
for (const signature of signatures) {
|
|
5014
|
+
const t2 = signature.params[k]?.type ?? signature.varargs;
|
|
5015
|
+
if (t2) candidates.push(t2);
|
|
5016
|
+
}
|
|
5017
|
+
if (!candidates.length) return;
|
|
5018
|
+
const t = union(candidates);
|
|
5019
|
+
this.contextualParams.set(p, containsTypeParam(t) ? anyType : t);
|
|
5020
|
+
});
|
|
5021
|
+
}
|
|
5022
|
+
/** The parameter type each written argument lands on, across `fns`. */
|
|
5023
|
+
expectedArguments(written, fns, selfOf) {
|
|
5024
|
+
return written.map((_, j) => {
|
|
5025
|
+
const candidates = [];
|
|
5026
|
+
for (const f of fns) {
|
|
5027
|
+
const i = j + selfOf(f);
|
|
5028
|
+
const param = i < f.params.length ? this.boundParams(f)[i] : f.varargs;
|
|
5029
|
+
if (param) candidates.push(param);
|
|
5030
|
+
}
|
|
5031
|
+
return candidates.length ? union(candidates) : void 0;
|
|
5032
|
+
});
|
|
4704
5033
|
}
|
|
4705
5034
|
/** Synthesize a type from a destructuring pattern used without an
|
|
4706
5035
|
* annotation (`function f({ a, b = 1 })`). */
|
|
@@ -4752,7 +5081,8 @@ var TypeAnalyzer = class {
|
|
|
4752
5081
|
f.params.forEach((p, i) => {
|
|
4753
5082
|
const arg = argTypes[i];
|
|
4754
5083
|
if (arg === void 0) return;
|
|
4755
|
-
|
|
5084
|
+
const param = p.type.kind === "typeParam" && p.type.constraint ? { ...p.type, constraint: this.reduceType(p.type.constraint) } : p.type;
|
|
5085
|
+
unify(p.type, keepsLiterals(param) ? arg : widen(arg), vars, subst);
|
|
4756
5086
|
});
|
|
4757
5087
|
for (const name of f.typeParams ?? []) if (!subst.has(name)) subst.set(name, unknownType);
|
|
4758
5088
|
return subst;
|
|
@@ -4813,12 +5143,13 @@ var TypeAnalyzer = class {
|
|
|
4813
5143
|
return;
|
|
4814
5144
|
}
|
|
4815
5145
|
const t = value;
|
|
5146
|
+
if (t.kind === "object" && t.class) return;
|
|
4816
5147
|
if (t.kind === "typeParam" && typeof t.name === "string" && bounds.has(t.name) && t.constraint && !containsTypeParam(t.constraint)) {
|
|
4817
5148
|
bounds.set(t.name, this.reduceType(t.constraint));
|
|
4818
5149
|
}
|
|
4819
5150
|
for (const child of Object.values(value)) walk(child);
|
|
4820
5151
|
};
|
|
4821
|
-
for (const p of f.params) walk(p.type);
|
|
5152
|
+
for (const p of f.params) if (containsTypeParam(p.type)) walk(p.type);
|
|
4822
5153
|
return f.params.map((p) => substitute(p.type, bounds));
|
|
4823
5154
|
}
|
|
4824
5155
|
/** Record what each written argument is expected to be — see
|
|
@@ -5157,7 +5488,7 @@ var TypeAnalyzer = class {
|
|
|
5157
5488
|
this.expandCache.set(key, t);
|
|
5158
5489
|
this.resolvingAliases.add(t.name);
|
|
5159
5490
|
try {
|
|
5160
|
-
const r = def.params.length ? this.instantiateAlias(def, t.typeArguments) : this.
|
|
5491
|
+
const r = def.params.length ? this.instantiateAlias(def, t.typeArguments) : this.resolveDef(def);
|
|
5161
5492
|
const named = def.params.length === 0 && (r.kind === "object" || r.kind === "intersection") && !r.name ? { ...r, name: t.name } : r;
|
|
5162
5493
|
this.expandCache.set(key, named);
|
|
5163
5494
|
return named;
|
|
@@ -5280,9 +5611,9 @@ var TypeAnalyzer = class {
|
|
|
5280
5611
|
case "not":
|
|
5281
5612
|
return booleanType;
|
|
5282
5613
|
case "-":
|
|
5283
|
-
return numberType;
|
|
5614
|
+
return this.operatorResult(expr, "-", arg, void 0) ?? numberType;
|
|
5284
5615
|
case "#":
|
|
5285
|
-
return numberType;
|
|
5616
|
+
return this.operatorResult(expr, "#", arg, void 0) ?? numberType;
|
|
5286
5617
|
}
|
|
5287
5618
|
return arg;
|
|
5288
5619
|
}
|
|
@@ -5304,7 +5635,7 @@ var TypeAnalyzer = class {
|
|
|
5304
5635
|
const r = this.infer(expr.right, env);
|
|
5305
5636
|
switch (op) {
|
|
5306
5637
|
case "..":
|
|
5307
|
-
return stringType;
|
|
5638
|
+
return this.operatorResult(expr, op, l, r) ?? stringType;
|
|
5308
5639
|
case "==":
|
|
5309
5640
|
case "~=":
|
|
5310
5641
|
case "<":
|
|
@@ -5319,7 +5650,7 @@ var TypeAnalyzer = class {
|
|
|
5319
5650
|
case "//":
|
|
5320
5651
|
case "%":
|
|
5321
5652
|
case "^":
|
|
5322
|
-
return numberType;
|
|
5653
|
+
return this.operatorResult(expr, op, l, r) ?? numberType;
|
|
5323
5654
|
}
|
|
5324
5655
|
return union([l, r]);
|
|
5325
5656
|
}
|
|
@@ -5338,8 +5669,10 @@ var TypeAnalyzer = class {
|
|
|
5338
5669
|
}
|
|
5339
5670
|
case "CallExpression": {
|
|
5340
5671
|
const callee = this.infer(expr.callee, env);
|
|
5341
|
-
const argTypes = expr.arguments.map((a) => this.infer(a, env));
|
|
5342
5672
|
const fns = this.overloadsOf(callee);
|
|
5673
|
+
const expected = this.expectedArguments(expr.arguments, fns, () => 0);
|
|
5674
|
+
expr.arguments.forEach((a, i) => this.applyContext(a, expected[i]));
|
|
5675
|
+
const argTypes = expr.arguments.map((a) => this.infer(a, env));
|
|
5343
5676
|
if (fns.length) {
|
|
5344
5677
|
this.recordExpected(expr.arguments, fns, () => 0);
|
|
5345
5678
|
const arityFits = this.checkArity(expr, fns, argTypes.length, 0);
|
|
@@ -5354,8 +5687,10 @@ var TypeAnalyzer = class {
|
|
|
5354
5687
|
}
|
|
5355
5688
|
case "MethodCallExpression": {
|
|
5356
5689
|
const objType = this.infer(expr.object, env);
|
|
5357
|
-
const argTypes = expr.arguments.map((a) => this.infer(a, env));
|
|
5358
5690
|
const fns = this.overloadsOf(this.propertyType(objType, expr.method.name));
|
|
5691
|
+
const expected = this.expectedArguments(expr.arguments, fns, (f) => this.takesSelf(f) ? 1 : 0);
|
|
5692
|
+
expr.arguments.forEach((a, i) => this.applyContext(a, expected[i]));
|
|
5693
|
+
const argTypes = expr.arguments.map((a) => this.infer(a, env));
|
|
5359
5694
|
if (fns.length) {
|
|
5360
5695
|
const withSelf = (f) => this.takesSelf(f) ? [objType, ...argTypes] : argTypes;
|
|
5361
5696
|
const selfOf = (f) => this.takesSelf(f) ? 1 : 0;
|
|
@@ -5747,6 +6082,35 @@ var TypeAnalyzer = class {
|
|
|
5747
6082
|
this.selfType = saved;
|
|
5748
6083
|
}
|
|
5749
6084
|
}
|
|
6085
|
+
/** What an operator on a value with metamethods gives: `a + b` calls
|
|
6086
|
+
* `__add` on `a`, or failing that on `b` with the operands swapped — the
|
|
6087
|
+
* order Luau tries them in. That is how `Vector3 + Vector3`, `CFrame *
|
|
6088
|
+
* Vector3` and `2 * vector` get their types from the declarations.
|
|
6089
|
+
* `undefined` when neither operand declares the metamethod; an operand
|
|
6090
|
+
* that declares it but accepts neither argument is reported. */
|
|
6091
|
+
operatorResult(node, op, left, right) {
|
|
6092
|
+
const name = right === void 0 ? op === "-" ? "__unm" : "__len" : METAMETHODS[op];
|
|
6093
|
+
if (!name) return void 0;
|
|
6094
|
+
const candidates = right === void 0 ? [[left, void 0]] : [[left, right], [right, left]];
|
|
6095
|
+
let declared;
|
|
6096
|
+
for (const [receiver, other] of candidates) {
|
|
6097
|
+
const t = this.expand(receiver);
|
|
6098
|
+
const method = t.kind === "object" ? t.properties.get(name) : void 0;
|
|
6099
|
+
if (!method) continue;
|
|
6100
|
+
declared ??= receiver;
|
|
6101
|
+
const args = other === void 0 ? [receiver] : [receiver, other];
|
|
6102
|
+
const picked = this.pickOverload(this.overloadsOf(method.type), args);
|
|
6103
|
+
if (picked) return this.callReturn(picked, args);
|
|
6104
|
+
}
|
|
6105
|
+
if (declared && this.emitDiagnostics) {
|
|
6106
|
+
this.diagnostics.push({
|
|
6107
|
+
node,
|
|
6108
|
+
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)}'`
|
|
6109
|
+
});
|
|
6110
|
+
return anyType;
|
|
6111
|
+
}
|
|
6112
|
+
return void 0;
|
|
6113
|
+
}
|
|
5750
6114
|
/** Does this signature take the receiver as its first parameter?
|
|
5751
6115
|
*
|
|
5752
6116
|
* Luau's `:` is sugar both ways: `function T:m(a)` declares
|
|
@@ -5823,22 +6187,366 @@ function briefType(t) {
|
|
|
5823
6187
|
return formatType(t);
|
|
5824
6188
|
}
|
|
5825
6189
|
|
|
5826
|
-
// src/
|
|
5827
|
-
import { readFileSync } from "fs";
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
6190
|
+
// src/project/host.ts
|
|
6191
|
+
import { readFileSync, statSync } from "fs";
|
|
6192
|
+
var nodeHost = {
|
|
6193
|
+
readFile(path) {
|
|
6194
|
+
try {
|
|
6195
|
+
return statSync(path).isFile() ? readFileSync(path, "utf8") : void 0;
|
|
6196
|
+
} catch {
|
|
6197
|
+
return void 0;
|
|
6198
|
+
}
|
|
6199
|
+
}
|
|
6200
|
+
};
|
|
6201
|
+
|
|
6202
|
+
// src/project/config.ts
|
|
6203
|
+
import { dirname, join, resolve } from "path";
|
|
6204
|
+
var CONFIG_FILE_NAMES = ["luaut.config.json", "luaut.config.jsonc"];
|
|
6205
|
+
function findConfig(file, host = nodeHost) {
|
|
6206
|
+
const searched = [];
|
|
6207
|
+
let directory = dirname(resolve(file));
|
|
6208
|
+
for (; ; ) {
|
|
6209
|
+
const found = [];
|
|
6210
|
+
for (const name of CONFIG_FILE_NAMES) {
|
|
6211
|
+
const path = join(directory, name);
|
|
6212
|
+
searched.push(path);
|
|
6213
|
+
if (host.readFile(path) !== void 0) found.push(path);
|
|
6214
|
+
}
|
|
6215
|
+
if (found.length > 1) {
|
|
6216
|
+
const message = `Only one luaut config may be in a folder, but both ${CONFIG_FILE_NAMES.join(" and ")} are in ${directory}`;
|
|
6217
|
+
return { searched, problems: found.map((path) => ({ file: path, message, line: 1, column: 1 })) };
|
|
6218
|
+
}
|
|
6219
|
+
if (found.length === 1) {
|
|
6220
|
+
const { config, problems } = loadConfig(found[0], host);
|
|
6221
|
+
return { config, problems, searched };
|
|
6222
|
+
}
|
|
6223
|
+
const parent = dirname(directory);
|
|
6224
|
+
if (parent === directory) return { searched, problems: [] };
|
|
6225
|
+
directory = parent;
|
|
6226
|
+
}
|
|
6227
|
+
}
|
|
6228
|
+
var OPTIONS = ["types", "paths", "baseUrl", "sourceMap"];
|
|
6229
|
+
function loadConfig(path, host = nodeHost) {
|
|
6230
|
+
const file = resolve(path);
|
|
6231
|
+
const source = host.readFile(file);
|
|
6232
|
+
if (source === void 0) return { problems: [{ file, message: "Cannot read the config file" }] };
|
|
6233
|
+
let raw;
|
|
6234
|
+
try {
|
|
6235
|
+
raw = JSON.parse(stripJsonComments(source));
|
|
6236
|
+
} catch (error) {
|
|
6237
|
+
const message = error.message;
|
|
6238
|
+
return { problems: [{ file, message: `Invalid JSON: ${message}`, ...jsonErrorPosition(source, message) }] };
|
|
6239
|
+
}
|
|
6240
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
6241
|
+
return { problems: [{ file, message: "The config must be a JSON object", line: 1, column: 1 }] };
|
|
6242
|
+
}
|
|
6243
|
+
const directory = dirname(file);
|
|
6244
|
+
const options = raw;
|
|
6245
|
+
const problems = [];
|
|
6246
|
+
const at = (key) => keyPosition(source, key);
|
|
6247
|
+
const problem = (key, message) => {
|
|
6248
|
+
problems.push({ file, message, ...at(key) });
|
|
6249
|
+
};
|
|
6250
|
+
for (const key of Object.keys(options)) {
|
|
6251
|
+
if (!OPTIONS.includes(key)) {
|
|
6252
|
+
problem(key, `Unknown option '${key}'. Options are: ${OPTIONS.join(", ")}`);
|
|
6253
|
+
}
|
|
6254
|
+
}
|
|
6255
|
+
let types = [];
|
|
6256
|
+
if (options.types !== void 0) {
|
|
6257
|
+
if (Array.isArray(options.types) && options.types.every((t) => typeof t === "string")) types = options.types;
|
|
6258
|
+
else problem("types", `'types' must be an array of strings, such as ["luau"]`);
|
|
6259
|
+
}
|
|
6260
|
+
const paths = {};
|
|
6261
|
+
if (options.paths !== void 0) {
|
|
6262
|
+
const value = options.paths;
|
|
6263
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
6264
|
+
for (const [pattern, targets] of Object.entries(value)) {
|
|
6265
|
+
if (Array.isArray(targets) && targets.every((t) => typeof t === "string")) paths[pattern] = targets;
|
|
6266
|
+
else problem(pattern, `'paths' entry '${pattern}' must be an array of strings`);
|
|
6267
|
+
if (pattern.split("*").length > 2) problem(pattern, `'paths' pattern '${pattern}' may contain at most one '*'`);
|
|
6268
|
+
}
|
|
6269
|
+
} else {
|
|
6270
|
+
problem("paths", `'paths' must be an object, such as { "@shared/*": ["src/shared/*"] }`);
|
|
6271
|
+
}
|
|
6272
|
+
}
|
|
6273
|
+
let baseUrl = directory;
|
|
6274
|
+
if (options.baseUrl !== void 0) {
|
|
6275
|
+
if (typeof options.baseUrl === "string") baseUrl = resolve(directory, options.baseUrl);
|
|
6276
|
+
else problem("baseUrl", "'baseUrl' must be a string");
|
|
6277
|
+
}
|
|
6278
|
+
let sourceMap = null;
|
|
6279
|
+
if (options.sourceMap !== void 0 && options.sourceMap !== null) {
|
|
6280
|
+
if (typeof options.sourceMap === "string") sourceMap = resolve(directory, options.sourceMap);
|
|
6281
|
+
else problem("sourceMap", "'sourceMap' must be a path string, or null for none");
|
|
6282
|
+
}
|
|
6283
|
+
return { config: { path: file, directory, source, types, paths, baseUrl, sourceMap }, problems };
|
|
6284
|
+
}
|
|
6285
|
+
function stripJsonComments(text) {
|
|
6286
|
+
const out = text.split("");
|
|
6287
|
+
let i = 0;
|
|
6288
|
+
let inString = false;
|
|
6289
|
+
while (i < text.length) {
|
|
6290
|
+
const ch = text[i];
|
|
6291
|
+
if (inString) {
|
|
6292
|
+
if (ch === "\\") i += 2;
|
|
6293
|
+
else {
|
|
6294
|
+
if (ch === '"') inString = false;
|
|
6295
|
+
i++;
|
|
6296
|
+
}
|
|
6297
|
+
continue;
|
|
6298
|
+
}
|
|
6299
|
+
if (ch === '"') {
|
|
6300
|
+
inString = true;
|
|
6301
|
+
i++;
|
|
6302
|
+
} else if (ch === "/" && text[i + 1] === "/") {
|
|
6303
|
+
while (i < text.length && text[i] !== "\n") out[i++] = " ";
|
|
6304
|
+
} else if (ch === "/" && text[i + 1] === "*") {
|
|
6305
|
+
out[i++] = " ";
|
|
6306
|
+
out[i++] = " ";
|
|
6307
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) {
|
|
6308
|
+
if (text[i] !== "\n") out[i] = " ";
|
|
6309
|
+
i++;
|
|
6310
|
+
}
|
|
6311
|
+
if (i < text.length) {
|
|
6312
|
+
out[i++] = " ";
|
|
6313
|
+
out[i++] = " ";
|
|
6314
|
+
}
|
|
6315
|
+
} else if (ch === ",") {
|
|
6316
|
+
let j = i + 1;
|
|
6317
|
+
while (j < text.length && /\s/.test(text[j])) j++;
|
|
6318
|
+
if (text[j] === "}" || text[j] === "]") out[i] = " ";
|
|
6319
|
+
i++;
|
|
6320
|
+
} else {
|
|
6321
|
+
i++;
|
|
6322
|
+
}
|
|
6323
|
+
}
|
|
6324
|
+
return out.join("");
|
|
6325
|
+
}
|
|
6326
|
+
function jsonErrorPosition(source, message) {
|
|
6327
|
+
const lineColumn = /line (\d+) column (\d+)/.exec(message);
|
|
6328
|
+
if (lineColumn) return { line: Number(lineColumn[1]), column: Number(lineColumn[2]) };
|
|
6329
|
+
const position = /position (\d+)/.exec(message);
|
|
6330
|
+
return position ? offsetPosition(source, Number(position[1])) : { line: 1, column: 1 };
|
|
6331
|
+
}
|
|
6332
|
+
function keyPosition(source, key) {
|
|
6333
|
+
const offset = source.indexOf(JSON.stringify(key));
|
|
6334
|
+
return offset < 0 ? { line: 1, column: 1 } : offsetPosition(source, offset);
|
|
6335
|
+
}
|
|
6336
|
+
function offsetPosition(source, offset) {
|
|
6337
|
+
const before = source.slice(0, offset);
|
|
6338
|
+
const line = before.split("\n").length;
|
|
6339
|
+
return { line, column: offset - before.lastIndexOf("\n") };
|
|
6340
|
+
}
|
|
6341
|
+
|
|
6342
|
+
// src/project/libraries.ts
|
|
6343
|
+
import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
6344
|
+
function resolveTypeLibraries(config, host = nodeHost) {
|
|
6345
|
+
const files = [];
|
|
6346
|
+
const problems = [];
|
|
6347
|
+
const loaded = /* @__PURE__ */ new Set();
|
|
6348
|
+
const addFile = (file) => {
|
|
6349
|
+
const key = pathKey(file);
|
|
6350
|
+
if (loaded.has(key)) return;
|
|
6351
|
+
loaded.add(key);
|
|
6352
|
+
files.push(file);
|
|
6353
|
+
};
|
|
6354
|
+
const addPackage = (directory, entryFile, visiting) => {
|
|
6355
|
+
const key = pathKey(directory);
|
|
6356
|
+
if (visiting.has(key)) return;
|
|
6357
|
+
visiting.add(key);
|
|
6358
|
+
for (const dependency of dependencyNames(directory, host)) {
|
|
6359
|
+
const found = findPackage(dependency, directory, host);
|
|
6360
|
+
if (found) addPackage(found.directory, found.file, visiting);
|
|
6361
|
+
}
|
|
6362
|
+
addFile(entryFile);
|
|
6363
|
+
};
|
|
6364
|
+
for (const entry of config.types) {
|
|
6365
|
+
const relative = entry.startsWith("./") || entry.startsWith("../") || entry.startsWith("/") || /^[A-Za-z]:[\\/]/.test(entry);
|
|
6366
|
+
if (relative) {
|
|
6367
|
+
const target = resolve2(config.directory, entry);
|
|
6368
|
+
if (entry.endsWith(".luaut")) {
|
|
6369
|
+
if (host.readFile(target) !== void 0) addFile(target);
|
|
6370
|
+
else problems.push({ file: config.path, message: `Cannot find type library file '${entry}'`, ...entryPosition(config, entry) });
|
|
6371
|
+
continue;
|
|
6372
|
+
}
|
|
6373
|
+
const file = packageEntry(target, host);
|
|
6374
|
+
if (file) addPackage(target, file, /* @__PURE__ */ new Set());
|
|
6375
|
+
else problems.push({ file: config.path, message: `'${entry}' has no ${ENTRY_FILE} (or 'luaut.types' in its package.json)`, ...entryPosition(config, entry) });
|
|
6376
|
+
continue;
|
|
6377
|
+
}
|
|
6378
|
+
const names = entry.startsWith("@") || entry.includes("/") ? [entry] : [`@luaut/${entry}`, entry];
|
|
6379
|
+
const found = names.map((name) => findPackage(name, config.directory, host)).find(Boolean);
|
|
6380
|
+
if (found) addPackage(found.directory, found.file, /* @__PURE__ */ new Set());
|
|
6381
|
+
else {
|
|
6382
|
+
problems.push({
|
|
6383
|
+
file: config.path,
|
|
6384
|
+
message: `Cannot find type library '${entry}'. Install it with: npm i -D ${names[0]}`,
|
|
6385
|
+
...entryPosition(config, entry)
|
|
6386
|
+
});
|
|
6387
|
+
}
|
|
6388
|
+
}
|
|
6389
|
+
return { files, problems };
|
|
6390
|
+
}
|
|
6391
|
+
var ENTRY_FILE = "index.d.luaut";
|
|
6392
|
+
function packageEntry(directory, host) {
|
|
6393
|
+
const manifest = readJson(join2(directory, "package.json"), host);
|
|
6394
|
+
const declared = manifest?.luaut?.types;
|
|
6395
|
+
const file = resolve2(directory, typeof declared === "string" ? declared : ENTRY_FILE);
|
|
6396
|
+
return host.readFile(file) !== void 0 ? file : void 0;
|
|
6397
|
+
}
|
|
6398
|
+
function findPackage(name, from, host) {
|
|
6399
|
+
let directory = resolve2(from);
|
|
6400
|
+
for (; ; ) {
|
|
6401
|
+
const candidate = join2(directory, "node_modules", ...name.split("/"));
|
|
6402
|
+
const file = packageEntry(candidate, host);
|
|
6403
|
+
if (file) return { directory: candidate, file };
|
|
6404
|
+
const parent = dirname2(directory);
|
|
6405
|
+
if (parent === directory) return void 0;
|
|
6406
|
+
directory = parent;
|
|
6407
|
+
}
|
|
6408
|
+
}
|
|
6409
|
+
function dependencyNames(directory, host) {
|
|
6410
|
+
const manifest = readJson(join2(directory, "package.json"), host);
|
|
6411
|
+
const names = /* @__PURE__ */ new Set();
|
|
6412
|
+
for (const field of ["dependencies", "peerDependencies"]) {
|
|
6413
|
+
const deps = manifest?.[field];
|
|
6414
|
+
if (deps && typeof deps === "object") for (const name of Object.keys(deps)) names.add(name);
|
|
6415
|
+
}
|
|
6416
|
+
return [...names];
|
|
6417
|
+
}
|
|
6418
|
+
function readJson(path, host) {
|
|
6419
|
+
const text = host.readFile(path);
|
|
6420
|
+
if (text === void 0) return void 0;
|
|
6421
|
+
try {
|
|
6422
|
+
const value = JSON.parse(text);
|
|
6423
|
+
return value && typeof value === "object" ? value : void 0;
|
|
6424
|
+
} catch {
|
|
6425
|
+
return void 0;
|
|
6426
|
+
}
|
|
6427
|
+
}
|
|
6428
|
+
function entryPosition(config, entry) {
|
|
6429
|
+
return keyPosition(config.source, entry);
|
|
6430
|
+
}
|
|
6431
|
+
function pathKey(path) {
|
|
6432
|
+
const normalized = resolve2(path);
|
|
6433
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
6434
|
+
}
|
|
6435
|
+
|
|
6436
|
+
// src/project/modules.ts
|
|
6437
|
+
import { dirname as dirname3, join as join3, resolve as resolve3 } from "path";
|
|
6438
|
+
function moduleCandidates(fromFile, specifier, config) {
|
|
6439
|
+
const bases = specifier.startsWith("./") || specifier.startsWith("../") ? [resolve3(dirname3(fromFile), specifier)] : config ? aliasTargets(config, specifier) : [];
|
|
6440
|
+
return bases.flatMap((base) => base.endsWith(".luaut") ? [base] : [`${base}.luaut`, `${base}.d.luaut`, join3(base, "index.luaut")]);
|
|
6441
|
+
}
|
|
6442
|
+
function resolveModulePath(fromFile, specifier, config, host = nodeHost) {
|
|
6443
|
+
return moduleCandidates(fromFile, specifier, config).find((path) => host.readFile(path) !== void 0);
|
|
6444
|
+
}
|
|
6445
|
+
function aliasTargets(config, specifier) {
|
|
6446
|
+
let match;
|
|
6447
|
+
let prefixLength = -1;
|
|
6448
|
+
for (const pattern2 of Object.keys(config.paths)) {
|
|
6449
|
+
const star = pattern2.indexOf("*");
|
|
6450
|
+
if (star < 0) {
|
|
6451
|
+
if (pattern2 === specifier) {
|
|
6452
|
+
match = { pattern: pattern2, wildcard: "" };
|
|
6453
|
+
break;
|
|
6454
|
+
}
|
|
6455
|
+
continue;
|
|
6456
|
+
}
|
|
6457
|
+
const prefix = pattern2.slice(0, star);
|
|
6458
|
+
const suffix = pattern2.slice(star + 1);
|
|
6459
|
+
const fits = specifier.length >= prefix.length + suffix.length && specifier.startsWith(prefix) && specifier.endsWith(suffix);
|
|
6460
|
+
if (fits && prefix.length > prefixLength) {
|
|
6461
|
+
prefixLength = prefix.length;
|
|
6462
|
+
match = { pattern: pattern2, wildcard: specifier.slice(prefix.length, specifier.length - suffix.length) };
|
|
6463
|
+
}
|
|
6464
|
+
}
|
|
6465
|
+
if (!match) return [];
|
|
6466
|
+
const { pattern, wildcard } = match;
|
|
6467
|
+
return config.paths[pattern].map((target) => resolve3(config.baseUrl, target.replace("*", wildcard)));
|
|
6468
|
+
}
|
|
5832
6469
|
|
|
5833
|
-
// src/
|
|
5834
|
-
import {
|
|
5835
|
-
|
|
5836
|
-
var
|
|
5837
|
-
var
|
|
5838
|
-
|
|
6470
|
+
// src/project/sourcemap.ts
|
|
6471
|
+
import { dirname as dirname4, extname, resolve as resolve4 } from "path";
|
|
6472
|
+
var IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
6473
|
+
var INSTANCE_MEMBERS = /* @__PURE__ */ new Set(["Name", "ClassName", "Parent", "Archivable"]);
|
|
6474
|
+
var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".luaut", ".luau", ".lua"]);
|
|
6475
|
+
function sourceMapTypes(text, path, options) {
|
|
6476
|
+
let root;
|
|
6477
|
+
try {
|
|
6478
|
+
root = JSON.parse(text);
|
|
6479
|
+
} catch (error) {
|
|
6480
|
+
return { problem: `Invalid sourcemap: ${error.message}` };
|
|
6481
|
+
}
|
|
6482
|
+
if (!isNode(root)) return { problem: "Invalid sourcemap: the root must be an object with 'name' and 'className'" };
|
|
6483
|
+
const directory = dirname4(resolve4(path));
|
|
6484
|
+
const lines = [];
|
|
6485
|
+
const aliasOfFile = /* @__PURE__ */ new Map();
|
|
6486
|
+
const used = /* @__PURE__ */ new Set();
|
|
6487
|
+
const aliasOfNode = /* @__PURE__ */ new Map();
|
|
6488
|
+
const aliasFor = (segments) => {
|
|
6489
|
+
const base = `SourceMap_${segments.map((s) => s.replace(/[^A-Za-z0-9_]/g, "_")).join("_")}`;
|
|
6490
|
+
let alias = base;
|
|
6491
|
+
for (let n = 2; used.has(alias); n++) alias = `${base}_${n}`;
|
|
6492
|
+
used.add(alias);
|
|
6493
|
+
return alias;
|
|
6494
|
+
};
|
|
6495
|
+
const visit = (node, segments, parent) => {
|
|
6496
|
+
const alias = aliasFor(segments);
|
|
6497
|
+
aliasOfNode.set(node, alias);
|
|
6498
|
+
for (const filePath of node.filePaths ?? []) aliasOfFile.set(fileKey(resolve4(directory, filePath)), alias);
|
|
6499
|
+
const className = IDENTIFIER.test(node.className) && options.classes.has(node.className) ? node.className : "Instance";
|
|
6500
|
+
const taken = options.membersOf?.(className) ?? INSTANCE_MEMBERS;
|
|
6501
|
+
const members = [];
|
|
6502
|
+
if (parent) members.push(`Parent: ${parent}`);
|
|
6503
|
+
const named = /* @__PURE__ */ new Set();
|
|
6504
|
+
for (const child of node.children ?? []) {
|
|
6505
|
+
if (!isNode(child)) continue;
|
|
6506
|
+
const childAlias = visit(child, [...segments, child.name], alias);
|
|
6507
|
+
if (!IDENTIFIER.test(child.name) || taken.has(child.name) || named.has(child.name)) continue;
|
|
6508
|
+
named.add(child.name);
|
|
6509
|
+
members.push(`${child.name}: ${childAlias}`);
|
|
6510
|
+
}
|
|
6511
|
+
lines.push(`declare class ${alias} extends ${className} { ${members.join(", ")} }`);
|
|
6512
|
+
return alias;
|
|
6513
|
+
};
|
|
6514
|
+
const rootAlias = visit(root, [root.name], void 0);
|
|
6515
|
+
if (root.className === "DataModel") {
|
|
6516
|
+
lines.push(`declare game: ${rootAlias}`);
|
|
6517
|
+
const workspace = (root.children ?? []).find((child) => isNode(child) && child.className === "Workspace");
|
|
6518
|
+
const workspaceAlias = workspace && aliasOfNode.get(workspace);
|
|
6519
|
+
if (workspaceAlias) lines.push(`declare workspace: ${workspaceAlias}`);
|
|
6520
|
+
}
|
|
6521
|
+
let program;
|
|
6522
|
+
try {
|
|
6523
|
+
program = parse(lines.join("\n"));
|
|
6524
|
+
} catch (error) {
|
|
6525
|
+
return { problem: `Could not turn the sourcemap into types: ${error.message}` };
|
|
6526
|
+
}
|
|
6527
|
+
return {
|
|
6528
|
+
types: {
|
|
6529
|
+
program,
|
|
6530
|
+
scriptFor(file) {
|
|
6531
|
+
const alias = aliasOfFile.get(fileKey(file));
|
|
6532
|
+
return alias ? parse(`declare script: ${alias}`) : void 0;
|
|
6533
|
+
}
|
|
6534
|
+
}
|
|
6535
|
+
};
|
|
6536
|
+
}
|
|
6537
|
+
function isNode(value) {
|
|
6538
|
+
if (!value || typeof value !== "object") return false;
|
|
6539
|
+
const node = value;
|
|
6540
|
+
return typeof node.name === "string" && typeof node.className === "string";
|
|
6541
|
+
}
|
|
6542
|
+
function fileKey(path) {
|
|
6543
|
+
const extension = extname(path);
|
|
6544
|
+
const bare = SCRIPT_EXTENSIONS.has(extension) ? path.slice(0, -extension.length) : path;
|
|
6545
|
+
const normalized = resolve4(bare);
|
|
6546
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
6547
|
+
}
|
|
5839
6548
|
|
|
5840
6549
|
// src/index.ts
|
|
5841
|
-
var defaultLibs = [luauLib, robloxLib];
|
|
5842
6550
|
var luautparser = {
|
|
5843
6551
|
tokenize,
|
|
5844
6552
|
parseTokens,
|
|
@@ -5854,6 +6562,7 @@ var luautparser = {
|
|
|
5854
6562
|
var index_default = luautparser;
|
|
5855
6563
|
export {
|
|
5856
6564
|
BinaryOperators,
|
|
6565
|
+
CONFIG_FILE_NAMES,
|
|
5857
6566
|
Keywords,
|
|
5858
6567
|
LexError,
|
|
5859
6568
|
Operators,
|
|
@@ -5868,25 +6577,25 @@ export {
|
|
|
5868
6577
|
bufferType,
|
|
5869
6578
|
containsTypeParam,
|
|
5870
6579
|
index_default as default,
|
|
5871
|
-
defaultLibs,
|
|
5872
6580
|
difference,
|
|
5873
6581
|
equalTypes,
|
|
5874
6582
|
falsyType,
|
|
6583
|
+
findConfig,
|
|
5875
6584
|
fn,
|
|
5876
6585
|
formatType,
|
|
5877
6586
|
getBinding,
|
|
5878
6587
|
intersection,
|
|
5879
6588
|
isAssignable,
|
|
6589
|
+
isClassType,
|
|
5880
6590
|
isGlobal,
|
|
5881
6591
|
isPossiblyFalsy,
|
|
5882
6592
|
isPossiblyTruthy,
|
|
5883
6593
|
isUnassignedGlobal,
|
|
5884
6594
|
literal,
|
|
5885
|
-
|
|
5886
|
-
luauDefsPath,
|
|
5887
|
-
luauLib,
|
|
6595
|
+
loadConfig,
|
|
5888
6596
|
luautparser,
|
|
5889
6597
|
matchInfer,
|
|
6598
|
+
moduleCandidates,
|
|
5890
6599
|
moduleExports,
|
|
5891
6600
|
narrowExclude,
|
|
5892
6601
|
narrowFalsy,
|
|
@@ -5894,6 +6603,7 @@ export {
|
|
|
5894
6603
|
narrowTruthy,
|
|
5895
6604
|
neverType,
|
|
5896
6605
|
nilType,
|
|
6606
|
+
nodeHost,
|
|
5897
6607
|
numberType,
|
|
5898
6608
|
objectType,
|
|
5899
6609
|
optional,
|
|
@@ -5903,11 +6613,12 @@ export {
|
|
|
5903
6613
|
parseTokens,
|
|
5904
6614
|
parseWithRecovery,
|
|
5905
6615
|
primitive,
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
robloxLib,
|
|
6616
|
+
resolveModulePath,
|
|
6617
|
+
resolveTypeLibraries,
|
|
5909
6618
|
setAliasExpander,
|
|
6619
|
+
sourceMapTypes,
|
|
5910
6620
|
stringType,
|
|
6621
|
+
stripJsonComments,
|
|
5911
6622
|
substitute,
|
|
5912
6623
|
templateMatches,
|
|
5913
6624
|
threadType,
|