Repository: openllb/hlb (go)

Issue:
comment at end of file causes parse error
input:
```
# this is ok
fs default() {
	scratch
}
# this is not ok
```

error:
```
$ ./hlb run ./bug.hlb
./bug.hlb:5:1: unable to parse
```

I have run into this a few times as I comment out the last function in a file while debugging and then suddenly get parse errors.


Pull request description:
Fix parsing error with inputs not ending with newline
Fixes #135 

- Wraps the `io.Reader` with a `NewlinedReader` that appends a newline before the natural EOF.
- Removes complexity dealing with zero-newline inputs. I didn't see much value in unparsing one-liners to retain one-liners.

Patch:
diff --git a/parse.go b/parse.go
index c582dd6..43af5eb 100644
--- a/parse.go
+++ b/parse.go
@@ -61,6 +61,7 @@ func Parse(r io.Reader, opts ...ParseOption) (*parser.Module, *report.IndexedBuf
 	if name == "" {
 		name = "<stdin>"
 	}
+	r = &parser.NewlinedReader{Reader: r}
 
 	ib := report.NewIndexedBuffer()
 	r = io.TeeReader(r, ib)
diff --git a/parser/parse.go b/parser/parse.go
index c311697..164c6aa 100644
--- a/parser/parse.go
+++ b/parser/parse.go
@@ -1,6 +1,7 @@
 package parser
 
 import (
+	"errors"
 	"io"
 
 	"github.com/alecthomas/participle/lexer"
@@ -11,6 +12,7 @@ func Parse(r io.Reader) (*Module, error) {
 	if name == "" {
 		name = "<stdin>"
 	}
+	r = &NewlinedReader{Reader: r}
 
 	mod := &Module{}
 	lex, err := Parser.Lexer().Lex(&NamedReader{r, name})
@@ -40,3 +42,30 @@ type NamedReader struct {
 func (nr *NamedReader) Name() string {
 	return nr.Value
 }
+
+// NewlinedReader appends one more newline after an EOF is reached, so that
+// parsing is made easier when inputs that don't end with a newline.
+type NewlinedReader struct {
+	io.Reader
+	newlined int
+}
+
+func (nr *NewlinedReader) Read(p []byte) (n int, err error) {
+	if nr.newlined > 1 {
+		return 0, io.EOF
+	} else if nr.newlined == 1 {
+		p[0] = byte('\n')
+		nr.newlined++
+		return 1, nil
+	}
+
+	n, err = nr.Reader.Read(p)
+	if err != nil {
+		if errors.Is(err, io.EOF) {
+			nr.newlined++
+			return n, nil
+		}
+		return n, err
+	}
+	return n, nil
+}
diff --git a/parser/unparse.go b/parser/unparse.go
index 7df5299..af34ed5 100644
--- a/parser/unparse.go
+++ b/parser/unparse.go
@@ -14,21 +14,6 @@ func (m *Module) String() string {
 		doc = fmt.Sprintf("%s\n", m.Doc)
 	}
 
-	hasNewline := false
-
-	for _, decl := range m.Decls {
-		if decl.Pos == (lexer.Position{}) {
-			hasNewline = true
-			break
-		}
-
-		str := decl.String()
-		if strings.Contains(str, "\n") {
-			hasNewline = true
-			break
-		}
-	}
-
 	skipNewlines := true
 
 	var decls []string
@@ -47,7 +32,7 @@ func (m *Module) String() string {
 			skipNewlines = false
 		}
 
-		if hasNewline && len(prevDecl) > 0 && prevDecl[len(prevDecl)-1] != '\n' {
+		if len(prevDecl) > 0 && prevDecl[len(prevDecl)-1] != '\n' {
 			switch {
 			case strings.HasPrefix(str, "#"):
 				str = fmt.Sprintf(" %s", str)
@@ -62,21 +47,17 @@ func (m *Module) String() string {
 		prevDecl = str
 	}
 
-	var sep string
-	if hasNewline {
-		// Strip trailing newlines
-		for i := len(decls) - 1; i > 0; i-- {
-			if len(decls[i]) == 1 {
-				decls = decls[:i]
-			} else {
-				break
-			}
+	// Strip trailing newlines
+	for i := len(decls) - 1; i > 0; i-- {
+		if len(decls[i]) == 1 {
+			decls = decls[:i]
+		} else {
+			break
 		}
-	} else {
-		sep = " "
 	}
 
-	return fmt.Sprintf("%s%s", doc, strings.Join(decls, sep))
+	module := fmt.Sprintf("%s%s", doc, strings.Join(decls, ""))
+	return fmt.Sprintf("%s\n", strings.TrimSpace(module))
 }
 
 func (b *Bad) String() string {


Test patch:
diff --git a/checker/checker_test.go b/checker/checker_test.go
index a758be3..7ad8925 100644
--- a/checker/checker_test.go
+++ b/checker/checker_test.go
@@ -369,6 +369,10 @@ func TestChecker_Check(t *testing.T) {
 				Name: "localEnv",
 			},
 		},
+	}, {
+		"no error when input doesn't end with newline",
+		`# comment\nfs default() {\n  scratch\n}\n# comment`,
+		nil,
 	}} {
 		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
diff --git a/unparse_test.go b/unparse_test.go
index 8cc72a3..e09dac8 100644
--- a/unparse_test.go
+++ b/unparse_test.go
@@ -1,6 +1,7 @@
 package hlb
 
 import (
+	"fmt"
 	"strings"
 	"testing"
 
@@ -14,7 +15,7 @@ type testCase struct {
 }
 
 func cleanup(value string) string {
-	result := strings.TrimSpace(value)
+	result := fmt.Sprintf("%s\n", strings.TrimSpace(value))
 	result = strings.ReplaceAll(result, strings.Repeat("\t", 3), "")
 	result = strings.ReplaceAll(result, "|\n", "| \n")
 	return result
@@ -257,7 +258,9 @@ func TestUnparse(t *testing.T) {
 			fs foo() { scratch; } fs bar() { scratch; }
 			`,
 			`
-			fs foo() { scratch; } fs bar() { scratch; }
+			fs foo() { scratch; }
+
+			fs bar() { scratch; }
 			`,
 		},
 		{