diff --git a/ast/ast.go b/ast/ast.go index 869cfc4..9e6fbd3 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -1,11 +1,5 @@ package ast -import ( - "strings" - - "github.com/laojianzi/kql-go/token" -) - // Expr represents an expression in KQL. type Expr interface { // Pos returns the position of the expression. @@ -17,192 +11,3 @@ type Expr interface { // String returns the string representation of the expression. String() string } - -// ParenExpr is a parenthesis expression. -// -// Example: -// -// `(f1: "v1" AND num: 1)` -type ParenExpr struct { - L, R int // left and right position of the parenthesis - Expr Expr -} - -// NewParenExpr creates a new parenthesis expression. -func NewParenExpr(L, R int, expr Expr) *ParenExpr { - return &ParenExpr{ - L: L, - R: R, - Expr: expr, - } -} - -// Pos returns the position of the parenthesis expression. -func (e *ParenExpr) Pos() int { - return e.L -} - -// End returns the end position of the parenthesis expression. -func (e *ParenExpr) End() int { - return e.R -} - -// String returns the string representation of the parenthesis expression. -func (e *ParenExpr) String() string { - var buf strings.Builder - - buf.WriteByte('(') - buf.WriteString(e.Expr.String()) - buf.WriteByte(')') - - return buf.String() -} - -// CombineExpr is a combination expression. -// -// Example: -// -// `f1: "v1" AND num: 1` -type CombineExpr struct { - LeftExpr Expr - Keyword token.Kind - RightExpr Expr -} - -// NewCombineExpr creates a new combination expression. -func NewCombineExpr(leftExpr Expr, keyword token.Kind, rightExpr Expr) *CombineExpr { - return &CombineExpr{ - LeftExpr: leftExpr, - Keyword: keyword, - RightExpr: rightExpr, - } -} - -// Pos returns the position of the combination expression. -func (e *CombineExpr) Pos() int { - return e.LeftExpr.Pos() -} - -// End returns the end position of the combination expression. -func (e *CombineExpr) End() int { - return e.RightExpr.End() -} - -// String returns the string representation of the combination expression. -func (e *CombineExpr) String() string { - var buf strings.Builder - if e.LeftExpr != nil { - buf.WriteString(e.LeftExpr.String()) - } - - if e.RightExpr != nil { - buf.WriteByte(' ') - buf.WriteString(e.Keyword.String()) - buf.WriteByte(' ') - buf.WriteString(e.RightExpr.String()) - } - - return buf.String() -} - -// BinaryExpr is a binary expression. -// -// Example: -// -// `NOT f1: "v1"` -type BinaryExpr struct { - pos int - Field string - Operator token.Kind - Value Expr - HasNot bool -} - -// NewBinaryExpr creates a new binary expression. -func NewBinaryExpr(pos int, field string, operator token.Kind, value Expr, hasNot bool) *BinaryExpr { - return &BinaryExpr{ - pos: pos, - Field: field, - Operator: operator, - Value: value, - HasNot: hasNot, - } -} - -// Pos returns the position of the binary expression. -func (e *BinaryExpr) Pos() int { - return e.pos -} - -// End returns the end position of the binary expression. -func (e *BinaryExpr) End() int { - if e.Value.End() < len(e.Value.String()) { // e.g. string values with double quotes - return e.Value.End() + 1 - } - - return e.Value.End() -} - -// String returns the string representation of the binary expression. -func (e *BinaryExpr) String() string { - var buf strings.Builder - if e.HasNot { - buf.WriteString("NOT ") - } - - if e.Field != "" { - buf.WriteString(e.Field) - - if e.Operator != token.TokenKindOperatorEql { - buf.WriteByte(' ') - } - - buf.WriteString(e.Operator.String()) - buf.WriteByte(' ') - } - - if e.Value != nil { - buf.WriteString(e.Value.String()) - } - - return buf.String() -} - -// Literal is a literal(int, float, string or identifier) value. -type Literal struct { - pos int - end int - Kind token.Kind // int, float, string or identifier - Value string - WithDoubleQuote bool -} - -// NewLiteral creates a new literal value. -func NewLiteral(pos, end int, kind token.Kind, value string) *Literal { - return &Literal{ - pos: pos, - end: end, - Kind: kind, - Value: value, - WithDoubleQuote: kind == token.TokenKindString, - } -} - -// Pos returns the position of the literal value. -func (e *Literal) Pos() int { - return e.pos -} - -// End returns the end position of the literal value. -func (e *Literal) End() int { - return e.end -} - -// String returns the string representation of the literal value. -func (e *Literal) String() string { - if e.WithDoubleQuote { - return `"` + e.Value + `"` - } - - return e.Value -} diff --git a/ast/ast_test.go b/ast/ast_test.go deleted file mode 100644 index 18566c7..0000000 --- a/ast/ast_test.go +++ /dev/null @@ -1,259 +0,0 @@ -package ast_test - -import ( - "testing" - - "github.com/laojianzi/kql-go/ast" - "github.com/laojianzi/kql-go/token" - "github.com/stretchr/testify/assert" -) - -func TestParenExpr(t *testing.T) { - type args struct { - L, R int - Expr ast.Expr - } - - cases := []struct { - name string - args args - wantPos int - wantEnd int - wantString string - }{ - { - name: `(f1: "v1")`, - args: args{ - R: 10, - Expr: ast.NewBinaryExpr(1, "f1", token.TokenKindOperatorEql, ast.NewLiteral(5, 9, token.TokenKindString, "v1"), false), - }, - wantEnd: 10, - wantString: `(f1: "v1")`, - }, - { - name: `("v1" OR "v2")`, - args: args{ - R: 14, - Expr: ast.NewCombineExpr( - ast.NewLiteral(1, 5, token.TokenKindString, "v1"), - token.TokenKindKeywordOr, - ast.NewLiteral(9, 13, token.TokenKindString, "v2"), - ), - }, - wantEnd: 14, - wantString: `("v1" OR "v2")`, - }, - } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - expr := ast.NewParenExpr(c.args.L, c.args.R, c.args.Expr) - assert.Equal(t, c.wantPos, expr.Pos()) - assert.Equal(t, c.wantEnd, expr.End()) - assert.Equal(t, c.wantString, expr.String()) - }) - } -} - -func TestCombineExpr(t *testing.T) { - type args struct { - leftExpr ast.Expr - keyword token.Kind - rightExpr ast.Expr - } - - cases := []struct { - name string - args args - wantPos int - wantEnd int - wantString string - }{ - { - name: `f1: "v1" OR NOT f1: "v2"`, - args: args{ - leftExpr: ast.NewBinaryExpr(0, "f1", token.TokenKindOperatorEql, ast.NewLiteral(4, 8, token.TokenKindString, "v1"), false), - keyword: token.TokenKindKeywordOr, - rightExpr: ast.NewBinaryExpr(12, "f1", token.TokenKindOperatorEql, ast.NewLiteral(20, 24, token.TokenKindString, "v2"), true), - }, - wantEnd: 24, - wantString: `f1: "v1" OR NOT f1: "v2"`, - }, - { - name: `NOT f1: ("v1" OR "v2") AND f3: "v3"`, - args: args{ - leftExpr: ast.NewBinaryExpr( - 0, - "f1", - token.TokenKindOperatorEql, - ast.NewParenExpr( - 8, - 22, - ast.NewCombineExpr( - ast.NewBinaryExpr(9, "", 0, ast.NewLiteral(9, 13, token.TokenKindString, "v1"), false), - token.TokenKindKeywordOr, - ast.NewBinaryExpr(17, "", 0, ast.NewLiteral(17, 21, token.TokenKindString, "v2"), false), - ), - ), - true, - ), - keyword: token.TokenKindKeywordAnd, - rightExpr: ast.NewBinaryExpr(27, "f3", token.TokenKindOperatorEql, ast.NewLiteral(31, 35, token.TokenKindString, "v3"), false), - }, - wantEnd: 35, - wantString: `NOT f1: ("v1" OR "v2") AND f3: "v3"`, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - expr := ast.NewCombineExpr(c.args.leftExpr, c.args.keyword, c.args.rightExpr) - assert.Equal(t, c.wantPos, expr.Pos()) - assert.Equal(t, c.wantEnd, expr.End()) - assert.Equal(t, c.wantString, expr.String()) - }) - } -} - -func TestBinaryExpr(t *testing.T) { - type args struct { - pos int - field string - operator token.Kind - value ast.Expr - hasNot bool - } - - cases := []struct { - name string - args args - wantPos int - wantEnd int - wantString string - }{ - { - name: `"v1"`, - args: args{ - pos: 0, - value: ast.NewLiteral(0, 4, token.TokenKindString, "v1"), - hasNot: false, - }, - wantEnd: 4, - wantString: `"v1"`, - }, - { - name: `NOT "v1"`, - args: args{ - pos: 0, - value: ast.NewLiteral(4, 8, token.TokenKindString, "v1"), - hasNot: true, - }, - wantEnd: 8, - wantString: `NOT "v1"`, - }, - { - name: `f1: "v1"`, - args: args{ - field: "f1", - operator: token.TokenKindOperatorEql, - value: ast.NewLiteral(4, 8, token.TokenKindString, "v1"), - hasNot: false, - }, - wantEnd: 8, - wantString: `f1: "v1"`, - }, - { - name: `NOT f1: "v1"`, - args: args{ - pos: 0, - field: "f1", - operator: token.TokenKindOperatorEql, - value: ast.NewLiteral(8, 12, token.TokenKindString, "v1"), - hasNot: true, - }, - wantEnd: 12, - wantString: `NOT f1: "v1"`, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - expr := ast.NewBinaryExpr(c.args.pos, c.args.field, c.args.operator, c.args.value, c.args.hasNot) - assert.Equal(t, c.wantPos, expr.Pos()) - assert.Equal(t, c.wantEnd, expr.End()) - assert.Equal(t, c.wantString, expr.String()) - }) - } -} - -func TestLiteral(t *testing.T) { - type args struct { - pos int - end int - kind token.Kind - value string - withDoubleQuote bool - } - - cases := []struct { - name string - args args - wantPos int - wantEnd int - wantString string - }{ - { - name: "int literal", - args: args{ - end: 3, - kind: token.TokenKindInt, - value: "101", - }, - wantEnd: 3, - wantString: `101`, - }, - { - name: "float literal", - args: args{ - end: 3, - kind: token.TokenKindFloat, - value: "10.1", - }, - wantEnd: 3, - wantString: `10.1`, - }, - { - name: "string literal", - args: args{ - pos: 1, - end: 3, - kind: token.TokenKindString, - value: "v1", - withDoubleQuote: true, - }, - wantPos: 1, - wantEnd: 3, - wantString: `"v1"`, - }, - { - name: `identifier literal`, - args: args{ - pos: 0, - end: 2, - kind: token.TokenKindIdent, - value: "v1", - withDoubleQuote: true, - }, - wantEnd: 2, - wantString: `v1`, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - expr := ast.NewLiteral(c.args.pos, c.args.end, c.args.kind, c.args.value) - assert.Equal(t, c.wantPos, expr.Pos()) - assert.Equal(t, c.wantEnd, expr.End()) - assert.Equal(t, c.wantString, expr.String()) - }) - } -} diff --git a/ast/binary.go b/ast/binary.go new file mode 100644 index 0000000..13ae017 --- /dev/null +++ b/ast/binary.go @@ -0,0 +1,70 @@ +package ast + +import ( + "strings" + + "github.com/laojianzi/kql-go/token" +) + +// BinaryExpr is a binary expression. +// +// Example: +// +// `NOT f1: "v1"` +type BinaryExpr struct { + pos int + Field string + Operator token.Kind + Value Expr + HasNot bool +} + +// NewBinaryExpr creates a new binary expression. +func NewBinaryExpr(pos int, field string, operator token.Kind, value Expr, hasNot bool) *BinaryExpr { + return &BinaryExpr{ + pos: pos, + Field: field, + Operator: operator, + Value: value, + HasNot: hasNot, + } +} + +// Pos returns the position of the binary expression. +func (e *BinaryExpr) Pos() int { + return e.pos +} + +// End returns the end position of the binary expression. +func (e *BinaryExpr) End() int { + if e.Value.End() < len(e.Value.String()) { // e.g. string values with double quotes + return e.Value.End() + 1 + } + + return e.Value.End() +} + +// String returns the string representation of the binary expression. +func (e *BinaryExpr) String() string { + var buf strings.Builder + if e.HasNot { + buf.WriteString("NOT ") + } + + if e.Field != "" { + buf.WriteString(e.Field) + + if e.Operator != token.TokenKindOperatorEql { + buf.WriteByte(' ') + } + + buf.WriteString(e.Operator.String()) + buf.WriteByte(' ') + } + + if e.Value != nil { + buf.WriteString(e.Value.String()) + } + + return buf.String() +} diff --git a/ast/binary_test.go b/ast/binary_test.go new file mode 100644 index 0000000..7beef8a --- /dev/null +++ b/ast/binary_test.go @@ -0,0 +1,80 @@ +package ast_test + +import ( + "testing" + + "github.com/laojianzi/kql-go/ast" + "github.com/laojianzi/kql-go/token" + "github.com/stretchr/testify/assert" +) + +func TestBinaryExpr(t *testing.T) { + type args struct { + pos int + field string + operator token.Kind + value ast.Expr + hasNot bool + } + + cases := []struct { + name string + args args + wantPos int + wantEnd int + wantString string + }{ + { + name: `"v1"`, + args: args{ + pos: 0, + value: ast.NewLiteral(0, 4, token.TokenKindString, "v1"), + hasNot: false, + }, + wantEnd: 4, + wantString: `"v1"`, + }, + { + name: `NOT "v1"`, + args: args{ + pos: 0, + value: ast.NewLiteral(4, 8, token.TokenKindString, "v1"), + hasNot: true, + }, + wantEnd: 8, + wantString: `NOT "v1"`, + }, + { + name: `f1: "v1"`, + args: args{ + field: "f1", + operator: token.TokenKindOperatorEql, + value: ast.NewLiteral(4, 8, token.TokenKindString, "v1"), + hasNot: false, + }, + wantEnd: 8, + wantString: `f1: "v1"`, + }, + { + name: `NOT f1: "v1"`, + args: args{ + pos: 0, + field: "f1", + operator: token.TokenKindOperatorEql, + value: ast.NewLiteral(8, 12, token.TokenKindString, "v1"), + hasNot: true, + }, + wantEnd: 12, + wantString: `NOT f1: "v1"`, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + expr := ast.NewBinaryExpr(c.args.pos, c.args.field, c.args.operator, c.args.value, c.args.hasNot) + assert.Equal(t, c.wantPos, expr.Pos()) + assert.Equal(t, c.wantEnd, expr.End()) + assert.Equal(t, c.wantString, expr.String()) + }) + } +} diff --git a/ast/combine.go b/ast/combine.go new file mode 100644 index 0000000..b7e2c95 --- /dev/null +++ b/ast/combine.go @@ -0,0 +1,54 @@ +package ast + +import ( + "strings" + + "github.com/laojianzi/kql-go/token" +) + +// CombineExpr is a combination expression. +// +// Example: +// +// `f1: "v1" AND num: 1` +type CombineExpr struct { + LeftExpr Expr + Keyword token.Kind + RightExpr Expr +} + +// NewCombineExpr creates a new combination expression. +func NewCombineExpr(leftExpr Expr, keyword token.Kind, rightExpr Expr) *CombineExpr { + return &CombineExpr{ + LeftExpr: leftExpr, + Keyword: keyword, + RightExpr: rightExpr, + } +} + +// Pos returns the position of the combination expression. +func (e *CombineExpr) Pos() int { + return e.LeftExpr.Pos() +} + +// End returns the end position of the combination expression. +func (e *CombineExpr) End() int { + return e.RightExpr.End() +} + +// String returns the string representation of the combination expression. +func (e *CombineExpr) String() string { + var buf strings.Builder + if e.LeftExpr != nil { + buf.WriteString(e.LeftExpr.String()) + } + + if e.RightExpr != nil { + buf.WriteByte(' ') + buf.WriteString(e.Keyword.String()) + buf.WriteByte(' ') + buf.WriteString(e.RightExpr.String()) + } + + return buf.String() +} diff --git a/ast/combine_test.go b/ast/combine_test.go new file mode 100644 index 0000000..4334a97 --- /dev/null +++ b/ast/combine_test.go @@ -0,0 +1,69 @@ +package ast_test + +import ( + "testing" + + "github.com/laojianzi/kql-go/ast" + "github.com/laojianzi/kql-go/token" + "github.com/stretchr/testify/assert" +) + +func TestCombineExpr(t *testing.T) { + type args struct { + leftExpr ast.Expr + keyword token.Kind + rightExpr ast.Expr + } + + cases := []struct { + name string + args args + wantPos int + wantEnd int + wantString string + }{ + { + name: `f1: "v1" OR NOT f1: "v2"`, + args: args{ + leftExpr: ast.NewBinaryExpr(0, "f1", token.TokenKindOperatorEql, ast.NewLiteral(4, 8, token.TokenKindString, "v1"), false), + keyword: token.TokenKindKeywordOr, + rightExpr: ast.NewBinaryExpr(12, "f1", token.TokenKindOperatorEql, ast.NewLiteral(20, 24, token.TokenKindString, "v2"), true), + }, + wantEnd: 24, + wantString: `f1: "v1" OR NOT f1: "v2"`, + }, + { + name: `NOT f1: ("v1" OR "v2") AND f3: "v3"`, + args: args{ + leftExpr: ast.NewBinaryExpr( + 0, + "f1", + token.TokenKindOperatorEql, + ast.NewParenExpr( + 8, + 22, + ast.NewCombineExpr( + ast.NewBinaryExpr(9, "", 0, ast.NewLiteral(9, 13, token.TokenKindString, "v1"), false), + token.TokenKindKeywordOr, + ast.NewBinaryExpr(17, "", 0, ast.NewLiteral(17, 21, token.TokenKindString, "v2"), false), + ), + ), + true, + ), + keyword: token.TokenKindKeywordAnd, + rightExpr: ast.NewBinaryExpr(27, "f3", token.TokenKindOperatorEql, ast.NewLiteral(31, 35, token.TokenKindString, "v3"), false), + }, + wantEnd: 35, + wantString: `NOT f1: ("v1" OR "v2") AND f3: "v3"`, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + expr := ast.NewCombineExpr(c.args.leftExpr, c.args.keyword, c.args.rightExpr) + assert.Equal(t, c.wantPos, expr.Pos()) + assert.Equal(t, c.wantEnd, expr.End()) + assert.Equal(t, c.wantString, expr.String()) + }) + } +} diff --git a/ast/literal.go b/ast/literal.go new file mode 100644 index 0000000..758ac70 --- /dev/null +++ b/ast/literal.go @@ -0,0 +1,42 @@ +package ast + +import "github.com/laojianzi/kql-go/token" + +// Literal is a literal(int, float, string or identifier) value. +type Literal struct { + pos int + end int + Kind token.Kind // int, float, string or identifier + Value string + WithDoubleQuote bool +} + +// NewLiteral creates a new literal value. +func NewLiteral(pos, end int, kind token.Kind, value string) *Literal { + return &Literal{ + pos: pos, + end: end, + Kind: kind, + Value: value, + WithDoubleQuote: kind == token.TokenKindString, + } +} + +// Pos returns the position of the literal value. +func (e *Literal) Pos() int { + return e.pos +} + +// End returns the end position of the literal value. +func (e *Literal) End() int { + return e.end +} + +// String returns the string representation of the literal value. +func (e *Literal) String() string { + if e.WithDoubleQuote { + return `"` + e.Value + `"` + } + + return e.Value +} diff --git a/ast/literal_test.go b/ast/literal_test.go new file mode 100644 index 0000000..e30cab3 --- /dev/null +++ b/ast/literal_test.go @@ -0,0 +1,82 @@ +package ast_test + +import ( + "testing" + + "github.com/laojianzi/kql-go/ast" + "github.com/laojianzi/kql-go/token" + "github.com/stretchr/testify/assert" +) + +func TestLiteral(t *testing.T) { + type args struct { + pos int + end int + kind token.Kind + value string + withDoubleQuote bool + } + + cases := []struct { + name string + args args + wantPos int + wantEnd int + wantString string + }{ + { + name: "int literal", + args: args{ + end: 3, + kind: token.TokenKindInt, + value: "101", + }, + wantEnd: 3, + wantString: `101`, + }, + { + name: "float literal", + args: args{ + end: 3, + kind: token.TokenKindFloat, + value: "10.1", + }, + wantEnd: 3, + wantString: `10.1`, + }, + { + name: "string literal", + args: args{ + pos: 1, + end: 3, + kind: token.TokenKindString, + value: "v1", + withDoubleQuote: true, + }, + wantPos: 1, + wantEnd: 3, + wantString: `"v1"`, + }, + { + name: `identifier literal`, + args: args{ + pos: 0, + end: 2, + kind: token.TokenKindIdent, + value: "v1", + withDoubleQuote: true, + }, + wantEnd: 2, + wantString: `v1`, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + expr := ast.NewLiteral(c.args.pos, c.args.end, c.args.kind, c.args.value) + assert.Equal(t, c.wantPos, expr.Pos()) + assert.Equal(t, c.wantEnd, expr.End()) + assert.Equal(t, c.wantString, expr.String()) + }) + } +} diff --git a/ast/paren.go b/ast/paren.go new file mode 100644 index 0000000..b683e89 --- /dev/null +++ b/ast/paren.go @@ -0,0 +1,43 @@ +package ast + +import "strings" + +// ParenExpr is a parenthesis expression. +// +// Example: +// +// `(f1: "v1" AND num: 1)` +type ParenExpr struct { + L, R int // left and right position of the parenthesis + Expr Expr +} + +// NewParenExpr creates a new parenthesis expression. +func NewParenExpr(L, R int, expr Expr) *ParenExpr { + return &ParenExpr{ + L: L, + R: R, + Expr: expr, + } +} + +// Pos returns the position of the parenthesis expression. +func (e *ParenExpr) Pos() int { + return e.L +} + +// End returns the end position of the parenthesis expression. +func (e *ParenExpr) End() int { + return e.R +} + +// String returns the string representation of the parenthesis expression. +func (e *ParenExpr) String() string { + var buf strings.Builder + + buf.WriteByte('(') + buf.WriteString(e.Expr.String()) + buf.WriteByte(')') + + return buf.String() +} diff --git a/ast/paren_test.go b/ast/paren_test.go new file mode 100644 index 0000000..76cf2a4 --- /dev/null +++ b/ast/paren_test.go @@ -0,0 +1,55 @@ +package ast_test + +import ( + "testing" + + "github.com/laojianzi/kql-go/ast" + "github.com/laojianzi/kql-go/token" + "github.com/stretchr/testify/assert" +) + +func TestParenExpr(t *testing.T) { + type args struct { + L, R int + Expr ast.Expr + } + + cases := []struct { + name string + args args + wantPos int + wantEnd int + wantString string + }{ + { + name: `(f1: "v1")`, + args: args{ + R: 10, + Expr: ast.NewBinaryExpr(1, "f1", token.TokenKindOperatorEql, ast.NewLiteral(5, 9, token.TokenKindString, "v1"), false), + }, + wantEnd: 10, + wantString: `(f1: "v1")`, + }, + { + name: `("v1" OR "v2")`, + args: args{ + R: 14, + Expr: ast.NewCombineExpr( + ast.NewLiteral(1, 5, token.TokenKindString, "v1"), + token.TokenKindKeywordOr, + ast.NewLiteral(9, 13, token.TokenKindString, "v2"), + ), + }, + wantEnd: 14, + wantString: `("v1" OR "v2")`, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + expr := ast.NewParenExpr(c.args.L, c.args.R, c.args.Expr) + assert.Equal(t, c.wantPos, expr.Pos()) + assert.Equal(t, c.wantEnd, expr.End()) + assert.Equal(t, c.wantString, expr.String()) + }) + } +} diff --git a/ast/wildcard.go b/ast/wildcard.go new file mode 100644 index 0000000..f12caf0 --- /dev/null +++ b/ast/wildcard.go @@ -0,0 +1,38 @@ +package ast + +// WildcardExpr is a wildcard expression. +// +// Example: +// +// `*` +// `5*0` +// `f*o` +// `"*foo*"` +type WildcardExpr struct { + *Literal // identifier or string + + Indexes []int // index of wildcard +} + +// NewWildcardExpr creates a new wildcard expression. +func NewWildcardExpr(lit *Literal, indexes []int) *WildcardExpr { + return &WildcardExpr{ + Literal: lit, + Indexes: indexes, + } +} + +// Pos returns the position of the wildcard expression. +func (e *WildcardExpr) Pos() int { + return e.pos +} + +// End returns the end position of the wildcard expression. +func (e *WildcardExpr) End() int { + return e.end +} + +// String returns the string representation of the wildcard expression. +func (e *WildcardExpr) String() string { + return e.Literal.String() +} diff --git a/ast/wildcard_test.go b/ast/wildcard_test.go new file mode 100644 index 0000000..0140a5e --- /dev/null +++ b/ast/wildcard_test.go @@ -0,0 +1,154 @@ +package ast_test + +import ( + "testing" + + "github.com/laojianzi/kql-go/ast" + "github.com/laojianzi/kql-go/token" + "github.com/stretchr/testify/assert" +) + +func TestWildcard(t *testing.T) { + type args struct { + pos int + end int + kind token.Kind + value string + withDoubleQuote bool + indexes []int + } + + cases := []struct { + name string + args args + wantPos int + wantEnd int + wantString string + wantIndexes []int + }{ + { + name: "only wildcard on ident", + args: args{ + end: 1, + kind: token.TokenKindIdent, + value: "*", + }, + wantEnd: 1, + wantString: "*", + wantIndexes: []int{0}, + }, + { + name: "only wildcard on string", + args: args{ + pos: 1, + end: 2, + kind: token.TokenKindString, + value: "*", + withDoubleQuote: true, + }, + wantPos: 1, + wantEnd: 2, + wantString: `"*"`, + wantIndexes: []int{1}, + }, + { + name: "int value with wildcard on ident", + args: args{ + end: 3, + kind: token.TokenKindIdent, + value: "4*9", + }, + wantEnd: 3, + wantString: "4*9", + wantIndexes: []int{1}, + }, + { + name: "int value with multi-wildcard on ident", + args: args{ + end: 3, + kind: token.TokenKindIdent, + value: "*0*", + }, + wantEnd: 3, + wantString: "*0*", + wantIndexes: []int{0, 2}, + }, + { + name: "float value with wildcard on ident", + args: args{ + end: 4, + kind: token.TokenKindIdent, + value: "0.*9", + }, + wantEnd: 4, + wantString: "0.*9", + wantIndexes: []int{2}, + }, + { + name: "float value with multi-wildcard on ident", + args: args{ + end: 4, + kind: token.TokenKindIdent, + value: "*.9*", + }, + wantEnd: 4, + wantString: "*.9*", + wantIndexes: []int{0, 3}, + }, + { + name: "string value with wildcard on ident", + args: args{ + end: 3, + kind: token.TokenKindIdent, + value: "f*o", + }, + wantEnd: 3, + wantString: "f*o", + wantIndexes: []int{1}, + }, + { + name: "string value with multi-wildcard on ident", + args: args{ + end: 3, + kind: token.TokenKindIdent, + value: "*o*", + }, + wantEnd: 3, + wantString: "*o*", + wantIndexes: []int{0, 2}, + }, + { + name: "value with wildcard on string", + args: args{ + end: 5, + kind: token.TokenKindString, + value: "f*o", + withDoubleQuote: true, + }, + wantEnd: 5, + wantString: `"f*o"`, + wantIndexes: []int{2}, + }, + { + name: "value with multi-wildcard on string", + args: args{ + end: 5, + kind: token.TokenKindString, + value: "*o*", + withDoubleQuote: true, + }, + wantEnd: 5, + wantString: `"*o*"`, + wantIndexes: []int{1, 3}, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + expr := ast.NewLiteral(c.args.pos, c.args.end, c.args.kind, c.args.value) + assert.Equal(t, c.wantPos, expr.Pos()) + assert.Equal(t, c.wantEnd, expr.End()) + assert.Equal(t, c.wantString, expr.String()) + }) + } +} diff --git a/parser/lexer.go b/parser/lexer.go index 115da6a..d28ecad 100644 --- a/parser/lexer.go +++ b/parser/lexer.go @@ -66,7 +66,6 @@ func (l *defaultLexer) consumeToken() error { return l.consumeNumber() case '"': // double quote string return l.consumeString() - case '*': // e.g * or *xx* or *"xx"* } // ident as keyword, field or value diff --git a/parser/parser.go b/parser/parser.go index 883926d..4bdcfa3 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -124,28 +124,16 @@ func (p *defaultParser) parseBinary() (ast.Expr, error) { } func (p *defaultParser) parseLiteral() (ast.Expr, error) { - kind := p.lexer.Token.Kind - if kind == token.TokenKindLparen { + if p.lexer.Token.Kind == token.TokenKindLparen { return p.parseParen() } - switch kind { + switch p.lexer.Token.Kind { case token.TokenKindInt, token.TokenKindFloat, token.TokenKindString, token.TokenKindIdent: - tok, err := p.expect(kind) - if err != nil { - return nil, err - } - - pos, end := tok.Pos, tok.End - if kind == token.TokenKindString { // with double quote " - pos -= 1 - end += 1 - } - - return ast.NewLiteral(pos, end, kind, tok.Value), nil + return p.parseWildcard() } - return nil, fmt.Errorf("unexpected token: %s", kind) + return nil, fmt.Errorf("unexpected token: %s", p.lexer.Token.Kind) } func (p *defaultParser) parseParen() (ast.Expr, error) { @@ -169,6 +157,41 @@ func (p *defaultParser) parseParen() (ast.Expr, error) { return ast.NewParenExpr(tok.Pos, rparen, expr), nil } +func (p *defaultParser) parseWildcard() (ast.Expr, error) { + kind := p.lexer.Token.Kind + + tok, err := p.expect(kind) + if err != nil { + return nil, err + } + + pos, end := tok.Pos, tok.End + if kind == token.TokenKindString { // with double quote " + pos -= 1 + end += 1 + } + + lit := ast.NewLiteral(pos, end, kind, tok.Value) + if kind != token.TokenKindIdent && kind != token.TokenKindString { + return lit, nil + } + + var indexes []int + + runes := []rune(tok.Value) + for i := range runes { + if runes[i] == '*' && (i == 0 || runes[i-1] != '\\') { // skip escaped wildcard + indexes = append(indexes, i) + } + } + + if len(indexes) == 0 { // not found wildcard + return lit, nil + } + + return ast.NewWildcardExpr(lit, indexes), nil +} + func (p *defaultParser) expect(kind token.Kind) (*Token, error) { if p.lexer.Token.Kind != kind { return nil, fmt.Errorf("expected token: %s, but: %s", kind, p.lexer.Token.Kind)