1# Copyright 2024 The Bazel Authors. All rights reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14"""Sphinx extension for documenting Bazel/Starlark objects.""" 15 16import ast 17import collections 18import enum 19import os 20import typing 21from collections.abc import Collection 22from typing import Callable, Iterable, TypeVar 23 24from docutils import nodes as docutils_nodes 25from docutils.parsers.rst import directives as docutils_directives 26from docutils.parsers.rst import states 27from sphinx import addnodes, builders 28from sphinx import directives as sphinx_directives 29from sphinx import domains, environment, roles 30from sphinx.highlighting import lexer_classes 31from sphinx.locale import _ 32from sphinx.util import docfields 33from sphinx.util import docutils as sphinx_docutils 34from sphinx.util import inspect, logging 35from sphinx.util import nodes as sphinx_nodes 36from sphinx.util import typing as sphinx_typing 37from typing_extensions import TypeAlias, override 38 39_logger = logging.getLogger(__name__) 40_LOG_PREFIX = f"[{_logger.name}] " 41 42_INDEX_SUBTYPE_NORMAL = 0 43_INDEX_SUBTYPE_ENTRY_WITH_SUB_ENTRIES = 1 44_INDEX_SUBTYPE_SUB_ENTRY = 2 45 46_T = TypeVar("_T") 47 48# See https://www.sphinx-doc.org/en/master/extdev/domainapi.html#sphinx.domains.Domain.get_objects 49_GetObjectsTuple: TypeAlias = tuple[str, str, str, str, str, int] 50 51# See SphinxRole.run definition; the docs for role classes are pretty sparse. 52_RoleRunResult: TypeAlias = tuple[ 53 list[docutils_nodes.Node], list[docutils_nodes.system_message] 54] 55 56 57def _log_debug(message, *args): 58 # NOTE: Non-warning log messages go to stdout and are only 59 # visible when -q isn't passed to Sphinx. Note that the sphinx_docs build 60 # rule passes -q by default; use --//sphinxdocs:quiet=false to disable it. 61 _logger.debug("%s" + message, _LOG_PREFIX, *args) 62 63 64def _position_iter(values: Collection[_T]) -> tuple[bool, bool, _T]: 65 last_i = len(values) - 1 66 for i, value in enumerate(values): 67 yield i == 0, i == last_i, value 68 69 70class InvalidValueError(Exception): 71 """Generic error for an invalid value instead of ValueError. 72 73 Sphinx treats regular ValueError to mean abort parsing the current 74 chunk and continue on as best it can. Their error means a more 75 fundamental problem that should cause a failure. 76 """ 77 78 79class _ObjectEntry: 80 """Metadata about a known object.""" 81 82 def __init__( 83 self, 84 full_id: str, 85 display_name: str, 86 object_type: str, 87 search_priority: int, 88 index_entry: domains.IndexEntry, 89 ): 90 """Creates an instance. 91 92 Args: 93 full_id: The fully qualified id of the object. Should be 94 globally unique, even between projects. 95 display_name: What to display the object as in casual context. 96 object_type: The type of object, typically one of the values 97 known to the domain. 98 search_priority: The search priority, see 99 https://www.sphinx-doc.org/en/master/extdev/domainapi.html#sphinx.domains.Domain.get_objects 100 for valid values. 101 index_entry: Metadata about the object for the domain index. 102 """ 103 self.full_id = full_id 104 self.display_name = display_name 105 self.object_type = object_type 106 self.search_priority = search_priority 107 self.index_entry = index_entry 108 109 def to_get_objects_tuple(self) -> _GetObjectsTuple: 110 # For the tuple definition 111 return ( 112 self.full_id, 113 self.display_name, 114 self.object_type, 115 self.index_entry.docname, 116 self.index_entry.anchor, 117 self.search_priority, 118 ) 119 120 def __repr__(self): 121 return f"ObjectEntry({self.full_id=}, {self.object_type=}, {self.display_name=}, {self.index_entry.docname=})" 122 123 124# A simple helper just to document what the index tuple nodes are. 125def _index_node_tuple( 126 entry_type: str, 127 entry_name: str, 128 target: str, 129 main: typing.Union[str, None] = None, 130 category_key: typing.Union[str, None] = None, 131) -> tuple[str, str, str, typing.Union[str, None], typing.Union[str, None]]: 132 # For this tuple definition, see: 133 # https://www.sphinx-doc.org/en/master/extdev/nodes.html#sphinx.addnodes.index 134 # For the definition of entry_type, see: 135 # And https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-index 136 return (entry_type, entry_name, target, main, category_key) 137 138 139class _BzlObjectId: 140 """Identifies an object defined by a directive. 141 142 This object is returned by `handle_signature()` and passed onto 143 `add_target_and_index()`. It contains information to identify the object 144 that is being described so that it can be indexed and tracked by the 145 domain. 146 """ 147 148 def __init__( 149 self, 150 *, 151 repo: str, 152 label: str, 153 namespace: str = None, 154 symbol: str = None, 155 ): 156 """Creates an instance. 157 158 Args: 159 repo: repository name, including leading "@". 160 bzl_file: label of file containing the object, e.g. //foo:bar.bzl 161 namespace: dotted name of the namespace the symbol is within. 162 symbol: dotted name, relative to `namespace` of the symbol. 163 """ 164 if not repo: 165 raise InvalidValueError("repo cannot be empty") 166 if not repo.startswith("@"): 167 raise InvalidValueError("repo must start with @") 168 if not label: 169 raise InvalidValueError("label cannot be empty") 170 if not label.startswith("//"): 171 raise InvalidValueError("label must start with //") 172 173 if not label.endswith(".bzl") and (symbol or namespace): 174 raise InvalidValueError( 175 "Symbol and namespace can only be specified for .bzl labels" 176 ) 177 178 self.repo = repo 179 self.label = label 180 self.package, self.target_name = self.label.split(":") 181 self.namespace = namespace 182 self.symbol = symbol # Relative to namespace 183 # doc-relative identifier for this object 184 self.doc_id = symbol or self.target_name 185 186 if not self.doc_id: 187 raise InvalidValueError("doc_id is empty") 188 189 self.full_id = _full_id_from_parts(repo, label, [namespace, symbol]) 190 191 @classmethod 192 def from_env( 193 cls, env: environment.BuildEnvironment, *, symbol: str = None, label: str = None 194 ) -> "_BzlObjectId": 195 label = label or env.ref_context["bzl:file"] 196 if symbol: 197 namespace = ".".join(env.ref_context["bzl:doc_id_stack"]) 198 else: 199 namespace = None 200 201 return cls( 202 repo=env.ref_context["bzl:repo"], 203 label=label, 204 namespace=namespace, 205 symbol=symbol, 206 ) 207 208 def __repr__(self): 209 return f"_BzlObjectId({self.full_id=})" 210 211 212def _full_id_from_env(env, object_ids=None): 213 return _full_id_from_parts( 214 env.ref_context["bzl:repo"], 215 env.ref_context["bzl:file"], 216 env.ref_context["bzl:object_id_stack"] + (object_ids or []), 217 ) 218 219 220def _full_id_from_parts(repo, bzl_file, symbol_names=None): 221 parts = [repo, bzl_file] 222 223 symbol_names = symbol_names or [] 224 symbol_names = list(filter(None, symbol_names)) # Filter out empty values 225 if symbol_names: 226 parts.append("%") 227 parts.append(".".join(symbol_names)) 228 229 full_id = "".join(parts) 230 return full_id 231 232 233def _parse_full_id(full_id): 234 repo, slashes, label = full_id.partition("//") 235 label = slashes + label 236 label, _, symbol = label.partition("%") 237 return (repo, label, symbol) 238 239 240class _TypeExprParser(ast.NodeVisitor): 241 """Parsers a string description of types to doc nodes.""" 242 243 def __init__(self, make_xref: Callable[[str], docutils_nodes.Node]): 244 self.root_node = addnodes.desc_inline("bzl", classes=["type-expr"]) 245 self.make_xref = make_xref 246 self._doc_node_stack = [self.root_node] 247 248 @classmethod 249 def xrefs_from_type_expr( 250 cls, 251 type_expr_str: str, 252 make_xref: Callable[[str], docutils_nodes.Node], 253 ) -> docutils_nodes.Node: 254 module = ast.parse(type_expr_str) 255 visitor = cls(make_xref) 256 visitor.visit(module.body[0]) 257 return visitor.root_node 258 259 def _append(self, node: docutils_nodes.Node): 260 self._doc_node_stack[-1] += node 261 262 def _append_and_push(self, node: docutils_nodes.Node): 263 self._append(node) 264 self._doc_node_stack.append(node) 265 266 def visit_Attribute(self, node: ast.Attribute): 267 current = node 268 parts = [] 269 while current: 270 if isinstance(current, ast.Attribute): 271 parts.append(current.attr) 272 current = current.value 273 elif isinstance(current, ast.Name): 274 parts.append(current.id) 275 break 276 else: 277 raise InvalidValueError(f"Unexpected Attribute.value node: {current}") 278 dotted_name = ".".join(reversed(parts)) 279 self._append(self.make_xref(dotted_name)) 280 281 def visit_Constant(self, node: ast.Constant): 282 if node.value is None: 283 self._append(self.make_xref("None")) 284 elif isinstance(node.value, str): 285 self._append(self.make_xref(node.value)) 286 else: 287 raise InvalidValueError( 288 f"Unexpected Constant node value: ({type(node.value)}) {node.value=}" 289 ) 290 291 def visit_Name(self, node: ast.Name): 292 xref_node = self.make_xref(node.id) 293 self._append(xref_node) 294 295 def visit_BinOp(self, node: ast.BinOp): 296 self.visit(node.left) 297 self._append(addnodes.desc_sig_space()) 298 if isinstance(node.op, ast.BitOr): 299 self._append(addnodes.desc_sig_punctuation("", "|")) 300 else: 301 raise InvalidValueError(f"Unexpected BinOp: {node}") 302 self._append(addnodes.desc_sig_space()) 303 self.visit(node.right) 304 305 def visit_Expr(self, node: ast.Expr): 306 self.visit(node.value) 307 308 def visit_Subscript(self, node: ast.Subscript): 309 self.visit(node.value) 310 self._append_and_push(addnodes.desc_type_parameter_list()) 311 self.visit(node.slice) 312 self._doc_node_stack.pop() 313 314 def visit_Tuple(self, node: ast.Tuple): 315 for element in node.elts: 316 self._append_and_push(addnodes.desc_type_parameter()) 317 self.visit(element) 318 self._doc_node_stack.pop() 319 320 def visit_List(self, node: ast.List): 321 self._append_and_push(addnodes.desc_type_parameter_list()) 322 for element in node.elts: 323 self._append_and_push(addnodes.desc_type_parameter()) 324 self.visit(element) 325 self._doc_node_stack.pop() 326 327 @override 328 def generic_visit(self, node): 329 raise InvalidValueError(f"Unexpected ast node: {type(node)} {node}") 330 331 332class _BzlXrefField(docfields.Field): 333 """Abstract base class to create cross references for fields.""" 334 335 @override 336 def make_xrefs( 337 self, 338 rolename: str, 339 domain: str, 340 target: str, 341 innernode: type[sphinx_typing.TextlikeNode] = addnodes.literal_emphasis, 342 contnode: typing.Union[docutils_nodes.Node, None] = None, 343 env: typing.Union[environment.BuildEnvironment, None] = None, 344 inliner: typing.Union[states.Inliner, None] = None, 345 location: typing.Union[docutils_nodes.Element, None] = None, 346 ) -> list[docutils_nodes.Node]: 347 if rolename in ("arg", "attr"): 348 return self._make_xrefs_for_arg_attr( 349 rolename, domain, target, innernode, contnode, env, inliner, location 350 ) 351 else: 352 return super().make_xrefs( 353 rolename, domain, target, innernode, contnode, env, inliner, location 354 ) 355 356 def _make_xrefs_for_arg_attr( 357 self, 358 rolename: str, 359 domain: str, 360 arg_name: str, 361 innernode: type[sphinx_typing.TextlikeNode] = addnodes.literal_emphasis, 362 contnode: typing.Union[docutils_nodes.Node, None] = None, 363 env: typing.Union[environment.BuildEnvironment, None] = None, 364 inliner: typing.Union[states.Inliner, None] = None, 365 location: typing.Union[docutils_nodes.Element, None] = None, 366 ) -> list[docutils_nodes.Node]: 367 bzl_file = env.ref_context["bzl:file"] 368 anchor_prefix = ".".join(env.ref_context["bzl:doc_id_stack"]) 369 if not anchor_prefix: 370 raise InvalidValueError( 371 f"doc_id_stack empty when processing arg {arg_name}" 372 ) 373 index_description = f"{arg_name} ({self.name} in {bzl_file}%{anchor_prefix})" 374 anchor_id = f"{anchor_prefix}.{arg_name}" 375 full_id = _full_id_from_env(env, [arg_name]) 376 377 env.get_domain(domain).add_object( 378 _ObjectEntry( 379 full_id=full_id, 380 display_name=arg_name, 381 object_type=self.name, 382 search_priority=1, 383 index_entry=domains.IndexEntry( 384 name=arg_name, 385 subtype=_INDEX_SUBTYPE_NORMAL, 386 docname=env.docname, 387 anchor=anchor_id, 388 extra="", 389 qualifier="", 390 descr=index_description, 391 ), 392 ), 393 # This allows referencing an arg as e.g `funcname.argname` 394 alt_names=[anchor_id], 395 ) 396 397 # Two changes to how arg xrefs are created: 398 # 2. Use the full id instead of base name. This makes it unambiguous 399 # as to what it's referencing. 400 pending_xref = super().make_xref( 401 # The full_id is used as the target so its unambiguious. 402 rolename, 403 domain, 404 f"{arg_name} <{full_id}>", 405 innernode, 406 contnode, 407 env, 408 inliner, 409 location, 410 ) 411 412 wrapper = docutils_nodes.inline(ids=[anchor_id]) 413 414 index_node = addnodes.index( 415 entries=[ 416 _index_node_tuple( 417 "single", f"{self.name}; {index_description}", anchor_id 418 ), 419 _index_node_tuple("single", index_description, anchor_id), 420 ] 421 ) 422 wrapper += index_node 423 wrapper += pending_xref 424 return [wrapper] 425 426 427class _BzlDocField(_BzlXrefField, docfields.Field): 428 """A non-repeated field with xref support.""" 429 430 431class _BzlGroupedField(_BzlXrefField, docfields.GroupedField): 432 """A repeated fieled grouped as a list with xref support.""" 433 434 435class _BzlCsvField(_BzlXrefField): 436 """Field with a CSV list of values.""" 437 438 def __init__(self, *args, body_domain: str = "", **kwargs): 439 super().__init__(*args, **kwargs) 440 self._body_domain = body_domain 441 442 def make_field( 443 self, 444 types: dict[str, list[docutils_nodes.Node]], 445 domain: str, 446 item: tuple, 447 env: environment.BuildEnvironment = None, 448 inliner: typing.Union[states.Inliner, None] = None, 449 location: typing.Union[docutils_nodes.Element, None] = None, 450 ) -> docutils_nodes.field: 451 field_text = item[1][0].astext() 452 parts = [p.strip() for p in field_text.split(",")] 453 field_body = docutils_nodes.field_body() 454 for _, is_last, part in _position_iter(parts): 455 node = self.make_xref( 456 self.bodyrolename, 457 self._body_domain or domain, 458 part, 459 env=env, 460 inliner=inliner, 461 location=location, 462 ) 463 field_body += node 464 if not is_last: 465 field_body += docutils_nodes.Text(", ") 466 467 field_name = docutils_nodes.field_name("", self.label) 468 return docutils_nodes.field("", field_name, field_body) 469 470 471class _BzlCurrentFile(sphinx_docutils.SphinxDirective): 472 """Sets what bzl file following directives are defined in. 473 474 The directive's argument is an absolute Bazel label, e.g. `//foo:bar.bzl` 475 or `@repo//foo:bar.bzl`. The repository portion is optional; if specified, 476 it will override the `bzl_default_repository_name` configuration setting. 477 478 Example MyST usage 479 480 ``` 481 :::{bzl:currentfile} //my:file.bzl 482 ::: 483 ``` 484 """ 485 486 has_content = False 487 required_arguments = 1 488 final_argument_whitespace = False 489 490 @override 491 def run(self) -> list[docutils_nodes.Node]: 492 label = self.arguments[0].strip() 493 repo, slashes, file_label = label.partition("//") 494 file_label = slashes + file_label 495 if not repo: 496 repo = self.env.config.bzl_default_repository_name 497 self.env.ref_context["bzl:repo"] = repo 498 self.env.ref_context["bzl:file"] = file_label 499 self.env.ref_context["bzl:object_id_stack"] = [] 500 self.env.ref_context["bzl:doc_id_stack"] = [] 501 return [] 502 503 504class _BzlAttrInfo(sphinx_docutils.SphinxDirective): 505 has_content = False 506 required_arguments = 1 507 optional_arguments = 0 508 option_spec = { 509 "executable": docutils_directives.flag, 510 "mandatory": docutils_directives.flag, 511 } 512 513 def run(self): 514 content_node = docutils_nodes.paragraph("", "") 515 content_node += docutils_nodes.paragraph( 516 "", "mandatory" if "mandatory" in self.options else "optional" 517 ) 518 if "executable" in self.options: 519 content_node += docutils_nodes.paragraph("", "Must be an executable") 520 521 return [content_node] 522 523 524class _BzlObject(sphinx_directives.ObjectDescription[_BzlObjectId]): 525 """Base class for describing a Bazel/Starlark object. 526 527 This directive takes a single argument: a string name with optional 528 function signature. 529 530 * The name can be a dotted name, e.g. `a.b.foo` 531 * The signature is in Python signature syntax, e.g. `foo(a=x) -> R` 532 * The signature supports default values. 533 * Arg type annotations are not supported; use `{bzl:type}` instead as 534 part of arg/attr documentation. 535 536 Example signatures: 537 * `foo` 538 * `foo(arg1, arg2)` 539 * `foo(arg1, arg2=default) -> returntype` 540 """ 541 542 option_spec = sphinx_directives.ObjectDescription.option_spec | { 543 "origin-key": docutils_directives.unchanged, 544 } 545 546 @override 547 def before_content(self) -> None: 548 symbol_name = self.names[-1].symbol 549 if symbol_name: 550 self.env.ref_context["bzl:object_id_stack"].append(symbol_name) 551 self.env.ref_context["bzl:doc_id_stack"].append(symbol_name) 552 553 @override 554 def transform_content(self, content_node: addnodes.desc_content) -> None: 555 def first_child_with_class_name( 556 root, class_name 557 ) -> typing.Union[None, docutils_nodes.Element]: 558 matches = root.findall( 559 lambda node: isinstance(node, docutils_nodes.Element) 560 and class_name in node["classes"] 561 ) 562 found = next(matches, None) 563 return found 564 565 def match_arg_field_name(node): 566 # fmt: off 567 return ( 568 isinstance(node, docutils_nodes.field_name) 569 and node.astext().startswith(("arg ", "attr ")) 570 ) 571 # fmt: on 572 573 # Move the spans for the arg type and default value to be first. 574 arg_name_fields = list(content_node.findall(match_arg_field_name)) 575 for arg_name_field in arg_name_fields: 576 arg_body_field = arg_name_field.next_node(descend=False, siblings=True) 577 # arg_type_node = first_child_with_class_name(arg_body_field, "arg-type-span") 578 arg_type_node = first_child_with_class_name(arg_body_field, "type-expr") 579 arg_default_node = first_child_with_class_name( 580 arg_body_field, "default-value-span" 581 ) 582 583 # Inserting into the body field itself causes the elements 584 # to be grouped into the paragraph node containing the arg 585 # name (as opposed to the paragraph node containing the 586 # doc text) 587 588 if arg_default_node: 589 arg_default_node.parent.remove(arg_default_node) 590 arg_body_field.insert(0, arg_default_node) 591 592 if arg_type_node: 593 arg_type_node.parent.remove(arg_type_node) 594 decorated_arg_type_node = docutils_nodes.inline( 595 "", 596 "", 597 docutils_nodes.Text("("), 598 arg_type_node, 599 docutils_nodes.Text(") "), 600 classes=["arg-type-span"], 601 ) 602 # arg_body_field.insert(0, arg_type_node) 603 arg_body_field.insert(0, decorated_arg_type_node) 604 605 @override 606 def after_content(self) -> None: 607 if self.names[-1].symbol: 608 self.env.ref_context["bzl:object_id_stack"].pop() 609 self.env.ref_context["bzl:doc_id_stack"].pop() 610 611 # docs on how to build signatures: 612 # https://www.sphinx-doc.org/en/master/extdev/nodes.html#sphinx.addnodes.desc_signature 613 @override 614 def handle_signature( 615 self, sig_text: str, sig_node: addnodes.desc_signature 616 ) -> _BzlObjectId: 617 self._signature_add_object_type(sig_node) 618 619 relative_name, lparen, params_text = sig_text.partition("(") 620 if lparen: 621 params_text = lparen + params_text 622 623 relative_name = relative_name.strip() 624 625 name_prefix, _, base_symbol_name = relative_name.rpartition(".") 626 627 if name_prefix: 628 # Respect whatever the signature wanted 629 display_prefix = name_prefix 630 else: 631 # Otherwise, show the outermost name. This makes ctrl+f finding 632 # for a symbol a bit easier. 633 display_prefix = ".".join(self.env.ref_context["bzl:doc_id_stack"]) 634 _, _, display_prefix = display_prefix.rpartition(".") 635 636 if display_prefix: 637 display_prefix = display_prefix + "." 638 sig_node += addnodes.desc_addname(display_prefix, display_prefix) 639 sig_node += addnodes.desc_name(base_symbol_name, base_symbol_name) 640 641 if type_expr := self.options.get("type"): 642 643 def make_xref(name, title=None): 644 content_node = addnodes.desc_type(name, name) 645 return addnodes.pending_xref( 646 "", 647 content_node, 648 refdomain="bzl", 649 reftype="type", 650 reftarget=name, 651 ) 652 653 attr_annotation_node = addnodes.desc_annotation( 654 type_expr, 655 "", 656 addnodes.desc_sig_punctuation("", ":"), 657 addnodes.desc_sig_space(), 658 _TypeExprParser.xrefs_from_type_expr(type_expr, make_xref), 659 ) 660 sig_node += attr_annotation_node 661 662 if params_text: 663 try: 664 signature = inspect.signature_from_str(params_text) 665 except SyntaxError: 666 # Stardoc doesn't provide accurate info, so the reconstructed 667 # signature might not be valid syntax. Rather than fail, just 668 # provide a plain-text description of the approximate signature. 669 # See https://github.com/bazelbuild/stardoc/issues/225 670 sig_node += addnodes.desc_parameterlist( 671 # Offset by 1 to remove the surrounding parentheses 672 params_text[1:-1], 673 params_text[1:-1], 674 ) 675 else: 676 last_kind = None 677 paramlist_node = addnodes.desc_parameterlist() 678 for param in signature.parameters.values(): 679 if param.kind == param.KEYWORD_ONLY and last_kind in ( 680 param.POSITIONAL_OR_KEYWORD, 681 param.POSITIONAL_ONLY, 682 None, 683 ): 684 # Add separator for keyword only parameter: * 685 paramlist_node += addnodes.desc_parameter( 686 "", "", addnodes.desc_sig_operator("", "*") 687 ) 688 689 last_kind = param.kind 690 node = addnodes.desc_parameter() 691 if param.kind == param.VAR_POSITIONAL: 692 node += addnodes.desc_sig_operator("", "*") 693 elif param.kind == param.VAR_KEYWORD: 694 node += addnodes.desc_sig_operator("", "**") 695 696 node += addnodes.desc_sig_name(rawsource="", text=param.name) 697 if param.default is not param.empty: 698 node += addnodes.desc_sig_operator("", "=") 699 node += docutils_nodes.inline( 700 "", 701 param.default, 702 classes=["default_value"], 703 support_smartquotes=False, 704 ) 705 paramlist_node += node 706 sig_node += paramlist_node 707 708 if signature.return_annotation is not signature.empty: 709 sig_node += addnodes.desc_returns("", signature.return_annotation) 710 711 obj_id = _BzlObjectId.from_env(self.env, symbol=relative_name) 712 713 sig_node["bzl:object_id"] = obj_id.full_id 714 return obj_id 715 716 def _signature_add_object_type(self, sig_node: addnodes.desc_signature): 717 if sig_object_type := self._get_signature_object_type(): 718 sig_node += addnodes.desc_annotation("", self._get_signature_object_type()) 719 sig_node += addnodes.desc_sig_space() 720 721 @override 722 def add_target_and_index( 723 self, obj_desc: _BzlObjectId, sig: str, sig_node: addnodes.desc_signature 724 ) -> None: 725 super().add_target_and_index(obj_desc, sig, sig_node) 726 if obj_desc.symbol: 727 display_name = obj_desc.symbol 728 location = obj_desc.label 729 if obj_desc.namespace: 730 location += f"%{obj_desc.namespace}" 731 else: 732 display_name = obj_desc.target_name 733 location = obj_desc.package 734 735 anchor_prefix = ".".join(self.env.ref_context["bzl:doc_id_stack"]) 736 if anchor_prefix: 737 anchor_id = f"{anchor_prefix}.{obj_desc.doc_id}" 738 else: 739 anchor_id = obj_desc.doc_id 740 741 sig_node["ids"].append(anchor_id) 742 743 object_type_display = self._get_object_type_display_name() 744 index_description = f"{display_name} ({object_type_display} in {location})" 745 self.indexnode["entries"].extend( 746 _index_node_tuple("single", f"{index_type}; {index_description}", anchor_id) 747 for index_type in [object_type_display] + self._get_additional_index_types() 748 ) 749 self.indexnode["entries"].append( 750 _index_node_tuple("single", index_description, anchor_id), 751 ) 752 753 object_entry = _ObjectEntry( 754 full_id=obj_desc.full_id, 755 display_name=display_name, 756 object_type=self.objtype, 757 search_priority=1, 758 index_entry=domains.IndexEntry( 759 name=display_name, 760 subtype=_INDEX_SUBTYPE_NORMAL, 761 docname=self.env.docname, 762 anchor=anchor_id, 763 extra="", 764 qualifier="", 765 descr=index_description, 766 ), 767 ) 768 769 alt_names = [] 770 if origin_key := self.options.get("origin-key"): 771 alt_names.append( 772 origin_key 773 # Options require \@ for leading @, but don't 774 # remove the escaping slash, so we have to do it manually 775 .lstrip("\\") 776 ) 777 extra_alt_names = self._get_alt_names(object_entry) 778 alt_names.extend(extra_alt_names) 779 780 self.env.get_domain(self.domain).add_object(object_entry, alt_names=alt_names) 781 782 def _get_additional_index_types(self): 783 return [] 784 785 @override 786 def _object_hierarchy_parts( 787 self, sig_node: addnodes.desc_signature 788 ) -> tuple[str, ...]: 789 return _parse_full_id(sig_node["bzl:object_id"]) 790 791 @override 792 def _toc_entry_name(self, sig_node: addnodes.desc_signature) -> str: 793 return sig_node["_toc_parts"][-1] 794 795 def _get_object_type_display_name(self) -> str: 796 return self.env.get_domain(self.domain).object_types[self.objtype].lname 797 798 def _get_signature_object_type(self) -> str: 799 return self._get_object_type_display_name() 800 801 def _get_alt_names(self, object_entry): 802 alt_names = [] 803 full_id = object_entry.full_id 804 label, _, symbol = full_id.partition("%") 805 if symbol: 806 # Allow referring to the file-relative fully qualified symbol name 807 alt_names.append(symbol) 808 if "." in symbol: 809 # Allow referring to the last component of the symbol 810 alt_names.append(symbol.split(".")[-1]) 811 else: 812 # Otherwise, it's a target. Allow referring to just the target name 813 _, _, target_name = label.partition(":") 814 alt_names.append(target_name) 815 816 return alt_names 817 818 819class _BzlCallable(_BzlObject): 820 """Abstract base class for objects that are callable.""" 821 822 823class _BzlTypedef(_BzlObject): 824 """Documents a typedef. 825 826 A typedef describes objects with well known attributes. 827 828 ````` 829 ::::{bzl:typedef} Square 830 831 :::{bzl:field} width 832 :type: int 833 ::: 834 835 :::{bzl:function} new(size) 836 ::: 837 838 :::{bzl:function} area() 839 ::: 840 :::: 841 ````` 842 """ 843 844 845class _BzlProvider(_BzlObject): 846 """Documents a provider type. 847 848 Example MyST usage 849 850 ``` 851 ::::{bzl:provider} MyInfo 852 853 Docs about MyInfo 854 855 :::{bzl:provider-field} some_field 856 :type: depset[str] 857 ::: 858 :::: 859 ``` 860 """ 861 862 863class _BzlField(_BzlObject): 864 """Documents a field of a provider. 865 866 Fields can optionally have a type specified using the `:type:` option. 867 868 The type can be any type expression understood by the `{bzl:type}` role. 869 870 ``` 871 :::{bzl:provider-field} foo 872 :type: str 873 ::: 874 ``` 875 """ 876 877 option_spec = _BzlObject.option_spec.copy() 878 option_spec.update( 879 { 880 "type": docutils_directives.unchanged, 881 } 882 ) 883 884 @override 885 def _get_signature_object_type(self) -> str: 886 return "" 887 888 @override 889 def _get_alt_names(self, object_entry): 890 alt_names = super()._get_alt_names(object_entry) 891 _, _, symbol = object_entry.full_id.partition("%") 892 # Allow refering to `mod_ext_name.tag_name`, even if the extension 893 # is nested within another object 894 alt_names.append(".".join(symbol.split(".")[-2:])) 895 return alt_names 896 897 898class _BzlProviderField(_BzlField): 899 pass 900 901 902class _BzlRepositoryRule(_BzlCallable): 903 """Documents a repository rule. 904 905 Doc fields: 906 * attr: Documents attributes of the rule. Takes a single arg, the 907 attribute name. Can be repeated. The special roles `{default-value}` 908 and `{arg-type}` can be used to indicate the default value and 909 type of attribute, respectively. 910 * environment-variables: a CSV list of environment variable names. 911 They will be cross referenced with matching environment variables. 912 913 Example MyST usage 914 915 ``` 916 :::{bzl:repo-rule} myrule(foo) 917 918 :attr foo: {default-value}`"foo"` {arg-type}`attr.string` foo doc string 919 920 :environment-variables: FOO, BAR 921 ::: 922 ``` 923 """ 924 925 doc_field_types = [ 926 _BzlGroupedField( 927 "attr", 928 label=_("Attributes"), 929 names=["attr"], 930 rolename="attr", 931 can_collapse=False, 932 ), 933 _BzlCsvField( 934 "environment-variables", 935 label=_("Environment Variables"), 936 names=["environment-variables"], 937 body_domain="std", 938 bodyrolename="envvar", 939 has_arg=False, 940 ), 941 ] 942 943 @override 944 def _get_signature_object_type(self) -> str: 945 return "repo rule" 946 947 948class _BzlRule(_BzlCallable): 949 """Documents a rule. 950 951 Doc fields: 952 * attr: Documents attributes of the rule. Takes a single arg, the 953 attribute name. Can be repeated. The special roles `{default-value}` 954 and `{arg-type}` can be used to indicate the default value and 955 type of attribute, respectively. 956 * provides: A type expression of the provider types the rule provides. 957 To indicate different groupings, use `|` and `[]`. For example, 958 `FooInfo | [BarInfo, BazInfo]` means it provides either `FooInfo` 959 or both of `BarInfo` and `BazInfo`. 960 961 Example MyST usage 962 963 ``` 964 :::{bzl:repo-rule} myrule(foo) 965 966 :attr foo: {default-value}`"foo"` {arg-type}`attr.string` foo doc string 967 968 :provides: FooInfo | BarInfo 969 ::: 970 ``` 971 """ 972 973 doc_field_types = [ 974 _BzlGroupedField( 975 "attr", 976 label=_("Attributes"), 977 names=["attr"], 978 rolename="attr", 979 can_collapse=False, 980 ), 981 _BzlDocField( 982 "provides", 983 label="Provides", 984 has_arg=False, 985 names=["provides"], 986 bodyrolename="type", 987 ), 988 ] 989 990 991class _BzlAspect(_BzlObject): 992 """Documents an aspect. 993 994 Doc fields: 995 * attr: Documents attributes of the aspect. Takes a single arg, the 996 attribute name. Can be repeated. The special roles `{default-value}` 997 and `{arg-type}` can be used to indicate the default value and 998 type of attribute, respectively. 999 * aspect-attributes: A CSV list of attribute names the aspect 1000 propagates along. 1001 1002 Example MyST usage 1003 1004 ``` 1005 :::{bzl:repo-rule} myaspect 1006 1007 :attr foo: {default-value}`"foo"` {arg-type}`attr.string` foo doc string 1008 1009 :aspect-attributes: srcs, deps 1010 ::: 1011 ``` 1012 """ 1013 1014 doc_field_types = [ 1015 _BzlGroupedField( 1016 "attr", 1017 label=_("Attributes"), 1018 names=["attr"], 1019 rolename="attr", 1020 can_collapse=False, 1021 ), 1022 _BzlCsvField( 1023 "aspect-attributes", 1024 label=_("Aspect Attributes"), 1025 names=["aspect-attributes"], 1026 has_arg=False, 1027 ), 1028 ] 1029 1030 1031class _BzlFunction(_BzlCallable): 1032 """Documents a general purpose function. 1033 1034 Doc fields: 1035 * arg: Documents the arguments of the function. Takes a single arg, the 1036 arg name. Can be repeated. The special roles `{default-value}` 1037 and `{arg-type}` can be used to indicate the default value and 1038 type of attribute, respectively. 1039 * returns: Documents what the function returns. The special role 1040 `{return-type}` can be used to indicate the return type of the function. 1041 1042 Example MyST usage 1043 1044 ``` 1045 :::{bzl:function} myfunc(a, b=None) -> bool 1046 1047 :arg a: {arg-type}`str` some arg doc 1048 :arg b: {arg-type}`int | None` {default-value}`42` more arg doc 1049 :returns: {return-type}`bool` doc about return value. 1050 ::: 1051 ``` 1052 """ 1053 1054 doc_field_types = [ 1055 _BzlGroupedField( 1056 "arg", 1057 label=_("Args"), 1058 names=["arg"], 1059 rolename="arg", 1060 can_collapse=False, 1061 ), 1062 docfields.Field( 1063 "returns", 1064 label=_("Returns"), 1065 has_arg=False, 1066 names=["returns"], 1067 ), 1068 ] 1069 1070 @override 1071 def _get_signature_object_type(self) -> str: 1072 return "" 1073 1074 1075class _BzlModuleExtension(_BzlObject): 1076 """Documents a module_extension. 1077 1078 Doc fields: 1079 * os-dependent: Documents if the module extension depends on the host 1080 architecture. 1081 * arch-dependent: Documents if the module extension depends on the host 1082 architecture. 1083 * environment-variables: a CSV list of environment variable names. 1084 They will be cross referenced with matching environment variables. 1085 1086 Tag classes are documented using the bzl:tag-class directives within 1087 this directive. 1088 1089 Example MyST usage: 1090 1091 ``` 1092 ::::{bzl:module-extension} myext 1093 1094 :os-dependent: True 1095 :arch-dependent: False 1096 1097 :::{bzl:tag-class} mytag(myattr) 1098 1099 :attr myattr: 1100 {arg-type}`attr.string_list` 1101 doc for attribute 1102 ::: 1103 :::: 1104 ``` 1105 """ 1106 1107 doc_field_types = [ 1108 _BzlDocField( 1109 "os-dependent", 1110 label="OS Dependent", 1111 has_arg=False, 1112 names=["os-dependent"], 1113 ), 1114 _BzlDocField( 1115 "arch-dependent", 1116 label="Arch Dependent", 1117 has_arg=False, 1118 names=["arch-dependent"], 1119 ), 1120 _BzlCsvField( 1121 "environment-variables", 1122 label=_("Environment Variables"), 1123 names=["environment-variables"], 1124 body_domain="std", 1125 bodyrolename="envvar", 1126 has_arg=False, 1127 ), 1128 ] 1129 1130 @override 1131 def _get_signature_object_type(self) -> str: 1132 return "module ext" 1133 1134 1135class _BzlTagClass(_BzlCallable): 1136 """Documents a tag class for a module extension. 1137 1138 Doc fields: 1139 * attr: Documents attributes of the tag class. Takes a single arg, the 1140 attribute name. Can be repeated. The special roles `{default-value}` 1141 and `{arg-type}` can be used to indicate the default value and 1142 type of attribute, respectively. 1143 1144 Example MyST usage, note that this directive should be nested with 1145 a `bzl:module-extension` directive. 1146 1147 ``` 1148 :::{bzl:tag-class} mytag(myattr) 1149 1150 :attr myattr: 1151 {arg-type}`attr.string_list` 1152 doc for attribute 1153 ::: 1154 ``` 1155 """ 1156 1157 doc_field_types = [ 1158 _BzlGroupedField( 1159 "arg", 1160 label=_("Attributes"), 1161 names=["attr"], 1162 rolename="arg", 1163 can_collapse=False, 1164 ), 1165 ] 1166 1167 @override 1168 def _get_signature_object_type(self) -> str: 1169 return "" 1170 1171 @override 1172 def _get_alt_names(self, object_entry): 1173 alt_names = super()._get_alt_names(object_entry) 1174 _, _, symbol = object_entry.full_id.partition("%") 1175 # Allow refering to `ProviderName.field`, even if the provider 1176 # is nested within another object 1177 alt_names.append(".".join(symbol.split(".")[-2:])) 1178 return alt_names 1179 1180 1181class _TargetType(enum.Enum): 1182 TARGET = "target" 1183 FLAG = "flag" 1184 1185 1186class _BzlTarget(_BzlObject): 1187 """Documents an arbitrary target.""" 1188 1189 _TARGET_TYPE = _TargetType.TARGET 1190 1191 def handle_signature(self, sig_text, sig_node): 1192 self._signature_add_object_type(sig_node) 1193 if ":" in sig_text: 1194 package, target_name = sig_text.split(":", 1) 1195 else: 1196 target_name = sig_text 1197 package = self.env.ref_context["bzl:file"] 1198 package = package[: package.find(":BUILD")] 1199 1200 package = package + ":" 1201 if self._TARGET_TYPE == _TargetType.FLAG: 1202 sig_node += addnodes.desc_addname("--", "--") 1203 sig_node += addnodes.desc_addname(package, package) 1204 sig_node += addnodes.desc_name(target_name, target_name) 1205 1206 obj_id = _BzlObjectId.from_env(self.env, label=package + target_name) 1207 sig_node["bzl:object_id"] = obj_id.full_id 1208 return obj_id 1209 1210 @override 1211 def _get_signature_object_type(self) -> str: 1212 # We purposely return empty here because having "target" in front 1213 # of every label isn't very helpful 1214 return "" 1215 1216 1217# TODO: Integrate with the option directive, since flags are options, afterall. 1218# https://www.sphinx-doc.org/en/master/usage/domains/standard.html#directive-option 1219class _BzlFlag(_BzlTarget): 1220 """Documents a flag""" 1221 1222 _TARGET_TYPE = _TargetType.FLAG 1223 1224 @override 1225 def _get_signature_object_type(self) -> str: 1226 return "flag" 1227 1228 def _get_additional_index_types(self): 1229 return ["target"] 1230 1231 1232class _DefaultValueRole(sphinx_docutils.SphinxRole): 1233 """Documents the default value for an arg or attribute. 1234 1235 This is a special role used within `:arg:` and `:attr:` doc fields to 1236 indicate the default value. The rendering process looks for this role 1237 and reformats and moves its content for better display. 1238 1239 Styling can be customized by matching the `.default_value` class. 1240 """ 1241 1242 def run(self) -> _RoleRunResult: 1243 node = docutils_nodes.emphasis( 1244 "", 1245 "(default ", 1246 docutils_nodes.inline("", self.text, classes=["sig", "default_value"]), 1247 docutils_nodes.Text(") "), 1248 classes=["default-value-span"], 1249 ) 1250 return ([node], []) 1251 1252 1253class _TypeRole(sphinx_docutils.SphinxRole): 1254 """Documents a type (or type expression) with crossreferencing. 1255 1256 This is an inline role used to create cross references to other types. 1257 1258 The content is interpreted as a reference to a type or an expression 1259 of types. The syntax uses Python-style sytax with `|` and `[]`, e.g. 1260 `foo.MyType | str | list[str] | dict[str, int]`. Each symbolic name 1261 will be turned into a cross reference; see the domain's documentation 1262 for how to reference objects. 1263 1264 Example MyST usage: 1265 1266 ``` 1267 This function accepts {bzl:type}`str | list[str]` for usernames 1268 ``` 1269 """ 1270 1271 def __init__(self): 1272 super().__init__() 1273 self._xref = roles.XRefRole() 1274 1275 def run(self) -> _RoleRunResult: 1276 outer_messages = [] 1277 1278 def make_xref(name): 1279 nodes, msgs = self._xref( 1280 "bzl:type", 1281 name, 1282 name, 1283 self.lineno, 1284 self.inliner, 1285 self.options, 1286 self.content, 1287 ) 1288 outer_messages.extend(msgs) 1289 if len(nodes) == 1: 1290 return nodes[0] 1291 else: 1292 return docutils_nodes.inline("", "", nodes) 1293 1294 root = _TypeExprParser.xrefs_from_type_expr(self.text, make_xref) 1295 return ([root], outer_messages) 1296 1297 1298class _ReturnTypeRole(_TypeRole): 1299 """Documents the return type for function. 1300 1301 This is a special role used within `:returns:` doc fields to 1302 indicate the return type of the function. The rendering process looks for 1303 this role and reformats and moves its content for better display. 1304 1305 Example MyST Usage 1306 1307 ``` 1308 :::{bzl:function} foo() 1309 1310 :returns: {return-type}`list[str]` 1311 ::: 1312 ``` 1313 """ 1314 1315 def run(self) -> _RoleRunResult: 1316 nodes, messages = super().run() 1317 nodes.append(docutils_nodes.Text(" -- ")) 1318 return nodes, messages 1319 1320 1321class _RequiredProvidersRole(_TypeRole): 1322 """Documents the providers an attribute requires. 1323 1324 This is a special role used within `:arg:` or `:attr:` doc fields to 1325 indicate the types of providers that are required. The rendering process 1326 looks for this role and reformats its content for better display, but its 1327 position is left as-is; typically it would be its own paragraph near the 1328 end of the doc. 1329 1330 The syntax is a pipe (`|`) delimited list of types or groups of types, 1331 where groups are indicated using `[...]`. e.g, to express that FooInfo OR 1332 (both of BarInfo and BazInfo) are supported, write `FooInfo | [BarInfo, 1333 BazInfo]` 1334 1335 Example MyST Usage 1336 1337 ``` 1338 :::{bzl:rule} foo(bar) 1339 1340 :attr bar: My attribute doc 1341 1342 {required-providers}`CcInfo | [PyInfo, JavaInfo]` 1343 ::: 1344 ``` 1345 """ 1346 1347 def run(self) -> _RoleRunResult: 1348 xref_nodes, messages = super().run() 1349 nodes = [ 1350 docutils_nodes.emphasis("", "Required providers: "), 1351 ] + xref_nodes 1352 return nodes, messages 1353 1354 1355class _BzlIndex(domains.Index): 1356 """An index of a bzl file's objects. 1357 1358 NOTE: This generates the entries for the *domain specific* index 1359 (bzl-index.html), not the general index (genindex.html). To affect 1360 the general index, index nodes and directives must be used (grep 1361 for `self.indexnode`). 1362 """ 1363 1364 name = "index" 1365 localname = "Bazel/Starlark Object Index" 1366 shortname = "Bzl" 1367 1368 def generate( 1369 self, docnames: Iterable[str] = None 1370 ) -> tuple[list[tuple[str, list[domains.IndexEntry]]], bool]: 1371 content = collections.defaultdict(list) 1372 1373 # sort the list of objects in alphabetical order 1374 objects = self.domain.data["objects"].values() 1375 objects = sorted(objects, key=lambda obj: obj.index_entry.name) 1376 1377 # Group by first letter 1378 for entry in objects: 1379 index_entry = entry.index_entry 1380 content[index_entry.name[0].lower()].append(index_entry) 1381 1382 # convert the dict to the sorted list of tuples expected 1383 content = sorted(content.items()) 1384 1385 return content, True 1386 1387 1388class _BzlDomain(domains.Domain): 1389 """Domain for Bazel/Starlark objects. 1390 1391 Directives 1392 1393 There are directives for defining Bazel objects and their functionality. 1394 See the respective directive classes for details. 1395 1396 Public Crossreferencing Roles 1397 1398 These are roles that can be used in docs to create cross references. 1399 1400 Objects are fully identified using dotted notation converted from the Bazel 1401 label and symbol name within a `.bzl` file. The `@`, `/` and `:` characters 1402 are converted to dots (with runs removed), and `.bzl` is removed from file 1403 names. The dotted path of a symbol in the bzl file is appended. For example, 1404 the `paths.join` function in `@bazel_skylib//lib:paths.bzl` would be 1405 identified as `bazel_skylib.lib.paths.paths.join`. 1406 1407 Shorter identifiers can be used. Within a project, the repo name portion 1408 can be omitted. Within a file, file-relative names can be used. 1409 1410 * obj: Used to reference a single object without concern for its type. 1411 This roles searches all object types for a name that matches the given 1412 value. Example usage in MyST: 1413 ``` 1414 {bzl:obj}`repo.pkg.file.my_function` 1415 ``` 1416 1417 * type: Transforms a type expression into cross references for objects 1418 with object type "type". For example, it parses `int | list[str]` into 1419 three links for each component part. 1420 1421 Public Typography Roles 1422 1423 These are roles used for special purposes to aid documentation. 1424 1425 * default-value: The default value for an argument or attribute. Only valid 1426 to use within arg or attribute documentation. See `_DefaultValueRole` for 1427 details. 1428 * required-providers: The providers an attribute requires. Only 1429 valud to use within an attribute documentation. See 1430 `_RequiredProvidersRole` for details. 1431 * return-type: The type of value a function returns. Only valid 1432 within a function's return doc field. See `_ReturnTypeRole` for details. 1433 1434 Object Types 1435 1436 These are the types of objects that this domain keeps in its index. 1437 1438 * arg: An argument to a function or macro. 1439 * aspect: A Bazel `aspect`. 1440 * attribute: An input to a rule (regular, repository, aspect, or module 1441 extension). 1442 * method: A function bound to an instance of a struct acting as a type. 1443 * module-extension: A Bazel `module_extension`. 1444 * provider: A Bazel `provider`. 1445 * provider-field: A field of a provider. 1446 * repo-rule: A Bazel `repository_rule`. 1447 * rule: A regular Bazel `rule`. 1448 * tag-class: A Bazel `tag_class` of a `module_extension`. 1449 * target: A Bazel target. 1450 * type: A builtin Bazel type or user-defined structural type. User defined 1451 structual types are typically instances `struct` created using a function 1452 that acts as a constructor with implicit state bound using closures. 1453 """ 1454 1455 name = "bzl" 1456 label = "Bzl" 1457 1458 # NOTE: Most every object type has "obj" as one of the roles because 1459 # an object type's role determine what reftypes (cross referencing) can 1460 # refer to it. By having "obj" for all of them, it allows writing 1461 # :bzl:obj`foo` to restrict object searching to the bzl domain. Under the 1462 # hood, this domain translates requests for the :any: role as lookups for 1463 # :obj:. 1464 # NOTE: We also use these object types for categorizing things in the 1465 # generated index page. 1466 object_types = { 1467 "arg": domains.ObjType("arg", "arg", "obj"), # macro/function arg 1468 "aspect": domains.ObjType("aspect", "aspect", "obj"), 1469 "attr": domains.ObjType("attr", "attr", "obj"), # rule attribute 1470 "function": domains.ObjType("function", "func", "obj"), 1471 "method": domains.ObjType("method", "method", "obj"), 1472 "module-extension": domains.ObjType( 1473 "module extension", "module_extension", "obj" 1474 ), 1475 # Providers are close enough to types that we include "type". This 1476 # also makes :type: Foo work in directive options. 1477 "provider": domains.ObjType("provider", "provider", "type", "obj"), 1478 "provider-field": domains.ObjType("provider field", "provider-field", "obj"), 1479 "field": domains.ObjType("field", "field", "obj"), 1480 "repo-rule": domains.ObjType("repository rule", "repo_rule", "obj"), 1481 "rule": domains.ObjType("rule", "rule", "obj"), 1482 "tag-class": domains.ObjType("tag class", "tag_class", "obj"), 1483 "target": domains.ObjType("target", "target", "obj"), # target in a build file 1484 # Flags are also targets, so include "target" for xref'ing 1485 "flag": domains.ObjType("flag", "flag", "target", "obj"), 1486 # types are objects that have a constructor and methods/attrs 1487 "type": domains.ObjType("type", "type", "obj"), 1488 "typedef": domains.ObjType("typedef", "typedef", "type", "obj"), 1489 } 1490 1491 # This controls: 1492 # * What is recognized when parsing, e.g. ":bzl:ref:`foo`" requires 1493 # "ref" to be in the role dict below. 1494 roles = { 1495 "arg": roles.XRefRole(), 1496 "attr": roles.XRefRole(), 1497 "default-value": _DefaultValueRole(), 1498 "flag": roles.XRefRole(), 1499 "obj": roles.XRefRole(), 1500 "required-providers": _RequiredProvidersRole(), 1501 "return-type": _ReturnTypeRole(), 1502 "rule": roles.XRefRole(), 1503 "target": roles.XRefRole(), 1504 "type": _TypeRole(), 1505 } 1506 # NOTE: Directives that have a corresponding object type should use 1507 # the same key for both directive and object type. Some directives 1508 # look up their corresponding object type. 1509 directives = { 1510 "aspect": _BzlAspect, 1511 "currentfile": _BzlCurrentFile, 1512 "function": _BzlFunction, 1513 "module-extension": _BzlModuleExtension, 1514 "provider": _BzlProvider, 1515 "typedef": _BzlTypedef, 1516 "provider-field": _BzlProviderField, 1517 "field": _BzlField, 1518 "repo-rule": _BzlRepositoryRule, 1519 "rule": _BzlRule, 1520 "tag-class": _BzlTagClass, 1521 "target": _BzlTarget, 1522 "flag": _BzlFlag, 1523 "attr-info": _BzlAttrInfo, 1524 } 1525 indices = { 1526 _BzlIndex, 1527 } 1528 1529 # NOTE: When adding additional data keys, make sure to update 1530 # merge_domaindata 1531 initial_data = { 1532 # All objects; keyed by full id 1533 # dict[str, _ObjectEntry] 1534 "objects": {}, 1535 # dict[str, dict[str, _ObjectEntry]] 1536 "objects_by_type": {}, 1537 # Objects within each doc 1538 # dict[str, dict[str, _ObjectEntry]] 1539 "doc_names": {}, 1540 # Objects by a shorter or alternative name 1541 # dict[str, dict[str id, _ObjectEntry]] 1542 "alt_names": {}, 1543 } 1544 1545 @override 1546 def get_full_qualified_name( 1547 self, node: docutils_nodes.Element 1548 ) -> typing.Union[str, None]: 1549 bzl_file = node.get("bzl:file") 1550 symbol_name = node.get("bzl:symbol") 1551 ref_target = node.get("reftarget") 1552 return ".".join(filter(None, [bzl_file, symbol_name, ref_target])) 1553 1554 @override 1555 def get_objects(self) -> Iterable[_GetObjectsTuple]: 1556 for entry in self.data["objects"].values(): 1557 yield entry.to_get_objects_tuple() 1558 1559 @override 1560 def resolve_any_xref( 1561 self, 1562 env: environment.BuildEnvironment, 1563 fromdocname: str, 1564 builder: builders.Builder, 1565 target: str, 1566 node: addnodes.pending_xref, 1567 contnode: docutils_nodes.Element, 1568 ) -> list[tuple[str, docutils_nodes.Element]]: 1569 del env, node # Unused 1570 entry = self._find_entry_for_xref(fromdocname, "obj", target) 1571 if not entry: 1572 return [] 1573 to_docname = entry.index_entry.docname 1574 to_anchor = entry.index_entry.anchor 1575 ref_node = sphinx_nodes.make_refnode( 1576 builder, fromdocname, to_docname, to_anchor, contnode, title=to_anchor 1577 ) 1578 1579 matches = [(f"bzl:{entry.object_type}", ref_node)] 1580 return matches 1581 1582 @override 1583 def resolve_xref( 1584 self, 1585 env: environment.BuildEnvironment, 1586 fromdocname: str, 1587 builder: builders.Builder, 1588 typ: str, 1589 target: str, 1590 node: addnodes.pending_xref, 1591 contnode: docutils_nodes.Element, 1592 ) -> typing.Union[docutils_nodes.Element, None]: 1593 _log_debug( 1594 "resolve_xref: fromdocname=%s, typ=%s, target=%s", fromdocname, typ, target 1595 ) 1596 del env, node # Unused 1597 entry = self._find_entry_for_xref(fromdocname, typ, target) 1598 if not entry: 1599 return None 1600 1601 to_docname = entry.index_entry.docname 1602 to_anchor = entry.index_entry.anchor 1603 return sphinx_nodes.make_refnode( 1604 builder, fromdocname, to_docname, to_anchor, contnode, title=to_anchor 1605 ) 1606 1607 def _find_entry_for_xref( 1608 self, fromdocname: str, object_type: str, target: str 1609 ) -> typing.Union[_ObjectEntry, None]: 1610 if target.startswith("--"): 1611 target = target.strip("-") 1612 object_type = "flag" 1613 1614 # Allow using parentheses, e.g. `foo()` or `foo(x=...)` 1615 target, _, _ = target.partition("(") 1616 1617 # Elide the value part of --foo=bar flags 1618 # Note that the flag value could contain `=` 1619 if "=" in target: 1620 target = target[: target.find("=")] 1621 1622 if target in self.data["doc_names"].get(fromdocname, {}): 1623 entry = self.data["doc_names"][fromdocname][target] 1624 # Prevent a local doc name masking a global alt name when its of 1625 # a different type. e.g. when the macro `foo` refers to the 1626 # rule `foo` in another doc. 1627 if object_type in self.object_types[entry.object_type].roles: 1628 return entry 1629 1630 if object_type == "obj": 1631 search_space = self.data["objects"] 1632 else: 1633 search_space = self.data["objects_by_type"].get(object_type, {}) 1634 if target in search_space: 1635 return search_space[target] 1636 1637 _log_debug("find_entry: alt_names=%s", sorted(self.data["alt_names"].keys())) 1638 if target in self.data["alt_names"]: 1639 # Give preference to shorter object ids. This is a work around 1640 # to allow e.g. `FooInfo` to refer to the FooInfo type rather than 1641 # the `FooInfo` constructor. 1642 entries = sorted( 1643 self.data["alt_names"][target].items(), key=lambda item: len(item[0]) 1644 ) 1645 for _, entry in entries: 1646 if object_type in self.object_types[entry.object_type].roles: 1647 return entry 1648 1649 return None 1650 1651 def add_object(self, entry: _ObjectEntry, alt_names=None) -> None: 1652 _log_debug( 1653 "add_object: full_id=%s, object_type=%s, alt_names=%s", 1654 entry.full_id, 1655 entry.object_type, 1656 alt_names, 1657 ) 1658 if entry.full_id in self.data["objects"]: 1659 existing = self.data["objects"][entry.full_id] 1660 raise Exception( 1661 f"Object {entry.full_id} already registered: " 1662 + f"existing={existing}, incoming={entry}" 1663 ) 1664 self.data["objects"][entry.full_id] = entry 1665 self.data["objects_by_type"].setdefault(entry.object_type, {}) 1666 self.data["objects_by_type"][entry.object_type][entry.full_id] = entry 1667 1668 repo, label, symbol = _parse_full_id(entry.full_id) 1669 if symbol: 1670 base_name = symbol.split(".")[-1] 1671 else: 1672 base_name = label.split(":")[-1] 1673 1674 if alt_names is not None: 1675 alt_names = list(alt_names) 1676 # Add the repo-less version as an alias 1677 alt_names.append(label + (f"%{symbol}" if symbol else "")) 1678 1679 for alt_name in sorted(set(alt_names)): 1680 self.data["alt_names"].setdefault(alt_name, {}) 1681 self.data["alt_names"][alt_name][entry.full_id] = entry 1682 1683 docname = entry.index_entry.docname 1684 self.data["doc_names"].setdefault(docname, {}) 1685 self.data["doc_names"][docname][base_name] = entry 1686 1687 def merge_domaindata( 1688 self, docnames: list[str], otherdata: dict[str, typing.Any] 1689 ) -> None: 1690 # Merge in simple dict[key, value] data 1691 for top_key in ("objects",): 1692 self.data[top_key].update(otherdata.get(top_key, {})) 1693 1694 # Merge in two-level dict[top_key, dict[sub_key, value]] data 1695 for top_key in ("objects_by_type", "doc_names", "alt_names"): 1696 existing_top_map = self.data[top_key] 1697 for sub_key, sub_values in otherdata.get(top_key, {}).items(): 1698 if sub_key not in existing_top_map: 1699 existing_top_map[sub_key] = sub_values 1700 else: 1701 existing_top_map[sub_key].update(sub_values) 1702 1703 1704def _on_missing_reference(app, env: environment.BuildEnvironment, node, contnode): 1705 if node["refdomain"] != "bzl": 1706 return None 1707 if node["reftype"] != "type": 1708 return None 1709 1710 # There's no Bazel docs for None, so prevent missing xrefs warning 1711 if node["reftarget"] == "None": 1712 return contnode 1713 return None 1714 1715 1716def setup(app): 1717 app.add_domain(_BzlDomain) 1718 1719 app.add_config_value( 1720 "bzl_default_repository_name", 1721 default=os.environ.get("SPHINX_BZL_DEFAULT_REPOSITORY_NAME", "@_main"), 1722 rebuild="env", 1723 types=[str], 1724 ) 1725 app.connect("missing-reference", _on_missing_reference) 1726 1727 # Pygments says it supports starlark, but it doesn't seem to actually 1728 # recognize `starlark` as a name. So just manually map it to python. 1729 app.add_lexer("starlark", lexer_classes["python"]) 1730 app.add_lexer("bzl", lexer_classes["python"]) 1731 1732 return { 1733 "version": "1.0.0", 1734 "parallel_read_safe": True, 1735 "parallel_write_safe": True, 1736 } 1737