Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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 @@ -7,6 +7,7 @@
### Added

* FSharpDiagnostic: add default severity ([#19152](https://github.com/dotnet/fsharp/pull/19152))
* Support for `<include>` XML documentation tag ([Issue #19175](https://github.com/dotnet/fsharp/issues/19175)) ([PR #NNNNN](https://github.com/dotnet/fsharp/pull/NNNNN))

### Breaking Changes

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 @@ -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
201 changes: 201 additions & 0 deletions src/Compiler/SyntaxTree/XmlDocIncludeExpander.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// 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

/// Try to parse a line as an include directive (must be include tag alone on the line)
let private tryParseIncludeLine (line: string) : IncludeInfo option =
let trimmed = line.Trim()
// Quick check: must start with < and contain "include"
if not (trimmed.StartsWith("<") && mayContainInclude trimmed) then
None
else
try
let elem = XElement.Parse(trimmed)

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 private loadAndExpand
(baseFileName: string)
(includeInfo: IncludeInfo)
(inProgressFiles: Set<string>)
(range: range)
(expandNodes: string -> XNode seq -> Set<string> -> range -> XNode seq)
: 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
match loadXmlFile resolvedPath with
| Result.Error msg -> Result.Error msg
| Result.Ok includeDoc ->
match evaluateXPath includeDoc includeInfo.XPath with
| Result.Error msg -> Result.Error msg
| Result.Ok elements ->
// Expand the loaded content recursively
let updatedInProgress = inProgressFiles.Add(resolvedPath)
let nodes = elements |> Seq.collect (fun e -> e.Nodes())
let expandedNodes = expandNodes resolvedPath nodes updatedInProgress range
Result.Ok expandedNodes

/// Recursively expand includes in XElement nodes
/// This is the ONLY recursive expansion - works on XElement level, never on strings
let rec private expandElements (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 =
expandElements 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 loadAndExpand baseFileName includeInfo inProgressFiles range expandElements 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
|> Seq.collect (fun line ->
if not (mayContainInclude line) then
Seq.singleton line
else
match tryParseIncludeLine line with
| None -> Seq.singleton line
| Some includeInfo ->
match loadAndExpand baseFileName includeInfo Set.empty doc.Range expandElements with
| Result.Error msg ->
warning (Error(FSComp.SR.xmlDocIncludeError msg, doc.Range))
Seq.singleton line
| Result.Ok nodes ->
// Convert nodes to strings (may be multiple lines)
nodes |> Seq.map (fun n -> n.ToString()))
|> Array.ofSeq

// 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.

Loading
Loading