Uh oh!
There was an error while loading. Please reload this page.
Implement <inheritdoc> XML documentation support for F# - #19188
Conversation
Co-authored-by: T-Gro <46543583+T-Gro@users.noreply.github.com>
Co-authored-by: T-Gro <46543583+T-Gro@users.noreply.github.com>
<inheritdoc> XML documentation support❗ 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>
…(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>
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-Gro
left a comment
There was a problem hiding this comment.
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
commented
Aug 4, 2026
Worth trying the external-XML lookup before the exhaustive |
…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>
Uh oh!
There was an error while loading. Please reload this page.
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 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.