Skip to content

change: scoped cross-references should give precedence to exact matches #343

Description

@pawamoy

Opening to keep as a memo for when working on a better solution in Zensical.

Scoped cross-reference resolution

Problem

With scoped_crossrefs=True, mkdocstrings-python currently expands every reference before mkdocs-autorefs resolves it.

For example, consider markdown.something in markdown.core, where markdown is also a local function. The handler changes this identifier to markdown.core.markdown.something.

This change removes the exact identifier before autorefs checks its local anchor maps or external inventories. A documented or imported markdown.something therefore cannot win over the scoped interpretation.

Resolution timing

AutorefsHook.expand_identifier() runs during docstring Markdown conversion. It runs when AutorefsInlineProcessor creates the temporary <autoref> element.

At that time, autorefs does not yet have its complete identifier-to-URL maps:

  • MkDocs can render and register targets on later pages.
  • Mkdocstrings registers external inventory URLs during on_env.
  • A Griffe object can exist without a rendered heading or registered URL.

Checking autorefs during expand_identifier() would therefore make resolution depend on page order. Checking Griffe would only prove that a Python object exists, not that autorefs can link to it.

Autorefs resolves <autoref> elements during on_env, after MkDocs has rendered all pages and mkdocstrings has registered external inventories. Both candidates must survive until this step.

Applicable reference forms

Relative and scoped cross-references are only manually written. They use an explicit target such as [text][identifier] or a code-style target such as [`identifier`][].

Autorefs treats both forms as exact references and does not create a normal Markdown slug for them. Automatic cross-references are already absolute and do not need scoped resolution.

The required order is therefore:

  1. For an ordinary manual reference, try the identifier exactly as written.
  2. If scoped_crossrefs=True, try the identifier expanded in the current Python scope.
  3. For an explicit relative reference, use only its context-directed expansion.

When scoped_crossrefs=False, do not emit or try a scoped fallback.

Implementation in mkdocstrings-python

Use the existing identifier and slug attributes of <autoref>. Autorefs already tries identifier first and slug second.

For an ordinary reference, AutorefsHook.expand_identifier() must preserve the exact identifier. When scoped cross-references are enabled, it must also compute and temporarily store the scoped identifier.

AutorefsHook.get_context() must return a context whose as_dict() adds the scoped identifier as slug. Here, slug transports a fallback identifier; it is not a Markdown slug.

For example, the hook must produce the equivalent of:

<autoref identifier="markdown.something" slug="markdown.core.markdown.something">...</autoref>

Autorefs will then try markdown.something before markdown.core.markdown.something. This happens after all local and external URLs are available.

The hook must clear its stored fallback before it processes each reference. It must not emit slug when the scoped identifier equals the exact identifier.

If an enabled relative reference starts with a dot, expand it with the existing relative-reference logic. Return the expanded identifier directly and do not emit a scoped fallback.

This design requires no changes or new capabilities in mkdocs-autorefs. It also requires no coordinated release or new minimum autorefs version.

Tests

Add integration coverage for these cases:

  1. With scoped_crossrefs=True, both the exact and scoped targets exist. The exact target wins.
  2. With scoped_crossrefs=True, only the scoped target exists. The scoped fallback resolves.
  3. With scoped_crossrefs=False, the handler does not emit or try a scoped fallback.
  4. An exact target from an external inventory wins over a local scoped interpretation.
  5. Explicit relative cross-references keep their context-directed behavior and do not emit a scoped fallback.
  6. Automatic cross-references remain absolute and unchanged.
  7. The hook clears its stored fallback between consecutive references.

Backlink limitation

Autorefs records backlinks against identifier before it tries slug. If the scoped fallback wins, autorefs does not record the backlink against the scoped target.

This behavior already applies to normal slug fallbacks. Link resolution needs no autorefs change, but backlink parity would require separate work in autorefs.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

cross-refsRelated to cross-referencesrefactorChange suggestion, not a bug nor a feature.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions