@@ -8,11 +8,12 @@
88import subprocess
99import sys
1010import warnings
11+from collections import defaultdict
1112from dataclasses import replace
1213from functools import lru_cache
1314from pathlib import Path
1415from re import Match, Pattern
15-from typing import TYPE_CHECKING, Any, Callable, ClassVar, Literal
16+from typing import TYPE_CHECKING, Any, Callable, ClassVar, Literal, TypeVar
16171718from griffe import (
1819Alias,
@@ -28,11 +29,11 @@
2829)
2930from jinja2 import TemplateNotFound, pass_context, pass_environment
3031from markupsafe import Markup
31-from mkdocs_autorefs import AutorefsHookInterface
32+from mkdocs_autorefs import AutorefsHookInterface, Backlink, BacklinkCrumb
3233from mkdocstrings import get_logger
33343435if TYPE_CHECKING:
35-from collections.abc import Iterator, Sequence
36+from collections.abc import Iterable, Iterator, Sequence
36373738from griffe import Attribute, Class, Function, Module
3839from jinja2 import Environment, Template
@@ -210,10 +211,15 @@ def do_format_attribute(
210211211212signature = str(attribute_path).strip()
212213if annotations and attribute.annotation:
213-annotation = template.render(context.parent, expression=attribute.annotation, signature=True)
214+annotation = template.render(
215+context.parent,
216+expression=attribute.annotation,
217+signature=True,
218+backlink_type="returned-by",
219+ )
214220signature += f": {annotation}"
215221if attribute.value:
216-value = template.render(context.parent, expression=attribute.value, signature=True)
222+value = template.render(context.parent, expression=attribute.value, signature=True, backlink_type="used-by")
217223signature += f" = {value}"
218224219225signature = do_format_code(signature, line_length)
@@ -725,3 +731,45 @@ def get_context(self) -> AutorefsHookInterface.Context:
725731filepath=str(filepath),
726732lineno=lineno,
727733 )
734+735+736+T = TypeVar("T")
737+Tree = dict[T, "Tree"]
738+CompactTree = dict[tuple[T, ...], "CompactTree"]
739+_rtree = lambda: defaultdict(_rtree) # type: ignore[has-type,var-annotated] # noqa: E731
740+741+742+def _tree(data: Iterable[tuple[T, ...]]) -> Tree:
743+new_tree = _rtree()
744+for nav in data:
745+*path, leaf = nav
746+node = new_tree
747+for key in path:
748+node = node[key]
749+node[leaf] = _rtree()
750+return new_tree
751+752+753+def _compact_tree(tree: Tree) -> CompactTree:
754+new_tree = _rtree()
755+for key, value in tree.items():
756+child = _compact_tree(value)
757+if len(child) == 1:
758+child_key, child_value = next(iter(child.items()))
759+new_key = (key, *child_key)
760+new_tree[new_key] = child_value
761+else:
762+new_tree[(key,)] = child
763+return new_tree
764+765+766+def do_backlink_tree(backlinks: list[Backlink]) -> CompactTree[BacklinkCrumb]:
767+"""Build a tree of backlinks.
768+769+ Parameters:
770+ backlinks: The list of backlinks.
771+772+ Returns:
773+ A tree of backlinks.
774+ """
775+return _compact_tree(_tree(backlink.crumbs for backlink in backlinks))