Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e61317c
Initial plan
Copilot Dec 31, 2025
432170d
Add XML documentation include tag support - implementation and tests
Copilot Dec 31, 2025
0b97f99
Fix compilation errors and improve test helpers
Copilot Dec 31, 2025
8de26fa
Complete XML documentation include tag implementation
Copilot Dec 31, 2025
ef5720d
Apply patch from /run fantomas
actions-user Jan 2, 2026
86cf959
Fix withXmlDoc to properly generate XML documentation files
Copilot Jan 2, 2026
dc3625e
Add release notes and run formatting
Copilot Jan 2, 2026
f8064a7
Switch expandIncludes to use GetXmlText() for proper XML structure
Copilot Jan 2, 2026
9338497
Simplify expandIncludes to work directly with XML text
Copilot Jan 2, 2026
ff60857
Fix XML include tests - all 9 tests passing
Copilot Jan 2, 2026
ef9b442
Apply patch from /run ilverify
actions-user Jan 2, 2026
72e041a
Refactor verifyXmlDocContains and verifyXmlDocNotContains to use help…
Copilot Jan 2, 2026
2c42492
Optimize XML include expansion for efficiency
Copilot Jan 2, 2026
fbe29ba
Optimize expandIncludes to work with UnprocessedLines before GetXmlTe…
Copilot Jan 2, 2026
3b51a01
Refactor to use generic expansion driver and preserve line array stru…
Copilot Jan 2, 2026
a7368fe
Merge branch 'main' into copilot/add-include-xml-support
T-Gro Jan 5, 2026
4b8cdb8
Apply patch from /run ilverify
actions-user Jan 5, 2026
b4acb7c
Major refactoring: eliminate duplication, fix architecture, add empty…
Copilot Jan 5, 2026
51f9315
Merge branch 'main' into copilot/add-include-xml-support
T-Gro Jan 6, 2026
e5041fe
Apply patch from /run ilverify
actions-user Jan 6, 2026
25a8640
Update src/Compiler/SyntaxTree/XmlDocIncludeExpander.fs
T-Gro Jan 6, 2026
40f008f
Apply all code review suggestions: active pattern, Result.bind, Array…
Copilot Jan 6, 2026
71c871f
Apply final cosmetic improvements: mutual recursion and function rena…
Copilot Jan 7, 2026
5f1fd11
Fix: Pass elements directly instead of their nodes to expandAllInclud…
Copilot Jan 7, 2026
d5bc862
Merge branch 'main' into copilot/add-include-xml-support
T-Gro Jan 9, 2026
146dae2
Merge branch 'main' into copilot/add-include-xml-support
T-Gro Jan 20, 2026
aacf6ab
Merge branch 'main' into copilot/add-include-xml-support
T-Gro Jan 22, 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/10.0.200.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
### Added

* FSharpDiagnostic: add default severity ([#19152](https://github.com/dotnet/fsharp/pull/19152))
* Support for `<include>` XML documentation tag ([PR #19186](https://github.com/dotnet/fsharp/pull/19186))
* Add warning FS3879 for XML documentation comments not positioned as first non-whitespace on line. ([PR #18891](https://github.com/dotnet/fsharp/pull/18891))
* FsiEvaluationSession.ParseAndCheckInteraction: add keepAssemblyContents optional parameter ([#19155](https://github.com/dotnet/fsharp/pull/19155))

Expand Down
4 changes: 3 additions & 1 deletion src/Compiler/Driver/XmlDocFileWriter.fs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ open FSharp.Compiler.DiagnosticsLogger
open FSharp.Compiler.IO
open FSharp.Compiler.Text
open FSharp.Compiler.Xml
open FSharp.Compiler.Xml.XmlDocIncludeExpander
open FSharp.Compiler.TypedTree
open FSharp.Compiler.TypedTreeOps

Expand Down Expand Up @@ -85,7 +86,8 @@ module XmlDocWriter =

let addMember id xmlDoc =
if hasDoc xmlDoc then
let doc = xmlDoc.GetXmlText()
let expandedDoc = expandIncludes xmlDoc
let doc = expandedDoc.GetXmlText()
members <- (id, doc) :: members

let doVal (v: Val) = addMember v.XmlDocSig v.XmlDoc
Expand Down
1 change: 1 addition & 0 deletions src/Compiler/FSComp.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1674,6 +1674,7 @@ forFormatInvalidForInterpolated4,"Interpolated strings used as type IFormattable
3392,containerDeprecated,"The 'AssemblyKeyNameAttribute' has been deprecated. Use 'AssemblyKeyFileAttribute' instead."
3393,containerSigningUnsupportedOnThisPlatform,"Key container signing is not supported on this platform."
3394,parsNewExprMemberAccess,"This member access is ambiguous. Please use parentheses around the object creation, e.g. '(new SomeType(args)).MemberName'"
3395,xmlDocIncludeError,"XML documentation include error: %s"
3395,tcImplicitConversionUsedForMethodArg,"This expression uses the implicit conversion '%s' to convert type '%s' to type '%s'."
3396,tcLiteralAttributeCannotUseActivePattern,"A [<Literal>] declaration cannot use an active pattern for its identifier"
3397,tcUnitToObjSubsumption,"This expression uses 'unit' for an 'obj'-typed argument. This will lead to passing 'null' at runtime. This warning may be disabled using '#nowarn \"3397\"."
Expand Down
2 changes: 2 additions & 0 deletions src/Compiler/FSharp.Compiler.Service.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,8 @@
<Compile Include="SyntaxTree\UnicodeLexing.fs" />
<Compile Include="SyntaxTree\XmlDoc.fsi" />
<Compile Include="SyntaxTree\XmlDoc.fs" />
<Compile Include="SyntaxTree\XmlDocIncludeExpander.fsi" />
<Compile Include="SyntaxTree\XmlDocIncludeExpander.fs" />
<Compile Include="SyntaxTree\SyntaxTrivia.fsi" />
<Compile Include="SyntaxTree\SyntaxTrivia.fs" />
<Compile Include="SyntaxTree\SyntaxTree.fsi" />
Expand Down
188 changes: 188 additions & 0 deletions src/Compiler/SyntaxTree/XmlDocIncludeExpander.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

module internal FSharp.Compiler.Xml.XmlDocIncludeExpander

open System
open System.IO
open System.Xml.Linq
open System.Xml.XPath
open FSharp.Compiler.Xml
open FSharp.Compiler.DiagnosticsLogger
open FSharp.Compiler.IO
open FSharp.Compiler.Text
open Internal.Utilities.Library

/// Thread-safe cache for loaded XML files
let private xmlDocCache =
let cacheOptions =
FSharp.Compiler.Caches.CacheOptions.getDefault StringComparer.OrdinalIgnoreCase

new FSharp.Compiler.Caches.Cache<string, Result<XDocument, string>>(cacheOptions, "XmlDocIncludeCache")

/// Load an XML file from disk with caching
let private loadXmlFile (filePath: string) : Result<XDocument, string> =
xmlDocCache.GetOrAdd(
filePath,
fun path ->
try
if not (FileSystem.FileExistsShim(path)) then
Result.Error $"File not found: {path}"
else
let doc = XDocument.Load(path)
Result.Ok doc
with ex ->
Result.Error $"Error loading file '{path}': {ex.Message}"
)

/// Resolve a file path (absolute or relative to source file)
let private resolveFilePath (baseFileName: string) (includePath: string) : string =
if Path.IsPathRooted(includePath) then
includePath
else
let baseDir =
if String.IsNullOrEmpty(baseFileName) || baseFileName = "unknown" then
Directory.GetCurrentDirectory()
else
match Path.GetDirectoryName(baseFileName) with
| Null -> Directory.GetCurrentDirectory()
| NonNull dir when String.IsNullOrEmpty(dir) -> Directory.GetCurrentDirectory()
| NonNull dir -> dir

Path.GetFullPath(Path.Combine(baseDir, includePath))

/// Evaluate XPath and return matching elements
let private evaluateXPath (doc: XDocument) (xpath: string) : Result<XElement seq, string> =
try
if String.IsNullOrWhiteSpace(xpath) then
Result.Error "XPath expression is empty"
else
let elements = doc.XPathSelectElements(xpath)

if obj.ReferenceEquals(elements, null) || Seq.isEmpty elements then
Result.Error $"XPath query returned no results: {xpath}"
else
Result.Ok elements
with ex ->
Result.Error $"Invalid XPath expression '{xpath}': {ex.Message}"

/// Include directive information
type private IncludeInfo = { FilePath: string; XPath: string }

/// Quick check if a string might contain an include tag (no allocations)
let private mayContainInclude (text: string) : bool =
not (String.IsNullOrEmpty(text)) && text.Contains("<include")

/// Extract include directive from an XElement if it has both required attributes
let private tryGetInclude (elem: XElement) : IncludeInfo option =
let fileAttr = elem.Attribute(!!(XName.op_Implicit "file"))
let pathAttr = elem.Attribute(!!(XName.op_Implicit "path"))

match fileAttr, pathAttr with
| NonNull file, NonNull path ->
Some
{
FilePath = file.Value
XPath = path.Value
}
| _ -> None

/// Active pattern to parse a line as an include directive (must be include tag alone on the line)
let private (|ParsedXmlInclude|_|) (line: string) : IncludeInfo option =
try
let elem = XElement.Parse(line.Trim())

if elem.Name.LocalName = "include" then
tryGetInclude elem
else
None
with _ ->
None

/// Load and expand includes from an external file
/// This is the single unified error-handling and expansion logic
let rec private resolveSingleInclude
(baseFileName: string)
(includeInfo: IncludeInfo)
(inProgressFiles: Set<string>)
(range: range)
: Result<XNode seq, string> =

let resolvedPath = resolveFilePath baseFileName includeInfo.FilePath

// Check for circular includes
if inProgressFiles.Contains(resolvedPath) then
Result.Error $"Circular include detected: {resolvedPath}"
else
loadXmlFile resolvedPath
|> Result.bind (fun includeDoc -> evaluateXPath includeDoc includeInfo.XPath)
|> Result.map (fun elements ->
// Expand the loaded content recursively
let updatedInProgress = inProgressFiles.Add(resolvedPath)
let nodes = elements |> Seq.cast<XNode>
expandAllIncludeNodes resolvedPath nodes updatedInProgress range)

/// Recursively expand includes in XElement nodes
/// This is the ONLY recursive expansion - works on XElement level, never on strings
and private expandAllIncludeNodes (baseFileName: string) (nodes: XNode seq) (inProgressFiles: Set<string>) (range: range) : XNode seq =
nodes
|> Seq.collect (fun node ->
if node.NodeType <> System.Xml.XmlNodeType.Element then
Seq.singleton node
else
let elem = node :?> XElement

match tryGetInclude elem with
| None ->
// Not an include element, recursively process children
let expandedChildren =
expandAllIncludeNodes baseFileName (elem.Nodes()) inProgressFiles range

let newElem = XElement(elem.Name, elem.Attributes(), expandedChildren)
Seq.singleton (newElem :> XNode)
| Some includeInfo ->
// This is an include element - expand it
match resolveSingleInclude baseFileName includeInfo inProgressFiles range with
| Result.Error msg ->
warning (Error(FSComp.SR.xmlDocIncludeError msg, range))
Seq.singleton node
| Result.Ok expandedNodes -> expandedNodes)

/// Expand all <include> elements in an XmlDoc
/// Works directly on line array without string concatenation
let expandIncludes (doc: XmlDoc) : XmlDoc =
if doc.IsEmpty then
doc
else
let unprocessedLines = doc.UnprocessedLines
let baseFileName = doc.Range.FileName

// Early exit: check if any line contains "<include" (cheap check)
let hasIncludes = unprocessedLines |> Array.exists mayContainInclude

if not hasIncludes then
doc
else
// Expand includes in the line array, keeping the array structure
let expandedLines =
unprocessedLines
|> Array.collect (fun line ->
match line with
| s when not (mayContainInclude s) -> [| line |]
| ParsedXmlInclude includeInfo ->
match resolveSingleInclude baseFileName includeInfo Set.empty doc.Range with
| Result.Error msg ->
warning (Error(FSComp.SR.xmlDocIncludeError msg, doc.Range))
[| line |]
| Result.Ok nodes ->
// Convert nodes to strings (may be multiple lines)
nodes |> Seq.map (fun n -> n.ToString()) |> Array.ofSeq
| _ -> [| line |])

// Only create new XmlDoc if something changed
if
expandedLines.Length = unprocessedLines.Length
&& Array.forall2 (=) expandedLines unprocessedLines
then
doc
else
XmlDoc(expandedLines, doc.Range)
9 changes: 9 additions & 0 deletions src/Compiler/SyntaxTree/XmlDocIncludeExpander.fsi
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

module internal FSharp.Compiler.Xml.XmlDocIncludeExpander

open FSharp.Compiler.Xml

/// Expand all <include file="..." path="..."/> elements in an XmlDoc.
/// Warnings are emitted via the diagnostics logger for any errors.
val expandIncludes: doc: XmlDoc -> XmlDoc
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.

5 changes: 5 additions & 0 deletions src/Compiler/xlf/FSComp.txt.fr.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.it.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.ja.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.ko.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.pl.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.pt-BR.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.ru.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.tr.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.zh-Hans.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.zh-Hant.xlf

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

Loading
Loading