sphinx-lua-ls 0.0.4__py3-none-any.whl
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.
- sphinx_lua_ls/__init__.py +166 -0
- sphinx_lua_ls/_version.py +21 -0
- sphinx_lua_ls/autodoc.py +632 -0
- sphinx_lua_ls/doctree.py +477 -0
- sphinx_lua_ls/domain.py +1156 -0
- sphinx_lua_ls/intersphinx.py +204 -0
- sphinx_lua_ls/lua_ls.py +694 -0
- sphinx_lua_ls/py.typed +0 -0
- sphinx_lua_ls-0.0.4.dist-info/LICENSE +21 -0
- sphinx_lua_ls-0.0.4.dist-info/METADATA +92 -0
- sphinx_lua_ls-0.0.4.dist-info/RECORD +13 -0
- sphinx_lua_ls-0.0.4.dist-info/WHEEL +5 -0
- sphinx_lua_ls-0.0.4.dist-info/top_level.txt +1 -0
sphinx_lua_ls/doctree.py
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parser for Lua-LS output.
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import dataclasses
|
|
9
|
+
import enum
|
|
10
|
+
import functools
|
|
11
|
+
import pathlib
|
|
12
|
+
import re
|
|
13
|
+
import typing as _t
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Kind(enum.Enum):
|
|
18
|
+
"""
|
|
19
|
+
Kind of a lua object.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
Module = "module"
|
|
24
|
+
|
|
25
|
+
Data = "data"
|
|
26
|
+
|
|
27
|
+
Function = "function"
|
|
28
|
+
|
|
29
|
+
Class = "class"
|
|
30
|
+
|
|
31
|
+
Alias = "alias"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Visibility(enum.Enum):
|
|
35
|
+
"""
|
|
36
|
+
Visibility of a lua object.
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
#: Public visibility.
|
|
41
|
+
Public = "public"
|
|
42
|
+
|
|
43
|
+
#: Protected visibility.
|
|
44
|
+
Protected = "protected"
|
|
45
|
+
|
|
46
|
+
#: Private visibility.
|
|
47
|
+
Private = "private"
|
|
48
|
+
|
|
49
|
+
#: Module visibility.
|
|
50
|
+
Package = "package"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class Param:
|
|
55
|
+
"""
|
|
56
|
+
Function parameter or return value.
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
#: Parameter's name.
|
|
61
|
+
name: str | None
|
|
62
|
+
|
|
63
|
+
#: Parameter's type.
|
|
64
|
+
type: str | None
|
|
65
|
+
|
|
66
|
+
#: Unparsed documentation text.
|
|
67
|
+
docstring: str | None
|
|
68
|
+
|
|
69
|
+
def __str__(self) -> str:
|
|
70
|
+
return f"{self.name or '_'}: {self.type or 'unknown'}"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(kw_only=True, repr=False)
|
|
74
|
+
class Object:
|
|
75
|
+
"""
|
|
76
|
+
A documented lua object.
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
#: When two objects with the same name are defined, the one with a higher
|
|
81
|
+
#: priority wins.
|
|
82
|
+
priority: _t.ClassVar[int] = 0
|
|
83
|
+
|
|
84
|
+
#: Kind of a lua object.
|
|
85
|
+
kind: Kind = dataclasses.field(default=Kind.Module, init=False)
|
|
86
|
+
|
|
87
|
+
#: Deprecation marker.
|
|
88
|
+
is_deprecated: bool = False
|
|
89
|
+
|
|
90
|
+
#: Async marker.
|
|
91
|
+
is_async: bool = False
|
|
92
|
+
|
|
93
|
+
#: Object visibility.
|
|
94
|
+
visibility: Visibility = Visibility.Public
|
|
95
|
+
|
|
96
|
+
#: Absolute path to the `.lua` file where this object was defined.
|
|
97
|
+
file: pathlib.Path | None = None
|
|
98
|
+
|
|
99
|
+
#: Line number in the file.
|
|
100
|
+
line: int | None = None
|
|
101
|
+
|
|
102
|
+
#: Unparsed documentation text.
|
|
103
|
+
docstring: str | None = None
|
|
104
|
+
|
|
105
|
+
#: Child objects.
|
|
106
|
+
children: dict[str, Object] = dataclasses.field(default_factory=dict)
|
|
107
|
+
|
|
108
|
+
def __repr__(self) -> str:
|
|
109
|
+
return self.__class__.__name__
|
|
110
|
+
|
|
111
|
+
def __str__(self) -> str:
|
|
112
|
+
res = ""
|
|
113
|
+
if self.is_deprecated:
|
|
114
|
+
res += " (deprecated)"
|
|
115
|
+
if self.is_async:
|
|
116
|
+
res += " (async)"
|
|
117
|
+
|
|
118
|
+
res += self._print_object()
|
|
119
|
+
tail = self._print_object_tail()
|
|
120
|
+
|
|
121
|
+
for name, ch in self.children.items():
|
|
122
|
+
if not name.startswith("_"):
|
|
123
|
+
first, *rest = str(ch).splitlines()
|
|
124
|
+
if rest:
|
|
125
|
+
rest = "\n " + "\n ".join(rest)
|
|
126
|
+
else:
|
|
127
|
+
rest = ""
|
|
128
|
+
res += f"\n {name}{first}{rest}"
|
|
129
|
+
|
|
130
|
+
if self.children and tail:
|
|
131
|
+
res += "\n"
|
|
132
|
+
res += tail
|
|
133
|
+
|
|
134
|
+
return res
|
|
135
|
+
|
|
136
|
+
def _print_object(self) -> str:
|
|
137
|
+
return " {"
|
|
138
|
+
|
|
139
|
+
def _print_object_tail(self) -> str:
|
|
140
|
+
return "}"
|
|
141
|
+
|
|
142
|
+
def find(self, path: str) -> Object | None:
|
|
143
|
+
"""
|
|
144
|
+
Find an object and return it.
|
|
145
|
+
|
|
146
|
+
:param path:
|
|
147
|
+
dot-separated object path.
|
|
148
|
+
:return:
|
|
149
|
+
a found object or ``None``.
|
|
150
|
+
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
root = self
|
|
154
|
+
for name in path.split("."):
|
|
155
|
+
if name not in root.children:
|
|
156
|
+
return None
|
|
157
|
+
root = root.children[name]
|
|
158
|
+
return root
|
|
159
|
+
|
|
160
|
+
def find_path(self, path: str) -> tuple[Object, str, str, str] | None:
|
|
161
|
+
"""
|
|
162
|
+
Find an object and return a path to it.
|
|
163
|
+
|
|
164
|
+
:param path:
|
|
165
|
+
dot-separated object path.
|
|
166
|
+
:return:
|
|
167
|
+
an object itself, a module path component, a class path component,
|
|
168
|
+
and an object name.
|
|
169
|
+
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
root = self
|
|
173
|
+
|
|
174
|
+
in_class = False
|
|
175
|
+
modname = []
|
|
176
|
+
classname = []
|
|
177
|
+
|
|
178
|
+
for name in path.split("."):
|
|
179
|
+
if name not in root.children:
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
if in_class or root.kind != Kind.Module:
|
|
183
|
+
in_class = True
|
|
184
|
+
classname.append(name)
|
|
185
|
+
else:
|
|
186
|
+
modname.append(name)
|
|
187
|
+
|
|
188
|
+
root = root.children[name]
|
|
189
|
+
|
|
190
|
+
if classname:
|
|
191
|
+
name = classname.pop()
|
|
192
|
+
elif modname:
|
|
193
|
+
name = modname.pop()
|
|
194
|
+
else:
|
|
195
|
+
name = ""
|
|
196
|
+
|
|
197
|
+
return root, ".".join(modname), ".".join(classname), name
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@dataclass(kw_only=True, repr=False)
|
|
201
|
+
class Data(Object):
|
|
202
|
+
"""
|
|
203
|
+
A lua variable.
|
|
204
|
+
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
kind = Kind.Data
|
|
208
|
+
|
|
209
|
+
priority = 1
|
|
210
|
+
|
|
211
|
+
#: Variable type.
|
|
212
|
+
type: str
|
|
213
|
+
|
|
214
|
+
def _print_object(self) -> str:
|
|
215
|
+
return f": {self.type}"
|
|
216
|
+
|
|
217
|
+
def _print_object_tail(self) -> str:
|
|
218
|
+
return ""
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@dataclass(kw_only=True, repr=False)
|
|
222
|
+
class Function(Object):
|
|
223
|
+
"""
|
|
224
|
+
A lua function.
|
|
225
|
+
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
kind = Kind.Function
|
|
229
|
+
|
|
230
|
+
priority = 2
|
|
231
|
+
|
|
232
|
+
#: Function parameters.
|
|
233
|
+
params: list[Param] = dataclasses.field(default_factory=list)
|
|
234
|
+
|
|
235
|
+
#: Function return values.
|
|
236
|
+
returns: list[Param] = dataclasses.field(default_factory=list)
|
|
237
|
+
|
|
238
|
+
#: Indicates that this function implicitly accepts ``self`` argument.
|
|
239
|
+
implicit_self: bool = False
|
|
240
|
+
|
|
241
|
+
def _print_object(self) -> str:
|
|
242
|
+
params = ", ".join(map(str, self.params))
|
|
243
|
+
returns = ", ".join(map(str, self.returns))
|
|
244
|
+
if returns:
|
|
245
|
+
returns = " -> " + returns
|
|
246
|
+
return f" = function ({params}){returns}"
|
|
247
|
+
|
|
248
|
+
def _print_object_tail(self) -> str:
|
|
249
|
+
return ""
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@dataclass(kw_only=True, repr=False)
|
|
253
|
+
class Class(Object):
|
|
254
|
+
"""
|
|
255
|
+
A lua class.
|
|
256
|
+
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
priority = 2
|
|
260
|
+
|
|
261
|
+
kind = Kind.Class # type: ignore
|
|
262
|
+
|
|
263
|
+
#: Base classes or types.
|
|
264
|
+
bases: list[str] = dataclasses.field(default_factory=list)
|
|
265
|
+
|
|
266
|
+
@functools.cached_property
|
|
267
|
+
def is_module(self):
|
|
268
|
+
"""
|
|
269
|
+
Indicates that this class is just a module or a namespace.
|
|
270
|
+
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
return self.bases == ["table"]
|
|
274
|
+
|
|
275
|
+
@property
|
|
276
|
+
def kind(self) -> Kind: # type: ignore
|
|
277
|
+
return Kind.Module if self.is_module else Kind.Class
|
|
278
|
+
|
|
279
|
+
def _print_object(self) -> str:
|
|
280
|
+
bases = ", ".join(self.bases)
|
|
281
|
+
if self.is_module:
|
|
282
|
+
return " = module {"
|
|
283
|
+
else:
|
|
284
|
+
return f" = class({bases}) {{"
|
|
285
|
+
|
|
286
|
+
def _print_object_tail(self) -> str:
|
|
287
|
+
return "}"
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@dataclass(kw_only=True, repr=False)
|
|
291
|
+
class Alias(Object):
|
|
292
|
+
"""
|
|
293
|
+
A lua type alias.
|
|
294
|
+
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
priority = 2
|
|
298
|
+
|
|
299
|
+
kind = Kind.Alias
|
|
300
|
+
|
|
301
|
+
#: Alias type.
|
|
302
|
+
type: str
|
|
303
|
+
|
|
304
|
+
def _print_object(self) -> str:
|
|
305
|
+
return f" = {self.type}"
|
|
306
|
+
|
|
307
|
+
def _print_object_tail(self) -> str:
|
|
308
|
+
return ""
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class Parser:
|
|
312
|
+
def __init__(self):
|
|
313
|
+
#: Root of the object tree.
|
|
314
|
+
self.root = Object()
|
|
315
|
+
|
|
316
|
+
def parse(self, json):
|
|
317
|
+
"""
|
|
318
|
+
Parse jua-ls json output.
|
|
319
|
+
|
|
320
|
+
"""
|
|
321
|
+
if not isinstance(json, list):
|
|
322
|
+
return
|
|
323
|
+
for ns in json:
|
|
324
|
+
self._parse_toplevel(ns)
|
|
325
|
+
|
|
326
|
+
def add(self, path: str, o: Object):
|
|
327
|
+
"""
|
|
328
|
+
Add an object to the object tree.
|
|
329
|
+
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
root = self.root
|
|
333
|
+
*components, name = path.split(".")
|
|
334
|
+
for component in components:
|
|
335
|
+
if component in root.children:
|
|
336
|
+
root = root.children[component]
|
|
337
|
+
else:
|
|
338
|
+
root.children[component] = root = Object()
|
|
339
|
+
self.add_child(root, name, o)
|
|
340
|
+
|
|
341
|
+
def merge_objects(self, a: Object, b: Object) -> Object:
|
|
342
|
+
"""
|
|
343
|
+
Merge two objects with the same name.
|
|
344
|
+
|
|
345
|
+
"""
|
|
346
|
+
|
|
347
|
+
# TODO: handle function overloads?
|
|
348
|
+
a, b = sorted([a, b], key=lambda x: (-x.priority, x.line))
|
|
349
|
+
for name, child in b.children.items():
|
|
350
|
+
self.add_child(a, name, child)
|
|
351
|
+
if not a.file:
|
|
352
|
+
a.file = b.file
|
|
353
|
+
a.line = b.line
|
|
354
|
+
if not a.docstring:
|
|
355
|
+
a.docstring = b.docstring
|
|
356
|
+
elif b.docstring:
|
|
357
|
+
if len(a.docstring) < len(b.docstring):
|
|
358
|
+
# Sometimes, `@see` directives are only included in one definition.
|
|
359
|
+
a.docstring = b.docstring
|
|
360
|
+
return a
|
|
361
|
+
|
|
362
|
+
def add_child(self, o: Object, name: str, child: Object):
|
|
363
|
+
"""
|
|
364
|
+
Add child to an object, merging objects if necessary.
|
|
365
|
+
|
|
366
|
+
"""
|
|
367
|
+
|
|
368
|
+
if name not in o.children:
|
|
369
|
+
o.children[name] = child
|
|
370
|
+
else:
|
|
371
|
+
o.children[name] = self.merge_objects(o.children[name], child)
|
|
372
|
+
|
|
373
|
+
def _parse_toplevel(self, ns):
|
|
374
|
+
if not isinstance(ns, dict):
|
|
375
|
+
return
|
|
376
|
+
|
|
377
|
+
o = self._parse_definitions(ns.get("defines", []))
|
|
378
|
+
|
|
379
|
+
for field in ns.get("fields", []):
|
|
380
|
+
if "name" not in field:
|
|
381
|
+
continue
|
|
382
|
+
|
|
383
|
+
self.add_child(o, field["name"], self._parse_field(field))
|
|
384
|
+
|
|
385
|
+
self.add(ns.get("name", ""), o)
|
|
386
|
+
|
|
387
|
+
def _parse_definitions(self, ns) -> Object:
|
|
388
|
+
if not ns or not isinstance(ns, list):
|
|
389
|
+
return Object()
|
|
390
|
+
first, *rest = ns
|
|
391
|
+
first = self._parse_definition(first)
|
|
392
|
+
for o in rest:
|
|
393
|
+
first = self.merge_objects(first, self._parse_definition(o))
|
|
394
|
+
return first
|
|
395
|
+
|
|
396
|
+
def _parse_definition(self, ns) -> Object:
|
|
397
|
+
if not isinstance(ns, dict):
|
|
398
|
+
return Object()
|
|
399
|
+
|
|
400
|
+
match ns.get("type"):
|
|
401
|
+
case "doc.class":
|
|
402
|
+
res = Class()
|
|
403
|
+
res.docstring = ns.get("desc")
|
|
404
|
+
for base in ns.get("extends", []):
|
|
405
|
+
if "view" in base:
|
|
406
|
+
res.bases.append(base["view"])
|
|
407
|
+
case "doc.alias":
|
|
408
|
+
res = Alias(type=ns.get("view", "unknown"))
|
|
409
|
+
_process_alias_doc(res, ns.get("desc"))
|
|
410
|
+
case _:
|
|
411
|
+
return self._parse_field(ns)
|
|
412
|
+
|
|
413
|
+
res.is_deprecated = bool(ns.get("deprecated", False))
|
|
414
|
+
res.is_async = bool(ns.get("async", False))
|
|
415
|
+
res.visibility = Visibility(ns.get("visible", "public"))
|
|
416
|
+
res.file = self._normalize_path(ns.get("file"))
|
|
417
|
+
res.line = ns.get("start", [None, None])[0]
|
|
418
|
+
|
|
419
|
+
return res
|
|
420
|
+
|
|
421
|
+
def _parse_field(self, ns) -> Object:
|
|
422
|
+
implicit_self = ns.get("type") == "setmethod"
|
|
423
|
+
if "extends" not in ns or not isinstance(ns["extends"], dict):
|
|
424
|
+
return Object()
|
|
425
|
+
extends = ns["extends"]
|
|
426
|
+
|
|
427
|
+
match extends.get("type"):
|
|
428
|
+
case "function":
|
|
429
|
+
res = Function()
|
|
430
|
+
for param in extends.get("args", []):
|
|
431
|
+
name = param.get("name")
|
|
432
|
+
if param.get("type") == "...":
|
|
433
|
+
name = "..."
|
|
434
|
+
if not isinstance(name, str):
|
|
435
|
+
name = None
|
|
436
|
+
typ = param.get("view")
|
|
437
|
+
res.params.append(Param(name, typ, param.get("desc")))
|
|
438
|
+
for param in extends.get("returns", []):
|
|
439
|
+
name = param.get("name")
|
|
440
|
+
if param.get("type") == "...":
|
|
441
|
+
name = "..."
|
|
442
|
+
if not isinstance(name, str):
|
|
443
|
+
name = None
|
|
444
|
+
typ = param.get("view")
|
|
445
|
+
res.returns.append(Param(name, typ, param.get("desc")))
|
|
446
|
+
res.implicit_self = implicit_self
|
|
447
|
+
case _:
|
|
448
|
+
res = Data(type=extends.get("view", "unknown"))
|
|
449
|
+
|
|
450
|
+
res.is_deprecated = bool(ns.get("deprecated", False))
|
|
451
|
+
res.is_async = bool(ns.get("async", False))
|
|
452
|
+
res.visibility = Visibility(ns.get("visible", "public"))
|
|
453
|
+
res.file = self._normalize_path(ns.get("file"))
|
|
454
|
+
res.line = ns.get("start", [None, None])[0]
|
|
455
|
+
res.docstring = ns.get("desc")
|
|
456
|
+
|
|
457
|
+
return res
|
|
458
|
+
|
|
459
|
+
def _normalize_path(self, path: str | None) -> pathlib.Path | None:
|
|
460
|
+
if path:
|
|
461
|
+
return pathlib.Path(re.sub(r"^.*://", "", path)).resolve()
|
|
462
|
+
else:
|
|
463
|
+
return None
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _process_alias_doc(node: Alias, doc: str | None):
|
|
467
|
+
if not doc or not (doc.startswith("```lua\n") and doc.endswith("\n```")):
|
|
468
|
+
node.docstring = doc
|
|
469
|
+
return
|
|
470
|
+
|
|
471
|
+
main_doc = []
|
|
472
|
+
|
|
473
|
+
for line in doc[7:-5].splitlines():
|
|
474
|
+
if line.startswith("--"):
|
|
475
|
+
main_doc.append(line[2:])
|
|
476
|
+
|
|
477
|
+
node.docstring = "\n".join(main_doc)
|