Skip to content

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

Merged
T-Gro merged 82 commits into
mainfrom
copilot/support-xmldoc-inherit-element
Aug 6, 2026
Merged

Implement <inheritdoc> XML documentation support for F##19188
T-Gro merged 82 commits into
mainfrom
copilot/support-xmldoc-inherit-element

Conversation

CopilotAI commented Jan 2, 2026

Copy link
Copy Markdown
Contributor

Implements the <inheritdoc> part of RFC FS-1337: XML <include> and <inheritdoc> support (tracking issue #19175). <include> is not implemented here and #19175 stays open for it.

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.

CopilotAIand 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>
CopilotAI changed the title [WIP] Add support for xmldoc <inheritdoc> elementAdd initial infrastructure for <inheritdoc> XML documentation supportJan 2, 2026
CopilotAI requested a review from T-GroJanuary 2, 2026 11:56
@github-actions

github-actionsBot 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 pathRelease notes pathDescription
`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>
CopilotAI requested a review from T-GroJanuary 7, 2026 13:44
T-Groand others added 7 commits July 24, 2026 15:37
…(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-Groand others added 7 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>
Restore Checking\Spreads.fs to FSharp.Compiler.Service.fsproj: the line-based
auto-merge silently dropped main's new RecordSpreads source file from the
compile order (adjacent edits), which broke the build with 'Spreads is not
defined' in CheckDeclarations.fs. Placed before CheckExpressionsOps.fs to match
main's ordering.
Verified: FCS builds 0/0; XmlDocInheritanceTests 47/1, XmlDocTests 55/2,
TooltipTests 71/0 all green.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- XmlDocInheritance.fs: remove three redundant `Operators.nonNull` calls on
`XName.op_Implicit` results. main strengthened nullness inference so these are
now flagged FS3262 (WarnAsError) and broke every Windows build leg. The local
build only passed because of a stale bootstrap proto; verified the fix with a
clean `./build.sh -c Debug` (fresh proto) building 0/0.
- Release notes: an earlier merge had kept a stale snapshot of the two release
notes files, deleting ~150 lines of main's accumulated entries. Reset both to
main and re-inserted only this PR's two entries, so the diff is now exactly
the intended two additions.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

@T-GroT-Gro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

CI green, diff is clean (feature + two release-note lines only). Approving.

Adds a .fsi+.fs project-compilation test proving that when a type's doc
comment lives in the signature file, the .fsi doc is authoritative (the .fs
doc is ignored) and its <inheritdoc> is expanded via FSharpSymbol.XmlDoc,
per RFC FS-1341. Passes by construction; guards the signature/impl merge path.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@abonie

Copy link
Copy Markdown
Member

buildCrefResolver step 3 is a hot spot for the common case of inheriting from an external base (T:System.Exception, an overridden System.Object.ToString, etc.): the direct path lookup misses in every F# CCU, so each one falls into the searchNested full entity-tree DFS before step 4's sidecar-.xml lookup succeeds. Since FSharpSymbol.XmlDoc rebuilds the resolver per access, a bulk consumer like fsdocs pays O(all entities in all CCUs) for every <inheritdoc> symbol.

Worth trying the external-XML lookup before the exhaustive searchNested, skipping the full-tree fallback for crefs in namespaces the CCU doesn't own, and/or memoizing the resolver per SymbolEnv. Correctness is fine — this is CPU/latency only, and it doesn't touch the Path B tooltip layer.

T-Groand others added 2 commits August 5, 2026 09:29
…oc test
Replace the hand-rolled temp-dir/file plumbing in the .fsi characterization
test with a shared createProjectOptionsFromNamedSources helper (factored out
of createProjectOptions, whose behavior is unchanged), and reuse the existing
allSymbolsInEntities walker instead of a bespoke recursion. No behavior change.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Source (behavior-preserving):
- Reuse InfoReader's TryFindXmlDocByAssemblyNameAndSig in buildCrefResolver
instead of a local re-implementation of the same lookup.
- Reuse nodesToString for node serialization in expandInheritDocFromXmlText.
- Remove doc/comments that restated the .fsi or narrated the code; keep the
non-obvious why-comments (CRLF normalization, silent XPath degradation,
overload-gate rationale, cross-path constructor invariant).
- Strip trailing whitespace introduced on new lines.
Tests (no coverage loss):
- Delete two Path A explicit-cref duplicates already covered elsewhere and a
permanently-skipped documented-limitation placeholder.
- Collapse 6 mis-parametrized Theory tests (rows differed only by expected
substring, recompiling the same source) into single Fact tests asserting
all substrings.
Net -101 LOC. All XmlDoc suites green (Service 134, engine 46, tooltip 30,
component 15); fantomas clean.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@T-Gro
T-Gro merged commit 93659f3 into mainAug 6, 2026
48 checks passed
@github-project-automationgithub-project-automationBot moved this from In Progress to Done in F# Compiler and ToolingAug 6, 2026
@T-Gro
T-Gro deleted the copilot/support-xmldoc-inherit-element branch August 6, 2026 11:31
bartelink pushed a commit to bartelink/fsharp that referenced this pull request Aug 6, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

4 participants

@T-Gro@abonie@actions-user