Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
ce6fd0d
Initial plan
Copilot Jan 2, 2026
20291f3
WIP: Add XML documentation infrastructure (incomplete - needs more work)
Copilot Jan 2, 2026
0916227
Add initial infrastructure for `<inheritdoc>` XML documentation support
Copilot Jan 2, 2026
8882d88
Merge branch 'main' into copilot/support-xmldoc-inherit-element
T-Gro Jan 6, 2026
f0471a5
Fix build warnings: parameter name mismatch and unused parameter
Copilot Jan 6, 2026
d01033e
Fix unused parameter error: mark infoReader as intentionally unused
Copilot Jan 6, 2026
7fb1835
Fix FS3218 warning: update signature to match implementation with _m …
Copilot Jan 7, 2026
0d42ef3
Add detailed SPEC-TODO.MD with implementation checklist based on MS L…
Copilot Jan 7, 2026
1cf9a6c
Impl added
T-Gro Jan 8, 2026
5aa295b
next batch of impls
T-Gro Jan 12, 2026
ddee485
inherits impl
T-Gro Jan 12, 2026
1b8e242
Deduplicate with go to definition
T-Gro Jan 13, 2026
14a1f05
fixing warnings
T-Gro Jan 13, 2026
e1fb343
Apply patch from /run xlf
actions-user Jan 13, 2026
c685017
Merge branch 'main' into copilot/support-xmldoc-inherit-element
T-Gro Jan 13, 2026
5ab6249
Apply suggestions from code review
T-Gro Jan 13, 2026
b06ccdd
Apply suggestions from code review
T-Gro Jan 14, 2026
d509cb6
Update Range.Zero to Range.range0 in XmlDoc tests
T-Gro Jan 15, 2026
b525f97
Merge branch 'main' into copilot/support-xmldoc-inherit-element
T-Gro Jan 15, 2026
5c8f7e3
Merge branch 'main' into copilot/support-xmldoc-inherit-element
T-Gro Jan 16, 2026
f79cbcd
Apply patch from /run fantomas
actions-user Jan 16, 2026
d96f3d2
Apply patch from /run test-baseline
actions-user Jan 16, 2026
2846e4e
Apply patch from /run ilverify
actions-user Jan 16, 2026
6a1b5da
Implement `<inheritdoc>` XML documentation support for F#
Copilot Jan 16, 2026
22f088c
Merge branch 'main' into copilot/support-xmldoc-inherit-element
T-Gro Jan 19, 2026
39a9cdb
Non hardcoded resolution of external assembly ref sigs
T-Gro Jan 19, 2026
1bce792
Merge branch 'main' into copilot/support-xmldoc-inherit-element
T-Gro Jan 19, 2026
b368155
FIx resolution of external .xml files for .dll imports
T-Gro Jan 19, 2026
34be62d
Adjust tests
T-Gro Jan 20, 2026
ac0806f
Apply patch from /run fantomas
actions-user Jan 20, 2026
c1b885a
Merge branch 'main' into copilot/support-xmldoc-inherit-element
T-Gro Jan 22, 2026
1ce64c7
Simplify inheritdoc: tooling-only resolution, no compile-time expansion
T-Gro Feb 7, 2026
08cc298
Add inheritdoc unit tests; commit perf optimization and test refactoring
T-Gro Feb 7, 2026
1ed0ea2
Remove agent artifacts and working files from PR
T-Gro Feb 7, 2026
fc391d8
Add release note for inheritdoc XML documentation support
T-Gro Feb 7, 2026
804ce28
Fixup: code quality, perf, test quality, and override inheritdoc reso…
T-Gro Feb 7, 2026
d779982
Fixup #1: perf, code quality, and test improvements
T-Gro Feb 8, 2026
1464402
Fixup #1: eliminate redundant allocations, extract shared cref dispat…
T-Gro Feb 8, 2026
1c450e4
Fixup #1: fix missing [<Theory>], remove dead code, deduplicate expan…
T-Gro Feb 8, 2026
122a8f9
Fixup #1: fix F: cref field resolution, remove unused import, strengt…
T-Gro Feb 8, 2026
6a41e29
Fixup #1: inline trivial wrapper, tighten parseCref fallback, improve…
T-Gro Feb 8, 2026
ed9cf45
Fixup #1: fix broken component tests using removed expandInheritDoc API
T-Gro Feb 8, 2026
ef670dd
Fixup #1: fix stale ILVerify baselines and use correct cref prefixes …
T-Gro Feb 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Compiler.Service/11.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
* Debugger: provide breakpoint ranges for short lambdas ([#19067](https://github.com/dotnet/fsharp/pull/19067))
* FSharpDiagnostic: add default severity ([#19152](https://github.com/dotnet/fsharp/pull/19152))
* Add support for triple quoted ASCII byte string ([#19182](https://github.com/dotnet/fsharp/pull/19182))
* Add `<inheritdoc>` XML documentation support, resolved at tooling time via FCS Symbols API. ([PR #19188](https://github.com/dotnet/fsharp/pull/19188))

### Changed

Expand Down
3 changes: 3 additions & 0 deletions src/Compiler/Driver/XmlDocFileWriter.fs
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,14 @@ module XmlDocWriter =
doModuleSig None generatedCcu.Contents

let WriteXmlDocFile (g, assemblyName, generatedCcu: CcuThunk, xmlFile) =

if not (FileSystemUtils.checkSuffix xmlFile "xml") then
error (Error(FSComp.SR.docfileNoXmlSuffix (), Range.rangeStartup))

let mutable members = []

// <inheritdoc> elements are written to the XML file as-is.
// Resolution happens at tooling time (IDE tooltips, FCS Symbols API).
let addMember id xmlDoc =
if hasDoc xmlDoc then
let doc = xmlDoc.GetXmlText()
Expand Down
1 change: 1 addition & 0 deletions src/Compiler/Driver/XmlDocFileWriter.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ module XmlDocWriter =

/// Writes the XmlDocSig property of each element (field, union case, etc)
/// of the specified compilation unit to an XML document in a new text file.
/// <inheritdoc> elements are written to the XML file as-is; resolution happens at tooling time.
val WriteXmlDocFile: g: TcGlobals * assemblyName: string * generatedCcu: CcuThunk * xmlFile: string -> unit
1 change: 1 addition & 0 deletions src/Compiler/FSComp.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1670,6 +1670,7 @@ forFormatInvalidForInterpolated4,"Interpolated strings used as type IFormattable
3390,xmlDocDuplicateParameter,"This XML comment is invalid: multiple documentation entries for parameter '%s'"
3390,xmlDocUnresolvedCrossReference,"This XML comment is invalid: unresolved cross-reference '%s'"
3390,xmlDocMissingParameter,"This XML comment is incomplete: no documentation for parameter '%s'"
3390,xmlDocInheritDocError,"This XML comment is invalid: inheritdoc error: %s"
3391,tcImplicitConversionUsedForNonMethodArg,"This expression uses the implicit conversion '%s' to convert type '%s' to type '%s'. See https://aka.ms/fsharp-implicit-convs. This warning may be disabled using '#nowarn \"3391\"."
3392,containerDeprecated,"The 'AssemblyKeyNameAttribute' has been deprecated. Use 'AssemblyKeyFileAttribute' instead."
3393,containerSigningUnsupportedOnThisPlatform,"Key container signing is not supported on this platform."
Expand Down
4 changes: 4 additions & 0 deletions src/Compiler/FSharp.Compiler.Service.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,10 @@
<Compile Include="Driver\CompilerOptions.fs" />
<Compile Include="Driver\OptimizeInputs.fsi" />
<Compile Include="Driver\OptimizeInputs.fs" />
<Compile Include="Symbols\XmlDocSigParser.fsi" />
<Compile Include="Symbols\XmlDocSigParser.fs" />
<Compile Include="Symbols\XmlDocInheritance.fsi" />
<Compile Include="Symbols\XmlDocInheritance.fs" />
<Compile Include="Driver\XmlDocFileWriter.fsi" />
<Compile Include="Driver\XmlDocFileWriter.fs" />
<Compile Include="Driver\BinaryResourceFormats.fsi" />
Expand Down
2 changes: 2 additions & 0 deletions src/Compiler/Symbols/SymbolHelpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,8 @@ module internal SymbolHelpers =
let GetXmlCommentForItemAux (xmlDoc: XmlDoc option) (infoReader: InfoReader) m d =
match xmlDoc with
| Some xmlDoc when not xmlDoc.IsEmpty ->
// <inheritdoc> elements are left intact here. Full resolution happens through the
// Symbols.fs path (FSharpEntity.XmlDoc / FSharpMemberOrFunctionOrValue.XmlDoc).
FSharpXmlDoc.FromXmlText xmlDoc
| _ -> GetXmlDocHelpSigOfItemForLookup infoReader m d

Expand Down
292 changes: 286 additions & 6 deletions src/Compiler/Symbols/Symbols.fs

Large diffs are not rendered by default.

161 changes: 161 additions & 0 deletions src/Compiler/Symbols/XmlDocInheritance.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

module internal FSharp.Compiler.XmlDocInheritance

open System.Xml.Linq
open System.Xml.XPath
open FSharp.Compiler.DiagnosticsLogger
open FSharp.Compiler.Text

/// Represents an inheritdoc directive found in XML documentation
type InheritDocDirective =
{
/// Optional cref attribute specifying explicit target
Cref: string option
/// Optional path attribute for XPath filtering
Path: string option
/// The original XElement for replacement
Element: XElement
}

/// Checks if an XML document contains <inheritdoc> elements
let private hasInheritDoc (xmlText: string) = xmlText.IndexOf("<inheritdoc") >= 0

/// Extracts inheritdoc directives from parsed XML
let private extractInheritDocDirectives (doc: XDocument) =
let inheritDocName = XName.op_Implicit "inheritdoc" |> Operators.nonNull

let crefName = XName.op_Implicit "cref" |> Operators.nonNull
let pathName = XName.op_Implicit "path" |> Operators.nonNull

doc.Descendants(inheritDocName)
|> Seq.map (fun elem ->
let crefAttr = elem.Attribute(crefName)
let pathAttr = elem.Attribute(pathName)

{
Cref =
match crefAttr with
| null -> None
| attr -> Some attr.Value
Path =
match pathAttr with
| null -> None
| attr -> Some attr.Value
Element = elem
})
|> List.ofSeq

/// Applies an XPath filter to XML content
let private applyXPathFilter (m: range) (xpath: string) (sourceXml: string) : string option =
try
let doc =
XDocument.Parse("<doc>" + sourceXml + "</doc>", LoadOptions.PreserveWhitespace)

// If the xpath starts with /, it's an absolute path that won't work with our wrapper
// Adjust to search within the doc
let adjustedXpath =
if xpath.StartsWith("/") && not (xpath.StartsWith("//")) then
"/doc" + xpath
else
xpath

let selectedElements = doc.XPathSelectElements(adjustedXpath)

if Seq.isEmpty selectedElements then
None
else
let result =
selectedElements
|> Seq.map (fun elem -> elem.ToString(SaveOptions.DisableFormatting))
|> String.concat "\n"

Some result
with ex ->
warning (Error(FSComp.SR.xmlDocInheritDocError ($"invalid XPath '{xpath}': {ex.Message}"), m))
None

/// Recursively expands inheritdoc in the retrieved documentation
let rec private expandInheritedDoc
(resolveCref: string -> string option)
(implicitTargetCrefOpt: string option)
(m: range)
(visited: Set<string>)
(cref: string)
(xmlText: string)
: string =
if visited.Contains(cref) then
xmlText
else
let newVisited = visited.Add(cref)
expandInheritDocFromXmlText resolveCref implicitTargetCrefOpt m newVisited xmlText

/// Expands `<inheritdoc>` elements in XML documentation text.
/// The caller provides a `resolveCref` function that maps a cref string to its resolved XML doc text.
/// Takes an optional implicit target cref for resolving <inheritdoc/> without cref attribute.
/// Tracks visited signatures to prevent infinite recursion.
/// Takes a pre-computed xmlText string, avoiding an extra GetXmlText() call.
and expandInheritDocFromXmlText
(resolveCref: string -> string option)
(implicitTargetCrefOpt: string option)
(m: range)
(visited: Set<string>)
(xmlText: string)
: string =
if not (hasInheritDoc xmlText) then
xmlText
else
try
let wrappedXml = "<doc>\n" + xmlText + "\n</doc>"
let xdoc = XDocument.Parse(wrappedXml, LoadOptions.PreserveWhitespace)

let directives = extractInheritDocDirectives xdoc

if directives.IsEmpty then
xmlText
else
let resolveAndReplace (directive: InheritDocDirective) (cref: string) (implicitCrefForRecursion: string option) =
if visited.Contains(cref) then
warning (Error(FSComp.SR.xmlDocInheritDocError ($"Circular reference detected for '{cref}'"), m))
else
match resolveCref cref with
| Some inheritedXml ->
let expandedInheritedXml =
expandInheritedDoc resolveCref implicitCrefForRecursion m visited cref inheritedXml

let contentToInherit =
match directive.Path with
| Some xpath ->
applyXPathFilter m xpath expandedInheritedXml
|> Option.defaultValue expandedInheritedXml
| None -> expandedInheritedXml

try
let newContent = XElement.Parse("<temp>" + contentToInherit + "</temp>")
directive.Element.ReplaceWith(newContent.Nodes())
with ex ->
warning (Error(FSComp.SR.xmlDocInheritDocError ($"Failed to process inheritdoc: {ex.Message}"), m))
| None -> warning (Error(FSComp.SR.xmlDocInheritDocError ($"Cannot resolve cref '{cref}'"), m))

for directive in directives do
match directive.Cref with
| Some cref -> resolveAndReplace directive cref implicitTargetCrefOpt
| None ->
match implicitTargetCrefOpt with
| Some implicitCref -> resolveAndReplace directive implicitCref None
| None ->
warning (
Error(
FSComp.SR.xmlDocInheritDocError ("Implicit inheritdoc (without cref) requires a base type or interface"),
m
)
)

match xdoc.Root with
| null -> xmlText
| root ->
root.Nodes()
|> Seq.map (fun node -> node.ToString(SaveOptions.DisableFormatting))
|> String.concat "\n"
with :? System.Xml.XmlException ->
xmlText
18 changes: 18 additions & 0 deletions src/Compiler/Symbols/XmlDocInheritance.fsi
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

module internal FSharp.Compiler.XmlDocInheritance

open FSharp.Compiler.Text

/// Expands `<inheritdoc>` elements in XML documentation text.
/// The caller provides a `resolveCref` function to look up documentation by cref string.
/// Takes an optional implicit target cref for resolving <inheritdoc/> without cref attribute.
/// Takes a set of visited signatures to prevent cycles.
/// Takes a pre-computed xmlText string, avoiding an extra GetXmlText() call.
val expandInheritDocFromXmlText:
resolveCref: (string -> string option) ->
implicitTargetCrefOpt: string option ->
m: range ->
visited: Set<string> ->
xmlText: string ->
string
89 changes: 89 additions & 0 deletions src/Compiler/Symbols/XmlDocSigParser.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

namespace FSharp.Compiler.Symbols

open System.Text.RegularExpressions

/// Represents the kind of element in a documentation comment ID
[<RequireQualifiedAccess>]
type DocCommentIdKind =
| Type
| Method
| Property
| Field
| Event
| Namespace
| Unknown

/// Represents a parsed documentation comment ID (cref format)
[<RequireQualifiedAccess>]
type ParsedDocCommentId =
/// Type reference (T:Namespace.Type)
| Type of path: string list
/// Member reference (M:, P:, E:) with type path, member name, generic arity, and kind
| Member of typePath: string list * memberName: string * genericArity: int * kind: DocCommentIdKind
/// Field reference (F:Namespace.Type.field)
| Field of typePath: string list * fieldName: string
/// Invalid or unparseable ID
| None

module XmlDocSigParser =
// Hoisted to module level to avoid re-creating compiled Regex on every call
let private docCommentIdRx =
Regex(@"^(?<kind>\w):(?<entity>[\w\d#`.]+)(?<args>\(.+\))?(?:~([\w\d.]+))?$", RegexOptions.Compiled)

let private fnGenericArgsRx =
Regex(@"^(?<entity>.+)``(?<typars>\d+)$", RegexOptions.Compiled)

/// Parse a documentation comment ID string (e.g., "M:Namespace.Type.Method(System.String)")
let parseDocCommentId (docCommentId: string) =

let m = docCommentIdRx.Match(docCommentId)
let kindStr = m.Groups["kind"].Value

match m.Success, kindStr with
| true, ("M" | "P" | "E") ->
let parts = m.Groups["entity"].Value.Split('.')

if parts.Length < 2 then
ParsedDocCommentId.None
else
let entityPath = parts[.. (parts.Length - 2)] |> List.ofArray
let memberOrVal = parts[parts.Length - 1]

// Try and parse generic params count from the name
let genericM = fnGenericArgsRx.Match(memberOrVal)

let (memberOrVal, genericParametersCount) =
if genericM.Success then
(genericM.Groups["entity"].Value, int genericM.Groups["typars"].Value)
else
memberOrVal, 0

let kind =
match kindStr with
| "M" -> DocCommentIdKind.Method
| "P" -> DocCommentIdKind.Property
| "E" -> DocCommentIdKind.Event
| _ -> DocCommentIdKind.Unknown

// Handle constructor name conversion (#ctor in doc comments, .ctor in F#)
let finalMemberName = if memberOrVal = "#ctor" then ".ctor" else memberOrVal

ParsedDocCommentId.Member(entityPath, finalMemberName, genericParametersCount, kind)

| true, "T" ->
let entityPath = m.Groups["entity"].Value.Split('.') |> List.ofArray
ParsedDocCommentId.Type entityPath

| true, "F" ->
let parts = m.Groups["entity"].Value.Split('.')

if parts.Length < 2 then
ParsedDocCommentId.None
else
let entityPath = parts[.. (parts.Length - 2)] |> List.ofArray
let memberOrVal = parts[parts.Length - 1]
ParsedDocCommentId.Field(entityPath, memberOrVal)

| _ -> ParsedDocCommentId.None
30 changes: 30 additions & 0 deletions src/Compiler/Symbols/XmlDocSigParser.fsi
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

namespace FSharp.Compiler.Symbols

/// Represents the kind of element in a documentation comment ID
[<RequireQualifiedAccess>]
type DocCommentIdKind =
| Type
| Method
| Property
| Field
| Event
| Namespace
| Unknown

/// Represents a parsed documentation comment ID (cref format)
[<RequireQualifiedAccess>]
type ParsedDocCommentId =
/// Type reference (T:Namespace.Type)
| Type of path: string list
/// Member reference (M:, P:, E:) with type path, member name, generic arity, and kind
| Member of typePath: string list * memberName: string * genericArity: int * kind: DocCommentIdKind
/// Field reference (F:Namespace.Type.field)
| Field of typePath: string list * fieldName: string
/// Invalid or unparseable ID
| None

module XmlDocSigParser =
/// Parse a documentation comment ID string (e.g., "M:Namespace.Type.Method(System.String)")
val parseDocCommentId: docCommentId: string -> ParsedDocCommentId
5 changes: 5 additions & 0 deletions src/Compiler/xlf/FSComp.txt.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Compiler/xlf/FSComp.txt.de.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Compiler/xlf/FSComp.txt.es.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading