// Copyright 2020 CUE Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package adt // This file contains error encodings. // // // *Bottom: // - an adt.Value // - always belongs to a single vertex. // - does NOT implement error // - marks error code used for control flow // // errors.Error // - CUE default error // - implements error // - tracks error locations // - has error message details // - supports multiple errors // import ( "slices" "cuelang.org/go/cue/ast" "cuelang.org/go/cue/errors" cueformat "cuelang.org/go/cue/format" "cuelang.org/go/cue/token" "cuelang.org/go/internal/iterutil" ) // ErrorCode indicates the type of error. The type of error may influence // control flow. No other aspects of an error may influence control flow. type ErrorCode int8 //go:generate go tool stringer -type=ErrorCode -linecomment const ( // An EvalError is a fatal evaluation error. EvalError ErrorCode = iota // eval // A UserError is a fatal error originating from the user using the error // builtin. UserError // user // A LegacyUserError is a fatal error originating from the user using the // _|_ token, which we intend to phase out. LegacyUserError // user // StructuralCycleError means a structural cycle was found. Structural // cycles are permanent errors, but they are not passed up recursively, // as a unification of a value with a structural cycle with one that // doesn't may still give a useful result. StructuralCycleError // structural cycle // IncompleteError means an evaluation could not complete because of // insufficient information that may still be added later. IncompleteError // incomplete // A CycleError indicates a reference error. It is considered to be // an incomplete error, as reference errors may be broken by providing // a concrete value. CycleError // cycle ) // Bottom represents an error or bottom symbol. // // Although a Bottom node holds control data, it should not be created until the // control information already resulted in an error. type Bottom struct { Src ast.Node Err errors.Error Code ErrorCode // Permanent indicates whether an incomplete error can be // resolved later without making the configuration more specific. // This may happen when an arc isn't fully resolved yet. Permanent bool HasRecursive bool ChildError bool // Err is the error of the child NotExists bool // This error originated from a failed lookup. CloseCheck bool // This error resulted from a close check. ForCycle bool // this is a for cycle // Value holds the computed value so far in case Value Value // Node marks the node at which an error occurred. This is used to // determine the package to which an error belongs. // TODO: use a more precise mechanism for tracking the package. Node *Vertex } func (x *Bottom) Source() ast.Node { return x.Src } func (x *Bottom) Kind() Kind { return BottomKind } func (b *Bottom) IsIncomplete() bool { if b == nil { return false } return b.Code == IncompleteError || b.Code == CycleError } // isLiteralBottom reports whether x is an error originating from a user. func isLiteralBottom(x Expr) bool { b, ok := x.(*Bottom) return ok && b.Code == LegacyUserError } // isError reports whether v is an error or nil. func isError(v Value) bool { if v == nil { return true } _, ok := v.(*Bottom) return ok } // isIncomplete reports whether v is associated with an incomplete error. func isIncomplete(v *Vertex) bool { if v == nil { return true } if b := v.Bottom(); b != nil { return b.IsIncomplete() } return false } // AddChildError updates x to record an error that occurred in one of // its descendent arcs. The resulting error will record the worst error code of // the current error or recursive error. // // If x is not already an error, the value is recorded in the error for // reference. func (n *nodeContext) AddChildError(recursive *Bottom) { v := n.node v.ChildErrors = CombineErrors(nil, v.ChildErrors, recursive) if recursive.IsIncomplete() { return } x := v.BaseValue err, _ := x.(*Bottom) if err == nil || err.CloseCheck { n.setBaseValue(&Bottom{ Code: recursive.Code, Value: v, HasRecursive: true, ChildError: true, CloseCheck: recursive.CloseCheck, Err: recursive.Err, Node: n.node, }) return } err.HasRecursive = true if err.Code > recursive.Code { err.Code = recursive.Code } n.setBaseValue(err) } // CombineErrors combines two errors that originate at the same Vertex. func CombineErrors(src ast.Node, x, y Value) *Bottom { a, _ := Unwrap(x).(*Bottom) b, _ := Unwrap(y).(*Bottom) switch { case a == nil && b == nil: return nil case a == nil: return b case b == nil: return a case a == b && isCyclePlaceholder(a): return a case a == b: // Don't return a (or b) because they may have other non-nil fields. return &Bottom{ Src: src, Err: a.Err, Code: a.Code, } } if a.Code != b.Code { if a.Code > b.Code { a, b = b, a } if b.Code >= IncompleteError { return a } } return &Bottom{ Src: src, Err: errors.Append(a.Err, b.Err), Code: a.Code, CloseCheck: a.CloseCheck || b.CloseCheck, } } func addPositions(ctx *OpContext, err *ValueError, c Conjunct) { switch x := c.x.(type) { case *Field: // if x.ArcType == ArcRequired { err.AddPosition(c.x) // } case *ConjunctGroup: for _, c := range *x { addPositions(ctx, err, c) } } err.AddPos(c.CloseInfo.Location(ctx)) } func NewRequiredNotPresentError(ctx *OpContext, v *Vertex, morePositions ...Node) *Bottom { saved := ctx.PushArc(v) err := ctx.Newf("field is required but not present") for _, p := range morePositions { err.AddPosition(p) } for c := range v.LeafConjuncts() { if f, ok := c.x.(*Field); ok && f.ArcType == ArcRequired { err.AddPosition(c.x) } err.AddPos(c.CloseInfo.Location(ctx)) } b := &Bottom{ Code: IncompleteError, Err: err, Node: v, } ctx.PopArc(saved) return b } func newRequiredFieldInComprehensionError(ctx *OpContext, x *ForClause, v *Vertex) *Bottom { err := ctx.Newf("missing required field in for comprehension: %v", v.Label) err.AddPosition(x.Src) for c := range v.LeafConjuncts() { addPositions(ctx, err, c) } return &Bottom{ Code: IncompleteError, Err: err, } } func (v *Vertex) reportFieldIndexError(c *OpContext, pos token.Pos, f Feature) { v.reportFieldError(c, pos, f, "index out of range [%d] with length %d", "undefined field: %s") } func (v *Vertex) reportFieldCycleError(c *OpContext, pos token.Pos, f Feature) *Bottom { const msg = "cyclic reference to field %[1]v" b := v.reportFieldError(c, pos, f, msg, msg) return b } func (v *Vertex) reportFieldError(c *OpContext, pos token.Pos, f Feature, intMsg, stringMsg string) *Bottom { code := IncompleteError // If v is an error, we need to adopt the worst error. if b := v.Bottom(); b != nil && !isCyclePlaceholder(b) { code = b.Code } else if !v.Accept(c, f) { code = EvalError } label := f.SelectorString(c.Runtime) var err errors.Error if f.IsInt() { err = c.NewPosf(pos, intMsg, f.Index(), iterutil.Count(v.Elems())) } else { err = c.NewPosf(pos, stringMsg, label) } b := &Bottom{ Code: code, Err: err, Node: v, } // TODO: yield failure c.AddBottom(b) // TODO: unify error mechanism. return b } // baseError contains common fields and methods for error types. type baseError struct { r Runtime v *Vertex pos token.Pos auxpos []token.Pos altPath []string } func (e *baseError) AddPos(p token.Pos) { if !p.IsValid() { return } if slices.Contains(e.auxpos, p) { return } e.auxpos = append(e.auxpos, p) } func (e *baseError) AddClosedPositions(ctx *OpContext, p posInfo) { for n := range p.AncestorPositions(ctx) { e.AddPos(n) } } func (e *baseError) Position() token.Pos { return e.pos } func (e *baseError) InputPositions() []token.Pos { return e.auxpos } func (e *baseError) Path() (a []string) { if len(e.altPath) > 0 { return e.altPath } if e.v == nil { return nil } for _, f := range appendPath(nil, e.v) { a = append(a, f.SelectorString(e.r)) } return a } // A ValueError is returned as a result of evaluating a value. type ValueError struct { baseError errors.Message } func (v *ValueError) AddPosition(n Node) { v.AddPos(Pos(n)) } func (c *OpContext) errNode() *Vertex { return c.vertex } // MarkPositions marks the current position stack. func (c *OpContext) MarkPositions() int { return len(c.positions) } // ReleasePositions sets the position state to one from a call to MarkPositions. func (c *OpContext) ReleasePositions(p int) { c.positions = c.positions[:p] } func (c *OpContext) AddPosition(n Node) { if n != nil { c.positions = append(c.positions, n) } } func (c *OpContext) Newf(format string, args ...interface{}) *ValueError { return c.NewPosf(c.pos(), format, args...) } func appendNodePositions(a []token.Pos, n Node) []token.Pos { if p := Pos(n); p.IsValid() { a = append(a, p) } if v, ok := n.(*Vertex); ok { for c := range v.LeafConjuncts() { a = appendNodePositions(a, c.Elem()) } } return a } func (c *OpContext) NewPosf(p token.Pos, format string, args ...interface{}) *ValueError { var a []token.Pos if len(c.positions) > 0 { a = make([]token.Pos, 0, len(c.positions)) for _, n := range c.positions { a = appendNodePositions(a, n) } } for i, arg := range args { switch x := arg.(type) { case Node: a = appendNodePositions(a, x) // Wrap nodes in a [fmt.Stringer] which delays the call to // [OpContext.Str] until the error needs to be rendered. // This helps avoid work, as in many cases, // errors are created but never shown to the user. // // A Vertex will set an error as its BaseValue via a Bottom node, // which might be this error we are creating. // Using the Vertex directly could then lead to endless recursion. // Make a shallow copy to avoid that. if v, ok := x.(*Vertex); ok { // TODO(perf): we could join this allocation with the creation // of the stringer below. vcopy := *v x = &vcopy } args[i] = c.Str(x) case ast.Node: // TODO: ideally the core evaluator should not depend on higher // level packages. This will allow the debug packages to be used // more widely. b, _ := cueformat.Node(x) if p := x.Pos(); p.IsValid() { a = append(a, p) } args[i] = string(b) case Feature: args[i] = x.SelectorString(c.Runtime) } } return &ValueError{ baseError: baseError{ r: c.Runtime, v: c.errNode(), pos: p, auxpos: a, altPath: c.makeAltPath(), }, Message: errors.NewMessagef(format, args...), } } func (c *OpContext) makeAltPath() (a []string) { if len(c.altPath) == 0 { return nil } for _, f := range appendPath(nil, c.altPath[0]) { a = append(a, f.SelectorString(c)) } for _, v := range c.altPath[1:] { if f := v.Label; f != 0 { a = append(a, f.SelectorString(c)) } } return a } func (e *ValueError) Error() string { return errors.String(e) } // ConflictError defers formatting of conflict messages until the error is // actually needed, avoiding expensive string conversions and allocations. type ConflictError struct { baseError format func(Runtime, Node) string v1, v2 Node k1, k2 Kind } func (e *ConflictError) Error() string { return errors.String(e) } func (e *ConflictError) Msg() (format string, args []interface{}) { v1Str := Formatter{X: e.v1, F: e.format, R: e.r} v2Str := Formatter{X: e.v2, F: e.format, R: e.r} if e.k1 == e.k2 { return "conflicting values %s and %s", []interface{}{v1Str, v2Str} } return "conflicting values %s and %s (mismatched types %s and %s)", []interface{}{v1Str, v2Str, e.k1, e.k2} }