From d34280a4d787e83045d451e18fc665a0a622da44 Mon Sep 17 00:00:00 2001 From: Dave New Date: Thu, 30 Jan 2025 14:41:47 +0200 Subject: [PATCH] fix: duration compatibility with cel --- expressions/options/options.go | 8 + expressions/parser.go | 1 + expressions/typing/types.go | 6 +- migrations/sql.go | 3 +- node/codegen_test.go | 2 +- schema/parser/consts.go | 7 - .../errors/set_attribute_built_in_fields.keel | 2 +- .../validation/rules/expression/expression.go | 566 ------------------ 8 files changed, 17 insertions(+), 578 deletions(-) delete mode 100644 schema/validation/rules/expression/expression.go diff --git a/expressions/options/options.go b/expressions/options/options.go index 670074dc1..c2736f628 100644 --- a/expressions/options/options.go +++ b/expressions/options/options.go @@ -25,6 +25,7 @@ var typeCompatibilityMapping = map[string][][]*types.Type{ {typing.Boolean, types.BoolType}, {types.NewListType(types.StringType), typing.TextArray, typing.IDArray, typing.MarkdownArray}, {types.NewListType(types.IntType), types.NewListType(types.DoubleType), typing.NumberArray, typing.DecimalArray}, + {typing.Duration}, }, operators.NotEquals: { {types.StringType, typing.Text, typing.ID, typing.Markdown}, @@ -33,28 +34,35 @@ var typeCompatibilityMapping = map[string][][]*types.Type{ {typing.Boolean, types.BoolType}, {types.NewListType(types.StringType), typing.TextArray, typing.IDArray, typing.MarkdownArray}, {types.NewListType(types.IntType), types.NewListType(types.DoubleType), typing.NumberArray, typing.DecimalArray}, + {typing.Duration}, }, operators.Greater: { {types.IntType, types.DoubleType, typing.Number, typing.Decimal}, {typing.Date, typing.Timestamp, types.TimestampType}, + {typing.Duration}, }, operators.GreaterEquals: { {types.IntType, types.DoubleType, typing.Number, typing.Decimal}, {typing.Date, typing.Timestamp, types.TimestampType}, + {typing.Duration}, }, operators.Less: { {types.IntType, types.DoubleType, typing.Number, typing.Decimal}, {typing.Date, typing.Timestamp, types.TimestampType}, + {typing.Duration}, }, operators.LessEquals: { {types.IntType, types.DoubleType, typing.Number, typing.Decimal}, {typing.Date, typing.Timestamp, types.TimestampType}, + {typing.Duration}, }, operators.Add: { {types.IntType, types.DoubleType, typing.Number, typing.Decimal}, + {typing.Duration}, }, operators.Subtract: { {types.IntType, types.DoubleType, typing.Number, typing.Decimal}, + {typing.Duration}, }, operators.Multiply: { {types.IntType, types.DoubleType, typing.Number, typing.Decimal}, diff --git a/expressions/parser.go b/expressions/parser.go index bee6ad771..07a746663 100644 --- a/expressions/parser.go +++ b/expressions/parser.go @@ -165,6 +165,7 @@ func typesAssignable(expected *types.Type, actual *types.Type) bool { typing.Text.String(): {mapType(typing.Text.String()), mapType(typing.Markdown.String()), mapType(typing.ID.String())}, typing.Number.String(): {mapType(typing.Number.String()), mapType(typing.Decimal.String())}, typing.Decimal.String(): {mapType(typing.Number.String()), mapType(typing.Decimal.String())}, + typing.Duration.String(): {mapType(typing.Duration.String()), mapType(typing.Text.String())}, } // Check if there are specific compatibility rules for the expected type diff --git a/expressions/typing/types.go b/expressions/typing/types.go index 22727b720..b881dc31f 100644 --- a/expressions/typing/types.go +++ b/expressions/typing/types.go @@ -20,8 +20,8 @@ var ( Boolean = cel.OpaqueType(parser.FieldTypeBoolean) Timestamp = cel.OpaqueType(parser.FieldTypeTimestamp) Date = cel.OpaqueType(parser.FieldTypeDate) + Duration = cel.OpaqueType(parser.FieldTypeDuration) ) - var ( IDArray = cel.OpaqueType(fmt.Sprintf("%s[]", parser.FieldTypeID)) TextArray = cel.OpaqueType(fmt.Sprintf("%s[]", parser.FieldTypeText)) @@ -31,6 +31,7 @@ var ( BooleanArray = cel.OpaqueType(fmt.Sprintf("%s[]", parser.FieldTypeBoolean)) TimestampArray = cel.OpaqueType(fmt.Sprintf("%s[]", parser.FieldTypeTimestamp)) DateArray = cel.OpaqueType(fmt.Sprintf("%s[]", parser.FieldTypeDate)) + DurationArray = cel.OpaqueType(fmt.Sprintf("%s[]", parser.FieldTypeDuration)) ) var ( @@ -64,7 +65,8 @@ func MapType(schema []*parser.AST, typeName string, isRepeated bool) (*types.Typ parser.FieldTypeFile, parser.FieldTypeVector, parser.FieldTypeSecret, - parser.FieldTypePassword: + parser.FieldTypePassword, + parser.FieldTypeDuration: if isRepeated { return cel.OpaqueType(fmt.Sprintf("%s[]", typeName)), nil } else { diff --git a/migrations/sql.go b/migrations/sql.go index e958b759a..ed8980cbd 100644 --- a/migrations/sql.go +++ b/migrations/sql.go @@ -502,7 +502,8 @@ func toSqlLiteral(value any, field *proto.Field) (string, error) { return fmt.Sprintf("%d", value), nil case field.Type.Type == proto.Type_TYPE_BOOL: return fmt.Sprintf("%v", value), nil - + case field.Type.Type == proto.Type_TYPE_DURATION: + return db.QuoteLiteral(fmt.Sprintf("%s", value)), nil default: return "", fmt.Errorf("field %s has unexpected default value %s", field.Name, value) } diff --git a/node/codegen_test.go b/node/codegen_test.go index dde9f408e..75257e23b 100644 --- a/node/codegen_test.go +++ b/node/codegen_test.go @@ -61,8 +61,8 @@ export interface PersonTable { height: number bio: string file: FileDbRecord - heightInMetres: number canHoldBreath: runtime.Duration + heightInMetres: number id: Generated createdAt: Generated updatedAt: Generated diff --git a/schema/parser/consts.go b/schema/parser/consts.go index 6266e2fa0..3f644bcf8 100644 --- a/schema/parser/consts.go +++ b/schema/parser/consts.go @@ -41,13 +41,6 @@ const ( FieldTypeDuration = "Duration" // a time duration ) -var ComparableTypes = [][]string{ - {FieldTypeDate, FieldTypeTimestamp}, - {FieldTypeMarkdown, FieldTypeText}, - {FieldTypeDuration, FieldTypeText}, - {FieldTypeDecimal, FieldTypeNumber}, -} - // Types for Message fields const ( MessageFieldTypeAny = "Any" diff --git a/schema/testdata/errors/set_attribute_built_in_fields.keel b/schema/testdata/errors/set_attribute_built_in_fields.keel index afa7396ae..bdbf74238 100755 --- a/schema/testdata/errors/set_attribute_built_in_fields.keel +++ b/schema/testdata/errors/set_attribute_built_in_fields.keel @@ -13,7 +13,7 @@ model Post { @set(post.createdAt = ctx.now) //expect-error:18:32:AttributeExpressionError:Cannot set the field 'updatedAt' as it is a built-in field and can only be mutated internally @set(post.updatedAt = ctx.now) - //expect-error:18:39:E026:post.timeToRead is Duration and 123 is Number + //expect-error:36:39:AttributeExpressionError:expression expected to resolve to type Duration but it is Number @set(post.timeToRead = 123) } create createPost2() with (name, published, publisher.name) { diff --git a/schema/validation/rules/expression/expression.go b/schema/validation/rules/expression/expression.go deleted file mode 100644 index c3b232790..000000000 --- a/schema/validation/rules/expression/expression.go +++ /dev/null @@ -1,566 +0,0 @@ -package expression - -import ( - "fmt" - - "github.com/samber/lo" - "github.com/teamkeel/keel/schema/expressions" - "github.com/teamkeel/keel/schema/parser" - "github.com/teamkeel/keel/schema/validation/errorhandling" - "golang.org/x/exp/slices" -) - -type Rule func(asts []*parser.AST, expression *parser.Expression, context expressions.ExpressionContext) []error - -func ValidateExpression(asts []*parser.AST, expression *parser.Expression, rules []Rule, context expressions.ExpressionContext) (errors []error) { - for _, rule := range rules { - errs := rule(asts, expression, context) - errors = append(errors, errs...) - } - - return errors -} - -// Validates that the field type has a zero value (no expression necessary). -// Zero values are the following: -// * Text -> "" -// * Number => 0 -// * Boolean -> false -// * ID -> a ksuid -// * Timestamp -> now -func DefaultCanUseZeroValueRule(asts []*parser.AST, attr *parser.AttributeNode, context expressions.ExpressionContext) (errors []*errorhandling.ValidationError) { - typesWithZeroValue := []string{"Text", "Number", "Boolean", "ID", "Timestamp"} - - if !lo.Contains(typesWithZeroValue, context.Field.Type.Value) { - errors = append(errors, - errorhandling.NewValidationError( - errorhandling.ErrorDefaultExpressionNeeded, - errorhandling.TemplateLiterals{}, - attr, - ), - ) - return errors - } - - return errors -} - -// Validates that the expression has a single value and it matches the expected type -func ValueTypechecksRule(asts []*parser.AST, expression *parser.Expression, context expressions.ExpressionContext) (errors []error) { - conditions := expression.Conditions() - if len(conditions) != 1 { - errors = append(errors, - errorhandling.NewValidationError( - errorhandling.ErrorExpressionMultipleConditions, - errorhandling.TemplateLiterals{}, - expression, - ), - ) - return errors - } - condition := conditions[0] - - if condition.RHS != nil { - errors = append(errors, - errorhandling.NewValidationError( - errorhandling.ErrorDefaultExpressionOperatorPresent, - errorhandling.TemplateLiterals{ - Literals: map[string]string{ - "Op": condition.Operator.Symbol, - }, - }, - condition, - ), - ) - return errors - } - operand := condition.LHS - - resolver := expressions.NewOperandResolver( - operand, - asts, - &context, - expressions.OperandPositionLhs, - ) - expressionScopeEntity, err := resolver.Resolve() - if err != nil { - errors = append(errors, err.ToValidationError()) - return errors - } - - expectedType := context.Field.Type.Value - resolvedType := expressionScopeEntity.GetType() - - if context.Field.Repeated { - expectedType = context.Field.Type.Value + "[]" - - if resolvedType != parser.TypeArray { - errors = append(errors, - errorhandling.NewValidationError( - errorhandling.ErrorExpressionFieldTypeMismatch, - errorhandling.TemplateLiterals{ - Literals: map[string]string{ - "Exp": operand.ToString(), - "Type": resolvedType, - "FieldName": context.Field.Name.Value, - "FieldType": expectedType, - }, - }, - expression, - ), - ) - return errors - } - } - - if resolvedType == parser.TypeArray { - isEmptyArray := len(expressionScopeEntity.Array) == 0 - if isEmptyArray { - return errors - } - - resolvedType = expressionScopeEntity.Array[0].GetType() + "[]" - - if !context.Field.Repeated { - errors = append(errors, - errorhandling.NewValidationError( - errorhandling.ErrorExpressionFieldTypeMismatch, - errorhandling.TemplateLiterals{ - Literals: map[string]string{ - "Exp": operand.ToString(), - "Type": resolvedType, - "FieldName": context.Field.Name.Value, - "FieldType": expectedType, - }, - }, - expression, - ), - ) - return errors - } - } - - for _, c := range parser.ComparableTypes { - if lo.Contains(c, expectedType) && lo.Contains(c, resolvedType) { - return errors - } - } - - if expectedType != resolvedType { - errors = append(errors, - errorhandling.NewValidationError( - errorhandling.ErrorExpressionFieldTypeMismatch, - errorhandling.TemplateLiterals{ - Literals: map[string]string{ - "Exp": operand.ToString(), - "Type": resolvedType, - "FieldName": context.Field.Name.Value, - "FieldType": expectedType, - }, - }, - expression, - ), - ) - return errors - } - - return errors -} - -// Validates that all operands resolve correctly -// This handles operands of all types including operands such as model.associationA.associationB -// as well as simple value types such as string, number, bool etc -func OperandResolutionRule(asts []*parser.AST, condition *parser.Condition, context expressions.ExpressionContext) (errors []error) { - resolver := expressions.NewConditionResolver(condition, asts, &context) - _, _, errs := resolver.Resolve() - errors = append(errors, errs...) - - return errors -} - -// Validates that all conditions in an expression use assignment -func OperatorAssignmentRule(asts []*parser.AST, expression *parser.Expression, context expressions.ExpressionContext) (errors []error) { - conditions := expression.Conditions() - - for _, condition := range conditions { - // If there is no operator, then it means there is no rhs - if condition.Operator == nil { - continue - } - - if condition.Type() != parser.AssignmentCondition { - correction := errorhandling.NewCorrectionHint([]string{"="}, condition.Operator.Symbol) - - errors = append(errors, - errorhandling.NewValidationError( - errorhandling.ErrorForbiddenExpressionAction, - errorhandling.TemplateLiterals{ - Literals: map[string]string{ - "Operator": condition.Operator.Symbol, - "Suggestion": correction.ToString(), - "Attribute": fmt.Sprintf("@%s", context.Attribute.Name.Value), - }, - }, - condition.Operator, - ), - ) - - continue - } - - errors = append(errors, runSideEffectOperandRules(asts, condition, context, parser.AssignmentOperators)...) - } - - return errors -} - -// Validates that all conditions in an expression use logical operators -func OperatorLogicalRule(asts []*parser.AST, expression *parser.Expression, context expressions.ExpressionContext) (errors []error) { - conditions := expression.Conditions() - - for _, condition := range conditions { - // If there is no operator, then it means there is no rhs - if condition.Type() != parser.LogicalCondition && condition.Operator != nil { - correction := errorhandling.NewCorrectionHint([]string{"=="}, condition.Operator.Symbol) - errors = append(errors, - errorhandling.NewValidationError( - errorhandling.ErrorForbiddenExpressionAction, - errorhandling.TemplateLiterals{ - Literals: map[string]string{ - "Operator": condition.Operator.Symbol, - "Attribute": fmt.Sprintf("@%s", context.Attribute.Name.Value), - "Suggestion": correction.ToString(), - }, - }, - condition.Operator, - ), - ) - continue - } - - errors = append(errors, runSideEffectOperandRules(asts, condition, context, parser.LogicalOperators)...) - } - - return errors -} - -func InvalidOperatorForOperandsRule(asts []*parser.AST, condition *parser.Condition, context expressions.ExpressionContext, permittedOperators []string) (errors []error) { - resolver := expressions.NewConditionResolver(condition, asts, &context) - resolvedLHS, resolvedRHS, _ := resolver.Resolve() - - // If there is no operator, then we are not interested in validating this rule - if condition.Operator == nil { - return nil - } - - allowedOperatorsLHS := resolvedLHS.AllowedOperators(asts) - allowedOperatorsRHS := resolvedRHS.AllowedOperators(asts) - - if len(allowedOperatorsLHS) == 0 || len(allowedOperatorsRHS) == 0 { - return nil - } - - if slices.Equal(allowedOperatorsLHS, allowedOperatorsRHS) { - if !lo.Contains(allowedOperatorsLHS, condition.Operator.Symbol) { - collection := lo.Intersect(permittedOperators, allowedOperatorsLHS) - corrections := errorhandling.NewCorrectionHint(collection, condition.Operator.Symbol) - - errors = append(errors, errorhandling.NewValidationError( - errorhandling.ErrorForbiddenOperator, - errorhandling.TemplateLiterals{ - Literals: map[string]string{ - "Type": resolvedLHS.GetType(), - "Operator": condition.Operator.Symbol, - "Suggestion": corrections.ToString(), - }, - }, - condition.Operator, - )) - } - - if condition.Operator.Symbol == parser.OperatorNotIn || condition.Operator.Symbol == parser.OperatorIn { - if resolvedLHS.IsRepeated() { - errors = append(errors, errorhandling.NewValidationErrorWithDetails( - errorhandling.ErrorForbiddenOperator, - errorhandling.ErrorDetails{ - Message: "left hand side operand cannot be an array for 'in' and 'not in'", - }, - condition.LHS, - ), - ) - } - } - - if condition.Operator.Symbol == parser.OperatorAssignment { - if resolvedRHS.IsRepeated() && resolvedRHS.Field != nil && !resolvedRHS.Field.Repeated { - errors = append(errors, - errorhandling.NewValidationErrorWithDetails( - errorhandling.ErrorExpressionTypeMismatch, - errorhandling.ErrorDetails{ - Message: "cannot assign from a to-many relationship lookup", - }, - condition.RHS, - ), - ) - return errors - } - } - } else if resolvedLHS.IsRepeated() || resolvedRHS.IsRepeated() { - if !resolvedLHS.IsRepeated() && resolvedRHS.IsRepeated() && !(condition.Operator.Symbol == parser.OperatorNotIn || condition.Operator.Symbol == parser.OperatorIn) { - errors = append(errors, errorhandling.NewValidationError( - errorhandling.ErrorExpressionArrayMismatchingOperator, - errorhandling.TemplateLiterals{ - Literals: map[string]string{ - "RHS": condition.RHS.ToString(), - "Operator": condition.Operator.Symbol, - }, - }, - condition.Operator, - )) - } else if resolvedLHS.IsRepeated() && !resolvedRHS.IsRepeated() { - // Only enforce this rule if the actual field is an array and not during nested to-many sets - if resolvedLHS.Field != nil && resolvedLHS.Field.Repeated && !resolvedRHS.IsNull() { - errors = append(errors, - errorhandling.NewValidationErrorWithDetails( - errorhandling.ErrorExpressionTypeMismatch, - errorhandling.ErrorDetails{ - Message: fmt.Sprintf("%s is %s[] but %s is %s", condition.LHS.ToString(), resolvedLHS.GetType(), condition.RHS.ToString(), resolvedRHS.GetType()), - }, - condition, - ), - ) - return errors - } - } else if resolvedLHS.IsRepeated() && resolvedRHS.IsRepeated() && (condition.Operator.Symbol == parser.OperatorNotIn || condition.Operator.Symbol == parser.OperatorIn) { - collection := lo.Intersect(permittedOperators, allowedOperatorsLHS) - corrections := errorhandling.NewCorrectionHint(collection, condition.Operator.Symbol) - - errors = append(errors, errorhandling.NewValidationError( - errorhandling.ErrorForbiddenOperator, - errorhandling.TemplateLiterals{ - Literals: map[string]string{ - "Type": resolvedLHS.GetType(), - "Operator": condition.Operator.Symbol, - "Suggestion": corrections.ToString(), - }, - }, - condition.Operator, - )) - } - } else if !lo.Contains(allowedOperatorsLHS, condition.Operator.Symbol) && !lo.Contains(allowedOperatorsRHS, condition.Operator.Symbol) { - collection := lo.Intersect(permittedOperators, allowedOperatorsLHS) - corrections := errorhandling.NewCorrectionHint(collection, condition.Operator.Symbol) - - errors = append(errors, errorhandling.NewValidationError( - errorhandling.ErrorForbiddenOperator, - errorhandling.TemplateLiterals{ - Literals: map[string]string{ - "Type": resolvedLHS.GetType(), - "Operator": condition.Operator.Symbol, - "Suggestion": corrections.ToString(), - }, - }, - condition.Operator, - )) - } - - return errors -} - -// OperandTypesMatchRule checks that the left-hand side and right-hand side are -// compatible. -// - LHS and RHS are the same type -// - LHS and RHS are of _compatible_ types -// - LHS is of type T and RHS is an array of type T -// - LHS or RHS is an optional field and the other side is an explicit null -func OperandTypesMatchRule(asts []*parser.AST, condition *parser.Condition, context expressions.ExpressionContext) (errors []error) { - resolver := expressions.NewConditionResolver(condition, asts, &context) - resolvedLHS, resolvedRHS, _ := resolver.Resolve() - - // If either side fails to resolve then no point checking compatibility - if resolvedLHS == nil || resolvedRHS == nil { - return nil - } - - // Case: LHS and RHS are the same type - if resolvedLHS.GetType() == resolvedRHS.GetType() { - if condition.Operator != nil && condition.Operator.Symbol == parser.OperatorAssignment { - if resolvedLHS.IsRepeated() == resolvedRHS.IsRepeated() { - return nil - } - if resolvedLHS.IsRepeated() && !resolvedRHS.IsRepeated() { - return nil - } - } else if resolvedLHS.IsRepeated() == resolvedRHS.IsRepeated() { - return nil - } - } - - for _, c := range parser.ComparableTypes { - if lo.Contains(c, resolvedLHS.GetType()) && lo.Contains(c, resolvedRHS.GetType()) { - return nil - } - } - - // Case: LHS is of type T and RHS is an array of type T - if resolvedRHS.IsRepeated() { - // First check array contains only one type - arrayType := resolvedRHS.GetType() - valid := true - for i, item := range resolvedRHS.Array { - if i == 0 { - arrayType = item.GetType() - continue - } - - if arrayType != item.GetType() { - valid = false - errors = append(errors, - errorhandling.NewValidationError( - errorhandling.ErrorExpressionMixedTypesInArrayLiteral, - errorhandling.TemplateLiterals{ - Literals: map[string]string{ - "Item": item.Literal.ToString(), - "Type": arrayType, - }, - }, - item.Literal, - ), - ) - } - } - - if !valid { - return errors - } - - // Now we know the RHS is an array of type T we can check if - // the LHS is also of type T - if arrayType == resolvedLHS.GetType() { - return nil - } - } - - // Case: RHS is of type T and LHS is an array of type T - if resolvedLHS.IsRepeated() { - // First check array contains only one type - arrayType := resolvedLHS.GetType() - valid := true - for i, item := range resolvedLHS.Array { - if i == 0 { - arrayType = item.GetType() - continue - } - - if arrayType != item.GetType() { - valid = false - errors = append(errors, - errorhandling.NewValidationError( - errorhandling.ErrorExpressionMixedTypesInArrayLiteral, - errorhandling.TemplateLiterals{ - Literals: map[string]string{ - "Item": item.Literal.ToString(), - "Type": arrayType, - }, - }, - item.Literal, - ), - ) - } - } - - if !valid { - return errors - } - - // Now we know the LHS is an array of type T we can check if - // the RHS is also of type T - if arrayType == resolvedRHS.GetType() { - return nil - } - } - - // Case: LHS or RHS is an optional field and the other side is an explicit null - if (!resolvedLHS.IsOptional() && resolvedRHS.IsNull()) || - (!resolvedRHS.IsOptional() && resolvedLHS.IsNull()) { - operandName := resolvedLHS.Name - if resolvedLHS.IsNull() { - operandName = resolvedRHS.Name - } - - errors = append(errors, - errorhandling.NewValidationError( - errorhandling.ErrorExpressionTypeNotNullable, - errorhandling.TemplateLiterals{ - Literals: map[string]string{ - "OperandName": operandName, - }, - }, - condition, - ), - ) - return errors - } else if resolvedRHS.IsNull() || resolvedLHS.IsNull() { - return nil - } - - if condition.Operator != nil && condition.Operator.Symbol == parser.OperatorAssignment { - if resolvedLHS.IsRepeated() && !resolvedRHS.IsRepeated() { - return nil - } - } - - lhsType := resolvedLHS.GetType() - if resolvedLHS.IsRepeated() { - if resolvedLHS.Array != nil { - lhsType = "an array of " + resolvedLHS.Array[0].GetType() - } else { - lhsType = "an array of " + lhsType - } - } - - rhsType := resolvedRHS.GetType() - if resolvedRHS.IsRepeated() { - if resolvedRHS.Array != nil { - rhsType = "an array of " + resolvedRHS.Array[0].GetType() - } else { - rhsType = "an array of " + rhsType - } - } - - // LHS and RHS types do not match, report error - errors = append(errors, - errorhandling.NewValidationError( - errorhandling.ErrorExpressionTypeMismatch, - errorhandling.TemplateLiterals{ - Literals: map[string]string{ - "Operator": condition.Operator.Symbol, - "LHS": condition.LHS.ToString(), - "LHSType": lhsType, - "RHS": condition.RHS.ToString(), - "RHSType": rhsType, - }, - }, - condition, - ), - ) - - return errors -} -func runSideEffectOperandRules(asts []*parser.AST, condition *parser.Condition, context expressions.ExpressionContext, permittedOperators []string) (errors []error) { - errors = append(errors, OperandResolutionRule(asts, condition, context)...) - - if len(errors) > 0 { - return errors - } - - errors = append(errors, OperandTypesMatchRule(asts, condition, context)...) - - if len(errors) > 0 { - return errors - } - - errors = append(errors, InvalidOperatorForOperandsRule(asts, condition, context, permittedOperators)...) - - return errors -}