From 5e4dc87ffec7f87bbf3ebfcf256777ad773e8450 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sat, 14 Mar 2020 16:47:38 +0100 Subject: cache: replace the all-in-one query parser by a complete one with AST/lexer/parser --- cache/filter.go | 47 +++++++++---- cache/query.go | 172 ----------------------------------------------- cache/query_test.go | 41 ----------- cache/repo_cache.go | 20 ++++-- cache/repo_cache_test.go | 5 +- cache/sorting.go | 18 ----- 6 files changed, 52 insertions(+), 251 deletions(-) delete mode 100644 cache/query.go delete mode 100644 cache/query_test.go delete mode 100644 cache/sorting.go (limited to 'cache') diff --git a/cache/filter.go b/cache/filter.go index ebe774ac..021962a7 100644 --- a/cache/filter.go +++ b/cache/filter.go @@ -5,6 +5,7 @@ import ( "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/query/ast" ) // resolver has the resolving functions needed by filters. @@ -17,15 +18,10 @@ type resolver interface { type Filter func(excerpt *BugExcerpt, resolver resolver) bool // StatusFilter return a Filter that match a bug status -func StatusFilter(query string) (Filter, error) { - status, err := bug.StatusFromString(query) - if err != nil { - return nil, err - } - +func StatusFilter(status bug.Status) Filter { return func(excerpt *BugExcerpt, resolver resolver) bool { return excerpt.Status == status - }, nil + } } // AuthorFilter return a Filter that match a bug author @@ -116,8 +112,8 @@ func NoLabelFilter() Filter { } } -// Filters is a collection of Filter that implement a complex filter -type Filters struct { +// Matcher is a collection of Filter that implement a complex filter +type Matcher struct { Status []Filter Author []Filter Actor []Filter @@ -127,8 +123,35 @@ type Filters struct { NoFilters []Filter } +// compileMatcher transform an ast.Filters into a specialized matcher +// for the cache. +func compileMatcher(filters ast.Filters) *Matcher { + result := &Matcher{} + + for _, value := range filters.Status { + result.Status = append(result.Status, StatusFilter(value)) + } + for _, value := range filters.Author { + result.Author = append(result.Author, AuthorFilter(value)) + } + for _, value := range filters.Actor { + result.Actor = append(result.Actor, ActorFilter(value)) + } + for _, value := range filters.Participant { + result.Participant = append(result.Participant, ParticipantFilter(value)) + } + for _, value := range filters.Label { + result.Label = append(result.Label, LabelFilter(value)) + } + for _, value := range filters.Title { + result.Title = append(result.Title, TitleFilter(value)) + } + + return result +} + // Match check if a bug match the set of filters -func (f *Filters) Match(excerpt *BugExcerpt, resolver resolver) bool { +func (f *Matcher) Match(excerpt *BugExcerpt, resolver resolver) bool { if match := f.orMatch(f.Status, excerpt, resolver); !match { return false } @@ -161,7 +184,7 @@ func (f *Filters) Match(excerpt *BugExcerpt, resolver resolver) bool { } // Check if any of the filters provided match the bug -func (*Filters) orMatch(filters []Filter, excerpt *BugExcerpt, resolver resolver) bool { +func (*Matcher) orMatch(filters []Filter, excerpt *BugExcerpt, resolver resolver) bool { if len(filters) == 0 { return true } @@ -175,7 +198,7 @@ func (*Filters) orMatch(filters []Filter, excerpt *BugExcerpt, resolver resolver } // Check if all of the filters provided match the bug -func (*Filters) andMatch(filters []Filter, excerpt *BugExcerpt, resolver resolver) bool { +func (*Matcher) andMatch(filters []Filter, excerpt *BugExcerpt, resolver resolver) bool { if len(filters) == 0 { return true } diff --git a/cache/query.go b/cache/query.go deleted file mode 100644 index 967c18d6..00000000 --- a/cache/query.go +++ /dev/null @@ -1,172 +0,0 @@ -package cache - -import ( - "fmt" - "strings" - "unicode" -) - -type Query struct { - Filters - OrderBy - OrderDirection -} - -// Return an identity query with default sorting (creation-desc) -func NewQuery() *Query { - return &Query{ - OrderBy: OrderByCreation, - OrderDirection: OrderDescending, - } -} - -// ParseQuery parse a query DSL -// -// Ex: "status:open author:descartes sort:edit-asc" -// -// Supported filter qualifiers and syntax are described in docs/queries.md -func ParseQuery(query string) (*Query, error) { - fields := splitQuery(query) - - result := &Query{ - OrderBy: OrderByCreation, - OrderDirection: OrderDescending, - } - - sortingDone := false - - for _, field := range fields { - split := strings.Split(field, ":") - if len(split) != 2 { - return nil, fmt.Errorf("can't parse \"%s\"", field) - } - - qualifierName := split[0] - qualifierQuery := removeQuote(split[1]) - - switch qualifierName { - case "status", "state": - f, err := StatusFilter(qualifierQuery) - if err != nil { - return nil, err - } - result.Status = append(result.Status, f) - - case "author": - f := AuthorFilter(qualifierQuery) - result.Author = append(result.Author, f) - - case "actor": - f := ActorFilter(qualifierQuery) - result.Actor = append(result.Actor, f) - - case "participant": - f := ParticipantFilter(qualifierQuery) - result.Participant = append(result.Participant, f) - - case "label": - f := LabelFilter(qualifierQuery) - result.Label = append(result.Label, f) - - case "title": - f := TitleFilter(qualifierQuery) - result.Title = append(result.Title, f) - - case "no": - err := result.parseNoFilter(qualifierQuery) - if err != nil { - return nil, err - } - - case "sort": - if sortingDone { - return nil, fmt.Errorf("multiple sorting") - } - - err := result.parseSorting(qualifierQuery) - if err != nil { - return nil, err - } - - sortingDone = true - - default: - return nil, fmt.Errorf("unknown qualifier name %s", qualifierName) - } - } - - return result, nil -} - -func splitQuery(query string) []string { - lastQuote := rune(0) - f := func(c rune) bool { - switch { - case c == lastQuote: - lastQuote = rune(0) - return false - case lastQuote != rune(0): - return false - case unicode.In(c, unicode.Quotation_Mark): - lastQuote = c - return false - default: - return unicode.IsSpace(c) - } - } - - return strings.FieldsFunc(query, f) -} - -func removeQuote(field string) string { - if len(field) >= 2 { - if field[0] == '"' && field[len(field)-1] == '"' { - return field[1 : len(field)-1] - } - } - return field -} - -func (q *Query) parseNoFilter(query string) error { - switch query { - case "label": - q.NoFilters = append(q.NoFilters, NoLabelFilter()) - default: - return fmt.Errorf("unknown \"no\" filter %s", query) - } - - return nil -} - -func (q *Query) parseSorting(query string) error { - switch query { - // default ASC - case "id-desc": - q.OrderBy = OrderById - q.OrderDirection = OrderDescending - case "id", "id-asc": - q.OrderBy = OrderById - q.OrderDirection = OrderAscending - - // default DESC - case "creation", "creation-desc": - q.OrderBy = OrderByCreation - q.OrderDirection = OrderDescending - case "creation-asc": - q.OrderBy = OrderByCreation - q.OrderDirection = OrderAscending - - // default DESC - case "edit", "edit-desc": - q.OrderBy = OrderByEdit - q.OrderDirection = OrderDescending - case "edit-asc": - q.OrderBy = OrderByEdit - q.OrderDirection = OrderAscending - - default: - return fmt.Errorf("unknown sorting %s", query) - } - - return nil -} diff --git a/cache/query_test.go b/cache/query_test.go deleted file mode 100644 index 9ae62ac4..00000000 --- a/cache/query_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package cache - -import "testing" - -func TestQueryParse(t *testing.T) { - - var tests = []struct { - input string - ok bool - }{ - {"gibberish", false}, - - {"status:", false}, - - {"status:open", true}, - {"status:closed", true}, - {"status:unknown", false}, - - {"author:rene", true}, - {`author:"René Descartes"`, true}, - - {"actor:bernhard", true}, - {"participant:leonhard", true}, - - {"label:hello", true}, - {`label:"Good first issue"`, true}, - - {"title:titleOne", true}, - {`title:"Bug titleTwo"`, true}, - - {"sort:edit", true}, - {"sort:unknown", false}, - } - - for _, test := range tests { - _, err := ParseQuery(test.input) - if (err == nil) != test.ok { - t.Fatalf("Unexpected parse result, expected: %v, err: %v", test.ok, err) - } - } -} diff --git a/cache/repo_cache.go b/cache/repo_cache.go index d859a966..6546b15e 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -18,6 +18,7 @@ import ( "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/query/ast" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/git" "github.com/MichaelMure/git-bug/util/process" @@ -525,7 +526,7 @@ func (c *RepoCache) resolveBugMatcher(f func(*BugExcerpt) bool) (entity.Id, erro } // QueryBugs return the id of all Bug matching the given Query -func (c *RepoCache) QueryBugs(query *Query) []entity.Id { +func (c *RepoCache) QueryBugs(query *ast.Query) []entity.Id { c.muBug.RLock() defer c.muBug.RUnlock() @@ -533,10 +534,12 @@ func (c *RepoCache) QueryBugs(query *Query) []entity.Id { return c.AllBugsIds() } + matcher := compileMatcher(query.Filters) + var filtered []*BugExcerpt for _, excerpt := range c.bugExcerpts { - if query.Match(excerpt, c) { + if matcher.Match(excerpt, c) { filtered = append(filtered, excerpt) } } @@ -544,18 +547,23 @@ func (c *RepoCache) QueryBugs(query *Query) []entity.Id { var sorter sort.Interface switch query.OrderBy { - case OrderById: + case ast.OrderById: sorter = BugsById(filtered) - case OrderByCreation: + case ast.OrderByCreation: sorter = BugsByCreationTime(filtered) - case OrderByEdit: + case ast.OrderByEdit: sorter = BugsByEditTime(filtered) default: panic("missing sort type") } - if query.OrderDirection == OrderDescending { + switch query.OrderDirection { + case ast.OrderAscending: + // Nothing to do + case ast.OrderDescending: sorter = sort.Reverse(sorter) + default: + panic("missing sort direction") } sort.Sort(sorter) diff --git a/cache/repo_cache_test.go b/cache/repo_cache_test.go index c3bd3cc4..51393dfd 100644 --- a/cache/repo_cache_test.go +++ b/cache/repo_cache_test.go @@ -5,6 +5,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/MichaelMure/git-bug/query" "github.com/MichaelMure/git-bug/repository" ) @@ -68,9 +69,9 @@ func TestCache(t *testing.T) { require.NoError(t, err) // Querying - query, err := ParseQuery("status:open author:descartes sort:edit-asc") + q, err := query.Parse("status:open author:descartes sort:edit-asc") require.NoError(t, err) - require.Len(t, cache.QueryBugs(query), 2) + require.Len(t, cache.QueryBugs(q), 2) // Close require.NoError(t, cache.Close()) diff --git a/cache/sorting.go b/cache/sorting.go deleted file mode 100644 index 19034a9d..00000000 --- a/cache/sorting.go +++ /dev/null @@ -1,18 +0,0 @@ -package cache - -type OrderBy int - -const ( - _ OrderBy = iota - OrderById - OrderByCreation - OrderByEdit -) - -type OrderDirection int - -const ( - _ OrderDirection = iota - OrderAscending - OrderDescending -) -- cgit