@@ -43,7 +43,14 @@ let bannerRegex = Regex (@"^\s*// ---- (?<category>.+?) ----\s*$", RegexOptions.
4343
4444let moduleRegex =
4545 Regex (
46- @" ^(?<indent> *)module\s+(?<access>(?:private|internal)\s+)?(?<name>[A-Za-z_][A-Za-z0-9_]*)\s*=" ,
46+ @" ^(?<indent> *)module\s+(?<access>(?:private|internal)\s+)?(?:rec\s+)?(?<name>[A-Za-z_][A-Za-z0-9_]*)\s*=" ,
47+ RegexOptions.CultureInvariant
48+ )
49+
50+ /// A file-scoped module, ` module A.B ` without ` = ` , whose members start at column 0.
51+ let fileModuleRegex =
52+ Regex (
53+ @" ^module\s+(?<access>(?:private|internal)\s+)?(?:rec\s+)?(?<name>[A-Za-z_][A-Za-z0-9_.]*)\s*$" ,
4754 RegexOptions.CultureInvariant
4855 )
4956
@@ -81,10 +88,15 @@ type Scope = {
8188
8289let indentOf ( line : string ) = line.Length - line.AsSpan() .TrimStart( ' ' ) .Length
8390
84- /// Reads the modules of a file. ` memberRegex ` recognises a member declaration (` val ` or ` let ` );
85- /// members at the content level of a public module count, nested bindings and non-public members do not.
91+ /// <summary >Reads the modules of a file.</summary >
92+ /// <remarks >
93+ /// <c >memberRegex</c > recognises a member declaration (<c >val</c > or <c >let</c >); members at the content level of a public
94+ /// module count, nested bindings and non-public members do not. The second result lists the members at column 0 outside
95+ /// any module: they belong to a module header that the script does not understand, so they must not pass unchecked.
96+ /// </remarks >
8697let parse ( path : string ) ( memberRegex : Regex ) =
8798 let scopes = ResizeArray< Scope>()
99+ let orphans = ResizeArray< Member>()
88100 let stack = Collections.Generic.Stack< Scope>()
89101 let lines = File.ReadAllLines path
90102
@@ -104,8 +116,21 @@ let parse (path : string) (memberRegex : Regex) =
104116 stack.Pop () |> ignore
105117
106118 let moduleMatch = moduleRegex.Match line
119+ let fileModuleMatch = fileModuleRegex.Match line
107120
108- if moduleMatch.Success then
121+ if fileModuleMatch.Success then
122+ // The members of a file-scoped module start at column 0, the content level of an indent of -4
123+ let scope = {
124+ Path = fileModuleMatch.Groups[ " name" ]. Value
125+ Indent = - 4
126+ IsPublic = not fileModuleMatch.Groups[ " access" ]. Success
127+ Banners = ResizeArray ()
128+ Members = ResizeArray ()
129+ }
130+
131+ scopes.Add scope
132+ stack.Push scope
133+ elif moduleMatch.Success then
109134 let name = moduleMatch.Groups[ " name" ]. Value
110135 let parent =
111136 if stack.Count > 0 then
@@ -154,8 +179,16 @@ let parse (path : string) (memberRegex : Regex) =
154179 Category = category
155180 Name = memberMatch.Groups[ " name" ]. Value
156181 }
182+ else
183+ let memberMatch = memberRegex.Match line
157184
158- scopes |> Seq.filter _. IsPublic |> Seq.toArray
185+ if
186+ memberMatch.Success
187+ && memberMatch.Groups[ " indent" ]. Length = 0
188+ then
189+ orphans.Add { Line = lineNumber; Category = " " ; Name = memberMatch.Groups[ " name" ]. Value }
190+
191+ struct ( scopes |> Seq.filter _. IsPublic |> Seq.toArray, orphans |> Seq.toArray)
159192
160193let isActivePattern ( name : string ) = name.StartsWith ( " (|" , StringComparison.Ordinal)
161194
@@ -212,7 +245,7 @@ let checkImplementation (signaturePath : string) (signatureScopes : Scope array)
212245 if not ( File.Exists implementationPath) then
213246 report signaturePath 1 " The signature file has no implementation file next to it"
214247 else
215- let implementationScopes = parse implementationPath letRegex
248+ let struct ( implementationScopes , _ ) = parse implementationPath letRegex
216249
217250 let findScope ( scopes : Scope array ) path =
218251 scopes
@@ -242,27 +275,34 @@ let checkImplementation (signaturePath : string) (signatureScopes : Scope array)
242275
243276 let count = min signature.Members.Count implementation.Members.Count
244277
278+ // A member must have the same name and the same category, so that a moved banner cannot go unnoticed
279+ let differs index =
280+ let expected = signature.Members[ index]
281+ let actual = implementation.Members[ index]
282+
283+ not ( String.Equals ( expected.Name, actual.Name, StringComparison.Ordinal))
284+ || not ( String.Equals ( expected.Category, actual.Category, StringComparison.Ordinal))
285+
245286 let firstDifference =
246287 seq { 0 .. count - 1 }
247- |> Seq.tryFind ( fun index ->
248- not (
249- String.Equals (
250- signature.Members[ index]. Name,
251- implementation.Members[ index]. Name,
252- StringComparison.Ordinal
253- )
254- )
255- )
288+ |> Seq.tryFind differs
256289 |> Option.toValueOption
257290
258291 match firstDifference with
259292 | ValueSome index ->
260293 let expected = signature.Members[ index]
261294 let actual = implementation.Members[ index]
262- report
263- implementationPath
264- actual.Line
265- $" `%s {actual.Name}` is defined where the signature declares `%s {expected.Name}` (line %d {expected.Line})"
295+
296+ if String.Equals ( expected.Name, actual.Name, StringComparison.Ordinal) then
297+ report
298+ implementationPath
299+ actual.Line
300+ $" `%s {actual.Name}` is in category '%s {actual.Category}'; the signature puts it in '%s {expected.Category}' (line %d {expected.Line})"
301+ else
302+ report
303+ implementationPath
304+ actual.Line
305+ $" `%s {actual.Name}` is defined where the signature declares `%s {expected.Name}` (line %d {expected.Line})"
266306 | ValueNone when signature.Members.Count <> implementation.Members.Count ->
267307 report
268308 implementationPath
@@ -288,7 +328,13 @@ let signatureFiles =
288328 |> Seq.toArray
289329
290330for signaturePath in signatureFiles do
291- let scopes = parse signaturePath valRegex
331+ let struct ( scopes , orphans ) = parse signaturePath valRegex
332+
333+ for orphan in orphans do
334+ report
335+ signaturePath
336+ orphan.Line
337+ $" `%s {orphan.Name}` is outside every module header that the script understands (`module X =` or `module A.B`)"
292338
293339 for scope in scopes do
294340 checkSignature signaturePath scope
0 commit comments