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 _BzlField(_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 if name_prefix: 627 # Respect whatever the signature wanted 628 display_prefix = name_prefix 629 else: 630 # Otherwise, show the outermost name. This makes ctrl+f finding 631 # for a symbol a bit easier. 632 display_prefix = ".".join(self.env.ref_context["bzl:doc_id_stack"]) 633 _, _, display_prefix = display_prefix.rpartition(".") 634 635 if display_prefix: 636 display_prefix = display_prefix + "." 637 sig_node += addnodes.desc_addname(display_prefix, display_prefix) 638 sig_node += addnodes.desc_name(base_symbol_name, base_symbol_name) 639 640 if type_expr := self.options.get("type"): 641 642 def make_xref(name, title=None): 643 content_node = addnodes.desc_type(name, name) 644 return addnodes.pending_xref( 645 "", 646 content_node, 647 refdomain="bzl", 648 reftype="type", 649 reftarget=name, 650 ) 651 652 attr_annotation_node = addnodes.desc_annotation( 653 type_expr, 654 "", 655 addnodes.desc_sig_punctuation("", ":"), 656 addnodes.desc_sig_space(), 657 _TypeExprParser.xrefs_from_type_expr(type_expr, make_xref), 658 ) 659 sig_node += attr_annotation_node 660 661 if params_text: 662 try: 663 signature = inspect.signature_from_str(params_text) 664 except SyntaxError: 665 # Stardoc doesn't provide accurate info, so the reconstructed 666 # signature might not be valid syntax. Rather than fail, just 667 # provide a plain-text description of the approximate signature. 668 # See https://github.com/bazelbuild/stardoc/issues/225 669 sig_node += addnodes.desc_parameterlist( 670 # Offset by 1 to remove the surrounding parentheses 671 params_text[1:-1], 672 params_text[1:-1], 673 ) 674 else: 675 last_kind = None 676 paramlist_node = addnodes.desc_parameterlist() 677 for param in signature.parameters.values(): 678 if param.kind == param.KEYWORD_ONLY and last_kind in ( 679 param.POSITIONAL_OR_KEYWORD, 680 param.POSITIONAL_ONLY, 681 None, 682 ): 683 # Add separator for keyword only parameter: * 684 paramlist_node += addnodes.desc_parameter( 685 "", "", addnodes.desc_sig_operator("", "*") 686 ) 687 688 last_kind = param.kind 689 node = addnodes.desc_parameter() 690 if param.kind == param.VAR_POSITIONAL: 691 node += addnodes.desc_sig_operator("", "*") 692 elif param.kind == param.VAR_KEYWORD: 693 node += addnodes.desc_sig_operator("", "**") 694 695 node += addnodes.desc_sig_name(rawsource="", text=param.name) 696 if param.default is not param.empty: 697 node += addnodes.desc_sig_operator("", "=") 698 node += docutils_nodes.inline( 699 "", 700 param.default, 701 classes=["default_value"], 702 support_smartquotes=False, 703 ) 704 paramlist_node += node 705 sig_node += paramlist_node 706 707 if signature.return_annotation is not signature.empty: 708 sig_node += addnodes.desc_returns("", signature.return_annotation) 709 710 obj_id = _BzlObjectId.from_env(self.env, symbol=relative_name) 711 712 sig_node["bzl:object_id"] = obj_id.full_id 713 return obj_id 714 715 def _signature_add_object_type(self, sig_node: addnodes.desc_signature): 716 if sig_object_type := self._get_signature_object_type(): 717 sig_node += addnodes.desc_annotation("", self._get_signature_object_type()) 718 sig_node += addnodes.desc_sig_space() 719 720 @override 721 def add_target_and_index( 722 self, obj_desc: _BzlObjectId, sig: str, sig_node: addnodes.desc_signature 723 ) -> None: 724 super().add_target_and_index(obj_desc, sig, sig_node) 725 if obj_desc.symbol: 726 display_name = obj_desc.symbol 727 location = obj_desc.label 728 if obj_desc.namespace: 729 location += f"%{obj_desc.namespace}" 730 else: 731 display_name = obj_desc.target_name 732 location = obj_desc.package 733 734 anchor_prefix = ".".join(self.env.ref_context["bzl:doc_id_stack"]) 735 if anchor_prefix: 736 anchor_id = f"{anchor_prefix}.{obj_desc.doc_id}" 737 else: 738 anchor_id = obj_desc.doc_id 739 740 sig_node["ids"].append(anchor_id) 741 742 object_type_display = self._get_object_type_display_name() 743 index_description = f"{display_name} ({object_type_display} in {location})" 744 self.indexnode["entries"].extend( 745 _index_node_tuple("single", f"{index_type}; {index_description}", anchor_id) 746 for index_type in [object_type_display] + self._get_additional_index_types() 747 ) 748 self.indexnode["entries"].append( 749 _index_node_tuple("single", index_description, anchor_id), 750 ) 751 752 object_entry = _ObjectEntry( 753 full_id=obj_desc.full_id, 754 display_name=display_name, 755 object_type=self.objtype, 756 search_priority=1, 757 index_entry=domains.IndexEntry( 758 name=display_name, 759 subtype=_INDEX_SUBTYPE_NORMAL, 760 docname=self.env.docname, 761 anchor=anchor_id, 762 extra="", 763 qualifier="", 764 descr=index_description, 765 ), 766 ) 767 768 alt_names = [] 769 if origin_key := self.options.get("origin-key"): 770 alt_names.append( 771 origin_key 772 # Options require \@ for leading @, but don't 773 # remove the escaping slash, so we have to do it manually 774 .lstrip("\\") 775 ) 776 extra_alt_names = self._get_alt_names(object_entry) 777 alt_names.extend(extra_alt_names) 778 779 self.env.get_domain(self.domain).add_object(object_entry, alt_names=alt_names) 780 781 def _get_additional_index_types(self): 782 return [] 783 784 @override 785 def _object_hierarchy_parts( 786 self, sig_node: addnodes.desc_signature 787 ) -> tuple[str, ...]: 788 return _parse_full_id(sig_node["bzl:object_id"]) 789 790 @override 791 def _toc_entry_name(self, sig_node: addnodes.desc_signature) -> str: 792 return sig_node["_toc_parts"][-1] 793 794 def _get_object_type_display_name(self) -> str: 795 return self.env.get_domain(self.domain).object_types[self.objtype].lname 796 797 def _get_signature_object_type(self) -> str: 798 return self._get_object_type_display_name() 799 800 def _get_alt_names(self, object_entry): 801 alt_names = [] 802 full_id = object_entry.full_id 803 label, _, symbol = full_id.partition("%") 804 if symbol: 805 # Allow referring to the file-relative fully qualified symbol name 806 alt_names.append(symbol) 807 if "." in symbol: 808 # Allow referring to the last component of the symbol 809 alt_names.append(symbol.split(".")[-1]) 810 else: 811 # Otherwise, it's a target. Allow referring to just the target name 812 _, _, target_name = label.partition(":") 813 alt_names.append(target_name) 814 815 return alt_names 816 817 818class _BzlCallable(_BzlObject): 819 """Abstract base class for objects that are callable.""" 820 821 822class _BzlProvider(_BzlObject): 823 """Documents a provider type. 824 825 Example MyST usage 826 827 ``` 828 ::::{bzl:provider} MyInfo 829 830 Docs about MyInfo 831 832 :::{bzl:provider-field} some_field 833 :type: depset[str] 834 ::: 835 :::: 836 ``` 837 """ 838 839 840class _BzlProviderField(_BzlObject): 841 """Documents a field of a provider. 842 843 Fields can optionally have a type specified using the `:type:` option. 844 845 The type can be any type expression understood by the `{bzl:type}` role. 846 847 ``` 848 :::{bzl:provider-field} foo 849 :type: str 850 ::: 851 ``` 852 """ 853 854 option_spec = _BzlObject.option_spec.copy() 855 option_spec.update( 856 { 857 "type": docutils_directives.unchanged, 858 } 859 ) 860 861 @override 862 def _get_signature_object_type(self) -> str: 863 return "" 864 865 @override 866 def _get_alt_names(self, object_entry): 867 alt_names = super()._get_alt_names(object_entry) 868 _, _, symbol = object_entry.full_id.partition("%") 869 # Allow refering to `mod_ext_name.tag_name`, even if the extension 870 # is nested within another object 871 alt_names.append(".".join(symbol.split(".")[-2:])) 872 return alt_names 873 874 875class _BzlRepositoryRule(_BzlCallable): 876 """Documents a repository rule. 877 878 Doc fields: 879 * attr: Documents attributes of the rule. Takes a single arg, the 880 attribute name. Can be repeated. The special roles `{default-value}` 881 and `{arg-type}` can be used to indicate the default value and 882 type of attribute, respectively. 883 * environment-variables: a CSV list of environment variable names. 884 They will be cross referenced with matching environment variables. 885 886 Example MyST usage 887 888 ``` 889 :::{bzl:repo-rule} myrule(foo) 890 891 :attr foo: {default-value}`"foo"` {arg-type}`attr.string` foo doc string 892 893 :environment-variables: FOO, BAR 894 ::: 895 ``` 896 """ 897 898 doc_field_types = [ 899 _BzlGroupedField( 900 "attr", 901 label=_("Attributes"), 902 names=["attr"], 903 rolename="attr", 904 can_collapse=False, 905 ), 906 _BzlCsvField( 907 "environment-variables", 908 label=_("Environment Variables"), 909 names=["environment-variables"], 910 body_domain="std", 911 bodyrolename="envvar", 912 has_arg=False, 913 ), 914 ] 915 916 @override 917 def _get_signature_object_type(self) -> str: 918 return "repo rule" 919 920 921class _BzlRule(_BzlCallable): 922 """Documents a rule. 923 924 Doc fields: 925 * attr: Documents attributes of the rule. Takes a single arg, the 926 attribute name. Can be repeated. The special roles `{default-value}` 927 and `{arg-type}` can be used to indicate the default value and 928 type of attribute, respectively. 929 * provides: A type expression of the provider types the rule provides. 930 To indicate different groupings, use `|` and `[]`. For example, 931 `FooInfo | [BarInfo, BazInfo]` means it provides either `FooInfo` 932 or both of `BarInfo` and `BazInfo`. 933 934 Example MyST usage 935 936 ``` 937 :::{bzl:repo-rule} myrule(foo) 938 939 :attr foo: {default-value}`"foo"` {arg-type}`attr.string` foo doc string 940 941 :provides: FooInfo | BarInfo 942 ::: 943 ``` 944 """ 945 946 doc_field_types = [ 947 _BzlGroupedField( 948 "attr", 949 label=_("Attributes"), 950 names=["attr"], 951 rolename="attr", 952 can_collapse=False, 953 ), 954 _BzlField( 955 "provides", 956 label="Provides", 957 has_arg=False, 958 names=["provides"], 959 bodyrolename="type", 960 ), 961 ] 962 963 964class _BzlAspect(_BzlObject): 965 """Documents an aspect. 966 967 Doc fields: 968 * attr: Documents attributes of the aspect. Takes a single arg, the 969 attribute name. Can be repeated. The special roles `{default-value}` 970 and `{arg-type}` can be used to indicate the default value and 971 type of attribute, respectively. 972 * aspect-attributes: A CSV list of attribute names the aspect 973 propagates along. 974 975 Example MyST usage 976 977 ``` 978 :::{bzl:repo-rule} myaspect 979 980 :attr foo: {default-value}`"foo"` {arg-type}`attr.string` foo doc string 981 982 :aspect-attributes: srcs, deps 983 ::: 984 ``` 985 """ 986 987 doc_field_types = [ 988 _BzlGroupedField( 989 "attr", 990 label=_("Attributes"), 991 names=["attr"], 992 rolename="attr", 993 can_collapse=False, 994 ), 995 _BzlCsvField( 996 "aspect-attributes", 997 label=_("Aspect Attributes"), 998 names=["aspect-attributes"], 999 has_arg=False, 1000 ), 1001 ] 1002 1003 1004class _BzlFunction(_BzlCallable): 1005 """Documents a general purpose function. 1006 1007 Doc fields: 1008 * arg: Documents the arguments of the function. Takes a single arg, the 1009 arg name. Can be repeated. The special roles `{default-value}` 1010 and `{arg-type}` can be used to indicate the default value and 1011 type of attribute, respectively. 1012 * returns: Documents what the function returns. The special role 1013 `{return-type}` can be used to indicate the return type of the function. 1014 1015 Example MyST usage 1016 1017 ``` 1018 :::{bzl:function} myfunc(a, b=None) -> bool 1019 1020 :arg a: {arg-type}`str` some arg doc 1021 :arg b: {arg-type}`int | None` {default-value}`42` more arg doc 1022 :returns: {return-type}`bool` doc about return value. 1023 ::: 1024 ``` 1025 """ 1026 1027 doc_field_types = [ 1028 _BzlGroupedField( 1029 "arg", 1030 label=_("Args"), 1031 names=["arg"], 1032 rolename="arg", 1033 can_collapse=False, 1034 ), 1035 docfields.Field( 1036 "returns", 1037 label=_("Returns"), 1038 has_arg=False, 1039 names=["returns"], 1040 ), 1041 ] 1042 1043 @override 1044 def _get_signature_object_type(self) -> str: 1045 return "" 1046 1047 1048class _BzlModuleExtension(_BzlObject): 1049 """Documents a module_extension. 1050 1051 Doc fields: 1052 * os-dependent: Documents if the module extension depends on the host 1053 architecture. 1054 * arch-dependent: Documents if the module extension depends on the host 1055 architecture. 1056 * environment-variables: a CSV list of environment variable names. 1057 They will be cross referenced with matching environment variables. 1058 1059 Tag classes are documented using the bzl:tag-class directives within 1060 this directive. 1061 1062 Example MyST usage: 1063 1064 ``` 1065 ::::{bzl:module-extension} myext 1066 1067 :os-dependent: True 1068 :arch-dependent: False 1069 1070 :::{bzl:tag-class} mytag(myattr) 1071 1072 :attr myattr: 1073 {arg-type}`attr.string_list` 1074 doc for attribute 1075 ::: 1076 :::: 1077 ``` 1078 """ 1079 1080 doc_field_types = [ 1081 _BzlField( 1082 "os-dependent", 1083 label="OS Dependent", 1084 has_arg=False, 1085 names=["os-dependent"], 1086 ), 1087 _BzlField( 1088 "arch-dependent", 1089 label="Arch Dependent", 1090 has_arg=False, 1091 names=["arch-dependent"], 1092 ), 1093 _BzlCsvField( 1094 "environment-variables", 1095 label=_("Environment Variables"), 1096 names=["environment-variables"], 1097 body_domain="std", 1098 bodyrolename="envvar", 1099 has_arg=False, 1100 ), 1101 ] 1102 1103 @override 1104 def _get_signature_object_type(self) -> str: 1105 return "module ext" 1106 1107 1108class _BzlTagClass(_BzlCallable): 1109 """Documents a tag class for a module extension. 1110 1111 Doc fields: 1112 * attr: Documents attributes of the tag class. Takes a single arg, the 1113 attribute name. Can be repeated. The special roles `{default-value}` 1114 and `{arg-type}` can be used to indicate the default value and 1115 type of attribute, respectively. 1116 1117 Example MyST usage, note that this directive should be nested with 1118 a `bzl:module-extension` directive. 1119 1120 ``` 1121 :::{bzl:tag-class} mytag(myattr) 1122 1123 :attr myattr: 1124 {arg-type}`attr.string_list` 1125 doc for attribute 1126 ::: 1127 ``` 1128 """ 1129 1130 doc_field_types = [ 1131 _BzlGroupedField( 1132 "arg", 1133 label=_("Attributes"), 1134 names=["attr"], 1135 rolename="arg", 1136 can_collapse=False, 1137 ), 1138 ] 1139 1140 @override 1141 def _get_signature_object_type(self) -> str: 1142 return "" 1143 1144 @override 1145 def _get_alt_names(self, object_entry): 1146 alt_names = super()._get_alt_names(object_entry) 1147 _, _, symbol = object_entry.full_id.partition("%") 1148 # Allow refering to `ProviderName.field`, even if the provider 1149 # is nested within another object 1150 alt_names.append(".".join(symbol.split(".")[-2:])) 1151 return alt_names 1152 1153 1154class _TargetType(enum.Enum): 1155 TARGET = "target" 1156 FLAG = "flag" 1157 1158 1159class _BzlTarget(_BzlObject): 1160 """Documents an arbitrary target.""" 1161 1162 _TARGET_TYPE = _TargetType.TARGET 1163 1164 def handle_signature(self, sig_text, sig_node): 1165 self._signature_add_object_type(sig_node) 1166 if ":" in sig_text: 1167 package, target_name = sig_text.split(":", 1) 1168 else: 1169 target_name = sig_text 1170 package = self.env.ref_context["bzl:file"] 1171 package = package[: package.find(":BUILD")] 1172 1173 package = package + ":" 1174 if self._TARGET_TYPE == _TargetType.FLAG: 1175 sig_node += addnodes.desc_addname("--", "--") 1176 sig_node += addnodes.desc_addname(package, package) 1177 sig_node += addnodes.desc_name(target_name, target_name) 1178 1179 obj_id = _BzlObjectId.from_env(self.env, label=package + target_name) 1180 sig_node["bzl:object_id"] = obj_id.full_id 1181 return obj_id 1182 1183 @override 1184 def _get_signature_object_type(self) -> str: 1185 # We purposely return empty here because having "target" in front 1186 # of every label isn't very helpful 1187 return "" 1188 1189 1190# TODO: Integrate with the option directive, since flags are options, afterall. 1191# https://www.sphinx-doc.org/en/master/usage/domains/standard.html#directive-option 1192class _BzlFlag(_BzlTarget): 1193 """Documents a flag""" 1194 1195 _TARGET_TYPE = _TargetType.FLAG 1196 1197 @override 1198 def _get_signature_object_type(self) -> str: 1199 return "flag" 1200 1201 def _get_additional_index_types(self): 1202 return ["target"] 1203 1204 1205class _DefaultValueRole(sphinx_docutils.SphinxRole): 1206 """Documents the default value for an arg or attribute. 1207 1208 This is a special role used within `:arg:` and `:attr:` doc fields to 1209 indicate the default value. The rendering process looks for this role 1210 and reformats and moves its content for better display. 1211 1212 Styling can be customized by matching the `.default_value` class. 1213 """ 1214 1215 def run(self) -> _RoleRunResult: 1216 node = docutils_nodes.emphasis( 1217 "", 1218 "(default ", 1219 docutils_nodes.inline("", self.text, classes=["sig", "default_value"]), 1220 docutils_nodes.Text(") "), 1221 classes=["default-value-span"], 1222 ) 1223 return ([node], []) 1224 1225 1226class _TypeRole(sphinx_docutils.SphinxRole): 1227 """Documents a type (or type expression) with crossreferencing. 1228 1229 This is an inline role used to create cross references to other types. 1230 1231 The content is interpreted as a reference to a type or an expression 1232 of types. The syntax uses Python-style sytax with `|` and `[]`, e.g. 1233 `foo.MyType | str | list[str] | dict[str, int]`. Each symbolic name 1234 will be turned into a cross reference; see the domain's documentation 1235 for how to reference objects. 1236 1237 Example MyST usage: 1238 1239 ``` 1240 This function accepts {bzl:type}`str | list[str]` for usernames 1241 ``` 1242 """ 1243 1244 def __init__(self): 1245 super().__init__() 1246 self._xref = roles.XRefRole() 1247 1248 def run(self) -> _RoleRunResult: 1249 outer_messages = [] 1250 1251 def make_xref(name): 1252 nodes, msgs = self._xref( 1253 "bzl:type", 1254 name, 1255 name, 1256 self.lineno, 1257 self.inliner, 1258 self.options, 1259 self.content, 1260 ) 1261 outer_messages.extend(msgs) 1262 if len(nodes) == 1: 1263 return nodes[0] 1264 else: 1265 return docutils_nodes.inline("", "", nodes) 1266 1267 root = _TypeExprParser.xrefs_from_type_expr(self.text, make_xref) 1268 return ([root], outer_messages) 1269 1270 1271class _ReturnTypeRole(_TypeRole): 1272 """Documents the return type for function. 1273 1274 This is a special role used within `:returns:` doc fields to 1275 indicate the return type of the function. The rendering process looks for 1276 this role and reformats and moves its content for better display. 1277 1278 Example MyST Usage 1279 1280 ``` 1281 :::{bzl:function} foo() 1282 1283 :returns: {return-type}`list[str]` 1284 ::: 1285 ``` 1286 """ 1287 1288 def run(self) -> _RoleRunResult: 1289 nodes, messages = super().run() 1290 nodes.append(docutils_nodes.Text(" -- ")) 1291 return nodes, messages 1292 1293 1294class _RequiredProvidersRole(_TypeRole): 1295 """Documents the providers an attribute requires. 1296 1297 This is a special role used within `:arg:` or `:attr:` doc fields to 1298 indicate the types of providers that are required. The rendering process 1299 looks for this role and reformats its content for better display, but its 1300 position is left as-is; typically it would be its own paragraph near the 1301 end of the doc. 1302 1303 The syntax is a pipe (`|`) delimited list of types or groups of types, 1304 where groups are indicated using `[...]`. e.g, to express that FooInfo OR 1305 (both of BarInfo and BazInfo) are supported, write `FooInfo | [BarInfo, 1306 BazInfo]` 1307 1308 Example MyST Usage 1309 1310 ``` 1311 :::{bzl:rule} foo(bar) 1312 1313 :attr bar: My attribute doc 1314 1315 {required-providers}`CcInfo | [PyInfo, JavaInfo]` 1316 ::: 1317 ``` 1318 """ 1319 1320 def run(self) -> _RoleRunResult: 1321 xref_nodes, messages = super().run() 1322 nodes = [ 1323 docutils_nodes.emphasis("", "Required providers: "), 1324 ] + xref_nodes 1325 return nodes, messages 1326 1327 1328class _BzlIndex(domains.Index): 1329 """An index of a bzl file's objects. 1330 1331 NOTE: This generates the entries for the *domain specific* index 1332 (bzl-index.html), not the general index (genindex.html). To affect 1333 the general index, index nodes and directives must be used (grep 1334 for `self.indexnode`). 1335 """ 1336 1337 name = "index" 1338 localname = "Bazel/Starlark Object Index" 1339 shortname = "Bzl" 1340 1341 def generate( 1342 self, docnames: Iterable[str] = None 1343 ) -> tuple[list[tuple[str, list[domains.IndexEntry]]], bool]: 1344 content = collections.defaultdict(list) 1345 1346 # sort the list of objects in alphabetical order 1347 objects = self.domain.data["objects"].values() 1348 objects = sorted(objects, key=lambda obj: obj.index_entry.name) 1349 1350 # Group by first letter 1351 for entry in objects: 1352 index_entry = entry.index_entry 1353 content[index_entry.name[0].lower()].append(index_entry) 1354 1355 # convert the dict to the sorted list of tuples expected 1356 content = sorted(content.items()) 1357 1358 return content, True 1359 1360 1361class _BzlDomain(domains.Domain): 1362 """Domain for Bazel/Starlark objects. 1363 1364 Directives 1365 1366 There are directives for defining Bazel objects and their functionality. 1367 See the respective directive classes for details. 1368 1369 Public Crossreferencing Roles 1370 1371 These are roles that can be used in docs to create cross references. 1372 1373 Objects are fully identified using dotted notation converted from the Bazel 1374 label and symbol name within a `.bzl` file. The `@`, `/` and `:` characters 1375 are converted to dots (with runs removed), and `.bzl` is removed from file 1376 names. The dotted path of a symbol in the bzl file is appended. For example, 1377 the `paths.join` function in `@bazel_skylib//lib:paths.bzl` would be 1378 identified as `bazel_skylib.lib.paths.paths.join`. 1379 1380 Shorter identifiers can be used. Within a project, the repo name portion 1381 can be omitted. Within a file, file-relative names can be used. 1382 1383 * obj: Used to reference a single object without concern for its type. 1384 This roles searches all object types for a name that matches the given 1385 value. Example usage in MyST: 1386 ``` 1387 {bzl:obj}`repo.pkg.file.my_function` 1388 ``` 1389 1390 * type: Transforms a type expression into cross references for objects 1391 with object type "type". For example, it parses `int | list[str]` into 1392 three links for each component part. 1393 1394 Public Typography Roles 1395 1396 These are roles used for special purposes to aid documentation. 1397 1398 * default-value: The default value for an argument or attribute. Only valid 1399 to use within arg or attribute documentation. See `_DefaultValueRole` for 1400 details. 1401 * required-providers: The providers an attribute requires. Only 1402 valud to use within an attribute documentation. See 1403 `_RequiredProvidersRole` for details. 1404 * return-type: The type of value a function returns. Only valid 1405 within a function's return doc field. See `_ReturnTypeRole` for details. 1406 1407 Object Types 1408 1409 These are the types of objects that this domain keeps in its index. 1410 1411 * arg: An argument to a function or macro. 1412 * aspect: A Bazel `aspect`. 1413 * attribute: An input to a rule (regular, repository, aspect, or module 1414 extension). 1415 * method: A function bound to an instance of a struct acting as a type. 1416 * module-extension: A Bazel `module_extension`. 1417 * provider: A Bazel `provider`. 1418 * provider-field: A field of a provider. 1419 * repo-rule: A Bazel `repository_rule`. 1420 * rule: A regular Bazel `rule`. 1421 * tag-class: A Bazel `tag_class` of a `module_extension`. 1422 * target: A Bazel target. 1423 * type: A builtin Bazel type or user-defined structural type. User defined 1424 structual types are typically instances `struct` created using a function 1425 that acts as a constructor with implicit state bound using closures. 1426 """ 1427 1428 name = "bzl" 1429 label = "Bzl" 1430 1431 # NOTE: Most every object type has "obj" as one of the roles because 1432 # an object type's role determine what reftypes (cross referencing) can 1433 # refer to it. By having "obj" for all of them, it allows writing 1434 # :bzl:obj`foo` to restrict object searching to the bzl domain. Under the 1435 # hood, this domain translates requests for the :any: role as lookups for 1436 # :obj:. 1437 # NOTE: We also use these object types for categorizing things in the 1438 # generated index page. 1439 object_types = { 1440 "arg": domains.ObjType("arg", "arg", "obj"), # macro/function arg 1441 "aspect": domains.ObjType("aspect", "aspect", "obj"), 1442 "attr": domains.ObjType("attr", "attr", "obj"), # rule attribute 1443 "function": domains.ObjType("function", "func", "obj"), 1444 "method": domains.ObjType("method", "method", "obj"), 1445 "module-extension": domains.ObjType( 1446 "module extension", "module_extension", "obj" 1447 ), 1448 # Providers are close enough to types that we include "type". This 1449 # also makes :type: Foo work in directive options. 1450 "provider": domains.ObjType("provider", "provider", "type", "obj"), 1451 "provider-field": domains.ObjType("provider field", "field", "obj"), 1452 "repo-rule": domains.ObjType("repository rule", "repo_rule", "obj"), 1453 "rule": domains.ObjType("rule", "rule", "obj"), 1454 "tag-class": domains.ObjType("tag class", "tag_class", "obj"), 1455 "target": domains.ObjType("target", "target", "obj"), # target in a build file 1456 # Flags are also targets, so include "target" for xref'ing 1457 "flag": domains.ObjType("flag", "flag", "target", "obj"), 1458 # types are objects that have a constructor and methods/attrs 1459 "type": domains.ObjType("type", "type", "obj"), 1460 } 1461 1462 # This controls: 1463 # * What is recognized when parsing, e.g. ":bzl:ref:`foo`" requires 1464 # "ref" to be in the role dict below. 1465 roles = { 1466 "arg": roles.XRefRole(), 1467 "attr": roles.XRefRole(), 1468 "default-value": _DefaultValueRole(), 1469 "flag": roles.XRefRole(), 1470 "obj": roles.XRefRole(), 1471 "required-providers": _RequiredProvidersRole(), 1472 "return-type": _ReturnTypeRole(), 1473 "rule": roles.XRefRole(), 1474 "target": roles.XRefRole(), 1475 "type": _TypeRole(), 1476 } 1477 # NOTE: Directives that have a corresponding object type should use 1478 # the same key for both directive and object type. Some directives 1479 # look up their corresponding object type. 1480 directives = { 1481 "aspect": _BzlAspect, 1482 "currentfile": _BzlCurrentFile, 1483 "function": _BzlFunction, 1484 "module-extension": _BzlModuleExtension, 1485 "provider": _BzlProvider, 1486 "provider-field": _BzlProviderField, 1487 "repo-rule": _BzlRepositoryRule, 1488 "rule": _BzlRule, 1489 "tag-class": _BzlTagClass, 1490 "target": _BzlTarget, 1491 "flag": _BzlFlag, 1492 "attr-info": _BzlAttrInfo, 1493 } 1494 indices = { 1495 _BzlIndex, 1496 } 1497 1498 # NOTE: When adding additional data keys, make sure to update 1499 # merge_domaindata 1500 initial_data = { 1501 # All objects; keyed by full id 1502 # dict[str, _ObjectEntry] 1503 "objects": {}, 1504 # dict[str, dict[str, _ObjectEntry]] 1505 "objects_by_type": {}, 1506 # Objects within each doc 1507 # dict[str, dict[str, _ObjectEntry]] 1508 "doc_names": {}, 1509 # Objects by a shorter or alternative name 1510 # dict[str, dict[str id, _ObjectEntry]] 1511 "alt_names": {}, 1512 } 1513 1514 @override 1515 def get_full_qualified_name( 1516 self, node: docutils_nodes.Element 1517 ) -> typing.Union[str, None]: 1518 bzl_file = node.get("bzl:file") 1519 symbol_name = node.get("bzl:symbol") 1520 ref_target = node.get("reftarget") 1521 return ".".join(filter(None, [bzl_file, symbol_name, ref_target])) 1522 1523 @override 1524 def get_objects(self) -> Iterable[_GetObjectsTuple]: 1525 for entry in self.data["objects"].values(): 1526 yield entry.to_get_objects_tuple() 1527 1528 @override 1529 def resolve_any_xref( 1530 self, 1531 env: environment.BuildEnvironment, 1532 fromdocname: str, 1533 builder: builders.Builder, 1534 target: str, 1535 node: addnodes.pending_xref, 1536 contnode: docutils_nodes.Element, 1537 ) -> list[tuple[str, docutils_nodes.Element]]: 1538 del env, node # Unused 1539 entry = self._find_entry_for_xref(fromdocname, "obj", target) 1540 if not entry: 1541 return [] 1542 to_docname = entry.index_entry.docname 1543 to_anchor = entry.index_entry.anchor 1544 ref_node = sphinx_nodes.make_refnode( 1545 builder, fromdocname, to_docname, to_anchor, contnode, title=to_anchor 1546 ) 1547 1548 matches = [(f"bzl:{entry.object_type}", ref_node)] 1549 return matches 1550 1551 @override 1552 def resolve_xref( 1553 self, 1554 env: environment.BuildEnvironment, 1555 fromdocname: str, 1556 builder: builders.Builder, 1557 typ: str, 1558 target: str, 1559 node: addnodes.pending_xref, 1560 contnode: docutils_nodes.Element, 1561 ) -> typing.Union[docutils_nodes.Element, None]: 1562 _log_debug( 1563 "resolve_xref: fromdocname=%s, typ=%s, target=%s", fromdocname, typ, target 1564 ) 1565 del env, node # Unused 1566 entry = self._find_entry_for_xref(fromdocname, typ, target) 1567 if not entry: 1568 return None 1569 1570 to_docname = entry.index_entry.docname 1571 to_anchor = entry.index_entry.anchor 1572 return sphinx_nodes.make_refnode( 1573 builder, fromdocname, to_docname, to_anchor, contnode, title=to_anchor 1574 ) 1575 1576 def _find_entry_for_xref( 1577 self, fromdocname: str, object_type: str, target: str 1578 ) -> typing.Union[_ObjectEntry, None]: 1579 if target.startswith("--"): 1580 target = target.strip("-") 1581 object_type = "flag" 1582 1583 # Allow using parentheses, e.g. `foo()` or `foo(x=...)` 1584 target, _, _ = target.partition("(") 1585 1586 # Elide the value part of --foo=bar flags 1587 # Note that the flag value could contain `=` 1588 if "=" in target: 1589 target = target[: target.find("=")] 1590 1591 if target in self.data["doc_names"].get(fromdocname, {}): 1592 entry = self.data["doc_names"][fromdocname][target] 1593 # Prevent a local doc name masking a global alt name when its of 1594 # a different type. e.g. when the macro `foo` refers to the 1595 # rule `foo` in another doc. 1596 if object_type in self.object_types[entry.object_type].roles: 1597 return entry 1598 1599 if object_type == "obj": 1600 search_space = self.data["objects"] 1601 else: 1602 search_space = self.data["objects_by_type"].get(object_type, {}) 1603 if target in search_space: 1604 return search_space[target] 1605 1606 _log_debug("find_entry: alt_names=%s", sorted(self.data["alt_names"].keys())) 1607 if target in self.data["alt_names"]: 1608 # Give preference to shorter object ids. This is a work around 1609 # to allow e.g. `FooInfo` to refer to the FooInfo type rather than 1610 # the `FooInfo` constructor. 1611 entries = sorted( 1612 self.data["alt_names"][target].items(), key=lambda item: len(item[0]) 1613 ) 1614 for _, entry in entries: 1615 if object_type in self.object_types[entry.object_type].roles: 1616 return entry 1617 1618 return None 1619 1620 def add_object(self, entry: _ObjectEntry, alt_names=None) -> None: 1621 _log_debug( 1622 "add_object: full_id=%s, object_type=%s, alt_names=%s", 1623 entry.full_id, 1624 entry.object_type, 1625 alt_names, 1626 ) 1627 if entry.full_id in self.data["objects"]: 1628 existing = self.data["objects"][entry.full_id] 1629 raise Exception( 1630 f"Object {entry.full_id} already registered: " 1631 + f"existing={existing}, incoming={entry}" 1632 ) 1633 self.data["objects"][entry.full_id] = entry 1634 self.data["objects_by_type"].setdefault(entry.object_type, {}) 1635 self.data["objects_by_type"][entry.object_type][entry.full_id] = entry 1636 1637 repo, label, symbol = _parse_full_id(entry.full_id) 1638 if symbol: 1639 base_name = symbol.split(".")[-1] 1640 else: 1641 base_name = label.split(":")[-1] 1642 1643 if alt_names is not None: 1644 alt_names = list(alt_names) 1645 # Add the repo-less version as an alias 1646 alt_names.append(label + (f"%{symbol}" if symbol else "")) 1647 1648 for alt_name in sorted(set(alt_names)): 1649 self.data["alt_names"].setdefault(alt_name, {}) 1650 self.data["alt_names"][alt_name][entry.full_id] = entry 1651 1652 docname = entry.index_entry.docname 1653 self.data["doc_names"].setdefault(docname, {}) 1654 self.data["doc_names"][docname][base_name] = entry 1655 1656 def merge_domaindata( 1657 self, docnames: list[str], otherdata: dict[str, typing.Any] 1658 ) -> None: 1659 # Merge in simple dict[key, value] data 1660 for top_key in ("objects",): 1661 self.data[top_key].update(otherdata.get(top_key, {})) 1662 1663 # Merge in two-level dict[top_key, dict[sub_key, value]] data 1664 for top_key in ("objects_by_type", "doc_names", "alt_names"): 1665 existing_top_map = self.data[top_key] 1666 for sub_key, sub_values in otherdata.get(top_key, {}).items(): 1667 if sub_key not in existing_top_map: 1668 existing_top_map[sub_key] = sub_values 1669 else: 1670 existing_top_map[sub_key].update(sub_values) 1671 1672 1673def _on_missing_reference(app, env: environment.BuildEnvironment, node, contnode): 1674 if node["refdomain"] != "bzl": 1675 return None 1676 if node["reftype"] != "type": 1677 return None 1678 1679 # There's no Bazel docs for None, so prevent missing xrefs warning 1680 if node["reftarget"] == "None": 1681 return contnode 1682 return None 1683 1684 1685def setup(app): 1686 app.add_domain(_BzlDomain) 1687 1688 app.add_config_value( 1689 "bzl_default_repository_name", 1690 default=os.environ.get("SPHINX_BZL_DEFAULT_REPOSITORY_NAME", "@_main"), 1691 rebuild="env", 1692 types=[str], 1693 ) 1694 app.connect("missing-reference", _on_missing_reference) 1695 1696 # Pygments says it supports starlark, but it doesn't seem to actually 1697 # recognize `starlark` as a name. So just manually map it to python. 1698 app.add_lexer("starlark", lexer_classes["python"]) 1699 app.add_lexer("bzl", lexer_classes["python"]) 1700 1701 return { 1702 "version": "1.0.0", 1703 "parallel_read_safe": True, 1704 "parallel_write_safe": True, 1705 } 1706