Skip to content

Implement <inheritdoc> XML documentation support for F# - #19188

Open
T-Gro with Copilot wants to merge 77 commits into
mainfrom
copilot/support-xmldoc-inherit-element
Open

Implement <inheritdoc> XML documentation support for F##19188
T-Gro with Copilot wants to merge 77 commits into
mainfrom
copilot/support-xmldoc-inherit-element

Conversation

Copilot AI commented Jan 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Resolves <inheritdoc> in F# XML documentation at tooling time — the same model C#/Roslyn uses. Inherited documentation is surfaced in IDE tooltips, completion and signature help (SymbolHelpers.fs) and through the FSharpSymbol.XmlDoc API consumed by FCS/IDE clients (Symbols.fs).

The generated .xml documentation file is intentionally left unchanged: <inheritdoc> is written verbatim, exactly as the C# compiler does. Downstream doc tools (and the IDE) are responsible for expansion. There is deliberately no compile-time rewriting of the .xml file.

Supported today

  • Implicit <inheritdoc/> inherits from:
    • a type's base class (skipping System.Object) or, failing that, its first implemented interface;
    • an overridden base method/property — signature-matched so the correct overload is chosen, and gated to genuine overrides;
    • a constructor's base-type constructor, matched by parameter signature (overloads disambiguated).
  • Explicit <inheritdoc cref="..."/> on the FSharpSymbol.XmlDoc path (name-based CCU walk, incl. same-file and cross-assembly types).
  • path="..." XPath filtering for element selection (e.g. path="/summary").
  • Recursion across multi-level inheritance chains, with cycle detection.
  • Generics: generic base classes, generic interfaces and overrides of generic base methods inherit correctly (base type parameters are instantiated before signature matching).

Deliberate limitations (documented, tested where practical)

  • Explicit cref is resolved only through FSharpSymbol.XmlDoc, not at the tooltip/completion InfoReader layer (no SymbolEnv/CCU walk there). Implicit <inheritdoc/> is fully supported at both layers.
  • <typeparamref> inside inherited text is passed through verbatim; it is not rewritten to a <see cref="..."> for the instantiated type (a Roslyn refinement).
  • Constructor inheritance is expanded on the tooltip path; on the FSharpSymbol.XmlDoc path it returns unexpanded, because that resolver is name-based and would collapse constructor overloads.
  • Cross-assembly BCL implicit inheritance (e.g. overriding a System.* member) is not expanded at the tooltip layer.
  • Interface-method implementations that are not genuine overrides are not expanded implicitly at the tooltip layer (they are on the FSharpSymbol.XmlDoc path via implemented slot signatures).
  • Event inheritance is resolved on the FSharpSymbol.XmlDoc path only, not at the tooltip layer.
  • Explicit cref to an overloaded member on the FSharpSymbol.XmlDoc path resolves only when exactly one same-named member is documented; an ambiguous (2+) documented overload set yields no inherited content rather than an arbitrary pick (a doc-comment-ID cref carrying a parameter signature is required to disambiguate, which the name-based resolver does not honor).
  • Multi-level implicit <inheritdoc/> chains (e.g. GrandBaseDerived, each with a bare <inheritdoc/>) resolve a single level: the implicit target is not recomputed per intermediate symbol.
  • Text-node-selecting XPaths (node(), text()) are not supported and degrade to no inherited content rather than throwing.

Intentional deviation from Roslyn: for a class whose only supertype is System.Object, F# falls through to the first implemented interface (or nothing) instead of inheriting System.Object's summary, to avoid tooltip noise.

Tests

  • tests/FSharp.Compiler.Service.Tests/XmlDocInheritanceTests.fs — primary suite: a pure-engine harness plus tooltip/completion/FSharpSymbol.XmlDoc coverage for implicit/explicit/base-member/constructor/generic/interface cases, recursion, cycle detection, well-formedness (no XML-escaping of inherited markup), and graceful XPath degradation. One test is skipped documenting the explicit-cref-at-tooltip-layer limitation.
  • tests/FSharp.Compiler.Service.Tests/XmlDocTests.fs and tests/FSharp.Compiler.ComponentTests/Miscellaneous/XmlDoc.fs — surrounding coverage, including that the .xml writer emits <inheritdoc> verbatim.

Files changed

Core: XmlDocInheritance.fs/.fsi (shared expansion engine, 168 LOC), XmlDocSigParser.fs/.fsi (doc-comment ID parser, 89 LOC), Symbols.fs (Path A resolver), SymbolHelpers.fs (Path B tooltip/completion), plus a documentation-only note in XmlDocFileWriter.fs/.fsi.

Copilot AI and others added 2 commits January 2, 2026 11:48
Co-authored-by: T-Gro <46543583+T-Gro@users.noreply.github.com>
Co-authored-by: T-Gro <46543583+T-Gro@users.noreply.github.com>
Copilot AI changed the title [WIP] Add support for xmldoc <inheritdoc> element Add initial infrastructure for <inheritdoc> XML documentation support Jan 2, 2026
Copilot AI requested a review from T-Gro January 2, 2026 11:56
@github-actions

github-actions Bot commented Jan 6, 2026

Copy link
Copy Markdown
Contributor

❗ Release notes required

You can open this PR in browser to add release notes: open in github.dev


✅ Found changes and release notes in following paths:

Change path Release notes path Description
src/Compiler docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
vsintegration/src docs/release-notes/.VisualStudio/18.vNext.md

Co-authored-by: T-Gro <46543583+T-Gro@users.noreply.github.com>
Co-authored-by: T-Gro <46543583+T-Gro@users.noreply.github.com>
…parameter

Co-authored-by: T-Gro <46543583+T-Gro@users.noreply.github.com>
…earn and Roslyn research

Co-authored-by: T-Gro <46543583+T-Gro@users.noreply.github.com>
Copilot AI requested a review from T-Gro January 7, 2026 13:44
T-Gro and others added 24 commits July 24, 2026 10:39
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Removes the duplicated node-serialization tail shared by applyXPathFilter
and selectDefaultInheritedContent (code-quality review, Task A compactness).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…th B)

Wire <inheritdoc> expansion into GetXmlCommentForItemAux (SymbolHelpers.fs),
the tooltip/completion/signature-help funnel. Adds tryGetImplicitInheritTarget
which computes the base type/member's cref token and raw XML doc text directly
from the in-memory typed tree (InfoReader has no SymbolEnv/CCU walk). Supports
implicit <inheritdoc/> for types (base class or first interface) and overriding
methods/properties; other Item kinds degrade silently. Explicit crefs remain a
documented limitation at this layer and continue to work via FSharpSymbol.XmlDoc.

Perf early-out: single ordinal scan for "<inheritdoc" before any work.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Scan XmlDoc.UnprocessedLines for the "<inheritdoc" substring before paying
for GetXmlText() elaboration+concat on the hot tooltip/completion/sig-help
path (Path B) and in Symbols.fs tryGetInheritDocXmlText (Path A). Also add
StringComparison.Ordinal to the Path A check and a clarifying comment on the
overload-matching heuristic in tryBaseMethodTarget.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…slyn parity)

Mirror Roslyn GetCandidateSymbol: structs, enums and delegates have no
inheritance candidate, so a bare inheritdoc must not surface their CLR
supertype docs (System.ValueType / System.Enum / System.MulticastDelegate).
Guard both the FSharpSymbol.XmlDoc path (Symbols.getImplicitTargetCrefForEntity)
and the tooltip/completion path (SymbolHelpers.tryBaseTypeTarget).

Also document the intentional deviation from Roslyn for a class whose only
supertype is System.Object (F# falls through to the first interface / nothing
rather than surfacing object's summary as tooltip noise), and add an engine
regression test proving an implicit target does not leak across cref chains.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…gnature

Adversarial review found two symmetric divergences from Roslyn GetCandidateSymbol
in the implicit inheritdoc target computation:

1. Path A (Symbols.getImplicitTargetCrefForMember): the empty-slotSig fallback
   fired for ANY member sharing a name with a base member, so a plain new
   (non-override) member with <inheritdoc/> wrongly inherited the base member's
   docs (and a struct member could reach System.ValueType). Gate the fallback on
   the member genuinely being an override (IsOverrideOrExplicitImpl /
   IsDefiniteFSharpOverride); non-overrides now yield no candidate.

2. Path B (SymbolHelpers.tryBaseMethodTarget/tryBasePropertyTarget): matched the
   base member by name only, first documented wins. Require the hovered member to
   be a definite override and match the base member by signature
   (MethInfosEquivByNameAndSig / PropInfosEquivByNameAndSig) so the correct
   overload's docs are inherited and non-overrides do not inherit at all.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…mit test, honest notes

- Make XmlDocSigParser + DocCommentIdKind/ParsedDocCommentId internal (all
  consumers - FSharp.Editor, ComponentTests, Service.Tests - have IVT); shrinks
  the FCS public surface-area baseline by 96 lines (regenerated .bsl).
- Remove the now-unused FS3390 xmlDocInheritDocError message from FSComp.txt and
  its 13 xlf trans-units (the IDE expansion engine degrades silently, matching
  C#/Roslyn - no user-facing diagnostics from display paths).
- ComponentTests/Miscellaneous/XmlDoc.fs: replace the duplicated engine tests
  (now owned by the Service.Tests XmlDocInheritanceTests harness) and two vacuous
  Assert.NotNull tests with a single verbatim-emission regression test that
  compiles with --doc and asserts the generated .xml contains the literal
  inheritdoc tag (proving the compiler does NOT expand at compile time).
- XmlDocFileWriter: clarifying comment only; confirms verbatim emit.
- Correct release notes to state IDE-time/tooling expansion, compile-time
  verbatim emission (matching C#), and that <include> is not implemented.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A constructor's <inheritdoc/> now inherits the base-type constructor's
documentation in tooltips, completion and signature help (Path B). The base
constructor is matched by parameter signature (MethInfosEquivByNameAndPartialSig)
rather than full signature, since a constructor's logical return type is its own
declaring type and would never match the base. Structs/enums/delegates have no
inheritance candidate. Mirrors Roslyn GetCandidateSymbol's constructor rule.

FSharpSymbol.XmlDoc (Path A) intentionally leaves constructors unresolved: its
name-based cref resolver collapses overloads and could surface a wrong overload's
docs, so it returns None (silent) rather than potentially-wrong text.

Adds RED-first tests for the basic case and for base-constructor overload
disambiguation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…struct)

Empirically pins three behaviours of the Path B constructor expansion:
- inheriting from a generic base constructor Base<'T> instantiated as Base<int>
  (the base ctor's parameter is seen as int and matches by signature);
- a derived constructor whose signature matches no base constructor drops the
  tag silently instead of fabricating a wrong overload's docs;
- a struct constructor with the inherit tag never leaks System.ValueType docs
  (structs have no inheritance candidate).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds a derived type with two <inheritdoc/> constructors and asserts each call
site (int vs string) inherits the matching base constructor's docs. Confirms the
tooltip layer expands against the resolved constructor for the call, not merely
the first constructor in the group.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Records that constructor <inheritdoc/> is expanded in IDE tooltips, completion
and signature help (matched by parameter signature), and that the FCS Symbols
API does not expand constructor inheritance.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…thod

Locks in <inheritdoc/> resolution across generic type hierarchies:
- a generic derived type inheriting a generic base type's docs;
- a type inheriting through a generic implemented interface;
- an override of a method declared on a generic base (signature matched after
  the base type parameter is instantiated).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The expansion engine returns already-elaborated XML whose first line is the
wrapper's leading whitespace. Both resolver hooks stored that output as a single
XmlDoc line, so the XmlDoc elaboration step (processLines) did not see a leading
'<', treated the whole thing as plain text, wrapped it in an implicit <summary>
and XML-escaped the inherited markup. The tooltip / FSharpSymbol.XmlDoc therefore
carried "<summary>&lt;summary&gt;...&lt;/summary&gt;</summary>", which an IDE
renders as literal angle brackets rather than formatted documentation.

Split the engine output back into lines at both call sites so elaboration
recognises it as already-XML and passes it through verbatim. Existing content
assertions did not catch this because the substrings survive escaping; added
regression tests on both the tooltip (Path B) and FSharpSymbol.XmlDoc (Path A)
paths asserting the inherited markup is spliced as real XML with no escaped
angle brackets.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A user-authored <inheritdoc path="..."/> whose XPath selects non-element nodes
(e.g. /summary/node() or //text()) made XPathSelectElements raise
InvalidOperationException. That is neither XPathException nor XmlException, so it
escaped both applyXPathFilter's handler and the engine's outer catch and could
crash the tooltip/completion caller. Broaden the catch so such unsupported
selections degrade to no inherited content instead of throwing. Element-selecting
XPaths are unaffected.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Two cases could surface the wrong documentation:

1. Engine recursion for an explicit-cref directive passed the CALLER's implicit
   target into the referenced doc's expansion. If that referenced doc contained a
   bare <inheritdoc/>, it resolved against the caller's base instead of its own,
   injecting unrelated documentation. Recurse with no implicit target so a nested
   bare <inheritdoc/> is dropped rather than resolved against the wrong target;
   explicit-cref chains still propagate. The now-constant recursion parameter is
   removed.

2. The FSharpSymbol.XmlDoc member-cref resolver matched by name only and returned
   the first documented overload. A member cref without a parameter signature is
   ambiguous when the name is overloaded, so return None when two or more
   documented overloads share the name instead of guessing.

Both guarded by regression tests (pure-engine leak test; ambiguous-overload
symbol test).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The `m: range` parameter was threaded through expandInheritDocFromXmlText
and expandInheritedDoc but never used (diagnostics were already removed for
silent degradation). Removed it from both engine functions, the internal
recursive call, the .fsi signature, and all three call sites (Symbols.fs
Path A, SymbolHelpers.fs Path B, and the test harness). Also dropped the now
unused `open FSharp.Compiler.Text` in the engine and its signature file.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…(Path A)

getImplicitTargetCrefForMember built a name-only member cref for an override's
inherited target. When the base declared several overloads of that name but only
one was documented, the name-based resolver returned that documented sibling even
when the user overrode a different overload -- surfacing the wrong method's docs
through the public FSharpSymbol.XmlDoc API, contradicting the "signature-matched"
guarantee that only Path B (tooltip) actually honors.

Gate both cref-building branches to the non-overloaded case: only emit a name-only
cref when the target type declares a single member of that name. Method abstract+
default pairs share a signature and are collapsed via MethInfosEquivByNameAndSig,
and a property's get/set collapse to one PropInfo, so plain virtual overrides and
read/write properties still inherit; only genuine overload sets (2+) are blocked.

Adds Path A regression tests: sibling-overload leak is blocked, and single method
/ get-set property overrides still inherit (guard not over-blocking).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The engine's outer handler only caught System.Xml.XmlException, but the caller-
supplied resolveCref runs inside that try and, on Path A (buildCrefResolver),
walks CCUs and can invalidOp on an unresolved assembly. Such an exception would
escape into the public FSharpSymbol.XmlDoc property (and any tooltip), which has
no guard of its own.

Broaden the fallback to degrade on any failure to the original text (which still
carries the verbatim <inheritdoc>, harmless downstream), matching the best-effort
degradation already used across the Path A helpers.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Path B (tooltip/completion/signature-help) previously scanned only the DIRECT
base type for an overridden member, so a `C : B : A` override where `A` declares
the abstract member and `B` does not redeclare it produced no inherited docs and
left the raw tag in the tooltip.

Derive the candidate declaring types from the override's implemented slot
signatures (which point at the true grandparent declaration and are already
generic-instantiated), falling back to the direct base for overrides that record
no F# slot signature. Applied symmetrically to methods and properties.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- SymbolMemberType.FromString (GoToDefinition.fs): no call sites; the doc-id
  kind is mapped directly from DocCommentIdKind, not from a raw string.
- DocCommentIdKind.Type/Field/Namespace: never constructed. Type, field and
  namespace doc-comment IDs each have their own ParsedDocCommentId case, so the
  member-kind enum only needs Method/Property/Event (plus the Unknown sentinel).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The overload gate added for methods was a no-op for properties: the slot branch
passes slot.Name, which for a property is the accessor name (get_Item), while the
intrinsic-property lookup filters by property name (Item) and so found nothing and
treated every property as unique. Overloaded indexers therefore still leaked a
sibling overload's docs onto FSharpSymbol.XmlDoc.

Count overloads by p.PropertyName instead, so an override of one indexer overload
no longer surfaces another overload's documentation. As with overloaded methods,
Path A abstains for the whole overload set and the signature-matched docs are
delivered by the tooltip layer.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
… chains

The visited-set only stops cycles. A deep ACYCLIC explicit-cref chain
(A -> B -> C -> ...) recurses non-tail and eventually raises an uncatchable
StackOverflowException that aborts the process/IDE (the best-effort outer
catch cannot recover from it). Add a generous depth cap (100) in
expandInheritedDoc, keyed on visited.Count (== chain depth since both
callers start from Set.empty). Past the cap the tag is left unexpanded
(graceful degradation); real chains are only a few levels deep so the cap
never truncates a legitimate chain.

Tests: a 300-deep chain stops before the leaf; a 50000-deep chain (past the
overflow threshold) completes instead of crashing the host.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
When a bare inheritdoc directive is nested inside another documentation
element (e.g. inside <summary>), Roslyn narrows the default selection to
that element's matching children (ancestor-aware XPath + text-node
selection). F#'s selection helper returns whole top-level elements, so the
target's summary AND remarks are spliced verbatim. The common top-level
usage is unaffected. Add a regression test pinning the current behavior and
a code comment on selectDefaultInheritedContent so a future change is
deliberate.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@T-Gro
T-Gro marked this pull request as ready for review July 25, 2026 18:58
T-Gro and others added 5 commits July 26, 2026 09:54
Rubber-duck review: "never truncates a legitimate chain" was too absolute
(a 100+ explicit-cref chain would be truncated). Reword to "sits well above
any expected real-world depth" to accurately describe the cap.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The name-only implicit-target guard in targetHasUniqueMember mis-counted an
abstract slot and its default implementation as two overloads: they surface as
MethInfos with mismatched curried-vs-flattened arities ([1;1] vs [2]), which the
arity-strict MethInfosEquivByNameAndSig failed to collapse. A two-parameter
virtual override with a bare inheritdoc therefore resolved no implicit target and
produced empty documentation (four Linux CI failures).

Deduplicate the target's methods by XML doc signature instead, while also
requiring return-type equivalence so return-type-only overloads (op_Implicit /
op_Explicit, whose IL doc signature omits the return type) stay distinct.

Also rewrite three InheritDocTooltipTests that encoded an early-design "retain the
inheritdoc marker" expectation to the finalized, Roslyn-consistent remove-on-failure
behavior (cycle / unresolvable cref / invalid XPath all drop the element while
preserving surrounding docs), and add a curated multi-argument override regression
test at the tooltip layer.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
On Windows (both .NET Framework and coreclr) XNode.ToString re-introduces
Environment.NewLine (CRLF) regardless of the LF used to join nodes. Downstream,
XmlDoc.processLines trims only spaces, so a spliced-content line holding a stray
'\r' is recognised as neither blank nor XML; the whole doc is then re-wrapped in
an implicit <summary> and XML-escaped. This made two regression guards
(XmlDocInheritanceTests "tooltip/symbol inherited markup is spliced as XML")
fail on every Windows CI leg while Linux passed.

Normalise the engine's final serialized output to LF so spliced markup
round-trips as real XML on every platform. No-op on mac/Linux.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

3 participants