From f6bd122f4b1bb8622a0bb6f22ae2c4219402a636 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Mon, 15 May 2017 19:37:20 -0700 Subject: [PATCH] json: parsing of keywords --- zcl/json/didyoumean.go | 20 ++++++++++ zcl/json/didyoumean_test.go | 49 +++++++++++++++++++++++ zcl/json/parser.go | 45 ++++++++++++++++++++- zcl/json/parser_test.go | 80 +++++++++++++++++++++++++++++++++++++ 4 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 zcl/json/didyoumean.go create mode 100644 zcl/json/didyoumean_test.go create mode 100644 zcl/json/parser_test.go diff --git a/zcl/json/didyoumean.go b/zcl/json/didyoumean.go new file mode 100644 index 0000000..1016026 --- /dev/null +++ b/zcl/json/didyoumean.go @@ -0,0 +1,20 @@ +package json + +import ( + "github.com/agext/levenshtein" +) + +var keywords = []string{"false", "true", "null"} + +// keywordSuggestion tries to find a valid JSON keyword that is close to the +// given string and returns it if found. If no keyword is close enough, returns +// the empty string. +func keywordSuggestion(given string) string { + for _, kw := range keywords { + dist := levenshtein.Distance(given, kw, nil) + if dist < 3 { // threshold determined experimentally + return kw + } + } + return "" +} diff --git a/zcl/json/didyoumean_test.go b/zcl/json/didyoumean_test.go new file mode 100644 index 0000000..999876a --- /dev/null +++ b/zcl/json/didyoumean_test.go @@ -0,0 +1,49 @@ +package json + +import "testing" + +func TestKeywordSuggestion(t *testing.T) { + tests := []struct { + Input, Want string + }{ + {"true", "true"}, + {"false", "false"}, + {"null", "null"}, + {"bananas", ""}, + {"NaN", ""}, + {"Inf", ""}, + {"Infinity", ""}, + {"void", ""}, + {"undefined", ""}, + + {"ture", "true"}, + {"tru", "true"}, + {"tre", "true"}, + {"treu", "true"}, + {"rtue", "true"}, + + {"flase", "false"}, + {"fales", "false"}, + {"flse", "false"}, + {"fasle", "false"}, + {"fasel", "false"}, + {"flue", "false"}, + + {"nil", "null"}, + {"nul", "null"}, + {"unll", "null"}, + {"nll", "null"}, + } + + for _, test := range tests { + t.Run(test.Input, func(t *testing.T) { + got := keywordSuggestion(test.Input) + if got != test.Want { + t.Errorf( + "wrong result\ninput: %q\ngot: %q\nwant: %q", + test.Input, got, test.Want, + ) + } + }) + } +} diff --git a/zcl/json/parser.go b/zcl/json/parser.go index 4736a7d..5d7a69f 100644 --- a/zcl/json/parser.go +++ b/zcl/json/parser.go @@ -1,6 +1,8 @@ package json import ( + "fmt" + "github.com/apparentlymart/go-zcl/zcl" ) @@ -180,5 +182,46 @@ func parseString(p *peeker) (node, zcl.Diagnostics) { } func parseKeyword(p *peeker) (node, zcl.Diagnostics) { - return nil, nil + tok := p.Read() + s := string(tok.Bytes) + + switch s { + case "true": + return &booleanVal{ + Value: true, + SrcRange: tok.Range, + }, nil + case "false": + return &booleanVal{ + Value: false, + SrcRange: tok.Range, + }, nil + case "null": + return &nullVal{ + SrcRange: tok.Range, + }, nil + case "undefined", "NaN", "Infinity": + return nil, zcl.Diagnostics{ + { + Severity: zcl.DiagError, + Summary: "Invalid JSON keyword", + Detail: fmt.Sprintf("The JavaScript identifier %q cannot be used in JSON.", s), + Subject: &tok.Range, + }, + } + default: + var dym string + if suggest := keywordSuggestion(s); suggest != "" { + dym = fmt.Sprintf(" Did you mean %q?", suggest) + } + + return nil, zcl.Diagnostics{ + { + Severity: zcl.DiagError, + Summary: "Invalid JSON keyword", + Detail: fmt.Sprintf("%q is not a valid JSON keyword.%s", s, dym), + Subject: &tok.Range, + }, + } + } } diff --git a/zcl/json/parser_test.go b/zcl/json/parser_test.go new file mode 100644 index 0000000..44311ee --- /dev/null +++ b/zcl/json/parser_test.go @@ -0,0 +1,80 @@ +package json + +import ( + "reflect" + "testing" + + "github.com/apparentlymart/go-zcl/zcl" + "github.com/davecgh/go-spew/spew" +) + +func TestParse(t *testing.T) { + tests := []struct { + Input string + Want node + DiagCount int + }{ + { + `true`, + &booleanVal{ + Value: true, + SrcRange: zcl.Range{ + Filename: "", + Start: zcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: zcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + }, + 0, + }, + { + `false`, + &booleanVal{ + Value: false, + SrcRange: zcl.Range{ + Filename: "", + Start: zcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: zcl.Pos{Line: 1, Column: 6, Byte: 5}, + }, + }, + 0, + }, + { + `null`, + &nullVal{ + SrcRange: zcl.Range{ + Filename: "", + Start: zcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: zcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + }, + 0, + }, + { + `undefined`, + nil, + 1, + }, + { + `flase`, + nil, + 1, + }, + } + + for _, test := range tests { + t.Run(test.Input, func(t *testing.T) { + got, diag := parseFileContent([]byte(test.Input), "") + + if len(diag) != test.DiagCount { + t.Errorf("got %d diagnostics; want %d\n%s", len(diag), test.DiagCount, spew.Sdump(diag)) + } + + if !reflect.DeepEqual(got, test.Want) { + t.Errorf( + "wrong result\ninput: %s\ngot: %#v\nwant: %#v", + test.Input, got, test.Want, + ) + } + }) + } +}