Skip to content

Commit 54f9dba

Browse files
xperiandriclaude
andcommitted
fixup! chore: lay the foundation for the per-collection modules
`build/check-order.fsx` (review of #25): - understands file-scoped modules (`module A.B` without `=`), whose members start at column 0, and `module rec`; - reports a `val` at column 0 outside every module that it understands, instead of passing an unknown header unchecked; - compares the category of each function of the implementation with the signature, so that a moved banner is reported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 5554d1f commit 54f9dba

1 file changed

Lines changed: 66 additions & 20 deletions

File tree

‎build/check-order.fsx‎

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,14 @@ let bannerRegex = Regex (@"^\s*// ---- (?<category>.+?) ----\s*$", RegexOptions.
4343

4444
let 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

8289
let 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>
8697
let 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

160193
let 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

290330
for 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

Comments
 (0)