From 624dcd15ce959dbb288e2bfc90561b8e7a3886cc Mon Sep 17 00:00:00 2001 From: Steven Kalt Date: Sun, 26 Apr 2020 23:16:23 -0400 Subject: [PATCH] feat(parsing): build a minimal commit-type parser Using parser combinators and names similar to https://github.com/Geal/nom --- go.mod | 3 + initial_test.go | 90 +++++++++++++++++++++++++++ main.go | 159 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 252 insertions(+) create mode 100644 go.mod create mode 100644 initial_test.go create mode 100644 main.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..90046e1 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/skalt/git-cc + +go 1.14 diff --git a/initial_test.go b/initial_test.go new file mode 100644 index 0000000..933ea01 --- /dev/null +++ b/initial_test.go @@ -0,0 +1,90 @@ +package main + +import ( + "fmt" + "testing" +) + +func TestParsingCommitTypes(t *testing.T) { + var testExpectedMatch = func( + input string, + expected string, + remainder string, + ) func(t *testing.T) { + result, err := CommitType([]rune(input)) + return func(t *testing.T) { + if err != nil { + fmt.Printf("%v", err) + t.Fail() + return + } + if result.Output != expected { + fmt.Printf( + "expected output %v, not %v\n", expected, result.Output, + ) + t.Fail() + } + if string(result.Remaining) != remainder { + fmt.Printf( + "expected remainder %v, not %v\n", expected, result.Output, + ) + t.Fail() + } + } + } + + t.Run( + "accepts valid commit types [fix]", + testExpectedMatch("fix", "fix", ""), + ) + + t.Run( + "accepts valid commit types [feat]", + testExpectedMatch("feat", "feat", ""), + ) + t.Run( + "watch out: only matches the first runes", + testExpectedMatch("fixing", "fix", "ing"), + ) + t.Run("rejects invalid commit types", func(t *testing.T) { + _, err := CommitType([]rune("foo")) + if err == nil { + t.Fail() + } + }) +} + +func TestTakeUntil(t *testing.T) { + var callback = func(chars []rune) interface{} { + return string(chars) + } + var test = func(input string, until rune, output string, remaining string) func(t *testing.T) { + return func(t *testing.T) { + result, err := TakeUntil(until, callback)([]rune(input)) + if err != nil { + fmt.Println("err != nil") + t.Fail() + } + if result.Output != output { + fmt.Printf("unexpected output %v (should be %v)\n", result.Output, output) + t.Fail() + } + if string(result.Remaining) != remaining { + fmt.Printf("unexpected remaining \"%v\" (should be %v)\n", string(result.Remaining), remaining) + t.Fail() + } + } + } + t.Run( + "matching in the middle of the input works", + test("abcdef", 'c' /* output: */, "ab" /* remaining: */, "cdef"), + ) + t.Run( + "matching at the start of the input works", + test("abcdef", 'a' /* output: */, "" /* remaining: */, "abcdef"), + ) + t.Run( + "matching at the end of the input works", + test("abcdef", 'f' /* output: */, "abcde" /* remaining: */, "f"), + ) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..f07dfc3 --- /dev/null +++ b/main.go @@ -0,0 +1,159 @@ +package main + +import ( + "fmt" +) + +// see https://medium.com/@armin.heller/using-parser-combinators-in-go-e63b3ad69c94 +// and https://github.com/Geal/nom + +type ParserResult struct { + Output interface{} + Remaining []rune +} +type ParserCallback func([]rune) interface{} +type Parser func([]rune) (ParserResult, error) + +func ident(input []rune) interface{} { + return []rune(input) +} +func TakeUntil(character rune, callback ParserCallback) Parser { + return func(input []rune) (ParserResult, error) { + for i, nextChar := range input { + if nextChar == character { + output := callback(input[:i]) + return ParserResult{output, input[i:]}, nil + } + } + return ParserResult{nil, input}, fmt.Errorf("didn't match '%v'", character) + } +} + +func Opt(parser Parser) Parser { + return func(input []rune) (ParserResult, error) { + result, _ := parser(input) + return result, nil + } +} + +func LiteralRune(match rune, callback ParserCallback) Parser { + return func(input []rune) (ParserResult, error) { + if len(input) > 0 { + if input[0] == match { + return ParserResult{callback([]rune{match}), input[1:]}, nil + } else { + return ParserResult{nil, input}, fmt.Errorf("%v not matched", match) + } + } else { + return ParserResult{nil, input}, fmt.Errorf("no input") + } + } +} + +func NotMatching(parser Parser) Parser { + return func(input []rune) (ParserResult, error) { + result, err := parser(input) + if err == nil { + return result, nil + } else { + return ParserResult{nil, input}, fmt.Errorf("wasn't expecting to match %v", parser) + } + } +} + +func toRunes(i interface{}) []rune { + switch i.(type) { + case string: + str, _ := i.(string) + return []rune(str) + case rune: + r, _ := i.(rune) + return []rune{r} + case []rune: + runes, _ := i.([]rune) + return runes + default: + panic(fmt.Errorf("%v is not string or []rune", i)) + } +} + +func Tag(tag interface{}) Parser { + toMatch := toRunes(tag) + return func(input []rune) (ParserResult, error) { + if len(toMatch) > len(input) { + return ParserResult{nil, input}, fmt.Errorf("input longer than tag") + } + for i, matching := range toMatch { + if input[i] != matching { + err := fmt.Errorf( + "\"%v\" does not match \"%v\"", + string(input[:i+1]), + string(toMatch), + ) + return ParserResult{nil, input}, err + } + } + return ParserResult{string(toMatch), input[len(toMatch):]}, nil + } +} + +// func MatchingString(str string, callback ParserCallback) Parser { +// var toMatch = []rune(str) + +// } + +func AnyOf(parsers ...Parser) Parser { + return func(input []rune) (ParserResult, error) { + for _, parser := range parsers { + result, err := parser(input) + if err == nil { + return result, err + } + } + return ParserResult{nil, input}, fmt.Errorf("expected a parser to match") + } +} + +func asString(result []rune) interface{} { + return string(result) +} + +func Sequence(parsers ...Parser) Parser { + return func(input []rune) (ParserResult, error) { + var currentInput = []rune{} + results := make([]interface{}, len(currentInput)) + copy(currentInput, input) + for _, parser := range parsers { + result, err := parser(currentInput) + if err != nil { + return ParserResult{nil, input}, err + } else { + currentInput = result.Remaining + results = append(results, result.Output) + } + } + return ParserResult{results, currentInput}, nil + } +} + +func Delimeted(start Parser, middle Parser, end Parser) Parser { + return func(input []rune) (ParserResult, error) { + result, err := Sequence(start, middle, end)(input) + if err != nil { + return ParserResult{nil, input}, err + } + results, _ := result.Output.([]string) + return ParserResult{results[1], result.Remaining}, nil + } +} + +var CommitType = AnyOf(Tag("feat"), Tag("fix")) +var Scope = Delimeted( + Tag('('), + NotMatching(Tag(')')), + Tag(')'), +) +var BreakingChangeBang = Tag("!") +var CommitTypeContext = Sequence(CommitType, Opt(Scope), Opt(BreakingChangeBang)) + +func main() {}