Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 @@ -1688,6 +1688,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
181 changes: 181 additions & 0 deletions src/Compiler/SyntaxTree/XmlDocIncludeExpander.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// 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
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}"

/// Recursively expand includes in XML content
let rec private expandIncludesInContent
(baseFileName: string)
(content: string)
(inProgressFiles: Set<string>)
(range: range)
: string =
// Early exit if content doesn't contain "<include" (case-insensitive check)
if not (content.IndexOf("<include", StringComparison.OrdinalIgnoreCase) >= 0) then
content
else
try
// Wrap content in a root element to handle multiple top-level elements
let wrappedContent = "<root>" + content + "</root>"
let doc = XDocument.Parse(wrappedContent)

let includeElements =
doc.Descendants(!!(XName.op_Implicit "include")) |> Seq.toList

if includeElements.IsEmpty then
content
else
let mutable modified = false

for includeElem in includeElements do
let fileAttr = includeElem.Attribute(!!(XName.op_Implicit "file"))
let pathAttr = includeElem.Attribute(!!(XName.op_Implicit "path"))

match fileAttr, pathAttr with
| Null, _ ->
warning (Error(FSComp.SR.xmlDocIncludeError "Missing 'file' attribute", range))
| _, Null ->
warning (Error(FSComp.SR.xmlDocIncludeError "Missing 'path' attribute", range))
| NonNull fileAttr, NonNull pathAttr ->
let includePath = fileAttr.Value
let xpath = pathAttr.Value
let resolvedPath = resolveFilePath baseFileName includePath

// Check for circular includes
if inProgressFiles.Contains(resolvedPath) then
warning (
Error(
FSComp.SR.xmlDocIncludeError $"Circular include detected: {resolvedPath}",
range
)
)
else
match loadXmlFile resolvedPath with
| Result.Error msg -> warning (Error(FSComp.SR.xmlDocIncludeError msg, range))
| Result.Ok includeDoc ->
match evaluateXPath includeDoc xpath with
| Result.Error msg -> warning (Error(FSComp.SR.xmlDocIncludeError msg, range))
| Result.Ok elements ->
// Get the inner content of selected elements
let newNodes =
elements
|> Seq.collect (fun elem -> elem.Nodes())
|> Seq.toList

// Recursively expand includes in the loaded content
let updatedInProgress = inProgressFiles.Add(resolvedPath)

let expandedNodes =
newNodes
|> List.map (fun node ->
if node.NodeType = System.Xml.XmlNodeType.Element then
let elemNode = node :?> XElement
let elemContent = elemNode.ToString()

let expanded =
expandIncludesInContent
resolvedPath
elemContent
updatedInProgress
range

XElement.Parse(expanded) :> XNode
else
node
)

// Replace the include element with expanded content
includeElem.ReplaceWith(expandedNodes)
modified <- true

if modified then
// Extract content from root wrapper
match doc.Root with
| Null -> content
| NonNull root ->
let resultDoc = root.Nodes() |> Seq.map (fun n -> n.ToString()) |> String.concat ""
resultDoc
else
content
with ex ->
warning (Error(FSComp.SR.xmlDocIncludeError $"Error parsing XML: {ex.Message}", range))
content

/// Expand all <include> elements in an XmlDoc
let expandIncludes (doc: XmlDoc) : XmlDoc =
if doc.IsEmpty then
doc
else
let content = doc.GetXmlText()

// Early exit if content doesn't contain "<include" (case-insensitive)
if not (content.IndexOf("<include", StringComparison.OrdinalIgnoreCase) >= 0) then
doc
else
let baseFileName = doc.Range.FileName
let expandedContent = expandIncludesInContent baseFileName content Set.empty doc.Range

// Create new XmlDoc with expanded content
if expandedContent = content then
doc
else
// Parse back into lines
let lines = expandedContent.Split([| '\r'; '\n' |], StringSplitOptions.RemoveEmptyEntries)
XmlDoc(lines, 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.

Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@
<Compile Include="Miscellaneous\ListLiterals.fs" />
<Compile Include="Miscellaneous\SemanticClassificationKeyBuilder.fs" />
<Compile Include="Miscellaneous\XmlDoc.fs" />
<Compile Include="Miscellaneous\XmlDocInclude.fs" />
<Compile Include="Miscellaneous\FsharpSuiteMigrated.fs" />
<Compile Include="Miscellaneous\MigratedCoreTests.fs" />
<Compile Include="Miscellaneous\MigratedOverloadTests.fs" />
Expand Down
Loading
Loading