luaut-parser 1.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -30
- package/dist/index.cjs +501 -41
- package/dist/index.d.cts +98 -23
- package/dist/index.d.ts +98 -23
- package/dist/index.js +493 -31
- package/package.json +4 -5
- package/dist/luau.d.luaut +0 -245
- package/dist/roblox.d.luaut +0 -503
package/dist/index.js
CHANGED
|
@@ -3176,7 +3176,11 @@ function isAssignableInner(a, b) {
|
|
|
3176
3176
|
if (a.kind === "union") return a.types.every((t) => isAssignable(t, b));
|
|
3177
3177
|
if (b.kind === "union") return b.types.some((t) => isAssignable(a, t));
|
|
3178
3178
|
if (b.kind === "intersection") return b.types.every((t) => isAssignable(a, t));
|
|
3179
|
-
if (a.kind === "intersection")
|
|
3179
|
+
if (a.kind === "intersection") {
|
|
3180
|
+
if (a.types.some((t) => isAssignable(t, b))) return true;
|
|
3181
|
+
const merged = mergeObjectMembers(a.types);
|
|
3182
|
+
return merged !== void 0 && isAssignable(merged, b);
|
|
3183
|
+
}
|
|
3180
3184
|
if (a.kind === "literal") {
|
|
3181
3185
|
if (b.kind === "literal") return a.value === b.value;
|
|
3182
3186
|
if (b.kind === "primitive") return b.name === a.base;
|
|
@@ -3531,6 +3535,39 @@ var IDENT_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
|
3531
3535
|
function formatKey(k) {
|
|
3532
3536
|
return IDENT_KEY.test(k) ? k : JSON.stringify(k);
|
|
3533
3537
|
}
|
|
3538
|
+
function mergeObjectMembers(types) {
|
|
3539
|
+
const objects = [];
|
|
3540
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3541
|
+
const collect = (t) => {
|
|
3542
|
+
if (seen.has(t)) return true;
|
|
3543
|
+
seen.add(t);
|
|
3544
|
+
if (t.kind === "genericRef") {
|
|
3545
|
+
const expanded = expandAlias?.(t);
|
|
3546
|
+
return expanded !== void 0 && expanded !== t && collect(expanded);
|
|
3547
|
+
}
|
|
3548
|
+
if (t.kind === "intersection") return t.types.every(collect);
|
|
3549
|
+
if (t.kind === "object") {
|
|
3550
|
+
objects.push(t);
|
|
3551
|
+
return true;
|
|
3552
|
+
}
|
|
3553
|
+
return false;
|
|
3554
|
+
};
|
|
3555
|
+
if (!types.every(collect) || objects.length < 2) return void 0;
|
|
3556
|
+
const properties = /* @__PURE__ */ new Map();
|
|
3557
|
+
let indexer;
|
|
3558
|
+
for (const object of objects) {
|
|
3559
|
+
indexer ??= object.indexer;
|
|
3560
|
+
for (const [name, property] of object.properties) {
|
|
3561
|
+
const existing = properties.get(name);
|
|
3562
|
+
properties.set(name, existing ? {
|
|
3563
|
+
type: intersection([existing.type, property.type]),
|
|
3564
|
+
optional: existing.optional && property.optional,
|
|
3565
|
+
readonly: existing.readonly || property.readonly
|
|
3566
|
+
} : property);
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
return objectType([...properties], indexer);
|
|
3570
|
+
}
|
|
3534
3571
|
|
|
3535
3572
|
// src/ast/analyzeTypes.ts
|
|
3536
3573
|
function analyzeTypes(program, scopes, options = {}) {
|
|
@@ -3738,6 +3775,7 @@ var TypeAnalyzer = class {
|
|
|
3738
3775
|
bindingType = /* @__PURE__ */ new Map();
|
|
3739
3776
|
narrowedTypeOf = /* @__PURE__ */ new Map();
|
|
3740
3777
|
typeOfTypeNode = /* @__PURE__ */ new Map();
|
|
3778
|
+
expectedTypeOf = /* @__PURE__ */ new Map();
|
|
3741
3779
|
/** Public: each alias resolved once (generic aliases keep their params as
|
|
3742
3780
|
* `typeParam` nodes in the body). */
|
|
3743
3781
|
aliases = /* @__PURE__ */ new Map();
|
|
@@ -3803,6 +3841,7 @@ var TypeAnalyzer = class {
|
|
|
3803
3841
|
bindingType: this.bindingType,
|
|
3804
3842
|
narrowedTypeOf: this.narrowedTypeOf,
|
|
3805
3843
|
typeOfTypeNode: this.typeOfTypeNode,
|
|
3844
|
+
expectedTypeOf: this.expectedTypeOf,
|
|
3806
3845
|
aliases: this.resolveDeferredAliases(),
|
|
3807
3846
|
diagnostics: this.diagnostics
|
|
3808
3847
|
};
|
|
@@ -4784,16 +4823,76 @@ var TypeAnalyzer = class {
|
|
|
4784
4823
|
return void 0;
|
|
4785
4824
|
}
|
|
4786
4825
|
/** Can this signature be called with these argument types? The signature's
|
|
4787
|
-
* own
|
|
4788
|
-
*
|
|
4826
|
+
* own type parameters stand for what the call would infer, so each is
|
|
4827
|
+
* checked only against its constraint — `<K extends keyof Services>`
|
|
4828
|
+
* accepts `"Players"` but not `""`. */
|
|
4789
4829
|
overloadAccepts(f, argTypes) {
|
|
4790
4830
|
if (!f.varargs && argTypes.length > f.params.length) return false;
|
|
4791
|
-
const
|
|
4831
|
+
const params = this.boundParams(f);
|
|
4792
4832
|
return f.params.every((p, i) => {
|
|
4793
4833
|
if (argTypes[i] === void 0) return p.optional === true;
|
|
4794
|
-
return isAssignable(argTypes[i],
|
|
4834
|
+
return isAssignable(argTypes[i], params[i]);
|
|
4835
|
+
});
|
|
4836
|
+
}
|
|
4837
|
+
/** A signature's parameter types as a call site sees them before inference:
|
|
4838
|
+
* each type parameter replaced by its constraint, or by `any` when it has
|
|
4839
|
+
* none — or when the constraint mentions another type parameter, which a
|
|
4840
|
+
* lone argument cannot be checked against without false errors. */
|
|
4841
|
+
boundParams(f) {
|
|
4842
|
+
if (!f.typeParams?.length) return f.params.map((p) => p.type);
|
|
4843
|
+
const bounds = new Map(f.typeParams.map((name) => [name, anyType]));
|
|
4844
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
4845
|
+
const walk = (value) => {
|
|
4846
|
+
if (!value || typeof value !== "object" || seen.has(value)) return;
|
|
4847
|
+
seen.add(value);
|
|
4848
|
+
if (value instanceof Map) {
|
|
4849
|
+
value.forEach(walk);
|
|
4850
|
+
return;
|
|
4851
|
+
}
|
|
4852
|
+
const t = value;
|
|
4853
|
+
if (t.kind === "typeParam" && typeof t.name === "string" && bounds.has(t.name) && t.constraint && !containsTypeParam(t.constraint)) {
|
|
4854
|
+
bounds.set(t.name, this.reduceType(t.constraint));
|
|
4855
|
+
}
|
|
4856
|
+
for (const child of Object.values(value)) walk(child);
|
|
4857
|
+
};
|
|
4858
|
+
for (const p of f.params) walk(p.type);
|
|
4859
|
+
return f.params.map((p) => substitute(p.type, bounds));
|
|
4860
|
+
}
|
|
4861
|
+
/** Record what each written argument is expected to be — see
|
|
4862
|
+
* `TypeAnalysis.expectedTypeOf`. */
|
|
4863
|
+
recordExpected(written, fns, selfOf) {
|
|
4864
|
+
written.forEach((arg, j) => {
|
|
4865
|
+
const candidates = [];
|
|
4866
|
+
for (const f of fns) {
|
|
4867
|
+
const i = j + selfOf(f);
|
|
4868
|
+
const param = i < f.params.length ? this.boundParams(f)[i] : f.varargs;
|
|
4869
|
+
if (param) candidates.push(param);
|
|
4870
|
+
}
|
|
4871
|
+
if (candidates.length) this.expectedTypeOf.set(arg, union(candidates));
|
|
4795
4872
|
});
|
|
4796
4873
|
}
|
|
4874
|
+
/** No signature accepts the call, and the argument count is not the
|
|
4875
|
+
* problem: say which argument is wrong, the way TypeScript does. */
|
|
4876
|
+
reportArguments(call, written, fns, argsFor, selfOf) {
|
|
4877
|
+
if (!this.emitDiagnostics) return;
|
|
4878
|
+
if (fns.length > 1) {
|
|
4879
|
+
this.diagnostics.push({ node: call, message: "No overload matches this call" });
|
|
4880
|
+
return;
|
|
4881
|
+
}
|
|
4882
|
+
const f = fns[0];
|
|
4883
|
+
const args = argsFor(f);
|
|
4884
|
+
const params = this.boundParams(f);
|
|
4885
|
+
const self = selfOf(f);
|
|
4886
|
+
for (let i = 0; i < f.params.length; i++) {
|
|
4887
|
+
const arg = args[i];
|
|
4888
|
+
if (arg === void 0 || isAssignable(arg, params[i])) continue;
|
|
4889
|
+
this.diagnostics.push({
|
|
4890
|
+
node: written[i - self] ?? call,
|
|
4891
|
+
message: `Argument of type '${formatType(arg)}' is not assignable to parameter of type '${briefType(params[i])}'`
|
|
4892
|
+
});
|
|
4893
|
+
return;
|
|
4894
|
+
}
|
|
4895
|
+
}
|
|
4797
4896
|
/** A required parameter may not follow an optional one — otherwise the
|
|
4798
4897
|
* optional one could never actually be omitted. Same rule as TypeScript,
|
|
4799
4898
|
* and it applies to a default (`a = 1`) as much as to a `?`. */
|
|
@@ -4823,22 +4922,25 @@ var TypeAnalyzer = class {
|
|
|
4823
4922
|
return { min, max: f.varargs ? void 0 : f.params.length };
|
|
4824
4923
|
}
|
|
4825
4924
|
/** Report a call that passes too few or too many arguments. Only fires
|
|
4826
|
-
* when *no* overload accepts the
|
|
4827
|
-
* once, against its first signature.
|
|
4925
|
+
* when *no* overload accepts the count, so an overload set still reports
|
|
4926
|
+
* once, against its first signature. Returns whether the count fits, so
|
|
4927
|
+
* an argument's type is only complained about when its count is right. */
|
|
4828
4928
|
checkArity(node, fns, argCount, selfArgs) {
|
|
4829
|
-
if (!
|
|
4929
|
+
if (!fns.length) return true;
|
|
4830
4930
|
const fits = fns.some((f) => {
|
|
4831
4931
|
const { min: min2, max: max2 } = this.arityOf(f);
|
|
4832
4932
|
const n = argCount + selfArgs;
|
|
4833
4933
|
return n >= min2 && (max2 === void 0 || n <= max2);
|
|
4834
4934
|
});
|
|
4835
|
-
if (fits) return;
|
|
4935
|
+
if (fits) return true;
|
|
4936
|
+
if (!this.emitDiagnostics) return false;
|
|
4836
4937
|
const { min, max } = this.arityOf(fns[0]);
|
|
4837
4938
|
const need = max === void 0 ? `at least ${min - selfArgs}` : min === max ? `${min - selfArgs}` : `${min - selfArgs}-${max - selfArgs}`;
|
|
4838
4939
|
this.diagnostics.push({
|
|
4839
4940
|
node,
|
|
4840
4941
|
message: `Expected ${need} argument${need === "1" ? "" : "s"}, got ${argCount}`
|
|
4841
4942
|
});
|
|
4943
|
+
return false;
|
|
4842
4944
|
}
|
|
4843
4945
|
signatureToFnType(sig) {
|
|
4844
4946
|
const names = sig.generics.map((g) => g.name);
|
|
@@ -5276,11 +5378,13 @@ var TypeAnalyzer = class {
|
|
|
5276
5378
|
const argTypes = expr.arguments.map((a) => this.infer(a, env));
|
|
5277
5379
|
const fns = this.overloadsOf(callee);
|
|
5278
5380
|
if (fns.length) {
|
|
5279
|
-
this.
|
|
5381
|
+
this.recordExpected(expr.arguments, fns, () => 0);
|
|
5382
|
+
const arityFits = this.checkArity(expr, fns, argTypes.length, 0);
|
|
5280
5383
|
const picked = this.pickOverload(fns, argTypes);
|
|
5281
5384
|
if (picked) {
|
|
5282
5385
|
return this.callReturn(picked, this.constArgs(picked, expr.arguments, argTypes, env));
|
|
5283
5386
|
}
|
|
5387
|
+
if (arityFits) this.reportArguments(expr, expr.arguments, fns, () => argTypes, () => 0);
|
|
5284
5388
|
return union(fns.map((f) => this.callReturn(f, argTypes)));
|
|
5285
5389
|
}
|
|
5286
5390
|
return callee.kind === "any" ? anyType : unknownType;
|
|
@@ -5291,13 +5395,16 @@ var TypeAnalyzer = class {
|
|
|
5291
5395
|
const fns = this.overloadsOf(this.propertyType(objType, expr.method.name));
|
|
5292
5396
|
if (fns.length) {
|
|
5293
5397
|
const withSelf = (f) => this.takesSelf(f) ? [objType, ...argTypes] : argTypes;
|
|
5294
|
-
|
|
5398
|
+
const selfOf = (f) => this.takesSelf(f) ? 1 : 0;
|
|
5399
|
+
this.recordExpected(expr.arguments, fns, selfOf);
|
|
5400
|
+
const arityFits = this.checkArity(expr, fns, argTypes.length, this.takesSelf(fns[0]) ? 1 : 0);
|
|
5295
5401
|
const picked = this.pickOverload(fns, argTypes, withSelf);
|
|
5296
5402
|
if (picked) {
|
|
5297
5403
|
const self = this.takesSelf(picked) ? 1 : 0;
|
|
5298
5404
|
const written = this.constArgs(picked, expr.arguments, argTypes, env, self);
|
|
5299
5405
|
return this.callReturn(picked, this.takesSelf(picked) ? [objType, ...written] : written);
|
|
5300
5406
|
}
|
|
5407
|
+
if (arityFits) this.reportArguments(expr, expr.arguments, fns, withSelf, selfOf);
|
|
5301
5408
|
return union(fns.map((f) => this.callReturn(f, withSelf(f))));
|
|
5302
5409
|
}
|
|
5303
5410
|
return objType.kind === "any" ? anyType : unknownType;
|
|
@@ -5745,23 +5852,376 @@ function containsTypeQuery(node) {
|
|
|
5745
5852
|
if (node.type === "TypeofTypeNode") return true;
|
|
5746
5853
|
return Object.values(node).some(containsTypeQuery);
|
|
5747
5854
|
}
|
|
5855
|
+
function briefType(t) {
|
|
5856
|
+
if (t.kind === "union" && t.types.length > 8) {
|
|
5857
|
+
const shown = t.types.slice(0, 6).map(formatType).join(" | ");
|
|
5858
|
+
return `${shown} | ... ${t.types.length - 6} more`;
|
|
5859
|
+
}
|
|
5860
|
+
return formatType(t);
|
|
5861
|
+
}
|
|
5862
|
+
|
|
5863
|
+
// src/project/host.ts
|
|
5864
|
+
import { readFileSync, statSync } from "fs";
|
|
5865
|
+
var nodeHost = {
|
|
5866
|
+
readFile(path) {
|
|
5867
|
+
try {
|
|
5868
|
+
return statSync(path).isFile() ? readFileSync(path, "utf8") : void 0;
|
|
5869
|
+
} catch {
|
|
5870
|
+
return void 0;
|
|
5871
|
+
}
|
|
5872
|
+
}
|
|
5873
|
+
};
|
|
5874
|
+
|
|
5875
|
+
// src/project/config.ts
|
|
5876
|
+
import { dirname, join, resolve } from "path";
|
|
5877
|
+
var CONFIG_FILE_NAMES = ["luaut.config.json", "luaut.config.jsonc"];
|
|
5878
|
+
function findConfig(file, host = nodeHost) {
|
|
5879
|
+
const searched = [];
|
|
5880
|
+
let directory = dirname(resolve(file));
|
|
5881
|
+
for (; ; ) {
|
|
5882
|
+
const found = [];
|
|
5883
|
+
for (const name of CONFIG_FILE_NAMES) {
|
|
5884
|
+
const path = join(directory, name);
|
|
5885
|
+
searched.push(path);
|
|
5886
|
+
if (host.readFile(path) !== void 0) found.push(path);
|
|
5887
|
+
}
|
|
5888
|
+
if (found.length > 1) {
|
|
5889
|
+
const message = `Only one luaut config may be in a folder, but both ${CONFIG_FILE_NAMES.join(" and ")} are in ${directory}`;
|
|
5890
|
+
return { searched, problems: found.map((path) => ({ file: path, message, line: 1, column: 1 })) };
|
|
5891
|
+
}
|
|
5892
|
+
if (found.length === 1) {
|
|
5893
|
+
const { config, problems } = loadConfig(found[0], host);
|
|
5894
|
+
return { config, problems, searched };
|
|
5895
|
+
}
|
|
5896
|
+
const parent = dirname(directory);
|
|
5897
|
+
if (parent === directory) return { searched, problems: [] };
|
|
5898
|
+
directory = parent;
|
|
5899
|
+
}
|
|
5900
|
+
}
|
|
5901
|
+
var OPTIONS = ["types", "paths", "baseUrl", "sourceMap"];
|
|
5902
|
+
function loadConfig(path, host = nodeHost) {
|
|
5903
|
+
const file = resolve(path);
|
|
5904
|
+
const source = host.readFile(file);
|
|
5905
|
+
if (source === void 0) return { problems: [{ file, message: "Cannot read the config file" }] };
|
|
5906
|
+
let raw;
|
|
5907
|
+
try {
|
|
5908
|
+
raw = JSON.parse(stripJsonComments(source));
|
|
5909
|
+
} catch (error) {
|
|
5910
|
+
const message = error.message;
|
|
5911
|
+
return { problems: [{ file, message: `Invalid JSON: ${message}`, ...jsonErrorPosition(source, message) }] };
|
|
5912
|
+
}
|
|
5913
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5914
|
+
return { problems: [{ file, message: "The config must be a JSON object", line: 1, column: 1 }] };
|
|
5915
|
+
}
|
|
5916
|
+
const directory = dirname(file);
|
|
5917
|
+
const options = raw;
|
|
5918
|
+
const problems = [];
|
|
5919
|
+
const at = (key) => keyPosition(source, key);
|
|
5920
|
+
const problem = (key, message) => {
|
|
5921
|
+
problems.push({ file, message, ...at(key) });
|
|
5922
|
+
};
|
|
5923
|
+
for (const key of Object.keys(options)) {
|
|
5924
|
+
if (!OPTIONS.includes(key)) {
|
|
5925
|
+
problem(key, `Unknown option '${key}'. Options are: ${OPTIONS.join(", ")}`);
|
|
5926
|
+
}
|
|
5927
|
+
}
|
|
5928
|
+
let types = [];
|
|
5929
|
+
if (options.types !== void 0) {
|
|
5930
|
+
if (Array.isArray(options.types) && options.types.every((t) => typeof t === "string")) types = options.types;
|
|
5931
|
+
else problem("types", `'types' must be an array of strings, such as ["luau"]`);
|
|
5932
|
+
}
|
|
5933
|
+
const paths = {};
|
|
5934
|
+
if (options.paths !== void 0) {
|
|
5935
|
+
const value = options.paths;
|
|
5936
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
5937
|
+
for (const [pattern, targets] of Object.entries(value)) {
|
|
5938
|
+
if (Array.isArray(targets) && targets.every((t) => typeof t === "string")) paths[pattern] = targets;
|
|
5939
|
+
else problem(pattern, `'paths' entry '${pattern}' must be an array of strings`);
|
|
5940
|
+
if (pattern.split("*").length > 2) problem(pattern, `'paths' pattern '${pattern}' may contain at most one '*'`);
|
|
5941
|
+
}
|
|
5942
|
+
} else {
|
|
5943
|
+
problem("paths", `'paths' must be an object, such as { "@shared/*": ["src/shared/*"] }`);
|
|
5944
|
+
}
|
|
5945
|
+
}
|
|
5946
|
+
let baseUrl = directory;
|
|
5947
|
+
if (options.baseUrl !== void 0) {
|
|
5948
|
+
if (typeof options.baseUrl === "string") baseUrl = resolve(directory, options.baseUrl);
|
|
5949
|
+
else problem("baseUrl", "'baseUrl' must be a string");
|
|
5950
|
+
}
|
|
5951
|
+
let sourceMap = null;
|
|
5952
|
+
if (options.sourceMap !== void 0 && options.sourceMap !== null) {
|
|
5953
|
+
if (typeof options.sourceMap === "string") sourceMap = resolve(directory, options.sourceMap);
|
|
5954
|
+
else problem("sourceMap", "'sourceMap' must be a path string, or null for none");
|
|
5955
|
+
}
|
|
5956
|
+
return { config: { path: file, directory, source, types, paths, baseUrl, sourceMap }, problems };
|
|
5957
|
+
}
|
|
5958
|
+
function stripJsonComments(text) {
|
|
5959
|
+
const out = text.split("");
|
|
5960
|
+
let i = 0;
|
|
5961
|
+
let inString = false;
|
|
5962
|
+
while (i < text.length) {
|
|
5963
|
+
const ch = text[i];
|
|
5964
|
+
if (inString) {
|
|
5965
|
+
if (ch === "\\") i += 2;
|
|
5966
|
+
else {
|
|
5967
|
+
if (ch === '"') inString = false;
|
|
5968
|
+
i++;
|
|
5969
|
+
}
|
|
5970
|
+
continue;
|
|
5971
|
+
}
|
|
5972
|
+
if (ch === '"') {
|
|
5973
|
+
inString = true;
|
|
5974
|
+
i++;
|
|
5975
|
+
} else if (ch === "/" && text[i + 1] === "/") {
|
|
5976
|
+
while (i < text.length && text[i] !== "\n") out[i++] = " ";
|
|
5977
|
+
} else if (ch === "/" && text[i + 1] === "*") {
|
|
5978
|
+
out[i++] = " ";
|
|
5979
|
+
out[i++] = " ";
|
|
5980
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) {
|
|
5981
|
+
if (text[i] !== "\n") out[i] = " ";
|
|
5982
|
+
i++;
|
|
5983
|
+
}
|
|
5984
|
+
if (i < text.length) {
|
|
5985
|
+
out[i++] = " ";
|
|
5986
|
+
out[i++] = " ";
|
|
5987
|
+
}
|
|
5988
|
+
} else if (ch === ",") {
|
|
5989
|
+
let j = i + 1;
|
|
5990
|
+
while (j < text.length && /\s/.test(text[j])) j++;
|
|
5991
|
+
if (text[j] === "}" || text[j] === "]") out[i] = " ";
|
|
5992
|
+
i++;
|
|
5993
|
+
} else {
|
|
5994
|
+
i++;
|
|
5995
|
+
}
|
|
5996
|
+
}
|
|
5997
|
+
return out.join("");
|
|
5998
|
+
}
|
|
5999
|
+
function jsonErrorPosition(source, message) {
|
|
6000
|
+
const lineColumn = /line (\d+) column (\d+)/.exec(message);
|
|
6001
|
+
if (lineColumn) return { line: Number(lineColumn[1]), column: Number(lineColumn[2]) };
|
|
6002
|
+
const position = /position (\d+)/.exec(message);
|
|
6003
|
+
return position ? offsetPosition(source, Number(position[1])) : { line: 1, column: 1 };
|
|
6004
|
+
}
|
|
6005
|
+
function keyPosition(source, key) {
|
|
6006
|
+
const offset = source.indexOf(JSON.stringify(key));
|
|
6007
|
+
return offset < 0 ? { line: 1, column: 1 } : offsetPosition(source, offset);
|
|
6008
|
+
}
|
|
6009
|
+
function offsetPosition(source, offset) {
|
|
6010
|
+
const before = source.slice(0, offset);
|
|
6011
|
+
const line = before.split("\n").length;
|
|
6012
|
+
return { line, column: offset - before.lastIndexOf("\n") };
|
|
6013
|
+
}
|
|
6014
|
+
|
|
6015
|
+
// src/project/libraries.ts
|
|
6016
|
+
import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
6017
|
+
function resolveTypeLibraries(config, host = nodeHost) {
|
|
6018
|
+
const files = [];
|
|
6019
|
+
const problems = [];
|
|
6020
|
+
const loaded = /* @__PURE__ */ new Set();
|
|
6021
|
+
const addFile = (file) => {
|
|
6022
|
+
const key = pathKey(file);
|
|
6023
|
+
if (loaded.has(key)) return;
|
|
6024
|
+
loaded.add(key);
|
|
6025
|
+
files.push(file);
|
|
6026
|
+
};
|
|
6027
|
+
const addPackage = (directory, entryFile, visiting) => {
|
|
6028
|
+
const key = pathKey(directory);
|
|
6029
|
+
if (visiting.has(key)) return;
|
|
6030
|
+
visiting.add(key);
|
|
6031
|
+
for (const dependency of dependencyNames(directory, host)) {
|
|
6032
|
+
const found = findPackage(dependency, directory, host);
|
|
6033
|
+
if (found) addPackage(found.directory, found.file, visiting);
|
|
6034
|
+
}
|
|
6035
|
+
addFile(entryFile);
|
|
6036
|
+
};
|
|
6037
|
+
for (const entry of config.types) {
|
|
6038
|
+
const relative = entry.startsWith("./") || entry.startsWith("../") || entry.startsWith("/") || /^[A-Za-z]:[\\/]/.test(entry);
|
|
6039
|
+
if (relative) {
|
|
6040
|
+
const target = resolve2(config.directory, entry);
|
|
6041
|
+
if (entry.endsWith(".luaut")) {
|
|
6042
|
+
if (host.readFile(target) !== void 0) addFile(target);
|
|
6043
|
+
else problems.push({ file: config.path, message: `Cannot find type library file '${entry}'`, ...entryPosition(config, entry) });
|
|
6044
|
+
continue;
|
|
6045
|
+
}
|
|
6046
|
+
const file = packageEntry(target, host);
|
|
6047
|
+
if (file) addPackage(target, file, /* @__PURE__ */ new Set());
|
|
6048
|
+
else problems.push({ file: config.path, message: `'${entry}' has no ${ENTRY_FILE} (or 'luaut.types' in its package.json)`, ...entryPosition(config, entry) });
|
|
6049
|
+
continue;
|
|
6050
|
+
}
|
|
6051
|
+
const names = entry.startsWith("@") || entry.includes("/") ? [entry] : [`@luaut/${entry}`, entry];
|
|
6052
|
+
const found = names.map((name) => findPackage(name, config.directory, host)).find(Boolean);
|
|
6053
|
+
if (found) addPackage(found.directory, found.file, /* @__PURE__ */ new Set());
|
|
6054
|
+
else {
|
|
6055
|
+
problems.push({
|
|
6056
|
+
file: config.path,
|
|
6057
|
+
message: `Cannot find type library '${entry}'. Install it with: npm i -D ${names[0]}`,
|
|
6058
|
+
...entryPosition(config, entry)
|
|
6059
|
+
});
|
|
6060
|
+
}
|
|
6061
|
+
}
|
|
6062
|
+
return { files, problems };
|
|
6063
|
+
}
|
|
6064
|
+
var ENTRY_FILE = "index.d.luaut";
|
|
6065
|
+
function packageEntry(directory, host) {
|
|
6066
|
+
const manifest = readJson(join2(directory, "package.json"), host);
|
|
6067
|
+
const declared = manifest?.luaut?.types;
|
|
6068
|
+
const file = resolve2(directory, typeof declared === "string" ? declared : ENTRY_FILE);
|
|
6069
|
+
return host.readFile(file) !== void 0 ? file : void 0;
|
|
6070
|
+
}
|
|
6071
|
+
function findPackage(name, from, host) {
|
|
6072
|
+
let directory = resolve2(from);
|
|
6073
|
+
for (; ; ) {
|
|
6074
|
+
const candidate = join2(directory, "node_modules", ...name.split("/"));
|
|
6075
|
+
const file = packageEntry(candidate, host);
|
|
6076
|
+
if (file) return { directory: candidate, file };
|
|
6077
|
+
const parent = dirname2(directory);
|
|
6078
|
+
if (parent === directory) return void 0;
|
|
6079
|
+
directory = parent;
|
|
6080
|
+
}
|
|
6081
|
+
}
|
|
6082
|
+
function dependencyNames(directory, host) {
|
|
6083
|
+
const manifest = readJson(join2(directory, "package.json"), host);
|
|
6084
|
+
const names = /* @__PURE__ */ new Set();
|
|
6085
|
+
for (const field of ["dependencies", "peerDependencies"]) {
|
|
6086
|
+
const deps = manifest?.[field];
|
|
6087
|
+
if (deps && typeof deps === "object") for (const name of Object.keys(deps)) names.add(name);
|
|
6088
|
+
}
|
|
6089
|
+
return [...names];
|
|
6090
|
+
}
|
|
6091
|
+
function readJson(path, host) {
|
|
6092
|
+
const text = host.readFile(path);
|
|
6093
|
+
if (text === void 0) return void 0;
|
|
6094
|
+
try {
|
|
6095
|
+
const value = JSON.parse(text);
|
|
6096
|
+
return value && typeof value === "object" ? value : void 0;
|
|
6097
|
+
} catch {
|
|
6098
|
+
return void 0;
|
|
6099
|
+
}
|
|
6100
|
+
}
|
|
6101
|
+
function entryPosition(config, entry) {
|
|
6102
|
+
return keyPosition(config.source, entry);
|
|
6103
|
+
}
|
|
6104
|
+
function pathKey(path) {
|
|
6105
|
+
const normalized = resolve2(path);
|
|
6106
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
6107
|
+
}
|
|
5748
6108
|
|
|
5749
|
-
// src/
|
|
5750
|
-
import {
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
6109
|
+
// src/project/modules.ts
|
|
6110
|
+
import { dirname as dirname3, join as join3, resolve as resolve3 } from "path";
|
|
6111
|
+
function moduleCandidates(fromFile, specifier, config) {
|
|
6112
|
+
const bases = specifier.startsWith("./") || specifier.startsWith("../") ? [resolve3(dirname3(fromFile), specifier)] : config ? aliasTargets(config, specifier) : [];
|
|
6113
|
+
return bases.flatMap((base) => base.endsWith(".luaut") ? [base] : [`${base}.luaut`, `${base}.d.luaut`, join3(base, "index.luaut")]);
|
|
6114
|
+
}
|
|
6115
|
+
function resolveModulePath(fromFile, specifier, config, host = nodeHost) {
|
|
6116
|
+
return moduleCandidates(fromFile, specifier, config).find((path) => host.readFile(path) !== void 0);
|
|
6117
|
+
}
|
|
6118
|
+
function aliasTargets(config, specifier) {
|
|
6119
|
+
let match;
|
|
6120
|
+
let prefixLength = -1;
|
|
6121
|
+
for (const pattern2 of Object.keys(config.paths)) {
|
|
6122
|
+
const star = pattern2.indexOf("*");
|
|
6123
|
+
if (star < 0) {
|
|
6124
|
+
if (pattern2 === specifier) {
|
|
6125
|
+
match = { pattern: pattern2, wildcard: "" };
|
|
6126
|
+
break;
|
|
6127
|
+
}
|
|
6128
|
+
continue;
|
|
6129
|
+
}
|
|
6130
|
+
const prefix = pattern2.slice(0, star);
|
|
6131
|
+
const suffix = pattern2.slice(star + 1);
|
|
6132
|
+
const fits = specifier.length >= prefix.length + suffix.length && specifier.startsWith(prefix) && specifier.endsWith(suffix);
|
|
6133
|
+
if (fits && prefix.length > prefixLength) {
|
|
6134
|
+
prefixLength = prefix.length;
|
|
6135
|
+
match = { pattern: pattern2, wildcard: specifier.slice(prefix.length, specifier.length - suffix.length) };
|
|
6136
|
+
}
|
|
6137
|
+
}
|
|
6138
|
+
if (!match) return [];
|
|
6139
|
+
const { pattern, wildcard } = match;
|
|
6140
|
+
return config.paths[pattern].map((target) => resolve3(config.baseUrl, target.replace("*", wildcard)));
|
|
6141
|
+
}
|
|
5755
6142
|
|
|
5756
|
-
// src/
|
|
5757
|
-
import {
|
|
5758
|
-
|
|
5759
|
-
var
|
|
5760
|
-
var
|
|
5761
|
-
|
|
6143
|
+
// src/project/sourcemap.ts
|
|
6144
|
+
import { dirname as dirname4, extname, resolve as resolve4 } from "path";
|
|
6145
|
+
var IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
6146
|
+
var INSTANCE_MEMBERS = /* @__PURE__ */ new Set(["Name", "ClassName", "Parent", "Archivable"]);
|
|
6147
|
+
var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".luaut", ".luau", ".lua"]);
|
|
6148
|
+
function sourceMapTypes(text, path, options) {
|
|
6149
|
+
let root;
|
|
6150
|
+
try {
|
|
6151
|
+
root = JSON.parse(text);
|
|
6152
|
+
} catch (error) {
|
|
6153
|
+
return { problem: `Invalid sourcemap: ${error.message}` };
|
|
6154
|
+
}
|
|
6155
|
+
if (!isNode(root)) return { problem: "Invalid sourcemap: the root must be an object with 'name' and 'className'" };
|
|
6156
|
+
const directory = dirname4(resolve4(path));
|
|
6157
|
+
const lines = [];
|
|
6158
|
+
const aliasOfFile = /* @__PURE__ */ new Map();
|
|
6159
|
+
const used = /* @__PURE__ */ new Set();
|
|
6160
|
+
const canOmit = options.classes.has("Omit");
|
|
6161
|
+
const aliasOfNode = /* @__PURE__ */ new Map();
|
|
6162
|
+
const aliasFor = (segments) => {
|
|
6163
|
+
const base = `SourceMap_${segments.map((s) => s.replace(/[^A-Za-z0-9_]/g, "_")).join("_")}`;
|
|
6164
|
+
let alias = base;
|
|
6165
|
+
for (let n = 2; used.has(alias); n++) alias = `${base}_${n}`;
|
|
6166
|
+
used.add(alias);
|
|
6167
|
+
return alias;
|
|
6168
|
+
};
|
|
6169
|
+
const visit = (node, segments, parent) => {
|
|
6170
|
+
const alias = aliasFor(segments);
|
|
6171
|
+
aliasOfNode.set(node, alias);
|
|
6172
|
+
for (const filePath of node.filePaths ?? []) aliasOfFile.set(fileKey(resolve4(directory, filePath)), alias);
|
|
6173
|
+
const className = IDENTIFIER.test(node.className) && options.classes.has(node.className) ? node.className : "Instance";
|
|
6174
|
+
const taken = options.membersOf?.(className) ?? INSTANCE_MEMBERS;
|
|
6175
|
+
const members = [];
|
|
6176
|
+
if (parent && canOmit) members.push(`Parent: ${parent}`);
|
|
6177
|
+
const named = /* @__PURE__ */ new Set();
|
|
6178
|
+
for (const child of node.children ?? []) {
|
|
6179
|
+
if (!isNode(child)) continue;
|
|
6180
|
+
const childAlias = visit(child, [...segments, child.name], alias);
|
|
6181
|
+
if (!IDENTIFIER.test(child.name) || taken.has(child.name) || named.has(child.name)) continue;
|
|
6182
|
+
named.add(child.name);
|
|
6183
|
+
members.push(`${child.name}: ${childAlias}`);
|
|
6184
|
+
}
|
|
6185
|
+
const base = parent && canOmit ? `Omit<${className}, "Parent">` : className;
|
|
6186
|
+
lines.push(`type ${alias} = ${members.length ? `${base} & { ${members.join(", ")} }` : base}`);
|
|
6187
|
+
return alias;
|
|
6188
|
+
};
|
|
6189
|
+
const rootAlias = visit(root, [root.name], void 0);
|
|
6190
|
+
if (root.className === "DataModel") {
|
|
6191
|
+
lines.push(`declare game: ${rootAlias}`);
|
|
6192
|
+
const workspace = (root.children ?? []).find((child) => isNode(child) && child.className === "Workspace");
|
|
6193
|
+
const workspaceAlias = workspace && aliasOfNode.get(workspace);
|
|
6194
|
+
if (workspaceAlias) lines.push(`declare workspace: ${workspaceAlias}`);
|
|
6195
|
+
}
|
|
6196
|
+
let program;
|
|
6197
|
+
try {
|
|
6198
|
+
program = parse(lines.join("\n"));
|
|
6199
|
+
} catch (error) {
|
|
6200
|
+
return { problem: `Could not turn the sourcemap into types: ${error.message}` };
|
|
6201
|
+
}
|
|
6202
|
+
return {
|
|
6203
|
+
types: {
|
|
6204
|
+
program,
|
|
6205
|
+
scriptFor(file) {
|
|
6206
|
+
const alias = aliasOfFile.get(fileKey(file));
|
|
6207
|
+
return alias ? parse(`declare script: ${alias}`) : void 0;
|
|
6208
|
+
}
|
|
6209
|
+
}
|
|
6210
|
+
};
|
|
6211
|
+
}
|
|
6212
|
+
function isNode(value) {
|
|
6213
|
+
if (!value || typeof value !== "object") return false;
|
|
6214
|
+
const node = value;
|
|
6215
|
+
return typeof node.name === "string" && typeof node.className === "string";
|
|
6216
|
+
}
|
|
6217
|
+
function fileKey(path) {
|
|
6218
|
+
const extension = extname(path);
|
|
6219
|
+
const bare = SCRIPT_EXTENSIONS.has(extension) ? path.slice(0, -extension.length) : path;
|
|
6220
|
+
const normalized = resolve4(bare);
|
|
6221
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
6222
|
+
}
|
|
5762
6223
|
|
|
5763
6224
|
// src/index.ts
|
|
5764
|
-
var defaultLibs = [luauLib, robloxLib];
|
|
5765
6225
|
var luautparser = {
|
|
5766
6226
|
tokenize,
|
|
5767
6227
|
parseTokens,
|
|
@@ -5777,6 +6237,7 @@ var luautparser = {
|
|
|
5777
6237
|
var index_default = luautparser;
|
|
5778
6238
|
export {
|
|
5779
6239
|
BinaryOperators,
|
|
6240
|
+
CONFIG_FILE_NAMES,
|
|
5780
6241
|
Keywords,
|
|
5781
6242
|
LexError,
|
|
5782
6243
|
Operators,
|
|
@@ -5791,10 +6252,10 @@ export {
|
|
|
5791
6252
|
bufferType,
|
|
5792
6253
|
containsTypeParam,
|
|
5793
6254
|
index_default as default,
|
|
5794
|
-
defaultLibs,
|
|
5795
6255
|
difference,
|
|
5796
6256
|
equalTypes,
|
|
5797
6257
|
falsyType,
|
|
6258
|
+
findConfig,
|
|
5798
6259
|
fn,
|
|
5799
6260
|
formatType,
|
|
5800
6261
|
getBinding,
|
|
@@ -5805,11 +6266,10 @@ export {
|
|
|
5805
6266
|
isPossiblyTruthy,
|
|
5806
6267
|
isUnassignedGlobal,
|
|
5807
6268
|
literal,
|
|
5808
|
-
|
|
5809
|
-
luauDefsPath,
|
|
5810
|
-
luauLib,
|
|
6269
|
+
loadConfig,
|
|
5811
6270
|
luautparser,
|
|
5812
6271
|
matchInfer,
|
|
6272
|
+
moduleCandidates,
|
|
5813
6273
|
moduleExports,
|
|
5814
6274
|
narrowExclude,
|
|
5815
6275
|
narrowFalsy,
|
|
@@ -5817,6 +6277,7 @@ export {
|
|
|
5817
6277
|
narrowTruthy,
|
|
5818
6278
|
neverType,
|
|
5819
6279
|
nilType,
|
|
6280
|
+
nodeHost,
|
|
5820
6281
|
numberType,
|
|
5821
6282
|
objectType,
|
|
5822
6283
|
optional,
|
|
@@ -5826,11 +6287,12 @@ export {
|
|
|
5826
6287
|
parseTokens,
|
|
5827
6288
|
parseWithRecovery,
|
|
5828
6289
|
primitive,
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
robloxLib,
|
|
6290
|
+
resolveModulePath,
|
|
6291
|
+
resolveTypeLibraries,
|
|
5832
6292
|
setAliasExpander,
|
|
6293
|
+
sourceMapTypes,
|
|
5833
6294
|
stringType,
|
|
6295
|
+
stripJsonComments,
|
|
5834
6296
|
substitute,
|
|
5835
6297
|
templateMatches,
|
|
5836
6298
|
threadType,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "luaut-parser",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "luaut parser for roblox",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
|
-
"test": "tsx scripts/test.ts",
|
|
26
|
+
"test": "tsx scripts/test.ts && tsx scripts/project.test.ts",
|
|
27
27
|
"build": "tsup",
|
|
28
28
|
"typecheck": "tsc --noEmit"
|
|
29
29
|
},
|
|
@@ -32,12 +32,11 @@
|
|
|
32
32
|
"license": "ISC",
|
|
33
33
|
"type": "module",
|
|
34
34
|
"devDependencies": {
|
|
35
|
+
"@luaut/luau": "^1.0.0",
|
|
36
|
+
"@luaut/roblox": "^1.0.0",
|
|
35
37
|
"@types/node": "^22.0.0",
|
|
36
38
|
"tsup": "^8.5.1",
|
|
37
39
|
"tsx": "^4.19.0",
|
|
38
40
|
"typescript": "^5.0.4"
|
|
39
|
-
},
|
|
40
|
-
"dependencies": {
|
|
41
|
-
"luaut-parser": "^1.0.0"
|
|
42
41
|
}
|
|
43
42
|
}
|