Skip to content

Resolve doc links once in the semantic layer; fix Self::/qualified links across all backends - #116

Merged
philpax merged 10 commits into
mainfrom
fix/doc-link-resolution
Jul 24, 2026
Merged

Resolve doc links once in the semantic layer; fix Self::/qualified links across all backends#116
philpax merged 10 commits into
mainfrom
fix/doc-link-resolution

Conversation

@philpax

@philpax philpax commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Fixes #114 and #115, then restructures doc-link handling so those classes of bug can't recur: links are resolved exactly once during semantic analysis, and every consumer (validation, Rust/JSON/C++ backends, LSP) works from the resolved targets instead of re-deriving structure from strings.

Language fixes

  • Self::member doc links resolve against the enclosing type (Self:: in doc-links fails to resolve #114), including bare [Self], nested items (Self = the nested item), and nested constants/extern values (Self = the parent type, matching where their emitted docs land in rustdoc). [Self] on module-level constants/type aliases is now correctly rejected.
  • Module-qualified links to freestanding functions and extern values resolve (Module-qualified doc-link to a free function fails to resolve #115).
  • Nested-item docs are now actually validated (previously skipped silently), recursively at any depth — including nested types' field docs and nested enums' associated functions.

Structural changes

  • DocLinkPath { self_prefixed, segments } is parsed once at the markdown boundary; the resolution path never splits strings again.
  • doc_links::resolve_all resolves every link crate-wide in one pass; validation is its failure path. Results are stored per module as ModuleDocLinks, keyed by DocBlockKey (a dedicated variant for the module's own doc block — Module::location() is a proxy borrowed from the first item, so location-keying would collide).
  • Rust backend renders each link's resolved target as an absolute crate:: path. The leaf-name rewrite maps (which collided when two types shared a nested-item name and rewrote matching segments in unrelated links), exact-text cross-module matches, doc-driven use imports, and use Flat as Leaf aliases are all gone. Module-function and constant docs — previously never rewritten — get uniform treatment, and code shortcuts convert to inline links so the written label survives instead of being mangled to the flattened name. The generated root allows rustdoc::redundant_explicit_links (deciding when rustdoc could resolve the label alone would mean re-implementing rustdoc's resolver).
  • JSON backend reads the stored tables, fixing Self:: links being silently dropped from doc_links (the viewer rendered them dead).
  • LSP determines the enclosing item for a doc line from the file's AST, so Self:: links get hover/go-to-definition/document-link and participate in rename.
  • C++ backend rewrites links into doxygen markdown ([label](@ref ns::Target)), rendered under C++ emission rules: namespaces, genuine nesting, get_<name>() accessors, module-scope Parent_NAME flattening for value items under enum/bitflags parents. Unlinkable targets (primitives, out-of-tree externs) flatten to their bare label instead of emitting dead hrefs.

Verification

  • python test.py gains a doxygen pass over the emitted C++ corpus that fails on any warning (unresolvable @refs in particular) — the C++ analogue of the cargo doc gate. Skipped with a warning when doxygen is absent; shell.nix provides it and CI installs it. Doxygen output is generated to a temp dir and discarded — the tracked C++ corpus shows the rewritten comments.
  • New semantic tests for DocLinkPath parsing, Self enclosing semantics (nested constants, module-level rejection), nested-item validation; new LSP integration test for Self:: navigation; corpus inputs exercising Self::, module-qualified links, and module-doc links.
  • Full suite green at every commit.

philpax added 10 commits July 24, 2026 08:52
Resolve doc-link paths as parsed segments rather than raw strings:
`Self::` substitutes the enclosing type's path (threaded through a new
walk_module_docs traversal shared by validate and collect), and
module-qualified paths to freestanding functions and extern values
resolve instead of being gated out by a bare-name check. The shared
walk also newly validates nested-item docs and visits nested enums'
associated functions.

Closes #114
Closes #115
Replace ad-hoc string handling in the doc-link resolution path with a
structured pipeline:

- `DocLinkPath` parses link text exactly once (leading `Self` prefix +
  segments); `resolve()` and every consumer take the structured form.
- `resolve_all` resolves every doc link in one crate-wide pass during
  semantic analysis, storing `ResolvedDocLink { text, target }` tables
  keyed by doc-block location, per module. Validation is this pass's
  failure path. The tables ride `SemanticAnalysis`/`SemanticOutput` so
  backends can consume targets instead of re-resolving.
- The doc walk is now genuinely recursive: nested types' fields,
  associated functions, vftables, and further-nested items are visited
  (and validated) at any depth, with the enclosing type's augmented
  scope.
- `Self` enclosing context now follows emitted-rustdoc reality: the
  type itself for type/enum/bitflags docs and their members, the parent
  type for nested constants/extern values (their docs land in the
  parent's impl block), and nothing for type aliases or module-level
  values — `[Self]` on a module-level constant is now rejected instead
  of validating and emitting a link rustdoc can't resolve.

Behavior-preserving for the emitted corpus (byte-identical output).
The JSON backend re-resolved doc links itself with no enclosing-type
context, silently dropping every `Self::` link from `doc_links` (the
viewer rendered them as dead links). `DocCx` now looks links up in the
location-keyed tables produced during semantic analysis, so the JSON
output cannot diverge from what the compiler validated.

Link tables are keyed by a `DocBlockKey` rather than a raw location:
a module's `location()` is a proxy borrowed from its first item, so
keying the module's own doc block by it collided with that item's doc
block (the module page showed the item's links). The module doc block
gets a dedicated key variant instead, exercised by new module-level
doc links in the doc_self_links corpus input.
Replace the string-map link rewriting (leaf-name keyed nested_rewrites,
exact-text cross-module matches, doc-driven `use` imports and
`use Flat as Leaf` aliases) with target rendering: each link's resolved
target — from the semantic link tables — is rendered as an absolute
Rust path, flattening nested items and substituting extern-value
accessors.

This removes two latent failure modes of the string approach: leaf-name
collisions (two types with same-named nested items shared one rewrite
key, and any matching segment in an unrelated link was rewritten) and
cross-module links written in non-canonical form matching no rewrite
key and emitting broken rustdoc. It also rewrites docs that previously
got no treatment at all (module functions, constants), and code
shortcuts now become inline links so their written label survives
instead of being mangled to the flattened name.

The generated root allows rustdoc::redundant_explicit_links: rewriting
is uniform, and knowing when rustdoc could have resolved the label
alone would mean re-implementing rustdoc's resolution.
The LSP passed no enclosing type at its three doc-link resolution
sites, so `Self::` links — valid to the compiler since #114 — were
inert in the editor: no document-link, no hover/go-to-definition, and
doc_link_occurrences missed them, leaving `Self::member` links stale
after a member rename.

`enclosing_type_for_doc_line` walks the file's parsed AST to find the
item whose definition (or impl block) contains a doc line, mirroring
the compiler's walk semantics: nested constants/extern values use the
parent type, type aliases and module-level docs get none.
Rewrite each resolved intra-doc link in emitted C++ doc comments into
doxygen markdown (`[label](@ref ns::Target)`), rendered from the link's
semantic target under the C++ emission rules: namespaces for modules,
genuine nesting for nested types, `get_<name>()` accessors for extern
values, and module-scope flattened names (`Parent_NAME`) for value
items under enum/bitflags parents. Targets with no documented C++
entity (primitives, out-of-tree externs) flatten to their bare label
instead of emitting a dead link.

test.py gains a doxygen pass over the emitted corpus that fails on any
warning — unresolvable `@ref`s in particular — mirroring the cargo doc
gate for Rust. It skips with a warning when doxygen is missing; a new
shell.nix provides it and CI installs it. Doxygen output is generated
into a temp dir and discarded; the tracked C++ corpus shows the
rewritten comments.
Remove DocLinkTarget::import_path/extern_value_path and the
ModuleDocLinks imports/extern-value accessors — all consumers now
render targets directly. Update docs/rust_backend.md and
docs/language.md for the resolve-once pipeline, Self:: and
module-qualified link forms, and per-backend target rendering.
Resolution scopes are not just module paths: docs inside a type resolve
under an augmented scope with the type's own path prepended (for bare
nested-item references), plus use-imported item paths. find_item and
find_in_modules assumed "first scope entry = current module", so for
type-scoped doc blocks same-module preference anchored at the *type*
path and the crate-wide bare-name search fell through to alphabetical
order — picking a same-named type from another module and failing the
member lookup. Caught by pyxis-defs in CI (`RenderContext::…` resolving
to graphics_engine's RenderContext instead of ui::scaleform's).

The resolver now locates the current module explicitly as the first
scope entry that is a module.
Some doxygen builds (Ubuntu's, on CI runners with graphviz present)
auto-enable dot and then fail on their own generated map files. The
check only cares about ref resolution, not diagrams.
Pin HAVE_DOT = YES — distro doxygen packages disagree on the default
(Ubuntu's assumes dot exists, nix's assumes it doesn't), which is how
CI failed while local runs passed — and provide graphviz alongside
doxygen in both shell.nix and CI.
@philpax
philpax merged commit ab5ecc9 into main Jul 24, 2026
4 checks passed
@philpax
philpax deleted the fix/doc-link-resolution branch July 24, 2026 19:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Self:: in doc-links fails to resolve

1 participant