Skip to content

Commit

Permalink
⭐ semver (#2962)
Browse files Browse the repository at this point in the history
This introduces the builtin semver type and the ability to use semver for comparisons.

Semvers are created via the `semver` keyword, which takes a string as an
argument:

```coffee
semver('1.2.3')
```

Semvers can be compared with each other and with strings:

```coffee
semver('1.2.3') < semver('2.3')

semver('1.10') >= '1.2'
```

Signed-off-by: Dominik Richter <[email protected]>
  • Loading branch information
arlimus authored Jan 7, 2024
1 parent a068989 commit 6866bf1
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 1 deletion.
3 changes: 3 additions & 0 deletions cli/printer/mql.go
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,9 @@ func (print *Printer) Data(typ types.Type, data interface{}, codeID string, bund
case types.Block:
return print.refMap(typ, data.(map[string]interface{}), codeID, bundle, indent)

case types.Semver:
return print.Secondary(data.(string))

case types.ArrayLike:
if data == nil {
return print.Secondary("null")
Expand Down
30 changes: 30 additions & 0 deletions llx/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,8 @@ func init() {
string("!=" + types.Float): {f: stringNotFloatV2, Label: "!="},
string("==" + types.Dict): {f: stringCmpDictV2, Label: "=="},
string("!=" + types.Dict): {f: stringNotDictV2, Label: "!="},
string("==" + types.Semver): {f: semverCmpSemver, Label: "=="},
string("!=" + types.Semver): {f: semverNotSemver, Label: "!="},
string("==" + types.ArrayLike): {f: chunkEqFalseV2, Label: "=="},
string("!=" + types.ArrayLike): {f: chunkNeqTrueV2, Label: "!="},
string("==" + types.Array(types.String)): {f: stringCmpStringarrayV2, Label: "=="},
Expand All @@ -293,6 +295,10 @@ func init() {
string("<=" + types.Dict): {f: stringLTEDictV2, Label: "<="},
string(">" + types.Dict): {f: stringGTDictV2, Label: ">"},
string(">=" + types.Dict): {f: stringGTEDictV2, Label: ">="},
string("<" + types.Semver): {f: semverLTsemver, Label: "<"},
string(">" + types.Semver): {f: semverGTsemver, Label: ">"},
string("<=" + types.Semver): {f: semverLTEsemver, Label: "<="},
string(">=" + types.Semver): {f: semverGTEsemver, Label: ">="},
string("&&" + types.Bool): {f: stringAndBoolV2, Label: "&&"},
string("||" + types.Bool): {f: stringOrBoolV2, Label: "||"},
string("&&" + types.Int): {f: stringAndIntV2, Label: "&&"},
Expand Down Expand Up @@ -453,6 +459,8 @@ func init() {
string("!=" + types.String): {f: dictNotStringV2, Label: "!="},
string("==" + types.Regex): {f: dictCmpRegexV2, Label: "=="},
string("!=" + types.Regex): {f: dictNotRegexV2, Label: "!="},
string("==" + types.Semver): {f: semverCmpSemver, Label: "=="},
string("!=" + types.Semver): {f: semverNotSemver, Label: "!="},
string("==" + types.ArrayLike): {f: dictCmpArrayV2, Label: "=="},
string("!=" + types.ArrayLike): {f: dictNotArrayV2, Label: "!="},
string("==" + types.Array(types.String)): {f: dictCmpStringarrayV2, Label: "=="},
Expand Down Expand Up @@ -481,6 +489,10 @@ func init() {
string("<=" + types.Dict): {f: dictLTEDictV2, Label: "<="},
string(">" + types.Dict): {f: dictGTDictV2, Label: ">"},
string(">=" + types.Dict): {f: dictGTEDictV2, Label: ">="},
string("<" + types.Semver): {f: semverLTsemver, Label: "<"},
string(">" + types.Semver): {f: semverGTsemver, Label: ">"},
string("<=" + types.Semver): {f: semverLTEsemver, Label: "<="},
string(">=" + types.Semver): {f: semverGTEsemver, Label: ">="},
string("&&" + types.Bool): {f: dictAndBoolV2, Label: "&&"},
string("||" + types.Bool): {f: dictOrBoolV2, Label: "||"},
string("&&" + types.Int): {f: dictAndIntV2, Label: "&&"},
Expand Down Expand Up @@ -547,6 +559,24 @@ func init() {
// We have not yet decided if and how these may be exposed to users
"notEmpty": {f: dictNotEmptyV2},
},
types.Semver: {
string("==" + types.Nil): {f: stringCmpNilV2, Label: "=="},
string("!=" + types.Nil): {f: stringNotNilV2, Label: "!="},
string("==" + types.Empty): {f: stringCmpEmptyV2, Label: "=="},
string("!=" + types.Empty): {f: stringNotEmptyV2, Label: "!="},
string("==" + types.Semver): {f: semverCmpSemver, Label: "=="},
string("!=" + types.Semver): {f: semverNotSemver, Label: "!="},
string("<" + types.Semver): {f: semverLTsemver, Label: "<"},
string(">" + types.Semver): {f: semverGTsemver, Label: ">"},
string("<=" + types.Semver): {f: semverLTEsemver, Label: "<="},
string(">=" + types.Semver): {f: semverGTEsemver, Label: ">="},
string("==" + types.String): {f: semverCmpSemver, Label: "=="},
string("!=" + types.String): {f: semverNotSemver, Label: "!="},
string("<" + types.String): {f: semverLTsemver, Label: "<"},
string(">" + types.String): {f: semverGTsemver, Label: ">"},
string("<=" + types.String): {f: semverLTEsemver, Label: "<="},
string(">=" + types.String): {f: semverGTEsemver, Label: ">="},
},
types.ArrayLike: {
"[]": {f: arrayGetIndexV2},
"first": {f: arrayGetFirstIndexV2},
Expand Down
14 changes: 14 additions & 0 deletions llx/builtin_global.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func init() {
"switch": switchCallV2,
"score": scoreCallV2,
"typeof": typeofCallV2,
"semver": semverCall,
"{}": blockV2,
"return": returnCallV2,
"createResource": globalCreateResource,
Expand Down Expand Up @@ -182,6 +183,19 @@ func typeofCallV2(e *blockExecutor, f *Function, ref uint64) (*RawData, uint64,
return StringData(res.Type.Label()), 0, nil
}

func semverCall(e *blockExecutor, f *Function, ref uint64) (*RawData, uint64, error) {
if len(f.Args) != 1 {
return nil, 0, errors.New("Called `semver` with " + strconv.Itoa(len(f.Args)) + " arguments, expected one")
}

res, dref, err := e.resolveValue(f.Args[0], ref)
if err != nil || dref != 0 || res == nil {
return res, dref, err
}

return &RawData{Type: types.Semver, Value: res.Value}, 0, nil
}

func expectV2(e *blockExecutor, f *Function, ref uint64) (*RawData, uint64, error) {
if len(f.Args) != 1 {
return nil, 0, errors.New("Called expect with " + strconv.Itoa(len(f.Args)) + " arguments, expected 1")
Expand Down
85 changes: 85 additions & 0 deletions llx/builtin_semver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright (c) Mondoo, Inc.
// SPDX-License-Identifier: BUSL-1.1

package llx

import (
"github.com/Masterminds/semver"
"go.mondoo.com/cnquery/v9/types"
)

func semverLT(left interface{}, right interface{}) *RawData {
leftv, err := semver.NewVersion(left.(string))
if err != nil {
return BoolData(left.(string) < right.(string))
}
rightv, err := semver.NewVersion(right.(string))
if err != nil {
return BoolData(left.(string) < right.(string))
}
return BoolData(leftv.LessThan(rightv))
}

func semverGT(left interface{}, right interface{}) *RawData {
leftv, err := semver.NewVersion(left.(string))
if err != nil {
return BoolData(left.(string) > right.(string))
}
rightv, err := semver.NewVersion(right.(string))
if err != nil {
return BoolData(left.(string) > right.(string))
}
return BoolData(leftv.GreaterThan(rightv))
}

func semverLTE(left interface{}, right interface{}) *RawData {
leftv, err := semver.NewVersion(left.(string))
if err != nil {
return BoolData(left.(string) <= right.(string))
}
rightv, err := semver.NewVersion(right.(string))
if err != nil {
return BoolData(left.(string) <= right.(string))
}
return BoolData(!leftv.GreaterThan(rightv))
}

func semverGTE(left interface{}, right interface{}) *RawData {
leftv, err := semver.NewVersion(left.(string))
if err != nil {
return BoolData(left.(string) >= right.(string))
}
rightv, err := semver.NewVersion(right.(string))
if err != nil {
return BoolData(left.(string) >= right.(string))
}
return BoolData(!leftv.LessThan(rightv))
}

func semverCmpSemver(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
return nonNilDataOpV2(e, bind, chunk, ref, types.Bool, func(left, right interface{}) *RawData {
return BoolData(left.(string) == right.(string))
})
}

func semverNotSemver(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
return nonNilDataOpV2(e, bind, chunk, ref, types.Bool, func(left, right interface{}) *RawData {
return BoolData(left.(string) != right.(string))
})
}

func semverLTsemver(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
return nonNilDataOpV2(e, bind, chunk, ref, types.Bool, semverLT)
}

func semverGTsemver(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
return nonNilDataOpV2(e, bind, chunk, ref, types.Bool, semverGT)
}

func semverLTEsemver(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
return nonNilDataOpV2(e, bind, chunk, ref, types.Bool, semverLTE)
}

func semverGTEsemver(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
return nonNilDataOpV2(e, bind, chunk, ref, types.Bool, semverGTE)
}
2 changes: 2 additions & 0 deletions llx/data_conversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func init() {
types.Score: score2result,
types.Empty: empty2result,
types.Block: block2result,
types.Semver: string2result,
types.ArrayLike: array2result,
types.MapLike: map2result,
types.ResourceLike: resource2result,
Expand All @@ -58,6 +59,7 @@ func init() {
types.Score: pscore2raw,
types.Empty: pempty2raw,
types.Block: pblock2rawV2,
types.Semver: pscore2raw,
types.ArrayLike: parray2raw,
types.MapLike: pmap2raw,
types.ResourceLike: presource2raw,
Expand Down
28 changes: 28 additions & 0 deletions mqlc/operators.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func init() {
"switch": compileSwitch,
"Never": compileNever,
"empty": compileEmpty,
"semver": compileSemver,
}
}

Expand Down Expand Up @@ -496,6 +497,33 @@ func compileTypeof(c *compiler, id string, call *parser.Call) (types.Type, error
return types.String, nil
}

func compileSemver(c *compiler, id string, call *parser.Call) (types.Type, error) {
if call == nil || len(call.Function) < 1 {
return types.Nil, errors.New("missing parameter for '" + id + "', it requires 1")
}

arg := call.Function[0]
if arg == nil || arg.Value == nil || arg.Value.Operand == nil || arg.Value.Operand.Value == nil {
return types.Nil, errors.New("failed to get parameter for '" + id + "'")
}

argValue, err := c.compileExpression(arg.Value)
if err != nil {
return types.Nil, err
}

c.addChunk(&llx.Chunk{
Call: llx.Chunk_FUNCTION,
Id: "semver",
Function: &llx.Function{
Type: string(types.String),
Args: []*llx.Primitive{argValue},
},
})

return types.String, nil
}

func compileSwitch(c *compiler, id string, call *parser.Call) (types.Type, error) {
var ref *llx.Primitive

Expand Down
25 changes: 25 additions & 0 deletions providers/core/resources/mql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,31 @@ func TestTime(t *testing.T) {
})
}

func TestSemver(t *testing.T) {
x.TestSimple(t, []testutils.SimpleTest{
{
Code: "semver('1.2.3') == semver('1.2.3')",
ResultIndex: 2, Expectation: true,
},
{
Code: "semver('1.2.3') == semver('1.2')",
ResultIndex: 2, Expectation: false,
},
{
Code: "semver('1.2') < semver('1.10.2')",
ResultIndex: 2, Expectation: true,
},
{
Code: "semver('1.10') >= semver('1.2.3')",
ResultIndex: 2, Expectation: true,
},
{
Code: "semver('1.10') >= '1.2'",
ResultIndex: 2, Expectation: true,
},
})
}

func TestResource_Default(t *testing.T) {
x := testutils.InitTester(testutils.LinuxMock())
res := x.TestQuery(t, "mondoo")
Expand Down
6 changes: 5 additions & 1 deletion types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ const (
byteScore
byteBlock
byteEmpty
byteArray = 1<<4 + iota - 5 // set to 25 to avoid breaking changes
byteSemver
byteArray = 1<<4 + iota - 6 // set to 25 to avoid breaking changes
byteMap
byteResource
byteFunction
Expand Down Expand Up @@ -87,6 +88,8 @@ const (
Block = Type(rune(byteBlock))
// Empty value
Empty = Type(rune(byteEmpty))
// Semver value
Semver = Type(rune(byteSemver))
// ArrayLike is the underlying type of all arrays
ArrayLike = Type(rune(byteArray))
// MapLike is the underlying type of all maps
Expand Down Expand Up @@ -255,6 +258,7 @@ var labels = map[byte]string{
byteScore: "score",
byteBlock: "block",
byteEmpty: "empty",
byteSemver: "semver",
byteStringSlice: "stringslice",
byteRange: "range",
}
Expand Down

0 comments on commit 6866bf1

Please sign in to comment.