Skip to content

Commit 9c39922

Browse files
committed
Adds proper handling of key quoting
1 parent 733e164 commit 9c39922

File tree

7 files changed

+171
-10
lines changed

7 files changed

+171
-10
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@ gron
22
*.tgz
33
*.swp
44
*.exe
5+
cpu.out
6+
gron.test
7+

CHANGELOG.mkd

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## 0.1.6
4+
- Adds proper handling of key quoting using Unicode ranges
5+
- Adds basic benchmarks
6+
- Adds profiling script
7+
38
## 0.1.5
49
- Adds scripted builds for darwin on amd64
510

script/lint

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ if [ $? -ne 0 ]; then
1010
gometalinter --install
1111
fi
1212

13-
gometalinter --disable=gocyclo
13+
gometalinter --disable=gocyclo --disable=dupl

script/profile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/sh
2+
set -e
3+
PROJDIR=$(cd `dirname $0`/.. && pwd)
4+
cd ${PROJDIR}
5+
6+
go test -bench . -benchmem -cpuprofile cpu.out
7+
go tool pprof gron.test cpu.out

statements.go

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package main
33
import (
44
"encoding/json"
55
"fmt"
6-
"regexp"
6+
"unicode"
77
)
88

99
// The javascript reserved words cannot be used as unquoted keys
@@ -159,20 +159,48 @@ func formatValue(s interface{}) string {
159159
// a key with spaces -> true
160160
// 1startsWithANumber -> true
161161
func keyMustBeQuoted(s string) bool {
162-
r := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
163-
if !r.MatchString(s) {
164-
return true
165-
}
166-
167-
// Check the list of reserved words
162+
// Check the list of reserved words first
163+
// to avoid more expensive checks where possible
168164
for _, i := range reservedWords {
169165
if s == i {
170166
return true
171167
}
172168
}
169+
170+
for i, r := range s {
171+
if i == 0 && !validFirstRune(r) {
172+
return true
173+
}
174+
if !validSecondaryRune(r) {
175+
return true
176+
}
177+
}
178+
173179
return false
174180
}
175181

182+
// validFirstRune returns true for runes that are valid
183+
// as the first rune in a key.
184+
// E.g:
185+
// 'r' -> true
186+
// '7' -> false
187+
func validFirstRune(r rune) bool {
188+
return unicode.In(r,
189+
unicode.Lu,
190+
unicode.Ll,
191+
unicode.Lm,
192+
unicode.Lo,
193+
unicode.Nl,
194+
) || r == '$' || r == '_'
195+
}
196+
197+
// validSecondaryRune returns true for runes that are valid
198+
// as anything other than the first rune in a key.
199+
func validSecondaryRune(r rune) bool {
200+
return validFirstRune(r) ||
201+
unicode.In(r, unicode.Mn, unicode.Mc, unicode.Nd, unicode.Pc)
202+
}
203+
176204
// makePrefix takes the previous prefix and the next key and
177205
// returns a new prefix or an error on failure
178206
func makePrefix(prev string, next interface{}) (string, error) {
@@ -181,9 +209,11 @@ func makePrefix(prev string, next interface{}) (string, error) {
181209
return fmt.Sprintf("%s[%d]", prev, v), nil
182210
case string:
183211
if keyMustBeQuoted(v) {
184-
return fmt.Sprintf("%s[%s]", prev, formatValue(v)), nil
212+
// This is a fairly hot code path, and concatination has
213+
// proven to be faster than fmt.Sprintf, despite the allocations
214+
return prev + "[" + formatValue(v) + "]", nil
185215
}
186-
return fmt.Sprintf("%s.%s", prev, v), nil
216+
return prev + "." + v, nil
187217
default:
188218
return "", fmt.Errorf("could not form prefix for %#v", next)
189219
}

statements_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ func TestKeyMustBeQuoted(t *testing.T) {
8787
{"dotted", false},
8888
{"dotted123", false},
8989
{"_under_scores", false},
90+
{"ಠ_ಠ", false},
9091

9192
// Invalid chars
9293
{"is-quoted", true},
@@ -105,3 +106,102 @@ func TestKeyMustBeQuoted(t *testing.T) {
105106
}
106107
}
107108
}
109+
110+
func TestValidFirstRune(t *testing.T) {
111+
tests := []struct {
112+
in rune
113+
want bool
114+
}{
115+
{'r', true},
116+
{'ಠ', true},
117+
{'4', false},
118+
{'-', false},
119+
}
120+
121+
for _, test := range tests {
122+
have := validFirstRune(test.in)
123+
if have != test.want {
124+
t.Errorf("Want %t for validFirstRune(%#U); have %t", test.want, test.in, have)
125+
}
126+
}
127+
}
128+
129+
func TestValidSecondaryRune(t *testing.T) {
130+
tests := []struct {
131+
in rune
132+
want bool
133+
}{
134+
{'r', true},
135+
{'ಠ', true},
136+
{'4', true},
137+
{'-', false},
138+
}
139+
140+
for _, test := range tests {
141+
have := validSecondaryRune(test.in)
142+
if have != test.want {
143+
t.Errorf("Want %t for validSecondaryRune(%#U); have %t", test.want, test.in, have)
144+
}
145+
}
146+
}
147+
148+
func BenchmarkKeyMustBeQuoted(b *testing.B) {
149+
for i := 0; i < b.N; i++ {
150+
keyMustBeQuoted("must-be-quoted")
151+
}
152+
}
153+
154+
func BenchmarkKeyMustBeQuotedUnquoted(b *testing.B) {
155+
for i := 0; i < b.N; i++ {
156+
keyMustBeQuoted("canbeunquoted")
157+
}
158+
}
159+
160+
func BenchmarkKeyMustBeQuotedReserved(b *testing.B) {
161+
for i := 0; i < b.N; i++ {
162+
keyMustBeQuoted("function")
163+
}
164+
}
165+
166+
func BenchmarkMakeStatements(b *testing.B) {
167+
j := []byte(`{
168+
"dotted": "A dotted value",
169+
"a quoted": "value",
170+
"bool1": true,
171+
"bool2": false,
172+
"anull": null,
173+
"anarr": [1, 1.5],
174+
"anob": {
175+
"foo": "bar"
176+
},
177+
"else": 1
178+
}`)
179+
180+
var top interface{}
181+
err := json.Unmarshal(j, &top)
182+
if err != nil {
183+
b.Fatalf("Failed to unmarshal test file: %s", err)
184+
}
185+
186+
for i := 0; i < b.N; i++ {
187+
_, _ = makeStatements("json", top)
188+
}
189+
}
190+
191+
func BenchmarkMakePrefixUnquoted(b *testing.B) {
192+
for i := 0; i < b.N; i++ {
193+
_, _ = makePrefix("json", "isunquoted")
194+
}
195+
}
196+
197+
func BenchmarkMakePrefixQuoted(b *testing.B) {
198+
for i := 0; i < b.N; i++ {
199+
_, _ = makePrefix("json", "this-is-quoted")
200+
}
201+
}
202+
203+
func BenchmarkMakePrefixInt(b *testing.B) {
204+
for i := 0; i < b.N; i++ {
205+
_, _ = makePrefix("json", 212)
206+
}
207+
}

testdata/three.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"one": 1,
3+
"two": 2.2,
4+
"three-b": "3",
5+
"four": [1,2,3,4],
6+
"five": {
7+
"alpha": ["fo", "fum"],
8+
"beta": {
9+
"hey": "How's tricks?"
10+
}
11+
},
12+
"abool": true,
13+
"abool2": false,
14+
"isnull": null,
15+
"ಠ_ಠ": "yarly"
16+
}

0 commit comments

Comments
 (0)