Skip to content

Do not splice the type name into constructor and field references - #1468

Open
MavenRain wants to merge 2 commits into
ocaml:masterfrom
MavenRain:372-reference-type-splice
Open

Do not splice the type name into constructor and field references#1468
MavenRain wants to merge 2 commits into
ocaml:masterfrom
MavenRain:372-reference-type-splice

Conversation

@MavenRain

Copy link
Copy Markdown

Summary

Fixes #372. A reference to a constructor or to a record field is no longer
rendered with the name of its type spliced in: {!Bla.Alpha} now renders as
Bla.Alpha rather than Bla.ha.Alpha.

Problem

Odoc_document.Comment.Reference.render_resolved builds the text a reference
is displayed as by walking the resolved reference and joining every component
with a dot. For a constructor or a record field, one of those components is
the type, and the type is not part of the OCaml path of its own constructors:
Bla.ha.Alpha is not syntax you can write, whereas Bla.Alpha is. As #372
puts it, odoc should not invent OCaml syntax that does not exist.

The type name is spliced in even when the author never wrote it, so
{!Bla.Alpha} and {!Bla.ha.Alpha} both came out as Bla.ha.Alpha.

Fix

The four arms that name a member of a type (`Constructor,
`PolyConstructor, `Field, `UnboxedField) now render the path
of the parent of the type rather than the path of the type, via two small
helpers in the same recursive group. Everything else is untouched.

The enclosing module path is kept, so only the invented component goes away:

reference before after
{!Bla.Alpha} Bla.ha.Alpha Bla.Alpha
{!Bla.ha.Alpha} Bla.ha.Alpha Bla.Alpha
{!t.Alpha} (same module) t.Alpha Alpha
{!Alpha} (same module) Alpha Alpha
{!Bla.Inner.i.Gamma} Bla.Inner.i.Gamma Bla.Inner.Gamma
{!Bla.ra.fla} Bla.ra.fla Bla.fla
{!switch.`On} (same module) switch.`On `On
{!Bla.Ext_a}, {!Bla.Exn_a}, {!Bla.ha} unchanged unchanged

Anchors are not affected. The link target is built from
Reference.Resolved.identifier and Url.from_identifier, a traversal
independent of the renderer, so a link whose text is now Bla.Alpha still
points at #type-ha.Alpha. Text and anchor already diverged this way for
environment-resolved references before this PR (github_issue_447.t rendered
Foo while linking #type-u.Foo); this generalises that.

Fields of the inline record of an extension constructor or of an exception are
parented by the enclosing signature rather than by a type, and keep that
parent.

Two things I would like your call on

  1. Polymorphic variant tags reached through a module. {!Bla.sw.`On}
    now renders Bla.`On. Dropping the type is clearly right, but a
    structural tag is never module-qualified in OCaml either, so Bla.`On is
    not valid syntax the way Bla.Alpha is. I kept the module prefix as a
    locator because it is the smaller, uniform change, and the common
    same-module case does become exactly `On. Rendering the bare tag
    instead is a one-line change if you prefer it.

  2. Tooltips. When the author supplies their own text, the rendered
    reference becomes the title= tooltip, so tooltips shorten too. Making
    the tooltip keep the full path while the text is shortened would recover
    the lost information, but it touches to_ir and would add a title= to
    many more links, so I left it out of this PR.

What is lost

{!Bla.Alpha} and {!Bla.ha.Alpha} now render identically, and two types of
one module sharing a constructor name render identically (only the anchor
distinguishes them). That is inherent to the fix rather than incidental, so
the new test pins it explicitly with an Ambiguous module. Nothing is lost
that was not already dropped for {!Alpha}, and the anchor still carries the
type.

Testing

  • New cram test test/xref2/github_issue_372.t, covering: the issue's case;
    the same reference with the type spelled out; same-module constructor and
    field references, both bare and type-qualified; a record field; polymorphic
    tags with a module parent; a nested module path; extension constructor,
    exception and type references (which must not change); two types sharing a
    constructor name; a reference with replacement text (the tooltip path); and
    an unresolved class-parented reference (which must stay plain text).
  • Two existing expectations move, both in the intended direction:
    github_issue_447.t (u.Foo to Foo, M.t.Foo to M.Foo) and
    reference_to_polymorphic.t (switch.`On to `On).
  • dune runtest --force on OCaml 5.3.0: 101 passed, 0 failed, including the
    sherlodoc lane. No other golden in the tree changes; in particular
    test/generators, test/search and test/occurrences are byte-identical,
    since the search index builds its names from Identifier.fullname rather
    than from this renderer.
  • Checked by mutation: reverting only the render_resolved hunk turns the new
    test red on the constructor, field and polymorphic lines while leaving the
    environment-resolved, extension, exception and unresolved lines green.
    Reverting only the `Field arm turns exactly the field lines red.
  • ocamlformat 0.27.0 (the pinned version) is clean.

One gap worth naming: the `UnboxedField arm is changed by symmetry but
is not exercised, because nothing in the test corpus declares an OxCaml
unboxed record and the case needs an OxCaml toolchain. It shares
render_datatype_parent with the `Constructor arm, whose behaviour is
pinned, and UnboxedFieldParent.t is the same two-arm sum as DataType.t.
Happy to add an %{ocaml-config:ox}-gated cram case if you would rather have
one.

Drafted with AI assistance; I reviewed, built and tested every change
myself.

@MavenRain
MavenRain force-pushed the 372-reference-type-splice branch from a51efac to 37e9047 Compare July 28, 2026 05:43
The name of a type is not part of the OCaml path of its constructors and of
its fields: `M.t.Foo` is not valid syntax whereas `M.Foo` is.  Rendering a
reference as `M.t.Foo` invents syntax that does not exist, which is what
ocaml#372 reports.

`render_resolved` now renders the path of the parent of the type rather than
the path of the type itself for the four arms that name a member of a type:
`Constructor, `PolyConstructor, `Field and `UnboxedField.  The enclosing
module path is kept, so `{!Bla.Alpha}` renders as `Bla.Alpha`.  Anchors are
unaffected: they are built from `Reference.Resolved.identifier`, an
independent traversal, so a link whose text is now `Bla.Alpha` still points
at `#type-ha.Alpha`.

Fields of the inline record of an extension constructor are parented by a
signature rather than by a type, and keep that parent in their path.

Fixes ocaml#372

Signed-off-by: Onyeka Obi <softwareengineerasaservant@isurvivable.cv>
@MavenRain
MavenRain force-pushed the 372-reference-type-splice branch from 37e9047 to 9745a5e Compare July 28, 2026 05:45

@panglesd panglesd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. Thanks for the PR.

About the two questions: I think both of them should not be done, but if they were done it should anyway be in another PR.

  • About polymorphic variants... yes the reference is invalid OCaml syntax. But "go to declaration" does not make sense in OCaml, yet it does in odoc. So I don't know what is the best, but I would tend to keep it as it is now.
  • Tooltips are good like they are here, in my opinion. They could be used to distinguish which type we are speaking about, in the very rare case where there is ambiguity, but the reader can just click on the link to find out in those cases.

Comment thread src/document/comment.ml Outdated
Comment on lines +88 to +94
| `Identifier
{
iv = `Root _ | `Module _ | `Parameter _ | `Result _ | `ModuleType _;
_;
}
| `Alias _ | `AliasModuleType _ | `Module _ | `Hidden _ | `ModuleType _ ->
render_resolved (r :> t) ^ "."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would improve the readability if we replaced at least the identifier with a type:

    | `Identifier { iv = #Identifier.Signature.t_pv;  _ }

(or something like that)

I don't think we can do the same with the rest of the clauses, but it is fine to keep them explicitly matched.

`render_field_parent` listed the five signature identifier constructors
explicitly.  Match them with `#Identifier.Signature.t_pv` instead, as
Paul-Elliot suggested in review.

`field_parent_pv` is `[ signature_pv | datatype_pv ]`, so this arm and the
`` `Type `` identifier arm above it still partition the identifier case
between them with no overlap and no wildcard, and the match stays exhaustive.
The remaining clauses are kept explicitly matched.

Also fill in the changelog placeholder now that the PR number exists.

Signed-off-by: Onyeka Obi <softwareengineerasaservant@isurvivable.cv>
@MavenRain

Copy link
Copy Markdown
Author

Thanks for the review, and for the call on the two questions.

The identifier pattern. Applied. render_field_parent's last identifier
arm is now

| `Identifier { iv = #Identifier.Signature.t_pv; _ }
| `Alias _ | `AliasModuleType _ | `Module _ | `Hidden _ | `ModuleType _ ->
    render_resolved (r :> t) ^ "."

It reads much better, and it keeps the exhaustiveness check rather than
weakening it: field_parent_pv is [ signature_pv | datatype_pv ], so this
arm and the `Identifier { iv = `Type _; _ } arm above it still partition
the identifier case between them, with no overlap and no wildcard. I confirmed
that by deleting the build artifacts for src/document and recompiling in the
dev profile, where warning 8 and warning 11 are errors.

I left the remaining clauses explicitly matched, as you suggested.
render_datatype_parent is already a two-arm match on
Reference.Resolved.DataType.t, so there is nothing to simplify there.

The two questions. Both left out of this PR, so polymorphic variant tags
keep the module prefix and tooltips keep the current behaviour. I read your
"I don't know what is the best, but I would tend to keep it as it is now" on
the tag question as a preference rather than a settled no, so if you or anyone
else wants to revisit it later, I am happy to do that as a separate PR the way
you suggest.

Changelog. I also filled in the #PLACEHOLDER_PR token I had left in
CHANGES.md, which now reads (@MavenRain, #1468).

Testing. ocamlformat 0.27.0 clean, dune build -p odoc,odoc-parser
clean, dune runtest --force green with no golden file changed, so the rewrite
is behaviour-preserving as expected.

CI. The red check is not this PR. Of the 50 lanes, 47 are green, the
windows-mingw one is still running, and two are red:
debian-13-5.5_riscv64_opam-2.5, which was cancelled, and
freebsd-15.1-4.14_opam-2.5, which died in opam install $DEPS while building
menhir.20260209, before a single odoc source file was compiled:

#=== ERROR while compiling menhir.20260209 ====================================#
# command     ~/.opam/4.14.4/bin/dune build -p menhir -j 39
### output ###
# Error: Error trying to read targets after a rule was run:
# - default/front/b0329ed589ca213bd4022a23702d4cde: chmod(_build/.actions/default/front/b0329ed589ca213bd4022a23702d4cde): Bad file descriptor

That is the nondeterministic FreeBSD builder fault tracked in
ocurrent/ocaml-ci#1068, where the standing advice is to rebuild. The same
chmod: Bad file descriptor fault took the same lane down on master at
5ec3d13, there on odoc_xref2.cmi rather than on menhir, and the two FreeBSD
lanes flip between red and green across odoc commits and PRs with nothing in
common. freebsd-15.1-5.5 and every other 4.14 lane are green on this same
commit. The push above will start a fresh run.

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.

References to variant cases: do not splice the type name in.

2 participants