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.
@@ -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)