Skip to content
This repository has been archived by the owner on Sep 9, 2020. It is now read-only.

Commit

Permalink
Merge pull request #149 from shomron/quotes
Browse files Browse the repository at this point in the history
Rework stringQuote to behave closer to `jsonnet fmt`
  • Loading branch information
shomron authored Sep 17, 2018
2 parents 83f20ee + 3e7bd46 commit ed0796f
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 15 deletions.
93 changes: 78 additions & 15 deletions ksonnet-gen/printer/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"regexp"
"strconv"
"strings"
"unicode/utf8"

"github.com/google/go-jsonnet/ast"
"github.com/google/go-jsonnet/parser"
Expand Down Expand Up @@ -176,29 +177,91 @@ func detectQuoteMode(s string, kind ast.LiteralStringKind) quoteMode {
return quoteModeNone
}

// stringQuote returns a quoted jsonnet string ready for serialization.
// useSingle specifies whether to use single quotes, otherwise double-quotes
// will be used instead.
// Note the following characters will be escaped (with leading backslash): "'\/bfnrt
// Quotes (single or double) will only be escaped to avoid conflict with the enclosing quote type.
func stringQuote(s string, useSingle bool) string {
quoted := strconv.Quote(s)
func unquote(s string) string {
if !strings.ContainsRune(s, '\\') {
return s
}

if !useSingle {
return quoted
sb := strings.Builder{}
sb.Grow(len(s))

tail := s
for len(tail) > 0 {
c, width := utf8.DecodeRuneInString(tail)

switch c {
case utf8.RuneError:
tail = tail[width:]
continue // Skip this character
case '"':
// strconv.UnquoteChar won't allow a bare quote in double-quote mode, but we will
sb.WriteRune(c)
tail = tail[width:]
default:
c, _, t2, err := strconv.UnquoteChar(tail, byte('"'))
if err != nil {
// Skip character. Ensure we move forward.
tail = tail[width:]
continue
}
tail = t2
sb.WriteRune(c)
}
}

return sb.String()
}

// quote returns a single or double quoted string, escaped for jsonnet.
// This function does *not* protect against double-escaping. Instead use `stringQuote`.
func quote(s string, useSingle bool) string {
var quote rune
if useSingle {
quote = '\''
} else {
quote = '"'
}

// Convert to single quotes
sb := strings.Builder{}
sb.WriteByte(singleQuote)
flipped := strings.Replace(quoted[1:len(quoted)-1], "'", "\\'", -1)
flipped = strings.Replace(flipped, "\\\"", "\"", -1) // TODO use Replacer
sb.WriteString(flipped)
sb.WriteByte(singleQuote)
sb.Grow(len(s) + 2)

sb.WriteRune(quote)

for _, c := range s {
switch c {
case '\'':
if useSingle {
sb.WriteString("\\'")
} else {
sb.WriteRune(c)
}
case '"':
if !useSingle {
sb.WriteString("\\\"")
} else {
sb.WriteRune(c)
}
default:
q := strconv.QuoteRune(c) // This is returned with unneeded quotes
sb.WriteString(q[1 : len(q)-1])
}
}

sb.WriteRune(quote)
return sb.String()
}

// stringQuote returns a quoted jsonnet string ready for serialization.
// Appropriate measures are taken to avoid double-escaping any control characters.
// `useSingle` specifies whether to use single quotes, otherwise double-quotes are used.
// Note the following characters will be escaped (with leading backslash): "'\/bfnrt
// Quotes (single or double) will only be escaped to avoid conflict with the enclosing quote type.
func stringQuote(s string, useSingle bool) string {
unquoted := unquote(s) // Avoid double-escaping control characters

return quote(unquoted, useSingle)
}

// printer prints a node.
// nolint: gocyclo
func (p *printer) print(n interface{}) {
Expand Down
45 changes: 45 additions & 0 deletions ksonnet-gen/printer/printer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/google/go-jsonnet/ast"
"github.com/google/go-jsonnet/parser"
"github.com/ksonnet/ksonnet-lib/ksonnet-gen/astext"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -1205,6 +1206,50 @@ func Test_handleObjectField_unknown_object(t *testing.T) {
require.Error(t, p.err)
}

func Test_quoteString(t *testing.T) {
tests := []struct {
s string
expected string
useSingle bool
}{
{
s: "\\tFoo\tBar",
expected: `'\tFoo\tBar'`,
useSingle: true,
},
{
s: "\\tFoo\tBar",
expected: `"\tFoo\tBar"`,
useSingle: false,
},
{
s: "Foo\n\u000aBar",
expected: `'Foo\n\nBar'`,
useSingle: true,
},
{
s: "Foo\n\\u000a\rBar",
expected: `'Foo\n\n\rBar'`,
useSingle: true,
},
{
s: "'Foo'\\n\"Bar\\\"",
expected: `'\'Foo\'\n"Bar"'`,
useSingle: true,
},
{
s: "'Foo'\\n\"Bar\\\"",
expected: `"'Foo'\n\"Bar\""`,
useSingle: false,
},
}

for _, tc := range tests {
actual := stringQuote(tc.s, tc.useSingle)
assert.Equal(t, tc.expected, actual)
}
}

func newLiteralNumber(in string) *ast.LiteralNumber {
f, err := strconv.ParseFloat(in, 64)
if err != nil {
Expand Down

0 comments on commit ed0796f

Please sign in to comment.