Open
Implement <inheritdoc> XML documentation support for F##19188
<inheritdoc> XML documentation support for F##19188Conversation
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 Jan 2, 2026
<inheritdoc> XML documentation support
Contributor
❗ Release notes requiredYou can open this PR in browser to add release notes: open in github.dev
|
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>
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><summary>...</summary></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
marked this pull request as ready for review
July 25, 2026 18:58
…c-inherit-element
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>
…c-inherit-element
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 theFSharpSymbol.XmlDocAPI consumed by FCS/IDE clients (Symbols.fs).The generated
.xmldocumentation 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.xmlfile.Supported today
<inheritdoc/>inherits from:System.Object) or, failing that, its first implemented interface;<inheritdoc cref="..."/>on theFSharpSymbol.XmlDocpath (name-based CCU walk, incl. same-file and cross-assembly types).path="..."XPath filtering for element selection (e.g.path="/summary").Deliberate limitations (documented, tested where practical)
crefis resolved only throughFSharpSymbol.XmlDoc, not at the tooltip/completion InfoReader layer (noSymbolEnv/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).FSharpSymbol.XmlDocpath it returns unexpanded, because that resolver is name-based and would collapse constructor overloads.System.*member) is not expanded at the tooltip layer.FSharpSymbol.XmlDocpath via implemented slot signatures).FSharpSymbol.XmlDocpath only, not at the tooltip layer.crefto an overloaded member on theFSharpSymbol.XmlDocpath 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).<inheritdoc/>chains (e.g.Grand→Base→Derived, each with a bare<inheritdoc/>) resolve a single level: the implicit target is not recomputed per intermediate symbol.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 inheritingSystem.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.XmlDoccoverage 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.fsandtests/FSharp.Compiler.ComponentTests/Miscellaneous/XmlDoc.fs— surrounding coverage, including that the.xmlwriter 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 inXmlDocFileWriter.fs/.fsi.