luaut-language-server 1.1.1 → 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 +1 -1
- package/dist/{chunk-OQWE7ISD.js → chunk-HJ2C3OFZ.js} +418 -145
- package/dist/chunk-HJ2C3OFZ.js.map +1 -0
- package/dist/cli.cjs +407 -138
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +407 -138
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -16
- package/dist/index.d.ts +50 -16
- package/dist/index.js +1 -1
- package/package.json +4 -2
- package/dist/chunk-OQWE7ISD.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -64,19 +64,83 @@ var import_vscode_languageserver_textdocument = require("vscode-languageserver-t
|
|
|
64
64
|
var import_node_fs = require("fs");
|
|
65
65
|
var import_node_path = require("path");
|
|
66
66
|
var import_node_url = require("url");
|
|
67
|
+
var import_luaut_parser2 = require("luaut-parser");
|
|
68
|
+
|
|
69
|
+
// src/features/members.ts
|
|
67
70
|
var import_luaut_parser = require("luaut-parser");
|
|
71
|
+
function membersOf(type, aliases, seen = /* @__PURE__ */ new Set()) {
|
|
72
|
+
if (!type || seen.has(type)) return [];
|
|
73
|
+
seen.add(type);
|
|
74
|
+
switch (type.kind) {
|
|
75
|
+
case "object": {
|
|
76
|
+
const out = [];
|
|
77
|
+
for (const [name, property] of type.properties) {
|
|
78
|
+
out.push({ name, property, isMethod: takesSelf(property.type) });
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
case "intersection": {
|
|
83
|
+
const merged = /* @__PURE__ */ new Map();
|
|
84
|
+
for (const part of type.types) {
|
|
85
|
+
for (const member of membersOf(part, aliases, seen)) merged.set(member.name, member);
|
|
86
|
+
}
|
|
87
|
+
return [...merged.values()];
|
|
88
|
+
}
|
|
89
|
+
case "union": {
|
|
90
|
+
const perBranch = type.types.map((part) => membersOf(part, aliases, seen));
|
|
91
|
+
if (!perBranch.length) return [];
|
|
92
|
+
const [first, ...rest] = perBranch;
|
|
93
|
+
return first.filter((member) => rest.every((other) => other.some((m) => m.name === member.name)));
|
|
94
|
+
}
|
|
95
|
+
case "genericRef": {
|
|
96
|
+
const alias = aliases.get(type.name);
|
|
97
|
+
return alias ? membersOf(alias, aliases, seen) : [];
|
|
98
|
+
}
|
|
99
|
+
case "typeParam":
|
|
100
|
+
return membersOf(type.constraint, aliases, seen);
|
|
101
|
+
default:
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function takesSelf(type) {
|
|
106
|
+
for (const signature of signaturesOf(type)) {
|
|
107
|
+
if (signature.params[0]?.name === "self") return true;
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
function signaturesOf(type, aliases) {
|
|
112
|
+
if (!type) return [];
|
|
113
|
+
if (type.kind === "function") return [type];
|
|
114
|
+
if (type.kind === "intersection") return type.types.flatMap((t) => signaturesOf(t, aliases));
|
|
115
|
+
if (type.kind === "genericRef" && aliases) {
|
|
116
|
+
const alias = aliases.get(type.name);
|
|
117
|
+
return alias ? signaturesOf(alias, aliases) : [];
|
|
118
|
+
}
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
function signatureLabel(signature) {
|
|
122
|
+
const parameters = signature.params.map((p, i) => {
|
|
123
|
+
const name = p.name ?? `arg${i + 1}`;
|
|
124
|
+
return `${name}${p.optional ? "?" : ""}: ${(0, import_luaut_parser.formatType)(p.type)}`;
|
|
125
|
+
});
|
|
126
|
+
const generics = signature.typeParams?.length ? `<${signature.typeParams.join(", ")}>` : "";
|
|
127
|
+
const varargs = signature.varargs ? [`...: ${(0, import_luaut_parser.formatType)(signature.varargs)}`] : [];
|
|
128
|
+
const label = `${generics}(${[...parameters, ...varargs].join(", ")}) -> ${(0, import_luaut_parser.formatType)(signature.returns)}`;
|
|
129
|
+
return { label, parameters };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/analysis.ts
|
|
68
133
|
function globalsOf(libs) {
|
|
69
134
|
const names = /* @__PURE__ */ new Set();
|
|
70
|
-
for (const lib of libs)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
for (const statement of statements) {
|
|
75
|
-
if (statement.type === "DeclareStatement") into.add(statement.name);
|
|
135
|
+
for (const lib of libs) {
|
|
136
|
+
for (const statement of lib.body.statements) {
|
|
137
|
+
if (statement.type === "DeclareStatement") names.add(statement.name);
|
|
138
|
+
}
|
|
76
139
|
}
|
|
140
|
+
return [...names];
|
|
77
141
|
}
|
|
78
142
|
function bindingOfNode(analysis, node) {
|
|
79
|
-
const used = (0,
|
|
143
|
+
const used = (0, import_luaut_parser2.getBinding)(analysis.scopes, node);
|
|
80
144
|
if (used) return used;
|
|
81
145
|
return declarationIndex(analysis).get(node);
|
|
82
146
|
}
|
|
@@ -111,20 +175,34 @@ function pathKey(path) {
|
|
|
111
175
|
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
112
176
|
}
|
|
113
177
|
var CYCLE = { values: /* @__PURE__ */ new Map(), types: /* @__PURE__ */ new Map(), partial: true };
|
|
178
|
+
var NO_PROJECT = { project: { fixed: false, problems: [] }, libs: [], globals: [], reads: /* @__PURE__ */ new Map() };
|
|
114
179
|
var Analyzer = class {
|
|
115
|
-
|
|
116
|
-
builtinGlobals;
|
|
180
|
+
fixed;
|
|
117
181
|
openDocument;
|
|
118
182
|
cache = /* @__PURE__ */ new Map();
|
|
119
183
|
/** Imported modules, by path key. */
|
|
120
184
|
modules = /* @__PURE__ */ new Map();
|
|
185
|
+
/** Project contexts, by folder. */
|
|
186
|
+
contexts = /* @__PURE__ */ new Map();
|
|
187
|
+
/** Parsed type libraries, by path — reparsed only when the text changes. */
|
|
188
|
+
libraries = /* @__PURE__ */ new Map();
|
|
189
|
+
/** Sourcemaps turned into types, by path, with what they were built from. */
|
|
190
|
+
sourceMaps = /* @__PURE__ */ new Map();
|
|
191
|
+
/** The analysis run in progress, if any. */
|
|
192
|
+
run;
|
|
121
193
|
constructor(options = {}) {
|
|
122
|
-
this.libs = options.libs ?? import_luaut_parser.defaultLibs;
|
|
123
|
-
this.builtinGlobals = globalsOf(this.libs);
|
|
124
194
|
this.openDocument = options.openDocument;
|
|
195
|
+
if (options.libs) {
|
|
196
|
+
this.fixed = {
|
|
197
|
+
project: { fixed: true, problems: [] },
|
|
198
|
+
libs: options.libs,
|
|
199
|
+
globals: globalsOf(options.libs),
|
|
200
|
+
reads: /* @__PURE__ */ new Map()
|
|
201
|
+
};
|
|
202
|
+
}
|
|
125
203
|
}
|
|
126
204
|
/** Analyze `document`, reusing the previous result while neither it nor
|
|
127
|
-
* anything it
|
|
205
|
+
* anything it read has changed. */
|
|
128
206
|
get(document) {
|
|
129
207
|
const cached = this.cache.get(document.uri);
|
|
130
208
|
const source = document.getText();
|
|
@@ -139,34 +217,41 @@ var Analyzer = class {
|
|
|
139
217
|
* completion, which analyzes a speculatively edited copy of the file. */
|
|
140
218
|
analyze(uri, version, source) {
|
|
141
219
|
const path = pathOfUri(uri);
|
|
142
|
-
return this.analyzeModule(uri, version, source, new Set(
|
|
220
|
+
if (!path) return this.analyzeModule(uri, version, source, /* @__PURE__ */ new Set());
|
|
221
|
+
const key = pathKey(path);
|
|
222
|
+
return this.resolvingCycles(key, () => {
|
|
223
|
+
const analysis = this.analyzeModule(uri, version, source, /* @__PURE__ */ new Set([key]));
|
|
224
|
+
return { result: analysis, exports: () => this.exportsFrom(analysis, /* @__PURE__ */ new Set([key])) };
|
|
225
|
+
});
|
|
143
226
|
}
|
|
144
227
|
forget(uri) {
|
|
145
228
|
this.cache.delete(uri);
|
|
146
229
|
}
|
|
147
|
-
/** The
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
resolveModulePath(fromUri, specifier) {
|
|
151
|
-
return this.moduleCandidates(fromUri, specifier).find((candidate) => this.sourceOf(candidate) !== void 0);
|
|
230
|
+
/** The project a file belongs to. */
|
|
231
|
+
projectOf(uri) {
|
|
232
|
+
return this.contextFor(pathOfUri(uri)).project;
|
|
152
233
|
}
|
|
153
|
-
/**
|
|
154
|
-
|
|
234
|
+
/** The file an import in `fromUri` names: a relative path, or a `paths`
|
|
235
|
+
* alias from the file's config. */
|
|
236
|
+
resolveModulePath(fromUri, specifier) {
|
|
155
237
|
const from = pathOfUri(fromUri);
|
|
156
|
-
if (!from
|
|
157
|
-
|
|
158
|
-
return specifier.endsWith(".luaut") ? [base] : [`${base}.luaut`, `${base}.d.luaut`, (0, import_node_path.join)(base, "index.luaut")];
|
|
238
|
+
if (!from) return void 0;
|
|
239
|
+
return this.candidatesFor(from, specifier).find((candidate) => this.readFile(candidate) !== void 0);
|
|
159
240
|
}
|
|
160
241
|
/** What the module at `path` exports, analyzing it if need be. */
|
|
161
242
|
exportsAt(path) {
|
|
162
|
-
return this.
|
|
243
|
+
return this.resolvingCycles(pathKey(path), () => {
|
|
244
|
+
const exports2 = this.exportsOf(path, /* @__PURE__ */ new Set());
|
|
245
|
+
return { result: exports2, exports: () => exports2 };
|
|
246
|
+
});
|
|
163
247
|
}
|
|
164
248
|
/** The analysis of the module at `path`, analyzing it if need be. */
|
|
165
249
|
moduleAt(path) {
|
|
166
|
-
this.
|
|
250
|
+
this.exportsAt(path);
|
|
167
251
|
return this.modules.get(pathKey(path))?.analysis;
|
|
168
252
|
}
|
|
169
|
-
|
|
253
|
+
/** A file's text: the open document if there is one, else the disk. */
|
|
254
|
+
readFile(path) {
|
|
170
255
|
const open = this.openDocument?.(path);
|
|
171
256
|
if (open) return open.getText();
|
|
172
257
|
try {
|
|
@@ -175,60 +260,206 @@ var Analyzer = class {
|
|
|
175
260
|
return void 0;
|
|
176
261
|
}
|
|
177
262
|
}
|
|
263
|
+
candidatesFor(from, specifier) {
|
|
264
|
+
return (0, import_luaut_parser2.moduleCandidates)(from, specifier, this.contextFor(from).project.config);
|
|
265
|
+
}
|
|
266
|
+
// ---------------------------------------------------------------- projects
|
|
267
|
+
contextFor(path) {
|
|
268
|
+
if (this.fixed) return this.fixed;
|
|
269
|
+
if (!path) return NO_PROJECT;
|
|
270
|
+
const key = pathKey((0, import_node_path.dirname)(path));
|
|
271
|
+
const cached = this.contexts.get(key);
|
|
272
|
+
if (cached && this.unchanged(cached.reads)) return cached;
|
|
273
|
+
const context = this.buildContext(path);
|
|
274
|
+
this.contexts.set(key, context);
|
|
275
|
+
return context;
|
|
276
|
+
}
|
|
277
|
+
buildContext(path) {
|
|
278
|
+
const reads = /* @__PURE__ */ new Map();
|
|
279
|
+
const host = {
|
|
280
|
+
readFile: (file) => {
|
|
281
|
+
const text = this.readFile(file);
|
|
282
|
+
reads.set(file, text);
|
|
283
|
+
return text;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
const lookup = (0, import_luaut_parser2.findConfig)(path, host);
|
|
287
|
+
const problems = [...lookup.problems];
|
|
288
|
+
const config = lookup.config;
|
|
289
|
+
if (!config) return { project: { fixed: false, problems }, libs: [], globals: [], reads };
|
|
290
|
+
const libraries = (0, import_luaut_parser2.resolveTypeLibraries)(config, host);
|
|
291
|
+
problems.push(...libraries.problems);
|
|
292
|
+
const libs = [];
|
|
293
|
+
for (const file of libraries.files) {
|
|
294
|
+
const program = this.library(file, host, problems);
|
|
295
|
+
if (program) libs.push(program);
|
|
296
|
+
}
|
|
297
|
+
let sourceMap;
|
|
298
|
+
if (config.sourceMap) {
|
|
299
|
+
const text = host.readFile(config.sourceMap);
|
|
300
|
+
if (text === void 0) {
|
|
301
|
+
problems.push({
|
|
302
|
+
file: config.path,
|
|
303
|
+
message: `Cannot find the sourceMap file ${config.sourceMap}`,
|
|
304
|
+
...optionPosition(config, "sourceMap")
|
|
305
|
+
});
|
|
306
|
+
} else {
|
|
307
|
+
const result = this.sourceMap(config.sourceMap, text, libs, libraries.files);
|
|
308
|
+
if (result.problem) problems.push({ file: config.sourceMap, message: result.problem, line: 1, column: 1 });
|
|
309
|
+
sourceMap = result.types;
|
|
310
|
+
if (sourceMap) libs.push(sourceMap.program);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return { project: { config, fixed: false, problems }, libs, globals: globalsOf(libs), sourceMap, reads };
|
|
314
|
+
}
|
|
315
|
+
/** A type library's definitions, parsed once per text. */
|
|
316
|
+
library(file, host, problems) {
|
|
317
|
+
const source = host.readFile(file);
|
|
318
|
+
if (source === void 0) return void 0;
|
|
319
|
+
const key = pathKey(file);
|
|
320
|
+
let entry = this.libraries.get(key);
|
|
321
|
+
if (!entry || entry.source !== source) {
|
|
322
|
+
try {
|
|
323
|
+
entry = { source, program: (0, import_luaut_parser2.parse)(source) };
|
|
324
|
+
} catch (error) {
|
|
325
|
+
const { message, line, column } = error;
|
|
326
|
+
entry = {
|
|
327
|
+
source,
|
|
328
|
+
problem: { file, message: `Syntax error in type library: ${message.replace(/\s*\(\d+:\d+\)$/, "")}`, line, column }
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
this.libraries.set(key, entry);
|
|
332
|
+
}
|
|
333
|
+
if (entry.problem) problems.push(entry.problem);
|
|
334
|
+
return entry.program;
|
|
335
|
+
}
|
|
336
|
+
/** A sourcemap's types, rebuilt only when it or the libraries change. */
|
|
337
|
+
sourceMap(path, text, libs, files) {
|
|
338
|
+
const key = pathKey(path);
|
|
339
|
+
const libraries = files.join("\n");
|
|
340
|
+
const cached = this.sourceMaps.get(key);
|
|
341
|
+
if (cached && cached.text === text && cached.libraries === libraries) return cached.result;
|
|
342
|
+
const aliases = aliasesOf(libs);
|
|
343
|
+
const members = /* @__PURE__ */ new Map();
|
|
344
|
+
const result = (0, import_luaut_parser2.sourceMapTypes)(text, path, {
|
|
345
|
+
classes: new Set(aliases.keys()),
|
|
346
|
+
membersOf: (className) => {
|
|
347
|
+
let names = members.get(className);
|
|
348
|
+
if (!names) {
|
|
349
|
+
names = new Set(membersOf(aliases.get(className), aliases).map((member) => member.name));
|
|
350
|
+
members.set(className, names);
|
|
351
|
+
}
|
|
352
|
+
return names;
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
this.sourceMaps.set(key, { text, libraries, result });
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
unchanged(reads) {
|
|
359
|
+
for (const [file, text] of reads) if (this.readFile(file) !== text) return false;
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
// ----------------------------------------------------------------- modules
|
|
363
|
+
/** Run one analysis of `root` and everything it imports; if that met an
|
|
364
|
+
* import cycle, run it once more with the first pass's exports standing
|
|
365
|
+
* in for the `any` the cycle left (see `Run`). A call made while a run is
|
|
366
|
+
* already going is part of that run. */
|
|
367
|
+
resolvingCycles(root, analyzeRoot) {
|
|
368
|
+
if (this.run) return analyzeRoot().result;
|
|
369
|
+
const run = { cycles: /* @__PURE__ */ new Set(), analyzed: /* @__PURE__ */ new Set(), provisional: /* @__PURE__ */ new Map() };
|
|
370
|
+
this.run = run;
|
|
371
|
+
try {
|
|
372
|
+
const first = analyzeRoot();
|
|
373
|
+
if (!run.cycles.size) return first.result;
|
|
374
|
+
for (const key of run.cycles) {
|
|
375
|
+
const exports2 = key === root ? first.exports() : this.modules.get(key)?.exports;
|
|
376
|
+
if (exports2 && !exports2.partial) run.provisional.set(key, exports2);
|
|
377
|
+
}
|
|
378
|
+
for (const key of run.analyzed) this.modules.delete(key);
|
|
379
|
+
return analyzeRoot().result;
|
|
380
|
+
} finally {
|
|
381
|
+
this.run = void 0;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/** A module's exports, from its analysis. */
|
|
385
|
+
exportsFrom(analysis, importing) {
|
|
386
|
+
return (0, import_luaut_parser2.moduleExports)(analysis.program, analysis.scopes, analysis.types, (specifier) => {
|
|
387
|
+
const next = this.resolveModulePath(analysis.uri, specifier);
|
|
388
|
+
return next ? this.exportsOf(next, importing) : void 0;
|
|
389
|
+
});
|
|
390
|
+
}
|
|
178
391
|
/** `importing` holds every module on the current import chain, so an
|
|
179
392
|
* import back into one of them is recognized as a cycle. */
|
|
180
393
|
analyzeModule(uri, version, source, importing) {
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
|
|
394
|
+
const path = pathOfUri(uri);
|
|
395
|
+
const context = this.contextFor(path);
|
|
396
|
+
const script = path ? context.sourceMap?.scriptFor(path) : void 0;
|
|
397
|
+
const libs = script ? [...context.libs, script] : context.libs;
|
|
398
|
+
const globals = script ? [...context.globals, "script"] : context.globals;
|
|
399
|
+
const { program, errors } = (0, import_luaut_parser2.parseWithRecovery)(source);
|
|
400
|
+
const scopes = (0, import_luaut_parser2.analyzeScopes)(program, { builtinGlobals: [...globals] });
|
|
401
|
+
const dependencies = new Map(context.reads);
|
|
402
|
+
const types = (0, import_luaut_parser2.analyzeTypes)(program, scopes, {
|
|
403
|
+
libs,
|
|
186
404
|
resolveModule: (specifier) => {
|
|
187
|
-
|
|
405
|
+
if (!path) return void 0;
|
|
406
|
+
const candidates = this.candidatesFor(path, specifier);
|
|
407
|
+
const target = candidates.find((candidate) => this.readFile(candidate) !== void 0);
|
|
188
408
|
if (!target) {
|
|
189
|
-
for (const candidate of
|
|
409
|
+
for (const candidate of candidates) dependencies.set(candidate, void 0);
|
|
190
410
|
return void 0;
|
|
191
411
|
}
|
|
192
412
|
const exports2 = this.exportsOf(target, importing);
|
|
193
|
-
dependencies.set(target, this.
|
|
413
|
+
dependencies.set(target, this.readFile(target));
|
|
194
414
|
return exports2;
|
|
195
415
|
}
|
|
196
416
|
});
|
|
197
|
-
return { uri, version, source, program, parseErrors: errors, scopes, types, dependencies };
|
|
417
|
+
return { uri, version, source, program, parseErrors: errors, scopes, types, dependencies, project: context.project };
|
|
198
418
|
}
|
|
199
419
|
exportsOf(path, importing) {
|
|
200
420
|
const key = pathKey(path);
|
|
201
|
-
if (importing.has(key))
|
|
202
|
-
|
|
421
|
+
if (importing.has(key)) {
|
|
422
|
+
this.run?.cycles.add(key);
|
|
423
|
+
return this.run?.provisional.get(key) ?? CYCLE;
|
|
424
|
+
}
|
|
425
|
+
const source = this.readFile(path);
|
|
203
426
|
if (source === void 0) return void 0;
|
|
204
427
|
const cached = this.modules.get(key);
|
|
205
428
|
if (cached && cached.analysis.source === source && this.isFresh(cached.analysis)) return cached.exports;
|
|
206
429
|
importing.add(key);
|
|
207
430
|
try {
|
|
208
431
|
const analysis = this.analyzeModule(uriOfPath(path), -1, source, importing);
|
|
209
|
-
const exports2 =
|
|
210
|
-
const next = this.resolveModulePath(analysis.uri, specifier);
|
|
211
|
-
return next ? this.exportsOf(next, importing) : void 0;
|
|
212
|
-
});
|
|
432
|
+
const exports2 = this.exportsFrom(analysis, importing);
|
|
213
433
|
this.modules.set(key, { analysis, exports: exports2 });
|
|
434
|
+
this.run?.analyzed.add(key);
|
|
214
435
|
return exports2;
|
|
215
436
|
} finally {
|
|
216
437
|
importing.delete(key);
|
|
217
438
|
}
|
|
218
439
|
}
|
|
219
|
-
/** Does every
|
|
220
|
-
* still have the text it was analyzed against? */
|
|
440
|
+
/** Does every file `analysis` read — and everything the modules it
|
|
441
|
+
* imported read — still have the text it was analyzed against? */
|
|
221
442
|
isFresh(analysis, seen = /* @__PURE__ */ new Set()) {
|
|
222
443
|
if (seen.has(analysis)) return true;
|
|
223
444
|
seen.add(analysis);
|
|
224
445
|
for (const [path, source] of analysis.dependencies) {
|
|
225
|
-
if (this.
|
|
446
|
+
if (this.readFile(path) !== source) return false;
|
|
226
447
|
const module2 = this.modules.get(pathKey(path));
|
|
227
448
|
if (module2 && !this.isFresh(module2.analysis, seen)) return false;
|
|
228
449
|
}
|
|
229
450
|
return true;
|
|
230
451
|
}
|
|
231
452
|
};
|
|
453
|
+
function aliasesOf(libs) {
|
|
454
|
+
const empty = (0, import_luaut_parser2.parse)("");
|
|
455
|
+
return (0, import_luaut_parser2.analyzeTypes)(empty, (0, import_luaut_parser2.analyzeScopes)(empty, {}), { libs, diagnostics: false }).aliases;
|
|
456
|
+
}
|
|
457
|
+
function optionPosition(config, key) {
|
|
458
|
+
const offset = config.source.indexOf(JSON.stringify(key));
|
|
459
|
+
if (offset < 0) return { line: 1, column: 1 };
|
|
460
|
+
const before = config.source.slice(0, offset);
|
|
461
|
+
return { line: before.split("\n").length, column: offset - before.lastIndexOf("\n") };
|
|
462
|
+
}
|
|
232
463
|
|
|
233
464
|
// src/features/imports.ts
|
|
234
465
|
var import_node_fs2 = require("fs");
|
|
@@ -264,16 +495,16 @@ function containsPosition(node, pos, inclusive = false) {
|
|
|
264
495
|
}
|
|
265
496
|
function children(node) {
|
|
266
497
|
const out = [];
|
|
267
|
-
|
|
498
|
+
collect(node, out);
|
|
268
499
|
return out;
|
|
269
500
|
}
|
|
270
|
-
function
|
|
501
|
+
function collect(container, out) {
|
|
271
502
|
for (const key of Object.keys(container)) {
|
|
272
503
|
if (key === "line" || key === "column") continue;
|
|
273
504
|
const value = container[key];
|
|
274
505
|
for (const item of Array.isArray(value) ? value : [value]) {
|
|
275
506
|
if (isSpanned(item)) out.push(item);
|
|
276
|
-
else if (isSpanlessNode(item))
|
|
507
|
+
else if (isSpanlessNode(item)) collect(item, out);
|
|
277
508
|
}
|
|
278
509
|
}
|
|
279
510
|
}
|
|
@@ -314,69 +545,6 @@ function walk(root, visit, parent) {
|
|
|
314
545
|
for (const child of children(root)) walk(child, visit, root);
|
|
315
546
|
}
|
|
316
547
|
|
|
317
|
-
// src/features/members.ts
|
|
318
|
-
var import_luaut_parser2 = require("luaut-parser");
|
|
319
|
-
function membersOf(type, aliases, seen = /* @__PURE__ */ new Set()) {
|
|
320
|
-
if (!type || seen.has(type)) return [];
|
|
321
|
-
seen.add(type);
|
|
322
|
-
switch (type.kind) {
|
|
323
|
-
case "object": {
|
|
324
|
-
const out = [];
|
|
325
|
-
for (const [name, property] of type.properties) {
|
|
326
|
-
out.push({ name, property, isMethod: takesSelf(property.type) });
|
|
327
|
-
}
|
|
328
|
-
return out;
|
|
329
|
-
}
|
|
330
|
-
case "intersection": {
|
|
331
|
-
const merged = /* @__PURE__ */ new Map();
|
|
332
|
-
for (const part of type.types) {
|
|
333
|
-
for (const member of membersOf(part, aliases, seen)) merged.set(member.name, member);
|
|
334
|
-
}
|
|
335
|
-
return [...merged.values()];
|
|
336
|
-
}
|
|
337
|
-
case "union": {
|
|
338
|
-
const perBranch = type.types.map((part) => membersOf(part, aliases, seen));
|
|
339
|
-
if (!perBranch.length) return [];
|
|
340
|
-
const [first, ...rest] = perBranch;
|
|
341
|
-
return first.filter((member) => rest.every((other) => other.some((m) => m.name === member.name)));
|
|
342
|
-
}
|
|
343
|
-
case "genericRef": {
|
|
344
|
-
const alias = aliases.get(type.name);
|
|
345
|
-
return alias ? membersOf(alias, aliases, seen) : [];
|
|
346
|
-
}
|
|
347
|
-
case "typeParam":
|
|
348
|
-
return membersOf(type.constraint, aliases, seen);
|
|
349
|
-
default:
|
|
350
|
-
return [];
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
function takesSelf(type) {
|
|
354
|
-
for (const signature of signaturesOf(type)) {
|
|
355
|
-
if (signature.params[0]?.name === "self") return true;
|
|
356
|
-
}
|
|
357
|
-
return false;
|
|
358
|
-
}
|
|
359
|
-
function signaturesOf(type, aliases) {
|
|
360
|
-
if (!type) return [];
|
|
361
|
-
if (type.kind === "function") return [type];
|
|
362
|
-
if (type.kind === "intersection") return type.types.flatMap((t) => signaturesOf(t, aliases));
|
|
363
|
-
if (type.kind === "genericRef" && aliases) {
|
|
364
|
-
const alias = aliases.get(type.name);
|
|
365
|
-
return alias ? signaturesOf(alias, aliases) : [];
|
|
366
|
-
}
|
|
367
|
-
return [];
|
|
368
|
-
}
|
|
369
|
-
function signatureLabel(signature) {
|
|
370
|
-
const parameters = signature.params.map((p, i) => {
|
|
371
|
-
const name = p.name ?? `arg${i + 1}`;
|
|
372
|
-
return `${name}${p.optional ? "?" : ""}: ${(0, import_luaut_parser2.formatType)(p.type)}`;
|
|
373
|
-
});
|
|
374
|
-
const generics = signature.typeParams?.length ? `<${signature.typeParams.join(", ")}>` : "";
|
|
375
|
-
const varargs = signature.varargs ? [`...: ${(0, import_luaut_parser2.formatType)(signature.varargs)}`] : [];
|
|
376
|
-
const label = `${generics}(${[...parameters, ...varargs].join(", ")}) -> ${(0, import_luaut_parser2.formatType)(signature.returns)}`;
|
|
377
|
-
return { label, parameters };
|
|
378
|
-
}
|
|
379
|
-
|
|
380
548
|
// src/features/imports.ts
|
|
381
549
|
var SUGGEST_AGAIN = { title: "Suggest", command: "editor.action.triggerSuggest" };
|
|
382
550
|
function importCompletion(analyzer, document, position) {
|
|
@@ -388,7 +556,7 @@ function importCompletion(analyzer, document, position) {
|
|
|
388
556
|
const after = text.slice(cursor, lineEnd);
|
|
389
557
|
if (!/^\s*(?:import|export)\b/.test(before)) return void 0;
|
|
390
558
|
const path = /\bfrom\s*(["'])([^"']*)$/.exec(before);
|
|
391
|
-
if (path) return pathItems(document.uri, position, path[2]);
|
|
559
|
+
if (path) return pathItems(analyzer, document.uri, position, path[2]);
|
|
392
560
|
const braces = /^\s*(import|export)\s+(?:[A-Za-z_][A-Za-z0-9_]*\s*,\s*)?\{[^}]*$/.exec(before);
|
|
393
561
|
if (braces) {
|
|
394
562
|
const module2 = /\}\s*from\s*(["'])([^"']+)\1/.exec(after);
|
|
@@ -397,21 +565,52 @@ function importCompletion(analyzer, document, position) {
|
|
|
397
565
|
}
|
|
398
566
|
return void 0;
|
|
399
567
|
}
|
|
400
|
-
function pathItems(fromUri, position, typed) {
|
|
568
|
+
function pathItems(analyzer, fromUri, position, typed) {
|
|
401
569
|
const from = pathOfUri(fromUri);
|
|
402
570
|
if (!from) return [];
|
|
403
|
-
if (
|
|
404
|
-
const
|
|
405
|
-
return
|
|
571
|
+
if (typed.startsWith("./") || typed.startsWith("../")) {
|
|
572
|
+
const slash = typed.lastIndexOf("/");
|
|
573
|
+
return entryItems((0, import_node_path2.resolve)((0, import_node_path2.dirname)(from), typed.slice(0, slash + 1)), rangeBack(position, typed.length - slash - 1), from);
|
|
574
|
+
}
|
|
575
|
+
const items = /* @__PURE__ */ new Map();
|
|
576
|
+
const whole = rangeBack(position, typed.length);
|
|
577
|
+
const offer = (label, folder) => {
|
|
578
|
+
if (!label.startsWith(typed) || label === typed) return;
|
|
579
|
+
items.set(label, {
|
|
406
580
|
label,
|
|
407
|
-
kind: import_vscode_languageserver.CompletionItemKind.Folder,
|
|
408
|
-
textEdit: { range:
|
|
409
|
-
command: SUGGEST_AGAIN
|
|
410
|
-
})
|
|
581
|
+
kind: folder ? import_vscode_languageserver.CompletionItemKind.Folder : import_vscode_languageserver.CompletionItemKind.File,
|
|
582
|
+
textEdit: { range: whole, newText: label },
|
|
583
|
+
command: folder ? SUGGEST_AGAIN : void 0
|
|
584
|
+
});
|
|
585
|
+
};
|
|
586
|
+
offer("./", true);
|
|
587
|
+
offer("../", true);
|
|
588
|
+
const config = analyzer.projectOf(fromUri).config;
|
|
589
|
+
for (const [pattern, targets] of Object.entries(config?.paths ?? {})) {
|
|
590
|
+
const star = pattern.indexOf("*");
|
|
591
|
+
if (star < 0) {
|
|
592
|
+
offer(pattern, false);
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
const prefix = pattern.slice(0, star);
|
|
596
|
+
if (!typed.startsWith(prefix)) {
|
|
597
|
+
offer(prefix, true);
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
const rest = typed.slice(prefix.length);
|
|
601
|
+
const slash = rest.lastIndexOf("/");
|
|
602
|
+
const range = rangeBack(position, rest.length - slash - 1);
|
|
603
|
+
for (const target of targets) {
|
|
604
|
+
const cut = target.indexOf("*");
|
|
605
|
+
const head = cut < 0 ? target : target.slice(0, cut);
|
|
606
|
+
for (const item of entryItems((0, import_node_path2.resolve)(config.baseUrl, head + rest.slice(0, slash + 1)), range, from)) {
|
|
607
|
+
items.set(item.label, item);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
411
610
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
611
|
+
return [...items.values()];
|
|
612
|
+
}
|
|
613
|
+
function entryItems(directory, range, from) {
|
|
415
614
|
let entries;
|
|
416
615
|
try {
|
|
417
616
|
entries = (0, import_node_fs2.readdirSync)(directory, { withFileTypes: true });
|
|
@@ -666,6 +865,9 @@ function describe(analysis, path, index) {
|
|
|
666
865
|
case "DeclareStatement":
|
|
667
866
|
if (parent.id === node) return declareText(analysis, parent);
|
|
668
867
|
break;
|
|
868
|
+
case "DeclareClassStatement":
|
|
869
|
+
if (parent.name === node) return classText(analysis, name);
|
|
870
|
+
break;
|
|
669
871
|
case "TableTypeProperty":
|
|
670
872
|
if (parent.key === node) {
|
|
671
873
|
const type2 = typeOfNode(parent.valueType);
|
|
@@ -722,9 +924,11 @@ function describe(analysis, path, index) {
|
|
|
722
924
|
if (parameter) return typeParameterText(analysis, parameter);
|
|
723
925
|
if (PRIMITIVES.has(base)) return `type ${base}`;
|
|
724
926
|
}
|
|
725
|
-
if (!node.
|
|
726
|
-
const
|
|
727
|
-
|
|
927
|
+
if (!node.typeArguments.length) {
|
|
928
|
+
const qualified = node.namespace ? `${node.namespace}.${base}` : base;
|
|
929
|
+
const alias = types.aliases.get(qualified);
|
|
930
|
+
if (alias && (0, import_luaut_parser4.isClassType)(alias)) return classText(analysis, qualified);
|
|
931
|
+
if (alias) return `type ${qualified} = ${pretty(alias)}`;
|
|
728
932
|
}
|
|
729
933
|
const type2 = typeOfNode(node);
|
|
730
934
|
return type2 && `type ${referenceText(analysis, node)} = ${pretty(type2)}`;
|
|
@@ -753,6 +957,19 @@ function declareText(analysis, statement) {
|
|
|
753
957
|
const overloads = others > 0 ? ` (+${others} overload${others > 1 ? "s" : ""})` : "";
|
|
754
958
|
return `declare function ${name}${(0, import_luaut_parser4.formatType)(own)}${overloads}`;
|
|
755
959
|
}
|
|
960
|
+
function classText(analysis, name) {
|
|
961
|
+
const type = analysis.types.aliases.get(name);
|
|
962
|
+
if (!type || !(0, import_luaut_parser4.isClassType)(type)) return void 0;
|
|
963
|
+
const superclass = type.class.superclass;
|
|
964
|
+
const inherited = superclass ? analysis.types.aliases.get(superclass) : void 0;
|
|
965
|
+
const own = [...type.properties].filter(([key, property]) => inherited?.kind !== "object" || inherited.properties.get(key) !== property);
|
|
966
|
+
const head = `declare class ${name}${superclass ? ` extends ${superclass}` : ""}`;
|
|
967
|
+
if (!own.length) return `${head} {}`;
|
|
968
|
+
const lines = own.map(([key, property]) => ` ${property.readonly ? "readonly " : ""}${key}${property.optional ? "?" : ""}: ${(0, import_luaut_parser4.formatType)(property.type)},`);
|
|
969
|
+
return `${head} {
|
|
970
|
+
${lines.join("\n")}
|
|
971
|
+
}`;
|
|
972
|
+
}
|
|
756
973
|
function typeParameterText(analysis, parameter) {
|
|
757
974
|
return `(type parameter) ${typeParameterSignature(analysis, parameter)}`;
|
|
758
975
|
}
|
|
@@ -917,10 +1134,10 @@ function completion(analyzer, document, position) {
|
|
|
917
1134
|
}
|
|
918
1135
|
if (operator || !first) return [];
|
|
919
1136
|
if (inTypePosition(first.path)) {
|
|
920
|
-
const named = [...first.analysis.types.aliases
|
|
1137
|
+
const named = [...first.analysis.types.aliases].map(([name, type]) => ({
|
|
921
1138
|
label: name,
|
|
922
|
-
kind: import_vscode_languageserver4.CompletionItemKind.Interface,
|
|
923
|
-
detail: "type"
|
|
1139
|
+
kind: (0, import_luaut_parser5.isClassType)(type) ? import_vscode_languageserver4.CompletionItemKind.Class : import_vscode_languageserver4.CompletionItemKind.Interface,
|
|
1140
|
+
detail: (0, import_luaut_parser5.isClassType)(type) ? "class" : "type"
|
|
924
1141
|
}));
|
|
925
1142
|
const primitives = PRIMITIVES2.map((name) => ({
|
|
926
1143
|
label: name,
|
|
@@ -1052,7 +1269,7 @@ function kindOf(type, bindingKind) {
|
|
|
1052
1269
|
}
|
|
1053
1270
|
function inTypePosition(path) {
|
|
1054
1271
|
return path.some(
|
|
1055
|
-
(n) => !!n.type && (n.type.endsWith("TypeNode") || n.type === "TypeReference" || n.type === "TypeAliasStatement" || n.type === "ExportTypeAliasStatement")
|
|
1272
|
+
(n) => !!n.type && (n.type.endsWith("TypeNode") || n.type === "TypeReference" || n.type === "TypeAliasStatement" || n.type === "ExportTypeAliasStatement" || n.type === "DeclareClassStatement")
|
|
1056
1273
|
);
|
|
1057
1274
|
}
|
|
1058
1275
|
var PRIMITIVES2 = [
|
|
@@ -1193,6 +1410,12 @@ function documentSymbols(analysis) {
|
|
|
1193
1410
|
}
|
|
1194
1411
|
break;
|
|
1195
1412
|
}
|
|
1413
|
+
case "DeclareClassStatement": {
|
|
1414
|
+
const name = node.name.name;
|
|
1415
|
+
const superclass = node.superclass?.base;
|
|
1416
|
+
out.push(symbol(name, import_vscode_languageserver5.SymbolKind.Class, node, superclass && `extends ${superclass}`));
|
|
1417
|
+
break;
|
|
1418
|
+
}
|
|
1196
1419
|
case "VariableDeclaration": {
|
|
1197
1420
|
for (const target of node.names ?? []) {
|
|
1198
1421
|
const name = target.name;
|
|
@@ -1234,6 +1457,7 @@ var import_luaut_parser7 = require("luaut-parser");
|
|
|
1234
1457
|
var TOKEN_TYPES = [
|
|
1235
1458
|
"namespace",
|
|
1236
1459
|
"type",
|
|
1460
|
+
"class",
|
|
1237
1461
|
"typeParameter",
|
|
1238
1462
|
"parameter",
|
|
1239
1463
|
"variable",
|
|
@@ -1250,6 +1474,7 @@ var semanticTokensLegend = {
|
|
|
1250
1474
|
var SOFT_KEYWORDS = /* @__PURE__ */ new Set([
|
|
1251
1475
|
"type",
|
|
1252
1476
|
"declare",
|
|
1477
|
+
"class",
|
|
1253
1478
|
"extends",
|
|
1254
1479
|
"keyof",
|
|
1255
1480
|
"infer",
|
|
@@ -1316,6 +1541,8 @@ function classify(analysis, spanned, ancestors, identifiers, add) {
|
|
|
1316
1541
|
if (!baseToken) return;
|
|
1317
1542
|
if (!namespace && typeParameterInScope2(ancestors, base)) {
|
|
1318
1543
|
add(baseToken, base.length, "typeParameter");
|
|
1544
|
+
} else if ((0, import_luaut_parser7.isClassType)(analysis.types.aliases.get(namespace ? `${namespace}.${base}` : base) ?? import_luaut_parser7.unknownType)) {
|
|
1545
|
+
add(baseToken, base.length, "class");
|
|
1319
1546
|
} else {
|
|
1320
1547
|
add(baseToken, base.length, "type", PRIMITIVES3.has(base) ? ["defaultLibrary"] : []);
|
|
1321
1548
|
}
|
|
@@ -1343,6 +1570,9 @@ function identifier(analysis, node, parent, add) {
|
|
|
1343
1570
|
case "ExportTypeAliasStatement":
|
|
1344
1571
|
if (parent.name === node) return as("type", ["declaration"]);
|
|
1345
1572
|
break;
|
|
1573
|
+
case "DeclareClassStatement":
|
|
1574
|
+
if (parent.name === node) return as("class", ["declaration"]);
|
|
1575
|
+
break;
|
|
1346
1576
|
case "DeclareStatement":
|
|
1347
1577
|
if (parent.id === node) return as(isFunction(typeOfNode(parent.valueType)) ? "function" : "variable", ["declaration"]);
|
|
1348
1578
|
break;
|
|
@@ -1493,17 +1723,56 @@ function createServer(connection, options = {}) {
|
|
|
1493
1723
|
const document = documents.get(p.textDocument.uri);
|
|
1494
1724
|
return document ? semanticTokens(analyzer.get(document)) : { data: [] };
|
|
1495
1725
|
});
|
|
1496
|
-
|
|
1497
|
-
void connection.sendDiagnostics({
|
|
1498
|
-
uri: document.uri,
|
|
1499
|
-
version: document.version,
|
|
1500
|
-
diagnostics: diagnostics(analyzer.get(document))
|
|
1501
|
-
});
|
|
1502
|
-
};
|
|
1503
|
-
documents.onDidOpen((e) => publish(e.document));
|
|
1726
|
+
let configUris = /* @__PURE__ */ new Set();
|
|
1504
1727
|
const publishAll = () => {
|
|
1505
|
-
|
|
1728
|
+
const problems = /* @__PURE__ */ new Map();
|
|
1729
|
+
for (const document of documents.all()) {
|
|
1730
|
+
const analysis = analyzer.get(document);
|
|
1731
|
+
void connection.sendDiagnostics({
|
|
1732
|
+
uri: document.uri,
|
|
1733
|
+
version: document.version,
|
|
1734
|
+
diagnostics: [...diagnostics(analysis), ...projectHint(analysis)]
|
|
1735
|
+
});
|
|
1736
|
+
for (const problem of analysis.project.problems) {
|
|
1737
|
+
const uri = uriOfPath(problem.file);
|
|
1738
|
+
const list = problems.get(uri) ?? [];
|
|
1739
|
+
if (!list.some((p) => p.message === problem.message && p.line === problem.line)) list.push(problem);
|
|
1740
|
+
problems.set(uri, list);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
for (const [uri, list] of problems) {
|
|
1744
|
+
void connection.sendDiagnostics({ uri, diagnostics: list.map(problemDiagnostic) });
|
|
1745
|
+
}
|
|
1746
|
+
for (const uri of configUris) {
|
|
1747
|
+
if (!problems.has(uri)) void connection.sendDiagnostics({ uri, diagnostics: [] });
|
|
1748
|
+
}
|
|
1749
|
+
configUris = new Set(problems.keys());
|
|
1750
|
+
};
|
|
1751
|
+
const problemDiagnostic = (problem) => {
|
|
1752
|
+
const line = Math.max((problem.line ?? 1) - 1, 0);
|
|
1753
|
+
const character = Math.max((problem.column ?? 1) - 1, 0);
|
|
1754
|
+
const text = analyzer.readFile(problem.file)?.split("\n")[line] ?? "";
|
|
1755
|
+
const end = Math.max(text.replace(/\r$/, "").trimEnd().length, character + 1);
|
|
1756
|
+
return {
|
|
1757
|
+
range: { start: { line, character }, end: { line, character: end } },
|
|
1758
|
+
severity: import_node.DiagnosticSeverity.Error,
|
|
1759
|
+
source: "luaut",
|
|
1760
|
+
code: "config",
|
|
1761
|
+
message: problem.message
|
|
1762
|
+
};
|
|
1763
|
+
};
|
|
1764
|
+
const projectHint = (analysis) => {
|
|
1765
|
+
const { project } = analysis;
|
|
1766
|
+
if (project.fixed || project.config || !pathOfUri(analysis.uri)) return [];
|
|
1767
|
+
return [{
|
|
1768
|
+
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
|
|
1769
|
+
severity: import_node.DiagnosticSeverity.Information,
|
|
1770
|
+
source: "luaut",
|
|
1771
|
+
code: "no-config",
|
|
1772
|
+
message: 'No luaut.config.json applies to this file, so no types are loaded \u2014 not even `print`. Add one to this folder or a folder above, such as { "types": ["luau"], "paths": {}, "sourceMap": null }'
|
|
1773
|
+
}];
|
|
1506
1774
|
};
|
|
1775
|
+
documents.onDidOpen(publishAll);
|
|
1507
1776
|
documents.onDidChangeContent(publishAll);
|
|
1508
1777
|
connection.onDidChangeWatchedFiles(publishAll);
|
|
1509
1778
|
documents.onDidClose((e) => {
|