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,1156 @@
1
+ """
2
+ Lua domain.
3
+
4
+ This code is based on ``sphinxcontrib.luadomain`` by Eliott Dumeix.
5
+
6
+ See the original code here: https://github.com/boolangery/sphinx-luadomain
7
+
8
+ """
9
+
10
+ import functools
11
+ import re
12
+ from collections.abc import Set
13
+ from typing import Any, Callable, ClassVar, Generic, Iterable, Iterator, TypeVar
14
+
15
+ from docutils import nodes
16
+ from docutils.parsers.rst import directives
17
+ from docutils.parsers.rst.states import Inliner
18
+ from sphinx import addnodes
19
+ from sphinx.builders import Builder
20
+ from sphinx.directives import ObjectDescription
21
+ from sphinx.domains import Domain, Index, IndexEntry, ObjType
22
+ from sphinx.environment import BuildEnvironment
23
+ from sphinx.locale import get_translation
24
+ from sphinx.roles import XRefRole
25
+ from sphinx.util import logging
26
+ from sphinx.util.docfields import TypedField
27
+ from sphinx.util.docutils import SphinxDirective
28
+ from sphinx.util.nodes import make_refnode
29
+
30
+ T = TypeVar("T")
31
+
32
+ MESSAGE_CATALOG_NAME = "sphinx-lua-ls"
33
+ _ = get_translation(MESSAGE_CATALOG_NAME)
34
+
35
+
36
+ logger = logging.getLogger("sphinx_lua_ls")
37
+
38
+
39
+ #: Regexp for parsing a single Lua identifier.
40
+ _OBJECT_NAME_RE = re.compile(r"^\s*[\w-]+(\s*\.\s*[\w-]+)*\s*")
41
+
42
+ #: A single function parameter name.
43
+ _PARAM_NAME_RE = re.compile(r"^\s*[\w-]+\s*$")
44
+
45
+
46
+ def _handle_signature_errors(handler):
47
+ @functools.wraps(handler)
48
+ def fn(self, sig: str, signode: addnodes.desc_signature):
49
+ try:
50
+ return handler(self, sig, signode)
51
+ except ValueError as e:
52
+ logger.warning(
53
+ "incorrect %s signature %r: %s",
54
+ self.objtype,
55
+ sig,
56
+ e,
57
+ type="lua-ls",
58
+ location=(signode.source, signode.line),
59
+ )
60
+ raise
61
+
62
+ return fn
63
+
64
+
65
+ def _separate_paren_prefix(sig: str) -> tuple[str, str]:
66
+ """
67
+ If string starts with a brace sequence, separate it out from the string.
68
+
69
+ """
70
+
71
+ if not sig.startswith("("):
72
+ return "", sig.strip()
73
+ else:
74
+ sig = sig[1:]
75
+
76
+ depth = 0
77
+ in_str = False
78
+ str_c = ""
79
+ esc = False
80
+ for i, c in enumerate(sig):
81
+ if in_str:
82
+ if esc:
83
+ esc = False
84
+ elif c == str_c:
85
+ in_str = False
86
+ elif c == "\\":
87
+ esc = True
88
+ elif c in "([{<":
89
+ depth += 1
90
+ elif depth == 0 and c == ")":
91
+ return sig[:i].strip(), sig[i + 1 :].strip()
92
+ elif c in ")]}>":
93
+ depth = max(depth - 1, 0)
94
+ elif c in "'\"`":
95
+ in_str = True
96
+ str_c = c
97
+
98
+ return sig.strip(), ""
99
+
100
+
101
+ def _separate_sig(sig: str, sep: str = ",", strip: bool = True) -> list[str]:
102
+ """
103
+ Separate a string by comas, ignoring comas within parens and string literals.
104
+
105
+ """
106
+
107
+ assert len(sep) == 1
108
+
109
+ res = []
110
+
111
+ pos = 0
112
+ depth = 0
113
+ in_str = False
114
+ str_c = ""
115
+ esc = False
116
+ for i, c in enumerate(sig):
117
+ if in_str:
118
+ if esc:
119
+ esc = False
120
+ elif c == str_c:
121
+ in_str = False
122
+ elif c == "\\":
123
+ esc = True
124
+ elif c in "([{<":
125
+ depth += 1
126
+ elif c in ")]}>":
127
+ depth = max(depth - 1, 0)
128
+ elif c in "'\"`":
129
+ in_str = True
130
+ str_c = c
131
+ elif depth == 0 and c == sep:
132
+ elem = sig[pos:i]
133
+ if strip:
134
+ elem = elem.strip()
135
+ if elem and not elem.isspace():
136
+ res.append(elem)
137
+ pos = i + 1
138
+
139
+ if pos < len(sig):
140
+ elem = sig[pos:]
141
+ if strip:
142
+ elem = elem.strip()
143
+ if elem and not elem.isspace():
144
+ res.append(elem)
145
+
146
+ return res
147
+
148
+
149
+ def _parse_types(
150
+ sig: str, parsingFunctionParams: bool = False
151
+ ) -> list[tuple[str, str]]:
152
+ """
153
+ Parse sequence of type annotations separated by comas.
154
+
155
+ Each type annotation might consist of a single type or a name-type pair.
156
+
157
+ """
158
+
159
+ res = []
160
+ for elem in _separate_sig(sig):
161
+ elems = _separate_sig(elem, ":", strip=False)
162
+ if not elems:
163
+ continue
164
+ elif (
165
+ len(elems) == 1 and not parsingFunctionParams
166
+ ) or not _PARAM_NAME_RE.match(elems[0]):
167
+ # A single type annotation.
168
+ res.append(("", ":".join(elems).strip()))
169
+ else:
170
+ # A name and a type annotation.
171
+ res.append((elems[0].strip(), ":".join(elems[1:]).strip()))
172
+ return res
173
+
174
+
175
+ def _type_to_nodes(typ: str, inliner) -> list[nodes.Node]:
176
+ """
177
+ Loosely parse a type definition, and return a list of nodes and xrefs.
178
+
179
+ :param typ:
180
+ string with lua type declaration.
181
+ :param inliner:
182
+ inliner for xrefs (available in directives as ``self.state.inliner``).
183
+
184
+ """
185
+
186
+ res = []
187
+
188
+ for match in re.finditer(
189
+ r"""
190
+ # Skip spaces, they're not meaningful in this context.
191
+ \s+
192
+ |
193
+ (?P<dots>[.]{3})
194
+ |
195
+ # Literal string with escapes.
196
+ # Example: `"foo"`, `"foo-\"-bar"`.
197
+ (?P<string>(?P<string_q>['"`])(?:\\.|[^\\])*(?P=string_q))
198
+ |
199
+ # Number with optional exponent.
200
+ # Example: `1.0`, `.1`, `1.`, `1e+5`.
201
+ (?P<number>(?:\d+(?:\.\d*)|\.\d+)(?:[eE][+-]?\d+)?)
202
+ |
203
+ # Function type followed by an opening brace.
204
+ # Example: `fun( ...`.
205
+ (?P<kwd>fun)\s*(?=\()
206
+ |
207
+ # Ident not followed by an open brace, semicolon, etc.
208
+ # Example: `module.Type`.
209
+ # Doesn't match: `name?: ...`, `name( ...`, etc.
210
+ (?P<ident>[\w-]+(?:\.[\w-]+)*)
211
+ \s*(?P<ident_qm>\??)\s*
212
+ (?![:(\w.?-])
213
+ |
214
+ # Built-in type not followed by an open brace, semicolon, etc.
215
+ # Example: `string`, `string?`.
216
+ # Doesn't match: `string?: ...`, `string( ...`, etc.
217
+ (?P<type>nil|any|boolean|string|number|integer|function|table|thread|userdata|lightuserdata)
218
+ \s*(?P<type_qm>\??)\s*
219
+ (?![:(\w.?-])
220
+ |
221
+ # Name component, only matches when `ident` and `type` didn't match.
222
+ # Example: `string: ...`.
223
+ (?P<name>[\w.-]+)
224
+ |
225
+ # Punctuation that we separate with spaces.
226
+ (?P<punct>[=:,|])
227
+ |
228
+ # Punctuation that we copy as-is, without adding spaces.
229
+ (?P<other_punct>[-!"#$%&'()*+/;<>?@[\]^_`{}~]+)
230
+ |
231
+ # Anything else is copied as-is.
232
+ (?P<other>.)
233
+ """,
234
+ typ,
235
+ re.VERBOSE,
236
+ ):
237
+ if text := match.group("dots"):
238
+ res.append(addnodes.desc_sig_name(text, text))
239
+ elif text := match.group("kwd"):
240
+ res.append(addnodes.desc_sig_keyword(text, text))
241
+ elif text := match.group("type"):
242
+ res.append(addnodes.desc_sig_keyword_type(text, text))
243
+ if qm := match.group("type_qm"):
244
+ res.append(addnodes.desc_sig_punctuation(qm, qm))
245
+ elif text := match.group("string"):
246
+ res.append(addnodes.desc_sig_literal_string(text, text))
247
+ elif text := match.group("number"):
248
+ res.append(addnodes.desc_sig_literal_number(text, text))
249
+ elif text := match.group("ident"):
250
+ ref_nodes, warn_nodes = LuaXRefRole()("lua:obj", text, text, 0, inliner)
251
+ res.extend(ref_nodes)
252
+ res.extend(warn_nodes)
253
+ if qm := match.group("ident_qm"):
254
+ res.append(addnodes.desc_sig_punctuation(qm, qm))
255
+ elif text := match.group("name"):
256
+ res.append(addnodes.desc_sig_name(text, text))
257
+ elif text := match.group("punct"):
258
+ if text in "=|":
259
+ res.append(addnodes.desc_sig_space())
260
+ res.append(addnodes.desc_sig_punctuation(text, text))
261
+ res.append(addnodes.desc_sig_space())
262
+ elif text := match.group("other_punct"):
263
+ res.append(addnodes.desc_sig_punctuation(text, text))
264
+ elif text := match.group("other"):
265
+ res.append(nodes.Text(text))
266
+
267
+ return res
268
+
269
+
270
+ class LuaTypedField(TypedField):
271
+ def make_field(
272
+ self,
273
+ types: dict[str, list[nodes.Node]],
274
+ domain: str,
275
+ items: list[tuple[str, list[nodes.Node]]],
276
+ env: BuildEnvironment | None = None,
277
+ inliner: Inliner | None = None,
278
+ location: nodes.Element | None = None,
279
+ ) -> nodes.field:
280
+ # Process names and types in :param: and :return: flags.
281
+ for i, (name, content) in enumerate(items):
282
+ if name in types:
283
+ fieldtype = types[name]
284
+ if len(fieldtype) == 1 and isinstance(fieldtype[0], nodes.Text):
285
+ typename = fieldtype[0].astext()
286
+
287
+ if typename.endswith("?"):
288
+ new_name, new_typename = name + "?", typename[:-1]
289
+ if new_typename.startswith("(") and new_typename.endswith(")"):
290
+ new_typename = new_typename[1:-1]
291
+ items[i] = (new_name, content)
292
+ types.pop(name)
293
+ name, typename = new_name, new_typename
294
+
295
+ if inliner is None:
296
+ type_body: list[nodes.Node] = [nodes.Text(typename)]
297
+ else:
298
+ type_body = _type_to_nodes(typename, inliner)
299
+
300
+ types[name] = type_body
301
+
302
+ return super().make_field(types, domain, items, env, inliner, location)
303
+
304
+
305
+ class LuaObject(ObjectDescription[tuple[str, str, str, str]], Generic[T]):
306
+ """
307
+ Description of a general Lua object.
308
+
309
+ Full object path consists of three parts:
310
+
311
+ 1. current module,
312
+ 2. current class,
313
+ 3. object name.
314
+
315
+ For example, if there's a module ``app.log``, a class ``Logger`` within,
316
+ and then ``LogLevel`` within ``Logger``, then a full name for ``LogLevel``
317
+ is ``app.log.Logger.LogLevel``.
318
+
319
+ """
320
+
321
+ option_spec: ClassVar[dict[str, Callable[[str], Any]]] = {
322
+ "no-index": directives.flag,
323
+ "module": directives.unchanged,
324
+ "annotation": directives.unchanged,
325
+ "virtual": directives.flag,
326
+ "private": directives.flag,
327
+ "protected": directives.flag,
328
+ "package": directives.flag,
329
+ "abstract": directives.flag,
330
+ "async": directives.flag,
331
+ "global": directives.flag,
332
+ "deprecated": directives.flag,
333
+ }
334
+
335
+ doc_field_types = [
336
+ LuaTypedField(
337
+ "parameter",
338
+ label=_("Parameters"),
339
+ names=(
340
+ "param",
341
+ "parameter",
342
+ "arg",
343
+ "argument",
344
+ ),
345
+ rolename="",
346
+ typerolename="obj",
347
+ typenames=("paramtype", "type"),
348
+ can_collapse=True,
349
+ ),
350
+ LuaTypedField(
351
+ "returnvalue",
352
+ label=_("Returns"),
353
+ names=("return", "returns"),
354
+ rolename="",
355
+ typerolename="obj",
356
+ typenames=("returntype", "rtype"),
357
+ can_collapse=True,
358
+ ),
359
+ ]
360
+
361
+ allow_nesting = False
362
+
363
+ def run(self) -> list[nodes.Node]:
364
+ for name, option in self.env.config["lua_ls_default_options"].items():
365
+ if name not in self.options:
366
+ self.options[name] = option
367
+ return super().run()
368
+
369
+ def parse_signature(self, sig: str) -> tuple[str, T]:
370
+ raise NotImplementedError()
371
+
372
+ def use_semicolon_path(self) -> bool:
373
+ return False
374
+
375
+ def handle_signature_prefix(
376
+ self, sig: str, signode: addnodes.desc_signature
377
+ ) -> tuple[str, str, str, str, T]:
378
+ name, sigdata = self.parse_signature(sig)
379
+
380
+ modname = self.options.get("module", self.env.ref_context.get("lua:module"))
381
+ classname = self.env.ref_context.get("lua:class", None)
382
+ fullname = ".".join(filter(None, [modname, classname, name]))
383
+
384
+ # Only display full path if we're not inside of a class.
385
+ prefix = "" if classname else ".".join(filter(None, [modname, classname]))
386
+ if prefix:
387
+ prefix += ":" if self.use_semicolon_path() else "."
388
+
389
+ signode["module"] = modname
390
+ signode["class"] = classname
391
+ signode["fullname"] = fullname
392
+
393
+ sig_prefix = self.get_signature_prefix(sig)
394
+ if sig_prefix:
395
+ signode += addnodes.desc_annotation("", "", *sig_prefix)
396
+
397
+ if prefix:
398
+ signode += addnodes.desc_addname(prefix, prefix)
399
+ signode += addnodes.desc_name(name, name)
400
+
401
+ return fullname, modname, classname, name, sigdata
402
+
403
+ def get_signature_prefix(self, signature: str) -> list[nodes.Node]:
404
+ prefix = []
405
+
406
+ annotation = self.options.get("annotation")
407
+ if annotation:
408
+ prefix.extend(
409
+ [
410
+ addnodes.desc_sig_keyword(annotation, annotation),
411
+ addnodes.desc_sig_space(),
412
+ ]
413
+ )
414
+
415
+ for option in [
416
+ "global",
417
+ "private",
418
+ "protected",
419
+ "package",
420
+ "abstract",
421
+ "virtual",
422
+ "async",
423
+ ]:
424
+ if option in self.options:
425
+ prefix.extend(
426
+ [
427
+ addnodes.desc_sig_keyword(option, option),
428
+ addnodes.desc_sig_space(),
429
+ ]
430
+ )
431
+
432
+ return prefix
433
+
434
+ def needs_arg_list(self) -> bool:
435
+ """May return true if an empty argument list is to be generated even if
436
+ the document contains none.
437
+ """
438
+ return False
439
+
440
+ def get_index_text(
441
+ self, fullname: str, modname: str, classname: str, name: str
442
+ ) -> str:
443
+ *prefix_parts, _ = fullname.split(".")
444
+ prefix = ".".join(prefix_parts)
445
+ return f"{name} ({self.objtype} in {prefix})"
446
+
447
+ def add_target_and_index(
448
+ self,
449
+ name: tuple[str, str, str, str],
450
+ sig: str,
451
+ signode: addnodes.desc_signature,
452
+ ) -> None:
453
+ fullname, modname, classname, objname = name
454
+ anchor = "lua:" + fullname
455
+ if anchor not in self.state.document.ids:
456
+ signode["names"].append(anchor)
457
+ signode["ids"].append(anchor)
458
+ signode["first"] = not self.names
459
+ self.state.document.note_explicit_target(signode)
460
+ objects = self.env.domaindata["lua"]["objects"]
461
+ if fullname in objects:
462
+ self.state_machine.reporter.warning(
463
+ "duplicate object description of %s, " % fullname
464
+ + "other instance in "
465
+ + self.env.doc2path(objects[fullname][0])
466
+ + ", use :no-index: for one of them",
467
+ line=self.lineno,
468
+ )
469
+ objects[fullname] = (self.env.docname, self.objtype)
470
+
471
+ indextext = self.get_index_text(fullname, modname, classname, objname)
472
+ if indextext:
473
+ self.indexnode["entries"].append(("single", indextext, anchor, "", None))
474
+
475
+ def before_content(self) -> None:
476
+ if self.names and self.allow_nesting:
477
+ _, _, classname, name = self.names[-1]
478
+ # Add name of the current object to the current classname,
479
+ # thus getting a new classname.
480
+ new_classname = ".".join(filter(None, [classname, name]))
481
+ classes = self.env.ref_context.setdefault("lua:classes", [])
482
+ classes.append(self.env.ref_context.get("lua:class"))
483
+ self.env.ref_context["lua:class"] = new_classname
484
+
485
+ if "module" in self.options:
486
+ modules = self.env.ref_context.setdefault("lua:modules", [])
487
+ modules.append(self.env.ref_context.get("lua:module"))
488
+ self.env.ref_context["lua:module"] = self.options["module"]
489
+
490
+ def after_content(self) -> None:
491
+ if self.names and self.allow_nesting:
492
+ classes = self.env.ref_context.setdefault("lua:classes", [])
493
+ self.env.ref_context["lua:class"] = classes.pop() if classes else None
494
+
495
+ if "module" in self.options:
496
+ modules = self.env.ref_context.setdefault("lua:modules", [])
497
+ self.env.ref_context["lua:module"] = modules.pop() if modules else None
498
+
499
+
500
+ class LuaFunction(LuaObject[tuple[list[tuple[str, str]], list[tuple[str, str]]]]):
501
+ """
502
+ Everything that looks like a function: functions, methods, static and class methods.
503
+
504
+ I.e. everything with signature ``name(params) -> returns``.
505
+
506
+ """
507
+
508
+ def parse_signature(self, sig):
509
+ if match := _OBJECT_NAME_RE.match(sig):
510
+ name = re.sub(r"\s", "", match.group())
511
+ sig = sig[match.span()[1] :]
512
+ else:
513
+ raise ValueError("Incorrect function name")
514
+
515
+ params, returns = _separate_paren_prefix(sig)
516
+
517
+ if returns and returns.startswith("->"):
518
+ returns = returns[2:].lstrip()
519
+ elif returns:
520
+ raise ValueError("Incorrect function return type")
521
+
522
+ if returns.startswith("(") and returns.endswith(")"):
523
+ returns = returns[1:-1]
524
+
525
+ return name, (
526
+ _parse_types(params, parsingFunctionParams=True),
527
+ _parse_types(returns),
528
+ )
529
+
530
+ @_handle_signature_errors
531
+ def handle_signature(
532
+ self, sig: str, signode: addnodes.desc_signature
533
+ ) -> tuple[str, str, str, str]:
534
+ (
535
+ fullname,
536
+ modname,
537
+ classname,
538
+ name,
539
+ (args, returns),
540
+ ) = self.handle_signature_prefix(sig, signode)
541
+
542
+ if not args:
543
+ if self.needs_arg_list():
544
+ signode += addnodes.desc_parameterlist()
545
+ else:
546
+ parameterslist = addnodes.desc_parameterlist()
547
+ signode += parameterslist
548
+ for arg, typ in args:
549
+ if arg and typ and typ.endswith("?"):
550
+ arg, typ = arg + "?", typ[:-1]
551
+ if typ.startswith("(") and typ.endswith(")"):
552
+ typ = typ[1:-1]
553
+
554
+ parameter = addnodes.desc_parameter()
555
+
556
+ parameter += addnodes.desc_sig_name(arg, arg)
557
+ if typ:
558
+ parameter += addnodes.desc_sig_punctuation(":", ":")
559
+ parameter += addnodes.desc_sig_space()
560
+ parameter += _type_to_nodes(typ, self.state.inliner)
561
+
562
+ parameterslist += parameter
563
+
564
+ if returns:
565
+ retnode = addnodes.desc_returns()
566
+ signode += retnode
567
+
568
+ for i, (arg, typ) in enumerate(returns):
569
+ if arg and typ and typ.endswith("?"):
570
+ arg, typ = arg + "?", typ[:-1]
571
+ if typ.startswith("(") and typ.endswith(")"):
572
+ typ = typ[1:-1]
573
+
574
+ if arg:
575
+ retnode += addnodes.desc_sig_name("", arg or "_")
576
+ retnode += addnodes.desc_sig_punctuation(":", ":")
577
+ retnode += addnodes.desc_sig_space()
578
+ retnode += _type_to_nodes(typ, self.state.inliner)
579
+ if i + 1 < len(returns):
580
+ retnode += addnodes.desc_sig_punctuation(",", ",")
581
+ retnode += addnodes.desc_sig_space()
582
+
583
+ return fullname, modname, classname, name
584
+
585
+ def needs_arg_list(self) -> bool:
586
+ return True
587
+
588
+ def use_semicolon_path(self) -> bool:
589
+ return self.objtype in ("method", "classmethod")
590
+
591
+ def get_signature_prefix(self, signature: str) -> list[nodes.Node]:
592
+ prefix = super().get_signature_prefix(signature)
593
+ if self.objtype not in ("function", "method"):
594
+ prefix.extend(
595
+ [
596
+ addnodes.desc_sig_keyword("", self.objtype),
597
+ addnodes.desc_sig_space(),
598
+ ]
599
+ )
600
+ return prefix
601
+
602
+
603
+ class LuaData(LuaObject[str]):
604
+ """
605
+ Variables and other things that have type annotations in their signature.
606
+
607
+ I.e. everything with signature ``name type``.
608
+
609
+ """
610
+
611
+ def parse_signature(self, sig):
612
+ if match := _OBJECT_NAME_RE.match(sig):
613
+ name = re.sub(r"\s", "", match.group())
614
+ sig = sig[match.span()[1] :]
615
+ else:
616
+ raise ValueError("Incorrect data name")
617
+
618
+ if sig.startswith("=") or sig.startswith(":"):
619
+ sig = sig[1:]
620
+
621
+ return name, sig.strip()
622
+
623
+ @_handle_signature_errors
624
+ def handle_signature(
625
+ self, sig: str, signode: addnodes.desc_signature
626
+ ) -> tuple[str, str, str, str]:
627
+ fullname, modname, classname, name, typ = self.handle_signature_prefix(
628
+ sig, signode
629
+ )
630
+
631
+ if typ:
632
+ signode += addnodes.desc_sig_punctuation(":", ":")
633
+ signode += addnodes.desc_sig_space()
634
+ signode += addnodes.desc_type(
635
+ "", "", *_type_to_nodes(typ, self.state.inliner)
636
+ )
637
+
638
+ return fullname, modname, classname, name
639
+
640
+ def get_signature_prefix(self, signature: str) -> list[nodes.Node]:
641
+ prefix = super().get_signature_prefix(signature)
642
+ if self.objtype not in ("data", "attribute"):
643
+ prefix.extend(
644
+ [
645
+ addnodes.desc_sig_keyword("", self.objtype),
646
+ addnodes.desc_sig_space(),
647
+ ]
648
+ )
649
+ return prefix
650
+
651
+
652
+ class LuaAlias(LuaObject[str]):
653
+ """
654
+ Type aliases and other things that have type assignments in their signature.
655
+
656
+ I.e. everything with signature ``name type``.
657
+
658
+ """
659
+
660
+ allow_nesting = True
661
+
662
+ def parse_signature(self, sig):
663
+ if match := _OBJECT_NAME_RE.match(sig):
664
+ name = re.sub(r"\s", "", match.group())
665
+ sig = sig[match.span()[1] :]
666
+ else:
667
+ raise ValueError("Incorrect alias name")
668
+
669
+ if sig.startswith("=") or sig.startswith(":"):
670
+ sig = sig[1:]
671
+
672
+ return name, sig.strip()
673
+
674
+ @_handle_signature_errors
675
+ def handle_signature(
676
+ self, sig: str, signode: addnodes.desc_signature
677
+ ) -> tuple[str, str, str, str]:
678
+ fullname, modname, classname, name, typ = self.handle_signature_prefix(
679
+ sig, signode
680
+ )
681
+
682
+ if typ:
683
+ signode += addnodes.desc_sig_space()
684
+ signode += addnodes.desc_sig_punctuation("=", "=")
685
+ signode += addnodes.desc_sig_space()
686
+ signode += addnodes.desc_type(
687
+ "", "", *_type_to_nodes(typ, self.state.inliner)
688
+ )
689
+
690
+ return fullname, modname, classname, name
691
+
692
+ def get_signature_prefix(self, signature: str) -> list[nodes.Node]:
693
+ prefix = super().get_signature_prefix(signature)
694
+ prefix.extend(
695
+ [
696
+ addnodes.desc_sig_keyword("", self.objtype),
697
+ addnodes.desc_sig_space(),
698
+ ]
699
+ )
700
+ return prefix
701
+
702
+
703
+ class LuaClass(LuaObject[list[str]]):
704
+ """
705
+ Classes and other things that have base types in their signature.
706
+
707
+ I.e. everything with signature ``name: base1, base2, ...``.
708
+
709
+ These are nested.
710
+
711
+ """
712
+
713
+ allow_nesting = True
714
+
715
+ def parse_signature(self, sig):
716
+ if match := _OBJECT_NAME_RE.match(sig):
717
+ name = re.sub(r"\s", "", match.group())
718
+ sig = sig[match.span()[1] :]
719
+ else:
720
+ raise ValueError("Incorrect data name")
721
+
722
+ if sig.startswith("=") or sig.startswith(":"):
723
+ sig = sig[1:]
724
+
725
+ return name, _separate_sig(sig)
726
+
727
+ @_handle_signature_errors
728
+ def handle_signature(
729
+ self, sig: str, signode: addnodes.desc_signature
730
+ ) -> tuple[str, str, str, str]:
731
+ fullname, modname, classname, name, bases = self.handle_signature_prefix(
732
+ sig, signode
733
+ )
734
+
735
+ if bases:
736
+ signode += addnodes.desc_sig_space()
737
+ signode += addnodes.desc_sig_punctuation(":", ":")
738
+ signode += addnodes.desc_sig_space()
739
+
740
+ sep = False
741
+ for typ in bases:
742
+ if sep:
743
+ signode += addnodes.desc_sig_punctuation(",", ",")
744
+ signode += addnodes.desc_sig_space()
745
+ signode += addnodes.desc_type(
746
+ "", "", *_type_to_nodes(typ, self.state.inliner)
747
+ )
748
+ sep = True
749
+
750
+ return fullname, modname, classname, name
751
+
752
+ def get_signature_prefix(self, signature: str) -> list[nodes.Node]:
753
+ prefix = super().get_signature_prefix(signature)
754
+ prefix.extend(
755
+ [
756
+ addnodes.desc_sig_keyword("", self.objtype),
757
+ addnodes.desc_sig_space(),
758
+ ]
759
+ )
760
+ return prefix
761
+
762
+
763
+ class LuaModule(SphinxDirective):
764
+ """
765
+ Directive to mark description of a new module.
766
+ """
767
+
768
+ has_content = False
769
+ required_arguments = 1
770
+ optional_arguments = 0
771
+ final_argument_whitespace = False
772
+ option_spec = {
773
+ "platform": lambda x: x,
774
+ "synopsis": lambda x: x,
775
+ "no-index": directives.flag,
776
+ "deprecated": directives.flag,
777
+ }
778
+
779
+ def run(self) -> list[nodes.Node]:
780
+ for name, option in self.env.config["lua_ls_default_options"].items():
781
+ if name not in self.options:
782
+ self.options[name] = option
783
+
784
+ sig = self.arguments[0]
785
+ if match := _OBJECT_NAME_RE.match(sig):
786
+ modname = re.sub(r"\s", "", match.group())
787
+ sig = sig[match.span()[1] :]
788
+ if sig:
789
+ raise ValueError("Unexpected symbols after module name")
790
+ else:
791
+ raise ValueError("Incorrect module name")
792
+
793
+ env = self.state.document.settings.env
794
+ no_index = "no-index" in self.options
795
+ env.ref_context["lua:module"] = modname
796
+ ret = []
797
+ if not no_index:
798
+ env.domaindata["lua"]["modules"][modname] = (
799
+ env.docname,
800
+ self.options.get("synopsis", ""),
801
+ self.options.get("platform", ""),
802
+ "deprecated" in self.options,
803
+ )
804
+ # make a duplicate entry in 'objects' to facilitate searching for
805
+ # the module in LuaDomain.find_obj()
806
+ env.domaindata["lua"]["objects"][modname] = (env.docname, "module")
807
+ target_node = nodes.target("", "", ids=["lua:" + modname], ismod=True)
808
+ self.state.document.note_explicit_target(target_node)
809
+ # the platform and synopsis aren't printed; in fact, they are only
810
+ # used in the modindex currently
811
+ ret.append(target_node)
812
+ indextext = _("%s (module)") % modname
813
+ inode = addnodes.index(
814
+ entries=[("single", indextext, "lua:" + modname, "", None)]
815
+ )
816
+ ret.append(inode)
817
+ return ret
818
+
819
+
820
+ class LuaCurrentModule(SphinxDirective):
821
+ """
822
+ This directive is just to tell Sphinx that we're documenting
823
+ stuff in module foo, but links to module foo won't lead here.
824
+ """
825
+
826
+ has_content = False
827
+ required_arguments = 1
828
+ optional_arguments = 0
829
+ final_argument_whitespace = False
830
+ option_spec = {}
831
+
832
+ def run(self) -> list[nodes.Node]:
833
+ sig = self.arguments[0]
834
+ if match := _OBJECT_NAME_RE.match(sig):
835
+ modname = re.sub(r"\s", "", match.group())
836
+ sig = sig[match.span()[1] :]
837
+ if sig:
838
+ raise ValueError("Unexpected symbols after module name")
839
+ else:
840
+ raise ValueError("Incorrect module name")
841
+
842
+ env = self.state.document.settings.env
843
+ if modname == "None":
844
+ env.ref_context.pop("lua:module", None)
845
+ else:
846
+ env.ref_context["lua:module"] = modname
847
+ return []
848
+
849
+
850
+ class LuaXRefRole(XRefRole):
851
+ def process_link(
852
+ self,
853
+ env: BuildEnvironment,
854
+ refnode: nodes.Element,
855
+ has_explicit_title: bool,
856
+ title: str,
857
+ target: str,
858
+ ) -> tuple[str, str]:
859
+ refnode["lua:module"] = env.ref_context.get("lua:module")
860
+ refnode["lua:class"] = env.ref_context.get("lua:class")
861
+ if not has_explicit_title:
862
+ title = title.lstrip(".") # only has a meaning for the target
863
+ target = target.lstrip("~") # only has a meaning for the title
864
+ # if the first character is a tilde, don't display the module/class
865
+ # parts of the contents
866
+ if title[0:1] == "~":
867
+ title = title[1:]
868
+ dot = title.rfind(".")
869
+ if dot != -1:
870
+ title = title[dot + 1 :]
871
+ return title, target
872
+
873
+
874
+ class LuaModuleIndex(Index):
875
+ """
876
+ Index subclass to provide the Lua module index.
877
+ """
878
+
879
+ name = "modindex"
880
+ localname = _("Lua Module Index")
881
+ shortname = _("modules")
882
+
883
+ def generate(
884
+ self, docnames: Iterable[str] | None = None
885
+ ) -> tuple[list[tuple[str, list[IndexEntry]]], bool]:
886
+ content: dict[str, list[IndexEntry]] = {}
887
+ # list of prefixes to ignore
888
+ ignores = self.domain.env.config["modindex_common_prefix"]
889
+ ignores = sorted(ignores, key=len, reverse=True)
890
+ # list of all modules, sorted by module name
891
+ modules: list[tuple[str, tuple[str, str, str, bool]]] = sorted(
892
+ self.domain.data["modules"].items(), key=lambda x: x[0].lower()
893
+ )
894
+ # sort out collapsable modules
895
+ prev_modname = ""
896
+ num_top_levels = 0
897
+ for modname, (docname, synopsis, platforms, deprecated) in modules:
898
+ if docnames and docname not in docnames:
899
+ continue
900
+ if not modname:
901
+ continue
902
+
903
+ for ignore in ignores:
904
+ if modname.startswith(ignore):
905
+ modname = modname[len(ignore) :]
906
+ stripped = ignore
907
+ break
908
+ else:
909
+ stripped = ""
910
+
911
+ # we stripped the whole module name?
912
+ if not modname:
913
+ modname, stripped = stripped, ""
914
+
915
+ entries = content.setdefault(modname[0].lower(), [])
916
+
917
+ package = modname.split(".")[0]
918
+ if package != modname:
919
+ # it's a submodule
920
+ if prev_modname == package:
921
+ # first submodule - make parent a group head
922
+ if entries:
923
+ name, _subtype, *rest = entries[-1]
924
+ entries[-1] = IndexEntry(name, 1, *rest)
925
+ elif not prev_modname.startswith(package):
926
+ # submodule without parent in list, add dummy entry
927
+ entries.append(
928
+ IndexEntry(stripped + package, 1, "", "", "", "", "")
929
+ )
930
+ subtype = 2
931
+ else:
932
+ num_top_levels += 1
933
+ subtype = 0
934
+
935
+ qualifier = deprecated and _("Deprecated") or ""
936
+ entries.append(
937
+ IndexEntry(
938
+ stripped + modname,
939
+ subtype,
940
+ docname,
941
+ "lua:" + stripped + modname,
942
+ platforms,
943
+ qualifier,
944
+ synopsis,
945
+ )
946
+ )
947
+ prev_modname = modname
948
+
949
+ # apply heuristics when to collapse modindex at page load:
950
+ # only collapse if number of toplevel modules is larger than
951
+ # number of submodules
952
+ collapse = len(modules) - num_top_levels < num_top_levels
953
+
954
+ # sort by first letter
955
+ sorted_content = sorted(content.items())
956
+
957
+ return sorted_content, collapse
958
+
959
+
960
+ class LuaDomain(Domain):
961
+ """Lua language domain."""
962
+
963
+ name = "lua"
964
+ label = "Lua"
965
+ object_types: dict[str, ObjType] = {
966
+ "function": ObjType(_("function"), "func", "obj"),
967
+ "data": ObjType(_("data"), "data", "obj"),
968
+ "const": ObjType(_("const"), "const", "obj"),
969
+ "class": ObjType(_("class"), "class", "obj"),
970
+ "alias": ObjType(_("alias"), "alias", "obj"),
971
+ "method": ObjType(_("method"), "meth", "obj"),
972
+ "classmethod": ObjType(_("class method"), "meth", "obj"),
973
+ "staticmethod": ObjType(_("static method"), "meth", "obj"),
974
+ "attribute": ObjType(_("attribute"), "attr", "obj"),
975
+ "module": ObjType(_("module"), "mod", "obj"),
976
+ }
977
+
978
+ directives = {
979
+ "function": LuaFunction,
980
+ "data": LuaData,
981
+ "const": LuaData,
982
+ "class": LuaClass,
983
+ "alias": LuaAlias,
984
+ "method": LuaFunction,
985
+ "classmethod": LuaFunction,
986
+ "staticmethod": LuaFunction,
987
+ "attribute": LuaData,
988
+ "module": LuaModule,
989
+ "currentmodule": LuaCurrentModule,
990
+ }
991
+ roles = {
992
+ "func": LuaXRefRole(),
993
+ "data": LuaXRefRole(),
994
+ "const": LuaXRefRole(),
995
+ "class": LuaXRefRole(),
996
+ "alias": LuaXRefRole(),
997
+ "meth": LuaXRefRole(),
998
+ "attr": LuaXRefRole(),
999
+ "mod": LuaXRefRole(),
1000
+ "obj": LuaXRefRole(),
1001
+ }
1002
+ initial_data: dict[str, dict[str, tuple[Any]]] = {
1003
+ "objects": {}, # fullname -> docname, objtype
1004
+ "modules": {}, # modname -> docname, synopsis, platform, deprecated
1005
+ }
1006
+ indices = [
1007
+ LuaModuleIndex,
1008
+ ]
1009
+
1010
+ def clear_doc(self, docname: str) -> None:
1011
+ for fullname, (fn, _l) in list(self.data["objects"].items()):
1012
+ if fn == docname:
1013
+ del self.data["objects"][fullname]
1014
+ for modname, (fn, _x, _x, _x) in list(self.data["modules"].items()):
1015
+ if fn == docname:
1016
+ del self.data["modules"][modname]
1017
+
1018
+ def merge_domaindata(self, docnames: Set[str], otherdata: dict[Any, Any]) -> None:
1019
+ # XXX check duplicates?
1020
+ for fullname, (fn, objtype) in otherdata["objects"].items():
1021
+ if fn in docnames:
1022
+ self.data["objects"][fullname] = (fn, objtype)
1023
+ for modname, data in otherdata["modules"].items():
1024
+ if data[0] in docnames:
1025
+ self.data["modules"][modname] = data
1026
+
1027
+ def _find_obj(
1028
+ self, modname: str, classname: str, name: str, typ: str | None
1029
+ ) -> tuple[str, Any] | None:
1030
+ if name[-2:] == "()":
1031
+ name = name[:-2]
1032
+
1033
+ if not name:
1034
+ return None
1035
+
1036
+ objects = self.data["objects"]
1037
+
1038
+ if typ == "mod":
1039
+ candidates = [[name]]
1040
+ else:
1041
+ candidates = [
1042
+ [modname, classname, name],
1043
+ [modname, name],
1044
+ [name],
1045
+ ]
1046
+
1047
+ if typ in ("func", "meth") and "." not in name:
1048
+ candidates.append(["object", name])
1049
+
1050
+ for candidate in candidates:
1051
+ path = ".".join(filter(None, candidate))
1052
+ if path in objects:
1053
+ return path, objects[path]
1054
+
1055
+ return None
1056
+
1057
+ def resolve_xref(
1058
+ self,
1059
+ env: BuildEnvironment,
1060
+ fromdocname: str,
1061
+ builder: Builder,
1062
+ typ: str,
1063
+ target: str,
1064
+ node: addnodes.pending_xref,
1065
+ contnode: nodes.Node,
1066
+ ) -> nodes.reference | None:
1067
+ modname = node.get("lua:module")
1068
+ classname = node.get("lua:class")
1069
+ if match := self._find_obj(modname, classname, target, typ):
1070
+ name, (docname, objtype) = match
1071
+ if typ not in ("any", "obj") and typ not in objtype:
1072
+ logger.warning(
1073
+ "reference :lua:%s:`%s` resolved to an object of unexpected type %r",
1074
+ typ,
1075
+ target,
1076
+ objtype,
1077
+ type="lua-ls",
1078
+ location=(node.source, node.line),
1079
+ )
1080
+ if (
1081
+ isinstance(contnode, nodes.literal)
1082
+ and not node["refexplicit"]
1083
+ and len(contnode.children) == 1
1084
+ and isinstance(contnode.children[0], nodes.Text)
1085
+ ):
1086
+ title = contnode.astext()
1087
+ new_title = title
1088
+ if objtype in (
1089
+ "function",
1090
+ "method",
1091
+ "classmethod",
1092
+ "staticmethod",
1093
+ ) and not new_title.endswith("()"):
1094
+ new_title += "()"
1095
+ if objtype in ("method", "classmethod"):
1096
+ i = new_title.rfind(".")
1097
+ if i != -1:
1098
+ new_title = new_title[:i] + ":" + new_title[i + 1 :]
1099
+ if new_title != title:
1100
+ contnode = contnode.deepcopy()
1101
+ contnode.clear()
1102
+ contnode += nodes.Text(new_title)
1103
+ return make_refnode(
1104
+ builder, fromdocname, docname, "lua:" + name, contnode, name
1105
+ )
1106
+
1107
+ def resolve_any_xref(
1108
+ self,
1109
+ env: BuildEnvironment,
1110
+ fromdocname: str,
1111
+ builder: Builder,
1112
+ target: str,
1113
+ node: addnodes.pending_xref,
1114
+ contnode: nodes.Node,
1115
+ ) -> list[tuple[str, nodes.reference]]:
1116
+ modname = node.get("lua:module")
1117
+ classname = node.get("lua:class")
1118
+ if match := self._find_obj(modname, classname, target, None):
1119
+ name, (docname, objtype) = match
1120
+ role = "lua:" + (self.role_for_objtype(objtype, None) or "obj")
1121
+ return [
1122
+ (
1123
+ role,
1124
+ make_refnode(
1125
+ builder, fromdocname, docname, "lua:" + name, contnode, name
1126
+ ),
1127
+ )
1128
+ ]
1129
+
1130
+ return []
1131
+
1132
+ def get_objects(self) -> Iterator[tuple[str, str, str, str, str, int]]:
1133
+ for modname, info in self.data["modules"].items():
1134
+ yield (modname, modname, "module", info[0], "lua:" + modname, 0)
1135
+ for refname, (docname, type) in self.data["objects"].items():
1136
+ if type != "module": # modules are already handled
1137
+ yield (refname, refname, type, docname, refname, 1)
1138
+
1139
+ def get_full_qualified_name(self, node: nodes.Element) -> str | None:
1140
+ modname = node.get("lua:module")
1141
+ classname = node.get("lua:class")
1142
+ target = node.get("reftarget")
1143
+ if target is None:
1144
+ return None
1145
+ else:
1146
+ return ".".join(filter(None, [modname, classname, target]))
1147
+
1148
+
1149
+ def setup(app):
1150
+ app.add_domain(LuaDomain)
1151
+
1152
+ return {
1153
+ "version": "builtin",
1154
+ "parallel_read_safe": True,
1155
+ "parallel_write_safe": True,
1156
+ }