Have dumbindent handle multi-line strings/comments
Fixes #31
diff --git a/cmd/dumbindent/main.go b/cmd/dumbindent/main.go
index fda4103..d01652f 100644
--- a/cmd/dumbindent/main.go
+++ b/cmd/dumbindent/main.go
@@ -44,9 +44,6 @@
// ----
//
// There are no configuration options (e.g. tabs versus spaces).
-//
-// Known bug: it cannot handle /* slash-star comments */ or multi-line strings
-// yet. This is tracked at https://github.com/google/wuffs/issues/31
package main
import (
@@ -145,10 +142,7 @@
return err
}
- dst, err := dumbindent.FormatBytes(nil, src)
- if err != nil {
- return err
- }
+ dst := dumbindent.FormatBytes(nil, src)
if r != nil {
if _, err := os.Stdout.Write(dst); err != nil {
diff --git a/internal/cgen/cgen.go b/internal/cgen/cgen.go
index 1abce1e..2db9c38 100644
--- a/internal/cgen/cgen.go
+++ b/internal/cgen/cgen.go
@@ -182,7 +182,7 @@
return unformatted, nil
}
- return dumbindent.FormatBytes(nil, unformatted)
+ return dumbindent.FormatBytes(nil, unformatted), nil
})
}
diff --git a/lib/dumbindent/dumbindent.go b/lib/dumbindent/dumbindent.go
index fbdd1f9..ad0c811 100644
--- a/lib/dumbindent/dumbindent.go
+++ b/lib/dumbindent/dumbindent.go
@@ -27,20 +27,18 @@
// `dumbindent` was 80 times faster than `clang-format`.
//
// There are no configuration options (e.g. tabs versus spaces).
-//
-// Known bug: it cannot handle /* slash-star comments */ or multi-line strings
-// yet. This is tracked at https://github.com/google/wuffs/issues/31
package dumbindent
import (
"bytes"
- "errors"
)
// 'Constants', but their type is []byte, not string.
var (
- externC = []byte("extern \"C\"")
- spaces = []byte(" ")
+ backTick = []byte("`")
+ externC = []byte("extern \"C\"")
+ spaces = []byte(" ")
+ starSlash = []byte("*/")
)
// hangingBytes is a look-up table for updating the hanging variable.
@@ -55,9 +53,9 @@
// It is valid to pass a dst slice (such as nil) whose spare capacity (not
// including its existing elements) is too short to hold the formatted program.
// In this case, a new slice will be allocated and returned.
-func FormatBytes(dst []byte, src []byte) ([]byte, error) {
+func FormatBytes(dst []byte, src []byte) []byte {
if len(src) == 0 {
- return dst, nil
+ return dst
} else if len(dst) == 0 {
dst = make([]byte, 0, len(src)+(len(src)/2))
}
@@ -69,11 +67,12 @@
blankLine := false // Whether the previous line was blank.
for line, remaining := src, []byte(nil); len(src) > 0; src = remaining {
+ src = trimLeadingWhiteSpace(src)
line, remaining = src, nil
if i := bytes.IndexByte(line, '\n'); i >= 0 {
line, remaining = line[:i], line[i+1:]
}
- line = trimSpace(line)
+ lineLength := len(line)
// Collapse 2 or more consecutive blank lines into 1. Also strip any
// blank lines:
@@ -99,6 +98,7 @@
if (line[0] == '#') ||
((line[0] == 'e') && bytes.HasPrefix(line, externC)) ||
((line[0] == '}') && bytes.HasSuffix(line, externC)) {
+ line = trimTrailingWhiteSpace(line)
dst = append(dst, line...)
dst = append(dst, '\n')
openBrace = false
@@ -150,16 +150,16 @@
indent -= n
}
- // Output the line itself.
- dst = append(dst, line...)
- dst = append(dst, "\n"...)
+ // Output the leading '}'s.
+ dst = append(dst, line[:closeBraces]...)
+ line = line[closeBraces:]
// Adjust the state according to the braces and parentheses within the
// line (except for those in comments and strings).
last := lastNonWhiteSpace(line)
loop:
- for s := line[closeBraces:]; ; {
- for i, c := range s {
+ for {
+ for i, c := range line {
switch c {
case '{':
nBraces++
@@ -169,23 +169,39 @@
nParens++
case ')':
nParens--
+
case '/':
- if (i + 1) >= len(s) {
+ if (i + 1) >= len(line) {
break
}
- if s[i+1] == '/' {
+ if line[i+1] == '/' {
// A slash-slash comment. Skip the rest of the line.
- last = lastNonWhiteSpace(s[:i])
+ last = lastNonWhiteSpace(line[:i])
break loop
- } else if s[i+1] == '*' {
- return nil, errors.New("dumbindent: TODO: support slash-star comments")
+ } else if line[i+1] == '*' {
+ // A slash-star comment.
+ dst = append(dst, line[:i+2]...)
+ restOfLine := line[i+2:]
+ restOfSrc := src[lineLength-len(restOfLine):]
+ dst, line, remaining = handleRaw(dst, restOfSrc, starSlash)
+ last = lastNonWhiteSpace(line)
+ continue loop
}
+
case '"', '\'':
- if suffix, err := skipString(s[i+1:], c); err != nil {
- return nil, err
- } else {
- s = suffix
- }
+ // A cooked string, whose contents are backslash-escaped.
+ suffix := skipCooked(line[i+1:], c)
+ dst = append(dst, line[:len(line)-len(suffix)]...)
+ line = suffix
+ continue loop
+
+ case '`':
+ // A raw string.
+ dst = append(dst, line[:i+1]...)
+ restOfLine := line[i+1:]
+ restOfSrc := src[lineLength-len(restOfLine):]
+ dst, line, remaining = handleRaw(dst, restOfSrc, backTick)
+ last = lastNonWhiteSpace(line)
continue loop
}
}
@@ -193,15 +209,25 @@
}
openBrace = last == '{'
hanging = hangingBytes[last]
+
+ // Output the line (minus any trailing space).
+ line = trimTrailingWhiteSpace(line)
+ dst = append(dst, line...)
+ dst = append(dst, "\n"...)
}
- return dst, nil
+ return dst
}
-// trimSpace converts "\t foo bar " to "foo bar".
-func trimSpace(s []byte) []byte {
+// trimLeadingWhiteSpace converts "\t foo bar " to "foo bar ".
+func trimLeadingWhiteSpace(s []byte) []byte {
for (len(s) > 0) && ((s[0] == ' ') || (s[0] == '\t')) {
s = s[1:]
}
+ return s
+}
+
+// trimTrailingWhiteSpace converts "\t foo bar " to "\t foo bar".
+func trimTrailingWhiteSpace(s []byte) []byte {
for (len(s) > 0) && ((s[len(s)-1] == ' ') || (s[len(s)-1] == '\t')) {
s = s[:len(s)-1]
}
@@ -241,11 +267,11 @@
return 0
}
-// skipString converts `ijk \" lmn" pqr` to ` pqr`.
-func skipString(s []byte, quote byte) (suffix []byte, retErr error) {
+// skipCooked converts `ijk \" lmn" pqr` to ` pqr`.
+func skipCooked(s []byte, quote byte) (suffix []byte) {
for i := 0; i < len(s); {
if x := s[i]; x == quote {
- return s[i+1:], nil
+ return s[i+1:]
} else if x != '\\' {
i += 1
} else if (i + 1) < len(s) {
@@ -254,5 +280,22 @@
break
}
}
- return nil, errors.New("dumbindent: TODO: support multi-line strings")
+ return nil
+}
+
+// handleRaw copies a raw string from restOfSrc to dst, re-calculating the
+// (line, remaining) pair afterwards.
+func handleRaw(dst []byte, restOfSrc []byte, endQuote []byte) (retDst []byte, line []byte, remaining []byte) {
+ end := bytes.Index(restOfSrc, endQuote)
+ if end < 0 {
+ end = len(restOfSrc)
+ } else {
+ end += len(endQuote)
+ }
+ dst = append(dst, restOfSrc[:end]...)
+ line, remaining = restOfSrc[end:], nil
+ if i := bytes.IndexByte(line, '\n'); i >= 0 {
+ line, remaining = line[:i], line[i+1:]
+ }
+ return dst, line, remaining
}
diff --git a/lib/dumbindent/dumbindent_test.go b/lib/dumbindent/dumbindent_test.go
index d788602..3b3bef7 100644
--- a/lib/dumbindent/dumbindent_test.go
+++ b/lib/dumbindent/dumbindent_test.go
@@ -23,6 +23,10 @@
src string
want string
}{{
+ // Leading and trailing space.
+ src: "\t\tx y \n",
+ want: "x y\n",
+ }, {
// Braces.
src: "foo{\nbar\n }\nbaz\n",
want: "foo{\n bar\n}\nbaz\n",
@@ -51,15 +55,21 @@
src: "a = \"{\"\nb = {\nc = 0\n",
want: "a = \"{\"\nb = {\n c = 0\n",
}, {
+ // Back-tick string.
+ src: "a['key'] = `{\n\n\nX` ; \nb = {\nc = 0\n",
+ want: "a['key'] = `{\n\n\nX` ;\nb = {\n c = 0\n",
+ }, {
+ // Slash-star comment.
+ src: " a['key'] = /*{\n\n\nX*/ ; \nb = {\nc = 0\n",
+ want: "a['key'] = /*{\n\n\nX*/ ;\nb = {\n c = 0\n",
+ }, {
// Label.
src: "if (b) {\nlabel:\nswitch (i) {\ncase 0:\nj = k\nbreak;\n}\n}\n",
want: "if (b) {\nlabel:\n switch (i) {\n case 0:\n j = k\n break;\n }\n}\n",
}}
for i, tc := range testCases {
- if g, err := FormatBytes(nil, []byte(tc.src)); err != nil {
- tt.Fatalf("i=%d, src=%q: %v", i, tc.src, err)
- } else if got := string(g); got != tc.want {
+ if got := string(FormatBytes(nil, []byte(tc.src))); got != tc.want {
tt.Fatalf("i=%d, src=%q:\ngot %q\nwant %q", i, tc.src, got, tc.want)
}
}