| # Copyright 2024 The Bazel Authors. All rights reserved. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| """Sphinx extension for documenting Bazel/Starlark objects.""" |
| |
| import ast |
| import collections |
| import enum |
| import os |
| import typing |
| from collections.abc import Collection |
| from typing import Callable, Iterable, TypeVar |
| |
| from docutils import nodes as docutils_nodes |
| from docutils.parsers.rst import directives as docutils_directives |
| from docutils.parsers.rst import states |
| from sphinx import addnodes, builders |
| from sphinx import directives as sphinx_directives |
| from sphinx import domains, environment, roles |
| from sphinx.highlighting import lexer_classes |
| from sphinx.locale import _ |
| from sphinx.util import docfields |
| from sphinx.util import docutils as sphinx_docutils |
| from sphinx.util import inspect, logging |
| from sphinx.util import nodes as sphinx_nodes |
| from sphinx.util import typing as sphinx_typing |
| from typing_extensions import TypeAlias, override |
| |
| _logger = logging.getLogger(__name__) |
| _LOG_PREFIX = f"[{_logger.name}] " |
| |
| _INDEX_SUBTYPE_NORMAL = 0 |
| _INDEX_SUBTYPE_ENTRY_WITH_SUB_ENTRIES = 1 |
| _INDEX_SUBTYPE_SUB_ENTRY = 2 |
| |
| _T = TypeVar("_T") |
| |
| # See https://www.sphinx-doc.org/en/master/extdev/domainapi.html#sphinx.domains.Domain.get_objects |
| _GetObjectsTuple: TypeAlias = tuple[str, str, str, str, str, int] |
| |
| # See SphinxRole.run definition; the docs for role classes are pretty sparse. |
| _RoleRunResult: TypeAlias = tuple[ |
| list[docutils_nodes.Node], list[docutils_nodes.system_message] |
| ] |
| |
| |
| def _log_debug(message, *args): |
| # NOTE: Non-warning log messages go to stdout and are only |
| # visible when -q isn't passed to Sphinx. Note that the sphinx_docs build |
| # rule passes -q by default; use --//sphinxdocs:quiet=false to disable it. |
| _logger.debug("%s" + message, _LOG_PREFIX, *args) |
| |
| |
| def _position_iter(values: Collection[_T]) -> tuple[bool, bool, _T]: |
| last_i = len(values) - 1 |
| for i, value in enumerate(values): |
| yield i == 0, i == last_i, value |
| |
| |
| class InvalidValueError(Exception): |
| """Generic error for an invalid value instead of ValueError. |
| |
| Sphinx treats regular ValueError to mean abort parsing the current |
| chunk and continue on as best it can. Their error means a more |
| fundamental problem that should cause a failure. |
| """ |
| |
| |
| class _ObjectEntry: |
| """Metadata about a known object.""" |
| |
| def __init__( |
| self, |
| full_id: str, |
| display_name: str, |
| object_type: str, |
| search_priority: int, |
| index_entry: domains.IndexEntry, |
| ): |
| """Creates an instance. |
| |
| Args: |
| full_id: The fully qualified id of the object. Should be |
| globally unique, even between projects. |
| display_name: What to display the object as in casual context. |
| object_type: The type of object, typically one of the values |
| known to the domain. |
| search_priority: The search priority, see |
| https://www.sphinx-doc.org/en/master/extdev/domainapi.html#sphinx.domains.Domain.get_objects |
| for valid values. |
| index_entry: Metadata about the object for the domain index. |
| """ |
| self.full_id = full_id |
| self.display_name = display_name |
| self.object_type = object_type |
| self.search_priority = search_priority |
| self.index_entry = index_entry |
| |
| def to_get_objects_tuple(self) -> _GetObjectsTuple: |
| # For the tuple definition |
| return ( |
| self.full_id, |
| self.display_name, |
| self.object_type, |
| self.index_entry.docname, |
| self.index_entry.anchor, |
| self.search_priority, |
| ) |
| |
| def __repr__(self): |
| return f"ObjectEntry({self.full_id=}, {self.object_type=}, {self.display_name=}, {self.index_entry.docname=})" |
| |
| |
| # A simple helper just to document what the index tuple nodes are. |
| def _index_node_tuple( |
| entry_type: str, |
| entry_name: str, |
| target: str, |
| main: typing.Union[str, None] = None, |
| category_key: typing.Union[str, None] = None, |
| ) -> tuple[str, str, str, typing.Union[str, None], typing.Union[str, None]]: |
| # For this tuple definition, see: |
| # https://www.sphinx-doc.org/en/master/extdev/nodes.html#sphinx.addnodes.index |
| # For the definition of entry_type, see: |
| # And https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-index |
| return (entry_type, entry_name, target, main, category_key) |
| |
| |
| class _BzlObjectId: |
| """Identifies an object defined by a directive. |
| |
| This object is returned by `handle_signature()` and passed onto |
| `add_target_and_index()`. It contains information to identify the object |
| that is being described so that it can be indexed and tracked by the |
| domain. |
| """ |
| |
| def __init__( |
| self, |
| *, |
| repo: str, |
| label: str, |
| namespace: str = None, |
| symbol: str = None, |
| ): |
| """Creates an instance. |
| |
| Args: |
| repo: repository name, including leading "@". |
| bzl_file: label of file containing the object, e.g. //foo:bar.bzl |
| namespace: dotted name of the namespace the symbol is within. |
| symbol: dotted name, relative to `namespace` of the symbol. |
| """ |
| if not repo: |
| raise InvalidValueError("repo cannot be empty") |
| if not repo.startswith("@"): |
| raise InvalidValueError("repo must start with @") |
| if not label: |
| raise InvalidValueError("label cannot be empty") |
| if not label.startswith("//"): |
| raise InvalidValueError("label must start with //") |
| |
| if not label.endswith(".bzl") and (symbol or namespace): |
| raise InvalidValueError( |
| "Symbol and namespace can only be specified for .bzl labels" |
| ) |
| |
| self.repo = repo |
| self.label = label |
| self.package, self.target_name = self.label.split(":") |
| self.namespace = namespace |
| self.symbol = symbol # Relative to namespace |
| # doc-relative identifier for this object |
| self.doc_id = symbol or self.target_name |
| |
| if not self.doc_id: |
| raise InvalidValueError("doc_id is empty") |
| |
| self.full_id = _full_id_from_parts(repo, label, [namespace, symbol]) |
| |
| @classmethod |
| def from_env( |
| cls, env: environment.BuildEnvironment, *, symbol: str = None, label: str = None |
| ) -> "_BzlObjectId": |
| label = label or env.ref_context["bzl:file"] |
| if symbol: |
| namespace = ".".join(env.ref_context["bzl:doc_id_stack"]) |
| else: |
| namespace = None |
| |
| return cls( |
| repo=env.ref_context["bzl:repo"], |
| label=label, |
| namespace=namespace, |
| symbol=symbol, |
| ) |
| |
| def __repr__(self): |
| return f"_BzlObjectId({self.full_id=})" |
| |
| |
| def _full_id_from_env(env, object_ids=None): |
| return _full_id_from_parts( |
| env.ref_context["bzl:repo"], |
| env.ref_context["bzl:file"], |
| env.ref_context["bzl:object_id_stack"] + (object_ids or []), |
| ) |
| |
| |
| def _full_id_from_parts(repo, bzl_file, symbol_names=None): |
| parts = [repo, bzl_file] |
| |
| symbol_names = symbol_names or [] |
| symbol_names = list(filter(None, symbol_names)) # Filter out empty values |
| if symbol_names: |
| parts.append("%") |
| parts.append(".".join(symbol_names)) |
| |
| full_id = "".join(parts) |
| return full_id |
| |
| |
| def _parse_full_id(full_id): |
| repo, slashes, label = full_id.partition("//") |
| label = slashes + label |
| label, _, symbol = label.partition("%") |
| return (repo, label, symbol) |
| |
| |
| class _TypeExprParser(ast.NodeVisitor): |
| """Parsers a string description of types to doc nodes.""" |
| |
| def __init__(self, make_xref: Callable[[str], docutils_nodes.Node]): |
| self.root_node = addnodes.desc_inline("bzl", classes=["type-expr"]) |
| self.make_xref = make_xref |
| self._doc_node_stack = [self.root_node] |
| |
| @classmethod |
| def xrefs_from_type_expr( |
| cls, |
| type_expr_str: str, |
| make_xref: Callable[[str], docutils_nodes.Node], |
| ) -> docutils_nodes.Node: |
| module = ast.parse(type_expr_str) |
| visitor = cls(make_xref) |
| visitor.visit(module.body[0]) |
| return visitor.root_node |
| |
| def _append(self, node: docutils_nodes.Node): |
| self._doc_node_stack[-1] += node |
| |
| def _append_and_push(self, node: docutils_nodes.Node): |
| self._append(node) |
| self._doc_node_stack.append(node) |
| |
| def visit_Attribute(self, node: ast.Attribute): |
| current = node |
| parts = [] |
| while current: |
| if isinstance(current, ast.Attribute): |
| parts.append(current.attr) |
| current = current.value |
| elif isinstance(current, ast.Name): |
| parts.append(current.id) |
| break |
| else: |
| raise InvalidValueError(f"Unexpected Attribute.value node: {current}") |
| dotted_name = ".".join(reversed(parts)) |
| self._append(self.make_xref(dotted_name)) |
| |
| def visit_Constant(self, node: ast.Constant): |
| if node.value is None: |
| self._append(self.make_xref("None")) |
| elif isinstance(node.value, str): |
| self._append(self.make_xref(node.value)) |
| else: |
| raise InvalidValueError( |
| f"Unexpected Constant node value: ({type(node.value)}) {node.value=}" |
| ) |
| |
| def visit_Name(self, node: ast.Name): |
| xref_node = self.make_xref(node.id) |
| self._append(xref_node) |
| |
| def visit_BinOp(self, node: ast.BinOp): |
| self.visit(node.left) |
| self._append(addnodes.desc_sig_space()) |
| if isinstance(node.op, ast.BitOr): |
| self._append(addnodes.desc_sig_punctuation("", "|")) |
| else: |
| raise InvalidValueError(f"Unexpected BinOp: {node}") |
| self._append(addnodes.desc_sig_space()) |
| self.visit(node.right) |
| |
| def visit_Expr(self, node: ast.Expr): |
| self.visit(node.value) |
| |
| def visit_Subscript(self, node: ast.Subscript): |
| self.visit(node.value) |
| self._append_and_push(addnodes.desc_type_parameter_list()) |
| self.visit(node.slice) |
| self._doc_node_stack.pop() |
| |
| def visit_Tuple(self, node: ast.Tuple): |
| for element in node.elts: |
| self._append_and_push(addnodes.desc_type_parameter()) |
| self.visit(element) |
| self._doc_node_stack.pop() |
| |
| def visit_List(self, node: ast.List): |
| self._append_and_push(addnodes.desc_type_parameter_list()) |
| for element in node.elts: |
| self._append_and_push(addnodes.desc_type_parameter()) |
| self.visit(element) |
| self._doc_node_stack.pop() |
| |
| @override |
| def generic_visit(self, node): |
| raise InvalidValueError(f"Unexpected ast node: {type(node)} {node}") |
| |
| |
| class _BzlXrefField(docfields.Field): |
| """Abstract base class to create cross references for fields.""" |
| |
| @override |
| def make_xrefs( |
| self, |
| rolename: str, |
| domain: str, |
| target: str, |
| innernode: type[sphinx_typing.TextlikeNode] = addnodes.literal_emphasis, |
| contnode: typing.Union[docutils_nodes.Node, None] = None, |
| env: typing.Union[environment.BuildEnvironment, None] = None, |
| inliner: typing.Union[states.Inliner, None] = None, |
| location: typing.Union[docutils_nodes.Element, None] = None, |
| ) -> list[docutils_nodes.Node]: |
| if rolename in ("arg", "attr"): |
| return self._make_xrefs_for_arg_attr( |
| rolename, domain, target, innernode, contnode, env, inliner, location |
| ) |
| else: |
| return super().make_xrefs( |
| rolename, domain, target, innernode, contnode, env, inliner, location |
| ) |
| |
| def _make_xrefs_for_arg_attr( |
| self, |
| rolename: str, |
| domain: str, |
| arg_name: str, |
| innernode: type[sphinx_typing.TextlikeNode] = addnodes.literal_emphasis, |
| contnode: typing.Union[docutils_nodes.Node, None] = None, |
| env: typing.Union[environment.BuildEnvironment, None] = None, |
| inliner: typing.Union[states.Inliner, None] = None, |
| location: typing.Union[docutils_nodes.Element, None] = None, |
| ) -> list[docutils_nodes.Node]: |
| bzl_file = env.ref_context["bzl:file"] |
| anchor_prefix = ".".join(env.ref_context["bzl:doc_id_stack"]) |
| if not anchor_prefix: |
| raise InvalidValueError( |
| f"doc_id_stack empty when processing arg {arg_name}" |
| ) |
| index_description = f"{arg_name} ({self.name} in {bzl_file}%{anchor_prefix})" |
| anchor_id = f"{anchor_prefix}.{arg_name}" |
| full_id = _full_id_from_env(env, [arg_name]) |
| |
| env.get_domain(domain).add_object( |
| _ObjectEntry( |
| full_id=full_id, |
| display_name=arg_name, |
| object_type=self.name, |
| search_priority=1, |
| index_entry=domains.IndexEntry( |
| name=arg_name, |
| subtype=_INDEX_SUBTYPE_NORMAL, |
| docname=env.docname, |
| anchor=anchor_id, |
| extra="", |
| qualifier="", |
| descr=index_description, |
| ), |
| ), |
| # This allows referencing an arg as e.g `funcname.argname` |
| alt_names=[anchor_id], |
| ) |
| |
| # Two changes to how arg xrefs are created: |
| # 2. Use the full id instead of base name. This makes it unambiguous |
| # as to what it's referencing. |
| pending_xref = super().make_xref( |
| # The full_id is used as the target so its unambiguious. |
| rolename, |
| domain, |
| f"{arg_name} <{full_id}>", |
| innernode, |
| contnode, |
| env, |
| inliner, |
| location, |
| ) |
| |
| wrapper = docutils_nodes.inline(ids=[anchor_id]) |
| |
| index_node = addnodes.index( |
| entries=[ |
| _index_node_tuple( |
| "single", f"{self.name}; {index_description}", anchor_id |
| ), |
| _index_node_tuple("single", index_description, anchor_id), |
| ] |
| ) |
| wrapper += index_node |
| wrapper += pending_xref |
| return [wrapper] |
| |
| |
| class _BzlDocField(_BzlXrefField, docfields.Field): |
| """A non-repeated field with xref support.""" |
| |
| |
| class _BzlGroupedField(_BzlXrefField, docfields.GroupedField): |
| """A repeated fieled grouped as a list with xref support.""" |
| |
| |
| class _BzlCsvField(_BzlXrefField): |
| """Field with a CSV list of values.""" |
| |
| def __init__(self, *args, body_domain: str = "", **kwargs): |
| super().__init__(*args, **kwargs) |
| self._body_domain = body_domain |
| |
| def make_field( |
| self, |
| types: dict[str, list[docutils_nodes.Node]], |
| domain: str, |
| item: tuple, |
| env: environment.BuildEnvironment = None, |
| inliner: typing.Union[states.Inliner, None] = None, |
| location: typing.Union[docutils_nodes.Element, None] = None, |
| ) -> docutils_nodes.field: |
| field_text = item[1][0].astext() |
| parts = [p.strip() for p in field_text.split(",")] |
| field_body = docutils_nodes.field_body() |
| for _, is_last, part in _position_iter(parts): |
| node = self.make_xref( |
| self.bodyrolename, |
| self._body_domain or domain, |
| part, |
| env=env, |
| inliner=inliner, |
| location=location, |
| ) |
| field_body += node |
| if not is_last: |
| field_body += docutils_nodes.Text(", ") |
| |
| field_name = docutils_nodes.field_name("", self.label) |
| return docutils_nodes.field("", field_name, field_body) |
| |
| |
| class _BzlCurrentFile(sphinx_docutils.SphinxDirective): |
| """Sets what bzl file following directives are defined in. |
| |
| The directive's argument is an absolute Bazel label, e.g. `//foo:bar.bzl` |
| or `@repo//foo:bar.bzl`. The repository portion is optional; if specified, |
| it will override the `bzl_default_repository_name` configuration setting. |
| |
| Example MyST usage |
| |
| ``` |
| :::{bzl:currentfile} //my:file.bzl |
| ::: |
| ``` |
| """ |
| |
| has_content = False |
| required_arguments = 1 |
| final_argument_whitespace = False |
| |
| @override |
| def run(self) -> list[docutils_nodes.Node]: |
| label = self.arguments[0].strip() |
| repo, slashes, file_label = label.partition("//") |
| file_label = slashes + file_label |
| if not repo: |
| repo = self.env.config.bzl_default_repository_name |
| self.env.ref_context["bzl:repo"] = repo |
| self.env.ref_context["bzl:file"] = file_label |
| self.env.ref_context["bzl:object_id_stack"] = [] |
| self.env.ref_context["bzl:doc_id_stack"] = [] |
| return [] |
| |
| |
| class _BzlAttrInfo(sphinx_docutils.SphinxDirective): |
| has_content = False |
| required_arguments = 1 |
| optional_arguments = 0 |
| option_spec = { |
| "executable": docutils_directives.flag, |
| "mandatory": docutils_directives.flag, |
| } |
| |
| def run(self): |
| content_node = docutils_nodes.paragraph("", "") |
| content_node += docutils_nodes.paragraph( |
| "", "mandatory" if "mandatory" in self.options else "optional" |
| ) |
| if "executable" in self.options: |
| content_node += docutils_nodes.paragraph("", "Must be an executable") |
| |
| return [content_node] |
| |
| |
| class _BzlObject(sphinx_directives.ObjectDescription[_BzlObjectId]): |
| """Base class for describing a Bazel/Starlark object. |
| |
| This directive takes a single argument: a string name with optional |
| function signature. |
| |
| * The name can be a dotted name, e.g. `a.b.foo` |
| * The signature is in Python signature syntax, e.g. `foo(a=x) -> R` |
| * The signature supports default values. |
| * Arg type annotations are not supported; use `{bzl:type}` instead as |
| part of arg/attr documentation. |
| |
| Example signatures: |
| * `foo` |
| * `foo(arg1, arg2)` |
| * `foo(arg1, arg2=default) -> returntype` |
| """ |
| |
| option_spec = sphinx_directives.ObjectDescription.option_spec | { |
| "origin-key": docutils_directives.unchanged, |
| } |
| |
| @override |
| def before_content(self) -> None: |
| symbol_name = self.names[-1].symbol |
| if symbol_name: |
| self.env.ref_context["bzl:object_id_stack"].append(symbol_name) |
| self.env.ref_context["bzl:doc_id_stack"].append(symbol_name) |
| |
| @override |
| def transform_content(self, content_node: addnodes.desc_content) -> None: |
| def first_child_with_class_name( |
| root, class_name |
| ) -> typing.Union[None, docutils_nodes.Element]: |
| matches = root.findall( |
| lambda node: isinstance(node, docutils_nodes.Element) |
| and class_name in node["classes"] |
| ) |
| found = next(matches, None) |
| return found |
| |
| def match_arg_field_name(node): |
| # fmt: off |
| return ( |
| isinstance(node, docutils_nodes.field_name) |
| and node.astext().startswith(("arg ", "attr ")) |
| ) |
| # fmt: on |
| |
| # Move the spans for the arg type and default value to be first. |
| arg_name_fields = list(content_node.findall(match_arg_field_name)) |
| for arg_name_field in arg_name_fields: |
| arg_body_field = arg_name_field.next_node(descend=False, siblings=True) |
| # arg_type_node = first_child_with_class_name(arg_body_field, "arg-type-span") |
| arg_type_node = first_child_with_class_name(arg_body_field, "type-expr") |
| arg_default_node = first_child_with_class_name( |
| arg_body_field, "default-value-span" |
| ) |
| |
| # Inserting into the body field itself causes the elements |
| # to be grouped into the paragraph node containing the arg |
| # name (as opposed to the paragraph node containing the |
| # doc text) |
| |
| if arg_default_node: |
| arg_default_node.parent.remove(arg_default_node) |
| arg_body_field.insert(0, arg_default_node) |
| |
| if arg_type_node: |
| arg_type_node.parent.remove(arg_type_node) |
| decorated_arg_type_node = docutils_nodes.inline( |
| "", |
| "", |
| docutils_nodes.Text("("), |
| arg_type_node, |
| docutils_nodes.Text(") "), |
| classes=["arg-type-span"], |
| ) |
| # arg_body_field.insert(0, arg_type_node) |
| arg_body_field.insert(0, decorated_arg_type_node) |
| |
| @override |
| def after_content(self) -> None: |
| if self.names[-1].symbol: |
| self.env.ref_context["bzl:object_id_stack"].pop() |
| self.env.ref_context["bzl:doc_id_stack"].pop() |
| |
| # docs on how to build signatures: |
| # https://www.sphinx-doc.org/en/master/extdev/nodes.html#sphinx.addnodes.desc_signature |
| @override |
| def handle_signature( |
| self, sig_text: str, sig_node: addnodes.desc_signature |
| ) -> _BzlObjectId: |
| self._signature_add_object_type(sig_node) |
| |
| relative_name, lparen, params_text = sig_text.partition("(") |
| if lparen: |
| params_text = lparen + params_text |
| |
| relative_name = relative_name.strip() |
| |
| name_prefix, _, base_symbol_name = relative_name.rpartition(".") |
| |
| if name_prefix: |
| # Respect whatever the signature wanted |
| display_prefix = name_prefix |
| else: |
| # Otherwise, show the outermost name. This makes ctrl+f finding |
| # for a symbol a bit easier. |
| display_prefix = ".".join(self.env.ref_context["bzl:doc_id_stack"]) |
| _, _, display_prefix = display_prefix.rpartition(".") |
| |
| if display_prefix: |
| display_prefix = display_prefix + "." |
| sig_node += addnodes.desc_addname(display_prefix, display_prefix) |
| sig_node += addnodes.desc_name(base_symbol_name, base_symbol_name) |
| |
| if type_expr := self.options.get("type"): |
| |
| def make_xref(name, title=None): |
| content_node = addnodes.desc_type(name, name) |
| return addnodes.pending_xref( |
| "", |
| content_node, |
| refdomain="bzl", |
| reftype="type", |
| reftarget=name, |
| ) |
| |
| attr_annotation_node = addnodes.desc_annotation( |
| type_expr, |
| "", |
| addnodes.desc_sig_punctuation("", ":"), |
| addnodes.desc_sig_space(), |
| _TypeExprParser.xrefs_from_type_expr(type_expr, make_xref), |
| ) |
| sig_node += attr_annotation_node |
| |
| if params_text: |
| try: |
| signature = inspect.signature_from_str(params_text) |
| except SyntaxError: |
| # Stardoc doesn't provide accurate info, so the reconstructed |
| # signature might not be valid syntax. Rather than fail, just |
| # provide a plain-text description of the approximate signature. |
| # See https://github.com/bazelbuild/stardoc/issues/225 |
| sig_node += addnodes.desc_parameterlist( |
| # Offset by 1 to remove the surrounding parentheses |
| params_text[1:-1], |
| params_text[1:-1], |
| ) |
| else: |
| last_kind = None |
| paramlist_node = addnodes.desc_parameterlist() |
| for param in signature.parameters.values(): |
| if param.kind == param.KEYWORD_ONLY and last_kind in ( |
| param.POSITIONAL_OR_KEYWORD, |
| param.POSITIONAL_ONLY, |
| None, |
| ): |
| # Add separator for keyword only parameter: * |
| paramlist_node += addnodes.desc_parameter( |
| "", "", addnodes.desc_sig_operator("", "*") |
| ) |
| |
| last_kind = param.kind |
| node = addnodes.desc_parameter() |
| if param.kind == param.VAR_POSITIONAL: |
| node += addnodes.desc_sig_operator("", "*") |
| elif param.kind == param.VAR_KEYWORD: |
| node += addnodes.desc_sig_operator("", "**") |
| |
| node += addnodes.desc_sig_name(rawsource="", text=param.name) |
| if param.default is not param.empty: |
| node += addnodes.desc_sig_operator("", "=") |
| node += docutils_nodes.inline( |
| "", |
| param.default, |
| classes=["default_value"], |
| support_smartquotes=False, |
| ) |
| paramlist_node += node |
| sig_node += paramlist_node |
| |
| if signature.return_annotation is not signature.empty: |
| sig_node += addnodes.desc_returns("", signature.return_annotation) |
| |
| obj_id = _BzlObjectId.from_env(self.env, symbol=relative_name) |
| |
| sig_node["bzl:object_id"] = obj_id.full_id |
| return obj_id |
| |
| def _signature_add_object_type(self, sig_node: addnodes.desc_signature): |
| if sig_object_type := self._get_signature_object_type(): |
| sig_node += addnodes.desc_annotation("", self._get_signature_object_type()) |
| sig_node += addnodes.desc_sig_space() |
| |
| @override |
| def add_target_and_index( |
| self, obj_desc: _BzlObjectId, sig: str, sig_node: addnodes.desc_signature |
| ) -> None: |
| super().add_target_and_index(obj_desc, sig, sig_node) |
| if obj_desc.symbol: |
| display_name = obj_desc.symbol |
| location = obj_desc.label |
| if obj_desc.namespace: |
| location += f"%{obj_desc.namespace}" |
| else: |
| display_name = obj_desc.target_name |
| location = obj_desc.package |
| |
| anchor_prefix = ".".join(self.env.ref_context["bzl:doc_id_stack"]) |
| if anchor_prefix: |
| anchor_id = f"{anchor_prefix}.{obj_desc.doc_id}" |
| else: |
| anchor_id = obj_desc.doc_id |
| |
| sig_node["ids"].append(anchor_id) |
| |
| object_type_display = self._get_object_type_display_name() |
| index_description = f"{display_name} ({object_type_display} in {location})" |
| self.indexnode["entries"].extend( |
| _index_node_tuple("single", f"{index_type}; {index_description}", anchor_id) |
| for index_type in [object_type_display] + self._get_additional_index_types() |
| ) |
| self.indexnode["entries"].append( |
| _index_node_tuple("single", index_description, anchor_id), |
| ) |
| |
| object_entry = _ObjectEntry( |
| full_id=obj_desc.full_id, |
| display_name=display_name, |
| object_type=self.objtype, |
| search_priority=1, |
| index_entry=domains.IndexEntry( |
| name=display_name, |
| subtype=_INDEX_SUBTYPE_NORMAL, |
| docname=self.env.docname, |
| anchor=anchor_id, |
| extra="", |
| qualifier="", |
| descr=index_description, |
| ), |
| ) |
| |
| alt_names = [] |
| if origin_key := self.options.get("origin-key"): |
| alt_names.append( |
| origin_key |
| # Options require \@ for leading @, but don't |
| # remove the escaping slash, so we have to do it manually |
| .lstrip("\\") |
| ) |
| extra_alt_names = self._get_alt_names(object_entry) |
| alt_names.extend(extra_alt_names) |
| |
| self.env.get_domain(self.domain).add_object(object_entry, alt_names=alt_names) |
| |
| def _get_additional_index_types(self): |
| return [] |
| |
| @override |
| def _object_hierarchy_parts( |
| self, sig_node: addnodes.desc_signature |
| ) -> tuple[str, ...]: |
| return _parse_full_id(sig_node["bzl:object_id"]) |
| |
| @override |
| def _toc_entry_name(self, sig_node: addnodes.desc_signature) -> str: |
| return sig_node["_toc_parts"][-1] |
| |
| def _get_object_type_display_name(self) -> str: |
| return self.env.get_domain(self.domain).object_types[self.objtype].lname |
| |
| def _get_signature_object_type(self) -> str: |
| return self._get_object_type_display_name() |
| |
| def _get_alt_names(self, object_entry): |
| alt_names = [] |
| full_id = object_entry.full_id |
| label, _, symbol = full_id.partition("%") |
| if symbol: |
| # Allow referring to the file-relative fully qualified symbol name |
| alt_names.append(symbol) |
| if "." in symbol: |
| # Allow referring to the last component of the symbol |
| alt_names.append(symbol.split(".")[-1]) |
| else: |
| # Otherwise, it's a target. Allow referring to just the target name |
| _, _, target_name = label.partition(":") |
| alt_names.append(target_name) |
| |
| return alt_names |
| |
| |
| class _BzlCallable(_BzlObject): |
| """Abstract base class for objects that are callable.""" |
| |
| |
| class _BzlTypedef(_BzlObject): |
| """Documents a typedef. |
| |
| A typedef describes objects with well known attributes. |
| |
| ````` |
| ::::{bzl:typedef} Square |
| |
| :::{bzl:field} width |
| :type: int |
| ::: |
| |
| :::{bzl:function} new(size) |
| ::: |
| |
| :::{bzl:function} area() |
| ::: |
| :::: |
| ````` |
| """ |
| |
| |
| class _BzlProvider(_BzlObject): |
| """Documents a provider type. |
| |
| Example MyST usage |
| |
| ``` |
| ::::{bzl:provider} MyInfo |
| |
| Docs about MyInfo |
| |
| :::{bzl:provider-field} some_field |
| :type: depset[str] |
| ::: |
| :::: |
| ``` |
| """ |
| |
| |
| class _BzlField(_BzlObject): |
| """Documents a field of a provider. |
| |
| Fields can optionally have a type specified using the `:type:` option. |
| |
| The type can be any type expression understood by the `{bzl:type}` role. |
| |
| ``` |
| :::{bzl:provider-field} foo |
| :type: str |
| ::: |
| ``` |
| """ |
| |
| option_spec = _BzlObject.option_spec.copy() |
| option_spec.update( |
| { |
| "type": docutils_directives.unchanged, |
| } |
| ) |
| |
| @override |
| def _get_signature_object_type(self) -> str: |
| return "" |
| |
| @override |
| def _get_alt_names(self, object_entry): |
| alt_names = super()._get_alt_names(object_entry) |
| _, _, symbol = object_entry.full_id.partition("%") |
| # Allow refering to `mod_ext_name.tag_name`, even if the extension |
| # is nested within another object |
| alt_names.append(".".join(symbol.split(".")[-2:])) |
| return alt_names |
| |
| |
| class _BzlProviderField(_BzlField): |
| pass |
| |
| |
| class _BzlRepositoryRule(_BzlCallable): |
| """Documents a repository rule. |
| |
| Doc fields: |
| * attr: Documents attributes of the rule. Takes a single arg, the |
| attribute name. Can be repeated. The special roles `{default-value}` |
| and `{arg-type}` can be used to indicate the default value and |
| type of attribute, respectively. |
| * environment-variables: a CSV list of environment variable names. |
| They will be cross referenced with matching environment variables. |
| |
| Example MyST usage |
| |
| ``` |
| :::{bzl:repo-rule} myrule(foo) |
| |
| :attr foo: {default-value}`"foo"` {arg-type}`attr.string` foo doc string |
| |
| :environment-variables: FOO, BAR |
| ::: |
| ``` |
| """ |
| |
| doc_field_types = [ |
| _BzlGroupedField( |
| "attr", |
| label=_("Attributes"), |
| names=["attr"], |
| rolename="attr", |
| can_collapse=False, |
| ), |
| _BzlCsvField( |
| "environment-variables", |
| label=_("Environment Variables"), |
| names=["environment-variables"], |
| body_domain="std", |
| bodyrolename="envvar", |
| has_arg=False, |
| ), |
| ] |
| |
| @override |
| def _get_signature_object_type(self) -> str: |
| return "repo rule" |
| |
| |
| class _BzlRule(_BzlCallable): |
| """Documents a rule. |
| |
| Doc fields: |
| * attr: Documents attributes of the rule. Takes a single arg, the |
| attribute name. Can be repeated. The special roles `{default-value}` |
| and `{arg-type}` can be used to indicate the default value and |
| type of attribute, respectively. |
| * provides: A type expression of the provider types the rule provides. |
| To indicate different groupings, use `|` and `[]`. For example, |
| `FooInfo | [BarInfo, BazInfo]` means it provides either `FooInfo` |
| or both of `BarInfo` and `BazInfo`. |
| |
| Example MyST usage |
| |
| ``` |
| :::{bzl:repo-rule} myrule(foo) |
| |
| :attr foo: {default-value}`"foo"` {arg-type}`attr.string` foo doc string |
| |
| :provides: FooInfo | BarInfo |
| ::: |
| ``` |
| """ |
| |
| doc_field_types = [ |
| _BzlGroupedField( |
| "attr", |
| label=_("Attributes"), |
| names=["attr"], |
| rolename="attr", |
| can_collapse=False, |
| ), |
| _BzlDocField( |
| "provides", |
| label="Provides", |
| has_arg=False, |
| names=["provides"], |
| bodyrolename="type", |
| ), |
| ] |
| |
| |
| class _BzlAspect(_BzlObject): |
| """Documents an aspect. |
| |
| Doc fields: |
| * attr: Documents attributes of the aspect. Takes a single arg, the |
| attribute name. Can be repeated. The special roles `{default-value}` |
| and `{arg-type}` can be used to indicate the default value and |
| type of attribute, respectively. |
| * aspect-attributes: A CSV list of attribute names the aspect |
| propagates along. |
| |
| Example MyST usage |
| |
| ``` |
| :::{bzl:repo-rule} myaspect |
| |
| :attr foo: {default-value}`"foo"` {arg-type}`attr.string` foo doc string |
| |
| :aspect-attributes: srcs, deps |
| ::: |
| ``` |
| """ |
| |
| doc_field_types = [ |
| _BzlGroupedField( |
| "attr", |
| label=_("Attributes"), |
| names=["attr"], |
| rolename="attr", |
| can_collapse=False, |
| ), |
| _BzlCsvField( |
| "aspect-attributes", |
| label=_("Aspect Attributes"), |
| names=["aspect-attributes"], |
| has_arg=False, |
| ), |
| ] |
| |
| |
| class _BzlFunction(_BzlCallable): |
| """Documents a general purpose function. |
| |
| Doc fields: |
| * arg: Documents the arguments of the function. Takes a single arg, the |
| arg name. Can be repeated. The special roles `{default-value}` |
| and `{arg-type}` can be used to indicate the default value and |
| type of attribute, respectively. |
| * returns: Documents what the function returns. The special role |
| `{return-type}` can be used to indicate the return type of the function. |
| |
| Example MyST usage |
| |
| ``` |
| :::{bzl:function} myfunc(a, b=None) -> bool |
| |
| :arg a: {arg-type}`str` some arg doc |
| :arg b: {arg-type}`int | None` {default-value}`42` more arg doc |
| :returns: {return-type}`bool` doc about return value. |
| ::: |
| ``` |
| """ |
| |
| doc_field_types = [ |
| _BzlGroupedField( |
| "arg", |
| label=_("Args"), |
| names=["arg"], |
| rolename="arg", |
| can_collapse=False, |
| ), |
| docfields.Field( |
| "returns", |
| label=_("Returns"), |
| has_arg=False, |
| names=["returns"], |
| ), |
| ] |
| |
| @override |
| def _get_signature_object_type(self) -> str: |
| return "" |
| |
| |
| class _BzlModuleExtension(_BzlObject): |
| """Documents a module_extension. |
| |
| Doc fields: |
| * os-dependent: Documents if the module extension depends on the host |
| architecture. |
| * arch-dependent: Documents if the module extension depends on the host |
| architecture. |
| * environment-variables: a CSV list of environment variable names. |
| They will be cross referenced with matching environment variables. |
| |
| Tag classes are documented using the bzl:tag-class directives within |
| this directive. |
| |
| Example MyST usage: |
| |
| ``` |
| ::::{bzl:module-extension} myext |
| |
| :os-dependent: True |
| :arch-dependent: False |
| |
| :::{bzl:tag-class} mytag(myattr) |
| |
| :attr myattr: |
| {arg-type}`attr.string_list` |
| doc for attribute |
| ::: |
| :::: |
| ``` |
| """ |
| |
| doc_field_types = [ |
| _BzlDocField( |
| "os-dependent", |
| label="OS Dependent", |
| has_arg=False, |
| names=["os-dependent"], |
| ), |
| _BzlDocField( |
| "arch-dependent", |
| label="Arch Dependent", |
| has_arg=False, |
| names=["arch-dependent"], |
| ), |
| _BzlCsvField( |
| "environment-variables", |
| label=_("Environment Variables"), |
| names=["environment-variables"], |
| body_domain="std", |
| bodyrolename="envvar", |
| has_arg=False, |
| ), |
| ] |
| |
| @override |
| def _get_signature_object_type(self) -> str: |
| return "module ext" |
| |
| |
| class _BzlTagClass(_BzlCallable): |
| """Documents a tag class for a module extension. |
| |
| Doc fields: |
| * attr: Documents attributes of the tag class. Takes a single arg, the |
| attribute name. Can be repeated. The special roles `{default-value}` |
| and `{arg-type}` can be used to indicate the default value and |
| type of attribute, respectively. |
| |
| Example MyST usage, note that this directive should be nested with |
| a `bzl:module-extension` directive. |
| |
| ``` |
| :::{bzl:tag-class} mytag(myattr) |
| |
| :attr myattr: |
| {arg-type}`attr.string_list` |
| doc for attribute |
| ::: |
| ``` |
| """ |
| |
| doc_field_types = [ |
| _BzlGroupedField( |
| "arg", |
| label=_("Attributes"), |
| names=["attr"], |
| rolename="arg", |
| can_collapse=False, |
| ), |
| ] |
| |
| @override |
| def _get_signature_object_type(self) -> str: |
| return "" |
| |
| @override |
| def _get_alt_names(self, object_entry): |
| alt_names = super()._get_alt_names(object_entry) |
| _, _, symbol = object_entry.full_id.partition("%") |
| # Allow refering to `ProviderName.field`, even if the provider |
| # is nested within another object |
| alt_names.append(".".join(symbol.split(".")[-2:])) |
| return alt_names |
| |
| |
| class _TargetType(enum.Enum): |
| TARGET = "target" |
| FLAG = "flag" |
| |
| |
| class _BzlTarget(_BzlObject): |
| """Documents an arbitrary target.""" |
| |
| _TARGET_TYPE = _TargetType.TARGET |
| |
| def handle_signature(self, sig_text, sig_node): |
| self._signature_add_object_type(sig_node) |
| if ":" in sig_text: |
| package, target_name = sig_text.split(":", 1) |
| else: |
| target_name = sig_text |
| package = self.env.ref_context["bzl:file"] |
| package = package[: package.find(":BUILD")] |
| |
| package = package + ":" |
| if self._TARGET_TYPE == _TargetType.FLAG: |
| sig_node += addnodes.desc_addname("--", "--") |
| sig_node += addnodes.desc_addname(package, package) |
| sig_node += addnodes.desc_name(target_name, target_name) |
| |
| obj_id = _BzlObjectId.from_env(self.env, label=package + target_name) |
| sig_node["bzl:object_id"] = obj_id.full_id |
| return obj_id |
| |
| @override |
| def _get_signature_object_type(self) -> str: |
| # We purposely return empty here because having "target" in front |
| # of every label isn't very helpful |
| return "" |
| |
| |
| # TODO: Integrate with the option directive, since flags are options, afterall. |
| # https://www.sphinx-doc.org/en/master/usage/domains/standard.html#directive-option |
| class _BzlFlag(_BzlTarget): |
| """Documents a flag""" |
| |
| _TARGET_TYPE = _TargetType.FLAG |
| |
| @override |
| def _get_signature_object_type(self) -> str: |
| return "flag" |
| |
| def _get_additional_index_types(self): |
| return ["target"] |
| |
| |
| class _DefaultValueRole(sphinx_docutils.SphinxRole): |
| """Documents the default value for an arg or attribute. |
| |
| This is a special role used within `:arg:` and `:attr:` doc fields to |
| indicate the default value. The rendering process looks for this role |
| and reformats and moves its content for better display. |
| |
| Styling can be customized by matching the `.default_value` class. |
| """ |
| |
| def run(self) -> _RoleRunResult: |
| node = docutils_nodes.emphasis( |
| "", |
| "(default ", |
| docutils_nodes.inline("", self.text, classes=["sig", "default_value"]), |
| docutils_nodes.Text(") "), |
| classes=["default-value-span"], |
| ) |
| return ([node], []) |
| |
| |
| class _TypeRole(sphinx_docutils.SphinxRole): |
| """Documents a type (or type expression) with crossreferencing. |
| |
| This is an inline role used to create cross references to other types. |
| |
| The content is interpreted as a reference to a type or an expression |
| of types. The syntax uses Python-style sytax with `|` and `[]`, e.g. |
| `foo.MyType | str | list[str] | dict[str, int]`. Each symbolic name |
| will be turned into a cross reference; see the domain's documentation |
| for how to reference objects. |
| |
| Example MyST usage: |
| |
| ``` |
| This function accepts {bzl:type}`str | list[str]` for usernames |
| ``` |
| """ |
| |
| def __init__(self): |
| super().__init__() |
| self._xref = roles.XRefRole() |
| |
| def run(self) -> _RoleRunResult: |
| outer_messages = [] |
| |
| def make_xref(name): |
| nodes, msgs = self._xref( |
| "bzl:type", |
| name, |
| name, |
| self.lineno, |
| self.inliner, |
| self.options, |
| self.content, |
| ) |
| outer_messages.extend(msgs) |
| if len(nodes) == 1: |
| return nodes[0] |
| else: |
| return docutils_nodes.inline("", "", nodes) |
| |
| root = _TypeExprParser.xrefs_from_type_expr(self.text, make_xref) |
| return ([root], outer_messages) |
| |
| |
| class _ReturnTypeRole(_TypeRole): |
| """Documents the return type for function. |
| |
| This is a special role used within `:returns:` doc fields to |
| indicate the return type of the function. The rendering process looks for |
| this role and reformats and moves its content for better display. |
| |
| Example MyST Usage |
| |
| ``` |
| :::{bzl:function} foo() |
| |
| :returns: {return-type}`list[str]` |
| ::: |
| ``` |
| """ |
| |
| def run(self) -> _RoleRunResult: |
| nodes, messages = super().run() |
| nodes.append(docutils_nodes.Text(" -- ")) |
| return nodes, messages |
| |
| |
| class _RequiredProvidersRole(_TypeRole): |
| """Documents the providers an attribute requires. |
| |
| This is a special role used within `:arg:` or `:attr:` doc fields to |
| indicate the types of providers that are required. The rendering process |
| looks for this role and reformats its content for better display, but its |
| position is left as-is; typically it would be its own paragraph near the |
| end of the doc. |
| |
| The syntax is a pipe (`|`) delimited list of types or groups of types, |
| where groups are indicated using `[...]`. e.g, to express that FooInfo OR |
| (both of BarInfo and BazInfo) are supported, write `FooInfo | [BarInfo, |
| BazInfo]` |
| |
| Example MyST Usage |
| |
| ``` |
| :::{bzl:rule} foo(bar) |
| |
| :attr bar: My attribute doc |
| |
| {required-providers}`CcInfo | [PyInfo, JavaInfo]` |
| ::: |
| ``` |
| """ |
| |
| def run(self) -> _RoleRunResult: |
| xref_nodes, messages = super().run() |
| nodes = [ |
| docutils_nodes.emphasis("", "Required providers: "), |
| ] + xref_nodes |
| return nodes, messages |
| |
| |
| class _BzlIndex(domains.Index): |
| """An index of a bzl file's objects. |
| |
| NOTE: This generates the entries for the *domain specific* index |
| (bzl-index.html), not the general index (genindex.html). To affect |
| the general index, index nodes and directives must be used (grep |
| for `self.indexnode`). |
| """ |
| |
| name = "index" |
| localname = "Bazel/Starlark Object Index" |
| shortname = "Bzl" |
| |
| def generate( |
| self, docnames: Iterable[str] = None |
| ) -> tuple[list[tuple[str, list[domains.IndexEntry]]], bool]: |
| content = collections.defaultdict(list) |
| |
| # sort the list of objects in alphabetical order |
| objects = self.domain.data["objects"].values() |
| objects = sorted(objects, key=lambda obj: obj.index_entry.name) |
| |
| # Group by first letter |
| for entry in objects: |
| index_entry = entry.index_entry |
| content[index_entry.name[0].lower()].append(index_entry) |
| |
| # convert the dict to the sorted list of tuples expected |
| content = sorted(content.items()) |
| |
| return content, True |
| |
| |
| class _BzlDomain(domains.Domain): |
| """Domain for Bazel/Starlark objects. |
| |
| Directives |
| |
| There are directives for defining Bazel objects and their functionality. |
| See the respective directive classes for details. |
| |
| Public Crossreferencing Roles |
| |
| These are roles that can be used in docs to create cross references. |
| |
| Objects are fully identified using dotted notation converted from the Bazel |
| label and symbol name within a `.bzl` file. The `@`, `/` and `:` characters |
| are converted to dots (with runs removed), and `.bzl` is removed from file |
| names. The dotted path of a symbol in the bzl file is appended. For example, |
| the `paths.join` function in `@bazel_skylib//lib:paths.bzl` would be |
| identified as `bazel_skylib.lib.paths.paths.join`. |
| |
| Shorter identifiers can be used. Within a project, the repo name portion |
| can be omitted. Within a file, file-relative names can be used. |
| |
| * obj: Used to reference a single object without concern for its type. |
| This roles searches all object types for a name that matches the given |
| value. Example usage in MyST: |
| ``` |
| {bzl:obj}`repo.pkg.file.my_function` |
| ``` |
| |
| * type: Transforms a type expression into cross references for objects |
| with object type "type". For example, it parses `int | list[str]` into |
| three links for each component part. |
| |
| Public Typography Roles |
| |
| These are roles used for special purposes to aid documentation. |
| |
| * default-value: The default value for an argument or attribute. Only valid |
| to use within arg or attribute documentation. See `_DefaultValueRole` for |
| details. |
| * required-providers: The providers an attribute requires. Only |
| valud to use within an attribute documentation. See |
| `_RequiredProvidersRole` for details. |
| * return-type: The type of value a function returns. Only valid |
| within a function's return doc field. See `_ReturnTypeRole` for details. |
| |
| Object Types |
| |
| These are the types of objects that this domain keeps in its index. |
| |
| * arg: An argument to a function or macro. |
| * aspect: A Bazel `aspect`. |
| * attribute: An input to a rule (regular, repository, aspect, or module |
| extension). |
| * method: A function bound to an instance of a struct acting as a type. |
| * module-extension: A Bazel `module_extension`. |
| * provider: A Bazel `provider`. |
| * provider-field: A field of a provider. |
| * repo-rule: A Bazel `repository_rule`. |
| * rule: A regular Bazel `rule`. |
| * tag-class: A Bazel `tag_class` of a `module_extension`. |
| * target: A Bazel target. |
| * type: A builtin Bazel type or user-defined structural type. User defined |
| structual types are typically instances `struct` created using a function |
| that acts as a constructor with implicit state bound using closures. |
| """ |
| |
| name = "bzl" |
| label = "Bzl" |
| |
| # NOTE: Most every object type has "obj" as one of the roles because |
| # an object type's role determine what reftypes (cross referencing) can |
| # refer to it. By having "obj" for all of them, it allows writing |
| # :bzl:obj`foo` to restrict object searching to the bzl domain. Under the |
| # hood, this domain translates requests for the :any: role as lookups for |
| # :obj:. |
| # NOTE: We also use these object types for categorizing things in the |
| # generated index page. |
| object_types = { |
| "arg": domains.ObjType("arg", "arg", "obj"), # macro/function arg |
| "aspect": domains.ObjType("aspect", "aspect", "obj"), |
| "attr": domains.ObjType("attr", "attr", "obj"), # rule attribute |
| "function": domains.ObjType("function", "func", "obj"), |
| "method": domains.ObjType("method", "method", "obj"), |
| "module-extension": domains.ObjType( |
| "module extension", "module_extension", "obj" |
| ), |
| # Providers are close enough to types that we include "type". This |
| # also makes :type: Foo work in directive options. |
| "provider": domains.ObjType("provider", "provider", "type", "obj"), |
| "provider-field": domains.ObjType("provider field", "provider-field", "obj"), |
| "field": domains.ObjType("field", "field", "obj"), |
| "repo-rule": domains.ObjType("repository rule", "repo_rule", "obj"), |
| "rule": domains.ObjType("rule", "rule", "obj"), |
| "tag-class": domains.ObjType("tag class", "tag_class", "obj"), |
| "target": domains.ObjType("target", "target", "obj"), # target in a build file |
| # Flags are also targets, so include "target" for xref'ing |
| "flag": domains.ObjType("flag", "flag", "target", "obj"), |
| # types are objects that have a constructor and methods/attrs |
| "type": domains.ObjType("type", "type", "obj"), |
| "typedef": domains.ObjType("typedef", "typedef", "type", "obj"), |
| } |
| |
| # This controls: |
| # * What is recognized when parsing, e.g. ":bzl:ref:`foo`" requires |
| # "ref" to be in the role dict below. |
| roles = { |
| "arg": roles.XRefRole(), |
| "attr": roles.XRefRole(), |
| "default-value": _DefaultValueRole(), |
| "flag": roles.XRefRole(), |
| "obj": roles.XRefRole(), |
| "required-providers": _RequiredProvidersRole(), |
| "return-type": _ReturnTypeRole(), |
| "rule": roles.XRefRole(), |
| "target": roles.XRefRole(), |
| "type": _TypeRole(), |
| } |
| # NOTE: Directives that have a corresponding object type should use |
| # the same key for both directive and object type. Some directives |
| # look up their corresponding object type. |
| directives = { |
| "aspect": _BzlAspect, |
| "currentfile": _BzlCurrentFile, |
| "function": _BzlFunction, |
| "module-extension": _BzlModuleExtension, |
| "provider": _BzlProvider, |
| "typedef": _BzlTypedef, |
| "provider-field": _BzlProviderField, |
| "field": _BzlField, |
| "repo-rule": _BzlRepositoryRule, |
| "rule": _BzlRule, |
| "tag-class": _BzlTagClass, |
| "target": _BzlTarget, |
| "flag": _BzlFlag, |
| "attr-info": _BzlAttrInfo, |
| } |
| indices = { |
| _BzlIndex, |
| } |
| |
| # NOTE: When adding additional data keys, make sure to update |
| # merge_domaindata |
| initial_data = { |
| # All objects; keyed by full id |
| # dict[str, _ObjectEntry] |
| "objects": {}, |
| # dict[str, dict[str, _ObjectEntry]] |
| "objects_by_type": {}, |
| # Objects within each doc |
| # dict[str, dict[str, _ObjectEntry]] |
| "doc_names": {}, |
| # Objects by a shorter or alternative name |
| # dict[str, dict[str id, _ObjectEntry]] |
| "alt_names": {}, |
| } |
| |
| @override |
| def get_full_qualified_name( |
| self, node: docutils_nodes.Element |
| ) -> typing.Union[str, None]: |
| bzl_file = node.get("bzl:file") |
| symbol_name = node.get("bzl:symbol") |
| ref_target = node.get("reftarget") |
| return ".".join(filter(None, [bzl_file, symbol_name, ref_target])) |
| |
| @override |
| def get_objects(self) -> Iterable[_GetObjectsTuple]: |
| for entry in self.data["objects"].values(): |
| yield entry.to_get_objects_tuple() |
| |
| @override |
| def resolve_any_xref( |
| self, |
| env: environment.BuildEnvironment, |
| fromdocname: str, |
| builder: builders.Builder, |
| target: str, |
| node: addnodes.pending_xref, |
| contnode: docutils_nodes.Element, |
| ) -> list[tuple[str, docutils_nodes.Element]]: |
| del env, node # Unused |
| entry = self._find_entry_for_xref(fromdocname, "obj", target) |
| if not entry: |
| return [] |
| to_docname = entry.index_entry.docname |
| to_anchor = entry.index_entry.anchor |
| ref_node = sphinx_nodes.make_refnode( |
| builder, fromdocname, to_docname, to_anchor, contnode, title=to_anchor |
| ) |
| |
| matches = [(f"bzl:{entry.object_type}", ref_node)] |
| return matches |
| |
| @override |
| def resolve_xref( |
| self, |
| env: environment.BuildEnvironment, |
| fromdocname: str, |
| builder: builders.Builder, |
| typ: str, |
| target: str, |
| node: addnodes.pending_xref, |
| contnode: docutils_nodes.Element, |
| ) -> typing.Union[docutils_nodes.Element, None]: |
| _log_debug( |
| "resolve_xref: fromdocname=%s, typ=%s, target=%s", fromdocname, typ, target |
| ) |
| del env, node # Unused |
| entry = self._find_entry_for_xref(fromdocname, typ, target) |
| if not entry: |
| return None |
| |
| to_docname = entry.index_entry.docname |
| to_anchor = entry.index_entry.anchor |
| return sphinx_nodes.make_refnode( |
| builder, fromdocname, to_docname, to_anchor, contnode, title=to_anchor |
| ) |
| |
| def _find_entry_for_xref( |
| self, fromdocname: str, object_type: str, target: str |
| ) -> typing.Union[_ObjectEntry, None]: |
| if target.startswith("--"): |
| target = target.strip("-") |
| object_type = "flag" |
| |
| # Allow using parentheses, e.g. `foo()` or `foo(x=...)` |
| target, _, _ = target.partition("(") |
| |
| # Elide the value part of --foo=bar flags |
| # Note that the flag value could contain `=` |
| if "=" in target: |
| target = target[: target.find("=")] |
| |
| if target in self.data["doc_names"].get(fromdocname, {}): |
| entry = self.data["doc_names"][fromdocname][target] |
| # Prevent a local doc name masking a global alt name when its of |
| # a different type. e.g. when the macro `foo` refers to the |
| # rule `foo` in another doc. |
| if object_type in self.object_types[entry.object_type].roles: |
| return entry |
| |
| if object_type == "obj": |
| search_space = self.data["objects"] |
| else: |
| search_space = self.data["objects_by_type"].get(object_type, {}) |
| if target in search_space: |
| return search_space[target] |
| |
| _log_debug("find_entry: alt_names=%s", sorted(self.data["alt_names"].keys())) |
| if target in self.data["alt_names"]: |
| # Give preference to shorter object ids. This is a work around |
| # to allow e.g. `FooInfo` to refer to the FooInfo type rather than |
| # the `FooInfo` constructor. |
| entries = sorted( |
| self.data["alt_names"][target].items(), key=lambda item: len(item[0]) |
| ) |
| for _, entry in entries: |
| if object_type in self.object_types[entry.object_type].roles: |
| return entry |
| |
| return None |
| |
| def add_object(self, entry: _ObjectEntry, alt_names=None) -> None: |
| _log_debug( |
| "add_object: full_id=%s, object_type=%s, alt_names=%s", |
| entry.full_id, |
| entry.object_type, |
| alt_names, |
| ) |
| if entry.full_id in self.data["objects"]: |
| existing = self.data["objects"][entry.full_id] |
| raise Exception( |
| f"Object {entry.full_id} already registered: " |
| + f"existing={existing}, incoming={entry}" |
| ) |
| self.data["objects"][entry.full_id] = entry |
| self.data["objects_by_type"].setdefault(entry.object_type, {}) |
| self.data["objects_by_type"][entry.object_type][entry.full_id] = entry |
| |
| repo, label, symbol = _parse_full_id(entry.full_id) |
| if symbol: |
| base_name = symbol.split(".")[-1] |
| else: |
| base_name = label.split(":")[-1] |
| |
| if alt_names is not None: |
| alt_names = list(alt_names) |
| # Add the repo-less version as an alias |
| alt_names.append(label + (f"%{symbol}" if symbol else "")) |
| |
| for alt_name in sorted(set(alt_names)): |
| self.data["alt_names"].setdefault(alt_name, {}) |
| self.data["alt_names"][alt_name][entry.full_id] = entry |
| |
| docname = entry.index_entry.docname |
| self.data["doc_names"].setdefault(docname, {}) |
| self.data["doc_names"][docname][base_name] = entry |
| |
| def merge_domaindata( |
| self, docnames: list[str], otherdata: dict[str, typing.Any] |
| ) -> None: |
| # Merge in simple dict[key, value] data |
| for top_key in ("objects",): |
| self.data[top_key].update(otherdata.get(top_key, {})) |
| |
| # Merge in two-level dict[top_key, dict[sub_key, value]] data |
| for top_key in ("objects_by_type", "doc_names", "alt_names"): |
| existing_top_map = self.data[top_key] |
| for sub_key, sub_values in otherdata.get(top_key, {}).items(): |
| if sub_key not in existing_top_map: |
| existing_top_map[sub_key] = sub_values |
| else: |
| existing_top_map[sub_key].update(sub_values) |
| |
| |
| def _on_missing_reference(app, env: environment.BuildEnvironment, node, contnode): |
| if node["refdomain"] != "bzl": |
| return None |
| if node["reftype"] != "type": |
| return None |
| |
| # There's no Bazel docs for None, so prevent missing xrefs warning |
| if node["reftarget"] == "None": |
| return contnode |
| return None |
| |
| |
| def setup(app): |
| app.add_domain(_BzlDomain) |
| |
| app.add_config_value( |
| "bzl_default_repository_name", |
| default=os.environ.get("SPHINX_BZL_DEFAULT_REPOSITORY_NAME", "@_main"), |
| rebuild="env", |
| types=[str], |
| ) |
| app.connect("missing-reference", _on_missing_reference) |
| |
| # Pygments says it supports starlark, but it doesn't seem to actually |
| # recognize `starlark` as a name. So just manually map it to python. |
| app.add_lexer("starlark", lexer_classes["python"]) |
| app.add_lexer("bzl", lexer_classes["python"]) |
| |
| return { |
| "version": "1.0.0", |
| "parallel_read_safe": True, |
| "parallel_write_safe": True, |
| } |