1+ // Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
2+
3+ module FSharp.Core.UnitTests.XmlDocumentationValidation
4+
5+ open System
6+ open System.IO
7+ open System.Text .RegularExpressions
8+ open System.Xml
9+ open Xunit
10+
11+ /// Extracts XML documentation blocks from F# signature files
12+ let extractXmlDocBlocks ( content : string ) =
13+ // Regex to match XML documentation comments (/// followed by XML content)
14+ let xmlDocPattern = @" ^\s*///\s*(.*)$"
15+ let regex = Regex( xmlDocPattern, RegexOptions.Multiline)
16+
17+ let lines = content.Split([| '\n' ; '\r' |], StringSplitOptions.RemoveEmptyEntries)
18+ let mutable xmlBlocks = []
19+ let mutable currentBlock = []
20+ let mutable lineNumber = 0
21+
22+ for line in lines do
23+ lineNumber <- lineNumber + 1
24+ let trimmedLine = line.Trim()
25+ if trimmedLine.StartsWith( " ///" ) then
26+ let xmlContent = trimmedLine.Substring( 3 ) .Trim()
27+ currentBlock <- ( xmlContent, lineNumber) :: currentBlock
28+ else
29+ if not ( List.isEmpty currentBlock) then
30+ xmlBlocks <- List.rev currentBlock :: xmlBlocks
31+ currentBlock <- []
32+
33+ // Don't forget the last block if file ends with XML comments
34+ if not ( List.isEmpty currentBlock) then
35+ xmlBlocks <- List.rev currentBlock :: xmlBlocks
36+
37+ List.rev xmlBlocks
38+
39+ /// Validates that XML content is well-formed
40+ let validateXmlBlock ( xmlLines : ( string * int ) list ) =
41+ if List.isEmpty xmlLines then
42+ Ok ()
43+ else
44+ let xmlContent = xmlLines |> List.map fst |> String.concat " \n "
45+ let firstLineNumber = xmlLines |> List.head |> snd
46+
47+ // Skip empty or whitespace-only blocks
48+ if String.IsNullOrWhiteSpace( xmlContent) then
49+ Ok ()
50+ else
51+ try
52+ // Wrap content in a root element to make it valid XML document
53+ let wrappedXml = sprintf " <root>%s </root>" xmlContent
54+ let doc = XmlDocument()
55+ doc.LoadXml( wrappedXml)
56+ Ok ()
57+ with
58+ | :? XmlException as ex ->
59+ Error ( sprintf " Line %d : Invalid XML - %s " firstLineNumber ex.Message)
60+ | ex ->
61+ Error ( sprintf " Line %d : XML parsing error - %s " firstLineNumber ex.Message)
62+
63+ /// Gets all .fsi files in FSharp.Core directory
64+ let getFSharpCoreFsiFiles () =
65+ let coreDir = Path.Combine(__ SOURCE_ DIRECTORY__, " .." , " .." , " .." , " src" , " FSharp.Core" )
66+ let fullPath = Path.GetFullPath( coreDir)
67+ if Directory.Exists( fullPath) then
68+ Directory.GetFiles( fullPath, " *.fsi" , SearchOption.AllDirectories)
69+ |> Array.toList
70+ else
71+ []
72+
73+ [<Fact>]
74+ let ``XML documentation in FSharp.Core fsi files should be well - formed`` () =
75+ let fsiFiles = getFSharpCoreFsiFiles()
76+
77+ Assert.False( List.isEmpty fsiFiles, " No .fsi files found in FSharp.Core directory" )
78+
79+ let mutable errors = []
80+ let mutable totalBlocks = 0
81+
82+ for fsiFile in fsiFiles do
83+ let relativePath = Path.GetFileName( fsiFile)
84+ try
85+ let content = File.ReadAllText( fsiFile)
86+ let xmlBlocks = extractXmlDocBlocks content
87+
88+ for xmlBlock in xmlBlocks do
89+ totalBlocks <- totalBlocks + 1
90+ match validateXmlBlock xmlBlock with
91+ | Ok () -> ()
92+ | Error errorMsg ->
93+ let error = sprintf " %s : %s " relativePath errorMsg
94+ errors <- error :: errors
95+ with
96+ | ex ->
97+ let error = sprintf " %s : Failed to read file - %s " relativePath ex.Message
98+ errors <- error :: errors
99+
100+ // Report statistics
101+ let validBlocks = totalBlocks - List.length errors
102+ let message = sprintf " Validated %d XML documentation blocks in %d .fsi files. %d valid, %d invalid."
103+ totalBlocks ( List.length fsiFiles) validBlocks ( List.length errors)
104+
105+ if not ( List.isEmpty errors) then
106+ let errorDetails = errors |> List.rev |> String.concat " \n "
107+ Assert.Fail( sprintf " %s \n\n Errors:\n %s " message errorDetails)
108+ else
109+ // This will show in test output for successful runs
110+ Assert.True( true , message)
0 commit comments