diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 358c0fc..cc3c7d4 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,4 +1,7 @@ -#### 11.4.0 - Unreleased +#### 11.4.1 - 28 August, 2026 +* Fix fslex generating an invalid signature file when the header defines a module #240 + +#### 11.4.0 - 6 July, 2026 * Add Fable support to FsLexYacc.Runtime. * Make the AssocTable lookup cache initial capacity configurable to avoid pre-allocating 2000 entries per parse #54 * Add `--assoc-cache-capacity` option for fsyacc to set the generated parser's AssocTable cache capacity from the command line #54 diff --git a/src/FsLex.Core/fslexdriver.fs b/src/FsLex.Core/fslexdriver.fs index 4412a14..04a8d67 100644 --- a/src/FsLex.Core/fslexdriver.fs +++ b/src/FsLex.Core/fslexdriver.fs @@ -118,17 +118,22 @@ let writeOpens opens (writer: Writer) = writer.WriteLine "" writer.WriteLineInterface "" +/// Picks the module declaration and open statements out of the header code, so they can be +/// repeated in the generated signature file. A nested module definition (a `module ... =` line) +/// is not a declaration: its body does not end up in the signature file, so copying it would +/// leave an empty module behind. +let getHeaderDeclarations (code: string) = + code.Split([| '\n'; '\r' |]) + |> Array.filter (fun s -> + (s.StartsWith("module ", StringComparison.Ordinal) + && not (s.TrimEnd().EndsWith("=", StringComparison.Ordinal))) + || s.StartsWith("open ", StringComparison.Ordinal)) + let writeTopCode code (writer: Writer) = writer.WriteCode code - let moduleAndOpens = - (fst code).Split([| '\n'; '\r' |]) - |> Array.filter (fun s -> - s.StartsWith("module ", StringComparison.Ordinal) - || s.StartsWith("open ", StringComparison.Ordinal)) - |> String.concat Environment.NewLine - - writer.WriteInterface "%s" moduleAndOpens + for line in getHeaderDeclarations (fst code) do + writer.WriteLineInterface "%s" line let writeUnicodeTranslationArray dfaNodes domain (writer: Writer) = let parseContext = diff --git a/tests/FsLex.Core.Tests/DriverTests.fs b/tests/FsLex.Core.Tests/DriverTests.fs new file mode 100644 index 0000000..92acb11 --- /dev/null +++ b/tests/FsLex.Core.Tests/DriverTests.fs @@ -0,0 +1,66 @@ +module FsLex.Core.Tests.DriverTests + +open System.IO +open FSharp.Text.Lexing +open FsLexYacc.FsLex.Driver +open Expecto + +let private writeTopCodeToInterface (header: string) = + let output = Path.GetTempFileName() + let outputi = String.concat "" [ output; "i" ] + + try + using (new Writer(output, outputi)) (writeTopCode (header, Position.Empty)) + File.ReadAllText outputi + finally + File.Delete output + File.Delete outputi + +[] +let tests = + testList "Driver" [ + testList "getHeaderDeclarations" [ + test "keeps the module declaration and the opens" { + let actual = + getHeaderDeclarations "module Test.Lexer\n\nopen System\nopen System.Text\n\nlet x = 1" + + Expect.sequenceEqual + actual + [| "module Test.Lexer"; "open System"; "open System.Text" |] + "Module declaration and opens should be kept" + } + + test "skips a nested module definition" { + let actual = + getHeaderDeclarations "open System\nmodule Ranges =\n let isInt8BadMax x = 1 <<< 7 = x" + + Expect.sequenceEqual actual [| "open System" |] "A nested module definition should be skipped" + } + + test "keeps a module abbreviation" { + let actual = getHeaderDeclarations "module Range = FSharp.Compiler.Text.Range" + + Expect.sequenceEqual + actual + [| "module Range = FSharp.Compiler.Text.Range" |] + "A module abbreviation should be kept" + } + ] + + testList "writeTopCode" [ + test "the header in the signature file is newline terminated" { + let actual = writeTopCodeToInterface "module Test.Lexer\nopen System\nlet x = 1" + + Expect.equal + actual + (sprintf "module Test.Lexer%sopen System%s" System.Environment.NewLine System.Environment.NewLine) + "Every header line should be written on its own line" + } + + test "a header without declarations writes nothing" { + let actual = writeTopCodeToInterface "let x = 1" + + Expect.equal actual "" "Nothing should be written when there is no declaration to repeat" + } + ] + ] diff --git a/tests/FsLex.Core.Tests/FsLex.Core.Tests.fsproj b/tests/FsLex.Core.Tests/FsLex.Core.Tests.fsproj index 669bed0..e7639cc 100644 --- a/tests/FsLex.Core.Tests/FsLex.Core.Tests.fsproj +++ b/tests/FsLex.Core.Tests/FsLex.Core.Tests.fsproj @@ -8,6 +8,7 @@ + diff --git a/tests/JsonLexAndYaccExample/.gitignore b/tests/JsonLexAndYaccExample/.gitignore index a0b68d0..12bea09 100644 --- a/tests/JsonLexAndYaccExample/.gitignore +++ b/tests/JsonLexAndYaccExample/.gitignore @@ -1,3 +1,4 @@ Lexer.fs +Lexer.fsi Parser.fs -Parser.fsi \ No newline at end of file +Parser.fsi diff --git a/tests/LexAndYaccMiniProject/.gitignore b/tests/LexAndYaccMiniProject/.gitignore index cbb331f..1a12258 100644 --- a/tests/LexAndYaccMiniProject/.gitignore +++ b/tests/LexAndYaccMiniProject/.gitignore @@ -1,4 +1,5 @@ Lexer.fs +Lexer.fsi Parser.fs Parser.fsi -test.txt \ No newline at end of file +test.txt