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
|
@@ -1,24 +1,90 @@
|
|
|
1
|
+
// src/features/members.ts
|
|
2
|
+
import { formatType } from "luaut-parser";
|
|
3
|
+
function membersOf(type, aliases, seen = /* @__PURE__ */ new Set()) {
|
|
4
|
+
if (!type || seen.has(type)) return [];
|
|
5
|
+
seen.add(type);
|
|
6
|
+
switch (type.kind) {
|
|
7
|
+
case "object": {
|
|
8
|
+
const out = [];
|
|
9
|
+
for (const [name, property] of type.properties) {
|
|
10
|
+
out.push({ name, property, isMethod: takesSelf(property.type) });
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
case "intersection": {
|
|
15
|
+
const merged = /* @__PURE__ */ new Map();
|
|
16
|
+
for (const part of type.types) {
|
|
17
|
+
for (const member of membersOf(part, aliases, seen)) merged.set(member.name, member);
|
|
18
|
+
}
|
|
19
|
+
return [...merged.values()];
|
|
20
|
+
}
|
|
21
|
+
case "union": {
|
|
22
|
+
const perBranch = type.types.map((part) => membersOf(part, aliases, seen));
|
|
23
|
+
if (!perBranch.length) return [];
|
|
24
|
+
const [first, ...rest] = perBranch;
|
|
25
|
+
return first.filter((member) => rest.every((other) => other.some((m) => m.name === member.name)));
|
|
26
|
+
}
|
|
27
|
+
case "genericRef": {
|
|
28
|
+
const alias = aliases.get(type.name);
|
|
29
|
+
return alias ? membersOf(alias, aliases, seen) : [];
|
|
30
|
+
}
|
|
31
|
+
case "typeParam":
|
|
32
|
+
return membersOf(type.constraint, aliases, seen);
|
|
33
|
+
default:
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function takesSelf(type) {
|
|
38
|
+
for (const signature of signaturesOf(type)) {
|
|
39
|
+
if (signature.params[0]?.name === "self") return true;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
function signaturesOf(type, aliases) {
|
|
44
|
+
if (!type) return [];
|
|
45
|
+
if (type.kind === "function") return [type];
|
|
46
|
+
if (type.kind === "intersection") return type.types.flatMap((t) => signaturesOf(t, aliases));
|
|
47
|
+
if (type.kind === "genericRef" && aliases) {
|
|
48
|
+
const alias = aliases.get(type.name);
|
|
49
|
+
return alias ? signaturesOf(alias, aliases) : [];
|
|
50
|
+
}
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
function signatureLabel(signature) {
|
|
54
|
+
const parameters = signature.params.map((p, i) => {
|
|
55
|
+
const name = p.name ?? `arg${i + 1}`;
|
|
56
|
+
return `${name}${p.optional ? "?" : ""}: ${formatType(p.type)}`;
|
|
57
|
+
});
|
|
58
|
+
const generics = signature.typeParams?.length ? `<${signature.typeParams.join(", ")}>` : "";
|
|
59
|
+
const varargs = signature.varargs ? [`...: ${formatType(signature.varargs)}`] : [];
|
|
60
|
+
const label = `${generics}(${[...parameters, ...varargs].join(", ")}) -> ${formatType(signature.returns)}`;
|
|
61
|
+
return { label, parameters };
|
|
62
|
+
}
|
|
63
|
+
|
|
1
64
|
// src/analysis.ts
|
|
2
65
|
import { readFileSync, statSync } from "fs";
|
|
3
|
-
import { dirname,
|
|
66
|
+
import { dirname, resolve } from "path";
|
|
4
67
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
5
68
|
import {
|
|
69
|
+
parse,
|
|
6
70
|
parseWithRecovery,
|
|
7
71
|
analyzeScopes,
|
|
8
72
|
analyzeTypes,
|
|
9
73
|
moduleExports,
|
|
10
|
-
|
|
11
|
-
|
|
74
|
+
getBinding,
|
|
75
|
+
findConfig,
|
|
76
|
+
resolveTypeLibraries,
|
|
77
|
+
moduleCandidates,
|
|
78
|
+
sourceMapTypes
|
|
12
79
|
} from "luaut-parser";
|
|
13
80
|
function globalsOf(libs) {
|
|
14
81
|
const names = /* @__PURE__ */ new Set();
|
|
15
|
-
for (const lib of libs)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
for (const statement of statements) {
|
|
20
|
-
if (statement.type === "DeclareStatement") into.add(statement.name);
|
|
82
|
+
for (const lib of libs) {
|
|
83
|
+
for (const statement of lib.body.statements) {
|
|
84
|
+
if (statement.type === "DeclareStatement") names.add(statement.name);
|
|
85
|
+
}
|
|
21
86
|
}
|
|
87
|
+
return [...names];
|
|
22
88
|
}
|
|
23
89
|
function bindingOfNode(analysis, node) {
|
|
24
90
|
const used = getBinding(analysis.scopes, node);
|
|
@@ -56,20 +122,34 @@ function pathKey(path) {
|
|
|
56
122
|
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
57
123
|
}
|
|
58
124
|
var CYCLE = { values: /* @__PURE__ */ new Map(), types: /* @__PURE__ */ new Map(), partial: true };
|
|
125
|
+
var NO_PROJECT = { project: { fixed: false, problems: [] }, libs: [], globals: [], reads: /* @__PURE__ */ new Map() };
|
|
59
126
|
var Analyzer = class {
|
|
60
|
-
|
|
61
|
-
builtinGlobals;
|
|
127
|
+
fixed;
|
|
62
128
|
openDocument;
|
|
63
129
|
cache = /* @__PURE__ */ new Map();
|
|
64
130
|
/** Imported modules, by path key. */
|
|
65
131
|
modules = /* @__PURE__ */ new Map();
|
|
132
|
+
/** Project contexts, by folder. */
|
|
133
|
+
contexts = /* @__PURE__ */ new Map();
|
|
134
|
+
/** Parsed type libraries, by path — reparsed only when the text changes. */
|
|
135
|
+
libraries = /* @__PURE__ */ new Map();
|
|
136
|
+
/** Sourcemaps turned into types, by path, with what they were built from. */
|
|
137
|
+
sourceMaps = /* @__PURE__ */ new Map();
|
|
138
|
+
/** The analysis run in progress, if any. */
|
|
139
|
+
run;
|
|
66
140
|
constructor(options = {}) {
|
|
67
|
-
this.libs = options.libs ?? defaultLibs;
|
|
68
|
-
this.builtinGlobals = globalsOf(this.libs);
|
|
69
141
|
this.openDocument = options.openDocument;
|
|
142
|
+
if (options.libs) {
|
|
143
|
+
this.fixed = {
|
|
144
|
+
project: { fixed: true, problems: [] },
|
|
145
|
+
libs: options.libs,
|
|
146
|
+
globals: globalsOf(options.libs),
|
|
147
|
+
reads: /* @__PURE__ */ new Map()
|
|
148
|
+
};
|
|
149
|
+
}
|
|
70
150
|
}
|
|
71
151
|
/** Analyze `document`, reusing the previous result while neither it nor
|
|
72
|
-
* anything it
|
|
152
|
+
* anything it read has changed. */
|
|
73
153
|
get(document) {
|
|
74
154
|
const cached = this.cache.get(document.uri);
|
|
75
155
|
const source = document.getText();
|
|
@@ -84,34 +164,41 @@ var Analyzer = class {
|
|
|
84
164
|
* completion, which analyzes a speculatively edited copy of the file. */
|
|
85
165
|
analyze(uri, version, source) {
|
|
86
166
|
const path = pathOfUri(uri);
|
|
87
|
-
return this.analyzeModule(uri, version, source, new Set(
|
|
167
|
+
if (!path) return this.analyzeModule(uri, version, source, /* @__PURE__ */ new Set());
|
|
168
|
+
const key = pathKey(path);
|
|
169
|
+
return this.resolvingCycles(key, () => {
|
|
170
|
+
const analysis = this.analyzeModule(uri, version, source, /* @__PURE__ */ new Set([key]));
|
|
171
|
+
return { result: analysis, exports: () => this.exportsFrom(analysis, /* @__PURE__ */ new Set([key])) };
|
|
172
|
+
});
|
|
88
173
|
}
|
|
89
174
|
forget(uri) {
|
|
90
175
|
this.cache.delete(uri);
|
|
91
176
|
}
|
|
92
|
-
/** The
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
resolveModulePath(fromUri, specifier) {
|
|
96
|
-
return this.moduleCandidates(fromUri, specifier).find((candidate) => this.sourceOf(candidate) !== void 0);
|
|
177
|
+
/** The project a file belongs to. */
|
|
178
|
+
projectOf(uri) {
|
|
179
|
+
return this.contextFor(pathOfUri(uri)).project;
|
|
97
180
|
}
|
|
98
|
-
/**
|
|
99
|
-
|
|
181
|
+
/** The file an import in `fromUri` names: a relative path, or a `paths`
|
|
182
|
+
* alias from the file's config. */
|
|
183
|
+
resolveModulePath(fromUri, specifier) {
|
|
100
184
|
const from = pathOfUri(fromUri);
|
|
101
|
-
if (!from
|
|
102
|
-
|
|
103
|
-
return specifier.endsWith(".luaut") ? [base] : [`${base}.luaut`, `${base}.d.luaut`, join(base, "index.luaut")];
|
|
185
|
+
if (!from) return void 0;
|
|
186
|
+
return this.candidatesFor(from, specifier).find((candidate) => this.readFile(candidate) !== void 0);
|
|
104
187
|
}
|
|
105
188
|
/** What the module at `path` exports, analyzing it if need be. */
|
|
106
189
|
exportsAt(path) {
|
|
107
|
-
return this.
|
|
190
|
+
return this.resolvingCycles(pathKey(path), () => {
|
|
191
|
+
const exports = this.exportsOf(path, /* @__PURE__ */ new Set());
|
|
192
|
+
return { result: exports, exports: () => exports };
|
|
193
|
+
});
|
|
108
194
|
}
|
|
109
195
|
/** The analysis of the module at `path`, analyzing it if need be. */
|
|
110
196
|
moduleAt(path) {
|
|
111
|
-
this.
|
|
197
|
+
this.exportsAt(path);
|
|
112
198
|
return this.modules.get(pathKey(path))?.analysis;
|
|
113
199
|
}
|
|
114
|
-
|
|
200
|
+
/** A file's text: the open document if there is one, else the disk. */
|
|
201
|
+
readFile(path) {
|
|
115
202
|
const open = this.openDocument?.(path);
|
|
116
203
|
if (open) return open.getText();
|
|
117
204
|
try {
|
|
@@ -120,60 +207,206 @@ var Analyzer = class {
|
|
|
120
207
|
return void 0;
|
|
121
208
|
}
|
|
122
209
|
}
|
|
210
|
+
candidatesFor(from, specifier) {
|
|
211
|
+
return moduleCandidates(from, specifier, this.contextFor(from).project.config);
|
|
212
|
+
}
|
|
213
|
+
// ---------------------------------------------------------------- projects
|
|
214
|
+
contextFor(path) {
|
|
215
|
+
if (this.fixed) return this.fixed;
|
|
216
|
+
if (!path) return NO_PROJECT;
|
|
217
|
+
const key = pathKey(dirname(path));
|
|
218
|
+
const cached = this.contexts.get(key);
|
|
219
|
+
if (cached && this.unchanged(cached.reads)) return cached;
|
|
220
|
+
const context = this.buildContext(path);
|
|
221
|
+
this.contexts.set(key, context);
|
|
222
|
+
return context;
|
|
223
|
+
}
|
|
224
|
+
buildContext(path) {
|
|
225
|
+
const reads = /* @__PURE__ */ new Map();
|
|
226
|
+
const host = {
|
|
227
|
+
readFile: (file) => {
|
|
228
|
+
const text = this.readFile(file);
|
|
229
|
+
reads.set(file, text);
|
|
230
|
+
return text;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
const lookup = findConfig(path, host);
|
|
234
|
+
const problems = [...lookup.problems];
|
|
235
|
+
const config = lookup.config;
|
|
236
|
+
if (!config) return { project: { fixed: false, problems }, libs: [], globals: [], reads };
|
|
237
|
+
const libraries = resolveTypeLibraries(config, host);
|
|
238
|
+
problems.push(...libraries.problems);
|
|
239
|
+
const libs = [];
|
|
240
|
+
for (const file of libraries.files) {
|
|
241
|
+
const program = this.library(file, host, problems);
|
|
242
|
+
if (program) libs.push(program);
|
|
243
|
+
}
|
|
244
|
+
let sourceMap;
|
|
245
|
+
if (config.sourceMap) {
|
|
246
|
+
const text = host.readFile(config.sourceMap);
|
|
247
|
+
if (text === void 0) {
|
|
248
|
+
problems.push({
|
|
249
|
+
file: config.path,
|
|
250
|
+
message: `Cannot find the sourceMap file ${config.sourceMap}`,
|
|
251
|
+
...optionPosition(config, "sourceMap")
|
|
252
|
+
});
|
|
253
|
+
} else {
|
|
254
|
+
const result = this.sourceMap(config.sourceMap, text, libs, libraries.files);
|
|
255
|
+
if (result.problem) problems.push({ file: config.sourceMap, message: result.problem, line: 1, column: 1 });
|
|
256
|
+
sourceMap = result.types;
|
|
257
|
+
if (sourceMap) libs.push(sourceMap.program);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return { project: { config, fixed: false, problems }, libs, globals: globalsOf(libs), sourceMap, reads };
|
|
261
|
+
}
|
|
262
|
+
/** A type library's definitions, parsed once per text. */
|
|
263
|
+
library(file, host, problems) {
|
|
264
|
+
const source = host.readFile(file);
|
|
265
|
+
if (source === void 0) return void 0;
|
|
266
|
+
const key = pathKey(file);
|
|
267
|
+
let entry = this.libraries.get(key);
|
|
268
|
+
if (!entry || entry.source !== source) {
|
|
269
|
+
try {
|
|
270
|
+
entry = { source, program: parse(source) };
|
|
271
|
+
} catch (error) {
|
|
272
|
+
const { message, line, column } = error;
|
|
273
|
+
entry = {
|
|
274
|
+
source,
|
|
275
|
+
problem: { file, message: `Syntax error in type library: ${message.replace(/\s*\(\d+:\d+\)$/, "")}`, line, column }
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
this.libraries.set(key, entry);
|
|
279
|
+
}
|
|
280
|
+
if (entry.problem) problems.push(entry.problem);
|
|
281
|
+
return entry.program;
|
|
282
|
+
}
|
|
283
|
+
/** A sourcemap's types, rebuilt only when it or the libraries change. */
|
|
284
|
+
sourceMap(path, text, libs, files) {
|
|
285
|
+
const key = pathKey(path);
|
|
286
|
+
const libraries = files.join("\n");
|
|
287
|
+
const cached = this.sourceMaps.get(key);
|
|
288
|
+
if (cached && cached.text === text && cached.libraries === libraries) return cached.result;
|
|
289
|
+
const aliases = aliasesOf(libs);
|
|
290
|
+
const members = /* @__PURE__ */ new Map();
|
|
291
|
+
const result = sourceMapTypes(text, path, {
|
|
292
|
+
classes: new Set(aliases.keys()),
|
|
293
|
+
membersOf: (className) => {
|
|
294
|
+
let names = members.get(className);
|
|
295
|
+
if (!names) {
|
|
296
|
+
names = new Set(membersOf(aliases.get(className), aliases).map((member) => member.name));
|
|
297
|
+
members.set(className, names);
|
|
298
|
+
}
|
|
299
|
+
return names;
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
this.sourceMaps.set(key, { text, libraries, result });
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
unchanged(reads) {
|
|
306
|
+
for (const [file, text] of reads) if (this.readFile(file) !== text) return false;
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
// ----------------------------------------------------------------- modules
|
|
310
|
+
/** Run one analysis of `root` and everything it imports; if that met an
|
|
311
|
+
* import cycle, run it once more with the first pass's exports standing
|
|
312
|
+
* in for the `any` the cycle left (see `Run`). A call made while a run is
|
|
313
|
+
* already going is part of that run. */
|
|
314
|
+
resolvingCycles(root, analyzeRoot) {
|
|
315
|
+
if (this.run) return analyzeRoot().result;
|
|
316
|
+
const run = { cycles: /* @__PURE__ */ new Set(), analyzed: /* @__PURE__ */ new Set(), provisional: /* @__PURE__ */ new Map() };
|
|
317
|
+
this.run = run;
|
|
318
|
+
try {
|
|
319
|
+
const first = analyzeRoot();
|
|
320
|
+
if (!run.cycles.size) return first.result;
|
|
321
|
+
for (const key of run.cycles) {
|
|
322
|
+
const exports = key === root ? first.exports() : this.modules.get(key)?.exports;
|
|
323
|
+
if (exports && !exports.partial) run.provisional.set(key, exports);
|
|
324
|
+
}
|
|
325
|
+
for (const key of run.analyzed) this.modules.delete(key);
|
|
326
|
+
return analyzeRoot().result;
|
|
327
|
+
} finally {
|
|
328
|
+
this.run = void 0;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/** A module's exports, from its analysis. */
|
|
332
|
+
exportsFrom(analysis, importing) {
|
|
333
|
+
return moduleExports(analysis.program, analysis.scopes, analysis.types, (specifier) => {
|
|
334
|
+
const next = this.resolveModulePath(analysis.uri, specifier);
|
|
335
|
+
return next ? this.exportsOf(next, importing) : void 0;
|
|
336
|
+
});
|
|
337
|
+
}
|
|
123
338
|
/** `importing` holds every module on the current import chain, so an
|
|
124
339
|
* import back into one of them is recognized as a cycle. */
|
|
125
340
|
analyzeModule(uri, version, source, importing) {
|
|
341
|
+
const path = pathOfUri(uri);
|
|
342
|
+
const context = this.contextFor(path);
|
|
343
|
+
const script = path ? context.sourceMap?.scriptFor(path) : void 0;
|
|
344
|
+
const libs = script ? [...context.libs, script] : context.libs;
|
|
345
|
+
const globals = script ? [...context.globals, "script"] : context.globals;
|
|
126
346
|
const { program, errors } = parseWithRecovery(source);
|
|
127
|
-
const scopes = analyzeScopes(program, { builtinGlobals:
|
|
128
|
-
const dependencies =
|
|
347
|
+
const scopes = analyzeScopes(program, { builtinGlobals: [...globals] });
|
|
348
|
+
const dependencies = new Map(context.reads);
|
|
129
349
|
const types = analyzeTypes(program, scopes, {
|
|
130
|
-
libs
|
|
350
|
+
libs,
|
|
131
351
|
resolveModule: (specifier) => {
|
|
132
|
-
|
|
352
|
+
if (!path) return void 0;
|
|
353
|
+
const candidates = this.candidatesFor(path, specifier);
|
|
354
|
+
const target = candidates.find((candidate) => this.readFile(candidate) !== void 0);
|
|
133
355
|
if (!target) {
|
|
134
|
-
for (const candidate of
|
|
356
|
+
for (const candidate of candidates) dependencies.set(candidate, void 0);
|
|
135
357
|
return void 0;
|
|
136
358
|
}
|
|
137
359
|
const exports = this.exportsOf(target, importing);
|
|
138
|
-
dependencies.set(target, this.
|
|
360
|
+
dependencies.set(target, this.readFile(target));
|
|
139
361
|
return exports;
|
|
140
362
|
}
|
|
141
363
|
});
|
|
142
|
-
return { uri, version, source, program, parseErrors: errors, scopes, types, dependencies };
|
|
364
|
+
return { uri, version, source, program, parseErrors: errors, scopes, types, dependencies, project: context.project };
|
|
143
365
|
}
|
|
144
366
|
exportsOf(path, importing) {
|
|
145
367
|
const key = pathKey(path);
|
|
146
|
-
if (importing.has(key))
|
|
147
|
-
|
|
368
|
+
if (importing.has(key)) {
|
|
369
|
+
this.run?.cycles.add(key);
|
|
370
|
+
return this.run?.provisional.get(key) ?? CYCLE;
|
|
371
|
+
}
|
|
372
|
+
const source = this.readFile(path);
|
|
148
373
|
if (source === void 0) return void 0;
|
|
149
374
|
const cached = this.modules.get(key);
|
|
150
375
|
if (cached && cached.analysis.source === source && this.isFresh(cached.analysis)) return cached.exports;
|
|
151
376
|
importing.add(key);
|
|
152
377
|
try {
|
|
153
378
|
const analysis = this.analyzeModule(uriOfPath(path), -1, source, importing);
|
|
154
|
-
const exports =
|
|
155
|
-
const next = this.resolveModulePath(analysis.uri, specifier);
|
|
156
|
-
return next ? this.exportsOf(next, importing) : void 0;
|
|
157
|
-
});
|
|
379
|
+
const exports = this.exportsFrom(analysis, importing);
|
|
158
380
|
this.modules.set(key, { analysis, exports });
|
|
381
|
+
this.run?.analyzed.add(key);
|
|
159
382
|
return exports;
|
|
160
383
|
} finally {
|
|
161
384
|
importing.delete(key);
|
|
162
385
|
}
|
|
163
386
|
}
|
|
164
|
-
/** Does every
|
|
165
|
-
* still have the text it was analyzed against? */
|
|
387
|
+
/** Does every file `analysis` read — and everything the modules it
|
|
388
|
+
* imported read — still have the text it was analyzed against? */
|
|
166
389
|
isFresh(analysis, seen = /* @__PURE__ */ new Set()) {
|
|
167
390
|
if (seen.has(analysis)) return true;
|
|
168
391
|
seen.add(analysis);
|
|
169
392
|
for (const [path, source] of analysis.dependencies) {
|
|
170
|
-
if (this.
|
|
393
|
+
if (this.readFile(path) !== source) return false;
|
|
171
394
|
const module = this.modules.get(pathKey(path));
|
|
172
395
|
if (module && !this.isFresh(module.analysis, seen)) return false;
|
|
173
396
|
}
|
|
174
397
|
return true;
|
|
175
398
|
}
|
|
176
399
|
};
|
|
400
|
+
function aliasesOf(libs) {
|
|
401
|
+
const empty = parse("");
|
|
402
|
+
return analyzeTypes(empty, analyzeScopes(empty, {}), { libs, diagnostics: false }).aliases;
|
|
403
|
+
}
|
|
404
|
+
function optionPosition(config, key) {
|
|
405
|
+
const offset = config.source.indexOf(JSON.stringify(key));
|
|
406
|
+
if (offset < 0) return { line: 1, column: 1 };
|
|
407
|
+
const before = config.source.slice(0, offset);
|
|
408
|
+
return { line: before.split("\n").length, column: offset - before.lastIndexOf("\n") };
|
|
409
|
+
}
|
|
177
410
|
|
|
178
411
|
// src/ast-utils.ts
|
|
179
412
|
function isSpanned(v) {
|
|
@@ -203,16 +436,16 @@ function containsPosition(node, pos, inclusive = false) {
|
|
|
203
436
|
}
|
|
204
437
|
function children(node) {
|
|
205
438
|
const out = [];
|
|
206
|
-
|
|
439
|
+
collect(node, out);
|
|
207
440
|
return out;
|
|
208
441
|
}
|
|
209
|
-
function
|
|
442
|
+
function collect(container, out) {
|
|
210
443
|
for (const key of Object.keys(container)) {
|
|
211
444
|
if (key === "line" || key === "column") continue;
|
|
212
445
|
const value = container[key];
|
|
213
446
|
for (const item of Array.isArray(value) ? value : [value]) {
|
|
214
447
|
if (isSpanned(item)) out.push(item);
|
|
215
|
-
else if (isSpanlessNode(item))
|
|
448
|
+
else if (isSpanlessNode(item)) collect(item, out);
|
|
216
449
|
}
|
|
217
450
|
}
|
|
218
451
|
}
|
|
@@ -253,69 +486,6 @@ function walk(root, visit, parent) {
|
|
|
253
486
|
for (const child of children(root)) walk(child, visit, root);
|
|
254
487
|
}
|
|
255
488
|
|
|
256
|
-
// src/features/members.ts
|
|
257
|
-
import { formatType } from "luaut-parser";
|
|
258
|
-
function membersOf(type, aliases, seen = /* @__PURE__ */ new Set()) {
|
|
259
|
-
if (!type || seen.has(type)) return [];
|
|
260
|
-
seen.add(type);
|
|
261
|
-
switch (type.kind) {
|
|
262
|
-
case "object": {
|
|
263
|
-
const out = [];
|
|
264
|
-
for (const [name, property] of type.properties) {
|
|
265
|
-
out.push({ name, property, isMethod: takesSelf(property.type) });
|
|
266
|
-
}
|
|
267
|
-
return out;
|
|
268
|
-
}
|
|
269
|
-
case "intersection": {
|
|
270
|
-
const merged = /* @__PURE__ */ new Map();
|
|
271
|
-
for (const part of type.types) {
|
|
272
|
-
for (const member of membersOf(part, aliases, seen)) merged.set(member.name, member);
|
|
273
|
-
}
|
|
274
|
-
return [...merged.values()];
|
|
275
|
-
}
|
|
276
|
-
case "union": {
|
|
277
|
-
const perBranch = type.types.map((part) => membersOf(part, aliases, seen));
|
|
278
|
-
if (!perBranch.length) return [];
|
|
279
|
-
const [first, ...rest] = perBranch;
|
|
280
|
-
return first.filter((member) => rest.every((other) => other.some((m) => m.name === member.name)));
|
|
281
|
-
}
|
|
282
|
-
case "genericRef": {
|
|
283
|
-
const alias = aliases.get(type.name);
|
|
284
|
-
return alias ? membersOf(alias, aliases, seen) : [];
|
|
285
|
-
}
|
|
286
|
-
case "typeParam":
|
|
287
|
-
return membersOf(type.constraint, aliases, seen);
|
|
288
|
-
default:
|
|
289
|
-
return [];
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
function takesSelf(type) {
|
|
293
|
-
for (const signature of signaturesOf(type)) {
|
|
294
|
-
if (signature.params[0]?.name === "self") return true;
|
|
295
|
-
}
|
|
296
|
-
return false;
|
|
297
|
-
}
|
|
298
|
-
function signaturesOf(type, aliases) {
|
|
299
|
-
if (!type) return [];
|
|
300
|
-
if (type.kind === "function") return [type];
|
|
301
|
-
if (type.kind === "intersection") return type.types.flatMap((t) => signaturesOf(t, aliases));
|
|
302
|
-
if (type.kind === "genericRef" && aliases) {
|
|
303
|
-
const alias = aliases.get(type.name);
|
|
304
|
-
return alias ? signaturesOf(alias, aliases) : [];
|
|
305
|
-
}
|
|
306
|
-
return [];
|
|
307
|
-
}
|
|
308
|
-
function signatureLabel(signature) {
|
|
309
|
-
const parameters = signature.params.map((p, i) => {
|
|
310
|
-
const name = p.name ?? `arg${i + 1}`;
|
|
311
|
-
return `${name}${p.optional ? "?" : ""}: ${formatType(p.type)}`;
|
|
312
|
-
});
|
|
313
|
-
const generics = signature.typeParams?.length ? `<${signature.typeParams.join(", ")}>` : "";
|
|
314
|
-
const varargs = signature.varargs ? [`...: ${formatType(signature.varargs)}`] : [];
|
|
315
|
-
const label = `${generics}(${[...parameters, ...varargs].join(", ")}) -> ${formatType(signature.returns)}`;
|
|
316
|
-
return { label, parameters };
|
|
317
|
-
}
|
|
318
|
-
|
|
319
489
|
// src/features/imports.ts
|
|
320
490
|
import { readdirSync } from "fs";
|
|
321
491
|
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
@@ -335,7 +505,7 @@ function importCompletion(analyzer, document, position) {
|
|
|
335
505
|
const after = text.slice(cursor, lineEnd);
|
|
336
506
|
if (!/^\s*(?:import|export)\b/.test(before)) return void 0;
|
|
337
507
|
const path = /\bfrom\s*(["'])([^"']*)$/.exec(before);
|
|
338
|
-
if (path) return pathItems(document.uri, position, path[2]);
|
|
508
|
+
if (path) return pathItems(analyzer, document.uri, position, path[2]);
|
|
339
509
|
const braces = /^\s*(import|export)\s+(?:[A-Za-z_][A-Za-z0-9_]*\s*,\s*)?\{[^}]*$/.exec(before);
|
|
340
510
|
if (braces) {
|
|
341
511
|
const module = /\}\s*from\s*(["'])([^"']+)\1/.exec(after);
|
|
@@ -344,21 +514,52 @@ function importCompletion(analyzer, document, position) {
|
|
|
344
514
|
}
|
|
345
515
|
return void 0;
|
|
346
516
|
}
|
|
347
|
-
function pathItems(fromUri, position, typed) {
|
|
517
|
+
function pathItems(analyzer, fromUri, position, typed) {
|
|
348
518
|
const from = pathOfUri(fromUri);
|
|
349
519
|
if (!from) return [];
|
|
350
|
-
if (
|
|
351
|
-
const
|
|
352
|
-
return
|
|
520
|
+
if (typed.startsWith("./") || typed.startsWith("../")) {
|
|
521
|
+
const slash = typed.lastIndexOf("/");
|
|
522
|
+
return entryItems(resolve2(dirname2(from), typed.slice(0, slash + 1)), rangeBack(position, typed.length - slash - 1), from);
|
|
523
|
+
}
|
|
524
|
+
const items = /* @__PURE__ */ new Map();
|
|
525
|
+
const whole = rangeBack(position, typed.length);
|
|
526
|
+
const offer = (label, folder) => {
|
|
527
|
+
if (!label.startsWith(typed) || label === typed) return;
|
|
528
|
+
items.set(label, {
|
|
353
529
|
label,
|
|
354
|
-
kind: CompletionItemKind.Folder,
|
|
355
|
-
textEdit: { range:
|
|
356
|
-
command: SUGGEST_AGAIN
|
|
357
|
-
})
|
|
530
|
+
kind: folder ? CompletionItemKind.Folder : CompletionItemKind.File,
|
|
531
|
+
textEdit: { range: whole, newText: label },
|
|
532
|
+
command: folder ? SUGGEST_AGAIN : void 0
|
|
533
|
+
});
|
|
534
|
+
};
|
|
535
|
+
offer("./", true);
|
|
536
|
+
offer("../", true);
|
|
537
|
+
const config = analyzer.projectOf(fromUri).config;
|
|
538
|
+
for (const [pattern, targets] of Object.entries(config?.paths ?? {})) {
|
|
539
|
+
const star = pattern.indexOf("*");
|
|
540
|
+
if (star < 0) {
|
|
541
|
+
offer(pattern, false);
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
const prefix = pattern.slice(0, star);
|
|
545
|
+
if (!typed.startsWith(prefix)) {
|
|
546
|
+
offer(prefix, true);
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
const rest = typed.slice(prefix.length);
|
|
550
|
+
const slash = rest.lastIndexOf("/");
|
|
551
|
+
const range = rangeBack(position, rest.length - slash - 1);
|
|
552
|
+
for (const target of targets) {
|
|
553
|
+
const cut = target.indexOf("*");
|
|
554
|
+
const head = cut < 0 ? target : target.slice(0, cut);
|
|
555
|
+
for (const item of entryItems(resolve2(config.baseUrl, head + rest.slice(0, slash + 1)), range, from)) {
|
|
556
|
+
items.set(item.label, item);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
358
559
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
560
|
+
return [...items.values()];
|
|
561
|
+
}
|
|
562
|
+
function entryItems(directory, range, from) {
|
|
362
563
|
let entries;
|
|
363
564
|
try {
|
|
364
565
|
entries = readdirSync(directory, { withFileTypes: true });
|
|
@@ -564,7 +765,8 @@ function diagnostics(analysis) {
|
|
|
564
765
|
|
|
565
766
|
// src/features/hover.ts
|
|
566
767
|
import {
|
|
567
|
-
formatType as formatType3
|
|
768
|
+
formatType as formatType3,
|
|
769
|
+
isClassType
|
|
568
770
|
} from "luaut-parser";
|
|
569
771
|
function hover(analysis, position) {
|
|
570
772
|
const path = pathAt(analysis.program, position, true);
|
|
@@ -615,6 +817,9 @@ function describe(analysis, path, index) {
|
|
|
615
817
|
case "DeclareStatement":
|
|
616
818
|
if (parent.id === node) return declareText(analysis, parent);
|
|
617
819
|
break;
|
|
820
|
+
case "DeclareClassStatement":
|
|
821
|
+
if (parent.name === node) return classText(analysis, name);
|
|
822
|
+
break;
|
|
618
823
|
case "TableTypeProperty":
|
|
619
824
|
if (parent.key === node) {
|
|
620
825
|
const type2 = typeOfNode(parent.valueType);
|
|
@@ -671,9 +876,11 @@ function describe(analysis, path, index) {
|
|
|
671
876
|
if (parameter) return typeParameterText(analysis, parameter);
|
|
672
877
|
if (PRIMITIVES.has(base)) return `type ${base}`;
|
|
673
878
|
}
|
|
674
|
-
if (!node.
|
|
675
|
-
const
|
|
676
|
-
|
|
879
|
+
if (!node.typeArguments.length) {
|
|
880
|
+
const qualified = node.namespace ? `${node.namespace}.${base}` : base;
|
|
881
|
+
const alias = types.aliases.get(qualified);
|
|
882
|
+
if (alias && isClassType(alias)) return classText(analysis, qualified);
|
|
883
|
+
if (alias) return `type ${qualified} = ${pretty(alias)}`;
|
|
677
884
|
}
|
|
678
885
|
const type2 = typeOfNode(node);
|
|
679
886
|
return type2 && `type ${referenceText(analysis, node)} = ${pretty(type2)}`;
|
|
@@ -702,6 +909,19 @@ function declareText(analysis, statement) {
|
|
|
702
909
|
const overloads = others > 0 ? ` (+${others} overload${others > 1 ? "s" : ""})` : "";
|
|
703
910
|
return `declare function ${name}${formatType3(own)}${overloads}`;
|
|
704
911
|
}
|
|
912
|
+
function classText(analysis, name) {
|
|
913
|
+
const type = analysis.types.aliases.get(name);
|
|
914
|
+
if (!type || !isClassType(type)) return void 0;
|
|
915
|
+
const superclass = type.class.superclass;
|
|
916
|
+
const inherited = superclass ? analysis.types.aliases.get(superclass) : void 0;
|
|
917
|
+
const own = [...type.properties].filter(([key, property]) => inherited?.kind !== "object" || inherited.properties.get(key) !== property);
|
|
918
|
+
const head = `declare class ${name}${superclass ? ` extends ${superclass}` : ""}`;
|
|
919
|
+
if (!own.length) return `${head} {}`;
|
|
920
|
+
const lines = own.map(([key, property]) => ` ${property.readonly ? "readonly " : ""}${key}${property.optional ? "?" : ""}: ${formatType3(property.type)},`);
|
|
921
|
+
return `${head} {
|
|
922
|
+
${lines.join("\n")}
|
|
923
|
+
}`;
|
|
924
|
+
}
|
|
705
925
|
function typeParameterText(analysis, parameter) {
|
|
706
926
|
return `(type parameter) ${typeParameterSignature(analysis, parameter)}`;
|
|
707
927
|
}
|
|
@@ -837,7 +1057,7 @@ import {
|
|
|
837
1057
|
CompletionItemKind as CompletionItemKind2,
|
|
838
1058
|
InsertTextFormat
|
|
839
1059
|
} from "vscode-languageserver";
|
|
840
|
-
import { formatType as formatType4 } from "luaut-parser";
|
|
1060
|
+
import { formatType as formatType4, isClassType as isClassType2 } from "luaut-parser";
|
|
841
1061
|
var PLACEHOLDER = "__luautCompletion__";
|
|
842
1062
|
var IDENTIFIER_CHAR = /[A-Za-z0-9_]/;
|
|
843
1063
|
function completion(analyzer, document, position) {
|
|
@@ -871,10 +1091,10 @@ function completion(analyzer, document, position) {
|
|
|
871
1091
|
}
|
|
872
1092
|
if (operator || !first) return [];
|
|
873
1093
|
if (inTypePosition(first.path)) {
|
|
874
|
-
const named = [...first.analysis.types.aliases
|
|
1094
|
+
const named = [...first.analysis.types.aliases].map(([name, type]) => ({
|
|
875
1095
|
label: name,
|
|
876
|
-
kind: CompletionItemKind2.Interface,
|
|
877
|
-
detail: "type"
|
|
1096
|
+
kind: isClassType2(type) ? CompletionItemKind2.Class : CompletionItemKind2.Interface,
|
|
1097
|
+
detail: isClassType2(type) ? "class" : "type"
|
|
878
1098
|
}));
|
|
879
1099
|
const primitives = PRIMITIVES2.map((name) => ({
|
|
880
1100
|
label: name,
|
|
@@ -1006,7 +1226,7 @@ function kindOf(type, bindingKind) {
|
|
|
1006
1226
|
}
|
|
1007
1227
|
function inTypePosition(path) {
|
|
1008
1228
|
return path.some(
|
|
1009
|
-
(n) => !!n.type && (n.type.endsWith("TypeNode") || n.type === "TypeReference" || n.type === "TypeAliasStatement" || n.type === "ExportTypeAliasStatement")
|
|
1229
|
+
(n) => !!n.type && (n.type.endsWith("TypeNode") || n.type === "TypeReference" || n.type === "TypeAliasStatement" || n.type === "ExportTypeAliasStatement" || n.type === "DeclareClassStatement")
|
|
1010
1230
|
);
|
|
1011
1231
|
}
|
|
1012
1232
|
var PRIMITIVES2 = [
|
|
@@ -1147,6 +1367,12 @@ function documentSymbols(analysis) {
|
|
|
1147
1367
|
}
|
|
1148
1368
|
break;
|
|
1149
1369
|
}
|
|
1370
|
+
case "DeclareClassStatement": {
|
|
1371
|
+
const name = node.name.name;
|
|
1372
|
+
const superclass = node.superclass?.base;
|
|
1373
|
+
out.push(symbol(name, SymbolKind.Class, node, superclass && `extends ${superclass}`));
|
|
1374
|
+
break;
|
|
1375
|
+
}
|
|
1150
1376
|
case "VariableDeclaration": {
|
|
1151
1377
|
for (const target of node.names ?? []) {
|
|
1152
1378
|
const name = target.name;
|
|
@@ -1184,10 +1410,11 @@ function symbol(name, kind, node, detail) {
|
|
|
1184
1410
|
}
|
|
1185
1411
|
|
|
1186
1412
|
// src/features/semanticTokens.ts
|
|
1187
|
-
import { tokenize } from "luaut-parser";
|
|
1413
|
+
import { tokenize, isClassType as isClassType3, unknownType } from "luaut-parser";
|
|
1188
1414
|
var TOKEN_TYPES = [
|
|
1189
1415
|
"namespace",
|
|
1190
1416
|
"type",
|
|
1417
|
+
"class",
|
|
1191
1418
|
"typeParameter",
|
|
1192
1419
|
"parameter",
|
|
1193
1420
|
"variable",
|
|
@@ -1204,6 +1431,7 @@ var semanticTokensLegend = {
|
|
|
1204
1431
|
var SOFT_KEYWORDS = /* @__PURE__ */ new Set([
|
|
1205
1432
|
"type",
|
|
1206
1433
|
"declare",
|
|
1434
|
+
"class",
|
|
1207
1435
|
"extends",
|
|
1208
1436
|
"keyof",
|
|
1209
1437
|
"infer",
|
|
@@ -1270,6 +1498,8 @@ function classify(analysis, spanned, ancestors, identifiers, add) {
|
|
|
1270
1498
|
if (!baseToken) return;
|
|
1271
1499
|
if (!namespace && typeParameterInScope2(ancestors, base)) {
|
|
1272
1500
|
add(baseToken, base.length, "typeParameter");
|
|
1501
|
+
} else if (isClassType3(analysis.types.aliases.get(namespace ? `${namespace}.${base}` : base) ?? unknownType)) {
|
|
1502
|
+
add(baseToken, base.length, "class");
|
|
1273
1503
|
} else {
|
|
1274
1504
|
add(baseToken, base.length, "type", PRIMITIVES3.has(base) ? ["defaultLibrary"] : []);
|
|
1275
1505
|
}
|
|
@@ -1297,6 +1527,9 @@ function identifier(analysis, node, parent, add) {
|
|
|
1297
1527
|
case "ExportTypeAliasStatement":
|
|
1298
1528
|
if (parent.name === node) return as("type", ["declaration"]);
|
|
1299
1529
|
break;
|
|
1530
|
+
case "DeclareClassStatement":
|
|
1531
|
+
if (parent.name === node) return as("class", ["declaration"]);
|
|
1532
|
+
break;
|
|
1300
1533
|
case "DeclareStatement":
|
|
1301
1534
|
if (parent.id === node) return as(isFunction(typeOfNode(parent.valueType)) ? "function" : "variable", ["declaration"]);
|
|
1302
1535
|
break;
|
|
@@ -1413,6 +1646,7 @@ function encode(entries) {
|
|
|
1413
1646
|
// src/server.ts
|
|
1414
1647
|
import {
|
|
1415
1648
|
createConnection,
|
|
1649
|
+
DiagnosticSeverity as DiagnosticSeverity2,
|
|
1416
1650
|
ProposedFeatures,
|
|
1417
1651
|
TextDocuments,
|
|
1418
1652
|
TextDocumentSyncKind
|
|
@@ -1454,17 +1688,56 @@ function createServer(connection, options = {}) {
|
|
|
1454
1688
|
const document = documents.get(p.textDocument.uri);
|
|
1455
1689
|
return document ? semanticTokens(analyzer.get(document)) : { data: [] };
|
|
1456
1690
|
});
|
|
1457
|
-
|
|
1458
|
-
void connection.sendDiagnostics({
|
|
1459
|
-
uri: document.uri,
|
|
1460
|
-
version: document.version,
|
|
1461
|
-
diagnostics: diagnostics(analyzer.get(document))
|
|
1462
|
-
});
|
|
1463
|
-
};
|
|
1464
|
-
documents.onDidOpen((e) => publish(e.document));
|
|
1691
|
+
let configUris = /* @__PURE__ */ new Set();
|
|
1465
1692
|
const publishAll = () => {
|
|
1466
|
-
|
|
1693
|
+
const problems = /* @__PURE__ */ new Map();
|
|
1694
|
+
for (const document of documents.all()) {
|
|
1695
|
+
const analysis = analyzer.get(document);
|
|
1696
|
+
void connection.sendDiagnostics({
|
|
1697
|
+
uri: document.uri,
|
|
1698
|
+
version: document.version,
|
|
1699
|
+
diagnostics: [...diagnostics(analysis), ...projectHint(analysis)]
|
|
1700
|
+
});
|
|
1701
|
+
for (const problem of analysis.project.problems) {
|
|
1702
|
+
const uri = uriOfPath(problem.file);
|
|
1703
|
+
const list = problems.get(uri) ?? [];
|
|
1704
|
+
if (!list.some((p) => p.message === problem.message && p.line === problem.line)) list.push(problem);
|
|
1705
|
+
problems.set(uri, list);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
for (const [uri, list] of problems) {
|
|
1709
|
+
void connection.sendDiagnostics({ uri, diagnostics: list.map(problemDiagnostic) });
|
|
1710
|
+
}
|
|
1711
|
+
for (const uri of configUris) {
|
|
1712
|
+
if (!problems.has(uri)) void connection.sendDiagnostics({ uri, diagnostics: [] });
|
|
1713
|
+
}
|
|
1714
|
+
configUris = new Set(problems.keys());
|
|
1467
1715
|
};
|
|
1716
|
+
const problemDiagnostic = (problem) => {
|
|
1717
|
+
const line = Math.max((problem.line ?? 1) - 1, 0);
|
|
1718
|
+
const character = Math.max((problem.column ?? 1) - 1, 0);
|
|
1719
|
+
const text = analyzer.readFile(problem.file)?.split("\n")[line] ?? "";
|
|
1720
|
+
const end = Math.max(text.replace(/\r$/, "").trimEnd().length, character + 1);
|
|
1721
|
+
return {
|
|
1722
|
+
range: { start: { line, character }, end: { line, character: end } },
|
|
1723
|
+
severity: DiagnosticSeverity2.Error,
|
|
1724
|
+
source: "luaut",
|
|
1725
|
+
code: "config",
|
|
1726
|
+
message: problem.message
|
|
1727
|
+
};
|
|
1728
|
+
};
|
|
1729
|
+
const projectHint = (analysis) => {
|
|
1730
|
+
const { project } = analysis;
|
|
1731
|
+
if (project.fixed || project.config || !pathOfUri(analysis.uri)) return [];
|
|
1732
|
+
return [{
|
|
1733
|
+
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
|
|
1734
|
+
severity: DiagnosticSeverity2.Information,
|
|
1735
|
+
source: "luaut",
|
|
1736
|
+
code: "no-config",
|
|
1737
|
+
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 }'
|
|
1738
|
+
}];
|
|
1739
|
+
};
|
|
1740
|
+
documents.onDidOpen(publishAll);
|
|
1468
1741
|
documents.onDidChangeContent(publishAll);
|
|
1469
1742
|
connection.onDidChangeWatchedFiles(publishAll);
|
|
1470
1743
|
documents.onDidClose((e) => {
|
|
@@ -1535,6 +1808,9 @@ function startServer(options = {}) {
|
|
|
1535
1808
|
}
|
|
1536
1809
|
|
|
1537
1810
|
export {
|
|
1811
|
+
membersOf,
|
|
1812
|
+
signaturesOf,
|
|
1813
|
+
signatureLabel,
|
|
1538
1814
|
pathOfUri,
|
|
1539
1815
|
uriOfPath,
|
|
1540
1816
|
samePath,
|
|
@@ -1547,9 +1823,6 @@ export {
|
|
|
1547
1823
|
nodeAt,
|
|
1548
1824
|
enclosing,
|
|
1549
1825
|
walk,
|
|
1550
|
-
membersOf,
|
|
1551
|
-
signaturesOf,
|
|
1552
|
-
signatureLabel,
|
|
1553
1826
|
importCompletion,
|
|
1554
1827
|
importDefinition,
|
|
1555
1828
|
exportDeclaration,
|
|
@@ -1569,4 +1842,4 @@ export {
|
|
|
1569
1842
|
createServer,
|
|
1570
1843
|
startServer
|
|
1571
1844
|
};
|
|
1572
|
-
//# sourceMappingURL=chunk-
|
|
1845
|
+
//# sourceMappingURL=chunk-HJ2C3OFZ.js.map
|